From 091b8f1934a5ad71b2a15e8570b6934f7119e44d Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 21 Oct 2026 09:41:53 -0700 Subject: [PATCH 001/409] Report which packages and modules would run in a Node island Adds the report-only Node island policy, configuration and detector of the scope-B plan (milestone NI-2). Coverage now lists the npm packages a build would run in a Node island and why, gap islands first, without changing code generation. - project-config.ts reads the `scriptc` key of the entry's package.json with a schema; an unknown value in `scriptc.packages` or `scriptc.nodeIslands` is SC8004 instead of being ignored. Package shims and the WebAssembly scanner read through it. - node-islands/: origin (user, npm, shim, builtin; package instances), rules R1.1-R2.5 and the four classes, a syntactic pre-scan for R1/R2 markers, classification of refusals and runtime fences (reason ids at the refusal sites that need them), the policy (auto, explicit, off; forced node and native packages; marked user modules) and the report with island closure. - coverage: a "Node islands" section, `--explain-island=`, a `nodeIslands` field in the diagnostics envelope, and `--fail-on` `node-islands`, `eager-node-islands` and `island-gaps`. - CLI: `--node-islands`, `--node-island`, `--native`, `--node` (validated, report-only) and `--strict-user-js` (SC8101-SC8105 for R2 constructs in user JavaScript). Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + packages/cli/src/bootstrap.ts | 23 +- packages/compiler/src/cli/command.ts | 85 +- packages/compiler/src/cli/usage.ts | 34 +- packages/compiler/src/compile-types.ts | 4 + packages/compiler/src/coverage/report.ts | 278 +++++- .../compiler/src/coverage/verdict-cache.ts | 11 +- .../compiler/src/diagnostics/diagnostic.ts | 67 ++ packages/compiler/src/diagnostics/envelope.ts | 53 ++ packages/compiler/src/frontend/analysis.ts | 26 +- .../lowering/builtins/module-resolution.ts | 2 + .../src/frontend/lowering/call-signatures.ts | 2 + .../frontend/lowering/cjs-inline-require.ts | 2 + .../lowering/expressions/identifiers.ts | 12 +- .../frontend/lowering/lower-node-module.ts | 2 + .../src/frontend/lowering/lower-stdlib.ts | 2 + .../compiler/src/frontend/lowering/lowerer.ts | 17 +- .../compiler/src/frontend/package-shims.ts | 33 +- packages/compiler/src/frontend/pipeline.ts | 107 ++- packages/compiler/src/index.ts | 3 + .../compiler/src/node-islands/classify.ts | 131 +++ .../compiler/src/node-islands/detect.test.ts | 165 ++++ packages/compiler/src/node-islands/detect.ts | 849 ++++++++++++++++++ packages/compiler/src/node-islands/origin.ts | 68 ++ packages/compiler/src/node-islands/policy.ts | 167 ++++ .../compiler/src/node-islands/report.test.ts | 201 +++++ packages/compiler/src/node-islands/report.ts | 355 ++++++++ packages/compiler/src/node-islands/rules.ts | 109 +++ packages/compiler/src/node-islands/scan.ts | 155 ++++ packages/compiler/src/node-islands/summary.ts | 110 +++ packages/compiler/src/project-config.test.ts | 126 +++ packages/compiler/src/project-config.ts | 421 +++++++++ packages/compiler/src/wasm/discover.ts | 39 +- .../node-islands-config/main.ts.json | 6 + .../node-islands-user-js.cjs | 22 + tests/coverage-fixtures/node-islands/main.ts | 16 + .../node_modules/ni-ctor/index.d.ts | 4 + .../node_modules/ni-ctor/index.js | 15 + .../node_modules/ni-ctor/package.json | 5 + .../node-islands/node_modules/ni-dep/index.js | 4 + .../node_modules/ni-dep/package.json | 5 + .../node_modules/ni-eval/index.d.ts | 1 + .../node_modules/ni-eval/index.js | 5 + .../node_modules/ni-eval/package.json | 5 + .../node_modules/ni-gap/index.d.ts | 1 + .../node-islands/node_modules/ni-gap/index.js | 8 + .../node_modules/ni-gap/package.json | 5 + .../node_modules/ni-minified/index.d.ts | 1 + .../node_modules/ni-minified/index.js | 1 + .../node_modules/ni-minified/package.json | 5 + .../node_modules/ni-typed/index.d.ts | 1 + .../node_modules/ni-typed/index.js | 4 + .../node_modules/ni-typed/package.json | 5 + .../node-islands/package.json | 5 + tests/diagnostics/node-islands-config/main.ts | 3 + .../node-islands-config/package.json | 14 + .../coverage-node-islands-explain.txt | 13 + .../coverage-node-islands-user-js.txt | 34 + .../__snapshots__/coverage-node-islands.txt | 39 + .../node-islands-config/main.ts.txt | 35 + tests/harness/coverage.test.ts | 87 ++ 61 files changed, 3928 insertions(+), 81 deletions(-) create mode 100644 packages/compiler/src/node-islands/classify.ts create mode 100644 packages/compiler/src/node-islands/detect.test.ts create mode 100644 packages/compiler/src/node-islands/detect.ts create mode 100644 packages/compiler/src/node-islands/origin.ts create mode 100644 packages/compiler/src/node-islands/policy.ts create mode 100644 packages/compiler/src/node-islands/report.test.ts create mode 100644 packages/compiler/src/node-islands/report.ts create mode 100644 packages/compiler/src/node-islands/rules.ts create mode 100644 packages/compiler/src/node-islands/scan.ts create mode 100644 packages/compiler/src/node-islands/summary.ts create mode 100644 packages/compiler/src/project-config.test.ts create mode 100644 packages/compiler/src/project-config.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json create mode 100644 tests/coverage-fixtures/node-islands-user-js.cjs create mode 100644 tests/coverage-fixtures/node-islands/main.ts create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.d.ts create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.js create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-ctor/package.json create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-dep/index.js create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-dep/package.json create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-eval/index.d.ts create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-eval/index.js create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-eval/package.json create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-gap/index.d.ts create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-gap/index.js create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-gap/package.json create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-minified/index.d.ts create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-minified/index.js create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-minified/package.json create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-typed/index.d.ts create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-typed/index.js create mode 100644 tests/coverage-fixtures/node-islands/node_modules/ni-typed/package.json create mode 100644 tests/coverage-fixtures/node-islands/package.json create mode 100644 tests/diagnostics/node-islands-config/main.ts create mode 100644 tests/diagnostics/node-islands-config/package.json create mode 100644 tests/harness/__snapshots__/coverage-node-islands-explain.txt create mode 100644 tests/harness/__snapshots__/coverage-node-islands-user-js.txt create mode 100644 tests/harness/__snapshots__/coverage-node-islands.txt create mode 100644 tests/harness/__snapshots__/node-islands-config/main.ts.txt diff --git a/.gitignore b/.gitignore index fda9414aa..fa352c85e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ node_modules/ !tests/corpus/*/node_modules/ !tests/fixtures/npm-static/node_modules/ !tests/coverage-fixtures/wasm/node_modules/ +!tests/coverage-fixtures/node-islands*/node_modules/ !tests/fixtures/npm-static/nested/inner/node_modules/ !tests/fixtures/npm-static/node_modules/*/node_modules/ !tests/fixtures/npm/store/*/node_modules/ diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts index 1a319f98d..3fb61232f 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -70,7 +70,9 @@ async function tryFastPath(): Promise { values["profile-generate"] !== undefined || values["profile-use"] !== undefined || values.sparse || - (values["external-types"] ?? []).length > 0 + (values["external-types"] ?? []).length > 0 || + // The full CLI validates the Node island policy flags. + hasNodeIslandFlags(values) ) return null; const backend = values.backend; @@ -176,6 +178,21 @@ async function tryFastPath(): Promise { }); } +function hasNodeIslandFlags( + values: ReturnType< + typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }> + >["values"], +): boolean { + return ( + values["node-islands"] !== undefined || + (values["node-island"] ?? []).length > 0 || + (values.native ?? []).length > 0 || + values.node !== undefined || + values["explain-island"] !== undefined || + values["strict-user-js"] + ); +} + /** An unchanged program's coverage verdict replays from the verdict cache * without loading the compiler graph. Anything else (a miss, a comment-only * edit, options the replay does not cover) defers to the full CLI. */ @@ -205,7 +222,9 @@ async function coverageFastPath( values["provenance-sources"] || values["emit-ir"] || (values["npm-static"] ?? []).length > 0 || - (values["external-types"] ?? []).length > 0 + (values["external-types"] ?? []).length > 0 || + // The replay covers the default Node island policy only. + hasNodeIslandFlags(values) ) return null; let startup: typeof import("@scriptc/compiler/startup-cache"); diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts index 03160c6fa..128dacb26 100644 --- a/packages/compiler/src/cli/command.ts +++ b/packages/compiler/src/cli/command.ts @@ -4,12 +4,19 @@ import { parseArgs } from "node:util"; import { isExactExternalTypeSpecifier } from "../frontend/program.js"; import { renderDiagnostics } from "../diagnostics/render.js"; import { + COVERAGE_FAIL_ON, coverageEnvelope, coveragePasses, renderCoverage, renderExplainDynamic, + renderExplainIsland, type CoverageFailOn, } from "../coverage/report.js"; +import { + NODE_ISLANDS_MODES, + type NodeIslandOptions, + type NodeIslandsMode, +} from "../node-islands/policy.js"; import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js"; import { buildEnvelope } from "../diagnostics/envelope.js"; import { renderWarnings } from "../diagnostics/render.js"; @@ -105,6 +112,7 @@ async function main(args: string[], host: CliHost): Promise { values["emit-ir"] || values.tier !== undefined || values["explain-dynamic"] !== undefined || + hasNodeIslandFlags(values) || !values["keep-llvm"] ) { fail( @@ -162,6 +170,7 @@ async function main(args: string[], host: CliHost): Promise { values["emit-ir"] || values.tier !== undefined || values["explain-dynamic"] !== undefined || + hasNodeIslandFlags(values) || !values["keep-llvm"]; if (values.out === undefined || inputs.length === 0 || others) { fail( @@ -214,7 +223,8 @@ async function main(args: string[], host: CliHost): Promise { (values["npm-static"] ?? []).length > 0 || externalTypeArgs.length > 0 || values.tier !== undefined || - values["explain-dynamic"] !== undefined + values["explain-dynamic"] !== undefined || + hasNodeIslandFlags(values) ) { fail( "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only", @@ -284,8 +294,10 @@ async function main(args: string[], host: CliHost): Promise { fail(`--print=diagnostics is a build/coverage option\n\n${USAGE}`); } const failOnRaw = values["fail-on"]; - if (failOnRaw !== undefined && failOnRaw !== "blockers" && failOnRaw !== "divergences") { - fail(`unknown --fail-on value "${failOnRaw}" (supported: blockers, divergences)\n\n${USAGE}`); + if (failOnRaw !== undefined && !(COVERAGE_FAIL_ON as readonly string[]).includes(failOnRaw)) { + fail( + `unknown --fail-on value "${failOnRaw}" (supported: ${COVERAGE_FAIL_ON.join(", ")})\n\n${USAGE}`, + ); } if (failOnRaw !== undefined && command !== "coverage") { fail(`--fail-on is a coverage option (a build already fails on blockers)\n\n${USAGE}`); @@ -300,6 +312,48 @@ async function main(args: string[], host: CliHost): Promise { if (explainDynamic !== undefined && command !== "coverage") { fail(`--explain-dynamic is a coverage option\n\n${USAGE}`); } + const explainIsland = values["explain-island"]; + if (explainIsland !== undefined && command !== "coverage") { + fail(`--explain-island is a coverage option\n\n${USAGE}`); + } + if (explainIsland === "") fail(`--explain-island needs a package name\n\n${USAGE}`); + if (values["strict-user-js"] && command !== "coverage") { + fail(`--strict-user-js is a coverage option\n\n${USAGE}`); + } + // The Node island policy's command-line half. Report-only: build and run + // validate it, coverage reports the islands it would choose. + const islandMode = values["node-islands"]; + if (islandMode !== undefined && !(NODE_ISLANDS_MODES as readonly string[]).includes(islandMode)) { + fail( + `unknown --node-islands value "${islandMode}" (supported: ${NODE_ISLANDS_MODES.join(", ")})\n\n${USAGE}`, + ); + } + const listValues = (raw: string[] | undefined): string[] => + (raw ?? []) + .flatMap((v) => v.split(",")) + .map((v) => v.trim()) + .filter((v) => v !== ""); + const islandArgs = listValues(values["node-island"]); + const nativeArgs = listValues(values.native); + for (const name of nativeArgs) { + if (islandArgs.includes(name)) + fail(`--native=${name} and --node-island=${name} contradict each other\n\n${USAGE}`); + } + if (values.node === "") fail(`--node needs the path of a node executable\n\n${USAGE}`); + const nodeIslands: NodeIslandOptions | undefined = + islandMode === undefined && + islandArgs.length === 0 && + nativeArgs.length === 0 && + values.node === undefined && + !values["strict-user-js"] + ? undefined + : { + ...(islandMode !== undefined ? { mode: islandMode as NodeIslandsMode } : {}), + ...(islandArgs.length > 0 ? { islands: islandArgs } : {}), + ...(nativeArgs.length > 0 ? { native: nativeArgs } : {}), + ...(values.node !== undefined ? { node: resolve(values.node) } : {}), + ...(values["strict-user-js"] ? { strictUserJs: true } : {}), + }; if (printNativeLinkInfo && command !== "build") { fail(`--print=native-link-info is a build option\n\n${USAGE}`); } @@ -429,6 +483,7 @@ async function main(args: string[], host: CliHost): Promise { ...(tier !== undefined ? { tier } : {}), ...(explainDynamic !== undefined ? { explainDynamic: true } : {}), ...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}), + ...(nodeIslands !== undefined ? { nodeIslands } : {}), }); if (printDiagnostics) { // Keep stdout pure JSON for tooling. @@ -443,6 +498,17 @@ async function main(args: string[], host: CliHost): Promise { process.stdout.write( renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n", ); + if (explainIsland !== undefined) { + process.stdout.write( + "\n" + + renderExplainIsland(coverage.nodeIslands, explainIsland, { + color, + sourceTexts, + root: process.cwd(), + }) + + "\n", + ); + } if (explainDynamic !== undefined && coverage.tiers !== undefined) { process.stdout.write( "\n" + @@ -564,6 +630,19 @@ async function main(args: string[], host: CliHost): Promise { return 0; } +/** Whether any Node island flag is present (they belong to build, run and + * coverage). */ +function hasNodeIslandFlags(values: ReturnType["values"]): boolean { + return ( + values["node-islands"] !== undefined || + (values["node-island"] ?? []).length > 0 || + (values.native ?? []).length > 0 || + values.node !== undefined || + values["explain-island"] !== undefined || + values["strict-user-js"] + ); +} + /** Both installed and seed commands use this argument and diagnostic contract. */ export async function runCli(args: string[], host: CliHost): Promise { try { diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index aa7408d2a..bd40aa57c 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -31,10 +31,12 @@ Options: (native-link-info implies --emit=obj and never links; diagnostics prints the versioned JSON diagnostics envelope for build and coverage) - --fail-on + --fail-on coverage only: exit 1 when the entry path has blockers (a build would fail), or also when it has Node - divergences + divergences, any Node island (eager-node-islands is the + same until deferred evaluation lands), or a gap island + (a package scriptc should compile but cannot yet) --backend code generator (llvm) --optimization native optimization posture (default: release/-O2). dev @@ -100,6 +102,28 @@ Options: values and the bindings, fields, functions, classes and modules the dynamic tier would take over, with the refusal behind each; a name shows only that unit's chain + --node-islands + Node island policy (default: auto): npm packages outside + the native subset run in Node automatically; explicit + allows only marked islands; off allows none. REPORT-ONLY + in this version: coverage reports the islands a build + would choose; builds compile as before + --node-island + mark an npm package or one of your modules (a path or + glob) to run in a Node island (repeatable, comma lists; + report-only) + --native require an npm package to compile natively; outside the + native subset it is SC8003 (repeatable; report-only) + --node the node executable Node islands use (report-only) + --explain-island + coverage only: why a package would run in a Node island, + with the first reason per module, the native import + sites and the packages inside its closure + --strict-user-js + coverage only: report constructor functions, prototype + and shape mutation, sloppy-mode semantics and builtin + patching in your own .js/.cjs/.mjs modules as + SC8101–SC8105 blockers --wasm coverage only: report a WebAssembly module the program loads whose path its sources do not spell (repeatable; @@ -132,6 +156,12 @@ export const CLI_OPTIONS = { tier: { type: "string" }, "explain-dynamic": { type: "string" }, wasm: { type: "string", multiple: true }, + "node-islands": { type: "string" }, + "node-island": { type: "string", multiple: true }, + native: { type: "string", multiple: true }, + node: { type: "string" }, + "explain-island": { type: "string" }, + "strict-user-js": { type: "boolean", default: false }, lib: { type: "boolean", default: false }, profile: { type: "string" }, help: { type: "boolean", short: "h", default: false }, diff --git a/packages/compiler/src/compile-types.ts b/packages/compiler/src/compile-types.ts index 9ecd75974..39fea3acf 100644 --- a/packages/compiler/src/compile-types.ts +++ b/packages/compiler/src/compile-types.ts @@ -4,6 +4,7 @@ import type { NativeOptimization } from "./backend/optimization.js"; import type { ScrDiagnostic } from "./diagnostics/diagnostic.js"; import type { CoverageInput } from "./coverage/report.js"; import type { TierMode } from "./frontend/tier-analysis.js"; +import type { NodeIslandOptions } from "./node-islands/policy.js"; export type CompileOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe"; @@ -183,6 +184,9 @@ export interface AnalyzeOptions { /** `--wasm `: WebAssembly module files the program loads that its * sources do not name (absolute paths); reported with the discovered ones. */ wasm?: readonly string[]; + /** The Node island policy's command-line half (report-only: coverage + * reports which packages and modules would run in a Node island). */ + nodeIslands?: NodeIslandOptions; } export interface AnalyzeResult { diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts index 89733e255..8fc12a040 100644 --- a/packages/compiler/src/coverage/report.ts +++ b/packages/compiler/src/coverage/report.ts @@ -17,6 +17,7 @@ import { type DiagnosticsEnvelope, type LocatedDiagnostic, type SourcePosition, + type NodeIslandsEnvelope, type TierUnitPosition, type TiersEnvelope, } from "../diagnostics/envelope.js"; @@ -25,6 +26,14 @@ import type { LowerStats } from "../frontend/lowering/lowerer.js"; import type { ProvenanceSources } from "../frontend/provenance-registry.js"; import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js"; import type { WasmModuleReport } from "../wasm/discover.js"; +import { + describe as describeReason, + hasNodeIslandDecisions, + type NodeIslandEntry, + type NodeIslandReport, +} from "../node-islands/summary.js"; +import { classDescription } from "../node-islands/rules.js"; +import type { SrcLoc } from "../ir/ir.js"; export interface CoverageInput { file: string; @@ -61,6 +70,10 @@ export interface CoverageInput { /** The WebAssembly modules the build can see (wasm/discover.ts): their * size, validity and the proposals they use. */ wasmModules?: WasmModuleReport[]; + /** Node islands, report-only: the packages and user modules that would + * run in a Node island, and why. Present when there is something to + * report. */ + nodeIslands?: NodeIslandReport; /** True when tsc preflight failed — no coverage can be computed. */ preflightFailed: boolean; } @@ -144,6 +157,19 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions out.push(` ${c(RED, "not analyzable")}: ${config[0]!.message}`); return out.join("\n"); } + const projectConfig = input.diagnostics.filter((d) => d.code === "SC8004"); + if (projectConfig.length > 0) { + out.push( + ` ${c(RED, "not analyzable")}: the project's scriptc configuration has ${projectConfig.length} unknown setting${projectConfig.length === 1 ? "" : "s"}`, + ); + for (const d of projectConfig) { + const p = locator.position(d.loc.file, d.loc.start); + out.push(` ${c(DIM, "SC8004")} ${d.message}`); + out.push(` ${c(DIM, `at ${where({ file: d.loc.file, ...p })}`)}`); + if (d.hint !== undefined) out.push(` ${c(DIM, `hint: ${d.hint}`)}`); + } + return out.join("\n"); + } const tsc = input.diagnostics.filter((d) => d.code === "SC0001"); if (tsc.length > 0) { const world = tsc.filter((d) => d.typeWorld === true).length; @@ -221,6 +247,22 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions out.push(""); } + // Node islands (report-only): what would run in Node, gap islands + // first (DESIGN.md §1.3: a gap island is compiler work an island hides). + if (input.nodeIslands !== undefined && hasNodeIslandDecisions(input.nodeIslands)) { + renderNodeIslands( + input.nodeIslands, + out, + c, + (loc) => { + const p = locator.position(loc.file, loc.start); + return where({ file: loc.file, ...p }); + }, + opts.root, + ); + out.push(""); + } + // The WebAssembly modules the build can see (wasm/discover.ts). A build // translates the valid ones to native code (wasm/translate.ts) when the // program uses the WebAssembly API; the others fail to load at run time. @@ -450,7 +492,20 @@ function displayPath(file: string, root: string | undefined): string { /** What `coverage --fail-on` checks: reached blockers (they fail a build) * and, optionally, divergences. Unreached blockers never fail. */ -export type CoverageFailOn = "blockers" | "divergences"; +export type CoverageFailOn = + | "blockers" + | "divergences" + | "node-islands" + | "eager-node-islands" + | "island-gaps"; + +export const COVERAGE_FAIL_ON: readonly CoverageFailOn[] = [ + "blockers", + "divergences", + "node-islands", + "eager-node-islands", + "island-gaps", +]; /** Whether the analysis passes the selected gate. Preflight failures * always fail; `failOn` undefined means only preflight fails (the report's @@ -462,6 +517,16 @@ export function coveragePasses(input: CoverageInput, failOn?: CoverageFailOn): b // runtime fences build (the statement throws only if it executes). if (input.diagnostics.length > 0) return false; if (failOn === "divergences" && (input.divergences?.length ?? 0) > 0) return false; + // The Node island gates (report-only islands count: they are what a + // build would choose). Every island evaluates eagerly until deferred + // evaluation lands, so eager-node-islands equals node-islands for now. + // Would-be policy errors (SC8002, SC8003) fail every island gate. + const islands = input.nodeIslands; + if (islands === undefined) return true; + const anyIsland = failOn === "node-islands" || failOn === "eager-node-islands"; + if ((anyIsland || failOn === "island-gaps") && islands.violations.length > 0) return false; + if (anyIsland && (islands.islands.length > 0 || islands.userModules.length > 0)) return false; + if (failOn === "island-gaps" && islands.islands.some((i) => i.class === "gap")) return false; return true; } @@ -524,6 +589,9 @@ export function coverageEnvelope( }), ...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}), ...(input.wasmModules !== undefined ? { wasmModules: input.wasmModules } : {}), + ...(input.nodeIslands !== undefined + ? { nodeIslands: nodeIslandsEnvelope(input.nodeIslands, locator) } + : {}), groups, diagnostics: located, }; @@ -661,3 +729,211 @@ export function renderExplainDynamic( } return out.join("\n"); } + +/* ── Node islands (report-only) ───────────────────────────────────────── */ + +const ISLAND_REASONS_SHOWN = 1; + +function islandName(i: { package: string; version: string | null }): string { + return i.version === null ? i.package : `${i.package}@${i.version}`; +} + +function renderNodeIslands( + report: NodeIslandReport, + out: string[], + c: (code: string, s: string) => string, + at: (loc: SrcLoc) => string, + root: string | undefined, +): void { + const n = report.islands.length + report.userModules.length; + out.push( + ` ${c(BOLD, "Node islands")} ${c(DIM, `(would-be, report-only: the build is unchanged; --node-islands=${report.policy})`)}`, + ); + if (n === 0) out.push(` ${c(DIM, "none")}`); + const widest = Math.max(0, ...report.islands.map((i) => i.class.length)); + for (const island of report.islands) { + const tone = island.class === "gap" ? RED : YELLOW; + const decided = + island.decision === "auto" + ? "" + : c( + DIM, + ` (marked by ${island.decision === "cli" ? "--node-island" : "scriptc.packages"})`, + ); + out.push(` ${c(tone, island.class.padEnd(widest))} ${islandName(island)}${decided}`); + for (const r of island.reasons.slice(0, ISLAND_REASONS_SHOWN)) { + const where = r.loc === null ? "" : `${at(r.loc)}: `; + out.push(` ${c(DIM, capMessage(`${where}${describeReason(r)}`))}`); + } + const more = island.reasons.length - ISLAND_REASONS_SHOWN; + const details = [ + more > 0 + ? `+${more} more reason${more === 1 ? "" : "s"} in ${counted(island.modules, "module")}` + : "", + island.carries.length > 0 ? `carries ${island.carries.join(", ")}` : "", + ].filter((t) => t !== ""); + if (details.length > 0) out.push(` ${c(DIM, details.join("; "))}`); + } + for (const m of report.userModules) { + out.push( + ` ${c(YELLOW, "marked".padEnd(widest))} ${displayPath(m.file, root)} ${c(DIM, `(user module, marked by ${m.source === "cli" ? "--node-island" : "nodeIslands.modules"})`)}`, + ); + } + for (const v of report.violations) { + out.push(` ${c(RED, v.code)} ${capMessage(v.message)}`); + } + const shared = report.packages.filter((p) => p.status === "shared"); + if (shared.length > 0) { + out.push( + ` ${c(DIM, `reached from native code and from an island: ${shared.map((p) => p.package).join(", ")}`)}`, + ); + } + const native = report.packages.filter((p) => p.status === "native"); + if (native.length > 0) { + out.push( + ` ${c(DIM, `${counted(native.length, "package")} stay${native.length === 1 ? "s" : ""} native: ${native.map((p) => p.package).join(", ")}`)}`, + ); + } + if (report.unknownPackages.length > 0) { + out.push( + ` ${c(YELLOW, "note")} not imported by the program: ${report.unknownPackages.join(", ")}`, + ); + } + if (report.islands.length > 0) { + out.push( + ` ${c(DIM, "hint: scriptc coverage --explain-island= lists every reason")}`, + ); + } +} + +/** `scriptc coverage --explain-island=`: why a package would run + * in a Node island — every module's first reason, the native import sites + * (the boundary), the packages inside its closure, and what later + * milestones decide. */ +export function renderExplainIsland( + report: NodeIslandReport | undefined, + query: string, + opts: CoverageRenderOptions = {}, +): string { + const c = (code: string, s: string) => (opts.color ? code + s + RESET : s); + const locator = new SourceLocator(opts.sourceTexts); + const at = (loc: SrcLoc) => { + const p = locator.position(loc.file, loc.start); + return `${displayPath(loc.file, opts.root)}:${p.line}:${p.column}`; + }; + const out: string[] = []; + const matches = (report?.islands ?? []).filter( + (i) => i.package === query || islandName(i) === query, + ); + if (matches.length === 0) { + const pkg = (report?.packages ?? []).find((p) => p.package === query); + if (pkg === undefined) { + out.push(` '${query}' is not a Node island: no npm package of that name is in the program`); + } else if (pkg.status === "inside") { + out.push( + ` '${query}' is not a Node island of its own: only ${pkg.islands.join(", ")} import${pkg.islands.length === 1 ? "s" : ""} it, so it would run inside that island`, + ); + } else if (pkg.status === "shared") { + out.push( + ` '${query}' compiles natively and is also reached from the Node island${pkg.islands.length === 1 ? "" : "s"} ${pkg.islands.join(", ")}`, + ); + } else { + out.push(` '${query}' is not a Node island: it compiles natively`); + } + return out.join("\n"); + } + for (const island of matches) { + out.push( + ` ${c(BOLD, "Node island")} ${islandName(island)} ${c(DIM, "(would-be, report-only)")}`, + ); + if (island.path !== null) out.push(` path ${displayPath(island.path, opts.root)}`); + out.push(` class ${island.class}: ${classDescription(island.class)}`); + out.push( + ` decision ${island.decision === "auto" ? "automatic (npm code outside the native subset)" : island.decision === "cli" ? "marked with --node-island" : 'marked "node" in scriptc.packages'}`, + ); + out.push( + ` evaluation eager at its module position ${c(DIM, "(deferred evaluation is decided by a later milestone)")}`, + ); + if (island.reasons.length > 0) { + out.push(` reasons ${c(DIM, "(first per module; decisive class first)")}`); + const shown = new Set(); + for (const r of island.reasons) { + const key = r.loc === null ? "" : r.loc.file; + if (shown.has(key)) continue; + shown.add(key); + const where = r.loc === null ? "package" : at(r.loc); + out.push(` ${r.class.padEnd(12)} ${where}`); + out.push(` ${c(DIM, capMessage(describeReason(r)))}`); + } + const counts = new Map(); + for (const r of island.reasons) counts.set(r.class, (counts.get(r.class) ?? 0) + 1); + const summary = [...counts].map(([cls, k]) => `${k} ${cls}`).join(", "); + out.push(` all reasons ${island.reasons.length} (${summary})`); + } + if (island.importedFrom.length > 0) { + out.push(` boundary ${counted(island.importedFrom.length, "native import site")}:`); + for (const loc of island.importedFrom.slice(0, SITES_SHOWN)) out.push(` ${at(loc)}`); + if (island.importedFrom.length > SITES_SHOWN) + out.push(` ${c(DIM, `+${island.importedFrom.length - SITES_SHOWN} more`)}`); + out.push( + ` ${c(DIM, "copy and handle decisions per boundary site come with compiler-managed islands")}`, + ); + } else { + out.push( + ` boundary ${c(DIM, "no native import site (reached only from other islands)")}`, + ); + } + if (island.carries.length > 0) out.push(` carries ${island.carries.join(", ")}`); + } + return out.join("\n"); +} + +function nodeIslandsEnvelope( + report: NodeIslandReport, + locator: SourceLocator, +): NodeIslandsEnvelope { + const pos = (loc: SrcLoc): SourcePosition => ({ + file: loc.file, + ...locator.position(loc.file, loc.start), + }); + const entry = (i: NodeIslandEntry) => { + const first = i.reasons[0]; + return { + package: i.package, + version: i.version, + path: i.path, + class: i.class, + reason: i.reason, + diagnostic: i.diagnostic, + decision: i.decision, + evaluation: i.evaluation, + ...(first !== undefined && first.loc !== null ? { at: pos(first.loc) } : {}), + reasons: i.reasons.map((r) => ({ + class: r.class, + rule: r.rule, + reason: r.reason, + code: r.code, + message: r.message, + source: r.source, + ...(r.loc !== null ? { at: pos(r.loc) } : {}), + })), + carries: i.carries, + importedFrom: i.importedFrom.map(pos), + }; + }; + return { + mode: report.mode, + policy: report.policy, + islands: report.islands.map(entry), + userModules: report.userModules.map((m) => ({ file: m.file, decision: m.source })), + npmPackages: report.packages.map((p) => ({ + package: p.package, + version: p.version, + path: p.path, + status: p.status, + islands: p.islands, + })), + violations: report.violations.map((v) => ({ code: v.code, message: v.message, ...pos(v.loc) })), + unknownPackages: report.unknownPackages, + }; +} diff --git a/packages/compiler/src/coverage/verdict-cache.ts b/packages/compiler/src/coverage/verdict-cache.ts index 422bf5e4d..af8269189 100644 --- a/packages/compiler/src/coverage/verdict-cache.ts +++ b/packages/compiler/src/coverage/verdict-cache.ts @@ -15,13 +15,15 @@ import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import type { LowerStats } from "../frontend/lowering/lowerer.js"; import type { NpmStaticStatus } from "./report.js"; import type { WasmModuleReport } from "../wasm/discover.js"; +import type { NodeIslandReport } from "../node-islands/summary.js"; +import { nodeIslandOptionsKey } from "../node-islands/policy.js"; import { FrontendInputTracker, frontendInputsStillMatch, type FrontendInputSnapshot, } from "../frontend/input-tracker.js"; -const SCHEMA = "scriptc.coverage-verdict.v2"; +const SCHEMA = "scriptc.coverage-verdict.v3"; /** Key-value storage for verdict entries (best effort: failures miss). */ export interface VerdictStore { @@ -40,6 +42,7 @@ export interface CachedCoverage { divergences?: ScrDiagnostic[]; npmStatic?: NpmStaticStatus[]; wasmModules?: WasmModuleReport[]; + nodeIslands?: NodeIslandReport; preflightFailed: boolean; } @@ -129,6 +132,10 @@ export function verdictKey( ...(opts.tier === "static-only" ? ["tier=static-only"] : []), // The module files' contents are probes; their names are options. ...(opts.wasm !== undefined && opts.wasm.length > 0 ? [`wasm=${opts.wasm.join("\n")}`] : []), + // The Node island policy's command-line half shapes the report. + ...(opts.nodeIslands !== undefined + ? [`node-islands=${nodeIslandOptionsKey(opts.nodeIslands)}`] + : []), ].join("\0"), ); } @@ -151,6 +158,7 @@ function cacheable(coverage: AnalyzeResult["coverage"]): CachedCoverage | null { if (coverage.divergences !== undefined) out.divergences = coverage.divergences; if (coverage.npmStatic !== undefined) out.npmStatic = coverage.npmStatic; if (coverage.wasmModules !== undefined) out.wasmModules = coverage.wasmModules; + if (coverage.nodeIslands !== undefined) out.nodeIslands = coverage.nodeIslands; return out; } @@ -168,6 +176,7 @@ function restore(cached: CachedCoverage, sources: CachedSource[]): AnalyzeResult if (cached.divergences !== undefined) coverage.divergences = cached.divergences; if (cached.npmStatic !== undefined) coverage.npmStatic = cached.npmStatic; if (cached.wasmModules !== undefined) coverage.wasmModules = cached.wasmModules; + if (cached.nodeIslands !== undefined) coverage.nodeIslands = cached.nodeIslands; return { coverage, sourceTexts }; } diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index e4d4006ca..b66efb351 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -62,6 +62,13 @@ * to a cast to an unrelated class (SC6004); SC6005 (Date strings * outside the native parser's grammar) is retired — the runtime * parses date strings with V8's grammar + * SC8xxx Node island policy: packages and modules outside the native + * subset that run (or, in report-only mode, would run) in a + * Node island — unknown project configuration (SC8004), a + * package required to be native (SC8003) or every island + * refused (SC8002) while outside the native subset, and the + * R2 object-model constructs in user JavaScript (SC8101–SC8105, + * behind --strict-user-js) * SC9xxx internal compiler errors (still source-anchored) */ import type { SrcLoc } from "../ir/ir.js"; @@ -89,6 +96,11 @@ export interface ScrDiagnostic { * the error may not reproduce under the project's tsc. Machine-readable * output reports these under the "environment" category. */ typeWorld?: boolean; + /** Why the construct is outside the native subset, as a Node island + * reason id (node-islands/rules.ts), at the refusal sites whose code + * alone does not say it (a computed require(), a native addon, `with`). + * Report-only: never part of the rendered message. */ + reason?: string; } /** SC3002–SC3004 — native-helper target, installation, and execution @@ -201,6 +213,60 @@ export function unrelatedClassCastDivergenceDiag( }; } +/* ── SC8xxx: Node island policy ───────────────────────────────────────── */ + +/** SC8004 — an unknown key or value in `scriptc.packages` or + * `scriptc.nodeIslands`. */ +export function projectConfigDiag( + path: string, + detail: string, + hint: string, + loc: SrcLoc, +): ScrDiagnostic { + return { + code: "SC8004", + message: `package.json '${path}' ${detail}`, + loc, + hint, + }; +} + +/** SC8002 — Node islands are off (or limited to marked islands) and an + * npm package is outside the native subset. */ +export function nodeIslandsOffDiag(pkg: string, why: string, loc: SrcLoc): ScrDiagnostic { + return { + code: "SC8002", + message: `npm package '${pkg}' is outside the native subset (${why}) and Node islands are off`, + loc, + hint: `allow Node islands (--node-islands=auto), mark it with --node-island=${pkg}, or replace the dependency`, + }; +} + +/** SC8003 — a package marked "native" is outside the native subset. */ +export function nativePackageDiag(pkg: string, why: string, loc: SrcLoc): ScrDiagnostic { + return { + code: "SC8003", + message: `npm package '${pkg}' is marked native but is outside the native subset: ${why}`, + loc, + hint: `remove the "native" marking to let it run in a Node island, or replace the construct the reason names`, + }; +} + +/** SC8101–SC8105 — an R2 object-model construct in the program's own + * JavaScript (R2.1 constructor functions … R2.5 builtin patching). */ +export function userJsObjectModelDiag( + code: "SC8101" | "SC8102" | "SC8103" | "SC8104" | "SC8105", + construct: string, + loc: SrcLoc, +): ScrDiagnostic { + return { + code, + message: `${construct} needs a mutable JavaScript object model, which native code does not have`, + loc, + hint: "rewrite it as a class or typed object, or mark this module as a Node island in scriptc.nodeIslands.modules", + }; +} + /* ── SC5xxx: native FFI ───────────────────────────────────────────────── */ /** SC5001 — the outbound native-FFI manifest is malformed or unreadable. */ @@ -766,6 +832,7 @@ export function nativeAddonDiag(specifier: string, loc: SrcLoc): ScrDiagnostic { loc, milestone: "later", hint: "expose the native operation as a plain C ABI function in an object or static archive, then bind a signature-only TypeScript declaration with --ffi; Node-API callbacks themselves require Node (https://scriptc.dev/ffi#replacing-a-node-api-addon)", + reason: "native-addon", }; } diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index c986fc5b1..42b424a7b 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -142,10 +142,60 @@ export interface DiagnosticsEnvelope { /** coverage only: the WebAssembly modules the build can see, with their * size, validity, proposals and the sources that found them. */ wasmModules?: WasmModuleReport[]; + /** coverage only, when there is something to report: the packages and + * user modules that would run in a Node island (report-only). */ + nodeIslands?: NodeIslandsEnvelope; groups: DiagnosticGroup[]; diagnostics: LocatedDiagnostic[]; } +/** One reason a package is outside the native subset. */ +export interface NodeIslandReasonEnvelope { + class: string; + /** R1.1–R2.5, or null for gap and packaging reasons. */ + rule: string | null; + reason: string; + code: string | null; + message: string; + source: string; + at?: SourcePosition; +} + +/** `scriptc coverage --print=diagnostics`: the Node island decisions. In + * this version they are would-be decisions (mode "would-be"): the build + * does not change. */ +export interface NodeIslandsEnvelope { + mode: "would-be"; + policy: "auto" | "explicit" | "off"; + /** Gap islands first, then by class and name. */ + islands: { + package: string; + version: string | null; + path: string | null; + class: string; + reason: string; + diagnostic: string | null; + decision: string; + evaluation: string; + at?: SourcePosition; + reasons: NodeIslandReasonEnvelope[]; + carries: string[]; + importedFrom: SourcePosition[]; + }[]; + userModules: { file: string; decision: string }[]; + /** Every npm package that is not an island: native, inside an island's + * closure, or shared between native code and an island. */ + npmPackages: { + package: string; + version: string | null; + path: string | null; + status: string; + islands: string[]; + }[]; + violations: (SourcePosition & { code: string; message: string })[]; + unknownPackages: string[]; +} + /** The divergence band: compiles, but the program can behave differently * from Node. Warnings, never build failures. */ export function isDivergenceCode(code: string): boolean { @@ -159,6 +209,7 @@ export function categoryOf(diag: ScrDiagnostic): DiagnosticCategory { if (code === "SC0003") return "invalid-source"; if (code === "SC0002" || code === "SC0004") return "environment"; if (/^SC[35]\d{3}$/.test(code)) return "environment"; + if (code === "SC8004" || code === "SC8005" || code === "SC8012") return "environment"; if (/^SC9\d{3}$/.test(code)) return "internal"; return "unsupported"; } @@ -179,6 +230,8 @@ export function hintOf(diag: ScrDiagnostic): string { return "restate the value's type inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')"; if (/^SC2\d{3}$/.test(code)) return "restate the type inside the compilable set (https://scriptc.dev/limitations describes the boundary)"; + if (/^SC8\d{3}$/.test(code)) + return "see the Node island policy: npm code outside the native subset runs in Node, user code must be marked (scriptc coverage --explain-island=)"; if (/^SC9\d{3}$/.test(code)) return "this is a compiler bug — please report it with the program that triggers it"; return "see https://scriptc.dev/limitations"; diff --git a/packages/compiler/src/frontend/analysis.ts b/packages/compiler/src/frontend/analysis.ts index 43d0aa2bb..5fbd96ba6 100644 --- a/packages/compiler/src/frontend/analysis.ts +++ b/packages/compiler/src/frontend/analysis.ts @@ -4,6 +4,10 @@ import type { AnalyzeOptions, AnalyzeResult } from "../compile-types.js"; import { loadFfiProfile, type FfiProfile } from "../ffi/ffi-manifest.js"; import { provenanceSources } from "./provenance-registry.js"; import type { FrontendFactory } from "./pipeline.js"; +import { readProjectConfig } from "../project-config.js"; +import { nodeIslandPolicy } from "../node-islands/policy.js"; +import { buildNodeIslandReport } from "../node-islands/report.js"; +import { hasNodeIslandFindings } from "../node-islands/summary.js"; /** Analysis without codegen: how much of the program compiles statically. * Unlike compile(), lowering diagnostics are data here, not failure. */ @@ -96,6 +100,23 @@ export function analyzeWithFrontend( ...(opts.wasm !== undefined && opts.wasm.length > 0 ? { wasmFiles: opts.wasm } : {}), }); const provenance = provenanceSources(); + // Node islands, report-only: which packages and modules would run in + // Node, from the detector's scan and the policy. Code generation does + // not depend on it. + const policy = nodeIslandPolicy(readProjectConfig(entryPath), opts.nodeIslands); + const islandReport = + fe.nodeIslandScan === undefined + ? null + : buildNodeIslandReport({ + entry: entryPath, + scan: fe.nodeIslandScan({ userJs: policy.strictUserJs }), + npm: fe.npmStatic, + diagnostics: [...lowered.diagnostics, ...(lowered.unreached?.diagnostics ?? [])], + fences: [...lowered.runtimeFences, ...(lowered.unreached?.runtimeFences ?? [])], + policy, + }); + const nodeIslands = + islandReport !== null && hasNodeIslandFindings(islandReport) ? islandReport : null; return { coverage: { file: entryPath, @@ -103,7 +124,9 @@ export function analyzeWithFrontend( // The import fences report as blockers alongside the statement-level // ones (use sites of the fenced bindings emit matching diagnostics, // which the report groups with these). - diagnostics: [...preflight, ...lowered.diagnostics], + // --strict-user-js: R2 constructs in user JavaScript are blockers + // (SC8101–SC8105), as scope B's builds will make them errors. + diagnostics: [...preflight, ...lowered.diagnostics, ...(nodeIslands?.userJs ?? [])], ...(lowered.runtimeFences.length > 0 ? { runtimeFences: lowered.runtimeFences } : {}), ...(lowered.divergences !== undefined ? { divergences: lowered.divergences } : {}), ...(lowered.unreached ? { unreached: lowered.unreached } : {}), @@ -115,6 +138,7 @@ export function analyzeWithFrontend( ...(lowered.provenanceElided ? { provenanceElided: lowered.provenanceElided } : {}), ...(lowered.tiers !== undefined ? { tiers: lowered.tiers } : {}), ...(lowered.wasmModules !== undefined ? { wasmModules: lowered.wasmModules } : {}), + ...(nodeIslands !== null ? { nodeIslands } : {}), preflightFailed: false, }, sourceTexts: fe.sourceTexts(), diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts index 6b2e9e589..bbaf15353 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts @@ -146,6 +146,8 @@ export function lowerCreateRequireCall( "createRequire's require with this argument shape", call, 'the compiled module graph is fixed at build time — the one lowered form is require("")', + undefined, + "dynamic-require", ); } const spec = cr.spec; diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index e5ce784be..f57d9aedb 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -934,6 +934,8 @@ function appendArgumentsParameter( "SC1090", node, "parameterized 'arguments' outside an ES module (sloppy-mode parameter aliases)", + undefined, + "sloppy-arguments", ); } } diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 2b28c6533..a4df8d910 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -211,6 +211,8 @@ export function lowerUncompiledRequireCall( "SC1090", call, 'require() with a computed specifier (the compiled module graph is fixed at build time: require("") lowers)', + undefined, + "dynamic-require", ); } if (canonicalBuiltinModule(spec) !== null) return null; diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index b5f99d682..bbf725fe9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -786,7 +786,17 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: "finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead", eval: "runtime code evaluation cannot be compiled ahead of time", }; - lowerer.noLowering(expr.text, expr, globalHints[expr.text], sym ?? undefined); + lowerer.noLowering( + expr.text, + expr, + globalHints[expr.text], + sym ?? undefined, + expr.text === "WeakRef" || expr.text === "FinalizationRegistry" + ? "weak-ref" + : expr.text === "eval" + ? "code-from-string" + : undefined, + ); } } // An ambient global nothing defines compiles to exactly what Node does diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index 37675777c..b383d2aff 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -586,6 +586,8 @@ export function fenceNodeModuleMutation( `${action} through require.cache`, target, "a compiled binary has a fixed module graph; cache lookup and Object.keys(require.cache) are read-only", + undefined, + "loader-hook", ); } if ( diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index e968af638..74b2a5709 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2203,6 +2203,8 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression call, "dynamic import() of computed specifiers (the module graph resolves at " + "build time — the specifier must be a string literal)", + undefined, + "dynamic-import", ); } if (call.arguments.nodes.length !== 1) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 543149f83..4fd9afbc0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -8067,8 +8067,11 @@ export class Lowerer { node: ts.Node, featureOverride?: string, hintOverride?: string, + reason?: string, ): never { - this.pushDiag(unsupportedDiag(code, locOf(node), featureOverride, hintOverride)); + const diag = unsupportedDiag(code, locOf(node), featureOverride, hintOverride); + if (reason !== undefined) diag.reason = reason; + this.pushDiag(diag); throw new PoisonError(); } @@ -8294,8 +8297,16 @@ export class Lowerer { * nothing lowers. Poisons the statement like every other rejection. * `sym`, when given, picks the wording: surface declared only by the * adopted @types/node is blamed at @types/node. */ - noLowering(surface: string, node: ts.Node, hint?: string, sym?: ts.Symbol | null): never { - this.pushDiag(noLoweringDiag(surface, locOf(node), hint, this.nodeTypesOnlySymbol(sym))); + noLowering( + surface: string, + node: ts.Node, + hint?: string, + sym?: ts.Symbol | null, + reason?: string, + ): never { + const diag = noLoweringDiag(surface, locOf(node), hint, this.nodeTypesOnlySymbol(sym)); + if (reason !== undefined) diag.reason = reason; + this.pushDiag(diag); throw new PoisonError(); } diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts index 86a41d3a6..1c315a4cc 100644 --- a/packages/compiler/src/frontend/package-shims.ts +++ b/packages/compiler/src/frontend/package-shims.ts @@ -42,6 +42,7 @@ import { dirname, join, resolve } from "node:path"; import { packageShimRoot, tsgoPath } from "./dts-paths.js"; import { trackedDirectoryExists, trackedReadFile, trackedRealpath } from "./input-tracker.js"; +import { readProjectConfig } from "../project-config.js"; import { packageNameOfSpecifier } from "./workspace-registry.js"; import { BUILTIN_ALIAS_PACKAGES, @@ -150,36 +151,12 @@ export function packageShimEntries(): string[] { let unloadedPackages: ReadonlySet = new Set(); /** Reads the project's package-shim configuration: the `scriptc.packages` - * map of the entry's nearest package.json. Called once per program load. */ + * entries marked "unloaded" in the entry's nearest package.json + * (project-config.ts validates the rest). Called once per program load. */ export function configurePackageShims(entryPath: string): void { const packages = new Set(); - for (let dir = dirname(resolve(entryPath)); ;) { - const text = trackedReadFile(join(dir, "package.json")); - if (text !== null) { - let parsed: unknown = null; - try { - parsed = JSON.parse(text); - } catch { - parsed = null; - } - const config = - parsed !== null && typeof parsed === "object" - ? (parsed as Record)["scriptc"] - : undefined; - const map = - config !== null && typeof config === "object" - ? (config as Record)["packages"] - : undefined; - if (map !== null && typeof map === "object") { - for (const [name, kind] of Object.entries(map as Record)) { - if (kind === "unloaded") packages.add(name); - } - } - break; - } - const parent = dirname(dir); - if (parent === dir) break; - dir = parent; + for (const [name, setting] of readProjectConfig(entryPath).packages) { + if (setting.unloaded) packages.add(name); } unloadedPackages = packages; } diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 193e8adc0..5ee2a8bea 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -46,6 +46,14 @@ import { CONTRACT_TYPES_MODULE } from "./contract-types.js"; import { THREADS_MODULE } from "./threads-import.js"; import { erasedTypeOnlyImport } from "./type-only-imports.js"; import { EXPERIMENTAL_MODULE } from "./experimental-import.js"; +import { readProjectConfig } from "../project-config.js"; +import { packageDirOf } from "../node-islands/origin.js"; +import { + scanFiles, + type NodeIslandScan, + type RefusalKind, + type RefusalRecord, +} from "../node-islands/scan.js"; /** Where a package-anchored preflight diagnostic sits, as * `:` — the SC2013 refusal that drops the package @@ -104,6 +112,10 @@ export interface Frontend { npmStatic: NpmStaticStatus[]; /** Each judged npm package's first import site, used to anchor refusals. */ npmImportSites: ReadonlyMap; + /** The Node island detector's input (report-only): the program's + * modules with their pre-scan markers, package import edges, and how + * each refused package was refused. Call before dispose. */ + nodeIslandScan?: (opts: { userJs: boolean }) => NodeIslandScan; /** Releases the frontend's resources (the spawned tsgo server). Call * exactly once, after the last lower(). */ dispose: () => void; @@ -384,6 +396,30 @@ function packagesNamedByDiag(message: string, optedIn: ReadonlySet): Set return hits; } +/** The modules of packages the drop-and-reload loop is about to remove, + * scanned on the program that still holds them. Report-only: a detector + * failure here never changes the build's outcome. */ +function scanDroppedPackages( + load: LoadResult, + packages: readonly string[], + into: { modules: NodeIslandScan["modules"]; edges: NodeIslandScan["edges"] }, +): void { + if (packages.length === 0) return; + try { + const wanted = new Set(packages); + const files = load.program.getImplementationSourceFiles().filter((sf) => { + if (sf.isDeclarationFile) return false; + const at = packageDirOf(sf.fileName); + return at !== null && wanted.has(at.name); + }); + const scanned = scanFiles(load.program, files, { userJs: false, dropped: true, edges: true }); + into.modules.push(...scanned.modules); + into.edges.push(...scanned.edges); + } catch { + /* report-only detection */ + } +} + export function runFrontend( entryPath: string, loadProgram: ProgramLoader, @@ -458,6 +494,16 @@ function loadFrontend( clearResolveCaches(); const statuses: NpmStaticStatus[] = []; const npmSites = new Map(); + // Node island detection (report-only): how each package was refused, and + // the modules of packages removed from the program. + const refusals: RefusalRecord[] = []; + const droppedScan: { modules: NodeIslandScan["modules"]; edges: NodeIslandScan["edges"] } = { + modules: [], + edges: [], + }; + const recordRefusal = (pkg: string, kind: RefusalKind, diag: ScrDiagnostic | null): void => { + refusals.push({ package: pkg, kind, diag }); + }; // Explicit library attempts bypass only the auto-selection heuristics. // The shared preflight/lowering refusal and dependency-closure checks remain. const judged = new Set(libraryNpmStatic); @@ -513,6 +559,7 @@ function loadFrontend( status: "refused", detail: `mapped as an external host module by --external-types (${specs.map((s) => JSON.stringify(s)).join(", ")})`, }); + recordRefusal(pkg, "external", null); return false; }); } @@ -546,19 +593,25 @@ function loadFrontend( const typecheckRefused = new Set(); while (effective.size > 0) { const reasons = new Map(npmStaticOffenders()); + const kinds = new Map(); + for (const pkg of reasons.keys()) kinds.set(pkg, { kind: "offender", diag: null }); // Bundler interop the rewrite could not finish refuses its package // only where the compiled program can load the file. if (npmStaticFileDegrades().size > 0) { const loadable = npmStaticLoadableFiles(load.program, load.moduleOrder); for (const [file, { pkg, reason }] of npmStaticFileDegrades()) { - if (!reasons.has(pkg) && (loadable === null || loadable.has(file))) + if (!reasons.has(pkg) && (loadable === null || loadable.has(file))) { reasons.set(pkg, reason); + kinds.set(pkg, { kind: "degrade", diag: null }); + } } } for (const d of preflight) { const pkg = npmStaticPackageOfPath(d.loc.file); - if (pkg !== null && !reasons.has(pkg)) + if (pkg !== null && !reasons.has(pkg)) { reasons.set(pkg, `${d.code} at ${packageDiagWhere(load, d.loc)}: ${d.message}`); + kinds.set(pkg, { kind: "preflight", diag: d }); + } } if (![...reasons.keys()].some((p) => effective.has(p))) { const named = new Map(); @@ -570,6 +623,7 @@ function loadFrontend( } for (const [pkg, count] of named) { typecheckRefused.add(pkg); + kinds.set(pkg, { kind: "typecheck", diag: null }); reasons.set( pkg, `its inferred export surface breaks ${count} import site${count === 1 ? "" : "s"} in program files (bundler-emitted surfaces type only as far as inference reaches)`, @@ -578,9 +632,12 @@ function loadFrontend( } const dropping = [...reasons.keys()].filter((p) => effective.has(p)); if (dropping.length === 0) break; + scanDroppedPackages(load, dropping, droppedScan); for (const p of dropping) { effective.delete(p); statuses.push({ package: p, status: "refused", detail: reasons.get(p)! }); + const kind = kinds.get(p); + recordRefusal(p, kind?.kind ?? "offender", kind?.diag ?? null); } load.dispose(); load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes }); @@ -600,9 +657,12 @@ function loadFrontend( // graceful per-package degradation, never a failed gate the user cannot // act on (the note carries the why). if (effective.size > 0 && preflight.some((d) => d.code === "SC0001")) { + const lastResortDropped: string[] = []; const dropWithNote = (p: string): void => { effective.delete(p); typecheckRefused.add(p); + lastResortDropped.push(p); + recordRefusal(p, "typecheck", null); statuses.push({ package: p, status: "refused", @@ -624,11 +684,13 @@ function loadFrontend( probe.dispose(); if (probeDiags.some((d) => d.code === "SC0001")) dropWithNote(p); } + scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); load.dispose(); load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes }); preflight = checkPreflight(load); if (preflight.some((d) => d.code === "SC0001") && effective.size > 0) { for (const p of [...effective]) dropWithNote(p); + scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); load.dispose(); load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes }); preflight = checkPreflight(load); @@ -649,8 +711,12 @@ function loadFrontend( } const finalLoad = load; + // An unknown key or value in the project's scriptc configuration fails + // the build before anything else (SC8004). + const projectConfig = readProjectConfig(entryPath); + const configDiagnostics = projectConfig.diagnostics; return { - preflight, + preflight: configDiagnostics.length > 0 ? [...configDiagnostics, ...preflight] : preflight, entryText: () => finalLoad.entry.text, entryExports: () => entryFunctionExports(finalLoad.entry), // The contract scans the PROGRAM's source files, not the runtime @@ -677,14 +743,23 @@ function loadFrontend( // are in moduleOrder like any program module, so their bytes join the // library identity hashes (source_hash/build_id) — compiled code is // identity, whatever directory it came from. - sourceTexts: () => - new Map( + sourceTexts: () => { + const texts = new Map( [ finalLoad.entry, ...finalLoad.moduleOrder, ...finalLoad.program.getImplementationSourceFiles().filter((sf) => !sf.isDeclarationFile), ].map((sf) => [sf.fileName, sf.text]), - ), + ); + // SC8004 diagnostics point into the project's package.json. + if ( + configDiagnostics.length > 0 && + projectConfig.manifest !== null && + projectConfig.manifestText !== null + ) + texts.set(projectConfig.manifest, projectConfig.manifestText); + return texts; + }, lower: (opts) => lowerToIr(finalLoad.program, finalLoad.entry, finalLoad.moduleOrder, { ...opts, @@ -698,6 +773,26 @@ function loadFrontend( }), npmStatic: statuses, npmImportSites: npmSites, + nodeIslandScan: ({ userJs }) => { + const files = new Set([finalLoad.entry, ...finalLoad.moduleOrder]); + addDynamicImportGraph(finalLoad, files); + // Package edges matter only when the program has npm packages: one + // that loads, one that was refused, or one judged and dropped. + const npm = + statuses.length > 0 || + droppedScan.modules.length > 0 || + [...files].some((sf) => sf.fileName.includes("/node_modules/")); + const scanned = scanFiles(finalLoad.program, [...files], { + userJs, + dropped: false, + edges: npm, + }); + return { + modules: [...droppedScan.modules, ...scanned.modules], + edges: [...droppedScan.edges, ...scanned.edges], + refusals, + }; + }, dispose: finalLoad.dispose, }; } diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index e9b8514af..78a6a1e24 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -182,11 +182,14 @@ export { export { renderCoverage, renderExplainDynamic, + renderExplainIsland, coverageEnvelope, coveragePasses, type CoverageInput, type CoverageFailOn, } from "./coverage/report.js"; +export type { NodeIslandReport, NodeIslandEntry } from "./node-islands/summary.js"; +export type { NodeIslandOptions, NodeIslandsMode } from "./node-islands/policy.js"; export { TIER_MODES, type DemotedUnit, diff --git a/packages/compiler/src/node-islands/classify.ts b/packages/compiler/src/node-islands/classify.ts new file mode 100644 index 000000000..c41b9437c --- /dev/null +++ b/packages/compiler/src/node-islands/classify.ts @@ -0,0 +1,131 @@ +/* Diagnostic → Node island class (DESIGN.md §1.3). A refusal in a module + * of an npm package is a reason for that package to run in a Node island; + * its class says why: the runtime itself (R1), a mutable object model (R2), + * a shipping problem (packaging), or a construct inside the native subset + * that scriptc cannot compile yet (gap). The reason id on the diagnostic + * (ScrDiagnostic.reason) decides first; the code and message decide for + * refusal sites that do not carry one. */ +import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; +import type { NodeIslandMarker } from "./detect.js"; +import { classOfRule, ruleInfo, ruleOfReason } from "./rules.js"; +import type { RefusalRecord } from "./scan.js"; +import type { NodeIslandReason, NodeIslandReasonSource } from "./summary.js"; + +/** Message shapes of refusal sites that predate reason ids. */ +const MESSAGE_REASONS: { code: string; pattern: RegExp; reason: string }[] = [ + { code: "SC2010", pattern: /./, reason: "code-from-string" }, + { code: "SC1010", pattern: /'(?:node:)?vm'/, reason: "vm" }, + { code: "SC1090", pattern: /^dynamic import\(\) of computed/, reason: "dynamic-import" }, + { code: "SC1090", pattern: /require\(\) with a computed specifier/, reason: "dynamic-require" }, + { code: "SC1090", pattern: /'with' statement/, reason: "with" }, + { code: "SC1090", pattern: /sloppy-mode parameter aliases/, reason: "sloppy-arguments" }, + { code: "SC2020", pattern: /^'(?:eval|Function)'/, reason: "code-from-string" }, + { + code: "SC2020", + pattern: /createRequire's require with this argument shape/, + reason: "dynamic-require", + }, + { code: "SC2020", pattern: /native addon/, reason: "native-addon" }, + { + code: "SC2020", + pattern: /through require\.cache|require\.extensions|Module\._|module\.register/, + reason: "loader-hook", + }, + { + code: "SC2020", + pattern: /process\.binding|_linkedBinding|internalBinding|setFlagsFromString/, + reason: "engine-internal", + }, + { code: "SC2020", pattern: /\bWeakRef\b|\bFinalizationRegistry\b/, reason: "weak-ref" }, + { code: "SC1100", pattern: /\bWeakRef\b|\bFinalizationRegistry\b/, reason: "weak-ref" }, +]; + +function reasonIdOf(d: ScrDiagnostic): string | null { + if (d.reason !== undefined) return d.reason; + for (const entry of MESSAGE_REASONS) { + if (entry.code === d.code && entry.pattern.test(d.message)) return entry.reason; + } + return null; +} + +/** Codes that are not reasons of the package they appear in: a cascade + * marker repeats a root cause, an import of another refused package is + * that package's island boundary, and divergences compile. */ +export function isIslandReasonCode(code: string): boolean { + if (code === "SC2004" || code === "SC2013") return false; + if (/^SC6\d{3}$/.test(code)) return false; + return true; +} + +export function classifyDiagnostic( + d: ScrDiagnostic, + source: NodeIslandReasonSource, +): NodeIslandReason { + const id = reasonIdOf(d); + const rule = id === null ? null : ruleOfReason(id); + if (rule !== null && id !== null) { + return { + class: classOfRule(rule), + rule, + reason: id, + code: d.code, + message: d.message, + loc: d.loc, + source, + }; + } + return { + class: "gap", + rule: null, + reason: "gap", + code: d.code, + message: d.message, + loc: d.loc, + source, + }; +} + +export function classifyMarker(m: NodeIslandMarker): NodeIslandReason { + return { + class: classOfRule(m.rule), + rule: m.rule, + reason: m.reason, + code: null, + message: `${ruleInfo(m.rule).title}: ${m.construct}`, + loc: m.loc, + source: "pre-scan", + }; +} + +/** A refused package's status (pipeline.ts): the drop-and-reload record + * when there is one, else the declaration and shipped-JS eligibility bar, + * which is a packaging reason. */ +export function classifyRefusal( + detail: string, + record: RefusalRecord | null, +): NodeIslandReason | null { + if (record !== null && record.kind === "external") return null; + if (record !== null && record.kind === "preflight" && record.diag !== null) { + return classifyDiagnostic(record.diag, "package"); + } + if (record !== null && record.kind === "offender" && /require|createRequire/.test(detail)) { + return { + class: "runtime", + rule: "R1.2", + reason: "dynamic-require", + code: "SC2013", + message: detail, + loc: null, + source: "package", + }; + } + return { + class: "packaging", + rule: null, + reason: "packaging", + code: "SC2013", + message: detail, + loc: null, + source: "package", + }; +} diff --git a/packages/compiler/src/node-islands/detect.test.ts b/packages/compiler/src/node-islands/detect.test.ts new file mode 100644 index 000000000..2a21395da --- /dev/null +++ b/packages/compiler/src/node-islands/detect.test.ts @@ -0,0 +1,165 @@ +import { afterAll, describe, expect, test } from "vitest"; +import { closeSourceParser, parseSourceFile } from "../frontend/ts7/source-parser-node.js"; +import { scanModule } from "./detect.js"; + +afterAll(() => closeSourceParser()); + +let serial = 0; +/** The markers of one JavaScript module as `rule reason` strings. */ +function markers(source: string, ext = "cjs"): string[] { + const sf = parseSourceFile(`/virtual/m${serial++}.${ext}`, source, "js"); + return scanModule(sf).map((m) => `${m.rule} ${m.reason}`); +} + +describe("R1 runtime markers", () => { + test("R1.1 code from strings", () => { + expect(markers("eval(code);")).toEqual(["R1.1 code-from-string"]); + expect(markers("(0, eval)(code);")).toEqual(["R1.1 code-from-string"]); + expect(markers("const f = new Function('a', 'return a');")).toEqual(["R1.1 code-from-string"]); + expect(markers("const f = Function('a', body);")).toEqual(["R1.1 code-from-string"]); + expect(markers("const vm = require('node:vm');")).toEqual(["R1.1 vm"]); + expect(markers("import vm from 'vm';", "mjs")).toEqual(["R1.1 vm"]); + }); + + test("R1.1 exceptions: the global-object idiom and a capability probe in try", () => { + expect(markers("const g = Function('return this')();")).toEqual([]); + expect(markers("let ok; try { new Function(''); ok = true; } catch { ok = false; }")).toEqual( + [], + ); + }); + + test("R1.2 dynamic module loading", () => { + expect(markers("require(name);")).toEqual(["R1.2 dynamic-require"]); + expect(markers("import(`./${x}.js`);", "mjs")).toEqual(["R1.2 dynamic-import"]); + expect(markers("const r = createRequire(import.meta.url); r(spec);", "mjs")).toEqual([ + "R1.2 dynamic-require", + ]); + expect(markers("require('./a' + '.js'); require.resolve(name); import('./b.js');")).toEqual([]); + }); + + test("R1.3 native addons", () => { + expect(markers("require('./build/addon.node');")).toEqual(["R1.3 native-addon"]); + expect(markers("const b = require('bindings')('addon');")).toEqual(["R1.3 native-addon"]); + expect(markers("process.dlopen(module, path);")).toEqual(["R1.3 native-addon"]); + }); + + test("R1.4 loader and require hooks", () => { + expect(markers("require.extensions['.coffee'] = compile;")).toEqual(["R1.4 loader-hook"]); + expect(markers("delete require.cache[key];")).toEqual(["R1.4 loader-hook"]); + expect(markers("const Module = require('module'); Module._load = load;")).toEqual([ + "R1.4 loader-hook", + ]); + expect(markers("const m = require('node:module'); m.registerHooks({});")).toEqual([ + "R1.4 loader-hook", + ]); + expect(markers("require('require-in-the-middle');")).toEqual(["R1.4 loader-hook"]); + }); + + test("R1.5 engine internals", () => { + expect(markers("process.binding('fs');")).toEqual(["R1.5 engine-internal"]); + expect(markers("const v8 = require('v8'); v8.setFlagsFromString('--x');")).toEqual([ + "R1.5 engine-internal", + ]); + expect(markers("global.gc();")).toEqual(["R1.5 engine-internal"]); + }); + + test("R1.6 runtime-only globals", () => { + expect(markers("const r = new WeakRef(o);")).toEqual(["R1.6 weak-ref"]); + expect(markers("const f = new FinalizationRegistry(cb);")).toEqual(["R1.6 weak-ref"]); + expect(markers("if (typeof WeakRef !== 'undefined') {}")).toEqual([]); + expect(markers("const o = { WeakRef: 1 }; o.WeakRef;")).toEqual([]); + }); +}); + +describe("R2 object-model markers", () => { + test("R2.1 constructor functions", () => { + expect(markers("function F() {} const f = new F();")).toEqual(["R2.1 constructor-function"]); + expect(markers("function F() {} F.prototype.m = function () {};")).toEqual([ + "R2.1 constructor-function", + ]); + expect(markers("const util = require('util'); util.inherits(A, B);")).toEqual([ + "R2.1 constructor-function", + ]); + expect(markers("function Child() { Parent.call(this, 1); }")).toEqual([ + "R2.1 constructor-function", + ]); + expect(markers("class C {} const c = new C();")).toEqual([]); + expect( + markers("function wrap(fn) { return function () { return fn.apply(this, arguments); }; }"), + ).toEqual([]); + }); + + test("R2.2 prototype mutation", () => { + expect(markers("Object.setPrototypeOf(a, b);")).toEqual(["R2.2 prototype-write"]); + expect(markers("o.__proto__ = p;")).toEqual(["R2.2 prototype-write"]); + expect(markers("const o = { __proto__: base };")).toEqual(["R2.2 prototype-write"]); + // The transpiled-Error idiom and null-prototype dictionaries stay. + expect( + markers( + "class E extends Error { constructor() { super(); Object.setPrototypeOf(this, E.prototype); } }", + ), + ).toEqual([]); + expect(markers("const d = { __proto__: null };")).toEqual([]); + }); + + test("R2.3 shape mutation outside the checker's view", () => { + expect(markers("Object.defineProperty(o, 'k', { get() { return 1; } });")).toEqual([ + "R2.3 define-property", + ]); + expect(markers("class C {} C.prototype.extra = 1;")).toEqual(["R2.3 define-property"]); + // Module objects and interop helpers build module records. + expect(markers("Object.defineProperty(exports, '__esModule', { value: true });")).toEqual([]); + expect( + markers( + "var __createBinding = function (o, m, k) { Object.defineProperty(o, k, { enumerable: true, get: function () { return m[k]; } }); };", + ), + ).toEqual([]); + }); + + test("R2.4 sloppy-mode semantics", () => { + expect(markers("with (o) { x; }")).toEqual(["R2.4 with"]); + expect(markers("function f(a) { arguments[0] = 1; return a; }")).toEqual([ + "R2.4 sloppy-arguments", + ]); + expect(markers("function f(a) { a = 2; return arguments[0]; }")).toEqual([ + "R2.4 sloppy-arguments", + ]); + expect(markers("function f() { return arguments.callee; }")).toEqual(["R2.4 sloppy-arguments"]); + expect(markers("var n = 010;")).toEqual(["R2.4 sloppy-mode"]); + expect(markers("var x = 1; delete x;")).toEqual(["R2.4 sloppy-mode"]); + // Strict code: a prologue, an ES module, a class body. + expect(markers("'use strict'; function f(a) { a = 2; return arguments[0]; }")).toEqual([]); + expect(markers("export function f(a) { a = 2; return arguments[0]; }", "mjs")).toEqual([]); + expect(markers("class C { m(a) { a = 2; return arguments[0]; } }")).toEqual([]); + }); + + test("R2.5 builtin and global patching", () => { + expect(markers("Array.prototype.last = function () {};")).toEqual(["R2.5 builtin-write"]); + expect(markers("Promise = Bluebird;")).toEqual(["R2.5 builtin-write"]); + expect(markers("globalThis.fetch = f;")).toEqual(["R2.5 builtin-write"]); + expect(markers("const fs = require('fs'); fs.close = patched;")).toEqual([ + "R2.5 builtin-write", + ]); + expect(markers("const p = new Proxy(t, h);")).toEqual(["R2.5 proxy"]); + // Modeled process state and tuning knobs stay; so do redeclared names. + expect( + markers("process.exitCode = 1; process.env.X = 'y'; Error.stackTraceLimit = 50;"), + ).toEqual([]); + expect(markers("var Promise = require('bluebird'); Promise.config = {};")).toEqual([]); + }); +}); + +test("markers come back in source order with their locations", () => { + const source = "Object.setPrototypeOf(a, b);\neval(x);\n"; + const sf = parseSourceFile(`/virtual/order${serial++}.cjs`, source, "js"); + const found = scanModule(sf); + expect(found.map((m) => [m.rule, source.slice(m.loc.start, m.loc.end)])).toEqual([ + ["R2.2", "Object.setPrototypeOf(a, b)"], + ["R1.1", "eval(x)"], + ]); +}); + +test("computed writes keep the member they write through", () => { + expect(markers("process.env[key] = value;")).toEqual([]); + expect(markers("Array.prototype[name] = fn;")).toEqual(["R2.5 builtin-write"]); +}); diff --git a/packages/compiler/src/node-islands/detect.ts b/packages/compiler/src/node-islands/detect.ts new file mode 100644 index 000000000..f612d58e5 --- /dev/null +++ b/packages/compiler/src/node-islands/detect.ts @@ -0,0 +1,849 @@ +/* The pre-scan (DESIGN.md §1.4, phase 1): a syntactic walk over one + * JavaScript module that finds the R1 (runtime) and R2 (object-model) + * markers of rules.ts. It is a fast path and a source of good reasons; it + * never decides alone that a module is native, and the authoritative signal + * stays the lowering's refusals (classify.ts). + * + * Every check is local to the file: a name is a "function" when the file + * declares it as a function, a builtin global when the file does not + * declare it at all, and a builtin module object when the file binds it to + * require("") or imports it from one. Checker-dependent parts of + * R2.3 (writes the declared type does not cover, freeze followed by writes) + * and R2.4 (assignments to undeclared identifiers, `this` as the global + * object) are left to the lowering. */ +import type { SrcLoc } from "../ir/ir.js"; +import * as ts from "../frontend/ts7/adapter.js"; +import { SUPPORTED_BUILTIN_MODULES } from "../frontend/builtin-modules.js"; +import type { NodeIslandRule } from "./rules.js"; + +export interface NodeIslandMarker { + rule: NodeIslandRule; + /** The reason id (rules.ts). */ + reason: string; + /** The construct, as a short phrase for messages. */ + construct: string; + loc: SrcLoc; +} + +/** Markers past this count in one module add nothing to a report. */ +const MARKER_CAP = 64; + +const BUILTIN_GLOBALS = new Set([ + "Object", + "Array", + "String", + "Number", + "Boolean", + "Function", + "Symbol", + "BigInt", + "Promise", + "Error", + "TypeError", + "RangeError", + "SyntaxError", + "ReferenceError", + "EvalError", + "URIError", + "AggregateError", + "Map", + "Set", + "WeakMap", + "WeakSet", + "RegExp", + "Date", + "Math", + "JSON", + "Reflect", + "Intl", + "ArrayBuffer", + "SharedArrayBuffer", + "DataView", + "Uint8Array", + "Int8Array", + "Uint16Array", + "Int16Array", + "Uint32Array", + "Int32Array", + "Float32Array", + "Float64Array", + "BigInt64Array", + "BigUint64Array", + "Uint8ClampedArray", + "Buffer", + "process", + "console", + "globalThis", + "global", + "URL", + "URLSearchParams", + "TextEncoder", + "TextDecoder", + "AbortController", + "AbortSignal", + "EventTarget", + "Event", + "fetch", + "setTimeout", + "setInterval", + "setImmediate", + "clearTimeout", + "clearInterval", + "clearImmediate", + "queueMicrotask", + "structuredClone", +]); + +/** Writes to builtin members that the native subset models (process + * state, tuning knobs). */ +const ALLOWED_BUILTIN_WRITES = new Set([ + "process.exitCode", + "process.title", + "process.env", + "Error.stackTraceLimit", + "Buffer.poolSize", + "events.defaultMaxListeners", + "events.EventEmitter.defaultMaxListeners", + "EventEmitter.defaultMaxListeners", +]); + +const ADDON_LOADERS = new Set([ + "bindings", + "node-gyp-build", + "node-pre-gyp", + "@mapbox/node-pre-gyp", + "prebuild-install", + "node-gyp-build-optional-packages", +]); + +const LOADER_HOOK_PACKAGES = new Set(["import-in-the-middle", "require-in-the-middle"]); + +function builtinModuleName(spec: string): string | null { + const bare = spec.startsWith("node:") ? spec.slice(5) : spec; + if ((SUPPORTED_BUILTIN_MODULES as readonly string[]).includes(bare)) return bare; + if (bare === "vm" || bare === "v8" || bare === "module" || bare === "inspector") return bare; + return null; +} + +function literalText(node: ts.Node | undefined): string | null { + if (node === undefined) return null; + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text; + return null; +} + +/** A specifier that folds at build time: a literal, or a concatenation of + * literals. */ +function isFoldableSpecifier(node: ts.Node): boolean { + if (literalText(node) !== null) return true; + if (ts.isParenthesizedExpression(node)) return isFoldableSpecifier(node.expression); + if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) + return isFoldableSpecifier(node.left) && isFoldableSpecifier(node.right); + return false; +} + +function isAssignmentOperator(kind: ts.SyntaxKind): boolean { + return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment; +} + +function propertyName(node: ts.Node): string | null { + if (ts.isPropertyAccessExpression(node)) { + const name = node.name; + return ts.isIdentifier(name) ? name.text : null; + } + if (ts.isElementAccessExpression(node)) return literalText(node.argumentExpression); + return null; +} + +/** `a.b.c` as "a.b.c" (element accesses with literal keys included), or + * null when the chain has a computed key or does not start at a name. */ +function chainText(node: ts.Node): string | null { + if (ts.isIdentifier(node)) return node.text; + if (node.kind === ts.SyntaxKind.ThisKeyword) return "this"; + if (ts.isParenthesizedExpression(node)) return chainText(node.expression); + if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) { + const base = chainText(node.expression); + const name = propertyName(node); + if (base === null || name === null) return null; + return `${base}.${name}`; + } + return null; +} + +/** Like chainText, with computed keys spelled `[…]`: `process.env[…]`. */ +function memberText(node: ts.Node): string | null { + const text = chainText(node); + if (text !== null) return text; + if (ts.isElementAccessExpression(node)) { + const base = memberText(node.expression); + return base === null ? null : `${base}[…]`; + } + if (ts.isPropertyAccessExpression(node)) { + const base = memberText(node.expression); + const name = propertyName(node); + return base === null || name === null ? null : `${base}.${name}`; + } + return null; +} + +/** The identifier a member chain starts at (`a` of `a.b[c].d`). */ +function chainRoot(node: ts.Node): ts.Identifier | null { + let cur = node; + for (;;) { + if (ts.isIdentifier(cur)) return cur; + if (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) { + cur = cur.expression; + continue; + } + if (ts.isParenthesizedExpression(cur)) { + cur = cur.expression; + continue; + } + return null; + } +} + +function hasUseStrict(statements: readonly ts.Node[]): boolean { + for (const stmt of statements) { + if (!ts.isExpressionStatement(stmt)) return false; + const text = literalText(stmt.expression); + if (text === null) return false; + if (text === "use strict") return true; + } + return false; +} + +/** ES module syntax at the top level (static import or export): the + * module is strict. The parser's module indicator is not used, since it + * also marks CommonJS files under forced module detection. */ +function hasEsmSyntax(sf: ts.SourceFile): boolean { + for (const stmt of sf.statements.nodes) { + if (ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt)) return true; + if (ts.isExportAssignment(stmt)) return true; + const head = sf.text.slice(ts.tokenStartOfNode(stmt), ts.tokenStartOfNode(stmt) + 7); + if (head === "export ") return true; + } + return false; +} + +function isFunctionBoundary(node: ts.Node): boolean { + return ( + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isMethodDeclaration(node) || + ts.isConstructorDeclaration(node) || + ts.isGetAccessorDeclaration(node) || + ts.isSetAccessorDeclaration(node) + ); +} + +/** One module's pre-scan. */ +class ModuleScanner { + readonly markers: NodeIslandMarker[] = []; + /** Every name the file declares (any scope): a builtin global the file + * redeclares is not the builtin. */ + private readonly declared = new Set(); + /** Names declared as functions (declarations and function-valued + * variables). */ + private readonly functions = new Set(); + private readonly classes = new Set(); + /** Local name → the builtin module (or `module.member`) it is bound to. */ + private readonly builtinBindings = new Map(); + /** Local names bound to a createRequire(...) result. */ + private readonly createdRequires = new Set(); + private readonly strictness = new Map(); + private readonly moduleStrict: boolean; + + constructor(private readonly sf: ts.SourceFile) { + const name = sf.fileName; + this.moduleStrict = + name.endsWith(".mjs") || + name.endsWith(".mts") || + hasEsmSyntax(sf) || + hasUseStrict(sf.statements.nodes); + } + + scan(): NodeIslandMarker[] { + this.collectDeclarations(); + ts.walkPreorder(this.sf, (node) => { + if (this.markers.length >= MARKER_CAP) return "stop"; + this.visit(node); + return undefined; + }); + return this.markers; + } + + private add(rule: NodeIslandRule, reason: string, construct: string, node: ts.Node): void { + if (this.markers.length >= MARKER_CAP) return; + this.markers.push({ + rule, + reason, + construct, + loc: { file: this.sf.fileName, start: ts.tokenStartOfNode(node), end: node.end }, + }); + } + + private bindName(name: ts.Node | undefined, init: ts.Node | undefined): void { + if (name === undefined) return; + if (ts.isIdentifier(name)) { + this.declared.add(name.text); + if (init === undefined) return; + let value = init; + while (ts.isParenthesizedExpression(value)) value = value.expression; + if (ts.isFunctionExpression(value) || ts.isArrowFunction(value)) + this.functions.add(name.text); + else if (ts.isClassExpression(value)) this.classes.add(name.text); + const bound = this.builtinOf(value); + if (bound !== null) this.builtinBindings.set(name.text, bound); + if (ts.isCallExpression(value) && this.isCreateRequireCallee(value.expression)) + this.createdRequires.add(name.text); + return; + } + if (ts.isObjectBindingPattern(name)) { + const base = init === undefined ? null : this.builtinOf(init); + for (const element of name.elements.nodes) { + if (!ts.isBindingElement(element)) continue; + const local = element.name; + if (local !== undefined && ts.isIdentifier(local) && base !== null) { + const key = element.propertyName; + const member = key !== undefined && ts.isIdentifier(key) ? key.text : local.text; + this.builtinBindings.set(local.text, `${base}.${member}`); + } + this.bindName(local, undefined); + } + return; + } + if (ts.isArrayBindingPattern(name)) { + for (const element of name.elements.nodes) { + if (ts.isBindingElement(element)) this.bindName(element.name, undefined); + } + } + } + + /** `require("fs")`, `require("node:util").inherits` → "fs", "util.inherits". */ + private builtinOf(node: ts.Node): string | null { + let value = node; + while (ts.isParenthesizedExpression(value)) value = value.expression; + if (ts.isCallExpression(value) && this.isRequireCallee(value.expression)) { + const spec = literalText(value.arguments.nodes[0]); + return spec === null ? null : builtinModuleName(spec); + } + if (ts.isPropertyAccessExpression(value)) { + const base = this.builtinOf(value.expression); + const name = propertyName(value); + return base === null || name === null ? null : `${base}.${name}`; + } + if (ts.isIdentifier(value)) return this.builtinBindings.get(value.text) ?? null; + return null; + } + + private isRequireCallee(node: ts.Node): boolean { + return ts.isIdentifier(node) && node.text === "require"; + } + + private isCreateRequireCallee(node: ts.Node): boolean { + if (ts.isIdentifier(node)) { + return ( + node.text === "createRequire" || + this.builtinBindings.get(node.text) === "module.createRequire" + ); + } + return propertyName(node) === "createRequire"; + } + + private collectDeclarations(): void { + ts.walkPreorder(this.sf, (node) => { + if (ts.isFunctionDeclaration(node) && node.name !== undefined) { + this.declared.add(node.name.text); + this.functions.add(node.name.text); + } else if (ts.isClassDeclaration(node) && node.name !== undefined) { + this.declared.add(node.name.text); + this.classes.add(node.name.text); + } else if (ts.isVariableDeclaration(node)) { + this.bindName(node.name, node.initializer); + } else if (ts.isParameter(node)) { + this.bindName(node.name, undefined); + } else if (ts.isImportDeclaration(node)) { + const spec = literalText(node.moduleSpecifier); + const module = spec === null ? null : builtinModuleName(spec); + const clause = node.importClause; + if (clause !== undefined) { + if (clause.name !== undefined) { + this.declared.add(clause.name.text); + if (module !== null) this.builtinBindings.set(clause.name.text, module); + } + const bindings = clause.namedBindings; + if (bindings !== undefined && ts.isNamespaceImport(bindings)) { + this.declared.add(bindings.name.text); + if (module !== null) this.builtinBindings.set(bindings.name.text, module); + } else if (bindings !== undefined && ts.isNamedImports(bindings)) { + for (const element of bindings.elements.nodes) { + this.declared.add(element.name.text); + const imported = element.propertyName; + const member = + imported !== undefined && ts.isIdentifier(imported) + ? imported.text + : element.name.text; + if (module !== null) + this.builtinBindings.set(element.name.text, `${module}.${member}`); + } + } + } + } + return undefined; + }); + } + + /** Whether the code at `node` runs in strict mode. */ + private isStrict(node: ts.Node): boolean { + if (this.moduleStrict) return true; + let cur: ts.Node | undefined = node.parent; + const visited: ts.Node[] = []; + let strict = false; + while (cur !== undefined) { + const known = this.strictness.get(cur); + if (known !== undefined) { + strict = known; + break; + } + visited.push(cur); + if (ts.isClassDeclaration(cur) || ts.isClassExpression(cur)) { + strict = true; + break; + } + if (isFunctionBoundary(cur) || ts.isArrowFunction(cur)) { + const body = (cur as ts.FunctionLikeDeclaration).body; + if (body !== undefined && ts.isBlock(body) && hasUseStrict(body.statements.nodes)) { + strict = true; + break; + } + } + cur = cur.parent; + } + for (const n of visited) this.strictness.set(n, strict); + return strict; + } + + private isGlobal(name: string): boolean { + return BUILTIN_GLOBALS.has(name) && !this.declared.has(name); + } + + private visit(node: ts.Node): void { + if (ts.isCallExpression(node)) this.visitCall(node); + else if (ts.isNewExpression(node)) this.visitNew(node); + else if (ts.isBinaryExpression(node) && isAssignmentOperator(node.operatorToken.kind)) + this.visitWrite(node.left, node, "write"); + else if (ts.isDeleteExpression(node)) this.visitDelete(node); + else if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) { + if ( + node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken + ) + this.visitWrite(node.operand, node, "write"); + } else if (ts.isWithStatement(node)) { + this.add("R2.4", "with", "a 'with' statement", node); + } else if (ts.isImportDeclaration(node)) { + const spec = literalText(node.moduleSpecifier); + if (spec !== null) this.visitSpecifier(spec, node); + } else if (ts.isIdentifier(node)) { + this.visitIdentifier(node); + } else if (ts.isNumericLiteral(node)) { + const text = this.sf.text.slice(ts.tokenStartOfNode(node), node.end); + if (/^0[0-9]+$/.test(text) && !this.isStrict(node)) + this.add("R2.4", "sloppy-mode", "a legacy octal literal", node); + } else if (ts.isPropertyAccessExpression(node)) { + const text = chainText(node); + if ((text === "arguments.callee" || text === "arguments.caller") && !this.isStrict(node)) + this.add("R2.4", "sloppy-arguments", `'${text}'`, node); + } else if (ts.isObjectLiteralExpression(node)) { + for (const prop of node.properties.nodes) { + if (!ts.isPropertyAssignment(prop)) continue; + const name = prop.name; + const key = ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : null; + if (key === "__proto__" && prop.initializer.kind !== ts.SyntaxKind.NullKeyword) + this.add("R2.2", "prototype-write", "an object literal with a '__proto__' value", prop); + } + } else if (isFunctionBoundary(node)) { + this.visitFunction(node as ts.FunctionLikeDeclaration); + } + } + + private visitSpecifier(spec: string, node: ts.Node): void { + const builtin = builtinModuleName(spec); + if (builtin === "vm") this.add("R1.1", "vm", "the 'vm' module", node); + else if (spec.endsWith(".node")) + this.add("R1.3", "native-addon", `the native addon '${spec}'`, node); + else if (ADDON_LOADERS.has(spec)) + this.add("R1.3", "native-addon", `the native addon loader '${spec}'`, node); + else if (LOADER_HOOK_PACKAGES.has(spec)) + this.add("R1.4", "loader-hook", `the loader hook package '${spec}'`, node); + } + + private visitCall(call: ts.CallExpression): void { + const callee = call.expression; + const args = call.arguments.nodes; + const first = args[0]; + // import(e) and require(e) + if (callee.kind === ts.SyntaxKind.ImportKeyword) { + if (first !== undefined && !isFoldableSpecifier(first)) + this.add("R1.2", "dynamic-import", "import() with a computed specifier", call); + else if (first !== undefined) this.visitSpecifier(literalText(first) ?? "", call); + return; + } + if ( + this.isRequireCallee(callee) || + (ts.isIdentifier(callee) && this.createdRequires.has(callee.text)) + ) { + if (first !== undefined && !isFoldableSpecifier(first)) + this.add("R1.2", "dynamic-require", "require() with a computed specifier", call); + else if (first !== undefined) this.visitSpecifier(literalText(first) ?? "", call); + return; + } + if (ts.isCallExpression(callee) && this.isCreateRequireCallee(callee.expression)) { + if (first !== undefined && !isFoldableSpecifier(first)) + this.add("R1.2", "dynamic-require", "createRequire(...)() with a computed specifier", call); + return; + } + // eval, Function + let target = callee; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (ts.isBinaryExpression(target) && target.operatorToken.kind === ts.SyntaxKind.CommaToken) + target = target.right; + const text = chainText(target); + if (text === null) return; + if ( + (text === "eval" && !this.declared.has("eval")) || + text === "globalThis.eval" || + text === "global.eval" || + text === "window.eval" + ) { + this.add("R1.1", "code-from-string", "eval()", call); + return; + } + if (text === "Function" && this.isGlobal("Function")) { + this.visitFunctionConstructor(call, args); + return; + } + const root = chainRoot(target); + const rootBinding = root === null ? undefined : this.builtinBindings.get(root.text); + const resolved = + rootBinding !== undefined && root !== null + ? rootBinding + text.slice(root.text.length) + : text; + // engine internals + if ( + (text === "process.binding" || text === "process._linkedBinding") && + this.isGlobal("process") + ) { + this.add("R1.5", "engine-internal", `'${text}()'`, call); + return; + } + if (text === "internalBinding" && !this.declared.has("internalBinding")) { + this.add("R1.5", "engine-internal", "'internalBinding()'", call); + return; + } + if (resolved === "v8.setFlagsFromString" || text.endsWith(".setFlagsFromString")) { + this.add("R1.5", "engine-internal", "'v8.setFlagsFromString()'", call); + return; + } + if ((text === "global.gc" || text === "globalThis.gc") && this.isGlobal(text.split(".")[0]!)) { + this.add("R1.5", "engine-internal", `'${text}()'`, call); + return; + } + if (text === "process.dlopen" && this.isGlobal("process")) { + this.add("R1.3", "native-addon", "'process.dlopen()'", call); + return; + } + // loader hooks + if (resolved === "module.register" || resolved === "module.registerHooks") { + this.add("R1.4", "loader-hook", `'${resolved}()'`, call); + return; + } + // prototype mutation + if ( + (text === "Object.setPrototypeOf" && this.isGlobal("Object")) || + (text === "Reflect.setPrototypeOf" && this.isGlobal("Reflect")) + ) { + // The transpiled-Error idiom `Object.setPrototypeOf(this, …)` stays. + if (first !== undefined && first.kind !== ts.SyntaxKind.ThisKeyword) + this.add("R2.2", "prototype-write", `'${text}()'`, call); + return; + } + // defineProperty outside the module object and interop helpers + if ( + ((text === "Object.defineProperty" || text === "Object.defineProperties") && + this.isGlobal("Object")) || + (text === "Reflect.defineProperty" && this.isGlobal("Reflect")) + ) { + if (first !== undefined && !this.isModuleObject(first) && !this.inInteropHelper(call)) + this.add("R2.3", "define-property", `'${text}()'`, call); + return; + } + if (text.endsWith(".__defineGetter__") || text.endsWith(".__defineSetter__")) { + this.add("R2.3", "define-property", `'${text.slice(text.lastIndexOf(".") + 1)}()'`, call); + return; + } + // constructor functions + if ( + args.length === 2 && + (resolved === "util.inherits" || + (text === "inherits" && !this.functions.has("inherits")) || + text.endsWith(".inherits")) + ) { + this.add("R2.1", "constructor-function", `'${text}()'`, call); + return; + } + if ( + ts.isPropertyAccessExpression(target) && + (propertyName(target) === "call" || propertyName(target) === "apply") && + first !== undefined && + first.kind === ts.SyntaxKind.ThisKeyword && + ts.isExpressionStatement(call.parent!) + ) { + const fn = target.expression; + const name = ts.isIdentifier(fn) ? fn.text : null; + if (name !== null && /^[A-Z]/.test(name) && !this.classes.has(name)) + this.add( + "R2.1", + "constructor-function", + `'${name}.${propertyName(target)}(this, …)'`, + call, + ); + } + } + + private visitFunctionConstructor(node: ts.Node, args: readonly ts.Node[]): void { + // The global-object idiom Function("return this")() folds to globalThis. + if (args.length === 1) { + const body = literalText(args[0]); + if (body !== null && body.trim() === "return this") return; + } + // A capability probe tested inside try (zod 4's allowsEval) keeps + // today's EvalError lowering. + for (let cur = node.parent; cur !== undefined; cur = cur.parent) { + if (isFunctionBoundary(cur) || ts.isArrowFunction(cur)) break; + if (ts.isBlock(cur) && cur.parent !== undefined && ts.isTryStatement(cur.parent)) { + if (cur.parent.tryBlock === cur) return; + } + } + this.add("R1.1", "code-from-string", "new Function()", node); + } + + private visitNew(node: ts.NewExpression): void { + const callee = node.expression; + if (!ts.isIdentifier(callee)) return; + const name = callee.text; + if (name === "Function" && this.isGlobal("Function")) { + this.visitFunctionConstructor(node, node.arguments?.nodes ?? []); + } else if (name === "Proxy" && !this.declared.has("Proxy")) { + this.add("R2.5", "proxy", "new Proxy()", node); + } else if (this.functions.has(name) && !this.classes.has(name)) { + this.add("R2.1", "constructor-function", `'new ${name}()' of a function`, node); + } + } + + private isModuleObject(node: ts.Node): boolean { + const text = chainText(node); + return text === "exports" || text === "module.exports" || text === "module"; + } + + /** Inside a module-interop helper (tsc's __createBinding/__exportStar, + * Babel's _interopRequireWildcard, esbuild's __export): their + * defineProperty calls build module objects the native subset models. */ + private inInteropHelper(node: ts.Node): boolean { + for (let cur = node.parent; cur !== undefined; cur = cur.parent) { + let name: string | null = null; + if (ts.isFunctionDeclaration(cur) && cur.name !== undefined) name = cur.name.text; + else if (ts.isVariableDeclaration(cur) && ts.isIdentifier(cur.name)) name = cur.name.text; + else if (ts.isBinaryExpression(cur) && isAssignmentOperator(cur.operatorToken.kind)) + name = chainText(cur.left); + if (name !== null && /(^|\.)_/.test(name)) return true; + } + return false; + } + + private visitWrite(target: ts.Node, node: ts.Node, what: "write" | "delete"): void { + let left = target; + while (ts.isParenthesizedExpression(left)) left = left.expression; + if (ts.isIdentifier(left)) { + if (this.isGlobal(left.text) && left.text !== "global" && left.text !== "globalThis") + this.add("R2.5", "builtin-write", `a ${what} of the global '${left.text}'`, node); + return; + } + if (!ts.isPropertyAccessExpression(left) && !ts.isElementAccessExpression(left)) return; + const text = chainText(left); + const root = chainRoot(left); + const base = left.expression; + const baseText = chainText(base); + // require.extensions / require.cache + if ( + text !== null && + (text.startsWith("require.extensions") || text.startsWith("require.cache")) + ) { + this.add( + "R1.4", + "loader-hook", + `a ${what} of '${text.startsWith("require.cache") ? "require.cache" : "require.extensions"}'`, + node, + ); + return; + } + if (baseText === "require.cache" || baseText === "require.extensions") { + this.add("R1.4", "loader-hook", `a ${what} of '${baseText}'`, node); + return; + } + // __proto__ writes + if (propertyName(left) === "__proto__") { + this.add("R2.2", "prototype-write", `a ${what} of '__proto__'`, node); + return; + } + if (root === null) return; + const binding = this.builtinBindings.get(root.text); + // Module internals: Module._load = …, Module.prototype.require = … + if (text !== null) { + const resolved = binding !== undefined ? binding + text.slice(root.text.length) : text; + if ( + /^(module|Module)\.(_load|_resolveFilename|_extensions|wrap|_compile|prototype\.require|prototype\._compile)$/.test( + resolved, + ) && + (binding !== undefined || !this.declared.has(root.text) || root.text === "Module") + ) { + this.add("R1.4", "loader-hook", `a ${what} of '${resolved}'`, node); + return; + } + // F.prototype… + const parts = text.split("."); + if (parts.length >= 2 && parts[1] === "prototype") { + const name = parts[0]!; + if (this.isGlobal(name)) { + this.add("R2.5", "builtin-write", `a ${what} of '${text}'`, node); + return; + } + if (binding === undefined && this.classes.has(name) && !this.functions.has(name)) { + this.add("R2.3", "define-property", `a ${what} of the class prototype '${text}'`, node); + return; + } + if (binding === undefined && name !== "this") { + this.add("R2.1", "constructor-function", `a ${what} of '${text}'`, node); + return; + } + } + } + // Builtin globals and builtin module objects. + const spelled = memberText(left) ?? `${root.text}[…]`; + if (binding !== undefined) { + const member = binding + spelled.slice(root.text.length); + if (!this.allowedWrite(member)) { + this.add( + "R2.5", + "builtin-write", + `a ${what} of the builtin module member '${member}'`, + node, + ); + } + return; + } + if (this.isGlobal(root.text)) { + const member = spelled; + if (!this.allowedWrite(member)) + this.add("R2.5", "builtin-write", `a ${what} of the builtin '${member}'`, node); + } + } + + private allowedWrite(member: string): boolean { + for (const allowed of ALLOWED_BUILTIN_WRITES) { + if ( + member === allowed || + member.startsWith(`${allowed}.`) || + member.startsWith(`${allowed}[`) + ) + return true; + } + return false; + } + + private visitDelete(node: ts.DeleteExpression): void { + let target = node.expression; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (ts.isIdentifier(target)) { + if (!this.isStrict(node)) + this.add("R2.4", "sloppy-mode", `'delete ${target.text}' of an identifier`, node); + return; + } + this.visitWrite(target, node, "delete"); + } + + private visitIdentifier(node: ts.Identifier): void { + const name = node.text; + if (name !== "WeakRef" && name !== "FinalizationRegistry") return; + if (this.declared.has(name)) return; + const parent = node.parent; + if (parent === undefined) return; + // Only value uses: not a property name, declaration name or typeof probe. + if (ts.isPropertyAccessExpression(parent) && parent.name === node) return; + if (ts.isTypeOfExpression(parent)) return; + if ((ts.isPropertyAssignment(parent) || ts.isMethodDeclaration(parent)) && parent.name === node) + return; + this.add("R1.6", "weak-ref", `'${name}'`, node); + } + + /** Observable `arguments` aliasing in a sloppy function: writes to + * arguments[i], or to a parameter while `arguments` is read. */ + private visitFunction(fn: ts.FunctionLikeDeclaration): void { + const body = fn.body; + if (body === undefined || !ts.isBlock(body)) return; + if (this.isStrict(body.statements.nodes[0] ?? body)) return; + if (hasUseStrict(body.statements.nodes)) return; + const params = new Set(); + for (const p of fn.parameters.nodes) { + if (ts.isIdentifier(p.name)) params.add(p.name.text); + } + const seen: ArgumentsUse = { reads: false, argumentsWrite: null, paramWrite: null }; + ts.walkPreorder(body, (node) => { + if ( + node !== body && + (isFunctionBoundary(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node)) + ) + return "skip"; + if (ts.isIdentifier(node) && node.text === "arguments") seen.reads = true; + if (ts.isBinaryExpression(node) && isAssignmentOperator(node.operatorToken.kind)) { + const left = node.left; + if ( + ts.isElementAccessExpression(left) && + ts.isIdentifier(left.expression) && + left.expression.text === "arguments" + ) { + if (seen.argumentsWrite === null) seen.argumentsWrite = node; + } else if (ts.isIdentifier(left) && params.has(left.text)) { + if (seen.paramWrite === null) seen.paramWrite = node; + } + } + return undefined; + }); + if (seen.argumentsWrite !== null) + this.add( + "R2.4", + "sloppy-arguments", + "a write to 'arguments[i]' in sloppy mode", + seen.argumentsWrite, + ); + else if (seen.reads && seen.paramWrite !== null) + this.add( + "R2.4", + "sloppy-arguments", + "a parameter write while 'arguments' is read in sloppy mode", + seen.paramWrite, + ); + } +} + +interface ArgumentsUse { + reads: boolean; + argumentsWrite: ts.Node | null; + paramWrite: ts.Node | null; +} + +/** The R1/R2 markers of one JavaScript module, in source order. */ +export function scanModule(sf: ts.SourceFile): NodeIslandMarker[] { + const markers = new ModuleScanner(sf).scan(); + return markers.sort((a, b) => a.loc.start - b.loc.start); +} diff --git a/packages/compiler/src/node-islands/origin.ts b/packages/compiler/src/node-islands/origin.ts new file mode 100644 index 000000000..65beebba0 --- /dev/null +++ b/packages/compiler/src/node-islands/origin.ts @@ -0,0 +1,68 @@ +/* Where a source file comes from. User code (the program's own tree and + * workspace packages compiled from source) is strict; npm code (installed + * packages under node_modules) is lenient and is decided per package + * instance; shims are scriptc's own registry implementations; builtin + * covers declaration files of the type world. */ +import { join } from "node:path"; +import { isPackageShimFile } from "../frontend/package-shims.js"; +import { trackedReadFile } from "../frontend/input-tracker.js"; + +export type Origin = "user" | "npm" | "shim" | "builtin"; + +export function originOf(file: string): Origin { + const path = file.split("\\").join("/"); + if (isPackageShimFile(path)) return "shim"; + if (/\.d\.[cm]?ts$/.test(path)) return "builtin"; + return path.includes("/node_modules/") ? "npm" : "user"; +} + +/** One installed package: the decision unit for npm code. pnpm keeps + * versions apart in different directories, so the directory identifies the + * instance. */ +export interface PackageInstance { + name: string; + /** The package directory (…/node_modules/). */ + dir: string; + version: string | null; +} + +/** The package instance of a file under node_modules: the LAST + * node_modules segment (nested installs belong to the innermost package), + * scope-aware. Null outside node_modules. */ +export function packageDirOf(file: string): { name: string; dir: string } | null { + const path = file.split("\\").join("/"); + const parts = path.split("/"); + const i = parts.lastIndexOf("node_modules"); + if (i < 0 || i + 1 >= parts.length - 1) return null; + const first = parts[i + 1]!; + if (first.startsWith("@")) { + if (i + 2 >= parts.length - 1) return null; + const name = `${first}/${parts[i + 2]!}`; + return { name, dir: parts.slice(0, i + 3).join("/") }; + } + return { name: first, dir: parts.slice(0, i + 2).join("/") }; +} + +/** The `version` field of a package directory's package.json. */ +export function packageVersionOf(dir: string): string | null { + let version: string | null = null; + const text = trackedReadFile(join(dir, "package.json")); + if (text !== null) { + try { + const parsed: unknown = JSON.parse(text); + if (parsed !== null && typeof parsed === "object") { + const v = (parsed as Record)["version"]; + if (typeof v === "string") version = v; + } + } catch { + version = null; + } + } + return version; +} + +export function packageInstanceOf(file: string): PackageInstance | null { + const at = packageDirOf(file); + if (at === null) return null; + return { name: at.name, dir: at.dir, version: packageVersionOf(at.dir) }; +} diff --git a/packages/compiler/src/node-islands/policy.ts b/packages/compiler/src/node-islands/policy.ts new file mode 100644 index 000000000..72dbf3cd0 --- /dev/null +++ b/packages/compiler/src/node-islands/policy.ts @@ -0,0 +1,167 @@ +/* The Node island policy (DESIGN.md §1.5–§1.6): user code strict, npm + * lenient. npm code outside the native subset becomes a Node island + * automatically (`nodeIslands.auto`, `--node-islands=auto`); user modules + * become islands only when marked (`nodeIslands.modules`, + * `--node-island=`); `"native"` (`--native=`) makes one + * dependency strict; `--node-islands=off` makes the whole program strict. + * + * This version is report-only: the policy decides what WOULD run in Node, + * and coverage reports it, but code generation does not change. */ +import { isAbsolute, join, resolve, dirname } from "node:path"; +import type { ProjectConfig } from "../project-config.js"; + +export type NodeIslandsMode = "auto" | "explicit" | "off"; +export const NODE_ISLANDS_MODES: readonly NodeIslandsMode[] = ["auto", "explicit", "off"]; + +/** The command-line half of the policy (`build`, `run` and `coverage` + * accept these). */ +export interface NodeIslandOptions { + /** `--node-islands=auto|explicit|off`. */ + mode?: NodeIslandsMode; + /** `--node-island=`: packages and user modules forced into a + * Node island. Paths are absolute. */ + islands?: readonly string[]; + /** `--native=`: packages that must compile natively. */ + native?: readonly string[]; + /** `--node=`: the node executable recorded for islands. */ + node?: string; + /** `--strict-user-js`: report R2 constructs in user JavaScript as + * SC8101–SC8105. */ + strictUserJs?: boolean; +} + +export type DecisionSource = "config" | "cli"; + +export interface UserModulePattern { + /** An absolute path or glob. */ + pattern: string; + source: DecisionSource; +} + +export interface NodeIslandPolicy { + mode: NodeIslandsMode; + /** Where the mode came from (an explicit `"auto": false` reads as + * "explicit"). */ + modeSource: DecisionSource | "default"; + forcedNode: Map; + forcedNative: Map; + /** `"unloaded"` packages: never loaded, never islands. */ + unloaded: Set; + userModules: UserModulePattern[]; + node: string | null; + strictUserJs: boolean; +} + +/** Whether a `--node-island` value names a module rather than a package. */ +export function isModuleIslandArgument(value: string): boolean { + return ( + value.startsWith(".") || + value.startsWith("/") || + /\.(?:[cm]?[jt]s|[jt]sx)$/.test(value) || + value.includes("*") + ); +} + +export function nodeIslandPolicy( + config: ProjectConfig, + options: NodeIslandOptions = {}, +): NodeIslandPolicy { + const forcedNode = new Map(); + const forcedNative = new Map(); + const unloaded = new Set(); + for (const [name, setting] of config.packages) { + if (setting.unloaded) unloaded.add(name); + else if (setting.run === "node") forcedNode.set(name, "config"); + else if (setting.run === "native") forcedNative.set(name, "config"); + } + const userModules: UserModulePattern[] = []; + const base = config.manifest === null ? process.cwd() : dirname(config.manifest); + for (const pattern of config.nodeIslands.modules) { + userModules.push({ + pattern: isAbsolute(pattern) ? pattern : join(base, pattern), + source: "config", + }); + } + for (const value of options.islands ?? []) { + if (isModuleIslandArgument(value)) { + userModules.push({ pattern: resolve(value), source: "cli" }); + } else { + forcedNative.delete(value); + forcedNode.set(value, "cli"); + } + } + for (const name of options.native ?? []) { + forcedNode.delete(name); + forcedNative.set(name, "cli"); + } + let mode: NodeIslandsMode = "auto"; + let modeSource: DecisionSource | "default" = "default"; + if (options.mode !== undefined) { + mode = options.mode; + modeSource = "cli"; + } else if (!config.nodeIslands.auto) { + mode = "explicit"; + modeSource = "config"; + } + return { + mode, + modeSource, + forcedNode, + forcedNative, + unloaded, + userModules, + node: options.node ?? config.nodeIslands.node, + strictUserJs: options.strictUserJs === true, + }; +} + +/** Whether a user module is marked to run in a Node island. */ +export function isMarkedUserModule(policy: NodeIslandPolicy, file: string): DecisionSource | null { + const path = file.split("\\").join("/"); + for (const entry of policy.userModules) { + if (globMatch(entry.pattern.split("\\").join("/"), path)) return entry.source; + } + return null; +} + +/** Glob matching over `/`-separated paths: `**` spans directories, `*` and + * `?` stay inside one segment. */ +export function globMatch(pattern: string, path: string): boolean { + return matchSegments(pattern.split("/"), 0, path.split("/"), 0); +} + +function matchSegments(pat: string[], pi: number, segs: string[], si: number): boolean { + if (pi === pat.length) return si === segs.length; + const p = pat[pi]!; + if (p === "**") { + for (let k = si; k <= segs.length; k++) if (matchSegments(pat, pi + 1, segs, k)) return true; + return false; + } + if (si === segs.length) return false; + return matchSegment(p, 0, segs[si]!, 0) && matchSegments(pat, pi + 1, segs, si + 1); +} + +function matchSegment(p: string, pi: number, s: string, si: number): boolean { + if (pi === p.length) return si === s.length; + const ch = p.charAt(pi); + if (ch === "*") { + for (let k = si; k <= s.length; k++) if (matchSegment(p, pi + 1, s, k)) return true; + return false; + } + if (si === s.length) return false; + if (ch === "?" || ch === s.charAt(si)) return matchSegment(p, pi + 1, s, si + 1); + return false; +} + +/** The policy's identity for caches: two analyses with the same identity + * decide the same islands. */ +export function nodeIslandOptionsKey(options: NodeIslandOptions | undefined): string { + if (options === undefined) return ""; + return [ + options.mode ?? "", + (options.islands ?? []).join(","), + (options.native ?? []).join(","), + options.node ?? "", + options.strictUserJs === true ? "strict-user-js" : "", + ].join("|"); +} diff --git a/packages/compiler/src/node-islands/report.test.ts b/packages/compiler/src/node-islands/report.test.ts new file mode 100644 index 000000000..a11d9fb8b --- /dev/null +++ b/packages/compiler/src/node-islands/report.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, test } from "vitest"; +import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; +import { emptyProjectConfig, parseProjectConfig } from "../project-config.js"; +import type { NodeIslandMarker } from "./detect.js"; +import { + globMatch, + isMarkedUserModule, + nodeIslandPolicy, + type NodeIslandOptions, +} from "./policy.js"; +import { buildNodeIslandReport, type NpmOutcome } from "./report.js"; +import type { NodeIslandScan, PackageEdge, ScannedModule } from "./scan.js"; + +const NM = "/app/node_modules"; + +function mod(pkg: string | null, file: string, markers: NodeIslandMarker[] = []): ScannedModule { + return { + file, + origin: pkg === null ? "user" : "npm", + packageDir: pkg === null ? null : `${NM}/${pkg}`, + packageName: pkg, + markers, + dropped: false, + }; +} + +function marker(file: string, rule: NodeIslandMarker["rule"], reason: string): NodeIslandMarker { + return { rule, reason, construct: reason, loc: { file, start: 0, end: 1 } }; +} + +function edge(from: string | null, to: string): PackageEdge { + return { + fromDir: from === null ? null : `${NM}/${from}`, + fromFile: from === null ? "/app/main.ts" : `${NM}/${from}/index.js`, + to, + toDir: `${NM}/${to}`, + shim: false, + loc: { file: from === null ? "/app/main.ts" : `${NM}/${from}/index.js`, start: 0, end: 1 }, + }; +} + +function gapFence(pkg: string): ScrDiagnostic { + return { + code: "SC1090", + message: "a builtin function alias as an escaping value", + loc: { file: `${NM}/${pkg}/index.js`, start: 5, end: 9 }, + }; +} + +/** A program: main.ts imports a (constructor functions), b (a gap fence) + * and c (clean); a imports d (clean, only from a) and c. */ +function program(): { scan: NodeIslandScan; fences: ScrDiagnostic[] } { + return { + scan: { + modules: [ + mod(null, "/app/main.ts"), + mod("a", `${NM}/a/index.js`, [marker(`${NM}/a/index.js`, "R2.1", "constructor-function")]), + mod("b", `${NM}/b/index.js`), + mod("c", `${NM}/c/index.js`), + mod("d", `${NM}/d/index.js`), + ], + edges: [edge(null, "a"), edge(null, "b"), edge(null, "c"), edge("a", "d"), edge("a", "c")], + refusals: [], + }, + fences: [gapFence("b")], + }; +} + +function report( + options: NodeIslandOptions = {}, + npm: NpmOutcome[] = [], + config = emptyProjectConfig(), +) { + const { scan, fences } = program(); + return buildNodeIslandReport({ + entry: "/app/main.ts", + scan, + npm, + diagnostics: [], + fences, + policy: nodeIslandPolicy(config, options), + }); +} + +describe("decisions", () => { + test("auto: packages with reasons are islands, gap islands first; closure and sharing", () => { + const r = report(); + expect(r.islands.map((i) => [i.package, i.class, i.decision])).toEqual([ + ["b", "gap", "auto"], + ["a", "object-model", "auto"], + ]); + expect(r.islands[0]!.reason).toBe("SC1090: a builtin function alias as an escaping value"); + expect(r.islands[1]!.carries).toEqual(["d"]); + expect(r.packages.map((p) => [p.package, p.status, p.islands])).toEqual([ + ["c", "shared", ["a"]], + ["d", "inside", ["a"]], + ]); + expect(r.violations).toEqual([]); + }); + + test("a refused package is a packaging island, classified by its refusal", () => { + const r = report({}, [ + { package: "c", status: "refused", detail: "auto: its shipped JS looks minified" }, + ]); + const c = r.islands.find((i) => i.package === "c")!; + expect([c.class, c.reason, c.diagnostic]).toEqual([ + "packaging", + "SC2013: auto: its shipped JS looks minified", + "SC2013", + ]); + }); + + test("forced native turns reasons into SC8003; forced node islands a clean package", () => { + const r = report({ native: ["a"], islands: ["c"] }); + expect(r.islands.map((i) => [i.package, i.class, i.decision])).toEqual([ + ["b", "gap", "auto"], + ["c", "marked", "cli"], + ]); + expect(r.violations.map((v) => v.code)).toEqual(["SC8003"]); + expect(r.violations[0]!.message).toContain("'a' is marked native"); + }); + + test("off: no islands, every package outside the native subset is SC8002", () => { + const r = report({ mode: "off" }); + expect(r.islands).toEqual([]); + expect(r.violations.map((v) => [v.code, v.message.split(" (")[0]])).toEqual([ + ["SC8002", "npm package 'a' is outside the native subset"], + ["SC8002", "npm package 'b' is outside the native subset"], + ]); + }); + + test('explicit (or "auto": false): only marked islands', () => { + const config = parseProjectConfig( + "/app/package.json", + JSON.stringify({ scriptc: { nodeIslands: { auto: false }, packages: { b: "node" } } }), + ); + const r = report({}, [], config); + expect(r.policy).toBe("explicit"); + expect(r.islands.map((i) => [i.package, i.decision])).toEqual([["b", "config"]]); + expect(r.violations.map((v) => v.code)).toEqual(["SC8002"]); + }); + + test("command-line names the program does not import are reported", () => { + expect(report({ native: ["nope"] }).unknownPackages).toEqual(["nope"]); + }); + + test("unloaded packages are never islands", () => { + const config = parseProjectConfig( + "/app/package.json", + JSON.stringify({ scriptc: { packages: { a: "unloaded" } } }), + ); + expect(report({}, [], config).islands.map((i) => i.package)).toEqual(["b"]); + }); +}); + +describe("user modules", () => { + test("marked by glob in the config or by path on the command line", () => { + const config = parseProjectConfig( + "/app/package.json", + JSON.stringify({ scriptc: { nodeIslands: { modules: ["src/plugins/**/*.ts"] } } }), + ); + const policy = nodeIslandPolicy(config, { islands: ["/app/src/legacy.js"] }); + expect(isMarkedUserModule(policy, "/app/src/plugins/a/loader.ts")).toBe("config"); + expect(isMarkedUserModule(policy, "/app/src/legacy.js")).toBe("cli"); + expect(isMarkedUserModule(policy, "/app/src/main.ts")).toBe(null); + }); + + test("SC81xx for R2 markers in user JavaScript, only with --strict-user-js", () => { + const file = "/app/legacy.js"; + const scan: NodeIslandScan = { + modules: [ + mod(null, file, [ + marker(file, "R2.1", "constructor-function"), + marker(file, "R2.5", "builtin-write"), + marker(file, "R1.1", "code-from-string"), + ]), + ], + edges: [], + refusals: [], + }; + const build = (strictUserJs: boolean) => + buildNodeIslandReport({ + entry: file, + scan, + npm: [], + diagnostics: [], + fences: [], + policy: nodeIslandPolicy(emptyProjectConfig(), { strictUserJs }), + }); + expect(build(false).userJs).toEqual([]); + expect(build(true).userJs.map((d) => d.code)).toEqual(["SC8101", "SC8105"]); + }); +}); + +test("globMatch", () => { + expect(globMatch("/a/**/*.ts", "/a/b/c/d.ts")).toBe(true); + expect(globMatch("/a/**/*.ts", "/a/d.ts")).toBe(true); + expect(globMatch("/a/*.ts", "/a/b/d.ts")).toBe(false); + expect(globMatch("/a/?.js", "/a/x.js")).toBe(true); + expect(globMatch("/a/x.js", "/a/x.js")).toBe(true); +}); diff --git a/packages/compiler/src/node-islands/report.ts b/packages/compiler/src/node-islands/report.ts new file mode 100644 index 000000000..a6898e855 --- /dev/null +++ b/packages/compiler/src/node-islands/report.ts @@ -0,0 +1,355 @@ +/* The Node island report: which packages and user modules would run in a + * Node island, and why (DESIGN.md §1.3–§1.7). Report-only: the decisions + * are computed from the detector and the policy, and nothing about the + * build changes. + * + * 1. Reasons per package instance: pre-scan markers in its modules, + * refusals and runtime fences the lowering produced in its modules, and + * the drop-and-reload refusal of a package that did not compile. + * 2. Decision per package: the policy (forced node or native, the mode) + * applied to its reasons. + * 3. Closure: a package imported only by island packages runs inside the + * island (it is Node's business); one reached from both sides is + * reported as shared (DESIGN.md §2.6 decides it later). */ +import { + nativePackageDiag, + nodeIslandsOffDiag, + userJsObjectModelDiag, + type ScrDiagnostic, +} from "../diagnostics/diagnostic.js"; +import type { SrcLoc } from "../ir/ir.js"; +import { + classifyDiagnostic, + classifyMarker, + classifyRefusal, + isIslandReasonCode, +} from "./classify.js"; +import { + describe, + type IslandDecision, + type NativePackage, + type NodeIslandEntry, + type NodeIslandReason, + type NodeIslandReasonSource, + type NodeIslandReport, + type UserModuleIsland, +} from "./summary.js"; +import { packageDirOf, packageVersionOf } from "./origin.js"; +import { isMarkedUserModule, type NodeIslandPolicy } from "./policy.js"; +import { classRank, reportRank, ruleInfo } from "./rules.js"; +import type { NodeIslandScan, RefusalRecord } from "./scan.js"; + +/** One dependency's native compilation outcome (coverage/report.ts's + * NpmStaticStatus, restated to keep this module free of coverage). */ +export interface NpmOutcome { + package: string; + status: "static" | "refused"; + detail?: string; +} + +export interface NodeIslandReportInput { + entry: string; + scan: NodeIslandScan; + npm: readonly NpmOutcome[]; + /** Lowering diagnostics of the analysis (reached and unreached), runtime + * fences separately. */ + diagnostics: readonly ScrDiagnostic[]; + fences: readonly ScrDiagnostic[]; + policy: NodeIslandPolicy; +} + +/** Reasons kept per package (the report renders the first per module). */ +const REASON_CAP = 200; + +interface PackageState { + key: string; + name: string; + dir: string | null; + reasons: NodeIslandReason[]; + importers: Set; + nativeSites: SrcLoc[]; + island: boolean; + decision: IslandDecision; + carriedBy: string | null; + violation: boolean; +} + +export function buildNodeIslandReport(input: NodeIslandReportInput): NodeIslandReport { + const policy = input.policy; + const states = new Map(); + const byName = new Map(); + const state = (name: string, dir: string | null): PackageState => { + const key = dir ?? `name:${name}`; + let s = states.get(key); + if (s === undefined) { + s = { + key, + name, + dir, + reasons: [], + importers: new Set(), + nativeSites: [], + island: false, + decision: "auto", + carriedBy: null, + violation: false, + }; + states.set(key, s); + const keys = byName.get(name) ?? []; + keys.push(key); + byName.set(name, keys); + } + return s; + }; + const addReason = (s: PackageState, r: NodeIslandReason): void => { + if (s.reasons.length < REASON_CAP) s.reasons.push(r); + }; + const ignored = (name: string): boolean => policy.unloaded.has(name); + + // Modules and their markers. + for (const m of input.scan.modules) { + if (m.origin !== "npm" || m.packageName === null) continue; + if (ignored(m.packageName)) continue; + const s = state(m.packageName, m.packageDir); + for (const marker of m.markers) addReason(s, classifyMarker(marker)); + } + // Import edges. + for (const e of input.scan.edges) { + if (e.shim || ignored(e.to)) continue; + const s = state(e.to, e.toDir); + if (e.fromDir === null) { + s.importers.add(""); + s.nativeSites.push(e.loc); + } else if (e.fromDir !== s.key) { + s.importers.add(e.fromDir); + } + } + // Lowering refusals and fences inside npm modules. + const addDiagnostics = (diags: readonly ScrDiagnostic[], source: NodeIslandReasonSource) => { + for (const d of diags) { + if (!isIslandReasonCode(d.code)) continue; + const at = packageDirOf(d.loc.file); + if (at === null || ignored(at.name)) continue; + addReason(state(at.name, at.dir), classifyDiagnostic(d, source)); + } + }; + addDiagnostics(input.diagnostics, "refusal"); + addDiagnostics(input.fences, "fence"); + // Packages that did not compile. + const records = new Map(); + for (const r of input.scan.refusals) if (!records.has(r.package)) records.set(r.package, r); + for (const outcome of input.npm) { + if (outcome.status !== "refused" || ignored(outcome.package)) continue; + const reason = classifyRefusal( + outcome.detail ?? "its static compilation was refused", + records.get(outcome.package) ?? null, + ); + if (reason === null) continue; + const keys = byName.get(outcome.package); + if (keys === undefined || keys.length === 0) { + addReason(state(outcome.package, null), reason); + continue; + } + for (const key of keys) addReason(states.get(key)!, reason); + } + + // Decisions. + const violations: ScrDiagnostic[] = []; + const fallbackLoc: SrcLoc = { file: input.entry, start: 0, end: 0 }; + for (const s of states.values()) { + sortReasons(s.reasons); + const forcedNode = policy.forcedNode.get(s.name); + const forcedNative = policy.forcedNative.get(s.name); + const outside = s.reasons.length > 0; + const loc = s.nativeSites[0] ?? fallbackLoc; + if (forcedNative !== undefined) { + if (outside) { + s.violation = true; + violations.push(nativePackageDiag(s.name, describe(s.reasons[0]!), loc)); + } + continue; + } + if (policy.mode === "off") { + if (outside || forcedNode !== undefined) { + s.violation = true; + violations.push( + nodeIslandsOffDiag( + s.name, + outside ? describe(s.reasons[0]!) : "it is marked to run in Node", + loc, + ), + ); + } + continue; + } + if (forcedNode !== undefined) { + s.island = true; + s.decision = forcedNode; + continue; + } + if (!outside) continue; + if (policy.mode === "explicit") { + s.violation = true; + violations.push(nodeIslandsOffDiag(s.name, describe(s.reasons[0]!), loc)); + continue; + } + s.island = true; + s.decision = "auto"; + } + + // Closure: packages imported only by islands (or by packages inside + // them) run inside the island. + for (let changed = true; changed;) { + changed = false; + for (const s of states.values()) { + if (s.island || s.carriedBy !== null || s.violation) continue; + if (s.importers.size === 0 || s.importers.has("")) continue; + let carrier: string | null = null; + let all = true; + for (const from of s.importers) { + const importer = states.get(from); + const owner = + importer === undefined ? null : importer.island ? importer.key : importer.carriedBy; + if (owner === null) { + all = false; + break; + } + carrier ??= owner; + } + if (all && carrier !== null) { + s.carriedBy = carrier; + changed = true; + } + } + } + + // Entries. + const islands: NodeIslandEntry[] = []; + const packages: NativePackage[] = []; + for (const s of states.values()) { + const version = s.dir === null ? null : packageVersionOf(s.dir); + if (s.island) { + const first = s.reasons[0]; + const modules = new Set(); + for (const r of s.reasons) if (r.loc !== null) modules.add(r.loc.file); + const carries: string[] = []; + for (const other of states.values()) { + if (other.carriedBy === s.key) carries.push(other.name); + } + carries.sort(); + islands.push({ + package: s.name, + version, + path: s.dir, + class: first === undefined ? "marked" : first.class, + reason: first === undefined ? markedReason(s.decision) : describe(first), + diagnostic: first === undefined ? null : first.code, + decision: s.decision, + reasons: s.reasons, + modules: modules.size, + carries, + importedFrom: s.nativeSites, + evaluation: "eager", + }); + continue; + } + if (s.violation) continue; + // A package that resolves to no installed JavaScript never loads (an + // optional dependency that is absent): nothing to compile or island. + if (s.dir === null && s.reasons.length === 0) continue; + const reachingIslands: string[] = []; + for (const from of s.importers) { + const importer = states.get(from); + if (importer === undefined) continue; + const owner = importer.island ? importer.key : importer.carriedBy; + if (owner !== null) { + const name = states.get(owner)!.name; + if (!reachingIslands.includes(name)) reachingIslands.push(name); + } + } + reachingIslands.sort(); + packages.push({ + package: s.name, + version, + path: s.dir, + status: s.carriedBy !== null ? "inside" : reachingIslands.length > 0 ? "shared" : "native", + islands: s.carriedBy !== null ? [states.get(s.carriedBy)!.name] : reachingIslands, + }); + } + islands.sort( + (a, b) => + reportRank(a.class) - reportRank(b.class) || + compareText(a.package, b.package) || + compareText(a.path ?? "", b.path ?? ""), + ); + packages.sort( + (a, b) => compareText(a.package, b.package) || compareText(a.path ?? "", b.path ?? ""), + ); + + // Marked user modules and SC81xx in user JavaScript. + const userModules: UserModuleIsland[] = []; + const userJs: ScrDiagnostic[] = []; + for (const m of input.scan.modules) { + if (m.origin !== "user") continue; + const marked = isMarkedUserModule(policy, m.file); + if (marked !== null) { + userModules.push({ file: m.file, source: marked }); + continue; + } + if (!policy.strictUserJs) continue; + for (const marker of m.markers) { + const code = ruleInfo(marker.rule).userJsCode; + if (code === null) continue; + userJs.push(userJsObjectModelDiag(code, capitalize(marker.construct), marker.loc)); + } + } + userModules.sort((a, b) => compareText(a.file, b.file)); + + const unknownPackages: string[] = []; + // Only command-line names: a project's configuration may list packages + // for other entries. + for (const forced of [policy.forcedNode, policy.forcedNative]) { + for (const [name, source] of forced) { + if (source === "cli" && !byName.has(name) && !unknownPackages.includes(name)) + unknownPackages.push(name); + } + } + unknownPackages.sort(); + + return { + mode: "would-be", + policy: policy.mode, + islands, + packages, + userModules, + violations, + userJs, + unknownPackages, + }; +} + +function compareText(a: string, b: string): number { + return a < b ? -1 : a > b ? 1 : 0; +} + +function capitalize(text: string): string { + return text.length === 0 ? text : text.charAt(0).toUpperCase() + text.slice(1); +} + +function markedReason(decision: IslandDecision): string { + return decision === "cli" ? "marked with --node-island" : 'marked "node" in scriptc.packages'; +} + +/** Decisive class first; within a class, refusals before pre-scan markers + * (the lowering is authoritative), then source order. */ +function sortReasons(reasons: NodeIslandReason[]): void { + const sourceRank = (r: NodeIslandReason): number => + r.source === "package" ? 0 : r.source === "refusal" ? 1 : r.source === "fence" ? 2 : 3; + reasons.sort( + (a, b) => + classRank(a.class) - classRank(b.class) || + sourceRank(a) - sourceRank(b) || + compareText(a.loc?.file ?? "", b.loc?.file ?? "") || + (a.loc?.start ?? 0) - (b.loc?.start ?? 0), + ); +} diff --git a/packages/compiler/src/node-islands/rules.ts b/packages/compiler/src/node-islands/rules.ts new file mode 100644 index 000000000..25f686314 --- /dev/null +++ b/packages/compiler/src/node-islands/rules.ts @@ -0,0 +1,109 @@ +/* The refusal rules of scope B (results/design-scope-b/DESIGN.md §1.2): + * the constructs that put a module outside the native subset, grouped in + * eleven rule families, and the four classes the detector reports. + * + * runtime (R1) needs the JavaScript runtime itself + * object-model (R2) needs a mutable object model + * gap inside the native subset, but scriptc cannot compile it yet + * packaging not compilable as shipped (no declarations, minified, ...) + * + * A reason id names one construct; refusal sites carry it on their + * diagnostic (ScrDiagnostic.reason) and the pre-scan (detect.ts) reports it + * with the rule. */ + +export type NodeIslandClass = "runtime" | "object-model" | "gap" | "packaging" | "marked"; + +export type NodeIslandRule = + | "R1.1" + | "R1.2" + | "R1.3" + | "R1.4" + | "R1.5" + | "R1.6" + | "R2.1" + | "R2.2" + | "R2.3" + | "R2.4" + | "R2.5"; + +export interface RuleInfo { + rule: NodeIslandRule; + title: string; + /** The SC81xx code the construct is in user JavaScript (R2 only). */ + userJsCode: "SC8101" | "SC8102" | "SC8103" | "SC8104" | "SC8105" | null; +} + +const RULES: RuleInfo[] = [ + { rule: "R1.1", title: "code from strings", userJsCode: null }, + { rule: "R1.2", title: "dynamic module loading", userJsCode: null }, + { rule: "R1.3", title: "native addons", userJsCode: null }, + { rule: "R1.4", title: "loader and require hooks", userJsCode: null }, + { rule: "R1.5", title: "engine internals", userJsCode: null }, + { rule: "R1.6", title: "runtime-only globals", userJsCode: null }, + { rule: "R2.1", title: "constructor functions", userJsCode: "SC8101" }, + { rule: "R2.2", title: "prototype mutation", userJsCode: "SC8102" }, + { rule: "R2.3", title: "shape mutation outside the checker's view", userJsCode: "SC8103" }, + { rule: "R2.4", title: "sloppy-mode semantics", userJsCode: "SC8104" }, + { rule: "R2.5", title: "builtin and global patching", userJsCode: "SC8105" }, +]; + +export function ruleInfo(rule: NodeIslandRule): RuleInfo { + for (const info of RULES) if (info.rule === rule) return info; + throw new Error(`unknown Node island rule ${rule}`); +} + +/** Reason id → rule. Ids without a rule (gap, packaging) are absent. */ +const REASON_RULES: Record = { + "code-from-string": "R1.1", + vm: "R1.1", + "dynamic-require": "R1.2", + "dynamic-import": "R1.2", + "unresolved-import": "R1.2", + "native-addon": "R1.3", + "loader-hook": "R1.4", + "engine-internal": "R1.5", + "weak-ref": "R1.6", + "constructor-function": "R2.1", + "prototype-write": "R2.2", + "define-property": "R2.3", + with: "R2.4", + "sloppy-arguments": "R2.4", + "sloppy-mode": "R2.4", + "builtin-write": "R2.5", + proxy: "R2.5", +}; + +export function ruleOfReason(reason: string): NodeIslandRule | null { + return REASON_RULES[reason] ?? null; +} + +export function classOfRule(rule: NodeIslandRule): NodeIslandClass { + return rule.startsWith("R1.") ? "runtime" : "object-model"; +} + +/** The decisive class first: a runtime need outranks an object-model need, + * which outranks shipping problems; a gap is a package's class only when + * nothing else keeps it out of the native subset (fixing the gap would make + * it native). */ +export function classRank(cls: NodeIslandClass): number { + if (cls === "runtime") return 0; + if (cls === "object-model") return 1; + if (cls === "packaging") return 2; + if (cls === "gap") return 3; + return 4; +} + +/** Report order: gap islands first (compiler work hidden behind an + * island), then the rest by rank. */ +export function reportRank(cls: NodeIslandClass): number { + if (cls === "gap") return 0; + return classRank(cls) + 1; +} + +export function classDescription(cls: NodeIslandClass): string { + if (cls === "runtime") return "needs the JavaScript runtime"; + if (cls === "object-model") return "needs a mutable object model"; + if (cls === "gap") return "inside the native subset, but scriptc cannot compile it yet"; + if (cls === "packaging") return "not compilable as shipped"; + return "marked to run in Node"; +} diff --git a/packages/compiler/src/node-islands/scan.ts b/packages/compiler/src/node-islands/scan.ts new file mode 100644 index 000000000..0ac082e73 --- /dev/null +++ b/packages/compiler/src/node-islands/scan.ts @@ -0,0 +1,155 @@ +/* The program-level half of the detector: which modules a program loads, + * the pre-scan markers in each, and the package import edges the island + * closure needs. The frontend calls it on its final program (and on the + * program that still held a package the drop-and-reload loop removed). */ +import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; +import type { SrcLoc } from "../ir/ir.js"; +import * as ts from "../frontend/ts7/adapter.js"; +import { canonicalBuiltinModule, npmBuiltinAliasOf } from "../frontend/builtin-modules.js"; +import { erasedTypeOnlyImport } from "../frontend/type-only-imports.js"; +import { isShimmedImport } from "../frontend/package-shims.js"; +import { resolveBareModule } from "../frontend/resolve.js"; +import { isJsSourceFileName } from "../frontend/tsc-codes.js"; +import { isRelativeSpecifier, packageNameOfSpecifier } from "../frontend/workspace-registry.js"; +import { moduleSourceCandidates } from "../frontend/module-source-candidates.js"; +import { scanModule, type NodeIslandMarker } from "./detect.js"; +import { originOf, packageDirOf, type Origin } from "./origin.js"; + +export interface ScannedModule { + file: string; + origin: Origin; + /** The npm package directory, for npm modules. */ + packageDir: string | null; + packageName: string | null; + markers: NodeIslandMarker[]; + /** True for a module of a package the drop-and-reload loop removed. */ + dropped: boolean; +} + +/** One import of an npm package. */ +export interface PackageEdge { + /** The importing package's directory; null for user code. */ + fromDir: string | null; + fromFile: string; + /** The imported package's name. */ + to: string; + /** Its directory when the import resolves to installed JavaScript. */ + toDir: string | null; + /** Served by a registry shim, a builtin alias, or marked unloaded. */ + shim: boolean; + loc: SrcLoc; +} + +/** How the drop-and-reload loop refused a package. */ +export type RefusalKind = "preflight" | "offender" | "degrade" | "typecheck" | "external"; + +export interface RefusalRecord { + package: string; + kind: RefusalKind; + /** The package-anchored preflight diagnostic, for kind "preflight". */ + diag: ScrDiagnostic | null; +} + +export interface NodeIslandScan { + modules: ScannedModule[]; + edges: PackageEdge[]; + refusals: RefusalRecord[]; +} + +export function emptyScan(): NodeIslandScan { + return { modules: [], edges: [], refusals: [] }; +} + +function locOfNode(node: ts.Node): SrcLoc { + const sf = ts.getSourceFileOfNode(node); + return { file: sf.fileName, start: ts.tokenStartOfNode(node), end: node.end }; +} + +/** The literal module specifiers of a file with their sites: top-level + * import and export declarations, and the require() and import() calls the + * program's shared module-source scan already collected (no second walk). */ +function specifiersOf(program: ts.Program, sf: ts.SourceFile): { spec: string; node: ts.Node }[] { + const out: { spec: string; node: ts.Node }[] = []; + for (const node of sf.statements.nodes) { + if ( + (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && + node.moduleSpecifier !== undefined && + ts.isStringLiteral(node.moduleSpecifier) + ) { + // Type-only imports load nothing. + const typeOnly = ts.isImportDeclaration(node) ? erasedTypeOnlyImport(node) : node.isTypeOnly; + if (!typeOnly) out.push({ spec: node.moduleSpecifier.text, node }); + } + } + for (const call of moduleSourceCandidates(program, sf).calls) { + const arg = call.arguments.nodes[0]; + if ( + arg !== undefined && + ts.isStringLiteralLike(arg) && + (call.expression.kind === ts.SyntaxKind.ImportKeyword || + (ts.isIdentifier(call.expression) && call.expression.text === "require")) + ) + out.push({ spec: arg.text, node: call }); + } + return out; +} + +/** Scans `files`: markers for npm JavaScript (and for user JavaScript when + * `userJs`), and package edges for every file when `edges` (a program with + * no npm package has none to find). */ +export function scanFiles( + program: ts.Program, + files: readonly ts.SourceFile[], + opts: { userJs: boolean; dropped: boolean; edges: boolean }, +): { modules: ScannedModule[]; edges: PackageEdge[] } { + const modules: ScannedModule[] = []; + const edges: PackageEdge[] = []; + const seen = new Set(); + for (const sf of files) { + if (seen.has(sf.fileName)) continue; + seen.add(sf.fileName); + const origin = originOf(sf.fileName); + if (origin === "builtin" || origin === "shim") continue; + const at = origin === "npm" ? packageDirOf(sf.fileName) : null; + const js = isJsSourceFileName(sf.fileName); + const markers = js && (origin === "npm" || opts.userJs) ? scanModule(sf) : []; + modules.push({ + file: sf.fileName, + origin, + packageDir: at === null ? null : at.dir, + packageName: at === null ? null : at.name, + markers, + dropped: opts.dropped, + }); + if (!opts.edges) continue; + for (const { spec, node } of specifiersOf(program, sf)) { + if (isRelativeSpecifier(spec) || spec.startsWith("/") || spec.startsWith("#")) continue; + // scriptc's own packages lower at their use sites. + if (spec.startsWith("@scriptc/")) continue; + const alias = npmBuiltinAliasOf(spec); + if (alias === null && (spec.startsWith("node:") || canonicalBuiltinModule(spec) !== null)) + continue; + const name = packageNameOfSpecifier(spec); + const shim = alias !== null || isShimmedImport(sf.fileName, spec); + let toDir: string | null = null; + if (!shim) { + const resolved = resolveBareModule(sf.fileName, spec, "js-only"); + if (resolved !== null) { + const target = packageDirOf(resolved.typesFile); + // A workspace package compiled from source is user code. + if (target === null) continue; + toDir = target.dir; + } + } + edges.push({ + fromDir: at === null ? null : at.dir, + fromFile: sf.fileName, + to: name, + toDir, + shim, + loc: locOfNode(node), + }); + } + } + return { modules, edges }; +} diff --git a/packages/compiler/src/node-islands/summary.ts b/packages/compiler/src/node-islands/summary.ts new file mode 100644 index 000000000..8108d985c --- /dev/null +++ b/packages/compiler/src/node-islands/summary.ts @@ -0,0 +1,110 @@ +/* The Node island report's data model and the small helpers coverage + * rendering needs. Kept free of compiler imports: the coverage report (and + * the CLI's verdict-replay fast path behind it) loads this module. */ +import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; +import type { SrcLoc } from "../ir/ir.js"; +import type { DecisionSource, NodeIslandPolicy } from "./policy.js"; +import type { NodeIslandClass, NodeIslandRule } from "./rules.js"; + +export type NodeIslandReasonSource = "pre-scan" | "refusal" | "fence" | "package"; + +export interface NodeIslandReason { + class: NodeIslandClass; + rule: NodeIslandRule | null; + /** Reason id: a rules.ts id, "gap" or "packaging". */ + reason: string; + /** The SC code of the refusal, when the reason is a diagnostic. */ + code: string | null; + message: string; + loc: SrcLoc | null; + source: NodeIslandReasonSource; +} + +export type IslandDecision = "auto" | "config" | "cli"; + +export interface NodeIslandEntry { + package: string; + version: string | null; + /** The package directory (the instance), when it is installed. */ + path: string | null; + class: NodeIslandClass; + /** The first decisive reason, rendered. */ + reason: string; + /** The SC code of the first decisive reason, when it is a refusal. */ + diagnostic: string | null; + decision: IslandDecision; + /** Every reason, in report order (decisive class first, then source + * order). */ + reasons: NodeIslandReason[]; + /** Modules of the package with at least one reason. */ + modules: number; + /** Packages imported only through this island, which run inside it. */ + carries: string[]; + /** Where native code imports the package: the island boundary. */ + importedFrom: SrcLoc[]; + /** Report-only: deferral (NI-7) is not decided yet, so every island + * would evaluate eagerly at its module position. */ + evaluation: "eager"; +} + +export interface UserModuleIsland { + file: string; + source: DecisionSource; +} + +export interface NativePackage { + package: string; + version: string | null; + path: string | null; + /** "native": compiles natively; "inside": runs inside an island's + * closure; "shared": reached from native code and from an island. */ + status: "native" | "inside" | "shared"; + /** For inside and shared: the islands that reach it. */ + islands: string[]; +} + +export interface NodeIslandReport { + mode: "would-be"; + policy: NodeIslandPolicy["mode"]; + islands: NodeIslandEntry[]; + packages: NativePackage[]; + userModules: UserModuleIsland[]; + /** Would-be errors: SC8002 (islands off) and SC8003 (marked native). */ + violations: ScrDiagnostic[]; + /** SC8101–SC8105 for user JavaScript (only with --strict-user-js). */ + userJs: ScrDiagnostic[]; + /** --native / --node-island / config package names the program does + * not import. */ + unknownPackages: string[]; +} + +/** "R2.1 constructor functions: 'new Foo()' of a function" or + * "SC2012: ". */ +export function describe(r: NodeIslandReason): string { + if (r.source === "pre-scan" && r.rule !== null) return `${r.rule} ${r.message}`; + const rule = r.rule === null ? "" : `${r.rule} `; + return r.code === null ? `${rule}${r.message}` : `${rule}${r.code}: ${r.message}`; +} + +/** True when the report has decisions to show (islands, marked modules, + * policy errors or unknown names); SC81xx findings alone render as + * blockers. */ +export function hasNodeIslandDecisions(report: NodeIslandReport): boolean { + return ( + report.islands.length > 0 || + report.userModules.length > 0 || + report.violations.length > 0 || + report.unknownPackages.length > 0 + ); +} + +/** True when the report has anything to show. */ +export function hasNodeIslandFindings(report: NodeIslandReport): boolean { + return ( + report.islands.length > 0 || + report.userModules.length > 0 || + report.violations.length > 0 || + report.userJs.length > 0 || + report.unknownPackages.length > 0 + ); +} diff --git a/packages/compiler/src/project-config.test.ts b/packages/compiler/src/project-config.test.ts new file mode 100644 index 000000000..b9750f24e --- /dev/null +++ b/packages/compiler/src/project-config.test.ts @@ -0,0 +1,126 @@ +import { expect, test } from "vitest"; +import { parseProjectConfig } from "./project-config.js"; + +const manifest = "/project/package.json"; + +function config(scriptc: unknown) { + return parseProjectConfig(manifest, JSON.stringify({ name: "app", scriptc }, null, 2)); +} + +test("a package.json without a scriptc key has the default configuration", () => { + const c = parseProjectConfig(manifest, '{ "name": "app" }'); + expect(c.diagnostics).toEqual([]); + expect(c.packages.size).toBe(0); + expect(c.nodeIslands).toEqual({ + auto: true, + modules: [], + node: null, + nodeVersion: null, + packaging: "embedded", + speculativeStart: false, + }); + expect(parseProjectConfig(manifest, "{ not json").diagnostics).toEqual([]); +}); + +test("package settings: strings and objects", () => { + const c = config({ + packages: { + encoding: "unloaded", + "dd-trace": "node", + "hot-shots": "native", + semver: "auto", + "proper-lockfile": { run: "node", evaluate: "lazy", share: "duplicate" }, + }, + }); + expect(c.diagnostics).toEqual([]); + expect(c.packages.get("encoding")?.unloaded).toBe(true); + expect(c.packages.get("dd-trace")?.run).toBe("node"); + expect(c.packages.get("hot-shots")?.run).toBe("native"); + expect(c.packages.get("semver")?.run).toBe("auto"); + expect(c.packages.get("proper-lockfile")).toEqual({ + unloaded: false, + run: "node", + evaluate: "lazy", + share: "duplicate", + marshal: "copy", + schedule: "lockstep", + }); +}); + +test("nodeIslands settings", () => { + const c = config({ + nodeIslands: { + auto: false, + modules: ["src/plugins/*.ts"], + node: "/usr/local/bin/node", + nodeVersion: ">=24.13.1", + packaging: "beside", + speculativeStart: true, + }, + wasm: ["build/a.wasm", 3], + }); + expect(c.diagnostics).toEqual([]); + expect(c.nodeIslands).toEqual({ + auto: false, + modules: ["src/plugins/*.ts"], + node: "/usr/local/bin/node", + nodeVersion: ">=24.13.1", + packaging: "beside", + speculativeStart: true, + }); + // scriptc.wasm keeps its lenient reading. + expect(c.wasm).toEqual(["/project/build/a.wasm"]); +}); + +test("unknown keys and values are SC8004, located at the key", () => { + const text = JSON.stringify( + { + name: "app", + scriptc: { + packages: { + encoding: "unloded", + debug: { run: "node", evaluate: "later", color: true }, + ms: 3, + }, + nodeIslands: { auto: "yes", mode: "auto", modules: "src/x.ts", packaging: "zip" }, + }, + }, + null, + 2, + ); + const c = parseProjectConfig(manifest, text); + expect(c.diagnostics.map((d) => [d.code, d.message])).toEqual([ + ["SC8004", `package.json 'scriptc.packages.encoding' has the unknown value "unloded"`], + ["SC8004", `package.json 'scriptc.packages.debug.evaluate' has the unknown value "later"`], + ["SC8004", "package.json 'scriptc.packages.debug.color' is not a known package setting"], + ["SC8004", "package.json 'scriptc.packages.ms' must be a string or an object, not 3"], + ["SC8004", `package.json 'scriptc.nodeIslands.auto' must be true or false, not "yes"`], + ["SC8004", "package.json 'scriptc.nodeIslands.mode' is not a known Node island setting"], + [ + "SC8004", + `package.json 'scriptc.nodeIslands.modules' must be an array of module paths or globs, not "src/x.ts"`, + ], + ["SC8004", `package.json 'scriptc.nodeIslands.packaging' has the unknown value "zip"`], + ]); + // Locations point at the offending key inside the scriptc object. + const at = (i: number) => text.slice(c.diagnostics[i]!.loc.start).split(":")[0]; + expect(at(0)).toBe('"encoding"'); + expect(at(1)).toBe('"evaluate"'); + expect(at(5)).toBe('"mode"'); + for (const d of c.diagnostics) expect(d.loc.file).toBe(manifest); + // Invalid entries are not applied; valid siblings are. + expect(c.packages.has("encoding")).toBe(false); + expect(c.packages.has("debug")).toBe(false); + expect(c.nodeIslands.auto).toBe(true); +}); + +test("a non-object packages or nodeIslands value is SC8004", () => { + expect(config({ packages: ["a"] }).diagnostics.map((d) => d.message)).toEqual([ + "package.json 'scriptc.packages' must be an object mapping package names to settings, not an array", + ]); + expect(config({ nodeIslands: true }).diagnostics.map((d) => d.message)).toEqual([ + "package.json 'scriptc.nodeIslands' must be an object, not true", + ]); + // Other keys of the scriptc object belong to other readers. + expect(config({ somethingElse: 1 }).diagnostics).toEqual([]); +}); diff --git a/packages/compiler/src/project-config.ts b/packages/compiler/src/project-config.ts new file mode 100644 index 000000000..cc13a868d --- /dev/null +++ b/packages/compiler/src/project-config.ts @@ -0,0 +1,421 @@ +/* The project configuration: the `scriptc` key of the entry's nearest + * package.json. One reader for every consumer (package shims, the + * WebAssembly scanner, the Node island policy), with a schema: a value + * this compiler does not know in `scriptc.packages` or + * `scriptc.nodeIslands` is SC8004 instead of being ignored, because an + * ignored setting is a silent divergence from what the project asked for. + * + * "scriptc": { + * "packages": { + * "encoding": "unloaded", // never loaded (package-shims.ts) + * "dd-trace": "node", // run in a Node island + * "hot-shots": "native", // must compile natively (SC8003) + * "proper-lockfile": { "run": "node", "evaluate": "lazy" } + * }, + * "nodeIslands": { "auto": true, "modules": ["src/plugins/*.ts"] }, + * "wasm": ["build/module.wasm"] + * } + * + * Node islands are report-only in this version: the policy decides which + * packages and modules WOULD run in Node, and coverage reports it, but code + * generation does not change. Other keys of the `scriptc` object are left + * to their own readers. */ +import { dirname, join, resolve } from "node:path"; +import { projectConfigDiag, type ScrDiagnostic } from "./diagnostics/diagnostic.js"; +import { trackedReadFile } from "./frontend/input-tracker.js"; + +/** How a package runs: compiled natively, in a Node island, or decided by + * the detector. */ +export type PackageRun = "native" | "node" | "auto"; +export type PackageEvaluate = "auto" | "lazy" | "eager"; +export type PackageShare = "auto" | "shared" | "duplicate"; +export type PackageMarshal = "copy" | "handles"; +export type PackageSchedule = "lockstep" | "free"; + +/** One `scriptc.packages.` entry. `unloaded` packages never load; + * the other settings choose where the package runs. */ +export interface PackageSetting { + unloaded: boolean; + run: PackageRun; + evaluate: PackageEvaluate; + share: PackageShare; + marshal: PackageMarshal; + schedule: PackageSchedule; +} + +export interface NodeIslandsSetting { + /** Automatic Node islands for npm code outside the native subset. */ + auto: boolean; + /** User modules (paths or globs relative to the package.json) that run + * in Node. */ + modules: string[]; + /** The `node` executable recorded into the binary. */ + node: string | null; + /** A semver range checked at the helper handshake. */ + nodeVersion: string | null; + packaging: "embedded" | "beside"; + speculativeStart: boolean; +} + +export interface ProjectConfig { + /** The package.json the configuration came from, or null. */ + manifest: string | null; + /** Its text, for diagnostic code frames. */ + manifestText: string | null; + packages: Map; + nodeIslands: NodeIslandsSetting; + /** `scriptc.wasm`: module paths, resolved against the manifest. */ + wasm: string[]; + /** SC8004 for every unknown key or value. */ + diagnostics: ScrDiagnostic[]; +} + +const PACKAGE_STRINGS = ["unloaded", "node", "native", "auto"]; +const PACKAGE_OBJECT_KEYS: Record = { + run: ["native", "node", "auto"], + evaluate: ["auto", "lazy", "eager"], + share: ["auto", "shared", "duplicate"], + marshal: ["copy", "handles"], + schedule: ["lockstep", "free"], +}; +const NODE_ISLANDS_KEYS = [ + "auto", + "modules", + "node", + "nodeVersion", + "packaging", + "speculativeStart", +]; + +function defaultPackageSetting(): PackageSetting { + return { + unloaded: false, + run: "auto", + evaluate: "auto", + share: "auto", + marshal: "copy", + schedule: "lockstep", + }; +} + +function defaultNodeIslands(): NodeIslandsSetting { + return { + auto: true, + modules: [], + node: null, + nodeVersion: null, + packaging: "embedded", + speculativeStart: false, + }; +} + +/** The empty configuration of a project without a package.json. */ +export function emptyProjectConfig(): ProjectConfig { + return { + manifest: null, + manifestText: null, + packages: new Map(), + nodeIslands: defaultNodeIslands(), + wasm: [], + diagnostics: [], + }; +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function quoted(values: string[]): string { + return values.map((v) => `"${v}"`).join(", "); +} + +/** A short rendering of a JSON value for a message. */ +function shown(value: unknown): string { + if (typeof value === "string") return `"${value}"`; + if (value === null) return "null"; + if (Array.isArray(value)) return "an array"; + if (typeof value === "object") return "an object"; + return String(value); +} + +/** Reads and validates one package.json text. Exported for tests. */ +export function parseProjectConfig(manifest: string, text: string): ProjectConfig { + const config = emptyProjectConfig(); + config.manifest = manifest; + config.manifestText = text; + let parsed: unknown = null; + try { + parsed = JSON.parse(text); + } catch { + // Node reports a malformed package.json itself; the configuration is + // simply absent here, as before. + return config; + } + if (!isRecord(parsed)) return config; + const root = parsed["scriptc"]; + if (!isRecord(root)) return config; + const locate = new JsonLocator(text); + const problem = (path: string[], detail: string, hint: string): void => { + config.diagnostics.push( + projectConfigDiag(`scriptc.${path.join(".")}`, detail, hint, { + file: manifest, + start: locate.offsetOf(["scriptc", ...path]), + end: locate.offsetOf(["scriptc", ...path]), + }), + ); + }; + + const packages = root["packages"]; + if (packages !== undefined) { + if (!isRecord(packages)) { + problem( + ["packages"], + `must be an object mapping package names to settings, not ${shown(packages)}`, + `write "packages": { "": "auto" }`, + ); + } else { + for (const name of Object.keys(packages)) { + const value = packages[name]; + const setting = defaultPackageSetting(); + if (typeof value === "string") { + if (!PACKAGE_STRINGS.includes(value)) { + problem( + ["packages", name], + `has the unknown value ${shown(value)}`, + `use one of ${quoted(PACKAGE_STRINGS)}, or an object with ${Object.keys(PACKAGE_OBJECT_KEYS).join(", ")}`, + ); + continue; + } + if (value === "unloaded") setting.unloaded = true; + else setting.run = value as PackageRun; + config.packages.set(name, setting); + continue; + } + if (!isRecord(value)) { + problem( + ["packages", name], + `must be a string or an object, not ${shown(value)}`, + `use one of ${quoted(PACKAGE_STRINGS)}, or an object with ${Object.keys(PACKAGE_OBJECT_KEYS).join(", ")}`, + ); + continue; + } + let valid = true; + for (const key of Object.keys(value)) { + const allowed = PACKAGE_OBJECT_KEYS[key]; + if (allowed === undefined) { + problem( + ["packages", name, key], + "is not a known package setting", + `the settings are ${Object.keys(PACKAGE_OBJECT_KEYS).join(", ")}`, + ); + valid = false; + continue; + } + const v = value[key]; + if (typeof v !== "string" || !allowed.includes(v)) { + problem( + ["packages", name, key], + `has the unknown value ${shown(v)}`, + `use one of ${quoted(allowed)}`, + ); + valid = false; + continue; + } + if (key === "run") setting.run = v as PackageRun; + else if (key === "evaluate") setting.evaluate = v as PackageEvaluate; + else if (key === "share") setting.share = v as PackageShare; + else if (key === "marshal") setting.marshal = v as PackageMarshal; + else setting.schedule = v as PackageSchedule; + } + if (valid) config.packages.set(name, setting); + } + } + } + + const islands = root["nodeIslands"]; + if (islands !== undefined) { + if (!isRecord(islands)) { + problem( + ["nodeIslands"], + `must be an object, not ${shown(islands)}`, + `write "nodeIslands": { "auto": true }`, + ); + } else { + const setting = config.nodeIslands; + for (const key of Object.keys(islands)) { + const v = islands[key]; + if (!NODE_ISLANDS_KEYS.includes(key)) { + problem( + ["nodeIslands", key], + "is not a known Node island setting", + `the settings are ${NODE_ISLANDS_KEYS.join(", ")}`, + ); + } else if (key === "auto" || key === "speculativeStart") { + if (typeof v !== "boolean") { + problem( + ["nodeIslands", key], + `must be true or false, not ${shown(v)}`, + "use a boolean", + ); + } else if (key === "auto") setting.auto = v; + else setting.speculativeStart = v; + } else if (key === "modules") { + const list: string[] = []; + let ok = Array.isArray(v); + if (Array.isArray(v)) { + for (const entry of v as unknown[]) { + if (typeof entry === "string" && entry !== "") list.push(entry); + else ok = false; + } + } + if (!ok) { + problem( + ["nodeIslands", key], + `must be an array of module paths or globs, not ${shown(v)}`, + `write "modules": ["src/plugins/loader.ts"]`, + ); + } else setting.modules = list; + } else if (key === "packaging") { + if (v !== "embedded" && v !== "beside") { + problem( + ["nodeIslands", key], + `has the unknown value ${shown(v)}`, + `use "embedded" or "beside"`, + ); + } else setting.packaging = v; + } else if (typeof v !== "string" || v === "") { + problem( + ["nodeIslands", key], + `must be a non-empty string, not ${shown(v)}`, + key === "node" + ? "give the path of the node executable" + : `write a range such as ">=24.13.1"`, + ); + } else if (key === "node") setting.node = v; + else setting.nodeVersion = v; + } + } + } + + // `scriptc.wasm` keeps its lenient reading: string entries of an array. + const wasm = root["wasm"]; + if (Array.isArray(wasm)) { + for (const entry of wasm as unknown[]) { + if (typeof entry === "string") config.wasm.push(resolve(dirname(manifest), entry)); + } + } + return config; +} + +let cachedManifest: string | null = null; +let cachedText: string | null = null; +let cachedConfig: ProjectConfig | null = null; + +/** The configuration of the entry's nearest package.json (the first one + * found walking up from the entry's directory, readable or not). */ +export function readProjectConfig(entryPath: string): ProjectConfig { + for (let dir = dirname(resolve(entryPath)); ;) { + const manifest = join(dir, "package.json"); + const text = trackedReadFile(manifest); + if (text !== null) { + if (cachedConfig !== null && cachedManifest === manifest && cachedText === text) + return cachedConfig; + const config = parseProjectConfig(manifest, text); + cachedManifest = manifest; + cachedText = text; + cachedConfig = config; + return config; + } + const parent = dirname(dir); + if (parent === dir) return emptyProjectConfig(); + dir = parent; + } +} + +/** Offsets of keys in a JSON text, for diagnostic locations. It follows + * object nesting so `["scriptc", "packages", "x"]` finds the key `"x"` + * inside `scriptc.packages`, not an earlier `"x"` elsewhere. */ +class JsonLocator { + constructor(private readonly text: string) {} + + /** The offset of the last key of `path` that could be found (0 when + * none). */ + offsetOf(path: string[]): number { + let start = 0; + let end = this.text.length; + let found = 0; + for (const key of path) { + const at = this.keyIn(key, start, end); + if (at < 0) break; + found = at; + // The key's value: from after the colon to the end of its object. + const colon = this.text.indexOf(":", at); + if (colon < 0) break; + start = colon + 1; + end = this.valueEnd(start, end); + } + return found; + } + + /** A direct key of the object starting at or after `start`. */ + private keyIn(key: string, start: number, end: number): number { + const text = this.text; + let depth = 0; + let i = start; + while (i < end) { + const ch = text.charCodeAt(i); + if (ch === 34 /* " */) { + const close = this.stringEnd(i); + if (depth === 1 && text.slice(i + 1, close) === key) { + let j = close + 1; + while (j < end && /\s/.test(text.charAt(j))) j++; + if (text.charAt(j) === ":") return i; + } + i = close + 1; + continue; + } + if (ch === 123 /* { */ || ch === 91 /* [ */) depth++; + else if (ch === 125 /* } */ || ch === 93 /* ] */) { + depth--; + if (depth <= 0) return -1; + } + i++; + } + return -1; + } + + private stringEnd(open: number): number { + const text = this.text; + let i = open + 1; + while (i < text.length) { + const ch = text.charCodeAt(i); + if (ch === 92 /* \ */) i += 2; + else if (ch === 34) return i; + else i++; + } + return text.length; + } + + /** The end of the JSON value that starts at or after `start`. */ + private valueEnd(start: number, end: number): number { + const text = this.text; + let i = start; + while (i < end && /\s/.test(text.charAt(i))) i++; + let depth = 0; + while (i < end) { + const ch = text.charCodeAt(i); + if (ch === 34) { + i = this.stringEnd(i) + 1; + if (depth === 0) return i; + continue; + } + if (ch === 123 || ch === 91) depth++; + else if (ch === 125 || ch === 93) { + depth--; + if (depth <= 0) return i + 1; + } else if (depth === 0 && ch === 44 /* , */) return i; + i++; + } + return end; + } +} diff --git a/packages/compiler/src/wasm/discover.ts b/packages/compiler/src/wasm/discover.ts index de38bf64f..3cdc3eb96 100644 --- a/packages/compiler/src/wasm/discover.ts +++ b/packages/compiler/src/wasm/discover.ts @@ -24,11 +24,8 @@ import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { dirname, isAbsolute, join, resolve } from "node:path"; import * as ts from "../frontend/ts7/adapter.js"; -import { - trackedAccessibleEntries, - trackedFileExists, - trackedReadFile, -} from "../frontend/input-tracker.js"; +import { trackedAccessibleEntries, trackedReadFile } from "../frontend/input-tracker.js"; +import { readProjectConfig } from "../project-config.js"; import { analyzeWasm, isWasmHeader, type WasmModuleSummary } from "./binary.js"; import { decodeBase64 } from "./base64.js"; @@ -282,24 +279,10 @@ class Discovery { // ── source 4: configuration ───────────────────────────────────────── scanConfig(): void { - const manifest = nearestPackageJson(dirname(this.input.entry)); - if (manifest !== null) { - const text = trackedReadFile(manifest); - if (text !== null) { - let listed: unknown; - try { - const parsed = JSON.parse(text) as { scriptc?: { wasm?: unknown } }; - listed = parsed.scriptc?.wasm; - } catch { - listed = undefined; - } - if (Array.isArray(listed)) { - for (const entry of listed) { - if (typeof entry === "string") { - this.addFile(resolve(dirname(manifest), entry), { kind: "config", file: manifest }); - } - } - } + const config = readProjectConfig(this.input.entry); + if (config.manifest !== null) { + for (const path of config.wasm) { + this.addFile(path, { kind: "config", file: config.manifest }); } } for (const path of this.input.configured) { @@ -396,16 +379,6 @@ function collectWasmFiles(dir: string, depth: number, out: string[]): void { } } -function nearestPackageJson(dir: string): string | null { - for (;;) { - const candidate = join(dir, "package.json"); - if (trackedFileExists(candidate)) return candidate; - const parent = dirname(dir); - if (parent === dir) return null; - dir = parent; - } -} - /** Every WebAssembly module the program's sources, packages and * configuration make visible to the build, in discovery order. */ export function discoverWasmModules(input: WasmDiscoveryInput): WasmModuleReport[] { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json new file mode 100644 index 000000000..6b86e7f9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/node-islands-config/main.ts" + ], + "diags": [] +} diff --git a/tests/coverage-fixtures/node-islands-user-js.cjs b/tests/coverage-fixtures/node-islands-user-js.cjs new file mode 100644 index 000000000..c01f8c255 --- /dev/null +++ b/tests/coverage-fixtures/node-islands-user-js.cjs @@ -0,0 +1,22 @@ +// User JavaScript with one R2 construct per rule. With --strict-user-js, +// coverage reports each as the SC81xx error scope B makes it; without the +// flag the report is unchanged. +function Point(x) { + this.x = x; +} +Point.prototype.norm = function norm() { + return Math.abs(this.x); +}; +const base = { kind: "base" }; +const child = {}; +Object.setPrototypeOf(child, base); +const box = {}; +Object.defineProperty(box, "size", { value: 3, enumerable: false }); +function legacy(a) { + arguments[0] = 2; + return a; +} +Array.prototype.last = function last() { + return this[this.length - 1]; +}; +console.log(new Point(-2).norm(), child.kind, box.size, legacy(1)); diff --git a/tests/coverage-fixtures/node-islands/main.ts b/tests/coverage-fixtures/node-islands/main.ts new file mode 100644 index 000000000..ac6037d1a --- /dev/null +++ b/tests/coverage-fixtures/node-islands/main.ts @@ -0,0 +1,16 @@ +// Node islands, report-only: one npm package per island class, a package +// that runs inside an island's closure, one reached from both sides, and +// one that stays native. Coverage reports the islands a build would +// choose; the build itself is unchanged. +import { describe } from "ni-gap"; +import { compile } from "ni-eval"; +import { Counter } from "ni-ctor"; +import { shout } from "ni-minified"; +import { twice } from "ni-typed"; + +console.log(describe()); +console.log(compile("a + 1")(2)); +const counter = new Counter(); +counter.add(twice(2)); +console.log(counter.total()); +console.log(shout("hi")); diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.d.ts b/tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.d.ts new file mode 100644 index 000000000..2b70027a9 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.d.ts @@ -0,0 +1,4 @@ +export declare class Counter { + add(n: number): void; + total(): number; +} diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.js b/tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.js new file mode 100644 index 000000000..39992107e --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.js @@ -0,0 +1,15 @@ +"use strict"; +// A constructor function with prototype methods needs a mutable object +// model (R2.1). Its helper package runs inside its island. +const { step } = require("ni-dep"); +const { twice } = require("ni-typed"); +function Counter() { + this.count = 0; +} +Counter.prototype.add = function add(n) { + this.count = step(this.count, twice(n)); +}; +Counter.prototype.total = function total() { + return this.count; +}; +exports.Counter = Counter; diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-ctor/package.json b/tests/coverage-fixtures/node-islands/node_modules/ni-ctor/package.json new file mode 100644 index 000000000..096b98d20 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-ctor/package.json @@ -0,0 +1,5 @@ +{ + "name": "ni-ctor", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-dep/index.js b/tests/coverage-fixtures/node-islands/node_modules/ni-dep/index.js new file mode 100644 index 000000000..71a0eec99 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-dep/index.js @@ -0,0 +1,4 @@ +"use strict"; +exports.step = function step(a, b) { + return a + b; +}; diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-dep/package.json b/tests/coverage-fixtures/node-islands/node_modules/ni-dep/package.json new file mode 100644 index 000000000..24b33b1ef --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-dep/package.json @@ -0,0 +1,5 @@ +{ + "name": "ni-dep", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-eval/index.d.ts b/tests/coverage-fixtures/node-islands/node_modules/ni-eval/index.d.ts new file mode 100644 index 000000000..4bdbf94f6 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-eval/index.d.ts @@ -0,0 +1 @@ +export declare function compile(body: string): (a: number) => number; diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-eval/index.js b/tests/coverage-fixtures/node-islands/node_modules/ni-eval/index.js new file mode 100644 index 000000000..c549c058e --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-eval/index.js @@ -0,0 +1,5 @@ +"use strict"; +// Code from strings needs the JavaScript runtime (R1.1). +exports.compile = function compile(body) { + return new Function("a", "return " + body); +}; diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-eval/package.json b/tests/coverage-fixtures/node-islands/node_modules/ni-eval/package.json new file mode 100644 index 000000000..449239a05 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-eval/package.json @@ -0,0 +1,5 @@ +{ + "name": "ni-eval", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-gap/index.d.ts b/tests/coverage-fixtures/node-islands/node_modules/ni-gap/index.d.ts new file mode 100644 index 000000000..6e1b743c2 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-gap/index.d.ts @@ -0,0 +1 @@ +export declare function describe(): string; diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-gap/index.js b/tests/coverage-fixtures/node-islands/node_modules/ni-gap/index.js new file mode 100644 index 000000000..2148e51fd --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-gap/index.js @@ -0,0 +1,8 @@ +"use strict"; +// A construct inside the native subset that scriptc does not compile yet: +// a builtin function alias used as an escaping value. +const { extname } = require("node:path"); +const ext = extname; +exports.describe = function describe() { + return typeof ext; +}; diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-gap/package.json b/tests/coverage-fixtures/node-islands/node_modules/ni-gap/package.json new file mode 100644 index 000000000..9804ad187 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-gap/package.json @@ -0,0 +1,5 @@ +{ + "name": "ni-gap", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-minified/index.d.ts b/tests/coverage-fixtures/node-islands/node_modules/ni-minified/index.d.ts new file mode 100644 index 000000000..ccebdab51 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-minified/index.d.ts @@ -0,0 +1 @@ +export declare function shout(s: string): string; diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-minified/index.js b/tests/coverage-fixtures/node-islands/node_modules/ni-minified/index.js new file mode 100644 index 000000000..25c0e11ac --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-minified/index.js @@ -0,0 +1 @@ +"use strict";exports.shout=function shout(s){return s.toUpperCase()+"!"}; \ No newline at end of file diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-minified/package.json b/tests/coverage-fixtures/node-islands/node_modules/ni-minified/package.json new file mode 100644 index 000000000..cc819dfc8 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-minified/package.json @@ -0,0 +1,5 @@ +{ + "name": "ni-minified", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-typed/index.d.ts b/tests/coverage-fixtures/node-islands/node_modules/ni-typed/index.d.ts new file mode 100644 index 000000000..bb1a852b1 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-typed/index.d.ts @@ -0,0 +1 @@ +export declare function twice(n: number): number; diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-typed/index.js b/tests/coverage-fixtures/node-islands/node_modules/ni-typed/index.js new file mode 100644 index 000000000..082402bfd --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-typed/index.js @@ -0,0 +1,4 @@ +"use strict"; +exports.twice = function twice(n) { + return n * 2; +}; diff --git a/tests/coverage-fixtures/node-islands/node_modules/ni-typed/package.json b/tests/coverage-fixtures/node-islands/node_modules/ni-typed/package.json new file mode 100644 index 000000000..c3048156c --- /dev/null +++ b/tests/coverage-fixtures/node-islands/node_modules/ni-typed/package.json @@ -0,0 +1,5 @@ +{ + "name": "ni-typed", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/coverage-fixtures/node-islands/package.json b/tests/coverage-fixtures/node-islands/package.json new file mode 100644 index 000000000..f18eb6012 --- /dev/null +++ b/tests/coverage-fixtures/node-islands/package.json @@ -0,0 +1,5 @@ +{ + "name": "node-islands-coverage-fixture", + "private": true, + "type": "module" +} diff --git a/tests/diagnostics/node-islands-config/main.ts b/tests/diagnostics/node-islands-config/main.ts new file mode 100644 index 000000000..cc7b3735f --- /dev/null +++ b/tests/diagnostics/node-islands-config/main.ts @@ -0,0 +1,3 @@ +// An unknown value in scriptc.packages or scriptc.nodeIslands is SC8004: +// the configuration the project asked for is never silently ignored. +console.log("configured"); diff --git a/tests/diagnostics/node-islands-config/package.json b/tests/diagnostics/node-islands-config/package.json new file mode 100644 index 000000000..1a01afa30 --- /dev/null +++ b/tests/diagnostics/node-islands-config/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "type": "module", + "scriptc": { + "packages": { + "encoding": "unloded", + "debug": { "run": "island" } + }, + "nodeIslands": { + "auto": "yes", + "mode": "auto" + } + } +} diff --git a/tests/harness/__snapshots__/coverage-node-islands-explain.txt b/tests/harness/__snapshots__/coverage-node-islands-explain.txt new file mode 100644 index 000000000..f416b9781 --- /dev/null +++ b/tests/harness/__snapshots__/coverage-node-islands-explain.txt @@ -0,0 +1,13 @@ + Node island ni-ctor@1.0.0 (would-be, report-only) + path tests/coverage-fixtures/node-islands/node_modules/ni-ctor + class object-model: needs a mutable object model + decision automatic (npm code outside the native subset) + evaluation eager at its module position (deferred evaluation is decided by a later milestone) + reasons (first per module; decisive class first) + object-model tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.js:9:1 + R2.1 constructor functions: a write of 'Counter.prototype.add' + all reasons 2 (2 object-model) + boundary 1 native import site: + tests/coverage-fixtures/node-islands/main.ts:7:1 + copy and handle decisions per boundary site come with compiler-managed islands + carries ni-dep \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-node-islands-user-js.txt b/tests/harness/__snapshots__/coverage-node-islands-user-js.txt new file mode 100644 index 000000000..61cb8bb33 --- /dev/null +++ b/tests/harness/__snapshots__/coverage-node-islands-user-js.txt @@ -0,0 +1,34 @@ +scriptc coverage tests/coverage-fixtures/node-islands-user-js.cjs + + statements analyzed 13 + compile statically 10 (76%) + in dynamic tier 10 (operate on values typed only at runtime; --explain-dynamic lists them) + (1 function not analyzed — signature blocked; their statements count as not static) + + deferred to runtime 2 sites (JS statements that throw their fence if executed) + ×1 SC1090 parameterized 'arguments' outside an ES module (sloppy-mode parameter aliases) + at tests/coverage-fixtures/node-islands-user-js.cjs:15:1 + hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary) + ×1 SC2004 uses of 'legacy' inherit the blocker on its declaration + at tests/coverage-fixtures/node-islands-user-js.cjs:22:57 + hint: the declaration of 'legacy' did not compile — fix the diagnostic reported there and these sites clear with it + + blockers: + ×1 SC8101 A write of 'Point.prototype.norm' needs a mutable JavaScript object model, which native code does not have + at tests/coverage-fixtures/node-islands-user-js.cjs:7:1 + hint: rewrite it as a class or typed object, or mark this module as a Node island in scriptc.nodeIslands.modules + ×1 SC8101 'new Point()' of a function needs a mutable JavaScript object model, which native code does not have + at tests/coverage-fixtures/node-islands-user-js.cjs:22:13 + hint: rewrite it as a class or typed object, or mark this module as a Node island in scriptc.nodeIslands.modules + ×1 SC8102 'Object.setPrototypeOf()' needs a mutable JavaScript object model, which native code does not have + at tests/coverage-fixtures/node-islands-user-js.cjs:12:1 + hint: rewrite it as a class or typed object, or mark this module as a Node island in scriptc.nodeIslands.modules + ×1 SC8103 'Object.defineProperty()' needs a mutable JavaScript object model, which native code does not have + at tests/coverage-fixtures/node-islands-user-js.cjs:14:1 + hint: rewrite it as a class or typed object, or mark this module as a Node island in scriptc.nodeIslands.modules + ×1 SC8104 A write to 'arguments[i]' in sloppy mode needs a mutable JavaScript object model, which native code does not have + at tests/coverage-fixtures/node-islands-user-js.cjs:16:3 + hint: rewrite it as a class or typed object, or mark this module as a Node island in scriptc.nodeIslands.modules + ×1 SC8105 A write of 'Array.prototype.last' needs a mutable JavaScript object model, which native code does not have + at tests/coverage-fixtures/node-islands-user-js.cjs:19:1 + hint: rewrite it as a class or typed object, or mark this module as a Node island in scriptc.nodeIslands.modules \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-node-islands.txt b/tests/harness/__snapshots__/coverage-node-islands.txt new file mode 100644 index 000000000..e8876bb0d --- /dev/null +++ b/tests/harness/__snapshots__/coverage-node-islands.txt @@ -0,0 +1,39 @@ +scriptc coverage tests/coverage-fixtures/node-islands/main.ts + + statements analyzed 29 + compile statically 27 (93%) + in dynamic tier 17 (operate on values typed only at runtime; --explain-dynamic lists them) + + npm package compilation: + ni-minified refused (its shipped JS looks minified) + ni-gap static + ni-eval static + ni-ctor static + ni-typed static + ni-dep static + + Node islands (would-be, report-only: the build is unchanged; --node-islands=auto) + gap ni-gap@1.0.0 + tests/coverage-fixtures/node-islands/node_modules/ni-gap/index.js:7:17: SC1090: a builtin function alias as an escaping value (call 'ext' directly) is not supported yet + runtime ni-eval@1.0.0 + tests/coverage-fixtures/node-islands/node_modules/ni-eval/index.js:4:10: R1.1 code from strings: new Function() + object-model ni-ctor@1.0.0 + tests/coverage-fixtures/node-islands/node_modules/ni-ctor/index.js:9:1: R2.1 constructor functions: a write of 'Counter.prototype.add' + +1 more reason in 1 module; carries ni-dep + packaging ni-minified@1.0.0 + SC2013: its shipped JS looks minified + reached from native code and from an island: ni-typed + hint: scriptc coverage --explain-island= lists every reason + + deferred to runtime 1 site (JS statements that throw their fence if executed) + ×1 SC1090 a builtin function alias as an escaping value (call 'ext' directly) + at tests/coverage-fixtures/node-islands/node_modules/ni-gap/index.js:7:17 + hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary) + + blockers: + ×2 SC2013 cannot compile npm package 'ni-minified' statically: its implementation is not part of the compiled module graph + at tests/coverage-fixtures/node-islands/main.ts:8:1, tests/coverage-fixtures/node-islands/main.ts:16:13 + hint: use a dependency with supported source, or replace the unsupported operation; scriptc has no interpreter fallback + ×1 SC2013 cannot compile npm package 'ni-minified' statically: its shipped JS looks minified + at tests/coverage-fixtures/node-islands/main.ts:8:1 + hint: to compile it anyway, name it with --npm-static=ni-minified (or --npm-static=ni-minified... to include its dependencies) \ No newline at end of file diff --git a/tests/harness/__snapshots__/node-islands-config/main.ts.txt b/tests/harness/__snapshots__/node-islands-config/main.ts.txt new file mode 100644 index 000000000..03cc714aa --- /dev/null +++ b/tests/harness/__snapshots__/node-islands-config/main.ts.txt @@ -0,0 +1,35 @@ +node-islands-config/package.json:6:7 - error SC8004: package.json 'scriptc.packages.encoding' has the unknown value "unloded" + + 5 | "packages": { + 6 | "encoding": "unloded", + | ^ + 7 | "debug": { "run": "island" } + + hint: use one of "unloaded", "node", "native", "auto", or an object with run, evaluate, share, marshal, schedule + +node-islands-config/package.json:7:18 - error SC8004: package.json 'scriptc.packages.debug.run' has the unknown value "island" + + 6 | "encoding": "unloded", + 7 | "debug": { "run": "island" } + | ^ + 8 | }, + + hint: use one of "native", "node", "auto" + +node-islands-config/package.json:10:7 - error SC8004: package.json 'scriptc.nodeIslands.auto' must be true or false, not "yes" + + 9 | "nodeIslands": { + 10 | "auto": "yes", + | ^ + 11 | "mode": "auto" + + hint: use a boolean + +node-islands-config/package.json:11:7 - error SC8004: package.json 'scriptc.nodeIslands.mode' is not a known Node island setting + + 10 | "auto": "yes", + 11 | "mode": "auto" + | ^ + 12 | } + + hint: the settings are auto, modules, node, nodeVersion, packaging, speculativeStart \ No newline at end of file diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index cc91c0922..954294821 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -9,6 +9,7 @@ import { coveragePasses, renderCoverage, renderExplainDynamic, + renderExplainIsland, } from "@scriptc/compiler"; import { shardSelect, shardSuffix } from "./shard.js"; @@ -704,6 +705,92 @@ test("a cycle whose top level calls a builtin with a builtin function value is a expect(out).toContain("statements analyzed"); }); +test("Node islands (report-only): one package per class, gap islands first", async () => { + // ni-gap (a fence inside the native subset), ni-eval (new Function), + // ni-ctor (a constructor function, carrying ni-dep), ni-minified (not + // compilable as shipped), ni-typed (native, also reached from ni-ctor). + const entry = fixture("node-islands/main.ts"); + await expect(report(entry)).toMatchFileSnapshot("__snapshots__/coverage-node-islands.txt"); + const { coverage, sourceTexts } = analyze(entry); + const islands = coverage.nodeIslands!; + expect(islands.islands.map((i) => [i.package, i.class])).toEqual([ + ["ni-gap", "gap"], + ["ni-eval", "runtime"], + ["ni-ctor", "object-model"], + ["ni-minified", "packaging"], + ]); + expect(islands.packages.map((p) => [p.package, p.status, p.islands])).toEqual([ + ["ni-dep", "inside", ["ni-ctor"]], + ["ni-typed", "shared", ["ni-ctor"]], + ]); + await expect( + renderExplainIsland(coverage.nodeIslands, "ni-ctor", { sourceTexts, root: repoRoot }), + ).toMatchFileSnapshot("__snapshots__/coverage-node-islands-explain.txt"); + expect(renderExplainIsland(coverage.nodeIslands, "ni-dep", { root: repoRoot })).toContain( + "only ni-ctor imports it", + ); + // The machine-readable form carries the same decisions. + const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts }); + expect(envelope.nodeIslands?.mode).toBe("would-be"); + expect(envelope.nodeIslands?.islands.map((i) => [i.package, i.class, i.at?.line])).toEqual([ + ["ni-gap", "gap", 7], + ["ni-eval", "runtime", 4], + ["ni-ctor", "object-model", 9], + ["ni-minified", "packaging", undefined], + ]); + // The gates: the blocker gate is today's build (which refuses + // ni-minified with SC2013, and nothing else); the island gates fail on + // the would-be islands. + expect(coverage.diagnostics.every((d) => d.code === "SC2013")).toBe(true); + expect(coveragePasses(coverage, "island-gaps")).toBe(false); + expect(coveragePasses(coverage, "node-islands")).toBe(false); +}); + +test("Node island policy from the command line: native, forced, off", () => { + const entry = fixture("node-islands/main.ts"); + const forced = analyze(entry, { + nodeIslands: { native: ["ni-eval"], islands: ["ni-typed", "not-a-dependency"] }, + }).coverage.nodeIslands!; + expect(forced.islands.map((i) => [i.package, i.class, i.decision])).toEqual([ + ["ni-gap", "gap", "auto"], + ["ni-ctor", "object-model", "auto"], + ["ni-minified", "packaging", "auto"], + ["ni-typed", "marked", "cli"], + ]); + expect(forced.violations.map((v) => v.code)).toEqual(["SC8003"]); + expect(forced.unknownPackages).toEqual(["not-a-dependency"]); + const off = analyze(entry, { nodeIslands: { mode: "off" } }).coverage; + expect(off.nodeIslands!.islands).toEqual([]); + expect(off.nodeIslands!.violations.map((v) => v.code)).toEqual([ + "SC8002", + "SC8002", + "SC8002", + "SC8002", + ]); + expect(coveragePasses(off, "island-gaps")).toBe(false); +}); + +test("--strict-user-js reports R2 constructs in user JavaScript as SC8101–SC8105", async () => { + const entry = fixture("node-islands-user-js.cjs"); + // Without the flag the user module is not part of any island report. + expect(analyze(entry).coverage.nodeIslands).toBeUndefined(); + const { coverage } = analyze(entry, { nodeIslands: { strictUserJs: true } }); + expect(coverage.diagnostics.map((d) => d.code).filter((c) => c.startsWith("SC81"))).toEqual([ + "SC8101", + "SC8102", + "SC8103", + "SC8104", + "SC8105", + "SC8101", + ]); + await expect( + renderCoverage( + { ...coverage, file: coverage.file.replace(repoRoot + "/", "") }, + { root: repoRoot }, + ), + ).toMatchFileSnapshot("__snapshots__/coverage-node-islands-user-js.txt"); +}); + test(`every corpus program is 100% static (corpus and coverage agree${shardSuffix()})`, async () => { // The differential corpus compiles by definition; coverage must agree. From fa98044a5cae785c92374ff0413eeb76c1257539 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 06:11:52 -0500 Subject: [PATCH 002/409] Run completion callbacks in their call's AsyncLocalStorage context, and add bind, snapshot and store options Node runs the callback of an operation that a callback-style API starts with the AsyncLocalStorage stores active when the call started it. The runtime invokes these callbacks from the event loop, where the context is empty, so getStore() answered undefined in fs, crypto, zlib, dns, child_process, net, http and dgram callbacks. In programs whose scheduling carries contexts (RuntimeFeatures asyncContext), the lowering now binds each completion-callback argument to the context active at the call (completion-callbacks.ts): typed closures through als.bindCallback, whose bound closure enters through a thunk the backend emits once per callback signature, and dyn functions through als.bindCallbackDyn. The fs callback family binds in the runtime, where scr_fs_cb_schedule queues the callback. Other programs compile as before. AsyncLocalStorage.bind(fn) binds fn to the active context, also when it is empty (a typed function keeps its type; in JavaScript files the bound function is named "bound" and forwards its receiver, as in Node). AsyncLocalStorage.snapshot() answers a function that runs its callback in the snapshot's context. new AsyncLocalStorage({ defaultValue, name }) validates its options as Node does; getStore() answers the default value where the store has no entry, exit() runs its callback with the store undefined, and the name property answers the name option or "". Event listeners on sockets, requests, responses and streams, stream callbacks, server.close(callback) and https and tls callbacks still run with no store; the limitations page lists them. Corpus 7110-7112. --- .../builtin-apis/asynclocalstorage.mdx | 4 +- .../generated/node-v24-internal.json | 43 +++++- internal/compatibility/static-support.json | 2 +- .../ambient/scriptc-node-fallback.d.ts | 13 +- .../compiler/src/backend/llvm/lib-process.ts | 52 ++++++- .../compiler/src/backend/llvm/lib-shared.ts | 4 + .../lowering/builtins/async-context.ts | 79 +++++++++- .../frontend/lowering/completion-callbacks.ts | 130 +++++++++++++++++ .../lowering/expressions/property-access.ts | 3 + .../src/frontend/lowering/lower-classes.ts | 23 +-- .../compiler/src/frontend/lowering/lowerer.ts | 2 + packages/compiler/src/frontend/type-mapper.ts | 23 +++ packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 14 ++ packages/compiler/src/ir/runtime-features.ts | 4 + packages/compiler/src/ir/validate.ts | 7 + .../test/completion-callbacks.test.ts | 60 ++++++++ .../7110-als-completion-callbacks.ts.json | 6 + .../7111-als-bind-snapshot-options.ts.json | 6 + .../7112-als-bind-snapshot-dynamic.mjs.json | 6 + packages/runtime/src/scr_async.c | 41 ++++++ packages/runtime/src/scr_async.h | 15 ++ packages/runtime/src/scr_async_dyn.c | 138 +++++++++++++++++- tests/corpus/7110-als-completion-callbacks.ts | 135 +++++++++++++++++ .../corpus/7111-als-bind-snapshot-options.ts | 59 ++++++++ .../corpus/7112-als-bind-snapshot-dynamic.mjs | 51 +++++++ .../7110-als-completion-callbacks.ts.json | 1 + .../7111-als-bind-snapshot-options.ts.json | 1 + .../7112-als-bind-snapshot-dynamic.mjs.json | 1 + 29 files changed, 898 insertions(+), 27 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/completion-callbacks.ts create mode 100644 packages/compiler/test/completion-callbacks.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json create mode 100644 tests/corpus/7110-als-completion-callbacks.ts create mode 100644 tests/corpus/7111-als-bind-snapshot-options.ts create mode 100644 tests/corpus/7112-als-bind-snapshot-dynamic.mjs create mode 100644 tests/harness/corpus-costs/7110-als-completion-callbacks.ts.json create mode 100644 tests/harness/corpus-costs/7111-als-bind-snapshot-options.ts.json create mode 100644 tests/harness/corpus-costs/7112-als-bind-snapshot-dynamic.mjs.json diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx index 341994faa..99f89d65c 100644 --- a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -1,2 +1,4 @@ - **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node. - Completion callbacks of callback-style I/O APIs, such as `fs.readFile` and `crypto.randomBytes`, run with no store, where Node runs them with the stores that were active when the operation started. + The completion callbacks of `fs`, `crypto` and `zlib` callback APIs, `dns.lookup`, `child_process.execFile`, `server.listen`, `net.connect`, `http.request`, `http.get` and `dgram` run with the stores that were active at the call, as in Node. + Event listeners on sockets, requests, responses and streams, stream callbacks such as `write(chunk, callback)`, `server.close(callback)`, and the callbacks of `https` and `tls` calls run with no store, where Node runs them in the context of the resource or of the code that triggers them. + `AsyncResource` has no lowering yet. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index d40bb7bfc..34fe2a843 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1387,7 +1387,10 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "chapter" @@ -1437,7 +1440,10 @@ "evidence": "compiler-dedicated:AsyncLocalStorage", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1510,7 +1516,10 @@ "evidence": "compiler-dedicated:asyncLocalStorage.disable", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1537,7 +1546,10 @@ "evidence": "compiler-dedicated:asyncLocalStorage.getStore", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1564,7 +1576,10 @@ "evidence": "compiler-dedicated:asyncLocalStorage.enterWith", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1591,7 +1606,10 @@ "evidence": "compiler-dedicated:asyncLocalStorage.run", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1618,7 +1636,10 @@ "evidence": "compiler-dedicated:asyncLocalStorage.exit", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" @@ -1922,6 +1943,9 @@ "tests": [ "tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/harness/node-test.test.ts" ] }, @@ -2484,7 +2508,10 @@ "evidence": "compiler-dedicated:AsyncLocalStorage", "tests": [ "tests/corpus/2213-async-local-storage.cjs", - "tests/corpus/5815-als-callback-context.ts" + "tests/corpus/5815-als-callback-context.ts", + "tests/corpus/7110-als-completion-callbacks.ts", + "tests/corpus/7111-als-bind-snapshot-options.ts", + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 6fc2c563c..efc60f263 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -429,7 +429,7 @@ { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index b3d2954a8..ac30fde4a 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -6887,9 +6887,18 @@ declare module "node:tty" { declare module "async_hooks" { /* AsyncLocalStorage: fiber-carried context snapshots — run/getStore/ * exit/enterWith/disable lower onto the runtime's active-slot machinery - * (als.* libCalls); values cross as dyn values. */ + * (als.* libCalls); values cross as dyn values. bind and snapshot bind + * functions to the active context; the options carry the store's + * default value and name. */ + export interface AsyncLocalStorageOptions { + defaultValue?: any; + name?: string | undefined; + } export class AsyncLocalStorage { - constructor(); + constructor(options?: AsyncLocalStorageOptions); + static bind any>(fn: Func): Func; + static snapshot(): (fn: (...args: TArgs) => R, ...args: TArgs) => R; + readonly name: string; run(store: T, callback: (...args: any[]) => R, ...args: any[]): R; exit(callback: (...args: any[]) => R, ...args: any[]): R; getStore(): T | undefined; diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 3e8a9bff5..1a3973b07 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -1,10 +1,11 @@ /* Focused LLVM library-call emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; import { undefinedArmTag } from "../../ir/analysis.js"; -import { F64, RUNTIME_ERROR_CLASSES, STRING } from "../../ir/ir.js"; +import { F64, type IrType, RUNTIME_ERROR_CLASSES, STRING, typeKey } from "../../ir/ir.js"; import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js"; import { f64Lit } from "./common.js"; import { mockTimerSym } from "./lib-shared.js"; +import { FN_ATTRS } from "./shapes.js"; export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; @@ -460,9 +461,58 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr for (const a of e.args) host.emitExpr(a); return { name: "", type: e.type }; } + if (e.fn === "als.bindCallback") { + // The closure moves into the bound closure, whose entry is the + // per-signature thunk; with nothing to bind the runtime answers the + // closure itself. + if (e.type.kind !== "func") + throw new InternalCompilerError("llvm emitter bug: als.bindCallback on non-func"); + const cb = host.emitExpr(e.args[0]!); + const always = host.emitExpr(e.args[1]!); + host.toHeap(cb); + const thunk = boundCallbackThunk(host, e.type); + host.declare("declare ptr @scr_als_bind_closure(ptr, ptr, i1 zeroext)"); + const t = B.tmp(); + B.line( + `${t} = call ptr @scr_als_bind_closure(ptr ${cb.name}, ptr @${thunk}, i1 ${always.name})`, + ); + return host.own({ name: t, type: e.type }); + } return host.emitGenericLibCall(e); } +/** The entry of a callback bound to an AsyncLocalStorage context, one per + * callback signature: enters the captured context, calls the target with + * the same arguments (the callValue ABI) and restores the caller's + * context, also when the target leaves an exception pending. */ +function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "func" }): string { + const key = `alsbound:${typeKey(cbT)}`; + let sym = host.resolveThunks.get(key); + if (sym) return sym; + sym = `sc_alsbound_${host.resolveThunks.size}`; + host.resolveThunks.set(key, sym); + host.declare("declare ptr @scr_als_bound_target(ptr)"); + host.declare("declare ptr @scr_als_bound_enter(ptr)"); + host.declare("declare void @scr_als_bound_leave(ptr, ptr)"); + const ret = host.llType(cbT.ret); + const params = cbT.params.map((p, i) => `${host.llType(p)} %a${i}`); + const call = `call ${ret} %fn(${["ptr %cb", ...params].join(", ")})`; + host.resolveThunkDefs.push( + `define internal ${ret} @${sym}(${["ptr %self", ...params].join(", ")}) ${FN_ATTRS} { ; AsyncLocalStorage-bound callback ${typeKey(cbT)}`, + `entry:`, + ` %cb = call ptr @scr_als_bound_target(ptr %self)`, + ` %prev = call ptr @scr_als_bound_enter(ptr %self)`, + ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ret === "void" ? ` ${call}` : ` %r = ${call}`, + ` call void @scr_als_bound_leave(ptr %prev, ptr %cb)`, + ret === "void" ? ` ret void` : ` ret ${ret} %r`, + `}`, + ``, + ); + return sym; +} + export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { const B = host.B; if (e.fn === "process.forkTarget") { diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 53c715108..5ed131567 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1438,6 +1438,10 @@ export const LIB_FN_SYMS: Record = { "als.exitRun": "scr_als_exit_run", "als.enterWith": "scr_als_enter_with", "als.disable": "scr_als_disable", + "als.newOptions": "scr_als_new_options", + "als.name": "scr_als_name", + "als.bindCallbackDyn": "scr_als_bind_dyn", + "als.snapshot": "scr_als_snapshot", "dc.chanBindStore": "scr_dc_chan_bind_store", "dc.chanUnbindStore": "scr_dc_chan_unbind_store", "dc.chanRunStores": "scr_dc_chan_run_stores", diff --git a/packages/compiler/src/frontend/lowering/builtins/async-context.ts b/packages/compiler/src/frontend/lowering/builtins/async-context.ts index 18bd93a86..a130df59b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts +++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts @@ -2,7 +2,7 @@ import { isBuiltinClassInstance } from "./receiver-types.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { locOf } from "../../program.js"; +import { isJsSourceFile, locOf } from "../../program.js"; import { BOOL, DYN, @@ -184,16 +184,89 @@ export function lowerDiagnosticsChannelMethodCall( ); } +/** Whether `node` names the AsyncLocalStorage class itself. */ +function isAsyncLocalStorageClass(lowerer: Lowerer, node: ts.Expression): boolean { + if (!ts.isIdentifier(node)) return false; + const symbol = lowerer.resolveValueSymbol(node); + return ( + symbol !== null && + lowerer.isStdlibSymbol(symbol) && + ts.symbolName(symbol) === "AsyncLocalStorage" + ); +} + +/** The static AsyncLocalStorage members: bind(fn) answers fn bound to the + * active context (its own type for a typed function; a dyn function in + * JavaScript files, where the bound function's name and receiver are + * observable, and for dyn values); snapshot() answers a dyn function + * (cb, ...args) that runs cb in the context active at the snapshot. */ +function lowerAsyncLocalStorageStaticCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr { + const name = access.name.text; + const loc = locOf(call); + const always: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc }; + if (name === "bind" && call.arguments.nodes.length === 1) { + const arg = call.arguments.nodes[0]!; + if (!isJsSourceFile(ts.getSourceFileOfNode(call))) { + const fn = lowerer.lowerExpr(arg); + if (fn.type.kind === "func") + return { kind: "libCall", fn: "als.bindCallback", args: [fn, always], type: fn.type, loc }; + } + return asyncLocalStorageResult(lowerer, call, { + kind: "libCall", + fn: "als.bindCallbackDyn", + args: [lowerDiagnosticsMessage(lowerer, arg), always], + type: DYN, + loc, + }); + } + if (name === "snapshot" && call.arguments.nodes.length === 0) + return { kind: "libCall", fn: "als.snapshot", args: [], type: DYN, loc }; + lowerer.noLowering( + `AsyncLocalStorage.${name}`, + call, + "AsyncLocalStorage.bind(fn) and AsyncLocalStorage.snapshot() are the supported static members", + lowerer.checker.getSymbolAtLocation(access.name), + ); +} + +/** new AsyncLocalStorage(options): the options object as a dyn value + * (plain property literals build it member by member). */ +export function lowerAsyncLocalStorageOptions(lowerer: Lowerer, node: ts.Expression): IrExpr { + return lowerTracingHandlers(lowerer, node); +} + +/** `.name` on AsyncLocalStorage receivers: the constructor's name option + * (als.name), converted to the property's type. Null otherwise. */ +export function lowerAsyncLocalStorageProperty( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.questionDotToken || access.name.text !== "name") return null; + if (!isBuiltinClassInstance(lowerer, access.expression, "AsyncLocalStorage")) return null; + if (!lowerer.isStdlibMember(access)) return null; + const loc = locOf(access); + const receiver = lowerer.lowerExprExpecting(access.expression, F64); + const value: IrExpr = { kind: "libCall", fn: "als.name", args: [receiver], type: DYN, loc }; + const expected = lowerer.mapTypeOf(lowerer.typeOf(access)); + return !expected || expected.kind === "dyn" ? value : lowerer.coerceInto(access, value, expected); +} + /** Method calls on AsyncLocalStorage receivers: run(store, fn, ...args), * exit(fn, ...args), getStore(), enterWith(store), disable() — over the - * f64 store handle (als.* libCalls; values cross as dyn values). Null - * for non-ALS receivers. */ + * f64 store handle (als.* libCalls; values cross as dyn values) — and the + * static bind/snapshot members. Null for other receivers. */ export function lowerAsyncLocalStorageMethodCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; + if (isAsyncLocalStorageClass(lowerer, access.expression)) + return lowerAsyncLocalStorageStaticCall(lowerer, call, access); if (!isBuiltinClassInstance(lowerer, access.expression, "AsyncLocalStorage")) return null; if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; diff --git a/packages/compiler/src/frontend/lowering/completion-callbacks.ts b/packages/compiler/src/frontend/lowering/completion-callbacks.ts new file mode 100644 index 000000000..402741da6 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/completion-callbacks.ts @@ -0,0 +1,130 @@ +/* Completion callbacks in programs that use AsyncLocalStorage. Node runs + * the callback of an operation a callback-style API starts (fs.readFile, + * crypto.randomBytes, zlib.gzip, dns.lookup, server.listen, net.connect, + * http.get, ...) with the stores active when the call started it. The runtime invokes these callbacks from + * the event loop, where the context is empty, so in programs whose + * scheduling carries contexts (RuntimeFeatures asyncContext) each such + * argument is bound to the active context at the call: als.bindCallback + * for typed closures, als.bindCallbackDyn for dyn functions. Other + * programs are left unchanged. */ +import { BOOL, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js"; +import type { IrLibFn } from "../../ir/builtin-signatures.js"; +import { mapExprChildren, mapStmtChildren } from "../../ir/traverse.js"; +import { moduleRuntimeFeatures } from "../../ir/runtime-features.js"; + +/** The argument that is the completion callback: its index; "last" for + * calls whose callback is the final argument when one is passed; or + * "arguments" for the fs callback calls, whose argument is the call's + * dyn argument vector, where the runtime finds the callback. */ +type CallbackPosition = number | "last" | "arguments"; + +const COMPLETION_CALLBACKS: ReadonlyMap = new Map< + IrLibFn, + CallbackPosition +>([ + ["cp.execFile", 2], + ["crypto.generateKeyCb", 2], + ["crypto.hkdfCb", 5], + ["crypto.pbkdf2Cb", 5], + ["crypto.randomBytesCb", 1], + ["crypto.randomFillCb", 4], + ["crypto.randomFillCbDyn", 4], + ["crypto.randomIntCb", 3], + ["crypto.scryptCb", 4], + ["dgram.bindCb", 3], + ["dgram.closeCb", 1], + ["dgram.connectCb", 3], + ["dns.lookupDyn", 2], + ["fs.callbackCall", "arguments"], + ["fs.callbackGlob", "arguments"], + ["fs.renameCb", 2], + ["http.requestAgentCb", "last"], + ["http.requestCb", "last"], + ["http.requestConnCb", "last"], + ["http.requestDynCb", "last"], + ["http.requestUrlCb", "last"], + ["net.connectCb", 2], + ["net.listenCb", 2], + ["net.listenOptsCb", 4], + ["net.listenOptsReusePortCb", 5], + ["net.listenPathCb", 2], + ["process.stderrWriteBytesCb", 2], + ["process.stdoutWriteBytesCb", 2], + ["zlib.cbOpts", 3], + ["zlib.cbOptsDyn", 3], + ["zlib.deflateCb", 1], + ["zlib.deflateRawCb", 1], + ["zlib.gunzipCb", 1], + ["zlib.gzipCb", 1], + ["zlib.inflateCb", 1], + ["zlib.inflateRawCb", 1], + ["zlib.unzipCb", 1], +]); + +/** `cb` bound to the context active where it is evaluated. An empty + * context leaves it unbound (`always` false): the loop already runs + * callbacks with an empty context. */ +function bindToActiveContext(cb: IrExpr): IrExpr { + const always: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc: cb.loc }; + if (cb.type.kind === "func") + return { + kind: "libCall", + fn: "als.bindCallback", + args: [cb, always], + type: cb.type, + loc: cb.loc, + }; + if (cb.type.kind === "dyn") + return { + kind: "libCall", + fn: "als.bindCallbackDyn", + args: [cb, always], + type: cb.type, + loc: cb.loc, + }; + return cb; +} + +/** An element of a dyn argument vector that can be the callback: a boxed + * closure binds as a typed closure; any other dyn value that is not a + * boxed primitive binds as a dyn function (non-functions pass through). */ +function bindArgumentElement(e: IrExpr): IrExpr { + if (e.kind === "dynFrom") { + return e.value.type.kind === "func" ? { ...e, value: bindToActiveContext(e.value) } : e; + } + return e.type.kind === "dyn" ? bindToActiveContext(e) : e; +} + +function bindCall(e: IrExpr): IrExpr { + if (e.kind !== "libCall") return e; + const position = COMPLETION_CALLBACKS.get(e.fn); + if (position === undefined) return e; + if (position === "arguments") { + const index = e.fn === "fs.callbackCall" ? 1 : 0; + const vector = e.args[index]; + if (vector === undefined || vector.kind !== "dynArrLit") return e; + const args = e.args.slice(); + args[index] = { + kind: "dynArrLit", + elems: vector.elems.map(bindArgumentElement), + type: vector.type, + loc: vector.loc, + }; + return { ...e, args }; + } + const index = position === "last" ? e.args.length - 1 : position; + const cb = e.args[index]; + if (cb === undefined || (position === "last" && cb.type.kind !== "func")) return e; + const args = e.args.slice(); + args[index] = bindToActiveContext(cb); + return { ...e, args }; +} + +/** Binds the completion callbacks of `mod` when its scheduling carries + * AsyncLocalStorage contexts. */ +export function bindCompletionCallbacks(mod: IrModule): void { + if (!moduleRuntimeFeatures(mod).asyncContext) return; + const rewriteExpr = (e: IrExpr): IrExpr => bindCall(mapExprChildren(e, rewriteExpr, rewriteStmt)); + const rewriteStmt = (s: IrStmt): IrStmt => mapStmtChildren(s, rewriteExpr, rewriteStmt); + for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7f3910c15..0777e8fb9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -134,6 +134,7 @@ import { import { expandoMemberRead } from "../lower-expando.js"; import { lowerProtocolPropertyRead } from "../class-protocol-dispatch.js"; import { lowerReadlineProperty } from "../builtins/readline.js"; +import { lowerAsyncLocalStorageProperty } from "../builtins/async-context.js"; import { lowerEventStaticProperty } from "../builtins/numbers.js"; import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "../lower-stream.js"; import { lowerValueStreamProperty } from "../builtins/hash-stream.js"; @@ -814,6 +815,8 @@ export function lowerPropertyAccessExpr( // diagnostics_channel Channel receivers — name/hasSubscribers // over the f64 channel handle. lowerer.lowerDiagnosticsChannelProperty(expr) ?? + // AsyncLocalStorage receivers — the name option. + lowerAsyncLocalStorageProperty(lowerer, expr) ?? // readline Interface receivers — the history and option members // over the f64 interface handle. lowerReadlineProperty(lowerer, expr) ?? diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..14371f46b 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,3 +1,4 @@ +import { lowerAsyncLocalStorageOptions } from "./builtins/async-context.js"; import { DATE_TIME_FIELD, dateRootOf, @@ -10471,21 +10472,23 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { lowerer.badType(expr, tsType); } // `new AsyncLocalStorage()` (node:async_hooks): a fresh store id — - // an f64 handle (type-mapper.ts), the Channel story. Construction options - // ({ defaultValue, name }) have no lowering yet. + // an f64 handle (type-mapper.ts), the Channel story. The options form + // ({ defaultValue, name }) passes the options object as a dyn value. if ( symbol !== null && ts.symbolName(symbol) === "AsyncLocalStorage" && lowerer.isStdlibSymbol(symbol) ) { - if ((expr.arguments?.nodes.length ?? 0) > 0) { - lowerer.noLowering( - "new AsyncLocalStorage(options)", - expr, - "the zero-argument constructor is the supported form (defaultValue/name options have no lowering yet)", - ); - } - return { kind: "libCall", fn: "als.new", args: [], type: F64, loc }; + const options = expr.arguments?.nodes[0]; + if (options === undefined) + return { kind: "libCall", fn: "als.new", args: [], type: F64, loc }; + return { + kind: "libCall", + fn: "als.newOptions", + args: [lowerAsyncLocalStorageOptions(lowerer, options)], + type: F64, + loc, + }; } // `new Promise((resolve) => ...)`: the ambient Promise constructor. if (symbol !== null && ts.symbolName(symbol) === "Promise" && lowerer.isStdlibSymbol(symbol)) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index edf0a694b..fd2f44d73 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -29,6 +29,7 @@ import { refuseUncheckedCycleReads, type CycleInitFlag, } from "./cycle-initialization.js"; +import { bindCompletionCallbacks } from "./completion-callbacks.js"; import { isJsonRequireDecl, type JsonRequireModule } from "./json-require.js"; import { checkRequireBindingAccesses, @@ -7678,6 +7679,7 @@ export class Lowerer { }; if (module) sanitizeUnregisteredClassTypes(module, (name) => this.classes.has(name)); if (module) checkCycleBindingAccesses(this, module); + if (module) bindCompletionCallbacks(module); // Execution tiers from the final bodies (ir/tiers.ts). Assigned with or // without a module: coverage reports the dynamic tier of programs that // still have blockers. diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index e6c5c4c93..535c2b584 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -4674,6 +4674,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (hybrid) return hybrid; } const callSigs = checker.getCallSignatures(widened); + // AsyncLocalStorage.snapshot()'s generic runner is a dyn function value + // (als.snapshot), called through the dyn call path. + if (callSigs.length === 1 && isAsyncLocalStorageSnapshot(callSigs[0]!, ctx)) return DYN; if (callSigs.length === 1) return mapCallSignature(callSigs[0]!, ctx); // Records: object types whose members are all data properties (shorthand // methods in type position count — they're func-typed fields) with @@ -5004,6 +5007,26 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { /** One non-overloaded call signature's func type (mapTypeInner's * single-signature rule; erasedOverloadMemberType maps each overload of a * member through it). */ +/** Whether `sig` is the function type AsyncLocalStorage.snapshot() returns: + * declared as that static method's return type in the async_hooks module + * of the standard library declarations. */ +function isAsyncLocalStorageSnapshot(sig: ts.Signature, ctx: TypeMapperCtx): boolean { + const decl = sig.declaration; + if (decl === undefined || !ts.isFunctionTypeNode(decl)) return false; + const method = decl.parent; + return ( + method !== undefined && + ts.isMethodDeclaration(method) && + ts.isIdentifier(method.name) && + method.name.text === "snapshot" && + method.parent !== undefined && + ts.isClassDeclaration(method.parent) && + method.parent.name?.text === "AsyncLocalStorage" && + ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) && + isDeclaredInAmbientModule(decl, "async_hooks") + ); +} + function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null { const { checker } = ctx; if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index c202b8538..34a0a2b14 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -194,6 +194,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "async.awaitDyn", "als.run", "als.exitRun", + "als.newOptions", + "als.bindCallbackDyn", "dc.chanBindStore", "dc.chanRunStores", "http.resWriteHeadDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f37e8809f..f426cb6c7 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4592,6 +4592,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "als.exitRun": { argTypes: [F64, DYN, DYN], result: DYN }, "als.enterWith": { argTypes: [F64, DYN], result: VOID }, "als.disable": { argTypes: [F64], result: VOID }, + /** new AsyncLocalStorage(options) (Node's object check throws; MAY + * THROW) and the store's name option (dyn; "" without one). */ + "als.newOptions": { argTypes: [DYN], result: F64 }, + "als.name": { argTypes: [F64], result: DYN }, + /** A function bound to the active context: the completion callbacks of + * callback-style APIs (always false: an empty context leaves the + * callback unbound) and AsyncLocalStorage.bind (always true). The typed + * form answers its closure argument's type; the dyn form throws Node's + * ERR_INVALID_ARG_TYPE for a non-function when always (MAY THROW). */ + "als.bindCallback": { argTypes: [null, BOOL], result: VOID }, + "als.bindCallbackDyn": { argTypes: [DYN, BOOL], result: DYN }, + /** AsyncLocalStorage.snapshot(): a dyn function (cb, ...args) that runs + * cb in the context active at the snapshot. */ + "als.snapshot": { argTypes: [], result: DYN }, /** Channel.bindStore/unbindStore/runStores (scr_dc.c): the * AsyncLocalStorage integration — set-semantics bindings per store, * runStores entering every bound store with transform(data) around the diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 5c30b0f69..05ebe2496 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -115,12 +115,16 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "perf.measure", // Blob and File (scr_blob.c, linked with this unit, which boxes their // reads as dyn promises). + "als.bindCallbackDyn", "als.disable", "als.enterWith", "als.exitRun", "als.get", + "als.name", "als.new", + "als.newOptions", "als.run", + "als.snapshot", "blob.arrayBuffer", "blob.bytes", "blob.fileNew", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bb36c7007..915f7a4af 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5310,6 +5310,13 @@ function validateFunction( } function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean { + if (e.fn === "als.bindCallback") { + // The bound function, at its closure argument's own type. + const fn = e.args[0]?.type; + if (fn?.kind !== "func" || !typeEquals(e.type, fn)) + err(`libCall ${e.fn} must return its function argument's type`, e.loc); + return true; + } if ( e.fn === "crypto.randomFillView" || e.fn === "crypto.randomFillViewRest" || diff --git a/packages/compiler/test/completion-callbacks.test.ts b/packages/compiler/test/completion-callbacks.test.ts new file mode 100644 index 000000000..817472c19 --- /dev/null +++ b/packages/compiler/test/completion-callbacks.test.ts @@ -0,0 +1,60 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-completion-callbacks-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +const randomBytes = ` +import { randomBytes } from "node:crypto"; +randomBytes(4, (error, buffer) => console.log(error === null, buffer.length)); +`; + +test("programs without AsyncLocalStorage pass completion callbacks unchanged", async () => { + const llvm = await emit(randomBytes); + expect(llvm).not.toContain("@scr_als_bind_closure"); + expect(llvm).not.toContain("sc_alsbound_"); +}); + +test("programs that enter stores bind each completion callback to the active context", async () => { + const llvm = await emit(` +import { AsyncLocalStorage } from "node:async_hooks"; +${randomBytes} +const store = new AsyncLocalStorage(); +store.run("a", () => randomBytes(1, (error, buffer) => console.log(store.getStore(), buffer.length))); +`); + const binds = llvm.match( + /call ptr @scr_als_bind_closure\(ptr [^,]+, ptr @sc_alsbound_\d+, i1 false\)/g, + ); + expect(binds?.length).toBe(2); + // One thunk per callback signature. + expect(llvm.match(/^define internal void @sc_alsbound_\d+\(/gm)?.length).toBe(1); +}); + +test("AsyncLocalStorage.bind binds a typed function with an empty context too", async () => { + const llvm = await emit(` +import { AsyncLocalStorage } from "node:async_hooks"; +const store = new AsyncLocalStorage(); +const read = AsyncLocalStorage.bind((n: number) => \`\${store.getStore()} \${n}\`); +console.log(store.run("x", () => read(1))); +`); + expect(llvm).toMatch( + /call ptr @scr_als_bind_closure\(ptr [^,]+, ptr @sc_alsbound_\d+, i1 true\)/, + ); + expect(llvm).toMatch(/^define internal ptr @sc_alsbound_\d+\(ptr %self, double %a0\)/m); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json new file mode 100644 index 000000000..5645ac5df --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7110-als-completion-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7110-als-completion-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json new file mode 100644 index 000000000..27aa14a8d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7111-als-bind-snapshot-options.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7111-als-bind-snapshot-options.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json new file mode 100644 index 000000000..1915cae63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7112-als-bind-snapshot-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 9e3365fbe..992ec3767 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -4012,6 +4012,47 @@ double scr_set_immediate_als(ScrClosure *cb) { return scr_set_immediate(scr_als_ void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(scr_als_bind(cb)); } void scr_queue_microtask_als(ScrClosure *cb) { scr_queue_microtask(scr_als_bind(cb)); } +/* A typed callback bound to the context active when it was bound: the + * completion callbacks of callback-style APIs in programs that use + * AsyncLocalStorage, and AsyncLocalStorage.bind. The bound closure's entry + * is a thunk the compiler emits per callback signature; it reads the + * target (scr_als_bound_target), enters the captured context + * (scr_als_bound_enter), calls the target with its own arguments and + * restores the caller's context (scr_als_bound_leave). With `always` + * false an empty context leaves the callback unbound, since the loop runs + * callbacks with an empty context; AsyncLocalStorage.bind passes true, so + * a bound function called later inside run() still sees no store. */ +ScrClosure *scr_als_bind_closure(ScrClosure *cb /*moves*/, void *thunk, bool always) { + ScrAlsCtx *active = *SCR_ALS_SLOT(); + if (cb == NULL || (active == NULL && !always)) return cb; + ScrClosure *bound = scr_closure_new(thunk, 2); + bound->caps[0] = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(bound->caps[0], cb); + bound->caps[1] = scr_box_new_obj(scr_als_ctx_retain_v, scr_als_ctx_release_v, NULL); + scr_box_set_ref(bound->caps[1], scr_als_ctx_retain(active)); + return bound; +} + +/* The bound target (+1, released by scr_als_bound_leave). */ +ScrClosure *scr_als_bound_target(ScrClosure *self) { + return scr_box_get_ref(self->caps[0]); +} + +/* Enters the captured context; returns the caller's (owned). */ +ScrAlsCtx *scr_als_bound_enter(ScrClosure *self) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_box_get_ref(self->caps[1]); /* the slot owns this reference */ + return prev; +} + +/* Restores the caller's context, replacing whatever the target entered + * (an enterWith inside it), and releases the target. */ +void scr_als_bound_leave(ScrAlsCtx *prev, ScrClosure *target) { + scr_als_restore(prev); + scr_closure_release(target); +} + /* The checked-dynamic argument form (JS files — common.mustCall wrappers * and the suite's invalid-input probes): a non-function throws Node's * ERR_INVALID_ARG_TYPE synchronously; a function value is called with diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index fc0128294..003bf3be8 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -198,6 +198,14 @@ void scr_als_disable(double id); * argument vector, restore (the finally). Result +1 or NULL pending. */ ScrDyn *scr_als_run(double id, ScrDyn *value, ScrDyn *fn, ScrDyn *args); ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args); +/* new AsyncLocalStorage(options) (options borrowed; NaN with the exception + * pending for a non-object) and the name it carries (+1). */ +double scr_als_new_options(const ScrDyn *options); +ScrDyn *scr_als_name(double id); +/* AsyncLocalStorage.bind over a dyn function and the dyn completion + * callbacks (fn borrowed; +1), and AsyncLocalStorage.snapshot() (+1). */ +ScrDyn *scr_als_bind_dyn(ScrDyn *fn, bool always); +ScrDyn *scr_als_snapshot(void); /* process.on/once('unhandledRejection', fn): dyn listeners dispatched * per never-observed rejection at the end of its nextTick/microtask @@ -473,6 +481,13 @@ double scr_set_timeout_handle_als(ScrClosure *cb, double ms); double scr_set_immediate_als(ScrClosure *cb); void scr_next_tick_als(ScrClosure *cb); void scr_queue_microtask_als(ScrClosure *cb); +/* A typed callback bound to the active context (scr_async.c): `thunk` is + * the compiler's per-signature entry, which brackets the call with + * scr_als_bound_target/enter/leave. cb moves in; the result is +1. */ +ScrClosure *scr_als_bind_closure(ScrClosure *cb, void *thunk, bool always); +ScrClosure *scr_als_bound_target(ScrClosure *self); +ScrAlsCtx *scr_als_bound_enter(ScrClosure *self); +void scr_als_bound_leave(ScrAlsCtx *prev, ScrClosure *target); /* A successful process.stdout/stderr.write completion on that same queue. * The callback and its program-shaped Error | null adapter both MOVE into * the entry; the adapter is invoked with NULL (Node's success argument). */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index a6cd813de..c47626557 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -11,6 +11,7 @@ * 'warning' listeners + Node's stderr report). */ #include "scr_runtime.h" +#include #include #include #include @@ -30,6 +31,21 @@ static void scr_ad_oom(void) { static SCR_TL double scr_als_counter = 0; +/* The constructor options of each store, indexed by id - 1: the default + * value getStore() answers where the store has no entry, and the name. + * NULL until a store with options exists. */ +typedef struct { + ScrDyn *default_value; /* NULL: no default */ + ScrDyn *name; /* NULL: "" */ +} ScrAlsOptions; +static SCR_TL ScrAlsOptions *scr_als_options = NULL; +static SCR_TL size_t scr_als_options_len = 0; + +static const ScrAlsOptions *scr_als_options_of(double id) { + size_t i = (size_t)id - 1; + return id >= 1 && i < scr_als_options_len ? &scr_als_options[i] : NULL; +} + /* Main-slot teardown (atexit, LIFO after scr_init's audit registration — * the dc-registry precedent): a top-level enterWith leaves a context in * the main slot at exit; release it before the RC audit counts. */ @@ -37,6 +53,13 @@ static void scr_als_teardown(void) { /* atexit runs on the main context — the active slot IS main's. */ scr_als_ctx_release(*SCR_ALS_SLOT()); *SCR_ALS_SLOT() = NULL; + for (size_t i = 0; i < scr_als_options_len; i++) { + scr_dyn_release(scr_als_options[i].default_value); + scr_dyn_release(scr_als_options[i].name); + } + free(scr_als_options); + scr_als_options = NULL; + scr_als_options_len = 0; } double scr_als_new(void) { @@ -48,6 +71,48 @@ double scr_als_new(void) { return ++scr_als_counter; } +/* new AsyncLocalStorage(options): Node validates that options is an + * object, then keeps options.defaultValue and options.name. NaN with the + * exception pending when options is not an object. */ +double scr_als_new_options(const ScrDyn *options) { + if (options->kind == SCR_DYN_UNDEF) return scr_als_new(); + /* A typed record or class instance reads through its snapshot. */ + ScrDyn *snapshot = options->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(options) : NULL; + if (snapshot) options = snapshot; + if (options->kind != SCR_DYN_OBJ) { + scr_dyn_arg_type_fail("options", "of type object", options); + scr_dyn_release(snapshot); + return NAN; + } + double id = scr_als_new(); + size_t index = (size_t)id - 1; + if (index >= scr_als_options_len) { + size_t len = index + 1; + ScrAlsOptions *grown = realloc(scr_als_options, len * sizeof *grown); + if (!grown) scr_ad_oom(); + for (size_t i = scr_als_options_len; i < len; i++) grown[i] = (ScrAlsOptions){NULL, NULL}; + scr_als_options = grown; + scr_als_options_len = len; + } + ScrDyn *value = scr_dyn_obj_get(options, "defaultValue", 12); + ScrDyn *name = scr_dyn_obj_get(options, "name", 4); + scr_als_options[index].default_value = scr_dyn_retain(value ? value : scr_dyn_undefined()); + scr_als_options[index].name = + name && name->kind != SCR_DYN_UNDEF ? scr_dyn_retain(name) : NULL; + scr_dyn_release(snapshot); + return id; +} + +/* asyncLocalStorage.name (+1): the constructor's name option, or "". */ +ScrDyn *scr_als_name(double id) { + const ScrAlsOptions *options = scr_als_options_of(id); + if (options && options->name) return scr_dyn_retain(options->name); + ScrStr *empty = scr_str_new("", 0); + ScrDyn *result = scr_dyn_new_str(empty); + scr_str_release(empty); + return result; +} + ScrDyn *scr_als_get(double id) { const ScrAlsCtx *c = *SCR_ALS_SLOT(); if (c) { @@ -55,6 +120,8 @@ ScrDyn *scr_als_get(double id) { if (c->entries[i].id == id) return scr_dyn_retain(c->entries[i].value); } } + const ScrAlsOptions *options = scr_als_options_of(id); + if (options && options->default_value) return scr_dyn_retain(options->default_value); return scr_dyn_retain(scr_dyn_undefined()); } @@ -104,8 +171,77 @@ ScrDyn *scr_als_run(double id, ScrDyn *value, ScrDyn *fn, ScrDyn *args) { return scr_als_call_in(scr_als_enter(id, value), fn, args); } +/* exit() runs fn with the store undefined: a store with a default value + * holds an undefined entry for the call (getStore() answers undefined, + * not the default, as in Node); other stores drop their entry. */ ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args) { - return scr_als_call_in(scr_als_enter_absent(id), fn, args); + const ScrAlsOptions *options = scr_als_options_of(id); + ScrAlsCtx *prev = options && options->default_value + ? scr_als_enter(id, scr_dyn_undefined()) + : scr_als_enter_absent(id); + return scr_als_call_in(prev, fn, args); +} + +/* AsyncLocalStorage.bind(fn) and the dyn completion callbacks of + * callback-style APIs: a function that calls fn in the context active + * when it was bound, with its own arguments and receiver. caps[0] holds + * fn, caps[1] the context, as scr_als_bind_closure lays them out. */ +static void *scr_als_ctx_retain_v(void *c) { return scr_als_ctx_retain(c); } +static void scr_als_ctx_release_v(void *c) { scr_als_ctx_release(c); } + +static ScrDyn *scr_als_bound_dyn_call(ScrClosure *self, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = scr_box_get_ref(self->caps[0]); + ScrAlsCtx *prev = scr_als_bound_enter(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, "callback"); + scr_als_restore(prev); + scr_dyn_release(fn); + return result; +} + +static ScrClosure *scr_als_context_closure(void *fn, size_t ncaps) { + ScrClosure *closure = scr_closure_new(fn, ncaps); + closure->caps[ncaps - 1] = scr_box_new_obj(scr_als_ctx_retain_v, scr_als_ctx_release_v, NULL); + scr_box_set_ref(closure->caps[ncaps - 1], scr_als_ctx_retain(*SCR_ALS_SLOT())); + return closure; +} + +/* fn borrowed; +1. With `always` false an empty context answers fn + * itself (the completion-callback form). AsyncLocalStorage.bind throws + * Node's ERR_INVALID_ARG_TYPE for a non-function. */ +ScrDyn *scr_als_bind_dyn(ScrDyn *fn, bool always) { + if (fn->kind != SCR_DYN_FUNC) { + if (always) { + scr_dyn_arg_type_fail("fn", "of type function", fn); + return NULL; + } + return scr_dyn_retain(fn); + } + if (!always && *SCR_ALS_SLOT() == NULL) return scr_dyn_retain(fn); + ScrClosure *closure = scr_als_context_closure(NULL, 2); + closure->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(closure->caps[0], scr_dyn_retain(fn)); + return scr_dyn_new_func(closure, scr_als_bound_dyn_call, fn->v.fn.arity, "(...args)", "bound"); +} + +/* AsyncLocalStorage.snapshot(): a function (cb, ...args) that calls cb + * with args in the context active at the snapshot. */ +static ScrDyn *scr_als_snapshot_call(ScrClosure *self, ScrDyn *const *args, size_t argc) { + const ScrDyn *cb = argc > 0 ? args[0] : scr_dyn_undefined(); + if (cb->kind != SCR_DYN_FUNC) { + static const char message[] = "cb is not a function"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_box_get_ref(self->caps[0]); /* the slot owns this reference */ + ScrDyn *result = scr_dyn_call(cb, args + 1, argc - 1, "callback"); + scr_als_restore(prev); + return result; +} + +ScrDyn *scr_als_snapshot(void) { + return scr_dyn_new_func(scr_als_context_closure(NULL, 1), scr_als_snapshot_call, 1, "(cb, ...args)", "bound"); } /* The two rejection-event registries share one shape: listeners with a diff --git a/tests/corpus/7110-als-completion-callbacks.ts b/tests/corpus/7110-als-completion-callbacks.ts new file mode 100644 index 000000000..4b4e97e0d --- /dev/null +++ b/tests/corpus/7110-als-completion-callbacks.ts @@ -0,0 +1,135 @@ +// The completion callback of a callback-style API runs with the +// AsyncLocalStorage stores that were active when the call started the +// operation, as in Node. +import { AsyncLocalStorage } from "node:async_hooks"; +import { execFile } from "node:child_process"; +import * as crypto from "node:crypto"; +import * as dgram from "node:dgram"; +import * as dns from "node:dns"; +import { readFile, rename, writeFileSync, unlinkSync } from "node:fs"; +import * as http from "node:http"; +import * as net from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import * as zlib from "node:zlib"; + +const store = new AsyncLocalStorage(); + +// Starts one operation inside run(name) and waits for its callback. +function step(name: string, start: (done: () => void) => void): Promise { + return new Promise((resolve) => { + store.run(name, () => + start(() => { + console.log(name, store.getStore()); + resolve(); + }), + ); + }); +} + +const dir = tmpdir(); +const file = join(dir, `scriptc-als-${process.pid}.txt`); +writeFileSync(file, "hello"); + +await step("fs.readFile", (done) => readFile(file, "utf8", () => done())); +await step("fs.rename", (done) => rename(file, `${file}.moved`, () => done())); +unlinkSync(`${file}.moved`); + +await step("crypto.randomBytes", (done) => crypto.randomBytes(4, () => done())); +await step("crypto.randomInt", (done) => crypto.randomInt(10, () => done())); +await step("crypto.randomFill", (done) => crypto.randomFill(new Uint8Array(4), () => done())); +await step("crypto.pbkdf2", (done) => crypto.pbkdf2("p", "s", 1, 8, "sha256", () => done())); +await step("crypto.scrypt", (done) => crypto.scrypt("p", "s", 8, () => done())); +await step("crypto.hkdf", (done) => crypto.hkdf("sha256", "k", "s", "i", 8, () => done())); +await step("crypto.generateKey", (done) => + crypto.generateKey("hmac", { length: 64 }, () => done()), +); + +await step("zlib.gzip", (done) => zlib.gzip(Buffer.from("hello"), () => done())); +await step("zlib.deflate options", (done) => + zlib.deflate(Buffer.from("hello"), { level: 1 }, () => done()), +); + +await step("dns.lookup", (done) => dns.lookup("localhost", () => done())); +await step("child_process.execFile", (done) => execFile("true", [], () => done())); + +// A server's listen callback, a client connection and an HTTP response +// each run in the stores of their own call. +const server = net.createServer((socket) => socket.end()); +await step("net listen", (done) => server.listen(0, "127.0.0.1", () => done())); +const port = (server.address() as net.AddressInfo).port; +await step("net.connect", (done) => { + const socket = net.connect(port, "127.0.0.1", () => { + socket.destroy(); + done(); + }); +}); +server.close(); + +const web = http.createServer((_req, res) => res.end("ok")); +await new Promise((resolve) => web.listen(0, "127.0.0.1", () => resolve())); +const webPort = (web.address() as net.AddressInfo).port; +await step("http.get", (done) => + http.get(`http://127.0.0.1:${webPort}/`, (res) => { + res.resume(); + done(); + }), +); +await step("http.request", (done) => + http + .request({ host: "127.0.0.1", port: webPort, path: "/", method: "GET" }, (res) => { + res.resume(); + done(); + }) + .end(), +); +web.close(); + +const optionsServer = net.createServer(); +await step("net listen options", (done) => + optionsServer.listen({ port: 0, host: "127.0.0.1" }, () => done()), +); +optionsServer.close(); +const pathServer = net.createServer(); +const socketPath = join(dir, `scriptc-als-${process.pid}.sock`); +await step("net listen path", (done) => pathServer.listen(socketPath, () => done())); +pathServer.close(); + +const udp = dgram.createSocket("udp4"); +await step("dgram bind", (done) => udp.bind(0, "127.0.0.1", () => done())); +const peer = dgram.createSocket("udp4"); +await new Promise((resolve) => peer.bind(0, "127.0.0.1", () => resolve())); +await step("dgram connect", (done) => udp.connect(peer.address().port, "127.0.0.1", () => done())); +peer.close(); +await step("dgram close", (done) => udp.close(() => done())); + +await step("process.stdout.write", (done) => process.stdout.write("", () => done())); + +// Nested: a callback starts another operation in a different store. +await step("outer", (done) => + crypto.randomBytes(1, () => { + console.log("outer callback", store.getStore()); + store.run("inner", () => + crypto.randomBytes(1, () => { + console.log("inner callback", store.getStore()); + done(); + }), + ); + }), +); + +// A callback whose call ran with no store sees no store, and a store the +// callback enters does not reach the next callback. +await new Promise((resolve) => + crypto.randomBytes(1, () => { + console.log("no store", store.getStore()); + store.enterWith("entered"); + resolve(); + }), +); +await new Promise((resolve) => + crypto.randomBytes(1, () => { + console.log("after enterWith", store.getStore()); + resolve(); + }), +); diff --git a/tests/corpus/7111-als-bind-snapshot-options.ts b/tests/corpus/7111-als-bind-snapshot-options.ts new file mode 100644 index 000000000..d1c6dbbf2 --- /dev/null +++ b/tests/corpus/7111-als-bind-snapshot-options.ts @@ -0,0 +1,59 @@ +// AsyncLocalStorage.bind, AsyncLocalStorage.snapshot and the constructor +// options, as in Node. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); + +// bind(fn) runs fn in the context active at the bind, with its arguments. +const add = store.run("bound", () => + AsyncLocalStorage.bind((a: number, b: number) => `${store.getStore()}:${a + b}`), +); +console.log(add(1, 2)); +console.log(store.run("other", () => add(3, 4))); + +// A function bound outside any store sees no store inside run(). +const plain = AsyncLocalStorage.bind(() => store.getStore()); +console.log(store.run("ignored", () => plain())); + +// The bound function keeps its result type. +const length = store.run("len", () => AsyncLocalStorage.bind((s: string) => s.length)); +console.log(length("four") * 2); + +// snapshot() runs a function in the context of the snapshot. +const snap = store.run("snapshot", () => AsyncLocalStorage.snapshot()); +console.log(snap(() => store.getStore())); +console.log(store.run("later", () => snap(() => store.getStore()))); +console.log(snap((x: number, y: number) => x * y, 6, 7)); + +// A store entered inside the bound call ends with it. +const enter = store.run("start", () => + AsyncLocalStorage.bind(() => { + store.enterWith("entered"); + return store.getStore(); + }), +); +console.log(enter(), store.getStore()); + +// The default value and the name. +const config = new AsyncLocalStorage<{ level: number }>({ + defaultValue: { level: 1 }, + name: "config", +}); +console.log(config.name, config.getStore()?.level); +config.run({ level: 2 }, () => console.log("run", config.getStore()?.level)); +config.exit(() => console.log("exit", config.getStore())); +config.disable(); +console.log("disabled", config.getStore()?.level); +const unnamed = new AsyncLocalStorage(); +console.log(JSON.stringify(unnamed.name), unnamed.getStore()); +const empty = new AsyncLocalStorage({}); +console.log(JSON.stringify(empty.name), empty.getStore()); + +// Bound functions and snapshots carry the context across asynchronous work. +const later = store.run("timer", () => AsyncLocalStorage.bind(() => store.getStore())); +await new Promise((resolve) => + setTimeout(() => { + console.log("in timer", later(), store.getStore()); + resolve(); + }, 1), +); diff --git a/tests/corpus/7112-als-bind-snapshot-dynamic.mjs b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs new file mode 100644 index 000000000..22e3238b8 --- /dev/null +++ b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs @@ -0,0 +1,51 @@ +// AsyncLocalStorage.bind, snapshot and the constructor options over +// untyped values, as in Node. +import { AsyncLocalStorage } from "node:async_hooks"; +import { readFile } from "node:fs"; + +const store = new AsyncLocalStorage(); + +function sum(a, b) { + return [this?.tag, a + b, store.getStore()]; +} +const bound = store.run("bound", () => AsyncLocalStorage.bind(sum)); +console.log(bound.name, bound.length, bound === sum, typeof bound); +console.log(JSON.stringify(bound.call({ tag: "receiver" }, 1, 2))); +console.log(JSON.stringify(store.run("other", () => bound(3, 4)))); + +const snap = store.run("snapshot", () => AsyncLocalStorage.snapshot()); +console.log(snap.name, snap.length, typeof snap); +console.log(snap((a, b) => `${store.getStore()} ${a + b}`, 2, 3)); + +for (const attempt of [ + () => AsyncLocalStorage.bind(5), + () => AsyncLocalStorage.bind("fn"), + () => snap(5), + () => snap(), +]) { + try { + console.log("ok", attempt()); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} + +for (const options of [1, "x", null, true]) { + try { + new AsyncLocalStorage(options); + console.log("ok", options); + } catch (error) { + console.log(error.name, error.code, error.message); + } +} +const named = new AsyncLocalStorage({ name: "named", defaultValue: 0 }); +console.log(named.name, named.getStore()); +named.run(5, () => console.log(named.getStore())); +console.log(new AsyncLocalStorage(undefined).name === ""); + +// An untyped callback runs in the stores of its call. +store.run("readFile", () => + readFile(import.meta.filename, "utf8", (error, text) => + console.log("readFile", store.getStore(), error === null, text.length > 0), + ), +); diff --git a/tests/harness/corpus-costs/7110-als-completion-callbacks.ts.json b/tests/harness/corpus-costs/7110-als-completion-callbacks.ts.json new file mode 100644 index 000000000..d8f3f6d12 --- /dev/null +++ b/tests/harness/corpus-costs/7110-als-completion-callbacks.ts.json @@ -0,0 +1 @@ +{ "plain": 6500, "san": 16000 } diff --git a/tests/harness/corpus-costs/7111-als-bind-snapshot-options.ts.json b/tests/harness/corpus-costs/7111-als-bind-snapshot-options.ts.json new file mode 100644 index 000000000..3f7384f20 --- /dev/null +++ b/tests/harness/corpus-costs/7111-als-bind-snapshot-options.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 6000 } diff --git a/tests/harness/corpus-costs/7112-als-bind-snapshot-dynamic.mjs.json b/tests/harness/corpus-costs/7112-als-bind-snapshot-dynamic.mjs.json new file mode 100644 index 000000000..3fd934f0e --- /dev/null +++ b/tests/harness/corpus-costs/7112-als-bind-snapshot-dynamic.mjs.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 12000 } From 1e0dabd74864c4c2f4e6c962cf550bad540047a6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:05:46 -0500 Subject: [PATCH 003/409] Declare every http2 listener event for once and addListener The lowering accepts on, once and addListener alike for each server, session and stream kind, but the fallback Node types (used when a project has no @types/node) declared fewer events for once than for on on the http2 servers, sessions and streams, and no addListener on any of them. `http2Server.once("connection", ...)` and `server.addListener("stream", ...)` failed to type-check (SC0001) where Node runs them. once and addListener on Http2Server, Http2SecureServer, Http2Session, Http2Stream, ServerHttp2Stream and ClientHttp2Stream now declare the same overloads as on. Corpus 7281 runs an HTTP CONNECT proxy on node:http, which #1791's declaration of the http.Server 'connect' event made compile: it tunnels to a local target and pipes both ways, with a non-empty head, once and addListener registrations, and an 'upgrade' handover. Corpus 7282 registers http2 server, session and stream listeners with once and addListener. Closes #1439 --- .../ambient/scriptc-node-fallback.d.ts | 142 +++++++++++++++++- .../7281-http-server-connect-tunnel.ts.json | 6 + .../7282-http2-once-add-listener.ts.json | 6 + .../corpus/7281-http-server-connect-tunnel.ts | 81 ++++++++++ tests/corpus/7282-http2-once-add-listener.ts | 54 +++++++ .../7281-http-server-connect-tunnel.ts.json | 1 + .../7282-http2-once-add-listener.ts.json | 1 + 7 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json create mode 100644 tests/corpus/7281-http-server-connect-tunnel.ts create mode 100644 tests/corpus/7282-http2-once-add-listener.ts create mode 100644 tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json create mode 100644 tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 2a3f11be9..02b88a677 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -5929,6 +5929,7 @@ declare module "http2" { export interface Http2Stream { on(event: "error" | "close", listener: (err: Error) => void): void; once(event: "error" | "close", listener: (err: Error) => void): void; + addListener(event: "error" | "close", listener: (err: Error) => void): void; destroy(): void; } export interface Http2ServerRequest { @@ -6029,11 +6030,6 @@ declare module "http2" { listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, ): void; on(event: "session", listener: (session: ServerHttp2Session) => void): void; - once( - event: "stream", - listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, - ): void; - once(event: "session", listener: (session: ServerHttp2Session) => void): void; /* HTTP CONNECT (and, in Node, the h2 extended CONNECT — which the * allowHTTP1 lowering never sees): the second argument is the raw * socket on HTTP/1.1 connections; narrow with instanceof. */ @@ -6051,6 +6047,42 @@ declare module "http2" { ): void; once(event: "error", listener: (err: Error) => void): void; once(event: "close", listener: () => void): void; + once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + once( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + once(event: "session", listener: (session: ServerHttp2Session) => void): void; + once( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; + addListener(event: "connection", listener: (socket: Socket) => void): void; + addListener( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "close", listener: () => void): void; + addListener( + event: "sessionError", + listener: (err: Error, session: ServerHttp2Session) => void, + ): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; + addListener( + event: "connect", + listener: ( + req: Http2ServerRequest | import("http").IncomingMessage, + resOrSocket: Http2ServerResponse | Socket, + ) => void, + ): void; } export interface SecureServerOptions { allowHTTP1?: boolean; @@ -6392,6 +6424,28 @@ declare module "http2" { listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, ): void; once(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "ping", listener: (payload: Uint8Array) => void): void; + addListener(event: "close" | "timeout", listener: () => void): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "connect", + listener: (session: Http2Session, socket: import("net").Socket) => void, + ): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "goaway", + listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void, + ): void; + addListener(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener(event: "ping", listener: (payload: Uint8Array) => void): void; } export interface ClientHttp2Session extends Http2Session { @@ -6469,7 +6523,30 @@ declare module "http2" { listener: () => void, ): void; once(event: "error", listener: (err: Error) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + addListener(event: "data", listener: (chunk: any) => void): void; + addListener( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener( + event: "trailers", + listener: (trailers: IncomingHttpHeaders, flags: number) => void, + ): void; respond( headers?: OutgoingHttpHeaders, options?: { endStream?: boolean; waitForTrailers?: boolean }, @@ -6519,6 +6596,7 @@ declare module "http2" { on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; once(event: "response", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; once(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; + once(event: "headers", listener: (headers: IncomingHttpHeaders, flags: number) => void): void; once(event: "data", listener: (chunk: any) => void): void; once( event: @@ -6533,6 +6611,42 @@ declare module "http2" { listener: () => void, ): void; once(event: "error", listener: (err: Error) => void): void; + once(event: "frameError", listener: (type: number, code: number, id: number) => void): void; + once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void; + addListener( + event: "response", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "push", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener( + event: "headers", + listener: (headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "data", listener: (chunk: any) => void): void; + addListener( + event: + | "end" + | "close" + | "aborted" + | "ready" + | "timeout" + | "drain" + | "finish" + | "wantTrailers", + listener: () => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener( + event: "frameError", + listener: (type: number, code: number, id: number) => void, + ): void; + addListener( + event: "trailers", + listener: (trailers: IncomingHttpHeaders, flags: number) => void, + ): void; } export interface Http2Server { @@ -6567,6 +6681,24 @@ declare module "http2" { ): void; once(event: "error", listener: (err: Error) => void): void; once(event: "close" | "listening" | "timeout", listener: () => void): void; + once(event: "connection", listener: (socket: import("net").Socket) => void): void; + once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void; + addListener( + event: "stream", + listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void, + ): void; + addListener(event: "session", listener: (session: ServerHttp2Session) => void): void; + addListener( + event: "request", + listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void, + ): void; + addListener(event: "error", listener: (err: Error) => void): void; + addListener(event: "close" | "listening" | "timeout", listener: () => void): void; + addListener(event: "connection", listener: (socket: import("net").Socket) => void): void; + addListener( + event: "sessionError", + listener: (err: Error, session: ServerHttp2Session) => void, + ): void; } export interface ServerOptions { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json new file mode 100644 index 000000000..2328d508b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7281-http-server-connect-tunnel.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7281-http-server-connect-tunnel.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json new file mode 100644 index 000000000..96c879197 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7282-http2-once-add-listener.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7282-http2-once-add-listener.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7281-http-server-connect-tunnel.ts b/tests/corpus/7281-http-server-connect-tunnel.ts new file mode 100644 index 000000000..d8b396c07 --- /dev/null +++ b/tests/corpus/7281-http-server-connect-tunnel.ts @@ -0,0 +1,81 @@ +// An HTTP CONNECT proxy on node:http: the server's 'connect' listener gets +// the request, the raw client socket and the bytes read past the request +// head, opens a TCP connection to the requested host and pipes both ways. +// 'once' and 'addListener' register the same event, and 'upgrade' hands +// over the socket for an Upgrade request. +import * as http from "node:http"; +import * as net from "node:net"; + +const target = net.createServer((socket) => { + socket.on("data", (d) => socket.write(`echo:${d.toString()}`)); +}); + +const proxy = http.createServer((_req, res) => res.end("plain")); +const seen: string[] = []; +proxy.on("connect", (req, clientSocket, head) => { + seen.push(`connect ${req.method} ${req.url} head=${head.length}`); + const [host, port] = (req.url ?? "").split(":"); + const upstream = net.connect(Number(port), host, () => { + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + if (head.length > 0) upstream.write(head); + upstream.pipe(clientSocket); + clientSocket.pipe(upstream); + }); +}); +proxy.once("connect", (req) => seen.push(`once ${req.url}`)); +proxy.addListener("upgrade", (req, socket, head) => { + seen.push(`upgrade ${req.headers.upgrade} head=${head.length}`); + socket.end("HTTP/1.1 101 Switching Protocols\r\nUpgrade: echo\r\nConnection: Upgrade\r\n\r\n"); +}); + +function tunnel( + proxyPort: number, + targetPort: number, + payload: string, + withHead: boolean, +): Promise { + return new Promise((resolve) => { + const c = net.connect(proxyPort, "127.0.0.1", () => { + const request = `CONNECT 127.0.0.1:${targetPort} HTTP/1.1\r\nHost: 127.0.0.1:${targetPort}\r\n\r\n`; + c.write(withHead ? request + payload : request); + }); + let text = ""; + let sent = withHead; + c.on("data", (d) => { + text += d.toString(); + if (!sent && text.includes("\r\n\r\n")) { + sent = true; + c.write(payload); + } + if (sent && text.includes(`echo:${payload}`)) { + c.end(); + resolve(text); + } + }); + }); +} + +function upgrade(proxyPort: number): Promise { + return new Promise((resolve) => { + const u = net.connect(proxyPort, "127.0.0.1", () => { + u.write("GET /ws HTTP/1.1\r\nHost: x\r\nConnection: Upgrade\r\nUpgrade: echo\r\n\r\n"); + }); + let text = ""; + u.on("data", (d) => (text += d.toString())); + u.on("end", () => resolve(text.split("\r\n")[0] ?? "")); + }); +} + +target.listen(0, "127.0.0.1", () => { + const targetPort = (target.address() as net.AddressInfo).port; + proxy.listen(0, "127.0.0.1", async () => { + const proxyPort = (proxy.address() as net.AddressInfo).port; + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "first", false))); + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "second", false))); + console.log(JSON.stringify(await tunnel(proxyPort, targetPort, "with-head", true))); + console.log(JSON.stringify(await upgrade(proxyPort))); + console.log(seen.join("\n").replaceAll(String(targetPort), "PORT")); + proxy.close(); + target.close(); + }); +}); diff --git a/tests/corpus/7282-http2-once-add-listener.ts b/tests/corpus/7282-http2-once-add-listener.ts new file mode 100644 index 000000000..ab1e24a9f --- /dev/null +++ b/tests/corpus/7282-http2-once-add-listener.ts @@ -0,0 +1,54 @@ +// node:http2 listeners registered with once and addListener on the server, +// the client session and both stream kinds. A once listener runs for the +// first event only; addListener is on. +import * as http2 from "node:http2"; +import * as net from "node:net"; + +const log: string[] = []; +const accepted: string[] = []; +const server = http2.createServer(); +server.once("connection", (socket: net.Socket) => accepted.push(`connection ${typeof socket.remotePort}`)); +server.addListener("session", () => accepted.push("session")); +server.once("stream", () => log.push("server first stream")); +server.addListener("stream", (stream, headers) => { + const header = headers[":path"]; + const path = typeof header === "string" ? header : ""; + log.push(`server stream ${path}`); + let body = ""; + stream.addListener("data", (chunk) => (body += chunk.toString())); + stream.once("end", () => { + stream.respond({ ":status": 200, "x-path": path }); + stream.end(`${path}:${body}`); + }); +}); +server.once("close", () => log.push("server close")); + +function request(client: http2.ClientHttp2Session, path: string, body: string): Promise { + return new Promise((resolve) => { + const req = client.request({ ":path": path, ":method": "POST" }); + req.once("response", (headers) => { + const status = headers[":status"]; + const echoed = headers["x-path"]; + log.push(`client response ${typeof status === "number" ? status : "?"} ${typeof echoed === "string" ? echoed : "?"}`); + }); + let text = ""; + req.addListener("data", (chunk) => (text += chunk.toString())); + req.once("end", () => resolve(text)); + req.end(body); + }); +} + +server.listen(0, () => { + const port = (server.address() as net.AddressInfo).port; + const client = http2.connect(`http://localhost:${port}`); + client.once("connect", () => log.push("client connect")); + client.addListener("close", () => { + log.push("client close"); + server.close(() => console.log([accepted.sort().join(", "), ...log].join("\n"))); + }); + void (async () => { + console.log(await request(client, "/a", "one")); + console.log(await request(client, "/b", "two")); + client.close(); + })(); +}); diff --git a/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json b/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json new file mode 100644 index 000000000..c35274f8c --- /dev/null +++ b/tests/harness/corpus-costs/7281-http-server-connect-tunnel.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 3100 } diff --git a/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json b/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json new file mode 100644 index 000000000..9e5407833 --- /dev/null +++ b/tests/harness/corpus-costs/7282-http2-once-add-listener.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 5100 } From ef8cc7f74f13470c21d3c47b92df75ad9055808a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:48:55 -0500 Subject: [PATCH 004/409] Settle Promise.all over different entry types on Node's turns `Promise.all([a, b])` whose entries have different types (the checker's tuple overload) ran through an async helper that awaited the combinator and then each fulfilled entry, so its awaits resumed three microtask turns after Node's. The combinator now stores the entry promises, and an adapter run inside its settlement reads them into the tuple, so the result settles on the combinator's turn: awaiting it, storing it first, chaining it with then and returning it from an async function all land on Node's turns. Plain entries join the combinator through Promise.resolve, as in Node, and unit-typed entries compile. --- .../compiler/src/backend/llvm/lib-dispatch.ts | 88 ++++++ .../frontend/lowering/builtins/promises.ts | 273 +++++++++--------- .../compiler/src/ir/builtin-signatures.ts | 12 + packages/compiler/src/ir/validate.ts | 26 ++ .../7035-promise-all-tuple-turns.ts.json | 6 + packages/runtime/src/scr_async.c | 6 + packages/runtime/src/scr_async.h | 2 + tests/corpus/7035-promise-all-tuple-turns.ts | 64 ++++ .../7035-promise-all-tuple-turns.ts.json | 1 + 9 files changed, 338 insertions(+), 140 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json create mode 100644 tests/corpus/7035-promise-all-tuple-turns.ts create mode 100644 tests/harness/corpus-costs/7035-promise-all-tuple-turns.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 24f2bf019..207132d05 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -5,6 +5,7 @@ import { emitTlsLibCall } from "./lib-tls.js"; import { emitHttp2LibCall } from "./lib-http2.js"; import { emitDatagramLibCall } from "./lib-datagram.js"; import { emitTestLibCall } from "./lib-test.js"; +import { FN_ATTRS, arrNewCall, traceArg, vAdapters } from "./shapes.js"; /* Focused LLVM library-call emission extracted from emitter.ts. */ import { InternalCompilerError } from "../../errors.js"; @@ -236,6 +237,32 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll host.emitPendingCheck(); return output; } + if (e.fn === "promise.allTuple") return emitPromiseAllTuple(host, e); + if (e.fn === "promise.fulfilledValue") { + // The payload of a fulfilled promise (borrowed operand): numbers and + // booleans by value, strings and references +1. + const promise = host.emitExpr(e.args[0]!); + const out = B.tmp(); + switch (e.type.kind) { + case "f64": + case "procStream": + host.declare(`declare double @scr_promise_payload_f64(ptr)`); + B.line(`${out} = call double @scr_promise_payload_f64(ptr ${promise.name})`); + return { name: out, type: e.type }; + case "bool": + host.declare(`declare zeroext i1 @scr_promise_payload_bool(ptr)`); + B.line(`${out} = call zeroext i1 @scr_promise_payload_bool(ptr ${promise.name})`); + return { name: out, type: e.type }; + case "string": + host.declare(`declare ptr @scr_promise_payload_str(ptr)`); + B.line(`${out} = call ptr @scr_promise_payload_str(ptr ${promise.name}) ; +1`); + return host.own({ name: out, type: e.type }); + default: + host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); + B.line(`${out} = call ptr @scr_promise_payload_ref(ptr ${promise.name}) ; +1`); + return host.own({ name: out, type: e.type }); + } + } if (e.fn === "dyn.typedRefIs" || e.fn === "dyn.typedRefKeyIs") { const literal = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null; if (e.fn === "dyn.typedRefKeyIs" && literal === null) @@ -877,6 +904,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "disposable": case "punycode": case "constants": + case "promise": return host.emitGenericLibCall(e); case "test": return emitTestLibCall(host, e); @@ -891,3 +919,63 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { } } } + +/** Promise.all's tuple form (promise.allTuple): the combinator over the + * entries stores each fulfilled entry promise, and a settlement adapter on + * the combinator calls the builder with them and fulfills the result with + * the tuple inside the combinator's settlement; a rejection settles the + * result the same way. The combinator itself counts as handled. */ +function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue { + const B = host.B; + const build = e.args[1]!; + if (build.kind !== "closure" || e.type.kind !== "promise") + throw new InternalCompilerError("llvm emitter bug: promise.allTuple shape"); + const entriesT = e.args[0]!.type; + const entries = host.emitExpr(e.args[0]!); + const tuple = e.type.inner; + const key = `allTuple:${build.fnName}`; + let adapter = host.resolveThunks.get(key); + if (adapter === undefined) { + adapter = `sc_all_tuple_${host.resolveThunks.size}`; + host.resolveThunks.set(key, adapter); + const rc = vAdapters(host.shapeHost, tuple); + host.declare(`declare ptr @scr_promise_payload_ref(ptr)`); + host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); + host.resolveThunkDefs.push( + `define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`, + `entry:`, + ` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`, + ` %tuple = call ptr @${host.callTarget(build.fnName)}(ptr %entries)`, + ` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`, + ` ret void`, + `}`, + ``, + ); + } + host.declare(`declare ptr @scr_promise_all(ptr, ptr, ptr)`); + host.declare(`declare void @scr_promise_all_store_promise(ptr, double, ptr)`); + host.declare(`declare ptr @scr_promise_new()`); + host.declare(`declare void @scr_promise_race_add(ptr, ptr, ptr)`); + host.declare(`declare void @scr_promise_mark_handled(ptr)`); + host.declare(`declare void @scr_promise_release(ptr)`); + host.declare(`declare double @scr_arr_len(ptr)`); + if (entriesT.kind !== "array") + throw new InternalCompilerError("llvm emitter bug: promise.allTuple entries"); + const len = B.tmp(); + const cap = B.tmp(); + B.line(`${len} = call double @scr_arr_len(ptr ${entries.name})`); + B.line(`${cap} = fptoui double ${len} to ${host.sizeType}`); + const values = B.tmp(); + B.line(`${values} = ${arrNewCall(host.shapeHost, entriesT.elem, cap)}`); + host.own({ name: values, type: entriesT }); + const all = B.tmp(); + B.line( + `${all} = call ptr @scr_promise_all(ptr ${entries.name}, ptr ${values}, ptr @scr_promise_all_store_promise)`, + ); + B.line(`call void @scr_promise_mark_handled(ptr ${all})`); + const result = B.tmp(); + B.line(`${result} = call ptr @scr_promise_new()`); + B.line(`call void @scr_promise_race_add(ptr ${result}, ptr ${all}, ptr @${adapter})`); + B.line(`call void @scr_promise_release(ptr ${all})`); + return host.own({ name: result, type: e.type }); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 75e527aa5..c30590619 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -2,7 +2,7 @@ import { keyedHelperName } from "../stable-names.js"; import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; -import { newFnCtx, type Lowerer } from "../lowerer.js"; +import type { Lowerer } from "../lowerer.js"; import { varRef } from "../../../ir/build.js"; import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js"; import { isJsSourceFile, locOf } from "../../program.js"; @@ -15,32 +15,25 @@ import { type IrType, UNDEFINED_T, VOID, + F64, arrayOf, + funcOf, isUnitType, typeEquals, typeKey, } from "../../../ir/ir.js"; /** `Promise.all([p, q, r])` over DIFFERENT promise types — the checker's - * tuple overload, `Promise<[A, B, C]>`. One interned async helper per - * entry/result signature takes the entries as arguments, awaits the - * runtime combinator over the same promises (each widened to - * Promise, a type-only view), so settlement is Node's exactly: the - * first rejection in settlement order wins and every later one counts as - * handled; then it reads each (now fulfilled) entry and returns the - * tuple. Entries keep their identity — no value converts through the - * checked-dynamic tree. Non-promise entries (numbers, strings, booleans, - * unit values — never thenable) pass straight into their slot; a - * checked-dynamic entry (a JavaScript function's result) joins through - * PromiseResolve: the promise it holds, a promise that adopts a thenable, - * or a promise fulfilled with its plain value. The tuple - * fulfills one microtask turn after the combinator does. Null when the - * result type or an entry has no typed home here. */ -function lowerTuplePromiseAll( + * tuple overload, `Promise<[A, B, C]>`: the result type and tuple shape, + * or null when the result type or an entry has no typed home here. + * Entries are promises, checked-dynamic values (a JavaScript function's + * result) and plain numbers, strings, booleans and unit values, which are + * never thenable. */ +function tuplePromiseAllShape( lowerer: Lowerer, call: ts.CallExpression, - elems: IrExpr[], -): IrExpr | null { + elems: readonly IrExpr[], +): { resultT: IrType & { kind: "promise" }; tuple: IrType & { kind: "record" } } | null { const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); if (resultT?.kind !== "promise" || resultT.inner.kind !== "record") return null; const shape = lowerer.shapes.get(resultT.inner.shapeId); @@ -52,135 +45,135 @@ function lowerTuplePromiseAll( const settles = (t: IrType): boolean => t.kind === "promise" || t.kind === "dyn"; if (!elems.some((e) => settles(e.type))) return null; if (!elems.every((e) => settles(e.type) || plainValue(e.type))) return null; + return { resultT, tuple: resultT.inner }; +} + +/** `Promise.all([p, q, r])` over different promise types. Each entry + * becomes a promise in input order (a plain value through Promise.resolve, + * a checked-dynamic one through its PromiseResolve view: the promise it + * holds, a promise that adopts a thenable, or a promise fulfilled with the + * value), widened to Promise, a type-only view. The runtime + * combinator over them gives Node's settlement: the first rejection in + * settlement order wins and every later one counts as handled. The + * result settles inside the combinator's own settlement, so it and its + * awaits land on Node's microtask turns; a fulfillment runs one interned + * function per entry/result signature that reads the fulfilled entries + * into the tuple. Entries keep their identity: no value converts through + * the checked-dynamic tree. */ +function lowerTuplePromiseAll( + lowerer: Lowerer, + call: ts.CallExpression, + elems: IrExpr[], +): IrExpr | null { + const plan = tuplePromiseAllShape(lowerer, call, elems); + if (!plan) return null; + const { resultT, tuple } = plan; const loc = locOf(call); - const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`; + const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID }; + const entriesT = arrayOf(voidPromise); + const dynPromise: IrType = { kind: "promise", inner: DYN }; + const entries = elems.map((e): IrExpr => { + let promise: IrExpr; + if (e.type.kind === "promise") promise = e; + else if (e.type.kind === "dyn") + promise = { + kind: "libCall", + fn: "dyn.promiseResolve", + args: [e, thenReader(lowerer, loc)], + type: dynPromise, + loc, + }; + else if (isUnitType(e.type)) { + // A unit entry's value is its type's one value; the promise only + // takes its place in the combinator. + const resolved: IrExpr = { + kind: "intrinsic", + name: "promise.resolve", + args: [], + type: voidPromise, + loc, + }; + promise = + e.kind === "unitLit" + ? resolved + : { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: e, loc }], + result: resolved, + type: voidPromise, + loc, + }; + } else + promise = { + kind: "intrinsic", + name: "promise.resolve", + args: [e], + type: { kind: "promise", inner: e.type }, + loc, + }; + return promise.type.kind === "promise" && promise.type.inner.kind === "void" + ? promise + : { kind: "promiseVoidWiden", value: promise, type: voidPromise, loc }; + }); + const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(tuple)}`; let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`); if (name === undefined) { name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`); lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name); - const ctx = newFnCtx(false, null, null, resultT.inner); - ctx.isAsync = true; - lowerer.fnStack.push(ctx); - try { - const params = elems.map((e, i) => ({ - localId: `entry.${i}`, - name: `entry${i}`, - type: e.type, - })); - const ref = (i: number): IrExpr => varRef(`entry.${i}`, elems[i]!.type, loc); - const dynPromise: IrType = { kind: "promise", inner: DYN }; - const body: IrStmt[] = []; - // Each dyn entry's promise view, in entry order: the promise it - // holds, a promise that adopts a thenable's settlement, or a promise - // fulfilled with the plain value (PromiseResolve's rule). - const views = new Map(); - elems.forEach((e, i) => { - if (e.type.kind !== "dyn") return; - const view = lowerer.declareHiddenLocal("%allEntry", dynPromise); - body.push({ - kind: "varDecl", - localId: view.id, - init: { - kind: "libCall", - fn: "dyn.promiseResolve", - args: [ref(i), thenReader(lowerer, loc)], - type: dynPromise, - loc, - }, - loc, - }); - views.set(i, varRef(view.id, dynPromise, loc)); - }); - const widened: IrExpr[] = []; - elems.forEach((e, i) => { - const view = views.get(i); - if (view !== undefined) { - widened.push({ - kind: "promiseVoidWiden", - value: view, - type: { kind: "promise", inner: VOID }, - loc, - }); - return; - } - if (e.type.kind !== "promise") return; - widened.push( - e.type.inner.kind === "void" - ? ref(i) - : { - kind: "promiseVoidWiden", - value: ref(i), - type: { kind: "promise", inner: VOID }, + const shape = lowerer.shapes.get(tuple.shapeId)!; + const entriesRef = varRef("entries.0", entriesT, loc); + // Shape fields are sorted by name ("10" before "2"); a tuple field's + // name is its position. + const fields = shape.fields.map((field) => { + const i = Number(field.name); + const type = elems[i]!.type; + const inner = type.kind === "promise" ? type.inner : type; + const value: IrExpr = + inner.kind === "void" + ? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } + : isUnitType(inner) + ? { + kind: "unitLit", + unit: inner.kind === "nullT" ? "null" : "undefined", + type: inner, loc, - }, - ); - }); - const settled: IrExpr = { - kind: "awaitExpr", - value: { - kind: "intrinsic", - name: "promise.all", - args: [ - { - kind: "arrayLit", - elems: widened, - type: arrayOf({ kind: "promise", inner: VOID }), - loc, - }, - ], - type: { kind: "promise", inner: VOID }, - loc, - }, - type: VOID, - loc, - }; - // Shape fields are sorted by name ("10" before "2"); a tuple field's - // name is its position. - const fields = shape.fields.map((field) => { - const i = Number(field.name); - const entry = elems[i]!; - let value: IrExpr; - const view = views.get(i); - if (view !== undefined) { - value = { kind: "awaitExpr", value: view, type: DYN, loc }; - } else if (entry.type.kind === "promise") { - value = - entry.type.inner.kind === "void" - ? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } - : { kind: "awaitExpr", value: ref(i), type: entry.type.inner, loc }; - } else { - value = ref(i); - } - return { name: field.name, value: lowerer.coerceToExpected(value, field.type) }; - }); - body.push( - { kind: "exprStmt", expr: settled, loc }, - { - kind: "return", - value: { kind: "recordLit", fields, type: resultT.inner, loc }, - loc, - }, - ); - lowerer.liftedFns.push({ - name, - params, - returnType: resultT.inner, - locals: [ - ...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), - ...lowerer.ctx.locals, - ], - body, - async: true, - loc, - }); - } catch (e) { - lowerer.arrHofHelpers.delete(`promise.allTuple:${key}`); - throw e; - } finally { - lowerer.fnStack.pop(); - } + } + : { + kind: "libCall", + fn: "promise.fulfilledValue", + args: [ + { + kind: "arrayGet", + arr: entriesRef, + index: { kind: "numLit", value: i, type: F64, loc }, + type: voidPromise, + loc, + }, + ], + type: inner, + loc, + }; + return { name: field.name, value: lowerer.coerceToExpected(value, field.type) }; + }); + lowerer.liftedFns.push({ + name, + params: [{ localId: "entries.0", name: "entries", type: entriesT }], + returnType: tuple, + locals: [{ id: "entries.0", name: "entries", type: entriesT, mutable: false }], + body: [{ kind: "return", value: { kind: "recordLit", fields, type: tuple, loc }, loc }], + loc, + }); } - return { kind: "call", callee: name, args: elems, type: resultT, loc }; + return { + kind: "libCall", + fn: "promise.allTuple", + args: [ + { kind: "arrayLit", elems: entries, type: entriesT, loc }, + { kind: "closure", fnName: name, captures: [], type: funcOf([entriesT], tuple), loc }, + ], + type: resultT, + loc, + }; } /** `Promise.race([...])` on THE Promise global: the entries lower diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fcdbe8694..d44f3ebbf 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3139,6 +3139,18 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ // or an optional Error | null slot (same success shape as fs.rename). "process.stdoutWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, "process.stderrWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL }, + /** The value of a FULFILLED promise, read without a microtask turn (+1 + * for references): the tuple form of Promise.all reads its entries this + * way once the combinator fulfills, each entry widened to Promise + * and read at the type it was built with (program-dependent). */ + "promise.fulfilledValue": { argTypes: [null], result: VOID }, + /** Promise.all's tuple form as a value: the entries (each widened to + * Promise) join the runtime combinator, and when it fulfills the + * function reads the fulfilled entries into the tuple inside that + * settlement, so the result settles on the combinator's turn; a + * rejection settles it the same way. The result is the tuple's promise + * (program-dependent). */ + "promise.allTuple": { argTypes: [null, null], result: VOID }, "fsp.readFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } }, "fsp.writeFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } }, /** fs.promises.writeFile(path, data, { mode }): the settled-promise diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bb36c7007..08c237fe2 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5310,6 +5310,32 @@ function validateFunction( } function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean { + if (e.fn === "promise.fulfilledValue") { + const promise = e.args[0]?.type; + if ( + promise?.kind !== "promise" || + e.type.kind === "void" || + isUnitType(e.type) || + promise.inner.kind !== "void" + ) + err(`libCall ${e.fn} must read a Promise entry at a non-void type`, e.loc); + return true; + } + if (e.fn === "promise.allTuple") { + const [entries, build] = e.args; + if ( + entries?.type.kind !== "array" || + entries.type.elem.kind !== "promise" || + entries.type.elem.inner.kind !== "void" || + build?.kind !== "closure" || + build.captures.length > 0 || + build.type.kind !== "func" || + e.type.kind !== "promise" || + !typeEquals(build.type.ret, e.type.inner) + ) + err(`libCall ${e.fn} must join Promise entries into its builder's tuple`, e.loc); + return true; + } if ( e.fn === "crypto.randomFillView" || e.fn === "crypto.randomFillViewRest" || diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json new file mode 100644 index 000000000..29b935e93 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7035-promise-all-tuple-turns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7035-promise-all-tuple-turns.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 9e3365fbe..5bdfe87a5 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -1839,6 +1839,12 @@ void scr_promise_all_store_ref(ScrArr *a, double i, ScrPromise *src) { scr_arr_set_ref(a, i, scr_promise_payload_ref(src)); } +/* The entry promise itself: Promise.all's tuple form reads each fulfilled + * entry at its own type once the combinator fulfills. */ +void scr_promise_all_store_promise(ScrArr *a, double i, ScrPromise *src) { + scr_arr_set_ref(a, i, scr_promise_retain(src)); +} + /* Fulfillment payload accessors for the emitted race adapters — all * retained/by-value (the source keeps its settlement). */ double scr_promise_payload_f64(ScrPromise *p) { return p->f64; } diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index fc0128294..cc79ee1a1 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -369,6 +369,8 @@ void scr_promise_all_store_f64(ScrArr *a, double i, ScrPromise *src); void scr_promise_all_store_bool(ScrArr *a, double i, ScrPromise *src); void scr_promise_all_store_str(ScrArr *a, double i, ScrPromise *src); void scr_promise_all_store_ref(ScrArr *a, double i, ScrPromise *src); +/* Stores the fulfilled entry promise itself (+1), for Promise.all's tuple form. */ +void scr_promise_all_store_promise(ScrArr *a, double i, ScrPromise *src); void scr_promise_adapt_copy(ScrPromise *dst, ScrPromise *src); double scr_promise_payload_f64(ScrPromise *p); bool scr_promise_payload_bool(ScrPromise *p); diff --git a/tests/corpus/7035-promise-all-tuple-turns.ts b/tests/corpus/7035-promise-all-tuple-turns.ts new file mode 100644 index 000000000..6989df9b9 --- /dev/null +++ b/tests/corpus/7035-promise-all-tuple-turns.ts @@ -0,0 +1,64 @@ +// Promise.all over entries of different types settles, and resumes its +// awaits and then callbacks, on the same microtask turns as Node, whether +// the result is awaited at once, stored first, chained with then, or +// returned from an async function. The first rejection in settlement +// order wins, later ones count as handled, and record entries keep their +// identity. +const log: string[] = []; +function later(label: string, turns: number, value: T, fail = false): Promise { + return new Promise((resolve, reject) => { + let left = turns; + const step = (): void => { + if (left-- > 0) return queueMicrotask(step); + log.push(`settle ${label}`); + if (fail) reject(new Error(label)); + else resolve(value); + }; + step(); + }); +} +async function num(): Promise { + return 1; +} +async function text(): Promise { + return "s"; +} +async function both(): Promise<[number, string]> { + return Promise.all([num(), text()]); +} +interface Item { + id: number; +} +const item: Item = { id: 7 }; + +async function main(): Promise { + let ticking = true; + void (async () => { + for (let i = 0; i < 40 && ticking; i++) { + log.push(`t${i}`); + await null; + } + })(); + const [a, b] = await Promise.all([num(), text()]); + log.push(`awaited ${a}${b}`); + const stored = Promise.all([text(), num(), Promise.resolve(item)]); + const [c, d, e] = await stored; + log.push(`stored ${c}${d} ${e === item}`); + Promise.all([text(), num()]).then(([f, g]) => log.push(`then ${f}${g}`)); + const [h, i] = await both(); + log.push(`returned ${h}${i}`); + const mixed = await Promise.all([later("x", 3, "x"), 5, later("y", 1, true), undefined]); + log.push(`mixed ${mixed[0]} ${mixed[1]} ${mixed[2]} ${mixed[3]}`); + const raw: any = JSON.parse('{"value":41}'); + const [dynamic, counted] = await Promise.all([raw.value, later("z", 2, 2)]); + log.push(`dynamic ${dynamic + counted}`); + try { + await Promise.all([later("late", 4, 1, true), later("early", 2, "e", true), num()]); + } catch (err) { + log.push(`rejected ${(err as Error).message}`); + } + await later("tail", 6, 0); + ticking = false; + console.log(log.join("\n")); +} +void main(); diff --git a/tests/harness/corpus-costs/7035-promise-all-tuple-turns.ts.json b/tests/harness/corpus-costs/7035-promise-all-tuple-turns.ts.json new file mode 100644 index 000000000..9e7986c22 --- /dev/null +++ b/tests/harness/corpus-costs/7035-promise-all-tuple-turns.ts.json @@ -0,0 +1 @@ +{ "plain": 2910, "san": 4860 } From d14c2ee05f7c6cb720ef6721433df5351ab2f4b8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:59:13 -0500 Subject: [PATCH 005/409] Read AsyncLocalStorage options as JavaScript does and call snapshot callbacks without a receiver new AsyncLocalStorage(options) read defaultValue and name as own data properties, so inherited values and getters were ignored, and a name that was not a string reached the compiled code unconverted. The options are now read with a full property get (prototypes, getters, proxies, the snapshot of a typed record or class instance) in Node's order: defaultValue once, name to see whether it is defined and again for its value. The name is kept as String(name); a Symbol throws Node's TypeError, and an exception from a getter propagates. A class instance whose class itself declares one of the two members, which a capsule does not expose to the runtime, is refused with a "not supported yet" error. A snapshot's runner called its callback with the runner's own receiver; Node applies the callback to an undefined receiver. --- packages/runtime/src/scr_async_dyn.c | 70 +++++++++++++++---- .../corpus/7111-als-bind-snapshot-options.ts | 7 ++ .../corpus/7112-als-bind-snapshot-dynamic.mjs | 43 ++++++++++++ 3 files changed, 107 insertions(+), 13 deletions(-) diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index c47626557..58a4ac3f8 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -71,17 +71,61 @@ double scr_als_new(void) { return ++scr_als_counter; } +/* options[key] as JavaScript reads it (+1, or NULL with the exception + * pending): inherited properties and getters included. A class instance + * reads its own fields through its snapshot; a member its class declares + * (a getter or a method) has no runtime view here and is refused. */ +static ScrDyn *scr_als_option(ScrDyn *options, const char *key, size_t key_len) { + ScrStr *name = scr_str_new(key, key_len); + if (options->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_class_has(options, name)) { + char message[160]; + snprintf(message, sizeof message, + "AsyncLocalStorage options whose class declares '%s' are not supported yet", key); + scr_str_release(name); + scr_throw_error_msg(SCR_ERR_ERROR, message, strlen(message)); + return NULL; + } + ScrDyn *key_value = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *value = scr_dyn_reflect_get(options, key_value, options); + scr_dyn_release(key_value); + if (value == NULL && !scr_exc_pending()) value = scr_dyn_retain(scr_dyn_undefined()); + return value; +} + /* new AsyncLocalStorage(options): Node validates that options is an - * object, then keeps options.defaultValue and options.name. NaN with the - * exception pending when options is not an object. */ + * object, then reads options.defaultValue once and options.name twice + * (whether it is defined, then its value), keeping String(name). NaN with + * the exception pending when options is not an object or a read throws. */ double scr_als_new_options(const ScrDyn *options) { if (options->kind == SCR_DYN_UNDEF) return scr_als_new(); - /* A typed record or class instance reads through its snapshot. */ - ScrDyn *snapshot = options->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(options) : NULL; - if (snapshot) options = snapshot; - if (options->kind != SCR_DYN_OBJ) { + if (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_TYPED_REF && options->kind != SCR_DYN_PROXY) { scr_dyn_arg_type_fail("options", "of type object", options); - scr_dyn_release(snapshot); + return NAN; + } + ScrDyn *value = scr_als_option((ScrDyn *)options, "defaultValue", 12); + if (!value) return NAN; + ScrDyn *name = scr_als_option((ScrDyn *)options, "name", 4); + if (name && name->kind != SCR_DYN_UNDEF) { + scr_dyn_release(name); + name = scr_als_option((ScrDyn *)options, "name", 4); + if (name && name->kind == SCR_DYN_SYMBOL) { + static const char message[] = "Cannot convert a Symbol value to a string"; + scr_dyn_release(name); + name = NULL; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } else if (name) { + ScrStr *text = scr_dyn_string_coerce_js(name); + scr_dyn_release(name); + name = text ? scr_dyn_new_str(text) : NULL; + scr_str_release(text); + } + } else { + scr_dyn_release(name); + name = scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); + } + if (!name) { + scr_dyn_release(value); return NAN; } double id = scr_als_new(); @@ -94,12 +138,9 @@ double scr_als_new_options(const ScrDyn *options) { scr_als_options = grown; scr_als_options_len = len; } - ScrDyn *value = scr_dyn_obj_get(options, "defaultValue", 12); - ScrDyn *name = scr_dyn_obj_get(options, "name", 4); - scr_als_options[index].default_value = scr_dyn_retain(value ? value : scr_dyn_undefined()); - scr_als_options[index].name = - name && name->kind != SCR_DYN_UNDEF ? scr_dyn_retain(name) : NULL; - scr_dyn_release(snapshot); + scr_als_options[index].default_value = value; /* the read's +1 moves */ + scr_als_options[index].name = name->kind != SCR_DYN_UNDEF ? name : NULL; + if (name->kind == SCR_DYN_UNDEF) scr_dyn_release(name); return id; } @@ -235,7 +276,10 @@ static ScrDyn *scr_als_snapshot_call(ScrClosure *self, ScrDyn *const *args, size ScrAlsCtx **slot = SCR_ALS_SLOT(); ScrAlsCtx *prev = *slot; *slot = scr_box_get_ref(self->caps[0]); /* the slot owns this reference */ + /* Node applies cb to an undefined receiver, whatever the runner's own. */ + scr_dyn_this_push_dyn(scr_dyn_undefined()); ScrDyn *result = scr_dyn_call(cb, args + 1, argc - 1, "callback"); + scr_dyn_this_pop(); scr_als_restore(prev); return result; } diff --git a/tests/corpus/7111-als-bind-snapshot-options.ts b/tests/corpus/7111-als-bind-snapshot-options.ts index d1c6dbbf2..cad93cd5c 100644 --- a/tests/corpus/7111-als-bind-snapshot-options.ts +++ b/tests/corpus/7111-als-bind-snapshot-options.ts @@ -48,6 +48,13 @@ const unnamed = new AsyncLocalStorage(); console.log(JSON.stringify(unnamed.name), unnamed.getStore()); const empty = new AsyncLocalStorage({}); console.log(JSON.stringify(empty.name), empty.getStore()); +// A class instance's fields are options too. +class StoreOptions { + defaultValue = 7; + name = "from-class"; +} +const fromClass = new AsyncLocalStorage(new StoreOptions()); +console.log(fromClass.name, fromClass.getStore()); // Bound functions and snapshots carry the context across asynchronous work. const later = store.run("timer", () => AsyncLocalStorage.bind(() => store.getStore())); diff --git a/tests/corpus/7112-als-bind-snapshot-dynamic.mjs b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs index 22e3238b8..1e20aefd3 100644 --- a/tests/corpus/7112-als-bind-snapshot-dynamic.mjs +++ b/tests/corpus/7112-als-bind-snapshot-dynamic.mjs @@ -43,6 +43,49 @@ console.log(named.name, named.getStore()); named.run(5, () => console.log(named.getStore())); console.log(new AsyncLocalStorage(undefined).name === ""); +// The options are read as JavaScript reads properties: inherited values +// and getters count, defaultValue is read once and name twice, and the +// name becomes a string. +const inherited = new AsyncLocalStorage(Object.create({ defaultValue: "inherited", name: "proto" })); +console.log(inherited.getStore(), inherited.name); +const reads = []; +const viaGetters = new AsyncLocalStorage({ + get defaultValue() { + reads.push("defaultValue"); + return "getter"; + }, + get name() { + reads.push("name"); + return "getter-name"; + }, +}); +console.log(viaGetters.getStore(), viaGetters.name, reads.join(",")); +const coerced = [5, null, { toString: () => "object" }].map((name) => new AsyncLocalStorage({ name }).name); +console.log(JSON.stringify(coerced)); +for (const options of [ + { + get defaultValue() { + throw new Error("from a getter"); + }, + }, + { name: Symbol("s") }, +]) { + try { + new AsyncLocalStorage(options); + console.log("constructed"); + } catch (error) { + console.log(error.name, error.message); + } +} + +// A snapshot calls its callback with an undefined receiver, also when the +// snapshot itself is called as a method. +const kind = function () { + return this === undefined ? "undefined" : typeof this; +}; +const holder = { snap, tag: "holder" }; +console.log(snap(kind), holder.snap(kind), snap.call({ tag: "receiver" }, kind)); + // An untyped callback runs in the stores of its call. store.run("readFile", () => readFile(import.meta.filename, "utf8", (error, text) => From de6dd133708f11851621487aa6ddc49c9252666c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:45:31 -0500 Subject: [PATCH 006/409] Run net, tls, http and https events in Node's AsyncLocalStorage contexts The runtime fired every socket, server, request and response event from the event loop with no store. Node runs them in the context of the resource or request that produced them, so getStore() answered undefined in listeners where Node answers the caller's store. Handles now keep the context Node uses and the runtime enters it around each dispatch: - a socket the context of its creation (net.connect, tls.connect, an http request's socket), for 'connect', 'data', 'end', 'close', 'error' and 'timeout'; - a server the contexts of its listen() call ('listening', 'connection', listen errors) and its close() call ('close' and close callbacks when no connection is open; a close that waits for the last connection settles in that connection's dispatch, as in Node); - socket write() callbacks and end() callbacks with 'finish' the contexts of their calls; - http server requests and responses, which are created in their connection's dispatch, the server's listen() context, and client requests and their responses the http.request() context, including a keep-alive socket an agent hands to a later request. An accepted net socket runs its own events with no store, as in Node; the sockets of an HTTP or TLS server run in the server's listen() context. Events and callbacks of other streams are unchanged. The compiler is unchanged. Programs without net or http link the same runtime code; net and http programs gain the capture and dispatch helpers (about 1.7 KB of text in a net program, 2.2 KB in an http server), and bench:runtime http-api is neutral. Corpus 7119 and 7380. --- .../builtin-apis/asynclocalstorage.mdx | 5 +- .../generated/node-v24-internal.json | 34 ++- internal/compatibility/static-support.json | 2 +- .../7119-als-net-event-context.mjs.json | 6 + .../7380-als-http-event-context.mjs.json | 6 + packages/runtime/src/scr_async.c | 11 + packages/runtime/src/scr_async.h | 9 + packages/runtime/src/scr_http.c | 40 +++ packages/runtime/src/scr_net.c | 252 +++++++++++++----- packages/runtime/src/scr_net.h | 4 + tests/corpus/7119-als-net-event-context.mjs | 124 +++++++++ tests/corpus/7380-als-http-event-context.mjs | 151 +++++++++++ .../7119-als-net-event-context.mjs.json | 1 + .../7380-als-http-event-context.mjs.json | 1 + 14 files changed, 573 insertions(+), 73 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json create mode 100644 tests/corpus/7119-als-net-event-context.mjs create mode 100644 tests/corpus/7380-als-http-event-context.mjs create mode 100644 tests/harness/corpus-costs/7119-als-net-event-context.mjs.json create mode 100644 tests/harness/corpus-costs/7380-als-http-event-context.mjs.json diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx index 99f89d65c..98fc92ea1 100644 --- a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -1,4 +1,7 @@ - **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node. The completion callbacks of `fs`, `crypto` and `zlib` callback APIs, `dns.lookup`, `child_process.execFile`, `server.listen`, `net.connect`, `http.request`, `http.get` and `dgram` run with the stores that were active at the call, as in Node. - Event listeners on sockets, requests, responses and streams, stream callbacks such as `write(chunk, callback)`, `server.close(callback)`, and the callbacks of `https` and `tls` calls run with no store, where Node runs them in the context of the resource or of the code that triggers them. + Events of `net`, `tls`, `http` and `https` sockets, servers, requests and responses run with the stores Node runs them with. + A socket's or client request's events see the context of the call that created it, and a server's events and its requests' and responses' events see the context of its `listen()` call. + Socket `write` and `end` callbacks, `'finish'` and `server.close(callback)` see the context of their call. + Events and callbacks of other streams run with no store, where Node runs them in the context of the code that triggers them. `AsyncResource` has no lowering yet. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 34fe2a843..6046daf0e 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1390,7 +1390,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "chapter" @@ -1443,7 +1445,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1519,7 +1523,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1549,7 +1555,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1579,7 +1587,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1609,7 +1619,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1639,7 +1651,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" @@ -1946,6 +1960,8 @@ "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs", "tests/harness/node-test.test.ts" ] }, @@ -2511,7 +2527,9 @@ "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", - "tests/corpus/7112-als-bind-snapshot-dynamic.mjs" + "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", + "tests/corpus/7119-als-net-event-context.mjs", + "tests/corpus/7380-als-http-event-context.mjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index efc60f263..c57d6013e 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -429,7 +429,7 @@ { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json new file mode 100644 index 000000000..7ff81ea34 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7119-als-net-event-context.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7119-als-net-event-context.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json new file mode 100644 index 000000000..1087d25fe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-als-http-event-context.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7380-als-http-event-context.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 992ec3767..90e7dd04f 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -774,6 +774,17 @@ void scr_als_restore(ScrAlsCtx *prev) { *SCR_ALS_SLOT() = prev; /* ownership moves back in */ } +ScrAlsCtx *scr_als_dispatch_enter(ScrAlsCtx *ctx) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_als_ctx_retain(ctx); + return prev; +} + +void scr_als_dispatch_leave(ScrAlsCtx *prev) { scr_als_restore(prev); } + +ScrAlsCtx *scr_als_capture(void) { return scr_als_ctx_retain(*SCR_ALS_SLOT()); } + /* The value of `id` in the active snapshot (borrowed), or NULL. */ ScrDyn *scr_als_peek(double id) { const ScrAlsCtx *c = *SCR_ALS_SLOT(); diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index 003bf3be8..25f25a842 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -192,6 +192,15 @@ ScrDyn *scr_als_get(double id); /* +1; the undefined value when unset */ ScrAlsCtx *scr_als_enter(double id, ScrDyn *value); /* borrows value; prev out (owned) */ ScrAlsCtx *scr_als_enter_absent(double id); /* exit()'s cleared arm */ void scr_als_restore(ScrAlsCtx *prev); /* moves prev back in */ +/* A runtime dispatch (an I/O event the loop delivers, a deferred emit) + * runs its listeners in the AsyncLocalStorage context of the object or + * request Node would run them in: enter installs `ctx` (borrowed, NULL + * for no store) and answers the previous context, which leave puts back. + * A store a listener enters (enterWith) ends with the dispatch. */ +ScrAlsCtx *scr_als_dispatch_enter(ScrAlsCtx *ctx); +void scr_als_dispatch_leave(ScrAlsCtx *prev); +/* The active context, +1 (NULL outside a store): what a handle keeps. */ +ScrAlsCtx *scr_als_capture(void); void scr_als_enter_with(double id, ScrDyn *value); /* no restore point (enterWith) */ void scr_als_disable(double id); /* run/exit: enter (or clear), call the dyn function with the forwarded diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index a5847e59b..18d997fb5 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -209,6 +209,11 @@ void scr_http_set_h2_upgrade_hook(void (*hook)(void *h2ctx, ScrClosure *cb, void struct ScrHttpReq { size_t rc; + /* The AsyncLocalStorage context of its creation (+1, NULL outside a + * store): the deferred emits run in it (scr_http_proto_sweep). A server + * request is created in its connection's dispatch, which runs in the + * server's listen() context; a client response in its request's. */ + ScrAlsCtx *als; int status; /* -1 on server requests (Node's undefined statusCode) */ ScrStr *method; ScrStr *url; @@ -301,6 +306,7 @@ void scr_http_req_set_encoding(ScrHttpReq *r, ScrStr *enc /*borrowed*/) { void scr_http_req_release(ScrHttpReq *r) { if (!r || r->rc == SIZE_MAX) return; if (--r->rc == 0) { + scr_als_ctx_release(r->als); scr_str_release(r->method); scr_str_release(r->url); for (size_t i = 0; i < r->nheaders; i++) { @@ -928,6 +934,7 @@ static bool scr_http_trailers_replace(ScrHttpTrailers *t, ScrArr *pairs /*borrow struct ScrHttpRes { size_t rc; + ScrAlsCtx *als; /* +1: as ScrHttpReq's */ ScrNetSocket *sock; /* +1; writes after the connection died are no-ops */ void *h2_stream; /* +1 through scr_http_h2_ops — set on h2 compat * responses; every write path routes to frames */ @@ -985,6 +992,7 @@ ScrHttpRes *scr_http_res_retain(ScrHttpRes *r) { void scr_http_res_release(ScrHttpRes *r) { if (!r || r->rc == SIZE_MAX) return; if (--r->rc == 0) { + scr_als_ctx_release(r->als); for (size_t i = 0; i < r->nheaders; i++) { scr_str_release(r->hnames[i]); scr_str_release(r->hvalues[i]); @@ -2257,9 +2265,25 @@ void scr_http_res_on_write_flush(ScrHttpRes *r, ScrClosure *cb /*moves*/) { static bool scr_http_proto_pending(void) { return scr_http_emits_head < scr_http_emits_len; } +static ScrAlsCtx *scr_http_client_ctx(struct ScrHttpClientReq *c); + +/* The context an emit's handle was created in (borrowed). */ +static ScrAlsCtx *scr_http_emit_ctx(const ScrHttpEmit *e) { + switch (e->kind) { + case SCR_HTTP_EMIT_RES_CLOSE: + case SCR_HTTP_EMIT_RES_WCB: + case SCR_HTTP_EMIT_RES_FINISH: return ((ScrHttpRes *)e->h)->als; + case SCR_HTTP_EMIT_REQ_CLOSE: + case SCR_HTTP_EMIT_REQ_DRAIN: + case SCR_HTTP_EMIT_REQ_ABORTED: return ((ScrHttpReq *)e->h)->als; + default: return scr_http_client_ctx((struct ScrHttpClientReq *)e->h); + } +} + static void scr_http_proto_sweep(void) { while (scr_http_emits_head < scr_http_emits_len) { ScrHttpEmit e = scr_http_emits[scr_http_emits_head++]; + ScrAlsCtx *prev = scr_als_dispatch_enter(scr_http_emit_ctx(&e)); switch (e.kind) { case SCR_HTTP_EMIT_RES_CLOSE: { ScrHttpRes *res = (ScrHttpRes *)e.h; @@ -2349,6 +2373,7 @@ static void scr_http_proto_sweep(void) { break; } } + scr_als_dispatch_leave(prev); scr_http_emit_release(&e); if (scr_exc_pending()) return; } @@ -2870,6 +2895,7 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o ScrHttpReq *req = calloc(1, sizeof *req); if (!req) scr_http_oom(); req->rc = 1; + req->als = scr_als_capture(); req->status = -1; /* Node: statusCode is undefined on server requests */ req->gated = true; req->join_dup = conn->srv != NULL && conn->srv->join_dup; @@ -3038,6 +3064,7 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len, bool *o ScrHttpRes *res = calloc(1, sizeof *res); if (!res) scr_http_oom(); res->rc = 1; + res->als = scr_als_capture(); #ifdef SCR_RC_AUDIT scr_http_live++; #endif @@ -3590,6 +3617,7 @@ ScrHttpReq *scr_http_h2_req_new(ScrNetSocket *sock /*borrowed, nullable*/, ScrHttpReq *req = calloc(1, sizeof *req); if (!req) scr_http_oom(); req->rc = 1; + req->als = scr_als_capture(); req->status = -1; /* server requests: statusCode is Node's undefined */ req->http2 = true; req->sock = sock ? scr_net_sock_retain(sock) : NULL; @@ -3661,6 +3689,7 @@ ScrHttpRes *scr_http_h2_res_new(ScrNetSocket *sock /*borrowed, nullable*/, ScrHttpRes *res = calloc(1, sizeof *res); if (!res) scr_http_oom(); res->rc = 1; + res->als = scr_als_capture(); res->sock = sock ? scr_net_sock_retain(sock) : NULL; res->h2_stream = scr_http_h2_ops->retain(stream); res->keep_alive = true; /* inert on h2 — no Connection header exists */ @@ -3770,6 +3799,9 @@ void scr_http_handler_thunk0(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res) { struct ScrHttpClientReq { size_t rc; + /* The http.request() call's AsyncLocalStorage context (+1): the + * request's deferred emits ('socket', 'finish', 'close') run in it. */ + ScrAlsCtx *als; ScrNetSocket *sock; /* +1 */ ScrHttpConn *conn; /* borrowed backref (the socket's ctx); NULL after free */ ScrStr *host, *path, *method; @@ -3834,6 +3866,7 @@ ScrHttpClientReq *scr_http_client_retain(ScrHttpClientReq *c) { void scr_http_client_release(ScrHttpClientReq *c) { if (!c || c->rc == SIZE_MAX) return; if (--c->rc == 0) { + scr_als_ctx_release(c->als); scr_net_ls_drop(&c->resp_ls); scr_net_ls_drop(&c->err_ls); scr_net_ls_drop(&c->timeout_ls); @@ -3872,6 +3905,8 @@ static void scr_http_client_release_internal(struct ScrHttpClientReq *c) { scr_http_client_release(c); } +static ScrAlsCtx *scr_http_client_ctx(struct ScrHttpClientReq *c) { return c->als; } + void *scr_http_client_retain_v(void *p) { return scr_http_client_retain((ScrHttpClientReq *)p); } void scr_http_client_release_v(void *p) { scr_http_client_release((ScrHttpClientReq *)p); } @@ -4648,6 +4683,7 @@ static bool scr_http_client_parse_head(ScrHttpConn *conn, size_t head_len) { ScrHttpReq *res = calloc(1, sizeof *res); if (!res) scr_http_oom(); res->rc = 1; + res->als = scr_als_capture(); res->status = status; res->sock = scr_net_sock_retain(conn->sock); #ifdef SCR_RC_AUDIT @@ -5041,6 +5077,7 @@ static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double ScrHttpClientReq *c = calloc(1, sizeof *c); if (!c) scr_http_oom(); c->rc = 1; + c->als = scr_als_capture(); #ifdef SCR_RC_AUDIT scr_http_live++; #endif @@ -5086,6 +5123,7 @@ static ScrHttpClientReq *scr_http_request_impl(ScrStr *host /*borrowed*/, double if (presock != NULL) { c->sock = presock; + scr_net_sock_set_context(c->sock, c->als); } else { ScrStr *dial = scr_net_blocking_lookup(host); c->sock = scr_net_connect(port, dial, NULL); @@ -5344,6 +5382,8 @@ static void scr_http_client_rebind(ScrHttpClientReq *c, ScrHttpAgentFree *f, boo scr_net_sock_release(old); ScrHttpConn *conn = f->conn; c->sock = f->sock; /* the free entry's +1 moves */ + /* Node runs a pooled socket's events for this request in its context. */ + scr_net_sock_set_context(c->sock, c->als); c->conn = conn; conn->client = scr_http_client_retain(c); conn->pool_agent = NULL; diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 29a4af393..33374b97c 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -420,6 +420,11 @@ enum { SCR_NET_K_SERVER = 1, SCR_NET_K_SOCKET = 2 }; struct ScrNetServer { int kind; /* FIRST: poller udata routes on it */ size_t rc; + /* The AsyncLocalStorage contexts of the listen() and close() calls + * (+1, NULL outside a store): 'listening', 'connection' and 'error' run + * in the first, 'close' and the close() callbacks in the second, as + * Node runs them in its handle's and its close request's contexts. */ + ScrAlsCtx *listen_ctx, *close_ctx; int fd; /* -1 while not listening */ int port; bool listening; @@ -486,6 +491,14 @@ struct ScrNetServer { struct ScrNetSocket { int kind; /* FIRST: poller udata routes on it */ size_t rc; + /* The AsyncLocalStorage context of the socket's creation (+1, NULL + * outside a store): its I/O events ('connect', 'data', 'end', 'close', + * 'error', 'timeout') run in it, as Node runs them in its handle's + * context. An accepted socket has none, except under an HTTP or TLS + * server, whose sockets take the server's listen() context. end_ctx is + * the end() call's context, for 'finish' and the end() callback (Node's + * shutdown request). */ + ScrAlsCtx *als, *end_ctx; int fd; /* -1 once closed */ bool unrefd; /* socket.unref(): does not keep the loop alive */ bool connecting; @@ -668,6 +681,13 @@ static SCR_TL ScrPoller *scr_net_poller = NULL; static void scr_net_close_fd_raw(int fd); /* forget-then-close, defined below */ +/* The listen() call's context: the server's 'listening', 'connection' + * and 'error' events run in it. */ +static void scr_net_server_capture_listen(ScrNetServer *s) { + scr_als_ctx_release(s->listen_ctx); + s->listen_ctx = scr_als_capture(); +} + ScrNetServer *scr_net_server_retain(ScrNetServer *s) { if (s->rc != SIZE_MAX) s->rc++; return s; @@ -683,6 +703,8 @@ void scr_net_server_release(ScrNetServer *s) { scr_net_ls_drop(&s->listening_cbs); scr_net_ls_drop(&s->timeout_ls); scr_net_ls_drop(&s->drop_ls); + scr_als_ctx_release(s->listen_ctx); + scr_als_ctx_release(s->close_ctx); scr_closure_release(s->close_override); for (size_t i = 0; i < 5; i++) scr_dyn_release(s->http_timeout_dyn[i]); scr_dyn_release(s->drop_max_connection); @@ -721,6 +743,8 @@ void scr_net_sock_release(ScrNetSocket *s) { scr_net_ls_drop(&s->finish_ls); scr_net_ls_drop(&s->wcb_ls); scr_net_ls_drop(&s->ready_ls); + scr_als_ctx_release(s->als); + scr_als_ctx_release(s->end_ctx); scr_str_release(s->pending_err); scr_str_release(s->remote_cache); for (size_t i = 0; i < s->dial_n; i++) scr_str_release(s->dial_ips[i]); @@ -965,6 +989,7 @@ static ScrNetSocket *scr_net_sock_new(void) { s->kind = SCR_NET_K_SOCKET; s->rc = 1; s->fd = -1; + s->als = scr_als_capture(); #ifdef SCR_RC_AUDIT scr_net_live++; #endif @@ -1675,6 +1700,7 @@ ScrNetServer *scr_net_create_server(ScrClosure *handler /*moves, nullable*/, Scr * listen: IPv6 any with dual-stack when the kernel allows, IPv4 fallback. */ static void scr_net_listen_any(ScrNetServer *s, double port, bool reuse_port, ScrClosure *cb /*moves, nullable*/) { + scr_net_server_capture_listen(s); if (cb) scr_net_ls_add(&s->listening_cbs, cb, NULL, true); if (s->listening || s->close_emitted) return; if (!scr_net_poller_init()) { @@ -1753,6 +1779,7 @@ void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullab static void scr_net_listen_opts_impl(ScrNetServer *s, double port, ScrStr *host /*borrowed*/, bool ipv6_only, bool reuse_port, ScrClosure *cb /*moves, nullable*/) { + scr_net_server_capture_listen(s); if (host == NULL || host->len == 0) { scr_net_listen_any(s, port, reuse_port, cb); return; @@ -1968,6 +1995,8 @@ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullab scr_closure_release(cb); return; } + scr_als_ctx_release(s->close_ctx); + s->close_ctx = scr_als_capture(); if (cb) scr_net_ls_add(&s->close_ls, cb, NULL, true); if (s->listening) { scr_net_close_fd_raw(s->fd); /* stops accepting NOW (registrations dropped first) */ @@ -2225,6 +2254,13 @@ static void scr_net_server_accept(ScrNetServer *srv) { continue; } ScrNetSocket *sock = scr_net_sock_new(); + /* Node runs an accepted socket's own events with no store, except + * under the protocol layer (an HTTP or TLS server), whose connections + * run in the listen() context this accept runs in. */ + if (!srv->native_conn && !srv->defer_conn) { + scr_als_ctx_release(sock->als); + sock->als = NULL; + } sock->fd = fd; sock->server = scr_net_server_retain(srv); srv->nconns++; @@ -2776,6 +2812,7 @@ void scr_net_server_address_check(ScrNetServer *s) { * deferred 'error', success the deferred 'listening', as for a port. */ void scr_net_listen_path(ScrNetServer *s, ScrStr *path /*borrowed*/, ScrClosure *cb /*moves, nullable*/) { + scr_net_server_capture_listen(s); /* Node's isPipeName: a string that reads as a non-negative number is a port */ double port = scr_string_to_number(path); if (port >= 0) { @@ -3078,6 +3115,8 @@ void scr_net_sock_end(ScrNetSocket *s) { } if (s->fd < 0 || s->wr_ending) return; s->wr_ending = true; + scr_als_ctx_release(s->end_ctx); + s->end_ctx = scr_als_capture(); scr_net_sock_maybe_finish_write(s); if (s->rd_eof && s->wr_done) scr_net_sock_close_fd(s); } @@ -3175,6 +3214,62 @@ void scr_net_sock_on_finish(ScrNetSocket *s, ScrClosure *cb /*moves*/) { if (s->wr_done) s->finish_pending = true; /* already finished: next sweep */ } +/* The context the socket's events run in from now on (borrowed): an HTTP + * agent hands a pooled socket to another request. */ +void scr_net_sock_set_context(ScrNetSocket *s, ScrAlsCtx *ctx) { + ScrAlsCtx *next = scr_als_ctx_retain(ctx); + scr_als_ctx_release(s->als); + s->als = next; +} + +/* A write callback runs in the write() call's AsyncLocalStorage context + * (Node's write request): with a store active, the list holds a closure + * that enters it around the callback. caps[0] is the callback, caps[1] + * the context. */ +static void *scr_net_ctx_retain_v(void *c) { return scr_als_ctx_retain(c); } +static void scr_net_ctx_release_v(void *c) { scr_als_ctx_release(c); } + +static void scr_net_ctx_fire0(ScrClosure *self) { + ScrClosure *cb = scr_box_get_ref(self->caps[0]); + ScrAlsCtx *ctx = scr_box_get_ref(self->caps[1]); + ScrAlsCtx *prev = scr_als_dispatch_enter(ctx); + ((void (*)(ScrClosure *))cb->fn)(cb); + scr_als_dispatch_leave(prev); + scr_als_ctx_release(ctx); + scr_closure_release(cb); +} + +static void scr_net_dyn_fire_arg(ScrClosure *cb, const ScrDyn *arg); + +static void scr_net_ctx_fire_arg(ScrClosure *self, const ScrDyn *arg) { + ScrClosure *cb = scr_box_get_ref(self->caps[0]); + ScrAlsCtx *ctx = scr_box_get_ref(self->caps[1]); + ScrAlsCtx *prev = scr_als_dispatch_enter(ctx); + scr_net_dyn_fire_arg(cb, arg); + scr_als_dispatch_leave(prev); + scr_als_ctx_release(ctx); + scr_closure_release(cb); +} + +/* Adds a write callback (moves) with its firing adapter (NULL: a + * zero-argument closure; scr_net_dyn_fire_arg: a dyn listener). */ +static __attribute__((noinline)) ScrClosure *scr_net_bind_write_cb(ScrClosure *cb /*moves*/, void **fn, + ScrAlsCtx *active) { + ScrClosure *bound = scr_closure_new(*fn == NULL ? (void *)scr_net_ctx_fire0 : NULL, 2); + bound->caps[0] = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(bound->caps[0], cb); + bound->caps[1] = scr_box_new_obj(scr_net_ctx_retain_v, scr_net_ctx_release_v, NULL); + scr_box_set_ref(bound->caps[1], scr_als_ctx_retain(active)); + if (*fn != NULL) *fn = (void *)&scr_net_ctx_fire_arg; + return bound; +} + +static void scr_net_sock_add_write_cb(ScrNetSocket *s, ScrClosure *cb, void *fn) { + ScrAlsCtx *active = *SCR_ALS_SLOT(); + if (active != NULL) cb = scr_net_bind_write_cb(cb, &fn, active); + scr_net_ls_add(&s->wcb_ls, cb, fn, true); +} + /* write(chunk, callback): fires when the write buffer drains (this * surface's flush moment), from the sweep. */ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) { @@ -3182,7 +3277,7 @@ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) { scr_closure_release(cb); return; } - scr_net_ls_add(&s->wcb_ls, cb, NULL, true); + scr_net_sock_add_write_cb(s, cb, NULL); } double scr_net_sock_bytes_written(ScrNetSocket *s) { return (double)s->bytes_written; } @@ -4584,8 +4679,8 @@ void scr_net_server_close_dyn(ScrNetServer *s, const ScrDyn *listener /*borrowed void scr_net_sock_on_write_flush_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/) { if (s->close_emitted) return; - scr_net_ls_add(&s->wcb_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), - (void *)&scr_net_dyn_fire_arg, true); + scr_net_sock_add_write_cb(s, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg); } /* socket.setTimeout(ms[, callback]) as the program calls it: Node's @@ -5382,7 +5477,9 @@ static void scr_net_sweep(void) { ((sock->tops && sock->t_est && sock->tops->pending && sock->tops->pending(sock->tctx)) || (!sock->tops && sock->rlen > sock->rhead))) { /* deliver bytes the kernel can't re-signal (see flags_pending) */ + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); scr_net_sock_read(sock); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_sock_release(sock); return; @@ -5404,14 +5501,18 @@ static void scr_net_sweep(void) { } if (sock->finish_pending) { sock->finish_pending = false; + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->end_ctx); scr_net_fire0_this(&sock->finish_ls, sock, SCR_DYNH_NET_SOCKET); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_sock_release(sock); return; } } if (sock->lookup_evs != NULL && sock->pending_err) { + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); scr_net_lookup_ops->flush(sock); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_sock_release(sock); return; @@ -5425,6 +5526,7 @@ static void scr_net_sweep(void) { * teardown without an event); unconsumed errors fall through to the socket's own * listeners — and the unhandled-'error' exit */ bool consumed = false; + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); if (sock->native_err) consumed = sock->native_err(sock->native_ctx, msg); if (!consumed && !scr_exc_pending()) { ScrError *coded = NULL; @@ -5438,6 +5540,7 @@ static void scr_net_sweep(void) { scr_event_error_payload = saved; if (coded != NULL) scr_error_release(coded); } + scr_als_dispatch_leave(prev); sock->pending_code = NULL; scr_str_release(msg); if (scr_exc_pending()) { @@ -5446,7 +5549,10 @@ static void scr_net_sweep(void) { } } if (sock->emit_close && !sock->close_emitted && sock->fd < 0) { - if (!scr_net_sock_settle(sock)) { + ScrAlsCtx *prev = scr_als_dispatch_enter(sock->als); + bool settled = scr_net_sock_settle(sock); + scr_als_dispatch_leave(prev); + if (!settled) { scr_net_sock_release(sock); return; } @@ -5460,7 +5566,9 @@ static void scr_net_sweep(void) { scr_net_server_retain(srv); if (srv->emit_listening) { srv->emit_listening = false; + ScrAlsCtx *prev = scr_als_dispatch_enter(srv->listen_ctx); scr_net_fire0_this(&srv->listening_cbs, srv, SCR_DYNH_NET_SERVER); + scr_als_dispatch_leave(prev); if (scr_exc_pending()) { scr_net_server_release(srv); return; @@ -5469,7 +5577,9 @@ static void scr_net_sweep(void) { if (srv->pending_err) { ScrStr *msg = srv->pending_err; srv->pending_err = NULL; + ScrAlsCtx *prev = scr_als_dispatch_enter(srv->listen_ctx); scr_net_fire_err_this(&srv->err_ls, msg, srv, SCR_DYNH_NET_SERVER); + scr_als_dispatch_leave(prev); scr_str_release(msg); /* A failed listen SETTLES the handle: listeners drop (an error * listener capturing its own server would otherwise cycle forever — @@ -5505,7 +5615,9 @@ static void scr_net_sweep(void) { } if (due == NULL) return; scr_net_server_retain(due); + ScrAlsCtx *prev = scr_als_dispatch_enter(due->close_ctx); scr_net_server_settle(due); + scr_als_dispatch_leave(prev); scr_net_server_release(due); if (scr_exc_pending()) return; } @@ -5539,6 +5651,69 @@ bool scr_loop_io_take(void); bool scr_loop_io_more(void); void scr_loop_io_reopen(void); +/* One poller event of a socket; false with an exception pending. */ +static bool scr_net_sock_event(ScrNetSocket *s, unsigned events) { + if (events & SCRP_TIMER) { + /* the idle period elapsed: 'timeout' fires once (one-shot — the + * next activity re-arms); the socket stays open, like Node */ + s->timeout_armed = false; + if (s->native_timeout) { + s->native_timeout(s->native_ctx); + if (scr_exc_pending()) return false; + } + if (s->server && s->server->http_timeout_surface && + s->native_http_timeout) { + ScrNetServer *srv = scr_net_server_retain(s->server); + bool handled = s->native_http_timeout(s->native_ctx); + if (scr_exc_pending()) { + scr_net_server_release(srv); + return false; + } + bool server_handled = srv->timeout_ls.n > 0; + if (server_handled) { + ScrNetL *snap; + size_t count = scr_net_ls_snapshot(&srv->timeout_ls, &snap); + scr_dyn_this_push(srv, SCR_DYNH_NET_SERVER); + for (size_t j = 0; j < count; j++) { + if (!scr_exc_pending()) ((ScrNetConnFn)snap[j].fn)(snap[j].cb, scr_net_sock_retain(s)); + scr_closure_release(snap[j].cb); + } + scr_dyn_this_pop(); + free(snap); + } + if (!handled && !server_handled && !scr_exc_pending()) + scr_net_sock_destroy(s); /* Node's unhandled server timeout */ + scr_net_server_release(srv); + if (scr_exc_pending()) return false; + } + scr_net_fire0_this(&s->timeout_ls, s, SCR_DYNH_NET_SOCKET); + if (scr_exc_pending()) return false; + if (s->fd < 0) return true; + } + /* kqueue delivers one direction per event (today's exact order); + * epoll may coalesce both into one — writes first (connect + * completion, flushes), then reads, re-checking the fd a write + * failure may have closed. */ + if (events & SCRP_WRITABLE) { + if (s->connecting) scr_net_sock_connect_done(s); + else if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); + else if (s->tops && !s->t_est) { + s->t_want_write = false; + scr_net_sock_transport_pump(s); + if (!scr_exc_pending() && s->fd >= 0) scr_net_sock_update_write(s); + } else { + s->t_want_write = false; + scr_net_sock_flush(s); + } + } + if ((events & SCRP_READABLE) && !scr_exc_pending() && s->fd >= 0) { + if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); + else if (s->tops && !s->t_est) scr_net_sock_transport_pump(s); + else scr_net_sock_read(s); + } + return true; +} + static void scr_net_dispatch(void) { if (!scr_net_servers && !scr_net_socks && !scr_net_proto_pending()) { @@ -5564,71 +5739,22 @@ static void scr_net_dispatch(void) { if (!udata) continue; int kind = *(int *)udata; if (kind == SCR_NET_K_SERVER) { - scr_net_server_accept((ScrNetServer *)udata); + /* 'connection' runs in the context of the listen() call. */ + ScrNetServer *srv = (ScrNetServer *)udata; + ScrAlsCtx *prev = scr_als_dispatch_enter(srv->listen_ctx); + scr_net_server_accept(srv); + scr_als_dispatch_leave(prev); } else if (kind == SCR_NET_K_SOCKET) { ScrNetSocket *s = (ScrNetSocket *)udata; /* A callback earlier in this batch may have closed this fd * (destroy inside a data listener) — its remaining events are * stale; the handle itself stays alive until the sweep. */ if (s->fd < 0) continue; - if (evs[i].events & SCRP_TIMER) { - /* the idle period elapsed: 'timeout' fires once (one-shot — the - * next activity re-arms); the socket stays open, like Node */ - s->timeout_armed = false; - if (s->native_timeout) { - s->native_timeout(s->native_ctx); - if (scr_exc_pending()) return; - } - if (s->server && s->server->http_timeout_surface && - s->native_http_timeout) { - ScrNetServer *srv = scr_net_server_retain(s->server); - bool handled = s->native_http_timeout(s->native_ctx); - if (scr_exc_pending()) { - scr_net_server_release(srv); - return; - } - bool server_handled = srv->timeout_ls.n > 0; - if (server_handled) { - ScrNetL *snap; - size_t count = scr_net_ls_snapshot(&srv->timeout_ls, &snap); - scr_dyn_this_push(srv, SCR_DYNH_NET_SERVER); - for (size_t j = 0; j < count; j++) { - if (!scr_exc_pending()) ((ScrNetConnFn)snap[j].fn)(snap[j].cb, scr_net_sock_retain(s)); - scr_closure_release(snap[j].cb); - } - scr_dyn_this_pop(); - free(snap); - } - if (!handled && !server_handled && !scr_exc_pending()) - scr_net_sock_destroy(s); /* Node's unhandled server timeout */ - scr_net_server_release(srv); - if (scr_exc_pending()) return; - } - scr_net_fire0_this(&s->timeout_ls, s, SCR_DYNH_NET_SOCKET); - if (scr_exc_pending()) return; - if (s->fd < 0) continue; - } - /* kqueue delivers one direction per event (today's exact order); - * epoll may coalesce both into one — writes first (connect - * completion, flushes), then reads, re-checking the fd a write - * failure may have closed. */ - if (evs[i].events & SCRP_WRITABLE) { - if (s->connecting) scr_net_sock_connect_done(s); - else if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); - else if (s->tops && !s->t_est) { - s->t_want_write = false; - scr_net_sock_transport_pump(s); - if (!scr_exc_pending() && s->fd >= 0) scr_net_sock_update_write(s); - } else { - s->t_want_write = false; - scr_net_sock_flush(s); - } - } - if ((evs[i].events & SCRP_READABLE) && !scr_exc_pending() && s->fd >= 0) { - if (s->tunnel != NULL) scr_net_sock_tunnel_pump(s); - else if (s->tops && !s->t_est) scr_net_sock_transport_pump(s); - else scr_net_sock_read(s); - } + /* The socket's events run in its AsyncLocalStorage context. */ + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + bool ok = scr_net_sock_event(s, evs[i].events); + scr_als_dispatch_leave(prev); + if (!ok) return; } if (scr_exc_pending()) return; } diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index ffa9b2f72..d31168a33 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -36,6 +36,10 @@ typedef struct { } ScrNetLs; void scr_net_ls_add(ScrNetLs *l, ScrClosure *cb, void *fn, bool once); +/* The AsyncLocalStorage context a socket's events run in from now on + * (borrowed; scr_net.c keeps the creation context otherwise). */ +struct ScrAlsCtx; +void scr_net_sock_set_context(ScrNetSocket *s, struct ScrAlsCtx *ctx); ScrNetOnce *scr_net_once_new(void); /* +1 */ ScrNetOnce *scr_net_once_retain(ScrNetOnce *once); /* +1 */ void scr_net_once_release(ScrNetOnce *once); diff --git a/tests/corpus/7119-als-net-event-context.mjs b/tests/corpus/7119-als-net-event-context.mjs new file mode 100644 index 000000000..3fb95ffa8 --- /dev/null +++ b/tests/corpus/7119-als-net-event-context.mjs @@ -0,0 +1,124 @@ +// AsyncLocalStorage contexts of node:net events. A client socket's events +// run in the context of the net.connect() call, whatever context the +// listener was registered in, also after a destroy() from elsewhere. The +// server's 'listening', 'connection' and 'error' events run in the +// context of the listen() call. An accepted socket's own events run with +// no store. write() and end() callbacks, and 'finish', run in the context +// of their call. server.close(callback) runs in the close() call's +// context when no connection is open, and with no store when it waits for +// the last connection. Lines are sorted at the end: events on different +// sockets may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import net from "node:net"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +function echoScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => + net.createServer((sock) => { + log("server connection callback"); + sock.on("data", () => { + log("accepted data"); + store.run("in-data", () => sock.write("pong")); + }); + sock.on("end", () => { + log("accepted end"); + sock.end(); + }); + sock.on("close", () => log("accepted close")); + }), + ); + server.on("connection", () => log("server connection event")); + server.on("close", () => { + log("server close event"); + resolve(); + }); + store.run("listen", () => + server.listen(0, () => { + log("listen callback"); + const client = store.run("connect", () => net.connect(server.address().port)); + store.run("register", () => { + client.on("connect", () => { + log("client connect"); + store.run("write", () => client.write("ping", () => log("write callback"))); + }); + client.on("data", () => { + log("client data"); + store.run("end-call", () => client.end(() => log("end callback"))); + }); + client.on("finish", () => log("client finish")); + client.on("end", () => log("client end")); + client.on("close", () => { + log("client close"); + store.run("close-call", () => server.close(() => log("server close callback"))); + }); + }); + }), + ); + }); +} + +function destroyScenario() { + return new Promise((resolve) => { + const server = net.createServer((sock) => sock.on("data", () => {})); + server.listen(0, () => { + const client = store.run("connect-2", () => net.connect(server.address().port)); + client.on("error", () => {}); + client.on("connect", () => { + client.on("close", () => { + log("close after a destroy elsewhere"); + server.close(() => resolve()); + }); + setTimeout(() => store.run("destroy-call", () => client.destroy()), 10); + }); + }); + }); +} + +function closeScenarios() { + return new Promise((resolve) => { + let pending = 3; + const done = () => { + if (--pending === 0) resolve(); + }; + const waiting = net.createServer((sock) => { + store.run("conn", () => waiting.close(() => { + log("close callback that waited"); + done(); + })); + setTimeout(() => store.run("end", () => sock.end()), 20); + }); + store.run("listen-3", () => + waiting.listen(0, () => { + const client = net.connect(waiting.address().port); + client.on("end", () => client.end()); + }), + ); + const idle = net.createServer(); + store.run("listen-4", () => + idle.listen(0, () => { + store.run("close-now", () => idle.close(() => { + log("close callback of an idle server"); + done(); + })); + }), + ); + const taken = net.createServer(); + taken.listen(0, () => { + const clash = net.createServer(); + clash.on("error", (e) => { + log(`listen error ${e.code}`); + taken.close(() => done()); + }); + store.run("clash-listen", () => clash.listen(taken.address().port)); + }); + }); +} + +await echoScenario(); +await destroyScenario(); +await closeScenarios(); +for (const line of lines.sort()) console.log(line); diff --git a/tests/corpus/7380-als-http-event-context.mjs b/tests/corpus/7380-als-http-event-context.mjs new file mode 100644 index 000000000..a881cbe7e --- /dev/null +++ b/tests/corpus/7380-als-http-event-context.mjs @@ -0,0 +1,151 @@ +// AsyncLocalStorage contexts of node:http, node:https and node:tls events. +// A server's 'request' event and its requests' and responses' events run +// in the context of the server's listen() call. A client request's +// events, its response callback and its response's events run in the +// context of the http.request() call, also on a keep-alive socket that +// an earlier request used. TLS client sockets run in the tls.connect() +// call's context, and a TLS server's connections in its listen() +// context. Lines are sorted at the end: events on different sockets may +// interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import * as fs from "node:fs"; +import http from "node:http"; +import https from "node:https"; +import tls from "node:tls"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +function httpScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => + http.createServer((req, res) => { + log("request callback"); + req.on("data", () => log("request data")); + req.on("end", () => { + log("request end"); + res.end("ok"); + }); + res.on("finish", () => log("response finish")); + res.on("close", () => log("response close")); + }), + ); + server.on("request", () => log("request event")); + store.run("listen", () => + server.listen(0, () => { + const port = server.address().port; + const req = store.run("request-call", () => + http.request({ port, method: "POST" }, (res) => { + log("response callback"); + res.on("data", () => log("client response data")); + res.on("end", () => log("client response end")); + }), + ); + store.run("register", () => { + req.on("socket", () => log("client socket")); + req.on("finish", () => log("client finish")); + req.on("close", () => { + log("client close"); + server.close(() => resolve()); + }); + }); + store.run("end-call", () => req.end("body")); + }), + ); + }); +} + +function keepAliveScenario() { + return new Promise((resolve) => { + const agent = new http.Agent({ keepAlive: true, maxSockets: 1 }); + const server = http.createServer((req, res) => res.end("ok")); + server.listen(0, () => { + const port = server.address().port; + let done = 0; + for (const name of ["first", "second"]) { + store.run(name, () => { + const req = http.get({ port, agent }, (res) => { + log(`${name} response`); + res.on("data", () => {}); + res.on("end", () => { + log(`${name} response end`); + if (++done === 2) { + agent.destroy(); + server.close(() => resolve()); + } + }); + }); + req.on("finish", () => log(`${name} finish`)); + req.on("close", () => log(`${name} close`)); + }); + } + }); + }); +} + +const key = fs.readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const cert = fs.readFileSync("tests/fixtures/server/certs/localhost.pem"); +const ca = fs.readFileSync("tests/fixtures/server/certs/ca.pem", "utf8"); + +function httpsScenario() { + return new Promise((resolve) => { + const server = https.createServer({ key, cert }, (req, res) => { + log("https request"); + req.resume(); + res.on("finish", () => log("https response finish")); + res.end("ok"); + }); + store.run("https-listen", () => + server.listen(0, () => { + const port = server.address().port; + store.run("https-get", () => + https + .get({ port, host: "localhost", ca }, (res) => { + log("https response"); + res.on("data", () => {}); + res.on("end", () => log("https response end")); + }) + .on("close", () => { + log("https client close"); + server.close(() => resolve()); + }), + ); + }), + ); + }); +} + +function tlsScenario() { + return new Promise((resolve) => { + const server = tls.createServer({ key, cert }, (sock) => { + log("tls secureConnection"); + sock.on("data", () => { + log("tls server data"); + sock.end("bye"); + }); + }); + store.run("tls-listen", () => + server.listen(0, () => { + const client = store.run("tls-connect", () => + tls.connect({ port: server.address().port, host: "localhost", ca }, () => { + log("tls secureConnect"); + client.write("hi"); + }), + ); + client.on("data", () => log("tls client data")); + client.on("end", () => log("tls client end")); + client.on("close", () => { + log("tls client close"); + server.close(() => resolve()); + }); + }), + ); + }); +} + +await httpScenario(); +await keepAliveScenario(); +await httpsScenario(); +await tlsScenario(); +for (const line of lines.sort()) console.log(line); diff --git a/tests/harness/corpus-costs/7119-als-net-event-context.mjs.json b/tests/harness/corpus-costs/7119-als-net-event-context.mjs.json new file mode 100644 index 000000000..a57dddb65 --- /dev/null +++ b/tests/harness/corpus-costs/7119-als-net-event-context.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 12000 } diff --git a/tests/harness/corpus-costs/7380-als-http-event-context.mjs.json b/tests/harness/corpus-costs/7380-als-http-event-context.mjs.json new file mode 100644 index 000000000..a57dddb65 --- /dev/null +++ b/tests/harness/corpus-costs/7380-als-http-event-context.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 12000 } From 84cf0ec19fd9288034678b8ce9aa5c09fcdba916 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 10:54:02 -0500 Subject: [PATCH 007/409] Inspect dynamic classes, functions and %o arguments as Node does - The inspector throws the 'not supported yet' Error for checked-dynamic values it does not render (a promise, a URL, a RegExp reached through `unknown`). Its calls were not marked as throwing, so `console.log("x", p)` printed "x " and the program went on; records and arrays with such a field printed `{ a: }` and `[ ]`. insp.dyn and insp.dynS are now may-throw library calls, so the Error propagates from the call that inspects the value, before anything is written. - A class reached through `unknown` prints `[class C]`, `[class C extends B]` or `[class (anonymous)]` instead of `[Function: C]`. Functions and classes reached as dynamic values print their enumerable own properties (`[Function: f] { x: 1 }`, `[class S extends A] { count: 3 }`), cycles through them as `[Circular *N]`, and `[Function]` past the depth limit. Class objects carry their base class's name. A compiled class's static fields are module globals that the class object does not carry, so inspecting a class that declares static fields through `unknown` throws the 'not supported yet' Error. - util.format's %o renders a checked-dynamic argument with showHidden: an array's `[length]` entry and an object's non-enumerable own properties, string- and symbol-keyed, in brackets. An array with more than 100 output entries keeps the last one out of the grid, as Node's groupArrayElements does. %o of a function, an Error, a typed array or a native handle other than Map, Set and Date throws the 'not supported yet' Error. ScrClassObj has two new fields, so RUNTIME_ABI_VERSION is now 27. --- packages/compiler/src/backend/llvm/classes.ts | 10 +- .../compiler/src/backend/llvm/emit-module.ts | 2 +- packages/compiler/src/backend/llvm/emitter.ts | 9 +- packages/compiler/src/backend/llvm/layouts.ts | 2 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/lower-classes.ts | 8 + .../src/frontend/lowering/lower-console.ts | 3 +- .../src/frontend/lowering/lower-inspect.ts | 32 ++- packages/compiler/src/ir/builtin-effects.ts | 7 + .../compiler/src/ir/builtin-signatures.ts | 4 + packages/compiler/src/ir/int32-slots.test.ts | 1 + packages/compiler/src/ir/int32-slots.ts | 1 + packages/compiler/src/ir/ir.ts | 4 + .../corpus/6343-inspect-class-values.ts.json | 6 + .../6344-inspect-function-properties.cjs.json | 6 + .../tests/corpus/6357-format-o-hidden.ts.json | 6 + .../corpus/6358-format-o-hidden-js.cjs.json | 6 + packages/runtime/src/scr_error.h | 6 + packages/runtime/src/scr_inspect.c | 187 +++++++++++++++--- packages/runtime/src/scr_inspect.h | 1 + packages/runtime/src/scr_object.c | 2 + tests/corpus/6343-inspect-class-values.ts | 31 +++ .../6344-inspect-function-properties.cjs | 34 ++++ tests/corpus/6357-format-o-hidden.ts | 19 ++ tests/corpus/6358-format-o-hidden-js.cjs | 9 + tests/fixtures/self-hosting/llvm-layouts.ts | 3 +- .../6343-inspect-class-values.ts.json | 1 + .../6344-inspect-function-properties.cjs.json | 1 + .../corpus-costs/6357-format-o-hidden.ts.json | 1 + .../6358-format-o-hidden-js.cjs.json | 1 + tests/harness/dyncheck.test.ts | 88 +++++++++ 31 files changed, 447 insertions(+), 45 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6343-inspect-class-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6344-inspect-function-properties.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6357-format-o-hidden.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6358-format-o-hidden-js.cjs.json create mode 100644 tests/corpus/6343-inspect-class-values.ts create mode 100644 tests/corpus/6344-inspect-function-properties.cjs create mode 100644 tests/corpus/6357-format-o-hidden.ts create mode 100644 tests/corpus/6358-format-o-hidden-js.cjs create mode 100644 tests/harness/corpus-costs/6343-inspect-class-values.ts.json create mode 100644 tests/harness/corpus-costs/6344-inspect-function-properties.cjs.json create mode 100644 tests/harness/corpus-costs/6357-format-o-hidden.ts.json create mode 100644 tests/harness/corpus-costs/6358-format-o-hidden-js.cjs.json diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 3073bd1bd..8b20ec264 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -1363,18 +1363,18 @@ export function emitClassShapes( * statics plus their construct thunks — allocate, run the constructor over * a +1 `this`, and hand the remaining +1 out. No pending check is needed: * in-tier constructors are throw-free by construction. - * `classObjs` maps className → { nameSym } registered during body emission - * (the literal interned while the table was open). */ + * `classObjs` maps className → { nameSym, baseNameSym } registered during + * body emission (the literals interned while the table was open). */ export function emitClassObjDefs( host: ClassHost, metaMap: Map, - classObjs: Map, + classObjs: Map, fnByName: Map, llType: (t: IrType) => string, threadLocal = false, ): string[] { const out: string[] = []; - for (const [className, { nameSym }] of classObjs) { + for (const [className, { nameSym, baseNameSym }] of classObjs) { const meta = metaMap.get(className); if (!meta) throw new InternalCompilerError( @@ -1400,7 +1400,7 @@ export function emitClassObjDefs( const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`); const classObj = `@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}global %ScrClassObj ` + - `{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null } ; class ${className}`; + `{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null, ptr ${baseNameSym ?? "null"}, ${host.sizeType} ${meta.def.ownStaticFields ? 1 : 0} } ; class ${className}`; if (meta.def.runtime) { // A runtime-provided class: %C.construct allocates through the // runtime and returns the instance. diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 3d9d41072..8da4bb140 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -483,7 +483,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // class-object shape { rc, pre, post, ctor, name } — field reads on // builtin errors and classval loads GEP through these. `%ScrError = type { ${emitter.sizeType}, ptr, ptr, ptr, ptr, ptr, i8, i8, i8, i8, i8, ptr, ptr }`, - `%ScrClassObj = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr }`, + `%ScrClassObj = type { ${emitter.sizeType}, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr, ptr, ptr, ${emitter.sizeType} }`, // The runtime emitter prefix { rc, vt, reg, cls } — user subclasses // embed it (classes.ts), and bare-emitter GEPs address through it. `%ScrEmitter = type { ${emitter.sizeType}, ptr, ptr, ptr }`, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bfde59a30..0b4ce184a 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1000,7 +1000,7 @@ export class LlEmitter { /** Class objects (classes as first-class values): className → the * interned .name literal ref — registered during body emission, the * statics and construct thunks assemble around the bodies. */ - readonly classObjs = new Map(); + readonly classObjs = new Map(); private readonly unionFieldReadGroups = new Map(); /** Preorder intervals of the runtime error classes under THIS module's * class-forest numbering (main() stamps scr_error_vts with them, exactly @@ -3638,7 +3638,12 @@ export class LlEmitter { classObjSym(className: string): string { if (!this.classObjs.has(className)) { const meta = this.classMetaOf(className); - this.classObjs.set(className, { nameSym: this.internLiteral(classJsName(meta.def)) }); + // util.inspect prints `[class C extends B]` with the base's name. + const baseName = meta.base ? classJsName(meta.base.def) : ""; + this.classObjs.set(className, { + nameSym: this.internLiteral(classJsName(meta.def)), + baseNameSym: baseName ? this.internLiteral(baseName) : null, + }); } return mangleClassObj(className); } diff --git a/packages/compiler/src/backend/llvm/layouts.ts b/packages/compiler/src/backend/llvm/layouts.ts index 8e1f0a4c1..5c426e910 100644 --- a/packages/compiler/src/backend/llvm/layouts.ts +++ b/packages/compiler/src/backend/llvm/layouts.ts @@ -17,7 +17,7 @@ export function emitLlvmLayouts( host: ClassHost, mod: IrModule, classes: Map, - classObjects: Map, + classObjects: Map, functions: Map, llType: (type: IrType) => string, nullable?: NullableRefFields, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9a65d9a24..be6595658 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1029,6 +1029,7 @@ export const LIB_FN_SYMS: Record = { "dyn.generatorThis": "scr_gen_receiver", "insp.dyn": "scr_insp_dyn", "insp.dynS": "scr_insp_dyn_s", + "insp.dynHidden": "scr_insp_dyn_hidden", "fs.readFileSyncDyn": "scr_fs_read_file_sync_dyn", "fs.readFileEncoded": "scr_fs_read_file_encoded", "fs.readFdEncoded": "scr_fs_read_fd_encoded", diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..3c294bb66 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -4395,6 +4395,14 @@ export function collectClassShapeInner( // INSTANTIATION prints its family's name — JS has one `Box`. jsName: jsNameOverride ?? decl.name?.text ?? "", ...(base ? { base: base.def.name } : {}), + ...(decl.members.nodes.some( + (member) => + ts.isPropertyDeclaration(member) && + !ts.isPrivateIdentifier(member.name) && + member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword), + ) + ? { ownStaticFields: true as const } + : {}), jsLength: ctor?.parameters?.nodes.findIndex( (parameter) => parameter.initializer || parameter.dotDotDotToken, diff --git a/packages/compiler/src/frontend/lowering/lower-console.ts b/packages/compiler/src/frontend/lowering/lower-console.ts index eec308342..b45218bd0 100644 --- a/packages/compiler/src/frontend/lowering/lower-console.ts +++ b/packages/compiler/src/frontend/lowering/lower-console.ts @@ -249,7 +249,8 @@ export function lowerConsoleArg( // inspect at the rest-args depth 2 (formatWithOptions) — scalar // kinds byte-exactly, boxed functions as [Function: name] / // [Function (anonymous)], composites through the dyn walk - // (insp.dyn). Never throws — Node's console.log never does. + // (insp.dyn). A value the inspector does not render throws the + // 'not supported yet' Error before anything is written. if (lowered.type.kind === "dyn") { return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 47f490c7b..71a7ea327 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -2607,10 +2607,21 @@ function formatSArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S } /** %O (depth 2) / %o (showHidden, depth 4) — inspect with the spec's - * option deltas; %o fences over arrays (showHidden adds their hidden - * [length] entry, which has no lowering). */ + * option deltas. A checked-dynamic argument renders through the runtime's + * showHidden walk; static %o fences over arrays and nested dynamic values + * (showHidden adds an array's hidden [length] entry, which the static + * rendering has no lowering for). */ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: SrcLoc): IrExpr { const value = lowerer.lowerExpr(node); + if (depth === 4 && value.type.kind === "dyn") { + return { + kind: "libCall", + fn: "insp.dynHidden", + args: [value, numLit(4, loc)], + type: STRING, + loc, + }; + } if (value.type.kind === "bytes") { lowerer.noLowering( `util.format %${depth === 4 ? "o" : "O"} of typed-array values`, @@ -2626,11 +2637,11 @@ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S node, reason, ); - if (depth === 4 && typeTreeHasArray(lowerer, value.type, new Set())) { + if (depth === 4 && typeTreeHasHidden(lowerer, value.type, new Set())) { lowerer.noLowering( "util.format %o over arrays", node, - "%o renders with showHidden (arrays gain a hidden [length] entry) — use %O for the default rendering", + "%o renders with showHidden (arrays gain a hidden [length] entry) — use %O for the default rendering, or pass the value as unknown", ); } return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(depth, loc), loc); @@ -3031,35 +3042,36 @@ export function lowerFormatCall( } /** Arrays anywhere in the tree — the %o (showHidden) gate. */ -function typeTreeHasArray(lowerer: Lowerer, t: IrType, visiting: Set): boolean { +function typeTreeHasHidden(lowerer: Lowerer, t: IrType, visiting: Set): boolean { switch (t.kind) { case "array": + case "dyn": return true; case "record": { if (visiting.has(t.shapeId)) return false; visiting.add(t.shapeId); const shape = lowerer.shapes.get(t.shapeId); if (shape?.tuple) return true; - return (shape?.fields ?? []).some((f) => typeTreeHasArray(lowerer, f.type, visiting)); + return (shape?.fields ?? []).some((f) => typeTreeHasHidden(lowerer, f.type, visiting)); } case "map": return ( - typeTreeHasArray(lowerer, t.key, visiting) || typeTreeHasArray(lowerer, t.value, visiting) + typeTreeHasHidden(lowerer, t.key, visiting) || typeTreeHasHidden(lowerer, t.value, visiting) ); case "set": - return typeTreeHasArray(lowerer, t.elem, visiting); + return typeTreeHasHidden(lowerer, t.elem, visiting); case "union": { if (visiting.has(t.unionId)) return false; visiting.add(t.unionId); return (lowerer.unions.get(t.unionId)?.arms ?? []).some((a) => - typeTreeHasArray(lowerer, a, visiting), + typeTreeHasHidden(lowerer, a, visiting), ); } case "object": { if (visiting.has(t.className)) return false; visiting.add(t.className); return (lowerer.classes.get(t.className)?.def.fields ?? []).some((f) => - typeTreeHasArray(lowerer, f.type, visiting), + typeTreeHasHidden(lowerer, f.type, visiting), ); } case "bytes": diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 16db45b6c..229fd8c28 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -174,6 +174,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "num.toStringRadix", "num.toStringRadixF64", "insp.jsonDyn", + // A checked-dynamic value the inspector does not render (a promise, a + // URL, a RegExp) throws the 'not supported yet' Error. + "insp.dyn", + "insp.dynS", + // %o's showHidden walk refuses the kinds whose hidden entries it cannot + // render. + "insp.dynHidden", // diagnostics_channel: publish runs subscribers synchronously (a throw // propagates — the documented divergence from triggerUncaughtException); // the subscribe/unsubscribe forms throw Node's ERR_INVALID_ARG_TYPE for diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fcdbe8694..9f224c72a 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4290,6 +4290,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "insp.errorCause": { argTypes: [null, F64, F64], result: STRING }, "insp.dyn": { argTypes: [DYN, F64, F64], result: STRING }, "insp.dynS": { argTypes: [DYN, F64], result: STRING }, + /** util.format's %o over a checked-dynamic value: inspect with + * showHidden. Kinds whose hidden entries have no rendering throw the + * 'not supported yet' Error. */ + "insp.dynHidden": { argTypes: [DYN, F64], result: STRING }, "insp.begin": { argTypes: [F64], result: VOID }, /** util.inspect's breakLength, compact (0 for false) and sorted for the * renders that follow; the call site restores 80, 3 and false afterwards. */ diff --git a/packages/compiler/src/ir/int32-slots.test.ts b/packages/compiler/src/ir/int32-slots.test.ts index be933ab38..f64e2de1f 100644 --- a/packages/compiler/src/ir/int32-slots.test.ts +++ b/packages/compiler/src/ir/int32-slots.test.ts @@ -281,6 +281,7 @@ test("dynamic stores disqualify the field names they can reach", () => { "dyn.typeof", "insp.dyn", "insp.dynS", + "insp.dynHidden", "dyn.objCreate", "dyn.getPrototype", "dyn.nameClassPrototype", diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 21223862a..221692e3f 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -407,6 +407,7 @@ const DYN_READ_ONLY = new Set([ "fs.utimesSync", "insp.dyn", "insp.dynS", + "insp.dynHidden", // The dynamic-tier operators run their dyn twins (scr_js_value.c); the // conversions move a value between dyn and jsval, and js.toF64 is // dynCheck's number check. diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 4a395feb2..5852eb8a6 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1432,6 +1432,10 @@ export interface IrClassDef { jsName?: string; /** JavaScript constructor arity, ending before the first default or rest parameter. */ jsLength?: number; + /** The class declares public static fields. util.inspect prints them + * after the class tag; a class object reached through `unknown` does + * not carry them unless the runtime holds its static table. */ + ownStaticFields?: true; /** Zero-argument native helper returning the shared prototype data object. * Used when materializing an instance's own-property view. */ prototypeDataHelper?: string; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6343-inspect-class-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6343-inspect-class-values.ts.json new file mode 100644 index 000000000..4fa8625ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6343-inspect-class-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6343-inspect-class-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6344-inspect-function-properties.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6344-inspect-function-properties.cjs.json new file mode 100644 index 000000000..003075140 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6344-inspect-function-properties.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6344-inspect-function-properties.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6357-format-o-hidden.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6357-format-o-hidden.ts.json new file mode 100644 index 000000000..cf0cd31d1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6357-format-o-hidden.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6357-format-o-hidden.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6358-format-o-hidden-js.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6358-format-o-hidden-js.cjs.json new file mode 100644 index 000000000..73d4eefa6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6358-format-o-hidden-js.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6358-format-o-hidden-js.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index a7c921f23..17d217cdc 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -48,6 +48,12 @@ typedef struct ScrClassObj { size_t length; struct ScrBox *prototype_data; struct ScrBox *static_data; + /* The base class's name, which util.inspect prints as + * `[class C extends B]`; NULL without a named base. */ + const ScrStr *base_name; + /* Nonzero when the class declares public static fields, which the + * class object does not carry without static_data. */ + size_t own_static_fields; struct ScrBox *caps[]; } ScrClassObj; diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index a4d124ac3..214648802 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -324,6 +324,9 @@ typedef struct { static SCR_TL InspSeenEnt *g_seen; static SCR_TL size_t g_nseen; +/* util.format's %o (scr_insp_dyn_hidden): this render shows hidden own + * properties, like util.inspect's showHidden. */ +static SCR_TL bool g_show_hidden; static SCR_TL size_t g_seen_cap; /* Detection-numbered circular targets (persist across the call). */ static SCR_TL const void *g_circ[64]; @@ -1375,18 +1378,25 @@ static ScrStr *scr_insp_dyn_collection(ScrDyn *d, double recurse, double depth, return out; } -/* An own enumerable symbol-keyed property of `d` (borrowed), or NULL. */ +/* An own symbol-keyed property of `d` (borrowed) this render shows, or + * NULL: enumerable ones, and under showHidden the non-enumerable ones too. */ static ScrDynEntry *scr_insp_symbol_entry(ScrDyn *d, size_t i) { ScrDynEntry *ent = scr_dyn_own_symbol_entry(d, d->symbol_keys->v.arr.items[i]); - return ent && ent->enumerable ? ent : NULL; + return ent && (ent->enumerable || g_show_hidden) ? ent : NULL; } /* An ordinary object: `Name { key: value }` with its constructor name * `cls` (NULL for a plain object). Node's default view lists the own * enumerable string keys, then the own enumerable symbol keys. */ +/* An own entry this render shows: enumerable ones, and under showHidden + * the non-enumerable ones too (never the encoding's `%` markers). */ +static bool insp_obj_shows(const ScrDynEntry *ent) { + return ent->enumerable || (g_show_hidden && !(ent->key_len && ent->key[0] == '%')); +} + static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char *cls) { size_t shown = 0; - for (size_t i = 0; i < d->v.obj.len; i++) shown += d->v.obj.entries[i].enumerable; + for (size_t i = 0; i < d->v.obj.len; i++) shown += insp_obj_shows(&d->v.obj.entries[i]); size_t nsymbols = d->symbol_keys ? d->symbol_keys->v.arr.len : 0; for (size_t i = 0; i < nsymbols; i++) shown += scr_insp_symbol_entry(d, i) != NULL; if (shown == 0) { @@ -1402,10 +1412,17 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char scr_insp_begin(recurse + 1); for (size_t i = 0; i < d->v.obj.len; i++) { ScrDynEntry *ent = &d->v.obj.entries[i]; - if (!ent->enumerable) continue; + if (!insp_obj_shows(ent)) continue; ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; - insp_key_into(&eb, ent->key, ent->key_len); + if (ent->enumerable) { + insp_key_into(&eb, ent->key, ent->key_len); + } else { + /* formatProperty's hidden key: the key's rendering in brackets. */ + ib_char(&eb, '['); + insp_key_into(&eb, ent->key, ent->key_len); + ib_char(&eb, ']'); + } ib_cstr(&eb, ": "); ib_bytes(&eb, val->data, val->len); scr_str_release(val); @@ -1419,7 +1436,10 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char ScrStr *key = scr_insp_dyn(d->symbol_keys->v.arr.items[i], recurse + 1, depth); ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; + /* A hidden symbol key is bracketed like a hidden string key. */ + if (!ent->enumerable) ib_char(&eb, '['); ib_bytes(&eb, key->data, key->len); + if (!ent->enumerable) ib_char(&eb, ']'); ib_cstr(&eb, ": "); ib_bytes(&eb, val->data, val->len); scr_str_release(key); @@ -1480,8 +1500,114 @@ ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth) { return out; } +/* Node's function rendering: [Function: name] / [Function (anonymous)], + * and [class C extends B] / [class (anonymous)] for a class. The boxed + * name is the compiler's best-effort static spelling (SEMANTICS.md — + * reference-site naming). Enumerable own properties (a class's static + * fields, properties assigned to a function) follow in braces; past the + * depth limit such a function is [Function]. Borrows d; +1. */ +static ScrStr *scr_insp_dyn_func(ScrDyn *d, double recurse, double depth) { + const char *name = d->v.fn.name; + const ScrClassObj *cls = d->v.fn.class_obj; + ScrClosure *owner = scr_closure_identity(d->v.fn.clo); + ScrBox *props = owner->props ? owner->props : cls ? cls->static_data : NULL; + ScrDyn *table = props ? (ScrDyn *)scr_box_get_ref(props) : NULL; /* +1 */ + InspBuf out = {0}; + if (cls) { + ib_cstr(&out, "[class "); + ib_cstr(&out, name && name[0] ? name : "(anonymous)"); + const ScrDyn *base = table ? table->prototype : NULL; + if (cls->base_name && cls->base_name->len) { + ib_cstr(&out, " extends "); + ib_bytes(&out, cls->base_name->data, cls->base_name->len); + } else if (base && base->kind == SCR_DYN_FUNC && base->v.fn.name && base->v.fn.name[0]) { + ib_cstr(&out, " extends "); + ib_cstr(&out, base->v.fn.name); + } + ib_char(&out, ']'); + } else if (name && name[0]) { + ib_cstr(&out, "[Function: "); + ib_cstr(&out, name); + ib_char(&out, ']'); + } else { + ib_cstr(&out, "[Function (anonymous)]"); + } + ScrStr *tag = ib_take(&out); + if (cls && !props && cls->own_static_fields) { + /* Node prints the static fields after the tag; the class object + * does not hold them (the reflection fence's stance). */ + scr_str_release(tag); + static const char message[] = "util.inspect of a class with static fields through unknown is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); + return scr_str_new("", 0); + } + size_t shown = 0; + if (table && table->kind == SCR_DYN_OBJ) { + for (size_t i = 0; i < table->v.obj.len; i++) shown += table->v.obj.entries[i].enumerable; + size_t nsymbols = table->symbol_keys ? table->symbol_keys->v.arr.len : 0; + for (size_t i = 0; i < nsymbols; i++) shown += scr_insp_symbol_entry(table, i) != NULL; + } + if (shown == 0) { + scr_dyn_release(table); + return tag; + } + if (recurse > depth) { + scr_dyn_release(table); + scr_str_release(tag); + return scr_str_new("[Function]", 10); + } + /* A function already on the traversal stack renders as [Circular *N], + * keyed by the closure every box of the function shares. */ + double circular = scr_insp_circ_check(owner); + if (circular > 0) { + scr_dyn_release(table); + scr_str_release(tag); + return scr_insp_circular(circular); + } + char *prefix = malloc(tag->len + 1); + if (!prefix) scr_trap("scriptc: out of memory\n"); + memcpy(prefix, tag->data, tag->len); + prefix[tag->len] = '\0'; + scr_str_release(tag); + size_t nseen = g_nseen; + g_pending_seen = owner; + ScrStr *rendered = scr_insp_obj(table, recurse, depth, prefix); + g_pending_seen = NULL; + if (g_nseen > nseen) g_nseen = nseen; + free(prefix); + scr_dyn_release(table); + return rendered; +} + +/* Under showHidden, the kinds whose hidden entries this inspector does not + * render (a function's length/name/prototype, an error's stack, a typed + * array's byte fields, a native handle's internals): the kind's name, NULL + * for the kinds it renders. */ +static const char *insp_hidden_refusal(const ScrDyn *d) { + switch (d->kind) { + case SCR_DYN_FUNC: return "function"; + case SCR_DYN_BYTES: return d->buffer ? "Buffer" : d->v.bytes->is_data_view ? "DataView" : "typed array"; + case SCR_DYN_OBJ: return scr_dyn_obj_get(d, "%error", 6) != NULL ? "Error" : NULL; + case SCR_DYN_HANDLE: + return d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_SET || + d->v.handle.tag == SCR_DYNH_DATE + ? NULL + : scr_dyn_handle_cls(d); + default: return NULL; + } +} + ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (d->kind == SCR_DYN_PROXY) return scr_insp_dyn(d->v.proxy.target, recurse, depth); + const char *refused = g_show_hidden ? insp_hidden_refusal(d) : NULL; + if (refused) { + InspBuf out = {0}; + ib_cstr(&out, "util.format %o of "); + ib_cstr(&out, refused); + ib_cstr(&out, " values is not supported yet"); + scr_throw_error(SCR_ERR_ERROR, ib_take(&out)); + return scr_str_new("", 0); + } switch (d->kind) { case SCR_DYN_NULL: return scr_str_new("null", 4); @@ -1499,14 +1625,14 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { return scr_insp_str(d->v.str); case SCR_DYN_ARR: { size_t len = d->v.arr.len; - if (len == 0) return scr_str_new("[]", 2); + if (len == 0 && !g_show_hidden) return scr_str_new("[]", 2); if (recurse > depth) return scr_str_new("[Array]", 7); scr_insp_begin(recurse + 1); /* Node's maxArrayLength (100) counts output entries; a run of holes * is one `` entry (formatSpecialArray). */ const unsigned char *presence = d->v.arr.presence; - size_t i = 0; - for (size_t shown = 0; i < len && shown < 100; shown++) { + size_t i = 0, shown = 0; + for (; i < len && shown < 100; shown++) { if (presence && !presence[i]) { size_t next = i + 1; while (next < len && !presence[next]) next++; @@ -1529,10 +1655,26 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { scr_insp_entry(m, d->v.arr.items[i]->kind == SCR_DYN_NUM); scr_str_release(m); } + if (g_show_hidden) { + /* The own non-enumerable `length`, after the items. Node's + * grouping reads value[k] for every output position k, this one + * included. */ + size_t at = shown + (more ? 1 : 0); + char line[48]; + int n = snprintf(line, sizeof line, "[length]: %zu", len); + ScrStr *s = scr_str_new(line, (size_t)n); + scr_insp_entry(s, at < len && (!presence || presence[at]) && + d->v.arr.items[at]->kind == SCR_DYN_NUM); + scr_str_release(s); + } ScrStr *base = scr_str_new("", 0); ScrStr *b0 = scr_str_new("[", 1); ScrStr *b1 = scr_str_new("]", 1); - ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, more); + /* groupArrayElements leaves the last entry out of the grid when + * there are more than maxArrayLength (100) entries: the "... N more + * items" tail, or under showHidden the `[length]` entry. */ + bool trailing = shown + (more ? 1 : 0) + (g_show_hidden ? 1 : 0) > 100; + ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, trailing); scr_str_release(base); scr_str_release(b0); scr_str_release(b1); @@ -1549,21 +1691,8 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { * the bare marker (where a plain object says [Object]). */ return scr_insp_obj(d, recurse, depth, scr_dyn_class_name(d)); } - case SCR_DYN_FUNC: { - /* Node's function rendering: [Function: name] / [Function - * (anonymous)]. The boxed name is the compiler's best-effort static - * spelling (SEMANTICS.md — reference-site naming). */ - const char *name = d->v.fn.name; - InspBuf out = {0}; - if (name && name[0]) { - ib_cstr(&out, "[Function: "); - ib_cstr(&out, name); - ib_char(&out, ']'); - } else { - ib_cstr(&out, "[Function (anonymous)]"); - } - return ib_take(&out); - } + case SCR_DYN_FUNC: + return scr_insp_dyn_func(d, recurse, depth); case SCR_DYN_HANDLE: { if (d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER) { @@ -1734,6 +1863,16 @@ ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth) { return scr_insp_dyn(d, 0, depth); } +/* util.format's %o over a dyn value: util.inspect with showHidden at the + * given depth. */ +ScrStr *scr_insp_dyn_hidden(ScrDyn *d, double depth) { + bool outer = g_show_hidden; + g_show_hidden = true; + ScrStr *out = scr_insp_dyn(d, 0, depth); + g_show_hidden = outer; + return out; +} + /* ── console.table cells ─────────────────────────────────────────────── */ size_t scr_insp_display_width(const char *data, size_t len) { return insp_width(data, len); } diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index 5ac3aa9dd..9bf18a23c 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -47,6 +47,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth); ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth); /* format's %s / rest-args twin: dyn strings pass verbatim. */ ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth); +ScrStr *scr_insp_dyn_hidden(ScrDyn *d, double depth); /* %o: showHidden */ void scr_insp_begin(double recurse); /* util.inspect's breakLength, compact (0 for false) and sorted for the * renders that follow; the call site restores the defaults (80, 3, false) diff --git a/packages/runtime/src/scr_object.c b/packages/runtime/src/scr_object.c index 7cafbeca3..2f47a1c7b 100644 --- a/packages/runtime/src/scr_object.c +++ b/packages/runtime/src/scr_object.c @@ -33,6 +33,8 @@ ScrClassObj *scr_classobj_new(const ScrClassObj *template, size_t ncaps) { c->length = template->length; c->prototype_data = NULL; c->static_data = NULL; + c->base_name = template->base_name; + c->own_static_fields = template->own_static_fields; scr_obj_alloc_note(); return c; } diff --git a/tests/corpus/6343-inspect-class-values.ts b/tests/corpus/6343-inspect-class-values.ts new file mode 100644 index 000000000..5e9931c8c --- /dev/null +++ b/tests/corpus/6343-inspect-class-values.ts @@ -0,0 +1,31 @@ +// Classes reached through `unknown` print as Node prints them: +// `[class C]`, `[class C extends B]` with the runtime class's base, and +// `[class (anonymous)]`, at the top level and inside arrays and records. +import { inspect } from "node:util"; + +class A { + x = 1; +} +class B extends A { + y = 2; +} +class C extends B {} +class MyErr extends Error {} +const Anon = [class {}][0]!; +const Sub = class extends A {}; + +const a: unknown = A; +const b: unknown = B; +console.log(a, b); +console.log([a], { b }); +const values: unknown[] = [C, MyErr, Anon, Sub]; +console.log(values); +console.log(inspect({ deep: { values } }, { depth: 0 }), inspect(values, { depth: 0 })); +console.log(String(typeof values[0])); + +// A slot typed as the base holds a subclass at runtime. +const pick = (k: number): typeof A => (k === 0 ? A : k === 1 ? B : C); +for (let k = 0; k < 3; k++) { + const picked: unknown = pick(k); + console.log(k, picked); +} diff --git a/tests/corpus/6344-inspect-function-properties.cjs b/tests/corpus/6344-inspect-function-properties.cjs new file mode 100644 index 000000000..94db45a40 --- /dev/null +++ b/tests/corpus/6344-inspect-function-properties.cjs @@ -0,0 +1,34 @@ +'use strict'; +// Functions and classes reached as dynamic values print their enumerable +// own properties after the tag, as Node prints them: a class's static +// properties, properties assigned to a function, cycles through them, +// and `[Function]` past the depth limit. +const util = require('util'); + +function f() {} +f.x = 1; +f.label = 'f'; +function g() {} +g.self = g; +function plain() {} +class A {} +class S extends A { + static get hidden() { + return 1; + } +} +S.count = 3; +function show(value) { + console.log(value); +} +show(f); +show(g); +show(S); +show(A); +show(plain); +function shallow(value) { + console.log(util.inspect({ value }, { depth: 0 })); +} +shallow(f); +shallow(S); +shallow(A); diff --git a/tests/corpus/6357-format-o-hidden.ts b/tests/corpus/6357-format-o-hidden.ts new file mode 100644 index 000000000..9e48ed4ba --- /dev/null +++ b/tests/corpus/6357-format-o-hidden.ts @@ -0,0 +1,19 @@ +// util.format's %o over values reached through `unknown` renders with +// showHidden, as Node does: arrays show their hidden `[length]` (with +// Node's grouping), at every depth, inside Maps and records too. +import { format } from "node:util"; +const cases: [string, unknown][] = [ + ["ten", Array.from({ length: 10 }, (_, i) => i)], + ["wide", Array.from({ length: 120 }, (_, i) => i)], + ["hundred", Array.from({ length: 100 }, (_, i) => i)], + ["hundred-one", Array.from({ length: 101 }, (_, i) => i)], + ["hundred-strs", Array.from({ length: 100 }, (_, i) => "s" + i)], + ["seven", ["aaaaaaaaaaaaaaaaaaaa", "b", "c", "d", "e", "f", "g"]], + ["strs", Array.from({ length: 30 }, (_, i) => "s" + i)], + ["holes", [1, , 3]], + ["empty", []], + ["nested", { list: [[]], m: new Map([[1, [2]]]) }], + ["deep", [[[[[[1]]]]]]], + ["date", [new Date(0)]], +]; +for (const [k, v] of cases) console.log(k, format("%o", v)); diff --git a/tests/corpus/6358-format-o-hidden-js.cjs b/tests/corpus/6358-format-o-hidden-js.cjs new file mode 100644 index 000000000..df54b2714 --- /dev/null +++ b/tests/corpus/6358-format-o-hidden-js.cjs @@ -0,0 +1,9 @@ +'use strict'; +// %o shows an object's non-enumerable own properties as `[key]`, symbol +// keys included. +const util = require('util'); +const hidden = Object.defineProperty(JSON.parse('{"shown":1}'), 'secret', { value: [2], enumerable: false }); +Object.defineProperty(hidden, 'a-b', { value: 'x', enumerable: false }); +Object.defineProperty(hidden, Symbol('tag'), { value: 3, enumerable: false }); +console.log(util.format('%o and %O', hidden, hidden)); +console.log(util.format('%o', [hidden])); diff --git a/tests/fixtures/self-hosting/llvm-layouts.ts b/tests/fixtures/self-hosting/llvm-layouts.ts index 6ddf3ca3a..36d55bddf 100644 --- a/tests/fixtures/self-hosting/llvm-layouts.ts +++ b/tests/fixtures/self-hosting/llvm-layouts.ts @@ -119,7 +119,8 @@ try { }, ); virtualBorrows = new VirtualBorrows(mod, graph, functions, lifetimes.borrowed); - const classObjects = new Map(request.classObjects.map((name) => [name, { nameSym: `@name_${mangleClassObj(name)}` }])); + const classObjects: Map = new Map(); + for (const name of request.classObjects) classObjects.set(name, { nameSym: `@name_${mangleClassObj(name)}`, baseNameSym: null }); const typeName = (type: IrType): string => type.kind === "void" ? "void" : type.kind === "f64" || type.kind === "date" ? "double" : type.kind === "bool" ? "i1" : "ptr"; // Mirrors the emitter: whole-program int32 fields store the exact i32. const layouts = emitLlvmLayouts(host, mod, graph, classObjects, functions, typeName, undefined, analyzeInt32Slots(mod)); diff --git a/tests/harness/corpus-costs/6343-inspect-class-values.ts.json b/tests/harness/corpus-costs/6343-inspect-class-values.ts.json new file mode 100644 index 000000000..fa2590573 --- /dev/null +++ b/tests/harness/corpus-costs/6343-inspect-class-values.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 4700 } diff --git a/tests/harness/corpus-costs/6344-inspect-function-properties.cjs.json b/tests/harness/corpus-costs/6344-inspect-function-properties.cjs.json new file mode 100644 index 000000000..71bf780f8 --- /dev/null +++ b/tests/harness/corpus-costs/6344-inspect-function-properties.cjs.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 3500 } diff --git a/tests/harness/corpus-costs/6357-format-o-hidden.ts.json b/tests/harness/corpus-costs/6357-format-o-hidden.ts.json new file mode 100644 index 000000000..f351a5044 --- /dev/null +++ b/tests/harness/corpus-costs/6357-format-o-hidden.ts.json @@ -0,0 +1 @@ +{ "plain": 2300, "san": 2100 } diff --git a/tests/harness/corpus-costs/6358-format-o-hidden-js.cjs.json b/tests/harness/corpus-costs/6358-format-o-hidden-js.cjs.json new file mode 100644 index 000000000..1f8ff72b2 --- /dev/null +++ b/tests/harness/corpus-costs/6358-format-o-hidden-js.cjs.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 2000 } diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index 6a2d4dc61..dab51a430 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -1278,4 +1278,92 @@ console.log("done", caught); expect(r.exitCode).toBe(0); expect(r.stdout).toBe("done 100\n"); }); + + test("an inspection refusal throws before console.log writes", async () => { + // A promise reached through `unknown` has no dynamic rendering; the + // refusal must stop the call, not print a partial line and continue. + const r = await compileAndRun( + "inspect-refusal-unwind", + `import { inspect } from "node:util"; +const p: unknown = Promise.resolve(3); +function attempt(label: string, f: () => void): void { + try { + f(); + console.log(label, "returned"); + } catch (e) { + console.log(label, (e as Error).message); + } +} +attempt("log", () => console.log("x", p)); +attempt("inspect", () => console.log(inspect(p).length)); +attempt("record", () => console.log({ a: p })); +attempt("array", () => console.log([p])); +console.log("before"); +console.log(p); +console.log("after"); +`, + ); + const refusal = "util.inspect of a promise value is not supported yet"; + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe( + ["log", "inspect", "record", "array"].map((l) => `${l} ${refusal}\n`).join("") + "before\n", + ); + expect(r.stderr).toContain(`Uncaught Error: ${refusal}`); + }); + + test("inspecting a compiled class with static fields through unknown refuses", async () => { + // Node prints `[class S] { count: 3 }`; the class object does not + // carry compiled static fields, so the inspector refuses rather than + // print `[class S]`. A class without static fields still prints. + const r = await compileAndRun( + "inspect-class-statics", + `class Plain {} +class S extends Plain { + static count = 3; +} +const plain: unknown = Plain; +const s: unknown = S; +console.log(plain); +try { + console.log(s); +} catch (e) { + console.log((e as Error).message); +} +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + "[class Plain]\nutil.inspect of a class with static fields through unknown is not supported yet\n", + ); + }); + + test("%o refuses the dynamic kinds whose hidden entries have no rendering", async () => { + // Node's showHidden adds a function's length/name/prototype, an + // error's stack and a typed array's byte fields; %o throws the 'not + // supported yet' Error for them instead of printing without them. + const r = await compileAndRun( + "format-o-hidden-refusals", + `import { format } from "node:util"; +function f(): void {} +const values: [string, unknown][] = [["fn", f], ["err", new Error("e")], ["u8", new Uint8Array(1)], ["arr", [1]]]; +for (const [label, value] of values) { + try { + console.log(label, format("%o", value)); + } catch (e) { + console.log(label, (e as Error).message); + } +} +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + [ + "fn util.format %o of function values is not supported yet", + "err util.format %o of Error values is not supported yet", + "u8 util.format %o of typed array values is not supported yet", + "arr [ 1, [length]: 1 ]", + "", + ].join("\n"), + ); + }); }); From 65a68470cf0e5dba6e51e473faebeaf9f0f2e7b0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:12:23 -0500 Subject: [PATCH 008/409] Free a typed teardown's blocks in the order it met them A typed teardown freed each block onto its free list as it went, and the free lists, which are LIFO, handed a torn-down structure's blocks to the next allocations in the reverse order. A structure that a loop rebuilds the same way each time was therefore laid out mirrored every other generation, and every walk over it, its teardown included, ran through memory backwards, which the hardware prefetchers handle three to four times worse. A program that rebuilds a tree of 2^21 - 1 object literals 12 times, summing each generation before dropping it, took 26 to 28 ns per node to build, 24 to sum and 16 to drop in those generations, against 6.8, 6.3 and 5.3 in the others. With this change, every generation runs at the second set of times, and the program takes 0.50 s instead of 1.18 s (Node: 0.91 s). The teardown of a class or record type with children of its own type now keeps the blocks it frees in a segment, linked through their first words and built in order through a tail pointer. A node releases its own block first, places it ahead of its children's when its address is the lower (object literals allocate before their fields) and behind them otherwise (constructor arguments build the children first), and the chain's first teardown frees the whole segment onto the free list in one step. A teardown past DIRECT_DESTROY_CHAIN appends through scr_rc_destroy_seg, which keeps scr_rc_destroy's depth budget and queue. Blocks outside the calling thread's arena are freed as before, and targets without the inline allocation paths keep the old teardown. --- packages/compiler/src/backend/llvm/alloc.ts | 77 +++++- .../compiler/src/backend/llvm/classes.test.ts | 59 +++++ packages/compiler/src/backend/llvm/classes.ts | 73 ++++-- packages/compiler/src/backend/llvm/shapes.ts | 242 ++++++++++++++++-- .../corpus/7260-teardown-segments.ts.json | 6 + packages/runtime/src/scr_cycle.c | 56 ++-- packages/runtime/src/scr_rc.h | 8 + scripts/ci-test-costs.json | 2 + tests/corpus/7260-teardown-segments.ts | 179 +++++++++++++ .../7260-teardown-segments.ts.json | 1 + tests/harness/teardown-segments.test.ts | 97 +++++++ 11 files changed, 732 insertions(+), 68 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json create mode 100644 tests/corpus/7260-teardown-segments.ts create mode 100644 tests/harness/corpus-costs/7260-teardown-segments.ts.json create mode 100644 tests/harness/teardown-segments.test.ts diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index aa6345dab..2efeb96ac 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -459,18 +459,72 @@ export type FreeObservers = "weak" | "weak-or-source"; * type's disposals), and the block — `scr_cyc_free` for collector-headered * objects, `scr_rt_free` otherwise. Labels use the `sf.` prefix; the lines * end in an open block. + * + * With `keep` (an alloca of ptr holding NULL; inline allocation paths only), + * a block that would go onto the calling thread's free list is stored there + * instead, for a teardown segment (emitSegmentFree); a block the system or + * another arena owns is still freed. */ export function emitObjectFree( host: AllocHost, cyc: boolean, observers: FreeObservers = "weak", + keep: string | null = null, ): string[] { - const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers)); + const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers, keep)); if (host.objectAudit === false) return lines.filter((line) => line !== FREE_NOTE); host.declare(`declare void @scr_obj_free_note()`); return lines; } +/** Lines that put the blocks `first` to `last` (linked through their first + * words; one block when they are equal) of the class at reservation offset + * `off` on the calling thread's free list. */ +function freeListPushLines(prefix: string, off: string, first: string, last: string): string[] { + return [ + ` %${prefix}.shp = getelementptr inbounds i8, ptr @scr_sa, i64 ${SA_SHIFT}`, + ` %${prefix}.sh32 = load i32, ptr %${prefix}.shp`, + ` %${prefix}.sh = zext i32 %${prefix}.sh32 to i64`, + ` %${prefix}.c = lshr i64 ${off}, %${prefix}.sh`, + ` %${prefix}.c8 = shl i64 %${prefix}.c, 3`, + ` %${prefix}.fo = add i64 %${prefix}.c8, ${SA_FREE}`, + ` %${prefix}.fp = getelementptr inbounds i8, ptr @scr_sa, i64 %${prefix}.fo`, + ` %${prefix}.head = load ptr, ptr %${prefix}.fp`, + ` store ptr %${prefix}.head, ptr ${last}`, + ` store ptr ${first}, ptr %${prefix}.fp`, + ]; +} + +/** + * Lines that free a teardown segment: the blocks `first` to `last` (ptr + * values, both NULL for an empty segment), linked through their first words, + * which emitObjectFree kept. Every block of a segment is in the calling + * thread's arena and of one size class, so the segment goes onto that + * class's free list in one step, and the next allocations of the class take + * its blocks in segment order. Labels use `prefix`; the lines end in the + * open block `${prefix}.done`. + */ +export function emitSegmentFree( + host: AllocHost, + prefix: string, + first: string, + last: string, +): string[] { + declareState(host); + const lines = [ + ` %${prefix}.none = icmp eq ptr ${first}, null`, + ` br i1 %${prefix}.none, label %${prefix}.done, label %${prefix}.push`, + `${prefix}.push:`, + ` %${prefix}.base = load i64, ptr @scr_sa`, + ` %${prefix}.bi = ptrtoint ptr ${first} to i64`, + ` %${prefix}.off = sub i64 %${prefix}.bi, %${prefix}.base`, + ...freeListPushLines(prefix, `%${prefix}.off`, first, last), + ` br label %${prefix}.done`, + `${prefix}.done:`, + ]; + return threadLocalState(host, prefix, lines); +} + /** Lines that leave in `%.obs` whether the program can observe a * disposal of this type (folds to a constant). */ function observedLines(prefix: string, observers: FreeObservers): string[] { @@ -482,8 +536,14 @@ function observedLines(prefix: string, observers: FreeObservers): string[] { ]; } -function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): string[] { +function freeLines( + host: AllocHost, + cyc: boolean, + observers: FreeObservers, + keep: string | null, +): string[] { if (host.inlineAlloc !== true) { + if (keep !== null) throw new Error("a kept block needs the inline allocation paths"); if (cyc) { host.declare(`declare void @scr_cyc_free(ptr)`); return [` call void @scr_obj_free_note()`, ` call void @scr_cyc_free(ptr %o)`]; @@ -548,16 +608,9 @@ function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): str ` %sf.inx = call i1 @llvm.expect.i1(i1 %sf.in, i1 true)`, ` br i1 %sf.inx, label %sf.push, label %sf.sys`, `sf.push:`, - ` %sf.shp = getelementptr inbounds i8, ptr @scr_sa, i64 ${SA_SHIFT}`, - ` %sf.sh32 = load i32, ptr %sf.shp`, - ` %sf.sh = zext i32 %sf.sh32 to i64`, - ` %sf.c = lshr i64 %sf.off, %sf.sh`, - ` %sf.c8 = shl i64 %sf.c, 3`, - ` %sf.fo = add i64 %sf.c8, ${SA_FREE}`, - ` %sf.fp = getelementptr inbounds i8, ptr @scr_sa, i64 %sf.fo`, - ` %sf.head = load ptr, ptr %sf.fp`, - ` store ptr %sf.head, ptr ${blk}`, - ` store ptr ${blk}, ptr %sf.fp`, + ...(keep !== null + ? [` store ptr ${blk}, ptr ${keep}`] + : freeListPushLines("sf", "%sf.off", blk, blk)), ` br label %sf.done`, `sf.sys: ; outside the reservation, or the allocator is compiled out`, ` call void @scr_obj_free_note()`, diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 2d95e942c..0c0b73aeb 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -68,6 +68,65 @@ test("leaf layouts keep direct teardown while recursive layouts share the depth } }); +test("teardowns of types with children of their own type free their blocks as one segment", () => { + const linked = { kind: "object" as const, className: "Linked" }; + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }], + classes: [ + { + name: "Linked", + fields: [ + { name: "next", type: linked }, + { name: "other", type: linked }, + ], + methods: [], + loc, + }, + ], + records: [{ id: "branch", fields: [{ name: "next", type: linked }] }], + }; + const define = (llvm: string, name: string): string => + new RegExp(`^define internal [^@]*@${name}\\([^]*?^}`, "m").exec(llvm)?.[0] ?? ""; + const linkedDestroy = `${mangleClassRelease("Linked")}_destroy`; + const branchDestroy = `${mangleRecordRelease("branch")}_destroy`; + for (const workers of [false, true]) { + const llvm = emitLlvmModule({ ...mod, workers }, { pointerBits: 64 }); + // Linked's teardown keeps its block and its children's in a segment: it + // releases its own block first and places it by address against its + // first child, a child torn down directly appends to the segment, one + // past the direct chain appends through the runtime, and the chain's + // first teardown frees the segment onto the free list in one step. + const seg = define(llvm, `${linkedDestroy}_s`); + expect(seg.startsWith("define internal ptr"), "segment form").toBe(true); + expect(seg).toMatch( + /store ptr %(?:sf\.blk|o), ptr %sg\.own[^]*call ptr @sc_release_Linked_destroy_s\(/, + ); + // The children's releases have a copy for each place of the own block. + for (const copy of ["ta", "tb"]) + expect(seg).toContain( + `call ptr @scr_rc_destroy_seg(ptr %v0, ptr @${linkedDestroy}_sc, ptr @${linkedDestroy}, ptr %${copy}0.bt)`, + ); + expect(seg).not.toContain("call void @scr_rc_destroy("); + expect(seg).toContain("icmp ult ptr %sg.o, %sg.r1"); + const first = define(llvm, `${linkedDestroy}_d`); + expect(first).toContain(`call ptr @${linkedDestroy}_s(ptr %o, i32 %d, ptr %head)`); + expect(first).toContain("store ptr %first, ptr %sgf.fp"); + expect(first.includes("llvm.threadlocal.address"), "thread-local state").toBe(workers); + expect(define(llvm, linkedDestroy)).toContain(`call void @${linkedDestroy}_d(ptr %o, i32 0)`); + // A record whose child is another type frees its own block as before. + expect(define(llvm, `${branchDestroy}_s`)).toBe(""); + expect(define(llvm, branchDestroy)).toContain(`call void @${mangleClassRelease("Linked")}(`); + } + // Without the inline allocation paths (32-bit targets) a teardown frees + // through the runtime, block by block. + const llvm32 = emitLlvmModule(mod, { pointerBits: 32 }); + expect(define(llvm32, `${linkedDestroy}_s`)).toBe(""); + expect(define(llvm32, `${linkedDestroy}_d`)).toContain(`call void @${linkedDestroy}_d(`); +}); + test("releases split into an inlinable fast path and an out-of-line remainder", () => { // An array of the class itself makes the hierarchy cycle-capable. const kids = { kind: "array" as const, elem: { kind: "object" as const, className: "Node" } }; diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 3073bd1bd..3d67fbfd3 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -43,6 +43,9 @@ import { destroyDefinitions, directChildReleaseLines, directDestroyTarget, + segmentPrologueLines, + segmentTeardownLines, + segmentedTeardown, usedDirectTargets, type DirectDestroy, type RegionHooks, @@ -1078,25 +1081,62 @@ export function emitClassShapes( bounded && (directTargets.has(`object:${cls.name}`) || refFields.some((f) => directDestroyTarget(host, f.type, directTargets) !== null)); + // The teardown's own symbol, and whether it keeps a segment (shapes.ts + // segmentedTeardown). + const destroy = `${meta.hierarchy ? mangleClassReleaseDirect(cls.name) : mangleClassRelease(cls.name)}_destroy`; + const segmented = + chained && + !meta.compact && + runtimePrefix === null && + !isStreamRooted && + !meta.threadSlot && + segmentedTeardown( + host, + destroy, + refFields.map((f) => f.type), + directTargets, + ); // The field-releasing teardown body shared by both release shapes // (the public one on standalone classes, the DIRECT one on hierarchy // members). Runs at rc == 0. const teardown = (lines: string[]): void => { - refFields.forEach((f, t) => { - lines.push( - ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`, - ` %v${t} = load ptr, ptr %f${t}`, - ); + const release = (prefix: string, f: (typeof refFields)[number], t: number): string[] => { const direct = bounded ? directDestroyTarget(host, f.type, directTargets) : null; - if (direct) - lines.push( - ...directChildReleaseLines(host, `td${t}`, `%v${t}`, direct, llvmCommentText(f.name)), - ); - else - lines.push( + if (direct === null) + return [ ` call void ${releaseSym(host, f.type)}(ptr %v${t}) ; ${llvmCommentText(f.name)}`, - ); - }); + ]; + const own = segmented && direct.destroy === destroy; + return directChildReleaseLines( + host, + `${prefix}${t}`, + `%v${t}`, + direct, + llvmCommentText(f.name), + own, + ); + }; + const loads = refFields.map((f, t) => [ + ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`, + ` %v${t} = load ptr, ptr %f${t}`, + ]); + if (segmented) { + // A segmented teardown loads its fields, releases its own block, and + // then releases the fields (shapes.ts segmentTeardownLines). + const ownType = refFields.flatMap((f, t) => + directDestroyTarget(host, f.type, directTargets)?.destroy === destroy ? [`%v${t}`] : [], + ); + lines.push( + ...segmentPrologueLines(), + ...loads.flat(), + ...emitObjectFree(host, traced, "weak", "%sg.own"), + ...segmentTeardownLines(ownType, (prefix) => + refFields.flatMap((f, t) => release(prefix, f, t)), + ), + ); + return; + } + refFields.forEach((f, t) => lines.push(...loads[t]!, ...release("td", f, t))); if (runtimePrefix) lines.push(...regCall("td", runtimePrefix.drop, "")); if (isStreamRooted) lines.push(...stCall("tds", "scr_stream_st_release", "")); if (meta.threadSlot) lines.push(...slotForgetLines(host, struct, meta, "tdw")); @@ -1178,10 +1218,12 @@ export function emitClassShapes( ]; if (traced) reld.push(...cycleDeadLines(host, "teardown")); if (bounded) { - const destroy = `${mangleClassReleaseDirect(cls.name)}_destroy`; const destroyBody: string[] = []; teardown(destroyBody); - defs.push(...destroyDefinitions(destroy, destroyBody, chained), ``); + defs.push( + ...destroyDefinitions(destroy, destroyBody, chained, segmented ? host : null), + ``, + ); host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); reld.push(` call void @scr_rc_destroy(ptr %o, ptr @${destroy})`); } else teardown(reld); @@ -1203,6 +1245,7 @@ export function emitClassShapes( bounded, lifetime, chained, + segmented, ), ``, ); diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 6de92f667..ae217a76c 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -34,6 +34,7 @@ import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree, + emitSegmentFree, CYC_APPEND, cycleAppendDecls, cycleAppendHelper, @@ -1340,17 +1341,23 @@ export interface DirectDestroy { /** The release of child `value` (a loaded field, possibly NULL) inside a * teardown whose chain length is `%d`: the child's own release for a shared * or immortal reference, its teardown for the last one. Labels use `tag`; - * the lines end in the open block `${tag}.next`. */ + * the lines end in the open block `${tag}.next`. + * + * With `segment` (the child has the teardown's own type, in a segmented + * teardown; segmentPrologueLines), the child's teardown appends its blocks + * to the teardown's segment. */ export function directChildReleaseLines( host: ShapeHost, tag: string, value: string, target: DirectDestroy, comment: string, + segment = false, ): string[] { const S = host.sizeType; const C = target.count ?? S; - host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); + if (segment) host.declare(`declare ptr @scr_rc_destroy_seg(ptr, ptr, ptr, ptr)`); + else host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); const lines = [ ` %${tag}.nn = icmp ne ptr ${value}, null`, ` br i1 %${tag}.nn, label %${tag}.chk, label %${tag}.next`, @@ -1382,16 +1389,139 @@ export function directChildReleaseLines( ` br i1 %${tag}.short, label %${tag}.direct, label %${tag}.bounded`, `${tag}.direct:`, ` %${tag}.d = add i32 %d, 1`, - ` call void @${target.destroy}_d(ptr ${value}, i32 %${tag}.d)`, + ); + if (!segment) { + lines.push( + ` call void @${target.destroy}_d(ptr ${value}, i32 %${tag}.d)`, + ` br label %${tag}.next`, + `${tag}.bounded:`, + ` call void @scr_rc_destroy(ptr ${value}, ptr @${target.destroy})`, + ` br label %${tag}.next`, + `${tag}.next:`, + ); + return lines; + } + lines.push( + ` %${tag}.t = load ptr, ptr %sg.t`, + ` %${tag}.dt = call ptr @${target.destroy}_s(ptr ${value}, i32 %${tag}.d, ptr %${tag}.t)`, + ` store ptr %${tag}.dt, ptr %sg.t`, ` br label %${tag}.next`, `${tag}.bounded:`, - ` call void @scr_rc_destroy(ptr ${value}, ptr @${target.destroy})`, + ` %${tag}.bt = load ptr, ptr %sg.t`, + ` %${tag}.rt = call ptr @scr_rc_destroy_seg(ptr ${value}, ptr @${target.destroy}_sc, ptr @${target.destroy}, ptr %${tag}.bt)`, + ` store ptr %${tag}.rt, ptr %sg.t`, ` br label %${tag}.next`, `${tag}.next:`, ); return lines; } +/* Teardown segments. A typed teardown that frees each block onto its free + * list as it goes frees a tree in walk order, and the free lists, which are + * LIFO, hand the blocks to the next allocations in the reverse order: a tree + * built again in the same order as the last (each generation of a structure + * a loop rebuilds) is laid out mirrored, and every walk over it, the next + * teardown included, runs through memory backwards, which defeats the + * hardware prefetchers. The runtime frees large dead sets of the cycle + * collector in address order for the same reason (scr_mem_order_for_free). + * + * So the teardown of a type with children of its own type (the `_s` form, + * segmentedTeardown) keeps the blocks it frees in a segment, linked through + * their first words, which it builds in order through a tail pointer: the + * `_s` form takes the segment's last link (the first word of its last block, + * or the head slot of an empty segment) and returns the new one. A node + * first releases its own block (emitObjectFree, keeping it), then appends it + * ahead of its children's blocks when its address is the lower, behind them + * otherwise (segmentTeardownLines), and the children append theirs in field + * order, so a structure + * built parent first (object literals allocate before their fields) or + * children first (constructor arguments) comes back in ascending order. The + * chain's first teardown (`_d`) frees the whole segment onto the class's + * free list in one step (emitSegmentFree), and the walks over the next + * generation run forward. A teardown past DIRECT_DESTROY_CHAIN appends + * through scr_rc_destroy_seg, under the same depth budget as + * scr_rc_destroy. Blocks outside the calling thread's arena are freed as + * before; one type's blocks are all of one class. */ + +/** Whether the teardown of a type whose reference fields have `fieldTypes` + * (with direct targets `targets`) and whose own teardown is `destroy` keeps + * a segment: a child field has its own type, and the inline allocation paths + * are in use. */ +export function segmentedTeardown( + host: ShapeHost, + destroy: string, + fieldTypes: readonly IrType[], + targets: ReadonlyMap, +): boolean { + if (host.inlineAlloc !== true) return false; + return fieldTypes.some((t) => directDestroyTarget(host, t, targets)?.destroy === destroy); +} + +/** The first lines of a segmented teardown, in its entry block: the + * segment's last link (`%sg.t`, from the `%tail` argument) and the kept own + * block (`%sg.own`, for emitObjectFree). */ +export function segmentPrologueLines(): string[] { + return [ + ` %sg.t = alloca ptr`, + ` %sg.own = alloca ptr`, + ` store ptr %tail, ptr %sg.t`, + ` store ptr null, ptr %sg.own`, + ]; +} + +/** The rest of a segmented teardown after emitObjectFree kept (or freed) + * its own block: the block goes ahead of its children's when its address is + * below the first non-NULL one of `children` (loaded fields of the + * teardown's own type, which stand for their blocks) or there is none, and + * behind them otherwise; then the children's releases (`releases`, given a + * label prefix), and the segment's last link is returned. Each order has its + * own copy of the releases, so neither keeps the decision, or the block once + * it is appended, across the children's teardowns. */ +export function segmentTeardownLines( + children: readonly string[], + releases: (prefix: string) => string[], +): string[] { + const lines = [` %sg.o = load ptr, ptr %sg.own`, ` %sg.kept = icmp ne ptr %sg.o, null`]; + let ref = "null"; + [...children].reverse().forEach((child, i) => { + lines.push( + ` %sg.nn${i} = icmp ne ptr ${child}, null`, + ` %sg.r${i} = select i1 %sg.nn${i}, ptr ${child}, ptr ${ref}`, + ); + ref = `%sg.r${i}`; + }); + lines.push( + ` %sg.leaf = icmp eq ptr ${ref}, null`, + ` %sg.lower = icmp ult ptr %sg.o, ${ref}`, + ` %sg.first = or i1 %sg.leaf, %sg.lower`, + ` %sg.ahead = and i1 %sg.kept, %sg.first`, + ` br i1 %sg.ahead, label %sg.front, label %sg.back`, + `sg.front:`, + ...segmentAppendLines("a"), + ...releases("ta"), + ` br label %sg.ret`, + `sg.back:`, + ...releases("tb"), + ` %sg.keep = icmp ne ptr %sg.o, null`, + ` br i1 %sg.keep, label %sg.last, label %sg.ret`, + `sg.last:`, + ...segmentAppendLines("l"), + ` br label %sg.ret`, + `sg.ret:`, + ` %sg.tr = load ptr, ptr %sg.t`, + ` ret ptr %sg.tr`, + ); + return lines; +} + +function segmentAppendLines(tag: string): string[] { + return [ + ` %sg.t${tag} = load ptr, ptr %sg.t`, + ` store ptr %sg.o, ptr %sg.t${tag}`, + ` store ptr %sg.o, ptr %sg.t`, + ]; +} + /** The direct-teardown target of a reference of `type`, if `targets` (keyed * `record:` / `object:`) has one. Nullable unions hold the arm's * pointer, NULL or an immortal sentinel, which the child release handles. */ @@ -1434,12 +1564,42 @@ export function usedDirectTargets( /** The teardown definitions for `destroy`: with `chained`, the body takes * the chain length `%d` (directChildReleaseLines) and the runtime's - * one-argument entry starts a chain. */ + * one-argument entry starts a chain. With `segment` (the host, for a + * segmented teardown), the body is a segment teardown's + * (segmentPrologueLines to segmentTeardownLines), defined as `_s`, with + * `_sc` for scr_rc_destroy_seg, and `_d` starts a segment and frees it. */ export function destroyDefinitions( destroy: string, body: readonly string[], chained: boolean, + segment: ShapeHost | null = null, ): string[] { + if (segment !== null) + return [ + `define internal ptr @${destroy}_s(ptr %o, i32 %d, ptr %tail) ${FN_ATTRS} {`, + `entry:`, + ...body, + `}`, + `define internal ptr @${destroy}_sc(ptr %o, ptr %tail) ${FN_ATTRS} {`, + `entry:`, + ` %t = call ptr @${destroy}_s(ptr %o, i32 0, ptr %tail)`, + ` ret ptr %t`, + `}`, + `define internal void @${destroy}_d(ptr %o, i32 %d) ${FN_ATTRS} {`, + `entry:`, + ` %head = alloca ptr`, + ` store ptr null, ptr %head`, + ` %last = call ptr @${destroy}_s(ptr %o, i32 %d, ptr %head)`, + ` %first = load ptr, ptr %head`, + ...emitSegmentFree(segment, "sgf", "%first", "%last"), + ` ret void`, + `}`, + `define internal void @${destroy}(ptr %o) ${FN_ATTRS} {`, + `entry:`, + ` call void @${destroy}_d(ptr %o, i32 0)`, + ` ret void`, + `}`, + ]; if (!chained) return [ `define internal void @${destroy}(ptr %o) ${FN_ATTRS} {`, @@ -1478,12 +1638,13 @@ export function releaseBody( bounded = true, immortal = false, chained = false, + segmented = false, ): string[] { const S = host.sizeType; if (bounded) host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); const destroy = `${fnName}_destroy`; const lines = [ - ...(bounded ? destroyDefinitions(destroy, freeBody, chained) : []), + ...(bounded ? destroyDefinitions(destroy, freeBody, chained, segmented ? host : null) : []), ...releaseFastPath(host, fnName, `${fnName}_slow`, traced, comment, immortal), `define internal void @${fnName}_slow(ptr %o) noinline ${FN_ATTRS} {${comment ? ` ; ${comment}` : ""}`, `entry:`, @@ -1715,26 +1876,58 @@ export function emitRecordShapes( const chained = directTargets.has(`record:${shape.id}`) || refMembers.some((m) => directDestroyTarget(host, m.type, directTargets) !== null); - for (const m of refMembers) { - freeBody.push( - ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${m.index}`, - ` %v${t} = load ptr, ptr %f${t}`, + const destroy = `${mangleRecordRelease(shape.id)}_destroy`; + const bounded = refMembers.some((member) => needsBoundedRelease(member.type)); + const segmented = + bounded && + segmentedTeardown( + host, + destroy, + refMembers.map((m) => m.type), + directTargets, ); - const direct = directDestroyTarget(host, m.type, directTargets); - if (direct) { - freeBody.push( - ...directChildReleaseLines(host, `td${t}`, `%v${t}`, direct, llvmCommentText(m.name)), - ); - } else { - freeBody.push( - ` call void ${releaseSym(host, m.type)}(ptr %v${t}) ; ${llvmCommentText(m.name)}`, - ); - } - t++; - } // Closed layouts can carry a kept record source (record-sources.ts). const observers = recordMayKeepSource(shape) ? "weak-or-source" : "weak"; - freeBody.push(...emitObjectFree(host, traced, observers)); + // A segmented teardown loads its fields, releases its own block, and + // then releases the fields (segmentTeardownLines). + const loads: string[] = []; + const ownType: string[] = []; + const release = (prefix: string, m: (typeof refMembers)[number], i: number): string[] => { + const direct = directDestroyTarget(host, m.type, directTargets); + if (direct === null) + return [` call void ${releaseSym(host, m.type)}(ptr %v${i}) ; ${llvmCommentText(m.name)}`]; + const own = segmented && direct.destroy === destroy; + return directChildReleaseLines( + host, + `${prefix}${i}`, + `%v${i}`, + direct, + llvmCommentText(m.name), + own, + ); + }; + for (const m of refMembers) { + const fieldLines = [ + ` %f${t} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${m.index}`, + ` %v${t} = load ptr, ptr %f${t}`, + ]; + if (segmented) { + loads.push(...fieldLines); + if (directDestroyTarget(host, m.type, directTargets)?.destroy === destroy) + ownType.push(`%v${t}`); + } else freeBody.push(...fieldLines, ...release("td", m, t)); + t++; + } + if (segmented) + freeBody.push( + ...segmentPrologueLines(), + ...loads, + ...emitObjectFree(host, traced, observers, "%sg.own"), + ...segmentTeardownLines(ownType, (prefix) => + refMembers.flatMap((m, i) => release(prefix, m, i)), + ), + ); + else freeBody.push(...emitObjectFree(host, traced, observers)); defs.push( ...releaseBody( host, @@ -1742,9 +1935,10 @@ export function emitRecordShapes( traced, freeBody, "", - refMembers.some((member) => needsBoundedRelease(member.type)), + bounded, false, chained, + segmented, ), ``, ); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json new file mode 100644 index 000000000..a7eac099f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7260-teardown-segments.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7260-teardown-segments.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 5e5029e14..1bd0ff7ac 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -293,24 +293,22 @@ static SCR_TL ScrDestroyEntry *scr_destroy_pending; static SCR_TL size_t scr_destroy_count, scr_destroy_capacity; #endif -void scr_rc_destroy(void *obj, void (*destroy)(void *)) { - if (scr_destroy_depth == SCR_DESTROY_DEPTH) { - if (scr_destroy_count == scr_destroy_capacity) { - size_t capacity = scr_destroy_capacity ? scr_destroy_capacity * 2 : 64; - if (capacity < scr_destroy_capacity || capacity > SIZE_MAX / sizeof(ScrDestroyEntry)) - scr_cyc_oom(); - ScrDestroyEntry *entries = realloc(scr_destroy_pending, capacity * sizeof(*entries)); - if (!entries) scr_cyc_oom(); - scr_destroy_pending = entries; - scr_destroy_capacity = capacity; - } - scr_destroy_pending[scr_destroy_count++] = (ScrDestroyEntry){ obj, destroy }; - return; +/* Queues a disposal past the depth budget; the outermost disposal runs it. */ +static void scr_rc_destroy_defer(void *obj, void (*destroy)(void *)) { + if (scr_destroy_count == scr_destroy_capacity) { + size_t capacity = scr_destroy_capacity ? scr_destroy_capacity * 2 : 64; + if (capacity < scr_destroy_capacity || capacity > SIZE_MAX / sizeof(ScrDestroyEntry)) + scr_cyc_oom(); + ScrDestroyEntry *entries = realloc(scr_destroy_pending, capacity * sizeof(*entries)); + if (!entries) scr_cyc_oom(); + scr_destroy_pending = entries; + scr_destroy_capacity = capacity; } - scr_destroy_depth++; - destroy(obj); - scr_destroy_depth--; - if (scr_destroy_depth) return; + scr_destroy_pending[scr_destroy_count++] = (ScrDestroyEntry){ obj, destroy }; +} + +/* Runs the queued disposals once the outermost one has returned. */ +static void scr_rc_destroy_drain(void) { while (scr_destroy_count) { ScrDestroyEntry entry = scr_destroy_pending[--scr_destroy_count]; scr_destroy_depth = 1; @@ -326,6 +324,30 @@ void scr_rc_destroy(void *obj, void (*destroy)(void *)) { } } +void scr_rc_destroy(void *obj, void (*destroy)(void *)) { + if (scr_destroy_depth == SCR_DESTROY_DEPTH) { + scr_rc_destroy_defer(obj, destroy); + return; + } + scr_destroy_depth++; + destroy(obj); + scr_destroy_depth--; + if (scr_destroy_depth == 0) scr_rc_destroy_drain(); +} + +void *scr_rc_destroy_seg(void *obj, void *(*destroy_seg)(void *, void *), + void (*destroy)(void *), void *tail) { + if (scr_destroy_depth == SCR_DESTROY_DEPTH) { + scr_rc_destroy_defer(obj, destroy); + return tail; + } + scr_destroy_depth++; + tail = destroy_seg(obj, tail); + scr_destroy_depth--; + if (scr_destroy_depth == 0) scr_rc_destroy_drain(); + return tail; +} + /* Live cycle-headered objects and the running count of OLD objects freed. * The inline allocation/free paths (scr_runtime.h, llvm/alloc.ts) maintain * both counters so full passes can measure old-generation reclamation. */ diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 7a921b451..83d0589c1 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -552,6 +552,14 @@ _Static_assert(sizeof(((ScrCycHdr *)0)->gen) == 1 && SCR_CYC_OLD == 2, * depth; deeper objects wait until the outer disposal drains the worklist. * The callback owns the object and must release its children and free it. */ void scr_rc_destroy(void *obj, void (*destroy)(void *)); +/* scr_rc_destroy for an emitted teardown that keeps the blocks it frees in + * a segment, linked through their first words, instead of freeing them onto + * the free list (llvm/shapes.ts): destroy_seg tears obj down, appending its + * blocks after the segment's last link `tail`, and returns the new last + * link, which this returns. A disposal past the depth budget is queued with + * `destroy`, which frees its own blocks, and leaves `tail` as it was. */ +void *scr_rc_destroy_seg(void *obj, void *(*destroy_seg)(void *, void *), + void (*destroy)(void *), void *tail); /* RC hooks for cycle-headered types. on_release: rc was decremented and * stayed above zero — buffer the object as a possible cycle root (may run diff --git a/scripts/ci-test-costs.json b/scripts/ci-test-costs.json index 4c4964579..99d47e972 100644 --- a/scripts/ci-test-costs.json +++ b/scripts/ci-test-costs.json @@ -409,6 +409,7 @@ "tests/harness/stdlib-surface.test.ts": 48, "tests/harness/strip-vt-linear.test.ts": 1795, "tests/harness/surface-manifest.test.ts": 5309, + "tests/harness/teardown-segments.test.ts": 2600, "tests/harness/terminal-geometry.test.ts": 2730, "tests/harness/test262-runner.test.ts": 207, "tests/harness/three-library.test.ts": 251122, @@ -828,6 +829,7 @@ "tests/harness/stdlib-surface.test.ts": 46, "tests/harness/strip-vt-linear.test.ts": 2546, "tests/harness/surface-manifest.test.ts": 5951, + "tests/harness/teardown-segments.test.ts": 5, "tests/harness/terminal-geometry.test.ts": 3270, "tests/harness/test262-runner.test.ts": 230, "tests/harness/three-library.test.ts": 361470, diff --git a/tests/corpus/7260-teardown-segments.ts b/tests/corpus/7260-teardown-segments.ts new file mode 100644 index 000000000..cbd2b144d --- /dev/null +++ b/tests/corpus/7260-teardown-segments.ts @@ -0,0 +1,179 @@ +// Teardowns that hand their blocks back in order. +// +// A typed teardown of a type with children of its own type keeps the blocks +// it frees in one segment and frees the segment when the chain's first +// teardown returns, so the next structure built of that type reuses them in +// order. This program rebuilds structures of such types for many +// generations and reads every value of each generation before dropping it: +// a block handed out twice, or one freed while still in use, shows up as a +// wrong checksum. The shapes cover the ways a segment is put together: +// record literals (allocated before their fields) and constructors whose +// arguments build the children first, lists built by prepending and by +// appending, trees far deeper than one direct teardown chain and lists +// longer than the runtime's whole depth budget (whose tails wait in its +// queue), subtrees still owned elsewhere when their parent goes, records of +// one type holding records of another, and nodes of a cycle-capable class +// that are still candidates of the cycle collector when the last reference +// goes, and rings of them that only the collector frees. + +interface Node { + left: Node | null; + right: Node | null; + value: number; +} + +function literalTree(depth: number, value: number): Node { + return depth === 0 + ? { left: null, right: null, value } + : { left: literalTree(depth - 1, value * 2), right: literalTree(depth - 1, value * 2 + 1), value }; +} + +function sum(node: Node | null): number { + return node === null ? 0 : node.value + sum(node.left) + sum(node.right); +} + +class Branch { + readonly left: Branch | null; + readonly right: Branch | null; + readonly label: string; + constructor(left: Branch | null, right: Branch | null, label: string) { + this.left = left; + this.right = right; + this.label = label; + } +} + +function classTree(depth: number, label: string): Branch { + return depth === 0 + ? new Branch(null, null, label) + : new Branch(classTree(depth - 1, label + "l"), classTree(depth - 1, label + "r"), label); +} + +function labels(branch: Branch | null): number { + return branch === null ? 0 : branch.label.length + labels(branch.left) + labels(branch.right); +} + +interface Cell { + next: Cell | null; + n: number; +} + +function prepended(count: number, base: number): Cell | null { + let head: Cell | null = null; + for (let i = 0; i < count; i++) head = { next: head, n: base + i }; + return head; +} + +function appended(count: number, base: number): Cell | null { + if (count === 0) return null; + const head: Cell = { next: null, n: base }; + let tail = head; + for (let i = 1; i < count; i++) { + const cell: Cell = { next: null, n: base + i }; + tail.next = cell; + tail = cell; + } + return head; +} + +function cells(head: Cell | null): [number, number] { + let count = 0; + let total = 0; + for (let cell = head; cell !== null; cell = cell.next) { + count++; + total += cell.n; + } + return [count, total]; +} + +// Generations of literal trees: shallow, one chain deep, and several chains +// deep. Each generation is checked against the sum the shape must have. +for (const depth of [0, 1, 5, 16, 17, 18]) { + let ok = true; + for (let generation = 0; generation < 6; generation++) { + const tree = literalTree(depth, 1); + const expected = ((2 ** (depth + 1)) * (2 ** (depth + 1) - 1)) / 2; + if (sum(tree) !== expected) ok = false; + } + console.log(`literal trees of depth ${depth}: ${ok}`); +} + +// Generations of class trees built children first. +for (const depth of [3, 12, 17]) { + let total = 0; + for (let generation = 0; generation < 4; generation++) total += labels(classTree(depth, "t")); + console.log(`class trees of depth ${depth}: ${total}`); +} + +// Lists both ways, alternating, so each reuses the other's blocks; the long +// ones outlast the runtime's depth budget. +for (const count of [1, 15, 16, 17, 1000, 3000, 40000]) { + const seen: string[] = []; + for (let generation = 0; generation < 4; generation++) { + const list = generation % 2 === 0 ? prepended(count, generation) : appended(count, generation); + const [length, total] = cells(list); + seen.push(`${length}/${total}`); + } + console.log(`lists of ${count}: ${seen.join(" ")}`); +} + +// Subtrees owned elsewhere survive their parent's teardown, and the blocks +// freed around them are not handed out while they live. +const kept: Node[] = []; +for (let generation = 0; generation < 5; generation++) { + const tree = literalTree(10, generation + 1); + if (tree.left !== null && tree.left.right !== null) kept.push(tree.left.right); + if (tree.right !== null) kept.push(tree.right); + // Building the next tree reuses the dropped blocks. + literalTree(10, 7); +} +console.log(`kept subtrees: ${kept.length} ${kept.map(sum).join(",")}`); + +// A record of one type holding trees of another: the outer teardown frees +// its own blocks as before and tears each inner tree down as a segment. +interface Forest { + first: Node; + second: Node | null; + rest: Forest | null; +} + +function forest(count: number): Forest | null { + let out: Forest | null = null; + for (let i = 0; i < count; i++) out = { first: literalTree(4, i), second: i % 3 === 0 ? null : literalTree(2, i), rest: out }; + return out; +} + +let forestTotal = 0; +for (let generation = 0; generation < 3; generation++) { + for (let f = forest(500); f !== null; f = f.rest) forestTotal += sum(f.first) + sum(f.second); +} +console.log(`forests: ${forestTotal}`); + +// A cycle-capable class: releases of shared links make nodes candidates of +// the cycle collector, which are then torn down while still buffered, and +// the peer links close some rings, which the collector frees. +class Ring { + next: Ring | null = null; + peer: Ring | null = null; + id: number; + constructor(id: number) { + this.id = id; + } +} + +let ringTotal = 0; +for (let generation = 0; generation < 3; generation++) { + let head: Ring | null = null; + const peers: Ring[] = []; + for (let i = 0; i < 2000; i++) { + const ring: Ring = new Ring(i); + ring.next = head; + if (i % 7 === 0) peers.push(ring); + head = ring; + } + for (let r = head; r !== null; r = r.next) if (r.id % 5 === 0) r.peer = peers[r.id % peers.length] ?? null; + for (const peer of peers) ringTotal += peer.id; + peers.length = 0; + for (let r = head; r !== null; r = r.next) ringTotal += r.peer === null ? 0 : r.peer.id; +} +console.log(`rings: ${ringTotal}`); diff --git a/tests/harness/corpus-costs/7260-teardown-segments.ts.json b/tests/harness/corpus-costs/7260-teardown-segments.ts.json new file mode 100644 index 000000000..7f7ce1c2b --- /dev/null +++ b/tests/harness/corpus-costs/7260-teardown-segments.ts.json @@ -0,0 +1 @@ +{ "plain": 5343, "san": 11960 } diff --git a/tests/harness/teardown-segments.test.ts b/tests/harness/teardown-segments.test.ts new file mode 100644 index 000000000..95a2d2795 --- /dev/null +++ b/tests/harness/teardown-segments.test.ts @@ -0,0 +1,97 @@ +/* Teardown segments (llvm/shapes.ts): a typed teardown of a type with + * children of its own type keeps the blocks it frees in a segment and frees + * the segment onto the free list when the chain's first teardown returns. + * Corpus program 7260 pins what such programs print; this suite checks the + * memory side natively, which Node parity cannot: a program that rebuilds + * large trees and lists for a hundred generations reuses every block of the + * last generation, so its resident set stays at about one generation's + * worth. A segment that lost blocks would grow it by a generation each time. + * The sanitized lane frees through the system allocator (no segments) and + * keeps freed memory in quarantine, so the bound is checked in the plain + * lane only. */ +import { execFile } from "node:child_process"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const exec = promisify(execFile); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +const PROGRAM = ` +interface Node { + left: Node | null; + right: Node | null; + value: number; +} +function tree(depth: number): Node { + return depth === 0 + ? { left: null, right: null, value: 1 } + : { left: tree(depth - 1), right: tree(depth - 1), value: depth }; +} +function size(node: Node | null): number { + return node === null ? 0 : 1 + size(node.left) + size(node.right); +} +class Branch { + readonly left: Branch | null; + readonly right: Branch | null; + constructor(left: Branch | null, right: Branch | null) { + this.left = left; + this.right = right; + } +} +function branches(depth: number): Branch { + return depth === 0 ? new Branch(null, null) : new Branch(branches(depth - 1), branches(depth - 1)); +} +function count(branch: Branch | null): number { + return branch === null ? 0 : 1 + count(branch.left) + count(branch.right); +} +interface Cell { + next: Cell | null; + n: number; +} +function cells(n: number): Cell | null { + let head: Cell | null = null; + for (let i = 0; i < n; i++) head = { next: head, n: i }; + return head; +} +let total = 0; +let settled = 0; +for (let generation = 0; generation < 100; generation++) { + total += size(tree(16)) + count(branches(15)); + for (let c = cells(100000); c !== null; c = c.next) total += 1; + if (generation === 2) settled = process.memoryUsage.rss(); +} +const growth = Math.round((process.memoryUsage.rss() - settled) / (1024 * 1024)); +console.log(total, growth); +`; + +test.skipIf(sanitize)( + "rebuilding large trees and lists reuses every block their teardowns free", + { timeout: 300_000 }, + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-segments-")); + try { + const source = join(dir, "churn.ts"); + await writeFile(source, PROGRAM); + const result = await compile(source, { + backend: "llvm", + outDir: dir, + outPath: join(dir, "churn"), + }); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + if (!result.ok) return; + const { stdout } = await exec(result.binaryPath, [], { timeout: 120_000 }); + const [total, growth] = stdout.trim().split(" ").map(Number); + // 100 generations of 131071 + 65535 nodes and 100000 cells. + expect(total).toBe(100 * (131071 + 65535 + 100000)); + // One generation is about 10 MiB; a lost generation per round would be + // hundreds. + expect(growth).toBeLessThan(8); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, +); From 74fcda0ad93deb463249c9a61c244f5e9d57ded2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:17:42 -0500 Subject: [PATCH 009/409] Evaluate every Promise.all entry before a dynamic entry's then lookup A checked-dynamic entry's PromiseResolve reads its `then` property, which can be a getter. The tuple form resolved each entry as the array was built, so that getter ran before later entries evaluated; Node evaluates the whole array first. With a checked-dynamic entry, every entry now evaluates into a hidden local before the entries resolve. --- .../frontend/lowering/builtins/promises.ts | 28 +++++++++++++++++-- tests/corpus/7035-promise-all-tuple-turns.ts | 13 +++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index c30590619..3aecf87e3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -72,7 +72,28 @@ function lowerTuplePromiseAll( const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID }; const entriesT = arrayOf(voidPromise); const dynPromise: IrType = { kind: "promise", inner: DYN }; - const entries = elems.map((e): IrExpr => { + // A checked-dynamic entry's PromiseResolve can run a `then` getter, which + // Node runs after the whole array literal evaluated. With such an entry, + // every entry evaluates into a hidden local first. + const staged: IrStmt[] = []; + const values = !elems.some((e) => e.type.kind === "dyn") + ? elems + : elems.map((e): IrExpr => { + if (e.kind === "unitLit") return e; + if (isUnitType(e.type)) { + staged.push({ kind: "exprStmt", expr: e, loc }); + return { + kind: "unitLit", + unit: e.type.kind === "nullT" ? "null" : "undefined", + type: e.type, + loc, + }; + } + const local = lowerer.declareHiddenLocal("%allValue", e.type); + staged.push({ kind: "varDecl", localId: local.id, init: e, loc }); + return varRef(local.id, e.type, loc); + }); + const entries = values.map((e): IrExpr => { let promise: IrExpr; if (e.type.kind === "promise") promise = e; else if (e.type.kind === "dyn") @@ -164,7 +185,7 @@ function lowerTuplePromiseAll( loc, }); } - return { + const all: IrExpr = { kind: "libCall", fn: "promise.allTuple", args: [ @@ -174,6 +195,9 @@ function lowerTuplePromiseAll( type: resultT, loc, }; + return staged.length === 0 + ? all + : { kind: "seqExpr", stmts: staged, result: all, type: resultT, loc }; } /** `Promise.race([...])` on THE Promise global: the entries lower diff --git a/tests/corpus/7035-promise-all-tuple-turns.ts b/tests/corpus/7035-promise-all-tuple-turns.ts index 6989df9b9..1f8a8803d 100644 --- a/tests/corpus/7035-promise-all-tuple-turns.ts +++ b/tests/corpus/7035-promise-all-tuple-turns.ts @@ -26,6 +26,16 @@ async function text(): Promise { async function both(): Promise<[number, string]> { return Promise.all([num(), text()]); } +const thenable: any = { + get then() { + log.push("then getter"); + return (resolve: (v: number) => void) => resolve(1); + }, +}; +function evaluated(): Promise { + log.push("second entry evaluated"); + return Promise.resolve("s"); +} interface Item { id: number; } @@ -49,6 +59,9 @@ async function main(): Promise { log.push(`returned ${h}${i}`); const mixed = await Promise.all([later("x", 3, "x"), 5, later("y", 1, true), undefined]); log.push(`mixed ${mixed[0]} ${mixed[1]} ${mixed[2]} ${mixed[3]}`); + // A dynamic entry's `then` getter runs after the whole array evaluated. + const [fromThenable, second] = await Promise.all([thenable, evaluated()]); + log.push(`thenable ${fromThenable} ${second}`); const raw: any = JSON.parse('{"value":41}'); const [dynamic, counted] = await Promise.all([raw.value, later("z", 2, 2)]); log.push(`dynamic ${dynamic + counted}`); From 9e1027e13a35c1e6503f4e1b72e9c1aa3bb1af69 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 11:46:44 -0500 Subject: [PATCH 010/409] Run async generator steps in the AsyncLocalStorage context of their resumer An async generator kept the context of its creation for its whole life, and a store its body entered carried into every later step. Node runs each step in the execution that resumes it: a step starts with the context of the next(), return() or throw() call, a store it enters before its first await is the caller's after the call, and after an await the step continues in its own copy. Async generators now start each new step from the resumer's context and hand the prefix's context back at the step's first suspension, as synchronous generator steps and async function prefixes already do; the continuation after an await keeps the generator's own. Two requests that start from the settlement job, which runs from the loop, take Node's contexts too: a request queued behind a running step starts in that step's context, and a return() at a yield, whose unwind waits a turn, runs in the return() call's context. Corpus 7381 and 7382. --- .../7381-als-async-generator-steps.ts.json | 6 + .../7382-als-async-generator-steps.mjs.json | 6 + packages/runtime/src/scr_async.c | 80 +++++++++-- .../corpus/7381-als-async-generator-steps.ts | 133 ++++++++++++++++++ .../corpus/7382-als-async-generator-steps.mjs | 43 ++++++ .../7381-als-async-generator-steps.ts.json | 1 + .../7382-als-async-generator-steps.mjs.json | 1 + 7 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json create mode 100644 tests/corpus/7381-als-async-generator-steps.ts create mode 100644 tests/corpus/7382-als-async-generator-steps.mjs create mode 100644 tests/harness/corpus-costs/7381-als-async-generator-steps.ts.json create mode 100644 tests/harness/corpus-costs/7382-als-async-generator-steps.mjs.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json new file mode 100644 index 000000000..47f93314d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-als-async-generator-steps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7381-als-async-generator-steps.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json new file mode 100644 index 000000000..5f55ba7bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-als-async-generator-steps.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7382-als-async-generator-steps.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 90e7dd04f..e6184ed9b 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -715,11 +715,13 @@ static void scr_als_prefix_end(ScrFiber *f) { } } -/* A synchronous generator has no context of its own: each step runs in its - * resumer's execution, as Node runs it. A fiber-backed one switches to its - * own slot, so its step starts from the resumer's store and a store it - * enters is the resumer's after the step (scr_als_prefix_end on the way - * back); a stackless one runs in the resumer's slot (scr_gen_coro_hop). */ +/* A generator step runs in its resumer's execution, as Node runs it. A + * fiber-backed generator switches to its own slot, so its step starts from + * the resumer's store and a store it enters is the resumer's after the + * step (scr_als_prefix_end on the way back); a stackless synchronous one + * runs in the resumer's slot (scr_gen_coro_hop). An async generator's step + * starts the same way, and after an await it continues in its own copy of + * the context, as an async function's body does after its prefix. */ static void scr_als_step_begin(ScrFiber *f) { ScrAlsCtx **slot = SCR_ALS_SLOT(); if (f->als != *slot) { @@ -5303,6 +5305,10 @@ struct ScrGen { * since that return takes its own turn to await x. */ bool return_queued; bool return_awaited; + /* The AsyncLocalStorage context of a return() request deferred at a + * yield (+1): the body's unwind runs in it, as Node resumes it from the + * return value's await. */ + ScrAlsCtx *return_ctx; #ifdef SCR_WORKERS bool context_abandoned; #endif @@ -5406,6 +5412,7 @@ void scr_gen_release(ScrGen *g) { scr_gen_slot_reset(&g->in); scr_gen_slot_reset(&g->ret); scr_gen_exc_reset(&g->async_error); + scr_als_ctx_release(g->return_ctx); if (g->fiber != NULL) { if (g->state == SCR_GEN_UNSTARTED) { /* Never ran: nothing on the stack owns anything — clean teardown. @@ -5760,6 +5767,12 @@ __attribute__((always_inline)) static inline void scr_gen_switched_back(ScrGen * if (release) scr_gen_release(g); } +/* Out of line, so the hop that synchronous generators share stays as it + * was: an async generator's step begins from its resumer's context and + * hands the prefix's context back at its first suspension. */ +static __attribute__((noinline)) void scr_async_gen_step_begin(ScrFiber *f) { scr_als_step_begin(f); } +static __attribute__((noinline)) void scr_async_gen_step_end(ScrFiber *f) { scr_als_prefix_end(f); } + #ifndef __wasi__ /* The stackless form of the hop: the coroutine frame runs on this stack * until its next yield or its final suspension returns here. `retain` @@ -5768,14 +5781,17 @@ __attribute__((always_inline)) static inline void scr_gen_switched_back(ScrGen * __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bool retain) { ScrFiber *f = g->fiber; if (retain) scr_gen_retain(g); + /* A synchronous generator has no AsyncLocalStorage context of its own: + * its step runs in the resumer's slot, as Node runs it in the execution + * that called next(). An async generator's step runs in its own slot, + * which a new step (not the continuation after an await) starts from the + * resumer's context (scr_als_step_begin). */ + if (g->is_async && g->state != SCR_GEN_AWAITING) scr_async_gen_step_begin(f); g->state = SCR_GEN_RUNNING; f->resumer = scr_current; ScrAlsCtx **resumer_slot = scr_als_active; scr_current = f; scr_exc_swap_cell(&f->exc); - /* A synchronous generator has no AsyncLocalStorage context of its own: - * its step runs in the resumer's slot, as Node runs it in the execution - * that called next(). An async generator's step runs in its own. */ if (g->is_async) scr_als_active = &f->als; if (f->coro == NULL) f->entry(f, f->argpack); else scr_coro_resume(f->coro); @@ -5786,6 +5802,7 @@ __attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bo scr_current = me; scr_exc_swap_cell(me != NULL ? &me->exc : NULL); scr_als_active = resumer_slot; + if (g->is_async) scr_async_gen_step_end(f); scr_gen_switched_back(g, f, retain, true); } @@ -5806,8 +5823,10 @@ static void scr_gen_switch_in(ScrGen *g) { } #endif scr_gen_retain(g); + /* Each step starts from the resumer's context; an async generator's + * continuation after an await keeps its own. */ + if (!g->is_async || g->state != SCR_GEN_AWAITING) scr_als_step_begin(f); g->state = SCR_GEN_RUNNING; - if (!g->is_async) scr_als_step_begin(f); #ifdef _WIN32 ScrCtx here = scr_win_self(); #elif defined(__wasi__) @@ -5979,6 +5998,25 @@ void scr_gen_resume_throw(ScrGen *g) { static void scr_async_gen_start_queued(ScrGen *g); +/* Runs fn(g) with `ctx` (borrowed) as the active AsyncLocalStorage + * context. A settlement job runs from the loop: a request queued behind + * another starts in the context of the step before it (Node drains the + * queue in the generator's own execution), and a deferred return() in its + * call's. */ +static __attribute__((noinline)) void scr_async_gen_in_context(ScrGen *g, ScrAlsCtx *ctx, + void (*fn)(ScrGen *)) { + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_als_ctx_retain(ctx); + fn(g); + scr_als_restore(prev); +} + +/* Starts the queued requests in the context of the step before them. */ +static __attribute__((noinline)) void scr_async_gen_start_queued_in_own(ScrGen *g) { + scr_async_gen_in_context(g, g->fiber != NULL ? g->fiber->als : NULL, scr_async_gen_start_queued); +} + static void scr_async_gen_schedule_settle(ScrGen *g) { if (g->settle_queued) return; ScrFiber *job = scr_mem_calloc(sizeof *job); @@ -6026,6 +6064,11 @@ static bool scr_async_gen_settles_now(ScrGen *g) { return false; } +static __attribute__((noinline)) void scr_async_gen_capture_return_ctx(ScrGen *g) { + scr_als_ctx_release(g->return_ctx); + g->return_ctx = scr_als_ctx_retain(*SCR_ALS_SLOT()); +} + static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { const bool was_done = g->state == SCR_GEN_DONE; if (req->mode == SCR_AG_NEXT) { @@ -6041,6 +6084,7 @@ static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { /* At a yield, AsyncGeneratorYield awaits the return value before * the body resumes, so finally blocks run one turn later. */ g->return_queued = true; + scr_async_gen_capture_return_ctx(g); scr_async_gen_schedule_settle(g); return; } @@ -6071,6 +6115,16 @@ static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { } } +static void scr_async_gen_resume_returning(ScrGen *g); + +/* The deferred return() resumes in its call's context. */ +static __attribute__((noinline)) void scr_async_gen_resume_returning_in_context(ScrGen *g) { + ScrAlsCtx *ctx = g->return_ctx; + g->return_ctx = NULL; + scr_async_gen_in_context(g, ctx, scr_async_gen_resume_returning); + scr_als_ctx_release(ctx); +} + /* The return() request's await has taken its turn: resume the body with * the return completion. */ static void scr_async_gen_resume_returning(ScrGen *g) { @@ -6203,12 +6257,12 @@ static void scr_async_gen_job(ScrGen *g) { } if (g->return_queued) { scr_gen_retain(g); - scr_async_gen_resume_returning(g); + scr_async_gen_resume_returning_in_context(g); scr_gen_release(g); return; } scr_async_gen_finish_active(g, false); - scr_async_gen_start_queued(g); + scr_async_gen_start_queued_in_own(g); } static void scr_async_gen_resume_ready(ScrGen *g) { @@ -6222,11 +6276,11 @@ static void scr_async_gen_resume_ready(ScrGen *g) { scr_gen_switch_in(g); if (g->async_error.kind != SCR_EXC_NONE) { scr_async_gen_finish_active(g, true); - scr_async_gen_start_queued(g); + scr_async_gen_start_queued_in_own(g); } else if (g->state != SCR_GEN_AWAITING) { if (scr_async_gen_settles_now(g)) { scr_async_gen_finish_active(g, false); - scr_async_gen_start_queued(g); + scr_async_gen_start_queued_in_own(g); } else { scr_async_gen_schedule_settle(g); } diff --git a/tests/corpus/7381-als-async-generator-steps.ts b/tests/corpus/7381-als-async-generator-steps.ts new file mode 100644 index 000000000..4f6ace58d --- /dev/null +++ b/tests/corpus/7381-als-async-generator-steps.ts @@ -0,0 +1,133 @@ +// AsyncLocalStorage contexts of async generator steps. Each step starts in +// the context of the next(), return() or throw() call that resumes it, +// whatever context created the generator; a store the step enters before +// its first await is the caller's after the call, and after an await the +// step keeps its own copy. A request queued behind a running step starts +// in that step's context, and a return() at a yield unwinds in the +// return() call's context. Covers yield*, for await with break, and two +// generators resumed in turn. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const lines: string[] = []; +const log = (what: string): void => { + lines.push(`${what}: ${store.getStore() ?? "none"}`); +}; + +async function* steps(): AsyncGenerator { + log("body start"); + yield 1; + log("after the first yield"); + await null; + log("after an await"); + store.enterWith("inside"); + yield 2; + log("after the second yield"); + yield 3; + log("body end"); +} +const it = store.run("create", () => steps()); +await store.run("next-1", () => it.next()); +log("caller after next-1"); +await store.run("next-2", () => it.next()); +log("caller after next-2"); +await store.run("next-3", () => it.next()); +await store.run("next-4", () => it.next()); + +async function* prefixes(): AsyncGenerator { + store.enterWith("prefix"); + yield 1; + store.enterWith("step-2"); + await null; + log("after an await in step 2"); + yield 2; +} +await store.run("outer", async () => { + const gen = prefixes(); + const first = gen.next(); + log("synchronously after next"); + await first; + log("after awaiting next"); + const second = gen.next(); + log("synchronously after the second next"); + await second; + log("after awaiting the second next"); +}); + +async function* queued(): AsyncGenerator { + log("queued start"); + yield 1; + log("queued resumed"); + yield 2; +} +const q = queued(); +await Promise.all([store.run("q-1", () => q.next()), store.run("q-2", () => q.next())]); + +async function* inner(): AsyncGenerator { + log("inner start"); + yield "a"; + log("inner resumed"); + yield "b"; +} +async function* outerGen(): AsyncGenerator { + log("outer start"); + yield* inner(); + log("outer after yield*"); +} +const delegating = outerGen(); +await store.run("o-1", () => delegating.next()); +await store.run("o-2", () => delegating.next()); +await store.run("o-3", () => delegating.next()); + +async function* catching(): AsyncGenerator { + try { + yield 1; + } catch (e) { + log(`caught ${(e as Error).message}`); + yield 2; + } +} +const c = catching(); +await store.run("t-1", () => c.next()); +await store.run("t-2", () => c.throw(new Error("boom"))); + +async function* finallyGen(): AsyncGenerator { + try { + yield 1; + } finally { + log("finally"); + } +} +const f = finallyGen(); +await store.run("n", () => f.next()); +await store.run("return-call", () => f.return()); + +async function* endless(): AsyncGenerator { + let i = 0; + try { + while (true) { + log(`endless ${i}`); + yield i++; + } + } finally { + log("endless finally"); + } +} +await store.run("loop", async () => { + for await (const v of endless()) { + if (v === 1) break; + } +}); + +async function* named(name: string): AsyncGenerator { + log(`${name} 1`); + yield 1; + log(`${name} 2`); + yield 2; +} +const a = named("A"); +const b = named("B"); +await Promise.all([store.run("a-1", () => a.next()), store.run("b-1", () => b.next())]); +await Promise.all([store.run("b-2", () => b.next()), store.run("a-2", () => a.next())]); + +for (const line of lines) console.log(line); diff --git a/tests/corpus/7382-als-async-generator-steps.mjs b/tests/corpus/7382-als-async-generator-steps.mjs new file mode 100644 index 000000000..bbabdb592 --- /dev/null +++ b/tests/corpus/7382-als-async-generator-steps.mjs @@ -0,0 +1,43 @@ +// AsyncLocalStorage contexts of async generator steps from JavaScript: +// steps start in the context of the call that resumes them, a store a step +// enters before its first await is the caller's, queued requests start in +// the running step's context, and return() at a yield unwinds in the +// return() call's context. +import { AsyncLocalStorage } from "node:async_hooks"; +const s = new AsyncLocalStorage(); +const lines = []; +const g = (t) => lines.push(`${t}: ${s.getStore() ?? "none"}`); +async function* gen() { + g("body start"); + yield 1; + g("after first yield"); + await null; + g("after await"); + s.enterWith("inside"); + yield 2; + g("after second yield"); + yield 3; + g("body end"); +} +const it = s.run("create", () => gen()); +await s.run("next1", () => it.next()); +g("caller after next1"); +await s.run("next2", () => it.next()); +g("caller after next2"); +await s.run("next3", () => it.next()); +await s.run("next4", () => it.next()); +// concurrent next calls queued +async function* gen2() { g("g2 start"); yield 1; g("g2 resumed"); yield 2; } +const it2 = gen2(); +const p1 = s.run("q1", () => it2.next()); +const p2 = s.run("q2", () => it2.next()); +await Promise.all([p1, p2]); +// for await +async function* gen3() { for (let i = 0; i < 2; i++) { g("g3 step " + i); yield i; } } +await s.run("for-await", async () => { for await (const v of gen3()) g("loop body " + v); }); +// return/throw +async function* gen4() { try { yield 1; } finally { g("g4 finally"); } } +const it4 = gen4(); +await s.run("n", () => it4.next()); +await s.run("ret", () => it4.return()); +for (const line of lines) console.log(line); diff --git a/tests/harness/corpus-costs/7381-als-async-generator-steps.ts.json b/tests/harness/corpus-costs/7381-als-async-generator-steps.ts.json new file mode 100644 index 000000000..3f7384f20 --- /dev/null +++ b/tests/harness/corpus-costs/7381-als-async-generator-steps.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 6000 } diff --git a/tests/harness/corpus-costs/7382-als-async-generator-steps.mjs.json b/tests/harness/corpus-costs/7382-als-async-generator-steps.mjs.json new file mode 100644 index 000000000..3f7384f20 --- /dev/null +++ b/tests/harness/corpus-costs/7382-als-async-generator-steps.mjs.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 6000 } From 35f2d8f791022b779972e54a55d2b369ab657fef Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:07:47 -0500 Subject: [PATCH 011/409] Report dictionary copies of `{}` views, and keep method calls direct after writes into dictionaries of dictionaries Fixes #1408 Co-authored-by: Chris Ainsley --- .../record-identity-and-property-order.mdx | 1 + .../compiler/src/diagnostics/diagnostic.ts | 10 +- .../lowering/class-method-mutations.ts | 16 ++- .../src/frontend/lowering/divergences.ts | 110 +++++++++++++++++- .../test/class-method-mutations.test.ts | 42 +++++++ ...onary-of-dictionaries-keep-methods.ts.json | 6 + ...dictionary-of-dictionaries-keep-methods.ts | 38 ++++++ .../dictionary-view-copies.ts | 29 +++++ tests/coverage-fixtures/no-divergences.ts | 11 +- .../coverage-dictionary-view-copies.txt | 21 ++++ ...onary-of-dictionaries-keep-methods.ts.json | 1 + tests/harness/coverage.test.ts | 14 +++ 12 files changed, 288 insertions(+), 11 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7010-dictionary-of-dictionaries-keep-methods.ts.json create mode 100644 tests/corpus/7010-dictionary-of-dictionaries-keep-methods.ts create mode 100644 tests/coverage-fixtures/dictionary-view-copies.ts create mode 100644 tests/harness/__snapshots__/coverage-dictionary-view-copies.txt create mode 100644 tests/harness/corpus-costs/7010-dictionary-of-dictionaries-keep-methods.ts.json diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index 0381803f6..224757d52 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -1,6 +1,7 @@ A record converted to a record type with a different set of properties, such as a narrower shape or one that adds optional properties, is copied. Mutations through the converted reference do not affect the original, and the copy is not `===` to it. Empty record layouts are also snapshots because they have no property storage. +A class instance or record held as `{}`, `object` or `unknown` and then converted to a dictionary type such as `Record` is copied as well; a dictionary of `unknown` values keeps the original object. `scriptc coverage` and `scriptc build` report `SC6001` where a copied value is then written or compared, whether the record was passed as an argument, stored in a typed binding, asserted with `as`, assigned to a typed binding later, or stored as an element of a typed array literal. Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage. diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 17cb2efc0..47db2193b 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -129,6 +129,7 @@ export function widthCopyDivergenceDiag( useText: string, site: "call" | "declaration" | "cast" | "element", loc: SrcLoc, + dictionary = false, ): ScrDiagnostic { const how = site === "call" @@ -142,11 +143,16 @@ export function widthCopyDivergenceDiag( use === "write" ? `the write to '${useText}' updates only the copy, while under Node it updates the original object` : `'${useText}' compares the copy, which is never the original object, while under Node it is the same object`; + const cause = dictionary + ? "converting an object held as '{}', 'object' or 'unknown' to a dictionary type" + : "converting to a record type with other properties"; return { code: "SC6001", - message: `${how} copies the object (converting to a record type with other properties compiles as a field copy): ${effect}`, + message: `${how} copies the object (${cause} compiles as a field copy): ${effect}`, loc, - hint: `declare the parameter or binding with the value's full type ('${sourceType}'), or make the shape a class — class instances keep their identity when passed as a base class`, + hint: dictionary + ? `keep the value at its own type ('${sourceType}'), or declare the dictionary's values as 'unknown', which keeps the original object` + : `declare the parameter or binding with the value's full type ('${sourceType}'), or make the shape a class — class instances keep their identity when passed as a base class`, }; } diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index abbaf3a75..7629377d4 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -125,9 +125,12 @@ export function collectClassMethodMutations( }; // Whether no function value can inhabit `type`. Primitives qualify, and so // do non-callable object types a function is not assignable to: one that - // requires a property functions lack, or a weak (all-optional) type that + // requires a property functions lack, a weak (all-optional) type that // shares no property with functions (TypeScript's weak-type rule rejects - // callable sources too). + // callable sources too), or a dictionary whose string index values are + // neither `any` nor a type parameter: a function type has no index + // signature, and only an `any` index admits a source without one (an + // instantiation can make a type parameter `any`). const functionFree = (type: ts.Type): boolean => { if (type.isUnionType()) return ts.constituentTypes(type).every(functionFree); if ( @@ -149,7 +152,14 @@ export function collectClassMethodMutations( ) return false; const properties = lowerer.checker.getPropertiesOfType(type); - if (properties.length === 0) return false; + if (properties.length === 0) + return lowerer.checker + .getIndexInfosOfType(type) + .some( + (index) => + (index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && + (index.type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Instantiable)) === 0, + ); const required = properties.filter( (property) => (property.flags & ts.SymbolFlags.Optional) === 0, ); diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index 1c362cb09..f766aba14 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -180,7 +180,15 @@ export function scanDivergences( const copy = safely(() => widthCopyBetween(lowerer, arg, param.type!)); if (copy === undefined || copy === null) return; out.push( - widthCopyDivergenceDiag(copy.source, copy.target, use.kind, use.text, "call", locOf(arg)), + widthCopyDivergenceDiag( + copy.source, + copy.target, + use.kind, + use.text, + "call", + locOf(arg), + copy.dictionary, + ), ); }); } @@ -200,6 +208,7 @@ export function scanDivergences( use.text, "declaration", locOf(decl.initializer!), + copy.dictionary, ), ); } @@ -215,7 +224,15 @@ export function scanDivergences( const copy = safely(() => widthCopyBetween(lowerer, source, type)); if (copy === undefined || copy === null) continue; out.push( - widthCopyDivergenceDiag(copy.source, copy.target, use.kind, use.text, "cast", locOf(source)), + widthCopyDivergenceDiag( + copy.source, + copy.target, + use.kind, + use.text, + "cast", + locOf(source), + copy.dictionary, + ), ); } for (const assignment of assignments) { @@ -240,6 +257,7 @@ export function scanDivergences( use.text, "declaration", locOf(assignment.right), + copy.dictionary, ), ); } @@ -254,7 +272,15 @@ export function scanDivergences( const copy = safely(() => widthCopyBetween(lowerer, item, element)); if (copy === undefined || copy === null) continue; out.push( - widthCopyDivergenceDiag(copy.source, copy.target, "write", use, "element", locOf(item)), + widthCopyDivergenceDiag( + copy.source, + copy.target, + "write", + use, + "element", + locOf(item), + copy.dictionary, + ), ); break; } @@ -653,14 +679,23 @@ function enclosingScope(node: ts.Node): ts.Node | undefined { * instance flowing into a plain record type that does not keep class * identity (ClassProtocols marks the targets a class instance visibly * flows into; a value reaching another record type is a field copy). */ +/** The types a width copy converts between, and whether the target is a + * dictionary. */ +interface WidthCopy { + source: string; + target: string; + dictionary?: boolean; +} + function widthCopyBetween( lowerer: Lowerer, source: ts.Expression, targetNode: ts.TypeNode, -): { source: string; target: string } | null { +): WidthCopy | null { const checker = lowerer.checker; const s = checker.getTypeAtLocation(source); const t = checker.getTypeFromTypeNode(targetNode); + if (isDictionaryType(checker, t)) return dictionaryViewCopy(lowerer, source, s, t); if (!isPlainRecordType(checker, t)) return null; const classSource = isProgramClassType(checker, s); if (!classSource && !isPlainRecordType(checker, s)) return null; @@ -680,6 +715,73 @@ function widthCopyBetween( return { source: checker.typeToString(s), target: checker.typeToString(t) }; } +/** A copy made by converting a checked-dynamic view (`{}`, `object`, + * `unknown`) to a dictionary type that compiles to a native record (a + * dictionary of `unknown` values stays checked-dynamic and keeps the + * object). A dictionary of that type comes back out of the view as itself, + * so a copy is reported only when the view is a constant binding + * initialized with a class instance or a record: that object is converted + * field by field into a new dictionary. */ +function dictionaryViewCopy( + lowerer: Lowerer, + source: ts.Expression, + s: ts.Type, + t: ts.Type, +): WidthCopy | null { + const checker = lowerer.checker; + if (!isDynamicViewType(checker, s)) return null; + if (lowerer.mapTypeOf(t)?.kind !== "record") return null; + while (ts.isParenthesizedExpression(source) || ts.isNonNullExpression(source)) + source = source.expression; + if (!ts.isIdentifier(source)) return null; + const decl = checker.getSymbolAtLocation(source)?.valueDeclaration; + if ( + decl === undefined || + !ts.isVariableDeclaration(decl) || + decl.initializer === undefined || + !ts.isVariableDeclarationList(decl.parent) || + !(decl.parent.flags & ts.NodeFlags.Const) + ) + return null; + let origin = decl.initializer; + while ( + ts.isParenthesizedExpression(origin) || + ts.isNonNullExpression(origin) || + ts.isAsExpression(origin) || + ts.isTypeAssertion(origin) + ) + origin = origin.expression; + const o = checker.getTypeAtLocation(origin); + if (!isProgramClassType(checker, o) && !isPlainRecordType(checker, o)) return null; + return { source: checker.typeToString(o), target: checker.typeToString(t), dictionary: true }; +} + +/** `{}`, `object` and `unknown`: types whose values lower to checked + * dynamic storage that keeps the original object. */ +function isDynamicViewType(checker: ts.TypeChecker, type: ts.Type): boolean { + if (type.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive)) return true; + return ( + (type.flags & ts.TypeFlags.Object) !== 0 && + checker.getPropertiesOfType(type).length === 0 && + checker.getIndexInfosOfType(type).length === 0 && + checker.getCallSignatures(type).length === 0 && + checker.getConstructSignatures(type).length === 0 + ); +} + +/** A string-keyed dictionary with no declared properties + * (`Record`, `{ [key: string]: number }`). */ +function isDictionaryType(checker: ts.TypeChecker, type: ts.Type): boolean { + if ((type.flags & ts.TypeFlags.Object) === 0) return false; + if (checker.isArrayType(type) || checker.isTupleType(type)) return false; + if (checker.getCallSignatures(type).length > 0) return false; + if (checker.getConstructSignatures(type).length > 0) return false; + if (checker.getPropertiesOfType(type).length > 0) return false; + return checker + .getIndexInfosOfType(type) + .some((index) => (index.keyType.flags & ts.TypeFlags.String) !== 0); +} + function isProgramClassType(checker: ts.TypeChecker, type: ts.Type): boolean { if ((type.flags & ts.TypeFlags.Object) === 0) return false; const symbol = type.getSymbol(); diff --git a/packages/compiler/test/class-method-mutations.test.ts b/packages/compiler/test/class-method-mutations.test.ts index b508a3949..3221e44ee 100644 --- a/packages/compiler/test/class-method-mutations.test.ts +++ b/packages/compiler/test/class-method-mutations.test.ts @@ -135,3 +135,45 @@ test("a reflection call with a well-known symbol key keeps checked dispatch", as const callers = await callersOf(`${range}${write}`, "%Range.sym:iterator"); expect(callers.filter((name) => !name.startsWith("%method.value:"))).toEqual([]); }); + +const groups = ` +class Tally { + n = 0; + add(): void { this.n++; } +} +interface Settings { groups: VALUE } +function addGroup(settings: Settings, name: string): void { + settings.groups[name] = {}; +} +const settings: Settings = { groups: {} }; +const tally = new Tally(); +addGroup(settings, String(tally.n)); +tally.add(); +console.log(tally.n, Object.keys(settings.groups)); +`; + +test("a computed write into a dictionary of dictionaries leaves method calls direct", async () => { + const source = groups.replace("VALUE", "Record>"); + expect(await callersOf(source, "%dyn.class.properties")).toEqual([]); +}); + +test("a computed write into a dictionary that can hold functions keeps checked dispatch", async () => { + for (const value of [ + "Record | (() => void)>", + "Record>", + ]) { + const source = groups.replace("VALUE", value); + expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]); + } +}); + +test("a computed write into a generic dictionary of dictionaries keeps checked dispatch", async () => { + const source = groups + .replace( + "interface Settings { groups: VALUE }", + "interface Settings { groups: Record> }", + ) + .replace("function addGroup(settings: Settings,", "function addGroup(settings: Settings,") + .replace("const settings: Settings =", "const settings: Settings ="); + expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7010-dictionary-of-dictionaries-keep-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7010-dictionary-of-dictionaries-keep-methods.ts.json new file mode 100644 index 000000000..cc3c9dabe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7010-dictionary-of-dictionaries-keep-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7010-dictionary-of-dictionaries-keep-methods.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7010-dictionary-of-dictionaries-keep-methods.ts b/tests/corpus/7010-dictionary-of-dictionaries-keep-methods.ts new file mode 100644 index 000000000..a53e82f83 --- /dev/null +++ b/tests/corpus/7010-dictionary-of-dictionaries-keep-methods.ts @@ -0,0 +1,38 @@ +// A string-keyed dictionary whose values are dictionaries cannot hold a +// function, so storing into it under a key known only at runtime leaves +// class methods in place: their calls stay direct, and a record a method +// returns is the record the instance holds, so writes through it are kept. +interface Data { + items: string[]; +} +class Store { + data: Data = { items: [] }; + get(): Data { + return this.data; + } +} +class Counter { + count = 0; + bump(): number { + return ++this.count; + } +} +interface Settings { + groups: Record>; +} +function addGroup(settings: Settings, name: string): void { + settings.groups[name] = {}; +} + +const settings: Settings = { groups: {} }; +const name = process.argv.length > 99 ? "never" : "colours"; +addGroup(settings, name); +const group = settings.groups[name]; +if (group !== undefined) group["sky"] = "blue"; +const store = new Store(); +store.get().items.push("a"); +store.get().items.push("b"); +const counter = new Counter(); +let total = 0; +for (let i = 0; i < 1000; i++) total += counter.bump(); +console.log(JSON.stringify(settings.groups), store.data.items.join(","), total); diff --git a/tests/coverage-fixtures/dictionary-view-copies.ts b/tests/coverage-fixtures/dictionary-view-copies.ts new file mode 100644 index 000000000..bfcfa61f4 --- /dev/null +++ b/tests/coverage-fixtures/dictionary-view-copies.ts @@ -0,0 +1,29 @@ +// A class instance or record held as `{}`, `object` or `unknown` keeps the +// original object, and converting it to a dictionary type copies it field +// by field. Each site below then writes through or compares the copy. +class Counter { + label = "c"; + add(): number { + return this.label.length; + } +} +const key = process.argv.length > 5 ? "z" : "b"; + +const counter = new Counter(); +const view: {} = counter; +const table: Record = view; +table[key] = "y"; + +const point = { a: "x" }; +const loose: object = point; +const dict = loose as Record; +dict[key] = "y"; + +const held: unknown = { a: "x" }; +const fromUnknown = held as Record; +console.log(fromUnknown === held); + +const empty: {} = {}; +const filled: Record = empty; +filled[key] = "y"; +console.log(JSON.stringify(counter), JSON.stringify(point), JSON.stringify(empty)); diff --git a/tests/coverage-fixtures/no-divergences.ts b/tests/coverage-fixtures/no-divergences.ts index 3739336e3..ca76bcc32 100644 --- a/tests/coverage-fixtures/no-divergences.ts +++ b/tests/coverage-fixtures/no-divergences.ts @@ -1,6 +1,6 @@ // Ordinary code: identity-preserving calls, declaration-ordered literals, -// Latin-script comparisons, related-class casts, ISO dates and views of the -// global object report no divergence. +// Latin-script comparisons, related-class casts, ISO dates, views of the +// global object and a dictionary held as `{}` report no divergence. interface Item { name: string; qty: number; @@ -38,3 +38,10 @@ const COUNT = Symbol.for("no-divergences.count"); const globalState = globalThis as { [COUNT]?: { n: number } }; globalState[COUNT] = { n: 1 }; console.log(globalState[COUNT]?.n); + +// A dictionary held as `{}` comes back out as itself. +const counts: Record = { a: "x" }; +const opaque: {} = counts; +const again: Record = opaque; +again[counts["a"] ?? "a"] = "y"; +console.log(JSON.stringify(counts)); diff --git a/tests/harness/__snapshots__/coverage-dictionary-view-copies.txt b/tests/harness/__snapshots__/coverage-dictionary-view-copies.txt new file mode 100644 index 000000000..2359a4ffa --- /dev/null +++ b/tests/harness/__snapshots__/coverage-dictionary-view-copies.txt @@ -0,0 +1,21 @@ +scriptc coverage tests/coverage-fixtures/dictionary-view-copies.ts + + statements analyzed 18 + compile statically 18 (100%) + in dynamic tier 3 (operate on values typed only at runtime; --explain-dynamic lists them) + + fully static — all analyzed statements compile to native code. + + differs from Node 4 sites (compiles; the native program can behave differently) + ×1 SC6001 storing a 'Counter' in a 'Record<…>' binding copies the object (converting an object held as '{}', 'object' or 'unknown' to a dictionary type compiles as a field copy): the write to 'table[key]' upda… + at tests/coverage-fixtures/dictionary-view-copies.ts:14:39 + hint: keep the value at its own type ('Counter'), or declare the dictionary's values as 'unknown', which keeps the original object + ×1 SC6001 storing a '{}' in a 'Record<…>' binding copies the object (converting an object held as '{}', 'object' or 'unknown' to a dictionary type compiles as a field copy): the write to 'filled[key]' updates … + at tests/coverage-fixtures/dictionary-view-copies.ts:27:40 + hint: keep the value at its own type ('{}'), or declare the dictionary's values as 'unknown', which keeps the original object + ×1 SC6001 asserting a '{ a: string; }' as 'Record<…>' copies the object (converting an object held as '{}', 'object' or 'unknown' to a dictionary type compiles as a field copy): the write to 'dict[key]' update… + at tests/coverage-fixtures/dictionary-view-copies.ts:19:14 + hint: keep the value at its own type ('{ a: string; }'), or declare the dictionary's values as 'unknown', which keeps the original object + ×1 SC6001 asserting a '{ a: string; }' as 'Record<…>' copies the object (converting an object held as '{}', 'object' or 'unknown' to a dictionary type compiles as a field copy): 'fromUnknown === held' compares… + at tests/coverage-fixtures/dictionary-view-copies.ts:23:21 + hint: keep the value at its own type ('{ a: string; }'), or declare the dictionary's values as 'unknown', which keeps the original object \ No newline at end of file diff --git a/tests/harness/corpus-costs/7010-dictionary-of-dictionaries-keep-methods.ts.json b/tests/harness/corpus-costs/7010-dictionary-of-dictionaries-keep-methods.ts.json new file mode 100644 index 000000000..cc07613e7 --- /dev/null +++ b/tests/harness/corpus-costs/7010-dictionary-of-dictionaries-keep-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 2184, "san": 1198 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 86d202b46..f3958c5fe 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -107,6 +107,20 @@ test("record width copies report at every storing site", async () => { ); }); +test("dictionary conversions of checked-dynamic views report copies", async () => { + const { coverage } = analyze(fixture("dictionary-view-copies.ts")); + expect(coverage.diagnostics).toEqual([]); + expect((coverage.divergences ?? []).map((d) => d.code)).toEqual([ + "SC6001", + "SC6001", + "SC6001", + "SC6001", + ]); + await expect(report(fixture("dictionary-view-copies.ts"))).toMatchFileSnapshot( + "__snapshots__/coverage-dictionary-view-copies.txt", + ); +}); + test("ordinary code reports no divergence", () => { const { coverage } = analyze(fixture("no-divergences.ts")); expect(coverage.diagnostics).toEqual([]); From ba80dc254d2b6355bbdcef44ae1f76f577db63d1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:56:21 -0500 Subject: [PATCH 012/409] Iterate classes with object-literal iterators and close them on early exits --- .../builtin-apis/synchronous-iteration.mdx | 2 +- .../language-features/async-iteration.mdx | 1 + .../lowering/class-dynamic-dispatch.ts | 10 +- .../src/frontend/lowering/dispatch-sites.ts | 1 + .../lowering/expressions/array-literals.ts | 8 + .../src/frontend/lowering/lower-classes.ts | 126 +++++++++++++-- .../src/frontend/lowering/lower-stmts.ts | 146 +++++++++++++++--- .../src/frontend/lowering/lower-stream.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 10 +- .../6928-class-iterator-protocol.ts.json | 6 + .../6929-class-iterator-protocol.mjs.json | 6 + ...6935-class-async-iterator-protocol.ts.json | 6 + ...936-class-async-iterator-protocol.mjs.json | 6 + packages/runtime/src/scr_dyn_for_await.c | 8 +- tests/corpus/6928-class-iterator-protocol.ts | 93 +++++++++++ tests/corpus/6929-class-iterator-protocol.mjs | 78 ++++++++++ .../6935-class-async-iterator-protocol.ts | 60 +++++++ .../6936-class-async-iterator-protocol.mjs | 60 +++++++ .../6928-class-iterator-protocol.ts.json | 1 + .../6929-class-iterator-protocol.mjs.json | 1 + ...6935-class-async-iterator-protocol.ts.json | 1 + ...936-class-async-iterator-protocol.mjs.json | 1 + 22 files changed, 582 insertions(+), 51 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json create mode 100644 tests/corpus/6928-class-iterator-protocol.ts create mode 100644 tests/corpus/6929-class-iterator-protocol.mjs create mode 100644 tests/corpus/6935-class-async-iterator-protocol.ts create mode 100644 tests/corpus/6936-class-async-iterator-protocol.mjs create mode 100644 tests/harness/corpus-costs/6928-class-iterator-protocol.ts.json create mode 100644 tests/harness/corpus-costs/6929-class-iterator-protocol.mjs.json create mode 100644 tests/harness/corpus-costs/6935-class-async-iterator-protocol.ts.json create mode 100644 tests/harness/corpus-costs/6936-class-async-iterator-protocol.mjs.json diff --git a/docs/content/limitations/builtin-apis/synchronous-iteration.mdx b/docs/content/limitations/builtin-apis/synchronous-iteration.mdx index 564f9af45..38cc9c475 100644 --- a/docs/content/limitations/builtin-apis/synchronous-iteration.mdx +++ b/docs/content/limitations/builtin-apis/synchronous-iteration.mdx @@ -1,4 +1,4 @@ - **Synchronous iteration:** Checked `for...of` follows live iterator methods, including compiled class methods and abrupt-completion cleanup. Mapper-less checked `Array.from` accepts those iterators and plain array-like objects. Sparse checked arrays and general borrowed iterator factories are unsupported. - Direct statically typed class loops also restrict iterator `return` and `throw` methods. + Direct statically typed class loops accept iterators that are class instances or object literals; a loop that exits early calls the iterator's zero-parameter `return()`, as Node's IteratorClose does. diff --git a/docs/content/limitations/language-features/async-iteration.mdx b/docs/content/limitations/language-features/async-iteration.mdx index 863c9be90..b15dca7fc 100644 --- a/docs/content/limitations/language-features/async-iteration.mdx +++ b/docs/content/limitations/language-features/async-iteration.mdx @@ -1,6 +1,7 @@ - **Async iteration:** Async `yield*` delegation, stored Web or Node iterator handles, and general structural async-iterator objects are unsupported. Typed async generators support lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. `for await` also accepts Node `Readable` streams, Web `ReadableStream` values, and compiled class iterators whose zero-parameter `next()` returns a promise of a `{ value, done? }` record. + A class instance whose `[Symbol.asyncIterator]()` returns an object literal, or that has only `[Symbol.iterator]()`, iterates through the checked-dynamic async protocol, and an early exit awaits the iterator's `return()`. Abrupt completion runs cleanup, including literal `destroyOnReturn` and `preventCancel` options. The `node:timers/promises` `setInterval(delay, value)` iterator compiles without AbortSignal options. `Array.fromAsync` accepts arrays of values or of promises and typed async generators, with an optional mapping function; a `thisArg` and arrays that mix values and promises are unsupported. diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index af3212ef0..86ad05386 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -2367,15 +2367,19 @@ export class ClassDynamicDispatch { } const iterator = expr.kind === "libCall" && - (expr.fn === "dyn.iterator" || expr.fn === "dyn.arrayFromIterator") + (expr.fn === "dyn.iterator" || + expr.fn === "dyn.arrayFromIterator" || + expr.fn === "dyn.asyncIteratorOpen") ? expr : null; + // `for await` opens a class instance's [Symbol.asyncIterator](). + const asyncOpen = iterator?.fn === "dyn.asyncIteratorOpen"; const invoke: Invoke | null = iterator ? { kind: "dynInvoke", recv: iterator.args[0]!, - method: "sym:iterator", - calleeName: "value[Symbol.iterator]", + method: asyncOpen ? "sym:asyncIterator" : "sym:iterator", + calleeName: asyncOpen ? "value[Symbol.asyncIterator]" : "value[Symbol.iterator]", args: [], type: DYN, loc: iterator.loc, diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index 2618dacf9..49d0bd243 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -59,6 +59,7 @@ const DISPATCH_LIB_CALLS = new Set([ "dyn.keySetComputed", "dyn.iterator", "dyn.arrayFromIterator", + "dyn.asyncIteratorOpen", "dyn.toString", "dyn.stringConstructor", "dyn.forInKeys", diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 8f119504c..6153b87f2 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -425,6 +425,14 @@ export function lowerArrayLiteral( if (src.type.kind === "object") { const drained = lowerer.classIteratorDrainCall(src, locOf(el), type.elem); if (drained) src = drained; + // In JavaScript an iterable class whose inferred iterator shape has + // no static protocol spreads through the runtime's iterator walk. + else if ( + isJsSourceFile(ts.getSourceFileOfNode(el)) && + lowerer.classes.get(src.type.className) && + lowerer.findMethodOn(lowerer.classes.get(src.type.className)!, "sym:iterator") + ) + src = lowerer.coerceInto(el.expression, src, DYN); } // `[...s]` on a STRING spreads its code-point characters (the // string iterator's walk — astral chars whole) through the same diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..9d994b76b 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -7923,31 +7923,83 @@ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): stri export interface ClassIteratorInfo { /** The `[Symbol.iterator]()` call's receiver class + its declarer. */ className: string; - /** The iterator object's type (the method's return). */ - iterT: IrType & { kind: "object" }; + /** The iterator's type (the method's return): a class instance, or a + * record (an object literal) whose `next` field is a function. */ + iterT: IrType & ({ kind: "object" } | { kind: "record" }); /** next()'s result record type. */ resultT: IrType & { kind: "record" }; valueT: IrType; /** False: no done field — the protocol never terminates. */ hasDone: boolean; + /** The iterator declares a zero-parameter return(), which a consumer + * that stops before the end calls (IteratorClose). Only consumers that + * ask for `close` see such iterators. */ + hasReturn: boolean; } -export function classIteratorOf(lowerer: Lowerer, t: IrType): ClassIteratorInfo | null { +/** A return() result type that is always an object: IteratorClose after a + * break throws a TypeError for any other result, which these desugars do + * not model. */ +function objectResult(t: IrType): boolean { + return t.kind === "record" || t.kind === "object"; +} + +/** What a class iterable's consumer handles: `records` accepts an + * object-literal iterator; `close` accepts an iterator with return(), + * which the consumer calls when it stops early (or never stops early). */ +export interface ClassIteratorOptions { + records?: boolean; + close?: boolean; +} +export function classIteratorOf( + lowerer: Lowerer, + t: IrType, + options: ClassIteratorOptions = {}, +): ClassIteratorInfo | null { if (t.kind !== "object") return null; const info = lowerer.classes.get(t.className); if (!info) return null; const iter = findMethodOn(lowerer, info, "sym:iterator"); if (!iter || iter.sig.params.length !== 0 || iter.sig.abstract === true) return null; const iterT = iter.sig.ret; - if (iterT.kind !== "object") return null; - const itInfo = lowerer.classes.get(iterT.className); - if (!itInfo) return null; - // IteratorClose honesty: a declared return()/throw() would be called - // by JS on abrupt completion; these desugars never close. - if (findMethodOn(lowerer, itInfo, "return") || findMethodOn(lowerer, itInfo, "throw")) - return null; - const next = findMethodOn(lowerer, itInfo, "next"); - if (!next || next.sig.params.length !== 0 || next.sig.abstract === true) return null; - const resultT = next.sig.ret; + let resultT: IrType; + let hasReturn: boolean; + if (iterT.kind === "object") { + const itInfo = lowerer.classes.get(iterT.className); + if (!itInfo) return null; + // IteratorClose honesty: a declared return() would be called by JS on + // abrupt completion, so only a consumer that closes takes it; for-of + // never calls throw(), the other consumers refuse it. + const close = findMethodOn(lowerer, itInfo, "return"); + if ( + close && + (!options.close || + close.sig.abstract === true || + close.sig.params.length !== 0 || + !objectResult(close.sig.ret)) + ) + return null; + if (findMethodOn(lowerer, itInfo, "throw") && !options.close) return null; + const next = findMethodOn(lowerer, itInfo, "next"); + if (!next || next.sig.params.length !== 0 || next.sig.abstract === true) return null; + resultT = next.sig.ret; + hasReturn = !!close; + } else if (iterT.kind === "record" && options.records) { + const fields = lowerer.shapes.get(iterT.shapeId)?.fields; + const next = fields?.find((f) => f.name === "next")?.type; + if (!next || next.kind !== "func" || next.params.length !== 0 || next.rest) return null; + const close = fields?.find((f) => f.name === "return")?.type; + if ( + close && + (!options.close || + close.kind !== "func" || + close.params.length !== 0 || + close.rest || + !objectResult(close.ret)) + ) + return null; + resultT = next.ret; + hasReturn = close !== undefined; + } else return null; if (resultT.kind !== "record") return null; const shape = lowerer.shapes.get(resultT.shapeId); const value = shape?.fields.find((f) => f.name === "value"); @@ -7960,6 +8012,7 @@ export function classIteratorOf(lowerer: Lowerer, t: IrType): ClassIteratorInfo resultT, valueT: value.type, hasDone: done !== undefined, + hasReturn, }; } @@ -7971,7 +8024,43 @@ export function classIteratorNextCall( itRef: IrExpr, loc: SrcLoc, ): IrExpr { - return accessorCall(lowerer, cit.iterT.className, "next", itRef, [], cit.resultT, loc); + return classIteratorMethodCall(lowerer, cit, "next", itRef, loc); +} + +/** `it.next()` or `it.return()` of a class iterator: a (possibly virtual) + * method call of an iterator class, or the call of an object-literal + * iterator's function field. */ +export function classIteratorMethodCall( + lowerer: Lowerer, + cit: ClassIteratorInfo, + method: "next" | "return", + itRef: IrExpr, + loc: SrcLoc, +): IrExpr { + const iterT = cit.iterT; + if (iterT.kind === "object") { + const ret = + method === "next" + ? cit.resultT + : findMethodOn(lowerer, lowerer.classes.get(iterT.className)!, "return")!.sig.ret; + return accessorCall(lowerer, iterT.className, method, itRef, [], ret, loc); + } + const field = lowerer.shapes.get(iterT.shapeId)!.fields.find((f) => f.name === method)!; + const fnT = field.type as IrType & { kind: "func" }; + return { + kind: "callValue", + callee: { + kind: "recordGet", + obj: itRef, + shapeId: iterT.shapeId, + field: method, + type: fnT, + loc, + }, + args: [], + type: fnT.ret, + loc, + }; } /** `recv[Symbol.iterator]()` as an ordinary method call. */ @@ -7999,12 +8088,15 @@ export function classIteratorDrainCall( loc: SrcLoc, elemT?: IrType, ): IrExpr | null { - const cit = classIteratorOf(lowerer, src.type); + // A spread takes every value, so it never closes the iterator. + const cit = classIteratorOf(lowerer, src.type, { records: true, close: true }); if (!cit) return null; const outElem = elemT ?? cit.valueT; // Probe the element coercion purely: identical types, or an arm of a // union destination (the wrap coerceToExpected applies below). - if (!typeEquals(outElem, cit.valueT)) { + // A checked-dynamic value (`IteratorResult`'s value is `T | any`) + // converts to the destination element with a check. + if (!typeEquals(outElem, cit.valueT) && cit.valueT.kind !== "dyn") { if (outElem.kind !== "union" || lowerer.armTag(outElem.unionId, cit.valueT) < 0) return null; } const outT = arrayOf(outElem); @@ -8122,7 +8214,7 @@ export function classIteratorRestDrainCall( loc: SrcLoc, ): IrExpr { const outT = arrayOf(cit.valueT); - const key = `it:${cit.iterT.className}`; + const key = `it:${cit.iterT.kind === "object" ? cit.iterT.className : typeKey(cit.iterT)}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { name = keyedHelperName(lowerer, "%iter.drain", key); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 213abfec6..bee887e0f 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -9467,8 +9467,23 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { // iterator are awaited as the async-from-sync iterator awaits them, and // a value that is not iterable throws Node's TypeError. const headT = lowerer.mapTypeOf(lowerer.typeOf(stmt.expression)); + // A class instance whose [Symbol.asyncIterator]() (or + // [Symbol.iterator]()) returns an iterator object takes the same + // checked-dynamic protocol over the boxed instance. + const headClass = headT?.kind === "object" ? lowerer.classes.get(headT.className) : undefined; + const classAsync = + headClass !== undefined && lowerer.findMethodOn(headClass, "sym:asyncIterator") !== null; + // Without [Symbol.asyncIterator], the loop opens [Symbol.iterator]() and + // awaits its values, as the async-from-sync iterator does. + const classSync = + headClass !== undefined && + !classAsync && + lowerer.findMethodOn(headClass, "sym:iterator") !== null; + const classIterable = classAsync || classSync; const syncGenerator = - (headT?.kind === "generator" && headT.async !== true) || isJsSourceFile(stmt.getSourceFile()); + (headT?.kind === "generator" && headT.async !== true) || + classIterable || + isJsSourceFile(stmt.getSourceFile()); if (lowerer.ctx.isAsync && (syncGenerator || isUntypedIterable(lowerer, stmt.expression))) { const iterable = lowerer.lowerExpr(stmt.expression); if (iterable.type.kind === "generator" && iterable.type.async) @@ -9480,10 +9495,19 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ); if (iterable.type.kind === "dyn") return lowerForOfDyn(lowerer, stmt, iterable, labels, true); if ( - (iterable.type.kind === "generator" || isJsSourceFile(stmt.getSourceFile())) && + (iterable.type.kind === "generator" || + classIterable || + isJsSourceFile(stmt.getSourceFile())) && lowerer.dynConvertible(iterable.type) ) - return lowerForOfDyn(lowerer, stmt, lowerer.coerceToExpected(iterable, DYN), labels, true); + return lowerForOfDyn( + lowerer, + stmt, + lowerer.coerceToExpected(iterable, DYN), + labels, + true, + classSync && iterable.type.kind === "object", + ); } // A NodeJS.ReadableStream iterates as the general Readable. requestGeneralStreamSurface(lowerer, stmt.expression); @@ -9732,17 +9756,30 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ); // Class iterables ([Symbol.iterator]() + next() — classIteratorOf) // drive the same protocol with ordinary method calls. - const cit = lowerer.classIteratorOf(iterable.type); + const cit = lowerer.classIteratorOf(iterable.type, { records: true, close: true }); if (cit) return lowerForOfClassIterator(lowerer, stmt, iterable, cit, labels); // A declared [Symbol.iterator] whose shape classIteratorOf refused // (an `any`-typed iterator or result, a declared return()/throw()): // name the protocol, not the class. const info = lowerer.classes.get(iterable.type.className); + // In JavaScript the inferred shape is often a union of result + // objects: the loop runs the iterator protocol over the boxed value. + if ( + info && + isJsSourceFile(ts.getSourceFileOfNode(stmt)) && + lowerer.findMethodOn(info, "sym:iterator") + ) + return lowerForOfDyn( + lowerer, + stmt, + lowerer.coerceInto(stmt.expression, iterable, DYN), + labels, + ); if (info && lowerer.findMethodOn(info, "sym:iterator")) { lowerer.unsupported( "SC1090", stmt.expression, - `for-of over this [Symbol.iterator] shape (the method must take no parameters and return an object whose zero-parameter next() returns a { value, done? } record with a boolean done; iterator classes declaring return()/throw() stay out — IteratorClose has no lowering)`, + `for-of over this [Symbol.iterator] shape (the method must take no parameters and return an object whose zero-parameter next() returns a { value, done? } record with a boolean done, and whose return(), if any, takes no parameters and returns an object)`, ); } } @@ -10641,11 +10678,13 @@ function lowerForOfMatchAll( * } } * * A result record with NO done field never terminates — exactly JS - * (undefined is falsy forever; Node loops forever too). No - * IteratorClose: classIteratorOf already refused iterator classes - * declaring return()/throw(), so a break exits with nothing to close. - * `done: true` results bind nothing (the loop breaks before the value - * read), matching JS's ignore-the-final-value rule. */ + * (undefined is falsy forever; Node loops forever too). An iterator with + * return() is closed when the loop leaves after a value arrived (break, + * return, or a throw from the body), as IteratorClose does: a done flag + * set around each step guards the call in a finally, and after a throw + * from the body an error from return() is dropped. A throw from next() + * does not close. `done: true` results bind nothing (the loop breaks + * before the value read), matching JS's ignore-the-final-value rule. */ function lowerForOfClassIterator( lowerer: Lowerer, stmt: ts.ForOfStatement, @@ -10688,6 +10727,19 @@ function lowerForOfClassIterator( const recvRef: IrExpr = { kind: "varRef", localId: recv.id, type: iterable.type, loc }; const itRef = (): IrExpr => ({ kind: "varRef", localId: it.id, type: cit.iterT, loc }); const rRef = (): IrExpr => ({ kind: "varRef", localId: r.id, type: cit.resultT, loc }); + const done = cit.hasReturn ? lowerer.declareHiddenLocal("%cid", BOOL) : null; + if (done) done.mutable = true; + const setDone = (value: boolean): IrStmt[] => + done + ? [ + { + kind: "assign", + localId: done.id, + value: { kind: "boolLit", value, type: BOOL, loc }, + loc, + }, + ] + : []; const varTarget = exprTarget ?? (decl ? forOfVarTarget(lowerer, decl) : null); const x = varTarget ? lowerer.declareHiddenLocal("%vof", cit.valueT) @@ -10700,6 +10752,7 @@ function lowerForOfClassIterator( const xRef: IrExpr = { kind: "varRef", localId: x.id, type: cit.valueT, loc }; const blame: ts.Node = decl?.name ?? exprTargetNode ?? stmt.initializer; const head: IrStmt[] = [ + ...setDone(true), { kind: "varDecl", localId: r.id, @@ -10747,8 +10800,16 @@ function lowerForOfClassIterator( } satisfies IrStmt, ] : []), + ...setDone(false), ]; const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels); + const loop: IrStmt = { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [...head, ...body], + ...(labels && { labels }), + loc, + }; return { kind: "block", body: [ @@ -10759,13 +10820,45 @@ function lowerForOfClassIterator( init: lowerer.classIteratorOpenCall(cit, recvRef, loc), loc, }, - { - kind: "while", - cond: { kind: "boolLit", value: true, type: BOOL, loc }, - body: [...head, ...body], - ...(labels && { labels }), - loc, - }, + ...(done + ? [ + { + kind: "varDecl", + localId: done.id, + init: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + } satisfies IrStmt, + { + kind: "tryCatch", + tryBody: [loop], + catchBody: null, + catchLocalId: null, + dropFinallyErrors: true, + finallyBody: [ + { + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { kind: "varRef", localId: done.id, type: BOOL, loc }, + type: BOOL, + loc, + }, + then: [ + { + kind: "exprStmt", + expr: lowerer.classIteratorReturnCall(cit, itRef(), loc), + loc, + }, + ], + else_: null, + loc, + }, + ], + loc, + } satisfies IrStmt, + ] + : [loop]), ], loc, }; @@ -10783,6 +10876,7 @@ function lowerForOfDyn( iterable: IrExpr, labels?: string[], asyncLoop = false, + syncOpen = false, ): IrStmt { const loc = locOf(stmt); const spell = notIterableSpelling(stmt.expression, asyncLoop ? "async iterable" : "iterable"); @@ -11010,7 +11104,7 @@ function lowerForOfDyn( localId: iterator.id, init: check({ kind: "libCall", - fn: asyncLoop ? "dyn.asyncIteratorOpen" : "dyn.iterator", + fn: asyncLoop && !syncOpen ? "dyn.asyncIteratorOpen" : "dyn.iterator", args: [iterable, { kind: "strLit", value: spell, type: STRING, loc }], type: DYN, loc, @@ -11022,13 +11116,15 @@ function lowerForOfDyn( { kind: "varDecl", localId: syncIterator.id, - init: { - kind: "libCall", - fn: "dyn.asyncIteratorWasSync", - args: [], - type: BOOL, - loc, - }, + init: syncOpen + ? { kind: "boolLit", value: true, type: BOOL, loc } + : { + kind: "libCall", + fn: "dyn.asyncIteratorWasSync", + args: [], + type: BOOL, + loc, + }, loc, } satisfies IrStmt, ] diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 6540b3a99..90bdf917e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1954,7 +1954,7 @@ function classIteratorObject( iter: IrExpr, loc: SrcLoc, ): IrExpr { - const key = `streamFromIter:${cit.iterT.className}`; + const key = `streamFromIter:${cit.iterT.kind === "object" ? cit.iterT.className : typeKey(cit.iterT)}`; let name = lowerer.valueHelpers.get(key); if (!name) { name = keyedHelperName(lowerer, "%stream.fromIter", key); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 747e396af..ea503e1f1 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -348,6 +348,7 @@ import { type ClassInfo, type ClassMethodSignature, type ClassIteratorInfo, + type ClassIteratorOptions, type GenericClassInfo, registerBuiltinErrorClasses, defineClassValueConstruct, @@ -359,6 +360,7 @@ import { builtinStreamInfoOf, analyzeClassDecoration, classIteratorDrainCall, + classIteratorMethodCall, classIteratorNextCall, classIteratorOf, classIteratorOpenCall, @@ -13049,8 +13051,12 @@ export class Lowerer { return accessorCall(this, className, member, obj, extraArgs, ret, loc); } - classIteratorOf(t: IrType): ClassIteratorInfo | null { - return classIteratorOf(this, t); + classIteratorOf(t: IrType, options?: ClassIteratorOptions): ClassIteratorInfo | null { + return classIteratorOf(this, t, options); + } + + classIteratorReturnCall(cit: ClassIteratorInfo, it: IrExpr, loc: SrcLoc): IrExpr { + return classIteratorMethodCall(this, cit, "return", it, loc); } classIteratorOpenCall(cit: ClassIteratorInfo, recv: IrExpr, loc: SrcLoc): IrExpr { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json new file mode 100644 index 000000000..2efd8521a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6928-class-iterator-protocol.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6928-class-iterator-protocol.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json new file mode 100644 index 000000000..49e5e4304 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6929-class-iterator-protocol.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6929-class-iterator-protocol.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json new file mode 100644 index 000000000..93775b96b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-class-async-iterator-protocol.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6935-class-async-iterator-protocol.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json new file mode 100644 index 000000000..378f7d18b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6936-class-async-iterator-protocol.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6936-class-async-iterator-protocol.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn_for_await.c b/packages/runtime/src/scr_dyn_for_await.c index 25183c4ee..2d39885c0 100644 --- a/packages/runtime/src/scr_dyn_for_await.c +++ b/packages/runtime/src/scr_dyn_for_await.c @@ -39,7 +39,11 @@ ScrDyn *scr_dyn_async_iterator_open(ScrDyn *value, ScrStr *spell) { } /* Whether the iterator the last scr_dyn_async_iterator_open returned is a - * synchronous one. Read immediately after the open. */ + * synchronous one. Read immediately after the open; the read clears it, so + * an iterator a class's [Symbol.asyncIterator]() returned (opened without + * the runtime) reads false. */ bool scr_dyn_async_iterator_was_sync(void) { - return scr_for_await_sync; + bool sync = scr_for_await_sync; + scr_for_await_sync = false; + return sync; } diff --git a/tests/corpus/6928-class-iterator-protocol.ts b/tests/corpus/6928-class-iterator-protocol.ts new file mode 100644 index 000000000..f097368de --- /dev/null +++ b/tests/corpus/6928-class-iterator-protocol.ts @@ -0,0 +1,93 @@ +// for-of and spread over classes whose [Symbol.iterator]() returns an iterator object, +// with return() called on early exits as Node's IteratorClose does. +class Bag { + readonly items: string[]; + constructor(items: string[]) { this.items = items; } + [Symbol.iterator]() { + let i = 0; + const items = this.items; + return { + next(): IteratorResult { + return i < items.length ? { value: items[i++]!, done: false } : { value: undefined, done: true }; + }, + return(): IteratorResult { + console.log("return at", i); + return { value: undefined, done: true }; + }, + }; + } +} +const bag = new Bag(["a", "b", "c", "d"]); +function firstLong(): string | undefined { + for (const s of bag) if (s >= "b") return s; + return undefined; +} +console.log(firstLong()); +try { + for (const s of bag) if (s === "c") throw new Error("stop at " + s); +} catch (e) { + console.log((e as Error).message); +} +outer: for (const n of [1, 2]) { + for (const s of bag) { + if (s === "b") continue outer; + console.log(n, s); + } +} +let all = ""; +for (const s of bag) all += s; +console.log(all); + +class Counter { + n = 0; + [Symbol.iterator](): CounterIterator { return new CounterIterator(3); } +} +class CounterIterator { + i = 0; + readonly limit: number; + constructor(limit: number) { this.limit = limit; } + next(): { value: number; done: boolean } { + return this.i < this.limit ? { value: this.i++, done: false } : { value: -1, done: true }; + } + return(): { value: number; done: boolean } { + console.log("class return", this.i); + return { value: -1, done: true }; + } +} +for (const v of new Counter()) { if (v === 1) break; console.log("v", v); } +console.log([...new Counter()].join()); + +class Fussy { + [Symbol.iterator]() { + let i = 0; + return { + next(): IteratorResult { return { value: i++, done: i > 5 }; }, + return(): IteratorResult { throw new Error("return failed"); }, + }; + } +} +try { + for (const v of new Fussy()) if (v === 1) break; +} catch (e) { + console.log("break:", (e as Error).message); +} +try { + for (const v of new Fussy()) if (v === 1) throw new Error("body failed"); +} catch (e) { + console.log("throw:", (e as Error).message); +} + +class Range { + readonly n: number; + constructor(n: number) { this.n = n; } + [Symbol.iterator]() { + let i = 0; + const n = this.n; + return { + next(): IteratorResult { return i < n ? { value: i++, done: false } : { value: undefined, done: true }; }, + }; + } +} +let total = 0; +for (const v of new Range(4)) total += v; +console.log(total, [...new Range(3)].join()); diff --git a/tests/corpus/6929-class-iterator-protocol.mjs b/tests/corpus/6929-class-iterator-protocol.mjs new file mode 100644 index 000000000..b5255ba35 --- /dev/null +++ b/tests/corpus/6929-class-iterator-protocol.mjs @@ -0,0 +1,78 @@ +// for-of and spread in JavaScript over classes whose [Symbol.iterator]() returns an +// iterator object, with return() called on early exits as Node's IteratorClose does. +class Bag { + + constructor(items) { this.items = items; } + [Symbol.iterator]() { + let i = 0; + const items = this.items; + return { + next() { + return i < items.length ? { value: items[i++], done: false } : { value: undefined, done: true }; + }, + return() { + console.log("return at", i); + return { value: undefined, done: true }; + }, + }; + } +} +const bag = new Bag(["a", "b", "c", "d"]); +function firstLong() { + for (const s of bag) if (s >= "b") return s; + return undefined; +} +console.log(firstLong()); +try { + for (const s of bag) if (s === "c") throw new Error("stop at " + s); +} catch (e) { + console.log(e.message); +} +outer: for (const n of [1, 2]) { + for (const s of bag) { + if (s === "b") continue outer; + console.log(n, s); + } +} +let all = ""; +for (const s of bag) all += s; +console.log(all); + +class Counter { + n = 0; + [Symbol.iterator]() { return new CounterIterator(3); } +} +class CounterIterator { + i = 0; + + constructor(limit) { this.limit = limit; } + next() { + return this.i < this.limit ? { value: this.i++, done: false } : { value: -1, done: true }; + } + return() { + console.log("class return", this.i); + return { value: -1, done: true }; + } +} +for (const v of new Counter()) { if (v === 1) break; console.log("v", v); } +console.log([...new Counter()].join()); + +class Fussy { + [Symbol.iterator]() { + let i = 0; + return { + next() { return { value: i++, done: i > 5 }; }, + return() { if (i >= 0) throw new Error("return failed"); return { value: undefined, done: true }; }, + }; + } +} +try { + for (const v of new Fussy()) if (v === 1) break; +} catch (e) { + console.log("break:", e.message); +} +try { + for (const v of new Fussy()) if (v === 1) throw new Error("body failed"); +} catch (e) { + console.log("throw:", e.message); +} diff --git a/tests/corpus/6935-class-async-iterator-protocol.ts b/tests/corpus/6935-class-async-iterator-protocol.ts new file mode 100644 index 000000000..9ec5a61a5 --- /dev/null +++ b/tests/corpus/6935-class-async-iterator-protocol.ts @@ -0,0 +1,60 @@ +// for await over classes whose [Symbol.asyncIterator]() or [Symbol.iterator]() +// returns an iterator object: early exits await return(), as in Node. +class Ticks { + readonly n: number; + constructor(n: number) { this.n = n; } + [Symbol.asyncIterator]() { + let i = 0; + const n = this.n; + return { + async next(): Promise> { + return i < n ? { value: i++, done: false } : { value: undefined, done: true }; + }, + async return(): Promise> { + console.log("closed at", i); + return { value: undefined, done: true }; + }, + }; + } +} +class Pages { + [Symbol.asyncIterator](): PageCursor { return new PageCursor(); } +} +class PageCursor { + page = 0; + async next(): Promise<{ value: string; done: boolean }> { + this.page++; + return this.page <= 3 ? { value: "page " + this.page, done: false } : { value: "", done: true }; + } +} +class Letters { + [Symbol.iterator]() { + const letters = ["x", "y", "z"]; + let i = 0; + return { + next(): IteratorResult { return i < letters.length ? { value: letters[i++]!, done: false } : { value: undefined, done: true }; }, + }; + } +} +async function main(): Promise { + let sum = 0; + for await (const v of new Ticks(4)) sum += v * 2; + console.log("sum", sum); + for await (const v of new Ticks(5)) { + if (v === 2) break; + console.log("early", v); + } + try { + for await (const v of new Ticks(5)) if (v === 1) throw new Error("body " + v); + } catch (e) { + console.log("caught", (e as Error).message); + } + async function find(): Promise { + for await (const v of new Ticks(9)) if (v > 2) return v; + return -1; + } + console.log("found", await find()); + for await (const p of new Pages()) console.log(p); + for await (const l of new Letters()) console.log("letter", l); +} +main(); diff --git a/tests/corpus/6936-class-async-iterator-protocol.mjs b/tests/corpus/6936-class-async-iterator-protocol.mjs new file mode 100644 index 000000000..cb7b1e68a --- /dev/null +++ b/tests/corpus/6936-class-async-iterator-protocol.mjs @@ -0,0 +1,60 @@ +// for await in JavaScript over classes whose [Symbol.asyncIterator]() or [Symbol.iterator]() +// returns an iterator object: early exits await return(), as in Node. +class Ticks { + + constructor(n) { this.n = n; } + [Symbol.asyncIterator]() { + let i = 0; + const n = this.n; + return { + async next() { + return i < n ? { value: i++, done: false } : { value: undefined, done: true }; + }, + async return() { + console.log("closed at", i); + return { value: undefined, done: true }; + }, + }; + } +} +class Pages { + [Symbol.asyncIterator]() { return new PageCursor(); } +} +class PageCursor { + page = 0; + async next() { + this.page++; + return this.page <= 3 ? { value: "page " + this.page, done: false } : { value: "", done: true }; + } +} +class Letters { + [Symbol.iterator]() { + const letters = ["x", "y", "z"]; + let i = 0; + return { + next() { return i < letters.length ? { value: letters[i++], done: false } : { value: undefined, done: true }; }, + }; + } +} +async function main() { + let sum = 0; + for await (const v of new Ticks(4)) sum += v * 2; + console.log("sum", sum); + for await (const v of new Ticks(5)) { + if (v === 2) break; + console.log("early", v); + } + try { + for await (const v of new Ticks(5)) if (v === 1) throw new Error("body " + v); + } catch (e) { + console.log("caught", e.message); + } + async function find() { + for await (const v of new Ticks(9)) if (v > 2) return v; + return -1; + } + console.log("found", await find()); + for await (const p of new Pages()) console.log(p); + for await (const l of new Letters()) console.log("letter", l); +} +main(); diff --git a/tests/harness/corpus-costs/6928-class-iterator-protocol.ts.json b/tests/harness/corpus-costs/6928-class-iterator-protocol.ts.json new file mode 100644 index 000000000..577adfbd9 --- /dev/null +++ b/tests/harness/corpus-costs/6928-class-iterator-protocol.ts.json @@ -0,0 +1 @@ +{ "plain": 3913, "san": 3785 } diff --git a/tests/harness/corpus-costs/6929-class-iterator-protocol.mjs.json b/tests/harness/corpus-costs/6929-class-iterator-protocol.mjs.json new file mode 100644 index 000000000..a653f7709 --- /dev/null +++ b/tests/harness/corpus-costs/6929-class-iterator-protocol.mjs.json @@ -0,0 +1 @@ +{ "plain": 3934, "san": 3488 } diff --git a/tests/harness/corpus-costs/6935-class-async-iterator-protocol.ts.json b/tests/harness/corpus-costs/6935-class-async-iterator-protocol.ts.json new file mode 100644 index 000000000..facd86f6d --- /dev/null +++ b/tests/harness/corpus-costs/6935-class-async-iterator-protocol.ts.json @@ -0,0 +1 @@ +{ "plain": 3179, "san": 2683 } diff --git a/tests/harness/corpus-costs/6936-class-async-iterator-protocol.mjs.json b/tests/harness/corpus-costs/6936-class-async-iterator-protocol.mjs.json new file mode 100644 index 000000000..cbe2cb7d2 --- /dev/null +++ b/tests/harness/corpus-costs/6936-class-async-iterator-protocol.mjs.json @@ -0,0 +1 @@ +{ "plain": 3547, "san": 2898 } From 14098794d08c09b7bbaf75566de7b0073a39b7df Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 15:46:43 -0500 Subject: [PATCH 013/409] Type the scenario promises of corpus 7119 and 7380 with JSDoc tsgo rejects a call of `resolve()` without an argument when the `new Promise()` that produced it has no type, and asks for a JSDoc hint; tsc-ts accepts it. The two programs compiled under tsc-ts only, so the checker-identity check failed on them. Each scenario function now declares `@returns {Promise}`, as corpus 1594 does. Their output is unchanged. --- tests/corpus/7119-als-net-event-context.mjs | 3 +++ tests/corpus/7380-als-http-event-context.mjs | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/tests/corpus/7119-als-net-event-context.mjs b/tests/corpus/7119-als-net-event-context.mjs index 3fb95ffa8..d17d1c31c 100644 --- a/tests/corpus/7119-als-net-event-context.mjs +++ b/tests/corpus/7119-als-net-event-context.mjs @@ -15,6 +15,7 @@ const store = new AsyncLocalStorage(); const lines = []; const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); +/** @returns {Promise} */ function echoScenario() { return new Promise((resolve) => { const server = store.run("create", () => @@ -61,6 +62,7 @@ function echoScenario() { }); } +/** @returns {Promise} */ function destroyScenario() { return new Promise((resolve) => { const server = net.createServer((sock) => sock.on("data", () => {})); @@ -78,6 +80,7 @@ function destroyScenario() { }); } +/** @returns {Promise} */ function closeScenarios() { return new Promise((resolve) => { let pending = 3; diff --git a/tests/corpus/7380-als-http-event-context.mjs b/tests/corpus/7380-als-http-event-context.mjs index a881cbe7e..fd6ef6225 100644 --- a/tests/corpus/7380-als-http-event-context.mjs +++ b/tests/corpus/7380-als-http-event-context.mjs @@ -17,6 +17,7 @@ const store = new AsyncLocalStorage(); const lines = []; const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); +/** @returns {Promise} */ function httpScenario() { return new Promise((resolve) => { const server = store.run("create", () => @@ -56,6 +57,7 @@ function httpScenario() { }); } +/** @returns {Promise} */ function keepAliveScenario() { return new Promise((resolve) => { const agent = new http.Agent({ keepAlive: true, maxSockets: 1 }); @@ -88,6 +90,7 @@ const key = fs.readFileSync("tests/fixtures/server/certs/localhost-key.pem"); const cert = fs.readFileSync("tests/fixtures/server/certs/localhost.pem"); const ca = fs.readFileSync("tests/fixtures/server/certs/ca.pem", "utf8"); +/** @returns {Promise} */ function httpsScenario() { return new Promise((resolve) => { const server = https.createServer({ key, cert }, (req, res) => { @@ -116,6 +119,7 @@ function httpsScenario() { }); } +/** @returns {Promise} */ function tlsScenario() { return new Promise((resolve) => { const server = tls.createServer({ key, cert }, (sock) => { From 3bbb88c43c8c5e0957047c6035c0a9ff42c66aba Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 16:51:25 -0500 Subject: [PATCH 014/409] Construct through arrays of class values that mix a class and its subclasses `const kinds = [Dog, Cat, Animal]; new kinds[0]()` now works as in Node: - TypeScript: the checker types the element as the common base (`typeof Animal`), and an element read keeps the `undefined` an index past the end reads. The construction now narrows the class value out of that union, and an index past the end throws Node's TypeError ("kinds[5] is not a constructor"). Before, it refused with SC2001. - JavaScript: such a literal is a checked-dynamic array stored in a slot of the base's class values, and the check accepted only the base's own class value, so storing it threw "expected class constructor". A class value of a subclass now checks into its base class's slot, as a subclass instance already checks into a base-class instance slot. Classes without subclasses keep the exact check. --- packages/compiler/src/backend/llvm/dyn.ts | 18 ++++ .../src/frontend/lowering/lower-classes.ts | 93 ++++++++++++++++++- .../corpus/6937-class-value-arrays.mjs.json | 6 ++ .../corpus/6938-class-value-arrays.ts.json | 6 ++ tests/corpus/6937-class-value-arrays.mjs | 30 ++++++ tests/corpus/6938-class-value-arrays.ts | 28 ++++++ .../6937-class-value-arrays.mjs.json | 1 + .../6938-class-value-arrays.ts.json | 1 + 8 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json create mode 100644 tests/corpus/6937-class-value-arrays.mjs create mode 100644 tests/corpus/6938-class-value-arrays.ts create mode 100644 tests/harness/corpus-costs/6937-class-value-arrays.mjs.json create mode 100644 tests/harness/corpus-costs/6938-class-value-arrays.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 5c9d8e60f..f0278bce6 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -1530,6 +1530,24 @@ export class LlDyn { const B = new BlockBuilder(); if (t.kind === "classval") { host.declare(`declare ptr @scr_dyn_class_check(ptr, ptr, ptr)`); + // A subclass's class value checks into the base class's slot. + const subclasses = host.classSubtypes(t.className).filter((name) => name !== t.className); + if (subclasses.length > 0) { + host.declare(`declare zeroext i1 @scr_dyn_class_is(ptr, ptr)`); + for (const className of subclasses) { + const subKey = host.cstr(typeKey({ kind: "classval", className })); + const matched = B.tmp(); + B.line(`${matched} = call zeroext i1 @scr_dyn_class_is(ptr %d, ptr ${subKey})`); + const yes = B.newLabel("sub.yes"); + const no = B.newLabel("sub.no"); + B.condBr(matched, yes, no); + B.startBlock(yes); + const sub = B.tmp(); + B.line(`${sub} = call ptr @scr_dyn_class_check(ptr %d, ptr ${subKey}, ptr %path)`); + B.terminate(`ret ptr ${sub}`); + B.startBlock(no); + } + } const checked = B.tmp(); B.line( `${checked} = call ptr @scr_dyn_class_check(ptr %d, ptr ${host.cstr(key)}, ptr %path)`, diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d8ccc15b7..34631cf53 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -10769,8 +10769,99 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { "constructing through a class value of an uninstantiated generic type (annotate the slot with a concrete instantiation — e.g. 'new (v: number) => Box')", ); } - const callee = lowerer.lowerExpr(expr.expression); + let callee = lowerer.lowerExpr(expr.expression); if (callee.type.kind === "dyn") return lowerCheckedConstructor(lowerer, expr, callee); + // An array element read (`new T[0]()` over `[Dog, Animal]`, whose + // checker type is the common base) keeps the undefined an index past + // the end reads: the class value is narrowed out with a check, which + // throws a TypeError for undefined. + if (callee.type.kind === "union") { + const arms = lowerer.unions.get(callee.type.unionId)?.arms ?? []; + const classArms = arms.filter((arm) => arm.kind === "classval"); + if ( + classArms.length === 1 && + classArms[0]!.kind === "classval" && + classArms[0]!.className === calleeT.className && + arms.every((arm) => arm.kind === "classval" || arm.kind === "undefinedT") + ) { + const unionT = callee.type; + const helper = lowerer.narrowedArmHelper(unionT.unionId, calleeT, callee.loc); + if (helper) { + const loc = callee.loc; + const name = `%new.callee:${unionT.unionId}`; + if (!lowerer.liftedFns.some((fn) => fn.name === name)) { + const undefinedTag = lowerer.armTag(unionT.unionId, UNDEFINED_T); + const value: IrExpr = { kind: "varRef", localId: "value", type: unionT, loc }; + lowerer.liftedFns.push({ + name, + params: [ + { localId: "value", name: "value", type: unionT }, + { localId: "message", name: "message", type: STRING }, + ], + locals: [ + { id: "value", name: "value", type: unionT, mutable: false }, + { id: "message", name: "message", type: STRING, mutable: false }, + ], + returnType: calleeT, + body: [ + ...(undefinedTag >= 0 + ? [ + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId: unionT.unionId, + tag: undefinedTag, + negated: false, + value, + type: BOOL, + loc, + }, + then: [ + { + kind: "throw", + value: { + kind: "libCall", + fn: "error.new", + args: [{ kind: "varRef", localId: "message", type: STRING, loc }], + type: { kind: "object", className: "%TypeError" }, + loc, + }, + loc, + }, + ], + else_: null, + loc, + } satisfies IrStmt, + ] + : []), + { + kind: "return", + value: lowerer.checkedNarrowCall(helper, value, calleeT, loc), + loc, + }, + ], + loc, + }); + } + callee = { + kind: "call", + callee: name, + args: [ + callee, + { + kind: "strLit", + value: `${expr.expression.getText()} is not a constructor`, + type: STRING, + loc, + }, + ], + type: calleeT, + loc, + }; + } + } + } if (callee.type.kind !== "classval") lowerer.badType(expr.expression, lowerer.typeOf(expr.expression)); lowerer.noteEdge(classValueConstructor(info.def).fn); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json new file mode 100644 index 000000000..eefa82916 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6937-class-value-arrays.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6937-class-value-arrays.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json new file mode 100644 index 000000000..ff2a51546 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6938-class-value-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6938-class-value-arrays.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6937-class-value-arrays.mjs b/tests/corpus/6937-class-value-arrays.mjs new file mode 100644 index 000000000..a563d7514 --- /dev/null +++ b/tests/corpus/6937-class-value-arrays.mjs @@ -0,0 +1,30 @@ +// Arrays of class values that mix a base class and its subclasses: the +// elements construct, compare and name like the classes themselves. +class Animal { + speak() { + return "..."; + } +} +class Dog extends Animal { + speak() { + return "woof"; + } +} +class Cat extends Animal { + speak() { + return "meow"; + } +} +const kinds = [Dog, Cat, Animal]; +console.log(kinds.length, kinds.map((C) => C.name).join()); +console.log(new kinds[0]().speak(), new kinds[1]().speak(), new kinds[2]().speak()); +console.log(kinds[0] === Dog, kinds.indexOf(Cat), kinds.includes(Animal)); +const made = kinds.map((C) => new C()); +console.log(made.map((a) => a.speak()).join(), made[0] instanceof Animal, made[1] instanceof Dog); +try { + new kinds[5](); +} catch (e) { + console.log(e.name, e.message); +} +const byName = { dog: Dog, cat: Cat }; +console.log(new byName.dog().speak(), new byName["cat"]().speak()); diff --git a/tests/corpus/6938-class-value-arrays.ts b/tests/corpus/6938-class-value-arrays.ts new file mode 100644 index 000000000..672700cbf --- /dev/null +++ b/tests/corpus/6938-class-value-arrays.ts @@ -0,0 +1,28 @@ +// Arrays of class values that mix a base class and its subclasses, with the +// element type the checker infers (the base class): elements construct and +// dispatch to the subclass, and an index past the end throws Node's TypeError. +class Shape { + area(): number { + return 0; + } +} +class Square extends Shape { + area(): number { + return 4; + } +} +class Circle extends Shape { + area(): number { + return 3; + } +} +const shapes = [Square, Circle, Shape]; +console.log(shapes.length, new shapes[0]().area(), new shapes[1]().area(), new shapes[2]().area()); +let total = 0; +for (const S of shapes) total += new S().area(); +console.log(total, shapes[1] === Circle, shapes.map((S) => S.name).join()); +try { + new shapes[3](); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} diff --git a/tests/harness/corpus-costs/6937-class-value-arrays.mjs.json b/tests/harness/corpus-costs/6937-class-value-arrays.mjs.json new file mode 100644 index 000000000..7e905d65a --- /dev/null +++ b/tests/harness/corpus-costs/6937-class-value-arrays.mjs.json @@ -0,0 +1 @@ +{ "plain": 4211, "san": 6383 } diff --git a/tests/harness/corpus-costs/6938-class-value-arrays.ts.json b/tests/harness/corpus-costs/6938-class-value-arrays.ts.json new file mode 100644 index 000000000..76098ce8d --- /dev/null +++ b/tests/harness/corpus-costs/6938-class-value-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2399, "san": 4080 } From 1d8d19e634982f238ccbfd204f4d7355d2a69efa Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 17:05:08 -0500 Subject: [PATCH 015/409] Run stream events and callbacks in Node's AsyncLocalStorage contexts Stream events and callbacks ran with no store. Node's streams run each step in the context of the code that triggers it: a step deferred with process.nextTick (resume, emitReadable, endReadable, afterWrite, the destroy and finish steps) in the context that scheduled it, and other events in the context of the push(), write(), end(), destroy() or pipe() call that emits them. Native-backed streams run their events in the context of their handle. The runtime now captures the active context where it defers a stream step and enters it around the step's dispatch: - the stream tick queue keeps each tick's scheduling context; - the runtime's own process.nextTick jobs (zlib completions, pipeline and finished steps, readline) run in the context they were queued in, as user ticks already did; - fs read and write stream requests keep the context of the call that started them, so a stream's open, reads, writes and close follow its creation; - zlib streams keep their creation context and run each engine step's completion in it. In programs that use AsyncLocalStorage, stream.finished and stream.pipeline callbacks are bound to the context of their call, like the other completion callbacks. Programs that do not use AsyncLocalStorage compile to the same IR. child_process and http2 events are unchanged. Corpus 7383 and 7384. --- .../builtin-apis/asynclocalstorage.mdx | 5 +- .../generated/node-v24-internal.json | 34 ++- internal/compatibility/static-support.json | 2 +- .../frontend/lowering/completion-callbacks.ts | 13 +- .../corpus/7383-als-stream-events.ts.json | 6 + .../corpus/7384-als-stream-events.mjs.json | 6 + packages/runtime/src/scr_async.c | 8 +- packages/runtime/src/scr_fs_stream.c | 13 +- packages/runtime/src/scr_stream.c | 39 ++-- packages/runtime/src/scr_zlib_stream.c | 7 + tests/corpus/7383-als-stream-events.ts | 204 +++++++++++++++++ tests/corpus/7384-als-stream-events.mjs | 214 ++++++++++++++++++ .../7383-als-stream-events.ts.json | 1 + .../7384-als-stream-events.mjs.json | 1 + 14 files changed, 525 insertions(+), 28 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json create mode 100644 tests/corpus/7383-als-stream-events.ts create mode 100644 tests/corpus/7384-als-stream-events.mjs create mode 100644 tests/harness/corpus-costs/7383-als-stream-events.ts.json create mode 100644 tests/harness/corpus-costs/7384-als-stream-events.mjs.json diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx index 98fc92ea1..016b712f6 100644 --- a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -3,5 +3,8 @@ Events of `net`, `tls`, `http` and `https` sockets, servers, requests and responses run with the stores Node runs them with. A socket's or client request's events see the context of the call that created it, and a server's events and its requests' and responses' events see the context of its `listen()` call. Socket `write` and `end` callbacks, `'finish'` and `server.close(callback)` see the context of their call. - Events and callbacks of other streams run with no store, where Node runs them in the context of the code that triggers them. + Stream events and callbacks run with the stores of the code that triggers them, as in Node. + Stream steps that Node defers with `process.nextTick` see the context that scheduled them, and `stream.finished` and `stream.pipeline` callbacks see the context of their call. + `fs` read and write streams and `zlib` streams run their events and callbacks in the context that created them. + Events of `child_process` children and of `http2` sessions and streams do not yet run in the contexts Node gives them. `AsyncResource` has no lowering yet. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 6046daf0e..b5542944d 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1392,7 +1392,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "chapter" @@ -1447,7 +1449,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1525,7 +1529,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1557,7 +1563,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1589,7 +1597,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1621,7 +1631,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1653,7 +1665,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" @@ -1962,6 +1976,8 @@ "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs", "tests/harness/node-test.test.ts" ] }, @@ -2529,7 +2545,9 @@ "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", - "tests/corpus/7380-als-http-event-context.mjs" + "tests/corpus/7380-als-http-event-context.mjs", + "tests/corpus/7383-als-stream-events.ts", + "tests/corpus/7384-als-stream-events.mjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index c57d6013e..640345a02 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -429,7 +429,7 @@ { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/packages/compiler/src/frontend/lowering/completion-callbacks.ts b/packages/compiler/src/frontend/lowering/completion-callbacks.ts index 402741da6..a00e4924d 100644 --- a/packages/compiler/src/frontend/lowering/completion-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/completion-callbacks.ts @@ -1,7 +1,8 @@ /* Completion callbacks in programs that use AsyncLocalStorage. Node runs * the callback of an operation a callback-style API starts (fs.readFile, * crypto.randomBytes, zlib.gzip, dns.lookup, server.listen, net.connect, - * http.get, ...) with the stores active when the call started it. The runtime invokes these callbacks from + * http.get, stream.finished, stream.pipeline, ...) with the stores active + * when the call started it. The runtime invokes these callbacks from * the event loop, where the context is empty, so in programs whose * scheduling carries contexts (RuntimeFeatures asyncContext) each such * argument is bound to the active context at the call: als.bindCallback @@ -50,6 +51,16 @@ const COMPLETION_CALLBACKS: ReadonlyMap = new Map< ["net.listenPathCb", 2], ["process.stderrWriteBytesCb", 2], ["process.stdoutWriteBytesCb", 2], + // stream.finished and stream.pipeline callbacks, which Node binds to + // the context of the call (eos' and pipeline's AsyncResource.bind). + ["stream.finished", 1], + ["stream.finishedDyn", 1], + ["stream.finishedOpts", "last"], + ["stream.finishedOptsSignal", "last"], + ["stream.pipeline", "last"], + ["stream.pipelineDyn", "last"], + ["stream.pipelineStages", "last"], + ["stream.pipelineStagesDyn", "last"], ["zlib.cbOpts", 3], ["zlib.cbOptsDyn", 3], ["zlib.deflateCb", 1], diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json new file mode 100644 index 000000000..d59808c2f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-als-stream-events.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7383-als-stream-events.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json new file mode 100644 index 000000000..51171c8d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7384-als-stream-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7384-als-stream-events.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index e6184ed9b..2bfffa59d 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -1235,10 +1235,14 @@ static SCR_TL ScrNtick *scr_nt_tail = NULL; * it, because 'exit' listeners run AFTER the loop's teardown and may * enqueue ticks that must never run (Node) yet must not leak. */ +static ScrClosure *scr_als_bind(ScrClosure *cb /*moves*/); + +/* A tick runs with the AsyncLocalStorage context active when it was + * queued, as Node's process.nextTick does. */ void scr_next_tick(ScrClosure *cb /*moves*/) { ScrNtick *t = calloc(1, sizeof *t); if (!t) scr_oom(); - t->cb = cb; + t->cb = scr_als_bind(cb); if (scr_nt_tail) scr_nt_tail->next = t; else scr_nt_head = t; scr_nt_tail = t; @@ -4022,7 +4026,7 @@ double scr_set_timeout_handle_als(ScrClosure *cb, double ms) { return scr_set_timeout_handle(scr_als_bind(cb), ms); } double scr_set_immediate_als(ScrClosure *cb) { return scr_set_immediate(scr_als_bind(cb)); } -void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(scr_als_bind(cb)); } +void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(cb); } /* scr_next_tick binds */ void scr_queue_microtask_als(ScrClosure *cb) { scr_queue_microtask(scr_als_bind(cb)); } /* A typed callback bound to the context active when it was bound: the diff --git a/packages/runtime/src/scr_fs_stream.c b/packages/runtime/src/scr_fs_stream.c index 70a5ab075..940068a7b 100644 --- a/packages/runtime/src/scr_fs_stream.c +++ b/packages/runtime/src/scr_fs_stream.c @@ -79,6 +79,9 @@ typedef struct ScrFssOp { int64_t result; /* answer or -errno */ 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 + * continuation runs in it, as Node runs an fs request's callback. */ + ScrAlsCtx *ctx; } ScrFssOp; static void scr_fss_work(void *p) { @@ -101,6 +104,7 @@ static void scr_fss_op_destroy(void *p) { free(op->buf); if (op->chunk) scr_bytes_release(op->chunk); if (op->carry) scr_error_release(op->carry); + scr_als_ctx_release(op->ctx); free(op); } @@ -111,13 +115,20 @@ static ScrFssOp *scr_fss_op_new(ScrStream *s, ScrFssOpKind kind, int fd) { op->kind = kind; op->fd = fd; op->pos = -1; + op->ctx = scr_als_capture(); return op; } static void scr_fss_after(void *p); +static void scr_fss_after_in_context(void *p) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrFssOp *)p)->ctx); + scr_fss_after(p); + scr_als_dispatch_leave(prev); +} + static void scr_fss_submit(ScrFssOp *op) { - scr_work_submit(op, &scr_fss_work, &scr_fss_after, &scr_fss_op_destroy); + scr_work_submit(op, &scr_fss_work, &scr_fss_after_in_context, &scr_fss_op_destroy); } /* ── close (Node's close/_close) ──────────────────────────────────────── */ diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index be5ccb910..835b1db9a 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -515,6 +515,10 @@ typedef struct ScrStreamTick { ScrError *err; /* owned or NULL (SCR_ST_ERROR) */ ScrClosure *cb; /* owned or NULL (SCR_ST_AFTER_WRITE / SCR_ST_WCB_ERR / SCR_ST_WRITE_ERR) */ bool hold; /* SCR_ST_AFTER_WRITE: counted in w.after_holds */ + /* The AsyncLocalStorage context active when the tick was scheduled (+1, + * NULL outside a store): Node schedules these steps with + * process.nextTick, so their events and callbacks run in it. */ + ScrAlsCtx *ctx; struct ScrStreamTick *next; } ScrStreamTick; @@ -531,6 +535,7 @@ static void scr_st_tick(ScrStream *s, ScrStreamTickOp op, ScrError *err /*moves* t->op = op; t->err = err; t->cb = cb; + t->ctx = scr_als_capture(); if (scr_st_tail) scr_st_tail->next = t; else scr_st_head = t; scr_st_tail = t; @@ -6861,6 +6866,21 @@ static void scr_stream_run_tick(ScrStreamTick *t) { static void scr_stream_dispatch(void); +/* A tick runs in the context it was scheduled in. */ +static void scr_stream_run_tick_in_context(ScrStreamTick *t) { + ScrAlsCtx *prev = scr_als_dispatch_enter(t->ctx); + scr_stream_run_tick(t); + scr_als_dispatch_leave(prev); +} + +static void scr_stream_tick_free(ScrStreamTick *t) { + scr_stream_release(t->s); + if (t->err) scr_error_release(t->err); + if (t->cb) scr_closure_release(t->cb); + scr_als_ctx_release(t->ctx); + free(t); +} + /* One tick-marker's dispatch: the queue's FIFO head (markers and entries * are enqueued 1:1 in the same order). After an uncaught throw the * remaining entries drop through the station's exc branch below, keeping @@ -6870,11 +6890,8 @@ static void scr_stream_dispatch_one(void) { if (t == NULL || scr_exc_pending()) return; scr_st_head = t->next; if (scr_st_head == NULL) scr_st_tail = NULL; - scr_stream_run_tick(t); - scr_stream_release(t->s); - if (t->err) scr_error_release(t->err); - if (t->cb) scr_closure_release(t->cb); - free(t); + scr_stream_run_tick_in_context(t); + scr_stream_tick_free(t); if (scr_exc_pending()) scr_stream_dispatch(); /* its exc branch drops the rest */ } @@ -6883,11 +6900,8 @@ static void scr_stream_dispatch(void) { ScrStreamTick *t = scr_st_head; scr_st_head = t->next; if (scr_st_head == NULL) scr_st_tail = NULL; - scr_stream_run_tick(t); - scr_stream_release(t->s); - if (t->err) scr_error_release(t->err); - if (t->cb) scr_closure_release(t->cb); - free(t); + scr_stream_run_tick_in_context(t); + scr_stream_tick_free(t); } /* An uncaught throw ends the process (the loop returns to main): the * undelivered ticks are DROPPED — release their references here so the @@ -6896,10 +6910,7 @@ static void scr_stream_dispatch(void) { while (scr_st_head != NULL) { ScrStreamTick *t = scr_st_head; scr_st_head = t->next; - scr_stream_release(t->s); - if (t->err) scr_error_release(t->err); - if (t->cb) scr_closure_release(t->cb); - free(t); + scr_stream_tick_free(t); } scr_st_tail = NULL; } diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c index 54ffacf02..f1437fc2c 100644 --- a/packages/runtime/src/scr_zlib_stream.c +++ b/packages/runtime/src/scr_zlib_stream.c @@ -51,6 +51,9 @@ typedef struct ScrZlibStream { bool done_pending; /* Transform's kCallback: completion waits for _read */ double rlen_before; /* readableLength when the chunk's transform began */ ScrBytes *flush_bufs[6]; /* zero-length sentinels, one per flush kind */ + /* The AsyncLocalStorage context of the stream's creation (+1, NULL + * outside a store): Node runs the handle's step callbacks in it. */ + ScrAlsCtx *als; } ScrZlibStream; static void scr_zlib_stream_oom(void) { @@ -81,6 +84,7 @@ static void scr_zs_release_v(void *p) { for (size_t i = 0; i < 6; i++) { if (zs->flush_bufs[i]) scr_bytes_release(zs->flush_bufs[i]); } + scr_als_ctx_release(zs->als); free(zs); } @@ -242,6 +246,7 @@ static void scr_zs_step_after(void *payload) { ScrZlibStep *op = payload; ScrZlibStream *zs = op->zs; ScrStream *s = op->s; + ScrAlsCtx *prev = scr_als_dispatch_enter(zs->als); zs->write_in_progress = false; ScrZlibError ze; if (scr_zlib_engine_error(zs->eng, &ze)) { @@ -252,6 +257,7 @@ static void scr_zs_step_after(void *payload) { scr_zs_process(zs, s); } if (zs->pending_close) scr_zs_close_engine(zs); + scr_als_dispatch_leave(prev); } static void scr_zs_step_destroy(void *payload) { @@ -463,6 +469,7 @@ ScrStream *scr_zlib_stream_new(double mode_d, ScrDyn *opts) { ScrZlibStream *zs = calloc(1, sizeof *zs); if (!zs) scr_zlib_stream_oom(); zs->rc = 1; + zs->als = scr_als_capture(); zs->mode = mode; zs->eng = scr_zlib_engine_from_options(mode, &zo); scr_zlib_options_drop(&zo); diff --git a/tests/corpus/7383-als-stream-events.ts b/tests/corpus/7383-als-stream-events.ts new file mode 100644 index 000000000..077e15346 --- /dev/null +++ b/tests/corpus/7383-als-stream-events.ts @@ -0,0 +1,204 @@ +// AsyncLocalStorage contexts of stream events and callbacks. A stream +// step that Node defers with process.nextTick runs in the context of the +// code that scheduled it; other events run in the context of the call that +// emits them: push(), write(), end(), destroy() or pipe(). stream.finished +// and stream.pipeline callbacks run in the context of their call. fs read +// and write streams and zlib streams run their events in the context that +// created them, as Node's handles do. +import { AsyncLocalStorage } from "node:async_hooks"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { finished, pipeline, Readable, Transform, Writable } from "node:stream"; +import { pipeline as pipelinePromise } from "node:stream/promises"; +import * as zlib from "node:zlib"; + +const store = new AsyncLocalStorage(); +const lines: string[] = []; +const log = (what: string): void => { + lines.push(`${what}: ${store.getStore() ?? "none"}`); +}; +const step = (fn: (done: () => void) => void): Promise => + new Promise((resolve) => fn(resolve)); + +// pipe(): the destination's writes and 'finish' follow the pipe() call. +await step((done) => { + const source = new Readable({ read(): void {} }); + const sink = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("pipe write"); + cb(); + }, + }); + sink.on("finish", () => { + log("pipe finish"); + done(); + }); + store.run("pipe", () => source.pipe(sink)); + store.run("push", () => { + source.push("a"); + source.push(null); + }); +}); + +// A Transform's steps follow write() and end(); 'end' follows end(). +await step((done) => { + const upper = new Transform({ + transform(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null, data?: string) => void): void { + log("transform"); + cb(null, chunk.toString().toUpperCase()); + }, + flush(cb: (error?: Error | null, data?: string) => void): void { + log("flush"); + cb(null, "!"); + }, + }); + upper.on("data", () => log("transform data")); + upper.on("end", () => { + log("transform end"); + done(); + }); + store.run("write", () => upper.write("a")); + store.run("end", () => upper.end()); +}); + +// A write that completes later: its callback, _final, 'prefinish', the +// end() callback, 'finish' and 'close' follow the write() call. +await step((done) => { + const slow = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("slow write"); + setImmediate(cb); + }, + final(cb: (error?: Error | null) => void): void { + log("final"); + cb(); + }, + }); + slow.on("prefinish", () => log("prefinish")); + slow.on("finish", () => log("finish")); + slow.on("close", () => { + log("close"); + done(); + }); + store.run("slow-write", () => slow.write("a", () => log("write callback"))); + store.run("slow-end", () => slow.end(() => log("end callback"))); +}); + +// 'readable' and 'end' follow the push() that triggers them. +await step((done) => { + const source = new Readable({ read(): void {} }); + source.on("readable", () => { + log("readable"); + while (source.read() !== null) {} + }); + source.on("end", () => { + log("readable end"); + done(); + }); + store.run("push-x", () => source.push("x")); + setTimeout(() => store.run("push-eof", () => source.push(null)), 5); +}); + +// destroy(): 'error' and 'close' follow the destroy() call. +await step((done) => { + const source = new Readable({ read(): void {} }); + source.on("error", () => log("destroy error")); + source.on("close", () => { + log("destroy close"); + done(); + }); + store.run("destroy", () => source.destroy(new Error("stopped"))); +}); + +// finished() and pipeline() callbacks follow their call, also when another +// context ends or destroys the stream. +await step((done) => { + const source = new Readable({ read(): void {} }); + store.run("finished", () => + finished(source, () => { + log("finished callback"); + done(); + }), + ); + source.resume(); + store.run("finished-eof", () => source.push(null)); +}); +await step((done) => { + const source = new Readable({ read(): void {} }); + const sink = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + cb(); + }, + }); + store.run("pipeline", () => + pipeline(source, sink, (err: NodeJS.ErrnoException | null) => { + log(`pipeline callback ${err ? err.message : "ok"}`); + done(); + }), + ); + setTimeout(() => store.run("pipeline-destroy", () => source.destroy(new Error("boom"))), 5); +}); + +// stream/promises pipeline: the stages follow the call; after the await +// the caller's context is back. +await store.run("promise-pipeline", () => + pipelinePromise( + Readable.from(["a"]), + new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("promise pipeline write"); + cb(); + }, + }), + ), +); +log("after the promise pipeline"); + +// fs streams: the events follow the context that created the stream. +const file = path.join(os.tmpdir(), `scriptc-7383-${process.pid}.txt`); +await step((done) => { + const out = store.run("fs-write-create", () => fs.createWriteStream(file)); + out.on("finish", () => log("fs finish")); + out.on("close", () => { + log("fs write close"); + done(); + }); + store.run("fs-write", () => out.write("hello", () => log("fs write callback"))); + store.run("fs-end", () => out.end()); +}); +await step((done) => { + const input = store.run("fs-read-create", () => fs.createReadStream(file)); + store.run("fs-listen", () => { + input.on("open", () => log("fs open")); + input.on("data", () => log("fs data")); + input.on("end", () => log("fs end")); + input.on("close", () => { + log("fs read close"); + done(); + }); + }); +}); +fs.unlinkSync(file); + +// zlib streams: the events follow the context that created the stream. +await step((done) => { + const gzip = store.run("gzip-create", () => zlib.createGzip()); + const gunzip = store.run("gunzip-create", () => zlib.createGunzip()); + const parts: Buffer[] = []; + const sink = new Writable({ + write(chunk: Buffer, encoding: BufferEncoding, cb: (error?: Error | null) => void): void { + log("gunzip output"); + parts.push(chunk); + cb(); + }, + }); + store.run("zlib-pipeline", () => + pipeline(Readable.from(["hello ", "world"]), gzip, gunzip, sink, () => { + log(`zlib pipeline callback ${Buffer.concat(parts).toString()}`); + done(); + }), + ); +}); + +for (const line of lines) console.log(line); diff --git a/tests/corpus/7384-als-stream-events.mjs b/tests/corpus/7384-als-stream-events.mjs new file mode 100644 index 000000000..6d67bf26f --- /dev/null +++ b/tests/corpus/7384-als-stream-events.mjs @@ -0,0 +1,214 @@ +// AsyncLocalStorage contexts of stream events and callbacks, from +// JavaScript. Stream steps that Node defers with process.nextTick run in +// the context that scheduled them: resume() after a 'data' listener, +// uncork() from a tick, a write after end(), a push() from a user tick. +// Other events follow the call that emits them. stream.finished callbacks +// run in the context of their call, with stream/promises' finished() +// resolving in the context that awaits it. readline lines follow the +// createInterface() call that resumes the input, and zlib streams run +// their events and flush() callbacks in the context that created them. +import { AsyncLocalStorage } from "node:async_hooks"; +import readline from "node:readline"; +import { Duplex, finished, PassThrough, Readable, Writable } from "node:stream"; +import { finished as finishedPromise } from "node:stream/promises"; +import zlib from "node:zlib"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); +const step = (fn) => new Promise((resolve) => fn(resolve)); + +// pipe() and unpipe() events, and writes through a PassThrough. +await step((done) => { + const through = new PassThrough(); + const sink = new Writable({ write(chunk, encoding, cb) { log("pipe write"); cb(); } }); + sink.on("pipe", () => log("pipe event")); + sink.on("unpipe", () => log("unpipe event")); + store.run("pipe", () => through.pipe(sink)); + store.run("through-write", () => through.write("q")); + setTimeout(() => { + store.run("unpipe", () => through.unpipe(sink)); + done(); + }, 5); +}); + +// cork() and uncork() from a tick: the writes follow the tick's context. +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { log("corked write"); cb(); } }); + sink.on("finish", () => { + log("corked finish"); + done(); + }); + store.run("cork", () => { + sink.cork(); + sink.write("a"); + sink.write("b"); + }); + store.run("uncork", () => process.nextTick(() => sink.uncork())); + setTimeout(() => store.run("corked-end", () => sink.end()), 5); +}); + +// A write after end(): its callback and 'error' follow the write() call. +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { cb(); } }); + sink.on("error", () => log("write after end error")); + sink.end(); + store.run("late-write", () => + sink.write("x", () => { + log("late write callback"); + done(); + }), + ); +}); + +// A failing _write: the callback, 'error' and 'close' follow write(). +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { cb(new Error("bad")); } }); + sink.on("error", () => log("failed write error")); + sink.on("close", () => { + log("failed write close"); + done(); + }); + store.run("failed-write", () => sink.write("x", () => log("failed write callback"))); +}); + +// A Duplex that pushes from _write: 'data' and 'end' follow the resume() +// tick, which the 'data' listener scheduled with no store. +await step((done) => { + const duplex = new Duplex({ + read() {}, + write(chunk, encoding, cb) { + log("duplex write"); + this.push(chunk); + cb(); + }, + }); + duplex.on("data", () => { + log("duplex data"); + duplex.push(null); + }); + duplex.on("end", () => { + log("duplex end"); + done(); + }); + store.run("duplex-write", () => duplex.write("z")); +}); + +// A push() from a user tick, a 'data' listener registered in a store with +// setEncoding(), and PassThrough's 'end' and 'close' after end(). +await step((done) => { + const source = new Readable({ read() {} }); + source.on("data", () => log("tick data")); + source.on("end", () => { + log("tick end"); + done(); + }); + store.run("user-tick", () => + process.nextTick(() => { + log("user tick"); + source.push("x"); + }), + ); + setTimeout(() => store.run("tick-eof", () => source.push(null)), 5); +}); +await step((done) => { + const source = new Readable({ read() {} }); + source.setEncoding("utf8"); + store.run("data-listener", () => source.on("data", () => log("text data"))); + source.on("end", () => { + log("text end"); + done(); + }); + source.push("hi"); + source.push(null); +}); +await step((done) => { + const through = new PassThrough(); + through.on("end", () => log("through end")); + through.on("close", () => { + log("through close"); + done(); + }); + through.resume(); + store.run("through-end", () => through.end("last")); +}); + +// finished() on a Writable, and stream/promises' finished(). +await step((done) => { + const sink = new Writable({ write(chunk, encoding, cb) { setTimeout(cb, 1); } }); + store.run("finished-writable", () => + finished(sink, () => { + log("finished callback"); + done(); + }), + ); + sink.on("finish", () => log("writable finish")); + store.run("writable-end", () => { + sink.write("a"); + sink.end(); + }); +}); +await step((done) => { + const source = new Readable({ read() {} }); + source.resume(); + store.run("finished-promise", () => finishedPromise(source)).then(() => { + log("finished promise"); + done(); + }); + setTimeout(() => store.run("promise-eof", () => source.push(null)), 5); +}); + +// Readable.from() over an async generator: the reads follow the resume() +// tick, scheduled with no store. +await step((done) => { + async function* letters() { + log("generator start"); + yield "a"; + log("generator end"); + } + const source = store.run("from", () => Readable.from(letters())); + source.on("data", () => log("from data")); + source.on("end", () => { + log("from end"); + done(); + }); +}); + +// readline over a Readable: createInterface() resumes the input. +await step((done) => { + const input = Readable.from(["a\nb\n"]); + const rl = store.run("readline", () => readline.createInterface({ input })); + rl.on("line", (line) => log(`line ${line}`)); + rl.on("close", () => { + log("readline close"); + done(); + }); +}); + +// zlib streams: events and flush() callbacks follow the creation context. +await step((done) => { + const gzip = store.run("gzip-create", () => zlib.createGzip()); + store.run("gzip-listen", () => { + gzip.on("data", () => log("gzip data")); + gzip.on("end", () => { + log("gzip end"); + done(); + }); + }); + store.run("gzip-write", () => { + gzip.write("hello"); + gzip.end(); + }); +}); +await step((done) => { + const deflate = store.run("deflate-create", () => zlib.createDeflate()); + store.run("deflate-flush", () => deflate.flush(() => log("deflate flush callback"))); + deflate.on("data", () => {}); + deflate.on("end", () => { + log("deflate end"); + done(); + }); + store.run("deflate-end", () => deflate.end("x")); +}); + +for (const line of lines) console.log(line); diff --git a/tests/harness/corpus-costs/7383-als-stream-events.ts.json b/tests/harness/corpus-costs/7383-als-stream-events.ts.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7383-als-stream-events.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } diff --git a/tests/harness/corpus-costs/7384-als-stream-events.mjs.json b/tests/harness/corpus-costs/7384-als-stream-events.mjs.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7384-als-stream-events.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } From 846552b0e28a9adc5fa813c207300c3d370c5d1d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 17:18:54 -0500 Subject: [PATCH 016/409] Support crypto.getCipherInfo, AES-CCM and the AES key wrap ciphers getCipherInfo(nameOrNid[, options]) runs in a new runtime unit, scr_cipher_info.c, over a table that scripts/gen-cipher-info.mjs generates from Node 24.22's getCipherInfo. The table covers every cipher OpenSSL names, including the ones without a native implementation. Lookups match names case-insensitively and accept OpenSSL's nids. The unit applies cipher.js's argument checks and GetCipherInfo's keyLength and ivLength rules, and returns the info object in Node's key order. CipherInfo maps to a checked-dynamic value, so TypeScript and JavaScript code receive Node's object. Reading the nid Node leaves out for the SIV and SM4 GCM, CCM and XTS ciphers, where the declared type gives number, throws a TypeError. createCipheriv and createDecipheriv support aes-128/192/256-ccm (and the id-aesN-ccm names) over OpenSSL's ccm128 steps. They follow Node's tag and IV lengths, the plaintextLength requirement, the message-length caps and the one-update rule, and report a failed authentication from final(). They also support the RFC 3394 and RFC 5649 key wrap ciphers (aes-N-wrap, aesN-wrap, id-aesN-wrap, the -pad forms and the -inv ciphers) over wrap128.c's routines, with Node's lengths, integrity failures and one-update rule. The new names follow the existing ones in the cipher table, so the lookup cost of the existing ciphers is unchanged. --- .../limitations/builtin-apis/crypto.mdx | 4 +- .../src/generated/node-v24-compatibility.json | 6 +- .../generated/node-v24-internal.json | 16 +- internal/compatibility/static-support.json | 5 +- .../ambient/scriptc-node-fallback.d.ts | 60 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/native/runtime-inputs.ts | 1 + .../src/frontend/lowering/builtins/crypto.ts | 31 +- .../src/frontend/lowering/surfaces.ts | 11 +- packages/compiler/src/frontend/type-mapper.ts | 11 + packages/compiler/src/ir/builtin-effects.ts | 2 + .../compiler/src/ir/builtin-signatures.ts | 3 + packages/compiler/surface-manifest.json | 4 +- .../corpus/7154-crypto-cipher-info.cjs.json | 6 + .../tests/corpus/7155-cipher-aes-ccm.cjs.json | 6 + .../corpus/7156-cipher-key-wrap.cjs.json | 6 + .../7157-crypto-cipher-info-typed.ts.json | 6 + .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_cipher.c | 516 ++++++++++++++- packages/runtime/src/scr_cipher_info.c | 161 +++++ packages/runtime/src/scr_cipher_info.h | 615 ++++++++++++++++++ packages/runtime/src/scr_crypto.h | 3 + scripts/gen-cipher-info.mjs | 125 ++++ tests/corpus/7154-crypto-cipher-info.cjs | 57 ++ tests/corpus/7155-cipher-aes-ccm.cjs | 158 +++++ tests/corpus/7156-cipher-key-wrap.cjs | 103 +++ tests/corpus/7157-crypto-cipher-info-typed.ts | 42 ++ tests/fixtures/node-types/fenced.ts | 2 +- .../__snapshots__/node-types-fenced.txt | 10 +- .../7154-crypto-cipher-info.cjs.json | 1 + .../corpus-costs/7155-cipher-aes-ccm.cjs.json | 1 + .../7156-cipher-key-wrap.cjs.json | 1 + .../7157-crypto-cipher-info-typed.ts.json | 1 + tests/harness/errors.test.ts | 37 +- 34 files changed, 1962 insertions(+), 52 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7154-crypto-cipher-info.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json create mode 100644 packages/runtime/src/scr_cipher_info.c create mode 100644 packages/runtime/src/scr_cipher_info.h create mode 100644 scripts/gen-cipher-info.mjs create mode 100644 tests/corpus/7154-crypto-cipher-info.cjs create mode 100644 tests/corpus/7155-cipher-aes-ccm.cjs create mode 100644 tests/corpus/7156-cipher-key-wrap.cjs create mode 100644 tests/corpus/7157-crypto-cipher-info-typed.ts create mode 100644 tests/harness/corpus-costs/7154-crypto-cipher-info.cjs.json create mode 100644 tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json create mode 100644 tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json create mode 100644 tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 971065408..bd8e96880 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -8,7 +8,9 @@ `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. - `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. + `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, GCM, CCM, ECB, CFB, CFB8, CFB1, and OFB modes and as key wrap (the wrap and wrap-pad ciphers and their -inv forms), des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. + `getCipherInfo` answers Node's info for every cipher OpenSSL names, including the ones without a native implementation. + The result is Node's object; the SIV ciphers and the SM4 GCM, CCM, and XTS ciphers have no `nid`, and reading it where the declared `CipherInfo` type gives `number` throws a TypeError. Cipheriv and Decipheriv values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, iterated with `for await`, and listened to, and their stream properties read as in Node; written chunks go through `update()`, and the end pushes `final()` or reports its error through `'error'`. Through `unknown` and `any` slots and untyped JavaScript, in a program that uses streams, their stream properties and their `on`, `once`, `off`, `pipe`, `unpipe`, `read`, `pause`, `resume`, `setEncoding`, `write`, `end`, `destroy`, `cork`, and `uncork` members work; `write` and `end` take no encoding or callback there, and the other stream members throw a coded SC2020 error. Hash, Hmac, Cipheriv, and Decipheriv streams count a written string's bytes in `writableLength`, where Node counts the string's length. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index c341da852..c3f1835ab 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11660,9 +11660,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 9393fcaa8..ba8692b49 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19332,6 +19332,10 @@ "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs", + "tests/corpus/7154-crypto-cipher-info.cjs", + "tests/corpus/7155-cipher-aes-ccm.cjs", + "tests/corpus/7156-cipher-key-wrap.cjs", + "tests/corpus/7157-crypto-cipher-info-typed.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -19719,6 +19723,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createCipheriv", "tests": [ + "tests/corpus/7155-cipher-aes-ccm.cjs", + "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", @@ -19760,6 +19766,8 @@ "status": "partial", "evidence": "surface-manifest:node-builtin.crypto.createDecipheriv", "tests": [ + "tests/corpus/7155-cipher-aes-ccm.cjs", + "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", @@ -20434,8 +20442,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.getCipherInfo" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.getCipherInfo", + "tests": [ + "tests/corpus/7154-crypto-cipher-info.cjs", + "tests/corpus/7157-crypto-cipher-info-typed.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 06aad26cf..100882c1a 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -44,8 +44,8 @@ "node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"], "node-builtin.crypto.checkPrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], "node-builtin.crypto.checkPrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], - "node-builtin.crypto.createCipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], - "node-builtin.crypto.createDecipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.createCipheriv": ["tests/corpus/7155-cipher-aes-ccm.cjs", "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.createDecipheriv": ["tests/corpus/7155-cipher-aes-ccm.cjs", "tests/corpus/7156-cipher-key-wrap.cjs", "tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], "node-builtin.crypto.createDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.createDiffieHellmanGroup": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.createECDH": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"], @@ -124,6 +124,7 @@ "node-builtin.fs.constants": ["tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs"], "node-builtin.crypto.generatePrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], "node-builtin.crypto.generatePrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"], + "node-builtin.crypto.getCipherInfo": ["tests/corpus/7154-crypto-cipher-info.cjs", "tests/corpus/7157-crypto-cipher-info-typed.ts"], "node-builtin.crypto.getDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"], "node-builtin.crypto.getRandomValues": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"], "node-builtin.url.parse": ["tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"], diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 5f3805813..b20eee94d 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3171,13 +3171,17 @@ declare module "crypto" { callback: (error: Error | null, buffer: Buffer) => void, ): void; export type BinaryLike = string | ArrayBuffer | ArrayBufferView; - /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, - * cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and - * chacha20-poly1305. Both are Transforms: written chunks go through - * update(), and the end pushes final(). */ + /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ccm, ecb, cfb, + * cfb8, cfb1, and ofb modes and their key wraps, des-ede3 and des-ede, + * chacha20, and chacha20-poly1305. Both are Transforms: written chunks go + * through update(), and the end pushes final(). */ + export type CipherCCMTypes = "aes-128-ccm" | "aes-192-ccm" | "aes-256-ccm"; export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; export type CipherChaCha20Poly1305Types = "chacha20-poly1305"; export type CipherKey = BinaryLike | KeyObject; + export interface CipherCCMOptions { + authTagLength: number; + } export interface CipherGCMOptions { authTagLength?: number; } @@ -3197,6 +3201,10 @@ declare module "crypto" { final(outputEncoding: BufferEncoding): string; setAutoPadding(autoPadding?: boolean): this; } + export interface CipherCCM extends Cipheriv { + setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this; + getAuthTag(): Buffer; + } export interface CipherGCM extends Cipheriv { setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; getAuthTag(): Buffer; @@ -3218,6 +3226,10 @@ declare module "crypto" { final(outputEncoding: BufferEncoding): string; setAutoPadding(autoPadding?: boolean): this; } + export interface DecipherCCM extends Decipheriv { + setAuthTag(buffer: ArrayBufferView): this; + setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this; + } export interface DecipherGCM extends Decipheriv { setAuthTag(buffer: ArrayBufferView): this; setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; @@ -3226,6 +3238,12 @@ declare module "crypto" { setAuthTag(buffer: ArrayBufferView): this; setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; } + export function createCipheriv( + algorithm: CipherCCMTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherCCMOptions, + ): CipherCCM; export function createCipheriv( algorithm: CipherGCMTypes, key: CipherKey, @@ -3244,6 +3262,12 @@ declare module "crypto" { iv: BinaryLike | null, options?: { authTagLength?: number }, ): Cipheriv; + export function createDecipheriv( + algorithm: CipherCCMTypes, + key: CipherKey, + iv: BinaryLike, + options: CipherCCMOptions, + ): DecipherCCM; export function createDecipheriv( algorithm: CipherGCMTypes, key: CipherKey, @@ -3468,6 +3492,34 @@ declare module "crypto" { * per member like http2.constants. */ export function getFips(): 1 | 0; export function getCiphers(): string[]; + export type CipherMode = + | "cbc" + | "ccm" + | "cfb" + | "ctr" + | "ecb" + | "gcm" + | "ocb" + | "ofb" + | "stream" + | "wrap" + | "xts"; + export interface CipherInfoOptions { + keyLength?: number | undefined; + ivLength?: number | undefined; + } + export interface CipherInfo { + name: string; + nid: number; + blockSize?: number | undefined; + ivLength?: number | undefined; + keyLength: number; + mode: CipherMode; + } + export function getCipherInfo( + nameOrNid: string | number, + options?: CipherInfoOptions, + ): CipherInfo | undefined; export function getHashes(): string[]; export function getCurves(): string[]; /* Node v24 crypto.constants, literal-typed (the Http2Constants diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index d0888ccbe..7d92b59f0 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -580,6 +580,7 @@ export const LIB_FN_SYMS: Record = { "crypto.ecdhConvertKey": "scr_crypto_ecdh_convert_key", "crypto.dhNew": "scr_crypto_dh_new", "crypto.checkPrimeSync": "scr_crypto_check_prime_sync", + "crypto.getCipherInfo": "scr_crypto_get_cipher_info", "crypto.checkPrimeCb": "scr_crypto_check_prime_async", "crypto.generatePrimeSync": "scr_crypto_generate_prime_sync", "crypto.generatePrimeCb": "scr_crypto_generate_prime_async", diff --git a/packages/compiler/src/backend/native/runtime-inputs.ts b/packages/compiler/src/backend/native/runtime-inputs.ts index bc9643670..6fb51fd32 100644 --- a/packages/compiler/src/backend/native/runtime-inputs.ts +++ b/packages/compiler/src/backend/native/runtime-inputs.ts @@ -72,6 +72,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [ "scr_itable.c", "scr_slots.c", "scr_alloc.c", + "scr_cipher_info.c", ] as const; export function runtimeSrcDir(): string { diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 47e66f9b2..9ae1cf7f3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -869,9 +869,17 @@ const IMPLEMENTED_CIPHERS = new Set([ "id-aes192-gcm", "id-aes256-gcm", "chacha20-poly1305", - ...["128", "192", "256"].flatMap((bits) => - ["ecb", "cfb", "cfb8", "cfb1", "ofb"].map((mode) => `aes-${bits}-${mode}`), - ), + ...["128", "192", "256"].flatMap((bits) => [ + ...["ecb", "cfb", "cfb8", "cfb1", "ofb", "ccm"].map((mode) => `aes-${bits}-${mode}`), + `id-aes${bits}-ccm`, + ...["wrap", "wrap-pad"].flatMap((wrap) => [ + `aes-${bits}-${wrap}`, + `aes${bits}-${wrap}`, + `id-aes${bits}-${wrap}`, + `aes-${bits}-${wrap}-inv`, + `aes${bits}-${wrap}-inv`, + ]), + ]), "des-ede3-cbc", "des3", "des-ede3", @@ -889,9 +897,10 @@ const IMPLEMENTED_CIPHERS = new Set([ ]); const SUPPORTED_CIPHERS_HINT = - "the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 " + - "and ofb modes, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb " + - "and ofb modes, chacha20, and chacha20-poly1305"; + "the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, " + + "cfb1 and ofb modes and their wrap, wrap-pad, wrap-inv and wrap-pad-inv key wraps, des-ede3 in " + + "cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20, and " + + "chacha20-poly1305"; /** A literal cipher name that Node knows but the runtime does not * implement is refused at compile time. Unknown literal names keep Node's @@ -1380,10 +1389,11 @@ function takesUndefinedError(lowerer: Lowerer, callback: ts.Expression): boolean } /** checkPrime, checkPrimeSync, generatePrime and generatePrimeSync - * (scr_dh.c): checked-dynamic arguments, which the runtime checks as Node - * does (a function in the options position is the callback), and the - * sync forms' boolean, ArrayBuffer or bigint converted to the call site's - * type. Null for other members. */ + * (scr_dh.c), and getCipherInfo (scr_cipher_info.c): checked-dynamic + * arguments, which the runtime checks as Node does (a function in the + * options position is the callback), and the sync forms' boolean, + * ArrayBuffer, bigint or info object converted to the call site's type. + * Null for other members. */ function lowerPrimeCall( lowerer: Lowerer, expr: ts.CallExpression, @@ -1395,6 +1405,7 @@ function lowerPrimeCall( checkPrimeSync: ["crypto.checkPrimeSync", 1, 2, "checkPrimeSync(candidate[, options])"], checkPrime: ["crypto.checkPrimeCb", 1, 3, "checkPrime(candidate[, options], callback)"], generatePrimeSync: ["crypto.generatePrimeSync", 1, 2, "generatePrimeSync(size[, options])"], + getCipherInfo: ["crypto.getCipherInfo", 0, 2, "getCipherInfo(nameOrNid[, options])"], generatePrime: ["crypto.generatePrimeCb", 1, 3, "generatePrime(size[, options], callback)"], }; const form = forms[member]; diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index cb16fd257..d16b30e34 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1593,6 +1593,9 @@ export const BUILTIN_MODULE_FNS: Record< // Prime checks and generation (lowerPrimeCall): checked-dynamic // arguments, results converted to the call site's type. checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN }, + // Checked-dynamic arguments, the result converted to the call site's + // type (lowerPrimeCall). + getCipherInfo: { fn: "crypto.getCipherInfo", params: [], result: DYN }, checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID }, generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN }, generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID }, @@ -3101,14 +3104,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< diffieHellman: "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; " + "the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets", - ...Object.fromEntries( - ["getCipherInfo"].map((m) => [ - m, - "cipher metadata has no lowering — createCipheriv and createDecipheriv support " + - "aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and " + - "des-ede, chacha20, and chacha20-poly1305", - ]), - ), setFips: "a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " + "in a non-FIPS build — getFips() answers 0 here", diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 14379f30f..9b57fa7a4 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -4074,6 +4074,17 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); return { kind: "record", shapeId: ctx.shapes.intern(fields, false, undefined, order) }; } + // crypto.getCipherInfo's result is Node's info object itself, read as a + // checked-dynamic value: Node leaves out the fields a cipher lacks, + // including the nid the declaration requires, so a record cannot hold + // every result. + if ( + isStdlibInterface("CipherInfo") && + psym!.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientModule(d, "crypto"), + ) + ) + return DYN; // Filesystem options are data records. Keep their declared optional fields // instead of allowing arbitrary standard-library interfaces into records. if ( diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 7d01bb83d..8738238e8 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -16,6 +16,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ // Node's prime, generator, size, encoding and group-name errors. "crypto.dhNew", "crypto.dhGroupNew", + // Node's nameOrNid and options errors. + "crypto.getCipherInfo", // Node's candidate, size, options, add and rem errors, and OpenSSL's // generation errors. "crypto.checkPrimeSync", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 6bf864da6..0afebe32f 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2840,6 +2840,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * undefined, and call it in a later macrotask; Node's argument errors * throw (may-throw). */ "crypto.checkPrimeSync": { argTypes: [DYN, DYN], result: DYN }, + /** getCipherInfo(nameOrNid, options) over checked-dynamic arguments + * (scr_cipher_info.c): the info object or undefined. */ + "crypto.getCipherInfo": { argTypes: [DYN, DYN], result: DYN }, "crypto.checkPrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, "crypto.generatePrimeSync": { argTypes: [DYN, DYN], result: DYN }, "crypto.generatePrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID }, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 81fb23ae2..e44f5d31e 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -612,9 +612,7 @@ "id": "node-builtin.crypto.getCipherInfo", "kind": "node-builtin", "name": "crypto.getCipherInfo", - "status": "unsupported", - "code": "SC2020", - "note": "cipher metadata has no lowering — createCipheriv and createDecipheriv support aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and chacha20-poly1305" + "status": "static" }, { "id": "node-builtin.crypto.getDiffieHellman", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7154-crypto-cipher-info.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7154-crypto-cipher-info.cjs.json new file mode 100644 index 000000000..65e5f162e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7154-crypto-cipher-info.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7154-crypto-cipher-info.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json new file mode 100644 index 000000000..a52818c74 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7155-cipher-aes-ccm.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7155-cipher-aes-ccm.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json new file mode 100644 index 000000000..f2473c303 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7156-cipher-key-wrap.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7156-cipher-key-wrap.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json new file mode 100644 index 000000000..340d6e7b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7157-crypto-cipher-info-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7157-crypto-cipher-info-typed.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 322832f9f..57610c80b 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -49,6 +49,8 @@ export const BASE_RUNTIME_SOURCES = [ "scr_itable.c", "scr_slots.c", "scr_alloc.c", + // crypto.getCipherInfo's table; it links only into programs that call it. + "scr_cipher_info.c", ]; export const OPTIONAL_RUNTIME_SOURCES = [ diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index 00d229bad..2894af8e2 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -37,17 +37,23 @@ typedef enum { SC_CFB1, SC_OFB, SC_CHACHA20, + /* CCM and the key wraps stay last: sc_transform tests mode >= SC_CCM. */ + SC_CCM, + SC_WRAP, /* RFC 3394 AES key wrap */ + SC_WRAP_PAD, /* RFC 5649 AES key wrap with padding */ } ScCipherMode; typedef struct { const char *name; ScCipherMode mode; unsigned key_len; - bool des; /* triple DES (two- or three-key) rather than AES */ + bool des; /* triple DES (two- or three-key) rather than AES */ + bool inverse; /* key wrap with the inverse cipher (the -inv names) */ } ScCipherSpec; /* Node's names for the implemented ciphers, including OpenSSL's aliases. - * Lookup is case-insensitive, like OpenSSL's. */ + * Lookup is case-insensitive, like OpenSSL's, and scans in order, so the + * CCM and key wrap names follow the others. */ static const ScCipherSpec sc_specs[] = { {"aes-128-cbc", SC_CBC, 16}, {"aes-192-cbc", SC_CBC, 24}, {"aes-256-cbc", SC_CBC, 32}, {"aes128", SC_CBC, 16}, {"aes192", SC_CBC, 24}, {"aes256", SC_CBC, 32}, @@ -68,6 +74,38 @@ static const ScCipherSpec sc_specs[] = { {"des-ede-ecb", SC_ECB, 16, true}, {"des-ede-cfb", SC_CFB, 16, true}, {"des-ede-ofb", SC_OFB, 16, true}, {"chacha20", SC_CHACHA20, 32}, + {"aes-128-ccm", SC_CCM, 16}, {"aes-192-ccm", SC_CCM, 24}, {"aes-256-ccm", SC_CCM, 32}, + {"id-aes128-ccm", SC_CCM, 16}, {"id-aes192-ccm", SC_CCM, 24}, {"id-aes256-ccm", SC_CCM, 32}, + {"aes-128-wrap", SC_WRAP, 16, false, false}, + {"aes128-wrap", SC_WRAP, 16, false, false}, + {"id-aes128-wrap", SC_WRAP, 16, false, false}, + {"aes-128-wrap-inv", SC_WRAP, 16, false, true}, + {"aes128-wrap-inv", SC_WRAP, 16, false, true}, + {"aes-128-wrap-pad", SC_WRAP_PAD, 16, false, false}, + {"aes128-wrap-pad", SC_WRAP_PAD, 16, false, false}, + {"id-aes128-wrap-pad", SC_WRAP_PAD, 16, false, false}, + {"aes-128-wrap-pad-inv", SC_WRAP_PAD, 16, false, true}, + {"aes128-wrap-pad-inv", SC_WRAP_PAD, 16, false, true}, + {"aes-192-wrap", SC_WRAP, 24, false, false}, + {"aes192-wrap", SC_WRAP, 24, false, false}, + {"id-aes192-wrap", SC_WRAP, 24, false, false}, + {"aes-192-wrap-inv", SC_WRAP, 24, false, true}, + {"aes192-wrap-inv", SC_WRAP, 24, false, true}, + {"aes-192-wrap-pad", SC_WRAP_PAD, 24, false, false}, + {"aes192-wrap-pad", SC_WRAP_PAD, 24, false, false}, + {"id-aes192-wrap-pad", SC_WRAP_PAD, 24, false, false}, + {"aes-192-wrap-pad-inv", SC_WRAP_PAD, 24, false, true}, + {"aes192-wrap-pad-inv", SC_WRAP_PAD, 24, false, true}, + {"aes-256-wrap", SC_WRAP, 32, false, false}, + {"aes256-wrap", SC_WRAP, 32, false, false}, + {"id-aes256-wrap", SC_WRAP, 32, false, false}, + {"aes-256-wrap-inv", SC_WRAP, 32, false, true}, + {"aes256-wrap-inv", SC_WRAP, 32, false, true}, + {"aes-256-wrap-pad", SC_WRAP_PAD, 32, false, false}, + {"aes256-wrap-pad", SC_WRAP_PAD, 32, false, false}, + {"id-aes256-wrap-pad", SC_WRAP_PAD, 32, false, false}, + {"aes-256-wrap-pad-inv", SC_WRAP_PAD, 32, false, true}, + {"aes256-wrap-pad-inv", SC_WRAP_PAD, 32, false, true}, }; /* Ciphers OpenSSL 3 keeps in its legacy provider, which Node 24 does not @@ -139,7 +177,8 @@ typedef struct ScCipher { int tag_state; unsigned char tag[16]; unsigned char block[16]; /* CBC: chaining value; CTR: counter block; CFB, OFB: - shift register; chacha20: the IV */ + shift register; chacha20, CCM: the IV; key wrap: + the integrity check value */ unsigned char buf[16]; /* CBC, ECB: buffered input; CTR: keystream block */ size_t bufsz; /* CBC, ECB: buffered bytes; CTR, CFB, OFB: keystream offset */ @@ -152,6 +191,18 @@ typedef struct ScCipher { uint64_t aad_len; /* chacha20-poly1305 AAD bytes */ uint64_t text_len; /* chacha20-poly1305 data bytes */ bool aad_open; /* AAD was added since the last data or padding */ + /* CCM: OpenSSL's ccm128 state (the counter block, whose first byte holds + * the L and M parameters and the Adata flag, and the CBC-MAC), its + * provider's flags, and Node's one-update rule. A decipher's expected + * tag is `tag`. */ + size_t ccm_iv_len; + unsigned char ccm_nonce[16]; + unsigned char ccm_cmac[16]; + bool ccm_iv_set, ccm_len_set, ccm_tag_set; + bool ccm_updated; /* a decipher's one update() ran; key wrap: the one update() */ + bool ccm_auth_failed; /* that update failed to authenticate */ + double ccm_max_message; + bool wrap_decrypt_block; /* key wrap: the AES key schedule decrypts */ ScrStr *decoder; /* the StringDecoder encoding once string output starts */ double pending; /* the decoder's carried bytes */ /* The Transform of a cipher used as a stream (+1) and its release @@ -513,6 +564,10 @@ static size_t sc_iv_length(ScCipherMode mode, bool des) { return 12; case SC_CHACHA20: return 16; + case SC_WRAP: + return 8; + case SC_WRAP_PAD: + return 4; default: return des ? 8 : 16; } @@ -530,9 +585,9 @@ static void sc_unsupported_cipher(const ScrStr *name) { scr_jb_puts(&b, "The cipher '"); scr_jb_put_str(&b, name); scr_jb_puts(&b, "' has no native implementation; the supported ciphers are aes-128, aes-192 and " - "aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in cbc, ecb, " - "cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " - "chacha20-poly1305"); + "aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, cfb1 and ofb modes and their wrap, " + "wrap-pad, wrap-inv and wrap-pad-inv key wraps, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 " + "and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and chacha20-poly1305"); ScrStr *message = scr_jb_finish(&b); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); scr_str_release(message); @@ -618,7 +673,10 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * goto done; } size_t expected_iv = sc_iv_length(spec->mode, spec->des); - if (spec->mode == SC_GCM ? iv_len == 0 : iv_len != expected_iv || (expected_iv > 0 && !iv_bytes)) { + bool iv_ok = spec->mode == SC_GCM ? iv_len > 0 + : spec->mode == SC_CCM ? iv_len >= 7 && iv_len <= 13 + : iv_len == expected_iv && (expected_iv == 0 || iv_bytes); + if (!iv_ok) { sc_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_IV", "Invalid initialization vector"); goto done; } @@ -646,6 +704,24 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * sc_invalid_tag((unsigned long)tag_option); goto done; } + } else if (spec->mode == SC_CCM) { + /* InitAuthenticated: CCM needs the tag length, which OpenSSL takes when + * it is even and 4 to 16 bytes. */ + if (tag_option < 0) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "authTagLength required for "); + scr_jb_put_str(&b, cipher->v.str); + ScrStr *message = scr_jb_finish(&b); + scr_throw_error_msg_code(SCR_ERR_TYPE, message->data, message->len, "ERR_CRYPTO_INVALID_AUTH_TAG"); + scr_str_release(message); + goto done; + } + if (tag_option < 4 || tag_option > 16 || (unsigned long)tag_option % 2 != 0) { + sc_invalid_tag((unsigned long)tag_option); + goto done; + } + tag_len = (int)tag_option; } if (key_bytes->len != spec->key_len) { sc_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Invalid key length"); @@ -709,6 +785,29 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); memcpy(c->block, iv_bytes->data, 16); break; + case SC_WRAP: + case SC_WRAP_PAD: + /* SP 800-38F: wrapping is the forward transformation (encryption) and + * unwrapping the inverse, and the -inv ciphers swap the two. The IV is + * the wrap's integrity check value. */ + rc = decrypt != spec->inverse ? mbedtls_aes_setkey_dec(&c->aes, key_bytes->data, bits) + : mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + c->wrap_decrypt_block = decrypt != spec->inverse; + memcpy(c->block, iv_bytes->data, iv_len); + break; + case SC_CCM: { + /* CRYPTO_ccm128_init with M = the tag length and L = 15 - the IV + * length; Node caps the message at what a 12- or 13-byte IV's length + * field holds, and at INT_MAX otherwise. */ + rc = mbedtls_aes_setkey_enc(&c->aes, key_bytes->data, bits); + unsigned L = 15 - (unsigned)iv_len; + c->ccm_nonce[0] = (unsigned char)(((L - 1) & 7) | ((((unsigned)tag_len - 2) / 2) & 7) << 3); + memcpy(c->block, iv_bytes->data, iv_len); + c->ccm_iv_len = iv_len; + c->ccm_iv_set = true; + c->ccm_max_message = iv_len == 13 ? 65535 : iv_len == 12 ? 16777215 : 2147483647; + break; + } case SC_GCM: rc = mbedtls_gcm_setkey(&c->gcm, MBEDTLS_CIPHER_ID_AES, key_bytes->data, bits); if (rc == 0) @@ -970,7 +1069,12 @@ static bool sc_chacha_finish(ScCipher *c, unsigned char tag[16]) { /* The cipher output of an update over len bytes (+1 Buffer), or NULL * after a backend failure (for example AES-GCM's 2^36 - 32 byte message * limit) has retired the context and thrown Node's update error. */ +static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len); +static ScrBytes *sc_wrap_update(ScCipher *c, const unsigned char *in, size_t len); + static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) { + /* CCM and the key wraps, the last modes, take their own update. */ + if (c->mode >= SC_CCM) return c->mode == SC_CCM ? sc_ccm_update(c, in, len) : sc_wrap_update(c, in, len); size_t room = len + 16; ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)room); out->is_buffer = true; @@ -1006,6 +1110,10 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) ok = len == 0 || sc_chacha20_update(c, in, len, out->data); produced = len; break; + case SC_CCM: + case SC_WRAP: + case SC_WRAP_PAD: + break; } if (!ok) { mbedtls_platform_zeroize(out->data, room); @@ -1019,6 +1127,376 @@ static ScrBytes *sc_transform(ScCipher *c, const unsigned char *in, size_t len) return out; } +/* ── CCM ────────────────────────────────────────────────────────────── + * OpenSSL's ccm128 (crypto/modes/ccm128.c) over the AES block, step for + * step, with its provider's flags (ciphercommon_ccm.c) and Node's rules + * (one update, the message-length cap, a decipher's failure reported by + * final()), so every call sequence answers as Node's does. */ + +static void sc_ccm_block(ScCipher *c, const unsigned char in[16], unsigned char out[16]) { + mbedtls_aes_crypt_ecb(&c->aes, MBEDTLS_AES_ENCRYPT, in, out); +} + +/* CRYPTO_ccm128_setiv: the nonce and the message length. */ +static void sc_ccm_setiv(ScCipher *c, uint64_t mlen) { + unsigned L = c->ccm_nonce[0] & 7; + if (L >= 3) { + c->ccm_nonce[8] = (unsigned char)(mlen >> 56); + c->ccm_nonce[9] = (unsigned char)(mlen >> 48); + c->ccm_nonce[10] = (unsigned char)(mlen >> 40); + c->ccm_nonce[11] = (unsigned char)(mlen >> 32); + } else { + memset(c->ccm_nonce + 8, 0, 8); + } + c->ccm_nonce[12] = (unsigned char)(mlen >> 24); + c->ccm_nonce[13] = (unsigned char)(mlen >> 16); + c->ccm_nonce[14] = (unsigned char)(mlen >> 8); + c->ccm_nonce[15] = (unsigned char)mlen; + c->ccm_nonce[0] &= (unsigned char)~0x40; + memcpy(c->ccm_nonce + 1, c->block, 14 - L); +} + +/* CRYPTO_ccm128_aad. */ +static void sc_ccm_aad(ScCipher *c, const unsigned char *aad, size_t alen) { + if (alen == 0) return; + c->ccm_nonce[0] |= 0x40; + sc_ccm_block(c, c->ccm_nonce, c->ccm_cmac); + size_t i; + if (alen < 0x10000 - 0x100) { + c->ccm_cmac[0] ^= (unsigned char)(alen >> 8); + c->ccm_cmac[1] ^= (unsigned char)alen; + i = 2; + } else if ((uint64_t)alen >= (uint64_t)1 << 32) { + c->ccm_cmac[0] ^= 0xFF; + c->ccm_cmac[1] ^= 0xFF; + for (int k = 0; k < 8; k++) c->ccm_cmac[2 + k] ^= (unsigned char)((uint64_t)alen >> (56 - 8 * k)); + i = 10; + } else { + c->ccm_cmac[0] ^= 0xFF; + c->ccm_cmac[1] ^= 0xFE; + for (int k = 0; k < 4; k++) c->ccm_cmac[2 + k] ^= (unsigned char)((uint64_t)alen >> (24 - 8 * k)); + i = 6; + } + do { + for (; i < 16 && alen; ++i, ++aad, --alen) c->ccm_cmac[i] ^= *aad; + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + i = 0; + } while (alen); +} + +/* ctr64_inc: the counter block's low eight bytes, big-endian. */ +static void sc_ccm_ctr_inc(unsigned char *counter) { + for (int n = 15; n >= 8; n--) + if (++counter[n]) return; +} + +/* CRYPTO_ccm128_encrypt and CRYPTO_ccm128_decrypt: false (-1) for a + * length that differs from the one the counter block holds. */ +static bool sc_ccm_crypt(ScCipher *c, const unsigned char *in, unsigned char *out, size_t len, bool decrypt) { + unsigned char flags0 = c->ccm_nonce[0]; + unsigned char scratch[16]; + if (!(flags0 & 0x40)) sc_ccm_block(c, c->ccm_nonce, c->ccm_cmac); + unsigned L = flags0 & 7; + c->ccm_nonce[0] = (unsigned char)L; + uint64_t n = 0; + for (unsigned i = 15 - L; i < 15; ++i) { + n |= c->ccm_nonce[i]; + c->ccm_nonce[i] = 0; + n <<= 8; + } + n |= c->ccm_nonce[15]; + c->ccm_nonce[15] = 1; + if (n != (uint64_t)len) return false; + /* Each full block runs the counter block's encryption before the + * CBC-MAC's, in both directions, and XORs in 64-bit words, so the two + * AES operations of a block overlap. `out` may be `in`. */ + while (len >= 16) { + uint64_t data[2], mac[2], pad[2]; + sc_ccm_block(c, c->ccm_nonce, scratch); + sc_ccm_ctr_inc(c->ccm_nonce); + memcpy(data, in, 16); + memcpy(mac, c->ccm_cmac, 16); + memcpy(pad, scratch, 16); + pad[0] ^= data[0]; + pad[1] ^= data[1]; + /* The MAC covers the plaintext: the input when encrypting, the output + * when decrypting. */ + mac[0] ^= decrypt ? pad[0] : data[0]; + mac[1] ^= decrypt ? pad[1] : data[1]; + memcpy(out, pad, 16); + memcpy(c->ccm_cmac, mac, 16); + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + in += 16; + out += 16; + len -= 16; + } + if (len) { + if (decrypt) { + sc_ccm_block(c, c->ccm_nonce, scratch); + for (size_t i = 0; i < len; ++i) c->ccm_cmac[i] ^= (out[i] = (unsigned char)(scratch[i] ^ in[i])); + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + } else { + for (size_t i = 0; i < len; ++i) c->ccm_cmac[i] ^= in[i]; + sc_ccm_block(c, c->ccm_cmac, c->ccm_cmac); + sc_ccm_block(c, c->ccm_nonce, scratch); + for (size_t i = 0; i < len; ++i) out[i] = (unsigned char)(scratch[i] ^ in[i]); + } + } + for (unsigned i = 15 - L; i < 16; ++i) c->ccm_nonce[i] = 0; + sc_ccm_block(c, c->ccm_nonce, scratch); + for (int i = 0; i < 16; i++) c->ccm_cmac[i] ^= scratch[i]; + c->ccm_nonce[0] = flags0; + mbedtls_platform_zeroize(scratch, sizeof scratch); + return true; +} + +/* CRYPTO_ccm128_tag: the MAC, when the counter block still holds the + * tag length M (a failed call clears it). */ +static bool sc_ccm_tag(const ScCipher *c, unsigned char *tag) { + unsigned M = ((c->ccm_nonce[0] >> 3) & 7) * 2 + 2; + if (M != (unsigned)c->tag_len) return false; + memcpy(tag, c->ccm_cmac, M); + return true; +} + +/* The provider's ccm_cipher_internal for data (`out` given): the length + * is set by the first data unless setAAD set it; encryption marks the tag + * computed; decryption needs the tag and checks it, and on success clears + * the flags so a second call fails. */ +static bool sc_ccm_data(ScCipher *c, const unsigned char *in, unsigned char *out, size_t len) { + if (!c->ccm_iv_set) return false; + if (!c->ccm_len_set) { + sc_ccm_setiv(c, len); + c->ccm_len_set = true; + } + if (!c->decrypt) { + if (!sc_ccm_crypt(c, in, out, len, false)) return false; + c->ccm_tag_set = true; + return true; + } + if (!c->ccm_tag_set) return false; + unsigned char computed[16]; + bool ok = sc_ccm_crypt(c, in, out, len, true) && sc_ccm_tag(c, computed) && + sc_tag_equal(computed, c->tag, (size_t)c->tag_len); + mbedtls_platform_zeroize(computed, sizeof computed); + if (!ok) { + if (len) mbedtls_platform_zeroize(out, len); + return false; + } + c->ccm_iv_set = c->ccm_tag_set = c->ccm_len_set = false; + return true; +} + +static bool sc_ccm_length_ok(const ScCipher *c, double len) { + if (len <= c->ccm_max_message) return true; + sc_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_MESSAGELEN", "Invalid message length"); + return false; +} + +/* update(): a decipher takes one, and its authentication failure leaves + * empty output for final() to report. Node passes empty data to OpenSSL + * without a buffer, which the provider skips. */ +static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len) { + if (!sc_ccm_length_ok(c, (double)len)) return NULL; + if (c->decrypt && c->ccm_updated) { + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(len ? len : 1)); + out->is_buffer = true; + bool ok = len == 0 || sc_ccm_data(c, in, out->data, len); + if (c->decrypt) { + c->ccm_updated = true; + if (!ok) c->ccm_auth_failed = true; + out->len = ok ? len : 0; + return out; + } + if (!ok) { + mbedtls_platform_zeroize(out->data, len); + scr_bytes_release(out); + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + out->len = len; + return out; +} + +/* final(): a decipher reports the update's verdict; a cipher runs the + * provider's final (an empty message when no data was encrypted) and + * reads the tag, which retires the provider's state. */ +static bool sc_ccm_final(ScCipher *c) { + if (c->decrypt) return c->tag_state == SC_TAG_SET_BY_USER && c->ccm_updated && !c->ccm_auth_failed; + unsigned char dummy = 0, sink = 0; + if (c->ccm_iv_set && !c->ccm_tag_set && !sc_ccm_data(c, &dummy, &sink, 0)) return false; + if (!c->ccm_tag_set || !sc_ccm_tag(c, c->tag)) return false; + c->tag_state = SC_TAG_COMPUTED; + c->ccm_tag_set = c->ccm_iv_set = c->ccm_len_set = false; + return true; +} + +/* setAAD(buffer, { plaintextLength }): the length goes first, as Node + * passes it to OpenSSL, then the AAD. False when the provider refuses. */ +static bool sc_ccm_set_aad(ScCipher *c, const ScrBytes *aad, double plaintext_length) { + if (!c->ccm_iv_set) return false; + sc_ccm_setiv(c, (uint64_t)plaintext_length); + c->ccm_len_set = true; + sc_ccm_aad(c, aad->data, aad->len); + return true; +} + +/* ── key wrap ───────────────────────────────────────────────────────── + * OpenSSL's CRYPTO_128_wrap, CRYPTO_128_unwrap and their RFC 5649 + * padded forms (crypto/modes/wrap128.c) over the key's AES transform, and + * Node's one-update rule: a failed update() leaves the cipher usable, an + * empty one counts, and final() without one fails. */ + +static void sc_wrap_block(ScCipher *c, const unsigned char in[16], unsigned char out[16]) { + mbedtls_aes_crypt_ecb(&c->aes, c->wrap_decrypt_block ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, in, out); +} + +static void sc_wrap_xor_t(unsigned char *a, size_t t) { + a[7] ^= (unsigned char)(t & 0xff); + if (t > 0xff) { + a[6] ^= (unsigned char)((t >> 8) & 0xff); + a[5] ^= (unsigned char)((t >> 16) & 0xff); + a[4] ^= (unsigned char)((t >> 24) & 0xff); + } +} + +/* CRYPTO_128_wrap: 0 unless the input is at least 16 bytes in 8-byte + * blocks. `out` may be `in`. */ +static size_t sc_wrap(ScCipher *c, const unsigned char *iv, unsigned char *out, const unsigned char *in, size_t inlen) { + if ((inlen & 7) || inlen < 16) return 0; + unsigned char b[16]; + size_t t = 1; + memmove(out + 8, in, inlen); + memcpy(b, iv, 8); + for (int j = 0; j < 6; j++) { + unsigned char *r = out + 8; + for (size_t i = 0; i < inlen; i += 8, t++, r += 8) { + memcpy(b + 8, r, 8); + sc_wrap_block(c, b, b); + sc_wrap_xor_t(b, t); + memcpy(r, b + 8, 8); + } + } + memcpy(out, b, 8); + mbedtls_platform_zeroize(b, sizeof b); + return inlen + 8; +} + +/* crypto_128_unwrap_raw: the plaintext into `out` and the recovered + * integrity check value into `aiv`. */ +static size_t sc_unwrap_raw(ScCipher *c, unsigned char aiv[8], unsigned char *out, const unsigned char *in, size_t inlen) { + inlen -= 8; + if ((inlen & 7) || inlen < 16) return 0; + unsigned char b[16]; + size_t t = 6 * (inlen >> 3); + memcpy(b, in, 8); + memmove(out, in + 8, inlen); + for (int j = 0; j < 6; j++) { + unsigned char *r = out + inlen - 8; + for (size_t i = 0; i < inlen; i += 8, t--, r -= 8) { + sc_wrap_xor_t(b, t); + memcpy(b + 8, r, 8); + sc_wrap_block(c, b, b); + memcpy(r, b + 8, 8); + } + } + memcpy(aiv, b, 8); + mbedtls_platform_zeroize(b, sizeof b); + return inlen; +} + +static size_t sc_unwrap(ScCipher *c, const unsigned char *iv, unsigned char *out, const unsigned char *in, size_t inlen) { + unsigned char got[8]; + size_t ret = sc_unwrap_raw(c, got, out, in, inlen); + if (!ret) return 0; + if (!sc_tag_equal(got, iv, 8)) { + mbedtls_platform_zeroize(out, ret); + return 0; + } + return ret; +} + +/* CRYPTO_128_wrap_pad: the 4-byte ICV and the 32-bit length, then the + * data padded with zeros to 8 bytes; one block is a single encryption. */ +static size_t sc_wrap_pad(ScCipher *c, const unsigned char *icv, unsigned char *out, const unsigned char *in, size_t inlen) { + size_t padded = (inlen + 7) / 8 * 8; + if (inlen == 0 || inlen >= ((size_t)1 << 31)) return 0; + unsigned char aiv[8]; + memcpy(aiv, icv, 4); + aiv[4] = (unsigned char)((inlen >> 24) & 0xFF); + aiv[5] = (unsigned char)((inlen >> 16) & 0xFF); + aiv[6] = (unsigned char)((inlen >> 8) & 0xFF); + aiv[7] = (unsigned char)(inlen & 0xFF); + if (padded == 8) { + memmove(out + 8, in, inlen); + memcpy(out, aiv, 8); + memset(out + 8 + inlen, 0, padded - inlen); + sc_wrap_block(c, out, out); + return 16; + } + memmove(out, in, inlen); + memset(out + inlen, 0, padded - inlen); + return sc_wrap(c, aiv, out, out, padded); +} + +/* CRYPTO_128_unwrap_pad: the ICV, the length within the last 8 bytes and + * zero padding must all check. */ +static size_t sc_unwrap_pad(ScCipher *c, const unsigned char *icv, unsigned char *out, const unsigned char *in, size_t inlen) { + if ((inlen & 7) || inlen < 16) return 0; + size_t n = inlen / 8 - 1, padded; + unsigned char aiv[8]; + if (inlen == 16) { + unsigned char buff[16]; + sc_wrap_block(c, in, buff); + memcpy(aiv, buff, 8); + memcpy(out, buff + 8, 8); + padded = 8; + mbedtls_platform_zeroize(buff, sizeof buff); + } else { + padded = inlen - 8; + if (sc_unwrap_raw(c, aiv, out, in, inlen) != padded) { + mbedtls_platform_zeroize(out, inlen); + return 0; + } + } + size_t plain = (size_t)aiv[4] << 24 | (size_t)aiv[5] << 16 | (size_t)aiv[6] << 8 | aiv[7]; + static const unsigned char zeros[8] = {0}; + if (!sc_tag_equal(aiv, icv, 4) || 8 * (n - 1) >= plain || plain > 8 * n || + !sc_tag_equal(out + plain, zeros, padded - plain)) { + mbedtls_platform_zeroize(out, padded); + return 0; + } + return plain; +} + +static ScrBytes *sc_wrap_update(ScCipher *c, const unsigned char *in, size_t len) { + if (c->ccm_updated) { + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(len + 16)); + out->is_buffer = true; + size_t produced = 0; + if (len > 0) { + bool pad = c->mode == SC_WRAP_PAD; + produced = !c->decrypt ? (pad ? sc_wrap_pad(c, c->block, out->data, in, len) : sc_wrap(c, c->block, out->data, in, len)) + : (pad ? sc_unwrap_pad(c, c->block, out->data, in, len) + : sc_unwrap(c, c->block, out->data, in, len)); + if (produced == 0) { + mbedtls_platform_zeroize(out->data, len + 16); + scr_bytes_release(out); + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + } + c->ccm_updated = true; + out->len = produced; + return out; +} + /* ── output encodings ─────────────────────────────────────────────────── */ /* Node's getDecoder: the StringDecoder for this encoding, created on first @@ -1131,7 +1609,7 @@ static ScrDyn *sc_update(ScCipher *c, ScrDyn *data, ScrDyn *input_encoding, ScrD } static bool sc_aead(const ScCipher *c) { - return c->mode == SC_GCM || c->mode == SC_CHACHA; + return c->mode == SC_GCM || c->mode == SC_CHACHA || c->mode == SC_CCM; } /* final()'s output bytes (+1 Buffer), or NULL with the error thrown. */ @@ -1164,6 +1642,13 @@ static ScrBytes *sc_final_bytes(ScCipher *c) { case SC_OFB: case SC_CHACHA20: break; + case SC_CCM: + ok = sc_ccm_final(c); + break; + case SC_WRAP: + case SC_WRAP_PAD: + backend = c->ccm_updated; + break; case SC_GCM: { unsigned char full[16]; size_t ignored; @@ -1231,6 +1716,20 @@ static ScrDyn *sc_set_aad(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn *opt ScrBytes *aad = sc_buffer_or_view(buffer, "aadbuf", encoding); scr_dyn_release(encoding); if (!aad) return NULL; + if (c->mode == SC_CCM && c->active) { + bool ok = true; + if (plaintext_length < 0) { + sc_throw(SCR_ERR_TYPE, "ERR_MISSING_ARGS", "options.plaintextLength required for CCM mode with AAD"); + ok = false; + } else if (!sc_ccm_length_ok(c, plaintext_length)) { + ok = false; + } else if (!sc_ccm_set_aad(c, aad, plaintext_length)) { + sc_invalid_state("setAAD"); + ok = false; + } + scr_bytes_release(aad); + return ok ? scr_dyn_retain(self) : NULL; + } /* After data, GCM refuses any AAD and chacha20-poly1305 refuses * non-empty AAD. */ bool ok = c->active && sc_aead(c) && !(c->data_started && (c->mode == SC_GCM || aad->len > 0)); @@ -1314,6 +1813,7 @@ static ScrDyn *sc_set_auth_tag(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn c->tag_len = (int)len; memcpy(c->tag, tag->data, len); c->tag_state = SC_TAG_SET_BY_USER; + if (c->mode == SC_CCM) c->ccm_tag_set = true; scr_bytes_release(tag); return scr_dyn_retain(self); } diff --git a/packages/runtime/src/scr_cipher_info.c b/packages/runtime/src/scr_cipher_info.c new file mode 100644 index 000000000..8264f4e87 --- /dev/null +++ b/packages/runtime/src/scr_cipher_info.c @@ -0,0 +1,161 @@ +/* scriptc runtime — crypto.getCipherInfo(nameOrNid[, options]) over the + * table Node's OpenSSL ciphers generate (scr_cipher_info.h): cipher.js's + * argument checks, then GetCipherInfo's keyLength and ivLength rules and + * its info object, in Node's key order. Names match case-insensitively; + * nids are OpenSSL's. Arguments are borrowed. */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +#include "scr_cipher_info.h" + +#define CI_COUNT(table) (sizeof(table) / sizeof((table)[0])) + +/* validateInt32(value, name): false with Node's error pending. */ +static bool ci_validate_int32(const char *name, const ScrDyn *value) { + if (value->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail(name, "of type number", value); + return false; + } + double v = value->v.num; + bool integer = isfinite(v) && trunc(v) == v; + if (integer && v >= INT32_MIN && v <= INT32_MAX) return true; + char received[48]; + scr_num_received(v, received); + char text[192]; + int n = integer ? snprintf(text, sizeof text, + "The value of \"%s\" is out of range. It must be >= -2147483648 && <= 2147483647. " + "Received %s", + name, received) + : snprintf(text, sizeof text, + "The value of \"%s\" is out of range. It must be an integer. Received %s", name, + received); + scr_throw_error_msg_code(SCR_ERR_RANGE, text, n < 0 ? 0 : (size_t)n, "ERR_OUT_OF_RANGE"); + return false; +} + +/* A plain view of a statically typed argument (+1), or NULL after a + * throw. */ +static ScrDyn *ci_plain(ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(value); + return scr_dyn_retain(value); +} + +static const ScCipherInfo *ci_by_name(const ScrStr *name) { + char lower[64]; + if (name->len == 0 || name->len >= sizeof lower) return NULL; + for (size_t i = 0; i < name->len; i++) { + unsigned char ch = (unsigned char)name->data[i]; + lower[i] = (char)(ch >= 'A' && ch <= 'Z' ? ch + 32 : ch); + } + lower[name->len] = '\0'; + size_t lo = 0, hi = CI_COUNT(sc_cipher_info_names); + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + int cmp = strcmp(sc_cipher_info_names[mid].name, lower); + if (cmp == 0) return &sc_cipher_infos[sc_cipher_info_names[mid].info]; + if (cmp < 0) lo = mid + 1; + else hi = mid; + } + return NULL; +} + +static const ScCipherInfo *ci_by_nid(double nid) { + size_t lo = 0, hi = CI_COUNT(sc_cipher_info_nids); + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + double at = sc_cipher_info_nids[mid].nid; + if (at == nid) return &sc_cipher_infos[sc_cipher_info_nids[mid].info]; + if (at < nid) lo = mid + 1; + else hi = mid; + } + return NULL; +} + +/* GetCipherInfo's option rules: a cipher that takes neither option (the + * legacy provider's) answers undefined for either; keyLength must equal + * the cipher's; ivLength must equal its IV length, or lie in 7 to 13 for + * CCM and 1 to 15 for OCB, and GCM takes any. */ +static bool ci_accepts(const ScCipherInfo *info, bool has_key, int key, bool has_iv, int iv) { + if ((has_key || has_iv) && (info->key_rule == SC_INFO_NONE || info->iv_rule == SC_INFO_NONE)) return false; + if (has_key && key != info->key_length) return false; + if (!has_iv) return true; + switch (info->iv_rule) { + case SC_INFO_CCM: return iv >= 7 && iv <= 13; + case SC_INFO_OCB: return iv >= 1 && iv <= 15; + case SC_INFO_ANY: return true; + default: return iv == (info->iv_length < 0 ? 0 : info->iv_length); + } +} + +static void ci_set_str(ScrDyn *obj, const char *key, const char *text) { + ScrStr *s = scr_str_new(text, strlen(text)); + ScrDyn *v = scr_dyn_new_str(s); + scr_str_release(s); + scr_dyn_obj_set(obj, key, strlen(key), v); +} + +static void ci_set_num(ScrDyn *obj, const char *key, double value) { + scr_dyn_obj_set(obj, key, strlen(key), scr_dyn_new_num(value)); +} + +/* The info object: mode, name, nid, blockSize (not for stream ciphers), + * ivLength (not when it is 0) and keyLength. */ +static ScrDyn *ci_object(const ScCipherInfo *info, int iv) { + ScrDyn *obj = scr_dyn_new_obj(); + if (info->mode) ci_set_str(obj, "mode", info->mode); + ci_set_str(obj, "name", info->name); + if (info->nid >= 0) ci_set_num(obj, "nid", info->nid); + if (info->block_size >= 0) ci_set_num(obj, "blockSize", info->block_size); + if (iv > 0) ci_set_num(obj, "ivLength", iv); + ci_set_num(obj, "keyLength", info->key_length); + return obj; +} + +static ScrDyn *ci_get(const ScrDyn *name_or_nid, const ScrDyn *options) { + if (name_or_nid->kind != SCR_DYN_STR && name_or_nid->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("nameOrNid", "one of type string or number", name_or_nid); + return NULL; + } + if (name_or_nid->kind == SCR_DYN_NUM && !ci_validate_int32("nameOrNid", name_or_nid)) return NULL; + bool has_key = false, has_iv = false; + int key = 0, iv = 0; + if (options->kind != SCR_DYN_UNDEF) { + ScrStr *type = scr_dyn_typeof(options); + bool object = type->len == 6 && memcmp(type->data, "object", 6) == 0; + scr_str_release(type); + if (!object || options->kind == SCR_DYN_NULL || options->kind == SCR_DYN_ARR) { + scr_dyn_arg_type_fail("options", "of type object", options); + return NULL; + } + const ScrDyn *key_value = options->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(options, "keyLength", 9) : NULL; + const ScrDyn *iv_value = options->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(options, "ivLength", 8) : NULL; + if (key_value && key_value->kind != SCR_DYN_UNDEF) { + if (!ci_validate_int32("options.keyLength", key_value)) return NULL; + has_key = true; + key = (int)key_value->v.num; + } + if (iv_value && iv_value->kind != SCR_DYN_UNDEF) { + if (!ci_validate_int32("options.ivLength", iv_value)) return NULL; + has_iv = true; + iv = (int)iv_value->v.num; + } + } + const ScCipherInfo *info = + name_or_nid->kind == SCR_DYN_STR ? ci_by_name(name_or_nid->v.str) : ci_by_nid(name_or_nid->v.num); + if (!info || !ci_accepts(info, has_key, key, has_iv, iv)) return scr_dyn_retain((ScrDyn *)scr_dyn_undefined()); + return ci_object(info, has_iv ? iv : info->iv_length); +} + +ScrDyn *scr_crypto_get_cipher_info(ScrDyn *name_or_nid, ScrDyn *options) { + ScrDyn *name = ci_plain(name_or_nid); + if (!name) return NULL; + ScrDyn *opts = ci_plain(options); + ScrDyn *result = opts ? ci_get(name, opts) : NULL; + if (opts) scr_dyn_release(opts); + scr_dyn_release(name); + return result; +} diff --git a/packages/runtime/src/scr_cipher_info.h b/packages/runtime/src/scr_cipher_info.h new file mode 100644 index 000000000..edb18caea --- /dev/null +++ b/packages/runtime/src/scr_cipher_info.h @@ -0,0 +1,615 @@ +/* Generated by scripts/gen-cipher-info.mjs from Node v24.22.0's + * crypto.getCipherInfo: every cipher OpenSSL names, with the fields Node + * reports (-1 or NULL for a field it leaves out) and the keyLength and + * ivLength options each accepts. Do not edit. */ +#ifndef SCR_CIPHER_INFO_H +#define SCR_CIPHER_INFO_H + +enum { SC_INFO_NONE, SC_INFO_EXACT, SC_INFO_CCM, SC_INFO_OCB, SC_INFO_ANY }; + +typedef struct { + const char *mode; + const char *name; + int nid; + int block_size; + int iv_length; + int key_length; + unsigned char key_rule; + unsigned char iv_rule; +} ScCipherInfo; + +static const ScCipherInfo sc_cipher_infos[] = { + {"ecb", "camellia-128-ecb", 754, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "camellia-192-ecb", 755, 16, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "camellia-192-ofb", 767, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-192-cfb", 758, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "camellia-192-ctr", 967, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "camellia-128-ofb", 766, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-128-cfb", 757, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "camellia-256-ecb", 756, 16, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "camellia-256-ofb", 768, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-256-cfb", 759, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "camellia-256-ctr", 971, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "camellia-128-ctr", 963, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "sm4-ecb", 1133, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "sm4-cbc", 1134, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "sm4-ofb", 1135, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "sm4-cfb", 1137, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "sm4-ctr", 1139, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-128-cbc", 751, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-192-cbc", 752, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-256-cbc", 753, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aria-128-ecb", 1065, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aria-192-ctr", 1074, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aria-256-ecb", 1075, 16, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aria-256-cbc", 1076, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-256-cfb", 1077, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aria-256-ofb", 1078, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aria-256-ctr", 1079, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aria-128-cbc", 1066, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-128-cfb", 1067, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "aria-128-gcm", 1123, 1, 12, 16, SC_INFO_EXACT, SC_INFO_ANY}, + {"gcm", "aria-192-gcm", 1124, 1, 12, 24, SC_INFO_EXACT, SC_INFO_ANY}, + {"gcm", "aria-256-gcm", 1125, 1, 12, 32, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "aria-128-ccm", 1120, 1, 12, 16, SC_INFO_EXACT, SC_INFO_CCM}, + {"ccm", "aria-192-ccm", 1121, 1, 12, 24, SC_INFO_EXACT, SC_INFO_CCM}, + {"ccm", "aria-256-ccm", 1122, 1, 12, 32, SC_INFO_EXACT, SC_INFO_CCM}, + {"ofb", "aria-128-ofb", 1068, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aria-128-ctr", 1069, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aria-192-ecb", 1070, 16, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aria-192-cbc", 1071, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-192-cfb", 1072, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aria-192-ofb", 1073, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-smime-alg-cms3deswrap", 246, 8, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "des-ede3-cbc", 44, 8, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"xts", "aes-128-xts", 913, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"xts", "aes-256-xts", 914, 1, 16, 64, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "des-ede", 32, 8, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aes-128-ecb", 418, 16, -1, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-128-cbc", 419, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aes-192-ecb", 422, 16, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-192-cbc", 423, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aes-192-ofb", 424, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-192-cfb", 425, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-aes192-wrap", 789, 8, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "id-aes192-gcm", 898, 1, 12, 24, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "id-aes192-ccm", 899, 1, 12, 24, SC_INFO_EXACT, SC_INFO_CCM}, + {"wrap", "id-aes192-wrap-pad", 900, 8, 4, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aes-128-ofb", 420, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-128-cfb", 421, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "aes-256-ecb", 426, 16, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-256-cbc", 427, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "aes-256-ofb", 428, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-256-cfb", 429, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-aes256-wrap", 790, 8, 8, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "id-aes256-gcm", 901, 1, 12, 32, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "id-aes256-ccm", 902, 1, 12, 32, SC_INFO_EXACT, SC_INFO_CCM}, + {"wrap", "id-aes256-wrap-pad", 903, 8, 4, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "id-aes128-wrap", 788, 8, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm", "id-aes128-gcm", 895, 1, 12, 16, SC_INFO_EXACT, SC_INFO_ANY}, + {"ccm", "id-aes128-ccm", 896, 1, 12, 16, SC_INFO_EXACT, SC_INFO_CCM}, + {"wrap", "id-aes128-wrap-pad", 897, 8, 4, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-128-cbc-cts", -1, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-128-cfb1", 650, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-128-cfb8", 653, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aes-128-ctr", 904, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm-siv", "aes-128-gcm-siv", -1, 1, 12, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ocb", "aes-128-ocb", 958, 16, 12, 16, SC_INFO_EXACT, SC_INFO_OCB}, + {"siv", "aes-128-siv", -1, 1, -1, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-128-wrap-inv", -1, 8, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-128-wrap-pad-inv", -1, 8, 4, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-192-cbc-cts", -1, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-192-cfb1", 651, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-192-cfb8", 654, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aes-192-ctr", 905, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm-siv", "aes-192-gcm-siv", -1, 1, 12, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ocb", "aes-192-ocb", 959, 16, 12, 24, SC_INFO_EXACT, SC_INFO_OCB}, + {"siv", "aes-192-siv", -1, 1, -1, 48, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-192-wrap-inv", -1, 8, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-192-wrap-pad-inv", -1, 8, 4, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "aes-256-cbc-cts", -1, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-256-cfb1", 652, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aes-256-cfb8", 655, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ctr", "aes-256-ctr", 906, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"gcm-siv", "aes-256-gcm-siv", -1, 1, 12, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ocb", "aes-256-ocb", 960, 16, 12, 32, SC_INFO_EXACT, SC_INFO_OCB}, + {"siv", "aes-256-siv", -1, 1, -1, 64, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-256-wrap-inv", -1, 8, 8, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"wrap", "aes-256-wrap-pad-inv", -1, 8, 4, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-128-cfb1", 1080, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-128-cfb8", 1083, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-192-cfb1", 1081, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-192-cfb8", 1084, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-256-cfb1", 1082, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "aria-256-cfb8", 1085, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "bf-cbc", 91, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "bf-cfb", 93, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "bf-ecb", 92, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "bf-ofb", 94, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "camellia-128-cbc-cts", -1, 16, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-128-cfb1", 760, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-128-cfb8", 763, 1, 16, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-192-cbc-cts", -1, 16, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-192-cfb1", 761, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-192-cfb8", 764, 1, 16, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "camellia-256-cbc-cts", -1, 16, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-256-cfb1", 762, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "camellia-256-cfb8", 765, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "cast5-cbc", 108, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "cast5-cfb", 110, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "cast5-ecb", 109, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "cast5-ofb", 111, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "chacha20", 1019, -1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"stream", "chacha20-poly1305", 1018, -1, 12, 32, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cbc", "des-cbc", 31, 8, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "des-cfb", 30, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "des-cfb1", 656, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "des-cfb8", 657, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "des-ecb", 29, 8, -1, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "des-ede-cbc", 43, 8, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede-cfb", 60, 1, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "des-ede-ofb", 62, 1, 8, 16, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ecb", "des-ede3", 33, 8, -1, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede3-cfb", 61, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede3-cfb1", 658, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"cfb", "des-ede3-cfb8", 659, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "des-ede3-ofb", 63, 1, 8, 24, SC_INFO_EXACT, SC_INFO_EXACT}, + {"ofb", "des-ofb", 45, 1, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "desx-cbc", 80, 8, 8, 24, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "idea-cbc", 34, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "idea-cfb", 35, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "idea-ecb", 36, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "idea-ofb", 46, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "rc2-cbc", 37, 8, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "rc2-40-cbc", 98, 8, 8, 5, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "rc2-64-cbc", 166, 8, 8, 8, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "rc2-cfb", 39, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "rc2-ecb", 38, 8, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "rc2-ofb", 40, 1, 8, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "rc4", 5, -1, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "rc4-40", 97, -1, -1, 5, SC_INFO_NONE, SC_INFO_NONE}, + {"stream", "rc4-hmac-md5", 915, -1, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cbc", "seed-cbc", 777, 16, 16, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"cfb", "seed-cfb", 779, 1, 16, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ecb", "seed-ecb", 776, 16, -1, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ofb", "seed-ofb", 778, 1, 16, 16, SC_INFO_NONE, SC_INFO_NONE}, + {"ccm", "sm4-ccm", -1, 1, 12, 16, SC_INFO_EXACT, SC_INFO_CCM}, + {"gcm", "sm4-gcm", -1, 1, 12, 16, SC_INFO_EXACT, SC_INFO_ANY}, + {"xts", "sm4-xts", -1, 1, 16, 32, SC_INFO_EXACT, SC_INFO_EXACT}, +}; + +/* Lowercase names, sorted, with their record. */ +static const struct { + const char *name; + unsigned short info; +} sc_cipher_info_names[] = { + {"0.3.4401.5.3.1.9.1", 0}, + {"0.3.4401.5.3.1.9.21", 1}, + {"0.3.4401.5.3.1.9.23", 2}, + {"0.3.4401.5.3.1.9.24", 3}, + {"0.3.4401.5.3.1.9.29", 4}, + {"0.3.4401.5.3.1.9.3", 5}, + {"0.3.4401.5.3.1.9.4", 6}, + {"0.3.4401.5.3.1.9.41", 7}, + {"0.3.4401.5.3.1.9.43", 8}, + {"0.3.4401.5.3.1.9.44", 9}, + {"0.3.4401.5.3.1.9.49", 10}, + {"0.3.4401.5.3.1.9.9", 11}, + {"1.2.156.10197.1.104.1", 12}, + {"1.2.156.10197.1.104.2", 13}, + {"1.2.156.10197.1.104.3", 14}, + {"1.2.156.10197.1.104.4", 15}, + {"1.2.156.10197.1.104.7", 16}, + {"1.2.392.200011.61.1.1.1.2", 17}, + {"1.2.392.200011.61.1.1.1.3", 18}, + {"1.2.392.200011.61.1.1.1.4", 19}, + {"1.2.410.200046.1.1.1", 20}, + {"1.2.410.200046.1.1.10", 21}, + {"1.2.410.200046.1.1.11", 22}, + {"1.2.410.200046.1.1.12", 23}, + {"1.2.410.200046.1.1.13", 24}, + {"1.2.410.200046.1.1.14", 25}, + {"1.2.410.200046.1.1.15", 26}, + {"1.2.410.200046.1.1.2", 27}, + {"1.2.410.200046.1.1.3", 28}, + {"1.2.410.200046.1.1.34", 29}, + {"1.2.410.200046.1.1.35", 30}, + {"1.2.410.200046.1.1.36", 31}, + {"1.2.410.200046.1.1.37", 32}, + {"1.2.410.200046.1.1.38", 33}, + {"1.2.410.200046.1.1.39", 34}, + {"1.2.410.200046.1.1.4", 35}, + {"1.2.410.200046.1.1.5", 36}, + {"1.2.410.200046.1.1.6", 37}, + {"1.2.410.200046.1.1.7", 38}, + {"1.2.410.200046.1.1.8", 39}, + {"1.2.410.200046.1.1.9", 40}, + {"1.2.840.113549.1.9.16.3.6", 41}, + {"1.2.840.113549.3.7", 42}, + {"1.3.111.2.1619.0.1.1", 43}, + {"1.3.111.2.1619.0.1.2", 44}, + {"1.3.14.3.2.17", 45}, + {"2.16.840.1.101.3.4.1.1", 46}, + {"2.16.840.1.101.3.4.1.2", 47}, + {"2.16.840.1.101.3.4.1.21", 48}, + {"2.16.840.1.101.3.4.1.22", 49}, + {"2.16.840.1.101.3.4.1.23", 50}, + {"2.16.840.1.101.3.4.1.24", 51}, + {"2.16.840.1.101.3.4.1.25", 52}, + {"2.16.840.1.101.3.4.1.26", 53}, + {"2.16.840.1.101.3.4.1.27", 54}, + {"2.16.840.1.101.3.4.1.28", 55}, + {"2.16.840.1.101.3.4.1.3", 56}, + {"2.16.840.1.101.3.4.1.4", 57}, + {"2.16.840.1.101.3.4.1.41", 58}, + {"2.16.840.1.101.3.4.1.42", 59}, + {"2.16.840.1.101.3.4.1.43", 60}, + {"2.16.840.1.101.3.4.1.44", 61}, + {"2.16.840.1.101.3.4.1.45", 62}, + {"2.16.840.1.101.3.4.1.46", 63}, + {"2.16.840.1.101.3.4.1.47", 64}, + {"2.16.840.1.101.3.4.1.48", 65}, + {"2.16.840.1.101.3.4.1.5", 66}, + {"2.16.840.1.101.3.4.1.6", 67}, + {"2.16.840.1.101.3.4.1.7", 68}, + {"2.16.840.1.101.3.4.1.8", 69}, + {"aes-128-cbc", 47}, + {"aes-128-cbc-cts", 70}, + {"aes-128-ccm", 68}, + {"aes-128-cfb", 57}, + {"aes-128-cfb1", 71}, + {"aes-128-cfb8", 72}, + {"aes-128-ctr", 73}, + {"aes-128-ecb", 46}, + {"aes-128-gcm", 67}, + {"aes-128-gcm-siv", 74}, + {"aes-128-ocb", 75}, + {"aes-128-ofb", 56}, + {"aes-128-siv", 76}, + {"aes-128-wrap", 66}, + {"aes-128-wrap-inv", 77}, + {"aes-128-wrap-pad", 69}, + {"aes-128-wrap-pad-inv", 78}, + {"aes-128-xts", 43}, + {"aes-192-cbc", 49}, + {"aes-192-cbc-cts", 79}, + {"aes-192-ccm", 54}, + {"aes-192-cfb", 51}, + {"aes-192-cfb1", 80}, + {"aes-192-cfb8", 81}, + {"aes-192-ctr", 82}, + {"aes-192-ecb", 48}, + {"aes-192-gcm", 53}, + {"aes-192-gcm-siv", 83}, + {"aes-192-ocb", 84}, + {"aes-192-ofb", 50}, + {"aes-192-siv", 85}, + {"aes-192-wrap", 52}, + {"aes-192-wrap-inv", 86}, + {"aes-192-wrap-pad", 55}, + {"aes-192-wrap-pad-inv", 87}, + {"aes-256-cbc", 59}, + {"aes-256-cbc-cts", 88}, + {"aes-256-ccm", 64}, + {"aes-256-cfb", 61}, + {"aes-256-cfb1", 89}, + {"aes-256-cfb8", 90}, + {"aes-256-ctr", 91}, + {"aes-256-ecb", 58}, + {"aes-256-gcm", 63}, + {"aes-256-gcm-siv", 92}, + {"aes-256-ocb", 93}, + {"aes-256-ofb", 60}, + {"aes-256-siv", 94}, + {"aes-256-wrap", 62}, + {"aes-256-wrap-inv", 95}, + {"aes-256-wrap-pad", 65}, + {"aes-256-wrap-pad-inv", 96}, + {"aes-256-xts", 44}, + {"aes128", 47}, + {"aes128-wrap", 66}, + {"aes128-wrap-inv", 77}, + {"aes128-wrap-pad", 69}, + {"aes128-wrap-pad-inv", 78}, + {"aes192", 49}, + {"aes192-wrap", 52}, + {"aes192-wrap-inv", 86}, + {"aes192-wrap-pad", 55}, + {"aes192-wrap-pad-inv", 87}, + {"aes256", 59}, + {"aes256-wrap", 62}, + {"aes256-wrap-inv", 95}, + {"aes256-wrap-pad", 65}, + {"aes256-wrap-pad-inv", 96}, + {"aria-128-cbc", 27}, + {"aria-128-ccm", 32}, + {"aria-128-cfb", 28}, + {"aria-128-cfb1", 97}, + {"aria-128-cfb8", 98}, + {"aria-128-ctr", 36}, + {"aria-128-ecb", 20}, + {"aria-128-gcm", 29}, + {"aria-128-ofb", 35}, + {"aria-192-cbc", 38}, + {"aria-192-ccm", 33}, + {"aria-192-cfb", 39}, + {"aria-192-cfb1", 99}, + {"aria-192-cfb8", 100}, + {"aria-192-ctr", 21}, + {"aria-192-ecb", 37}, + {"aria-192-gcm", 30}, + {"aria-192-ofb", 40}, + {"aria-256-cbc", 23}, + {"aria-256-ccm", 34}, + {"aria-256-cfb", 24}, + {"aria-256-cfb1", 101}, + {"aria-256-cfb8", 102}, + {"aria-256-ctr", 26}, + {"aria-256-ecb", 22}, + {"aria-256-gcm", 31}, + {"aria-256-ofb", 25}, + {"aria128", 27}, + {"aria192", 38}, + {"aria256", 23}, + {"bf", 103}, + {"bf-cbc", 103}, + {"bf-cfb", 104}, + {"bf-ecb", 105}, + {"bf-ofb", 106}, + {"blowfish", 103}, + {"camellia-128-cbc", 17}, + {"camellia-128-cbc-cts", 107}, + {"camellia-128-cfb", 6}, + {"camellia-128-cfb1", 108}, + {"camellia-128-cfb8", 109}, + {"camellia-128-ctr", 11}, + {"camellia-128-ecb", 0}, + {"camellia-128-ofb", 5}, + {"camellia-192-cbc", 18}, + {"camellia-192-cbc-cts", 110}, + {"camellia-192-cfb", 3}, + {"camellia-192-cfb1", 111}, + {"camellia-192-cfb8", 112}, + {"camellia-192-ctr", 4}, + {"camellia-192-ecb", 1}, + {"camellia-192-ofb", 2}, + {"camellia-256-cbc", 19}, + {"camellia-256-cbc-cts", 113}, + {"camellia-256-cfb", 9}, + {"camellia-256-cfb1", 114}, + {"camellia-256-cfb8", 115}, + {"camellia-256-ctr", 10}, + {"camellia-256-ecb", 7}, + {"camellia-256-ofb", 8}, + {"camellia128", 17}, + {"camellia192", 18}, + {"camellia256", 19}, + {"cast", 116}, + {"cast-cbc", 116}, + {"cast5-cbc", 116}, + {"cast5-cfb", 117}, + {"cast5-ecb", 118}, + {"cast5-ofb", 119}, + {"chacha20", 120}, + {"chacha20-poly1305", 121}, + {"des", 122}, + {"des-cbc", 122}, + {"des-cfb", 123}, + {"des-cfb1", 124}, + {"des-cfb8", 125}, + {"des-ecb", 126}, + {"des-ede", 45}, + {"des-ede-cbc", 127}, + {"des-ede-cfb", 128}, + {"des-ede-ecb", 45}, + {"des-ede-ofb", 129}, + {"des-ede3", 130}, + {"des-ede3-cbc", 42}, + {"des-ede3-cfb", 131}, + {"des-ede3-cfb1", 132}, + {"des-ede3-cfb8", 133}, + {"des-ede3-ecb", 130}, + {"des-ede3-ofb", 134}, + {"des-ofb", 135}, + {"des3", 42}, + {"des3-wrap", 41}, + {"desx", 136}, + {"desx-cbc", 136}, + {"id-aes128-ccm", 68}, + {"id-aes128-gcm", 67}, + {"id-aes128-wrap", 66}, + {"id-aes128-wrap-pad", 69}, + {"id-aes192-ccm", 54}, + {"id-aes192-gcm", 53}, + {"id-aes192-wrap", 52}, + {"id-aes192-wrap-pad", 55}, + {"id-aes256-ccm", 64}, + {"id-aes256-gcm", 63}, + {"id-aes256-wrap", 62}, + {"id-aes256-wrap-pad", 65}, + {"id-smime-alg-cms3deswrap", 41}, + {"idea-cbc", 137}, + {"idea-cfb", 138}, + {"idea-ecb", 139}, + {"idea-ofb", 140}, + {"rc2", 141}, + {"rc2-128", 141}, + {"rc2-40", 142}, + {"rc2-40-cbc", 142}, + {"rc2-64", 143}, + {"rc2-64-cbc", 143}, + {"rc2-cbc", 141}, + {"rc2-cfb", 144}, + {"rc2-ecb", 145}, + {"rc2-ofb", 146}, + {"rc4", 147}, + {"rc4-40", 148}, + {"rc4-hmac-md5", 149}, + {"seed", 150}, + {"seed-cbc", 150}, + {"seed-cfb", 151}, + {"seed-ecb", 152}, + {"seed-ofb", 153}, + {"sm4", 13}, + {"sm4-cbc", 13}, + {"sm4-ccm", 154}, + {"sm4-cfb", 15}, + {"sm4-cfb128", 15}, + {"sm4-ctr", 16}, + {"sm4-ecb", 12}, + {"sm4-gcm", 155}, + {"sm4-ofb", 14}, + {"sm4-ofb128", 14}, + {"sm4-xts", 156}, +}; + +/* Nids, ascending, with their record. */ +static const struct { + unsigned short nid; + unsigned short info; +} sc_cipher_info_nids[] = { + {5, 147}, + {29, 126}, + {30, 123}, + {31, 122}, + {32, 45}, + {33, 130}, + {34, 137}, + {35, 138}, + {36, 139}, + {37, 141}, + {38, 145}, + {39, 144}, + {40, 146}, + {43, 127}, + {44, 42}, + {45, 135}, + {46, 140}, + {60, 128}, + {61, 131}, + {62, 129}, + {63, 134}, + {80, 136}, + {91, 103}, + {92, 105}, + {93, 104}, + {94, 106}, + {97, 148}, + {98, 142}, + {108, 116}, + {109, 118}, + {110, 117}, + {111, 119}, + {166, 143}, + {246, 41}, + {418, 46}, + {419, 47}, + {420, 56}, + {421, 57}, + {422, 48}, + {423, 49}, + {424, 50}, + {425, 51}, + {426, 58}, + {427, 59}, + {428, 60}, + {429, 61}, + {650, 71}, + {651, 80}, + {652, 89}, + {653, 72}, + {654, 81}, + {655, 90}, + {656, 124}, + {657, 125}, + {658, 132}, + {659, 133}, + {751, 17}, + {752, 18}, + {753, 19}, + {754, 0}, + {755, 1}, + {756, 7}, + {757, 6}, + {758, 3}, + {759, 9}, + {760, 108}, + {761, 111}, + {762, 114}, + {763, 109}, + {764, 112}, + {765, 115}, + {766, 5}, + {767, 2}, + {768, 8}, + {776, 152}, + {777, 150}, + {778, 153}, + {779, 151}, + {788, 66}, + {789, 52}, + {790, 62}, + {895, 67}, + {896, 68}, + {897, 69}, + {898, 53}, + {899, 54}, + {900, 55}, + {901, 63}, + {902, 64}, + {903, 65}, + {904, 73}, + {905, 82}, + {906, 91}, + {913, 43}, + {914, 44}, + {915, 149}, + {958, 75}, + {959, 84}, + {960, 93}, + {963, 11}, + {967, 4}, + {971, 10}, + {1018, 121}, + {1019, 120}, + {1065, 20}, + {1066, 27}, + {1067, 28}, + {1068, 35}, + {1069, 36}, + {1070, 37}, + {1071, 38}, + {1072, 39}, + {1073, 40}, + {1074, 21}, + {1075, 22}, + {1076, 23}, + {1077, 24}, + {1078, 25}, + {1079, 26}, + {1080, 97}, + {1081, 99}, + {1082, 101}, + {1083, 98}, + {1084, 100}, + {1085, 102}, + {1120, 32}, + {1121, 33}, + {1122, 34}, + {1123, 29}, + {1124, 30}, + {1125, 31}, + {1133, 12}, + {1134, 13}, + {1135, 14}, + {1137, 15}, + {1139, 16}, + {1198, 76}, + {1199, 85}, + {1200, 94}, + {1248, 155}, + {1249, 154}, + {1290, 156}, +}; + +#endif diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 0e6c6e07f..f61d27406 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -108,6 +108,9 @@ ScrDyn *scr_crypto_ecdh_convert_key(ScrDyn *key, ScrDyn *curve, ScrDyn *in_enc, * Node's error pending. */ ScrDyn *scr_crypto_dh_new(ScrDyn *size_or_key, ScrDyn *key_encoding, ScrDyn *generator, ScrDyn *generator_encoding); ScrDyn *scr_crypto_dh_group_new(ScrDyn *name); +/* crypto.getCipherInfo(nameOrNid, options) (scr_cipher_info.c): the info + * object or undefined (+1), or NULL with Node's argument error pending. */ +ScrDyn *scr_crypto_get_cipher_info(ScrDyn *name_or_nid, ScrDyn *options); /* crypto.checkPrimeSync, checkPrime, generatePrimeSync and generatePrime * (scr_dh.c): checked-dynamic arguments, borrowed; the sync forms answer * +1 or NULL with the error pending. The callbacks receive undefined as diff --git a/scripts/gen-cipher-info.mjs b/scripts/gen-cipher-info.mjs new file mode 100644 index 000000000..06bd8bce3 --- /dev/null +++ b/scripts/gen-cipher-info.mjs @@ -0,0 +1,125 @@ +// Writes packages/runtime/src/scr_cipher_info.h from the running Node's +// crypto.getCipherInfo: one record per cipher, the names (lowercase) and nids +// that find it, and the keyLength and ivLength options each accepts. The +// candidate names are getCiphers() and `openssl list -cipher-algorithms` +// (OpenSSL's aliases and its legacy-provider ciphers, which getCiphers() +// leaves out). Run with the pinned Node: node scripts/gen-cipher-info.mjs +import crypto from "node:crypto"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; + +const listed = execFileSync("openssl", ["list", "-cipher-algorithms"], { encoding: "utf8" }) + .match(/[A-Za-z0-9][A-Za-z0-9._-]*/g) + .filter((word) => !["Legacy", "Provided", "default", "legacy", "base", "fips"].includes(word)); +const candidates = new Set([...crypto.getCiphers(), ...listed]); +for (let nid = 1; nid < 1400; nid++) { + const info = crypto.getCipherInfo(nid); + if (info) candidates.add(info.name); +} + +function ivRule(name, info) { + const accepted = []; + for (let v = 0; v <= 70; v++) if (crypto.getCipherInfo(name, { ivLength: v })) accepted.push(v); + const large = !!crypto.getCipherInfo(name, { ivLength: 100000 }); + const own = info.ivLength ?? 0; + if (accepted.length === 0) return "SC_INFO_NONE"; + if (accepted.length === 1 && accepted[0] === own && !large) return "SC_INFO_EXACT"; + if (accepted.join() === "7,8,9,10,11,12,13" && !large) return "SC_INFO_CCM"; + if (accepted.join() === [...Array(15)].map((_, i) => i + 1).join() && !large) + return "SC_INFO_OCB"; + if (accepted.length === 71 && large) return "SC_INFO_ANY"; + throw new Error(`unclassified ivLength rule for ${name}: ${accepted}`); +} +function keyRule(name, info) { + const accepted = []; + for (let k = 0; k <= 80; k++) if (crypto.getCipherInfo(name, { keyLength: k })) accepted.push(k); + if (accepted.length === 0) return "SC_INFO_NONE"; + if (accepted.length === 1 && accepted[0] === info.keyLength) return "SC_INFO_EXACT"; + throw new Error(`unclassified keyLength rule for ${name}: ${accepted}`); +} + +const records = []; +const recordIndex = new Map(); +function recordOf(name, info) { + const key = JSON.stringify(info); + let index = recordIndex.get(key); + const rules = [keyRule(name, info), ivRule(name, info)]; + if (index === undefined) { + index = records.length; + records.push({ info, rules }); + recordIndex.set(key, index); + } else if (records[index].rules.join() !== rules.join()) { + throw new Error(`rules differ for ${name}`); + } + return index; +} + +const names = []; +for (const name of [...candidates].map((n) => n.toLowerCase()).sort()) { + if (names.length && names.at(-1)[0] === name) continue; + const info = crypto.getCipherInfo(name); + if (info) names.push([name, recordOf(name, info)]); +} +const nids = []; +for (let nid = 1; nid < 1400; nid++) { + const info = crypto.getCipherInfo(nid); + if (info) nids.push([nid, recordOf(info.name, info)]); +} + +const str = (s) => (s === undefined ? "NULL" : JSON.stringify(s)); +const num = (n) => (n === undefined ? -1 : n); +let out = `/* Generated by scripts/gen-cipher-info.mjs from Node ${process.version}'s + * crypto.getCipherInfo: every cipher OpenSSL names, with the fields Node + * reports (-1 or NULL for a field it leaves out) and the keyLength and + * ivLength options each accepts. Do not edit. */ +#ifndef SCR_CIPHER_INFO_H +#define SCR_CIPHER_INFO_H + +enum { SC_INFO_NONE, SC_INFO_EXACT, SC_INFO_CCM, SC_INFO_OCB, SC_INFO_ANY }; + +typedef struct { + const char *mode; + const char *name; + int nid; + int block_size; + int iv_length; + int key_length; + unsigned char key_rule; + unsigned char iv_rule; +} ScCipherInfo; + +static const ScCipherInfo sc_cipher_infos[] = { +`; +for (const { info, rules } of records) + out += ` {${str(info.mode)}, ${str(info.name)}, ${num(info.nid)}, ${num(info.blockSize)}, ${num(info.ivLength)}, ${info.keyLength}, ${rules[0]}, ${rules[1]}},\n`; +out += `}; + +/* Lowercase names, sorted, with their record. */ +static const struct { + const char *name; + unsigned short info; +} sc_cipher_info_names[] = { +`; +for (const [name, index] of names) out += ` {${JSON.stringify(name)}, ${index}},\n`; +out += `}; + +/* Nids, ascending, with their record. */ +static const struct { + unsigned short nid; + unsigned short info; +} sc_cipher_info_nids[] = { +`; +for (const [nid, index] of nids) out += ` {${nid}, ${index}},\n`; +out += `}; + +#endif +`; +for (const { info } of records) { + const keys = Object.keys(info).join(); + const expected = ["mode", "name", "nid", "blockSize", "ivLength", "keyLength"] + .filter((k) => k in info) + .join(); + if (keys !== expected) throw new Error(`unexpected key order ${keys}`); +} +fs.writeFileSync(new URL("../packages/runtime/src/scr_cipher_info.h", import.meta.url), out); +console.log(records.length, "records,", names.length, "names,", nids.length, "nids"); diff --git a/tests/corpus/7154-crypto-cipher-info.cjs b/tests/corpus/7154-crypto-cipher-info.cjs new file mode 100644 index 000000000..95e72f71d --- /dev/null +++ b/tests/corpus/7154-crypto-cipher-info.cjs @@ -0,0 +1,57 @@ +// crypto.getCipherInfo: every getCiphers() name, OpenSSL's aliases in any +// case and its legacy-provider ciphers, lookups by nid, the keyLength and +// ivLength options (exact lengths, CCM's 7 to 13 bytes, OCB's 1 to 15 and +// any length for GCM; the legacy ciphers answer undefined for either), +// and Node's argument errors. +const crypto = require("crypto"); + +for (const name of crypto.getCiphers()) console.log(name, JSON.stringify(crypto.getCipherInfo(name))); +for (const name of ["AES-128-CBC", "Aes256", "id-aes128-CCM", "chacha20-POLY1305", "des", "bf", "bf-cbc", "rc4", "seed-cbc", "cast5-cbc", "rc2-cbc", "idea-cbc", "des-ecb", "desx", "nope", ""]) + console.log(JSON.stringify(name), JSON.stringify(crypto.getCipherInfo(name))); +for (const nid of [0, -1, 1, 5, 31, 91, 419, 427, 895, 896, 901, 1018, 1198, 1248, 99999]) + console.log("nid", nid, JSON.stringify(crypto.getCipherInfo(nid))); + +const options = [ + { keyLength: 16 }, + { keyLength: 32 }, + { ivLength: 16 }, + { ivLength: 12 }, + { ivLength: 7 }, + { ivLength: 13 }, + { ivLength: 14 }, + { ivLength: 0 }, + { ivLength: 1 }, + { ivLength: 15 }, + { ivLength: 1000 }, + { keyLength: 16, ivLength: 12 }, + { keyLength: undefined, ivLength: undefined }, + {}, +]; +for (const name of ["aes-128-cbc", "aes-128-gcm", "aes-128-ccm", "aes-128-ocb", "aes-128-ecb", "chacha20-poly1305", "aes-128-wrap-pad", "bf-cbc", "rc4"]) + for (const option of options) console.log(name, JSON.stringify(option), JSON.stringify(crypto.getCipherInfo(name, option))); + +function attempt(label, run) { + try { + console.log(label, "ok", JSON.stringify(run())); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +attempt("no argument", () => crypto.getCipherInfo()); +attempt("null", () => crypto.getCipherInfo(null)); +attempt("boolean", () => crypto.getCipherInfo(true)); +attempt("object", () => crypto.getCipherInfo({})); +attempt("fraction", () => crypto.getCipherInfo(1.5)); +attempt("NaN", () => crypto.getCipherInfo(NaN)); +attempt("2^31", () => crypto.getCipherInfo(2 ** 31)); +attempt("options null", () => crypto.getCipherInfo("aes-128-cbc", null)); +attempt("options number", () => crypto.getCipherInfo("aes-128-cbc", 5)); +attempt("options string", () => crypto.getCipherInfo("aes-128-cbc", "x")); +attempt("options array", () => crypto.getCipherInfo("aes-128-cbc", [])); +attempt("keyLength string", () => crypto.getCipherInfo("aes-128-cbc", { keyLength: "16" })); +attempt("keyLength fraction", () => crypto.getCipherInfo("aes-128-cbc", { keyLength: 16.5 })); +attempt("ivLength 2^31", () => crypto.getCipherInfo("aes-128-cbc", { ivLength: 2 ** 31 })); +attempt("ivLength null", () => crypto.getCipherInfo("aes-128-cbc", { ivLength: null })); +attempt("unknown name with bad options", () => crypto.getCipherInfo("nope", { keyLength: "x" })); +const info = crypto.getCipherInfo("aes-256-gcm", { ivLength: 7 }); +console.log(Object.keys(info).join(), Object.getPrototypeOf(info) === Object.prototype); diff --git a/tests/corpus/7155-cipher-aes-ccm.cjs b/tests/corpus/7155-cipher-aes-ccm.cjs new file mode 100644 index 000000000..2ed95c382 --- /dev/null +++ b/tests/corpus/7155-cipher-aes-ccm.cjs @@ -0,0 +1,158 @@ +// AES-CCM through createCipheriv and createDecipheriv: RFC 3610 and NIST +// SP 800-38C vectors, every tag and IV length, AAD with plaintextLength, +// the message-length caps of 12- and 13-byte IVs, a decipher's failed +// authentication reported by final(), the one-update rule, the AAD and +// tag calls in each state, and as a stream. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const v = run(); + console.log(label, "ok", Buffer.isBuffer(v) ? v.toString("hex") : v); + } catch (e) { + console.log(label, e.name, e.code, JSON.stringify(e.message), JSON.stringify(Object.keys(e))); + } +} +const hex = (s) => Buffer.from(s, "hex"); + +// NIST SP 800-38C examples C.1 to C.3. +const nistKey = hex("404142434445464748494a4b4c4d4e4f"); +for (const [iv, aad, pt, tagLen] of [ + ["10111213141516", "0001020304050607", "20212223", 4], + ["1011121314151617", "000102030405060708090a0b0c0d0e0f", "202122232425262728292a2b2c2d2e2f", 6], + ["101112131415161718191a1b", "000102030405060708090a0b0c0d0e0f10111213", "202122232425262728292a2b2c2d2e2f3031323334353637", 8], +]) { + attempt(`nist iv ${iv.length / 2}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", nistKey, hex(iv), { authTagLength: tagLen }); + c.setAAD(hex(aad), { plaintextLength: pt.length / 2 }); + return Buffer.concat([c.update(hex(pt)), c.final(), c.getAuthTag()]); + }); +} +// RFC 3610 packet vector 1. +attempt("rfc3610 #1", () => { + const c = crypto.createCipheriv("aes-128-ccm", hex("c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"), hex("00000003020100a0a1a2a3a4a5"), { authTagLength: 8 }); + c.setAAD(hex("0001020304050607"), { plaintextLength: 23 }); + return Buffer.concat([c.update(hex("08090a0b0c0d0e0f101112131415161718191a1b1c1d1e")), c.final(), c.getAuthTag()]); +}); + +const key = Buffer.alloc(16, 1); +const pt = Buffer.from("hello world, ccm mode, three blocks of it!"); +for (const size of [16, 24, 32]) + for (const name of [`aes-${size * 8}-ccm`, `id-aes${size * 8}-ccm`, `AES-${size * 8}-CCM`]) + attempt(name, () => { + const c = crypto.createCipheriv(name, Buffer.alloc(size, 3), Buffer.alloc(12, 2), { authTagLength: 10 }); + return Buffer.concat([c.update(pt), c.final(), c.getAuthTag()]); + }); +for (const tagLen of [2, 3, 4, 5, 6, 8, 10, 12, 14, 15, 16, 18]) + attempt(`tag ${tagLen}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12), { authTagLength: tagLen }); + return Buffer.concat([c.update(pt), c.final(), c.getAuthTag()]); + }); +for (const ivLen of [0, 6, 7, 8, 11, 12, 13, 14]) + attempt(`iv ${ivLen}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(ivLen, 9), { authTagLength: 16 }); + return Buffer.concat([c.update(pt), c.final(), c.getAuthTag()]); + }); +attempt("no tag length", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12))); +attempt("no tag length, mixed case", () => crypto.createDecipheriv("Aes-128-Ccm", key, Buffer.alloc(12))); +attempt("tag length string", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12), { authTagLength: "16" })); +attempt("bad iv and no tag length", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(5))); +attempt("bad key", () => crypto.createCipheriv("aes-128-ccm", Buffer.alloc(15), Buffer.alloc(12), { authTagLength: 16 })); + +const iv = Buffer.alloc(12, 2), aad = Buffer.from("header"); +function seal(steps) { + const c = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + const parts = []; + for (const step of steps) { + try { + const out = step(c); + if (Buffer.isBuffer(out)) parts.push(out.toString("hex")); + } catch (e) { + parts.push(`<${e.code ?? e.name}: ${e.message}>`); + } + } + try { + parts.push("tag " + c.getAuthTag().toString("hex")); + } catch (e) { + parts.push(`<${e.code}: ${e.message}>`); + } + return parts.join(" "); +} +const update = (data) => (c) => c.update(data); +const final = () => (c) => c.final(); +const setAAD = (data, options) => (c) => (c.setAAD(data, options), Buffer.alloc(0)); +console.log("plain", seal([update(pt), final()])); +console.log("aad", seal([setAAD(aad, { plaintextLength: pt.length }), update(pt), final()])); +console.log("aad without length", seal([setAAD(aad), update(pt), final()])); +console.log("aad bad length", seal([setAAD(aad, { plaintextLength: -1 }), update(pt), final()])); +console.log("aad length mismatch", seal([setAAD(aad, { plaintextLength: 5 }), update(pt), final()])); +console.log("aad twice", seal([setAAD(aad, { plaintextLength: pt.length }), setAAD(aad, { plaintextLength: pt.length }), update(pt), final()])); +console.log("empty aad", seal([setAAD(Buffer.alloc(0), { plaintextLength: pt.length }), update(pt), final()])); +console.log("aad after update", seal([update(pt), setAAD(aad, { plaintextLength: 3 }), final()])); +console.log("update twice", seal([update(pt), update(pt), final()])); +console.log("update empty twice", seal([update(Buffer.alloc(0)), update(Buffer.alloc(0)), final()])); +console.log("no update", seal([final()])); +console.log("aad, no update", seal([setAAD(aad, { plaintextLength: pt.length }), final()])); +console.log("aad length 0, no update", seal([setAAD(aad, { plaintextLength: 0 }), final()])); +console.log("tag before final", seal([update(pt)])); +console.log("final twice", seal([update(pt), final(), final()])); +console.log("update after final", seal([update(pt), final(), update(pt)])); +console.log("auto padding", seal([(c) => (c.setAutoPadding(false), Buffer.alloc(0)), update(pt), final()])); +console.log("strings", seal([(c) => Buffer.from(c.update("abcd", "utf8", "hex"), "hex"), (c) => Buffer.from(c.final("hex"), "hex")])); + +// Message-length caps: 2^16 - 1 bytes with a 13-byte IV, 2^24 - 1 with 12. +for (const [ivLen, size] of [[13, 65535], [13, 65536], [12, 65536]]) + attempt(`length ${size}, iv ${ivLen}`, () => { + const c = crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(ivLen), { authTagLength: 4 }); + return crypto.createHash("sha256").update(c.update(Buffer.alloc(size, 1))).digest("hex").slice(0, 16); + }); +attempt("aad length 65536, iv 13", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(13), { authTagLength: 4 }).setAAD(aad, { plaintextLength: 65536 })); +attempt("aad length 16777216, iv 12", () => crypto.createCipheriv("aes-128-ccm", key, Buffer.alloc(12), { authTagLength: 4 }).setAAD(aad, { plaintextLength: 16777216 })); + +const sealer = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); +sealer.setAAD(aad, { plaintextLength: pt.length }); +const ct = Buffer.concat([sealer.update(pt), sealer.final()]); +const tag = sealer.getAuthTag(); +function open(steps) { + const d = crypto.createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + const parts = []; + for (const step of steps) { + try { + const out = step(d); + if (Buffer.isBuffer(out)) parts.push(JSON.stringify(out.toString())); + } catch (e) { + parts.push(`<${e.code ?? e.name}: ${e.message}>`); + } + } + return parts.join(" "); +} +const setAuthTag = (t) => (d) => (d.setAuthTag(t), Buffer.alloc(0)); +console.log("open", open([setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open, tag after aad", open([setAAD(aad, { plaintextLength: ct.length }), setAuthTag(tag), update(ct), final()])); +console.log("open without tag", open([setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open bad tag", open([setAuthTag(Buffer.alloc(16)), setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open without aad", open([setAuthTag(tag), update(ct), final()])); +console.log("open tampered", open([setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(Buffer.concat([ct.subarray(0, 5), Buffer.from([ct[5] ^ 1]), ct.subarray(6)])), final()])); +console.log("open tag after update", open([setAAD(aad, { plaintextLength: ct.length }), update(ct), setAuthTag(tag), final()])); +console.log("open tag twice", open([setAuthTag(tag), setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(ct), final()])); +console.log("open tag wrong length", open([setAuthTag(tag.subarray(0, 8))])); +console.log("open update twice", open([setAuthTag(tag), setAAD(aad, { plaintextLength: ct.length }), update(ct), update(ct), final()])); +console.log("open final only", open([setAuthTag(tag), final()])); +console.log("open length mismatch", open([setAuthTag(tag), setAAD(aad, { plaintextLength: 3 }), update(ct), final()])); +console.log("open getAuthTag", open([(d) => d.getAuthTag()])); +console.log("open empty message", (() => { + const c = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); + c.update(Buffer.alloc(0)); + c.final(); + const t = c.getAuthTag(); + const d = crypto.createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); + d.setAuthTag(t); + return JSON.stringify(Buffer.concat([d.update(Buffer.alloc(0)), d.final()]).toString()); +})()); + +// As a stream: one write and the end. +const stream = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 12 }); +const chunks = []; +stream.on("data", (chunk) => chunks.push(chunk)); +stream.on("end", () => console.log("stream", Buffer.concat(chunks).toString("hex"), stream.getAuthTag().toString("hex"))); +stream.end(pt); diff --git a/tests/corpus/7156-cipher-key-wrap.cjs b/tests/corpus/7156-cipher-key-wrap.cjs new file mode 100644 index 000000000..a958d16c5 --- /dev/null +++ b/tests/corpus/7156-cipher-key-wrap.cjs @@ -0,0 +1,103 @@ +// The AES key-wrap ciphers through createCipheriv and createDecipheriv: +// RFC 3394 and RFC 5649 vectors under every name (aes-N-wrap, aesN-wrap, +// id-aesN-wrap, the -pad forms and the -inv ciphers), the input and IV +// lengths each takes, integrity failures, and the one-update rule. +const crypto = require("crypto"); + +function attempt(label, run) { + try { + const v = run(); + console.log(label, "ok", Buffer.isBuffer(v) ? v.toString("hex") : v); + } catch (e) { + console.log(label, e.name, e.code, JSON.stringify(e.message), JSON.stringify(Object.keys(e))); + } +} +const hex = (s) => Buffer.from(s, "hex"); +const wrap = (name, key, iv, input) => { + const c = crypto.createCipheriv(name, key, iv); + return Buffer.concat([c.update(input), c.final()]); +}; +const unwrap = (name, key, iv, input) => { + const d = crypto.createDecipheriv(name, key, iv); + return Buffer.concat([d.update(input), d.final()]); +}; +const iv8 = hex("a6a6a6a6a6a6a6a6"), iv4 = hex("a65959a6"); + +// RFC 3394 section 4. +const kek = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"); +const keyData = hex("00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f"); +for (const [kekBits, dataBits] of [[128, 128], [192, 128], [256, 128], [192, 192], [256, 192], [256, 256]]) { + const k = kek.subarray(0, kekBits / 8), d = keyData.subarray(0, dataBits / 8); + for (const name of [`aes-${kekBits}-wrap`, `aes${kekBits}-wrap`, `id-aes${kekBits}-wrap`, `ID-AES${kekBits}-WRAP`]) { + attempt(`${name} ${dataBits}`, () => wrap(name, k, iv8, d)); + attempt(`${name} ${dataBits} unwrap`, () => unwrap(name, k, iv8, wrap(name, k, iv8, d))); + } +} +// RFC 5649 section 6. +const kek5649 = hex("5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"); +for (const data of ["c37b7e6492584340bed12207808941155068f738", "466f7250617369"]) + for (const name of ["aes-192-wrap-pad", "aes192-wrap-pad", "id-aes192-wrap-pad"]) { + attempt(`${name} ${data.length / 2}`, () => wrap(name, kek5649, iv4, hex(data))); + attempt(`${name} ${data.length / 2} unwrap`, () => unwrap(name, kek5649, iv4, wrap(name, kek5649, iv4, hex(data)))); + } + +const k128 = kek.subarray(0, 16); +for (const size of [0, 1, 7, 8, 9, 15, 16, 17, 24, 40]) { + attempt(`wrap ${size} bytes`, () => wrap("id-aes128-wrap", k128, iv8, Buffer.alloc(size, 5))); + attempt(`wrap-pad ${size} bytes`, () => { + const w = wrap("id-aes128-wrap-pad", k128, iv4, Buffer.alloc(size, 5)); + return w.toString("hex") + " -> " + unwrap("id-aes128-wrap-pad", k128, iv4, w).toString("hex"); + }); +} +for (const size of [8, 16, 20, 24, 32]) + for (const name of ["id-aes128-wrap", "id-aes128-wrap-pad"]) { + const iv = name.endsWith("pad") ? iv4 : iv8; + attempt(`${name} unwrap ${size} bytes`, () => unwrap(name, k128, iv, Buffer.alloc(size, 1))); + } +attempt("other iv", () => wrap("id-aes128-wrap", k128, Buffer.alloc(8, 1), keyData.subarray(0, 16))); +attempt("unwrap with another iv", () => unwrap("id-aes128-wrap", k128, Buffer.alloc(8, 1), wrap("id-aes128-wrap", k128, iv8, keyData.subarray(0, 16)))); +attempt("pad other iv", () => wrap("id-aes128-wrap-pad", k128, Buffer.alloc(4, 7), Buffer.from("key"))); +attempt("pad unwrap with another iv", () => unwrap("id-aes128-wrap-pad", k128, Buffer.alloc(4, 7), wrap("id-aes128-wrap-pad", k128, iv4, Buffer.from("key")))); +attempt("pad tampered length", () => { + const w = wrap("id-aes128-wrap-pad", k128, iv4, Buffer.alloc(20, 9)); + w[3] ^= 1; + return unwrap("id-aes128-wrap-pad", k128, iv4, w); +}); +for (const name of ["aes-128-wrap-inv", "aes128-wrap-inv", "aes-256-wrap-inv", "aes-128-wrap-pad-inv", "aes256-wrap-pad-inv"]) { + const bits = Number(name.match(/\d+/)[0]); + const iv = name.includes("pad") ? iv4 : iv8; + attempt(name, () => { + const w = wrap(name, kek.subarray(0, bits / 8), iv, keyData.subarray(0, 24)); + return w.toString("hex") + " -> " + unwrap(name, kek.subarray(0, bits / 8), iv, w).toString("hex"); + }); +} +attempt("null iv", () => crypto.createCipheriv("id-aes128-wrap", k128, null)); +attempt("iv 16", () => crypto.createCipheriv("id-aes128-wrap", k128, Buffer.alloc(16))); +attempt("pad iv 8", () => crypto.createCipheriv("id-aes128-wrap-pad", k128, iv8)); +attempt("key 24", () => crypto.createCipheriv("id-aes128-wrap", Buffer.alloc(24), iv8)); + +function steps(list) { + const c = crypto.createCipheriv("id-aes128-wrap", k128, iv8); + return list + .map((step) => { + try { + const out = step(c); + return Buffer.isBuffer(out) ? out.toString("hex") || '""' : String(out); + } catch (e) { + return `<${e.code ?? e.name}: ${e.message}>`; + } + }) + .join(" "); +} +const data = keyData.subarray(0, 16); +console.log("update twice", steps([(c) => c.update(data), (c) => c.update(data), (c) => c.final()])); +console.log("final only", steps([(c) => c.final()])); +console.log("final twice", steps([(c) => c.update(data), (c) => c.final(), (c) => c.final()])); +console.log("update after final", steps([(c) => c.update(data), (c) => c.final(), (c) => c.update(data)])); +console.log("bad then good", steps([(c) => c.update(Buffer.alloc(12)), (c) => c.update(data), (c) => c.final()])); +console.log("bad then final", steps([(c) => c.update(Buffer.alloc(12)), (c) => c.final()])); +console.log("empty then data", steps([(c) => c.update(Buffer.alloc(0)), (c) => c.update(data), (c) => c.final()])); +console.log("hex", steps([(c) => c.update(data, undefined, "hex"), (c) => c.final("hex")])); +console.log("auto padding", steps([(c) => c.setAutoPadding(false) === c, (c) => c.update(data)])); +console.log("setAAD", steps([(c) => c.setAAD(Buffer.alloc(1))])); +console.log("getAuthTag", steps([(c) => c.update(data), (c) => c.final(), (c) => c.getAuthTag()])); diff --git a/tests/corpus/7157-crypto-cipher-info-typed.ts b/tests/corpus/7157-crypto-cipher-info-typed.ts new file mode 100644 index 000000000..2de0f0069 --- /dev/null +++ b/tests/corpus/7157-crypto-cipher-info-typed.ts @@ -0,0 +1,42 @@ +// getCipherInfo, AES-CCM and AES key wrap through their declared types: the +// CipherInfo record's fields, absent fields and key order, the keyLength and +// ivLength options, and the CipherCCM and DecipherCCM members. +import { createCipheriv, createDecipheriv, getCipherInfo, type CipherInfo } from "node:crypto"; + +const info: CipherInfo | undefined = getCipherInfo("aes-128-ccm"); +if (info) console.log(info.name, info.nid, info.mode, info.keyLength, info.ivLength, info.blockSize); +const byNid = getCipherInfo(901); +console.log(byNid?.name, byNid?.mode); +const sized = getCipherInfo("aes-128-ccm", { ivLength: 7 }); +console.log(sized?.ivLength, getCipherInfo("aes-128-ccm", { ivLength: 6 })); +console.log(getCipherInfo("nope")); +console.log(JSON.stringify(getCipherInfo("AES-256-WRAP-PAD")!)); +console.log(Object.keys(getCipherInfo("chacha20")!), JSON.stringify(getCipherInfo("aes-128-ecb")!)); +for (const name of ["rc4", "des-ede3-cbc", "aes-256-xts", "chacha20-poly1305"]) { + const found = getCipherInfo(name, { keyLength: 32 }); + console.log(name, found ? Object.keys(found).join() : "none", found?.blockSize, found?.ivLength); +} + +const key = Buffer.alloc(16, 1); +const iv = Buffer.alloc(12, 2); +const aad = Buffer.from("header"); +const plain = Buffer.from("typed ccm message"); +const cipher = createCipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); +cipher.setAAD(aad, { plaintextLength: plain.length }); +const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); +const tag: Buffer = cipher.getAuthTag(); +console.log(ciphertext.toString("hex"), tag.toString("hex")); +const decipher = createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 8 }); +decipher.setAuthTag(tag); +decipher.setAAD(aad, { plaintextLength: ciphertext.length }); +const opened: Buffer = decipher.update(ciphertext); +decipher.final(); +console.log(opened.toString()); + +const kek = Buffer.alloc(24, 3); +const icv = Buffer.from("A6A6A6A6A6A6A6A6", "hex"); +const wrap = createCipheriv("aes192-wrap", kek, icv); +const wrapped = Buffer.concat([wrap.update(Buffer.alloc(16, 9)), wrap.final()]); +console.log(wrapped.toString("base64")); +const unwrap = createDecipheriv("id-aes192-wrap", kek, icv); +console.log(Buffer.concat([unwrap.update(wrapped), unwrap.final()]).toString("hex")); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 27e68a618..7c468de01 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -74,7 +74,7 @@ http2.connect("https://localhost"); * secure heap the generic hint, and setFips the FIPS truth (getFips() lowers to 0). */ import { createCipheriv, publicEncrypt, secureHeapUsed, setFips } from "node:crypto"; publicEncrypt("key", Buffer.alloc(1)); -createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); +createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); secureHeapUsed(); setFips(false); fetch("https://example.invalid/", { diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index cabce49c4..562733f53 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -120,22 +120,22 @@ fenced.ts:76:1 - error SC2020: 'crypto.publicEncrypt' is typed by @types/node bu 75 | import { createCipheriv, publicEncrypt, secureHeapUsed, setFips } from "node:crypto"; 76 | publicEncrypt("key", Buffer.alloc(1)); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); + 77 | createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); hint: RSA encryption has no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, createECDH, and createDiffieHellman -fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ccm'' is part of the standard library types but has no scriptc lowering yet +fenced.ts:77:16 - error SC2020: 'crypto.createCipheriv with cipher 'aes-128-ocb'' is part of the standard library types but has no scriptc lowering yet 76 | publicEncrypt("key", Buffer.alloc(1)); - 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); + 77 | createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); | ^~~~~~~~~~~~~ 78 | secureHeapUsed(); - hint: the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20, and chacha20-poly1305 + hint: the supported ciphers are aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, cfb1 and ofb modes and their wrap, wrap-pad, wrap-inv and wrap-pad-inv key wraps, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20, and chacha20-poly1305 fenced.ts:78:1 - error SC2020: 'crypto.secureHeapUsed' is typed by @types/node but has no scriptc lowering yet - 77 | createCipheriv("aes-128-ccm", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); + 77 | createCipheriv("aes-128-ocb", Buffer.alloc(16), Buffer.alloc(12), { authTagLength: 16 }); 78 | secureHeapUsed(); | ^~~~~~~~~~~~~~~~ 79 | setFips(false); diff --git a/tests/harness/corpus-costs/7154-crypto-cipher-info.cjs.json b/tests/harness/corpus-costs/7154-crypto-cipher-info.cjs.json new file mode 100644 index 000000000..4eec93aec --- /dev/null +++ b/tests/harness/corpus-costs/7154-crypto-cipher-info.cjs.json @@ -0,0 +1 @@ +{ "plain": 6115, "san": 62881 } diff --git a/tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json b/tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json new file mode 100644 index 000000000..4968524bd --- /dev/null +++ b/tests/harness/corpus-costs/7155-cipher-aes-ccm.cjs.json @@ -0,0 +1 @@ +{ "plain": 8462, "san": 75851 } diff --git a/tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json b/tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json new file mode 100644 index 000000000..b5e45dae6 --- /dev/null +++ b/tests/harness/corpus-costs/7156-cipher-key-wrap.cjs.json @@ -0,0 +1 @@ +{ "plain": 5548, "san": 52523 } diff --git a/tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json b/tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json new file mode 100644 index 000000000..425ec1dc2 --- /dev/null +++ b/tests/harness/corpus-costs/7157-crypto-cipher-info-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 3930, "san": 6119 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index c8b0b51b8..35583a188 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -829,7 +829,7 @@ describe("unimplemented cipher names", () => { const r = await compileAndRun( "cipher-computed-name", `import { createCipheriv } from "node:crypto"; -const mode = ["c", "c", "m"].join(""); +const mode = ["o", "c", "b"].join(""); try { createCipheriv("aes-128-" + mode, Buffer.alloc(16), Buffer.alloc(12)); } catch (error) { @@ -846,16 +846,43 @@ try { ); expect(r.exitCode).toBe(0); expect(r.stdout).toBe( - "Error SC2020 The cipher 'aes-128-ccm' has no native implementation; the supported ciphers are " + - "aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in " + - "cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " + - "chacha20-poly1305\n" + + "Error SC2020 The cipher 'aes-128-ocb' has no native implementation; the supported ciphers are " + + "aes-128, aes-192 and aes-256 in cbc, ctr, gcm, ccm, ecb, cfb, cfb8, cfb1 and ofb modes and their " + + "wrap, wrap-pad, wrap-inv and wrap-pad-inv key wraps, des-ede3 in cbc, ecb, cfb, cfb8, cfb1 and " + + "ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and chacha20-poly1305\n" + "Error ERR_CRYPTO_UNKNOWN_CIPHER Unknown cipher\n", ); expect(r.stderr).toBe(""); }); }); +// getCipherInfo's result is Node's info object; CipherInfo declares nid as +// required, and Node reports the SIV ciphers without one. +describe("getCipherInfo through CipherInfo", () => { + test("a typed read of a missing nid throws a TypeError", async () => { + const r = await compileAndRun( + "cipher-info-no-nid", + `import { getCipherInfo } from "node:crypto"; +const siv = getCipherInfo("aes-128-siv")!; +console.log(JSON.stringify(siv), getCipherInfo("aes-128-gcm")?.nid); +try { + const nid: number = siv.nid; + console.log(nid); +} catch (error) { + const e = error as Error; + console.log(e.name, e.message); +} +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + '{"mode":"siv","name":"aes-128-siv","blockSize":1,"keyLength":32} 895\n' + + "TypeError expected number at $, got undefined\n", + ); + expect(r.stderr).toBe(""); + }); +}); + // new tls.TLSSocket reads a runtime options record: the client mode, whose // handshake Node never starts, and members without a lowering throw the // fence, as do sockets it cannot take over (a second wrap, and From fd4f14936ff235d6ca9a2b6e587239b5a36b6c5b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 15:39:35 -0700 Subject: [PATCH 017/409] Check cycle bindings only where early code can reach the reading function The import-cycle analysis treated every deferred reference (a function body, parameter default or instance field) to a cycle binding as early once any user code could run before the declaration completed. With a static evaluation order it now walks the cycle's top-level code in evaluation order, following stored callables (function and class declarations, functions and methods stored by initializers) across the cycle's modules through import aliases, and counts a deferred reference as early only when that walk reaches it before the declaration ran. Top-level reads, unknown orders, top-level await and default-exported declarations keep the previous rule. This removes the SC1016 refusal on zod 4's `fn[CONSTANT_CATCH] = true` (a computed key folded from a const's literal type inside a function nothing calls during the cycle's evaluation), and drops initialization flags and checks that can never fire. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/program.test.ts | 30 +- packages/compiler/src/frontend/program.ts | 355 ++++++++++++++---- .../main.ts.json | 8 + .../main.ts.json | 8 + .../7380-cycle-deferred-folded-key/core.ts | 5 + .../7380-cycle-deferred-folded-key/main.ts | 10 + .../7380-cycle-deferred-folded-key/util.ts | 23 ++ .../7381-cycle-deferred-reached-tdz/alpha.ts | 13 + .../7381-cycle-deferred-reached-tdz/beta.ts | 12 + .../7381-cycle-deferred-reached-tdz/main.ts | 7 + .../main.ts.json | 1 + .../main.ts.json | 1 + 12 files changed, 407 insertions(+), 66 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json create mode 100644 tests/corpus/7380-cycle-deferred-folded-key/core.ts create mode 100644 tests/corpus/7380-cycle-deferred-folded-key/main.ts create mode 100644 tests/corpus/7380-cycle-deferred-folded-key/util.ts create mode 100644 tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts create mode 100644 tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts create mode 100644 tests/corpus/7381-cycle-deferred-reached-tdz/main.ts create mode 100644 tests/harness/corpus-costs/7380-cycle-deferred-folded-key/main.ts.json create mode 100644 tests/harness/corpus-costs/7381-cycle-deferred-reached-tdz/main.ts.json diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index bdd6260ed..5eb3e363e 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -371,15 +371,39 @@ test("repeated cycle edges reuse a file's binding index", () => { test.for([ [ - // A shared instance built by a user constructor: the reads of it in - // the partner's function can run early; the class cannot. + // A shared instance built by a user constructor that calls the + // partner's function: its read of the instance can run early. + { + "main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);', + "a.ts": + 'import { f } from "./b.ts"; export class C { n = 1; constructor() { console.log(f()); } } export const shared = new C();', + "b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }', + }, + ["shared"], + ], + [ + // User code runs while the instance is built, but nothing it reaches + // calls the partner's function before the declaration completed. { "main.ts": 'import { shared } from "./a.ts"; console.log(shared.n);', "a.ts": 'import { f } from "./b.ts"; export class C { n = 1; } export const shared = new C(); console.log(f());', "b.ts": 'import { shared } from "./a.ts"; export function f(): number { return shared.n; }', }, - ["shared"], + [], + ], + [ + // The partner's top level runs first and calls a hoisted function of + // the importer that reads a const declared later; a second function + // reads another const but nothing calls it early. + { + "main.ts": 'import { late } from "./a.ts"; console.log(late());', + "a.ts": + 'import { probe } from "./b.ts"; export function read(): string { return name; } export function late(): string { return other + probe; } export const name = "n"; export const other = "o";', + "b.ts": + 'import { read } from "./a.ts"; function attempt(): string { try { return read(); } catch { return "tdz"; } } export const probe = attempt();', + }, + ["name"], ], [ // The partner's top level reads the importer's const before it ran. diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 507d3392d..f42bc426b 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -3760,22 +3760,7 @@ function computeCycleEarlyBindings7( const evaluation = known ? comp.slice().sort((a, b) => position.get(a)! - position.get(b)!) : comp.slice(); - interface Candidate { - symbol: ts.Symbol; - name: ts.Identifier; - kind: CycleEarlyBinding["kind"]; - module: ts.SourceFile; - /** User code can run between the cycle's start and the end of the - * declaration (always true when the order is unknown). */ - exposed: boolean; - rank: number; - start: number; - end: number; - reference: ts.Identifier | null; - folded: ts.Identifier | null; - localNames: Map; - } - const candidates = new Map(); + const candidates = new Map(); const names = new Set(); let dirty = !known; evaluation.forEach((m, rank) => { @@ -3812,6 +3797,7 @@ function computeCycleEarlyBindings7( start: ts.tokenStartOfNode(stmt), end: stmt.end, reference: null, + deferredReference: null, folded: null, localNames: new Map(), }); @@ -3880,15 +3866,27 @@ function computeCycleEarlyBindings7( ref.end <= c.end ) continue; + if (inDeferredPosition7(ref)) { + if (c.exposed && c.deferredReference === null) c.deferredReference = ref; + continue; + } let early: boolean; - if (inDeferredPosition7(ref)) early = c.exposed; - else if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start; + if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start; else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false; else early = !known || rankOf.get(sf)! < c.rank; if (early) c.reference = ref; } } + // With a static evaluation order, a deferred reference only runs early + // when the code that runs before the declaration completes can reach + // the function holding it. + const reached = + known && [...candidates.values()].some((c) => c.reference === null && c.deferredReference) + ? cycleReachedDeferredReferences7(program, evaluation, candidates) + : null; for (const c of candidates.values()) { + if (c.reference === null && c.deferredReference !== null) + c.reference = reached === null ? c.deferredReference : (reached.get(c.symbol) ?? null); if (c.reference === null) continue; const settled = new Set(order.filter((sf) => !rankOf.has(sf))); if (known) for (const m of evaluation.slice(c.rank + 1)) settled.add(m); @@ -3907,6 +3905,280 @@ function computeCycleEarlyBindings7( return out; } +/** Collects into `units` the callables an initializer expression stores + * (see moduleEarlyBindingsOf7), recording in `immediate` the parts of a + * stored node that run where it is evaluated. */ +function storedCallables7( + checker: ts.TypeChecker, + e: ts.Expression, + units: ts.Node[], + immediate: Map, +): void { + while ( + ts.isParenthesizedExpression(e) || + ts.isAsExpression(e) || + ts.isSatisfiesExpression(e) || + ts.isNonNullExpression(e) + ) + e = e.expression; + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { + units.push(e); + } else if (ts.isClassExpression(e)) { + units.push(e); + immediate.set(e, classSyncParts7(e)); + } else if (ts.isObjectLiteralExpression(e)) { + for (const p of e.properties.nodes) { + if ( + ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p) + ) { + // A computed key evaluates where the literal does; a plain + // member-chain key (`[Equal.symbol]`) runs no user code. + if (!ts.isComputedPropertyName(p.name)) units.push(p); + else if (memberChainKey7(p.name.expression)) { + units.push(p); + immediate.set(p, [p.name]); + } + } else if (ts.isPropertyAssignment(p)) + storedCallables7(checker, p.initializer, units, immediate); + } + } else if (ts.isArrayLiteralExpression(e)) { + for (const el of e.elements.nodes) + if (!ts.isSpreadElement(el)) storedCallables7(checker, el, units, immediate); + } else if (ts.isCallExpression(e)) { + // `const f = wrap(callback)` where wrap only returns closures over + // the callback: it runs when the stored result is called. + e.arguments.nodes.forEach((arg, index) => { + let fn: ts.Expression = arg; + while (ts.isParenthesizedExpression(fn)) fn = fn.expression; + if ( + (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && + returnsOnlyClosuresOver7(checker, e.expression, index) + ) + units.push(fn); + }); + } +} + +/** A binding of an import cycle and what the reference scan found. */ +interface CycleCandidate7 { + symbol: ts.Symbol; + name: ts.Identifier; + kind: CycleEarlyBinding["kind"]; + module: ts.SourceFile; + /** User code can run between the cycle's start and the end of the + * declaration (always true when the order is unknown). */ + exposed: boolean; + rank: number; + start: number; + end: number; + /** A reference read at top level before the declaration ran. */ + reference: ts.Identifier | null; + /** A deferred reference (function body, parameter default, instance + * field) when user code may run before the declaration completes. */ + deferredReference: ts.Identifier | null; + folded: ts.Identifier | null; + localNames: Map; +} + +/** For one import cycle with a static evaluation order: the first deferred + * reference to each candidate that code running before the candidate's + * declaration completes can reach, or null when the walk cannot be precise + * (top-level await, a default-exported function or class declaration). + * + * The cycle's top-level code runs in evaluation order. A stored callable + * (a function or class declaration, or a function, class or object-literal + * method stored by a variable initializer) becomes reachable once + * reachable code names its binding, through an import alias or a + * namespace member too; every other function-like in reachable code counts + * as reachable, since it may be handed to a callee. A namespace import of + * a cycle member used other than for a member read makes every stored + * callable reachable. moduleEarlyBindingsOf7 is the single-module form. */ +function cycleReachedDeferredReferences7( + program: ts.Program, + evaluation: readonly ts.SourceFile[], + candidates: ReadonlyMap, +): Map | null { + const checker = program.getTypeChecker(); + const members = new Set(evaluation); + for (const sf of evaluation) { + if (hasTopLevelAwait7(sf)) return null; + for (const stmt of sf.statements.nodes) { + if ( + (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && + (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & + ts.ModifierFlags.Default) !== + 0 + ) + return null; + } + } + const stored = new Set(); + const immediate = new Map(); + const storedNames: ts.Identifier[] = []; + const unitsOfName = new Map(); + const store = (name: ts.Identifier, units: ts.Node[]): void => { + if (units.length === 0) return; + for (const u of units) stored.add(u); + storedNames.push(name); + unitsOfName.set(name, units); + }; + for (const sf of evaluation) { + for (const stmt of sf.statements.nodes) { + if ( + (ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & + ts.ModifierFlags.Ambient) !== + 0 + ) + continue; + if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) { + store(stmt.name, [stmt]); + } else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) { + store(stmt.name, [stmt]); + } else if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations.nodes) { + if (ts.isIdentifier(d.name) && d.initializer !== undefined) { + const units: ts.Node[] = []; + storedCallables7(checker, d.initializer, units, immediate); + store(d.name, units); + } + } + } + } + } + const unitsBySymbol = new Map(); + const allUnits: ts.Node[] = []; + for (const name of storedNames) { + const symbol = checker.getSymbolAtLocation(name); + if (symbol === undefined) continue; + const units = unitsOfName.get(name)!; + unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]); + allUnits.push(...units); + } + // The identifier texts worth resolving: declared names, and the local + // names of imports (named, default and namespace) in cycle members. + const names = new Set(); + for (const c of candidates.values()) names.add(c.name.text); + for (const name of storedNames) names.add(name.text); + for (const sf of evaluation) { + for (const stmt of sf.statements.nodes) { + if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue; + const clause = stmt.importClause; + if (clause.name !== undefined) names.add(clause.name.text); + const bindings = clause.namedBindings; + if (bindings === undefined) continue; + if (ts.isNamedImports(bindings)) + for (const element of bindings.elements.nodes) names.add(element.name.text); + else names.add(bindings.name.text); + } + } + const idents: ts.Identifier[] = []; + for (const sf of evaluation) { + ts.walkPreorder(sf, (node) => { + if (ts.isImportDeclaration(node)) return "skip"; + if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node); + return undefined; + }); + } + checker.prefetchSymbolNodesExact(idents); + const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { + const parent = id.parent; + let symbol = checker.getSymbolAtLocation(id); + if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) { + symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol; + } + if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { + symbol = checker.getAliasedSymbol(symbol); + } + return symbol; + }; + const isMemberNamespace = (symbol: ts.Symbol): boolean => + (symbol.flags & ts.SymbolFlags.ValueModule) !== 0 && + symbol.declarations.some((d) => ts.isSourceFile(d) && members.has(d)); + const declared = new Set(); + const reachedUnits = new Set(); + const out = new Map(); + const work: ts.Node[] = []; + const reach = (unit: ts.Node): void => { + if (reachedUnits.has(unit)) return; + reachedUnits.add(unit); + work.push(unit); + }; + const visit = (root: ts.Node): void => { + ts.walkPreorder(root, (node) => { + if (node !== root && stored.has(node)) { + for (const part of immediate.get(node) ?? []) visit(part); + return "skip"; + } + if (ts.isImportDeclaration(node)) return "skip"; + if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined; + if (inTypePosition7(node)) return undefined; + const parent = node.parent; + if (parent !== undefined && ts.isExportSpecifier(parent)) return undefined; + if (parent !== undefined && declaredName7(parent) === node) return undefined; + const symbol = symbolOf(node); + if (symbol === undefined) return undefined; + if (isMemberNamespace(symbol)) { + // `ns.member` resolves through the member's own identifier; any + // other use can reach every export. + if ( + !( + parent !== undefined && + ts.isPropertyAccessExpression(parent) && + parent.expression === node + ) + ) + for (const unit of allUnits) reach(unit); + return undefined; + } + const c = candidates.get(symbol); + if ( + c !== undefined && + node !== c.name && + !declared.has(symbol) && + !out.has(symbol) && + inDeferredPosition7(node) && + !( + c.kind === "class" && + ts.getSourceFileOfNode(node) === c.module && + ts.tokenStartOfNode(node) >= c.start && + node.end <= c.end + ) + ) + out.set(symbol, node); + for (const unit of unitsBySymbol.get(symbol) ?? []) reach(unit); + return undefined; + }); + }; + const drain = (): void => { + while (work.length > 0) visit(work.pop()!); + }; + const declare = (name: ts.BindingName): void => { + for (const id of boundNames7(name)) { + const symbol = checker.getSymbolAtLocation(id); + if (symbol !== undefined) declared.add(symbol); + } + }; + for (const sf of evaluation) { + for (const stmt of sf.statements.nodes) { + if (ts.isClassDeclaration(stmt)) { + for (const part of classSyncParts7(stmt)) visit(part); + drain(); + if (stmt.name !== undefined) declare(stmt.name); + } else if (ts.isVariableStatement(stmt)) { + for (const d of stmt.declarationList.declarations.nodes) { + visit(d); + drain(); + declare(d.name); + } + } else if (!ts.isFunctionDeclaration(stmt)) visit(stmt); + drain(); + } + } + return out; +} + /** Top-level let/const and class bindings, in modules outside the * exactly-initialized cycles, that code running before their declaration * completes can read. @@ -3971,51 +4243,8 @@ function moduleEarlyBindingsOf7( storedBy.set(name.text, list); }; const checker = program.getTypeChecker(); - const storedIn = (e: ts.Expression, units: ts.Node[]): void => { - while ( - ts.isParenthesizedExpression(e) || - ts.isAsExpression(e) || - ts.isSatisfiesExpression(e) || - ts.isNonNullExpression(e) - ) - e = e.expression; - if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { - units.push(e); - } else if (ts.isClassExpression(e)) { - units.push(e); - immediate.set(e, classSyncParts7(e)); - } else if (ts.isObjectLiteralExpression(e)) { - for (const p of e.properties.nodes) { - if ( - ts.isMethodDeclaration(p) || - ts.isGetAccessorDeclaration(p) || - ts.isSetAccessorDeclaration(p) - ) { - // A computed key evaluates where the literal does; a plain - // member-chain key (`[Equal.symbol]`) runs no user code. - if (!ts.isComputedPropertyName(p.name)) units.push(p); - else if (memberChainKey7(p.name.expression)) { - units.push(p); - immediate.set(p, [p.name]); - } - } else if (ts.isPropertyAssignment(p)) storedIn(p.initializer, units); - } - } else if (ts.isArrayLiteralExpression(e)) { - for (const el of e.elements.nodes) if (!ts.isSpreadElement(el)) storedIn(el, units); - } else if (ts.isCallExpression(e)) { - // `const f = wrap(callback)` where wrap only returns closures over - // the callback: it runs when the stored result is called. - e.arguments.nodes.forEach((arg, index) => { - let fn: ts.Expression = arg; - while (ts.isParenthesizedExpression(fn)) fn = fn.expression; - if ( - (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && - returnsOnlyClosuresOver7(checker, e.expression, index) - ) - units.push(fn); - }); - } - }; + const storedIn = (e: ts.Expression, units: ts.Node[]): void => + storedCallables7(checker, e, units, immediate); sf.statements.nodes.forEach((stmt, index) => { if (ambient(stmt)) return; if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json new file mode 100644 index 000000000..18ef785c5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-cycle-deferred-folded-key/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7380-cycle-deferred-folded-key/util.ts", + "/tests/corpus/7380-cycle-deferred-folded-key/core.ts", + "/tests/corpus/7380-cycle-deferred-folded-key/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json new file mode 100644 index 000000000..65d9b19e9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts", + "/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts", + "/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7380-cycle-deferred-folded-key/core.ts b/tests/corpus/7380-cycle-deferred-folded-key/core.ts new file mode 100644 index 000000000..7f4f4ab14 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/core.ts @@ -0,0 +1,5 @@ +import { describe, kind, mark } from "./util.ts"; + +export const version = 3; +export const marked = mark({}); +console.log("core body", kind(), describe()); diff --git a/tests/corpus/7380-cycle-deferred-folded-key/main.ts b/tests/corpus/7380-cycle-deferred-folded-key/main.ts new file mode 100644 index 000000000..92617eba3 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/main.ts @@ -0,0 +1,10 @@ +// A cycle member runs user code before it declares a constant that only +// functions read, one of them as a computed key and one under `typeof` +// (both resolved from the constant's literal type). Nothing calls those +// functions before the declaration ran, so they read the constant +// normally once the other member's top level calls them. +import { marked } from "./core.ts"; +import { describe, kind, mark } from "./util.ts"; + +console.log(JSON.stringify(marked), kind()); +console.log(JSON.stringify(mark({ seen: false })), describe()); diff --git a/tests/corpus/7380-cycle-deferred-folded-key/util.ts b/tests/corpus/7380-cycle-deferred-folded-key/util.ts new file mode 100644 index 000000000..a038d9e63 --- /dev/null +++ b/tests/corpus/7380-cycle-deferred-folded-key/util.ts @@ -0,0 +1,23 @@ +import { version } from "./core.ts"; + +export function makeCounter(): () => number { + let n = 0; + return () => ++n; +} +const next = makeCounter(); +console.log("util body", next(), next()); + +export const TAG = "~tagged"; + +export function mark(box: Record): Record { + box[TAG] = true; + return box; +} + +export function kind(): string { + return typeof TAG; +} + +export function describe(): string { + return `core v${version}`; +} diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts new file mode 100644 index 000000000..8dc002c27 --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/alpha.ts @@ -0,0 +1,13 @@ +import { probe } from "./beta.ts"; + +export function report(): string { + return `alpha ${NAME}`; +} + +export function late(): string { + return `${OTHER} after ${probe}`; +} + +export const NAME = "ready"; +export const OTHER = "other"; +console.log("alpha body", report()); diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts new file mode 100644 index 000000000..c3d1551ff --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/beta.ts @@ -0,0 +1,12 @@ +import { report } from "./alpha.ts"; + +function attempt(): string { + try { + return report(); + } catch (error) { + return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error"; + } +} + +export const probe = attempt(); +console.log("beta body", probe); diff --git a/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts b/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts new file mode 100644 index 000000000..6a0b7e3ca --- /dev/null +++ b/tests/corpus/7381-cycle-deferred-reached-tdz/main.ts @@ -0,0 +1,7 @@ +// The first cycle member to run calls a function of the other member +// before that member declared the constant the function reads: Node's +// temporal-dead-zone ReferenceError. A second function of the same member +// reads another constant but runs only after the cycle finished. +import { late, report } from "./alpha.ts"; + +console.log(report(), late()); diff --git a/tests/harness/corpus-costs/7380-cycle-deferred-folded-key/main.ts.json b/tests/harness/corpus-costs/7380-cycle-deferred-folded-key/main.ts.json new file mode 100644 index 000000000..d5d1c5f65 --- /dev/null +++ b/tests/harness/corpus-costs/7380-cycle-deferred-folded-key/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3500 } diff --git a/tests/harness/corpus-costs/7381-cycle-deferred-reached-tdz/main.ts.json b/tests/harness/corpus-costs/7381-cycle-deferred-reached-tdz/main.ts.json new file mode 100644 index 000000000..b24c0c970 --- /dev/null +++ b/tests/harness/corpus-costs/7381-cycle-deferred-reached-tdz/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2600 } From 862d5e836d42da00b05dc948d77d9b42c688e5d6 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 16:12:37 -0700 Subject: [PATCH 018/409] Give the tsgo/tsc-ts IR parity job 60 minutes Its release pass alone takes about 15 minutes, so the dev pass is regularly cancelled at the 30-minute limit on main and on PRs. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a100e374..3b9ef44a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -524,7 +524,7 @@ jobs: if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }} name: test (tsgo and tsc-ts give the same IR) runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 60 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 From aaca1f1e2b838e0ef15d91743b06bfd5803d6dc0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:17:48 -0500 Subject: [PATCH 019/409] Count the arguments a function receives when it is called through a value `arguments.length` in a typed function folds to a constant. In a function declaration it was the declared parameter count even when the function is called through a value with more arguments: Node prints 4 for `[1].map(f)` with `function f(v) { return v + arguments.length; }`, the compiled program printed 2. A top-level function declaration that reads `arguments` and that the program uses as a value now gets a count slot. Its value is an adapter that stores the parameter count of the function type the value is passed as (3 for map, 4 for reduce, 2 for a sort comparator) and forwards; the function reads the slot on entry and clears it, so a direct call sees the declared count. Function expressions and declarations passed where the expected function type has optional or rest parameters (a replace callback) are called with a varying count and are refused with SC1090 instead of folding to a wrong count, as are methods, nested functions, and arrows inside a function used as a value. --- .../src/frontend/lowering/argument-counts.ts | 230 ++++++++++++++++++ .../lowering/expressions/identifiers.ts | 13 +- .../lowering/expressions/property-access.ts | 50 ++-- .../src/frontend/lowering/function-context.ts | 3 + .../src/frontend/lowering/lower-functions.ts | 2 + .../compiler/src/frontend/lowering/lowerer.ts | 3 + ...81-function-value-arguments-length.ts.json | 6 + .../function-value-arguments-length.ts.json | 6 + .../7381-function-value-arguments-length.ts | 27 ++ .../function-value-arguments-length.ts | 36 +++ .../function-value-arguments-length.ts.txt | 43 ++++ ...81-function-value-arguments-length.ts.json | 1 + 12 files changed, 398 insertions(+), 22 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/argument-counts.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json create mode 100644 tests/corpus/7381-function-value-arguments-length.ts create mode 100644 tests/diagnostics/function-value-arguments-length.ts create mode 100644 tests/harness/__snapshots__/function-value-arguments-length.ts.txt create mode 100644 tests/harness/corpus-costs/7381-function-value-arguments-length.ts.json diff --git a/packages/compiler/src/frontend/lowering/argument-counts.ts b/packages/compiler/src/frontend/lowering/argument-counts.ts new file mode 100644 index 000000000..ec1753160 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/argument-counts.ts @@ -0,0 +1,230 @@ +import * as ts from "../ts7/adapter.js"; +import { bodyReadsArguments } from "../arguments-usage.js"; +import { isJsSourceFile, locOf } from "../program.js"; +import { BOOL, F64, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import { varRef } from "../../ir/build.js"; +import { typeKey } from "../type-mapper.js"; +import type { Lowerer } from "./lowerer.js"; +import { isThisParameter } from "./call-signatures.js"; +import { buildFunctionAdapter } from "./coercions/builders.js"; +import { keyedHelperName } from "./stable-names.js"; + +/** `arguments.length` in a top-level function declaration that the program + * also uses as a value. A direct call passes the declared parameters (tsc + * checks the arity), while a call through the value passes the parameter + * count of the function type the value was passed as (`[1].map(f)` calls f + * with 3). The value is an adapter that stores that count in a global slot + * before forwarding; the function reads the slot on entry and clears it, + * so a direct call (the cleared slot) sees the declared count. */ + +/** The count slot of `decl`, or null when its `arguments.length` folds to + * the declared count (or the declaration does not qualify). */ +export function argumentCountSlot(lowerer: Lowerer, decl: ts.FunctionDeclaration): string | null { + const cached = lowerer.argumentCountSlots.get(decl); + if (cached !== undefined) return cached; + let slot: string | null = null; + if ( + decl.body !== undefined && + decl.name !== undefined && + decl.asteriskToken === undefined && + !decl.typeParameters?.nodes.length && + lowerer.isTopLevelFnSymbol(decl.name) && + decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) !== true && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && + fixedArity(decl) && + bodyReadsArguments(decl) && + usedAsValue(lowerer, decl) + ) { + const sig = lowerer.fnSigOf(decl.name); + if (sig) { + slot = `%g.argc.${sig.name}`; + lowerer.globalsList.push({ id: slot, name: "%argc", type: F64, mutable: true }); + } + } + lowerer.argumentCountSlots.set(decl, slot); + return slot; +} + +/** Reads `decl`'s count slot into a hidden local on entry and clears it; the + * local holds the declared count when the slot was clear. */ +export function declareArgumentCount( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, + prologue: IrStmt[], +): void { + const slot = argumentCountSlot(lowerer, decl); + if (slot === null) return; + const loc = locOf(decl); + const local = lowerer.declareHiddenLocal("%argc", F64); + const stored = varRef(slot, F64, loc); + prologue.push( + { + kind: "varDecl", + localId: local.id, + init: { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: stored, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + then: { kind: "numLit", value: declaredCount(decl), type: F64, loc }, + else_: stored, + type: F64, + loc, + }, + loc, + }, + { kind: "assign", localId: slot, value: { kind: "numLit", value: 0, type: F64, loc }, loc }, + ); + lowerer.ctx.argumentCount = local; +} + +/** `decl` as a value at `reference`: the plain closure when every call + * through the expected function type passes the declared count, otherwise + * an adapter that stores the expected type's count in the slot first. */ +export function argumentCountValue( + lowerer: Lowerer, + decl: ts.FunctionDeclaration, + reference: ts.Expression, + closure: IrExpr & { type: IrType & { kind: "func" } }, +): IrExpr { + const slot = argumentCountSlot(lowerer, decl); + if (slot === null) return closure; + const count = contextualCallCount(lowerer, reference); + if (count === null || count === "varies") { + lowerer.unsupported( + "SC1090", + reference, + count === null + ? "functions that read 'arguments.length' used as values where no function type is expected (the count each call passes is not known)" + : "functions that read 'arguments.length' used as values where the expected function type has optional or rest parameters (the count varies per call)", + ); + } + const declared = declaredCount(decl); + if (count <= declared) return closure; + const loc = locOf(reference); + const type = closure.type; + const key = `argc:${slot}:${count}:${typeKey(type)}`; + let name = lowerer.arrHofHelpers.get(key); + if (name === undefined) { + name = keyedHelperName(lowerer, "%argc.adapter", key); + lowerer.arrHofHelpers.set(key, name); + const params = type.params.map((t, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: t })); + lowerer.liftedFns.push( + ...buildFunctionAdapter( + name, + type, + type, + params, + [], + [ + { + kind: "assign", + localId: slot, + value: { kind: "numLit", value: count, type: F64, loc }, + loc, + }, + { + kind: "return", + value: { + kind: "callValue", + callee: varRef("f.0", type, loc), + args: params.map((p) => varRef(p.localId, p.type, loc)), + type: type.ret, + loc, + }, + loc, + }, + ], + loc, + ), + ); + } + return { kind: "call", callee: name, args: [closure], type, loc }; +} + +/** The argument count every call through the function type `node` is + * expected as passes: that type's parameter count when it has one call + * signature with a fixed parameter list (a `this` parameter is type-only), + * "varies" when it has optional or rest parameters, null when there is no + * such type. */ +export function contextualCallCount( + lowerer: Lowerer, + node: ts.Expression, +): number | "varies" | null { + const contextual = lowerer.checker.getContextualType(node); + if (contextual === undefined) return null; + const signatures = lowerer.checker.getCallSignatures( + lowerer.checker.getNonNullableType(contextual), + ); + if (signatures.length !== 1) return null; + const declaration = signatures[0]!.declaration; + if (declaration === undefined) return null; + let count = 0; + for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) { + if (isThisParameter(parameter)) continue; + if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken) + return "varies"; + count++; + } + return count; +} + +/** Whether a program reference to the function declaration or method `fn` + * reads it as a value (`xs.map(f)`, `const g = f`, `xs.map(C.parse)`) rather + * than calling it. Export specifiers and default exports only name a + * function; the importing files' references resolve to it and are checked + * the same way. */ +export function usedAsValue( + lowerer: Lowerer, + fn: ts.FunctionDeclaration | ts.MethodDeclaration, +): boolean { + const name = fn.name; + if (!name || !(ts.isIdentifier(name) || ts.isPrivateIdentifier(name))) return false; + const method = ts.isMethodDeclaration(fn); + const symbolAt = (n: ts.Node): ts.Symbol | null | undefined => + method + ? lowerer.checker.getSymbolAtLocation(n) + : lowerer.resolveValueSymbol(n as ts.Identifier); + const symbol = symbolAt(name); + if (!symbol) return false; + let found = false; + for (const sf of lowerer.program.getSourceFiles()) { + if (sf.isDeclarationFile) continue; + ts.walkPreorder(sf, (n) => { + if (found) return "stop"; + if (ts.isTypeNode(n)) return "skip"; + if (n === name || !(ts.isIdentifier(n) || ts.isPrivateIdentifier(n))) return undefined; + if (n.text !== name.text || !n.parent) return undefined; + const parent = n.parent; + // A method is read through its member name (`C.parse`, `this.#m`). + const read = ts.isPropertyAccessExpression(parent) && parent.name === n ? parent : n; + if (method && read === n && !ts.isBindingElement(parent)) return undefined; + const use = read.parent; + if (use && ts.isCallExpression(use) && use.expression === read) return undefined; + if (ts.isExportSpecifier(parent) || ts.isExportAssignment(parent)) return undefined; + if (ts.isImportSpecifier(parent) || ts.isImportClause(parent)) return undefined; + if (symbolAt(n) === symbol) found = true; + return undefined; + }); + if (found) break; + } + return found; +} + +function fixedArity(fn: ts.FunctionLikeDeclaration): boolean { + return fn.parameters.nodes.every( + (p) => + p.questionToken === undefined && + p.initializer === undefined && + p.dotDotDotToken === undefined, + ); +} + +function declaredCount(fn: ts.FunctionLikeDeclaration): number { + return fn.parameters.nodes.filter((p) => !isThisParameter(p)).length; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 63175d50a..ff6f65f26 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -25,6 +25,7 @@ import { lowerNodeModuleIdentifier } from "../lower-node-module.js"; import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js"; import { captureContextArguments } from "../function-context.js"; import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js"; +import { argumentCountValue } from "../argument-counts.js"; import { isCjsExportTableLiteral, isCjsJsFile, @@ -424,7 +425,17 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // semantics); the rest marker carries the trailing dyn-array ABI. const funcType: IrType = funcTypeFromParamShapes(sig.params, sig.returnType); lowerer.requireExactArityValue(expr, expr, sig.params, funcType); - return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc }; + const closure = { + kind: "closure" as const, + fnName: sig.name, + captures: [], + type: funcType, + loc, + }; + const decl = lowerer.resolveValueSymbol(expr)?.valueDeclaration; + return decl && ts.isFunctionDeclaration(decl) && funcType.kind === "func" + ? argumentCountValue(lowerer, decl, expr, { ...closure, type: funcType }) + : closure; } { // A generic function as a VALUE: monomorphized by flow — the diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 5df3c7fe0..5f1683ff7 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -6,6 +6,7 @@ import { lowerHistogramProperty } from "../builtins/performance.js"; import { staticFunctionNameRead } from "../static-function-names.js"; import { typedArgumentsArity } from "../typed-arguments.js"; import { isThisParameter } from "../call-signatures.js"; +import { argumentCountSlot, contextualCallCount, usedAsValue } from "../argument-counts.js"; import { checkedPrivateValue, localPrivateBrandClass, @@ -669,7 +670,31 @@ export function lowerPropertyAccessExpr( const declared = (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter( (p) => !isThisParameter(p), ).length; - const count = Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0); + const contextual = contextualArgumentCount(lowerer, fn); + if (contextual === "varies") { + lowerer.unsupported( + "SC1090", + expr, + "'arguments.length' in function expressions passed where the expected function type has optional or rest parameters (the count varies per call)", + ); + } + // A top-level declaration also used as a value reads the count its + // caller stored (argument-counts.ts). + if (ts.isFunctionDeclaration(fn) && lowerer.ctx.argumentCount !== undefined) + return varRef(lowerer.ctx.argumentCount.id, F64, loc); + if ( + (ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && + usedAsValue(lowerer, fn) + ) { + lowerer.unsupported( + "SC1090", + expr, + ts.isFunctionDeclaration(fn) && argumentCountSlot(lowerer, fn) !== null + ? "'arguments.length' in an arrow function inside a function that is also used as a value" + : "'arguments.length' in methods, nested, async and generator functions used as values (a caller of the value can pass more arguments than the function declares)", + ); + } + const count = Math.max(declared, contextual ?? 0); // Calls through the expression's own name pass the declared count, // calls through the wider slot pass more: no constant answers both. if (count > declared && referencesOwnName(lowerer, fn)) { @@ -4469,24 +4494,7 @@ function referencesOwnName(lowerer: Lowerer, fn: ts.Node): boolean { } /** The argument count every caller passes a function expression whose - * value flows into one fixed-arity function type, an array method's - * callback or an annotated slot: that type's parameter count. Null when - * there is no such type or its arity varies. */ -function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null { - if (!ts.isFunctionExpression(fn)) return null; - const contextual = lowerer.checker.getContextualType(fn); - if (contextual === undefined) return null; - const signatures = lowerer.checker.getCallSignatures( - lowerer.checker.getNonNullableType(contextual), - ); - if (signatures.length !== 1) return null; - const declaration = signatures[0]!.declaration; - if (declaration === undefined) return null; - let count = 0; - for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) { - if (isThisParameter(parameter)) continue; - if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken) return null; - count++; - } - return count; + * value flows into a function type (argument-counts.ts contextualCallCount). */ +function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | "varies" | null { + return ts.isFunctionExpression(fn) ? contextualCallCount(lowerer, fn) : null; } diff --git a/packages/compiler/src/frontend/lowering/function-context.ts b/packages/compiler/src/frontend/lowering/function-context.ts index 92042b7d7..702574b63 100644 --- a/packages/compiler/src/frontend/lowering/function-context.ts +++ b/packages/compiler/src/frontend/lowering/function-context.ts @@ -46,6 +46,9 @@ export interface FnCtx { * rest param): the synthetic trailing dyn-array param `arguments` * reads resolve to. */ argumentsLocal?: IrLocal | null; + /** The count `arguments.length` reads in a top-level function declaration + * that is also used as a value (argument-counts.ts). */ + argumentCount?: IrLocal; /** Arrows inherit the nearest ordinary function's arguments binding. */ inheritsArguments?: boolean; /** A function lowered on demand on top of an unrelated caller's frames diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index c1cca9c64..c1682f7c8 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -1,5 +1,6 @@ import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { declareTypedArguments } from "./typed-arguments.js"; +import { declareArgumentCount } from "./argument-counts.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; import { bindingNeverReassigned } from "./binding-analysis.js"; @@ -551,6 +552,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I (sig.params[sig.params.length - 1]!.mode === "dynRest" || sig.params[sig.params.length - 1]!.mode === "arguments"), ); + declareArgumentCount(lowerer, decl, prologue); const bodyBlock = blockBodyOf(decl); if (!bodyBlock) { lowerer.unsupported( diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4add3193c..4b60d9e1b 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2335,6 +2335,9 @@ export class Lowerer { * every name destructured from a present source optional. */ readonly runtimeOptionalPatternTypes = new Map(); readonly runtimeOptionalReduceTypes = new Map(); + /** Each function declaration's `arguments.length` count slot, or null + * (argument-counts.ts argumentCountSlot). */ + readonly argumentCountSlots = new Map(); /** Non-null assertions whose value is forwarded into a slot promoted to * hold undefined. `x!` has no runtime effect, so these keep the stored * undefined state instead of trapping before any dereference. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json new file mode 100644 index 000000000..b9e5eacd1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-function-value-arguments-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7381-function-value-arguments-length.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json new file mode 100644 index 000000000..6aa894918 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/function-value-arguments-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/function-value-arguments-length.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7381-function-value-arguments-length.ts b/tests/corpus/7381-function-value-arguments-length.ts new file mode 100644 index 000000000..059ff1487 --- /dev/null +++ b/tests/corpus/7381-function-value-arguments-length.ts @@ -0,0 +1,27 @@ +// `arguments.length` in a function declaration counts the arguments its +// caller passes: a direct call passes the declared parameters, and a call +// through the function as a value passes the parameters of the function +// type it was passed as (3 for map and filter, 4 for reduce, 2 for sort). +function scaled(value: number): number { + return value * 10 + arguments.length; +} +function total(acc: number, value: number): number { + return acc + value + arguments.length; +} +function compare(a: number, b: number): number { + return arguments.length === 2 ? a - b : 0; +} +console.log([1, 2].map(scaled).join(), scaled(3)); +console.log([5, 6].filter(scaled).length, [1, 2].reduce(total, 0), total(1, 2)); +console.log([3, 1, 2].sort(compare).join()); + +function apply(f: (a: number, b: number, c: number) => number): number { + return f(1, 2, 3); +} +console.log(apply(scaled)); + +// A direct call inside a call through the value sees its own count. +function depth(n: number): number { + return n <= 0 ? arguments.length : depth(n - 1) + arguments.length; +} +console.log([2].map(depth).join(), depth(2)); diff --git a/tests/diagnostics/function-value-arguments-length.ts b/tests/diagnostics/function-value-arguments-length.ts new file mode 100644 index 000000000..ab0b53f82 --- /dev/null +++ b/tests/diagnostics/function-value-arguments-length.ts @@ -0,0 +1,36 @@ +// `arguments.length` has no single value when the calls pass varying +// counts: a function expression or declaration passed where the expected +// function type has optional or rest parameters. Methods and nested +// functions used as values, and arrows inside a function used as a value, +// keep the fence. +const replaced = "a-b".replace(/-/, function (match) { + return String(arguments.length) + match; +}); +function call(f: (a: number, b?: number) => number): number { + return f(1, 2); +} +const counted = call(function (a) { + return arguments.length + a; +}); +function offset(value: number): number { + return value + arguments.length; +} +const offsets = call(offset); +class Scale { + static by(value: number): number { + return value * arguments.length; + } +} +const scaled = [2].map(Scale.by); +function outer(): number[] { + function inner(value: number): number { + return value + arguments.length; + } + return [1].map(inner); +} +function deferred(value: number): number { + const read = (): number => arguments.length; + return value + read(); +} +const late = [1].map(deferred); +export { replaced, counted, offsets, scaled, outer, late }; diff --git a/tests/harness/__snapshots__/function-value-arguments-length.ts.txt b/tests/harness/__snapshots__/function-value-arguments-length.ts.txt new file mode 100644 index 000000000..adcb48d2a --- /dev/null +++ b/tests/harness/__snapshots__/function-value-arguments-length.ts.txt @@ -0,0 +1,43 @@ +function-value-arguments-length.ts:7:17 - error SC1090: 'arguments.length' in function expressions passed where the expected function type has optional or rest parameters (the count varies per call) are not supported yet + + 6 | const replaced = "a-b".replace(/-/, function (match) { + 7 | return String(arguments.length) + match; + | ^~~~~~~~~~~~~~~~ + 8 | }); + +function-value-arguments-length.ts:13:10 - error SC1090: 'arguments.length' in function expressions passed where the expected function type has optional or rest parameters (the count varies per call) are not supported yet + + 12 | const counted = call(function (a) { + 13 | return arguments.length + a; + | ^~~~~~~~~~~~~~~~ + 14 | }); + +function-value-arguments-length.ts:18:22 - error SC1090: functions that read 'arguments.length' used as values where the expected function type has optional or rest parameters (the count varies per call) are not supported yet + + 17 | } + 18 | const offsets = call(offset); + | ^~~~~~ + 19 | class Scale { + +function-value-arguments-length.ts:21:20 - error SC1090: 'arguments.length' in methods, nested, async and generator functions used as values (a caller of the value can pass more arguments than the function declares) are not supported yet + + 20 | static by(value: number): number { + 21 | return value * arguments.length; + | ^~~~~~~~~~~~~~~~ + 22 | } + +function-value-arguments-length.ts:32:30 - error SC1090: 'arguments.length' in an arrow function inside a function that is also used as a value is not supported yet + + 31 | function deferred(value: number): number { + 32 | const read = (): number => arguments.length; + | ^~~~~~~~~~~~~~~~ + 33 | return value + read(); + +function-value-arguments-length.ts:33:18 - error SC2004: uses of 'read' inherit the blocker on its declaration + + 32 | const read = (): number => arguments.length; + 33 | return value + read(); + | ^~~~ + 34 | } + + hint: the declaration of 'read' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file diff --git a/tests/harness/corpus-costs/7381-function-value-arguments-length.ts.json b/tests/harness/corpus-costs/7381-function-value-arguments-length.ts.json new file mode 100644 index 000000000..b4659b9a0 --- /dev/null +++ b/tests/harness/corpus-costs/7381-function-value-arguments-length.ts.json @@ -0,0 +1 @@ +{ "plain": 3400, "san": 3300 } From a3b9cfb1466649db971d5562405a8a01f028c048 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 18:51:14 -0500 Subject: [PATCH 020/409] Find checker reply element ends in C and list dynamic imports from module references --- packages/compiler/native/ts7-json.test.c | 33 +++++ packages/compiler/native/ts7-process.c | 42 +++++++ packages/compiler/native/ts7-process.ffi.json | 3 +- packages/compiler/native/ts7-process.h | 4 + .../src/frontend/lowering/lower-modules.ts | 30 +++-- .../src/frontend/ts7/native-process.ts | 6 + .../src/frontend/ts7/semantic-json.ts | 26 +++- .../test/native-json-value-end.test.ts | 118 ++++++++++++++++++ 8 files changed, 243 insertions(+), 19 deletions(-) create mode 100644 packages/compiler/native/ts7-json.test.c create mode 100644 packages/compiler/test/native-json-value-end.test.ts diff --git a/packages/compiler/native/ts7-json.test.c b/packages/compiler/native/ts7-json.test.c new file mode 100644 index 000000000..b696d77c2 --- /dev/null +++ b/packages/compiler/native/ts7-json.test.c @@ -0,0 +1,33 @@ +/* Reads cases from standard input, each a little-endian u32 length, a u32 + * start and the bytes, and prints scriptc_ts7_json_value_end for each. */ +#include "ts7-process.h" +#include +#include +#ifdef _WIN32 +#include +#include +#endif + +static int read_u32(uint32_t *value) { + unsigned char bytes[4]; + if (fread(bytes, 1, 4, stdin) != 4) return 0; + *value = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 | (uint32_t)bytes[2] << 16 | + (uint32_t)bytes[3] << 24; + return 1; +} + +int main(void) { +#ifdef _WIN32 + /* The cases are bytes: text mode would rewrite line endings. */ + _setmode(_fileno(stdin), _O_BINARY); +#endif + uint32_t length, start; + while (read_u32(&length)) { + if (!read_u32(&start)) return 1; + uint8_t *bytes = malloc(length == 0 ? 1 : length); + if (bytes == NULL || fread(bytes, 1, length, stdin) != length) return 1; + printf("%d\n", (int)scriptc_ts7_json_value_end(bytes, length, start)); + free(bytes); + } + return 0; +} diff --git a/packages/compiler/native/ts7-process.c b/packages/compiler/native/ts7-process.c index ea451345d..3333cf4e8 100644 --- a/packages/compiler/native/ts7-process.c +++ b/packages/compiler/native/ts7-process.c @@ -405,3 +405,45 @@ void scriptc_ts7_close(uint32_t handle) { void scriptc_ts7_close_all(void) { while (processes != NULL) scriptc_ts7_close(processes->id); } + +static int json_space(uint8_t byte) { + return byte == ' ' || byte == '\n' || byte == '\r' || byte == '\t'; +} + +int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start) { + if (size > INT32_MAX) return -1; + size_t depth = 0; + size_t index = start; + while (index < size) { + uint8_t byte = bytes[index]; + if (byte == '"') { + index++; + for (;;) { + if (index >= size) return -1; + const uint8_t *quote = memchr(bytes + index, '"', size - index); + if (quote == NULL) return -1; + const uint8_t *escape = memchr(bytes + index, '\\', (size_t)(quote - (bytes + index))); + if (escape == NULL) { + index = (size_t)(quote - bytes); + break; + } + index = (size_t)(escape - bytes) + 2; + } + index++; + if (depth == 0) return (int32_t)index; + continue; + } + if (byte == '{' || byte == '[') { + depth++; + } else if (byte == '}' || byte == ']') { + if (depth == 0) return (int32_t)index; + depth--; + if (depth == 0) return (int32_t)(index + 1); + } else if (depth == 0 && (byte == ',' || json_space(byte))) { + return (int32_t)index; + } + index++; + } + if (depth != 0) return -1; + return (int32_t)index; +} diff --git a/packages/compiler/native/ts7-process.ffi.json b/packages/compiler/native/ts7-process.ffi.json index 86f3cb4d2..9a9e39526 100644 --- a/packages/compiler/native/ts7-process.ffi.json +++ b/packages/compiler/native/ts7-process.ffi.json @@ -7,6 +7,7 @@ { "name": "ts7NativePid", "symbol": "scriptc_ts7_pid", "params": ["u32"], "returns": "u32" }, { "name": "ts7NativeClose", "symbol": "scriptc_ts7_close", "params": ["u32"], "returns": "void" }, { "name": "ts7NativeCloseAll", "symbol": "scriptc_ts7_close_all", "params": [], "returns": "void" }, - { "name": "ts7NativeError", "symbol": "scriptc_ts7_error", "params": ["mutable-bytes"], "returns": "i32" } + { "name": "ts7NativeError", "symbol": "scriptc_ts7_error", "params": ["mutable-bytes"], "returns": "i32" }, + { "name": "ts7NativeJsonValueEnd", "symbol": "scriptc_ts7_json_value_end", "params": ["bytes", "u32"], "returns": "i32" } ] } diff --git a/packages/compiler/native/ts7-process.h b/packages/compiler/native/ts7-process.h index 3d2a0ba05..5bc0d11c4 100644 --- a/packages/compiler/native/ts7-process.h +++ b/packages/compiler/native/ts7-process.h @@ -21,5 +21,9 @@ uint32_t scriptc_ts7_pid(uint32_t handle); void scriptc_ts7_close(uint32_t handle); void scriptc_ts7_close_all(void); int32_t scriptc_ts7_error(uint8_t *bytes, size_t size); +/* The index after the JSON value that starts at `start` in a checker reply, + * as jsonValueEnd in semantic-json.ts computes it, or -1 when the reply is + * malformed there. */ +int32_t scriptc_ts7_json_value_end(const uint8_t *bytes, size_t size, uint32_t start); #endif diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 5238333ad..1301a152f 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -346,24 +346,28 @@ function dynamicImportProgramTargetOf( } /** Every distinct program module a file's `import("literal")` sites name - * (source order, deduped). */ + * (source order, deduped). The parser lists the specifier of every such call + * in the file's module references, in source order after the static imports, + * so the file's nodes need no walk. */ function dynamicProgramImportsOf(program: ts.Program, sf: ts.SourceFile): ts.SourceFile[] { const out: ts.SourceFile[] = []; const seen = new Set(); - ts.walkPreorder(sf, (node) => { + for (const specifier of sf.imports) { + const call = specifier.parent; if ( - ts.isCallExpression(node) && - node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments.nodes[0] !== undefined && - ts.isStringLiteralLike(node.arguments.nodes[0]) - ) { - const dep = dynamicImportProgramTargetOf(program, sf, node.arguments.nodes[0].text); - if (dep && !seen.has(dep)) { - seen.add(dep); - out.push(dep); - } + call === undefined || + !ts.isCallExpression(call) || + call.expression.kind !== ts.SyntaxKind.ImportKeyword || + call.arguments.nodes[0] !== specifier || + !ts.isStringLiteralLike(specifier) + ) + continue; + const dep = dynamicImportProgramTargetOf(program, sf, specifier.text); + if (dep && !seen.has(dep)) { + seen.add(dep); + out.push(dep); } - }); + } return out; } diff --git a/packages/compiler/src/frontend/ts7/native-process.ts b/packages/compiler/src/frontend/ts7/native-process.ts index 7bb523d74..b36189e64 100644 --- a/packages/compiler/src/frontend/ts7/native-process.ts +++ b/packages/compiler/src/frontend/ts7/native-process.ts @@ -1,5 +1,6 @@ import { Ts7Wire, type Ts7WireIo } from "./rpc-wire.js"; import { TS7_FILE_SYSTEM_CALLBACKS } from "./rpc-filesystem.js"; +import { useNativeJsonValueEnd } from "./semantic-json.js"; // Bound by native/ts7-process.ffi.json. This module is a native entry point; // the Node host continues to use rpc-process.ts's public spawn boundary. @@ -25,6 +26,11 @@ declare function ts7NativePid(handle: number): number; declare function ts7NativeClose(handle: number): void; declare function ts7NativeCloseAll(): void; declare function ts7NativeError(bytes: Uint8Array): number; +declare function ts7NativeJsonValueEnd(bytes: Uint8Array, start: number): number; + +// Reply decoding finds element ends with the C scanner. FFI functions can +// only be called, not passed as values, so an arrow function calls it. +useNativeJsonValueEnd((bytes, start) => ts7NativeJsonValueEnd(bytes, start)); // process.exit skips C atexit handlers. Its ordinary exit listener closes // servers too, while the C atexit sweep covers normal native termination. diff --git a/packages/compiler/src/frontend/ts7/semantic-json.ts b/packages/compiler/src/frontend/ts7/semantic-json.ts index 3a04c741f..1a4d93bd6 100644 --- a/packages/compiler/src/frontend/ts7/semantic-json.ts +++ b/packages/compiler/src/frontend/ts7/semantic-json.ts @@ -76,8 +76,8 @@ function malformedReply(): never { /** The index after the JSON value that starts at `start`: an object, an * array, a string or a bare literal. Strings are skipped with their escapes, - * so brackets inside them do not count. */ -function jsonValueEnd(bytes: Uint8Array, start: number): number { + * so brackets inside them do not count. Throws when the value is unclosed. */ +export function jsonValueEnd(bytes: Uint8Array, start: number): number { let depth = 0; let index = start; while (index < bytes.length) { @@ -103,6 +103,22 @@ function jsonValueEnd(bytes: Uint8Array, start: number): number { return index; } +/** The scanner the decoders find value ends with: jsonValueEnd, or the + * native compiler's scanner (useNativeJsonValueEnd). */ +let valueEnd: (bytes: Uint8Array, start: number) => number = jsonValueEnd; + +/** Has the decoders find value ends with `scan`, which returns what + * jsonValueEnd returns, or -1 where jsonValueEnd throws. The native compiler + * installs its C scanner (native/ts7-process.c), which skips the strings that + * make up most reply bytes with memchr. */ +export function useNativeJsonValueEnd(scan: (bytes: Uint8Array, start: number) => number): void { + valueEnd = (bytes, start) => { + const end = scan(bytes, start); + if (end < 0) malformedReply(); + return end; + }; +} + /** The id an object reply starts with (`{"id":N`), or -1 when it starts * otherwise. */ function leadingId(bytes: Uint8Array, start: number, end: number): number { @@ -158,7 +174,7 @@ export function decodeSemanticObject( ): R | null { const start = skipJsonSpace(bytes, 0); if (start === bytes.length) return null; - const end = jsonValueEnd(bytes, start); + const end = valueEnd(bytes, start); if (skipJsonSpace(bytes, end) !== bytes.length) malformedReply(); return replyElement(bytes, start, end, cached, build); } @@ -174,7 +190,7 @@ export function decodeSemanticObjects( let index = skipJsonSpace(bytes, 0); if (index === bytes.length) return null; if (bytes[index] !== OPEN_BRACKET) { - const end = jsonValueEnd(bytes, index); + const end = valueEnd(bytes, index); if ( skipJsonSpace(bytes, end) !== bytes.length || parseJsonSlice(bytes, index, end) !== null @@ -189,7 +205,7 @@ export function decodeSemanticObjects( return result; } for (;;) { - const end = jsonValueEnd(bytes, index); + const end = valueEnd(bytes, index); if (end === index) malformedReply(); result.push(replyElement(bytes, index, end, cached, build)); index = skipJsonSpace(bytes, end); diff --git a/packages/compiler/test/native-json-value-end.test.ts b/packages/compiler/test/native-json-value-end.test.ts new file mode 100644 index 000000000..9d9cc3950 --- /dev/null +++ b/packages/compiler/test/native-json-value-end.test.ts @@ -0,0 +1,118 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { jsonValueEnd } from "../src/frontend/ts7/semantic-json.js"; + +const sources = join(import.meta.dirname, "../native"); +const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; + +/** jsonValueEnd's result, or -1 where it throws. */ +function expectedEnd(bytes: Uint8Array, start: number): number { + try { + return jsonValueEnd(bytes, start); + } catch { + return -1; + } +} + +/** A deterministic generator, so a failing case can be reproduced. */ +function random(seed: number): () => number { + let state = seed; + return () => { + state = (state * 1103515245 + 12345) % 2147483648; + return state / 2147483648; + }; +} + +const ALPHABET = ["{", "}", "[", "]", '"', "\\", ",", " ", "\n", "\t", "a", "1", "n", ":", "é"]; + +function cases(): { bytes: Uint8Array; start: number }[] { + const next = random(1); + const out: { bytes: Uint8Array; start: number }[] = []; + const texts = [ + "", + '""', + '"unclosed', + '"\\"', + '"\\\\"', + '"a\\"b"', + '"\\\\\\""', + "null", + "null,", + "123 ", + '{"id":1}', + '{"id":1,"name":"a}b"}', + '[{"id":1},null,{"id":2,"declarations":["1.2./a.ts","3.4./b \\"c\\".ts"]}]', + '{"id":1,"nested":{"list":[1,{"x":"]"}]}}, next', + "{", + "[[]", + "}", + "]", + ",", + " ", + ]; + for (const text of texts) { + const bytes = Buffer.from(text, "utf8"); + for (let start = 0; start <= bytes.length; start++) out.push({ bytes, start }); + } + for (let n = 0; n < 4000; n++) { + let text = ""; + const length = Math.floor(next() * 40); + for (let k = 0; k < length; k++) text += ALPHABET[Math.floor(next() * ALPHABET.length)]; + const bytes = Buffer.from(text, "utf8"); + out.push({ bytes, start: Math.floor(next() * (bytes.length + 1)) }); + } + const reply = JSON.stringify( + Array.from({ length: 200 }, (_, id) => + id % 7 === 0 + ? null + : { id, name: `name ${id} "q" \\ {[`, declarations: [`${id}.80./src/a ${id}.ts`] }, + ), + ); + const bytes = Buffer.from(reply, "utf8"); + for (let start = 0; start < bytes.length; start += 3) out.push({ bytes, start }); + return out; +} + +test("the native scanner finds the value ends jsonValueEnd finds", () => { + const dir = mkdtempSync(join(tempRoot, "scriptc-ts7-json-")); + try { + const harness = join(dir, "scan" + (process.platform === "win32" ? ".exe" : "")); + execFileSync("clang", [ + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + ...(process.env["SCRIPTC_SAN"] === "1" ? ["-fsanitize=address,undefined"] : []), + join(sources, "ts7-json.test.c"), + join(sources, "ts7-process.c"), + "-o", + harness, + ]); + const all = cases(); + const chunks: Buffer[] = []; + for (const { bytes, start } of all) { + const header = Buffer.alloc(8); + header.writeUInt32LE(bytes.length, 0); + header.writeUInt32LE(start, 4); + chunks.push(header, Buffer.from(bytes)); + } + const output = execFileSync(harness, { input: Buffer.concat(chunks), encoding: "utf8" }); + const actual = output.trim().split(/\r?\n/).map(Number); + expect(actual.length).toBe(all.length); + for (let k = 0; k < all.length; k++) { + const { bytes, start } = all[k]!; + const expected = expectedEnd(bytes, start); + if (actual[k] !== expected) + expect({ text: Buffer.from(bytes).toString("utf8"), start, end: actual[k] }).toEqual({ + text: Buffer.from(bytes).toString("utf8"), + start, + end: expected, + }); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); From b386e9195805895dff7050f6b5b1702dd352152b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:50:12 -0500 Subject: [PATCH 021/409] Compile instanceof against constructors held as values --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../lowering/class-dynamic-dispatch.ts | 128 ++++++++++++------ .../src/frontend/lowering/dispatch-sites.ts | 1 + .../lowering/expressions/relational.ts | 63 ++++++++- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 3 + packages/compiler/src/ir/int32-slots.ts | 1 + .../main.ts.json | 7 + ...anceof-builtin-constructor-values.mjs.json | 6 + ...24-instanceof-class-values-native.mjs.json | 6 + ...25-instanceof-dynamic-constructors.ts.json | 6 + packages/runtime/src/scr_dyn.h | 1 + packages/runtime/src/scr_json.c | 47 +++++-- .../errors.cjs | 24 ++++ .../6349-instanceof-subclass-capsules/main.ts | 27 ++++ ...-instanceof-builtin-constructor-values.mjs | 13 ++ .../6924-instanceof-class-values-native.mjs | 19 +++ .../6925-instanceof-dynamic-constructors.ts | 64 +++++++++ .../main.ts.json | 1 + ...anceof-builtin-constructor-values.mjs.json | 1 + ...24-instanceof-class-values-native.mjs.json | 1 + ...25-instanceof-dynamic-constructors.ts.json | 1 + 22 files changed, 364 insertions(+), 58 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json create mode 100644 tests/corpus/6349-instanceof-subclass-capsules/errors.cjs create mode 100644 tests/corpus/6349-instanceof-subclass-capsules/main.ts create mode 100644 tests/corpus/6923-instanceof-builtin-constructor-values.mjs create mode 100644 tests/corpus/6924-instanceof-class-values-native.mjs create mode 100644 tests/corpus/6925-instanceof-dynamic-constructors.ts create mode 100644 tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json create mode 100644 tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json create mode 100644 tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json create mode 100644 tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 0b6041034..e8d4e01c6 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1555,6 +1555,7 @@ export const LIB_FN_SYMS: Record = { "dyn.nameClassPrototype": "scr_dyn_name_class_prototype", "dyn.objCreateWithProperties": "scr_dyn_obj_create_with_properties", "dyn.getPrototype": "scr_dyn_get_prototype", + "dyn.getPrototypeForIdentity": "scr_dyn_get_prototype_for_identity", "dyn.setPrototype": "scr_dyn_set_prototype", "dyn.getOwnPropertyNames": "scr_dyn_get_own_property_names", "dyn.ownKeys": "scr_dyn_own_keys", diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 8a4a1e417..b18615ef6 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -350,7 +350,7 @@ export function classInstanceOf( localId: "candidate", value: { kind: "libCall", - fn: "dyn.getPrototype", + fn: "dyn.getPrototypeForIdentity", args: [candidate], type: DYN, loc, @@ -862,6 +862,9 @@ export class ClassDynamicDispatch { private deletedNames: Set | null | undefined; private readonly boxed = new Set(); private readonly boxedConstructors = new Set(); + /** Every class a boxed value was statically typed as, runtime error + * classes included: the value's capsule can be any of its subclasses. */ + private readonly boxedStatic = new Set(); private readonly dispatches = new Map(); private readonly properties = new Map(); private readonly computed = new Map< @@ -906,8 +909,12 @@ export class ClassDynamicDispatch { private descriptorHelper: IrFunction | null = null; private prototypeHelper: IrFunction | null = null; private readonly prototypeClasses = new Set(); - private prototypeIdentityHelper: IrFunction | null = null; - private readonly prototypeIdentityClasses = new Set(); + /** The prototype reads of identity walks, by libCall, and the classes + * each one answers for boxed instances. */ + private readonly prototypeIdentityHelpers = new Map< + "dyn.getPrototype" | "dyn.getPrototypeForIdentity", + { fn: IrFunction; classes: Set } + >(); private readonly instancePrototypes = new Map }>(); private readonly checkedCasts = new Map }>(); /** The for-in key helpers, by libCall (dyn and native string[] keys). */ @@ -956,6 +963,7 @@ export class ClassDynamicDispatch { if (seenTypes.has(key)) return; seenTypes.add(key); if (type.kind === "object") { + this.boxedStatic.add(type.className); if (isDynTypedRefType(type)) this.boxed.add(type.className); } else if (type.kind === "classval") this.boxedConstructors.add(type.className); else if (type.kind === "array") discover(type.elem); @@ -1087,33 +1095,6 @@ export class ClassDynamicDispatch { }); changed = true; } - if (this.prototypeIdentityHelper && !this.prototypeIdentityClasses.has(className)) { - this.prototypeIdentityClasses.add(className); - const loc = this.prototypeIdentityHelper.loc; - const type: IrType = { kind: "object", className }; - const value = varRef("value", DYN, loc); - const prototype = classPrototypeData(lowerer, info, loc, { - kind: "dynCheck", - value, - type, - loc, - }); - if (prototype) - this.prototypeIdentityHelper.body.unshift({ - kind: "if", - cond: { - kind: "libCall", - fn: "dyn.typedRefIs", - args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], - type: BOOL, - loc, - }, - then: [{ kind: "return", value: prototype, loc }], - else_: null, - loc, - }); - changed = true; - } if ( this.serializesClasses && !this.jsonPrototypes.has(className) && @@ -1181,6 +1162,53 @@ export class ClassDynamicDispatch { } } } + // The runtime boxes a class instance as its exact class, so a value + // boxed at a base (or Error) static type reaches the identity views as + // any subclass's capsule. + for (const helper of this.prototypeIdentityHelpers.values()) + for (const [className, info] of lowerer.classes) { + if (helper.classes.has(className)) continue; + if (info.builtinEmitter || info.builtinStream || info.builtinError) continue; + if (!isDynTypedRefType({ kind: "object", className })) continue; + if ( + !this.boxed.has(className) && + ![...this.boxedStatic].some((base) => lowerer.isSubclassOf(className, base)) + ) + continue; + helper.classes.add(className); + const loc = helper.fn.loc; + const type: IrType = { kind: "object", className }; + const value = varRef("value", DYN, loc); + // A class with no prototype data in its chain (one extending + // Error, or with no methods) has no prototype a program can name: + // a fresh object stands in, so an identity comparison or an + // instanceof walk (through Object.prototype to null) answers false + // instead of reaching the reflection refusal. + const standIn: IrExpr = { kind: "dynObjLit", fields: [], type: DYN, loc }; + const prototype = hasClassPrototypeData(info) + ? classPrototypeData(lowerer, info, loc, { + kind: "dynCheck", + value, + type, + loc, + }) + : standIn; + if (prototype) + helper.fn.body.unshift({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then: [{ kind: "return", value: prototype, loc }], + else_: null, + loc, + }); + changed = true; + } for (const plan of this.propertyReceivers(lowerer, false, false)) { const info = plan.info; const className = info.def.name; @@ -1518,29 +1546,41 @@ export class ClassDynamicDispatch { } if ( expr.kind === "libCall" && - expr.fn === "dyn.getPrototype" && + (expr.fn === "dyn.getPrototype" || expr.fn === "dyn.getPrototypeForIdentity") && expr.args[0]?.kind !== "dynObjLit" ) { if (expr.prototypeIdentityOnly) { - if (!this.prototypeIdentityHelper) { + let helper = this.prototypeIdentityHelpers.get(expr.fn); + if (!helper) { const loc = expr.loc; - this.prototypeIdentityHelper = { - name: "%dyn.class.prototypeIdentity", - params: [{ localId: "value", name: "value", type: DYN }], - locals: [{ id: "value", name: "value", type: DYN, mutable: false }], - returnType: DYN, - loc, - body: [ - { kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc }, - ], + helper = { + fn: { + name: + expr.fn === "dyn.getPrototype" + ? "%dyn.class.prototypeIdentity" + : "%dyn.class.prototypeChainIdentity", + params: [{ localId: "value", name: "value", type: DYN }], + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], + returnType: DYN, + loc, + body: [ + { + kind: "return", + value: { ...expr, args: [varRef("value", DYN, loc)] }, + loc, + }, + ], + }, + classes: new Set(), }; - lowerer.liftedFns.push(this.prototypeIdentityHelper); - this.generated.add(this.prototypeIdentityHelper); + this.prototypeIdentityHelpers.set(expr.fn, helper); + lowerer.liftedFns.push(helper.fn); + this.generated.add(helper.fn); changed = true; } return { kind: "call", - callee: this.prototypeIdentityHelper.name, + callee: helper.fn.name, args: expr.args, type: DYN, loc: expr.loc, diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index c8a8d6ba6..1dcffc811 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -80,6 +80,7 @@ const DISPATCH_LIB_CALLS = new Set([ "dyn.propertyIsEnumerableComputed", "dyn.defineProperty", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.reflectGet", "dyn.reflectSet", ]); diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 759687ff0..ac8fdc96b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -57,6 +57,44 @@ import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors import { hasRuntimeStatics } from "../class-runtime-statics.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; +/** The class value an `instanceof` right-hand side holds. A JavaScript + * object's property holding a program class (`holder.type`) reads as a + * checked-dynamic value that the checker types as the class: it converts + * to the class value. Runtime-provided classes keep their own lowering. */ +function classValueOperand( + lowerer: Lowerer, + right: ts.Expression, + type: IrType & { kind: "classval" }, +): IrExpr { + if ( + !isJsSourceFile(ts.getSourceFileOfNode(right)) || + ts.isIdentifier(right) || + lowerer.classes.get(type.className)?.def.runtime + ) + return lowerer.lowerExpr(right); + const value = lowerer.lowerExprExpecting(right, DYN); + if (value.type.kind !== "dyn") return value; + return { kind: "dynCheck", value, type, loc: locOf(right) }; +} + +/** The value kinds that never hold an instance of a program class. */ +const NO_CLASS_INSTANCE_KINDS: ReadonlySet = new Set([ + "f64", + "bigint", + "date", + "string", + "bool", + "array", + "map", + "set", + "regex", + "bytes", + "symbol", + "promise", + "undefinedT", + "nullT", +]); + /** `x instanceof C` for a program-declared class C. When x's static * class and C are both in extends-hierarchies the test is dynamic — the * O(1) preorder-interval check against the vtable (`instanceOf` node). @@ -427,7 +465,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc "'instanceof' through a runtime-provided class value on an untyped value (give the operand a class type first)", ); } - const classValue = lowerer.lowerExpr(expr.right); + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); return classInstanceOf(lowerer, left, targetInfo, loc, classValue); } // A union of class instances against a runtime class value (`new K()` @@ -436,6 +474,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const tested = unionClassValueInstanceOf(lowerer, left, expr.right, targetInfo, loc); if (tested) return tested; } + // A value whose type holds no class instance (a primitive, a native + // collection, a date, a regex, an array) is never an instance + // of a program class: false, after both operands evaluate. + if (!targetInfo.def.runtime && NO_CLASS_INSTANCE_KINDS.has(left.type.kind)) { + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); + return { + kind: "seqExpr", + stmts: [ + { kind: "exprStmt", expr: left, loc }, + { kind: "exprStmt", expr: classValue, loc }, + ], + result: { kind: "boolLit", value: false, type: BOOL, loc }, + type: BOOL, + loc, + }; + } if (left.type.kind !== "object") { lowerer.unsupported( "SC1090", @@ -450,7 +504,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc (lowerer.inHierarchy(targetInfo) && lowerer.inHierarchy(lhsInfo)) || (targetInfo.localClass !== undefined && lhsInfo.localClass !== undefined) ) { - const classValue = lowerer.lowerExpr(expr.right); + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); if (classValue.type.kind !== "classval") lowerer.badType(expr.right, lowerer.typeOf(expr.right)); const writableTest = writableClassValueTest( @@ -488,8 +542,11 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc "statically-decided 'instanceof' on computed operands (bind the values to variables first)", ); } + // A checked-dynamic right-hand side (a constructor read from a table, a + // structural constructor type, a builtin constructor held as `any`): + // the runtime's instanceof over the value, in both source languages. const constructor = tryLowerExpression(lowerer, expr.right); - if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { + if (constructor?.type.kind === "dyn") { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return checkedClassInstanceOf( lowerer, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index eb3f8c6ab..503ea94a9 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -630,6 +630,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.objCreate", "dyn.objCreateWithProperties", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.setPrototype", // Error.captureStackTrace: Node's TypeError for a primitive target, and // the error string conversion of name and message diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 8edb223b3..7af1d4ef9 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3749,6 +3749,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.nameClassPrototype": { argTypes: [DYN, STRING], result: VOID }, "dyn.objCreateWithProperties": { argTypes: [DYN, DYN], result: DYN }, "dyn.getPrototype": { argTypes: [DYN], result: DYN }, + // dyn.getPrototype for walks that compare each prototype with a program + // class's prototype: native builtin values end the walk (null). + "dyn.getPrototypeForIdentity": { argTypes: [DYN], result: DYN }, "dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN }, "dyn.getOwnPropertyNames": { argTypes: [DYN], result: DYN }, "dyn.ownKeys": { argTypes: [DYN], result: DYN }, diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index ac0747276..ec2ebd3ab 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -365,6 +365,7 @@ const DYN_READ_ONLY = new Set([ "dyn.forInStable", "dyn.getOwnPropertyNames", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.hasKey", "dyn.hasOwn", "dyn.in", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json new file mode 100644 index 000000000..6b31ee9ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs", + "/tests/corpus/6349-instanceof-subclass-capsules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json new file mode 100644 index 000000000..a2184f9a3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6923-instanceof-builtin-constructor-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json new file mode 100644 index 000000000..54090ccc3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6924-instanceof-class-values-native.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json new file mode 100644 index 000000000..20085dc0d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6925-instanceof-dynamic-constructors.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 74b710dc9..d9a230276 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -917,6 +917,7 @@ void *scr_dyn_obj_source_cast(const ScrDyn *d, ScrDyn *scr_dyn_new_obj_null_proto(void); ScrDyn *scr_dyn_obj_create(ScrDyn *prototype); ScrDyn *scr_dyn_obj_create_with_properties(ScrDyn *prototype, ScrDyn *descriptors); +ScrDyn *scr_dyn_get_prototype_for_identity(ScrDyn *object); ScrDyn *scr_dyn_get_prototype(ScrDyn *object); ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object); ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index cf658313f..43596272c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2147,6 +2147,26 @@ ScrDyn *scr_dyn_get_prototype(ScrDyn *object) { return NULL; } +/* scr_dyn_get_prototype for a walk that compares each prototype with a + * program class's prototype (instanceof against a class value): a native + * Date, RegExp, Map, Set, WeakMap or WeakSet, or a promise, has only builtin + * prototypes on its chain, and no program class extends these (a class + * extending Date makes instances of its own), so the walk ends there with + * null. */ +ScrDyn *scr_dyn_get_prototype_for_identity(ScrDyn *object) { + if (object->kind == SCR_DYN_PROMISE) return scr_dyn_new_null(); + if (object->kind == SCR_DYN_HANDLE) { + switch (object->v.handle.tag) { + case SCR_DYNH_DATE: case SCR_DYNH_REGEXP: case SCR_DYNH_MAP: case SCR_DYNH_SET: + case SCR_DYNH_WEAK_MAP: case SCR_DYNH_WEAK_SET: + return scr_dyn_new_null(); + default: + break; + } + } + return scr_dyn_get_prototype(object); +} + ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { /* A primitive receiver is returned unchanged when the prototype is null or * any object or function; nothing is mutated, so the prototype's kind is the @@ -3956,27 +3976,32 @@ ScrDyn *scr_array_buffer_constructor(void) { /* `value instanceof C` for C a global constructor held as a value * (`{ instanceOf: Date }`, scr_dyn_global_constructor): 1 or 0 by the - * value's native kind, or -1 when callee is not one of them, or for Object - * and for class instances, which these kinds cannot answer (except Date, - * whose subclasses' instances are known). A typed record or array is none - * of these kinds. */ + * value's native kind, or -1 when callee is not one of them, or for Object. + * A typed record or array is none of these kinds, and neither is an + * instance of a program class: a class can extend Date (answered through + * scr_dyn_date_instance_is), and extending RegExp, Map, Set, WeakMap, + * WeakSet or Promise refuses where the class is defined. The error + * constructors (scr_dyn_error_constructor) answer -1. */ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrDyn *callee) { if (!callee->v.fn.sig || !callee->v.fn.name || strcmp(callee->v.fn.sig, "native:constructor") != 0 || strcmp(callee->v.fn.name, "Object") == 0) return -1; /* A native Date, or an instance of a class declared `extends Date`. */ if (strcmp(callee->v.fn.name, "Date") == 0) return scr_dyn_date_instance_is(value); - if (value->kind == SCR_DYN_TYPED_REF) - return strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value) ? 0 : -1; static const struct { const char *name; int tag; } kinds[] = { - {"Date", SCR_DYNH_DATE}, {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, + {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, {"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET}, }; - if (strcmp(callee->v.fn.name, "Promise") == 0) return value->kind == SCR_DYN_PROMISE; - for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) { - if (strcmp(callee->v.fn.name, kinds[i].name) == 0) - return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == kinds[i].tag; + bool promise = strcmp(callee->v.fn.name, "Promise") == 0; + int tag = -1; + for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) + if (strcmp(callee->v.fn.name, kinds[i].name) == 0) tag = kinds[i].tag; + if (value->kind == SCR_DYN_TYPED_REF) { + if (strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value)) return 0; + return (promise || tag >= 0) && strncmp(value->v.typed_ref.type_key, "object:", 7) == 0 ? 0 : -1; } + if (promise) return value->kind == SCR_DYN_PROMISE; + if (tag >= 0) return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == tag; return -1; } diff --git a/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs b/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs new file mode 100644 index 000000000..bf32f8550 --- /dev/null +++ b/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs @@ -0,0 +1,24 @@ +'use strict'; +class UndiciError extends Error { + constructor(message) { + super(message); + this.name = 'UndiciError'; + this.code = 'UND_ERR'; + } + get kind() { + return 'undici'; + } +} +class SocketError extends UndiciError { + constructor(message) { + super(message); + this.name = 'SocketError'; + } +} +class HTTPParserError extends Error { + constructor(message) { + super(message); + this.name = 'HTTPParserError'; + } +} +module.exports = { UndiciError, SocketError, HTTPParserError }; diff --git a/tests/corpus/6349-instanceof-subclass-capsules/main.ts b/tests/corpus/6349-instanceof-subclass-capsules/main.ts new file mode 100644 index 000000000..59a545198 --- /dev/null +++ b/tests/corpus/6349-instanceof-subclass-capsules/main.ts @@ -0,0 +1,27 @@ +// `instanceof` against a JavaScript class reads the prototype chain of a +// value that crossed into the dynamic tier at a base static type (here +// Error): the value is its exact class at runtime, including a subclass +// without methods of its own, which answers false rather than throwing. +import errors from "./errors.cjs"; + +function describe(err: unknown): string { + if (err instanceof errors.UndiciError) return `undici ${err.name}`; + if (err instanceof Error) return `error ${err.name}`; + return "other"; +} +function isSocket(err: unknown): boolean { + return err instanceof errors.SocketError; +} +function fail(kind: number): Error { + if (kind === 0) return new errors.HTTPParserError("bad chunk"); + if (kind === 1) return new errors.SocketError("closed"); + return new RangeError("range"); +} +for (let kind = 0; kind < 3; kind++) { + const err: Error = fail(kind); + console.log(describe(err), isSocket(err)); +} +const base: Error = new errors.UndiciError("base"); +console.log(describe(base), isSocket(base)); +const rejected = Promise.reject(fail(0)); +rejected.catch((err: unknown) => console.log("rejected", describe(err))); diff --git a/tests/corpus/6923-instanceof-builtin-constructor-values.mjs b/tests/corpus/6923-instanceof-builtin-constructor-values.mjs new file mode 100644 index 000000000..929b048e3 --- /dev/null +++ b/tests/corpus/6923-instanceof-builtin-constructor-values.mjs @@ -0,0 +1,13 @@ +// `instanceof` against builtin constructors held as values, with instances of +// program classes and native values on the left. +class Dog { + constructor(name) { + this.name = name; + } +} +class Puppy extends Dog {} +const kinds = [Map, Set, RegExp, Promise, WeakMap, WeakSet, Date]; +const values = []; +values.push(new Dog("rex"), new Puppy("bit"), new Map(), new Set(), /x/g, Promise.resolve(1)); +values.push(new WeakMap(), new WeakSet(), new Date(0), { name: "plain" }, [1, 2]); +for (const C of kinds) console.log(C.name, values.map((v) => (v instanceof C ? 1 : 0)).join("")); diff --git a/tests/corpus/6924-instanceof-class-values-native.mjs b/tests/corpus/6924-instanceof-class-values-native.mjs new file mode 100644 index 000000000..82e8bdcf2 --- /dev/null +++ b/tests/corpus/6924-instanceof-class-values-native.mjs @@ -0,0 +1,19 @@ +// `instanceof` against program classes held as values, with native builtin +// values on the left: their prototype chains hold no program class. +class Animal { + constructor(name) { + this.name = name; + } +} +class Dog extends Animal {} +class Cat extends Animal {} +const classes = { Animal: Animal, Dog: Dog, Cat: Cat }; +const values = []; +values.push(new Map(), new Set(), /x/, Promise.resolve(1), new Date(0), new WeakMap(), new WeakSet()); +values.push(new Dog("rex"), new Cat("tom"), new Animal("any"), {}, [], null); +for (const name of Object.keys(classes)) { + const C = classes[name]; + console.log(name, values.map((v) => (v instanceof C ? 1 : 0)).join("")); +} +const holder = { type: Dog }; +console.log(new Map() instanceof holder.type, new Dog("a") instanceof holder.type); diff --git a/tests/corpus/6925-instanceof-dynamic-constructors.ts b/tests/corpus/6925-instanceof-dynamic-constructors.ts new file mode 100644 index 000000000..0a39b92b2 --- /dev/null +++ b/tests/corpus/6925-instanceof-dynamic-constructors.ts @@ -0,0 +1,64 @@ +// `instanceof` in TypeScript against right-hand sides that are not class names: +// constructor-typed parameters, constructors read from tables as any, builtin +// constructors held as values, and class values held in properties. +class Animal { + name: string; + constructor(name: string) { this.name = name; } +} +class Dog extends Animal { + bark(): string { return this.name + " barks"; } +} +class Plain { v = 1; } + +// Constructor-typed parameters. +function isA(value: unknown, C: new (...args: never[]) => unknown): boolean { + return value instanceof C; +} +const dog = new Dog("rex"); +console.log(isA(dog, Animal), isA(dog, Dog), isA(new Animal("a"), Dog), isA(new Plain(), Animal), isA(dog, Plain)); +console.log(isA(new Date(0), Date), isA(new Map(), Map), isA({}, Map), isA("text", Animal), isA(null, Animal)); + +// Narrowing through a constructor-typed value. +function describe(value: Animal, C: typeof Dog): string { + if (value instanceof C) return value.bark(); + return value.name; +} +console.log(describe(dog, Dog), describe(new Animal("cat"), Dog)); + +// A table of constructors read as any. +const table: Record = { Date, Map, Set, RegExp, Promise, WeakMap, WeakSet, Dog }; +const samples: unknown[] = [new Date(0), new Map(), new Set(), /x/, Promise.resolve(1), new WeakMap(), new WeakSet(), [1], dog, { a: 1 }]; +for (const name of Object.keys(table)) { + const C = table[name]; + console.log(name, samples.map((s) => (s instanceof C ? 1 : 0)).join("")); +} + +// A right-hand side that is not callable throws Node's TypeError. +const notCallable: any = {}; +try { + console.log(dog instanceof notCallable); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} + +// Typed native values passed as unknown, against builtin constructors held as any. +const typedValues: [string, unknown][] = [ + ["map", new Map([["a", 1]])], + ["set", new Set([1])], + ["regex", /a/g], + ["promise", Promise.resolve(1)], + ["weak", new WeakMap()], + ["date", new Date(5)], + ["array", [1]], + ["record", { a: 1 }], +]; +const builtins: Record = { Map, Set, RegExp, Promise, WeakMap, Date }; +for (const [label, value] of typedValues) { + console.log(label, Object.keys(builtins).map((name) => (value instanceof builtins[name] ? 1 : 0)).join("")); +} + +// Class values held in properties, against native values and instances. +const holder = { type: Dog, base: Animal }; +const map = new Map(); +const pattern = /x/; +console.log(map instanceof holder.type, pattern instanceof holder.base, dog instanceof holder.type, dog instanceof holder.base); diff --git a/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json b/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json new file mode 100644 index 000000000..6474582a0 --- /dev/null +++ b/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2000 } diff --git a/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json b/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json new file mode 100644 index 000000000..53f54c1dd --- /dev/null +++ b/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 3810, "san": 5921 } diff --git a/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json b/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json new file mode 100644 index 000000000..8de22d657 --- /dev/null +++ b/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json @@ -0,0 +1 @@ +{ "plain": 3033, "san": 4212 } diff --git a/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json b/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json new file mode 100644 index 000000000..e2d372e0c --- /dev/null +++ b/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json @@ -0,0 +1 @@ +{ "plain": 3097, "san": 4707 } From 43917080c9f8e2af993a632a04178b9b720901c7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:01:47 -0500 Subject: [PATCH 022/409] Reach a class's methods from 'this' in its static initializers The cycle walk resolved only identifiers, so a static field initializer or static block that called a method through 'this' did not reach the method, and its reads of a cycle binding declared later lost their temporal-dead-zone checks. 'this' in static code now reaches the class's stored unit, as naming the class does. --- .../compiler/src/frontend/program.test.ts | 21 ++++++++++ packages/compiler/src/frontend/program.ts | 41 +++++++++++++++++++ .../7382-cycle-static-this-tdz/main.ts.json | 8 ++++ .../7382-cycle-static-this-tdz/alpha.ts | 11 +++++ .../corpus/7382-cycle-static-this-tdz/beta.ts | 26 ++++++++++++ .../corpus/7382-cycle-static-this-tdz/main.ts | 8 ++++ .../7382-cycle-static-this-tdz/main.ts.json | 1 + 7 files changed, 116 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json create mode 100644 tests/corpus/7382-cycle-static-this-tdz/alpha.ts create mode 100644 tests/corpus/7382-cycle-static-this-tdz/beta.ts create mode 100644 tests/corpus/7382-cycle-static-this-tdz/main.ts create mode 100644 tests/harness/corpus-costs/7382-cycle-static-this-tdz/main.ts.json diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index 5eb3e363e..6a068353a 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -405,6 +405,27 @@ test.for([ }, ["name"], ], + [ + // The partner's class calls a method through `this` in a static + // initializer; the method reads a const of the importer declared later. + { + "main.ts": 'import { name } from "./a.ts"; console.log(name);', + "a.ts": 'import { Probe } from "./b.ts"; export const name = "n"; console.log(Probe.value);', + "b.ts": + 'import { name } from "./a.ts"; export class Probe { static read(): string { return name; } static value = this.read(); }', + }, + ["name"], + ], + [ + // The same through a static block of a class stored by a const. + { + "main.ts": 'import { name } from "./a.ts"; console.log(name);', + "a.ts": 'import { Box } from "./b.ts"; export const name = "n"; console.log(Box.value);', + "b.ts": + 'import { name } from "./a.ts"; export const Box = class { static value = ""; static read(): string { return name; } static { this.value = this.read(); } };', + }, + ["name"], + ], [ // The partner's top level reads the importer's const before it ran. { diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index f42bc426b..c8384ef65 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -4112,6 +4112,13 @@ function cycleReachedDeferredReferences7( return "skip"; } if (ts.isImportDeclaration(node)) return "skip"; + if (node.kind === ts.SyntaxKind.ThisKeyword) { + // `this` in a static initializer or block names the class, so it + // reaches the class's methods as naming the class does. + const owner = staticThisClass7(node); + if (owner !== undefined && stored.has(owner)) reach(owner); + return undefined; + } if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined; if (inTypePosition7(node)) return undefined; const parent = node.parent; @@ -4497,6 +4504,40 @@ function returnsOnlyClosuresOver7( * expressions, decorators, computed member keys, static field * initializers and static blocks. Methods and instance fields wait for a * call or a construction. */ +/** The class whose static field initializer or static block evaluates `this` + * (through arrow functions, which keep the enclosing `this`), or undefined + * when another function or a class's instance side binds it. */ +function staticThisClass7(node: ts.Node): ts.ClassLikeDeclaration | undefined { + let child = node; + for (let n = node.parent; n !== undefined; child = n, n = n.parent) { + if (ts.isArrowFunction(n)) continue; + if (ts.isClassStaticBlockDeclaration(n)) return n.parent as ts.ClassLikeDeclaration; + if (ts.isPropertyDeclaration(n)) { + if (n.initializer !== child) return undefined; + return (ts.getCombinedModifierFlags(n) & ts.ModifierFlags.Static) !== 0 + ? (n.parent as ts.ClassLikeDeclaration) + : undefined; + } + if (ts.isComputedPropertyName(n)) { + // A computed key evaluates in the scope around the class. + const member = n.parent; + const cls = member?.parent; + if (cls === undefined) return undefined; + child = cls; + n = cls; + continue; + } + if ( + ts.isFunctionLike(n) || + ts.isClassDeclaration(n) || + ts.isClassExpression(n) || + ts.isSourceFile(n) + ) + return undefined; + } + return undefined; +} + function classSyncParts7(c: ts.ClassLikeDeclaration): ts.Node[] { const decorators = (n: ts.Node): ts.Node[] => ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).filter( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json new file mode 100644 index 000000000..986683684 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-cycle-static-this-tdz/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7382-cycle-static-this-tdz/beta.ts", + "/tests/corpus/7382-cycle-static-this-tdz/alpha.ts", + "/tests/corpus/7382-cycle-static-this-tdz/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7382-cycle-static-this-tdz/alpha.ts b/tests/corpus/7382-cycle-static-this-tdz/alpha.ts new file mode 100644 index 000000000..ff8eb4a87 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/alpha.ts @@ -0,0 +1,11 @@ +import { Probe, Lazy } from "./beta.ts"; + +export { Probe }; +export const label = "ready"; +export const other = "other"; + +export function later(): string { + return `${Lazy.describe()} ${other}`; +} + +console.log("alpha body", Probe.result, Lazy.value); diff --git a/tests/corpus/7382-cycle-static-this-tdz/beta.ts b/tests/corpus/7382-cycle-static-this-tdz/beta.ts new file mode 100644 index 000000000..428907967 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/beta.ts @@ -0,0 +1,26 @@ +import { label, other } from "./alpha.ts"; + +function guard(read: () => string): string { + try { + return read(); + } catch (error) { + return error instanceof ReferenceError ? `ReferenceError: ${error.message}` : "other error"; + } +} + +export class Probe { + static helper(): string { + return guard(() => `label ${label}`); + } + static result = this.helper(); +} + +export class Lazy { + static read(): string { + return guard(() => `other ${other}`); + } + static describe(): string { + return `lazy ${other}`; + } + static value = [0].map(() => this.read()).join(""); +} diff --git a/tests/corpus/7382-cycle-static-this-tdz/main.ts b/tests/corpus/7382-cycle-static-this-tdz/main.ts new file mode 100644 index 000000000..e3bbf1581 --- /dev/null +++ b/tests/corpus/7382-cycle-static-this-tdz/main.ts @@ -0,0 +1,8 @@ +// The first cycle member to run builds classes whose static initializers +// call a method through `this`, directly and from an arrow function; both +// methods read constants of the other member before it declared them: +// Node's temporal-dead-zone ReferenceError. A third method reads a constant +// only after the cycle finished. +import { label, Probe, later } from "./alpha.ts"; + +console.log(label, Probe.result, later()); diff --git a/tests/harness/corpus-costs/7382-cycle-static-this-tdz/main.ts.json b/tests/harness/corpus-costs/7382-cycle-static-this-tdz/main.ts.json new file mode 100644 index 000000000..b24c0c970 --- /dev/null +++ b/tests/harness/corpus-costs/7382-cycle-static-this-tdz/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 2600 } From 6bbabd5fc9c680f5a0c4ae352f525672efd997cc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:15:02 -0500 Subject: [PATCH 023/409] Convert dynamic values into nested unions in one pass Converting a dynamic value into a union (`JSON.parse(text) as T`, a cast from `any`) tries the arms in order and builds the first arm whose literal discriminator and whole structure match. The builder of that arm converts its members, and a member typed as another union matched its arm's structure again. Every value was matched once per union around it, so converting a tree of depth d cost about d times as much as a flat one. The arm a union matched now builds as a trusted conversion (scr_dyn_trust_enter). Inside it, a nested union picks an arm without matching the arm's structure, which the enclosing match already checked, when the value's arm is known: a plain object's arm by its literal, when the arms' literals are disjoint and no arm without a literal takes plain objects, or the only arm that takes plain objects (a record) or arrays (an array), as in an optional or nullable member. Other unions, and values that are not plain objects or arrays, match as before. Getters and proxy traps run outside the trusted conversion. Failed conversions throw the same TypeError with the same path. --- packages/compiler/src/backend/llvm/dyn.ts | 103 ++++++++++- .../7160-json-parse-deep-unions.ts.json | 6 + .../7161-dynamic-values-deep-unions.ts.json | 6 + packages/runtime/src/scr_dyn.h | 12 ++ packages/runtime/src/scr_json.c | 22 ++- tests/corpus/7160-json-parse-deep-unions.ts | 164 ++++++++++++++++++ .../corpus/7161-dynamic-values-deep-unions.ts | 73 ++++++++ .../7160-json-parse-deep-unions.ts.json | 1 + .../7161-dynamic-values-deep-unions.ts.json | 1 + tests/harness/dyncheck.test.ts | 98 +++++++++++ 10 files changed, 478 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json create mode 100644 tests/corpus/7160-json-parse-deep-unions.ts create mode 100644 tests/corpus/7161-dynamic-values-deep-unions.ts create mode 100644 tests/harness/corpus-costs/7160-json-parse-deep-unions.ts.json create mode 100644 tests/harness/corpus-costs/7161-dynamic-values-deep-unions.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index bb00e4563..40deb1d30 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -135,6 +135,18 @@ export const DYN_KIND = { SYMBOL: 15, } as const; +/** Union arm kinds whose matchers reject every plain dyn object + * (trustedArmByLiteral). */ +const NON_OBJECT_ARM_KINDS: ReadonlySet = new Set([ + "f64", + "string", + "bool", + "nullT", + "undefinedT", + "bigint", + "symbol", +]); + /** What the dyn helpers need beyond the walker host: interned immortal * unit instances (undefined-armed dynCheck targets build them). */ export interface DynHost extends WalkerHost { @@ -365,8 +377,56 @@ export class LlDyn { B.line(`call void @scr_dyn_build_add(ptr %d, ptr @${name}, ptr ${value})`); } + /** Whether a builder of `def` may pick a plain object's arm by its literal + * discriminator alone inside a trusted build (scr_dyn_trust_enter): the + * arms' literals are disjoint, and every arm without a literal has a kind + * no plain object matches. The value then matched exactly one arm with a + * literal, the arm whose literal it carries. */ + private trustedArmByLiteral(def: IrUnionDef): boolean { + const discriminant = def.discriminant; + if (!discriminant) return false; + const seen = new Set(); + for (const { values } of discriminant.cases) { + for (const value of values) { + const key = `${typeof value}:${String(value)}`; + if (seen.has(key)) return false; + seen.add(key); + } + } + return def.arms.every( + (arm, tag) => + discriminant.cases.some((candidate) => candidate.tag === tag) || + NON_OBJECT_ARM_KINDS.has(arm.kind), + ); + } + + /** Per arm of `def`, the dyn kind of the values a builder may convert into + * that arm without matching its structure inside a trusted build, or null. + * A value there already matched one of the union's arms, so the arm is + * known when it is the only arm that takes values of its kind: the arm + * whose literal a plain object carries (trustedArmByLiteral), or the one + * arm a plain object or an array can match at all. */ + private trustedArmKinds(def: IrUnionDef): (number | null)[] { + const byLiteral = this.trustedArmByLiteral(def); + const objectArms = def.arms.flatMap((arm, tag) => + NON_OBJECT_ARM_KINDS.has(arm.kind) ? [] : [tag], + ); + return def.arms.map((arm, tag) => { + if (def.discriminant?.cases.some((candidate) => candidate.tag === tag)) + return byLiteral ? DYN_KIND.OBJ : null; + if (objectArms.length !== 1 || objectArms[0] !== tag) return null; + if (arm.kind === "array") return DYN_KIND.ARR; + if (arm.kind === "record" && this.host.recordsById.get(arm.shapeId)?.tuple !== true) + return DYN_KIND.OBJ; + return null; + }); + } + /** Branch to a variant only after its literal discriminator and complete - * structure match. Use the same path for predicates and builders. */ + * structure match. Use the same path for predicates and builders. With a + * `trustedKind` (builders, trustedArmKinds), a value of that dyn kind + * inside a trusted build skips the structure match once its literal + * matched. */ private unionArmMatch( B: BlockBuilder, def: IrUnionDef, @@ -374,6 +434,7 @@ export class LlDyn { yes: string, no: string, preserveRefs = false, + trustedKind: number | null = null, ): void { const guard = def.discriminant?.cases.find((candidate) => candidate.tag === tag); if (guard && def.discriminant) { @@ -402,6 +463,19 @@ export class LlDyn { }); B.startBlock(structure); } + if (trustedKind !== null) { + this.host.declare(`declare zeroext i1 @scr_dyn_trusted_build()`); + const kind = this.kindOf(B, "%d"); + const sameKind = B.tmp(); + const trusted = B.tmp(); + const skip = B.tmp(); + B.line(`${sameKind} = icmp eq i32 ${kind}, ${trustedKind}`); + B.line(`${trusted} = call zeroext i1 @scr_dyn_trusted_build()`); + B.line(`${skip} = and i1 ${sameKind}, ${trusted}`); + const full = B.newLabel("du.full"); + B.condBr(skip, yes, full); + B.startBlock(full); + } const matches = B.tmp(); B.line( `${matches} = call zeroext i1 @${this.dynMatchHelper(def.arms[tag]!, preserveRefs)}(ptr %d)`, @@ -2357,11 +2431,22 @@ export class LlDyn { `llvm emitter bug: dynCheck of unknown union ${t.unionId}`, ); // Arms in CANONICAL order, first FULL match wins. The matched - // arm's builder can no longer fail. + // arm's builder can no longer fail, and it runs as a trusted build: + // a nested union inside it picks the arm trustedArmKinds names + // without matching the structure that this match already checked. + const trustedKinds = preserveRefs ? def.arms.map(() => null) : this.trustedArmKinds(def); + const trustedCall = (call: () => void): void => { + host.declare(`declare zeroext i1 @scr_dyn_trust_enter()`); + host.declare(`declare void @scr_dyn_trust_exit(i1 zeroext)`); + const previous = B.tmp(); + B.line(`${previous} = call zeroext i1 @scr_dyn_trust_enter()`); + call(); + B.line(`call void @scr_dyn_trust_exit(i1 zeroext ${previous})`); + }; def.arms.forEach((arm, i) => { const lHit = B.newLabel("dcu.h"); const lNext = B.newLabel("dcu.n"); - this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs); + this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs, trustedKinds[i]!); B.startBlock(lHit); if (arm.kind === "undefinedT" || arm.kind === "nullT") { // A matched unit arm builds nothing: THE interned immortal @@ -2388,14 +2473,22 @@ export class LlDyn { } else if (host.nullableUnions.has(t.unionId)) { // A nullable union is its (+1) reference payload. const x = B.tmp(); - B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`); + trustedCall(() => + B.line( + `${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`, + ), + ); B.terminate(`ret ptr ${x}`); } else { const rc = vAdapters(host, arm); host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`); const x = B.tmp(); const u = B.tmp(); - B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`); + trustedCall(() => + B.line( + `${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`, + ), + ); B.line( `${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${x}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`, ); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json new file mode 100644 index 000000000..fb4ef3c1f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7160-json-parse-deep-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7160-json-parse-deep-unions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json new file mode 100644 index 000000000..8ad1579da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7161-dynamic-values-deep-unions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7161-dynamic-values-deep-unions.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 28c2accce..e9b4d33bc 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1442,6 +1442,18 @@ void scr_dyn_build_note(const ScrDyn *d, const void *builder, void *value); void scr_dyn_build_rekey(const ScrDyn *d, const void *builder, void *value); ScrDyn *scr_dyn_visit_view(const ScrDyn *capsule); +/* A union's builder converts the arm whose literal discriminator and + * complete structure matched; every value inside that arm then matched its + * own type too. Between scr_dyn_trust_enter and scr_dyn_trust_exit (which + * restores the value enter returned), scr_dyn_trusted_build answers true, + * and the builder of a nested union whose arms' literals are disjoint picks + * a plain object's arm by its literal alone instead of matching the arm's + * structure again (dyn.ts unionArmMatch). Code a getter or proxy trap runs + * sees false. */ +bool scr_dyn_trust_enter(void); +void scr_dyn_trust_exit(bool previous); +bool scr_dyn_trusted_build(void); + /* ── native handles in the checked-dynamic tree (SCR_DYN_HANDLE) ──────────────────────── * Per-tag dispatch ops, registered by the handle's owning unit * (scr_http_dyn_install / scr_net_dyn_install — emitted main() calls diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 324233028..9fb5efd6d 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -9245,6 +9245,9 @@ static SCR_TL ScrDynVisitLog scr_dyn_viewed_log; /* The member value a matcher or builder read last and holds one * reference to until it calls scr_dyn_release_held. */ static SCR_TL const ScrDyn *scr_dyn_visit_held; +/* Set while a union's builder builds the arm it matched in full + * (scr_dyn_trust_enter). */ +static SCR_TL bool scr_dyn_trusted; /* Tables up to this many slots stay allocated for the next session. */ #define SCR_DYN_VISIT_KEEP 4096 @@ -9356,13 +9359,15 @@ typedef struct { ScrDynVisitLog built_log, viewed_log; bool track; const ScrDyn *held; + bool trusted; } ScrDynVisitState; static void scr_dyn_visit_suspend(ScrDynVisitState *saved) { - *saved = (ScrDynVisitState){scr_dyn_visit_depth, scr_dyn_matching, scr_dyn_built, - scr_dyn_viewed, scr_dyn_built_log, scr_dyn_viewed_log, - scr_dyn_build_track, scr_dyn_visit_held}; + *saved = (ScrDynVisitState){scr_dyn_visit_depth, scr_dyn_matching, scr_dyn_built, + scr_dyn_viewed, scr_dyn_built_log, scr_dyn_viewed_log, + scr_dyn_build_track, scr_dyn_visit_held, scr_dyn_trusted}; scr_dyn_visit_depth = 0; + scr_dyn_trusted = false; scr_dyn_matching = scr_dyn_built = scr_dyn_viewed = (ScrDynVisitSet){NULL, 0, 0}; scr_dyn_built_log = scr_dyn_viewed_log = (ScrDynVisitLog){NULL, 0, 0}; scr_dyn_visit_held = NULL; @@ -9382,6 +9387,7 @@ static void scr_dyn_visit_resume(const ScrDynVisitState *saved) { scr_dyn_viewed_log = saved->viewed_log; scr_dyn_build_track = saved->track; scr_dyn_visit_held = saved->held; + scr_dyn_trusted = saved->trusted; } /* Calls a getter for a member read, outside any visit session. */ @@ -9404,6 +9410,16 @@ static ScrDyn *scr_dyn_visit_proxy_get(const ScrDyn *d, ScrStr *name) { return result; } +bool scr_dyn_trust_enter(void) { + bool previous = scr_dyn_trusted; + scr_dyn_trusted = true; + return previous; +} + +void scr_dyn_trust_exit(bool previous) { scr_dyn_trusted = previous; } + +bool scr_dyn_trusted_build(void) { return scr_dyn_trusted; } + ScrDyn *scr_dyn_obj_read_held(const ScrDyn *d, const char *key, size_t key_len) { ScrDyn *value = scr_dyn_obj_read(d, key, key_len); scr_dyn_visit_held = value; diff --git a/tests/corpus/7160-json-parse-deep-unions.ts b/tests/corpus/7160-json-parse-deep-unions.ts new file mode 100644 index 000000000..bcbf5ea9d --- /dev/null +++ b/tests/corpus/7160-json-parse-deep-unions.ts @@ -0,0 +1,164 @@ +// JSON.parse into recursive discriminated unions: deep and wide trees, +// unions with primitive arms beside the records, unions whose records share +// a literal, and unions with a record arm that has no literal. + +type Expr = + | { kind: "num"; value: number } + | { kind: "str"; text: string } + | { kind: "add"; left: Expr; right: Expr } + | { kind: "neg"; operand: Expr } + | { kind: "call"; callee: string; args: Expr[] } + | { kind: "cond"; test: Expr; then: Expr; otherwise: Expr | null } + | { kind: "label"; inner: Expr; name?: string }; + +type Stmt = + | { kind: "expr"; expr: Expr } + | { kind: "ret"; value: Expr | undefined } + | { kind: "block"; body: Stmt[] }; + +interface Fn { + name: string; + body: Stmt[]; +} + +function evaluate(e: Expr): number { + switch (e.kind) { + case "num": + return e.value; + case "str": + return e.text.length; + case "add": + return evaluate(e.left) + evaluate(e.right); + case "neg": + return -evaluate(e.operand); + case "call": + return e.args.reduce((sum, arg) => sum + evaluate(arg), e.callee.length); + case "cond": + return evaluate(e.test) !== 0 + ? evaluate(e.then) + : e.otherwise === null + ? 0 + : evaluate(e.otherwise); + case "label": + return evaluate(e.inner) + (e.name?.length ?? 0); + } +} + +function depth(e: Expr): number { + switch (e.kind) { + case "num": + case "str": + return 1; + case "add": + return 1 + Math.max(depth(e.left), depth(e.right)); + case "neg": + return 1 + depth(e.operand); + case "call": + return 1 + e.args.reduce((max, arg) => Math.max(max, depth(arg)), 0); + case "cond": + return 1 + Math.max(depth(e.test), depth(e.then), e.otherwise ? depth(e.otherwise) : 0); + case "label": + return 1 + depth(e.inner); + } +} + +function chain(n: number): string { + let text = '{"kind":"num","value":1}'; + for (let i = 0; i < n; i++) { + text = + i % 3 === 0 + ? `{"kind":"neg","operand":${text}}` + : i % 3 === 1 + ? `{"kind":"add","left":${text},"right":{"kind":"str","text":"ab"}}` + : `{"kind":"label","inner":${text}${i % 2 === 0 ? ',"name":"x"' : ""}}`; + } + return text; +} + +function wide(n: number, level: number): string { + if (level === 0) return `{"kind":"num","value":${n}}`; + const args: string[] = []; + for (let i = 0; i < 3; i++) args.push(wide(n + i, level - 1)); + return `{"kind":"call","callee":"f${level}","args":[${args.join(",")}]}`; +} + +for (const n of [0, 1, 5, 60, 400]) { + const e = JSON.parse(chain(n)) as Expr; + console.log("chain", n, e.kind, depth(e), evaluate(e)); +} + +const tree = JSON.parse(wide(1, 6)) as Expr; +console.log("wide", depth(tree), evaluate(tree)); + +const cond = JSON.parse( + '{"kind":"cond","test":{"kind":"num","value":0},"then":{"kind":"num","value":5},"otherwise":null}', +) as Expr; +console.log("cond", evaluate(cond)); + +const fn = JSON.parse( + JSON.stringify({ + name: "main", + body: [ + { kind: "expr", expr: JSON.parse(chain(30)) }, + { kind: "block", body: [{ kind: "ret", value: JSON.parse(wide(2, 3)) }, { kind: "ret" }] }, + ], + }), +) as Fn; +let statements = 0; +const visit = (stmts: Stmt[]): void => { + for (const s of stmts) { + statements++; + if (s.kind === "block") visit(s.body); + else if (s.kind === "expr") console.log("stmt expr", evaluate(s.expr)); + else console.log("stmt ret", s.value === undefined ? "none" : evaluate(s.value)); + } +}; +visit(fn.body); +console.log("statements", statements); + +// Primitive arms beside the records. +type Value = number | string | null | { kind: "list"; items: Value[] } | { kind: "pair"; first: Value; second: Value }; +function show(v: Value): string { + if (v === null || typeof v === "number" || typeof v === "string") return JSON.stringify(v); + return v.kind === "list" ? `[${v.items.map(show).join(" ")}]` : `(${show(v.first)} . ${show(v.second)})`; +} +const values = JSON.parse( + '[1,"a",null,{"kind":"list","items":[2,{"kind":"pair","first":"b","second":{"kind":"list","items":[null,3]}}]}]', +) as Value[]; +console.log("values", values.map(show).join(" | ")); + +// Two records that share a literal: the arm follows the other fields. +type Shared = { kind: "a"; count: number } | { kind: "a"; name: string } | { kind: "b"; next: Shared[] }; +function describe(s: Shared): string { + if (s.kind === "b") return `b(${s.next.map(describe).join(",")})`; + return "count" in s ? `count=${s.count}` : `name=${s.name}`; +} +const shared = JSON.parse( + '{"kind":"b","next":[{"kind":"a","count":1},{"kind":"a","name":"n"},{"kind":"b","next":[{"kind":"a","name":"m"}]}]}', +) as Shared; +console.log("shared", describe(shared)); + +// A record arm without a literal beside records that have one. +type Loose = { kind: "leaf"; value: number } | { kind: "node"; children: Loose[] } | { weight: number; child: Loose }; +function total(l: Loose): number { + if ("weight" in l) return l.weight * total(l.child); + return l.kind === "leaf" ? l.value : l.children.reduce((sum, c) => sum + total(c), 0); +} +const loose = JSON.parse( + '{"kind":"node","children":[{"kind":"leaf","value":2},{"weight":3,"child":{"kind":"node","children":[{"kind":"leaf","value":4},{"weight":2,"child":{"kind":"leaf","value":5}}]}}]}', +) as Loose; +console.log("loose", total(loose)); + +// Optional and nullable members holding records and arrays of records. +type Box = { value: number; child?: Box; rest: Box[] | null }; +function weigh(b: Box): number { + let total = b.value; + if (b.child !== undefined) total += 2 * weigh(b.child); + for (const r of b.rest ?? []) total += weigh(r); + return total; +} +let boxText = '{"value":1,"rest":null}'; +for (let i = 2; i < 120; i++) + boxText = i % 2 === 0 ? `{"value":${i},"child":${boxText},"rest":null}` : `{"value":${i},"rest":[${boxText},{"value":1,"rest":[]}]}`; +const boxes = JSON.parse(boxText) as Box; +console.log("boxes", weigh(boxes) % 1000003); diff --git a/tests/corpus/7161-dynamic-values-deep-unions.ts b/tests/corpus/7161-dynamic-values-deep-unions.ts new file mode 100644 index 000000000..3e85b2689 --- /dev/null +++ b/tests/corpus/7161-dynamic-values-deep-unions.ts @@ -0,0 +1,73 @@ +// Dynamic values cast into recursive discriminated unions: trees built as +// untyped objects, trees that mix typed records into untyped ones, and casts +// of the same values twice. + +type Node = + | { kind: "leaf"; value: number } + | { kind: "pair"; left: Node; right: Node } + | { kind: "wrap"; inner: Node; tag: string } + | { kind: "many"; items: Node[] }; + +function sum(n: Node): number { + switch (n.kind) { + case "leaf": + return n.value; + case "pair": + return sum(n.left) + sum(n.right); + case "wrap": + return sum(n.inner) + n.tag.length; + case "many": + return n.items.reduce((total, item) => total + sum(item), 0); + } +} + +function height(n: Node): number { + switch (n.kind) { + case "leaf": + return 1; + case "pair": + return 1 + Math.max(height(n.left), height(n.right)); + case "wrap": + return 1 + height(n.inner); + case "many": + return 1 + n.items.reduce((max, item) => Math.max(max, height(item)), 0); + } +} + +function untyped(levels: number): any { + let node: any = { kind: "leaf", value: 1 }; + for (let i = 0; i < levels; i++) { + node = + i % 3 === 0 + ? { kind: "wrap", inner: node, tag: "t" } + : i % 3 === 1 + ? { kind: "pair", left: node, right: { kind: "leaf", value: i } } + : { kind: "many", items: [node, { kind: "leaf", value: 2 }] }; + } + return node; +} + +for (const levels of [0, 2, 10, 90, 300]) { + const value: unknown = untyped(levels); + const node = value as Node; + console.log("untyped", levels, node.kind, height(node), sum(node)); +} + +// Typed records inside an untyped tree. +const typedLeaf: Node = { kind: "leaf", value: 7 }; +const typedPair: Node = { kind: "pair", left: typedLeaf, right: { kind: "leaf", value: 3 } }; +const mixed: any = { kind: "many", items: [typedPair, untyped(20), { kind: "wrap", inner: typedLeaf, tag: "xy" }] }; +const fromMixed = mixed as Node; +console.log("mixed", height(fromMixed), sum(fromMixed)); + +// The same untyped value cast twice gives equal trees. +const again: any = untyped(40); +const first = again as Node; +const second = again as Node; +console.log("twice", sum(first) === sum(second), height(first), height(second)); + +// A tree that changes between casts. +const changing: any = { kind: "pair", left: { kind: "leaf", value: 1 }, right: untyped(5) }; +console.log("before", sum(changing as Node)); +changing.left = { kind: "wrap", inner: { kind: "leaf", value: 10 }, tag: "abc" }; +console.log("after", sum(changing as Node)); diff --git a/tests/harness/corpus-costs/7160-json-parse-deep-unions.ts.json b/tests/harness/corpus-costs/7160-json-parse-deep-unions.ts.json new file mode 100644 index 000000000..5d8e97d52 --- /dev/null +++ b/tests/harness/corpus-costs/7160-json-parse-deep-unions.ts.json @@ -0,0 +1 @@ +{ "plain": 11054, "san": 84858 } diff --git a/tests/harness/corpus-costs/7161-dynamic-values-deep-unions.ts.json b/tests/harness/corpus-costs/7161-dynamic-values-deep-unions.ts.json new file mode 100644 index 000000000..bf9a71561 --- /dev/null +++ b/tests/harness/corpus-costs/7161-dynamic-values-deep-unions.ts.json @@ -0,0 +1 @@ +{ "plain": 5843, "san": 57446 } diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index 6a2d4dc61..fca914792 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -427,6 +427,104 @@ console.log("unreachable", t.label); ); }); + test("nested unions: a failure inside a matched arm names the union that holds it", async () => { + // A union's builder converts the arm that matched; the unions nested in + // it pick their arms by literal or by the kind of value only one arm + // takes. A value that fails anywhere below fails its outermost union, + // whose arms the message names. + const r = await compileAndRun( + "deep-discriminated", + `type Expr = + | { kind: "num"; value: number } + | { kind: "str"; text: string } + | { kind: "add"; left: Expr; right: Expr } + | { kind: "call"; callee: string; args: Expr[] } + | { kind: "neg"; operand: Expr } + | { kind: "opt"; inner: Expr | undefined; label?: string }; +type Stmt = + | { kind: "expr"; expr: Expr } + | { kind: "ret"; value: Expr | null } + | { kind: "block"; body: Stmt[] }; +type Fn = { name: string; body: Stmt[] }; + +function attempt(label: string, text: string, as: "expr" | "stmt" | "fn" | "list"): void { + try { + if (as === "expr") { + const e = JSON.parse(text) as Expr; + console.log(label, "ok", e.kind); + } else if (as === "stmt") { + const s = JSON.parse(text) as Stmt; + console.log(label, "ok", s.kind); + } else if (as === "fn") { + const f = JSON.parse(text) as Fn; + console.log(label, "ok", f.name, f.body.length); + } else { + const l = JSON.parse(text) as Expr[]; + console.log(label, "ok", l.length); + } + } catch (e) { + console.log(label, "error", (e as Error).message); + } +} + +const deep = (leaf: string, depth: number): string => { + let s = leaf; + for (let i = 0; i < depth; i++) s = \`{"kind":"neg","operand":\${s}}\`; + return s; +}; +attempt("valid", deep('{"kind":"num","value":1}', 20), "expr"); +attempt("deep wrong field", deep('{"kind":"num","value":"x"}', 5), "expr"); +attempt("deep unknown kind", deep('{"kind":"nope","value":1}', 5), "expr"); +attempt("deep string node", deep('"text"', 5), "expr"); +attempt("deep missing field", deep('{"kind":"add","left":{"kind":"num","value":1}}', 4), "expr"); +attempt("deep array arg", deep('{"kind":"call","callee":"f","args":[{"kind":"num","value":1},{"kind":"str","text":2}]}', 3), "expr"); +attempt("optional inner", deep('{"kind":"opt","inner":{"kind":"num","value":true}}', 3), "expr"); +attempt("optional label", deep('{"kind":"opt","inner":{"kind":"num","value":1},"label":3}', 3), "expr"); +attempt("stmt deep", '{"kind":"block","body":[{"kind":"expr","expr":' + deep('{"kind":"num","value":null}', 3) + '}]}', "stmt"); +attempt("ret null", '{"kind":"ret","value":null}', "stmt"); +attempt("fn deep", '{"name":"f","body":[{"kind":"ret","value":' + deep('{"kind":"str","text":5}', 4) + '}]}', "fn"); +attempt("list element", '[{"kind":"num","value":1},' + deep('{"kind":"num"}', 2) + "]", "list"); +attempt("kind not string", deep('{"kind":3,"value":1}', 2), "expr"); +attempt("valid list", '[' + deep('{"kind":"num","value":1}', 3) + ',{"kind":"str","text":"t"}]', "list"); + +type Box = { value: number; child?: Box; rest: Box[] | null }; +function box(label: string, text: string): void { + try { + const b = JSON.parse(text) as Box; + console.log(label, "ok", b.value); + } catch (e) { + console.log(label, "error", (e as Error).message); + } +} +box("box valid", '{"value":1,"rest":null,"child":{"value":2,"rest":[{"value":3,"rest":null}]}}'); +box("box deep child", '{"value":1,"rest":null,"child":{"value":2,"rest":null,"child":{"value":"x","rest":null}}}'); +box("box deep rest", '{"value":1,"rest":[{"value":2,"rest":[{"value":3,"rest":[1]}]}]}'); +box("box child array", '{"value":1,"rest":null,"child":[]}'); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout.trimEnd().split("\n")).toEqual([ + "valid ok neg", + "deep wrong field error expected object | object | object | object | object | object at $, got object", + "deep unknown kind error expected object | object | object | object | object | object at $, got object", + "deep string node error expected object | object | object | object | object | object at $, got object", + "deep missing field error expected object | object | object | object | object | object at $, got object", + "deep array arg error expected object | object | object | object | object | object at $, got object", + "optional inner error expected object | object | object | object | object | object at $, got object", + "optional label error expected object | object | object | object | object | object at $, got object", + "stmt deep error expected object | object | object at $, got object", + "ret null ok ret", + "fn deep error expected object | object | object at $.body[0], got object", + "list element error expected object | object | object | object | object | object at $[1], got object", + "kind not string error expected object | object | object | object | object | object at $, got object", + "valid list ok 2", + "box valid ok 1", + "box deep child error expected number at $.child.child.value, got string", + "box deep rest error expected object at $.rest[0].rest[0].rest[0], got number", + "box child array error expected object | undefined at $.child, got array", + ]); + }); + test("array where a record is expected names the mismatch at the root", async () => { const r = await compileAndRun( "root-kind", From 8d5b3377aa54e2cbfbc89297b3559114cc903808 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:33:09 -0500 Subject: [PATCH 024/409] Represent Dates as objects with identity A Date is a refcounted object holding its time value, shared with the checked-dynamic Date handle, so it keeps its identity through unknown. Dates compile in arrays (includes, indexOf, lastIndexOf, join, default sort by toString() text), in Date | undefined and Date | null slots, as Map and Set keys and values, in index signatures, and in records, tuples, class fields, closures, promises and generators. === compares identity, relational operators compare time values, new Date(date) makes a new Date, and JSON.stringify writes toJSON(). Closes #225 Co-authored-by: Max B --- .../content/limitations/builtin-apis/date.mdx | 9 +- .../compiler/src/backend/llvm/debug-info.ts | 7 +- packages/compiler/src/backend/llvm/dyn.ts | 15 ++- .../src/backend/llvm/emit-function.ts | 12 +- .../compiler/src/backend/llvm/emitter.test.ts | 3 +- packages/compiler/src/backend/llvm/emitter.ts | 4 +- .../compiler/src/backend/llvm/expr-async.ts | 24 ++-- .../src/backend/llvm/expr-containers.ts | 16 +++ .../src/backend/llvm/expr-stream-bridges.ts | 7 +- .../compiler/src/backend/llvm/lib-dispatch.ts | 35 +++++- packages/compiler/src/backend/llvm/publish.ts | 6 +- packages/compiler/src/backend/llvm/shapes.ts | 9 +- .../src/backend/llvm/stack-captures.ts | 8 +- packages/compiler/src/backend/llvm/walkers.ts | 15 +++ .../compiler/src/diagnostics/diagnostic.ts | 2 +- .../src/frontend/lowering/builtins/dates.ts | 8 +- .../src/frontend/lowering/call-signatures.ts | 1 - .../lowering/containers/array-callbacks.ts | 13 +- .../lowering/containers/array-methods.ts | 17 ++- .../frontend/lowering/expressions/binary.ts | 45 ++++--- .../src/frontend/lowering/lower-classes.ts | 9 +- .../src/frontend/lowering/lower-generators.ts | 2 +- .../src/frontend/lowering/lower-stmts.ts | 2 +- packages/compiler/src/frontend/type-mapper.ts | 15 +-- packages/compiler/src/ir/ir.test.ts | 3 +- packages/compiler/src/ir/ir.ts | 27 +++- packages/compiler/src/ir/validate.ts | 21 +++- .../compiler/test/collection-keys.test.ts | 4 +- packages/compiler/test/ir.test.ts | 11 +- .../tests/corpus/5965-date-objects.ts.json | 6 + .../diagnostics/date-union-fences.ts.json | 6 - packages/runtime/src/scr_array.c | 47 ++++++- packages/runtime/src/scr_array.h | 4 + packages/runtime/src/scr_dyn.h | 5 + packages/runtime/src/scr_json.h | 4 + packages/runtime/src/scr_lib.c | 110 +++++++++++++---- packages/runtime/src/scr_number.h | 17 +++ tests/corpus/5965-date-objects.ts | 115 ++++++++++++++++++ tests/diagnostics/component-fences.ts | 6 +- tests/diagnostics/date-union-fences.ts | 22 ---- tests/diagnostics/stdlib-fence.ts | 8 -- tests/diagnostics/utility-types.ts | 8 +- .../class-interface-refinements.ts.txt | 2 +- .../__snapshots__/component-fences.ts.txt | 30 ++--- .../__snapshots__/date-union-fences.ts.txt | 56 --------- tests/harness/__snapshots__/generics.ts.txt | 2 +- .../index-signature-values.ts.txt | 4 +- .../harness/__snapshots__/stdlib-fence.ts.txt | 101 ++++++--------- tests/harness/__snapshots__/tuples.ts.txt | 4 +- .../__snapshots__/utility-types.ts.txt | 17 +-- .../corpus-costs/5965-date-objects.ts.json | 1 + 51 files changed, 571 insertions(+), 354 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json create mode 100644 tests/corpus/5965-date-objects.ts delete mode 100644 tests/diagnostics/date-union-fences.ts delete mode 100644 tests/harness/__snapshots__/date-union-fences.ts.txt create mode 100644 tests/harness/corpus-costs/5965-date-objects.ts.json diff --git a/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx index ac8bf9115..89266b745 100644 --- a/docs/content/limitations/builtin-apis/date.mdx +++ b/docs/content/limitations/builtin-apis/date.mdx @@ -1,5 +1,6 @@ -- **Date:** Date unions, spread constructor arguments, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported. - Zero-argument, single number/string, and local-time year/month field constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. - JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. - A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. +- **Date:** Spread constructor arguments, setters, thrown Date values, and locale formatters are unsupported. + Dates are objects: `===` compares identity, `new Date(date)` makes a new Date, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in `Date | undefined` and `Date | null` slots, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators. + Unions of a Date with other non-nullish types are unsupported. + Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, relational comparison, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. + A Date converted to `unknown` is the same Date object. See [Dates and timezones](#dates-and-timezones) for timezone data. diff --git a/packages/compiler/src/backend/llvm/debug-info.ts b/packages/compiler/src/backend/llvm/debug-info.ts index 4e37ee5bc..3a75c6263 100644 --- a/packages/compiler/src/backend/llvm/debug-info.ts +++ b/packages/compiler/src/backend/llvm/debug-info.ts @@ -139,7 +139,6 @@ export class LlvmDebugInfo { let id: string; switch (type.kind) { case "f64": - case "date": case "procStream": id = this.add( `!DIBasicType(name: ${quoted(type.kind === "f64" ? "number" : type.kind)}, size: 64, encoding: DW_ATE_float)`, @@ -190,7 +189,7 @@ export class LlvmDebugInfo { ? [] : [ this.add( - `!DIDerivedType(tag: DW_TAG_member, name: "arm${i}", baseType: ${this.type(arm)}, size: ${arm.kind === "bool" ? 8 : arm.kind === "f64" || arm.kind === "date" ? 64 : this.pointerBits})`, + `!DIDerivedType(tag: DW_TAG_member, name: "arm${i}", baseType: ${this.type(arm)}, size: ${arm.kind === "bool" ? 8 : arm.kind === "f64" ? 64 : this.pointerBits})`, ), ], ); @@ -239,9 +238,7 @@ export class LlvmDebugInfo { // Scalar forward captures use an array-backed TDZ box. Expose that // exceptional representation opaquely rather than reporting a bad value. const opaqueBox = - local.boxed && - local.tdz && - (local.type.kind === "f64" || local.type.kind === "bool" || local.type.kind === "date"); + local.boxed && local.tdz && (local.type.kind === "f64" || local.type.kind === "bool"); const type = opaqueBox ? this.pointer( this.add( diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index bb00e4563..a46c81658 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -421,7 +421,7 @@ export class LlDyn { /** The value LLVM type of a dynCheck result / toDyn operand for `t`. */ private valTy(t: IrType): string { - return t.kind === "f64" || t.kind === "date" || t.kind === "procStream" + return t.kind === "f64" || t.kind === "procStream" ? "double" : t.kind === "bool" ? "i1" @@ -1856,10 +1856,10 @@ export class LlDyn { }; switch (t.kind) { case "date": { - host.declare(`declare double @scr_dyn_native_date_value(ptr)`); + host.declare(`declare ptr @scr_dyn_native_date_check(ptr, ptr)`); const r = B.tmp(); - B.line(`${r} = call double @scr_dyn_native_date_value(ptr %d)`); - B.terminate(`ret double ${r}`); + B.line(`${r} = call ptr @scr_dyn_native_date_check(ptr %d, ptr %path)`); + B.terminate(`ret ptr ${r}`); break; } case "url": @@ -2645,11 +2645,10 @@ export class LlDyn { let sourceAccessor: { name: string; release: string } | null = null; switch (t.kind) { case "date": { - // A Date handle holding the same time value; native Dates are - // immutable values, and so is the handle. - host.declare(`declare ptr @scr_dyn_native_date_from_ms(double)`); + // A Date handle holding the same Date object. + host.declare(`declare ptr @scr_dyn_native_date(ptr)`); const r = B.tmp(); - B.line(`${r} = call ptr @scr_dyn_native_date_from_ms(double %v)`); + B.line(`${r} = call ptr @scr_dyn_native_date(ptr %v)`); B.terminate(`ret ptr ${r}`); break; } diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 16e654393..501224369 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -301,11 +301,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { // ScrGen owns the frame), so its OUT slot address is fixed. let out: string | null = null; const yieldKind = fn.generator?.yieldT.kind; - if ( - !emitter.wasi && - fn.async !== true && - (yieldKind === "f64" || yieldKind === "date" || yieldKind === "bool") - ) { + if (!emitter.wasi && fn.async !== true && (yieldKind === "f64" || yieldKind === "bool")) { emitter.declare(`declare ptr @scr_gen_of_fiber(ptr)`); const gen = B.tmp(); B.line(`${gen} = call ptr @scr_gen_of_fiber(ptr ${self})`); @@ -701,7 +697,6 @@ export function emitGenScaffolding(emitter: LlEmitter): string[] { tr.push(` br label %done ; void body: the done value is undefined (NONE)`); break; case "f64": - case "date": emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`); tr.push(` call void @scr_gen_out_f64(ptr %g, double %r)`, ` br label %done`); break; @@ -1050,7 +1045,6 @@ export function emitAsyncAwaitCall( emitter.declare(`declare void @scr_await_void(ptr)`); return ` call void @scr_await_void(ptr ${promise})`; case "f64": - case "date": emitter.declare(`declare double @scr_await_f64(ptr)`); return ` ${dest} = call double @scr_await_f64(ptr ${promise})`; case "bool": @@ -1105,7 +1099,7 @@ export function emitAsyncAwaitCall( const kind = ret.kind === "void" ? "void" - : ret.kind === "f64" || ret.kind === "date" + : ret.kind === "f64" ? "f64" : ret.kind === "bool" ? "bool" @@ -1184,7 +1178,6 @@ export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void { case "void": return; case "f64": - case "date": emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`); emitter.B.line(`call void @scr_gen_out_f64(ptr ${gen}, double ${v!.name})`); return; @@ -1209,7 +1202,6 @@ export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void { emitter.B.line(`call void @scr_promise_fulfill_void(ptr ${pr})`); break; case "f64": - case "date": emitter.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); emitter.B.line(`call void @scr_promise_fulfill_f64(ptr ${pr}, double ${v!.name})`); break; diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index d1fd62a83..8653dfd4e 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -1,7 +1,6 @@ import { expect, test } from "vitest"; import { BOOL, - DATE_T, F64, PROCSTREAM_T, STRING, @@ -287,7 +286,7 @@ test("shared exceptional exits preserve the function's scalar return ABI", () => }); test("exceptional exits use each scalar kind's LLVM return type", () => { - for (const type of [F64, DATE_T, BOOL, PROCSTREAM_T]) { + for (const type of [F64, BOOL, PROCSTREAM_T]) { for (const ownedLocals of [false, true]) { const module = moduleFor([call(), call(), { kind: "throw", value: literal("failed"), loc }]); const work = module.functions[1]!; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 9c42920b9..8db63daa3 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1762,7 +1762,6 @@ export class LlEmitter { if (POINTER_KINDS.has(t.kind)) return "ptr"; switch (t.kind) { case "f64": - case "date": return "double"; case "bool": return "i1"; @@ -1895,7 +1894,6 @@ export class LlEmitter { this.declare(`declare void @scr_promise_fulfill_void(ptr)`); return [` call void @scr_promise_fulfill_void(ptr ${promise})`]; case "f64": - case "date": this.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); return [` call void @scr_promise_fulfill_f64(ptr ${promise}, double %r)`]; case "bool": @@ -2125,7 +2123,7 @@ export class LlEmitter { return { name: "", type }; } const t = B.tmp(); - if (type.kind === "f64" || type.kind === "date") { + if (type.kind === "f64") { this.declare(`declare double @scr_coro_direct_f64(ptr)`); B.line(`${t} = call double @scr_coro_direct_f64(ptr ${record})`); } else if (type.kind === "bool") { diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index b5d765b16..ee9ff2983 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -184,7 +184,7 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic } const v = host.emitExpr(e.args[0]!); const t = e.args[0]!.type; - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`); B.line(`call void @scr_promise_fulfill_f64(ptr ${p}, double ${v.name})`); } else if (t.kind === "bool") { @@ -500,7 +500,7 @@ export function emitAsyncExpr( host.declare(`declare void @scr_async_gen_hop_done()`); B.line(`call void @scr_async_gen_hop_done()`); } - if (yt.kind === "f64" || yt.kind === "date") { + if (yt.kind === "f64") { if (host.currentCoro !== null) { const coro = host.currentCoro; if (coro.out !== null) host.emitGenSlotStore(coro.out, "f64", v.name); @@ -582,7 +582,7 @@ export function emitAsyncExpr( return { name: "", type: e.type }; } const t = B.tmp(); - if (e.type.kind === "f64" || e.type.kind === "date") { + if (e.type.kind === "f64") { host.declare(`declare double @scr_gen_take_in_f64()`); B.line(`${t} = call double @scr_gen_take_in_f64()`); return { name: t, type: e.type }; @@ -710,12 +710,12 @@ export function emitAsyncExpr( throw new InternalCompilerError(`llvm emitter bug: genResume ${e.mode} on async`); const gb = emitBorrowedInput(host, e.gen); if (e.mode === "value") { - if (!host.wasi && (e.type.kind === "f64" || e.type.kind === "date")) + if (!host.wasi && e.type.kind === "f64") return { name: host.emitGenSlotTake(host.genOutSlot(gb.name), "f64"), type: e.type }; if (!host.wasi && e.type.kind === "bool") return { name: host.emitGenSlotTake(host.genOutSlot(gb.name), "bool"), type: e.type }; const t = B.tmp(); - if (e.type.kind === "f64" || e.type.kind === "date") { + if (e.type.kind === "f64") { host.declare(`declare double @scr_gen_take_out_f64(ptr)`); B.line(`${t} = call double @scr_gen_take_out_f64(ptr ${gb.name})`); return { name: t, type: e.type }; @@ -752,7 +752,7 @@ export function emitAsyncExpr( store(a.name, e.arg!.type); }; const parkIn = (name: string, t: IrType): void => { - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare void @scr_gen_in_f64(ptr, double)`); B.line(`call void @scr_gen_in_f64(ptr ${g.name}, double ${name})`); } else if (t.kind === "bool") { @@ -787,7 +787,7 @@ export function emitAsyncExpr( const a = host.emitExpr(e.arg); const t = e.arg.type; if (isRefCounted(t)) host.toHeap(a); - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare ptr @scr_async_gen_next_f64(ptr, double)`); call = `call ptr @scr_async_gen_next_f64(ptr ${g.name}, double ${a.name})`; } else if (t.kind === "bool") { @@ -806,7 +806,7 @@ export function emitAsyncExpr( const a = host.emitExpr(e.arg); const t = e.arg.type; if (isRefCounted(t)) host.toHeap(a); - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare ptr @scr_async_gen_return_f64(ptr, double)`); call = `call ptr @scr_async_gen_return_f64(ptr ${g.name}, double ${a.name})`; } else if (t.kind === "bool") { @@ -849,7 +849,7 @@ export function emitAsyncExpr( B.line(`call void @scr_gen_ret_none(ptr ${g.name})`); } else { sendArg((name, t) => { - if (t.kind === "f64" || t.kind === "date") { + if (t.kind === "f64") { host.declare(`declare void @scr_gen_ret_f64(ptr, double)`); B.line(`call void @scr_gen_ret_f64(ptr ${g.name}, double ${name})`); } else if (t.kind === "bool") { @@ -960,7 +960,7 @@ export function emitAsyncExpr( return { name: "", type: e.type }; } const t = B.tmp(); - if (e.type.kind === "f64" || e.type.kind === "date") { + if (e.type.kind === "f64") { const f64Await = host.awaitResultFn("f64"); host.declare(`declare double @${f64Await}(ptr)`); B.line(`${t} = call double @${f64Await}(ptr ${pr.name})`); @@ -1050,7 +1050,7 @@ export function emitAsyncExpr( const peek = host.unionPeek(u.name, def.id); if (host.coroAwait) host.emitCoroAwait(peek); let awaited: LlValue; - if (inner.kind === "f64" || inner.kind === "date") { + if (inner.kind === "f64") { const f64Await = host.awaitResultFn("f64"); host.declare(`declare double @${f64Await}(ptr)`); const x = B.tmp(); @@ -1136,7 +1136,6 @@ export function emitAsyncExpr( let resolve: string; const kindNums: Partial> = { f64: 0, - date: 0, bool: 1, string: 2, void: 3, @@ -1191,7 +1190,6 @@ export function emitAsyncExpr( B.line(`${p} = call ptr @scr_promise_new()`); const kindNums: Partial> = { f64: 0, - date: 0, bool: 1, string: 2, void: 3, diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 2f4cd98e9..b70bfcd23 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -863,6 +863,12 @@ export function emitArrIntrinsic( B.line(`${t} = call ptr @${helper}(ptr ${r.name}, ptr ${sep.name})`); return host.own({ name: t, type: e.type }); } + if (elem.kind === "date") { + host.declare(`declare ptr @scr_date_arr_join(ptr, ptr)`); + const t = B.tmp(); + B.line(`${t} = call ptr @scr_date_arr_join(ptr ${r.name}, ptr ${sep.name})`); + return host.own({ name: t, type: e.type }); + } host.declare(`declare ptr @scr_arr_join(ptr, ptr)`); const t = B.tmp(); B.line(`${t} = call ptr @scr_arr_join(ptr ${r.name}, ptr ${sep.name})`); @@ -887,6 +893,16 @@ export function emitArrIntrinsic( case "sortPrimitive": case "toSortedPrimitive": { if (e.method === "sortPrimitive") host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + if (elem.kind === "date") { + // Dates order by their toString() text. + host.declare(`declare ptr @scr_arr_sort_by_text(ptr, i1 zeroext, ptr)`); + host.declare(`declare ptr @scr_date_object_to_string(ptr)`); + const sorted = B.tmp(); + B.line( + `${sorted} = call ptr @scr_arr_sort_by_text(ptr ${r.name}, i1 ${e.method === "toSortedPrimitive" ? 1 : 0}, ptr @scr_date_object_to_string)`, + ); + return host.own({ name: sorted, type: e.type }); + } host.declare(`declare ptr @scr_arr_sort_primitive(ptr, i1 zeroext)`); const result = B.tmp(); B.line( diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 7cb0fa772..877a01601 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -118,12 +118,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri if (inner.kind === "void") { host.declare(`declare void @scr_promise_fulfill_void(ptr)`); B.line(`call void @scr_promise_fulfill_void(ptr %dst)`); - } else if ( - inner.kind === "f64" || - inner.kind === "date" || - inner.kind === "procStream" || - inner.kind === "bool" - ) { + } else if (inner.kind === "f64" || inner.kind === "procStream" || inner.kind === "bool") { const fn = inner.kind !== "bool" ? "scr_promise_fulfill_f64" : "scr_promise_fulfill_bool"; host.declare( `declare void @${fn}(ptr, ${host.llType(inner)}${inner.kind === "bool" ? " zeroext" : ""})`, diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index cebe661d4..753f296e6 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -9,7 +9,7 @@ import { emitTestLibCall } from "./lib-test.js"; import { InternalCompilerError } from "../../errors.js"; import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js"; -import type { IrExpr, IrType } from "../../ir/ir.js"; +import { F64, type IrExpr, type IrType } from "../../ir/ir.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import { emitJsLibCall } from "./jsval.js"; import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js"; @@ -569,6 +569,39 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll // The deep comparisons' number test: computed here, with no runtime call. if (e.fn === "assert.sameValue") return { name: emitSameValueF64(host, args[0]!.name, args[1]!.name), type: e.type }; + // The runtime's Date functions take and answer time values: a Date + // operand passes its time value, and a Date result is a new Date object. + const callArgs = args.map((a) => + a.type.kind === "date" ? { name: emitDateTime(host, a.name), type: F64 } : a, + ); + if (e.type.kind === "date") { + const ms = emitSymbolLibCall(host, { ...e, type: F64 }, sym, callArgs); + host.declare(`declare ptr @scr_date_new(double)`); + const date = B.tmp(); + B.line(`${date} = call ptr @scr_date_new(double ${ms.name})`); + return host.own({ name: date, type: e.type }); + } + return emitSymbolLibCall(host, e, sym, callArgs); +} + +/** A Date object's time value (ScrDate.ms). */ +function emitDateTime(host: LlvmEmitterContext, date: string): string { + const B = host.B; + const field = B.tmp(); + const ms = B.tmp(); + B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`); + B.line(`${ms} = load double, ptr ${field}`); + return ms; +} + +/** The call of runtime function `sym` with evaluated arguments. */ +function emitSymbolLibCall( + host: LlvmEmitterContext, + e: LibCallExpr, + sym: string, + args: readonly LlValue[], +): LlValue { + const B = host.B; const argDecls = args.map((a) => { const ty = host.llType(a.type); return ty === "i1" ? "i1 zeroext" : ty; diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index 60270adda..62fdd75ec 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -596,10 +596,10 @@ export class PublishWalkers { } } -/** Unpublishable types stored as scalars (dates are time values, process - * streams file descriptors): no pointer to push, and always a value. */ +/** Unpublishable types stored as scalars (process streams are file + * descriptors): no pointer to push, and always a value. */ function scalarRefusal(t: IrType): boolean { - return t.kind === "date" || t.kind === "procStream"; + return t.kind === "procStream"; } /** The Node.js implementation's refusal for a value of type `t`. */ diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index d13f80a78..e6e375223 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -1099,6 +1099,7 @@ export function arrNewCall( elem.kind === "stats" || // fs.Stats snapshots: scr_stats_* adapters, no trace elem.kind === "dyn" || // native collection seeds/drains: traced REF elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside) + elem.kind === "date" || // Date objects: scr_date_* adapters, no trace (no refs inside) (elem.kind === "array" && traceAdapter(host, elem) !== null); if (!useRef) { if (small) { @@ -1162,7 +1163,6 @@ const SCR_ARR_OPS_RELEASE_NOTHING = 2; export function boxNewCall(host: ShapeHost, t: IrType): string { const plain: Partial> = { f64: 0, - date: 0, procStream: 0, bool: 1, string: 2, @@ -1187,11 +1187,7 @@ export function boxNewCall(host: ShapeHost, t: IrType): string { /** Box accessor suffix (boxAccess): scalars unboxed, ref kinds pointers. */ export function boxAccess(t: IrType): "f64" | "bool" | "ref" { - return t.kind === "f64" || t.kind === "date" || t.kind === "procStream" - ? "f64" - : t.kind === "bool" - ? "bool" - : "ref"; + return t.kind === "f64" || t.kind === "procStream" ? "f64" : t.kind === "bool" ? "bool" : "ref"; } /* ── record shapes ────────────────────────────────────────────────────── */ @@ -1207,7 +1203,6 @@ export function llFieldType( if (POINTER_KINDS.has(t.kind)) return "ptr"; switch (t.kind) { case "f64": - case "date": case "procStream": return "double"; case "bool": diff --git a/packages/compiler/src/backend/llvm/stack-captures.ts b/packages/compiler/src/backend/llvm/stack-captures.ts index f0812b7b0..40e1a0428 100644 --- a/packages/compiler/src/backend/llvm/stack-captures.ts +++ b/packages/compiler/src/backend/llvm/stack-captures.ts @@ -105,13 +105,7 @@ export class StackCaptures { // sites, including duplicated finally bodies and versioned loops. if (!this.locals.has(id) || this.remaining < 48) return null; const ref = isRefCounted(type); - if ( - !ref && - type.kind !== "f64" && - type.kind !== "date" && - type.kind !== "procStream" && - type.kind !== "bool" - ) + if (!ref && type.kind !== "f64" && type.kind !== "procStream" && type.kind !== "bool") return null; this.remaining -= 48; const B = host.B; diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts index 044cb79ae..7c17a94f4 100644 --- a/packages/compiler/src/backend/llvm/walkers.ts +++ b/packages/compiler/src/backend/llvm/walkers.ts @@ -233,6 +233,10 @@ export class LlWalkers { this.host.declare(`declare void @scr_jb_put_json_str(ptr, ptr)`); B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`); break; + case "date": + this.host.declare(`declare void @scr_jb_put_date(ptr, ptr)`); + B.line(`call void @scr_jb_put_date(ptr %b, ptr %v) ; toJSON(): ISO text, null if invalid`); + break; case "dyn": // A checked-dynamic value serializes itself (runtime walker). // Bare stringify of dyn stays frontend-fenced, and overflow @@ -924,6 +928,17 @@ export class LlWalkers { case "string": this.putScrStr(B, buf, this.unionPeek(B, u, unionId)); break; + case "date": { + host.declare(`declare ptr @scr_date_object_to_string(ptr)`); + host.declare(`declare void @scr_str_release(ptr)`); + const text = B.tmp(); + B.line( + `${text} = call ptr @scr_date_object_to_string(ptr ${this.unionPeek(B, u, unionId)})`, + ); + this.putScrStr(B, buf, text); + B.line(`call void @scr_str_release(ptr ${text})`); + break; + } case "f64": { host.declare(`declare double @scr_union_get_f64(ptr)`); host.declare(`declare void @scr_jb_put_number(ptr, double)`); diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 625ee93af..2fd8b6fb0 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -656,7 +656,7 @@ export function genericSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiag export function indexSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnostic { return { code: "SC2006", - message: `values of type '${typeText}' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown')`, + message: `values of type '${typeText}' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown')`, loc, milestone: "M2", hint: "string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering", diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index a598f4ee6..0204386ab 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -53,7 +53,13 @@ export function lowerDateCall( call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { - if (call.questionDotToken || access.questionDotToken) return null; + // An optional chain lowers the call again once it has narrowed the + // receiver (Lowerer.chainHandled). + if ( + (call.questionDotToken && !lowerer.chainHandled.has(call)) || + (access.questionDotToken && !lowerer.chainHandled.has(access)) + ) + return null; const loc = locOf(call); const member = lowerer.stdlibGlobalMember(access, "Date"); if (member === "now" || member === "parse" || member === "UTC") diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index dacb4d324..c9fd954b5 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -611,7 +611,6 @@ export function checkDefaultParamBodyType( if ( bodyType.kind === "void" || bodyType.kind === "regex" || - bodyType.kind === "date" || bodyType.kind === "dyn" || isUnitType(bodyType) ) { diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 86f214bc1..edf65ab2b 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -2358,10 +2358,11 @@ function reduceHelper( * the left element first. Undefined values sink without reaching the user * comparator. The callback sequence differs from V8's TimSort, so exact * order and count parity are not claimed; results agree for consistent - * comparators. Without a comparator, string, number and boolean arrays use - * native stable ordering: their String conversions cannot call user code. - * Strings compare UTF-16 units; numbers compare their decimal spellings - * ([10, 9, 1] → [1, 10, 9]). Other element types still need a comparator. */ + * comparators. Without a comparator, string, number, boolean and Date + * arrays use native stable ordering: their String conversions cannot call + * user code. Strings compare UTF-16 units; numbers compare their decimal + * spellings ([10, 9, 1] → [1, 10, 9]); Dates compare their toString() text. + * Other element types still need a comparator. */ export function lowerArraySortCall( lowerer: Lowerer, call: ts.CallExpression, @@ -2374,7 +2375,7 @@ export function lowerArraySortCall( const copyFirst = method === "toSorted"; if ( call.arguments.nodes.length === 0 && - (elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool") + (elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool" || elem.kind === "date") ) { return { kind: "arrIntrinsic", @@ -2389,7 +2390,7 @@ export function lowerArraySortCall( lowerer.noLowering( `.${method} with ${call.arguments.nodes.length} arguments`, call, - "default ordering supports string[], number[] and boolean[]; other element types need a comparator", + "default ordering supports string[], number[], boolean[] and Date[]; other element types need a comparator", ); } const receiver = lowerer.lowerExpr(access.expression); diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 44182fbda..8a736e75f 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1066,14 +1066,25 @@ export function lowerArrayMethodCall( (lowerer.unions .get(elem.unionId) ?.arms.every( - (a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a), + (a) => + a.kind === "f64" || + a.kind === "string" || + a.kind === "bool" || + a.kind === "date" || + isUnitType(a), ) ?? false); - if (elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool" && !joinableUnion) { + if ( + elem.kind !== "f64" && + elem.kind !== "string" && + elem.kind !== "bool" && + elem.kind !== "date" && + !joinableUnion + ) { lowerer.unsupported( "SC1090", call, - "'.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS)", + "'.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS)", ); } const receiver = lowerer.lowerExpr(access.expression); diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 9702a6dd9..ed4771b05 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -759,10 +759,21 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr (lowerer.unions .get(elem.unionId) ?.arms.every( - (a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a), + (a) => + a.kind === "f64" || + a.kind === "string" || + a.kind === "bool" || + a.kind === "date" || + isUnitType(a), ) ?? false); - if (elem.kind === "f64" || elem.kind === "string" || elem.kind === "bool" || joinableUnion) { + if ( + elem.kind === "f64" || + elem.kind === "string" || + elem.kind === "bool" || + elem.kind === "date" || + joinableUnion + ) { const sep: IrExpr = { kind: "strLit", value: ",", type: STRING, loc: e.loc }; return { kind: "arrIntrinsic", @@ -2166,14 +2177,18 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; if (bothStr) return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc }; - // A Date converts to a new checked-dynamic Date on each crossing, so - // comparisons keep their Date fence instead of comparing copies. - if ( - left.type.kind !== "date" && - right.type.kind !== "date" && - lowerer.dynConvertible(left.type) && - lowerer.dynConvertible(right.type) - ) { + // Dates compare by time value, the number hint of ToPrimitive. + if (left.type.kind === "date" && right.type.kind === "date") { + const time = (d: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "date.getTime", + args: [d], + type: F64, + loc, + }); + return { kind: "bin", op: cmpOp, left: time(left), right: time(right), type: BOOL, loc }; + } + if (lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) { return { kind: "libCall", fn: "dyn.compare", @@ -2387,6 +2402,7 @@ export function lowerStrictEquality( idLeft.type.kind === "map" || idLeft.type.kind === "set" || idLeft.type.kind === "regex" || + idLeft.type.kind === "date" || idLeft.type.kind === "url" || idLeft.type.kind === "object" || idLeft.type.kind === "record" || @@ -2423,14 +2439,7 @@ export function lowerStrictEquality( loc, }; } - // A Date converts to a new checked-dynamic Date on each crossing; its - // identity has no representation, so equality keeps the fence. - if ( - idLeft.type.kind !== "date" && - idRight.type.kind !== "date" && - lowerer.dynConvertible(idLeft.type) && - lowerer.dynConvertible(idRight.type) - ) { + if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) { return { kind: "dynScalarEq", left: lowerer.coerceToExpected(idLeft, DYN), diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index e264e0a3a..5079f3c9e 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1415,7 +1415,7 @@ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null { const arms = mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped]; for (const a of arms) { - if (a.kind === "map" || a.kind === "regex" || a.kind === "date" || a.kind === "generator") { + if (a.kind === "map" || a.kind === "regex" || a.kind === "generator") { return null; } byKey.set(typeKey(a), a); @@ -3971,7 +3971,6 @@ export function collectClassShapeInner( declared.kind !== "union" && declared.kind !== "dyn" && declared.kind !== "map" && - declared.kind !== "date" && declared.kind !== "generator" && declared.kind !== "void" && declared.kind !== "caught"; @@ -10324,8 +10323,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (arg.type.kind === "string") { return { kind: "libCall", fn: "date.newString", args: [arg], type: DATE_T, loc }; } + // new Date(date): a new Date object with the same time value. if (arg.type.kind === "date") { - return arg; + const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [arg], type: F64, loc }; + return { kind: "libCall", fn: "date.newMs", args: [time], type: DATE_T, loc }; } lowerer.noLowering( `new Date of '${lowerer.fmt(arg.type)}' values`, @@ -10528,7 +10529,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { expr, `Map values of type '${lowerer.checker.typeToString(targs[1])}' ` + `(Map values must be number, string, boolean, records, class instances, functions, ` + - `typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values)`, + `typed arrays, arrays, promises, Dates, Maps, Sets, or supported unions and checked values)`, ); } lowerer.badType(expr, tsType); diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index 80463fa6c..f837cc0af 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -333,7 +333,7 @@ export function stepDrivable(genT: GenType): boolean { /** Whether a yield channel's values have an OUT-slot form `value` reads. */ function hasOutSlot(yt: IrType): boolean { - return yt.kind === "f64" || yt.kind === "date" || yt.kind === "bool" || isRefCounted(yt); + return yt.kind === "f64" || yt.kind === "bool" || isRefCounted(yt); } /** `step` and `value` resumes of a generator-typed borrowed read. */ diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index ea5ef72ac..750057bf9 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6687,7 +6687,7 @@ function lowerDynSwitch( loc: locOf(expression), }; } - if (test.type.kind === "date" || !lowerer.dynConvertible(test.type)) { + if (!lowerer.dynConvertible(test.type)) { lowerer.unsupported( "SC1100", expression, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 35514d992..baafd230a 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -3204,10 +3204,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } const collectionView = mapCollectionView(widened, ctx); if (collectionView) return collectionView; - // Date: a TimeClip'd epoch-millisecond scalar in the static runtime. - // This supports stored/passed values and the read-only getter slice; - // identity and mutation stay fenced because the scalar deliberately - // carries no object identity. + // Date: a refcounted object holding its TimeClip'd time value (ScrDate), + // shared with the checked-dynamic Date handle, so it keeps its identity + // through every slot and through `unknown`. Setters stay fenced. if (isStdlibInterface("Date")) return DATE_T; // RegExp: a reference to the lib RegExp interface — provenance, not the // name (a user's own `interface RegExp` maps as a record). Regex values @@ -5025,7 +5024,6 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (a) => a.kind === "void" || a.kind === "union" || - a.kind === "date" || a.kind === "dyn" || // A generator arm only comes from an Iterable (an optional // one keeps its unit arms beside the iterable union's). @@ -6558,7 +6556,7 @@ export function genResultRecord( for (const a of unions.get(t.unionId)?.arms ?? []) byKey.set(typeKey(a), a); return true; } - if (t.kind === "regex" || t.kind === "date" || t.kind === "generator") { + if (t.kind === "regex" || t.kind === "generator") { return false; // no legal union arm exists for these kinds } byKey.set(typeKey(t), t); @@ -6647,15 +6645,15 @@ export function withUnitArm( if (t.kind === "union") { const def = unions.get(t.unionId); if (!def) return null; - if (def.arms.some((a) => a.kind === "date")) return null; if (def.arms.some((a) => a.kind === kind)) return t; const arms = [...def.arms, unit]; + // Date and Promise arms beside other data arms have no union home. + if (!unionContainerArmsOk(arms)) return null; arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); return { kind: "union", unionId: unions.transform(def, arms) }; } if ( t.kind === "void" || - t.kind === "date" || t.kind === "dyn" || // A bare unit field type cannot occur (units live only inside unions), // but guard against constructing a single-arm union from one. @@ -8245,7 +8243,6 @@ function armHasUnionHome(arm: IrType): boolean { case "void": case "union": case "regex": - case "date": case "generator": case "dyn": return false; diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index d62c9c83c..30fe6f50e 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -57,7 +57,8 @@ describe("IR kind sets", () => { test("distinguishes pointer values from object-like scalars", () => { expect(POINTER_KINDS.has("record")).toBe(true); - expect(POINTER_KINDS.has("date")).toBe(false); + expect(POINTER_KINDS.has("date")).toBe(true); + expect(POINTER_KINDS.has("procStream")).toBe(false); }); test("distinguishes typed-rest closure ABIs", () => { diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 12400fee9..5afe2b09b 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -440,6 +440,7 @@ const POINTER_KIND_LIST = [ "url", "searchParams", "symbol", + "date", ...POINTER_HANDLE_KINDS, "func", "object", @@ -463,7 +464,7 @@ export const POINTER_KINDS = irKindSet(POINTER_KIND_LIST); export const RUNTIME_RC_STEMS: Record = { f64: "", bigint: "scr_bigint", - date: "", + date: "scr_date", string: "scr_str", bool: "", array: "scr_arr", @@ -645,6 +646,7 @@ export function isSupportedArrayElem(t: IrType): boolean { case "func": case "promise": case "regex": + case "date": case "child": case "netServer": // fs.Stats snapshots (`await Promise.all(paths.map(stat))`): immutable @@ -689,7 +691,8 @@ export function isIdentityCollectionKey(t: IrType): boolean { // functions intern one immortal closure, inner closures allocate once // per evaluation), so pointer identity is JS function identity. // Closures are cycle-headered and traced like records. - t.kind === "func" + t.kind === "func" || + t.kind === "date" ); } @@ -797,6 +800,9 @@ export function isSupportedMapValue(t: IrType): boolean { // the pre-settle closures. case "secureCtx": return true; + // A Date object: immutable, holds no references. + case "date": + return true; // A promise (Map> — the SNI callback's // in-flight dedupe map): refcounted, with trace/cycle support already // in place (scr_promise_trace_v — the race machinery's refcounting). @@ -849,8 +855,9 @@ export function funcOf(params: IrType[], ret: IrType): IrType { * promises nullable-only; declared maybe-promise unions are admitted by * maybePromiseUnionArmsOk. */ export function unionContainerArmsOk(arms: IrType[]): boolean { - return arms.every( - (a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b)), + return ( + arms.every((a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b))) && + dateUnionArmsOk(arms) ); } @@ -861,7 +868,13 @@ export function unionContainerArmsOk(arms: IrType[]): boolean { * non-thenable path). Data arms must not be thenable; the type mapper * checks that against the checker types before minting such a union. */ export function maybePromiseUnionArmsOk(arms: IrType[]): boolean { - return arms.filter((a) => a.kind === "promise").length <= 1; + return arms.filter((a) => a.kind === "promise").length <= 1 && dateUnionArmsOk(arms); +} + +/** A Date arm has a union home beside undefined and null arms only: no + * narrowing contract tells it from other data arms yet. */ +function dateUnionArmsOk(arms: IrType[]): boolean { + return arms.every((a, i) => a.kind !== "date" || arms.every((b, j) => j === i || isUnitType(b))); } /** Canonical, injective text form of an IrType — the building block of @@ -3975,6 +3988,9 @@ function isJsonSafeAt( case "string": case "bool": return true; + // JSON.stringify writes a Date's toJSON(); JSON text never parses to one. + case "date": + return stringify; case "array": return isJsonSafeAt( t.elem, @@ -4073,7 +4089,6 @@ function isJsonSafeAt( // Maps are not JSON (JSON.stringify(new Map()) is "{}" in Node — an // empty-object husk nobody wants; stringify/dynCheck reject instead). case "map": - case "date": // URLSearchParams stringifies as the same "{}" husk — rejected; use // sp.toString() instead. case "searchParams": diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 63aa8ff5c..230d2385d 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -55,6 +55,7 @@ import { typeEquals, typeKey, maybePromiseUnionArmsOk, + unionContainerArmsOk, VOID, } from "./ir.js"; import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js"; @@ -801,7 +802,6 @@ export function validateModule(mod: IrModule): IrValidationError[] { arm.kind === "void" || arm.kind === "union" || arm.kind === "dyn" || - arm.kind === "date" || (arm.kind === "generator" && iterableArmsElem(u.arms) === null) ) { errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc }); @@ -812,6 +812,9 @@ export function validateModule(mod: IrModule): IrValidationError[] { loc: noLoc, }); } + if (arm.kind === "date" && !unionContainerArmsOk(u.arms)) { + errors.push({ message: `union ${u.id}: date arm ${i} beside non-unit arms`, loc: noLoc }); + } if (arm.kind === "record" && !recordsById.has(arm.shapeId)) { errors.push({ message: `union ${u.id}: arm ${i} references undeclared shape "${arm.shapeId}"`, @@ -1505,6 +1508,7 @@ function validateFunction( e.left.type.kind === "map" || e.left.type.kind === "set" || e.left.type.kind === "regex" || + e.left.type.kind === "date" || e.left.type.kind === "url" || e.left.type.kind === "object" || e.left.type.kind === "record" || @@ -2370,9 +2374,13 @@ function validateFunction( (e.method === "sortPrimitive" || e.method === "toSortedPrimitive") && elem.kind !== "string" && elem.kind !== "f64" && - elem.kind !== "bool" + elem.kind !== "bool" && + elem.kind !== "date" ) { - err(`arrIntrinsic ${e.method} requires primitive elements, got ${elem.kind}`, e.loc); + err( + `arrIntrinsic ${e.method} requires primitive or Date elements, got ${elem.kind}`, + e.loc, + ); } if (e.method === "sortValues" || e.method === "sortDense") { const arms = elem.kind === "union" ? unions.get(elem.unionId)?.arms : undefined; @@ -2454,13 +2462,18 @@ function validateFunction( elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool" && + elem.kind !== "date" && !( elem.kind === "union" && (unions .get(elem.unionId) ?.arms.every( (a) => - a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a), + a.kind === "f64" || + a.kind === "string" || + a.kind === "bool" || + a.kind === "date" || + isUnitType(a), ) ?? false) ) diff --git a/packages/compiler/test/collection-keys.test.ts b/packages/compiler/test/collection-keys.test.ts index d73655ef1..ec049f23c 100644 --- a/packages/compiler/test/collection-keys.test.ts +++ b/packages/compiler/test/collection-keys.test.ts @@ -90,7 +90,7 @@ test("mixed-value unions select value equality while unsupported arms remain fen expect(mapKeyKindNum(union, [reference, scalar])).toBe(7); expect(isSupportedSetElem(union, [reference, scalar])).toBe(true); } - for (const unsupported of [{ kind: "date" } as IrType, mapOf(STRING, F64), setOf(STRING)]) { + for (const unsupported of [{ kind: "regex" } as IrType, mapOf(STRING, F64), setOf(STRING)]) { expect(isSupportedMapKey(unsupported)).toBe(false); } }); @@ -171,7 +171,7 @@ test.each(["map", "set"] as const)("validator checks the arms behind a %s key un expect(validateModule(mod)).toEqual([]); mod.unions![0]!.arms = [reference, F64]; expect(validateModule(mod)).toEqual([]); - mod.unions![0]!.arms = [reference, { kind: "date" }]; + mod.unions![0]!.arms = [reference, { kind: "regex" }]; expect( validateModule(mod).some((error) => error.message.includes( diff --git a/packages/compiler/test/ir.test.ts b/packages/compiler/test/ir.test.ts index 7f1a1ef78..7f5e9f3c9 100644 --- a/packages/compiler/test/ir.test.ts +++ b/packages/compiler/test/ir.test.ts @@ -8,10 +8,15 @@ test("hand-built fib module validates", () => { expect(validateModule(fibModule)).toEqual([]); }); -test("validator rejects Date union arms unsupported by the backends", () => { +test("validator rejects Date union arms beside other data arms", () => { const bad = structuredClone(fibModule); - bad.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "undefinedT" }] }]; - expect(validateModule(bad).map((e) => e.message)).toContain("union u-date: arm 0 is date"); + bad.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "string" }] }]; + expect(validateModule(bad).map((e) => e.message)).toContain( + "union u-date: date arm 0 beside non-unit arms", + ); + const nullable = structuredClone(fibModule); + nullable.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "undefinedT" }] }]; + expect(validateModule(nullable)).toEqual([]); }); test("fib module JSON round-trips", () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json new file mode 100644 index 000000000..c2497d502 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5965-date-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5965-date-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json deleted file mode 100644 index cf6ad7398..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/date-union-fences.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/date-union-fences.ts" - ], - "diags": [] -} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index f3e216a36..a3b95146c 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -3058,10 +3058,12 @@ typedef struct { typedef struct { ScrElemKind kind; const ScrNumberSortKey *numbers; + ScrStr *const *texts; /* reference elements: each element's String() text */ } ScrPrimitiveOrder; static int scr_arr_primitive_compare(uint64_t left, uint64_t right, const ScrPrimitiveOrder *order) { + if (order->texts) return scr_str_cmp_u16(order->texts[left], order->texts[right]); if (order->kind == SCR_ELEM_STR) return scr_str_cmp_u16(scr_slot_to_ptr(left), scr_slot_to_ptr(right)); if (order->kind == SCR_ELEM_BOOL) return (left > right) - (left < right); @@ -3149,10 +3151,28 @@ static void scr_arr_primitive_order(uint64_t *values, size_t count, if (runs != local_runs) free(runs); } +static ScrArr *scr_arr_sort_default(ScrArr *a, bool copy, ScrStr *(*text)(void *)); + +static ScrArr *scr_arr_new_like(const ScrArr *a, size_t cap) { + return a->elem == SCR_ELEM_REF ? scr_arr_new_ops(scr_arr_ops(a), cap) : scr_arr_new(a->elem, cap); +} + ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { if (a->elem != SCR_ELEM_STR && a->elem != SCR_ELEM_F64 && a->elem != SCR_ELEM_BOOL) scr_trap("scriptc: invalid primitive array ordering\n"); - if (a->len == 0) return copy ? scr_arr_new(a->elem, 0) : scr_arr_retain(a); + return scr_arr_sort_default(a, copy, NULL); +} + +/* The default ordering of an array of reference elements that cannot call + * user code while converting to strings (Dates): `text` answers each + * element's String() text (+1), computed once per element. */ +ScrArr *scr_arr_sort_by_text(ScrArr *a, bool copy, ScrStr *(*text)(void *)) { + if (a->elem != SCR_ELEM_REF) scr_trap("scriptc: invalid text array ordering\n"); + return scr_arr_sort_default(a, copy, text); +} + +static ScrArr *scr_arr_sort_default(ScrArr *a, bool copy, ScrStr *(*text)(void *)) { + if (a->len == 0) return copy ? scr_arr_new_like(a, 0) : scr_arr_retain(a); size_t dense = a->len < a->cap ? a->len : a->cap; size_t count = 0, undefined_count = 0; const uint8_t *states = scr_arr_scan_states(a); @@ -3184,14 +3204,35 @@ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy) { values[i] = i; } } - ScrPrimitiveOrder order = {a->elem, numbers}; + ScrStr **texts = NULL; + uint64_t *originals = NULL; + if (text && count > 1) { + if (count > SIZE_MAX / sizeof(*texts)) scr_arr_oom(); + texts = malloc(count * sizeof(*texts)); + originals = malloc(count * sizeof(*originals)); + if (!texts || !originals) scr_arr_oom(); + for (size_t i = 0; i < count; i++) { + originals[i] = values[i]; + texts[i] = text(scr_slot_to_ptr(values[i])); + values[i] = i; + } + } + ScrPrimitiveOrder order = {a->elem, numbers, texts}; scr_arr_primitive_order(values, count, &order); if (numbers) { for (size_t i = 0; i < count; i++) values[i] = numbers[values[i]].value; free(numbers); } + if (texts) { + for (size_t i = 0; i < count; i++) { + values[i] = originals[values[i]]; + scr_str_release(texts[i]); + } + free(texts); + free(originals); + } size_t present = copy ? a->len : count + undefined_count; - ScrArr *out = copy ? scr_arr_new(a->elem, present) : a; + ScrArr *out = copy ? scr_arr_new_like(a, present) : a; if (!copy && !states) { /* Values only, in place. */ if (count) scr_arr_put_slots(a, 0, count, values); diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index e13acb233..2aa1f4c84 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -576,6 +576,10 @@ double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src); ScrArr *scr_arr_reverse(ScrArr *a); /* Default primitive String ordering; stable, borrowed receiver, owned result. */ ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy); +/* Default ordering of reference elements by their String() text, which + * `text` answers (+1) for each element once (Date arrays). Like + * scr_arr_sort_primitive otherwise. */ +ScrArr *scr_arr_sort_by_text(ScrArr *a, bool copy, ScrStr *(*text)(void *)); /* Stable comparator sort of the first `count` slots of a private, compacted * snapshot (every slot a value; f64 or reference elements). `f` is a borrowed * closure of `arity` (0..2) element parameters returning a number. Elements diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 28c2accce..c86d2a71d 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -767,6 +767,8 @@ ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments); ScrStr *scr_dyn_object_tag_of(const ScrDyn *d); ScrDyn *scr_dyn_native_date_from_ms(double ms); +typedef struct ScrDate ScrDate; +ScrDyn *scr_dyn_native_date(ScrDate *date); double scr_dyn_native_date_value(const ScrDyn *value); ScrDyn *scr_dyn_native_regex(ScrRegex *value); bool scr_dyn_native_regex_is(const ScrDyn *value); @@ -1406,6 +1408,9 @@ ScrClassObj *scr_dyn_class_check(const ScrDyn *value, const char *type_key, cons * prints as "Uncaught TypeError: ..." and exits 1). This is * scriptc-specific behavior — JS `as` never checks (SEMANTICS.md). */ void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got); +/* The Date object a checked-dynamic Date handle holds, +1, or NULL with + * the check failure pending. */ +ScrDate *scr_dyn_native_date_check(const ScrDyn *value, const ScrDynPath *path); /* Matchers and builders of recursive shapes (records and arrays whose type * reaches itself) track the dyn values they visit, so cyclic values match diff --git a/packages/runtime/src/scr_json.h b/packages/runtime/src/scr_json.h index e3077703e..de4510264 100644 --- a/packages/runtime/src/scr_json.h +++ b/packages/runtime/src/scr_json.h @@ -162,6 +162,10 @@ void scr_jb_put_number(ScrJsonBuf *b, double v); * as \u00XX, lone surrogates as \udXXX, everything else (UTF-8 included) * verbatim — exactly the JS JSON.stringify escape set. */ void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s); +/* JSON.stringify of a Date object: toJSON()'s ISO text, null if invalid + * (scr_lib.c). */ +typedef struct ScrDate ScrDate; +void scr_jb_put_date(ScrJsonBuf *b, const ScrDate *date); /* JSON-serialize a dyn value into the buffer — the sc_jw_* walker for the * dyn leaves the compiler cannot type: undefined prints null (an array diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index c17f44f9d..18d9c39e5 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -12549,6 +12549,39 @@ static ScrStr *scr_date_text(double ms, int mode) { } ScrStr *scr_date_to_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_FULL); } + +/* JSON.stringify of a Date object: its toJSON(), the quoted + * toISOString() text, or null for an invalid date. */ +void scr_jb_put_date(ScrJsonBuf *b, const ScrDate *date) { + if (isnan(date->ms)) { + scr_jb_puts(b, "null"); + return; + } + ScrStr *iso = scr_date_to_iso(date->ms); + scr_jb_put_json_str(b, iso); + scr_str_release(iso); +} + +/* String(date) of a Date object: Date arrays' default ordering key. +1. */ +ScrStr *scr_date_object_to_string(void *date) { + return scr_date_text(((ScrDate *)date)->ms, SCR_DATE_TEXT_FULL); +} + +/* Array.prototype.join over Date objects: each element's String() text, + * holes empty. Both borrowed; +1. */ +ScrStr *scr_date_arr_join(ScrArr *dates, const ScrStr *sep) { + ScrJsonBuf b; + scr_jb_init(&b); + double len = scr_arr_len(dates); + for (double i = 0; i < len; i++) { + if (i > 0) scr_jb_put_str(&b, sep); + if (scr_arr_state(dates, i) != 1) continue; + ScrStr *text = scr_date_object_to_string(scr_arr_peek_ref(dates, i)); + scr_jb_put_str(&b, text); + scr_str_release(text); + } + return scr_jb_finish(&b); +} ScrStr *scr_date_to_date_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_DATE); } ScrStr *scr_date_to_time_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_TIME); } @@ -13009,11 +13042,29 @@ ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops) { return out; } -/* JS Date storage owns an identity; the typed read-only Date ABI continues - * to use milliseconds and explicitly extracts them for native getters. */ -typedef struct { size_t rc; double milliseconds; } ScrNativeDate; -static void *scr_native_date_retain(void *ptr) { ((ScrNativeDate *)ptr)->rc++; return ptr; } -static void scr_native_date_release(void *ptr) { if (--((ScrNativeDate *)ptr)->rc == 0) free(ptr); } +/* A Date object: one time value, and its identity. Compiled code and + * checked-dynamic Date handles share these objects, so a Date keeps its + * identity across `unknown` conversions. */ +ScrDate *scr_date_new(double ms) { + ScrDate *date = malloc(sizeof *date); + if (!date) scr_trap("scriptc: out of memory\n"); + date->rc = 1; + date->ms = ms; + return date; +} + +ScrDate *scr_date_retain(ScrDate *date) { + if (date->rc != SIZE_MAX) date->rc++; + return date; +} + +void scr_date_release(ScrDate *date) { + if (!date || date->rc == SIZE_MAX) return; + if (--date->rc == 0) free(date); +} + +void *scr_date_retain_v(void *p) { return scr_date_retain(p); } +void scr_date_release_v(void *p) { scr_date_release(p); } bool scr_dyn_native_date_is(const ScrDyn *value) { return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE; @@ -13124,13 +13175,25 @@ static ScrStr *scr_date_class_name(const ScrDyn *value) { } double scr_dyn_native_date_value(const ScrDyn *value) { - if (scr_dyn_native_date_is(value)) return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; + if (scr_dyn_native_date_is(value)) return ((ScrDate *)value->v.handle.ptr)->ms; double time; if (scr_dyn_date_class_time(value, &time)) return time; scr_dyn_check_fail(NULL, "Date", value); return NAN; } +/* The Date object a checked-dynamic Date value holds, +1: the Date itself, + * or a new Date with the time value of an instance of a class declared + * `extends Date`. NULL with the check failure pending for any other + * value. */ +ScrDate *scr_dyn_native_date_check(const ScrDyn *value, const ScrDynPath *path) { + if (scr_dyn_native_date_is(value)) return scr_date_retain(value->v.handle.ptr); + double time; + if (scr_dyn_date_class_time(value, &time)) return scr_date_new(time); + scr_dyn_check_fail(path, "Date", value); + return NULL; +} + /* Method names without another use in this file, in named arrays (see * scr_tz_posix_ids). */ static const char scr_date_to_date_string_name[] = "toDateString"; @@ -13139,7 +13202,7 @@ static const char scr_date_to_time_string_name[] = "toTimeString"; static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)self; (void)what; - double ms = ((ScrNativeDate *)ptr)->milliseconds; + double ms = ((ScrDate *)ptr)->ms; if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms); if (!strcmp(method, "toString") || !strcmp(method, scr_date_to_date_string_name) || !strcmp(method, scr_date_to_time_string_name) || !strcmp(method, "toUTCString")) { @@ -13162,8 +13225,8 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho if (!strcmp(method, "setTime")) { double time = NAN; if (argc > 0 && !scr_dyn_number_coerce_js(args[0], &time)) return NULL; - ((ScrNativeDate *)ptr)->milliseconds = scr_date_new_ms(time); - return scr_dyn_new_num(((ScrNativeDate *)ptr)->milliseconds); + ((ScrDate *)ptr)->ms = scr_date_new_ms(time); + return scr_dyn_new_num(((ScrDate *)ptr)->ms); } #define DATE_GET(name, fn) if (!strcmp(method, name)) return scr_dyn_new_num(fn(ms)) DATE_GET("getFullYear", scr_date_get_full_year_local); @@ -13209,7 +13272,7 @@ double scr_dyn_date_arguments_time(const ScrDyn *arguments) { ScrDyn *date = arguments->v.arr.len > 1 ? scr_dyn_native_date_new_fields(arguments) : scr_dyn_native_date_new_value(arguments); if (!date) return NAN; - double time = ((ScrNativeDate *)date->v.handle.ptr)->milliseconds; + double time = ((ScrDate *)date->v.handle.ptr)->ms; scr_dyn_release(date); return time; } @@ -13274,10 +13337,10 @@ static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *a result = isfinite(time) ? scr_date_class_call(SCR_DATE_CLASS_TO_ISO_STRING, self, NULL) : scr_dyn_new_null(); } else { - ScrNativeDate date = { 1, time }; + ScrDate date = { 1, time }; result = scr_native_date_invoke(&date, self, method, args, argc, method); - if (result && date.milliseconds != time && !(isnan(date.milliseconds) && isnan(time))) { - ScrDyn *stored = scr_dyn_new_num(date.milliseconds); + if (result && date.ms != time && !(isnan(date.ms) && isnan(time))) { + ScrDyn *stored = scr_dyn_new_num(date.ms); ScrDyn *set = scr_date_class_call(SCR_DATE_CLASS_SET_TIME, self, stored); scr_dyn_release(stored); if (!set) { @@ -13330,19 +13393,22 @@ static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) { return NULL; } -/* A Date handle holding the time value `ms` (already TimeClip'd). */ -ScrDyn *scr_dyn_native_date_from_ms(double ms) { +/* A checked-dynamic handle for the Date object `date` (borrowed; the + * handle retains it), so the value stays the same object. */ +ScrDyn *scr_dyn_native_date(ScrDate *date) { static const ScrDynHandleOps ops = { - "Date", &scr_native_date_retain, &scr_native_date_release, &scr_native_date_invoke, + "Date", &scr_date_retain_v, &scr_date_release_v, &scr_native_date_invoke, &scr_native_date_get, NULL, NULL, NULL, }; scr_dyn_handle_install(SCR_DYNH_DATE, &ops); - ScrNativeDate *date = malloc(sizeof *date); - if (!date) scr_trap("scriptc: out of memory\n"); - date->rc = 1; - date->milliseconds = ms; - ScrDyn *result = scr_dyn_new_handle(date, SCR_DYNH_DATE); - scr_native_date_release(date); + return scr_dyn_new_handle(date, SCR_DYNH_DATE); +} + +/* A Date handle holding the time value `ms` (already TimeClip'd). */ +ScrDyn *scr_dyn_native_date_from_ms(double ms) { + ScrDate *date = scr_date_new(ms); + ScrDyn *result = scr_dyn_native_date(date); + scr_date_release(date); return result; } diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 78d19614b..a71b0ccfd 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -111,6 +111,18 @@ double scr_date_now(void); /* integer ms since epoch, like Node */ extern SCR_TL const double *scr_date_mock_now; double scr_date_new_ms(double ms); /* TimeClip (NaN when invalid) */ double scr_date_get_time(double ms); +/* A Date object: the TimeClip'd time value in milliseconds (NaN for an + * invalid date). Dates are immutable; their identity is the pointer. + * Retain/release follow the runtime's RC conventions (SIZE_MAX immortal). */ +typedef struct ScrDate { + size_t rc; + double ms; +} ScrDate; +ScrDate *scr_date_new(double ms); /* +1 */ +ScrDate *scr_date_retain(ScrDate *date); +void scr_date_release(ScrDate *date); +void *scr_date_retain_v(void *p); +void scr_date_release_v(void *p); /* Node's exact ISO 8601 UTC format (expanded ±YYYYYY years outside * 0–9999). THROWS Node's "Invalid time value" RangeError on NaN or * |ms| > 8.64e15 and returns NULL; +1 otherwise. */ @@ -150,6 +162,11 @@ double scr_date_get_timezone_offset(double ms); ScrStr *scr_date_to_string(double ms); ScrStr *scr_date_to_date_string(double ms); ScrStr *scr_date_to_time_string(double ms); +/* String() of a Date object (ScrDate *), and Array.prototype.join over an + * array of Date objects (holes empty). +1. */ +ScrStr *scr_date_object_to_string(void *date); +struct ScrArr; +ScrStr *scr_date_arr_join(struct ScrArr *dates, const ScrStr *sep); /* The long time zone name Node prints after the GMT offset for the host * zone at time value `ms`; `isdst` is tm_isdst at `ms`, or -1 when not * known. Borrowed; valid until the next call on the same thread. */ diff --git a/tests/corpus/5965-date-objects.ts b/tests/corpus/5965-date-objects.ts new file mode 100644 index 000000000..c4bb01fb9 --- /dev/null +++ b/tests/corpus/5965-date-objects.ts @@ -0,0 +1,115 @@ +// Dates are objects with identity: arrays of Dates, `Date | undefined` +// and `Date | null` slots, === by identity, includes/indexOf/lastIndexOf, +// at/find/pop answering undefined, Map and Set keys and values, join, +// String() and default sort through toString(), and Dates in records, +// tuples, class fields, closures, async results and generators. +export {}; +process.env.TZ = "America/Chicago"; + +const base = new Date(Date.UTC(2026, 3, 1, 12)); +const same = base; +const copy = new Date(base); +const equalTime = new Date(base.getTime()); +console.log(base === same, base === copy, copy === equalTime, base !== copy); +console.log(copy.getTime() === base.getTime(), base < new Date(base.getTime() + 1), base >= copy); + +// Arrays. +const dates: Date[] = [base, new Date(Date.UTC(2025, 0, 1)), new Date(Date.UTC(2026, 0, 1))]; +console.log(dates.length, dates.includes(base), dates.includes(copy), dates.indexOf(dates[2]!)); +console.log(dates.indexOf(equalTime), dates.lastIndexOf(base), dates.indexOf(base, 1)); +console.log(dates.join(" | ")); +console.log(String(dates)); +console.log(`${dates}`); +console.log([...dates].sort().map((d) => d.toISOString())); +console.log(dates.toSorted((a, b) => a.getTime() - b.getTime()).map((d) => d.getUTCFullYear())); +const first = dates.at(0); +const missing = dates.at(10); +console.log(first === base, missing, missing === undefined, first?.toISOString(), missing?.toISOString()); +const found = dates.find((d) => d.getUTCFullYear() === 2025); +console.log(found === dates[1], dates.find((d) => d.getTime() < 0), dates.findLast((d) => d > base)); +const stack = [...dates]; +console.log(stack.pop() === dates[2], stack.pop()?.getUTCFullYear(), stack.pop() === base, stack.pop()); +const latest = dates.reduce((m, d) => (d > m ? d : m)); +console.log(latest === base, dates.filter((d) => d < base).length, dates.map((d) => d.getUTCMonth())); +const grid: Date[][] = [[base], [copy, base]]; +console.log(grid[1]!.indexOf(base), grid.flat().length, grid.flat().includes(copy)); +const holes: (Date | undefined)[] = [base, undefined, copy]; +console.log(holes.join(","), holes.indexOf(undefined), holes.filter((d) => d !== undefined).length); + +// Nullable slots. +let maybe: Date | null = null; +console.log(maybe === null, maybe); +maybe = base; +console.log(maybe === base, maybe.getUTCHours()); +function stamp(d?: Date): string { + return d === undefined ? "none" : d.toISOString(); +} +function withDefault(d: Date = new Date(0)): number { + return d.getTime(); +} +console.log(stamp(), stamp(base), withDefault(), withDefault(base) === base.getTime()); + +// Collections. +const notes = new Map([[base, "base"]]); +notes.set(copy, "copy"); +console.log(notes.get(base), notes.get(copy), notes.get(equalTime), notes.size); +const byName = new Map([["start", base]]); +console.log(byName.get("start") === base, byName.get("end")); +const seen = new Set([base, same, copy]); +console.log(seen.size, seen.has(base), seen.has(equalTime)); + +// Records, tuples, classes and closures. +interface Event { + name: string; + at: Date; + ends?: Date; +} +const events: Event[] = [ + { name: "a", at: base }, + { name: "b", at: copy, ends: base }, +]; +console.log(events.map((e) => `${e.name}@${e.at.toISOString()} ends=${e.ends?.toISOString() ?? "-"}`)); +console.log(events[1]!.ends === events[0]!.at, events[1]!.at === events[0]!.at); +const pair: [Date, string] = [base, "x"]; +console.log(pair[0] === base, pair[1]); +class Window { + from: Date; + to: Date | null = null; + later!: Date; + constructor(from: Date) { + this.from = from; + } + span(): number { + return this.to === null ? 0 : this.to.getTime() - this.from.getTime(); + } +} +const w = new Window(base); +w.later = copy; +console.log(w.span(), w.later === copy); +w.to = new Date(base.getTime() + 60000); +console.log(w.span(), w.to === w.from); +let captured: Date | undefined; +const remember = (d: Date): void => { + captured = d; +}; +remember(base); +console.log(captured === base, captured?.getUTCDate()); + +// Async results, generators and unknown. +async function later(d: Date): Promise { + await null; + return new Date(d.getTime() + 1); +} +const next = await later(base); +console.log(next.getTime() - base.getTime(), next === base); +function* ticks(start: Date, n: number): Generator { + for (let i = 0; i < n; i++) yield new Date(start.getTime() + i * 1000); +} +console.log([...ticks(base, 3)].map((d) => d.getUTCSeconds())); +const held: unknown = base; +console.log((held as Date) === base, held instanceof Date, String(held) === String(base)); +const bag: unknown = { at: base, list: dates }; +console.log(JSON.stringify(bag)); +console.log(JSON.stringify(events), JSON.stringify([base, new Date(NaN)])); +const byKey: { [k: string]: Date } = { boot: base }; +console.log(byKey["boot"] === base, byKey["missing"], Object.keys(byKey)); diff --git a/tests/diagnostics/component-fences.ts b/tests/diagnostics/component-fences.ts index 6d88f9af5..0d382bf28 100644 --- a/tests/diagnostics/component-fences.ts +++ b/tests/diagnostics/component-fences.ts @@ -5,9 +5,9 @@ const byFlag = new Map(); console.log(byFlag); -// Map values have no slot for Dates. -const stamps = new Map(); -console.log(stamps); +// Map values have no slot for URLSearchParams. +const queries = new Map(); +console.log(queries); // Boolean Set elements remain outside the supported element domain. const flags = new Set(); diff --git a/tests/diagnostics/date-union-fences.ts b/tests/diagnostics/date-union-fences.ts deleted file mode 100644 index 478df74d6..000000000 --- a/tests/diagnostics/date-union-fences.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Compiler-synthesized `T | undefined` slots must observe the same Date -// union fence as source-written unions. Each construct below used to reach -// IR validation as an SC9001 internal compiler error. - -function defaultDate(value: Date = new Date(0)): number { - return value.getTime(); -} -defaultDate(); - -function* dates(): Generator { - yield new Date(0); -} -for (const value of dates()) console.log(value.getTime()); - -class DeferredDate { - value!: Date; - initialize(): void { - this.value = new Date(0); - } -} -const deferred = new DeferredDate(); -deferred.initialize(); diff --git a/tests/diagnostics/stdlib-fence.ts b/tests/diagnostics/stdlib-fence.ts index 1fdc01675..d28333178 100644 --- a/tests/diagnostics/stdlib-fence.ts +++ b/tests/diagnostics/stdlib-fence.ts @@ -23,7 +23,6 @@ const fmt = new Intl.NumberFormat(); const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced const mutableDate = new Date(0); mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice -const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity const wr = new WeakRef({}); const px = new Proxy({ a: 1 }, {}); const buf = new ArrayBuffer(8); @@ -74,13 +73,6 @@ function pick(a: string | number, b: number | boolean): boolean { } const picked = pick(1, 2); -// Date values deliberately have no tagged-union payload yet. Nullable -// Date positions must fence in the frontend instead of reaching a backend -// scalar/ref mismatch. -function optionalDate(value: Date | undefined): number { - return value === undefined ? -1 : value.getTime(); -} -optionalDate(undefined); function throwDate(): void { throw new Date(0); // the exception cell cannot preserve Date's object kind } diff --git a/tests/diagnostics/utility-types.ts b/tests/diagnostics/utility-types.ts index c6d10648b..cad1fcca4 100644 --- a/tests/diagnostics/utility-types.ts +++ b/tests/diagnostics/utility-types.ts @@ -5,9 +5,7 @@ // fields + an overflow map; number keys canonicalize to their JS string // spelling; function/Map/Set/nested-record values ride the same store — // differential corpus); what stays rejected: -// - symbol-keyed index signatures (no lowering for symbol keys); -// - index-signature value types outside the overflow-map slice (Dates and -// other lib API objects, even when the value itself has a representation). +// - symbol-keyed index signatures (no lowering for symbol keys). // (Readonly bindings compile, holding the Date as a checked value. // Dot access/writes to index-signature keys compile now — the overflow // path in dot spelling — and self-referential mapped types intern as @@ -15,9 +13,5 @@ const bySymbol: { [s: symbol]: string } = {}; console.log(bySymbol); -const dateBag: { [k: string]: Date } = {}; -const boot = dateBag["boot"]; -console.log(boot); - const stamped: Readonly = new Date(0); console.log(stamped.getTime()); diff --git a/tests/harness/__snapshots__/class-interface-refinements.ts.txt b/tests/harness/__snapshots__/class-interface-refinements.ts.txt index 043cb3996..9d7d7b87f 100644 --- a/tests/harness/__snapshots__/class-interface-refinements.ts.txt +++ b/tests/harness/__snapshots__/class-interface-refinements.ts.txt @@ -39,7 +39,7 @@ class-interface-refinements.ts:17:13 - error SC2004: uses of 'changed' inherit t hint: the declaration of 'changed' did not compile — fix the diagnostic reported there and these sites clear with it -class-interface-refinements.ts:20:18 - error SC2006: values of type 'Indexed' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') +class-interface-refinements.ts:20:18 - error SC2006: values of type 'Indexed' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown') 19 | type Indexed = Box & { [name: string]: number }; 20 | function indexed(view: Indexed, key: string): number { return view[key]!; } diff --git a/tests/harness/__snapshots__/component-fences.ts.txt b/tests/harness/__snapshots__/component-fences.ts.txt index 5ed6ad6b9..45d476089 100644 --- a/tests/harness/__snapshots__/component-fences.ts.txt +++ b/tests/harness/__snapshots__/component-fences.ts.txt @@ -1,27 +1,27 @@ -component-fences.ts:9:7 - error SC2009: values of type 'Map' cannot be compiled: the Map shape is supported, but 'Date' values have no Map slot yet +component-fences.ts:9:7 - error SC2009: values of type 'Map' cannot be compiled: the Map shape is supported, but 'URLSearchParams' values have no Map slot yet - 8 | // Map values have no slot for Dates. - 9 | const stamps = new Map(); - | ^~~~~~ - 10 | console.log(stamps); + 8 | // Map values have no slot for URLSearchParams. + 9 | const queries = new Map(); + | ^~~~~~~ + 10 | console.log(queries); 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') -component-fences.ts:9:16 - error SC1090: Map values of type 'Date' (Map values must be number, string, boolean, records, class instances, functions, typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values) is not supported yet +component-fences.ts:9:17 - error SC1090: Map values of type 'URLSearchParams' (Map values must be number, string, boolean, records, class instances, functions, typed arrays, arrays, promises, Dates, Maps, Sets, or supported unions and checked values) is not supported yet - 8 | // Map values have no slot for Dates. - 9 | const stamps = new Map(); - | ^~~~~~~~~~~~~~~~~~~~~~~ - 10 | console.log(stamps); + 8 | // Map values have no slot for URLSearchParams. + 9 | const queries = new Map(); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 10 | console.log(queries); -component-fences.ts:10:13 - error SC2004: uses of 'stamps' inherit the blocker on its declaration +component-fences.ts:10:13 - error SC2004: uses of 'queries' inherit the blocker on its declaration - 9 | const stamps = new Map(); - 10 | console.log(stamps); - | ^~~~~~ + 9 | const queries = new Map(); + 10 | console.log(queries); + | ^~~~~~~ 11 | - hint: the declaration of 'stamps' did not compile — fix the diagnostic reported there and these sites clear with it + hint: the declaration of 'queries' did not compile — fix the diagnostic reported there and these sites clear with it component-fences.ts:35:7 - error SC2009: values of type '() => WeakRef' cannot be compiled: the function shape is supported, but its return type 'WeakRef' does not compile diff --git a/tests/harness/__snapshots__/date-union-fences.ts.txt b/tests/harness/__snapshots__/date-union-fences.ts.txt deleted file mode 100644 index 1b504f23a..000000000 --- a/tests/harness/__snapshots__/date-union-fences.ts.txt +++ /dev/null @@ -1,56 +0,0 @@ -date-union-fences.ts:5:22 - error SC1090: parameter default values on 'Date'-typed parameters are not supported yet - - 4 | - 5 | function defaultDate(value: Date = new Date(0)): number { - | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | return value.getTime(); - -date-union-fences.ts:8:1 - error SC2004: uses of 'defaultDate' inherit the blocker on its declaration - - 7 | } - 8 | defaultDate(); - | ^~~~~~~~~~~ - 9 | - - hint: the declaration of 'defaultDate' did not compile — fix the diagnostic reported there and these sites clear with it - -date-union-fences.ts:10:11 - error SC2009: values of type 'Generator' cannot be compiled: the Generator shape is supported, but this instantiation's type arguments are outside the supported set - - 9 | - 10 | function* dates(): Generator { - | ^~~~~ - 11 | yield new Date(0); - - 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') - -date-union-fences.ts:13:21 - error SC2004: uses of 'dates' inherit the blocker on its declaration - - 12 | } - 13 | for (const value of dates()) console.log(value.getTime()); - | ^~~~~ - 14 | - - hint: the declaration of 'dates' did not compile — fix the diagnostic reported there and these sites clear with it - -date-union-fences.ts:16:3 - error SC1090: definite assignment assertions on fields not assigned at the constructor's top level ('value!' defers the first assignment past construction — the field would hold garbage, not undefined, until it runs; assign it in the constructor or include undefined in its type) is not supported yet - - 15 | class DeferredDate { - 16 | value!: Date; - | ^~~~~ - 17 | initialize(): void { - -date-union-fences.ts:21:18 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet - - 20 | } - 21 | const deferred = new DeferredDate(); - | ^~~~~~~~~~~~~~~~~~ - 22 | deferred.initialize(); - -date-union-fences.ts:22:1 - error SC2004: uses of 'deferred' inherit the blocker on its declaration - - 21 | const deferred = new DeferredDate(); - 22 | deferred.initialize(); - | ^~~~~~~~ - 23 | - - hint: the declaration of 'deferred' 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/__snapshots__/generics.ts.txt b/tests/harness/__snapshots__/generics.ts.txt index df6e552db..4dfc8019f 100644 --- a/tests/harness/__snapshots__/generics.ts.txt +++ b/tests/harness/__snapshots__/generics.ts.txt @@ -5,7 +5,7 @@ generics.ts:23:3 - error SC1090: generic nested functions (only top-level generi | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 24 | return x; -generics.ts:30:10 - error SC1090: '.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet (instantiating 'joinAll' with ) +generics.ts:30:10 - error SC1090: '.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet (instantiating 'joinAll' with ) 29 | function joinAll(a: T[]): string { 30 | return a.join(","); diff --git a/tests/harness/__snapshots__/index-signature-values.ts.txt b/tests/harness/__snapshots__/index-signature-values.ts.txt index bbbed7fe2..084d6a259 100644 --- a/tests/harness/__snapshots__/index-signature-values.ts.txt +++ b/tests/harness/__snapshots__/index-signature-values.ts.txt @@ -1,4 +1,4 @@ -index-signature-values.ts:8:7 - error SC2006: values of type '{ [s: symbol]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') +index-signature-values.ts:8:7 - error SC2006: values of type '{ [s: symbol]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown') 7 | // A symbol-keyed signature has no lowering. 8 | const bySymbol: { [s: symbol]: number } = {}; @@ -14,7 +14,7 @@ index-signature-values.ts:13:13 - error SC1090: console.log of '{ [key: string]: | ^~~~~~~ 14 | -index-signature-values.ts:18:7 - error SC2006: values of type '{ [k: string]: string | number; [n: number]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') +index-signature-values.ts:18:7 - error SC2006: values of type '{ [k: string]: string | number; [n: number]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown') 17 | // answer per key). 18 | const dual: { [k: string]: string | number; [n: number]: number } = {}; diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index af1725323..c183f7cdc 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -64,104 +64,79 @@ stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with argumen 24 | const mutableDate = new Date(0); 25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity + 26 | const wr = new WeakRef({}); hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering -stdlib-fence.ts:26:18 - error SC1043: comparing non-number, non-string values are not supported yet +stdlib-fence.ts:26:7 - error SC2020: 'WeakRef<{}>' is part of the standard library types but has no scriptc lowering yet 25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice - 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ - 27 | const wr = new WeakRef({}); - -stdlib-fence.ts:27:7 - error SC2020: 'WeakRef<{}>' is part of the standard library types but has no scriptc lowering yet - - 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - 27 | const wr = new WeakRef({}); + 26 | const wr = new WeakRef({}); | ^~ - 28 | const px = new Proxy({ a: 1 }, {}); + 27 | const px = new Proxy({ a: 1 }, {}); hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -stdlib-fence.ts:27:12 - error SC2020: 'new WeakRef' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:26:12 - error SC2020: 'new WeakRef' is part of the standard library types but has no scriptc lowering yet - 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - 27 | const wr = new WeakRef({}); + 25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice + 26 | const wr = new WeakRef({}); | ^~~~~~~~~~~~~~~ - 28 | const px = new Proxy({ a: 1 }, {}); + 27 | const px = new Proxy({ a: 1 }, {}); hint: deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead -stdlib-fence.ts:42:31 - error SC1090: '.split()' on a 'RegExp | string' separator (pass a string, or a regex literal) is not supported yet +stdlib-fence.ts:41:31 - error SC1090: '.split()' on a 'RegExp | string' separator (pass a string, or a regex literal) is not supported yet - 41 | const norm = "abc".normalize(); - 42 | const limited = "a,b,c".split(Math.random() ? "," : /,/, 2); // union separators stay fenced; limits lower + 40 | const norm = "abc".normalize(); + 41 | const limited = "a,b,c".split(Math.random() ? "," : /,/, 2); // union separators stay fenced; limits lower | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 43 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) + 42 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) -stdlib-fence.ts:44:19 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:43:19 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet - 43 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) - 44 | const localized = (1234.5).toLocaleString(); + 42 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer) + 43 | const localized = (1234.5).toLocaleString(); | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 45 | // Unlowered call FORMS of lowered members. (push is fully variadic now — + 44 | // Unlowered call FORMS of lowered members. (push is fully variadic now — hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") -stdlib-fence.ts:50:1 - error SC2020: '.forEach with 2 arguments' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:49:1 - error SC2020: '.forEach with 2 arguments' is part of the standard library types but has no scriptc lowering yet - 49 | const joined = nums.join(); // the omitted separator lowers - 50 | nums.forEach((x) => console.log(x), { unused: true }); // thisArg stays fenced + 48 | const joined = nums.join(); // the omitted separator lowers + 49 | nums.forEach((x) => console.log(x), { unused: true }); // thisArg stays fenced | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 51 | const pos = "abc".includes("b", 1); // the position form lowers now (indexOf's clamp) + 50 | const pos = "abc".includes("b", 1); // the position form lowers now (indexOf's clamp) hint: the thisArg parameter has no lowering — use an arrow function -stdlib-fence.ts:80:23 - error SC2009: values of type 'Date | undefined' cannot be compiled: the union shape is supported, but 'Date' arms have no home in a compiled union yet (no runtime narrowing test exists against sibling arms) +stdlib-fence.ts:77:9 - error SC1090: throwing Date values (catch bindings cannot preserve Date's object kind; throw an Error or primitive instead) are not supported yet - 79 | // scalar/ref mismatch. - 80 | function optionalDate(value: Date | undefined): number { - | ^~~~~ - 81 | return value === undefined ? -1 : value.getTime(); + 76 | function throwDate(): void { + 77 | throw new Date(0); // the exception cell cannot preserve Date's object kind + | ^~~~~~~~~~~ + 78 | } - 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') - -stdlib-fence.ts:83:1 - error SC2004: uses of 'optionalDate' inherit the blocker on its declaration +stdlib-fence.ts:83:23 - error SC1090: .throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead) is not supported yet 82 | } - 83 | optionalDate(undefined); - | ^~~~~~~~~~~~ - 84 | function throwDate(): void { - - hint: the declaration of 'optionalDate' did not compile — fix the diagnostic reported there and these sites clear with it - -stdlib-fence.ts:85:9 - error SC1090: throwing Date values (catch bindings cannot preserve Date's object kind; throw an Error or primitive instead) are not supported yet - - 84 | function throwDate(): void { - 85 | throw new Date(0); // the exception cell cannot preserve Date's object kind - | ^~~~~~~~~~~ - 86 | } - -stdlib-fence.ts:91:23 - error SC1090: .throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead) is not supported yet - - 90 | } - 91 | dateGenerator().throw(new Date(0)); + 83 | dateGenerator().throw(new Date(0)); | ^~~~~~~~~~~ - 92 | // Related classes may store one object under different union tags. A + 84 | // Related classes may store one object under different union tags. A -stdlib-fence.ts:102:10 - error SC1090: comparisons of related class values beside structural union members (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet +stdlib-fence.ts:94:10 - error SC1090: comparisons of related class values beside structural union members (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet - 101 | function relatedStrict(a: FenceValue, b: FenceValue): boolean { - 102 | return a === b; - | ^~~~~~~ - 103 | } + 93 | function relatedStrict(a: FenceValue, b: FenceValue): boolean { + 94 | return a === b; + | ^~~~~~~ + 95 | } -stdlib-fence.ts:105:10 - error SC2020: 'Object.is over related class values beside structural union members' is part of the standard library types but has no scriptc lowering yet +stdlib-fence.ts:97:10 - error SC2020: 'Object.is over related class values beside structural union members' is part of the standard library types but has no scriptc lowering yet - 104 | function relatedSameValue(a: FenceValue, b: FenceSub): boolean { - 105 | return Object.is(a, b); - | ^~~~~~~~~~~~~~~ - 106 | } + 96 | function relatedSameValue(a: FenceValue, b: FenceSub): boolean { + 97 | return Object.is(a, b); + | ^~~~~~~~~~~~~~~ + 98 | } hint: narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms \ No newline at end of file diff --git a/tests/harness/__snapshots__/tuples.ts.txt b/tests/harness/__snapshots__/tuples.ts.txt index 6b43432fe..a6f58710e 100644 --- a/tests/harness/__snapshots__/tuples.ts.txt +++ b/tests/harness/__snapshots__/tuples.ts.txt @@ -40,14 +40,14 @@ tuples.ts:31:13 - error SC1090: '.length' of a computed tuple expression (the ar | ^~~~~~~~~~~ 32 | -tuples.ts:35:13 - error SC1090: '.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet +tuples.ts:35:13 - error SC1090: '.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet 34 | const recs: { id: number }[] = [{ id: 1 }]; 35 | console.log(recs.join(",")); | ^~~~~~~~~~~~~~ 36 | const tuples: [string, string][] = [["k", "v"]]; -tuples.ts:37:13 - error SC1090: '.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet +tuples.ts:37:13 - error SC1090: '.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS) is not supported yet 36 | const tuples: [string, string][] = [["k", "v"]]; 37 | console.log(tuples.join(";")); diff --git a/tests/harness/__snapshots__/utility-types.ts.txt b/tests/harness/__snapshots__/utility-types.ts.txt index 02595967b..0c6c98489 100644 --- a/tests/harness/__snapshots__/utility-types.ts.txt +++ b/tests/harness/__snapshots__/utility-types.ts.txt @@ -1,17 +1,8 @@ -utility-types.ts:15:7 - error SC2006: values of type '{ [s: symbol]: string; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') +utility-types.ts:13:7 - error SC2006: values of type '{ [s: symbol]: string; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown') - 14 | // named recursive shapes; differential corpus covers both.) - 15 | const bySymbol: { [s: symbol]: string } = {}; + 12 | // named recursive shapes; differential corpus covers both.) + 13 | const bySymbol: { [s: symbol]: string } = {}; | ^~~~~~~~ - 16 | console.log(bySymbol); - - hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering - -utility-types.ts:18:7 - error SC2006: values of type '{ [k: string]: Date; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') - - 17 | - 18 | const dateBag: { [k: string]: Date } = {}; - | ^~~~~~~ - 19 | const boot = dateBag["boot"]; + 14 | console.log(bySymbol); hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering \ No newline at end of file diff --git a/tests/harness/corpus-costs/5965-date-objects.ts.json b/tests/harness/corpus-costs/5965-date-objects.ts.json new file mode 100644 index 000000000..c59069ff3 --- /dev/null +++ b/tests/harness/corpus-costs/5965-date-objects.ts.json @@ -0,0 +1 @@ +{ "plain": 3455, "san": 5063 } From fa843548a54d0e1ebe0b34a64df2ed4718eadd3f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:36:52 -0500 Subject: [PATCH 025/409] Keep Dates as time values where no use observes their identity --- .../compiler/src/backend/llvm/lib-dispatch.ts | 33 ++- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/date-values.ts | 189 ++++++++++++++++++ .../lowering/expressions/identifiers.ts | 7 + .../src/frontend/lowering/lower-stmts.ts | 12 ++ .../compiler/src/frontend/lowering/lowerer.ts | 4 + .../compiler/src/ir/builtin-signatures.ts | 4 + .../compiler/test/date-time-values.test.ts | 100 +++++++++ .../corpus/5966-date-time-values.ts.json | 6 + tests/corpus/5966-date-time-values.ts | 80 ++++++++ .../5966-date-time-values.ts.json | 1 + 11 files changed, 429 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/date-values.ts create mode 100644 packages/compiler/test/date-time-values.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json create mode 100644 tests/corpus/5966-date-time-values.ts create mode 100644 tests/harness/corpus-costs/5966-date-time-values.ts.json diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 753f296e6..293d98a91 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -559,21 +559,25 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll ? "scr_stream_dyn_async_iterator_open" : LIB_FN_SYMS[e.fn]); if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); + // The runtime's Date functions take and answer time values: a Date + // operand passes its time value, read at once from the borrowed object, + // and a Date result is a new Date object. const args = READ_ONLY_STRING_LIB_FNS.has(e.fn) ? emitBorrowedInputs(host, e.args) : e.args.map((a) => - borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn) - ? emitBorrowedInput(host, a) - : host.emitExpr(a), + a.type.kind === "date" + ? { name: emitDateTimeArg(host, a), type: F64 } + : borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn) + ? emitBorrowedInput(host, a) + : host.emitExpr(a), ); + // getTime and valueOf answer the time value the argument already is. + if ((e.fn === "date.getTime" || e.fn === "date.valueOf") && e.args[0]?.type.kind === "date") + return { name: args[0]!.name, type: e.type }; // The deep comparisons' number test: computed here, with no runtime call. if (e.fn === "assert.sameValue") return { name: emitSameValueF64(host, args[0]!.name, args[1]!.name), type: e.type }; - // The runtime's Date functions take and answer time values: a Date - // operand passes its time value, and a Date result is a new Date object. - const callArgs = args.map((a) => - a.type.kind === "date" ? { name: emitDateTime(host, a.name), type: F64 } : a, - ); + const callArgs = args; if (e.type.kind === "date") { const ms = emitSymbolLibCall(host, { ...e, type: F64 }, sym, callArgs); host.declare(`declare ptr @scr_date_new(double)`); @@ -584,6 +588,19 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll return emitSymbolLibCall(host, e, sym, callArgs); } +/** The time value of a Date argument. A Date that a runtime Date function + * produces for this argument alone (`new Date(ms).getTime()`, a + * time-value local's read) passes the function's time value and creates + * no Date object. */ +function emitDateTimeArg(host: LlvmEmitterContext, date: IrExpr): string { + // A time-value local's read is its time value itself. + if (date.kind === "libCall" && date.fn === "date.fromTime") + return host.emitExpr(date.args[0]!).name; + if (date.kind === "libCall" && date.fn.startsWith("date.")) + return emitGenericLibCall(host, { ...date, type: F64 }).name; + return emitDateTime(host, emitBorrowedInput(host, date).name); +} + /** A Date object's time value (ScrDate.ms). */ function emitDateTime(host: LlvmEmitterContext, date: string): string { const B = host.B; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8e7c74cad..abe43cb5e 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -330,6 +330,7 @@ export const LIB_FN_SYMS: Record = { "date.parse": "scr_date_parse", "date.getTime": "scr_date_get_time", "date.valueOf": "scr_date_get_time", + "date.fromTime": "scr_date_get_time", "date.parseGetTime": "scr_date_parse", "date.utc": "scr_date_utc", "date.newLocal": "scr_date_new_local", diff --git a/packages/compiler/src/frontend/lowering/date-values.ts b/packages/compiler/src/frontend/lowering/date-values.ts new file mode 100644 index 000000000..f40db4e6d --- /dev/null +++ b/packages/compiler/src/frontend/lowering/date-values.ts @@ -0,0 +1,189 @@ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Date methods whose receiver is only read for its time value: a Date + * local used as their receiver can store the time value instead of a Date + * object (builtins/dates.ts lowers them). */ +const TIME_VALUE_METHODS = new Set([ + "getTime", + "valueOf", + "toISOString", + "toJSON", + "toString", + "toDateString", + "toTimeString", + "toUTCString", + "getFullYear", + "getUTCFullYear", + "getMonth", + "getUTCMonth", + "getDate", + "getUTCDate", + "getDay", + "getUTCDay", + "getHours", + "getUTCHours", + "getMinutes", + "getUTCMinutes", + "getSeconds", + "getUTCSeconds", + "getMilliseconds", + "getUTCMilliseconds", + "getTimezoneOffset", +]); + +const RELATIONAL = new Set([ + ts.SyntaxKind.LessThanToken, + ts.SyntaxKind.LessThanEqualsToken, + ts.SyntaxKind.GreaterThanToken, + ts.SyntaxKind.GreaterThanEqualsToken, +]); + +/** True when a local `const` (or a `let` that is never assigned again) + * holding a Date can keep the Date's time value instead of a Date object: + * no use observes the Date's identity. Every use reads the time value as + * a Date method receiver, a String() or template operand, a relational or + * string `+` operand, or `new Date(d)`'s argument, inside the declaring + * function, or inside the block or loop that scopes a module-level + * binding (no closure captures it). Top-level module bindings keep the + * object. Other uses box the time value into a new Date at the use. */ +export function isTimeValueDateDeclaration( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): boolean { + const known = lowerer.timeValueDateDecisions.get(decl); + if (known !== undefined) return known; + const answer = decideTimeValueDate(lowerer, decl); + lowerer.timeValueDateDecisions.set(decl, answer); + return answer; +} + +function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return false; + const sf = decl.getSourceFile(); + if (sf.isDeclarationFile || /\.[cm]?jsx?$/.test(sf.fileName)) return false; + const list = decl.parent; + if (!ts.isVariableDeclarationList(list)) return false; + const isConst = (list.flags & ts.NodeFlags.Const) !== 0; + const isLet = (list.flags & ts.NodeFlags.Let) !== 0; + if (!isConst && !isLet) return false; + if (!ts.isVariableStatement(list.parent) && !ts.isForStatement(list.parent)) return false; + const fn = enclosingFunction(decl); + // The uses to check: the function body, or for a binding inside a block + // or loop at module level (a local of the module's initializer, not a + // module binding) the block or loop that scopes it. + let scope: ts.Node | undefined; + if (fn !== null) scope = fn.body; + else if (!inClassStaticBlock(decl)) scope = moduleBlockScope(decl); + if (scope === undefined) return false; + // The lib Date interface by its symbol first: mapping every candidate + // binding's type would intern its unions before the lowering does. + const typeSymbol = lowerer.typeOf(decl.name).getSymbol(); + if ( + typeSymbol === undefined || + ts.symbolName(typeSymbol) !== "Date" || + !typeSymbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) + ) + return false; + if (lowerer.mapTypeOf(lowerer.typeOf(decl.name))?.kind !== "date") return false; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + if (!symbol) return false; + let ok = true; + const visit = (node: ts.Node): void => { + if (!ok) return; + if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) { + if (lowerer.checker.getSymbolAtLocation(node) === symbol) { + if (enclosingFunction(node) !== fn || !readsTimeValue(lowerer, node)) ok = false; + } + return; + } + ts.forEachChild(node, visit); + }; + visit(scope); + return ok; +} + +/** The block or for statement that scopes a module-level binding which is + * not a module binding: a declaration in a block or loop body, or in a + * for statement's initializer. Undefined for a top-level declaration. */ +function moduleBlockScope(decl: ts.VariableDeclaration): ts.Node | undefined { + const list = decl.parent; + if (!list || !ts.isVariableDeclarationList(list)) return undefined; + if (ts.isForStatement(list.parent)) return list.parent; + const statement = list.parent; + if (!ts.isVariableStatement(statement) || ts.isSourceFile(statement.parent)) return undefined; + for (let cur: ts.Node | undefined = statement.parent; cur; cur = cur.parent) { + if (ts.isBlock(cur) || ts.isCaseBlock(cur)) return cur; + if (ts.isSourceFile(cur)) return undefined; + } + return undefined; +} + +function inClassStaticBlock(node: ts.Node): boolean { + for (let cur = node.parent; cur; cur = cur.parent) { + if (ts.isClassStaticBlockDeclaration(cur)) return true; + if (ts.isSourceFile(cur)) return false; + } + return false; +} + +function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null { + for (let cur = node.parent; cur; cur = cur.parent) { + if ( + ts.isFunctionDeclaration(cur) || + ts.isFunctionExpression(cur) || + ts.isArrowFunction(cur) || + ts.isMethodDeclaration(cur) || + ts.isConstructorDeclaration(cur) || + ts.isGetAccessorDeclaration(cur) || + ts.isSetAccessorDeclaration(cur) + ) + return cur; + if (ts.isClassStaticBlockDeclaration(cur) || ts.isSourceFile(cur)) return null; + } + return null; +} + +/** A use of the binding that reads only its time value. */ +function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean { + let node: ts.Node = id; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (ts.isPropertyAccessExpression(parent) && parent.expression === node) { + const call = parent.parent; + return ( + !parent.questionDotToken && + ts.isCallExpression(call) && + call.expression === parent && + !call.questionDotToken && + TIME_VALUE_METHODS.has(parent.name.text) && + lowerer.isStdlibMember(parent) + ); + } + if (ts.isTemplateSpan(parent)) return parent.expression === node; + if (ts.isCallExpression(parent)) { + return ( + parent.arguments.nodes.length === 1 && + parent.arguments.nodes[0] === node && + ts.isIdentifier(parent.expression) && + lowerer.isStdlibGlobal(parent.expression, "String") + ); + } + if (ts.isNewExpression(parent)) { + return ( + parent.arguments?.nodes.length === 1 && + parent.arguments.nodes[0] === node && + ts.isIdentifier(parent.expression) && + lowerer.isStdlibGlobal(parent.expression, "Date") + ); + } + if (ts.isBinaryExpression(parent)) { + const other = parent.left === node ? parent.right : parent.left; + const otherKind = lowerer.mapTypeOf(lowerer.typeOf(other))?.kind; + if (RELATIONAL.has(parent.operatorToken.kind)) return otherKind === "date"; + if (parent.operatorToken.kind === ts.SyntaxKind.PlusToken) return otherKind === "string"; + } + return false; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index ff7848930..2c82a3c64 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -8,6 +8,7 @@ import { type Lowerer, PoisonError } from "../lowerer.js"; import { BOOL, BYTES_ELEMENT_NAME, + DATE_T, DYN, F64, type IrExpr, @@ -170,6 +171,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // when the checker types this USE as a single arm, maybeNarrow // bridges the tagged representation with a unionNarrow. const local = lowerer.resolveLocal(expr); + // A Date local holding its time value reads as that Date: only uses + // that read the time value reach here (date-values.ts). + if (local && lowerer.timeValueDateLocals.has(local)) { + const time: IrExpr = { kind: "varRef", localId: local.id, type: F64, loc }; + return { kind: "libCall", fn: "date.fromTime", args: [time], type: DATE_T, loc }; + } if (local) { // A switch can dispatch past a lexical declaration. Keep this // previously refused codec form out until switch-scope TDZ boxes diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 750057bf9..f4d6564ba 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -28,6 +28,7 @@ import * as ts from "../ts7/adapter.js"; import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js"; import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; +import { isTimeValueDateDeclaration } from "./date-values.js"; import { bindingSource } from "../binding-source.js"; import { lowerConstructorReturn } from "./constructor-returns.js"; import type { Lowerer } from "./lowerer.js"; @@ -63,6 +64,7 @@ import { BOOL, BYTES_U8, CAUGHT, + DATE_T, DYN, F64, type IrExpr, @@ -5234,6 +5236,16 @@ export function lowerVarDecl( if (alias) return alias; } + // A Date local whose identity no use observes stores its time value. + if (isTimeValueDateDeclaration(lowerer, decl)) { + const loc = locOf(decl); + const date = lowerer.lowerExprExpecting(decl.initializer!, DATE_T); + const local = lowerer.declareLocal(decl.name, decl.name.text, F64, isLet); + lowerer.timeValueDateLocals.add(local); + const init: IrExpr = { kind: "libCall", fn: "date.getTime", args: [date], type: F64, loc }; + return { kind: "varDecl", localId: local.id, init, loc }; + } + // A NULLISH generic binding (`const i: I = null as any` — the // declared type has no mapping, the value is provably null/undefined // forever): no storage exists; reads know the value (member reads and diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3ba81dd6b..637dfcaa8 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2331,6 +2331,10 @@ export class Lowerer { readonly runtimeOptionalFields = new Set(); /** Bindings initialized by a Date's `toJSON()` (analyzeDateJsonResults). */ readonly dateJsonBindings = new Set(); + /** isTimeValueDateDeclaration's answers, and the locals that hold a + * Date's time value instead of a Date object (date-values.ts). */ + readonly timeValueDateDecisions = new Map(); + readonly timeValueDateLocals = new Set(); /** Record bindings whose flow class reaches storage outside the class: * they keep the declared layout, so an initializer from unchecked reads * converts once instead of the binding adopting the widened variant. */ diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f5da56818..b89ca61c4 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4738,6 +4738,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "date.getMilliseconds": { argTypes: [DATE_T], result: F64 }, "date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 }, "date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 }, + /** A Date object for a time value a local holds (date-values.ts): a + * lib-call argument passes the time value itself, and any other use + * creates the Date. */ + "date.fromTime": { argTypes: [F64], result: DATE_T }, /** Date.prototype.toString / toDateString / toTimeString and String(date): * V8's local-time formats with the host zone's long name, "Invalid Date" * for an invalid date. Owned (+1) string; never throws. */ diff --git a/packages/compiler/test/date-time-values.test.ts b/packages/compiler/test/date-time-values.test.ts new file mode 100644 index 000000000..4e90ef3e6 --- /dev/null +++ b/packages/compiler/test/date-time-values.test.ts @@ -0,0 +1,100 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; +import type { IrModule } from "../src/ir/ir.js"; + +async function lowerSource(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-date-time-values-")); + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + const outPath = join(dir, "main.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 deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** The IR type of each local named `name` in function `fn`. */ +function localKinds(mod: IrModule, fn: string, name: string): string[] { + const f = mod.functions.find((candidate) => candidate.name === fn); + if (!f) throw new Error(`no function ${fn}`); + return f.locals.filter((local) => local.name === name).map((local) => local.type.kind); +} + +test("a Date local read only for its time value stores the time value", async () => { + const mod = await lowerSource(` +function format(ms: number): string { + const d = new Date(ms); + const later = new Date(ms + 1); + return d.toISOString() + String(d) + \`\${later}\` + (d < later) + d.getUTCHours() + new Date(d).getTime(); +} +console.log(format(0)); +`); + expect(localKinds(mod, "format", "d")).toEqual(["f64"]); + expect(localKinds(mod, "format", "later")).toEqual(["f64"]); + // Reading the time value creates no Date object. + const llvm = emitLlvmModule(mod, { pointerBits: 64 }); + const body = /^define internal [^\n]*@sc_f_format\([^]*?^}/m.exec(llvm)?.[0] ?? ""; + expect(body).not.toContain("@scr_date_new("); +}); + +test("a Date local whose identity a use can observe keeps the Date object", async () => { + const mod = await lowerSource(` +const keep: Date[] = []; +function uses(ms: number): boolean { + const compared = new Date(ms); + const stored = new Date(ms); + const passed = new Date(ms); + const captured = new Date(ms); + const returned = new Date(ms); + let reassigned = new Date(ms); + reassigned = new Date(ms + 1); + keep.push(stored); + const read = (): number => captured.getTime(); + console.log(String(passed), read(), returned.getTime(), reassigned.getTime()); + return compared === compared && pass(passed) === passed && back(returned) === returned; +} +function pass(d: Date): Date { + return d; +} +function back(d: Date): Date { + return d; +} +const top = new Date(0); +console.log(uses(0), top.getTime()); +`); + for (const name of ["compared", "stored", "passed", "captured", "returned", "reassigned"]) { + expect(localKinds(mod, "uses", name), name).toEqual(["date"]); + } +}); + +test("a Date in a block or loop at module level stores the time value unless a use observes it", async () => { + const mod = await lowerSource(` +const seen: Date[] = []; +let total = 0; +for (let i = 0; i < 3; i++) { + const read = new Date(i * 1000); + const kept = new Date(i * 1000); + seen.push(kept); + total += read.getUTCSeconds() + read.toISOString().length; +} +{ + const block = new Date(5); + const captured = new Date(6); + const later = (): number => captured.getTime(); + total += block.getTime() + later(); +} +const top = new Date(7); +console.log(total, seen.length, top.getTime()); +`); + expect(localKinds(mod, "%init.0", "read")).toEqual(["f64"]); + expect(localKinds(mod, "%init.0", "block")).toEqual(["f64"]); + expect(localKinds(mod, "%init.0", "kept")).toEqual(["date"]); + expect(localKinds(mod, "%init.0", "captured")).toEqual(["date"]); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json new file mode 100644 index 000000000..e6155bcc4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5966-date-time-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5966-date-time-values.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5966-date-time-values.ts b/tests/corpus/5966-date-time-values.ts new file mode 100644 index 000000000..342b4662c --- /dev/null +++ b/tests/corpus/5966-date-time-values.ts @@ -0,0 +1,80 @@ +// Date locals that are only read for their time value (method receivers, +// String() and template operands, relational and string `+` operands, +// `new Date(d)`) print and compare as Dates; locals whose identity a use +// can observe keep it. +export {}; +process.env.TZ = "America/New_York"; + +function describe(ms: number): string { + const d = new Date(ms); + const later = new Date(ms + 3_600_000); + const copy = new Date(d); + return [ + d.toISOString(), + d.toUTCString(), + String(d), + `${later}`, + "at " + d, + d < later, + later >= d, + d.getHours(), + d.getUTCDay(), + d.getTimezoneOffset(), + copy.getTime() === d.getTime(), + d.toJSON(), + ].join(" | "); +} +console.log(describe(Date.UTC(2026, 2, 8, 6, 30))); +console.log(describe(Date.UTC(2026, 10, 1, 5, 30))); +function invalid(): string { + const d = new Date(NaN); + return [String(d), d.getTime(), String(d.toJSON()), d < new Date(0), `${d}`].join(" | "); +} +console.log(invalid()); + +function sum(days: number): number { + let total = 0; + for (let i = 0; i < days; i++) { + const day = new Date(Date.UTC(2026, 0, 1) + i * 86_400_000); + total += day.getUTCDate() + day.getUTCMonth() + day.toISOString().length; + } + return total; +} +console.log(sum(400)); + +// Identity-observing uses keep the Date object. +const kept: Date[] = []; +function identities(ms: number): string { + const a = new Date(ms); + const b = a; + const stored = new Date(ms); + kept.push(stored); + const captured = new Date(ms); + const read = (): number => captured.getTime(); + const passed = new Date(ms); + return [a === b, kept.includes(stored), kept[0] === stored, read(), same(passed, passed)].join(","); +} +function same(x: Date, y: Date): boolean { + return x === y; +} +console.log(identities(5)); +const held: unknown = kept[0]; +console.log(held instanceof Date, (held as Date) === kept[0]); + +// Dates in blocks and loops at module level, read for their time value or +// kept, stored and captured. +const moduleKept: Date[] = []; +let moduleTotal = ""; +for (let i = 0; i < 3; i++) { + const read = new Date(Date.UTC(2026, 2, 8 + i, 6, 30)); + const kept = new Date(read.getTime()); + moduleKept.push(kept); + moduleTotal += `${read.toISOString()} ${read.getHours()} ${String(read)}\n`; +} +{ + const block = new Date(Date.UTC(2026, 10, 1, 5, 59)); + const captured = new Date(block); + const later = (): string => captured.toISOString(); + moduleTotal += `${block.getTimezoneOffset()} ${later()} ${captured === moduleKept[0]}`; +} +console.log(moduleTotal, moduleKept.map((d) => d.getUTCDate()).join(",")); diff --git a/tests/harness/corpus-costs/5966-date-time-values.ts.json b/tests/harness/corpus-costs/5966-date-time-values.ts.json new file mode 100644 index 000000000..a3a32b65e --- /dev/null +++ b/tests/harness/corpus-costs/5966-date-time-values.ts.json @@ -0,0 +1 @@ +{ "plain": 3096, "san": 3319 } From 961aad6ac99b9b3cf8d101c24415cb5b60d9a616 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 20:37:10 -0500 Subject: [PATCH 026/409] Keep the identity of records and class instances converted to dictionaries A record or class instance converted to a string-keyed dictionary type (`Record`, `{ [key: string]: V }`) was copied field by field into a native dictionary record, directly, through a dictionary parameter, or through a `{}`, `object` or `unknown` binding or parameter. Writes through the dictionary missed the original object, and the dictionary was never `===` to it, where under Node it is the same object. ClassProtocols now marks a dictionary value type when a dictionary of that value type receives an existing object (a binding, member, element or `this` whose type is a program class instance, a record with declared properties, or a `{}`, `object` or `unknown` binding or parameter that holds one), and the program writes through, compares or passes to a whole-object observer a dictionary of that value type. usesCheckedIdentity answers true for those dictionaries, so they lower to checked-dynamic storage, which holds the original object. Dictionaries of `unknown` or `any` values are checked-dynamic already and are not marked. The dictionary case of SC6001 is removed, since these conversions now keep the object. --- .../record-identity-and-property-order.mdx | 3 +- .../compiler/src/diagnostics/diagnostic.ts | 10 +- .../src/frontend/lowering/class-protocols.ts | 205 +++++++++++++++++- .../src/frontend/lowering/divergences.ts | 110 +--------- .../compiler/test/dictionary-views.test.ts | 77 +++++++ ...7012-dictionary-views-keep-objects.ts.json | 6 + .../7013-dictionary-view-parameter.ts.json | 6 + .../7012-dictionary-views-keep-objects.ts | 61 ++++++ .../corpus/7013-dictionary-view-parameter.ts | 11 + .../dictionary-view-copies.ts | 29 --- tests/coverage-fixtures/no-divergences.ts | 7 + .../coverage-dictionary-view-copies.txt | 21 -- ...7012-dictionary-views-keep-objects.ts.json | 1 + .../7013-dictionary-view-parameter.ts.json | 1 + tests/harness/coverage.test.ts | 14 -- 15 files changed, 382 insertions(+), 180 deletions(-) create mode 100644 packages/compiler/test/dictionary-views.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7012-dictionary-views-keep-objects.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7013-dictionary-view-parameter.ts.json create mode 100644 tests/corpus/7012-dictionary-views-keep-objects.ts create mode 100644 tests/corpus/7013-dictionary-view-parameter.ts delete mode 100644 tests/coverage-fixtures/dictionary-view-copies.ts delete mode 100644 tests/harness/__snapshots__/coverage-dictionary-view-copies.txt create mode 100644 tests/harness/corpus-costs/7012-dictionary-views-keep-objects.ts.json create mode 100644 tests/harness/corpus-costs/7013-dictionary-view-parameter.ts.json diff --git a/docs/content/limitations/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index 224757d52..2299122a4 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -1,10 +1,11 @@ A record converted to a record type with a different set of properties, such as a narrower shape or one that adds optional properties, is copied. Mutations through the converted reference do not affect the original, and the copy is not `===` to it. Empty record layouts are also snapshots because they have no property storage. -A class instance or record held as `{}`, `object` or `unknown` and then converted to a dictionary type such as `Record` is copied as well; a dictionary of `unknown` values keeps the original object. `scriptc coverage` and `scriptc build` report `SC6001` where a copied value is then written or compared, whether the record was passed as an argument, stored in a typed binding, asserted with `as`, assigned to a typed binding later, or stored as an element of a typed array literal. Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage. +A record or class instance converted to a string-keyed dictionary type such as `Record`, directly or through a `{}`, `object` or `unknown` binding or parameter, keeps its identity when the program writes through, compares or inspects values of that dictionary type. +Every dictionary type with the same value type then uses checked dynamic storage, which is slower than a native dictionary. Class instances retain identity at their exact type or a base class, and at any interface or object type that a class instance is passed, stored, returned, or asserted as when the program could tell a copy from the instance: values of such a type are checked views of the original object, so writes, identity comparisons, Set and Map keys, `util.inspect`, and `JSON.stringify` through them reach the instance, as in Node.js. These types use checked dynamic storage, which is slower than native record fields, for all of their values, including plain objects. A type whose values are only read field by field, from fields the class does not write after construction and does not implement as accessors, stays a native record and holds a copy. diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts index 47db2193b..17cb2efc0 100644 --- a/packages/compiler/src/diagnostics/diagnostic.ts +++ b/packages/compiler/src/diagnostics/diagnostic.ts @@ -129,7 +129,6 @@ export function widthCopyDivergenceDiag( useText: string, site: "call" | "declaration" | "cast" | "element", loc: SrcLoc, - dictionary = false, ): ScrDiagnostic { const how = site === "call" @@ -143,16 +142,11 @@ export function widthCopyDivergenceDiag( use === "write" ? `the write to '${useText}' updates only the copy, while under Node it updates the original object` : `'${useText}' compares the copy, which is never the original object, while under Node it is the same object`; - const cause = dictionary - ? "converting an object held as '{}', 'object' or 'unknown' to a dictionary type" - : "converting to a record type with other properties"; return { code: "SC6001", - message: `${how} copies the object (${cause} compiles as a field copy): ${effect}`, + message: `${how} copies the object (converting to a record type with other properties compiles as a field copy): ${effect}`, loc, - hint: dictionary - ? `keep the value at its own type ('${sourceType}'), or declare the dictionary's values as 'unknown', which keeps the original object` - : `declare the parameter or binding with the value's full type ('${sourceType}'), or make the shape a class — class instances keep their identity when passed as a base class`, + hint: `declare the parameter or binding with the value's full type ('${sourceType}'), or make the shape a class — class instances keep their identity when passed as a base class`, }; } diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 76091b13e..4edf1ab7e 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -10,6 +10,9 @@ export class ClassProtocols { private readonly accepted = new Map(); /** What markIdentityFlows learned about the program, while it runs. */ private facts: FlowFacts | null = null; + /** The value types of the string-keyed dictionaries that keep checked + * identity (see markDictionaryViews). */ + private readonly dictionaryViews = new Set(); constructor( private readonly checker: ts.TypeChecker, @@ -323,6 +326,7 @@ export class ClassProtocols { else if (names !== "all") names.add(name); } } + this.markDictionaryViews(sites, sourceNodes, observedNodes, writeNodes); for (let i = 0; i < looseArguments.length; i++) { const type = typeOf(looseNodes[i]!); if (type === undefined) continue; @@ -697,6 +701,163 @@ export class ClassProtocols { return source !== undefined && this.isProgramClassInstance(source); } + /** String-keyed dictionaries (`Record`, `{ [key: string]: V }`) + * that receive an existing record or class instance. Natively a + * dictionary is a key/value record, and converting another object into + * one copies its fields, so writes through the dictionary and identity + * checks would miss the original. When the program writes through, or + * observes, a value of such a dictionary type, every dictionary with that + * value type keeps checked identity instead (records with the same value + * type share one native layout, so values pass between them). A source + * counts when it is an existing object (a binding, member, element or + * `this`, not a fresh literal or call result) whose type is a program + * class instance or a record with declared properties, or a constant + * `{}`, `object` or `unknown` binding initialized with one. */ + private markDictionaryViews( + sites: readonly { + source: ts.Expression; + target: () => ts.Type | undefined; + contextual?: ts.Expression; + }[], + sourceNodes: readonly ts.Expression[], + observedNodes: readonly ts.Expression[], + writeNodes: readonly ts.Expression[], + ): void { + // The calls the sites' arguments belong to, by callee name, built on + // first use. + let calls: Map | null = null; + const callsNamed = (name: string): readonly (ts.CallExpression | ts.NewExpression)[] => { + if (calls === null) { + calls = new Map(); + for (const site of sites) { + const call = site.contextual?.parent; + if (call === undefined || (!ts.isCallExpression(call) && !ts.isNewExpression(call))) + continue; + const callee = call.expression; + const named = ts.isIdentifier(callee) + ? callee.text + : ts.isPropertyAccessExpression(callee) + ? callee.name.text + : undefined; + if (named === undefined) continue; + const list = calls.get(named) ?? []; + if (!list.includes(call)) list.push(call); + calls.set(named, list); + } + } + return calls.get(name) ?? []; + }; + const candidates: { index: number; sourceType: ts.Type }[] = []; + for (let i = 0; i < sites.length; i++) { + const source = sourceNodes[i]!; + if (!isExistingObject(source)) continue; + const sourceType = attempt(() => this.checker.getTypeAtLocation(source)); + if (sourceType !== undefined && this.dictionarySource(source, sourceType, callsNamed)) + candidates.push({ index: i, sourceType }); + } + if (candidates.length === 0) return; + // One batch for the contextual targets of the candidate sites. + attempt(() => + this.checker.prefetchContextualTypes( + candidates.flatMap(({ index }) => { + const contextual = sites[index]!.contextual; + return contextual === undefined ? [] : [contextual]; + }), + ), + ); + const received = new Set(); + for (const { index, sourceType } of candidates) { + const target = attempt(sites[index]!.target); + const parts = + target === undefined ? [] : target.isUnionType() ? ts.constituentTypes(target) : [target]; + for (const part of parts) { + const value = this.dictionaryValueType(part); + // A dictionary of `unknown` or `any` values is checked-dynamic already. + if ( + value !== undefined && + (value.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.Any)) === 0 && + this.dictionaryValueType(sourceType) !== value + ) + received.add(value); + } + } + if (received.size === 0) return; + // The dictionary values the program writes through or observes. + const seen = new Set(); + for (const node of [...writeNodes, ...observedNodes]) { + const type = attempt(() => this.checker.getTypeAtLocation(node)); + const parts = + type === undefined ? [] : type.isUnionType() ? ts.constituentTypes(type) : [type]; + for (const part of parts) { + const value = this.dictionaryValueType(part); + if (value !== undefined) seen.add(value); + } + } + for (const value of received) if (seen.has(value)) this.dictionaryViews.add(value); + } + + /** Whether `source` holds an existing object that a dictionary + * conversion would copy. */ + private dictionarySource( + source: ts.Expression, + type: ts.Type, + callsNamed: (name: string) => readonly (ts.CallExpression | ts.NewExpression)[], + ): boolean { + if (this.isProgramClassInstance(type) || this.isRecordWithFields(type)) return true; + if (!isDynamicView(this.checker, type) || !ts.isIdentifier(source)) return false; + const decl = attempt(() => this.checker.getSymbolAtLocation(source))?.valueDeclaration; + if (decl === undefined) return false; + const holdsObject = (expr: ts.Expression): boolean => { + const held = attempt(() => this.checker.getTypeAtLocation(this.flowSourceNode(expr))); + return ( + held !== undefined && (this.isProgramClassInstance(held) || this.isRecordWithFields(held)) + ); + }; + if (ts.isParameter(decl)) { + // A parameter holds what its callers pass: the calls of the function + // by name whose signature resolves to it. + const fn = decl.parent; + if (fn === undefined || !ts.isFunctionLike(fn)) return false; + const index = fn.parameters.nodes.indexOf(decl); + const name = functionName(fn); + if (name === undefined || index < 0) return false; + return callsNamed(name).some((call) => { + const argument = call.arguments?.nodes[index]; + return ( + argument !== undefined && + !ts.isSpreadElement(argument) && + attempt(() => this.checker.getResolvedSignature(call)?.declaration) === fn && + holdsObject(argument) + ); + }); + } + return ( + ts.isVariableDeclaration(decl) && + decl.initializer !== undefined && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 && + holdsObject(decl.initializer) + ); + } + + /** A program object type with declared properties that is not a class + * instance, array, tuple, function or library type. */ + private isRecordWithFields(type: ts.Type): boolean { + return this.isStructuralTarget(type) && this.checker.getPropertiesOfType(type).length > 0; + } + + /** The value type of a string-keyed dictionary with no declared + * properties, or undefined for any other type. */ + private dictionaryValueType(type: ts.Type): ts.Type | undefined { + if ((type.flags & ts.TypeFlags.Object) === 0) return undefined; + if (this.checker.isArrayType(type) || this.checker.isTupleType(type)) return undefined; + if (this.checker.getCallSignatures(type).length > 0) return undefined; + if (this.checker.getPropertiesOfType(type).length > 0) return undefined; + return this.checker + .getIndexInfosOfType(type) + .find((index) => (index.keyType.flags & ts.TypeFlags.String) !== 0)?.type; + } + private isProgramClassInstance(type: ts.Type): boolean { const symbol = type.getSymbol(); return ( @@ -822,7 +983,11 @@ export class ClassProtocols { if ((type.flags & ts.TypeFlags.Object) === 0) return false; const previous = this.answers.get(type); if (previous !== undefined) return previous; - const answer = this.protocol(type); + const dictionaryValue = this.dictionaryValueType(type); + const answer = + dictionaryValue !== undefined + ? (this.index(), this.dictionaryViews.has(dictionaryValue)) + : this.protocol(type); this.answers.set(type, answer); return answer; } @@ -922,6 +1087,44 @@ const IDENTITY_METHODS = new Set([ "includes", ]); +/** An expression that denotes an object that already exists, not a fresh + * literal or call result. */ +function isExistingObject(node: ts.Expression): boolean { + return ( + ts.isIdentifier(node) || + ts.isPropertyAccessExpression(node) || + ts.isElementAccessExpression(node) || + node.kind === ts.SyntaxKind.ThisKeyword + ); +} + +/** `{}`, `object` and `unknown`: types whose values are checked-dynamic + * and keep the original object. */ +function isDynamicView(checker: ts.TypeChecker, type: ts.Type): boolean { + if (type.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive)) return true; + return ( + (type.flags & ts.TypeFlags.Object) !== 0 && + checker.getPropertiesOfType(type).length === 0 && + checker.getIndexInfosOfType(type).length === 0 && + checker.getCallSignatures(type).length === 0 && + checker.getConstructSignatures(type).length === 0 + ); +} + +/** The name calls use for a function, method or function-valued binding. */ +function functionName(fn: ts.Node): string | undefined { + if ((ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && fn.name !== undefined) + return ts.isIdentifier(fn.name) ? fn.name.text : undefined; + if ( + (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && + fn.parent !== undefined && + ts.isVariableDeclaration(fn.parent) && + ts.isIdentifier(fn.parent.name) + ) + return fn.parent.name.text; + return undefined; +} + function isNullishLiteral(node: ts.Expression): boolean { while (ts.isParenthesizedExpression(node)) node = node.expression; return ( diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index f766aba14..1c362cb09 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -180,15 +180,7 @@ export function scanDivergences( const copy = safely(() => widthCopyBetween(lowerer, arg, param.type!)); if (copy === undefined || copy === null) return; out.push( - widthCopyDivergenceDiag( - copy.source, - copy.target, - use.kind, - use.text, - "call", - locOf(arg), - copy.dictionary, - ), + widthCopyDivergenceDiag(copy.source, copy.target, use.kind, use.text, "call", locOf(arg)), ); }); } @@ -208,7 +200,6 @@ export function scanDivergences( use.text, "declaration", locOf(decl.initializer!), - copy.dictionary, ), ); } @@ -224,15 +215,7 @@ export function scanDivergences( const copy = safely(() => widthCopyBetween(lowerer, source, type)); if (copy === undefined || copy === null) continue; out.push( - widthCopyDivergenceDiag( - copy.source, - copy.target, - use.kind, - use.text, - "cast", - locOf(source), - copy.dictionary, - ), + widthCopyDivergenceDiag(copy.source, copy.target, use.kind, use.text, "cast", locOf(source)), ); } for (const assignment of assignments) { @@ -257,7 +240,6 @@ export function scanDivergences( use.text, "declaration", locOf(assignment.right), - copy.dictionary, ), ); } @@ -272,15 +254,7 @@ export function scanDivergences( const copy = safely(() => widthCopyBetween(lowerer, item, element)); if (copy === undefined || copy === null) continue; out.push( - widthCopyDivergenceDiag( - copy.source, - copy.target, - "write", - use, - "element", - locOf(item), - copy.dictionary, - ), + widthCopyDivergenceDiag(copy.source, copy.target, "write", use, "element", locOf(item)), ); break; } @@ -679,23 +653,14 @@ function enclosingScope(node: ts.Node): ts.Node | undefined { * instance flowing into a plain record type that does not keep class * identity (ClassProtocols marks the targets a class instance visibly * flows into; a value reaching another record type is a field copy). */ -/** The types a width copy converts between, and whether the target is a - * dictionary. */ -interface WidthCopy { - source: string; - target: string; - dictionary?: boolean; -} - function widthCopyBetween( lowerer: Lowerer, source: ts.Expression, targetNode: ts.TypeNode, -): WidthCopy | null { +): { source: string; target: string } | null { const checker = lowerer.checker; const s = checker.getTypeAtLocation(source); const t = checker.getTypeFromTypeNode(targetNode); - if (isDictionaryType(checker, t)) return dictionaryViewCopy(lowerer, source, s, t); if (!isPlainRecordType(checker, t)) return null; const classSource = isProgramClassType(checker, s); if (!classSource && !isPlainRecordType(checker, s)) return null; @@ -715,73 +680,6 @@ function widthCopyBetween( return { source: checker.typeToString(s), target: checker.typeToString(t) }; } -/** A copy made by converting a checked-dynamic view (`{}`, `object`, - * `unknown`) to a dictionary type that compiles to a native record (a - * dictionary of `unknown` values stays checked-dynamic and keeps the - * object). A dictionary of that type comes back out of the view as itself, - * so a copy is reported only when the view is a constant binding - * initialized with a class instance or a record: that object is converted - * field by field into a new dictionary. */ -function dictionaryViewCopy( - lowerer: Lowerer, - source: ts.Expression, - s: ts.Type, - t: ts.Type, -): WidthCopy | null { - const checker = lowerer.checker; - if (!isDynamicViewType(checker, s)) return null; - if (lowerer.mapTypeOf(t)?.kind !== "record") return null; - while (ts.isParenthesizedExpression(source) || ts.isNonNullExpression(source)) - source = source.expression; - if (!ts.isIdentifier(source)) return null; - const decl = checker.getSymbolAtLocation(source)?.valueDeclaration; - if ( - decl === undefined || - !ts.isVariableDeclaration(decl) || - decl.initializer === undefined || - !ts.isVariableDeclarationList(decl.parent) || - !(decl.parent.flags & ts.NodeFlags.Const) - ) - return null; - let origin = decl.initializer; - while ( - ts.isParenthesizedExpression(origin) || - ts.isNonNullExpression(origin) || - ts.isAsExpression(origin) || - ts.isTypeAssertion(origin) - ) - origin = origin.expression; - const o = checker.getTypeAtLocation(origin); - if (!isProgramClassType(checker, o) && !isPlainRecordType(checker, o)) return null; - return { source: checker.typeToString(o), target: checker.typeToString(t), dictionary: true }; -} - -/** `{}`, `object` and `unknown`: types whose values lower to checked - * dynamic storage that keeps the original object. */ -function isDynamicViewType(checker: ts.TypeChecker, type: ts.Type): boolean { - if (type.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive)) return true; - return ( - (type.flags & ts.TypeFlags.Object) !== 0 && - checker.getPropertiesOfType(type).length === 0 && - checker.getIndexInfosOfType(type).length === 0 && - checker.getCallSignatures(type).length === 0 && - checker.getConstructSignatures(type).length === 0 - ); -} - -/** A string-keyed dictionary with no declared properties - * (`Record`, `{ [key: string]: number }`). */ -function isDictionaryType(checker: ts.TypeChecker, type: ts.Type): boolean { - if ((type.flags & ts.TypeFlags.Object) === 0) return false; - if (checker.isArrayType(type) || checker.isTupleType(type)) return false; - if (checker.getCallSignatures(type).length > 0) return false; - if (checker.getConstructSignatures(type).length > 0) return false; - if (checker.getPropertiesOfType(type).length > 0) return false; - return checker - .getIndexInfosOfType(type) - .some((index) => (index.keyType.flags & ts.TypeFlags.String) !== 0); -} - function isProgramClassType(checker: ts.TypeChecker, type: ts.Type): boolean { if ((type.flags & ts.TypeFlags.Object) === 0) return false; const symbol = type.getSymbol(); diff --git a/packages/compiler/test/dictionary-views.test.ts b/packages/compiler/test/dictionary-views.test.ts new file mode 100644 index 000000000..b841447cd --- /dev/null +++ b/packages/compiler/test/dictionary-views.test.ts @@ -0,0 +1,77 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; + +/** The IR type kind of each named module-level binding. */ +async function globalKinds(source: string): Promise> { + const dir = await mkdtemp(join(tmpdir(), "scriptc-dictionary-views-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + return Object.fromEntries((mod.globals ?? []).map((g) => [g.name, g.type.kind])); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const program = (body: string): string => ` +const key = String(process.argv.length); +const counts: Record = { a: 1 }; +counts[key] = 2; +${body} +console.log(JSON.stringify(counts)); +`; + +test("a record written through a dictionary keeps that dictionary type checked", async () => { + const kinds = await globalKinds( + program(`const point: { a: string } = { a: "x" }; +const table: Record = point; +table[key] = "y"; +console.log(JSON.stringify(point));`), + ); + expect(kinds["table"]).toBe("dyn"); + // Dictionaries of other value types keep their native layout. + expect(kinds["counts"]).toBe("record"); +}); + +test("a record reaching a dictionary through a {} parameter keeps it checked", async () => { + const kinds = await globalKinds( + program(`function stamp(value: {}): void { + const inner: Record = value; + inner[key] = "y"; +} +const point: { a: string } = { a: "x" }; +stamp(point); +const other: Record = { b: "z" }; +console.log(JSON.stringify(point), other["b"]);`), + ); + expect(kinds["other"]).toBe("dyn"); +}); + +test("dictionaries keep their native layout without a record flowing in", async () => { + const kinds = await globalKinds( + program(`const table: Record = { a: "x" }; +table[key] = "y"; +const copy: Record = table; +copy[key] = "z"; +console.log(JSON.stringify(table));`), + ); + expect(kinds["table"]).toBe("record"); + expect(kinds["copy"]).toBe("record"); +}); + +test("a record converted to a dictionary that is only read keeps the native layout", async () => { + const kinds = await globalKinds( + program(`const point: { a: string } = { a: "x" }; +const table: Record = point; +console.log(table["a"]);`), + ); + expect(kinds["table"]).toBe("record"); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7012-dictionary-views-keep-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7012-dictionary-views-keep-objects.ts.json new file mode 100644 index 000000000..6d7944cf7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7012-dictionary-views-keep-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7012-dictionary-views-keep-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7013-dictionary-view-parameter.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7013-dictionary-view-parameter.ts.json new file mode 100644 index 000000000..41f80c41e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7013-dictionary-view-parameter.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7013-dictionary-view-parameter.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7012-dictionary-views-keep-objects.ts b/tests/corpus/7012-dictionary-views-keep-objects.ts new file mode 100644 index 000000000..8ede80926 --- /dev/null +++ b/tests/corpus/7012-dictionary-views-keep-objects.ts @@ -0,0 +1,61 @@ +// A record or class instance converted to a string-keyed dictionary is +// the same object: writes through the dictionary reach it, and the +// dictionary is === to it. +const key = process.argv.length > 99 ? "never" : "added"; +type Point = { a: string }; + +// A type-literal record stored straight in a dictionary binding. +const direct: Point = { a: "x" }; +const directTable: Record = direct; +directTable[key] = "y"; +console.log(JSON.stringify(direct), (directTable as unknown) === direct); + +// The same record through a `{}` view. +const viewed: Point = { a: "x" }; +const view: {} = viewed; +const viewTable: Record = view; +viewTable[key] = "y"; +console.log(JSON.stringify(viewed)); + +// A declared index type, and a parameter. +interface Labels { + [name: string]: string; +} +function label(table: Labels, name: string): void { + table[name] = "set"; +} +const passed: Point = { a: "x" }; +label(passed, key); +console.log(JSON.stringify(passed)); + +// A `{}` parameter converted inside the function, called with a record +// and with a dictionary. +function viaView(value: {}): void { + const table: Record = value; + table[key] = "y"; +} +const record: Point = { a: "x" }; +viaView(record); +const dictionary: Record = { a: "x" }; +viaView(dictionary); +console.log(JSON.stringify(record), JSON.stringify(dictionary)); + +// A class instance through a `{}` view: a computed write replaces a method. +class Greeter { + name = "g"; + greet(): string { + return "hello " + this.name; + } +} +const greeter = new Greeter(); +const greeterView: {} = greeter; +const slots: Record = greeterView; +slots[process.argv.length > 99 ? "never" : "greet"] = "replaced"; +slots["name"] = "renamed"; +console.log(typeof greeter.greet, greeter.name, JSON.stringify(greeter)); + +// Deleting through the dictionary removes the key from the object. +const pruned: { a: string; b?: string } = { a: "x", b: "y" }; +const prunedTable: Record = pruned; +delete prunedTable[process.argv.length > 99 ? "never" : "b"]; +console.log(JSON.stringify(pruned), "b" in pruned); diff --git a/tests/corpus/7013-dictionary-view-parameter.ts b/tests/corpus/7013-dictionary-view-parameter.ts new file mode 100644 index 000000000..859f22c23 --- /dev/null +++ b/tests/corpus/7013-dictionary-view-parameter.ts @@ -0,0 +1,11 @@ +// A record passed to a `{}` parameter and converted to a dictionary inside +// the function is the same object: the write through the dictionary +// reaches the caller's record. +const key = process.argv.length > 99 ? "never" : "added"; +function stamp(value: {}): void { + const table: Record = value; + table[key] = "y"; +} +const record: { a: string } = { a: "x" }; +stamp(record); +console.log(JSON.stringify(record), key in record); diff --git a/tests/coverage-fixtures/dictionary-view-copies.ts b/tests/coverage-fixtures/dictionary-view-copies.ts deleted file mode 100644 index bfcfa61f4..000000000 --- a/tests/coverage-fixtures/dictionary-view-copies.ts +++ /dev/null @@ -1,29 +0,0 @@ -// A class instance or record held as `{}`, `object` or `unknown` keeps the -// original object, and converting it to a dictionary type copies it field -// by field. Each site below then writes through or compares the copy. -class Counter { - label = "c"; - add(): number { - return this.label.length; - } -} -const key = process.argv.length > 5 ? "z" : "b"; - -const counter = new Counter(); -const view: {} = counter; -const table: Record = view; -table[key] = "y"; - -const point = { a: "x" }; -const loose: object = point; -const dict = loose as Record; -dict[key] = "y"; - -const held: unknown = { a: "x" }; -const fromUnknown = held as Record; -console.log(fromUnknown === held); - -const empty: {} = {}; -const filled: Record = empty; -filled[key] = "y"; -console.log(JSON.stringify(counter), JSON.stringify(point), JSON.stringify(empty)); diff --git a/tests/coverage-fixtures/no-divergences.ts b/tests/coverage-fixtures/no-divergences.ts index ca76bcc32..70409d319 100644 --- a/tests/coverage-fixtures/no-divergences.ts +++ b/tests/coverage-fixtures/no-divergences.ts @@ -45,3 +45,10 @@ const opaque: {} = counts; const again: Record = opaque; again[counts["a"] ?? "a"] = "y"; console.log(JSON.stringify(counts)); + +// A record stored in a dictionary binding and written through it stays +// the same object. +const named: { first: string } = { first: "a" }; +const byName: Record = named; +byName[named.first] = "b"; +console.log(JSON.stringify(named)); diff --git a/tests/harness/__snapshots__/coverage-dictionary-view-copies.txt b/tests/harness/__snapshots__/coverage-dictionary-view-copies.txt deleted file mode 100644 index 2359a4ffa..000000000 --- a/tests/harness/__snapshots__/coverage-dictionary-view-copies.txt +++ /dev/null @@ -1,21 +0,0 @@ -scriptc coverage tests/coverage-fixtures/dictionary-view-copies.ts - - statements analyzed 18 - compile statically 18 (100%) - in dynamic tier 3 (operate on values typed only at runtime; --explain-dynamic lists them) - - fully static — all analyzed statements compile to native code. - - differs from Node 4 sites (compiles; the native program can behave differently) - ×1 SC6001 storing a 'Counter' in a 'Record<…>' binding copies the object (converting an object held as '{}', 'object' or 'unknown' to a dictionary type compiles as a field copy): the write to 'table[key]' upda… - at tests/coverage-fixtures/dictionary-view-copies.ts:14:39 - hint: keep the value at its own type ('Counter'), or declare the dictionary's values as 'unknown', which keeps the original object - ×1 SC6001 storing a '{}' in a 'Record<…>' binding copies the object (converting an object held as '{}', 'object' or 'unknown' to a dictionary type compiles as a field copy): the write to 'filled[key]' updates … - at tests/coverage-fixtures/dictionary-view-copies.ts:27:40 - hint: keep the value at its own type ('{}'), or declare the dictionary's values as 'unknown', which keeps the original object - ×1 SC6001 asserting a '{ a: string; }' as 'Record<…>' copies the object (converting an object held as '{}', 'object' or 'unknown' to a dictionary type compiles as a field copy): the write to 'dict[key]' update… - at tests/coverage-fixtures/dictionary-view-copies.ts:19:14 - hint: keep the value at its own type ('{ a: string; }'), or declare the dictionary's values as 'unknown', which keeps the original object - ×1 SC6001 asserting a '{ a: string; }' as 'Record<…>' copies the object (converting an object held as '{}', 'object' or 'unknown' to a dictionary type compiles as a field copy): 'fromUnknown === held' compares… - at tests/coverage-fixtures/dictionary-view-copies.ts:23:21 - hint: keep the value at its own type ('{ a: string; }'), or declare the dictionary's values as 'unknown', which keeps the original object \ No newline at end of file diff --git a/tests/harness/corpus-costs/7012-dictionary-views-keep-objects.ts.json b/tests/harness/corpus-costs/7012-dictionary-views-keep-objects.ts.json new file mode 100644 index 000000000..dcf40a9fa --- /dev/null +++ b/tests/harness/corpus-costs/7012-dictionary-views-keep-objects.ts.json @@ -0,0 +1 @@ +{ "plain": 2368, "san": 3286 } diff --git a/tests/harness/corpus-costs/7013-dictionary-view-parameter.ts.json b/tests/harness/corpus-costs/7013-dictionary-view-parameter.ts.json new file mode 100644 index 000000000..911881768 --- /dev/null +++ b/tests/harness/corpus-costs/7013-dictionary-view-parameter.ts.json @@ -0,0 +1 @@ +{ "plain": 1466, "san": 2392 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index f3958c5fe..86d202b46 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -107,20 +107,6 @@ test("record width copies report at every storing site", async () => { ); }); -test("dictionary conversions of checked-dynamic views report copies", async () => { - const { coverage } = analyze(fixture("dictionary-view-copies.ts")); - expect(coverage.diagnostics).toEqual([]); - expect((coverage.divergences ?? []).map((d) => d.code)).toEqual([ - "SC6001", - "SC6001", - "SC6001", - "SC6001", - ]); - await expect(report(fixture("dictionary-view-copies.ts"))).toMatchFileSnapshot( - "__snapshots__/coverage-dictionary-view-copies.txt", - ); -}); - test("ordinary code reports no divergence", () => { const { coverage } = analyze(fixture("no-divergences.ts")); expect(coverage.diagnostics).toEqual([]); From 4780e7ef22243c28fd0ac36a25e19fdd9e87e08a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:01:18 -0500 Subject: [PATCH 027/409] Compare optional string index reads as code units `s[i]` with an index the frontend cannot prove in bounds reads the character or undefined: temps for the string and the index, a range and integer test, and a string-or-undefined union. A lexer that tests `src[pos] === " "` or `c < "a"` on `const c = src[pos]` compared that union with a union-wrapped literal, or boxed it for a dynamic comparison, on every character. Tests of such a read against a literal of one UTF-16 code unit now compare `i === Math.floor(i) ? s.charCodeAt(i) : NaN` instead. Undefined, like NaN, equals no literal and fails every ordered comparison, and charCodeAt reads NaN out of range. A read of plain operands is evaluated in place; others keep the read's temps. An immutable local initialized from such a read, and used only in those tests, in tests for undefined and in a switch over one-unit cases, holds the code unit. Tests for undefined become NaN tests. Copies of the local, such as a switch's discriminant temp, follow it. The code-unit reads are charCodeAt reads, so a loop version can cover them. --- .../compiler/src/ir/char-unit-locals.test.ts | 70 +++ packages/compiler/src/ir/char-unit-locals.ts | 404 ++++++++++++++++-- .../7304-string-optional-char-reads.ts.json | 6 + .../corpus/7304-string-optional-char-reads.ts | 121 ++++++ .../7304-string-optional-char-reads.ts.json | 1 + 5 files changed, 567 insertions(+), 35 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json create mode 100644 tests/corpus/7304-string-optional-char-reads.ts create mode 100644 tests/harness/corpus-costs/7304-string-optional-char-reads.ts.json diff --git a/packages/compiler/src/ir/char-unit-locals.test.ts b/packages/compiler/src/ir/char-unit-locals.test.ts index ad16ecebd..378a7f0df 100644 --- a/packages/compiler/src/ir/char-unit-locals.test.ts +++ b/packages/compiler/src/ir/char-unit-locals.test.ts @@ -96,3 +96,73 @@ console.log(escapes("a b", 1), rebound(" x", 0), wide("ab", 0), against("a", "b expect(fn(after, name)).toBe(fn(before, name)); } }); + +/** The union and dynamic comparisons left in the function. */ +function unionTests(f: IrFunction): string[] { + const out: string[] = []; + everyStmtList(f.body, { + stmt: () => true, + expr: (e) => { + if (e.kind === "unionEq" || e.kind === "unionIsTag") out.push(e.kind); + if (e.kind === "libCall" && e.fn === "js.compare") out.push(e.fn); + if (e.kind === "strIntrinsic") out.push(e.method); + return true; + }, + }); + return out; +} + +test("optional character reads compare their code unit", async () => { + const mod = holdCharLocalsAsUnits( + await lower(` +function scan(src: string, pos: number): number { + let n = 0; + if (src[pos] === " ") n += 1; + if (src[pos] !== "x") n += 2; + if (src[pos] < "a" || "z" <= src[pos]) n += 4; + if (src[pos + 1] === "=") n += 8; + const c = src[pos]; + if (c === undefined) n += 16; + if (c !== undefined && c > "m") n += 32; + switch (c) { + case "q": + n += 64; + break; + case "r": + n += 128; + } + return n; +} +console.log(scan("a b", 1), scan("q", 0), scan("ab", 5), scan("r=", 0.5)); +`), + ); + const scan = fn(mod, "scan"); + // Only charCodeAt reads remain: no union tests, dynamic compares, charAt + // or length reads. + expect(new Set(unionTests(scan))).toEqual(new Set(["charCodeAt"])); + expect(scan.locals.find((l) => l.name === "c")!.type).toEqual({ kind: "f64" }); +}); + +test("optional character reads with other uses stay reads", async () => { + const before = await lower(` +function kept(src: string, pos: number): string { + const c = src[pos]; + if (c === " ") return "space"; + return c ?? "end"; +} +function longer(src: string, pos: number): boolean { + return src[pos] === "ab"; +} +function twice(src: string, pos: number): boolean { + return src[pos * 2] === undefined; +} +console.log(kept("a b", 1), kept("ab", 0), kept("", 3), longer("ab", 0), twice("ab", 1)); +`); + const after = holdCharLocalsAsUnits(before); + for (const name of ["kept", "longer", "twice"]) { + expect(fn(before, name)).toBeDefined(); + expect(unionTests(fn(after, name))).toEqual(unionTests(fn(before, name))); + } + // The local's own test still reads it as a union. + expect(unionTests(fn(after, "kept"))).toContain("unionEq"); +}); diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts index 4008f4e1e..9917a148b 100644 --- a/packages/compiler/src/ir/char-unit-locals.ts +++ b/packages/compiler/src/ir/char-unit-locals.ts @@ -11,8 +11,11 @@ import { mapExprChildren, mapStmtChildren } from "./traverse.js"; type CharAt = IrExpr & { kind: "strIntrinsic" }; type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; +type CompareOp = "<" | "<=" | ">" | ">="; -/** Character locals held as their code unit. +const FLIPPED = { "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const; + +/** Character reads and locals held as their code unit. * * `const c = s[i]` (or `s.charAt(i)`) followed only by tests such as * `c === " "`, `c >= "a" && c <= "z"` or `switch (c) { case "x": ... }` @@ -28,7 +31,16 @@ type UnitUse = IrExpr & { kind: "strEq" | "strCmp" }; * * Only immutable unboxed string locals qualify, with one declaration whose * initializer is a one-argument charAt, and with no other mention of the - * local. */ + * local. + * + * `s[i]` with an index not proven in bounds reads the character or + * undefined (an optional read, see `optionalCharRead`). Its tests against + * one-unit literals compare `i === Math.floor(i) ? s.charCodeAt(i) : NaN` + * instead: undefined, like NaN, equals no literal and fails every ordered + * comparison, and charCodeAt answers NaN out of range. That holds both for + * the read in place and for an immutable local initialized from it and + * used only in such tests and in tests for undefined, which become NaN + * tests. */ export function holdCharLocalsAsUnits(mod: IrModule): IrModule { let changed = false; const functions = mod.functions.map((fn) => { @@ -39,11 +51,275 @@ export function holdCharLocalsAsUnits(mod: IrModule): IrModule { return changed ? { ...mod, functions } : mod; } +/** The read of `s[i]` for an index not proven in bounds: temps for the + * string and the index, then the string's charAt when the index is an + * integer in [0, length) and undefined otherwise. */ +interface OptionalCharRead { + /** The temps' declarations, and whether both operands are plain (local + * reads and literals), which the unit read may evaluate in place. */ + temps: [IrStmt & { kind: "varDecl" }, IrStmt & { kind: "varDecl" }]; + plain: boolean; + receiver: IrExpr; + index: IrExpr; + unionId: string; + stringTag: number; + undefinedTag: number; +} + +function optionalCharRead(e: IrExpr): OptionalCharRead | null { + if (e.kind !== "seqExpr" || e.stmts.length !== 2 || e.diverges) return null; + const [str, idx] = e.stmts; + if (str?.kind !== "varDecl" || idx?.kind !== "varDecl" || !str.init || !idx.init) return null; + const isStr = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === str.localId; + const isIdx = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === idx.localId; + const r = e.result; + if (r.kind !== "ternary") return null; + const c = r.cond; + if (c.kind !== "logical" || c.op !== "&&" || c.left.kind !== "logical" || c.left.op !== "&&") + return null; + const [low, high, whole] = [c.left.left, c.left.right, c.right]; + if ( + !( + low.kind === "bin" && + low.op === ">=" && + isIdx(low.left) && + low.right.kind === "numLit" && + low.right.value === 0 + ) || + !( + high.kind === "bin" && + high.op === "<" && + isIdx(high.left) && + high.right.kind === "strIntrinsic" && + high.right.method === "length" && + isStr(high.right.receiver) + ) || + !( + whole.kind === "bin" && + whole.op === "===" && + isIdx(whole.left) && + whole.right.kind === "libCall" && + whole.right.fn === "math.floor" && + whole.right.args.length === 1 && + isIdx(whole.right.args[0]!) + ) + ) + return null; + const [present, absent] = [r.then, r.else_]; + if (present.kind !== "unionWrap" || absent.kind !== "unionWrap") return null; + if (present.unionId !== absent.unionId) return null; + const read = present.value; + if ( + read.kind !== "strIntrinsic" || + read.method !== "charAt" || + !isStr(read.receiver) || + read.args.length !== 1 || + !isIdx(read.args[0]!) + ) + return null; + if (absent.value.kind !== "unitLit" || absent.value.unit !== "undefined") return null; + return { + temps: [str, idx], + plain: plainOperand(str.init) && plainOperand(idx.init), + receiver: str.init, + index: idx.init, + unionId: present.unionId, + stringTag: present.tag, + undefinedTag: absent.tag, + }; +} + +function plainOperand(e: IrExpr): boolean { + return e.kind === "varRef" || e.kind === "numLit" || e.kind === "strLit"; +} + +/** `i === Math.floor(i) ? s.charCodeAt(i) : NaN`: the code unit of an + * optional read, or NaN where it reads undefined. Plain operands are read + * in place; others keep the read's temps, so each is evaluated once. */ +function unitRead(read: OptionalCharRead, loc: IrExpr["loc"]): IrExpr { + if (!read.plain) { + const [str, idx] = read.temps; + const temp = (decl: IrStmt & { kind: "varDecl" }, type: IrExpr["type"]): IrExpr => ({ + kind: "varRef", + localId: decl.localId, + type, + loc, + }); + return { + kind: "seqExpr", + stmts: [{ ...str }, { ...idx }], + result: unitOf(temp(str, read.receiver.type), () => temp(idx, F64), loc), + type: F64, + loc, + }; + } + return unitOf({ ...read.receiver }, () => ({ ...read.index }), loc); +} + +function unitOf(receiver: IrExpr, index: () => IrExpr, loc: IrExpr["loc"]): IrExpr { + return { + kind: "ternary", + cond: { + kind: "bin", + op: "===", + left: index(), + right: { kind: "libCall", fn: "math.floor", args: [index()], type: F64, loc }, + type: BOOL, + loc, + }, + then: { + kind: "strIntrinsic", + method: "charCodeAt", + receiver, + args: [index()], + type: F64, + loc, + }, + else_: { kind: "numLit", value: NaN, type: F64, loc }, + type: F64, + loc, + }; +} + +/** A test of an operand that reads one character or undefined, against a + * literal of one code unit or for undefined, and the same test on the + * operand's code unit. `tagsOf` gives the operand's union tags. */ +interface UnitTest { + operand: IrExpr; + /** The test reads the unit twice. */ + twice?: boolean; + test: (unit: () => IrExpr) => IrExpr; +} + +function optionalUnitTest( + e: IrExpr, + tagsOf: (operand: IrExpr) => OptionalCharRead | null, +): UnitTest | null { + const loc = e.loc; + const compare = (op: "===" | "!==" | CompareOp, u: IrExpr, right: IrExpr): IrExpr => ({ + kind: "bin", + op, + left: u, + right, + type: BOOL, + loc, + }); + const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + if (e.kind === "unionEq" && !e.sameValue) { + for (const [operand, other] of [ + [e.left, e.right], + [e.right, e.left], + ] as const) { + const tags = tagsOf(operand); + if ( + tags && + tags.unionId === e.unionId && + other.kind === "unionWrap" && + other.tag === tags.stringTag && + other.value.kind === "strLit" && + other.value.value.length === 1 + ) { + const code = other.value.value.charCodeAt(0); + return { + operand, + test: (unit) => compare(e.negated ? "!==" : "===", unit(), num(code)), + }; + } + // `case undefined:` of a switch: undefined is a NaN unit. + if ( + tags && + tags.unionId === e.unionId && + other.kind === "unionWrap" && + other.tag === tags.undefinedTag && + other.value.kind === "unitLit" && + other.value.unit === "undefined" + ) + return { + operand, + twice: true, + test: (unit) => compare(e.negated ? "===" : "!==", unit(), unit()), + }; + } + return null; + } + if (e.kind === "libCall" && e.fn === "js.compare" && e.args.length === 3) { + const [a, b, opArg] = e.args as [IrExpr, IrExpr, IrExpr]; + if (opArg.kind !== "strLit" || !(opArg.value in FLIPPED)) return null; + const op = opArg.value as CompareOp; + const literalOf = (x: IrExpr): number | null => + x.kind === "libCall" && + x.fn === "js.fromStr" && + x.args.length === 1 && + x.args[0]!.kind === "strLit" && + x.args[0]!.value.length === 1 + ? x.args[0]!.value.charCodeAt(0) + : null; + const operandOf = (x: IrExpr): IrExpr | null => + x.kind === "libCall" && + x.fn === "js.fromDyn" && + x.args.length === 1 && + x.args[0]!.kind === "dynFrom" && + tagsOf(x.args[0]!.value) + ? x.args[0]!.value + : null; + // The comparison with the operand on the left. + const left = operandOf(a); + const right = operandOf(b); + const code = left ? literalOf(b) : right ? literalOf(a) : null; + const operand = left ?? right; + if (operand === null || code === null) return null; + const ordered = left ? op : FLIPPED[op]; + return { operand, test: (unit) => compare(ordered, unit(), num(code)) }; + } + if (e.kind === "unionIsTag") { + const tags = tagsOf(e.value); + if (!tags || tags.unionId !== e.unionId) return null; + if (e.tag !== tags.undefinedTag && e.tag !== tags.stringTag) return null; + // Undefined is a NaN unit; a character is any other. + const present = (e.tag === tags.stringTag) !== e.negated; + return { + operand: e.value, + twice: true, + test: (unit) => compare(present ? "===" : "!==", unit(), unit()), + }; + } + return null; +} + function rewriteFunction(fn: IrFunction): IrFunction { + const locals = new Map(fn.locals.map((l) => [l.id, l])); const candidates = new Set( - fn.locals.filter((l) => !l.mutable && !l.boxed && l.type.kind === "string").map((l) => l.id), + fn.locals + .filter( + (l) => !l.mutable && !l.boxed && (l.type.kind === "string" || l.type.kind === "union"), + ) + .map((l) => l.id), ); - if (candidates.size === 0) return fn; + // The optional reads candidate union locals are declared from, directly + // or as copies of another such local (a switch's discriminant temp). + const reads = new Map(); + const copyOf = new Map(); + const collect = (value: unknown): void => { + if (Array.isArray(value)) for (const item of value) collect(item); + else if (value !== null && typeof value === "object") { + const node = value as Record; + if (node.kind === "varDecl" && candidates.has(node.localId as string) && node.init) { + const id = node.localId as string; + const init = node.init as IrExpr; + const read = optionalCharRead(init); + if (read) reads.set(id, read); + else if (init.kind === "varRef" && reads.has(init.localId)) { + reads.set(id, reads.get(init.localId)!); + copyOf.set(id, init.localId); + } + } + for (const key in node) if (key !== "loc") collect(node[key]); + } + }; + collect(fn.body); + const localTags = (operand: IrExpr): OptionalCharRead | null => + operand.kind === "varRef" ? (reads.get(operand.localId) ?? null) : null; + // Every mention of a candidate, in any field, against the mentions the // rewrite understands. const mentions = new Map(); @@ -59,7 +335,7 @@ function rewriteFunction(fn: IrFunction): IrFunction { for (const item of value) count(item); } else if (value !== null && typeof value === "object") { const node = value as Record; - const id = understoodMention(node); + const id = understoodMention(node, locals, reads, localTags); if (id !== null && candidates.has(id)) { note(understood, id); if (node.kind === "varDecl") note(declarations, id); @@ -68,15 +344,26 @@ function rewriteFunction(fn: IrFunction): IrFunction { } }; count(fn.body); + // A copy is held only with its source, and the source's mention in the + // copy's initializer is understood only while the copy is held. const held = new Set(); for (const id of candidates) - if ( - declarations.get(id) === 1 && - (mentions.get(id) ?? 0) > 0 && - mentions.get(id) === understood.get(id) - ) - held.add(id); - if (held.size === 0) return fn; + if (declarations.get(id) === 1 && (mentions.get(id) ?? 0) > 0) held.add(id); + for (let settled = false; !settled;) { + settled = true; + for (const id of held) { + let copies = 0; + for (const [copy, source] of copyOf) if (source === id && held.has(copy)) copies++; + const source = copyOf.get(id); + if ( + mentions.get(id) !== (understood.get(id) ?? 0) + copies || + (source !== undefined && !held.has(source)) + ) { + held.delete(id); + settled = false; + } + } + } const unitRef = (id: string, loc: IrExpr["loc"]): IrExpr => ({ kind: "varRef", @@ -84,10 +371,19 @@ function rewriteFunction(fn: IrFunction): IrFunction { type: F64, loc, }); + // Operands of optional unit tests: held union locals and reads in place. + const operandTags = (operand: IrExpr): OptionalCharRead | null => + operand.kind === "varRef" + ? held.has(operand.localId) + ? localTags(operand) + : null + : optionalCharRead(operand); + let changed = false; const rewriteExpr = (e: IrExpr): IrExpr => { if (e.kind === "strEq" || e.kind === "strCmp") { const use = unitUse(e); if (use && held.has(use.id)) { + changed = true; const code: IrExpr = { kind: "numLit", value: use.code, type: F64, loc: e.loc }; const c = unitRef(use.id, e.loc); if (e.kind === "strEq") @@ -100,9 +396,7 @@ function rewriteFunction(fn: IrFunction): IrFunction { loc: e.loc, }; // The comparison with the local on the left. - const op = use.left - ? e.op - : ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[e.op]; + const op = use.left ? e.op : FLIPPED[e.op]; if (op === ">" || op === ">=") return { kind: "bin", op, left: c, right: code, type: BOOL, loc: e.loc }; const negation: IrExpr = { @@ -116,21 +410,43 @@ function rewriteFunction(fn: IrFunction): IrFunction { return { kind: "unary", op: "!", operand: negation, type: BOOL, loc: e.loc }; } } + const optional = optionalUnitTest(e, operandTags); + if (optional) { + const operand = optional.operand; + if (operand.kind === "varRef") { + changed = true; + return optional.test(() => unitRef(operand.localId, e.loc)); + } + // A read that keeps its temps declares them once: one unit read. + const read = optionalCharRead(operand)!; + if (read.plain || !optional.twice) { + changed = true; + return optional.test(() => unitRead(read, operand.loc)); + } + } return mapExprChildren(e, rewriteExpr, rewriteStmt); }; const rewriteStmt = (s: IrStmt): IrStmt => { - if (s.kind === "varDecl" && held.has(s.localId) && s.init?.kind === "strIntrinsic") - return { - ...s, - init: { - ...s.init, - method: "charCodeAt", - receiver: rewriteExpr(s.init.receiver), - args: s.init.args.map(rewriteExpr), - type: F64, - }, - }; - if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) + if (s.kind === "varDecl" && held.has(s.localId) && s.init) { + changed = true; + const source = copyOf.get(s.localId); + if (source !== undefined) return { ...s, init: unitRef(source, s.init.loc) }; + const read = reads.get(s.localId); + if (read) return { ...s, init: unitRead(read, s.init.loc) }; + if (s.init.kind === "strIntrinsic") + return { + ...s, + init: { + ...s.init, + method: "charCodeAt", + receiver: rewriteExpr(s.init.receiver), + args: s.init.args.map(rewriteExpr), + type: F64, + }, + }; + } + if (s.kind === "switch" && s.disc.kind === "varRef" && held.has(s.disc.localId)) { + changed = true; return { ...s, disc: unitRef(s.disc.localId, s.disc.loc), @@ -143,11 +459,14 @@ function rewriteFunction(fn: IrFunction): IrFunction { body: c.body.map(rewriteStmt), })), }; + } return mapStmtChildren(s, rewriteExpr, rewriteStmt); }; + const body = fn.body.map(rewriteStmt); + if (!changed) return fn; return { ...fn, - body: fn.body.map(rewriteStmt), + body, locals: fn.locals.map((l) => (held.has(l.id) ? unitLocal(l) : l)), }; } @@ -157,22 +476,37 @@ function unitLocal(local: IrLocal): IrLocal { return { ...local, type: F64 }; } -/** The local a node mentions in a form the rewrite handles: the - * declaration from a one-argument charAt, a one-unit test, or a switch - * over one-unit cases. */ -function understoodMention(node: Record): string | null { +/** The local a node mentions in a form the rewrite handles: a string + * local's declaration from a one-argument charAt, one-unit test, or switch + * over one-unit cases; a union local's declaration from an optional read, + * or its test against a one-unit literal or for undefined. */ +function understoodMention( + node: Record, + locals: ReadonlyMap, + reads: ReadonlyMap, + localTags: (operand: IrExpr) => OptionalCharRead | null, +): string | null { const n = node as unknown as IrExpr | IrStmt; - if (n.kind === "varDecl") return n.init && isCharAt(n.init) ? n.localId : null; + if (n.kind === "varDecl") { + if (!n.init) return null; + const local = locals.get(n.localId); + if (local?.type.kind === "string") return isCharAt(n.init) ? n.localId : null; + const read = reads.get(n.localId); + return read && local?.type.kind === "union" && local.type.unionId === read.unionId + ? n.localId + : null; + } if (n.kind === "strEq" || n.kind === "strCmp") return unitUse(n)?.id ?? null; if (n.kind === "switch") { - if (n.disc.kind !== "varRef") return null; + if (n.disc.kind !== "varRef" || n.disc.type.kind !== "string") return null; if (!n.cases.some((c) => c.test !== null)) return null; const units = n.cases.every( (c) => c.test === null || (c.test.kind === "strLit" && c.test.value.length === 1), ); return units ? n.disc.localId : null; } - return null; + const optional = optionalUnitTest(n as IrExpr, localTags); + return optional?.operand.kind === "varRef" ? optional.operand.localId : null; } function isCharAt(e: IrExpr): e is CharAt { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json new file mode 100644 index 000000000..36ea6f8ec --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7304-string-optional-char-reads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7304-string-optional-char-reads.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7304-string-optional-char-reads.ts b/tests/corpus/7304-string-optional-char-reads.ts new file mode 100644 index 000000000..13222219d --- /dev/null +++ b/tests/corpus/7304-string-optional-char-reads.ts @@ -0,0 +1,121 @@ +// s[i] with an index not proven in bounds, tested against one-unit +// literals: equality, inequality and ordered comparisons on either side, +// peeks at i - 1 and i + 1, tests for undefined, switches, character +// locals with and without other uses, fractional, negative, -0, NaN and +// infinite indices, reads past the end, a two-unit literal, Object.is, a +// class-based lexer with its position in a field, and ASCII, Latin-1, BMP, +// surrogate pairs, lone surrogates and empty strings. + +class Lexer { + pos = 0; + src: string; + constructor(src: string) { + this.src = src; + } + skipSpaces(): void { + const src = this.src; + let pos = this.pos; + while (pos < src.length && (src[pos] === " " || src[pos] === "\t")) pos++; + this.pos = pos; + } + word(): string { + const src = this.src; + const start = this.pos; + let pos = start; + while (pos < src.length) { + const c = src[pos]; + if ((c < "a" || c > "z") && (c < "0" || c > "9")) break; + pos++; + } + this.pos = pos; + return src.slice(start, pos); + } + operator(): string { + const src = this.src; + const c = src[this.pos]; + switch (c) { + case "=": + this.pos += src[this.pos + 1] === "=" ? 2 : 1; + return src[this.pos - 1] === "=" && src[this.pos - 2] === "=" ? "==" : "="; + case "+": + case "-": + this.pos++; + return c; + case undefined: + return ""; + default: + this.pos++; + return "?"; + } + } + tokens(): string[] { + const out: string[] = []; + while (this.pos < this.src.length) { + this.skipSpaces(); + const w = this.word(); + if (w !== "") out.push(w); + else if (this.pos < this.src.length) out.push(this.operator()); + } + return out; + } +} + +function tests(src: string, pos: number): string { + const bits: number[] = []; + bits.push(src[pos] === " " ? 1 : 0, src[pos] !== "x" ? 1 : 0); + bits.push(src[pos] < "a" ? 1 : 0, src[pos] <= "a" ? 1 : 0); + bits.push(src[pos] > "a" ? 1 : 0, src[pos] >= "a" ? 1 : 0); + bits.push("m" < src[pos] ? 1 : 0, "m" >= src[pos] ? 1 : 0); + bits.push(src[pos + 1] === "b" ? 1 : 0, src[pos - 1] > "\0" ? 1 : 0); + bits.push(src[pos] === undefined ? 1 : 0, src[pos] !== undefined ? 1 : 0); + bits.push(src[pos * 2] === undefined ? 1 : 0, src[pos] === "ab" ? 1 : 0); + bits.push(Object.is(src[pos], "a") ? 1 : 0); + return bits.join(""); +} + +function local(src: string, pos: number): string { + const c = src[pos]; + let n = 0; + if (c === undefined) n += 1; + if (c !== undefined && c >= "a") n += 2; + if ("z" > c) n += 4; + switch (c) { + case "é": + n += 8; + break; + case "b": + n += 16; + break; + default: + n += 32; + } + return String(n); +} + +function kept(src: string, pos: number): string { + const c = src[pos]; + if (c === " ") return "space"; + return c ?? "end"; +} + +const samples = ["abc", "", " ", "a b\tc", "café", "日本 語", "😀x", "\uD800z", "ab=b"]; +const indices = [0, 1, 2, 3, -1, -0, 0.5, 1.5, NaN, Infinity, 10]; +for (const s of samples) { + const row: string[] = []; + for (const i of indices) row.push(`${tests(s, i)}:${local(s, i)}:${kept(s, i)}`); + console.log(JSON.stringify(s), row.join(" ")); +} + +const programs = [ + "let x = a + b - c", + " alpha==beta gamma = delta1 ", + "x=y==z+", + "", + "abc é def", + "\tq\t=\t9", +]; +for (const p of programs) console.log(JSON.stringify(p), JSON.stringify(new Lexer(p).tokens())); + +let text = ""; +for (let k = 0; k < 30; k++) text += (k % 4 === 0 ? "==" : "w" + k) + " "; +console.log(new Lexer(text).tokens().length, new Lexer(text).tokens().slice(0, 6).join(",")); diff --git a/tests/harness/corpus-costs/7304-string-optional-char-reads.ts.json b/tests/harness/corpus-costs/7304-string-optional-char-reads.ts.json new file mode 100644 index 000000000..207668232 --- /dev/null +++ b/tests/harness/corpus-costs/7304-string-optional-char-reads.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 4200 } From 8b5e8617702fd2438dfb049d4cf8d66961c10aa4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 21:21:09 -0500 Subject: [PATCH 028/409] Give each class one prototype in identity comparisons of unknown values `Object.getPrototypeOf(a) === Object.getPrototypeOf(b)`, for two instances held as unknown, compared fresh objects when their class has no prototype data (no methods, or a subclass of Error), so it was false for instances of one class. Each such class now has one stand-in object, created on first use. A subclass instance boxed at its base type answered with the base's prototype, so instances of two sibling subclasses compared true. The answer for a value boxed at a class now tests the class's subclasses, most derived first, and answers with the prototype of the one the instance belongs to. --- .../lowering/class-dynamic-dispatch.ts | 120 +++++++++++++++--- .../7541-prototype-identity-stand-ins.ts.json | 6 + ...42-prototype-identity-without-data.ts.json | 6 + .../7541-prototype-identity-stand-ins.ts | 22 ++++ .../7542-prototype-identity-without-data.ts | 19 +++ .../7541-prototype-identity-stand-ins.ts.json | 1 + ...42-prototype-identity-without-data.ts.json | 1 + 7 files changed, 157 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json create mode 100644 tests/corpus/7541-prototype-identity-stand-ins.ts create mode 100644 tests/corpus/7542-prototype-identity-without-data.ts create mode 100644 tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json create mode 100644 tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index b18615ef6..8dd5fd2c1 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -909,6 +909,9 @@ export class ClassDynamicDispatch { private descriptorHelper: IrFunction | null = null; private prototypeHelper: IrFunction | null = null; private readonly prototypeClasses = new Set(); + /** The stand-in prototype object of each class without prototype data + * (prototypeStandIn), by class name. */ + private readonly prototypeStandIns = new Set(); /** The prototype reads of identity walks, by libCall, and the classes * each one answers for boxed instances. */ private readonly prototypeIdentityHelpers = new Map< @@ -1181,32 +1184,76 @@ export class ClassDynamicDispatch { const value = varRef("value", DYN, loc); // A class with no prototype data in its chain (one extending // Error, or with no methods) has no prototype a program can name: - // a fresh object stands in, so an identity comparison or an - // instanceof walk (through Object.prototype to null) answers false - // instead of reaching the reflection refusal. - const standIn: IrExpr = { kind: "dynObjLit", fields: [], type: DYN, loc }; - const prototype = hasClassPrototypeData(info) - ? classPrototypeData(lowerer, info, loc, { - kind: "dynCheck", - value, - type, - loc, - }) - : standIn; - if (prototype) - helper.fn.body.unshift({ + // an object of the class's own stands in, so instances of one class + // share it (`Object.getPrototypeOf(a) === Object.getPrototypeOf(b)`), + // other prototypes differ from it, and an instanceof walk (through + // Object.prototype to null) answers false instead of reaching the + // reflection refusal. + const answer = (name: string): IrStmt[] | null => { + const of = lowerer.classes.get(name)!; + const prototype = hasClassPrototypeData(of) + ? classPrototypeData(lowerer, of, loc, { + kind: "dynCheck", + value, + type: { kind: "object", className: name }, + loc, + }) + : null; + if (prototype) return [{ kind: "return", value: prototype, loc }]; + return hasClassPrototypeData(of) ? null : this.prototypeStandIn(lowerer, name, loc); + }; + const own = answer(className); + if (!own) { + changed = true; + continue; + } + // A capsule boxed at this class can hold a subclass instance: the + // most derived subclass the instance belongs to answers. + const depth = (name: string): number => { + let n = 0; + for (let at = lowerer.classes.get(name)?.base; at; at = at.base) n++; + return n; + }; + const subclasses = [...lowerer.classes.keys()] + .filter( + (name) => + name !== className && + lowerer.isSubclassOf(name, className) && + isDynTypedRefType({ kind: "object", className: name }), + ) + .sort((a, b) => depth(b) - depth(a)); + const then: IrStmt[] = []; + for (const name of subclasses) { + const sub = answer(name); + if (!sub) continue; + then.push({ kind: "if", cond: { - kind: "libCall", - fn: "dyn.typedRefIs", - args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + kind: "instanceOf", + value: { kind: "dynCheck", value, type, loc }, + className: name, type: BOOL, loc, }, - then: [{ kind: "return", value: prototype, loc }], + then: sub, else_: null, loc, }); + } + then.push(...own); + helper.fn.body.unshift({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then, + else_: null, + loc, + }); changed = true; } for (const plan of this.propertyReceivers(lowerer, false, false)) { @@ -3528,6 +3575,43 @@ export class ClassDynamicDispatch { /** The receiver's own dynamic properties for a read: undefined while it has * none, and never stored, so reads stay valid on published objects. */ + /** Statements that return `className`'s stand-in prototype: one empty + * object per class, created on first use and kept in a module global. */ + private prototypeStandIn(lowerer: Lowerer, className: string, loc: SrcLoc): IrStmt[] { + const globalId = `%g.%prototype.standIn.${className}`; + const readyId = `${globalId}.ready`; + if (!this.prototypeStandIns.has(className)) { + this.prototypeStandIns.add(className); + lowerer.globalsList.push( + { id: globalId, name: `${className}.prototype`, type: DYN, mutable: true }, + { id: readyId, name: `${className}.prototype.ready`, type: BOOL, mutable: true }, + ); + } + return [ + { + kind: "if", + cond: { kind: "unary", op: "!", operand: varRef(readyId, BOOL, loc), type: BOOL, loc }, + then: [ + { + kind: "assign", + localId: globalId, + value: { kind: "dynObjLit", fields: [], type: DYN, loc }, + loc, + }, + { + kind: "assign", + localId: readyId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: varRef(globalId, DYN, loc), loc }, + ]; + } + private readPropertyBag(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc): IrExpr { const helper = classPropertiesHelper(lowerer, loc, true); this.generated.add(helper); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json new file mode 100644 index 000000000..c5179b475 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7541-prototype-identity-stand-ins.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json new file mode 100644 index 000000000..09f1a6369 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7542-prototype-identity-without-data.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7541-prototype-identity-stand-ins.ts b/tests/corpus/7541-prototype-identity-stand-ins.ts new file mode 100644 index 000000000..81b1e8c71 --- /dev/null +++ b/tests/corpus/7541-prototype-identity-stand-ins.ts @@ -0,0 +1,22 @@ +// Instances held as unknown, of a class whose chain has no prototype data +// (a subclass without methods, or one extending Error), still share one +// prototype: comparing the prototypes of two instances of the same class is +// true, and of instances of different classes is false. +class Base { + tag = "b"; +} +class Sub extends Base {} +class Other extends Base {} +class Oops extends Error {} +const bases: Base[] = [new Sub(), new Sub(), new Other()]; +const errors: Error[] = [new Oops("a"), new Oops("b")]; +const first: unknown = bases[0]; +const second: unknown = bases[1]; +const other: unknown = bases[2]; +const e1: unknown = errors[0]; +const e2: unknown = errors[1]; +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(second)); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(other)); +console.log(Object.getPrototypeOf(e1) === Object.getPrototypeOf(e2)); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(e1)); +console.log(first instanceof Sub, other instanceof Sub, e1 instanceof Oops, e1 instanceof Error); diff --git a/tests/corpus/7542-prototype-identity-without-data.ts b/tests/corpus/7542-prototype-identity-without-data.ts new file mode 100644 index 000000000..9ac1209f8 --- /dev/null +++ b/tests/corpus/7542-prototype-identity-without-data.ts @@ -0,0 +1,19 @@ +// Instances held as unknown, of classes whose chains have no prototype data +// a program can name (no methods, or a subclass of Error): each class's +// instances share one prototype, and different classes' differ, as in Node. +class Base { + tag = "b"; +} +class Sub extends Base {} +class Other extends Base {} +class Oops extends Error {} +const bases: Base[] = [new Sub(), new Sub(), new Other(), new Base()]; +const errors: Error[] = [new Oops("a"), new Oops("b")]; +const [a, b, c, d] = bases.map((value): unknown => value); +const e1: unknown = errors[0]; +const e2: unknown = errors[1]; +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(d)); +console.log(Object.getPrototypeOf(e1) === Object.getPrototypeOf(e2)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(e1)); diff --git a/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json b/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json new file mode 100644 index 000000000..046e0461c --- /dev/null +++ b/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json @@ -0,0 +1 @@ +{ "plain": 4700, "san": 6500 } diff --git a/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json b/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json new file mode 100644 index 000000000..d2c83f967 --- /dev/null +++ b/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3300 } From 9bf76af9745b368c83ac6051da0b08e143736c4b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 15:43:59 -0700 Subject: [PATCH 029/409] Build @hono/node-server 2.x into scriptc @hono/node-server replaces the global Request and Response with lightweight objects and reparents prototypes, tricks that only pay off under V8 and that native compilation cannot follow. scriptc now compiles the package from a built-in implementation (shims/hono-node-server/v2.ts) over node:http and the native Request/Response/Headers: serve, createAdaptorServer, getRequestListener and RequestError. Responses are written the way the package writes them, byte for byte on the wire: the general path (header record in Headers order, Set-Cookie gathered, default content type, content length for bodies that finish within the first reads, streamed bodies, error responses) and the package's cached-response path for a Response that is still exactly its constructor arguments. The runtime tracks that lightweight state on each constructed Response (header form, raw plain-record headers, body length; cleared by body and metadata reads, turned into Headers form by a headers read) and exposes it to the shim through scriptcResponseLight. The declaration entry of a shimmed package may now be a .d.mts/.d.cts file. The npm case hono-node-server drives buffered, streamed, cookie, header-form, HEAD, error-handler and URL cases against the real package under Node. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + docs/content/docs/dependencies.mdx | 7 + .../compiler/shims/hono-node-server/v2.ts | 479 ++++++ .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/lower-calls.ts | 1 + .../compiler/src/frontend/package-shims.ts | 15 +- .../compiler/src/ir/builtin-signatures.ts | 1 + .../compiler/test/package-shim-guard.test.ts | 1 + .../npm/cases/hono-node-server/main.ts.json | 7 + packages/runtime/src/scr_fetch.c | 162 +- packages/runtime/src/scr_generator.h | 1 + .../npm/cases/hono-node-server/main.ts | 238 +++ .../npm/cases/hono-node-server/package.json | 1 + .../node_modules/@hono/node-server/LICENSE | 21 + .../node_modules/@hono/node-server/README.md | 450 ++++++ .../@hono/node-server/dist/conninfo.cjs | 22 + .../@hono/node-server/dist/conninfo.d.cts | 11 + .../@hono/node-server/dist/conninfo.d.mts | 11 + .../@hono/node-server/dist/conninfo.mjs | 20 + .../node-server/dist/constants-BLSFu_RU.mjs | 5 + .../node-server/dist/constants-BXAKTxRC.cjs | 11 + .../@hono/node-server/dist/early-hints.cjs | 35 + .../@hono/node-server/dist/early-hints.d.cts | 16 + .../@hono/node-server/dist/early-hints.d.mts | 16 + .../@hono/node-server/dist/early-hints.mjs | 33 + .../@hono/node-server/dist/index.cjs | 1340 +++++++++++++++++ .../@hono/node-server/dist/index.d.cts | 101 ++ .../@hono/node-server/dist/index.d.mts | 101 ++ .../@hono/node-server/dist/index.mjs | 1335 ++++++++++++++++ .../@hono/node-server/dist/serve-static.cjs | 153 ++ .../@hono/node-server/dist/serve-static.d.cts | 19 + .../@hono/node-server/dist/serve-static.d.mts | 19 + .../@hono/node-server/dist/serve-static.mjs | 152 ++ .../@hono/node-server/dist/utils/response.cjs | 8 + .../node-server/dist/utils/response.d.cts | 4 + .../node-server/dist/utils/response.d.mts | 4 + .../@hono/node-server/dist/utils/response.mjs | 7 + .../@hono/node-server/dist/utils/stream.cjs | 65 + .../@hono/node-server/dist/utils/stream.d.cts | 6 + .../@hono/node-server/dist/utils/stream.d.mts | 6 + .../@hono/node-server/dist/utils/stream.mjs | 64 + .../@hono/node-server/package.json | 128 ++ tests/fixtures/npm/node_modules/hono/LICENSE | 21 + .../hono/dist/helper/websocket/index.js | 53 + .../npm/node_modules/hono/package.json | 12 + 45 files changed, 5158 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/shims/hono-node-server/v2.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-node-server/main.ts.json create mode 100644 tests/fixtures/npm/cases/hono-node-server/main.ts create mode 100644 tests/fixtures/npm/cases/hono-node-server/package.json create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/LICENSE create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/README.md create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/node-server/package.json create mode 100644 tests/fixtures/npm/node_modules/hono/LICENSE create mode 100644 tests/fixtures/npm/node_modules/hono/dist/helper/websocket/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/package.json diff --git a/.gitignore b/.gitignore index cd44fe11d..b271af8e1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ output/ /packages/runtime-*/runtime-pack.json /packages/runtime-*/.runtime-pack-* !tests/fixtures/npm/node_modules/*/dist/ +!tests/fixtures/npm/node_modules/@*/*/dist/ !tests/fixtures/npm/workspace/*/dist/ !tests/fixtures/npm/cases/workspace-copied/members/*/dist/ !tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/ diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 409b9cb30..3098dcad4 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -92,6 +92,13 @@ scriptc implements a few packages itself and compiles that implementation in pla - graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying. - An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there. +`@hono/node-server` 2.x is built in. `serve`, `createAdaptorServer`, `getRequestListener` and `RequestError` serve a fetch handler such as Hono's `app.fetch` over `node:http` with native `Request` and `Response` objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies are read before the handler runs. Known differences: + +- Only HTTP/1.1 servers are declared: the `createServer` and `serverOptions` options for `node:https` and `node:http2`, the `websocket` option and `upgradeWebSocket` aren't declared, so using one is a type error at build time. Subpath modules such as `@hono/node-server/serve-static` compile from the package's own files. +- The fetch handler must return a `Response` or a promise of one; the handler's second argument holds `incoming` and `outgoing`. +- The global `Request` and `Response` stay the built-in classes, so `overrideGlobalObjects` has no effect, and a `Response` reads its default text content type as `text/plain;charset=UTF-8` where the package's replacement reads `text/plain; charset=UTF-8`. Written responses carry the package's spelling. +- A streamed response is written without waiting for `drain`, and a failed handler answers 504 only for an error whose `name` is `TimeoutError`. + Small polyfill packages whose module is exactly a Node.js built-in on Node.js 24 compile as that built-in, at the `require()` that loads them, from your program's CommonJS files, from compiled packages, and from esbuild bundles whose module paths name the package and its version (as pnpm installs do). The package's own files aren't compiled. diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts new file mode 100644 index 000000000..3708f0e4c --- /dev/null +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -0,0 +1,479 @@ +// scriptc's built-in implementation of `@hono/node-server` 2.x: the Node.js +// adapter that serves a fetch-style handler (Hono's `app.fetch`) over +// node:http. The published package swaps the global Request and Response +// for lightweight objects and reparents prototypes, which only pays off +// under V8; this port builds native Request objects from the incoming +// message and writes the handler's Response the way the package's +// listener does (header record, content length for bodies that complete +// within the first reads, streamed bodies, error responses), so a client +// sees the same status lines, headers and bodies. +// +// HTTP/1.1 only: the http2/https server options and WebSocket upgrades are +// not declared. A Response that is still exactly its constructor arguments +// takes the package's cached-response path (its lightweight Response), read +// from the runtime through scriptcResponseLight; every other Response takes +// the general path. +import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; +import type { AddressInfo } from "node:net"; + +export type HttpBindings = { + incoming: IncomingMessage; + outgoing: ServerResponse; +}; + +export type ServerType = Server; + +export type FetchCallback = (request: Request, env: HttpBindings) => Response | Promise; + +export type CustomErrorHandler = (err: unknown) => void | Response | Promise; + +export type Options = { + fetch: FetchCallback; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; + port?: number; + hostname?: string; +}; + +export interface RequestListenerOptions { + hostname?: string; + errorHandler?: CustomErrorHandler; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; +} + +export class RequestError extends Error { + constructor(message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "RequestError"; + } +} + +const DEFAULT_CONTENT_TYPE = "text/plain; charset=UTF-8"; +const X_ALREADY_SENT = "x-hono-already-sent"; + +const reValidRequestUrl = /^\/[!#$&-;=?-[\]_a-z~]*$/; +const reDotSegment = /\/\.\.?(?:[/?#]|$)/; +const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/; + +function buildUrl(scheme: string, host: string, incomingUrl: string): string { + const url = `${scheme}://${host}${incomingUrl}`; + if (!reValidHost.test(host)) { + const urlObj = new URL(url); + if ( + urlObj.hostname.length !== host.length && + urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase() + ) + throw new RequestError("Invalid host header"); + return urlObj.href; + } + if (incomingUrl.length === 0) return url + "/"; + if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL"); + if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl)) + return new URL(url).href; + return url; +} + +function normalizeIncomingMethod(method: string | undefined): string { + if (method === undefined || method.length === 0) return "GET"; + switch (method) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "PATCH": + case "POST": + case "PUT": + case "QUERY": + return method; + } + const upper = method.toUpperCase(); + switch (upper) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "POST": + case "PUT": + return upper; + default: + return method; + } +} + +/** The request URL the package builds: an absolute request target as is, + * otherwise the Host header (or the configured hostname) over http. */ +function requestUrl(incoming: IncomingMessage, defaultHostname: string | undefined): string { + const incomingUrl = incoming.url ?? ""; + if ( + incomingUrl[0] !== "/" && + (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://")) + ) { + try { + return new URL(incomingUrl).href; + } catch (e) { + throw new RequestError("Invalid absolute URL", { cause: e }); + } + } + const hostHeader = incoming.headers.host; + const host = hostHeader !== undefined && hostHeader !== "" ? hostHeader : (defaultHostname ?? ""); + if (host === "") throw new RequestError("Missing host header"); + try { + return buildUrl("http", host, incomingUrl); + } catch (e) { + if (e instanceof RequestError) throw e; + throw new RequestError("Invalid URL", { cause: e }); + } +} + +function requestHeaders(incoming: IncomingMessage): Headers { + const headers = new Headers(); + const raw = incoming.rawHeaders; + for (let i = 0; i < raw.length; i += 2) { + const name = raw[i]!; + if (!name.startsWith(":")) headers.append(name, raw[i + 1]!); + } + return headers; +} + +function readIncoming(incoming: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + incoming.on("data", (chunk: Buffer) => { + chunks.push(chunk); + }); + incoming.on("end", () => resolve(chunks.length === 1 ? chunks[0]! : Buffer.concat(chunks))); + incoming.on("error", reject); + }); +} + +async function newRequest( + incoming: IncomingMessage, + defaultHostname: string | undefined, +): Promise { + const method = normalizeIncomingMethod(incoming.method); + const url = requestUrl(incoming, defaultHostname); + const headers = requestHeaders(incoming); + if (method === "GET" || method === "HEAD") return new Request(url, { method, headers }); + const body = await readIncoming(incoming); + return new Request(url, { method, headers, body }); +} + +/** The package's outgoing header record: Headers order (lowercase, + * sorted), Set-Cookie values gathered into one array entry at the end, + * and the default content type for a response with a body. */ +function buildOutgoingHttpHeaders( + headers: Headers, + hasBody: boolean, +): Record { + const res: Record = {}; + if (headers.has("set-cookie")) { + const cookies: string[] = []; + headers.forEach((v, k) => { + if (k === "set-cookie") cookies.push(v); + else res[k] = v; + }); + // Headers.forEach joins nothing but set-cookie, which getSetCookie + // lists value by value, as the package's iteration does. + if (cookies.length > 0) res["set-cookie"] = headers.getSetCookie(); + } else { + headers.forEach((v, k) => { + res[k] = v; + }); + } + if (hasBody && res["content-type"] === undefined) res["content-type"] = DEFAULT_CONTENT_TYPE; + return res; +} + +type ReadResult = ReadableStreamReadResult; + +function readWithoutBlocking(read: Promise): Promise { + return Promise.race([read, Promise.resolve().then(() => Promise.resolve(undefined))]); +} + +function nextTick(): Promise { + return new Promise((resolve) => { + setTimeout(resolve); + }); +} + +async function pump( + reader: ReadableStreamDefaultReader, + outgoing: ServerResponse, + pending: Promise | undefined, +): Promise { + let read = pending ?? reader.read(); + for (;;) { + const { done, value } = await read; + if (done) { + outgoing.end(); + return; + } + outgoing.write(value); + read = reader.read(); + } +} + +function handleResponseError(e: unknown, outgoing: ServerResponse): void { + const err = e instanceof Error ? e : new Error("unknown error", { cause: e }); + console.error(e); + if (!outgoing.headersSent) outgoing.writeHead(500, { "Content-Type": "text/plain" }); + outgoing.end(`Error: ${err.message}`); + outgoing.destroy(); +} + +function emptyResponse(status: number): Response { + return new Response(null, { status }); +} + +/** The lightweight state of a constructed Response (the runtime's + * scr_fetch_response_light): [form, flags, byte length, ...record pairs]. */ +declare function scriptcResponseLight(res: Response): string[]; + +const UNDICI_TEXT_TYPE = "text/plain;charset=UTF-8"; + +/** Content-Length as Node's implicit header: set the headers one by one and + * let end(body) compute the length, which Node then writes after its own + * Date/Connection headers, as the package's preset length does. */ +function endWithImplicitLength( + outgoing: ServerResponse, + status: number, + header: Record, + body: Uint8Array, +): void { + outgoing.statusCode = status; + for (const name of Object.keys(header)) outgoing.setHeader(name, header[name]!); + outgoing.end(body); +} + +/** The package's cached-response path: a Response that is still exactly + * its constructor arguments (no body or metadata read since) is written + * from them. Form 1 has no init headers, form 2 Headers, form 3 a plain + * record kept verbatim. */ +async function writeLightResponse( + res: Response, + outgoing: ServerResponse, + light: string[], + head: boolean, +): Promise { + const form = light[0]!; + const flags = light[1]!; + const length = Number(light[2]!); + const status = res.status; + const sized = length >= 0; + if (form === "1") { + if (length === -1) { + outgoing.writeHead(status); + outgoing.end(); + } else if (sized) { + const body = await res.bytes(); + if (head) { + outgoing.writeHead(status, { + "Content-Type": DEFAULT_CONTENT_TYPE, + "Content-Length": String(length), + }); + outgoing.end(body); + } else + endWithImplicitLength(outgoing, status, { "Content-Type": DEFAULT_CONTENT_TYPE }, body); + } else { + outgoing.writeHead(status, { "Content-Type": DEFAULT_CONTENT_TYPE }); + outgoing.flushHeaders(); + await pump(res.body!.getReader(), outgoing, undefined); + } + return; + } + let header: Record; + let hasContentLength = false; + let plain = false; + let canAutoLength = true; + if (form === "2") { + const headers = new Headers(res.headers); + if (flags.includes("a") && headers.get("content-type") === UNDICI_TEXT_TYPE) { + if (flags.includes("d")) headers.set("content-type", DEFAULT_CONTENT_TYPE); + else headers.delete("content-type"); + } else if (flags.includes("d") && !headers.has("content-type")) { + headers.set("content-type", DEFAULT_CONTENT_TYPE); + } + hasContentLength = headers.has("content-length"); + header = buildOutgoingHttpHeaders(headers, length !== -1); + } else { + plain = true; + header = {}; + for (let i = 3; i + 1 < light.length; i += 2) { + const name = light[i]!; + const value = light[i + 1]!; + const previous = header[name]; + if (previous === undefined) header[name] = value; + else if (typeof previous === "string") header[name] = [previous, value]; + else previous.push(value); + const lower = name.toLowerCase(); + if (lower === "content-length") hasContentLength = true; + else if (lower === "transfer-encoding" || lower === "trailer") canAutoLength = false; + } + } + const body = sized ? await res.bytes() : undefined; + if (!hasContentLength && body !== undefined) { + if (plain && canAutoLength && !head && status !== 204 && status !== 304) { + endWithImplicitLength(outgoing, status, header, body); + return; + } + header["Content-Length"] = String(length); + } + outgoing.writeHead(status, header); + if (length === -1) outgoing.end(); + else if (body !== undefined) outgoing.end(body); + else { + outgoing.flushHeaders(); + await pump(res.body!.getReader(), outgoing, undefined); + } +} + +async function writeResponse( + res: Response, + outgoing: ServerResponse, + head: boolean, +): Promise { + const light = scriptcResponseLight(res); + if (light[0] !== "0") return writeLightResponse(res, outgoing, light, head); + const body = res.body; + const record = buildOutgoingHttpHeaders(res.headers, body !== null); + if (body !== null) { + const reader = body.getReader(); + const values: Uint8Array[] = []; + let done = false; + let pending: Promise | undefined = undefined; + if (record["transfer-encoding"] !== "chunked") { + let maxReadCount = 2; + for (let i = 0; i < maxReadCount; i++) { + pending ??= reader.read(); + let chunk: ReadResult | undefined = undefined; + try { + chunk = await readWithoutBlocking(pending); + } catch (e) { + console.error(e); + done = true; + } + if (chunk === undefined) { + if (i === 1 && !done) { + await nextTick(); + maxReadCount = 3; + continue; + } + break; + } + pending = undefined; + if (chunk.value !== undefined) values.push(chunk.value); + if (chunk.done) { + done = true; + break; + } + } + if (done && record["content-length"] === undefined) { + let length = 0; + for (const value of values) length += value.length; + record["content-length"] = String(length); + } + } + outgoing.writeHead(res.status, record); + for (const value of values) outgoing.write(value); + if (done) outgoing.end(); + else { + if (values.length === 0) outgoing.flushHeaders(); + await pump(reader, outgoing, pending); + } + } else if (record[X_ALREADY_SENT] === undefined) { + outgoing.writeHead(res.status, record); + outgoing.end(); + } +} + +async function respond( + pending: Response | Promise, + outgoing: ServerResponse, + errorHandler: CustomErrorHandler | undefined, + head: boolean, +): Promise { + let res: Response; + if (pending instanceof Promise) { + try { + res = await pending; + } catch (err) { + if (errorHandler === undefined) { + res = handleFetchError(err); + } else { + const errRes = await errorHandler(err); + if (errRes === undefined) return; + res = errRes; + } + } + } else res = pending; + await writeResponse(res, outgoing, head); +} + +function handleFetchError(e: unknown): Response { + return emptyResponse(e instanceof Error && e.name === "TimeoutError" ? 504 : 500); +} + +async function handleRequest( + fetchCallback: FetchCallback, + options: RequestListenerOptions, + incoming: IncomingMessage, + outgoing: ServerResponse, +): Promise { + let req: Request | undefined = undefined; + let res: Response | Promise | undefined = undefined; + try { + req = await newRequest(incoming, options.hostname); + res = fetchCallback(req, { incoming, outgoing }); + } catch (e) { + if (res !== undefined) return handleResponseError(e, outgoing); + const errorHandler = options.errorHandler; + if (errorHandler !== undefined) { + const handled = await errorHandler( + req !== undefined ? e : e instanceof RequestError ? e : toRequestError(e), + ); + if (handled === undefined) return; + res = handled; + } else if (req === undefined) res = emptyResponse(400); + else res = handleFetchError(e); + } + try { + await respond(res, outgoing, options.errorHandler, incoming.method === "HEAD"); + } catch (e) { + handleResponseError(e, outgoing); + } +} + +function toRequestError(e: unknown): RequestError { + return new RequestError(e instanceof Error ? e.message : String(e), { cause: e }); +} + +export function getRequestListener( + fetchCallback: FetchCallback, + options: RequestListenerOptions = {}, +): (incoming: IncomingMessage, outgoing: ServerResponse) => Promise { + return (incoming, outgoing) => handleRequest(fetchCallback, options, incoming, outgoing); +} + +export function createAdaptorServer(options: Options): ServerType { + const listener = getRequestListener(options.fetch, { + hostname: options.hostname, + overrideGlobalObjects: options.overrideGlobalObjects, + autoCleanupIncoming: options.autoCleanupIncoming, + }); + return createServer((incoming, outgoing) => { + void listener(incoming, outgoing); + }); +} + +export function serve( + options: Options, + listeningListener?: (info: AddressInfo) => void, +): ServerType { + const server = createAdaptorServer(options); + server.listen(options.port ?? 3000, options.hostname, () => { + const info = server.address() as AddressInfo; + if (listeningListener !== undefined) listeningListener(info); + }); + return server; +} diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8e7c74cad..8c7d44cdb 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -893,6 +893,7 @@ export const LIB_FN_SYMS: Record = { // ambient-this read never throw. The fs dyn read is the sync-fs story. "fetch.start": "scr_fetch_static", "fetch.responseNew": "scr_fetch_response_new", + "fetch.responseLight": "scr_fetch_response_light", "fetch.responseJsonStatic": "scr_fetch_response_json_static", "fetch.input": "scr_fetch_input", "abort.controllerNew": "scr_fetch_abort_controller_new", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index c1bc044ae..c7332016c 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -11157,6 +11157,7 @@ const SHIM_INTRINSICS: ReadonlyMap = new Map< variants: ["hot-shots/v8-unix-dgram.ts"], }, ], + [ + "@hono/node-server", + { + name: "@hono/node-server", + mainFile: "dist/index.mjs", + typesFile: "dist/index.d.mts", + versions: new Map([[2, "hono-node-server/v2.ts"]]), + }, + ], [ "undici", { @@ -324,7 +333,7 @@ export function isShimmedImport(fromFile: string, specifier: string): boolean { /** The shimmed package whose declaration entry `path` is, with the * package directory: `/index.d.ts`, or the entry's own `typesFile`. */ function shimOfDeclarationPath(path: string): { pkgDir: string; shim: ImplementationShim } | null { - if (!path.endsWith(".d.ts")) return null; + if (!/\.d\.[cm]?ts$/.test(path)) return null; if (path.endsWith("/index.d.ts")) { const pkgDir = dirname(path); const shim = IMPLEMENTATION_SHIMS.get(pkgDir.slice(pkgDir.lastIndexOf("/") + 1)); @@ -377,7 +386,7 @@ export function isPackageShimFile(fileName: string): boolean { /** True for a name a shipped shim may declare as a bodiless runtime * intrinsic (lower-calls.ts lowers its calls to runtime calls). */ export function isShimIntrinsicName(name: string): boolean { - return name.startsWith("scriptcUnixDgram"); + return name.startsWith("scriptcUnixDgram") || name === "scriptcResponseLight"; } /** Resets per-compile caches. */ diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index f5da56818..8d2de2a87 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -66,6 +66,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, "fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN }, + "fetch.responseLight": { argTypes: [DYN], result: arrayOf(STRING) }, "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, "abort.controllerNew": { argTypes: [], result: DYN }, /** EventTarget / Event / CustomEvent construction (scr_event_target.c): diff --git a/packages/compiler/test/package-shim-guard.test.ts b/packages/compiler/test/package-shim-guard.test.ts index a8620c1ce..9843739cd 100644 --- a/packages/compiler/test/package-shim-guard.test.ts +++ b/packages/compiler/test/package-shim-guard.test.ts @@ -51,6 +51,7 @@ describe("package shims compile without refusals", () => { expect(entries.map((e) => e.slice(shimRoot.length + 1))).toEqual([ "chalk/v5.ts", "fs-extra/index.ts", + "hono-node-server/v2.ts", "hot-shots/v8-unix-dgram.ts", "hot-shots/v8.ts", "undici/v5.ts", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-node-server/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-node-server/main.ts.json new file mode 100644 index 000000000..58f450083 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-node-server/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/packages/compiler/shims/hono-node-server/v2.ts", + "/tests/fixtures/npm/cases/hono-node-server/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 96e90e47c..18daed32f 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -184,6 +184,23 @@ struct SfResponse { int status; bool redirected; bool null_body; + /* @hono/node-server's lightweight-response state (scr_fetch_response_light): + * whether a constructed Response is still one the package would write + * from its constructor arguments, and in which header form. 0: not (it + * was materialized by a body/metadata read, or never qualified); 1: no + * init headers; 2: a Headers (or pair list) form; 3: a plain record, + * whose raw entries light_record keeps in order. */ + int8_t light; + /* Form 2 reached from form 1 by a headers read: the package's Headers + * start with its default text content type when the body is not null. */ + bool light_default_ct; + /* The constructor added the text content type (a string body without + * one), which the package's lightweight headers never hold. */ + bool auto_ct; + /* The body's byte length (string or Uint8Array), -1 for a null body, + * -2 for a stream. */ + double light_length; + ScrArr *light_record; }; #ifndef SCR_FETCH_SIGNAL_ONLY @@ -2627,6 +2644,7 @@ static void sf_response_release(SfResponse *r) { sf_headers_release(r->headers); scr_str_release(r->url); scr_str_release(r->status_text); + scr_arr_release(r->light_record); free(r); } @@ -2637,6 +2655,13 @@ static void sf_response_release_v(void *p) { sf_response_release((SfResponse *)p); } +/* A read the package's lightweight Response answers from its native + * response materializes it; status, ok and headers keep it light, and a + * headers read turns the header form into a Headers object. */ +static void sf_response_materialize(SfResponse *r) { + r->light = 0; +} + static ScrPromise *sf_response_collect(SfResponse *r, int mode) { if (!r->null_body) return sf_stream_collect(r->body, mode); @@ -2675,6 +2700,7 @@ static ScrDyn *sf_response_invoke(void *ptr, ScrDyn *self, ScrDyn *const *args, size_t argc, const char *what) { SfResponse *r = ptr; + sf_response_materialize(r); int mode = -1; if (strcmp(method, "json") == 0) mode = SF_COLLECT_JSON; else if (strcmp(method, "text") == 0) mode = SF_COLLECT_TEXT; @@ -2698,6 +2724,19 @@ static ScrDyn *sf_response_get(void *ptr, const char *key, size_t len) { if (sf_name(key, len, "ok")) { return scr_dyn_new_bool(r->status >= 200 && r->status <= 299); } + if (sf_name(key, len, "headers")) { + if (r->light == 1) { + r->light = 2; + r->light_default_ct = !r->null_body; + } else if (r->light == 3) { + r->light = 2; + } + return scr_dyn_new_handle(r->headers, SCR_DYNH_FETCH_HEADERS); + } + if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") || + sf_name(key, len, "url") || sf_name(key, len, "redirected") || + sf_name(key, len, "statusText")) + sf_response_materialize(r); if (sf_name(key, len, "bodyUsed")) { return scr_dyn_new_bool(!r->null_body && r->body->disturbed); } @@ -2713,9 +2752,6 @@ static ScrDyn *sf_response_get(void *ptr, const char *key, size_t len) { if (sf_name(key, len, "statusText")) { return scr_dyn_new_str(r->status_text); } - if (sf_name(key, len, "headers")) { - return scr_dyn_new_handle(r->headers, SCR_DYNH_FETCH_HEADERS); - } return NULL; } @@ -2727,6 +2763,7 @@ static ScrPromise *sf_response_collect_dyn(ScrDyn *response, int mode) { &scr_dyn_release_v, NULL); } SfResponse *r = response->v.handle.ptr; + sf_response_materialize(r); return sf_response_collect(r, mode); } @@ -4719,6 +4756,61 @@ static ScrDyn *sf_response_json_serialize(const ScrDyn *value) { return serialized; } +/* The lightweight header form of a constructor's init headers (see + * SfResponse.light): absent headers are form 1 (Response.json's plain + * {"content-type": "application/json"} record is form 3), a Headers object + * or pair list form 2, and a plain record form 3, whose names and values + * *record receives in order (an array value contributes one entry per + * element, as writeHead writes it). 0 for any other init. */ +static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, + ScrArr **record) { + if (!headers || headers->kind == SCR_DYN_UNDEF) { + if (!json_body) return 1; + *record = scr_arr_new(SCR_ELEM_STR, 2); + sf_push_header_text(*record, "content-type", "application/json", 16); + return 3; + } + if (headers->kind == SCR_DYN_ARR || + (headers->kind == SCR_DYN_HANDLE && + headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS)) + return 2; + ScrDyn *materialized = NULL; + if (headers->kind == SCR_DYN_TYPED_REF) { + materialized = scr_dyn_typed_ref_materialize(headers); + if (!materialized) return 0; + headers = materialized; + } + int8_t form = 0; + if (headers->kind == SCR_DYN_OBJ) { + ScrArr *out = scr_arr_new(SCR_ELEM_STR, 2 * headers->v.obj.len); + form = 3; + for (size_t i = 0; i < headers->v.obj.len && form == 3; i++) { + const ScrDynEntry *e = &headers->v.obj.entries[i]; + const ScrDyn *value = e->value; + size_t count = value->kind == SCR_DYN_ARR ? value->v.arr.len : 1; + for (size_t j = 0; j < count; j++) { + const ScrDyn *item = value->kind == SCR_DYN_ARR ? value->v.arr.items[j] : value; + if (item->kind != SCR_DYN_STR && item->kind != SCR_DYN_NUM && + item->kind != SCR_DYN_BOOL) { + form = 0; + break; + } + ScrStr *text = scr_dyn_string_coerce_js(item); + if (!text) { + form = 0; + break; + } + scr_arr_push_ref(out, scr_str_new(e->key, e->key_len)); + scr_arr_push_ref(out, text); + } + } + if (form == 3) *record = out; + else scr_arr_release(out); + } + scr_dyn_release(materialized); + return form; +} + static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, const ScrDyn *json_data, SfJsonSerialize serialize) { @@ -4733,6 +4825,23 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, bool text_body = false; int status = 200; bool status_range_error = false; + /* The lightweight-response classification (see SfResponse.light): a + * Response init qualifies only while it is itself still light. */ + bool light_ok = !(init && init->kind == SCR_DYN_HANDLE && + init->v.handle.tag == SCR_DYNH_FETCH_RESPONSE && + ((SfResponse *)init->v.handle.ptr)->light == 0); + if (!json_body && !null_body) { + if (body->kind == SCR_DYN_BYTES) + light_ok = light_ok && body->v.bytes->elem == SCR_BYTES_U8 && + !body->v.bytes->is_data_view; + else if (!(body->kind == SCR_DYN_STR || + (body->kind == SCR_DYN_HANDLE && + body->v.handle.tag == SCR_DYNH_WEB_STREAM))) + light_ok = false; + } + int8_t light_form = 0; + ScrArr *light_record = NULL; + size_t light_length_bytes = 0; /* WebIDL converts constructor arguments from left to right. BodyInit's * string arm therefore runs before ResponseInit's dictionary conversion, @@ -4770,6 +4879,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, * coercions precede status and statusText. Header syntax validation is a * later response-initialization step, after body extraction. */ ScrDyn *init_headers = sf_response_init_get(init, "headers", 7); + if (light_ok) light_form = sf_light_header_form(init_headers, json_body, &light_record); converted_headers = sf_headers_convert_init(init_headers); scr_dyn_release(init_headers); if (!converted_headers) goto fail; @@ -4826,6 +4936,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, scr_str_release(encoding); scr_dyn_release(serialized); stream = sf_stream_new_native(); + light_length_bytes = body_bytes->len; sf_stream_enqueue_bytes(stream, body_bytes); sf_stream_close(stream); scr_bytes_release(body_bytes); @@ -4844,6 +4955,7 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, sf_stream_retain(stream); } else { stream = sf_stream_new_native(); + light_length_bytes = body_bytes->len; sf_stream_enqueue_bytes(stream, body_bytes); sf_stream_close(stream); scr_bytes_release(body_bytes); @@ -4877,9 +4989,11 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, sf_type_error(message); goto fail; } + bool auto_ct = false; if (text_body && !sf_pairs_have(header_pairs, "content-type")) { sf_push_header_text(header_pairs, "content-type", "text/plain;charset=UTF-8", 24); + auto_ct = true; } if (json_body && !sf_pairs_have(header_pairs, "content-type")) { static const char json_type[16] = "application/json"; @@ -4897,6 +5011,14 @@ static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, response->status = status; response->redirected = false; response->null_body = null_body; + response->light = light_form; + response->auto_ct = auto_ct; + response->light_record = light_record; + light_record = NULL; + response->light_length = + null_body ? -1.0 + : (!json_body && body->kind == SCR_DYN_HANDLE) ? -2.0 + : (double)light_length_bytes; ScrDyn *out = scr_dyn_new_handle(response, SCR_DYNH_FETCH_RESPONSE); sf_response_release(response); @@ -4908,9 +5030,43 @@ fail: scr_str_release(status_text); scr_arr_release(header_pairs); scr_arr_release(converted_headers); + scr_arr_release(light_record); return NULL; } +/* @hono/node-server's built-in implementation reads a Response's + * lightweight state to write it as the package does: [form, flags, + * byte length, ...raw record name/value pairs], with form as in + * SfResponse.light ("0" alone when the response is not light), flags "d" + * (the package's Headers hold its default content type) and "a" (the + * constructor added the text content type), and the length -1 for a null + * body or -2 for a stream. */ +ScrArr *scr_fetch_response_light(ScrDyn *response) { + ScrArr *out = scr_arr_new(SCR_ELEM_STR, 4); + if (!response || response->kind != SCR_DYN_HANDLE || + response->v.handle.tag != SCR_DYNH_FETCH_RESPONSE) { + scr_arr_push_ref(out, scr_str_new("0", 1)); + return out; + } + SfResponse *r = response->v.handle.ptr; + char buf[32]; + int n = snprintf(buf, sizeof buf, "%d", (int)r->light); + scr_arr_push_ref(out, scr_str_new(buf, (size_t)n)); + if (r->light == 0) return out; + char flags[3]; + size_t f = 0; + if (r->light_default_ct) flags[f++] = 'd'; + if (r->auto_ct) flags[f++] = 'a'; + scr_arr_push_ref(out, scr_str_new(flags, f)); + n = snprintf(buf, sizeof buf, "%.0f", r->light_length); + scr_arr_push_ref(out, scr_str_new(buf, (size_t)n)); + if (r->light == 3 && r->light_record) { + for (size_t i = 0; i < r->light_record->len; i++) + scr_arr_push_ref(out, scr_arr_get_ref(r->light_record, (double)i)); + } + return out; +} + ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { return sf_response_construct(body, init, NULL, NULL); } diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index c3dac861d..828f339d5 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -181,6 +181,7 @@ ScrDyn *scr_fetch_headers_new(ScrDyn *init); ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise */ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */ ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init); /* Response.json; borrowed args; +1 Response handle or NULL pending */ +ScrArr *scr_fetch_response_light(ScrDyn *response); /* @hono/node-server shim: borrowed; +1 string[] */ ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise */ diff --git a/tests/fixtures/npm/cases/hono-node-server/main.ts b/tests/fixtures/npm/cases/hono-node-server/main.ts new file mode 100644 index 000000000..ed732f64e --- /dev/null +++ b/tests/fixtures/npm/cases/hono-node-server/main.ts @@ -0,0 +1,238 @@ +// @hono/node-server 2.x (compiled from scriptc's built-in implementation) +// serving a fetch handler: a raw HTTP client prints each exchange's status +// line, wire-order headers (Date dropped) and body, covering buffered and +// streamed bodies, header forms, cookies, request bodies, URL handling, +// HEAD, empty statuses, and handler errors. +import * as http from "node:http"; +import type { AddressInfo } from "node:net"; +import { getRequestListener, RequestError, serve } from "@hono/node-server"; + +function delay(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function slowStream(parts: string[]): ReadableStream { + const encoder = new TextEncoder(); + let index = 0; + return new ReadableStream({ + async pull(controller) { + await delay(5); + if (index < parts.length) controller.enqueue(encoder.encode(parts[index++]!)); + else controller.close(); + }, + }); +} + +function readyStream(parts: string[]): ReadableStream { + const encoder = new TextEncoder(); + return new ReadableStream({ + start(controller) { + for (const part of parts) controller.enqueue(encoder.encode(part)); + controller.close(); + }, + }); +} + +async function handler(request: Request): Promise { + const url = new URL(request.url); + switch (url.pathname) { + case "/echo": { + const body = await request.text(); + const headers = new Headers({ "content-type": "application/json" }); + headers.set("x-echo", "yes"); + return new Response( + JSON.stringify({ + method: request.method, + url: request.url.replace(/:\d+\//, ":PORT/"), + type: request.headers.get("content-type"), + agent: request.headers.get("x-agent"), + body, + }), + { status: 201, headers }, + ); + } + case "/headers-object": { + const headers = new Headers(); + headers.set("x-b", "2"); + headers.set("X-A", "1"); + headers.set("content-type", "text/html; charset=utf-8"); + return new Response("

hi

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

Hello Hono!

') +}) + +serve(app) +``` + +#### Dynamic links + +```ts +app.use( + earlyHints({ + link: (c) => + c.req.query('theme') === 'dark' + ? '; rel=preload; as=style' + : '; rel=preload; as=style', + }) +) +``` + +> [!NOTE] +> Early Hints are sent only for requests that look like document navigations. If `Sec-Fetch-Mode` or `Sec-Fetch-Dest` is present with a value other than `navigate` or `document`, for example a `fetch()` or XHR call from a browser, a subresource request, or an iframe navigation, the middleware skips the hints and continues to the handler. Requests without these headers, such as `curl` or `fetch()` from a JavaScript runtime, are treated as navigations and do receive Early Hints. + +## Direct response from Node.js API + +You can directly respond to the client from the Node.js API. +In that case, the response from Hono should be ignored, so return `RESPONSE_ALREADY_SENT`. + +> [!NOTE] +> This feature can be used when migrating existing Node.js applications to Hono, but we recommend using Hono's API for new applications. + +```ts +import { serve } from '@hono/node-server' +import type { HttpBindings } from '@hono/node-server' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import { Hono } from 'hono' + +const app = new Hono<{ Bindings: HttpBindings }>() + +app.get('/', (c) => { + const { outgoing } = c.env + outgoing.writeHead(200, { 'Content-Type': 'text/plain' }) + outgoing.end('Hello World\n') + + return RESPONSE_ALREADY_SENT +}) + +serve(app) +``` + +## Listen to a UNIX domain socket + +You can configure the HTTP server to listen to a UNIX domain socket instead of a TCP port. + +```ts +import { createAdaptorServer } from '@hono/node-server' + +// ... + +const socketPath = '/tmp/example.sock' + +const server = createAdaptorServer(app) +server.listen(socketPath, () => { + console.log(`Listening on ${socketPath}`) +}) +``` + +## Related projects + +- Hono - +- Hono GitHub repository - + +## Authors + +- Yusuke Wada +- Taku Amano + +## License + +MIT diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs new file mode 100644 index 000000000..b41116486 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.cjs @@ -0,0 +1,22 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + +//#region src/conninfo.ts +/** +* ConnInfo Helper for Node.js +* @param c Context +* @returns ConnInfo +*/ +const getConnInfo = (c) => { + const bindings = c.env.server ? c.env.server : c.env; + const address = bindings.incoming.socket.remoteAddress; + const port = bindings.incoming.socket.remotePort; + const family = bindings.incoming.socket.remoteFamily; + return { remote: { + address, + port, + addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0 + } }; +}; + +//#endregion +exports.getConnInfo = getConnInfo; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts new file mode 100644 index 000000000..ed43a48f0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.cts @@ -0,0 +1,11 @@ +import { GetConnInfo } from "hono/conninfo"; + +//#region src/conninfo.d.ts +/** + * ConnInfo Helper for Node.js + * @param c Context + * @returns ConnInfo + */ +declare const getConnInfo: GetConnInfo; +//#endregion +export { getConnInfo }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts new file mode 100644 index 000000000..ed43a48f0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.d.mts @@ -0,0 +1,11 @@ +import { GetConnInfo } from "hono/conninfo"; + +//#region src/conninfo.d.ts +/** + * ConnInfo Helper for Node.js + * @param c Context + * @returns ConnInfo + */ +declare const getConnInfo: GetConnInfo; +//#endregion +export { getConnInfo }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs new file mode 100644 index 000000000..29ec46d0d --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/conninfo.mjs @@ -0,0 +1,20 @@ +//#region src/conninfo.ts +/** +* ConnInfo Helper for Node.js +* @param c Context +* @returns ConnInfo +*/ +const getConnInfo = (c) => { + const bindings = c.env.server ? c.env.server : c.env; + const address = bindings.incoming.socket.remoteAddress; + const port = bindings.incoming.socket.remotePort; + const family = bindings.incoming.socket.remoteFamily; + return { remote: { + address, + port, + addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0 + } }; +}; + +//#endregion +export { getConnInfo }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs new file mode 100644 index 000000000..db7c31e48 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs @@ -0,0 +1,5 @@ +//#region src/utils/response/constants.ts +const X_ALREADY_SENT = "x-hono-already-sent"; + +//#endregion +export { X_ALREADY_SENT as t }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs new file mode 100644 index 000000000..8888a5804 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs @@ -0,0 +1,11 @@ + +//#region src/utils/response/constants.ts +const X_ALREADY_SENT = "x-hono-already-sent"; + +//#endregion +Object.defineProperty(exports, 'X_ALREADY_SENT', { + enumerable: true, + get: function () { + return X_ALREADY_SENT; + } +}); \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs new file mode 100644 index 000000000..aa3ceba6a --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.cjs @@ -0,0 +1,35 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + +//#region src/early-hints.ts +/** +* Early Hints middleware for Node.js +* Automatically sends a 103 Early Hints informational response with the specified Link header(s). +* +* @param options EarlyHintsOptions +* @returns MiddlewareHandler +*/ +const earlyHints = (options) => { + let warned = false; + return async (c, next) => { + const mode = c.req.header("Sec-Fetch-Mode"); + const dest = c.req.header("Sec-Fetch-Dest"); + if (mode && mode !== "navigate" || dest && dest !== "document") return next(); + const env = c.env || {}; + const outgoing = ("server" in env ? env.server : env)?.outgoing; + if (typeof outgoing?.writeEarlyHints !== "function") { + if (!warned) { + console.warn("Early Hints Middleware is not supported because writeEarlyHints is not defined."); + warned = true; + } + return await next(); + } + if (!outgoing.headersSent) { + const link = typeof options.link === "function" ? options.link(c) : options.link; + if (link !== void 0 && (Array.isArray(link) ? link.length > 0 : Boolean(link))) outgoing.writeEarlyHints({ link }); + } + await next(); + }; +}; + +//#endregion +exports.earlyHints = earlyHints; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts new file mode 100644 index 000000000..9bd27d5e3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.cts @@ -0,0 +1,16 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/early-hints.d.ts +type EarlyHintsOptions = { + link: string | string[] | ((c: Context) => string | string[] | undefined); +}; +/** + * Early Hints middleware for Node.js + * Automatically sends a 103 Early Hints informational response with the specified Link header(s). + * + * @param options EarlyHintsOptions + * @returns MiddlewareHandler + */ +declare const earlyHints: (options: EarlyHintsOptions) => MiddlewareHandler; +//#endregion +export { EarlyHintsOptions, earlyHints }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts new file mode 100644 index 000000000..9bd27d5e3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.d.mts @@ -0,0 +1,16 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/early-hints.d.ts +type EarlyHintsOptions = { + link: string | string[] | ((c: Context) => string | string[] | undefined); +}; +/** + * Early Hints middleware for Node.js + * Automatically sends a 103 Early Hints informational response with the specified Link header(s). + * + * @param options EarlyHintsOptions + * @returns MiddlewareHandler + */ +declare const earlyHints: (options: EarlyHintsOptions) => MiddlewareHandler; +//#endregion +export { EarlyHintsOptions, earlyHints }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs new file mode 100644 index 000000000..7999a3d0c --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/early-hints.mjs @@ -0,0 +1,33 @@ +//#region src/early-hints.ts +/** +* Early Hints middleware for Node.js +* Automatically sends a 103 Early Hints informational response with the specified Link header(s). +* +* @param options EarlyHintsOptions +* @returns MiddlewareHandler +*/ +const earlyHints = (options) => { + let warned = false; + return async (c, next) => { + const mode = c.req.header("Sec-Fetch-Mode"); + const dest = c.req.header("Sec-Fetch-Dest"); + if (mode && mode !== "navigate" || dest && dest !== "document") return next(); + const env = c.env || {}; + const outgoing = ("server" in env ? env.server : env)?.outgoing; + if (typeof outgoing?.writeEarlyHints !== "function") { + if (!warned) { + console.warn("Early Hints Middleware is not supported because writeEarlyHints is not defined."); + warned = true; + } + return await next(); + } + if (!outgoing.headersSent) { + const link = typeof options.link === "function" ? options.link(c) : options.link; + if (link !== void 0 && (Array.isArray(link) ? link.length > 0 : Boolean(link))) outgoing.writeEarlyHints({ link }); + } + await next(); + }; +}; + +//#endregion +export { earlyHints }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs new file mode 100644 index 000000000..e578de054 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.cjs @@ -0,0 +1,1340 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +const require_constants = require('./constants-BXAKTxRC.cjs'); +let node_http = require("node:http"); +let node_http2 = require("node:http2"); +let node_stream = require("node:stream"); +let hono_ws = require("hono/ws"); + +//#region src/error.ts +var RequestError = class extends Error { + constructor(message, options) { + super(message, options); + this.name = "RequestError"; + } +}; + +//#endregion +//#region src/headers.ts +const nonJoinedHeaders = new Set([ + "age", + "authorization", + "content-length", + "content-type", + "etag", + "expires", + "from", + "host", + "if-modified-since", + "if-unmodified-since", + "last-modified", + "location", + "max-forwards", + "proxy-authorization", + "referer", + "retry-after", + "server", + "user-agent" +]); +const validHeaderName = /^[!#$%&'*+\-.^_`|~\dA-Za-z]+$/; +const isHttpWhitespace = (code) => code === 9 || code === 10 || code === 13 || code === 32; +const normalizeHeaderValue = (value) => { + if (!isHttpWhitespace(value.charCodeAt(0)) && !isHttpWhitespace(value.charCodeAt(value.length - 1))) return value; + let start = 0; + let end = value.length; + while (start < end && isHttpWhitespace(value.charCodeAt(start))) start++; + while (end > start && isHttpWhitespace(value.charCodeAt(end - 1))) end--; + return value.slice(start, end); +}; +const forbiddenHeaderValue = /[\0\r\n]/; +const GlobalHeaders = globalThis.Headers; +const materializeHeaders = (rawHeaders, HeadersCtor = GlobalHeaders) => { + const headers = new HeadersCtor(); + for (let i = 0; i < rawHeaders.length; i += 2) { + const name = rawHeaders[i]; + if (!name.startsWith(":")) headers.append(name, rawHeaders[i + 1]); + } + return headers; +}; +var RequestHeaders = class { + #incoming; + #rawHeaders; + #headers; + #invalidValue; + constructor(incoming) { + this.#incoming = incoming; + if (incoming instanceof node_http2.Http2ServerRequest) this.#rawHeaders = incoming.rawHeaders.slice(); + } + get #lazyRawHeaders() { + return this.#rawHeaders ??= this.#incoming.rawHeaders.slice(); + } + get #native() { + if (!this.#headers) { + this.#headers = materializeHeaders(this.#lazyRawHeaders); + this.#rawHeaders = void 0; + } + return this.#headers; + } + #normalizedName(name) { + if (typeof name !== "string") return; + if (!validHeaderName.test(name)) throw new TypeError(`Invalid header name: ${name}`); + return name.toLowerCase(); + } + #lookupHttp1(lowerName) { + const headers = this.#incoming instanceof node_http2.Http2ServerRequest ? void 0 : this.#incoming.headers; + if (!headers || nonJoinedHeaders.has(lowerName) || lowerName === "set-cookie" || lowerName === "__proto__") return; + if (!Object.hasOwn(headers, lowerName)) return null; + const rawValue = headers[lowerName]; + if (typeof rawValue === "string") { + const value = normalizeHeaderValue(rawValue); + return forbiddenHeaderValue.test(value) ? void 0 : value; + } + } + #lookup(rawHeaders, lowerName) { + const separator = lowerName === "cookie" ? "; " : ", "; + let value = null; + for (let i = 0; i < rawHeaders.length; i += 2) { + const rawName = rawHeaders[i]; + if (rawName.length === lowerName.length && rawName.toLowerCase() === lowerName) { + const rawValue = normalizeHeaderValue(rawHeaders[i + 1]); + if (forbiddenHeaderValue.test(rawValue)) { + this.#invalidValue = true; + return; + } + value = value === null ? rawValue : value + separator + rawValue; + } + } + return value; + } + append(name, value) { + this.#native.append(name, value); + } + delete(name) { + this.#native.delete(name); + } + get(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value; + } + return this.#native.get(name); + } + has(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value !== null; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value !== null; + } + return this.#native.has(name); + } + set(name, value) { + this.#native.set(name, value); + } + getSetCookie() { + return this.#native.getSetCookie(); + } + keys() { + return this.#native.keys(); + } + values() { + return this.#native.values(); + } + entries() { + return this.#native.entries(); + } + forEach(callback, thisArg) { + this.#native.forEach((value, key) => { + callback.call(thisArg, value, key, this); + }); + } + [Symbol.iterator]() { + return this.entries(); + } +}; +Object.defineProperty(RequestHeaders.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Headers (lightweight) ${inspectFn(Object.fromEntries(this), { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(RequestHeaders.prototype, GlobalHeaders.prototype); +const newHeadersFromIncoming = (incoming) => globalThis.Headers === GlobalHeaders ? new RequestHeaders(incoming) : materializeHeaders(incoming.rawHeaders, globalThis.Headers); + +//#endregion +//#region src/url.ts +const reValidRequestUrl = /^\/[!#$&-;=?-\[\]_a-z~]*$/; +const reDotSegment = /\/\.\.?(?:[/?#]|$)/; +const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/; +const buildUrl = (scheme, host, incomingUrl) => { + const url = `${scheme}://${host}${incomingUrl}`; + if (!reValidHost.test(host)) { + const urlObj = new URL(url); + if (urlObj.hostname.length !== host.length && urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase()) throw new RequestError("Invalid host header"); + return urlObj.href; + } else if (incomingUrl.length === 0) return url + "/"; + else { + if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL"); + if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl)) return new URL(url).href; + return url; + } +}; + +//#endregion +//#region src/request.ts +const toRequestError = (e) => { + if (e instanceof RequestError) return e; + return new RequestError(e.message, { cause: e }); +}; +const GlobalRequest = global.Request; +var Request$1 = class extends GlobalRequest { + constructor(input, options) { + if (typeof input === "object" && getRequestCache in input) { + const hasReplacementBody = options !== void 0 && "body" in options && options.body != null; + if (input[bodyConsumedDirectlyKey] && !hasReplacementBody) throw new TypeError("Cannot construct a Request with a Request object that has already been used."); + input = input[getRequestCache](); + } + if (typeof (options?.body)?.getReader !== "undefined") options.duplex ??= "half"; + super(input, options); + } +}; +const wrapBodyStream = Symbol("wrapBodyStream"); +const byteExactEncodings = new Set([ + "latin1", + "binary", + "hex", + "base64", + "base64url" +]); +const isByteExactEncoding = (encoding) => encoding === null || byteExactEncodings.has(encoding); +const bodyBufferedBeforeDisconnectKey = Symbol("bodyBufferedBeforeDisconnect"); +const bodyBufferedLengthBeforeDisconnectKey = Symbol("bodyBufferedLengthBeforeDisconnect"); +const toBufferChunk = (chunk, encoding) => Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding ?? "utf8"); +const isRecoverableDisconnectedIncoming = (incoming) => !(incoming instanceof node_http2.Http2ServerRequest) && !!incoming.complete && !!incoming.readableAborted && typeof incoming.read === "function" && isByteExactEncoding(incoming.readableEncoding); +const recordBodyBufferedBeforeDisconnect = (incoming) => { + if (incoming.readableDidRead || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] ??= incoming.readableLength; +}; +const readBodyBufferedBeforeDisconnect = (incoming, chunks) => { + if (incoming.readableDidRead && !chunks || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + if (incomingWithRecovery[bodyBufferedBeforeDisconnectKey] !== void 0) return incomingWithRecovery[bodyBufferedBeforeDisconnectKey]; + let result; + const errored = incoming.errored; + if (errored && errored.code !== "ECONNRESET") result = errored; + else if (incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== void 0 && incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]) result = newBodyUnusableError(); + else { + const bodyChunks = chunks ?? []; + const chunk = incoming.read(); + if (chunk !== null) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks); + result = buffer; + const contentLength = incoming.headers["content-length"]; + if (typeof contentLength === "string" && /^\d+$/.test(contentLength)) { + const expectedLength = Number(contentLength); + if (Number.isSafeInteger(expectedLength) && buffer.length !== expectedLength) result = newBodyUnusableError(); + } + } + incomingWithRecovery[bodyBufferedBeforeDisconnectKey] = result; + return result; +}; +const enqueueBufferedBody = (controller, buffered) => { + if (buffered instanceof Error) { + controller.error(buffered); + return; + } + if (buffered.length > 0) controller.enqueue(buffered); + controller.close(); +}; +const newRequestFromIncoming = (method, url, headers, incoming, abortController) => { + const init = { + method, + headers, + signal: abortController.signal + }; + if (method === "TRACE") { + init.method = "GET"; + const req = new Request$1(url, init); + Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return req; + } + if (!(method === "GET" || method === "HEAD")) if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) init.body = new ReadableStream({ start(controller) { + controller.enqueue(incoming.rawBody); + controller.close(); + } }); + else if (incoming[wrapBodyStream]) { + let reader; + init.body = new ReadableStream({ async pull(controller) { + try { + if (!reader) { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + enqueueBufferedBody(controller, buffered); + return; + } + } + reader ||= node_stream.Readable.toWeb(incoming).getReader(); + const { done, value } = await reader.read(); + if (done) controller.close(); + else controller.enqueue(value); + } catch (error) { + controller.error(error); + } + } }); + } else { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) init.body = new ReadableStream({ start(controller) { + enqueueBufferedBody(controller, buffered); + } }); + else init.body = node_stream.Readable.toWeb(incoming); + } + return new Request$1(url, init); +}; +const getRequestCache = Symbol("getRequestCache"); +const requestCache = Symbol("requestCache"); +const incomingKey = Symbol("incomingKey"); +const urlKey = Symbol("urlKey"); +const methodKey = Symbol("methodKey"); +const headersKey = Symbol("headersKey"); +const abortControllerKey = Symbol("abortControllerKey"); +const getAbortController = Symbol("getAbortController"); +const abortRequest = Symbol("abortRequest"); +const bodyBufferKey = Symbol("bodyBuffer"); +const bodyReadPromiseKey = Symbol("bodyReadPromise"); +const bodyConsumedDirectlyKey = Symbol("bodyConsumedDirectly"); +const bodyLockReaderKey = Symbol("bodyLockReader"); +const abortReasonKey = Symbol("abortReason"); +const newBodyUnusableError = () => { + return /* @__PURE__ */ new TypeError("Body is unusable"); +}; +const rejectBodyUnusable = () => { + return Promise.reject(newBodyUnusableError()); +}; +const textDecoder = new TextDecoder(); +const consumeBodyDirectOnce = (request) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + request[bodyConsumedDirectlyKey] = true; +}; +const toArrayBuffer = (buf) => { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +}; +const contentType = (request) => { + return (request[headersKey] ||= newHeadersFromIncoming(request[incomingKey])).get("content-type") || ""; +}; +const methodTokenRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; +const normalizeIncomingMethod = (method) => { + if (typeof method !== "string" || method.length === 0) return "GET"; + switch (method) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "PATCH": + case "POST": + case "PUT": + case "QUERY": return method; + } + const upper = method.toUpperCase(); + switch (upper) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "POST": + case "PUT": return upper; + default: return method; + } +}; +const validateDirectReadMethod = (method) => { + if (!methodTokenRegExp.test(method)) return /* @__PURE__ */ new TypeError(`'${method}' is not a valid HTTP method.`); + const normalized = method.toUpperCase(); + if (normalized === "CONNECT" || normalized === "TRACK" || normalized === "TRACE" && method !== "TRACE") return /* @__PURE__ */ new TypeError(`'${method}' HTTP method is unsupported.`); +}; +const readBodyWithFastPath = (request, method, fromBuffer) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + const methodName = request.method; + if (methodName === "GET" || methodName === "HEAD") return request[getRequestCache]()[method](); + const methodValidationError = validateDirectReadMethod(methodName); + if (methodValidationError) return Promise.reject(methodValidationError); + if (request[requestCache]) { + if (methodName !== "TRACE") return request[requestCache][method](); + } + const alreadyUsedError = consumeBodyDirectOnce(request); + if (alreadyUsedError) return alreadyUsedError; + const raw = readRawBodyIfAvailable(request); + if (raw) { + const result = Promise.resolve(fromBuffer(raw, request)); + request[bodyBufferKey] = void 0; + return result; + } + return readBodyDirect(request).then((buf) => { + const result = fromBuffer(buf, request); + request[bodyBufferKey] = void 0; + return result; + }); +}; +const readRawBodyIfAvailable = (request) => { + const incoming = request[incomingKey]; + if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) return incoming.rawBody; +}; +const normalizeAbortError = (request, incoming) => { + if (incoming.errored) return incoming.errored; + const reason = request[abortReasonKey]; + if (reason !== void 0) return reason instanceof Error ? reason : new Error(String(reason)); + return /* @__PURE__ */ new Error("Client connection prematurely closed."); +}; +const readBodyDirect = (request) => { + if (request[bodyBufferKey]) return Promise.resolve(request[bodyBufferKey]); + if (request[bodyReadPromiseKey]) return request[bodyReadPromiseKey]; + const incoming = request[incomingKey]; + if (incoming.readableDidRead) return rejectBodyUnusable(); + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + if (buffered instanceof Error) return Promise.reject(buffered); + request[bodyBufferKey] = buffered; + return Promise.resolve(buffered); + } + const promise = new Promise((resolve, reject) => { + const chunks = []; + let settled = false; + const finish = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback(); + }; + const recoverCompleteBodyAfterDisconnect = (error) => { + const streamError = incoming.errored ?? error; + if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false; + finish(() => { + const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks); + if (recovered instanceof Error) reject(recovered); + else if (recovered === void 0) reject(error ?? normalizeAbortError(request, incoming)); + else { + request[bodyBufferKey] = recovered; + resolve(recovered); + } + }); + return true; + }; + const onData = (chunk) => { + chunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + }; + const onEnd = () => { + finish(() => { + const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks); + request[bodyBufferKey] = buffer; + resolve(buffer); + }); + }; + const onError = (error) => { + if (recoverCompleteBodyAfterDisconnect(error)) return; + finish(() => { + reject(error); + }); + }; + const onClose = () => { + if (incoming.readableEnded) { + onEnd(); + return; + } + if (recoverCompleteBodyAfterDisconnect()) return; + finish(() => { + reject(normalizeAbortError(request, incoming)); + }); + }; + const cleanup = () => { + incoming.off("data", onData); + incoming.off("end", onEnd); + incoming.off("error", onError); + incoming.off("close", onClose); + request[bodyReadPromiseKey] = void 0; + }; + incoming.on("data", onData); + incoming.on("end", onEnd); + incoming.on("error", onError); + incoming.on("close", onClose); + queueMicrotask(() => { + if (settled) return; + if (incoming.readableEnded) onEnd(); + else if (incoming.errored) onError(incoming.errored); + else if (incoming.destroyed) onClose(); + }); + }); + request[bodyReadPromiseKey] = promise; + return promise; +}; +const requestPrototype = { + get method() { + return this[methodKey]; + }, + get url() { + return this[urlKey]; + }, + get headers() { + return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]); + }, + [abortRequest](reason) { + if (this[abortReasonKey] === void 0) this[abortReasonKey] = reason; + const abortController = this[abortControllerKey]; + if (abortController && !abortController.signal.aborted) abortController.abort(reason); + }, + [getAbortController]() { + this[abortControllerKey] ||= new AbortController(); + if (this[abortReasonKey] !== void 0 && !this[abortControllerKey].signal.aborted) this[abortControllerKey].abort(this[abortReasonKey]); + return this[abortControllerKey]; + }, + [getRequestCache]() { + const abortController = this[getAbortController](); + if (this[requestCache]) return this[requestCache]; + const method = this.method; + if (this[bodyConsumedDirectlyKey] && !(method === "GET" || method === "HEAD")) { + this[bodyBufferKey] = void 0; + const init = { + method: method === "TRACE" ? "GET" : method, + headers: this.headers, + signal: abortController.signal + }; + if (method !== "TRACE") { + init.body = new ReadableStream({ start(c) { + c.close(); + } }); + init.duplex = "half"; + } + const req = new Request$1(this[urlKey], init); + if (method === "TRACE") Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return this[requestCache] = req; + } + return this[requestCache] = newRequestFromIncoming(this.method, this[urlKey], this.headers, this[incomingKey], abortController); + }, + get body() { + if (!this[bodyConsumedDirectlyKey]) return this[getRequestCache]().body; + const request = this[getRequestCache](); + if (!this[bodyLockReaderKey] && request.body) this[bodyLockReaderKey] = request.body.getReader(); + return request.body; + }, + get bodyUsed() { + if (this[bodyConsumedDirectlyKey]) return true; + if (this[requestCache]) return this[requestCache].bodyUsed; + return false; + } +}; +Object.defineProperty(requestPrototype, "signal", { get() { + return this[getAbortController]().signal; +} }); +[ + "cache", + "credentials", + "destination", + "integrity", + "mode", + "redirect", + "referrer", + "referrerPolicy", + "keepalive" +].forEach((k) => { + Object.defineProperty(requestPrototype, k, { get() { + return this[getRequestCache]()[k]; + } }); +}); +["clone", "formData"].forEach((k) => { + Object.defineProperty(requestPrototype, k, { value: function() { + if (this[bodyConsumedDirectlyKey]) { + if (k === "clone") throw newBodyUnusableError(); + return rejectBodyUnusable(); + } + return this[getRequestCache]()[k](); + } }); +}); +Object.defineProperty(requestPrototype, "text", { value: function() { + return readBodyWithFastPath(this, "text", (buf) => textDecoder.decode(buf)); +} }); +Object.defineProperty(requestPrototype, "arrayBuffer", { value: function() { + return readBodyWithFastPath(this, "arrayBuffer", (buf) => toArrayBuffer(buf)); +} }); +Object.defineProperty(requestPrototype, "blob", { value: function() { + return readBodyWithFastPath(this, "blob", (buf, request) => { + const type = contentType(request); + const init = type ? { headers: { "content-type": type } } : void 0; + return new Response(buf, init).blob(); + }); +} }); +Object.defineProperty(requestPrototype, "json", { value: function() { + if (this[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + return this.text().then(JSON.parse); +} }); +Object.defineProperty(requestPrototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Request (lightweight) ${inspectFn({ + method: this.method, + url: this.url, + headers: this.headers, + nativeRequest: this[requestCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(requestPrototype, Request$1.prototype); +const newRequest = (incoming, defaultHostname) => { + const req = Object.create(requestPrototype); + req[incomingKey] = incoming; + req[methodKey] = normalizeIncomingMethod(incoming.method); + const incomingUrl = incoming.url || ""; + if (incomingUrl[0] !== "/" && (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))) { + if (incoming instanceof node_http2.Http2ServerRequest) throw new RequestError("Absolute URL for :path is not allowed in HTTP/2"); + try { + req[urlKey] = new URL(incomingUrl).href; + } catch (e) { + throw new RequestError("Invalid absolute URL", { cause: e }); + } + return req; + } + const host = (incoming instanceof node_http2.Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname; + if (!host) throw new RequestError("Missing host header"); + let scheme; + if (incoming instanceof node_http2.Http2ServerRequest) { + scheme = incoming.scheme; + if (!(scheme === "http" || scheme === "https")) throw new RequestError("Unsupported scheme"); + } else scheme = incoming.socket && incoming.socket.encrypted ? "https" : "http"; + try { + req[urlKey] = buildUrl(scheme, host, incomingUrl); + } catch (e) { + if (e instanceof RequestError) throw e; + else throw new RequestError("Invalid URL", { cause: e }); + } + return req; +}; + +//#endregion +//#region src/response.ts +const defaultContentType = "text/plain; charset=UTF-8"; +const responseCache = Symbol("responseCache"); +const getResponseCache = Symbol("getResponseCache"); +const cacheKey = Symbol("cache"); +const GlobalResponse = global.Response; +var Response$1 = class Response$1 { + #body; + #init; + [getResponseCache]() { + const cache = this[cacheKey]; + const liveHeaders = cache && cache[2] instanceof Headers ? cache[2] : void 0; + delete this[cacheKey]; + return this[responseCache] ||= new GlobalResponse(this.#body, liveHeaders ? { + status: this.#init?.status, + statusText: this.#init?.statusText, + headers: liveHeaders + } : this.#init); + } + constructor(body, init) { + let headers; + this.#body = body; + if (init instanceof GlobalResponse) { + const cachedGlobalResponse = init[responseCache]; + if (cachedGlobalResponse) { + this.#init = cachedGlobalResponse; + this[getResponseCache](); + return; + } + this.#init = init instanceof Response$1 ? init.#init : init; + headers = new Headers(init.headers); + } else this.#init = init; + if (body == null || typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) this[cacheKey] = [ + init?.status || 200, + body ?? null, + headers || init?.headers + ]; + } + get headers() { + const cache = this[cacheKey]; + if (cache) { + if (!(cache[2] instanceof Headers)) cache[2] = new Headers(cache[2] || (cache[1] === null ? void 0 : { "content-type": defaultContentType })); + return cache[2]; + } + return this[getResponseCache]().headers; + } + get status() { + return this[cacheKey]?.[0] ?? this[getResponseCache]().status; + } + get ok() { + const status = this.status; + return status >= 200 && status < 300; + } +}; +[ + "body", + "bodyUsed", + "redirected", + "statusText", + "trailers", + "type", + "url" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { get() { + return this[getResponseCache]()[k]; + } }); +}); +[ + "arrayBuffer", + "blob", + "clone", + "formData", + "json", + "text" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { value: function() { + return this[getResponseCache]()[k](); + } }); +}); +Object.defineProperty(Response$1.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Response (lightweight) ${inspectFn({ + status: this.status, + headers: this.headers, + ok: this.ok, + nativeResponse: this[responseCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(Response$1, GlobalResponse); +Object.setPrototypeOf(Response$1.prototype, GlobalResponse.prototype); +const validRedirectUrl = /^https?:\/\/[!#-;=?-[\]_a-z~A-Z]+$/; +const parseRedirectUrl = (url) => { + if (url instanceof URL) return url.href; + if (validRedirectUrl.test(url)) return url; + return new URL(url).href; +}; +const validRedirectStatuses = new Set([ + 301, + 302, + 303, + 307, + 308 +]); +Object.defineProperty(Response$1, "redirect", { + value: function redirect(url, status = 302) { + if (!validRedirectStatuses.has(status)) throw new RangeError("Invalid status code"); + return new Response$1(null, { + status, + headers: { location: parseRedirectUrl(url) } + }); + }, + writable: true, + configurable: true +}); +Object.defineProperty(Response$1, "json", { + value: function json(data, init) { + const body = JSON.stringify(data); + if (body === void 0) throw new TypeError("The data is not JSON serializable"); + const initHeaders = init?.headers; + let headers; + if (initHeaders) { + headers = new Headers(initHeaders); + if (!headers.has("content-type")) headers.set("content-type", "application/json"); + } else headers = { "content-type": "application/json" }; + return new Response$1(body, { + status: init?.status ?? 200, + statusText: init?.statusText, + headers + }); + }, + writable: true, + configurable: true +}); + +//#endregion +//#region src/utils.ts +async function readWithoutBlocking(readPromise) { + return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]); +} +function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) { + const cancel = (error) => { + reader.cancel(error).catch(() => {}); + }; + writable.on("close", cancel); + writable.on("error", cancel); + (currentReadPromise ?? reader.read()).then(flow, handleStreamError); + return reader.closed.finally(() => { + writable.off("close", cancel); + writable.off("error", cancel); + }); + function handleStreamError(error) { + if (error) writable.destroy(error); + } + function onDrain() { + reader.read().then(flow, handleStreamError); + } + function flow({ done, value }) { + try { + if (done) writable.end(); + else if (!writable.write(value)) writable.once("drain", onDrain); + else return reader.read().then(flow, handleStreamError); + } catch (e) { + handleStreamError(e); + } + } +} +function writeFromReadableStream(stream, writable) { + if (stream.locked) throw new TypeError("ReadableStream is locked."); + else if (writable.destroyed) return; + return writeFromReadableStreamDefaultReader(stream.getReader(), writable); +} +const buildOutgoingHttpHeaders = (headers, defaultContentType) => { + const res = {}; + if (!(headers instanceof Headers)) headers = new Headers(headers ?? void 0); + if (headers.has("set-cookie")) { + const cookies = []; + for (const [k, v] of headers) if (k === "set-cookie") cookies.push(v); + else res[k] = v; + if (cookies.length > 0) res["set-cookie"] = cookies; + } else for (const [k, v] of headers) res[k] = v; + if (defaultContentType) res["content-type"] ??= defaultContentType; + return res; +}; + +//#endregion +//#region src/listener.ts +const outgoingEnded = Symbol("outgoingEnded"); +const incomingDraining = Symbol("incomingDraining"); +const DRAIN_TIMEOUT_MS = 500; +const MAX_DRAIN_BYTES = 64 * 1024 * 1024; +const drainIncoming = (incoming) => { + const incomingWithDrainState = incoming; + if (incoming.destroyed || incomingWithDrainState[incomingDraining]) return; + incomingWithDrainState[incomingDraining] = true; + if (incoming instanceof node_http2.Http2ServerRequest) { + try { + incoming.stream?.close?.(node_http2.constants.NGHTTP2_NO_ERROR); + } catch {} + return; + } + let bytesRead = 0; + const cleanup = () => { + clearTimeout(timer); + incoming.off("data", onData); + incoming.off("end", cleanup); + incoming.off("error", cleanup); + }; + const forceClose = () => { + cleanup(); + const socket = incoming.socket; + if (socket && !socket.destroyed) { + if (typeof socket.destroySoon === "function") socket.destroySoon(); + else if (typeof socket.destroy === "function") socket.destroy(); + } + }; + const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS); + timer.unref?.(); + const onData = (chunk) => { + bytesRead += chunk.length; + if (bytesRead > MAX_DRAIN_BYTES) forceClose(); + }; + incoming.on("data", onData); + incoming.on("end", cleanup); + incoming.on("error", cleanup); + incoming.resume(); +}; +const makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => { + if (incoming.errored) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest](incoming.errored.toString()); + } else if (!outgoing.writableFinished || incoming instanceof node_http2.Http2ServerRequest && incoming.aborted) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest]("Client connection prematurely closed."); + } + if (needsBodyCleanup && !incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + drainIncoming(incoming); + }); + }); +}; +const isImmediateCacheableResponse = (res) => { + if (!(cacheKey in res)) return false; + const body = res[cacheKey][1]; + return body === null || typeof body === "string" || body instanceof Uint8Array; +}; +const handleRequestError = () => new Response(null, { status: 400 }); +const handleFetchError = (e) => new Response(null, { status: e instanceof Error && (e.name === "TimeoutError" || e.constructor.name === "TimeoutError") ? 504 : 500 }); +const handleResponseError = (e, outgoing) => { + const err = e instanceof Error ? e : new Error("unknown error", { cause: e }); + if (err.code === "ERR_STREAM_PREMATURE_CLOSE") console.info("The user aborted a request."); + else { + console.error(e); + if (!outgoing.headersSent) { + if (outgoing instanceof node_http.ServerResponse) outgoing._contentLength = null; + outgoing.writeHead(500, { "Content-Type": "text/plain" }); + } + outgoing.end(`Error: ${err.message}`); + outgoing.destroy(err); + } +}; +const flushHeaders = (outgoing) => { + if ("flushHeaders" in outgoing && outgoing.writable) outgoing.flushHeaders(); +}; +const trySetContentLength = (outgoing, status, length) => { + const http1 = outgoing; + if (http1._contentLength === null && http1._hasBody && http1.useChunkedEncodingByDefault && !http1._removedContLen && status >= 200 && status !== 204 && status !== 304 && !outgoing.hasHeader("content-length") && !outgoing.hasHeader("transfer-encoding") && !outgoing.hasHeader("trailer")) { + http1._contentLength = length; + return true; + } + return false; +}; +const writeDefaultHeaders = (outgoing, status, length) => { + if (trySetContentLength(outgoing, status, length)) outgoing.writeHead(status, { "Content-Type": defaultContentType }); + else outgoing.writeHead(status, { + "Content-Type": defaultContentType, + "Content-Length": length + }); +}; +const responseViaCache = async (res, outgoing) => { + let [status, body, header] = res[cacheKey]; + if (!header) { + if (body === null) { + outgoing.writeHead(status); + outgoing.end(); + } else if (typeof body === "string") { + writeDefaultHeaders(outgoing, status, Buffer.byteLength(body)); + outgoing.end(body); + } else if (body instanceof Uint8Array) { + writeDefaultHeaders(outgoing, status, body.byteLength); + outgoing.end(body); + } else if (body instanceof Blob) { + writeDefaultHeaders(outgoing, status, body.size); + outgoing.end(new Uint8Array(await body.arrayBuffer())); + } else { + outgoing.writeHead(status, { "Content-Type": defaultContentType }); + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); + return; + } + let hasContentLength = false; + let plainHeaders = false; + let canAutoLength = true; + if (header instanceof Headers) { + hasContentLength = header.has("content-length"); + header = buildOutgoingHttpHeaders(header, body === null ? void 0 : defaultContentType); + } else if (Array.isArray(header)) { + const headerObj = new Headers(header); + hasContentLength = headerObj.has("content-length"); + header = buildOutgoingHttpHeaders(headerObj, body === null ? void 0 : defaultContentType); + } else { + plainHeaders = true; + for (const key in header) { + if (key.length === 14 && key.toLowerCase() === "content-length") { + hasContentLength = true; + break; + } + if (key.length === 17 && key.toLowerCase() === "transfer-encoding" || key.length === 7 && key.toLowerCase() === "trailer") canAutoLength = false; + } + } + if (!hasContentLength) { + let length; + if (typeof body === "string") length = Buffer.byteLength(body); + else if (body instanceof Uint8Array) length = body.byteLength; + else if (body instanceof Blob) length = body.size; + if (length !== void 0 && (!plainHeaders || !canAutoLength || !trySetContentLength(outgoing, status, length))) { + if (plainHeaders) header = { ...header }; + header["Content-Length"] = length; + } + } + outgoing.writeHead(status, header); + if (body == null) outgoing.end(); + else if (typeof body === "string" || body instanceof Uint8Array) outgoing.end(body); + else if (body instanceof Blob) outgoing.end(new Uint8Array(await body.arrayBuffer())); + else { + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); +}; +const isPromise = (res) => typeof res.then === "function"; +const responseViaResponseObject = async (res, outgoing, options = {}) => { + if (isPromise(res)) if (options.errorHandler) try { + res = await res; + } catch (err) { + const errRes = await options.errorHandler(err); + if (!errRes) return; + res = errRes; + } + else res = await res.catch(handleFetchError); + if (cacheKey in res) return responseViaCache(res, outgoing); + const resHeaderRecord = buildOutgoingHttpHeaders(res.headers, res.body === null ? void 0 : defaultContentType); + if (res.body) { + const reader = res.body.getReader(); + const values = []; + let done = false; + let currentReadPromise = void 0; + if (resHeaderRecord["transfer-encoding"] !== "chunked") { + let maxReadCount = 2; + for (let i = 0; i < maxReadCount; i++) { + currentReadPromise ||= reader.read(); + const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => { + console.error(e); + done = true; + }); + if (!chunk) { + if (i === 1) { + await new Promise((resolve) => setTimeout(resolve)); + maxReadCount = 3; + continue; + } + break; + } + currentReadPromise = void 0; + if (chunk.value) values.push(chunk.value); + if (chunk.done) { + done = true; + break; + } + } + if (done && !("content-length" in resHeaderRecord)) resHeaderRecord["content-length"] = values.reduce((acc, value) => acc + value.length, 0); + } + outgoing.writeHead(res.status, resHeaderRecord); + values.forEach((value) => { + outgoing.write(value); + }); + if (done) outgoing.end(); + else { + if (values.length === 0) flushHeaders(outgoing); + await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise); + } + } else if (resHeaderRecord[require_constants.X_ALREADY_SENT]) {} else { + outgoing.writeHead(res.status, resHeaderRecord); + outgoing.end(); + } + outgoing[outgoingEnded]?.(); +}; +const getRequestListener = (fetchCallback, options = {}) => { + const autoCleanupIncoming = options.autoCleanupIncoming ?? true; + if (options.overrideGlobalObjects !== false && global.Request !== Request$1) { + Object.defineProperty(global, "Request", { value: Request$1 }); + Object.defineProperty(global, "Response", { value: Response$1 }); + } + return async (incoming, outgoing) => { + let res, req; + let needsBodyCleanup = false; + let closeHandlerAttached = false; + const ensureCloseHandler = () => { + if (!req || closeHandlerAttached) return; + closeHandlerAttached = true; + outgoing.on("close", makeCloseHandler(req, incoming, outgoing, needsBodyCleanup)); + }; + try { + req = newRequest(incoming, options.hostname); + needsBodyCleanup = autoCleanupIncoming && !(incoming.method === "GET" || incoming.method === "HEAD"); + if (needsBodyCleanup) { + incoming[wrapBodyStream] = true; + if (incoming instanceof node_http2.Http2ServerRequest) outgoing[outgoingEnded] = () => { + if (!incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + incoming.destroy(); + outgoing.destroy(); + }); + }); + }; + } + res = fetchCallback(req, { + incoming, + outgoing + }); + if (!isPromise(res) && isImmediateCacheableResponse(res)) { + if (needsBodyCleanup && !incoming.readableEnded) outgoing.once("finish", () => { + if (!incoming.readableEnded) drainIncoming(incoming); + }); + return responseViaCache(res, outgoing); + } + ensureCloseHandler(); + } catch (e) { + if (!res) if (options.errorHandler) { + ensureCloseHandler(); + res = await options.errorHandler(req ? e : toRequestError(e)); + if (!res) return; + } else if (!req) res = handleRequestError(); + else res = handleFetchError(e); + else return handleResponseError(e, outgoing); + } + try { + return await responseViaResponseObject(res, outgoing, options); + } catch (e) { + return handleResponseError(e, outgoing); + } + }; +}; + +//#endregion +//#region src/websocket.ts +/** +* @link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent +*/ +const CloseEvent = globalThis.CloseEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get wasClean() { + return this.#eventInitDict.wasClean ?? false; + } + get code() { + return this.#eventInitDict.code ?? 0; + } + get reason() { + return this.#eventInitDict.reason ?? ""; + } +}; +/** +* Node.js has no global `ErrorEvent`, unlike `Event`/`MessageEvent`/`CloseEvent`. +* @link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent +*/ +const ErrorEvent = globalThis.ErrorEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get message() { + return this.#eventInitDict.message ?? ""; + } + get filename() { + return this.#eventInitDict.filename ?? ""; + } + get lineno() { + return this.#eventInitDict.lineno ?? 0; + } + get colno() { + return this.#eventInitDict.colno ?? 0; + } + get error() { + return this.#eventInitDict.error ?? null; + } +}; +const generateConnectionSymbol = () => Symbol("connection"); +const CONNECTION_SYMBOL_KEY = Symbol("CONNECTION_SYMBOL_KEY"); +const WAIT_FOR_WEBSOCKET_SYMBOL = Symbol("WAIT_FOR_WEBSOCKET_SYMBOL"); +const responseHeadersToSkip = new Set([ + "connection", + "content-length", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "sec-websocket-accept", + "sec-websocket-extensions", + "sec-websocket-protocol" +]); +const appendResponseHeaders = (headers, responseHeaders) => { + if (!responseHeaders) return; + responseHeaders.forEach((value, key) => { + if (responseHeadersToSkip.has(key.toLowerCase())) return; + headers.push(`${key}: ${value}`); + }); +}; +const rejectUpgradeRequest = (socket, status, responseHeaders) => { + const responseLines = ["Connection: close", "Content-Length: 0"]; + appendResponseHeaders(responseLines, responseHeaders); + socket.end(`HTTP/1.1 ${status.toString()} ${node_http.STATUS_CODES[status] ?? ""}\r\n${responseLines.join("\r\n")}\r\n\r +`); +}; +const createUpgradeRequest = (request) => { + const protocol = request.socket.encrypted ? "https" : "http"; + const url = new URL(request.url ?? "/", `${protocol}://${request.headers.host ?? "localhost"}`); + const headers = new Headers(); + for (const key in request.headers) { + const value = request.headers[key]; + if (!value) continue; + headers.append(key, Array.isArray(value) ? value[0] : value); + } + return new Request(url, { headers }); +}; +const setupWebSocket = (options) => { + const { server, fetchCallback, wss } = options; + const waiterMap = /* @__PURE__ */ new Map(); + wss.on("connection", (ws, request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiter.resolve(ws); + waiterMap.delete(request); + } + }); + const rejectWaiter = (request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiterMap.delete(request); + waiter.reject(/* @__PURE__ */ new Error("WebSocket handshake aborted")); + } + }; + const waitForWebSocket = (request, connectionSymbol) => { + return new Promise((resolve, reject) => { + waiterMap.set(request, { + resolve, + reject, + connectionSymbol + }); + }); + }; + server.on("upgrade", async (request, socket, head) => { + if (request.headers.upgrade?.toLowerCase() !== "websocket") return; + const env = { + incoming: request, + outgoing: void 0, + wss, + [WAIT_FOR_WEBSOCKET_SYMBOL]: waitForWebSocket + }; + let status = 400; + let responseHeaders; + try { + const response = await fetchCallback(createUpgradeRequest(request), env); + if (response instanceof Response) { + status = response.status; + responseHeaders = response.headers; + } + } catch { + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, 500); + return; + } + const waiter = waiterMap.get(request); + if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { + rejectWaiter(request); + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, status, responseHeaders); + return; + } + const addResponseHeaders = (headers) => { + appendResponseHeaders(headers, responseHeaders); + }; + const reclaimWaiterOnClose = () => rejectWaiter(request); + socket.once("close", reclaimWaiterOnClose); + wss.on("headers", addResponseHeaders); + try { + wss.handleUpgrade(request, socket, head, (ws) => { + socket.off("close", reclaimWaiterOnClose); + wss.emit("connection", ws, request); + }); + } finally { + wss.off("headers", addResponseHeaders); + } + }); + server.on("close", () => { + wss.close(); + }); +}; +const upgradeWebSocket = (0, hono_ws.defineWebSocketHelper)(async (c, events, options) => { + if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return; + const env = c.env; + const waitForWebSocket = env[WAIT_FOR_WEBSOCKET_SYMBOL]; + if (!waitForWebSocket || !env.incoming) return new Response(null, { status: 500 }); + const connectionSymbol = generateConnectionSymbol(); + env[CONNECTION_SYMBOL_KEY] = connectionSymbol; + (async () => { + let ws; + try { + ws = await waitForWebSocket(env.incoming, connectionSymbol); + } catch { + return; + } + const messagesReceivedInStarting = []; + const bufferMessage = (data, isBinary) => { + messagesReceivedInStarting.push([data, isBinary]); + }; + ws.on("message", bufferMessage); + const ctx = { + binaryType: "arraybuffer", + close(code, reason) { + ws.close(code, reason); + }, + protocol: ws.protocol, + raw: ws, + get readyState() { + return ws.readyState; + }, + send(source, opts) { + ws.send(source, { compress: opts?.compress }); + }, + url: new URL(c.req.url) + }; + try { + events?.onOpen?.(new Event("open"), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + const handleMessage = (data, isBinary) => { + const datas = Array.isArray(data) ? data : [data]; + for (const data of datas) try { + events?.onMessage?.(new MessageEvent("message", { data: isBinary ? data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : typeof data === "string" ? data : Buffer.from(data).toString("utf-8") }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }; + ws.off("message", bufferMessage); + for (const message of messagesReceivedInStarting) handleMessage(...message); + ws.on("message", (data, isBinary) => { + handleMessage(data, isBinary); + }); + ws.on("close", (code, reason) => { + try { + events?.onClose?.(new CloseEvent("close", { + code, + reason: reason.toString() + }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + ws.on("error", (error) => { + try { + events?.onError?.(new ErrorEvent("error", { error }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + })(); + return new Response(); +}); + +//#endregion +//#region src/server.ts +const createAdaptorServer = (options) => { + const fetchCallback = options.fetch; + const requestListener = getRequestListener(fetchCallback, { + hostname: options.hostname, + overrideGlobalObjects: options.overrideGlobalObjects, + autoCleanupIncoming: options.autoCleanupIncoming + }); + const server = (options.createServer || node_http.createServer)(options.serverOptions || {}, requestListener); + if (options.websocket && options.websocket.server) { + if (options.websocket.server.options.noServer !== true) throw new Error("WebSocket server must be created with { noServer: true } option"); + setupWebSocket({ + server, + fetchCallback, + wss: options.websocket.server + }); + } + return server; +}; +const serve = (options, listeningListener) => { + const server = createAdaptorServer(options); + server.listen(options?.port ?? 3e3, options.hostname, () => { + const serverInfo = server.address(); + listeningListener && listeningListener(serverInfo); + }); + return server; +}; + +//#endregion +exports.RequestError = RequestError; +exports.createAdaptorServer = createAdaptorServer; +exports.getRequestListener = getRequestListener; +exports.serve = serve; +exports.upgradeWebSocket = upgradeWebSocket; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts new file mode 100644 index 000000000..d3c55ce99 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.cts @@ -0,0 +1,101 @@ +import { AddressInfo } from "node:net"; +import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http"; +import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2"; +import { ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https"; +import { Duplex } from "node:stream"; +import { UpgradeWebSocket } from "hono/ws"; + +//#region src/websocket-types.d.ts +type WSReadyState = 0 | 1 | 2 | 3; +type WebSocketData = string | ArrayBuffer | Uint8Array | readonly Uint8Array[]; +type WebSocketSendOptions = { + compress?: boolean; +}; +interface WebSocketLike { + protocol: string; + readyState: WSReadyState; + close(code?: number, reason?: string): void; + send(data: string | ArrayBuffer | ArrayBufferView, options?: WebSocketSendOptions): void; + on(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; + on(event: 'close', listener: (code: number, reason: Uint8Array) => void): this; + on(event: 'error', listener: (error: unknown) => void): this; + off(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; +} +interface WebSocketServerLike { + options: { + noServer?: boolean; + }; + on(event: 'connection', listener: (ws: WebSocketLike, request: IncomingMessage) => void): this; + on(event: 'headers', listener: (headers: string[]) => void): this; + off(event: 'headers', listener: (headers: string[]) => void): this; + emit(event: 'connection', ws: WebSocketLike, request: IncomingMessage): boolean; + handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer, callback: (ws: WebSocketLike) => void): void; + close(): void; +} +//#endregion +//#region src/types.d.ts +type HttpBindings = { + incoming: IncomingMessage; + outgoing: ServerResponse; +}; +type Http2Bindings = { + incoming: Http2ServerRequest; + outgoing: Http2ServerResponse; +}; +type FetchCallback = (request: Request, env: HttpBindings | Http2Bindings) => Promise | unknown; +type ServerType = Server | Http2Server | Http2SecureServer; +type createHttpOptions = { + serverOptions?: ServerOptions; + createServer?: typeof createServer; +}; +type createHttpsOptions = { + serverOptions?: ServerOptions$2; + createServer?: typeof createServer$2; +}; +type createHttp2Options = { + serverOptions?: ServerOptions$1; + createServer?: typeof createServer$1; +}; +type createSecureHttp2Options = { + serverOptions?: SecureServerOptions; + createServer?: typeof createSecureServer; +}; +type ServerOptions$3 = createHttpOptions | createHttpsOptions | createHttp2Options | createSecureHttp2Options; +type Options = { + fetch: FetchCallback; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; + port?: number; + hostname?: string; + websocket?: { + server: WebSocketServerLike; + }; +} & ServerOptions$3; +type CustomErrorHandler = (err: unknown) => void | Response | Promise; +//#endregion +//#region src/server.d.ts +declare const createAdaptorServer: (options: Options) => ServerType; +declare const serve: (options: Options, listeningListener?: (info: AddressInfo) => void) => ServerType; +//#endregion +//#region src/websocket.d.ts +type UpgradeWebSocketOptions = { + onError: (err: unknown) => void; +}; +declare const upgradeWebSocket: UpgradeWebSocket; +//#endregion +//#region src/listener.d.ts +declare const getRequestListener: (fetchCallback: FetchCallback, options?: { + hostname?: string; + errorHandler?: CustomErrorHandler; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; +}) => (incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse) => Promise; +//#endregion +//#region src/error.d.ts +declare class RequestError extends Error { + constructor(message: string, options?: { + cause?: unknown; + }); +} +//#endregion +export { type Http2Bindings, type HttpBindings, RequestError, type ServerType, type WebSocketData, type WebSocketLike, type WebSocketServerLike, createAdaptorServer, getRequestListener, serve, upgradeWebSocket }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts new file mode 100644 index 000000000..a815b86f0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.d.mts @@ -0,0 +1,101 @@ +import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http"; +import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2"; +import { Duplex } from "node:stream"; +import { UpgradeWebSocket } from "hono/ws"; +import { AddressInfo } from "node:net"; +import { ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https"; + +//#region src/websocket-types.d.ts +type WSReadyState = 0 | 1 | 2 | 3; +type WebSocketData = string | ArrayBuffer | Uint8Array | readonly Uint8Array[]; +type WebSocketSendOptions = { + compress?: boolean; +}; +interface WebSocketLike { + protocol: string; + readyState: WSReadyState; + close(code?: number, reason?: string): void; + send(data: string | ArrayBuffer | ArrayBufferView, options?: WebSocketSendOptions): void; + on(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; + on(event: 'close', listener: (code: number, reason: Uint8Array) => void): this; + on(event: 'error', listener: (error: unknown) => void): this; + off(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this; +} +interface WebSocketServerLike { + options: { + noServer?: boolean; + }; + on(event: 'connection', listener: (ws: WebSocketLike, request: IncomingMessage) => void): this; + on(event: 'headers', listener: (headers: string[]) => void): this; + off(event: 'headers', listener: (headers: string[]) => void): this; + emit(event: 'connection', ws: WebSocketLike, request: IncomingMessage): boolean; + handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer, callback: (ws: WebSocketLike) => void): void; + close(): void; +} +//#endregion +//#region src/types.d.ts +type HttpBindings = { + incoming: IncomingMessage; + outgoing: ServerResponse; +}; +type Http2Bindings = { + incoming: Http2ServerRequest; + outgoing: Http2ServerResponse; +}; +type FetchCallback = (request: Request, env: HttpBindings | Http2Bindings) => Promise | unknown; +type ServerType = Server | Http2Server | Http2SecureServer; +type createHttpOptions = { + serverOptions?: ServerOptions; + createServer?: typeof createServer; +}; +type createHttpsOptions = { + serverOptions?: ServerOptions$2; + createServer?: typeof createServer$2; +}; +type createHttp2Options = { + serverOptions?: ServerOptions$1; + createServer?: typeof createServer$1; +}; +type createSecureHttp2Options = { + serverOptions?: SecureServerOptions; + createServer?: typeof createSecureServer; +}; +type ServerOptions$3 = createHttpOptions | createHttpsOptions | createHttp2Options | createSecureHttp2Options; +type Options = { + fetch: FetchCallback; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; + port?: number; + hostname?: string; + websocket?: { + server: WebSocketServerLike; + }; +} & ServerOptions$3; +type CustomErrorHandler = (err: unknown) => void | Response | Promise; +//#endregion +//#region src/server.d.ts +declare const createAdaptorServer: (options: Options) => ServerType; +declare const serve: (options: Options, listeningListener?: (info: AddressInfo) => void) => ServerType; +//#endregion +//#region src/websocket.d.ts +type UpgradeWebSocketOptions = { + onError: (err: unknown) => void; +}; +declare const upgradeWebSocket: UpgradeWebSocket; +//#endregion +//#region src/listener.d.ts +declare const getRequestListener: (fetchCallback: FetchCallback, options?: { + hostname?: string; + errorHandler?: CustomErrorHandler; + overrideGlobalObjects?: boolean; + autoCleanupIncoming?: boolean; +}) => (incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse) => Promise; +//#endregion +//#region src/error.d.ts +declare class RequestError extends Error { + constructor(message: string, options?: { + cause?: unknown; + }); +} +//#endregion +export { type Http2Bindings, type HttpBindings, RequestError, type ServerType, type WebSocketData, type WebSocketLike, type WebSocketServerLike, createAdaptorServer, getRequestListener, serve, upgradeWebSocket }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs new file mode 100644 index 000000000..4f5954d70 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/index.mjs @@ -0,0 +1,1335 @@ +import { t as X_ALREADY_SENT } from "./constants-BLSFu_RU.mjs"; +import { STATUS_CODES, ServerResponse, createServer } from "node:http"; +import { Http2ServerRequest, constants } from "node:http2"; +import { Readable } from "node:stream"; +import { defineWebSocketHelper } from "hono/ws"; + +//#region src/error.ts +var RequestError = class extends Error { + constructor(message, options) { + super(message, options); + this.name = "RequestError"; + } +}; + +//#endregion +//#region src/headers.ts +const nonJoinedHeaders = new Set([ + "age", + "authorization", + "content-length", + "content-type", + "etag", + "expires", + "from", + "host", + "if-modified-since", + "if-unmodified-since", + "last-modified", + "location", + "max-forwards", + "proxy-authorization", + "referer", + "retry-after", + "server", + "user-agent" +]); +const validHeaderName = /^[!#$%&'*+\-.^_`|~\dA-Za-z]+$/; +const isHttpWhitespace = (code) => code === 9 || code === 10 || code === 13 || code === 32; +const normalizeHeaderValue = (value) => { + if (!isHttpWhitespace(value.charCodeAt(0)) && !isHttpWhitespace(value.charCodeAt(value.length - 1))) return value; + let start = 0; + let end = value.length; + while (start < end && isHttpWhitespace(value.charCodeAt(start))) start++; + while (end > start && isHttpWhitespace(value.charCodeAt(end - 1))) end--; + return value.slice(start, end); +}; +const forbiddenHeaderValue = /[\0\r\n]/; +const GlobalHeaders = globalThis.Headers; +const materializeHeaders = (rawHeaders, HeadersCtor = GlobalHeaders) => { + const headers = new HeadersCtor(); + for (let i = 0; i < rawHeaders.length; i += 2) { + const name = rawHeaders[i]; + if (!name.startsWith(":")) headers.append(name, rawHeaders[i + 1]); + } + return headers; +}; +var RequestHeaders = class { + #incoming; + #rawHeaders; + #headers; + #invalidValue; + constructor(incoming) { + this.#incoming = incoming; + if (incoming instanceof Http2ServerRequest) this.#rawHeaders = incoming.rawHeaders.slice(); + } + get #lazyRawHeaders() { + return this.#rawHeaders ??= this.#incoming.rawHeaders.slice(); + } + get #native() { + if (!this.#headers) { + this.#headers = materializeHeaders(this.#lazyRawHeaders); + this.#rawHeaders = void 0; + } + return this.#headers; + } + #normalizedName(name) { + if (typeof name !== "string") return; + if (!validHeaderName.test(name)) throw new TypeError(`Invalid header name: ${name}`); + return name.toLowerCase(); + } + #lookupHttp1(lowerName) { + const headers = this.#incoming instanceof Http2ServerRequest ? void 0 : this.#incoming.headers; + if (!headers || nonJoinedHeaders.has(lowerName) || lowerName === "set-cookie" || lowerName === "__proto__") return; + if (!Object.hasOwn(headers, lowerName)) return null; + const rawValue = headers[lowerName]; + if (typeof rawValue === "string") { + const value = normalizeHeaderValue(rawValue); + return forbiddenHeaderValue.test(value) ? void 0 : value; + } + } + #lookup(rawHeaders, lowerName) { + const separator = lowerName === "cookie" ? "; " : ", "; + let value = null; + for (let i = 0; i < rawHeaders.length; i += 2) { + const rawName = rawHeaders[i]; + if (rawName.length === lowerName.length && rawName.toLowerCase() === lowerName) { + const rawValue = normalizeHeaderValue(rawHeaders[i + 1]); + if (forbiddenHeaderValue.test(rawValue)) { + this.#invalidValue = true; + return; + } + value = value === null ? rawValue : value + separator + rawValue; + } + } + return value; + } + append(name, value) { + this.#native.append(name, value); + } + delete(name) { + this.#native.delete(name); + } + get(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value; + } + return this.#native.get(name); + } + has(name) { + const lowerName = this.#normalizedName(name); + if (lowerName && !this.#headers && !this.#invalidValue) { + const http1Value = this.#lookupHttp1(lowerName); + if (http1Value !== void 0) return http1Value !== null; + const value = this.#lookup(this.#lazyRawHeaders, lowerName); + if (value !== void 0) return value !== null; + } + return this.#native.has(name); + } + set(name, value) { + this.#native.set(name, value); + } + getSetCookie() { + return this.#native.getSetCookie(); + } + keys() { + return this.#native.keys(); + } + values() { + return this.#native.values(); + } + entries() { + return this.#native.entries(); + } + forEach(callback, thisArg) { + this.#native.forEach((value, key) => { + callback.call(thisArg, value, key, this); + }); + } + [Symbol.iterator]() { + return this.entries(); + } +}; +Object.defineProperty(RequestHeaders.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Headers (lightweight) ${inspectFn(Object.fromEntries(this), { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(RequestHeaders.prototype, GlobalHeaders.prototype); +const newHeadersFromIncoming = (incoming) => globalThis.Headers === GlobalHeaders ? new RequestHeaders(incoming) : materializeHeaders(incoming.rawHeaders, globalThis.Headers); + +//#endregion +//#region src/url.ts +const reValidRequestUrl = /^\/[!#$&-;=?-\[\]_a-z~]*$/; +const reDotSegment = /\/\.\.?(?:[/?#]|$)/; +const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/; +const buildUrl = (scheme, host, incomingUrl) => { + const url = `${scheme}://${host}${incomingUrl}`; + if (!reValidHost.test(host)) { + const urlObj = new URL(url); + if (urlObj.hostname.length !== host.length && urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase()) throw new RequestError("Invalid host header"); + return urlObj.href; + } else if (incomingUrl.length === 0) return url + "/"; + else { + if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL"); + if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl)) return new URL(url).href; + return url; + } +}; + +//#endregion +//#region src/request.ts +const toRequestError = (e) => { + if (e instanceof RequestError) return e; + return new RequestError(e.message, { cause: e }); +}; +const GlobalRequest = global.Request; +var Request$1 = class extends GlobalRequest { + constructor(input, options) { + if (typeof input === "object" && getRequestCache in input) { + const hasReplacementBody = options !== void 0 && "body" in options && options.body != null; + if (input[bodyConsumedDirectlyKey] && !hasReplacementBody) throw new TypeError("Cannot construct a Request with a Request object that has already been used."); + input = input[getRequestCache](); + } + if (typeof (options?.body)?.getReader !== "undefined") options.duplex ??= "half"; + super(input, options); + } +}; +const wrapBodyStream = Symbol("wrapBodyStream"); +const byteExactEncodings = new Set([ + "latin1", + "binary", + "hex", + "base64", + "base64url" +]); +const isByteExactEncoding = (encoding) => encoding === null || byteExactEncodings.has(encoding); +const bodyBufferedBeforeDisconnectKey = Symbol("bodyBufferedBeforeDisconnect"); +const bodyBufferedLengthBeforeDisconnectKey = Symbol("bodyBufferedLengthBeforeDisconnect"); +const toBufferChunk = (chunk, encoding) => Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding ?? "utf8"); +const isRecoverableDisconnectedIncoming = (incoming) => !(incoming instanceof Http2ServerRequest) && !!incoming.complete && !!incoming.readableAborted && typeof incoming.read === "function" && isByteExactEncoding(incoming.readableEncoding); +const recordBodyBufferedBeforeDisconnect = (incoming) => { + if (incoming.readableDidRead || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] ??= incoming.readableLength; +}; +const readBodyBufferedBeforeDisconnect = (incoming, chunks) => { + if (incoming.readableDidRead && !chunks || !isRecoverableDisconnectedIncoming(incoming)) return; + const incomingWithRecovery = incoming; + if (incomingWithRecovery[bodyBufferedBeforeDisconnectKey] !== void 0) return incomingWithRecovery[bodyBufferedBeforeDisconnectKey]; + let result; + const errored = incoming.errored; + if (errored && errored.code !== "ECONNRESET") result = errored; + else if (incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== void 0 && incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]) result = newBodyUnusableError(); + else { + const bodyChunks = chunks ?? []; + const chunk = incoming.read(); + if (chunk !== null) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks); + result = buffer; + const contentLength = incoming.headers["content-length"]; + if (typeof contentLength === "string" && /^\d+$/.test(contentLength)) { + const expectedLength = Number(contentLength); + if (Number.isSafeInteger(expectedLength) && buffer.length !== expectedLength) result = newBodyUnusableError(); + } + } + incomingWithRecovery[bodyBufferedBeforeDisconnectKey] = result; + return result; +}; +const enqueueBufferedBody = (controller, buffered) => { + if (buffered instanceof Error) { + controller.error(buffered); + return; + } + if (buffered.length > 0) controller.enqueue(buffered); + controller.close(); +}; +const newRequestFromIncoming = (method, url, headers, incoming, abortController) => { + const init = { + method, + headers, + signal: abortController.signal + }; + if (method === "TRACE") { + init.method = "GET"; + const req = new Request$1(url, init); + Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return req; + } + if (!(method === "GET" || method === "HEAD")) if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) init.body = new ReadableStream({ start(controller) { + controller.enqueue(incoming.rawBody); + controller.close(); + } }); + else if (incoming[wrapBodyStream]) { + let reader; + init.body = new ReadableStream({ async pull(controller) { + try { + if (!reader) { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + enqueueBufferedBody(controller, buffered); + return; + } + } + reader ||= Readable.toWeb(incoming).getReader(); + const { done, value } = await reader.read(); + if (done) controller.close(); + else controller.enqueue(value); + } catch (error) { + controller.error(error); + } + } }); + } else { + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) init.body = new ReadableStream({ start(controller) { + enqueueBufferedBody(controller, buffered); + } }); + else init.body = Readable.toWeb(incoming); + } + return new Request$1(url, init); +}; +const getRequestCache = Symbol("getRequestCache"); +const requestCache = Symbol("requestCache"); +const incomingKey = Symbol("incomingKey"); +const urlKey = Symbol("urlKey"); +const methodKey = Symbol("methodKey"); +const headersKey = Symbol("headersKey"); +const abortControllerKey = Symbol("abortControllerKey"); +const getAbortController = Symbol("getAbortController"); +const abortRequest = Symbol("abortRequest"); +const bodyBufferKey = Symbol("bodyBuffer"); +const bodyReadPromiseKey = Symbol("bodyReadPromise"); +const bodyConsumedDirectlyKey = Symbol("bodyConsumedDirectly"); +const bodyLockReaderKey = Symbol("bodyLockReader"); +const abortReasonKey = Symbol("abortReason"); +const newBodyUnusableError = () => { + return /* @__PURE__ */ new TypeError("Body is unusable"); +}; +const rejectBodyUnusable = () => { + return Promise.reject(newBodyUnusableError()); +}; +const textDecoder = new TextDecoder(); +const consumeBodyDirectOnce = (request) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + request[bodyConsumedDirectlyKey] = true; +}; +const toArrayBuffer = (buf) => { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +}; +const contentType = (request) => { + return (request[headersKey] ||= newHeadersFromIncoming(request[incomingKey])).get("content-type") || ""; +}; +const methodTokenRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; +const normalizeIncomingMethod = (method) => { + if (typeof method !== "string" || method.length === 0) return "GET"; + switch (method) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "PATCH": + case "POST": + case "PUT": + case "QUERY": return method; + } + const upper = method.toUpperCase(); + switch (upper) { + case "DELETE": + case "GET": + case "HEAD": + case "OPTIONS": + case "POST": + case "PUT": return upper; + default: return method; + } +}; +const validateDirectReadMethod = (method) => { + if (!methodTokenRegExp.test(method)) return /* @__PURE__ */ new TypeError(`'${method}' is not a valid HTTP method.`); + const normalized = method.toUpperCase(); + if (normalized === "CONNECT" || normalized === "TRACK" || normalized === "TRACE" && method !== "TRACE") return /* @__PURE__ */ new TypeError(`'${method}' HTTP method is unsupported.`); +}; +const readBodyWithFastPath = (request, method, fromBuffer) => { + if (request[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + const methodName = request.method; + if (methodName === "GET" || methodName === "HEAD") return request[getRequestCache]()[method](); + const methodValidationError = validateDirectReadMethod(methodName); + if (methodValidationError) return Promise.reject(methodValidationError); + if (request[requestCache]) { + if (methodName !== "TRACE") return request[requestCache][method](); + } + const alreadyUsedError = consumeBodyDirectOnce(request); + if (alreadyUsedError) return alreadyUsedError; + const raw = readRawBodyIfAvailable(request); + if (raw) { + const result = Promise.resolve(fromBuffer(raw, request)); + request[bodyBufferKey] = void 0; + return result; + } + return readBodyDirect(request).then((buf) => { + const result = fromBuffer(buf, request); + request[bodyBufferKey] = void 0; + return result; + }); +}; +const readRawBodyIfAvailable = (request) => { + const incoming = request[incomingKey]; + if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) return incoming.rawBody; +}; +const normalizeAbortError = (request, incoming) => { + if (incoming.errored) return incoming.errored; + const reason = request[abortReasonKey]; + if (reason !== void 0) return reason instanceof Error ? reason : new Error(String(reason)); + return /* @__PURE__ */ new Error("Client connection prematurely closed."); +}; +const readBodyDirect = (request) => { + if (request[bodyBufferKey]) return Promise.resolve(request[bodyBufferKey]); + if (request[bodyReadPromiseKey]) return request[bodyReadPromiseKey]; + const incoming = request[incomingKey]; + if (incoming.readableDidRead) return rejectBodyUnusable(); + const buffered = readBodyBufferedBeforeDisconnect(incoming); + if (buffered !== void 0) { + if (buffered instanceof Error) return Promise.reject(buffered); + request[bodyBufferKey] = buffered; + return Promise.resolve(buffered); + } + const promise = new Promise((resolve, reject) => { + const chunks = []; + let settled = false; + const finish = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback(); + }; + const recoverCompleteBodyAfterDisconnect = (error) => { + const streamError = incoming.errored ?? error; + if (!isRecoverableDisconnectedIncoming(incoming) || streamError && streamError.code !== "ECONNRESET") return false; + finish(() => { + const recovered = readBodyBufferedBeforeDisconnect(incoming, chunks); + if (recovered instanceof Error) reject(recovered); + else if (recovered === void 0) reject(error ?? normalizeAbortError(request, incoming)); + else { + request[bodyBufferKey] = recovered; + resolve(recovered); + } + }); + return true; + }; + const onData = (chunk) => { + chunks.push(toBufferChunk(chunk, incoming.readableEncoding)); + }; + const onEnd = () => { + finish(() => { + const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks); + request[bodyBufferKey] = buffer; + resolve(buffer); + }); + }; + const onError = (error) => { + if (recoverCompleteBodyAfterDisconnect(error)) return; + finish(() => { + reject(error); + }); + }; + const onClose = () => { + if (incoming.readableEnded) { + onEnd(); + return; + } + if (recoverCompleteBodyAfterDisconnect()) return; + finish(() => { + reject(normalizeAbortError(request, incoming)); + }); + }; + const cleanup = () => { + incoming.off("data", onData); + incoming.off("end", onEnd); + incoming.off("error", onError); + incoming.off("close", onClose); + request[bodyReadPromiseKey] = void 0; + }; + incoming.on("data", onData); + incoming.on("end", onEnd); + incoming.on("error", onError); + incoming.on("close", onClose); + queueMicrotask(() => { + if (settled) return; + if (incoming.readableEnded) onEnd(); + else if (incoming.errored) onError(incoming.errored); + else if (incoming.destroyed) onClose(); + }); + }); + request[bodyReadPromiseKey] = promise; + return promise; +}; +const requestPrototype = { + get method() { + return this[methodKey]; + }, + get url() { + return this[urlKey]; + }, + get headers() { + return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]); + }, + [abortRequest](reason) { + if (this[abortReasonKey] === void 0) this[abortReasonKey] = reason; + const abortController = this[abortControllerKey]; + if (abortController && !abortController.signal.aborted) abortController.abort(reason); + }, + [getAbortController]() { + this[abortControllerKey] ||= new AbortController(); + if (this[abortReasonKey] !== void 0 && !this[abortControllerKey].signal.aborted) this[abortControllerKey].abort(this[abortReasonKey]); + return this[abortControllerKey]; + }, + [getRequestCache]() { + const abortController = this[getAbortController](); + if (this[requestCache]) return this[requestCache]; + const method = this.method; + if (this[bodyConsumedDirectlyKey] && !(method === "GET" || method === "HEAD")) { + this[bodyBufferKey] = void 0; + const init = { + method: method === "TRACE" ? "GET" : method, + headers: this.headers, + signal: abortController.signal + }; + if (method !== "TRACE") { + init.body = new ReadableStream({ start(c) { + c.close(); + } }); + init.duplex = "half"; + } + const req = new Request$1(this[urlKey], init); + if (method === "TRACE") Object.defineProperty(req, "method", { get() { + return "TRACE"; + } }); + return this[requestCache] = req; + } + return this[requestCache] = newRequestFromIncoming(this.method, this[urlKey], this.headers, this[incomingKey], abortController); + }, + get body() { + if (!this[bodyConsumedDirectlyKey]) return this[getRequestCache]().body; + const request = this[getRequestCache](); + if (!this[bodyLockReaderKey] && request.body) this[bodyLockReaderKey] = request.body.getReader(); + return request.body; + }, + get bodyUsed() { + if (this[bodyConsumedDirectlyKey]) return true; + if (this[requestCache]) return this[requestCache].bodyUsed; + return false; + } +}; +Object.defineProperty(requestPrototype, "signal", { get() { + return this[getAbortController]().signal; +} }); +[ + "cache", + "credentials", + "destination", + "integrity", + "mode", + "redirect", + "referrer", + "referrerPolicy", + "keepalive" +].forEach((k) => { + Object.defineProperty(requestPrototype, k, { get() { + return this[getRequestCache]()[k]; + } }); +}); +["clone", "formData"].forEach((k) => { + Object.defineProperty(requestPrototype, k, { value: function() { + if (this[bodyConsumedDirectlyKey]) { + if (k === "clone") throw newBodyUnusableError(); + return rejectBodyUnusable(); + } + return this[getRequestCache]()[k](); + } }); +}); +Object.defineProperty(requestPrototype, "text", { value: function() { + return readBodyWithFastPath(this, "text", (buf) => textDecoder.decode(buf)); +} }); +Object.defineProperty(requestPrototype, "arrayBuffer", { value: function() { + return readBodyWithFastPath(this, "arrayBuffer", (buf) => toArrayBuffer(buf)); +} }); +Object.defineProperty(requestPrototype, "blob", { value: function() { + return readBodyWithFastPath(this, "blob", (buf, request) => { + const type = contentType(request); + const init = type ? { headers: { "content-type": type } } : void 0; + return new Response(buf, init).blob(); + }); +} }); +Object.defineProperty(requestPrototype, "json", { value: function() { + if (this[bodyConsumedDirectlyKey]) return rejectBodyUnusable(); + return this.text().then(JSON.parse); +} }); +Object.defineProperty(requestPrototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Request (lightweight) ${inspectFn({ + method: this.method, + url: this.url, + headers: this.headers, + nativeRequest: this[requestCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(requestPrototype, Request$1.prototype); +const newRequest = (incoming, defaultHostname) => { + const req = Object.create(requestPrototype); + req[incomingKey] = incoming; + req[methodKey] = normalizeIncomingMethod(incoming.method); + const incomingUrl = incoming.url || ""; + if (incomingUrl[0] !== "/" && (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))) { + if (incoming instanceof Http2ServerRequest) throw new RequestError("Absolute URL for :path is not allowed in HTTP/2"); + try { + req[urlKey] = new URL(incomingUrl).href; + } catch (e) { + throw new RequestError("Invalid absolute URL", { cause: e }); + } + return req; + } + const host = (incoming instanceof Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname; + if (!host) throw new RequestError("Missing host header"); + let scheme; + if (incoming instanceof Http2ServerRequest) { + scheme = incoming.scheme; + if (!(scheme === "http" || scheme === "https")) throw new RequestError("Unsupported scheme"); + } else scheme = incoming.socket && incoming.socket.encrypted ? "https" : "http"; + try { + req[urlKey] = buildUrl(scheme, host, incomingUrl); + } catch (e) { + if (e instanceof RequestError) throw e; + else throw new RequestError("Invalid URL", { cause: e }); + } + return req; +}; + +//#endregion +//#region src/response.ts +const defaultContentType = "text/plain; charset=UTF-8"; +const responseCache = Symbol("responseCache"); +const getResponseCache = Symbol("getResponseCache"); +const cacheKey = Symbol("cache"); +const GlobalResponse = global.Response; +var Response$1 = class Response$1 { + #body; + #init; + [getResponseCache]() { + const cache = this[cacheKey]; + const liveHeaders = cache && cache[2] instanceof Headers ? cache[2] : void 0; + delete this[cacheKey]; + return this[responseCache] ||= new GlobalResponse(this.#body, liveHeaders ? { + status: this.#init?.status, + statusText: this.#init?.statusText, + headers: liveHeaders + } : this.#init); + } + constructor(body, init) { + let headers; + this.#body = body; + if (init instanceof GlobalResponse) { + const cachedGlobalResponse = init[responseCache]; + if (cachedGlobalResponse) { + this.#init = cachedGlobalResponse; + this[getResponseCache](); + return; + } + this.#init = init instanceof Response$1 ? init.#init : init; + headers = new Headers(init.headers); + } else this.#init = init; + if (body == null || typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) this[cacheKey] = [ + init?.status || 200, + body ?? null, + headers || init?.headers + ]; + } + get headers() { + const cache = this[cacheKey]; + if (cache) { + if (!(cache[2] instanceof Headers)) cache[2] = new Headers(cache[2] || (cache[1] === null ? void 0 : { "content-type": defaultContentType })); + return cache[2]; + } + return this[getResponseCache]().headers; + } + get status() { + return this[cacheKey]?.[0] ?? this[getResponseCache]().status; + } + get ok() { + const status = this.status; + return status >= 200 && status < 300; + } +}; +[ + "body", + "bodyUsed", + "redirected", + "statusText", + "trailers", + "type", + "url" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { get() { + return this[getResponseCache]()[k]; + } }); +}); +[ + "arrayBuffer", + "blob", + "clone", + "formData", + "json", + "text" +].forEach((k) => { + Object.defineProperty(Response$1.prototype, k, { value: function() { + return this[getResponseCache]()[k](); + } }); +}); +Object.defineProperty(Response$1.prototype, Symbol.for("nodejs.util.inspect.custom"), { value: function(depth, options, inspectFn) { + return `Response (lightweight) ${inspectFn({ + status: this.status, + headers: this.headers, + ok: this.ok, + nativeResponse: this[responseCache] + }, { + ...options, + depth: depth == null ? null : depth - 1 + })}`; +} }); +Object.setPrototypeOf(Response$1, GlobalResponse); +Object.setPrototypeOf(Response$1.prototype, GlobalResponse.prototype); +const validRedirectUrl = /^https?:\/\/[!#-;=?-[\]_a-z~A-Z]+$/; +const parseRedirectUrl = (url) => { + if (url instanceof URL) return url.href; + if (validRedirectUrl.test(url)) return url; + return new URL(url).href; +}; +const validRedirectStatuses = new Set([ + 301, + 302, + 303, + 307, + 308 +]); +Object.defineProperty(Response$1, "redirect", { + value: function redirect(url, status = 302) { + if (!validRedirectStatuses.has(status)) throw new RangeError("Invalid status code"); + return new Response$1(null, { + status, + headers: { location: parseRedirectUrl(url) } + }); + }, + writable: true, + configurable: true +}); +Object.defineProperty(Response$1, "json", { + value: function json(data, init) { + const body = JSON.stringify(data); + if (body === void 0) throw new TypeError("The data is not JSON serializable"); + const initHeaders = init?.headers; + let headers; + if (initHeaders) { + headers = new Headers(initHeaders); + if (!headers.has("content-type")) headers.set("content-type", "application/json"); + } else headers = { "content-type": "application/json" }; + return new Response$1(body, { + status: init?.status ?? 200, + statusText: init?.statusText, + headers + }); + }, + writable: true, + configurable: true +}); + +//#endregion +//#region src/utils.ts +async function readWithoutBlocking(readPromise) { + return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]); +} +function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) { + const cancel = (error) => { + reader.cancel(error).catch(() => {}); + }; + writable.on("close", cancel); + writable.on("error", cancel); + (currentReadPromise ?? reader.read()).then(flow, handleStreamError); + return reader.closed.finally(() => { + writable.off("close", cancel); + writable.off("error", cancel); + }); + function handleStreamError(error) { + if (error) writable.destroy(error); + } + function onDrain() { + reader.read().then(flow, handleStreamError); + } + function flow({ done, value }) { + try { + if (done) writable.end(); + else if (!writable.write(value)) writable.once("drain", onDrain); + else return reader.read().then(flow, handleStreamError); + } catch (e) { + handleStreamError(e); + } + } +} +function writeFromReadableStream(stream, writable) { + if (stream.locked) throw new TypeError("ReadableStream is locked."); + else if (writable.destroyed) return; + return writeFromReadableStreamDefaultReader(stream.getReader(), writable); +} +const buildOutgoingHttpHeaders = (headers, defaultContentType) => { + const res = {}; + if (!(headers instanceof Headers)) headers = new Headers(headers ?? void 0); + if (headers.has("set-cookie")) { + const cookies = []; + for (const [k, v] of headers) if (k === "set-cookie") cookies.push(v); + else res[k] = v; + if (cookies.length > 0) res["set-cookie"] = cookies; + } else for (const [k, v] of headers) res[k] = v; + if (defaultContentType) res["content-type"] ??= defaultContentType; + return res; +}; + +//#endregion +//#region src/listener.ts +const outgoingEnded = Symbol("outgoingEnded"); +const incomingDraining = Symbol("incomingDraining"); +const DRAIN_TIMEOUT_MS = 500; +const MAX_DRAIN_BYTES = 64 * 1024 * 1024; +const drainIncoming = (incoming) => { + const incomingWithDrainState = incoming; + if (incoming.destroyed || incomingWithDrainState[incomingDraining]) return; + incomingWithDrainState[incomingDraining] = true; + if (incoming instanceof Http2ServerRequest) { + try { + incoming.stream?.close?.(constants.NGHTTP2_NO_ERROR); + } catch {} + return; + } + let bytesRead = 0; + const cleanup = () => { + clearTimeout(timer); + incoming.off("data", onData); + incoming.off("end", cleanup); + incoming.off("error", cleanup); + }; + const forceClose = () => { + cleanup(); + const socket = incoming.socket; + if (socket && !socket.destroyed) { + if (typeof socket.destroySoon === "function") socket.destroySoon(); + else if (typeof socket.destroy === "function") socket.destroy(); + } + }; + const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS); + timer.unref?.(); + const onData = (chunk) => { + bytesRead += chunk.length; + if (bytesRead > MAX_DRAIN_BYTES) forceClose(); + }; + incoming.on("data", onData); + incoming.on("end", cleanup); + incoming.on("error", cleanup); + incoming.resume(); +}; +const makeCloseHandler = (req, incoming, outgoing, needsBodyCleanup) => () => { + if (incoming.errored) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest](incoming.errored.toString()); + } else if (!outgoing.writableFinished || incoming instanceof Http2ServerRequest && incoming.aborted) { + recordBodyBufferedBeforeDisconnect(incoming); + req[abortRequest]("Client connection prematurely closed."); + } + if (needsBodyCleanup && !incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + drainIncoming(incoming); + }); + }); +}; +const isImmediateCacheableResponse = (res) => { + if (!(cacheKey in res)) return false; + const body = res[cacheKey][1]; + return body === null || typeof body === "string" || body instanceof Uint8Array; +}; +const handleRequestError = () => new Response(null, { status: 400 }); +const handleFetchError = (e) => new Response(null, { status: e instanceof Error && (e.name === "TimeoutError" || e.constructor.name === "TimeoutError") ? 504 : 500 }); +const handleResponseError = (e, outgoing) => { + const err = e instanceof Error ? e : new Error("unknown error", { cause: e }); + if (err.code === "ERR_STREAM_PREMATURE_CLOSE") console.info("The user aborted a request."); + else { + console.error(e); + if (!outgoing.headersSent) { + if (outgoing instanceof ServerResponse) outgoing._contentLength = null; + outgoing.writeHead(500, { "Content-Type": "text/plain" }); + } + outgoing.end(`Error: ${err.message}`); + outgoing.destroy(err); + } +}; +const flushHeaders = (outgoing) => { + if ("flushHeaders" in outgoing && outgoing.writable) outgoing.flushHeaders(); +}; +const trySetContentLength = (outgoing, status, length) => { + const http1 = outgoing; + if (http1._contentLength === null && http1._hasBody && http1.useChunkedEncodingByDefault && !http1._removedContLen && status >= 200 && status !== 204 && status !== 304 && !outgoing.hasHeader("content-length") && !outgoing.hasHeader("transfer-encoding") && !outgoing.hasHeader("trailer")) { + http1._contentLength = length; + return true; + } + return false; +}; +const writeDefaultHeaders = (outgoing, status, length) => { + if (trySetContentLength(outgoing, status, length)) outgoing.writeHead(status, { "Content-Type": defaultContentType }); + else outgoing.writeHead(status, { + "Content-Type": defaultContentType, + "Content-Length": length + }); +}; +const responseViaCache = async (res, outgoing) => { + let [status, body, header] = res[cacheKey]; + if (!header) { + if (body === null) { + outgoing.writeHead(status); + outgoing.end(); + } else if (typeof body === "string") { + writeDefaultHeaders(outgoing, status, Buffer.byteLength(body)); + outgoing.end(body); + } else if (body instanceof Uint8Array) { + writeDefaultHeaders(outgoing, status, body.byteLength); + outgoing.end(body); + } else if (body instanceof Blob) { + writeDefaultHeaders(outgoing, status, body.size); + outgoing.end(new Uint8Array(await body.arrayBuffer())); + } else { + outgoing.writeHead(status, { "Content-Type": defaultContentType }); + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); + return; + } + let hasContentLength = false; + let plainHeaders = false; + let canAutoLength = true; + if (header instanceof Headers) { + hasContentLength = header.has("content-length"); + header = buildOutgoingHttpHeaders(header, body === null ? void 0 : defaultContentType); + } else if (Array.isArray(header)) { + const headerObj = new Headers(header); + hasContentLength = headerObj.has("content-length"); + header = buildOutgoingHttpHeaders(headerObj, body === null ? void 0 : defaultContentType); + } else { + plainHeaders = true; + for (const key in header) { + if (key.length === 14 && key.toLowerCase() === "content-length") { + hasContentLength = true; + break; + } + if (key.length === 17 && key.toLowerCase() === "transfer-encoding" || key.length === 7 && key.toLowerCase() === "trailer") canAutoLength = false; + } + } + if (!hasContentLength) { + let length; + if (typeof body === "string") length = Buffer.byteLength(body); + else if (body instanceof Uint8Array) length = body.byteLength; + else if (body instanceof Blob) length = body.size; + if (length !== void 0 && (!plainHeaders || !canAutoLength || !trySetContentLength(outgoing, status, length))) { + if (plainHeaders) header = { ...header }; + header["Content-Length"] = length; + } + } + outgoing.writeHead(status, header); + if (body == null) outgoing.end(); + else if (typeof body === "string" || body instanceof Uint8Array) outgoing.end(body); + else if (body instanceof Blob) outgoing.end(new Uint8Array(await body.arrayBuffer())); + else { + flushHeaders(outgoing); + await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing)); + } + outgoing[outgoingEnded]?.(); +}; +const isPromise = (res) => typeof res.then === "function"; +const responseViaResponseObject = async (res, outgoing, options = {}) => { + if (isPromise(res)) if (options.errorHandler) try { + res = await res; + } catch (err) { + const errRes = await options.errorHandler(err); + if (!errRes) return; + res = errRes; + } + else res = await res.catch(handleFetchError); + if (cacheKey in res) return responseViaCache(res, outgoing); + const resHeaderRecord = buildOutgoingHttpHeaders(res.headers, res.body === null ? void 0 : defaultContentType); + if (res.body) { + const reader = res.body.getReader(); + const values = []; + let done = false; + let currentReadPromise = void 0; + if (resHeaderRecord["transfer-encoding"] !== "chunked") { + let maxReadCount = 2; + for (let i = 0; i < maxReadCount; i++) { + currentReadPromise ||= reader.read(); + const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => { + console.error(e); + done = true; + }); + if (!chunk) { + if (i === 1) { + await new Promise((resolve) => setTimeout(resolve)); + maxReadCount = 3; + continue; + } + break; + } + currentReadPromise = void 0; + if (chunk.value) values.push(chunk.value); + if (chunk.done) { + done = true; + break; + } + } + if (done && !("content-length" in resHeaderRecord)) resHeaderRecord["content-length"] = values.reduce((acc, value) => acc + value.length, 0); + } + outgoing.writeHead(res.status, resHeaderRecord); + values.forEach((value) => { + outgoing.write(value); + }); + if (done) outgoing.end(); + else { + if (values.length === 0) flushHeaders(outgoing); + await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise); + } + } else if (resHeaderRecord[X_ALREADY_SENT]) {} else { + outgoing.writeHead(res.status, resHeaderRecord); + outgoing.end(); + } + outgoing[outgoingEnded]?.(); +}; +const getRequestListener = (fetchCallback, options = {}) => { + const autoCleanupIncoming = options.autoCleanupIncoming ?? true; + if (options.overrideGlobalObjects !== false && global.Request !== Request$1) { + Object.defineProperty(global, "Request", { value: Request$1 }); + Object.defineProperty(global, "Response", { value: Response$1 }); + } + return async (incoming, outgoing) => { + let res, req; + let needsBodyCleanup = false; + let closeHandlerAttached = false; + const ensureCloseHandler = () => { + if (!req || closeHandlerAttached) return; + closeHandlerAttached = true; + outgoing.on("close", makeCloseHandler(req, incoming, outgoing, needsBodyCleanup)); + }; + try { + req = newRequest(incoming, options.hostname); + needsBodyCleanup = autoCleanupIncoming && !(incoming.method === "GET" || incoming.method === "HEAD"); + if (needsBodyCleanup) { + incoming[wrapBodyStream] = true; + if (incoming instanceof Http2ServerRequest) outgoing[outgoingEnded] = () => { + if (!incoming.readableEnded) setTimeout(() => { + if (!incoming.readableEnded) setTimeout(() => { + incoming.destroy(); + outgoing.destroy(); + }); + }); + }; + } + res = fetchCallback(req, { + incoming, + outgoing + }); + if (!isPromise(res) && isImmediateCacheableResponse(res)) { + if (needsBodyCleanup && !incoming.readableEnded) outgoing.once("finish", () => { + if (!incoming.readableEnded) drainIncoming(incoming); + }); + return responseViaCache(res, outgoing); + } + ensureCloseHandler(); + } catch (e) { + if (!res) if (options.errorHandler) { + ensureCloseHandler(); + res = await options.errorHandler(req ? e : toRequestError(e)); + if (!res) return; + } else if (!req) res = handleRequestError(); + else res = handleFetchError(e); + else return handleResponseError(e, outgoing); + } + try { + return await responseViaResponseObject(res, outgoing, options); + } catch (e) { + return handleResponseError(e, outgoing); + } + }; +}; + +//#endregion +//#region src/websocket.ts +/** +* @link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent +*/ +const CloseEvent = globalThis.CloseEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get wasClean() { + return this.#eventInitDict.wasClean ?? false; + } + get code() { + return this.#eventInitDict.code ?? 0; + } + get reason() { + return this.#eventInitDict.reason ?? ""; + } +}; +/** +* Node.js has no global `ErrorEvent`, unlike `Event`/`MessageEvent`/`CloseEvent`. +* @link https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent +*/ +const ErrorEvent = globalThis.ErrorEvent ?? class extends Event { + #eventInitDict; + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + this.#eventInitDict = eventInitDict; + } + get message() { + return this.#eventInitDict.message ?? ""; + } + get filename() { + return this.#eventInitDict.filename ?? ""; + } + get lineno() { + return this.#eventInitDict.lineno ?? 0; + } + get colno() { + return this.#eventInitDict.colno ?? 0; + } + get error() { + return this.#eventInitDict.error ?? null; + } +}; +const generateConnectionSymbol = () => Symbol("connection"); +const CONNECTION_SYMBOL_KEY = Symbol("CONNECTION_SYMBOL_KEY"); +const WAIT_FOR_WEBSOCKET_SYMBOL = Symbol("WAIT_FOR_WEBSOCKET_SYMBOL"); +const responseHeadersToSkip = new Set([ + "connection", + "content-length", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "sec-websocket-accept", + "sec-websocket-extensions", + "sec-websocket-protocol" +]); +const appendResponseHeaders = (headers, responseHeaders) => { + if (!responseHeaders) return; + responseHeaders.forEach((value, key) => { + if (responseHeadersToSkip.has(key.toLowerCase())) return; + headers.push(`${key}: ${value}`); + }); +}; +const rejectUpgradeRequest = (socket, status, responseHeaders) => { + const responseLines = ["Connection: close", "Content-Length: 0"]; + appendResponseHeaders(responseLines, responseHeaders); + socket.end(`HTTP/1.1 ${status.toString()} ${STATUS_CODES[status] ?? ""}\r\n${responseLines.join("\r\n")}\r\n\r +`); +}; +const createUpgradeRequest = (request) => { + const protocol = request.socket.encrypted ? "https" : "http"; + const url = new URL(request.url ?? "/", `${protocol}://${request.headers.host ?? "localhost"}`); + const headers = new Headers(); + for (const key in request.headers) { + const value = request.headers[key]; + if (!value) continue; + headers.append(key, Array.isArray(value) ? value[0] : value); + } + return new Request(url, { headers }); +}; +const setupWebSocket = (options) => { + const { server, fetchCallback, wss } = options; + const waiterMap = /* @__PURE__ */ new Map(); + wss.on("connection", (ws, request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiter.resolve(ws); + waiterMap.delete(request); + } + }); + const rejectWaiter = (request) => { + const waiter = waiterMap.get(request); + if (waiter) { + waiterMap.delete(request); + waiter.reject(/* @__PURE__ */ new Error("WebSocket handshake aborted")); + } + }; + const waitForWebSocket = (request, connectionSymbol) => { + return new Promise((resolve, reject) => { + waiterMap.set(request, { + resolve, + reject, + connectionSymbol + }); + }); + }; + server.on("upgrade", async (request, socket, head) => { + if (request.headers.upgrade?.toLowerCase() !== "websocket") return; + const env = { + incoming: request, + outgoing: void 0, + wss, + [WAIT_FOR_WEBSOCKET_SYMBOL]: waitForWebSocket + }; + let status = 400; + let responseHeaders; + try { + const response = await fetchCallback(createUpgradeRequest(request), env); + if (response instanceof Response) { + status = response.status; + responseHeaders = response.headers; + } + } catch { + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, 500); + return; + } + const waiter = waiterMap.get(request); + if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { + rejectWaiter(request); + if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, status, responseHeaders); + return; + } + const addResponseHeaders = (headers) => { + appendResponseHeaders(headers, responseHeaders); + }; + const reclaimWaiterOnClose = () => rejectWaiter(request); + socket.once("close", reclaimWaiterOnClose); + wss.on("headers", addResponseHeaders); + try { + wss.handleUpgrade(request, socket, head, (ws) => { + socket.off("close", reclaimWaiterOnClose); + wss.emit("connection", ws, request); + }); + } finally { + wss.off("headers", addResponseHeaders); + } + }); + server.on("close", () => { + wss.close(); + }); +}; +const upgradeWebSocket = defineWebSocketHelper(async (c, events, options) => { + if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return; + const env = c.env; + const waitForWebSocket = env[WAIT_FOR_WEBSOCKET_SYMBOL]; + if (!waitForWebSocket || !env.incoming) return new Response(null, { status: 500 }); + const connectionSymbol = generateConnectionSymbol(); + env[CONNECTION_SYMBOL_KEY] = connectionSymbol; + (async () => { + let ws; + try { + ws = await waitForWebSocket(env.incoming, connectionSymbol); + } catch { + return; + } + const messagesReceivedInStarting = []; + const bufferMessage = (data, isBinary) => { + messagesReceivedInStarting.push([data, isBinary]); + }; + ws.on("message", bufferMessage); + const ctx = { + binaryType: "arraybuffer", + close(code, reason) { + ws.close(code, reason); + }, + protocol: ws.protocol, + raw: ws, + get readyState() { + return ws.readyState; + }, + send(source, opts) { + ws.send(source, { compress: opts?.compress }); + }, + url: new URL(c.req.url) + }; + try { + events?.onOpen?.(new Event("open"), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + const handleMessage = (data, isBinary) => { + const datas = Array.isArray(data) ? data : [data]; + for (const data of datas) try { + events?.onMessage?.(new MessageEvent("message", { data: isBinary ? data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : typeof data === "string" ? data : Buffer.from(data).toString("utf-8") }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }; + ws.off("message", bufferMessage); + for (const message of messagesReceivedInStarting) handleMessage(...message); + ws.on("message", (data, isBinary) => { + handleMessage(data, isBinary); + }); + ws.on("close", (code, reason) => { + try { + events?.onClose?.(new CloseEvent("close", { + code, + reason: reason.toString() + }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + ws.on("error", (error) => { + try { + events?.onError?.(new ErrorEvent("error", { error }), ctx); + } catch (e) { + (options?.onError ?? console.error)(e); + } + }); + })(); + return new Response(); +}); + +//#endregion +//#region src/server.ts +const createAdaptorServer = (options) => { + const fetchCallback = options.fetch; + const requestListener = getRequestListener(fetchCallback, { + hostname: options.hostname, + overrideGlobalObjects: options.overrideGlobalObjects, + autoCleanupIncoming: options.autoCleanupIncoming + }); + const server = (options.createServer || createServer)(options.serverOptions || {}, requestListener); + if (options.websocket && options.websocket.server) { + if (options.websocket.server.options.noServer !== true) throw new Error("WebSocket server must be created with { noServer: true } option"); + setupWebSocket({ + server, + fetchCallback, + wss: options.websocket.server + }); + } + return server; +}; +const serve = (options, listeningListener) => { + const server = createAdaptorServer(options); + server.listen(options?.port ?? 3e3, options.hostname, () => { + const serverInfo = server.address(); + listeningListener && listeningListener(serverInfo); + }); + return server; +}; + +//#endregion +export { RequestError, createAdaptorServer, getRequestListener, serve, upgradeWebSocket }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs new file mode 100644 index 000000000..bcfb0e52e --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.cjs @@ -0,0 +1,153 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +const require_utils_stream = require('./utils/stream.cjs'); +let hono_utils_mime = require("hono/utils/mime"); +let node_fs = require("node:fs"); +let node_path = require("node:path"); + +//#region src/serve-static.ts +const COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i; +const ENCODINGS = { + br: ".br", + zstd: ".zst", + gzip: ".gz" +}; +const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS); +const getStats = (path) => { + let stats; + try { + stats = (0, node_fs.statSync)(path); + } catch {} + return stats; +}; +const BYTE_RANGE_PATTERN = /^(?:bytes=)?(?!-$)(\d*)-(\d*)$/; +const parseByteRange = (range) => { + const match = range.match(BYTE_RANGE_PATTERN); + if (!match) return; + const [, start, end] = match; + if (start === "") return { + type: "suffix", + length: Number(end) + }; + if (end === "") return { + type: "open-ended", + start: Number(start) + }; + return { + type: "bounded", + start: Number(start), + end: Number(end) + }; +}; +const resolveByteRange = (spec, size) => { + if (size === 0) return; + if (spec.type === "suffix") { + if (spec.length === 0) return; + return { + start: Math.max(size - spec.length, 0), + end: size - 1 + }; + } + const end = spec.type === "bounded" ? Math.min(spec.end, size - 1) : size - 1; + if (spec.start >= size || spec.start > end) return; + return { + start: spec.start, + end + }; +}; +const tryDecode = (str, decoder) => { + try { + return decoder(str); + } catch { + return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => { + try { + return decoder(match); + } catch { + return match; + } + }); + } +}; +const tryDecodeURI = (str) => tryDecode(str, decodeURI); +const serveStatic = (options = { root: "" }) => { + const root = options.root || ""; + const optionPath = options.path; + if (root !== "" && !(0, node_fs.existsSync)(root)) console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`); + return async (c, next) => { + if (c.finalized) return next(); + if (c.req.method !== "GET" && c.req.method !== "HEAD") return next(); + let filename; + if (optionPath) filename = optionPath; + else try { + if (!options.allowPercentInPath && c.req.path.includes("%")) throw new Error(); + filename = tryDecodeURI(c.req.path); + if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}|\\/.test(filename)) throw new Error(); + } catch { + await options.onNotFound?.(c.req.path, c); + return next(); + } + let path = (0, node_path.join)(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename); + let stats = getStats(path); + if (stats && stats.isDirectory()) { + const indexFile = options.index ?? "index.html"; + path = (0, node_path.join)(path, indexFile); + stats = getStats(path); + } + if (!stats) { + await options.onNotFound?.(path, c); + return next(); + } + const mimeType = (0, hono_utils_mime.getMimeType)(path); + c.header("Content-Type", mimeType || "application/octet-stream"); + if (options.precompressed && (!mimeType || mimeType === "application/octet-stream" || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) { + const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim())); + for (const encoding of ENCODINGS_ORDERED_KEYS) { + if (!acceptEncodingSet.has(encoding)) continue; + const precompressedStats = getStats(path + ENCODINGS[encoding]); + if (precompressedStats) { + c.header("Content-Encoding", encoding); + c.header("Vary", "Accept-Encoding", { append: true }); + stats = precompressedStats; + path = path + ENCODINGS[encoding]; + break; + } + } + } + let result; + const size = stats.size; + const range = c.req.header("range") || ""; + c.header("Last-Modified", stats.mtime.toUTCString()); + if (c.req.method === "HEAD") { + c.header("Content-Length", size.toString()); + c.status(200); + result = c.body(null); + } else if (!range) { + c.header("Content-Length", size.toString()); + result = c.body(require_utils_stream.createStreamBody((0, node_fs.createReadStream)(path)), 200); + } else { + c.header("Accept-Ranges", "bytes"); + const resolvedRange = resolveByteRange(parseByteRange(range) ?? { + type: "open-ended", + start: 0 + }, size); + if (!resolvedRange) { + c.header("Content-Range", `bytes */${size}`); + result = c.body(null, 416); + } else { + const { start, end } = resolvedRange; + const chunkSize = end - start + 1; + const stream = (0, node_fs.createReadStream)(path, { + start, + end + }); + c.header("Content-Length", chunkSize.toString()); + c.header("Content-Range", `bytes ${start}-${end}/${size}`); + result = c.body(require_utils_stream.createStreamBody(stream), 206); + } + } + await options.onFound?.(path, c); + return result; + }; +}; + +//#endregion +exports.serveStatic = serveStatic; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts new file mode 100644 index 000000000..f4dae1201 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.cts @@ -0,0 +1,19 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/serve-static.d.ts +type ServeStaticOptions = { + /** + * Root path. Relative path is based on current working directory from which the app was started. + */ + root?: string; + path?: string; + index?: string; + precompressed?: boolean; /** Allow percent signs in the routed request path. Defaults to false. */ + allowPercentInPath?: boolean; + rewriteRequestPath?: (path: string, c: Context) => string; + onFound?: (path: string, c: Context) => void | Promise; + onNotFound?: (path: string, c: Context) => void | Promise; +}; +declare const serveStatic: (options?: ServeStaticOptions) => MiddlewareHandler; +//#endregion +export { ServeStaticOptions, serveStatic }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts new file mode 100644 index 000000000..f4dae1201 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.d.mts @@ -0,0 +1,19 @@ +import { Context, Env, MiddlewareHandler } from "hono"; + +//#region src/serve-static.d.ts +type ServeStaticOptions = { + /** + * Root path. Relative path is based on current working directory from which the app was started. + */ + root?: string; + path?: string; + index?: string; + precompressed?: boolean; /** Allow percent signs in the routed request path. Defaults to false. */ + allowPercentInPath?: boolean; + rewriteRequestPath?: (path: string, c: Context) => string; + onFound?: (path: string, c: Context) => void | Promise; + onNotFound?: (path: string, c: Context) => void | Promise; +}; +declare const serveStatic: (options?: ServeStaticOptions) => MiddlewareHandler; +//#endregion +export { ServeStaticOptions, serveStatic }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs new file mode 100644 index 000000000..97505a187 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/serve-static.mjs @@ -0,0 +1,152 @@ +import { createStreamBody } from "./utils/stream.mjs"; +import { getMimeType } from "hono/utils/mime"; +import { createReadStream, existsSync, statSync } from "node:fs"; +import { join } from "node:path"; + +//#region src/serve-static.ts +const COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i; +const ENCODINGS = { + br: ".br", + zstd: ".zst", + gzip: ".gz" +}; +const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS); +const getStats = (path) => { + let stats; + try { + stats = statSync(path); + } catch {} + return stats; +}; +const BYTE_RANGE_PATTERN = /^(?:bytes=)?(?!-$)(\d*)-(\d*)$/; +const parseByteRange = (range) => { + const match = range.match(BYTE_RANGE_PATTERN); + if (!match) return; + const [, start, end] = match; + if (start === "") return { + type: "suffix", + length: Number(end) + }; + if (end === "") return { + type: "open-ended", + start: Number(start) + }; + return { + type: "bounded", + start: Number(start), + end: Number(end) + }; +}; +const resolveByteRange = (spec, size) => { + if (size === 0) return; + if (spec.type === "suffix") { + if (spec.length === 0) return; + return { + start: Math.max(size - spec.length, 0), + end: size - 1 + }; + } + const end = spec.type === "bounded" ? Math.min(spec.end, size - 1) : size - 1; + if (spec.start >= size || spec.start > end) return; + return { + start: spec.start, + end + }; +}; +const tryDecode = (str, decoder) => { + try { + return decoder(str); + } catch { + return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => { + try { + return decoder(match); + } catch { + return match; + } + }); + } +}; +const tryDecodeURI = (str) => tryDecode(str, decodeURI); +const serveStatic = (options = { root: "" }) => { + const root = options.root || ""; + const optionPath = options.path; + if (root !== "" && !existsSync(root)) console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`); + return async (c, next) => { + if (c.finalized) return next(); + if (c.req.method !== "GET" && c.req.method !== "HEAD") return next(); + let filename; + if (optionPath) filename = optionPath; + else try { + if (!options.allowPercentInPath && c.req.path.includes("%")) throw new Error(); + filename = tryDecodeURI(c.req.path); + if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}|\\/.test(filename)) throw new Error(); + } catch { + await options.onNotFound?.(c.req.path, c); + return next(); + } + let path = join(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename); + let stats = getStats(path); + if (stats && stats.isDirectory()) { + const indexFile = options.index ?? "index.html"; + path = join(path, indexFile); + stats = getStats(path); + } + if (!stats) { + await options.onNotFound?.(path, c); + return next(); + } + const mimeType = getMimeType(path); + c.header("Content-Type", mimeType || "application/octet-stream"); + if (options.precompressed && (!mimeType || mimeType === "application/octet-stream" || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) { + const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim())); + for (const encoding of ENCODINGS_ORDERED_KEYS) { + if (!acceptEncodingSet.has(encoding)) continue; + const precompressedStats = getStats(path + ENCODINGS[encoding]); + if (precompressedStats) { + c.header("Content-Encoding", encoding); + c.header("Vary", "Accept-Encoding", { append: true }); + stats = precompressedStats; + path = path + ENCODINGS[encoding]; + break; + } + } + } + let result; + const size = stats.size; + const range = c.req.header("range") || ""; + c.header("Last-Modified", stats.mtime.toUTCString()); + if (c.req.method === "HEAD") { + c.header("Content-Length", size.toString()); + c.status(200); + result = c.body(null); + } else if (!range) { + c.header("Content-Length", size.toString()); + result = c.body(createStreamBody(createReadStream(path)), 200); + } else { + c.header("Accept-Ranges", "bytes"); + const resolvedRange = resolveByteRange(parseByteRange(range) ?? { + type: "open-ended", + start: 0 + }, size); + if (!resolvedRange) { + c.header("Content-Range", `bytes */${size}`); + result = c.body(null, 416); + } else { + const { start, end } = resolvedRange; + const chunkSize = end - start + 1; + const stream = createReadStream(path, { + start, + end + }); + c.header("Content-Length", chunkSize.toString()); + c.header("Content-Range", `bytes ${start}-${end}/${size}`); + result = c.body(createStreamBody(stream), 206); + } + } + await options.onFound?.(path, c); + return result; + }; +}; + +//#endregion +export { serveStatic }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs new file mode 100644 index 000000000..87b8e95ab --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.cjs @@ -0,0 +1,8 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +const require_constants = require('../constants-BXAKTxRC.cjs'); + +//#region src/utils/response.ts +const RESPONSE_ALREADY_SENT = new Response(null, { headers: { [require_constants.X_ALREADY_SENT]: "true" } }); + +//#endregion +exports.RESPONSE_ALREADY_SENT = RESPONSE_ALREADY_SENT; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts new file mode 100644 index 000000000..169a3b551 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.cts @@ -0,0 +1,4 @@ +//#region src/utils/response.d.ts +declare const RESPONSE_ALREADY_SENT: Response; +//#endregion +export { RESPONSE_ALREADY_SENT }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts new file mode 100644 index 000000000..169a3b551 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.d.mts @@ -0,0 +1,4 @@ +//#region src/utils/response.d.ts +declare const RESPONSE_ALREADY_SENT: Response; +//#endregion +export { RESPONSE_ALREADY_SENT }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs new file mode 100644 index 000000000..92495c1f5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/response.mjs @@ -0,0 +1,7 @@ +import { t as X_ALREADY_SENT } from "../constants-BLSFu_RU.mjs"; + +//#region src/utils/response.ts +const RESPONSE_ALREADY_SENT = new Response(null, { headers: { [X_ALREADY_SENT]: "true" } }); + +//#endregion +export { RESPONSE_ALREADY_SENT }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs new file mode 100644 index 000000000..b4f4f7d42 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.cjs @@ -0,0 +1,65 @@ +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +let node_stream = require("node:stream"); +let node_process = require("node:process"); + +//#region src/utils/stream.ts +const pr54206Applied = () => { + const [major, minor] = node_process.versions.node.split(".").map((component) => parseInt(component)); + return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18; +}; +const useReadableToWeb = pr54206Applied(); +const createStreamBody = (stream, useNativeReadableToWeb = useReadableToWeb) => { + if (useNativeReadableToWeb) return node_stream.Readable.toWeb(stream); + let controller; + let settled = false; + const cleanup = () => { + stream.off("data", onData); + stream.off("error", onError); + stream.off("end", onTerminate); + stream.off("close", onTerminate); + }; + const settle = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback?.(); + }; + const onData = (chunk) => { + if (settled || !controller) return; + controller.enqueue(chunk); + if ((controller.desiredSize ?? 0) <= 0) stream.pause(); + }; + const onError = (error) => { + settle(() => { + controller?.error(error); + }); + }; + const onTerminate = () => { + settle(() => { + controller?.close(); + }); + }; + return new ReadableStream({ + start(streamController) { + controller = streamController; + stream.on("data", onData); + stream.on("error", onError); + stream.on("end", onTerminate); + stream.on("close", onTerminate); + stream.pause(); + }, + pull() { + if (!settled) stream.resume(); + }, + cancel() { + settle(); + const ignoreError = () => {}; + stream.on("error", ignoreError); + stream.once("close", () => stream.off("error", ignoreError)); + stream.destroy(); + } + }); +}; + +//#endregion +exports.createStreamBody = createStreamBody; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts new file mode 100644 index 000000000..5f92c4f79 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.cts @@ -0,0 +1,6 @@ +import { ReadStream } from "node:fs"; + +//#region src/utils/stream.d.ts +declare const createStreamBody: (stream: ReadStream, useNativeReadableToWeb?: boolean) => ReadableStream; +//#endregion +export { createStreamBody }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts new file mode 100644 index 000000000..5f92c4f79 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.d.mts @@ -0,0 +1,6 @@ +import { ReadStream } from "node:fs"; + +//#region src/utils/stream.d.ts +declare const createStreamBody: (stream: ReadStream, useNativeReadableToWeb?: boolean) => ReadableStream; +//#endregion +export { createStreamBody }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs new file mode 100644 index 000000000..8d18a9a2e --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/dist/utils/stream.mjs @@ -0,0 +1,64 @@ +import { Readable } from "node:stream"; +import { versions } from "node:process"; + +//#region src/utils/stream.ts +const pr54206Applied = () => { + const [major, minor] = versions.node.split(".").map((component) => parseInt(component)); + return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18; +}; +const useReadableToWeb = pr54206Applied(); +const createStreamBody = (stream, useNativeReadableToWeb = useReadableToWeb) => { + if (useNativeReadableToWeb) return Readable.toWeb(stream); + let controller; + let settled = false; + const cleanup = () => { + stream.off("data", onData); + stream.off("error", onError); + stream.off("end", onTerminate); + stream.off("close", onTerminate); + }; + const settle = (callback) => { + if (settled) return; + settled = true; + cleanup(); + callback?.(); + }; + const onData = (chunk) => { + if (settled || !controller) return; + controller.enqueue(chunk); + if ((controller.desiredSize ?? 0) <= 0) stream.pause(); + }; + const onError = (error) => { + settle(() => { + controller?.error(error); + }); + }; + const onTerminate = () => { + settle(() => { + controller?.close(); + }); + }; + return new ReadableStream({ + start(streamController) { + controller = streamController; + stream.on("data", onData); + stream.on("error", onError); + stream.on("end", onTerminate); + stream.on("close", onTerminate); + stream.pause(); + }, + pull() { + if (!settled) stream.resume(); + }, + cancel() { + settle(); + const ignoreError = () => {}; + stream.on("error", ignoreError); + stream.once("close", () => stream.off("error", ignoreError)); + stream.destroy(); + } + }); +}; + +//#endregion +export { createStreamBody }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/node-server/package.json b/tests/fixtures/npm/node_modules/@hono/node-server/package.json new file mode 100644 index 000000000..b3ab47d33 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/node-server/package.json @@ -0,0 +1,128 @@ +{ + "name": "@hono/node-server", + "version": "2.1.5", + "description": "Node.js Adapter for Hono", + "main": "dist/index.mjs", + "type": "module", + "types": "dist/index.d.mts", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./serve-static": { + "import": { + "types": "./dist/serve-static.d.mts", + "default": "./dist/serve-static.mjs" + }, + "require": { + "types": "./dist/serve-static.d.cts", + "default": "./dist/serve-static.cjs" + } + }, + "./utils/*": { + "import": { + "types": "./dist/utils/*.d.mts", + "default": "./dist/utils/*.mjs" + }, + "require": { + "types": "./dist/utils/*.d.cts", + "default": "./dist/utils/*.cjs" + } + }, + "./conninfo": { + "import": { + "types": "./dist/conninfo.d.mts", + "default": "./dist/conninfo.mjs" + }, + "require": { + "types": "./dist/conninfo.d.cts", + "default": "./dist/conninfo.cjs" + } + }, + "./early-hints": { + "import": { + "types": "./dist/early-hints.d.mts", + "default": "./dist/early-hints.mjs" + }, + "require": { + "types": "./dist/early-hints.d.cts", + "default": "./dist/early-hints.cjs" + } + } + }, + "typesVersions": { + "*": { + ".": [ + "./dist/index.d.mts" + ], + "serve-static": [ + "./dist/serve-static.d.mts" + ], + "utils/*": [ + "./dist/utils/*.d.mts" + ], + "conninfo": [ + "./dist/conninfo.d.mts" + ], + "early-hints": [ + "./dist/early-hints.d.mts" + ] + } + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/honojs/node-server.git" + }, + "homepage": "https://github.com/honojs/node-server", + "author": "Yusuke Wada (https://github.com/yusukebe)", + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public", + "provenance": true + }, + "engines": { + "node": ">=20" + }, + "devDependencies": { + "@hono/eslint-config": "^1.0.1", + "@types/node": "^20.10.0", + "@types/ws": "^8.18.1", + "@whatwg-node/fetch": "^0.9.14", + "eslint": "^9.10.0", + "hono": "^4.13.9", + "np": "^11.2.1", + "prettier": "^3.2.4", + "publint": "^0.3.18", + "tsdown": "^0.20.3", + "typescript": "^5.3.2", + "vitest": "^4.0.18", + "ws": "^8.19.0" + }, + "peerDependencies": { + "hono": "^4 || ^5.0.0-0" + }, + "scripts": { + "test": "vitest", + "build": "tsdown --external hono", + "watch": "tsdown --watch", + "postbuild": "publint", + "prerelease": "pnpm run build && pnpm run test --run", + "release": "np --no-publish", + "typecheck": "tsc --noEmit", + "lint": "eslint src test", + "lint:fix": "eslint src test --fix", + "format": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"", + "format:fix": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"" + } +} \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/LICENSE b/tests/fixtures/npm/node_modules/hono/LICENSE new file mode 100644 index 000000000..bd174facc --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 - present, Yusuke Wada and Hono contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/npm/node_modules/hono/dist/helper/websocket/index.js b/tests/fixtures/npm/node_modules/hono/dist/helper/websocket/index.js new file mode 100644 index 000000000..620de4105 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/helper/websocket/index.js @@ -0,0 +1,53 @@ +//#region src/helper/websocket/index.ts +/** +* A context for controlling WebSockets +*/ +var WSContext = class { + #init; + constructor(init) { + this.#init = init; + this.raw = init.raw; + this.url = init.url ? new URL(init.url) : null; + this.protocol = init.protocol ?? null; + } + send(source, options) { + this.#init.send(source, options ?? {}); + } + raw; + binaryType = "arraybuffer"; + get readyState() { + return this.#init.readyState; + } + url; + protocol; + close(code, reason) { + this.#init.close(code, reason); + } +}; +const createWSMessageEvent = (source) => { + return new MessageEvent("message", { data: source }); +}; +/** +* Create a WebSocket adapter/helper +*/ +const defineWebSocketHelper = (handler) => { + return ((...args) => { + if (typeof args[0] === "function") { + const [createEvents, options] = args; + return async function upgradeWebSocket(c, next) { + const result = await handler(c, await createEvents(c), options); + if (result) return result; + await next(); + }; + } else { + const [c, events, options] = args; + return (async () => { + const upgraded = await handler(c, events, options); + if (!upgraded) throw new Error("Failed to upgrade WebSocket"); + return upgraded; + })(); + } + }); +}; +//#endregion +export { WSContext, createWSMessageEvent, defineWebSocketHelper }; diff --git a/tests/fixtures/npm/node_modules/hono/package.json b/tests/fixtures/npm/node_modules/hono/package.json new file mode 100644 index 000000000..6264c824a --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/package.json @@ -0,0 +1,12 @@ +{ + "name": "hono", + "version": "4.13.13", + "description": "Test fixture: only the hono/ws helper that @hono/node-server's index imports.", + "type": "module", + "license": "MIT", + "exports": { + "./ws": { + "import": "./dist/helper/websocket/index.js" + } + } +} From 1940e063048c564d7ac5ee7835fe2869f3faf135 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 19:18:53 -0500 Subject: [PATCH 030/409] Cancel streamed bodies on disconnect and type Response.json headers A response whose client disconnects while its body streams now cancels the body's stream when the response closes, as the package does, so the producer stops instead of being read until it ends. A response that fails closes too and is cancelled the same way. Response.json(data, { headers }) with init headers is written from its Headers, as the package's Response.json passes them on as a Headers object with the JSON content type added when they lack one. Before, the cached path wrote only the init record and sent no Content-Type. The hono-node-server case covers both: Response.json with and without an init content type, and a client that destroys the response after the first chunk of an endless stream. --- .../compiler/shims/hono-node-server/v2.ts | 30 ++++++++--- packages/runtime/src/scr_fetch.c | 6 ++- .../npm/cases/hono-node-server/main.ts | 52 ++++++++++++++++++- 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts index 3708f0e4c..0402fe0af 100644 --- a/packages/compiler/shims/hono-node-server/v2.ts +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -197,20 +197,34 @@ function nextTick(): Promise { }); } +/** Writes the rest of a body stream. As in the package, a response that + * closes before the stream ends (the client disconnected) cancels the + * stream, so its producer stops; the pending read then answers done. A + * response that fails closes too, and its stream is cancelled the same way + * (the package passes the error as the reason). The listener stays attached + * and does nothing once the stream has ended. */ async function pump( reader: ReadableStreamDefaultReader, outgoing: ServerResponse, pending: Promise | undefined, ): Promise { - let read = pending ?? reader.read(); - for (;;) { - const { done, value } = await read; - if (done) { - outgoing.end(); - return; + let finished = false; + outgoing.on("close", () => { + if (!finished) reader.cancel().catch(() => {}); + }); + try { + let read = pending ?? reader.read(); + for (;;) { + const { done, value } = await read; + if (done) { + outgoing.end(); + return; + } + outgoing.write(value); + read = reader.read(); } - outgoing.write(value); - read = reader.read(); + } finally { + finished = true; } } diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 18daed32f..24cb512b6 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -4761,7 +4761,10 @@ static ScrDyn *sf_response_json_serialize(const ScrDyn *value) { * {"content-type": "application/json"} record is form 3), a Headers object * or pair list form 2, and a plain record form 3, whose names and values * *record receives in order (an array value contributes one entry per - * element, as writeHead writes it). 0 for any other init. */ + * element, as writeHead writes it). Response.json with init headers is + * form 2 whatever their shape: the package's Response.json passes them on + * as a Headers object, adding the JSON content type when they lack one. + * 0 for any other init. */ static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, ScrArr **record) { if (!headers || headers->kind == SCR_DYN_UNDEF) { @@ -4770,6 +4773,7 @@ static int8_t sf_light_header_form(const ScrDyn *headers, bool json_body, sf_push_header_text(*record, "content-type", "application/json", 16); return 3; } + if (json_body) return 2; if (headers->kind == SCR_DYN_ARR || (headers->kind == SCR_DYN_HANDLE && headers->v.handle.tag == SCR_DYNH_FETCH_HEADERS)) diff --git a/tests/fixtures/npm/cases/hono-node-server/main.ts b/tests/fixtures/npm/cases/hono-node-server/main.ts index ed732f64e..b47dae8e3 100644 --- a/tests/fixtures/npm/cases/hono-node-server/main.ts +++ b/tests/fixtures/npm/cases/hono-node-server/main.ts @@ -2,7 +2,8 @@ // serving a fetch handler: a raw HTTP client prints each exchange's status // line, wire-order headers (Date dropped) and body, covering buffered and // streamed bodies, header forms, cookies, request bodies, URL handling, -// HEAD, empty statuses, and handler errors. +// HEAD, empty statuses, handler errors, and a client that disconnects +// during a streamed body. import * as http from "node:http"; import type { AddressInfo } from "node:net"; import { getRequestListener, RequestError, serve } from "@hono/node-server"; @@ -35,6 +36,27 @@ function readyStream(parts: string[]): ReadableStream { }); } +// A stream that produces until it is cancelled (bounded, so a missed cancel +// ends the program instead of hanging it). +let reportCancel: (reason: string) => void = () => {}; +const endlessCancelled = new Promise((resolve) => { + reportCancel = resolve; +}); +function endlessStream(): ReadableStream { + const encoder = new TextEncoder(); + let pulls = 0; + return new ReadableStream({ + async pull(controller) { + await delay(5); + if (++pulls > 400) controller.close(); + else controller.enqueue(encoder.encode("tick\n")); + }, + cancel(reason) { + reportCancel(`cancelled with ${String(reason)}`); + }, + }); +} + async function handler(request: Request): Promise { const url = new URL(request.url); switch (url.pathname) { @@ -101,6 +123,12 @@ async function handler(request: Request): Promise { return new Response("rec", { status: 201, headers: { "X-Custom": "a", "Content-Type": "text/html" } }); case "/light-json": return Response.json({ ok: true, n: [1, 2] }); + case "/light-json-headers": + return Response.json({ ok: true }, { status: 201, headers: { "X-Test": "yes" } }); + case "/light-json-typed": + return Response.json([1], { headers: { "Content-Type": "application/vnd.test+json" } }); + case "/endless": + return new Response(endlessStream(), { headers: new Headers({ "content-type": "text/plain" }) }); case "/light-touched": { const r = new Response("touched"); r.headers.set("x-touched", "1"); @@ -184,6 +212,8 @@ const exchanges: Exchange[] = [ { method: "GET", path: "/light-record" }, { method: "HEAD", path: "/light-record" }, { method: "GET", path: "/light-json" }, + { method: "GET", path: "/light-json-headers" }, + { method: "GET", path: "/light-json-typed" }, { method: "GET", path: "/light-touched" }, { method: "GET", path: "/light-record-touched" }, { method: "GET", path: "/materialized" }, @@ -198,10 +228,30 @@ const exchanges: Exchange[] = [ { method: "get", path: "/lower-method" }, ]; +/** Reads the first chunk of /endless, then destroys the response: the + * server cancels the response's stream. */ +function disconnectMidStream(port: number): Promise { + return new Promise((resolve) => { + const req = http.request({ host: "127.0.0.1", port, method: "GET", path: "/endless" }, (res) => { + res.once("data", () => { + res.destroy(); + }); + }); + req.end(); + const timeout = new Promise((done) => { + setTimeout(() => done("not cancelled"), 3000); + }); + void Promise.race([endlessCancelled, timeout]).then((outcome) => { + resolve(`### disconnect GET /endless\n${outcome}\n`); + }); + }); +} + const server = serve({ fetch: handler, port: 0, hostname: "127.0.0.1" }, (info: AddressInfo) => { console.log("listening", info.family, info.address, typeof info.port); void (async () => { for (const ex of exchanges) console.log(await exchange(info.port, ex)); + console.log(await disconnectMidStream(info.port)); server.close(() => { console.log("closed"); startSecond(); From 56f70c47b1ae251451c8f1bb42da5fd859c5493e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 20:06:30 -0700 Subject: [PATCH 031/409] Answer 'in' on native Node handles from their declared members `"closeAllConnections" in server` (the feature test servers use before calling it) was refused for an http.Server and the other native handles. A member Node's declarations give the class is on its prototype, so the test now answers true after evaluating the receiver. Undeclared names keep the refusal, since internal fields can still be present. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/relational.ts | 48 +++++++++++++++++++ .../server/cases/http-in-members/driver.mjs | 7 +++ .../server/cases/http-in-members/main.ts | 18 +++++++ 3 files changed, 73 insertions(+) create mode 100644 tests/fixtures/server/cases/http-in-members/driver.mjs create mode 100644 tests/fixtures/server/cases/http-in-members/main.ts diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 0eb8ae653..edec5ba5f 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1762,6 +1762,25 @@ export function lowerInExpression( }; } } + // A native Node handle (an http.Server, a socket, a child process...): + // a member Node's declarations give the class is on its prototype, so + // `"closeAllConnections" in server` answers true. Undeclared names can + // still be real (internal `_`-prefixed fields), so they keep the fence. + if ( + NATIVE_HANDLE_KINDS.has(recv.type.kind) && + nodeDeclaredMember(lowerer, lowerer.typeOf(expr.right), key) + ) { + const answer: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc }; + return isSafeToDiscard(recv) + ? answer + : { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: recv, loc }], + result: answer, + type: BOOL, + loc, + }; + } if (recv.type.kind !== "record") { lowerer.unsupported( "SC1090", @@ -2059,3 +2078,32 @@ function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): stri const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1; return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`; } + +/** IR kinds of native Node handles whose members come from Node's own + * declarations. */ +const NATIVE_HANDLE_KINDS: ReadonlySet = new Set([ + "netServer", + "netSocket", + "httpReq", + "httpRes", + "httpClientReq", + "child", + "dgramSocket", + "fsWatcher", + "http2Session", + "http2Stream", +]); + +/** Whether every constituent of `t` declares a required member `key` in + * a declaration file (Node's @types/node). */ +function nodeDeclaredMember(lowerer: Lowerer, t: ts.Type, key: string): boolean { + const parts = t.isUnionType() ? ts.constituentTypes(t) : [t]; + return parts.every((part) => { + const prop = lowerer.checker.getPropertyOfType(part, key); + if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) !== 0) return false; + return ( + prop.declarations.length > 0 && + prop.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile) + ); + }); +} diff --git a/tests/fixtures/server/cases/http-in-members/driver.mjs b/tests/fixtures/server/cases/http-in-members/driver.mjs new file mode 100644 index 000000000..9c989bb70 --- /dev/null +++ b/tests/fixtures/server/cases/http-in-members/driver.mjs @@ -0,0 +1,7 @@ +import http from "node:http"; +const port = Number(process.argv[2]); +http.get({ host: "127.0.0.1", port, path: "/" }, (res) => { + let body = ""; + res.on("data", (c) => (body += c)); + res.on("end", () => console.log(res.statusCode, body)); +}); diff --git a/tests/fixtures/server/cases/http-in-members/main.ts b/tests/fixtures/server/cases/http-in-members/main.ts new file mode 100644 index 000000000..80babfc13 --- /dev/null +++ b/tests/fixtures/server/cases/http-in-members/main.ts @@ -0,0 +1,18 @@ +// `in` tests on native Node handles answer from Node's declared members. +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; + +const server = createServer((req, res) => { + console.log("request", "method" in req, "setHeader" in res, "writeHead" in res); + res.end("ok"); +}); +console.log("closeAllConnections" in server, "closeIdleConnections" in server, "listen" in server); +server.listen(0, "127.0.0.1", () => { + process.stderr.write(`PORT ${(server.address() as AddressInfo).port}\n`); +}); +server.on("request", () => { + setTimeout(() => { + if ("closeAllConnections" in server) server.closeAllConnections(); + server.close(() => console.log("closed")); + }, 10); +}); From 9ab876faa9771df730c446628aad8665814bf68e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 20:15:30 -0700 Subject: [PATCH 032/409] Compile the @hono/node-server shim under any program's stream types The shim compiles inside each program that imports the package, and under some lib and @types/node settings ReadableStreamReadResult lowered as unknown while reader.read() produced a record, so the buffered-read path refused (SC2003). The shim spells out its read-result record and types the body reader. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/shims/hono-node-server/v2.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts index 0402fe0af..94a9c8d85 100644 --- a/packages/compiler/shims/hono-node-server/v2.ts +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -185,7 +185,10 @@ function buildOutgoingHttpHeaders( return res; } -type ReadResult = ReadableStreamReadResult; +/* Spelled out rather than ReadableStreamReadResult, whose + * declaration depends on the program's lib and @types/node settings: the + * shim compiles inside every program that imports the package. */ +type ReadResult = { done: boolean; value?: Uint8Array }; function readWithoutBlocking(read: Promise): Promise { return Promise.race([read, Promise.resolve().then(() => Promise.resolve(undefined))]); @@ -220,7 +223,7 @@ async function pump( outgoing.end(); return; } - outgoing.write(value); + outgoing.write(value!); read = reader.read(); } } finally { @@ -352,7 +355,7 @@ async function writeResponse( const body = res.body; const record = buildOutgoingHttpHeaders(res.headers, body !== null); if (body !== null) { - const reader = body.getReader(); + const reader: ReadableStreamDefaultReader = body.getReader(); const values: Uint8Array[] = []; let done = false; let pending: Promise | undefined = undefined; From b161d0be137fa85db1f12ab0e54afc1e1d94f9cf Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 22:46:00 -0500 Subject: [PATCH 033/409] Authenticate empty AES-CCM messages in decipher A CCM decipher skipped its one update() when the data was empty, and final() accepted when an update had run without failing, so it never compared the tag. setAuthTag() with any tag of the right length, then update() with empty data, then final(), succeeded: anyone could forge an empty message, or one with different AAD or a different declared plaintextLength, without the key. A decipher's update() now runs the authenticated decryption for empty data too. As in OpenSSL, the tag covers the nonce, the AAD and the length, so the right tag still opens an empty message and any other tag, AAD or length fails in final() with "Unsupported state or unable to authenticate data". final() succeeds only after a tag comparison has succeeded. A cipher's empty update() is unchanged. --- packages/runtime/src/scr_cipher.c | 18 +++++++----- tests/corpus/7155-cipher-aes-ccm.cjs | 41 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index 2894af8e2..a2db11434 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -201,6 +201,7 @@ typedef struct ScCipher { bool ccm_iv_set, ccm_len_set, ccm_tag_set; bool ccm_updated; /* a decipher's one update() ran; key wrap: the one update() */ bool ccm_auth_failed; /* that update failed to authenticate */ + bool ccm_verified; /* a decipher's tag matched the one it computed */ double ccm_max_message; bool wrap_decrypt_block; /* key wrap: the AES key schedule decrypts */ ScrStr *decoder; /* the StringDecoder encoding once string output starts */ @@ -1284,6 +1285,7 @@ static bool sc_ccm_data(ScCipher *c, const unsigned char *in, unsigned char *out return false; } c->ccm_iv_set = c->ccm_tag_set = c->ccm_len_set = false; + c->ccm_verified = true; return true; } @@ -1294,8 +1296,10 @@ static bool sc_ccm_length_ok(const ScCipher *c, double len) { } /* update(): a decipher takes one, and its authentication failure leaves - * empty output for final() to report. Node passes empty data to OpenSSL - * without a buffer, which the provider skips. */ + * empty output for final() to report. A decipher authenticates even an + * empty message, as OpenSSL does: its tag still covers the nonce, the AAD + * and the length. A cipher's empty update is skipped, and final() then + * runs the empty message. */ static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len) { if (!sc_ccm_length_ok(c, (double)len)) return NULL; if (c->decrypt && c->ccm_updated) { @@ -1304,7 +1308,7 @@ static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len) } ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)(len ? len : 1)); out->is_buffer = true; - bool ok = len == 0 || sc_ccm_data(c, in, out->data, len); + bool ok = (len == 0 && !c->decrypt) || sc_ccm_data(c, len ? in : out->data, out->data, len); if (c->decrypt) { c->ccm_updated = true; if (!ok) c->ccm_auth_failed = true; @@ -1321,11 +1325,11 @@ static ScrBytes *sc_ccm_update(ScCipher *c, const unsigned char *in, size_t len) return out; } -/* final(): a decipher reports the update's verdict; a cipher runs the - * provider's final (an empty message when no data was encrypted) and - * reads the tag, which retires the provider's state. */ +/* final(): a decipher succeeds only when its update verified the tag; a + * cipher runs the provider's final (an empty message when no data was + * encrypted) and reads the tag, which retires the provider's state. */ static bool sc_ccm_final(ScCipher *c) { - if (c->decrypt) return c->tag_state == SC_TAG_SET_BY_USER && c->ccm_updated && !c->ccm_auth_failed; + if (c->decrypt) return c->tag_state == SC_TAG_SET_BY_USER && c->ccm_verified; unsigned char dummy = 0, sink = 0; if (c->ccm_iv_set && !c->ccm_tag_set && !sc_ccm_data(c, &dummy, &sink, 0)) return false; if (!c->ccm_tag_set || !sc_ccm_tag(c, c->tag)) return false; diff --git a/tests/corpus/7155-cipher-aes-ccm.cjs b/tests/corpus/7155-cipher-aes-ccm.cjs index 2ed95c382..5e6407e52 100644 --- a/tests/corpus/7155-cipher-aes-ccm.cjs +++ b/tests/corpus/7155-cipher-aes-ccm.cjs @@ -150,6 +150,47 @@ console.log("open empty message", (() => { return JSON.stringify(Buffer.concat([d.update(Buffer.alloc(0)), d.final()]).toString()); })()); +// An empty message is still authenticated: its tag covers the nonce, the +// AAD and the declared length, so a wrong tag, other AAD or another +// plaintextLength fails in final(), as does a decipher without an update. +const empty = Buffer.alloc(0); +const sealEmpty = (aadBuf) => { + const c = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + if (aadBuf) c.setAAD(aadBuf, { plaintextLength: 0 }); + c.update(empty); + c.final(); + return c.getAuthTag(); +}; +const emptyTag = sealEmpty(aad); +const emptyTagNoAad = sealEmpty(null); +const flipped = Buffer.from(emptyTag); +flipped[0] ^= 1; +const openEmpty = (steps) => { + const d = crypto.createDecipheriv("aes-128-ccm", key, iv, { authTagLength: 16 }); + const out = []; + try { + for (const step of steps) { + const v = step(d); + if (Buffer.isBuffer(v)) out.push(v); + } + return `ok ${JSON.stringify(Buffer.concat(out).toString())}`; + } catch (e) { + return `${e.name} ${e.code} ${JSON.stringify(e.message)} after ${out.length} outputs`; + } +}; +console.log("empty tag", emptyTag.toString("hex"), emptyTagNoAad.toString("hex")); +console.log("empty, right tag", openEmpty([setAuthTag(emptyTag), setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, wrong tag", openEmpty([setAuthTag(Buffer.alloc(16, 9)), setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, flipped tag", openEmpty([setAuthTag(flipped), setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, other aad", openEmpty([setAuthTag(emptyTag), setAAD(Buffer.from("other"), { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, plaintextLength 5", openEmpty([setAuthTag(emptyTag), setAAD(aad, { plaintextLength: 5 }), update(empty), final()])); +console.log("empty, no update", openEmpty([setAuthTag(emptyTag), setAAD(aad, { plaintextLength: 0 }), final()])); +console.log("empty, no tag", openEmpty([setAAD(aad, { plaintextLength: 0 }), update(empty), final()])); +console.log("empty, no aad, right tag", openEmpty([setAuthTag(emptyTagNoAad), update(empty), final()])); +console.log("empty, no aad, wrong tag", openEmpty([setAuthTag(Buffer.alloc(16, 9)), update(empty), final()])); +console.log("empty, aad dropped", openEmpty([setAuthTag(emptyTag), update(empty), final()])); +console.log("empty, a string update", openEmpty([setAuthTag(Buffer.alloc(16, 9)), (d) => d.update(""), final()])); + // As a stream: one write and the end. const stream = crypto.createCipheriv("aes-128-ccm", key, iv, { authTagLength: 12 }); const chunks = []; From c009b582e2da398c6e229fab779b9354325fae96 Mon Sep 17 00:00:00 2001 From: Luis Alvarez D Date: Thu, 22 Oct 2026 23:05:28 -0500 Subject: [PATCH 034/409] Support split CommonJS require bindings --- .../frontend/lowering/cjs-inline-require.ts | 323 ++++++++++++++++++ .../lowering/expressions/identifiers.ts | 26 ++ .../src/frontend/lowering/lower-modules.ts | 24 +- .../src/frontend/lowering/lower-stmts.ts | 52 +++ .../compiler/src/frontend/lowering/lowerer.ts | 23 +- .../split-cjs-require-binding/main.cjs.json | 12 + .../split-cjs-require-binding/chain-wrap.cjs | 3 + .../split-cjs-require-binding/chain.cjs | 1 + .../corpus/split-cjs-require-binding/lib.cjs | 5 + .../corpus/split-cjs-require-binding/main.cjs | 55 +++ .../split-cjs-require-binding/package.json | 1 + .../reexport-extra.cjs | 6 + .../split-cjs-require-binding/reexport.cjs | 4 + .../split-cjs-require-binding/whole.cjs | 4 + .../split-cjs-require-binding/main.cjs.json | 1 + 15 files changed, 536 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json create mode 100644 tests/corpus/split-cjs-require-binding/chain-wrap.cjs create mode 100644 tests/corpus/split-cjs-require-binding/chain.cjs create mode 100644 tests/corpus/split-cjs-require-binding/lib.cjs create mode 100644 tests/corpus/split-cjs-require-binding/main.cjs create mode 100644 tests/corpus/split-cjs-require-binding/package.json create mode 100644 tests/corpus/split-cjs-require-binding/reexport-extra.cjs create mode 100644 tests/corpus/split-cjs-require-binding/reexport.cjs create mode 100644 tests/corpus/split-cjs-require-binding/whole.cjs create mode 100644 tests/harness/corpus-costs/split-cjs-require-binding/main.cjs.json diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 2b28c6533..eb05550c0 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -28,6 +28,7 @@ import { isModuleExportsAccess, locOf, npmStaticDepSf7, + requireBindingEarlyUse7, requireSpecOf, resolveImport, } from "../program.js"; @@ -43,6 +44,328 @@ export interface InlineRequire { dep: ts.SourceFile; } +/** A top-level `var m; m = require("./x")` binding. Node evaluates the + * require at the assignment; the binding is alias plumbing exactly like + * `const m = require("./x")` for every later read. */ +export interface SplitRequireBinding { + decl: ts.VariableDeclaration; + assign: ts.BinaryExpression; + call: ts.CallExpression; + spec: string; + dep: ts.SourceFile; +} + +function symbolOfIdentifierValue(lowerer: Lowerer, ident: ts.Identifier): ts.Symbol | undefined { + const parent = ident.parent; + // A destructuring shorthand target resolves to the property symbol at + // the name, but reads/writes the variable through its value symbol. + if (ts.isShorthandPropertyAssignment(parent) && parent.name === ident) { + return ( + lowerer.checker.getShorthandAssignmentValueSymbol(parent) ?? + lowerer.checker.getSymbolAtLocation(ident) + ); + } + return lowerer.checker.getSymbolAtLocation(ident); +} + +function destructuringContainerOf(node: ts.Node): ts.Node | null { + const parent = node.parent; + if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) return parent; + if (ts.isPropertyAssignment(parent) && parent.initializer === node) return parent; + if (ts.isSpreadElement(parent) && parent.expression === node) return parent; + if (ts.isSpreadAssignment(parent) && parent.expression === node) return parent; + return null; +} + +function writeTargetOf(node: ts.Node): ts.Node | null { + let target = node; + for (;;) { + const parent = target.parent; + if ( + ts.isParenthesizedExpression(parent) || + ts.isArrayLiteralExpression(parent) || + ts.isObjectLiteralExpression(parent) + ) { + target = parent; + continue; + } + const container = destructuringContainerOf(target); + if (container === null) break; + target = container; + } + const parent = target.parent; + if ( + ts.isBinaryExpression(parent) && + parent.left === target && + ts.isAssignmentOperator(parent.operatorToken.kind) + ) + return parent; + if ( + (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) && + parent.operand === target && + (parent.operator === ts.SyntaxKind.PlusPlusToken || + parent.operator === ts.SyntaxKind.MinusMinusToken) + ) + return parent; + if ((ts.isForInStatement(parent) || ts.isForOfStatement(parent)) && parent.initializer === target) + return parent; + return null; +} + +/** The split form of a top-level require binding: `var m; m = require("./x")`. + * Exactly one write, at a top-level `m = require("")` statement. + * Null for the ordinary `const m = require("./x")` declaration and for + * every form that needs runtime module selection. */ +export function splitRequireBindingOf( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): SplitRequireBinding | null { + // Cheap shape gate before the cache: ordinary declarations never enter the + // per-Lowerer map. + if (decl.initializer !== undefined || !ts.isIdentifier(decl.name)) return null; + const list = decl.parent as ts.VariableDeclarationList; + if ((list.flags & ts.NodeFlags.Const) !== 0) return null; + const stmt = list.parent; + if (!ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) return null; + const cached = lowerer.cjsSplitRequireBindings.get(decl); + if (cached !== undefined) return cached; + const result = computeSplitRequireBinding(lowerer, decl); + lowerer.cjsSplitRequireBindings.set(decl, result); + return result; +} + +function computeSplitRequireBinding( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): SplitRequireBinding | null { + const sf = ts.getSourceFileOfNode(decl); + if (!isCjsJsFile(sf, lowerer.program)) return null; + if (!sf.text.includes("require")) return null; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + if (symbol === undefined) return null; + // Merged declarations make the binding's initialization order ambiguous; + // keep ordinary storage instead of guessing. + let declarations = 0; + for (const other of symbol.declarations) { + if (!ts.isVariableDeclaration(other) || ++declarations > 1) return null; + } + if (declarations !== 1) return null; + + // Find the one top-level `m = ...` statement first: most uninitialized + // variables never reach that shape, so they avoid the full-file write scan. + let assign: ts.BinaryExpression | null = null; + for (const stmt of sf.statements.nodes) { + if (!ts.isExpressionStatement(stmt)) continue; + const expr = stmt.expression; + if ( + !ts.isBinaryExpression(expr) || + expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(expr.left) || + expr.left.text !== decl.name.text || + lowerer.checker.getSymbolAtLocation(expr.left) !== symbol + ) + continue; + if (assign !== null) return null; + assign = expr; + } + if (assign === null) return null; + + const req = inlineProgramRequireOf(lowerer, assign.right); + if (req === null) return null; + + // Any other write makes the value depend on runtime control flow. + let writes = 0; + ts.walkPreorder(sf, (node) => { + if (!ts.isIdentifier(node) || node.text !== decl.name.text) return; + if (symbolOfIdentifierValue(lowerer, node) !== symbol) return; + if (writeTargetOf(node) === null) return; + if (++writes > 1) return "stop"; + }); + if (writes !== 1) return null; + + return { decl, assign, call: req.call, spec: req.spec, dep: req.dep }; +} + +/** The split binding an identifier names, or null. */ +export function splitRequireBindingOfIdent( + lowerer: Lowerer, + ident: ts.Identifier, +): SplitRequireBinding | null { + const symbol = symbolOfIdentifierValue(lowerer, ident); + return symbol === undefined ? null : splitBindingOfSymbol(lowerer, symbol); +} + +/** The exact top-level assignment statement that initializes `expr`, or null. */ +export function splitRequireAssignmentOf( + lowerer: Lowerer, + expr: ts.Expression, +): SplitRequireBinding | null { + if ( + !ts.isBinaryExpression(expr) || + expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(expr.left) + ) + return null; + const bindingOfIdent = splitRequireBindingOfIdent(lowerer, expr.left); + return bindingOfIdent !== null && bindingOfIdent.assign === expr ? bindingOfIdent : null; +} + +/** The whole-export symbol a module's `module.exports = ` names, + * or null when the module is a table. */ +export function wholeExportSymbolOfModule(lowerer: Lowerer, dep: ts.SourceFile): ts.Symbol | null { + const cached = lowerer.cjsWholeExportSymbols.get(dep); + if (cached !== undefined) return cached; + if (cjsNamespaceModule(lowerer, dep)) { + lowerer.cjsWholeExportSymbols.set(dep, null); + return null; + } + const moduleSymbol = lowerer.checker.getSymbolAtLocation(dep); + let symbol = moduleSymbol?.getExports()?.get("export=" as ts.__String); + if (symbol === undefined) { + lowerer.cjsWholeExportSymbols.set(dep, null); + return null; + } + if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol); + lowerer.cjsWholeExportSymbols.set(dep, symbol); + return symbol; +} + +function splitBindingOfSymbol(lowerer: Lowerer, symbol: ts.Symbol): SplitRequireBinding | null { + const decl = symbol.declarations.find(ts.isVariableDeclaration); + return decl === undefined ? null : splitRequireBindingOf(lowerer, decl); +} + +/** The target whole-export symbol behind a split-binding symbol. */ +function splitWholeExportSymbolOfSymbol(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null { + const binding = splitBindingOfSymbol(lowerer, symbol); + return binding === null ? null : wholeExportSymbolOfModule(lowerer, binding.dep); +} + +/** True when a CommonJS module has a live member export (`exports.x` or + * `module.exports.x`) after all `module.exports =` replacements. Those + * members need a mutable whole-export root. */ +export function cjsHasLiveMemberExports(lowerer: Lowerer, sf: ts.SourceFile): boolean { + const cached = lowerer.cjsLiveMemberExports.get(sf); + if (cached !== undefined) return cached; + const stmts = sf.statements.nodes; + let lastTable = -1; + for (let i = 0; i < stmts.length; i++) { + if (cjsExportAssignmentOf(stmts[i]!)?.kind === "table") lastTable = i; + } + let live = false; + // Only members attached after the last replacement survive in Node. + if (lastTable >= 0) { + for (let i = lastTable + 1; i < stmts.length; i++) { + const stmt = stmts[i]!; + if (cjsExportAssignmentOf(stmt)?.kind === "member" && cjsExportDiscardReason(stmt) === null) { + live = true; + break; + } + } + } + lowerer.cjsLiveMemberExports.set(sf, live); + return live; +} + +/** The wrapper's export= symbol when a split binding is re-exported whole + * (`module.exports = m`) and that assignment owns storage for later member + * writes. Importers must read the wrapper's storage, not the split + * binding's underlying value. */ +function splitWholeExportStorageSymbolOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null { + const binding = splitBindingOfSymbol(lowerer, symbol); + if (binding === null) return null; + if (wholeExportSymbolOfModule(lowerer, binding.dep) === null) return null; + const sf = ts.getSourceFileOfNode(binding.decl); + if (!cjsHasLiveMemberExports(lowerer, sf)) return null; + const whole = cjsWholeExportValueOf(sf); + if (whole === null) return null; + let rhs: ts.Expression = whole.rhs; + while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + if (!ts.isIdentifier(rhs) || lowerer.checker.getSymbolAtLocation(rhs) !== symbol) return null; + const moduleSymbol = lowerer.checker.getSymbolAtLocation(sf); + let exportSymbol = moduleSymbol?.getExports()?.get("export=" as ts.__String); + if (exportSymbol === undefined) return null; + if (exportSymbol.flags & ts.SymbolFlags.Alias) + exportSymbol = lowerer.checker.getAliasedSymbol(exportSymbol); + return exportSymbol; +} + +/** The source file whose `module.exports =` assignment declares `symbol`. */ +function moduleExportSourceFileOf(symbol: ts.Symbol): ts.SourceFile | null { + for (const decl of symbol.declarations) { + if ( + ts.isBinaryExpression(decl) && + isModuleExportsAccess(decl.left) && + ts.isExpressionStatement(decl.parent) && + ts.isSourceFile(decl.parent.parent) + ) + return decl.getSourceFile(); + } + return null; +} + +/** One hop from a wrapper's export= symbol to the value it re-exports + * (`module.exports = require(...)` or `= `), or null. A + * wrapper with live members owns storage for them, so it is not followed. */ +function wholeReexportTargetOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null { + const sf = moduleExportSourceFileOf(symbol); + if (sf === null) return null; + // A wrapper with live members owns storage for them; importers must keep + // reading that root rather than the inner value. + if (cjsHasLiveMemberExports(lowerer, sf)) return null; + const whole = cjsWholeExportValueOf(sf); + if (whole === null) return null; + const inner = inlineProgramRequireOf(lowerer, whole.rhs); + if (inner !== null) return wholeExportSymbolOfModule(lowerer, inner.dep); + let rhs: ts.Expression = whole.rhs; + while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + if (!ts.isIdentifier(rhs)) return null; + const rhsSymbol = lowerer.checker.getSymbolAtLocation(rhs); + return rhsSymbol === undefined ? null : splitWholeExportSymbolOfSymbol(lowerer, rhsSymbol); +} + +/** Follow split bindings and whole re-exports to the ultimate value symbol. */ +function followSplitWholeTarget(lowerer: Lowerer, candidate: ts.Symbol | null): ts.Symbol | null { + if (candidate === null) return null; + let symbol = candidate; + const seen = new Set(); + for (let depth = 0; depth < 32 && !seen.has(symbol); depth++) { + seen.add(symbol); + const next = + splitWholeExportSymbolOfSymbol(lowerer, symbol) ?? wholeReexportTargetOf(lowerer, symbol); + if (next === null) break; + symbol = next; + } + return symbol; +} + +/** The target whole-export symbol behind `module.exports = `, + * letting importers of that wrapper resolve to the real value symbol. */ +export function splitWholeExportAliasTargetOf( + lowerer: Lowerer, + symbol: ts.Symbol, +): ts.Symbol | null { + const storage = splitWholeExportStorageSymbolOf(lowerer, symbol); + if (storage !== null) return followSplitWholeTarget(lowerer, storage); + const direct = followSplitWholeTarget(lowerer, splitWholeExportSymbolOfSymbol(lowerer, symbol)); + if (direct !== null) return direct; + return followSplitWholeTarget(lowerer, wholeReexportTargetOf(lowerer, symbol)); +} + +/** The first read of a split binding that code before its assignment can + * reach, using the same reachability model as ordinary require bindings. */ +export function splitRequireEarlyReadOf( + lowerer: Lowerer, + binding: SplitRequireBinding, +): ts.Identifier | null { + const sf = ts.getSourceFileOfNode(binding.decl); + const stmt = binding.assign.parent; + // `binding` was recognized only from a top-level `m = require(...)` + // statement, so the statement index is always present. + const index = sf.statements.nodes.indexOf(stmt as ts.ExpressionStatement); + return requireBindingEarlyUse7(lowerer.program, sf, index, binding.decl, []); +} + /** A `require("")` call of the CommonJS global in a CommonJS JS * file whose target is a compiled program module (relative, aliased, or an * opted-in --npm-static package). Builtins, JSON documents and diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index b5f99d682..d032cb263 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -22,6 +22,11 @@ import { } from "../../../ir/ir.js"; import { lowerNodeModuleIdentifier } from "../lower-node-module.js"; import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js"; +import { + splitRequireBindingOfIdent, + wholeExportSymbolOfModule, + wholeExportValue, +} from "../cjs-inline-require.js"; import { captureContextArguments } from "../function-context.js"; import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js"; import { @@ -920,6 +925,27 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // Client`): the member holds that binding's value. const memberAlias = cjsMemberAliasValueOf(lowerer, expr); if (memberAlias !== null) return lowerer.lowerExpr(memberAlias); + // `var m; m = require("./x")` as a bare value: a whole-export target is + // the exported value; a table target has no first-class module object. + { + const split = splitRequireBindingOfIdent(lowerer, expr); + if (split !== null) { + if (wholeExportSymbolOfModule(lowerer, split.dep) !== null) { + const value = wholeExportValue(lowerer, split.dep, expr); + if (value !== null) return value; + lowerer.unsupported( + "SC1090", + expr, + `the whole export of '${split.spec}' as a value (its 'module.exports =' value has no static representation)`, + ); + } + lowerer.unsupported( + "SC1090", + expr, + `the exports object of '${split.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(split.spec)}).member directly)`, + ); + } + } // 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/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 6ca21ddaa..a86decf4c 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -42,10 +42,14 @@ import { } from "./cycle-initialization.js"; import { markRequireEarlyBindings } from "./require-initialization.js"; import { + cjsHasLiveMemberExports, cjsTableMethodEntry, inlineProgramRequireOf, nestedCjsExportAssignmentsOf, requireNestedPatternsOf, + splitRequireBindingOf, + splitRequireBindingOfIdent, + wholeExportSymbolOfModule, } from "./cjs-inline-require.js"; import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js"; import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js"; @@ -1588,14 +1592,26 @@ export function collectGlobals( // require() of a program module is not: the exporter re-exports // that module's exports, which requirers resolve through the // checker's alias (the statement lowers to the dep's %init). + // A split binding re-exported whole with later member writes needs + // a mutable root: `module.exports = m; module.exports.extra = 1` + // extends the SAME object Node exposes. Without later members the + // binder is pure alias plumbing (no storage). + const splitWholeBinding = ts.isIdentifier(rhs) + ? splitRequireBindingOfIdent(lowerer, rhs) + : null; + const hasMemberExports = cjsHasLiveMemberExports(lowerer, sf); + const splitWhole = + splitWholeBinding !== null && + wholeExportSymbolOfModule(lowerer, splitWholeBinding.dep) !== null && + hasMemberExports; const factoryValued = - !ts.isIdentifier(rhs) && + (!ts.isIdentifier(rhs) || splitWhole) && !ts.isClassExpression(rhs) && !ts.isObjectLiteralExpression(rhs) && !( requireSpecOf(rhs) !== null && inlineProgramRequireOf(lowerer, rhs) !== null && - !sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "member") + !hasMemberExports ) && cjsExportTargetLiteral(rhs, sf) === null; if (cjsScalarLiteral(rhs) || fnValued || factoryValued) { @@ -1608,6 +1624,9 @@ export function collectGlobals( // A returned object remains the live CommonJS root; later // exports.member writes must update that same open object. if (factoryValued && t?.kind === "record" && lowerer.dynConvertible(t)) t = DYN; + // A split whole-export root is extended by later member writes; + // keep it checked-dynamic so those writes reach the same object. + if (splitWhole && t && t.kind !== "dyn") t = DYN; if (t && t.kind !== "void") { const g: IrGlobal = { id: `%g.${tag}exports`, @@ -2699,6 +2718,7 @@ function noteVarGlobalEntryInit(lowerer: Lowerer, sf: ts.SourceFile, g: IrGlobal * plumbing (lowerVarDeclarator): no storage, the reads resolve through to * the module. JSON documents and native FFI requires keep their storage. */ function requireAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if (splitRequireBindingOf(lowerer, decl) !== null) return true; return ( decl.initializer !== undefined && requireSpecOf(decl.initializer) !== null && diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index b04ba0b09..d10d3b9d2 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -36,6 +36,11 @@ import { cjsTableMethodEntry, inlineProgramRequireOf, requireNestedPatternsOf, + splitRequireAssignmentOf, + splitRequireBindingOf, + splitRequireBindingOfIdent, + splitRequireEarlyReadOf, + wholeExportSymbolOfModule, } from "./cjs-inline-require.js"; import { cjsExportMemberWriteGlobalOf, @@ -1811,6 +1816,23 @@ function lowerVarDeclarator( isConst: boolean, isLet: boolean, ): IrStmt[] { + // `var m; m = require("./x")`: the declaration is alias plumbing; the + // assignment owns the module's run-once %init (cjs-inline-require.ts). + // A read that code before the assignment can reach keeps Node's hoisted + // `undefined` divergence visible at build time, like a var require + // declaration below runnable code. + const split = splitRequireBindingOf(lowerer, decl); + if (split !== null) { + const early = splitRequireEarlyReadOf(lowerer, split); + if (early !== null) { + lowerer.unsupported( + "SC1013", + early, + `var require() binding '${(decl.name as ts.Identifier).text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — assign the require above the code that can run first)`, + ); + } + return []; + } // The canonical ESM bridge to a compiled program module. Like a // top-level CommonJS require declaration, the binding is alias // plumbing while the dependency evaluates at this statement's @@ -7015,6 +7037,26 @@ function lowerCjsSingleValueExport( return lowerer.requireInitStmt(req.spec, req.call) ?? { kind: "block", body: [], loc }; } if (ts.isIdentifier(rhs)) { + const split = splitRequireBindingOfIdent(lowerer, rhs); + if (split !== null) { + if (wholeExportSymbolOfModule(lowerer, split.dep) === null) { + lowerer.unsupported( + "SC1090", + rhs, + `the exports object of '${split.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(split.spec)}).member directly)`, + ); + } + const root = lowerer.globalsByDeclNode.get(expr); + if (root !== undefined) { + return { + kind: "assign", + localId: root.id, + value: lowerer.lowerExprExpecting(rhs, root.type), + loc: locOf(rhs), + }; + } + return { kind: "block", body: [], loc }; + } const sym = lowerer.resolveValueSymbol(rhs); if (!sym) return null; if ( @@ -7924,6 +7966,16 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm if (!ts.isIdentifier(expr.left)) { lowerer.unsupported("SC1090", expr.left, "assignment to non-variables"); } + const splitAssign = splitRequireAssignmentOf(lowerer, expr); + if (splitAssign !== null) { + return ( + lowerer.requireInitStmt(splitAssign.spec, splitAssign.call) ?? { + kind: "block", + body: [], + loc: locOf(expr), + } + ); + } const target = lowerer.resolveWritable(expr.left); if (!target) lowerer.rejectUnresolved( diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index c4d24cef1..f1e7606f2 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -331,10 +331,13 @@ import { } from "./lower-modules.js"; import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js"; import { + type SplitRequireBinding, cjsNamespaceModule, cjsWholeExportValueOf, inlineNamespaceRequireOf, inlineProgramRequireOf, + splitRequireBindingOf, + splitWholeExportAliasTargetOf, } from "./cjs-inline-require.js"; import { tscInteropModuleOf } from "./cjs-tsc-interop.js"; import { usesCjsExportsIdentity } from "./cjs-exports-object.js"; @@ -2176,6 +2179,14 @@ export class Lowerer { /** Per CommonJS file using top-level `this`: the global holding the * object that stands for its original exports (lowerCjsTopLevelThis). */ readonly cjsExportsThis = new Map(); + /** `var m; m = require("./x")` alias bindings (cjs-inline-require.ts). + * Null entries cache declarations that keep ordinary storage. */ + readonly cjsSplitRequireBindings = new Map(); + /** Per-module whole-export symbol (cjs-inline-require.ts). Null means the + * module's exports are a table. */ + readonly cjsWholeExportSymbols = new Map(); + /** Per-module live member-export classification (cjs-inline-require.ts). */ + readonly cjsLiveMemberExports = new Map(); /** Inline program-module require calls whose %init the enclosing member * read or member call is already sequencing (cjs-inline-require.ts). */ readonly inlineRequireInitDone = new Set(); @@ -3711,6 +3722,11 @@ export class Lowerer { // registry (globals, fn signatures, classes) applies unchanged. const cjsValue = this.cjsExportValueSymbol(symbol); if (cjsValue) symbol = cjsValue; + // `var m; m = require("./x"); module.exports = m` over a whole-export + // target: TS types the wrapper's export= as `any`, so reconnect it to + // the target's value symbol explicitly. + const splitAlias = splitWholeExportAliasTargetOf(this, symbol); + if (splitAlias) symbol = splitAlias; this.flushDeferred(symbol); return symbol; } @@ -4104,8 +4120,8 @@ export class Lowerer { const decl = decls.find(ts.isImportClause) ?? decls[0]; if (!decl) return null; if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl); - if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || !decl.initializer) - return null; + if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null; + if (decl.initializer === undefined) return splitRequireBindingOf(this, decl)?.dep ?? null; { const interop = tscInteropModuleOf(this, expr); if (interop !== null) return cjsNamespaceModule(this, interop.dep) ? interop.dep : null; @@ -4210,6 +4226,9 @@ export class Lowerer { // a binding (cjs-inline-require.ts sequences the module's %init). if (!ts.isIdentifier(expr)) return this.cjsModuleRefDepOf(expr) !== null; const sym = this.checker.getSymbolAtLocation(expr); + const splitDecl = sym?.declarations.find(ts.isVariableDeclaration); + const split = splitDecl === undefined ? null : splitRequireBindingOf(this, splitDecl); + if (split !== null) return cjsNamespaceModule(this, split.dep); if (this.localModuleBindingDepOf(expr) === null) return false; if (this.createRequireWholeExportSymbolOf(expr) !== null) return false; // SINGLE-VALUE exporters (`module.exports = Countdown` / `= double` / diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json new file mode 100644 index 000000000..c01617f80 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json @@ -0,0 +1,12 @@ +{ + "order": [ + "/tests/corpus/split-cjs-require-binding/whole.cjs", + "/tests/corpus/split-cjs-require-binding/reexport.cjs", + "/tests/corpus/split-cjs-require-binding/reexport-extra.cjs", + "/tests/corpus/split-cjs-require-binding/lib.cjs", + "/tests/corpus/split-cjs-require-binding/chain.cjs", + "/tests/corpus/split-cjs-require-binding/chain-wrap.cjs", + "/tests/corpus/split-cjs-require-binding/main.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/split-cjs-require-binding/chain-wrap.cjs b/tests/corpus/split-cjs-require-binding/chain-wrap.cjs new file mode 100644 index 000000000..435dcaeab --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/chain-wrap.cjs @@ -0,0 +1,3 @@ +var chained; +chained = require("./chain.cjs"); +module.exports = chained; diff --git a/tests/corpus/split-cjs-require-binding/chain.cjs b/tests/corpus/split-cjs-require-binding/chain.cjs new file mode 100644 index 000000000..a279f456e --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/chain.cjs @@ -0,0 +1 @@ +module.exports = require("./whole.cjs"); diff --git a/tests/corpus/split-cjs-require-binding/lib.cjs b/tests/corpus/split-cjs-require-binding/lib.cjs new file mode 100644 index 000000000..b318eb85c --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/lib.cjs @@ -0,0 +1,5 @@ +console.log("lib: init"); +exports.n = 7; +exports.f = function (x) { + return x * 2; +}; diff --git a/tests/corpus/split-cjs-require-binding/main.cjs b/tests/corpus/split-cjs-require-binding/main.cjs new file mode 100644 index 000000000..974421bd3 --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/main.cjs @@ -0,0 +1,55 @@ +console.log("main: start"); +var lib, whole; +lib = require("./lib.cjs"); +whole = require("./whole.cjs"); +console.log("main: after", lib.n, lib.f(3), whole(4)); +exports.fromLib = lib.f; +exports.whole = whole; +console.log("main: exported", exports.fromLib(5), whole(6)); +console.log("main: identity", exports.fromLib === lib.f, exports.whole === whole); +const re = require("./reexport.cjs"); +console.log("main: reexport", re(7)); +let later; +later = require("./lib.cjs"); +console.log("main: let", later.n); +const rx = require("./reexport-extra.cjs"); +console.log("main: reexport extra", rx(8), rx.extra); +const splitObject = { whole }; +console.log("main: shorthand value", splitObject.whole(10)); +var libAgain; +libAgain = require("./lib.cjs"); +const { n: splitN, f: splitF } = libAgain; +console.log("main: destructured split", splitN, splitF(4)); +var chained; +chained = require("./chain-wrap.cjs"); +console.log("main: chained", chained(11)); + +// Any later write, including a destructuring target, keeps ordinary storage. +var shorthand; +shorthand = require("./whole.cjs"); +({ shorthand } = { shorthand: "shorthand" }); +console.log("main: shorthand write", shorthand); +var renamed; +renamed = require("./whole.cjs"); +({ value: renamed } = { value: "renamed" }); +console.log("main: renamed write", renamed); +var nested; +nested = require("./whole.cjs"); +({ a: { nested } } = { a: { nested: "nested" } }); +console.log("main: nested write", nested); +var rest; +rest = require("./whole.cjs"); +({ ...rest } = { a: 1, b: 2 }); +console.log("main: rest write", rest.a, rest.b); +var array; +array = require("./whole.cjs"); +[array] = ["array"]; +console.log("main: array write", array); +var arrayRest; +arrayRest = require("./whole.cjs"); +[...arrayRest] = [1, 2]; +console.log("main: array-rest write", arrayRest[0], arrayRest[1]); +var each; +each = require("./whole.cjs"); +for ({ each } of [{ each: "each" }]) {} +console.log("main: for-of write", each); diff --git a/tests/corpus/split-cjs-require-binding/package.json b/tests/corpus/split-cjs-require-binding/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/split-cjs-require-binding/reexport-extra.cjs b/tests/corpus/split-cjs-require-binding/reexport-extra.cjs new file mode 100644 index 000000000..584de98fe --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/reexport-extra.cjs @@ -0,0 +1,6 @@ +var whole; +whole = require("./whole.cjs"); +console.log("extra: before", whole.extra, whole(9)); +module.exports = whole; +module.exports.extra = 1; +console.log("extra: after", whole.extra, module.exports.extra); diff --git a/tests/corpus/split-cjs-require-binding/reexport.cjs b/tests/corpus/split-cjs-require-binding/reexport.cjs new file mode 100644 index 000000000..9a931bdef --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/reexport.cjs @@ -0,0 +1,4 @@ +console.log("reexport: init"); +var whole; +whole = require("./whole.cjs"); +module.exports = whole; diff --git a/tests/corpus/split-cjs-require-binding/whole.cjs b/tests/corpus/split-cjs-require-binding/whole.cjs new file mode 100644 index 000000000..31b476c4e --- /dev/null +++ b/tests/corpus/split-cjs-require-binding/whole.cjs @@ -0,0 +1,4 @@ +console.log("whole: init"); +module.exports = function whole(x) { + return x + 1; +}; diff --git a/tests/harness/corpus-costs/split-cjs-require-binding/main.cjs.json b/tests/harness/corpus-costs/split-cjs-require-binding/main.cjs.json new file mode 100644 index 000000000..9d570f700 --- /dev/null +++ b/tests/harness/corpus-costs/split-cjs-require-binding/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 12000 } From da289bb593ef27c17e45c0121b63c61fdee1dbdb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 23:13:49 -0500 Subject: [PATCH 035/409] Support writev on Transform and PassThrough, and runtime stream option values --- .../builtin-apis/streams-and-readline.mdx | 4 +- packages/cli/test/native-link-info.test.ts | 4 +- .../src/backend/llvm/expr-stream-callbacks.ts | 11 + .../compiler/src/backend/llvm/lib-shared.ts | 2 + packages/compiler/src/backend/runtime-abi.ts | 4 +- .../src/backend/runtime-pack-targets.test.ts | 2 +- .../compiler/src/backend/runtime-pack.test.ts | 2 +- .../src/frontend/lowering/lower-classes.ts | 6 +- .../src/frontend/lowering/lower-stream.ts | 230 ++++++++++++++++-- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 5 + packages/compiler/src/ir/runtime-features.ts | 2 + packages/compiler/src/ir/validate.ts | 2 + .../test/native-codegen-integration.test.ts | 4 +- .../corpus/7460-transform-writev.mjs.json | 6 + .../corpus/7461-transform-writev.cjs.json | 6 + .../corpus/7462-transform-writev.ts.json | 6 + .../corpus/7463-stream-option-values.ts.json | 6 + .../corpus/7464-stream-option-values.mjs.json | 6 + .../7465-transform-callback-values.ts.json | 6 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/scripts/build.mjs | 2 +- packages/runtime/src/scr_console.c | 2 +- packages/runtime/src/scr_core.h | 2 +- packages/runtime/src/scr_dyn_builtin_stream.c | 9 +- packages/runtime/src/scr_stream.c | 77 +++++- packages/runtime/src/scr_stream.h | 8 + tests/corpus/7460-transform-writev.mjs | 33 +++ tests/corpus/7461-transform-writev.cjs | 33 +++ tests/corpus/7462-transform-writev.ts | 109 +++++++++ tests/corpus/7463-stream-option-values.ts | 58 +++++ tests/corpus/7464-stream-option-values.mjs | 28 +++ .../corpus/7465-transform-callback-values.ts | 41 ++++ .../7460-transform-writev.mjs.json | 1 + .../7461-transform-writev.cjs.json | 1 + .../7462-transform-writev.ts.json | 1 + .../7463-stream-option-values.ts.json | 1 + .../7464-stream-option-values.mjs.json | 1 + .../7465-transform-callback-values.ts.json | 1 + .../self-hosting-native-toolchain.test.ts | 2 +- 40 files changed, 670 insertions(+), 57 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json create mode 100644 tests/corpus/7460-transform-writev.mjs create mode 100644 tests/corpus/7461-transform-writev.cjs create mode 100644 tests/corpus/7462-transform-writev.ts create mode 100644 tests/corpus/7463-stream-option-values.ts create mode 100644 tests/corpus/7464-stream-option-values.mjs create mode 100644 tests/corpus/7465-transform-callback-values.ts create mode 100644 tests/harness/corpus-costs/7460-transform-writev.mjs.json create mode 100644 tests/harness/corpus-costs/7461-transform-writev.cjs.json create mode 100644 tests/harness/corpus-costs/7462-transform-writev.ts.json create mode 100644 tests/harness/corpus-costs/7463-stream-option-values.ts.json create mode 100644 tests/harness/corpus-costs/7464-stream-option-values.mjs.json create mode 100644 tests/harness/corpus-costs/7465-transform-callback-values.ts.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index fe6bb2dfb..b88ead1f4 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -32,9 +32,9 @@ `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. - The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on Writable and Duplex; `writev` on a Transform or PassThrough, assigning `_construct` after construction, and a non-literal `decodeStrings` or `defaultEncoding` in an options object literal are unsupported. + The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on every class with a writable side. + In typed code, a `writev` entry's `encoding` declared as a string receives `""` where Node passes `undefined` or `null` (an object-mode write). `setDefaultEncoding()` takes any encoding Node knows. - A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. `on`, `once`, and `addListener` return the interface, so registrations chain. The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node. diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index bf5b3be14..8715d9356 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v33"); + expect(error?.stderr).toContain("scr_runtime_abi_v34"); }); }); diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 14a2e3432..b368c91f4 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -727,6 +727,17 @@ export function streamCbThunkFor( const ty = host.llType(p); host.declare(`declare zeroext i1 @scr_exc_pending()`); if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`); + // An entry `encoding` declared as a string (the declarations' + // `encoding: string`) receives "" where Node passes undefined or + // null (an object-mode write), as a write's encoding parameter does. + const entry = + p.kind === "array" && p.elem.kind === "record" + ? host.recordsById.get(p.elem.shapeId) + : undefined; + if (entry?.fields.some((f) => f.name === "encoding" && f.type.kind === "string")) { + host.declare(`declare void @scr_stream_writev_fill_encodings(ptr)`); + d.push(` call void @scr_stream_writev_fill_encodings(ptr %chunks)`); + } d.push( ` %vt${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %chunks, ptr null)`, ` %vtbad${i} = call zeroext i1 @scr_exc_pending()`, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 054d9341b..548e8e597 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1135,6 +1135,8 @@ export const LIB_FN_SYMS: Record = { "readable.pushEncoding": "scr_stream_set_push_encoding", "stream.objectMode": "scr_stream_object_mode", "stream.decodeStrings": "scr_stream_decode_strings", + "stream.optionValues": "scr_stream_option_values", + "stream.checkEncodingOption": "scr_stream_check_encoding_option", "stream.duplexFromGen": "scr_stream_duplex_from_gen", "stream.duplexFromDyn": "scr_stream_duplex_from_dyn", "stream.duplexifyReadable": "scr_stream_duplexify_readable", diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 7d3dcbc02..173de6408 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 33 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v33" as const; +export const RUNTIME_ABI_VERSION = 34 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v34" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 532051b88..923b55c5b 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 18b04b9a6..681f86817 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 6dd0124bd..e3ab29dba 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -2734,8 +2734,8 @@ export function collectClassShapeInner( // state, so an override or shadowing field would split-brain. // Underscore methods are the SUPPORTED override form — but only // the ones the class's own base consumes (a `_read` on a - // Transform, or `_writev`/`_construct` anywhere, would be consumed - // by Node machinery that has no lowering here). + // Transform, or `_writev` on a Readable, would be consumed by Node + // machinery that has no lowering here). if (memberName && ts.isIdentifier(memberName)) { const streamBase = (() => { for (let c = base; c; c = c.base) if (c.builtinStream) return c; @@ -2754,7 +2754,7 @@ export function collectClassShapeInner( lowerer.unsupported( "SC1090", memberName, - `declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (batched writes are supported on Writable and Duplex subclasses)`, + `declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (a Readable has no writable side)`, ); } const accepted = streamCtorShape(streamBase.def.name).accepted; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index c37ae9074..15afb741e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -179,15 +179,15 @@ export const UNDERSCORE_METHODS: ReadonlyMap = new Map([ /** The option callbacks applied after construction rather than through * the constructor's callback slots (option name → method name): construct - * on every stream class, and writev on the writable classes whose - * machinery hands it batches (Writable and Duplex). */ + * on every stream class, and writev on every class with a writable side + * (a Transform's batches reach writev, not the transform, as in Node). */ export const STREAM_EXTRA_CALLBACKS: ReadonlyMap = new Map([ ["construct", "_construct"], ["writev", "_writev"], ]); export function streamExtraCallbacks(cls: string): readonly string[] { - return cls === "%Writable" || cls === "%Duplex" ? ["construct", "writev"] : ["construct"]; + return cls === "%Readable" ? ["construct"] : ["construct", "writev"]; } /** The completion callback's parameter position in each underscore @@ -442,6 +442,13 @@ interface StreamOptions { * right after construction (Node's constructors call addAbortSignal * once the state exists), or null when absent. */ signal: IrExpr | null; + /** Non-literal decodeStrings and defaultEncoding values: valueDecls + * evaluates them into locals before construction (the defaultEncoding + * validated there, as the state constructors throw before the stream + * exists), and stream.optionValues applies them to the fresh stream. */ + valueDecls: IrStmt[]; + decodeStringsValue: IrExpr | null; + defaultEncodingValue: IrExpr | null; /** The objectMode option's sides (a Duplex's readableObjectMode and * writableObjectMode set one side): applied with stream.objectMode * right after construction. */ @@ -732,6 +739,12 @@ function pinAnyDataCompletion( if (!symbol || lowerer.stageSourceTypes.has(symbol)) return null; const declared = lowerer.paramShape(param).type; if (declared.kind !== "func" || declared.rest || declared.params[1]?.kind !== "dyn") return null; + // Only when every use calls the callback with a Buffer, a string or + // nothing as `data`: another value (or a callback that escapes) keeps + // `data` checked-dynamic, so the runtime pushes it as push(data) does + // and reports ERR_INVALID_ARG_TYPE through 'error', as Node does. + if (fnNode.body === undefined || !completionDataIsChunk(lowerer, fnNode.body, symbol, param.name)) + return null; const data = unionOf(lowerer, [BYTES, STRING, { kind: "undefinedT" }]); lowerer.stageSourceTypes.set( symbol, @@ -740,6 +753,59 @@ function pinAnyDataCompletion( return symbol; } +/** Whether every reference to the completion callback `symbol` in `body` + * calls it with a `data` argument that is a Buffer, a string, null or + * undefined (or with no data). */ +function completionDataIsChunk( + lowerer: Lowerer, + body: ts.Node, + symbol: ts.Symbol, + declaration: ts.Node, +): boolean { + let ok = true; + const visit = (node: ts.Node): void => { + if (!ok) return; + if ( + ts.isIdentifier(node) && + node !== declaration && + lowerer.checker.getSymbolAtLocation(node) === symbol + ) { + const call = node.parent; + if (!call || !ts.isCallExpression(call) || call.expression !== node) { + ok = false; + return; + } + const data = call.arguments.nodes[1]; + if (data !== undefined) { + let t: IrType | null; + try { + t = lowerer.mapTypeOf(lowerer.typeOf(data)); + } catch { + t = null; + } + const arms = + t?.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? null) : t ? [t] : null; + if ( + arms === null || + !arms.every( + (a) => + (a.kind === "bytes" && a.elem === "u8") || + a.kind === "string" || + a.kind === "undefinedT" || + a.kind === "nullT", + ) + ) { + ok = false; + return; + } + } + } + ts.forEachChild(node, visit); + }; + visit(body); + return ok; +} + /** Parses the options object literal of a stream constructor. `which` * names the callbacks this class accepts, in canonical flag order. */ function parseStreamOptions( @@ -767,6 +833,9 @@ function parseStreamOptions( badEncoding: null, pushEncoding: null, signal: null, + valueDecls: [], + decodeStringsValue: null, + defaultEncodingValue: null, readableObject: false, writableObject: false, decodeStringsOff: false, @@ -802,6 +871,21 @@ function parseStreamOptions( optsLit !== undefined && ts.isObjectLiteralExpression(optsLit) && literalWritesValues(optsLit, ctorName !== "Readable" && ctorName !== "Writable"); + // In JavaScript a decodeStrings value known only at run time may let + // string writes through, so the write and transform callbacks' chunk + // parameters read as any (the declarations type it Buffer). + const runtimeDecodeStrings = + optsLit !== undefined && + isJsSourceFile(ts.getSourceFileOfNode(optsLit)) && + ts.isObjectLiteralExpression(optsLit) && + (optsLit.properties?.nodes ?? []).some( + (p) => + ts.isPropertyAssignment(p) && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && + p.name.text === "decodeStrings" && + p.initializer.kind !== ts.SyntaxKind.TrueKeyword && + p.initializer.kind !== ts.SyntaxKind.FalseKeyword, + ); if (out.readableObject) out.hwmR = numLit(16, loc); if (out.writableObject) out.hwmW = numLit(16, loc); const tuples = cbTuples(lowerer); @@ -851,6 +935,7 @@ function parseStreamOptions( } const full = tuples[name]!.map((t) => (t === "fn" ? null : t)); const pinned = out.readableObject ? null : pinAnyDataCompletion(lowerer, name, fnNode); + let anyChunk: ts.Symbol | undefined; try { // The completion-callback tail: typed by the checker contextually; // accept the user's declared func type as the tuple's entry so the @@ -867,12 +952,22 @@ function parseStreamOptions( } return declared ?? funcOf([], VOID); }); + const chunkParam = + runtimeDecodeStrings && (name === "write" || name === "transform") + ? fnNode.parameters.nodes[0] + : undefined; + anyChunk = + chunkParam !== undefined && ts.isIdentifier(chunkParam.name) && !chunkParam.type + ? lowerer.checker.getSymbolAtLocation(chunkParam.name) + : undefined; + if (anyChunk) lowerer.anySymbols.add(anyChunk); present.set( name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple, writesValues), ); } finally { if (pinned) lowerer.stageSourceTypes.delete(pinned); + if (anyChunk) lowerer.anySymbols.delete(anyChunk); } continue; } @@ -925,11 +1020,15 @@ function parseStreamOptions( if (writableClass) out.decodeStringsOff = true; break; } - lowerer.noLowering( - `${ctorName} with a runtime 'decodeStrings' value`, - prop, - "write 'decodeStrings: true' or 'decodeStrings: false'", - ); + // a runtime value: WritableState's `options.decodeStrings !== false` + const local = lowerer.declareHiddenLocal("%decodeStrings", DYN); + out.valueDecls.push({ + kind: "varDecl", + localId: local.id, + init: lowerer.lowerExprExpecting(value, DYN), + loc, + }); + if (writableClass) out.decodeStringsValue = varRef(local.id, DYN, loc); break; } case "encoding": { @@ -973,11 +1072,33 @@ function parseStreamOptions( break; } } - lowerer.noLowering( - `the ${ctorName} option 'defaultEncoding' with a runtime value`, - prop, - "write the encoding as a string literal", - ); + { + // a runtime value: null, undefined and utf8 are the default, + // another name Buffer.isEncoding knows is kept as given, and any + // other value throws ERR_UNKNOWN_ENCODING before construction + const local = lowerer.declareHiddenLocal("%defaultEncoding", DYN); + const ref = varRef(local.id, DYN, loc); + out.valueDecls.push( + { + kind: "varDecl", + localId: local.id, + init: lowerer.lowerExprExpecting(value, DYN), + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.checkEncodingOption", + args: [ref], + type: VOID, + loc, + }, + loc, + }, + ); + out.defaultEncodingValue = ref; + } break; case "signal": { // `if (options.signal) addAbortSignal(options.signal, this)`: @@ -1041,9 +1162,7 @@ function parseStreamOptions( lowerer.noLowering( `the ${ctorName} option 'writev'`, prop, - ctorName === "Readable" - ? "a Readable has no writable side" - : "batched writes on a Transform would bypass its transform — writev is supported on Writable and Duplex", + "a Readable has no writable side", ); break; default: @@ -1083,6 +1202,9 @@ function streamCtorArgs( badEncoding: string | null; pushEncoding: string | null; signal: IrExpr | null; + valueDecls: IrStmt[]; + decodeStringsValue: IrExpr | null; + defaultEncodingValue: IrExpr | null; readableObject: boolean; writableObject: boolean; decodeStringsOff: boolean; @@ -1117,6 +1239,9 @@ function streamCtorArgs( badEncoding: o.badEncoding, pushEncoding: o.pushEncoding, signal: o.signal, + valueDecls: o.valueDecls, + decodeStringsValue: o.decodeStringsValue, + defaultEncodingValue: o.defaultEncodingValue, readableObject: o.readableObject, writableObject: o.writableObject, decodeStringsOff: o.decodeStringsOff, @@ -1237,6 +1362,18 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C loc, }; } + const values = optionValuesArgs(parsed, loc); + if (values) { + // runtime decodeStrings and defaultEncoding values (answers the + // receiver +1) + built = { + kind: "libCall", + fn: "stream.optionValues", + args: [built, ...values], + type: thisType, + loc, + }; + } if (parsed.pushEncoding !== null) { // { defaultEncoding }: chain the push-side decode default (the // setEncoding chaining shape — answers the receiver +1). @@ -1269,8 +1406,11 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C loc, }; } - if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0) - return built; + if (!parsed.decodeStringsOff && parsed.writeEncoding === null && parsed.extras.length === 0) { + return parsed.valueDecls.length > 0 + ? { kind: "seqExpr", stmts: parsed.valueDecls, result: built, type: thisType, loc } + : built; + } const fresh = lowerer.declareHiddenLocal("%stream", thisType); const followUps = streamOptionFollowUps( parsed, @@ -1280,13 +1420,30 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C ); return { kind: "seqExpr", - stmts: [{ kind: "varDecl", localId: fresh.id, init: built, loc }, ...followUps], + stmts: [ + ...parsed.valueDecls, + { kind: "varDecl", localId: fresh.id, init: built, loc }, + ...followUps, + ], result: varRef(fresh.id, thisType, loc), type: thisType, loc, }; } +/** stream.optionValues' (decodeStrings, defaultEncoding) arguments, or + * null when both options are literals or absent. */ +function optionValuesArgs( + parsed: { decodeStringsValue: IrExpr | null; defaultEncodingValue: IrExpr | null }, + loc: SrcLoc, +): IrExpr[] | null { + if (parsed.decodeStringsValue === null && parsed.defaultEncodingValue === null) return null; + return [ + parsed.decodeStringsValue ?? dynUndefinedExpr(loc), + parsed.defaultEncodingValue ?? dynUndefinedExpr(loc), + ]; +} + /** True when a value of Node's argument type `passed` widens implicitly * into the declared parameter type `declared` (one of its union arms, or a * sub-union of it). */ @@ -1589,6 +1746,7 @@ export function lowerStreamSuperCall( ]; } const out: IrStmt[] = [ + ...parsed.valueDecls, { kind: "exprStmt", expr: { @@ -1614,6 +1772,20 @@ export function lowerStreamSuperCall( loc, }); } + const values = optionValuesArgs(parsed, loc); + if (values) { + out.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.optionValues", + args: [thisRef(), ...values], + type: thisType, + loc, + }, + loc, + }); + } if (parsed.pushEncoding !== null) { out.push({ kind: "exprStmt", @@ -1699,12 +1871,20 @@ export function lowerStreamUnderscoreAssign( const clsName = cls.slice(1); if (methodName === "_construct") { // Node's constructor reads _construct once; a later assignment has no - // effect there - lowerer.noLowering( - `assigning '${methodName}' on a ${clsName}`, - expr.left, - "declare _construct on the class or pass the construct option", - ); + // effect on the stream, so only the receiver and the value are + // evaluated + const value = lowerer.lowerExpr(expr.right); + return { + kind: "exprStmt", + expr: { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: recv, loc }], + result: value, + type: value.type, + loc, + }, + loc, + }; } const which = methodName === "_writev" ? "writev" : optionByMethod.get(methodName)!; const { accepted } = streamCtorShape(cls); diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index d8e78be08..e786fa9a8 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -362,6 +362,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "transform.newDyn", "passthrough.newDyn", "readable.initDyn", + "stream.checkEncodingOption", "writable.initDyn", "duplex.initDyn", "transform.initDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 7cb666ccf..ed135a800 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4103,6 +4103,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * after construction; answers the receiver (+1, the chaining shape). */ "stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID }, "stream.decodeStrings": { argTypes: [null, BOOL], result: VOID }, + /** Runtime decodeStrings and defaultEncoding option values, applied + * after construction (answers the receiver +1); the encoding was + * validated by stream.checkEncodingOption before construction. */ + "stream.optionValues": { argTypes: [null, DYN, DYN], result: VOID }, + "stream.checkEncodingOption": { argTypes: [DYN], result: VOID }, "stream.duplexFromGen": { argTypes: [null, null, null, null, null, null], result: VOID }, "stream.duplexFromDyn": { argTypes: [DYN], result: VOID }, /** Duplex.from over a lone Readable or Writable, or a { readable, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 85910369a..bd4727b95 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -219,6 +219,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.chunkDone", "stream.objectMode", "stream.decodeStrings", + "stream.optionValues", + "stream.checkEncodingOption", "stream.duplexFromGen", "stream.duplexFromDyn", "stream.duplexFromPromise", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 770fee6c6..1f3692c9c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5495,6 +5495,7 @@ function validateFunction( e.fn === "stream.destroyErr" || e.fn === "stream.objectMode" || e.fn === "stream.decodeStrings" || + e.fn === "stream.optionValues" || e.fn === "stream.duplexFromGen" || e.fn === "stream.duplexFromDyn" || e.fn === "stream.duplexifyReadable" || @@ -5867,6 +5868,7 @@ function validateFunction( e.fn === "readable.pushEncoding" || e.fn === "stream.objectMode" || e.fn === "stream.decodeStrings" || + e.fn === "stream.optionValues" || e.fn === "writable.writeEncoding" || e.fn === "writable.setDefaultEncoding" || e.fn === "stream.destroy" || diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index 0fd5249d3..3a1f8fdee 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v33"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v34"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v33"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v34"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json new file mode 100644 index 000000000..1015e1354 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7460-transform-writev.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7460-transform-writev.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json new file mode 100644 index 000000000..4d3c86215 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7461-transform-writev.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7461-transform-writev.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json new file mode 100644 index 000000000..9b42f2d60 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7462-transform-writev.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7462-transform-writev.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json new file mode 100644 index 000000000..5aa543dea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7463-stream-option-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7463-stream-option-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json new file mode 100644 index 000000000..2a438d418 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7464-stream-option-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7464-stream-option-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json new file mode 100644 index 000000000..17633d208 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7465-transform-callback-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7465-transform-callback-values.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index e17312713..9f856b376 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -340,7 +340,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index 60ef9dc49..50da4a78e 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -437,7 +437,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index f8dd9a0c0..193352424 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v33(void) {} +void scr_runtime_abi_v34(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index d716f0de3..1b5e28545 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v33(void); +void scr_runtime_abi_v34(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index 926f85eea..2875cfa06 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -34,7 +34,7 @@ * receiver without a reference; the hook closures outlive the record. * * Not supported here (each throws the "not supported yet" Error where it - * runs): the options scr_stream.c's dyn option walk refuses, `_writev` and `_construct`, symbol event names, listeners() / + * runs): the options scr_stream.c's dyn option walk refuses, symbol event names, listeners() / * rawListeners(), `wrap()`, and pipes into values that are neither runtime * streams nor constructor-function streams. */ #include "scr_runtime.h" @@ -898,20 +898,15 @@ static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t } bool failed = false; bool writes = kind != BCS_READABLE; - bool transformish = kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH; ScrDyn *opts = bcs_arg(args, argc, 0); /* Node's constructors assign options.writev and options.construct to * the instance (this._writev = options.writev), then read _writev and * _construct, so an option shadows the prototype's method. */ if (opts->kind == SCR_DYN_OBJ) { - if (writes && !transformish && !bcs_copy_option(self, opts, "writev", "_writev")) return NULL; + if (writes && !bcs_copy_option(self, opts, "writev", "_writev")) return NULL; if (!bcs_copy_option(self, opts, "construct", "_construct")) return NULL; } bool has_writev = writes && bcs_is_function(self, "_writev", &failed); - if (has_writev && transformish) { - bcs_unsupported("_writev on a constructor-function Transform is not supported yet"); - return NULL; - } bool has_construct = !failed && bcs_is_function(self, "_construct", &failed); if (failed) return NULL; ScrStream *s = kind == BCS_READABLE ? scr_stream_new_readable_dyn(opts) diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 275c03039..4b962675a 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -2157,6 +2157,24 @@ static ScrDyn *scr_stream_wentry_encoding(const ScrStreamState *st, ScrDyn *enc) return scr_dyn_retain(enc ? enc : scr_dyn_undefined()); } +/* A compiled writev whose entries declare `encoding` a string: an entry + * without one (an object-mode write passes undefined or null) gets "". + * The batch array is the one this call receives. */ +void scr_stream_writev_fill_encodings(ScrDyn *chunks) { + if (!chunks || chunks->kind != SCR_DYN_ARR) return; + double n = scr_dyn_arr_len(chunks); + for (double i = 0; i < n; i++) { + ScrDyn *entry = scr_dyn_arr_at(chunks, i); /* +1 */ + ScrDyn *enc = entry->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(entry, "encoding", 8) : NULL; + if (entry->kind == SCR_DYN_OBJ && (!enc || enc->kind == SCR_DYN_UNDEF || enc->kind == SCR_DYN_NULL)) { + ScrStr *empty = scr_str_new("", 0); + scr_dyn_obj_set(entry, "encoding", 8, scr_dyn_new_str(empty)); + scr_str_release(empty); + } + scr_dyn_release(entry); + } +} + /* A batch entry's callback: the write's callback, or Node's nop. */ static ScrDyn *scr_stream_writev_entry_cb_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { (void)args; @@ -2675,9 +2693,8 @@ void *scr_stream_native(ScrStream *s, void (*release)(void *)) { * way the compiler-emitted dyn thunks do (chunk as Buffer-flavored bytes, * encoding 'buffer', the error via the boundary encoding, the completion * callback as a callable dyn over the *_done glue). Options that Node's - * base WOULD consume but have no lowering here (objectMode truthy, - * decodeStrings:false, non-utf8 defaultEncoding, signal, construct, - * writev, and read/write/final on the transformish bases) throw the + * base WOULD consume but have no lowering here (signal, and + * read/write/final on the transformish bases) throw the * "not supported yet" Error — the compile-time fence's runtime twin. */ /* The boxed dyn callable (+1 — scr_box_get_ref retains; callers release @@ -2903,11 +2920,8 @@ static bool scr_stream_parse_dyn_opts(const ScrDyn *opts, char base, ScrStreamDy } } out->construct = scr_stream_dynopt_fn(opts, "construct", 9); + /* a Transform's batches reach writev, not the transform, as in Node */ if (base != 'r') out->writev = scr_stream_dynopt_fn(opts, "writev", 6); - if (transformish && out->writev != NULL) { - static const char m[] = "the stream option 'writev' on a Transform is not supported yet (batches would bypass the transform)"; - return scr_stream_dynopt_unsupported(m, sizeof m - 1); - } { /* a known encoding or ERR_UNKNOWN_ENCODING (the states' constructors) */ ScrDyn *v = scr_dyn_obj_get((ScrDyn *)opts, "defaultEncoding", 15); @@ -6296,6 +6310,55 @@ ScrStream *scr_stream_set_default_encoding(ScrStream *s, ScrDyn *enc) { return scr_stream_retain(s); } +/* Whether a defaultEncoding option value is the default: null, undefined, + * 'utf8' or 'utf-8' (the state constructors' kDefaultUTF8Encoding). */ +static bool scr_stream_default_encoding_value(const ScrDyn *v) { + if (!v || v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL) return true; + return v->kind == SCR_DYN_STR && ((v->v.str->len == 4 && memcmp(v->v.str->data, "utf8", 4) == 0) || + (v->v.str->len == 5 && memcmp(v->v.str->data, "utf-8", 5) == 0)); +} + +/* A runtime defaultEncoding option value, checked before construction as + * the state constructors check it: the default, a name Buffer.isEncoding + * knows, or ERR_UNKNOWN_ENCODING (thrown). Borrows v. */ +void scr_stream_check_encoding_option(ScrDyn *v) { + if (scr_stream_default_encoding_value(v)) return; + if (v->kind == SCR_DYN_STR) { + ScrStr *canonical = scr_bytes_normalize_encoding(v->v.str); + if (canonical) scr_str_release(canonical); + return; + } + ScrStr *shown = scr_dyn_string_coerce_js(v); + if (!shown) return; + char msg[128]; + int len = snprintf(msg, sizeof msg, "Unknown encoding: %.*s", (int)(shown->len < 64 ? shown->len : 64), + shown->data); + scr_str_release(shown); + scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)(len < 0 ? 0 : len), "ERR_UNKNOWN_ENCODING"); +} + +/* Runtime decodeStrings and defaultEncoding option values over a fresh + * stream (after its objectMode): decodeStrings === false keeps string + * writes, and a known defaultEncoding is the write side's default as + * given and how a byte-mode readable side decodes push(string). Borrows + * both; answers the receiver (+1). */ +ScrStream *scr_stream_option_values(ScrStream *s, ScrDyn *decode_strings, ScrDyn *default_encoding) { + ScrStreamState *st = s->st; + if (st->has_w && decode_strings && decode_strings->kind == SCR_DYN_BOOL && !decode_strings->v.b) + scr_stream_set_decode_strings(s, false); + if (!scr_stream_default_encoding_value(default_encoding) && default_encoding->kind == SCR_DYN_STR) { + ScrStr *canonical = scr_bytes_normalize_encoding(default_encoding->v.str); + if (canonical) { + scr_stream_set_write_encoding(s, default_encoding->v.str); + if (st->has_r && !st->r.object_entries) scr_stream_release(scr_stream_set_push_encoding(s, canonical)); + scr_str_release(canonical); + } else if (scr_exc_pending()) { + scr_exc_clear(); /* checked before construction */ + } + } + return scr_stream_retain(s); +} + /* stream.objectMode: scr_stream_set_object_mode, answering the receiver * (+1). */ ScrStream *scr_stream_object_mode(ScrStream *s, bool readable, bool writable) { diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index d26077b5c..e368ec7ff 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -563,6 +563,9 @@ void scr_stream_set_writev_concat(ScrStream *s); void scr_stream_set_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); void scr_stream_set_writev(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); +/* A compiled writev whose entries declare `encoding` a string: entries + * without an encoding (object mode) get "" before the conversion. */ +void scr_stream_writev_fill_encodings(struct ScrDyn *chunks); /* Duplex.from (duplexify.js): over an async generator function (the * compiled stage takes the source generator that the compiled source * makes from next() and ack(); all borrowed, helpers too), and over a @@ -605,6 +608,11 @@ ScrStream *scr_stream_duplex_from_promise(struct ScrPromise *p, ScrClosure *box) * forms answer the stream (+1). */ void scr_stream_set_decode_strings(ScrStream *s, bool decode); ScrStream *scr_stream_decode_strings(ScrStream *s, bool decode); +/* Runtime decodeStrings/defaultEncoding option values: checked before + * construction (ERR_UNKNOWN_ENCODING thrown), then applied to the fresh + * stream, answering it (+1). Values borrowed. */ +void scr_stream_check_encoding_option(struct ScrDyn *v); +ScrStream *scr_stream_option_values(ScrStream *s, struct ScrDyn *decode_strings, struct ScrDyn *default_encoding); void scr_stream_set_write_encoding(ScrStream *s, ScrStr *enc); ScrStream *scr_stream_write_encoding(ScrStream *s, ScrStr *enc); ScrStream *scr_stream_set_default_encoding(ScrStream *s, struct ScrDyn *enc); diff --git a/tests/corpus/7460-transform-writev.mjs b/tests/corpus/7460-transform-writev.mjs new file mode 100644 index 000000000..d78bdfc77 --- /dev/null +++ b/tests/corpus/7460-transform-writev.mjs @@ -0,0 +1,33 @@ +// writev on a Transform and a PassThrough (the writev option): a batch of +// buffered writes reaches writev instead of the transform, a lone write +// still reaches the transform, and the readable side gets what writev +// pushes. +import { Transform, PassThrough } from "node:stream"; +const t = new Transform({ + transform(chunk, enc, cb) { console.log("transform", String(chunk), enc); cb(null, chunk); }, + writev(chunks, cb) { + console.log("writev", chunks.map((c) => `${c.chunk}/${c.encoding}`).join(",")); + for (const { chunk } of chunks) this.push(String(chunk).toUpperCase()); + cb(); + }, +}); +t.on("data", (d) => console.log("data", String(d))); +t.on("finish", () => console.log("finish")); +t.on("end", () => console.log("end")); +t.write("a"); +t.cork(); +t.write("b"); +t.write("c", "latin1"); +process.nextTick(() => { t.uncork(); t.end("d"); }); + +const p = new PassThrough({ + objectMode: true, + writev(chunks, cb) { console.log("pt writev", JSON.stringify(chunks.map((c) => c.chunk))); cb(); }, +}); +p.on("data", (d) => console.log("pt data", JSON.stringify(d))); +p.cork(); +p.write({ x: 1 }); +p.write([2]); +p.uncork(); +p.write("solo"); +p.end(); diff --git a/tests/corpus/7461-transform-writev.cjs b/tests/corpus/7461-transform-writev.cjs new file mode 100644 index 000000000..dd45cf09d --- /dev/null +++ b/tests/corpus/7461-transform-writev.cjs @@ -0,0 +1,33 @@ +// Constructor-function Transform and PassThrough streams with _writev: +// the prototype's _writev takes the batches, and a writev option shadows +// it with the stream as this. +'use strict'; +const { Transform, PassThrough } = require('node:stream'); +function T(opts) { Transform.call(this, opts); } +Object.setPrototypeOf(T.prototype, Transform.prototype); +Object.setPrototypeOf(T, Transform); +T.prototype._transform = function (chunk, enc, cb) { cb(null, 'T(' + chunk + ')'); }; +T.prototype._writev = function (chunks, cb) { + this.push('V(' + chunks.map((c) => String(c.chunk)).join('+') + ')'); + cb(); +}; +const t = Object.create(T.prototype); +T.call(t, {}); +t.on('data', (d) => console.log('t data', String(d))); +t.on('end', () => console.log('t end')); +t.write('a'); +t.cork(); +t.write('b'); +t.write('c'); +process.nextTick(() => { t.uncork(); t.end(); }); +function P(opts) { PassThrough.call(this, opts); } +Object.setPrototypeOf(P.prototype, PassThrough.prototype); +Object.setPrototypeOf(P, PassThrough); +const p = Object.create(P.prototype); +P.call(p, { writev(chunks, cb) { console.log('p writev', chunks.length, this === p); cb(); } }); +p.on('data', (d) => console.log('p data', String(d))); +p.cork(); +p.write('x'); +p.write('y'); +p.uncork(); +p.write('z'); diff --git a/tests/corpus/7462-transform-writev.ts b/tests/corpus/7462-transform-writev.ts new file mode 100644 index 000000000..ca079b3c9 --- /dev/null +++ b/tests/corpus/7462-transform-writev.ts @@ -0,0 +1,109 @@ +// Typed writev on Transform and PassThrough: an options writev with +// decodeStrings: false and flush, a Transform subclass's _writev, a +// batch completing with an error, and object-mode entries whose +// encoding is declared as a string or as Node's string | null | +// undefined. +import { Transform, PassThrough, Writable } from "node:stream"; +type WritevChunks = Array<{ chunk: any; encoding: BufferEncoding }>; +type Cb = (error?: Error | null) => void; +type TransformCallback = (error?: Error | null, data?: any) => void; +const log: string[] = []; +const t = new Transform({ + decodeStrings: false, + transform(chunk: string, enc: BufferEncoding, cb: TransformCallback): void { + log.push(`transform ${chunk} ${enc}`); + cb(null, chunk); + }, + flush(cb: TransformCallback): void { + log.push("flush"); + cb(null, "!"); + }, + writev(chunks: WritevChunks, cb: Cb): void { + log.push(`writev ${chunks.map((c) => `${c.chunk}/${c.encoding}`).join(",")}`); + this.push(chunks.map((c) => String(c.chunk)).join("")); + cb(); + }, +}); +t.on("data", (d: Buffer) => log.push(`data ${d.toString()}`)); +t.on("end", () => log.push("end")); +t.on("finish", () => log.push("finish")); +t.on("close", () => { + console.log(log.join("\n")); + next(); +}); +t.write("one"); +t.cork(); +t.write("two", "latin1"); +t.write("three"); +t.uncork(); +t.end("four"); + +class Batcher extends Transform { + _transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + console.log("batcher transform", chunk.toString()); + cb(null, chunk); + } + _writev(chunks: Array<{ chunk: any; encoding: BufferEncoding }>, cb: Cb): void { + console.log("batcher writev", chunks.length); + this.push(Buffer.concat(chunks.map((c) => c.chunk as Buffer))); + cb(); + } +} + +function next(): void { + const b = new Batcher(); + b.on("data", (d: Buffer) => console.log("batcher data", d.toString())); + b.write("x"); + b.cork(); + b.write("y"); + b.write("z"); + process.nextTick(() => { + b.uncork(); + b.end(); + }); + b.on("finish", () => { + const p = new PassThrough({ + writev(chunks: WritevChunks, cb: Cb): void { + console.log("pt writev", chunks.length, String(chunks[0]!.chunk)); + cb(new Error("batch failed")); + }, + }); + p.on("error", (e: Error) => { + console.log("pt error", e.message); + objectEntries(); + }); + p.cork(); + p.write("1"); + p.write("2"); + p.uncork(); + }); +} + +function objectEntries(): void { + const declared = new Writable({ + objectMode: true, + write(_chunk: any, _enc: BufferEncoding, cb: Cb): void { cb(); }, + writev(chunks: WritevChunks, cb: Cb): void { + console.log("string encodings", JSON.stringify(chunks.map((c) => c.chunk))); + cb(); + }, + }); + declared.write({ n: 0 }); + declared.cork(); + declared.write({ n: 1 }); + declared.write({ n: 2 }, () => {}); + declared.uncork(); + const nodeTyped = new Writable({ + objectMode: true, + write(_chunk: any, _enc: BufferEncoding, cb: Cb): void { cb(); }, + writev(chunks: Array<{ chunk: any; encoding: string | null | undefined }>, cb: Cb): void { + console.log("node encodings", JSON.stringify(chunks.map((c) => [c.chunk, c.encoding === undefined ? "undefined" : c.encoding]))); + cb(); + }, + }); + nodeTyped.write({ n: 0 }); + nodeTyped.cork(); + nodeTyped.write({ n: 1 }); + nodeTyped.write({ n: 2 }, () => {}); + nodeTyped.uncork(); +} diff --git a/tests/corpus/7463-stream-option-values.ts b/tests/corpus/7463-stream-option-values.ts new file mode 100644 index 000000000..04f497b6b --- /dev/null +++ b/tests/corpus/7463-stream-option-values.ts @@ -0,0 +1,58 @@ +// Typed decodeStrings and defaultEncoding options whose values are known +// only at run time: decodeStrings === false keeps string writes, a known +// defaultEncoding is the write default as given and how push(string) +// decodes, null and utf8 are the default, an unknown name throws +// ERR_UNKNOWN_ENCODING before construction, and a subclass's super() +// options take them the same way. +import { Writable, Readable, Duplex } from "node:stream"; +type Cb = (error?: Error | null) => void; +const flags: boolean[] = JSON.parse("[false, true]"); +const encodings: Array = JSON.parse('["hex", "latin1", null, "UTF8"]'); +for (const decode of flags) { + const w = new Writable({ + decodeStrings: decode, + defaultEncoding: encodings[0] as BufferEncoding, + write(chunk: any, enc: string, cb: Cb): void { + console.log("write", decode, typeof chunk, enc, String(chunk)); + cb(); + }, + }); + w.write("6869"); +} +const r = new Readable({ defaultEncoding: encodings[1] as BufferEncoding, read(): void {} }); +r.push("é"); +r.push(null); +r.on("data", (d: Buffer) => console.log("readable", d.toString("hex"))); +const dflt = new Writable({ + defaultEncoding: encodings[2] as any, + write(chunk: Buffer, enc: string, cb: Cb): void { + console.log("null default", enc, chunk.toString()); + cb(); + }, +}); +dflt.write("ok"); +const upper = new Duplex({ + defaultEncoding: encodings[3] as BufferEncoding, + decodeStrings: flags[0], + read(): void {}, + write(chunk: any, enc: string, cb: Cb): void { + console.log("duplex", typeof chunk, enc); + cb(); + }, +}); +upper.write("x"); +try { + new Writable({ defaultEncoding: JSON.parse('"nope"') }); +} catch (e) { + console.log("bad", (e as Error & { code?: string }).code, (e as Error).message); +} +class Sink extends Writable { + constructor(decode: boolean, encoding: BufferEncoding) { + super({ decodeStrings: decode, defaultEncoding: encoding }); + } + _write(chunk: any, enc: string, cb: Cb): void { + console.log("sink", typeof chunk, enc); + cb(); + } +} +new Sink(flags[0]!, encodings[1] as BufferEncoding).write("y"); diff --git a/tests/corpus/7464-stream-option-values.mjs b/tests/corpus/7464-stream-option-values.mjs new file mode 100644 index 000000000..dfeb7a447 --- /dev/null +++ b/tests/corpus/7464-stream-option-values.mjs @@ -0,0 +1,28 @@ +// Option values known only at run time in JavaScript: decodeStrings and +// defaultEncoding read from an object, a write callback whose chunk is +// then a string, decodeStrings: 0 (only false turns decoding off), a +// defaultEncoding that is not a string, and a _construct assigned after +// construction, which Node's constructor never reads. +import { Writable, Readable } from "node:stream"; +const opts = { decode: false, enc: "base64" }; +const w = new Writable({ + decodeStrings: opts.decode, + defaultEncoding: opts.enc, + write(c, e, cb) { console.log("js", typeof c, e, String(c)); cb(); }, +}); +w.write("aGk="); +try { new Writable({ defaultEncoding: 5 }); } catch (e) { console.log("bad", e.code, e.message); } +const zero = new Writable({ decodeStrings: 0, write(c, e, cb) { console.log("zero", Buffer.isBuffer(c), e); cb(); } }); +zero.write("z"); +const late = new Writable({ write(chunk, enc, cb) { console.log("write", chunk.toString()); cb(); } }); +let ran = false; +late._construct = (cb) => { ran = true; cb(); }; +late.write("a"); +late.end(() => console.log("ended, construct ran:", ran)); +const r = new Readable({ read() {} }); +const order = []; +(order.push("receiver"), r)._construct = (order.push("value"), function (cb) { console.log("never"); cb(); }); +console.log(order.join(",")); +r.push("x"); +r.push(null); +r.on("data", (d) => console.log("data", d.toString())); diff --git a/tests/corpus/7465-transform-callback-values.ts b/tests/corpus/7465-transform-callback-values.ts new file mode 100644 index 000000000..e7003e894 --- /dev/null +++ b/tests/corpus/7465-transform-callback-values.ts @@ -0,0 +1,41 @@ +// A transform or flush callback passing a value that is not a Buffer or a +// string: the push reports Node's ERR_INVALID_ARG_TYPE through 'error' +// and destroys the stream, for option callbacks and _transform methods, +// while Buffer and string values take the typed path. +import { Transform } from "node:stream"; +type TransformCallback = (error?: Error | null, data?: any) => void; +const t = new Transform({ + transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + console.log("transform", chunk.toString()); + cb(null, 42); + }, +}); +t.on("error", (e: Error & { code?: string }) => console.log("error", e.name, e.code, e.message)); +t.on("data", (d: Buffer) => console.log("data", d.toString())); +t.on("close", () => console.log("close", t.destroyed)); +console.log("write", t.write("a")); +const f = new Transform({ + transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { cb(null, chunk); }, + flush(cb: TransformCallback): void { cb(null, { tail: true }); }, +}); +f.on("error", (e: Error & { code?: string }) => console.log("flush error", e.code, e.message)); +f.on("data", (d: Buffer) => console.log("flush data", d.toString())); +f.on("close", () => console.log("flush close")); +f.end("z"); +class Bad extends Transform { + _transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + cb(null, chunk.length); + } +} +const bad = new Bad(); +bad.on("error", (e: Error & { code?: string }) => console.log("error", e.code, e.message)); +bad.on("close", () => console.log("close")); +bad.write("abc"); +class Good extends Transform { + _transform(chunk: Buffer, _enc: BufferEncoding, cb: TransformCallback): void { + cb(null, chunk.toString().toUpperCase()); + } +} +const good = new Good(); +good.on("data", (d: Buffer) => console.log("good", d.toString())); +good.end("xyz"); diff --git a/tests/harness/corpus-costs/7460-transform-writev.mjs.json b/tests/harness/corpus-costs/7460-transform-writev.mjs.json new file mode 100644 index 000000000..f6c21fd3d --- /dev/null +++ b/tests/harness/corpus-costs/7460-transform-writev.mjs.json @@ -0,0 +1 @@ +{ "plain": 2266, "san": 2991 } diff --git a/tests/harness/corpus-costs/7461-transform-writev.cjs.json b/tests/harness/corpus-costs/7461-transform-writev.cjs.json new file mode 100644 index 000000000..b5868c90b --- /dev/null +++ b/tests/harness/corpus-costs/7461-transform-writev.cjs.json @@ -0,0 +1 @@ +{ "plain": 1828, "san": 2413 } diff --git a/tests/harness/corpus-costs/7462-transform-writev.ts.json b/tests/harness/corpus-costs/7462-transform-writev.ts.json new file mode 100644 index 000000000..f8fd66c2a --- /dev/null +++ b/tests/harness/corpus-costs/7462-transform-writev.ts.json @@ -0,0 +1 @@ +{ "plain": 2178, "san": 2875 } diff --git a/tests/harness/corpus-costs/7463-stream-option-values.ts.json b/tests/harness/corpus-costs/7463-stream-option-values.ts.json new file mode 100644 index 000000000..0f66a4266 --- /dev/null +++ b/tests/harness/corpus-costs/7463-stream-option-values.ts.json @@ -0,0 +1 @@ +{ "plain": 1786, "san": 4192 } diff --git a/tests/harness/corpus-costs/7464-stream-option-values.mjs.json b/tests/harness/corpus-costs/7464-stream-option-values.mjs.json new file mode 100644 index 000000000..b609b128f --- /dev/null +++ b/tests/harness/corpus-costs/7464-stream-option-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 1678, "san": 4502 } diff --git a/tests/harness/corpus-costs/7465-transform-callback-values.ts.json b/tests/harness/corpus-costs/7465-transform-callback-values.ts.json new file mode 100644 index 000000000..1c67bb43f --- /dev/null +++ b/tests/harness/corpus-costs/7465-transform-callback-values.ts.json @@ -0,0 +1 @@ +{ "plain": 1571, "san": 2074 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 4d5546ab6..669a3a0d6 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -108,7 +108,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 33, marker: "scr_runtime_abi_v33" }, + runtime_abi: { version: 34, marker: "scr_runtime_abi_v34" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: { From 2f882e1e1e52e9d3692758d1669f5b67162211fc Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:44:07 -0700 Subject: [PATCH 036/409] Reflect the prototype of function-local classes without prototype members Object.getPrototypeOf on an instance of a function-local class refused unless its prototype members were installed at run time. A class that declares no prototype methods or accessors (zod 4's class Definition extends Parent {}) has nothing to install, so its shared prototype view is returned, as is a bare Local.prototype value. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-prototypes.ts | 24 ++++++++++++- ...3-local-class-prototype-reflection.js.json | 6 ++++ ...ocal-class-prototype-reflection-ts.ts.json | 6 ++++ .../7613-local-class-prototype-reflection.js | 36 +++++++++++++++++++ ...615-local-class-prototype-reflection-ts.ts | 14 ++++++++ ...3-local-class-prototype-reflection.js.json | 1 + ...ocal-class-prototype-reflection-ts.ts.json | 1 + 7 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json create mode 100644 tests/corpus/7613-local-class-prototype-reflection.js create mode 100644 tests/corpus/7615-local-class-prototype-reflection-ts.ts create mode 100644 tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json create mode 100644 tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 2a05cf977..f65509196 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -278,6 +278,16 @@ export function hasClassPrototypeData(info: ClassInfo): boolean { ); } +/** Methods and accessors that live on the class's prototype. */ +function declaresPrototypeMembers(decl: ts.ClassLikeDeclaration): boolean { + return decl.members.nodes.some( + (member) => + (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && + !ts.isPrivateIdentifier(member.name) && + !ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + ); +} + /** Populate the shared prototype view before exposing it to reflection. * Methods and accessors are non-enumerable and retain declaration identity. */ export function reflectClassPrototype( @@ -307,7 +317,10 @@ export function reflectClassPrototype( // Local JavaScript factories initialize captured prototype members on // their constructor value, so the shared runtime view is already complete. if (info.localClass && info.runtimePrototypeMembers) return prototype; - if (info.localClass) + // A function-local class declaring no prototype members (zod 4's + // `class Definition extends Parent {}`) has nothing to install: its + // shared runtime view is complete once its base's is. + if (info.localClass && declaresPrototypeMembers(info.decl)) return refuse("prototype reflection on a function-local class is not supported yet"); if (info.base && !info.base.builtinError) { reflectClassPrototype( @@ -533,6 +546,15 @@ export function lowerClassPrototypeData( if (name === "constructor") { return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr)); } + // The whole prototype of a function-local class: the shared view + // Object.getPrototypeOf answers for its instances. + if (name === null && info.localClass) { + const receiver = lowerer.lowerExprExpecting(expr.expression, { + kind: "classval", + className: info.def.name, + }); + return reflectClassPrototype(lowerer, info, locOf(expr), receiver); + } if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json new file mode 100644 index 000000000..bbf827605 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7613-local-class-prototype-reflection.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json new file mode 100644 index 000000000..229a4e2ff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7615-local-class-prototype-reflection-ts.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7613-local-class-prototype-reflection.js b/tests/corpus/7613-local-class-prototype-reflection.js new file mode 100644 index 000000000..61a116569 --- /dev/null +++ b/tests/corpus/7613-local-class-prototype-reflection.js @@ -0,0 +1,36 @@ +// Prototype reflection on instances of a function-local class without +// methods (zod 4's `$constructor` Definition): the prototype is the class's +// shared prototype object, it chains to the parent's, and members defined +// on it reach every instance. +const installed = new WeakSet(); +function factory(name, Parent) { + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + return function make(label) { + const inst = new Definition(); + inst.label = label; + const proto = Object.getPrototypeOf(inst); + console.log(name, proto === Definition.prototype, proto === Object.getPrototypeOf(new Definition())); + if (!installed.has(proto)) { + installed.add(proto); + Object.defineProperty(proto, "describe", { + configurable: true, + enumerable: false, + get() { + return () => `${name}:${this.label}`; + }, + }); + console.log("installed", name); + } + return inst; + }; +} +const makeError = factory("Failure", Error); +const makePlain = factory("Thing", Object); +const a = makeError("a"); +const b = makeError("b"); +const c = makePlain("c"); +console.log(a.describe(), b.describe(), c.describe()); +console.log(a instanceof Error, c instanceof Error, Object.keys(a).join(), Object.keys(c).join()); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b), Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); +console.log(typeof Object.getPrototypeOf(c).describe, "describe" in a, Object.hasOwn(a, "describe")); diff --git a/tests/corpus/7615-local-class-prototype-reflection-ts.ts b/tests/corpus/7615-local-class-prototype-reflection-ts.ts new file mode 100644 index 000000000..92d3b5668 --- /dev/null +++ b/tests/corpus/7615-local-class-prototype-reflection-ts.ts @@ -0,0 +1,14 @@ +// Prototype reflection on an instance of a TypeScript function-local class +// without methods: one shared prototype per class evaluation. +function make(label: string): { label: string } { + class Box { + label = ""; + } + const box = new Box(); + box.label = label; + return box; +} +const a = make("a"); +const b = make("b"); +const pa = Object.getPrototypeOf(a); +console.log(pa === Object.getPrototypeOf(a), pa === Object.getPrototypeOf(b), typeof pa, Object.keys(a).join()); diff --git a/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json b/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json new file mode 100644 index 000000000..9c4d9369a --- /dev/null +++ b/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json @@ -0,0 +1 @@ +{ "plain": 1967, "san": 2510 } diff --git a/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json b/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json new file mode 100644 index 000000000..2e0a455c2 --- /dev/null +++ b/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json @@ -0,0 +1 @@ +{ "plain": 1466, "san": 2143 } From 2ebb89b0ef90150b2f5015732b31fe64c90c9908 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 01:24:41 -0500 Subject: [PATCH 037/409] Store number arrays of proven int32s in 4-byte slots across direct calls An int32 array candidate can now be a module global or a parameter, not only a local. A number[] parameter qualifies when its function is reached by direct calls alone (not as a value, a method or a constructor, and not through a result-slot entry the backend calls in its place), it is never written, and every call passes a candidate at its position; a local, global or parameter may then be passed there as well. A global qualifies when every function reads it only in candidate positions and every write stores a fresh array. moduleInt32ArrayCandidates finds them by dropping, pass after pass, the ones a function or call disqualifies. The candidates one array can reach through calls form a group (int32ArrayGroups): the int32 slot analysis keeps or drops a group as one, since a store through any member writes the same array. A module with a module namespace object keeps its globals in doubles, as member reads can reach any global by name. --- packages/compiler/src/backend/llvm/emitter.ts | 22 +- packages/compiler/src/ir/int32-arrays.ts | 361 ++++++++++++++++-- packages/compiler/src/ir/int32-slots.ts | 102 +++-- .../test/int32-element-array-emission.test.ts | 39 ++ .../7580-int32-arrays-across-calls.ts.json | 6 + .../corpus/7580-int32-arrays-across-calls.ts | 146 +++++++ .../7580-int32-arrays-across-calls.ts.json | 1 + 7 files changed, 608 insertions(+), 69 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7580-int32-arrays-across-calls.ts.json create mode 100644 tests/corpus/7580-int32-arrays-across-calls.ts create mode 100644 tests/harness/corpus-costs/7580-int32-arrays-across-calls.ts.json diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index c0cc9fef2..bfa02dfca 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -68,7 +68,7 @@ import { type StringSplit, } from "./split-loops.js"; import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; -import { addRecordResultEntries } from "./record-results.js"; +import { addRecordResultEntries, resultEntryName } from "./record-results.js"; import { specializeCallbackCalls } from "../../ir/callback-specialization.js"; import { type ChunkResult, @@ -82,6 +82,7 @@ import { versionLoopBoundParameters } from "../../ir/parameter-versions.js"; import { allocatedLiteral, fillAllocationLength, + int32ArrayKey, selectInt32Arrays, } from "../../ir/int32-arrays.js"; import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js"; @@ -509,6 +510,15 @@ function provenInt32Arrays(mod: IrModule): ReadonlySet { return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]); } +/** The functions whose calls the backend may emit as calls of their + * result-slot entries (record-results.ts). */ +function resultEntryTargets(mod: IrModule): ReadonlySet { + const names = new Set(mod.functions.map((fn) => fn.name)); + const targets = new Set(); + for (const name of names) if (names.has(resultEntryName(name))) targets.add(name); + return targets; +} + /** The emitter for `input` after the module passes every emission runs. */ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { // Facts are keyed by node identity: emit the same module whether it was @@ -544,6 +554,7 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter { ), ), provenInt32Arrays, + resultEntryTargets, ) : mod, options, @@ -780,7 +791,7 @@ export class LlEmitter { everyStmtList(fn.body, { stmt: (s) => { if (s.kind !== "varDecl" && s.kind !== "assign") return true; - const key = `${fn.name}\0${s.localId}`; + const key = int32ArrayKey(fn.name, s.localId); const checked = checkedInt32Arrays.has(key); if (!checked && !int32Arrays.has(key)) return true; const value = s.kind === "varDecl" ? s.init : s.value; @@ -798,15 +809,16 @@ export class LlEmitter { }); } - /** Whether `arr` is one of the current function's int32 arrays - * (ir/int32-arrays.ts, kept by the int32 slot analysis). */ + /** Whether `arr` reads one of the int32 arrays (ir/int32-arrays.ts, kept + * by the int32 slot analysis): a local or parameter of the current + * function, or a module global. */ int32Array(arr: IrExpr): boolean { if (arr.kind !== "varRef") return false; if (this.int32KindArray(arr)) return true; if (this.int32Slots.int32Arrays.size === 0) return false; return ( this.int32ArrayTemps.has(arr.localId) || - this.int32Slots.int32Arrays.has(`${this.currentFunctionName}\0${arr.localId}`) + this.int32Slots.int32Arrays.has(int32ArrayKey(this.currentFunctionName, arr.localId)) ); } diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts index 02de7268f..0872fcddb 100644 --- a/packages/compiler/src/ir/int32-arrays.ts +++ b/packages/compiler/src/ir/int32-arrays.ts @@ -1,3 +1,4 @@ +import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js"; import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js"; type SwitchCase = Extract["cases"][number]; @@ -12,16 +13,21 @@ import { type IrTransform, } from "./traverse.js"; -/** Local number arrays the backend may store as int32 slots. +/** Number arrays the backend may store as int32 slots. * - * A candidate is a local `number[]` of a synchronous function that no - * other binding can hold and only compiled element operations reach: - * every write of the local stores a fresh array (an array literal, or - * `new Array(n).fill(v)`), and every read of it is the receiver of an - * element read, write, state test, `length`, `push` or `pop` (a discarded - * `pop` included), or the array of an element-reader helper call. The - * array never reaches a runtime function that reads its slots, a library - * call, a closure, a return or untyped code. + * A candidate is a `number[]` binding that only compiled element + * operations reach: a local of a synchronous function, a module global, or + * a parameter. Every write of a local or a global stores a fresh array (an + * array literal, or `new Array(n).fill(v)`); a parameter is never written. + * Every read of a candidate is the receiver of an element read, write, + * state test, `length`, `push` or `pop` (a discarded `pop` included), the + * array of an element-reader helper call, or the argument of a direct call + * whose parameter at that position is a candidate itself. A parameter is a + * candidate only when its function is reached by direct calls alone and + * every call passes a candidate there, so every binding that can hold one + * of these arrays is a candidate: the array never reaches a runtime + * function that reads its slots, a library call, a closure, a return or + * untyped code. * * Candidates are only proposed here. The int32 slot analysis * (int32-slots.ts) keeps the ones whose every stored element is a proven @@ -33,6 +39,18 @@ import { * calls are inlined first (rewriteCandidates), so no shared helper ever * receives an int32 array. */ +/** The candidate key of the binding `localId` as `fnName` reads it: + * `function\0local` for a local or parameter, `\0global` for a module + * global (`%g.` ids), which every function reads alike. */ +export function int32ArrayKey(fnName: string, localId: string): string { + return localId.startsWith("%g.") ? `\0${localId}` : `${fnName}\0${localId}`; +} + +/** The int32 element slot (int32-slots.ts) of a candidate key. */ +export function int32ArraySlot(key: string): string { + return key.startsWith("\0") ? `g:${key.slice(1)}` : `l:${key}`; +} + /** Array methods a candidate may be the receiver of: the runtime paths * behind them read and write int32 slots through the double-valued slot * accessors. */ @@ -202,10 +220,51 @@ function writesLocal(e: IrExpr, id: string): boolean { return !expr(e); } -/** The candidate locals of `fn` (see the module comment). */ -export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet): Set { +/** The parameters and globals a module's functions share as candidates, + * still assumed to qualify. */ +export interface SharedCandidates { + /** Candidate parameters, as `function\0local` keys. */ + params: ReadonlySet; + /** Candidate globals, as `%g.` ids. */ + globals: ReadonlySet; + /** Whether the parameter at `index` of `callee` is a candidate. */ + paramAt: (callee: string, index: number) => boolean; +} + +const NO_SHARED: SharedCandidates = { + params: new Set(), + globals: new Set(), + paramAt: () => false, +}; + +/** The candidates of `fn` (see the module comment): its own locals, its + * parameters in `shared.params`, and the globals of `shared.globals` that + * it reads only in candidate positions. `written` receives the globals it + * assigns fresh arrays. */ +export function int32ArrayCandidates( + fn: IrFunction, + readers: ReadonlySet, + shared: SharedCandidates = NO_SHARED, + written: Set = new Set(), +): Set { const result = new Set(); - if (fn.async || fn.generator) return result; + if (fn.async || fn.generator) { + // Globals read here leave the candidates. + for (const id of shared.globals) result.add(id); + everyStmtList(fn.body, { + expr: (e) => { + if ((e.kind === "varRef" || e.kind === "assignExpr") && result.has(e.localId)) + result.delete(e.localId); + return true; + }, + stmt: (s) => { + if ((s.kind === "varDecl" || s.kind === "assign") && result.has(s.localId)) + result.delete(s.localId); + return true; + }, + }); + return result; + } const captured = new Set( [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId), ); @@ -217,11 +276,12 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet(); + const allocated = new Set(); // A receiver position: the candidate is read there without escaping. const receiver = (e: IrExpr): void => { if (e.kind !== "varRef") expr(e); @@ -255,13 +315,22 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet { + if (shared.paramAt(e.callee, index)) receiver(arg); + else expr(arg); + }); return true; } } @@ -272,7 +341,8 @@ export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet | undefined, live: ReadonlySet): Set { + const result = new Set(); + if (ids) for (const id of ids) if (live.has(id)) result.add(id); + return result; +} + +/** The candidates of every function of `mod` that has any, only those + * whose keys are in `keep` when given. Parameters and globals are shared: + * starting from every one that may qualify, each pass drops the ones a + * function reads outside candidate positions, the parameters some call + * passes anything but a candidate, and the globals never assigned a fresh + * array, until a pass drops nothing. Functions in `redirected` keep their + * parameters: some of their calls reach another function. */ +export function moduleInt32ArrayCandidates( + mod: IrModule, + readers: ReadonlySet, + keep: ReadonlySet | null = null, + redirected: ReadonlySet = new Set(), +): Map> { + const kept = (key: string): boolean => keep === null || keep.has(key); + const arrayParams = new Map(); + for (const fn of mod.functions) { + // Class methods, constructors and runtime helpers (`%` names) keep + // their parameters. + if (fn.name.startsWith("%") || fn.async || fn.generator || redirected.has(fn.name)) continue; + const indexes: number[] = []; + fn.params.forEach((p, i) => { + if (p.type.kind === "array" && p.type.elem.kind === "f64") indexes.push(i); + }); + if (indexes.length > 0) arrayParams.set(fn.name, indexes); + } + let globals = new Set(); + for (const g of mod.globals ?? []) + if ( + g.type.kind === "array" && + g.type.elem.kind === "f64" && + g.initFlag === undefined && + kept(`\0${g.id}`) + ) + globals.add(g.id); + const result = new Map>(); + if (arrayParams.size === 0 && globals.size === 0) { + for (const fn of mod.functions) { + const candidates = int32ArrayCandidates(fn, readers); + for (const id of candidates) if (!kept(`${fn.name}\0${id}`)) candidates.delete(id); + if (candidates.size > 0) result.set(fn.name, candidates); + } + return result; + } + // One walk: the functions reached other than by direct calls, the direct + // calls of functions with number[] parameters, the globals each function + // reads or writes, and whether a module namespace object exists (its + // member reads can reach any global by name). + const reached = new Set(moduleFunctionReferences(mod)); + const calls: { caller: string; call: IrExpr & { kind: "call" } }[] = []; + const globalsIn = new Map>(); + let namespaces = false; + for (const fn of mod.functions) { + const used = new Set(); + const note = (id: string): void => { + if (globals.has(id)) used.add(id); + }; + everyStmtList(fn.body, { + expr: (e) => { + const name = exprFunctionReference(e); + if (name !== undefined) reached.add(name); + if (e.kind === "moduleNsRef") namespaces = true; + if (e.kind === "call" && arrayParams.has(e.callee)) + calls.push({ caller: fn.name, call: e }); + if (e.kind === "varRef" || e.kind === "assignExpr") note(e.localId); + return true; + }, + stmt: (s) => { + if (s.kind === "varDecl" || s.kind === "assign") note(s.localId); + return true; + }, + }); + if (used.size > 0) globalsIn.set(fn.name, used); + } + if (namespaces) { + globals = new Set(); + globalsIn.clear(); + } + const paramIds = new Map(); + let params = new Set(); + for (const fn of mod.functions) { + const indexes = arrayParams.get(fn.name); + if (!indexes || reached.has(fn.name)) continue; + paramIds.set( + fn.name, + fn.params.map((p) => p.localId), + ); + for (const i of indexes) { + const key = `${fn.name}\0${fn.params[i]!.localId}`; + if (kept(key)) params.add(key); + } + } + for (;;) { + const current = params; + const written = new Set(); + const liveGlobals = new Set(globals); + result.clear(); + for (const fn of mod.functions) { + const shared: SharedCandidates = { + params: current, + globals: liveIn(globalsIn.get(fn.name), globals), + paramAt: (callee, index) => { + const id = paramIds.get(callee)?.[index]; + return id !== undefined && current.has(`${callee}\0${id}`); + }, + }; + const candidates = int32ArrayCandidates(fn, readers, shared, written); + for (const id of shared.globals) if (!candidates.has(id)) liveGlobals.delete(id); + for (const id of candidates) + if (!shared.globals.has(id) && !kept(`${fn.name}\0${id}`)) candidates.delete(id); + if (candidates.size > 0) result.set(fn.name, candidates); + } + for (const id of liveGlobals) if (!written.has(id)) liveGlobals.delete(id); + const live = new Set( + [...current].filter((key) => { + const cut = key.indexOf("\0"); + return result.get(key.slice(0, cut))?.has(key.slice(cut + 1)) === true; + }), + ); + // A parameter needs a candidate at that position of every call; a call + // with fewer arguments passes undefined. + for (const { caller, call } of calls) { + const ids = paramIds.get(call.callee); + if (!ids) continue; + ids.forEach((id, i) => { + const key = `${call.callee}\0${id}`; + if (!live.has(key)) return; + const arg = call.args[i]; + const ok = + arg?.kind === "varRef" && + (arg.localId.startsWith("%g.") + ? liveGlobals.has(arg.localId) + : result.get(caller)?.has(arg.localId) === true); + if (!ok) live.delete(key); + }); + } + if (live.size === current.size && liveGlobals.size === globals.size) break; + params = live; + globals = liveGlobals; + } + return result; +} + +/** The candidates one array can reach together: a direct call's argument + * and the parameter it binds hold the same array, so their element facts + * stand or fall together. `group` maps each of `keys` to its group's keys + * (itself alone for most), and `globalReaders` each candidate global's + * key to the functions that read or write it. */ +export function int32ArrayGroups( + mod: IrModule, + keys: ReadonlySet, +): { group: Map; globalReaders: Map> } { + const parent = new Map(); + const root = (key: string): string => { + let r = key; + while (parent.has(r)) r = parent.get(r)!; + if (r !== key) parent.set(key, r); + return r; + }; + const globalReaders = new Map>(); + const params = new Map(mod.functions.map((fn) => [fn.name, fn.params])); + const paramKeys = new Set( + mod.functions.flatMap((fn) => fn.params.map((p) => `${fn.name}\0${p.localId}`)), + ); + if ([...keys].some((key) => key.startsWith("\0") || paramKeys.has(key))) { + for (const fn of mod.functions) { + const touch = (id: string): void => { + const key = int32ArrayKey(fn.name, id); + if (!key.startsWith("\0") || !keys.has(key)) return; + const set = globalReaders.get(key) ?? new Set(); + set.add(fn.name); + globalReaders.set(key, set); + }; + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "varRef" || e.kind === "assignExpr") touch(e.localId); + if (e.kind === "call") + e.args.forEach((arg, i) => { + const param = params.get(e.callee)?.[i]; + if (arg.kind !== "varRef" || !param) return; + const a = int32ArrayKey(fn.name, arg.localId), + p = `${e.callee}\0${param.localId}`; + if (!keys.has(a) || !keys.has(p)) return; + const ra = root(a), + rp = root(p); + if (ra !== rp) parent.set(ra, rp); + }); + return true; + }, + stmt: (s) => { + if (s.kind === "varDecl" || s.kind === "assign") touch(s.localId); + return true; + }, + }); + } + } + const members = new Map(); + for (const key of keys) { + const r = root(key); + const list = members.get(r); + if (list) list.push(key); + else members.set(r, [key]); + } + const group = new Map(); + for (const key of keys) group.set(key, members.get(root(key))!); + return { group, globalReaders }; +} + /** The element values stored into each candidate: element writes, pushed * values, literal elements and fill values. */ export function int32ArrayStores( @@ -471,40 +759,45 @@ function rewriteCandidates( return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) }; } -/** Propose the module's int32 array candidates (`function\0local` keys in +/** Propose the module's int32 array candidates (int32ArrayKey keys in * `int32ArrayCandidates`), only those in `keep` when given, and rewrite * their functions. */ export function markInt32ArrayCandidates( mod: IrModule, keep: ReadonlySet | null = null, + redirected: ReadonlySet = new Set(), ): IrModule { if (mod.lib !== undefined) return mod; const readers = new Map(); for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn); - const names = new Set(readers.keys()); - const keys: string[] = []; + const all = moduleInt32ArrayCandidates(mod, new Set(readers.keys()), keep, redirected); + if (all.size === 0) return mod; + const keys = new Set(); const functions = mod.functions.map((fn) => { - const candidates = int32ArrayCandidates(fn, names); - for (const id of candidates) - if (keep === null || keep.has(`${fn.name}\0${id}`)) keys.push(`${fn.name}\0${id}`); - else candidates.delete(id); - return candidates.size === 0 ? fn : rewriteCandidates(fn, candidates, readers); + const candidates = all.get(fn.name); + if (!candidates) return fn; + for (const id of candidates) keys.add(int32ArrayKey(fn.name, id)); + return rewriteCandidates(fn, candidates, readers); }); - return keys.length === 0 ? mod : { ...mod, functions, int32ArrayCandidates: keys }; + return { ...mod, functions, int32ArrayCandidates: [...keys] }; } /** The module the backend emits: rewritten for exactly the candidates the * int32 slot analysis (`prove`, returning its int32Arrays) keeps, so a * module without int32 arrays is `mod` itself. A dropped candidate's * rewrite is undone by marking again without it, until every candidate - * marked is kept. */ + * marked is kept. `redirected` names the functions some of whose calls the + * backend emits as calls of another function (a result-slot entry): their + * parameters keep doubles. */ export function selectInt32Arrays( mod: IrModule, prove: (marked: IrModule) => ReadonlySet, + redirected: (mod: IrModule) => ReadonlySet = () => new Set(), ): IrModule { let keep: ReadonlySet | null = null; + const others = redirected(mod); for (let round = 0; round < 4; round++) { - const marked = markInt32ArrayCandidates(mod, keep); + const marked = markInt32ArrayCandidates(mod, keep, others); if (marked === mod) return mod; const kept = prove(marked); const proposed = marked.int32ArrayCandidates ?? []; diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 4a2abc5b0..a8e91c86e 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -10,7 +10,13 @@ import { } from "./integer-ranges.js"; import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js"; import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js"; -import { checkableInt32Allocations, int32ArrayStores } from "./int32-arrays.js"; +import { + checkableInt32Allocations, + int32ArrayGroups, + int32ArrayKey, + int32ArraySlot, + int32ArrayStores, +} from "./int32-arrays.js"; import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js"; /** The storage slot an array-valued expression reads, as an @@ -247,9 +253,10 @@ export class Int32Slots { /** Whether the module has int32 array candidates (the narrowedReads * range fact). */ narrowedReads = false; - /** Local number arrays (`function\0local`) stored as int32 slots: the - * module's int32 array candidates whose every stored element the - * fixpoint proved an int32. */ + /** Number arrays stored as int32 slots (int32ArrayKey keys: locals and + * parameters as `function\0local`, globals as `\0global`): the module's + * int32 array candidates whose every stored element, through every + * binding that can hold the same array, the fixpoint proved an int32. */ readonly int32Arrays = new Set(); /** Local number arrays that may hold int32 slots with checked stores: the * candidates whose element stores the fixpoint proved exact integers but @@ -779,17 +786,29 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): for (const key of contracts?.int32Locals ?? []) slots.addInt32Local(key); for (const key of contracts?.int32Elements ?? []) slots.addInt32Elements(key); // Int32 array candidates (ir/int32-arrays.ts): assumed to hold int32 - // elements until a store the fixpoint cannot prove drops one. + // elements until a store the fixpoint cannot prove drops one, with every + // candidate the same array can reach (its group). const arrayCandidates = new Map>(); - slots.narrowedReads = (mod.int32ArrayCandidates?.length ?? 0) > 0; - for (const key of mod.int32ArrayCandidates ?? []) { + const globalArrays = new Set(); + const arrayKeys = new Set(mod.int32ArrayCandidates ?? []); + slots.narrowedReads = arrayKeys.size > 0; + for (const key of arrayKeys) { const cut = key.indexOf("\0"); - const fnName = key.slice(0, cut); - const set = arrayCandidates.get(fnName) ?? new Set(); - set.add(key.slice(cut + 1)); - arrayCandidates.set(fnName, set); - slots.addInt32Elements(`l:${key}`); + if (cut === 0) globalArrays.add(key.slice(1)); + else { + const fnName = key.slice(0, cut); + const set = arrayCandidates.get(fnName) ?? new Set(); + set.add(key.slice(cut + 1)); + arrayCandidates.set(fnName, set); + } + slots.addInt32Elements(int32ArraySlot(key)); } + const arrayGroups = arrayKeys.size > 0 ? int32ArrayGroups(mod, arrayKeys) : null; + // Functions that read or write a candidate global. + const globalArrayFunctions = new Set(); + if (arrayGroups) + for (const names of arrayGroups.globalReaders.values()) + for (const name of names) globalArrayFunctions.add(name); /* ── candidate fields ── */ const children = new Map(); @@ -1070,6 +1089,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): }; const hasCandidateWrite = (fn: IrFunction): boolean => { if (arrayCandidates.has(fn.name)) return true; + if (globalArrayFunctions.has(fn.name)) return true; const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId)); const returns = slots.hasReturn(fn.name); return !everyStmtList(fn.body, { @@ -1124,25 +1144,49 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): // (checkableInt32Allocations), it keeps int32 slots with checked stores // (checkedInt32Arrays), which widen it to doubles in place at the first // store that is not an int32. - const arrays = arrayCandidates.get(fn.name); - if (arrays) { - let dropped = false; + const locals = arrayCandidates.get(fn.name); + let arrays = locals; + if (globalArrayFunctions.has(fn.name)) { + const both = new Set(); + if (locals) for (const id of locals) both.add(id); + for (const id of globalArrays) both.add(id); + arrays = both; + } + if (arrays && arrayGroups) { + const requeue = (name: string): void => { + if (queued.has(name)) return; + queued.add(name); + queue.push(name); + }; + const params = new Set(fn.params.map((param) => param.localId)); for (const [id, values] of int32ArrayStores(fn, arrays)) { - const key = `l:${fn.name}\0${id}`; - if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue; - slots.dropInt32Elements(key); + const key = int32ArrayKey(fn.name, id); + if (!slots.hasInt32ElementSlot(int32ArraySlot(key)) || values.every(int32)) continue; + // The array's group drops together: every function reading a + // member is analyzed again without its facts. + const group = arrayGroups.group.get(key) ?? [key]; + for (const member of group) { + slots.dropInt32Elements(int32ArraySlot(member)); + if (member.startsWith("\0")) { + const names = arrayGroups.globalReaders.get(member); + if (names) for (const name of names) requeue(name); + } else requeue(member.slice(0, member.indexOf("\0"))); + } + // Checked stores serve a lone local: its allocations are the only + // arrays it holds. const checkable = (value: IrExpr): boolean => { const range = ranges.get(value); return int32(value) || range != null; }; - if (values.every(checkable) && checkableInt32Allocations(fn, id)) - slots.checkedInt32Arrays.add(`${fn.name}\0${id}`); - else reason(`array:${fn.name}\0${id}`, "an element store is not int32"); - dropped = true; - } - if (dropped && !queued.has(fn.name)) { - queued.add(fn.name); - queue.push(fn.name); + if ( + group.length === 1 && + !key.startsWith("\0") && + !params.has(id) && + values.every(checkable) && + checkableInt32Allocations(fn, id) + ) + slots.checkedInt32Arrays.add(key); + else for (const member of group) reason(`array:${member}`, "an element store is not int32"); } } // An int32 written into a slot still assumed non-negative: widen the @@ -1213,10 +1257,8 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}): } for (const proven of provenByFunction.values()) for (const value of proven) slots.provenWrites.add(value); - for (const [fnName, ids] of arrayCandidates) - for (const id of ids) - if (slots.hasInt32ElementSlot(`l:${fnName}\0${id}`)) - slots.int32Arrays.add(`${fnName}\0${id}`); + for (const key of arrayKeys) + if (slots.hasInt32ElementSlot(int32ArraySlot(key))) slots.int32Arrays.add(key); const ranged = rangedParameters(slots, functions, retry, numeric, (callee) => readers.get(`ret:${callee}`), ); diff --git a/packages/compiler/test/int32-element-array-emission.test.ts b/packages/compiler/test/int32-element-array-emission.test.ts index 0b68a5d3e..cd62278db 100644 --- a/packages/compiler/test/int32-element-array-emission.test.ts +++ b/packages/compiler/test/int32-element-array-emission.test.ts @@ -196,3 +196,42 @@ test("only the candidates the proof keeps are rewritten", async () => { for (const fn of selected.functions) if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name)); }); + +const sharedSource = ` +const data: number[] = []; +for (let i = 0; i < 64; i++) data.push((i * 7) & 255); +function fnv(values: number[]): number { + let h = 2166136261; + for (let i = 0; i < values.length; i++) h = Math.imul(h ^ values[i]!, 16777619) >>> 0; + return h; +} +function mean(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s += values[i]!; + return s / values.length; +} +function first(values: number[]): number { + return values[0]!; +} +const ints = [1, 2, 3]; +const halves = [0.5, 1.5]; +const pick: (values: number[]) => number = first; +console.log(fnv(data), mean(ints), mean(halves), pick([4])); +`; + +test("arrays passed to helpers and held in module globals get 4-byte slots", async () => { + const mod = await lowerIr(sharedSource); + const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? []; + // A global and the parameter every call passes it to; a function used as + // a value keeps its parameter. + expect(proposed).toEqual(expect.arrayContaining(["\0%g.e.data", "fnv\0values.0"])); + expect(proposed.some((key) => key.startsWith("first\0"))).toBe(false); + const llvm = await emitLlvm(sharedSource); + // The helper reads int32 slots; `mean`, also called with fractions, + // reads doubles. + expect(functions(llvm, "fnv")).toMatch( + /getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/, + ); + expect(functions(llvm, "mean")).not.toMatch(/getelementptr inbounds i32, ptr/); + expect(llvm).toMatch(/@scr_arr_new(_small)?\(i32 7/); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7580-int32-arrays-across-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7580-int32-arrays-across-calls.ts.json new file mode 100644 index 000000000..33282756c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7580-int32-arrays-across-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7580-int32-arrays-across-calls.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7580-int32-arrays-across-calls.ts b/tests/corpus/7580-int32-arrays-across-calls.ts new file mode 100644 index 000000000..13df54609 --- /dev/null +++ b/tests/corpus/7580-int32-arrays-across-calls.ts @@ -0,0 +1,146 @@ +// Number arrays of proven int32s that reach other functions: passed to +// helpers called directly, along chains of helpers and recursion, and held +// in module globals that several functions read. A helper that also +// receives an array of other numbers or one that is not a fresh array, +// stores a fraction through its parameter, or escapes as a value keeps +// every array it can reach in doubles, as does a global an async function +// reads. Each case uses its own arrays and helpers. + +const data: number[] = []; +let seed = 7; +for (let i = 0; i < 2000; i++) { + seed = (Math.imul(seed, 48271) + 11) & 0x7fffffff; + data.push(seed % 1000); +} + +function fnv(values: number[]): number { + let h = 2166136261; + for (let i = 0; i < values.length; i++) { + h ^= values[i]! & 0xff; + h = Math.imul(h, 16777619) >>> 0; + } + return h; +} + +function histogram(values: number[], buckets: number): number[] { + const counts: number[] = new Array(buckets).fill(0); + for (let i = 0; i < values.length; i++) { + const b = values[i]! % buckets; + counts[b] = counts[b]! + 1; + } + return counts; +} + +// A helper that writes through its parameter and calls another helper. +function scale(values: number[], factor: number): number { + for (let i = 0; i < values.length; i++) values[i] = (values[i]! * factor) & 0xffff; + return sum(values); +} + +function sum(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s = (s + values[i]!) | 0; + return s; +} + +// Recursion passes the array on, and the elements index another array. +function chase(next: number[], at: number, steps: number): number { + if (steps === 0) return at; + return chase(next, next[at]!, steps - 1); +} + +function pushAll(target: number[], count: number): void { + for (let i = 0; i < count; i++) target.push((i * 3) & 0xffff); +} + +// `n` is a power of two: the masked elements are indexes of `perm`. +function locals(n: number): string { + const perm: number[] = []; + for (let i = 0; i < n; i++) perm.push((i * 37 + 11) & (n - 1)); + const copy: number[] = []; + pushAll(copy, n); + const s1 = scale(copy, 5); + return `locals ${chase(perm, 0, 1000)} ${s1} ${copy[n - 1]} ${copy.length} ${sum(perm)}`; +} + +// Read only through the element reader (`a[i] ?? 0`) and states. +function readSparse(values: number[], at: number): string { + return `${values[at] ?? -1} ${at in values} ${values.length}`; +} + +// The same helper receives an array of other numbers too: the arrays it +// can reach keep doubles. +function mean(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s += values[i]!; + return s / values.length; +} + +// A store through the parameter that is not an int32. +function halve(values: number[]): void { + for (let i = 0; i < values.length; i++) values[i] = values[i]! / 2; +} + +// A function used as a value keeps its parameters. +function first(values: number[]): number { + return values[0]!; +} + +let reassigned: number[] = [1, 2, 3]; +function regrow(n: number): number { + reassigned = []; + for (let i = 0; i < n; i++) reassigned.push(i & 15); + let s = 0; + for (let i = 0; i < reassigned.length; i++) s += reassigned[i]!; + return s; +} + +// Not a fresh array: the result of a call. +function total(values: number[]): number { + let s = 0; + for (let i = 0; i < values.length; i++) s += values[i]!; + return s; +} + +const awaited: number[] = [4, 5, 6]; +async function later(): Promise { + return awaited[0]! + awaited.length; +} + +// A helper whose record result its callers only project: the backend may +// call its result-slot entry instead, so its parameter keeps doubles. +interface Span { + lo: number; + hi: number; +} +function spanAt(values: number[], i: number): Span { + return { lo: values[i]!, hi: values[i + 1]! }; +} +const spans: number[] = [5, 9, 2, 7, 11]; +function spanTotal(): number { + let total = 0; + for (let i = 0; i < 4; i++) { + const span = spanAt(spans, i); + total += span.hi * 10 - span.lo; + } + return total; +} + +const ints: number[] = [3, 4, 5]; +const fractions: number[] = [0.5, 1.5, 2.5]; +const halved: number[] = [8, 6, 4]; +halve(halved); +const pick: (values: number[]) => number = first; +const viaValue: number[] = [9, 8, 7]; + +console.log(`fnv ${fnv(data)} ${fnv(data)}`); +const h = histogram(data, 10); +console.log(`hist ${h.length} ${h[0]} ${h[9]} ${total(h)}`); +console.log(locals(128)); +console.log(readSparse(data, 5), readSparse(data, 5000)); +console.log(`mean ${mean(ints)} ${mean(fractions)}`); +console.log(`halved ${halved[0]} ${halved[2]} ${halved.length}`); +console.log(`value ${pick(viaValue)} ${viaValue.length}`); +console.log(`regrow ${regrow(40)} ${reassigned.length} ${reassigned[39]} ${regrow(5)}`); +console.log(`span ${spanTotal()}`); +void later().then((v) => console.log(`later ${v}`)); diff --git a/tests/harness/corpus-costs/7580-int32-arrays-across-calls.ts.json b/tests/harness/corpus-costs/7580-int32-arrays-across-calls.ts.json new file mode 100644 index 000000000..7c9d20632 --- /dev/null +++ b/tests/harness/corpus-costs/7580-int32-arrays-across-calls.ts.json @@ -0,0 +1 @@ +{ "plain": 3100, "san": 3500 } From b6d6eb60aed3849954d9f69cc0ff4607d63a7504 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 01:34:19 -0500 Subject: [PATCH 038/409] Replace a scheduled _construct hook, and keep runtime option values in order Node's constructNT reads _construct on the construction tick, so an assignment after the constructor and before that tick replaces a scheduled construct hook. The assignment was dropped, so the original hook ran. It now lowers to stream.replaceConstruct, which swaps the hook while its tick has not run and has no effect after it or on a stream without a hook. Runtime decodeStrings and defaultEncoding values are read into locals before the constructor evaluates the other option members, while Node evaluates the members in order. A runtime value after a member whose value has effects (a call or an assignment) now refuses instead of reading a value that member could still change. --- .../builtin-apis/streams-and-readline.mdx | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../compiler/src/backend/llvm/lib-stream.ts | 3 + .../src/frontend/lowering/lower-stream.ts | 63 +++++++++++++------ .../compiler/src/ir/builtin-signatures.ts | 1 + packages/compiler/src/ir/validate.ts | 1 + .../7466-stream-construct-assign.mjs.json | 6 ++ .../stream-runtime-option-order.ts.json | 6 ++ packages/runtime/src/scr_stream.c | 13 ++++ packages/runtime/src/scr_stream.h | 4 ++ tests/corpus/7466-stream-construct-assign.mjs | 46 ++++++++++++++ .../stream-runtime-option-order.ts | 28 +++++++++ .../stream-runtime-option-order.ts.txt | 17 +++++ .../7466-stream-construct-assign.mjs.json | 1 + 14 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json create mode 100644 tests/corpus/7466-stream-construct-assign.mjs create mode 100644 tests/diagnostics/stream-runtime-option-order.ts create mode 100644 tests/harness/__snapshots__/stream-runtime-option-order.ts.txt create mode 100644 tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index b88ead1f4..166fc3402 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -33,6 +33,7 @@ `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on every class with a writable side. + A `decodeStrings` or `defaultEncoding` value known only at run time is unsupported after an option member whose value has effects (a call or an assignment). In typed code, a `writev` entry's `encoding` declared as a string receives `""` where Node passes `undefined` or `null` (an object-mode write). `setDefaultEncoding()` takes any encoding Node knows. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index f31906354..25af12ad6 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1768,6 +1768,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ "stream.setTransform", "stream.setFlush", "stream.setConstruct", + "stream.replaceConstruct", "stream.duplexFromGen", "stream.duplexFromDyn", "stream.duplexifyReadable", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 5fd4af1a9..fdbf13d4f 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -189,6 +189,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV e.fn === "stream.setTransform" || e.fn === "stream.setFlush" || e.fn === "stream.setConstruct" || + e.fn === "stream.replaceConstruct" || e.fn === "stream.setWritev" || e.fn === "stream.setWritevDefault" ) { @@ -203,6 +204,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV "stream.setTransform": "t", "stream.setFlush": "l", "stream.setConstruct": "c", + "stream.replaceConstruct": "c", "stream.setWritev": "v", "stream.setWritevDefault": "v", }; @@ -214,6 +216,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV "stream.setTransform": "scr_stream_set_transform", "stream.setFlush": "scr_stream_set_flush", "stream.setConstruct": "scr_stream_set_construct", + "stream.replaceConstruct": "scr_stream_replace_construct", "stream.setWritev": "scr_stream_set_writev", "stream.setWritevDefault": "scr_stream_set_writev_default", }; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 15afb741e..cf9f19b9d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -106,6 +106,21 @@ function pureRead(node: ts.Expression): boolean { return false; } +/** Whether every option member before `prop` evaluates without effects. + * Runtime decodeStrings and defaultEncoding values are read into locals + * before the constructor evaluates the other members, while Node evaluates + * the members in order: an earlier member with effects could change them. */ +function earlierOptionsPure(options: ts.ObjectLiteralExpression, prop: ts.Node): boolean { + for (const member of options.properties.nodes) { + if (member === prop) return true; + if (ts.isPropertyAssignment(member)) { + if (!sideEffectFreeOptionValue(member.initializer) && !pureRead(member.initializer)) + return false; + } else if (ts.isSpreadAssignment(member)) return false; + } + return true; +} + /** The stream sides of a receiver class: the nearest stream-class * ancestor's, or null off the stream hierarchy. */ export function streamSidesOf( @@ -1021,6 +1036,16 @@ function parseStreamOptions( break; } // a runtime value: WritableState's `options.decodeStrings !== false` + if ( + optsLit !== undefined && + ts.isObjectLiteralExpression(optsLit) && + !earlierOptionsPure(optsLit, prop) + ) + lowerer.noLowering( + `the ${ctorName} option 'decodeStrings' with a runtime value after an option value with effects`, + prop, + "compute the earlier option values into consts before the options object (the runtime value is read before the other options run)", + ); const local = lowerer.declareHiddenLocal("%decodeStrings", DYN); out.valueDecls.push({ kind: "varDecl", @@ -1076,6 +1101,16 @@ function parseStreamOptions( // a runtime value: null, undefined and utf8 are the default, // another name Buffer.isEncoding knows is kept as given, and any // other value throws ERR_UNKNOWN_ENCODING before construction + if ( + optsLit !== undefined && + ts.isObjectLiteralExpression(optsLit) && + !earlierOptionsPure(optsLit, prop) + ) + lowerer.noLowering( + `the ${ctorName} option 'defaultEncoding' with a runtime value after an option value with effects`, + prop, + "compute the earlier option values into consts before the options object (the runtime value is read before the other options run)", + ); const local = lowerer.declareHiddenLocal("%defaultEncoding", DYN); const ref = varRef(local.id, DYN, loc); out.valueDecls.push( @@ -1869,24 +1904,15 @@ export function lowerStreamUnderscoreAssign( const loc = locOf(expr); const cls = base.def.name; const clsName = cls.slice(1); - if (methodName === "_construct") { - // Node's constructor reads _construct once; a later assignment has no - // effect on the stream, so only the receiver and the value are - // evaluated - const value = lowerer.lowerExpr(expr.right); - return { - kind: "exprStmt", - expr: { - kind: "seqExpr", - stmts: [{ kind: "exprStmt", expr: recv, loc }], - result: value, - type: value.type, - loc, - }, - loc, - }; - } - const which = methodName === "_writev" ? "writev" : optionByMethod.get(methodName)!; + // `_construct`: Node's constructNT reads it on the construction tick, so + // an assignment before that tick replaces a scheduled hook + // (stream.replaceConstruct) and has no effect after it or without one. + const which = + methodName === "_writev" + ? "writev" + : methodName === "_construct" + ? "construct" + : optionByMethod.get(methodName)!; const { accepted } = streamCtorShape(cls); if (!accepted.includes(which) && !streamExtraCallbacks(cls).includes(which)) { lowerer.noLowering( @@ -1925,6 +1951,7 @@ export function lowerStreamUnderscoreAssign( transform: "stream.setTransform", flush: "stream.setFlush", writev: "stream.setWritev", + construct: "stream.replaceConstruct", }; return { kind: "exprStmt", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index aa31105e7..ca91b399c 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3975,6 +3975,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "stream.setTransform": { argTypes: [null, null], result: VOID }, "stream.setFlush": { argTypes: [null, null], result: VOID }, "stream.setConstruct": { argTypes: [null, null], result: VOID }, + "stream.replaceConstruct": { argTypes: [null, null], result: VOID }, "stream.setWritev": { argTypes: [null, null], result: VOID }, "stream.setWritevDefault": { argTypes: [null, null], result: VOID }, /** The dyn-options twins (a checked-dynamic options record — the JS diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 44c032620..8b3b6c048 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5508,6 +5508,7 @@ function validateFunction( e.fn === "stream.duplexFromGenSignal" || e.fn === "stream.duplexFromAsyncFnSignal" || e.fn === "stream.setConstruct" || + e.fn === "stream.replaceConstruct" || e.fn === "stream.setWritev" || e.fn === "stream.setWritevDefault" || e.fn === "stream.iteratorClose" || diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json new file mode 100644 index 000000000..f5bb52517 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7466-stream-construct-assign.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7466-stream-construct-assign.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json new file mode 100644 index 000000000..40b64edac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/stream-runtime-option-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/stream-runtime-option-order.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 4b962675a..cd95e12cd 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -269,6 +269,7 @@ struct ScrStreamState { * destroyed and runs its _destroy once construction completes. */ bool constructing, destroy_deferred; bool construct_finished; /* onConstruct ran: a second callback is ERR_MULTIPLE_CALLBACK */ + bool construct_called; /* the construct tick called the hook */ ScrError *destroy_err; /* owned: the deferred destroy's error */ ScrClosure *construct_cb; ScrStreamPlainInv construct_inv; @@ -7523,6 +7524,17 @@ void scr_stream_set_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamP scr_stream_construct(s, cb, inv); } +void scr_stream_replace_construct(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamPlainInv inv) { + ScrStreamState *st = s->st; + if (!st->constructing || st->construct_called || !st->construct_cb) { + scr_closure_release(cb); + return; + } + scr_closure_release(st->construct_cb); + st->construct_cb = cb; + st->construct_inv = inv; +} + /* A class's _writev under options that may carry their own writev (the * option, an instance property in Node, wins). */ void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamWritevInv inv) { @@ -7791,6 +7803,7 @@ static void scr_stream_run_tick(ScrStreamTick *t) { scr_stream_error_buffer(s); break; case SCR_ST_CONSTRUCT: { + st->construct_called = true; if (st->construct_cb) { st->construct_inv(st->construct_cb, s); /* a throw inside _construct reaches onConstruct on a tick */ diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index e368ec7ff..a4798c629 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -561,6 +561,10 @@ void scr_stream_set_writev_concat(ScrStream *s); /* The user construct option / _construct method (scheduled now; cb * moves), and the writev option / _writev method (cb moves). */ void scr_stream_set_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); +/* `s._construct = fn` after construction: replaces a construct hook whose + * tick has not run yet (Node's constructNT reads _construct then), and has + * no effect otherwise (cb moves). */ +void scr_stream_replace_construct(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); void scr_stream_set_writev(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); void scr_stream_set_writev_default(ScrStream *s, ScrClosure *cb, ScrStreamWritevInv inv); /* A compiled writev whose entries declare `encoding` a string: entries diff --git a/tests/corpus/7466-stream-construct-assign.mjs b/tests/corpus/7466-stream-construct-assign.mjs new file mode 100644 index 000000000..37684afcd --- /dev/null +++ b/tests/corpus/7466-stream-construct-assign.mjs @@ -0,0 +1,46 @@ +// Assigning _construct after construction: Node reads it on the +// construction tick, so an assignment before that tick replaces the +// scheduled hook, and one after it, or on a stream without a hook, has no +// effect. +import { Readable, Writable } from "node:stream"; + +const w = new Writable({ + construct(cb) { + console.log("original construct"); + cb(); + }, + write(chunk, _enc, cb) { + console.log("write", chunk.toString()); + cb(); + }, +}); +w._construct = function (cb) { + console.log("replacement construct"); + cb(); +}; +w.write("a"); +w.end(); +w.on("finish", () => { + w._construct = function (cb) { + console.log("late construct"); + cb(); + }; + console.log("finish"); + readable(); +}); + +// A stream without a construct hook: the assignment has no effect. +function readable() { + const r = new Readable({ + read() { + this.push("x"); + this.push(null); + }, + }); + r._construct = function (cb) { + console.log("never"); + cb(); + }; + r.on("data", (d) => console.log("data", d.toString())); + r.on("end", () => console.log("end")); +} diff --git a/tests/diagnostics/stream-runtime-option-order.ts b/tests/diagnostics/stream-runtime-option-order.ts new file mode 100644 index 000000000..b3854e328 --- /dev/null +++ b/tests/diagnostics/stream-runtime-option-order.ts @@ -0,0 +1,28 @@ +import { Writable } from "node:stream"; + +// Runtime decodeStrings and defaultEncoding values are read before the +// other option members are evaluated, while Node evaluates the members in +// order: a value after a member with effects refuses. +let encoding = (process.env["STREAM_ENCODING"] ?? "utf8") as BufferEncoding; +let decode = process.env["STREAM_DECODE"] !== "0"; +function bump(): number { + encoding = "hex"; + decode = false; + return 16; +} +const sink = new Writable({ + highWaterMark: bump(), + defaultEncoding: encoding, + write(_chunk, _encoding, done) { + done(); + }, +}); +const other = new Writable({ + highWaterMark: bump(), + decodeStrings: decode, + write(_chunk, _encoding, done) { + done(); + }, +}); +sink.end(); +other.end(); diff --git a/tests/harness/__snapshots__/stream-runtime-option-order.ts.txt b/tests/harness/__snapshots__/stream-runtime-option-order.ts.txt new file mode 100644 index 000000000..8e08dfbf0 --- /dev/null +++ b/tests/harness/__snapshots__/stream-runtime-option-order.ts.txt @@ -0,0 +1,17 @@ +stream-runtime-option-order.ts:15:3 - error SC2020: 'the Writable option 'defaultEncoding' with a runtime value after an option value with effects' is part of the standard library types but has no scriptc lowering yet + + 14 | highWaterMark: bump(), + 15 | defaultEncoding: encoding, + | ^~~~~~~~~~~~~~~~~~~~~~~~~ + 16 | write(_chunk, _encoding, done) { + + hint: compute the earlier option values into consts before the options object (the runtime value is read before the other options run) + +stream-runtime-option-order.ts:22:3 - error SC2020: 'the Writable option 'decodeStrings' with a runtime value after an option value with effects' is part of the standard library types but has no scriptc lowering yet + + 21 | highWaterMark: bump(), + 22 | decodeStrings: decode, + | ^~~~~~~~~~~~~~~~~~~~~ + 23 | write(_chunk, _encoding, done) { + + hint: compute the earlier option values into consts before the options object (the runtime value is read before the other options run) \ No newline at end of file diff --git a/tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json b/tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json new file mode 100644 index 000000000..1c67bb43f --- /dev/null +++ b/tests/harness/corpus-costs/7466-stream-construct-assign.mjs.json @@ -0,0 +1 @@ +{ "plain": 1571, "san": 2074 } From 14ba361985d6003636d6b654a8a58346b73c89e4 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:44:06 -0700 Subject: [PATCH 039/409] Call own accessors over an Error's name and message from typed code zod 4 defines its error message with Object.defineProperty(inst, "message", { get, set }). The typed-ref commit of an Error subclass read the accessor as a data value and threw "expected string". The commit and the slot sync now leave the slot alone and mark the property absent; typed reads and writes of name and message call the accessor when the program reflects on an untyped or Error-shaped target. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 3 + .../src/frontend/lowering/error-methods.ts | 52 +++++++- .../src/frontend/lowering/lower-calls.ts | 25 ++++ .../compiler/src/frontend/lowering/lowerer.ts | 5 + packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 8 ++ packages/compiler/src/ir/validate.ts | 3 + .../7611-error-message-accessor.ts.json | 6 + .../7612-error-message-accessor-lazy.js.json | 6 + packages/runtime/src/scr_error.h | 3 + packages/runtime/src/scr_json.c | 112 ++++++++++++++---- tests/corpus/7611-error-message-accessor.ts | 43 +++++++ .../7612-error-message-accessor-lazy.js | 36 ++++++ .../7611-error-message-accessor.ts.json | 1 + .../7612-error-message-accessor-lazy.js.json | 1 + 15 files changed, 283 insertions(+), 24 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json create mode 100644 tests/corpus/7611-error-message-accessor.ts create mode 100644 tests/corpus/7612-error-message-accessor-lazy.js create mode 100644 tests/harness/corpus-costs/7611-error-message-accessor.ts.json create mode 100644 tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 054d9341b..d88abbc68 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -134,6 +134,9 @@ export const LIB_FN_SYMS: Record = { "error.setCodeDyn": "scr_error_set_code_dyn", "error.assign": "scr_error_assign_dyn", "error.hasKey": "scr_error_has_key", + "error.ownAccessor": "scr_error_own_accessor", + "error.accessorGet": "scr_error_accessor_get", + "error.accessorSet": "scr_error_accessor_set", "error.hasCode": "scr_error_has_code", "error.defineCause": "scr_error_define_cause", "error.deleteCause": "scr_error_delete_cause", diff --git a/packages/compiler/src/frontend/lowering/error-methods.ts b/packages/compiler/src/frontend/lowering/error-methods.ts index b545f4147..067f9c286 100644 --- a/packages/compiler/src/frontend/lowering/error-methods.ts +++ b/packages/compiler/src/frontend/lowering/error-methods.ts @@ -94,12 +94,15 @@ export function refreshErrorPropertyDispatch( lowerer.isSubclassOf(info.def.name, "%Error"), ); const properties: ("name" | "message")[] = ["name", "message"]; + // Test lowerers built from partial objects leave the location undefined. + const accessors = Boolean(lowerer.errorAccessorReflection); for (const property of properties) { if ( - overrides.length && + (overrides.length || accessors) && !lowerer.liftedFns.some((fn) => fn.name === `%error.${property}.read`) ) { - errorPropertyRead(lowerer, varRef("error.0", errorType, overrides[0]!.def.loc), property); + const loc = overrides[0]?.def.loc ?? lowerer.errorAccessorReflection!; + errorPropertyRead(lowerer, varRef("error.0", errorType, loc), property); return true; } } @@ -115,6 +118,7 @@ export function refreshErrorPropertyDispatch( const revision = JSON.stringify([ readers.map((fn) => fn.name), overrides.map((info) => [info.def.name, hasClassPrototypeData(info)]), + accessors, ]); if (lowerer.errorPropertyDispatchRevision === revision) return false; lowerer.errorPropertyDispatchRevision = revision; @@ -183,6 +187,50 @@ export function refreshErrorPropertyDispatch( }, ]; const body: IrStmt[] = [{ kind: "if", cond: own, then: fallback, else_: null, loc }]; + // An own accessor a reflective definition installed (the slot reads as + // absent then): the read calls its getter, the write its setter. + if (accessors) + body.push({ + kind: "if", + cond: { kind: "libCall", fn: "error.ownAccessor", args: [receiver, key], type: BOOL, loc }, + then: write + ? [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "error.accessorSet", + args: [ + receiver, + key, + lowerer.coerceToExpected(varRef("value", STRING, loc), DYN), + ], + type: VOID, + loc, + }, + loc, + }, + { kind: "return", value: null, loc }, + ] + : [ + { + kind: "return", + value: lowerer.coerceToExpected( + { + kind: "libCall", + fn: "error.accessorGet", + args: [receiver, key], + type: DYN, + loc, + }, + STRING, + ), + loc, + }, + ], + else_: null, + loc, + }); helper.locals = helper.locals.filter((local) => !local.id.startsWith("descriptor.")); for (const info of overrides) { const member = `${write ? "set" : "get"}:${property}`; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index afae96b26..54bf9a30d 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -267,8 +267,33 @@ import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generati const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "close", "reset"]); +/** `Object.defineProperty(target, ...)`, `Object.defineProperties`, + * `Reflect.defineProperty`, and `target.__defineGetter__/__defineSetter__` + * over an untyped or Error-shaped target can install an accessor over an + * error's `name` or `message` (error-methods.ts). */ +function noteErrorAccessorReflection(lowerer: Lowerer, expr: ts.CallExpression): void { + const callee = expr.expression; + if (!ts.isPropertyAccessExpression(callee)) return; + const member = callee.name.text; + const target = + member === "defineProperty" || member === "defineProperties" + ? expr.arguments.nodes[0] + : member === "__defineGetter__" || member === "__defineSetter__" + ? callee.expression + : undefined; + if (!target) return; + const type = lowerer.checker.getTypeAtLocation(target); + if ( + type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown) || + (lowerer.checker.getPropertyOfType(type, "message") && + lowerer.checker.getPropertyOfType(type, "name")) + ) + lowerer.errorAccessorReflection = locOf(expr); +} + export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); + if (lowerer.errorAccessorReflection === null) noteErrorAccessorReflection(lowerer, expr); // Object's checker return type is `any`. Claim known object inputs // before generic checked-dynamic dispatch tries to load the constructor. if ( diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4c417e86e..f6b23f13c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3207,6 +3207,11 @@ export class Lowerer { * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; errorPropertyDispatchRevision: string | null = null; + /** The program defines properties by reflection on an untyped or + * Error-shaped target, so an own accessor may replace an error's `name` + * or `message`: typed reads and writes of those slots check for one. + * Holds the first such call's location. */ + errorAccessorReflection: SrcLoc | null = null; /** Structural source positions (structural-positions.ts) for names that * stay the same when unrelated code moves. */ /** Files are named by their path relative to the entry's directory, so diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index d8e78be08..69766374a 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -446,6 +446,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "global.undefRead", // The compiler-resolved Node-parity throw: always throws, catchably. "error.nodeThrow", + // An accessor over an error's own name or message runs user code. + "error.accessorGet", + "error.accessorSet", // USVString coercion runs user toString/valueOf — throws propagate. "dyn.toStringCoerce", "dyn.stringConstructor", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 7cb666ccf..0ab55e0c4 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3429,6 +3429,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * throw-site fields, dynamic writes) or an Object.prototype member. */ "error.hasKey": { argTypes: [null, STRING], result: BOOL }, "error.hasCode": { argTypes: [null], result: BOOL }, + /** Whether the error's own `name` or `message` (the key) is an accessor + * a reflective definition installed. Borrows both; never throws. */ + "error.ownAccessor": { argTypes: [null, STRING], result: BOOL }, + /** That accessor's getter called on the error (owned result), and its + * setter called with the value (V8's TypeError without one). Borrow + * their arguments; throw what the accessor throws. */ + "error.accessorGet": { argTypes: [null, STRING], result: DYN }, + "error.accessorSet": { argTypes: [null, STRING, DYN], result: VOID }, "error.defineCause": { argTypes: [null, DYN], result: VOID }, "error.deleteCause": { argTypes: [null], result: VOID }, /** The compiler-resolved Node-parity throw for always-throwing lowered diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 770fee6c6..d1ce8cdf0 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4144,6 +4144,9 @@ function validateFunction( e.fn === "error.assign" || e.fn === "error.hasKey" || e.fn === "error.hasCode" || + e.fn === "error.ownAccessor" || + e.fn === "error.accessorGet" || + e.fn === "error.accessorSet" || e.fn === "error.defineCause" || e.fn === "error.deleteCause" ) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json new file mode 100644 index 000000000..ef262841b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7611-error-message-accessor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json new file mode 100644 index 000000000..bc852b7bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7612-error-message-accessor-lazy.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index a7c921f23..fbb64aad9 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -296,6 +296,9 @@ void scr_error_assign_dyn(ScrError *e, const struct ScrDyn *sources); /* `key in err` for a key no compiled class declares: a runtime-set own * property or an Object.prototype member. Borrows both (scr_json.c). */ bool scr_error_has_key(ScrError *e, const ScrStr *key); +bool scr_error_own_accessor(ScrError *e, const ScrStr *key); +struct ScrDyn *scr_error_accessor_get(ScrError *e, const ScrStr *key); +void scr_error_accessor_set(ScrError *e, const ScrStr *key, struct ScrDyn *value); bool scr_error_has_code(ScrError *e); const char *scr_error_message_code(const ScrStr *msg); ScrStr *scr_error_code(ScrError *e); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 3fd57da73..9cf474d75 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -8010,35 +8010,36 @@ ScrDyn *scr_errdyn_view_of(const ScrError *e) { static void scr_error_cause_drop_impl(void *obj); void scr_error_commit_dyn(ScrError *e, const ScrDyn *view) { - ScrDynEntry *name_entry = NULL; - for (size_t i = 0; i < view->v.obj.len; i++) { - ScrDynEntry *entry = &view->v.obj.entries[i]; - if (entry->key_len == 4 && memcmp(entry->key, "name", 4) == 0) { name_entry = entry; break; } - } - ScrDyn *name = name_entry ? name_entry->value : NULL; + ScrDynEntry *name_entry = scr_dyn_find_entry(view, "name", 4); + ScrDynEntry *message_entry = scr_dyn_find_entry(view, "message", 7); + /* An own accessor over `name` or `message` (zod's lazy error message) + * has no value to snapshot: the slot keeps its last value, and the + * property reads as absent so typed reads take the accessor path + * (scr_error_own_accessor) and toString asks the installed reader. */ + ScrDyn *name = name_entry && !name_entry->accessor ? name_entry->value : NULL; if (name && name->kind != SCR_DYN_STR) { scr_dyn_check_fail(NULL, "string", name); return; } - ScrStr *next_name = name ? scr_str_retain(name->v.str) : scr_error_default_name(e); - scr_str_release(e->name); - e->name = next_name; - e->name_present = name != NULL; - e->name_enumerable = name_entry && name_entry->enumerable; - ScrDyn *message = scr_dyn_obj_get(view, "message", 7); + ScrDyn *message = message_entry && !message_entry->accessor ? message_entry->value : NULL; if (message && message->kind != SCR_DYN_STR) { scr_dyn_check_fail(NULL, "string", message); return; } - ScrStr *next_message = message ? scr_str_retain(message->v.str) : scr_str_new("", 0); - scr_str_release(e->message); - e->message = next_message; - e->message_present = message != NULL; - e->message_enumerable = false; - for (size_t i = 0; message && i < view->v.obj.len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; - if (entry->key_len == 7 && memcmp(entry->key, "message", 7) == 0) e->message_enumerable = entry->enumerable; + if (!name_entry || !name_entry->accessor) { + ScrStr *next_name = name ? scr_str_retain(name->v.str) : scr_error_default_name(e); + scr_str_release(e->name); + e->name = next_name; } + e->name_present = name != NULL; + e->name_enumerable = name_entry && name_entry->enumerable; + if (!message_entry || !message_entry->accessor) { + ScrStr *next_message = message ? scr_str_retain(message->v.str) : scr_str_new("", 0); + scr_str_release(e->message); + e->message = next_message; + } + e->message_present = message != NULL; + e->message_enumerable = message_entry && message_entry->enumerable; ScrDyn *stack = scr_dyn_obj_get(view, "stack", 5); if (stack && stack->kind == SCR_DYN_STR) { ScrStr *replacement = scr_str_retain(stack->v.str); @@ -8134,8 +8135,20 @@ static void scr_error_sync_slots(const ScrDyn *view, ScrDyn *obj, const ScrStr * return; } if (name || message) { - /* The native error's toString and inspect read these fields. */ - if (!entry || entry->accessor || entry->value->kind != SCR_DYN_STR) return; + /* The native error's toString and inspect read these fields. An own + * accessor leaves the slot, which then reads as absent: typed reads + * call it (scr_error_own_accessor). */ + if (entry && entry->accessor) { + if (name) { + e->name_present = false; + e->name_enumerable = entry->enumerable; + } else { + e->message_present = false; + e->message_enumerable = entry->enumerable; + } + return; + } + if (!entry || entry->value->kind != SCR_DYN_STR) return; ScrStr **field = name ? &e->name : &e->message; scr_str_release(*field); *field = scr_str_retain(entry->value->v.str); @@ -16044,6 +16057,61 @@ bool scr_error_has_code(ScrError *e) { * property set at runtime, which lives on the error's checked-dynamic node * (that node exists once anything reflected or wrote through it), or a * member of Object.prototype. Borrows both; never throws. */ +/* Whether the error's own `name` or `message` (the key) is an accessor a + * reflective definition installed on its checked-dynamic node (the slot + * then reads as absent): typed reads call its getter and typed writes its + * setter through that node. A class view keeps the accessor in its last + * materialized object. Borrows both; never throws. */ +static ScrDynEntry *scr_error_accessor_entry(ScrError *e, const ScrStr *key) { + const ScrDyn *view = scr_errdyn_view_of(e); + if (view != NULL && view->kind == SCR_DYN_TYPED_REF) view = view->v.typed_ref.materialized; + if (view == NULL || view->kind != SCR_DYN_OBJ) return NULL; + ScrDynEntry *entry = scr_dyn_find_entry(view, key->data, key->len); + return entry != NULL && entry->accessor ? entry : NULL; +} + +bool scr_error_own_accessor(ScrError *e, const ScrStr *key) { return scr_error_accessor_entry(e, key) != NULL; } + +/* A typed read of that accessor: its getter called on the error's node + * (the undefined value without one). Owned result, or NULL with the + * getter's exception pending. */ +ScrDyn *scr_error_accessor_get(ScrError *e, const ScrStr *key) { + ScrDynEntry *entry = scr_error_accessor_entry(e, key); + if (entry == NULL || entry->getter == NULL) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *getter = scr_dyn_retain(entry->getter); + ScrDyn *receiver = scr_dyn_from_error(e); + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter"); + scr_dyn_this_pop(); + scr_dyn_release(receiver); + scr_dyn_release(getter); + return result; +} + +/* A typed write of that accessor: its setter called on the error's node + * with the value, or V8's strict-mode TypeError without one. Borrows all; + * the setter's exception stays pending. */ +void scr_error_accessor_set(ScrError *e, const ScrStr *key, ScrDyn *value) { + ScrDynEntry *entry = scr_error_accessor_entry(e, key); + if (entry == NULL) return; + if (entry->setter == NULL) { + char message[256]; + int n = snprintf(message, sizeof message, "Cannot set property %.*s of %.*s which has only a getter", + (int)key->len, key->data, (int)e->name->len, e->name->data); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n < sizeof message ? (size_t)n : sizeof message - 1); + return; + } + ScrDyn *setter = scr_dyn_retain(entry->setter); + ScrDyn *receiver = scr_dyn_from_error(e); + ScrDyn *args[1] = {value}; + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(setter, args, 1, "setter"); + scr_dyn_this_pop(); + scr_dyn_release(result); + scr_dyn_release(receiver); + scr_dyn_release(setter); +} + bool scr_error_has_key(ScrError *e, const ScrStr *key) { const ScrDyn *view = scr_errdyn_view_of(e); if (view != NULL && view->kind == SCR_DYN_OBJ && scr_dyn_find_entry(view, key->data, key->len) != NULL) diff --git a/tests/corpus/7611-error-message-accessor.ts b/tests/corpus/7611-error-message-accessor.ts new file mode 100644 index 000000000..c4528e488 --- /dev/null +++ b/tests/corpus/7611-error-message-accessor.ts @@ -0,0 +1,43 @@ +// An accessor defined over an Error subclass instance's own `message` +// (zod 4's lazy error message): typed reads and writes, String(), and +// reads through an untyped alias all go through the accessor, and a later +// data definition replaces it. +class E extends Error {} +const e = new E("orig"); +let store = "lazy"; +let reads = 0; +Object.defineProperty(e, "message", { + get() { + reads++; + return store; + }, + set(value: string) { + store = `set:${value}`; + }, + enumerable: true, + configurable: true, +}); +console.log(e.message, String(e), reads); +e.message = "typed"; +console.log(e.message, store); +const loose: any = e; +console.log(loose.message); +loose.message = "loose"; +console.log(e.message, Object.keys(e).join()); +const desc = Object.getOwnPropertyDescriptor(e, "message")!; +console.log(typeof desc.get, typeof desc.set, desc.enumerable); +Object.defineProperty(e, "message", { value: "data", writable: true, configurable: true }); +console.log(e.message, String(e), reads); +e.message = "after"; +console.log(e.message, store); + +class Named extends Error {} +const n = new Named("m"); +const loosen: any = n; +Object.defineProperty(loosen, "name", { get() { return "Custom"; }, configurable: true }); +console.log(n.name, String(n)); + +class Plain extends Error {} +const p = new Plain("kept"); +p.message = "changed"; +console.log(p.message, String(p)); diff --git a/tests/corpus/7612-error-message-accessor-lazy.js b/tests/corpus/7612-error-message-accessor-lazy.js new file mode 100644 index 000000000..26d54c09c --- /dev/null +++ b/tests/corpus/7612-error-message-accessor-lazy.js @@ -0,0 +1,36 @@ +// zod 4's lazy error message: an initializer defines a shared accessor +// descriptor over `message` on an instance whose class extends a runtime +// Error parent, and the message is computed on first read and cached. +function _getMessage() { + const internals = this._zod; + internals.message ?? (internals.message = JSON.stringify(internals.def)); + return internals.message; +} +function _setMessage(value) { + this._zod.message = value; +} +const _messageDesc = { get: _getMessage, set: _setMessage, enumerable: true, configurable: true }; +function initializer(inst, def) { + Object.defineProperty(inst, "_zod", { value: { def }, enumerable: false }); + inst.name = "$ZodError"; + Object.defineProperty(inst, "message", _messageDesc); +} +function make(Parent, def) { + class Definition extends Parent {} + const inst = new Definition(); + initializer(inst, def); + return inst; +} +const err = make(Error, [{ code: "too_small", path: ["a"] }]); +console.log(err._zod.message, err.message, err._zod.message === err.message); +console.log(String(err), Object.keys(err).join()); +err.message = "replaced"; +console.log(err.message, err._zod.message, `${err}`); +try { + throw err; +} catch (caught) { + console.log(caught === err, caught.message, caught instanceof Error); +} +const plain = new Error("start"); +Object.defineProperty(plain, "message", { get() { return "computed"; }, configurable: true }); +console.log(plain.message, String(plain)); diff --git a/tests/harness/corpus-costs/7611-error-message-accessor.ts.json b/tests/harness/corpus-costs/7611-error-message-accessor.ts.json new file mode 100644 index 000000000..8c97e80f9 --- /dev/null +++ b/tests/harness/corpus-costs/7611-error-message-accessor.ts.json @@ -0,0 +1 @@ +{ "plain": 2962, "san": 3600 } diff --git a/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json b/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json new file mode 100644 index 000000000..bbc81f519 --- /dev/null +++ b/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json @@ -0,0 +1 @@ +{ "plain": 2217, "san": 2721 } From 1d68edc0f6063ee0b2e288dbf6e02a66f7536d41 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 01:39:26 -0500 Subject: [PATCH 040/409] Give function-local class prototypes their own constructor The prototype object of a function-local class, created once per evaluation of the class, now starts with its own non-enumerable `constructor`, the class value of that evaluation, as every class prototype does. Reflecting the prototype of a function-local class without prototype members exposed an object without it: `proto.constructor` answered the base class, and `Object.hasOwn(proto, "constructor")` and its property descriptor did not match Node. --- packages/runtime/src/scr_json.c | 5 +++++ .../7613-local-class-prototype-reflection.js | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 3fd57da73..22066e186 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4415,6 +4415,11 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) { * the class object and no registry keeps it alive, so evaluating a * function-local class again releases the previous one's prototype. */ if (cls->name && cls->name->len) data->v.obj.class_name = cls->name->data; + /* Like every class prototype, it starts with its own non-enumerable + * `constructor`: the class value. The cycle it closes through the class + * object is one the collector traces. */ + scr_dyn_obj_set(data, "constructor", 11, scr_dyn_retain(value)); + data->v.obj.entries[data->v.obj.len - 1].enumerable = false; cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); scr_box_set_ref(cls->prototype_data, data); } diff --git a/tests/corpus/7613-local-class-prototype-reflection.js b/tests/corpus/7613-local-class-prototype-reflection.js index 61a116569..a48bd305c 100644 --- a/tests/corpus/7613-local-class-prototype-reflection.js +++ b/tests/corpus/7613-local-class-prototype-reflection.js @@ -34,3 +34,22 @@ console.log(a.describe(), b.describe(), c.describe()); console.log(a instanceof Error, c instanceof Error, Object.keys(a).join(), Object.keys(c).join()); console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b), Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); console.log(typeof Object.getPrototypeOf(c).describe, "describe" in a, Object.hasOwn(a, "describe")); + +// The prototype has its own non-enumerable `constructor`, the class value +// of that evaluation, as every class prototype does. +function local(Parent) { + class Definition extends Parent {} + const proto = Object.getPrototypeOf(new Definition()); + const descriptor = Object.getOwnPropertyDescriptor(proto, "constructor"); + console.log( + proto.constructor === Definition, + Object.hasOwn(proto, "constructor"), + JSON.stringify(Object.getOwnPropertyNames(proto)), + descriptor.value === Definition, + descriptor.writable, + descriptor.enumerable, + descriptor.configurable, + ); + return Definition; +} +console.log(local(Object) !== local(Error)); From 14a2769f58d0ccafbb89effec4f4d17234bf11ae Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 21:18:02 -0700 Subject: [PATCH 041/409] Create untyped class instances' property-bag entries without the class tables Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../lowering/class-dynamic-dispatch.ts | 315 ++++++++++++++---- .../compiler/src/frontend/lowering/lowerer.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 4 + packages/runtime/src/scr_json.c | 15 + 5 files changed, 269 insertions(+), 69 deletions(-) diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 4a9578e5e..88425ca66 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1553,6 +1553,7 @@ export const LIB_FN_SYMS: Record = { "dyn.assignPrototype": "scr_dyn_assign_prototype", "dyn.definePrototypeProps": "scr_dyn_define_prototype_props", "dyn.bagGet": "scr_dyn_bag_get", + "dyn.bagCreateField": "scr_dyn_bag_create_field", "dyn.bagSet": "scr_dyn_bag_set", "dyn.objCreate": "scr_dyn_obj_create", "dyn.nameClassPrototype": "scr_dyn_name_class_prototype", diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index e394718d1..b5b585989 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -685,16 +685,154 @@ export function trackClassFieldCreation(lowerer: Lowerer, functions: IrFunction[ if (![...lowerer.classes.values()].some((info) => info.def.tracksOwnFields)) return; const loc = functions[0]!.loc; const name = "%dyn.class.fieldCreated"; - const value = varRef("value", DYN, loc), - key = varRef("key", DYN, loc); - const bag = varRef("bag", DYN, loc); - const created = (receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrStmt => ({ + const key = varRef("key", DYN, loc); + // One helper per class: the instance's bag is its own hidden field, so a + // creation reads it directly instead of boxing the instance and testing + // it against every class's capsule (%dyn.class.properties), which runs + // only to create the bag, once per instance. + const perClass = new Map(); + const helperFor = (className: string): string => { + const known = perClass.get(className); + if (known !== undefined) return known; + const helperName = `${name}:${className}`; + perClass.set(className, helperName); + const self = varRef("this", { kind: "object", className }, loc); + const bagRef = varRef("bag", DYN, loc); + functions.push({ + name: helperName, + params: [ + { localId: "this", name: "this", type: { kind: "object", className } }, + { localId: "key", name: "key", type: DYN }, + ], + returnType: VOID, + locals: [ + { id: "this", name: "this", type: { kind: "object", className }, mutable: false }, + { id: "key", name: "key", type: DYN, mutable: false }, + { id: "bag", name: "bag", type: DYN, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "bag", + init: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: bagRef, type: BOOL, loc }, + then: initializeBag(lowerer, className, self, loc), + else_: null, + loc, + }, + ...defineIfAbsent(lowerer, bagRef, key, loc), + ], + loc, + }); + return helperName; + }; + // String-keyed fields take the runtime's direct insert for an ordinary + // bag; the general path stays for any other bag and for symbol keys. + const stringHelperFor = (className: string): string => { + const known = perClass.get(`str:${className}`); + if (known !== undefined) return known; + const general = helperFor(className); + const helperName = `${name}.string:${className}`; + perClass.set(`str:${className}`, helperName); + const type: IrType = { kind: "object", className }; + const self = varRef("this", type, loc); + const bagRef = varRef("bag", DYN, loc); + const field = varRef("field", STRING, loc); + functions.push({ + name: helperName, + params: [ + { localId: "this", name: "this", type }, + { localId: "field", name: "field", type: STRING }, + ], + returnType: VOID, + locals: [ + { id: "this", name: "this", type, mutable: false }, + { id: "field", name: "field", type: STRING, mutable: false }, + { id: "bag", name: "bag", type: DYN, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "bag", + init: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: bagRef, type: BOOL, loc }, + then: initializeBag(lowerer, className, self, loc), + else_: null, + loc, + }, + { + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.bagCreateField", + args: [bagRef, field], + type: BOOL, + loc, + }, + then: [{ kind: "return", value: null, loc }], + else_: null, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "call", + callee: general, + args: [self, lowerer.coerceToExpected(field, DYN)], + type: VOID, + loc, + }, + loc, + }, + ], + loc, + }); + return helperName; + }; + const created = (receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrStmt => { + const symbolKey = symbolMemberKey(lowerer, lowerer.classes.get(className)!, field, loc); + if (symbolKey === null) { + return { + kind: "exprStmt", + expr: { + kind: "call", + callee: stringHelperFor(className), + args: [ + receiver.type.kind === "object" && receiver.type.className !== className + ? { kind: "upcast", value: receiver, type: { kind: "object", className }, loc } + : receiver, + { kind: "strLit", value: field, type: STRING, loc }, + ], + type: VOID, + loc, + }, + loc, + }; + } + return createdGeneral(receiver, className, field, loc); + }; + const createdGeneral = ( + receiver: IrExpr, + className: string, + field: string, + loc: SrcLoc, + ): IrStmt => ({ kind: "exprStmt", expr: { kind: "call", - callee: name, + callee: helperFor(className), args: [ - lowerer.coerceToExpected(receiver, DYN), + receiver.type.kind === "object" && receiver.type.className !== className + ? { kind: "upcast", value: receiver, type: { kind: "object", className }, loc } + : receiver, symbolMemberKey(lowerer, lowerer.classes.get(className)!, field, loc) ?? lowerer.coerceToExpected({ kind: "strLit", value: field, type: STRING, loc }, DYN), ], @@ -769,80 +907,103 @@ export function trackClassFieldCreation(lowerer: Lowerer, functions: IrFunction[ }, }); } - functions.push({ - name, - params: [ - { localId: "value", name: "value", type: DYN }, - { localId: "key", name: "key", type: DYN }, - ], - returnType: VOID, - locals: [ - { id: "value", name: "value", type: DYN, mutable: false }, - { id: "key", name: "key", type: DYN, mutable: false }, - { id: "bag", name: "bag", type: DYN, mutable: false }, - ], - body: [ +} + +/** `bag = `, stored in the instance's hidden field: + * the class's own initializer when recorded, else the class-table + * dispatch, which creates it. */ +function initializeBag(lowerer: Lowerer, className: string, self: IrExpr, loc: SrcLoc): IrStmt[] { + const init = lowerer.classBagInitializers?.get(className); + if (init === undefined) { + return [ { - kind: "varDecl", + kind: "assign", localId: "bag", - init: { + value: { kind: "call", callee: classPropertiesHelper(lowerer, loc).name, - args: [value], + args: [lowerer.coerceToExpected(self, DYN)], type: DYN, loc, }, loc, }, - { - kind: "if", - cond: { - kind: "unary", - op: "!", - operand: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc }, - type: BOOL, - loc, - }, - then: [ - { - kind: "exprStmt", - expr: { - kind: "libCall", - fn: "dyn.defineProperty", - args: [ - bag, - lowerer.coerceToExpected(key, DYN), - { - kind: "dynObjLit", - fields: [ - { - key: { kind: "strLit", value: "value", type: STRING, loc }, - value: dynUndefinedExpr(loc), - }, - ...["writable", "enumerable", "configurable"].map((key) => ({ - key: { kind: "strLit" as const, value: key, type: STRING, loc }, - value: lowerer.coerceToExpected( - { kind: "boolLit", value: true, type: BOOL, loc }, - DYN, - ), - })), - ], - type: DYN, - loc, - }, - ], - type: DYN, - loc, - }, - loc, - }, - ], - else_: null, + ]; + } + return [ + { + kind: "fieldSet", + obj: self, + className, + field: PROPERTY_BAG, + value: structuredClone(init), + loc, + }, + { + kind: "assign", + localId: "bag", + value: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc }, + loc, + }, + ]; +} + +/** Whether `value` reads local `id`. */ +function mentionsLocal(value: IrExpr, id: string): boolean { + return JSON.stringify(value).includes(`"localId":${JSON.stringify(id)}`); +} + +/** `if (!hasOwn(bag, key)) defineProperty(bag, key, )`: + * a field's creation in its instance's property bag. */ +function defineIfAbsent(lowerer: Lowerer, bag: IrExpr, key: IrExpr, loc: SrcLoc): IrStmt[] { + return [ + { + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc }, + type: BOOL, loc, }, - ], - loc, - }); + then: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.defineProperty", + args: [ + bag, + lowerer.coerceToExpected(key, DYN), + { + kind: "dynObjLit", + fields: [ + { + key: { kind: "strLit", value: "value", type: STRING, loc }, + value: dynUndefinedExpr(loc), + }, + ...["writable", "enumerable", "configurable"].map((key) => ({ + key: { kind: "strLit" as const, value: key, type: STRING, loc }, + value: lowerer.coerceToExpected( + { kind: "boolLit", value: true, type: BOOL, loc }, + DYN, + ), + })), + ], + type: DYN, + loc, + }, + ], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + ]; } /** Calls on native class capsules keep the instance's compiled methods. The @@ -4516,6 +4677,7 @@ export class ClassDynamicDispatch { finalize(lowerer?: Lowerer): void { // Each table serves every site that reaches it. if (lowerer) for (const fn of this.generated) lowerer.shareHelper(fn); + if (lowerer) this.recordBagInitializers(lowerer); for (const { helper, initialize, orEmpty } of this.readPropertyBags) { const absent = helper.body[0]; if (isEmptyBag(initialize.value) && !orEmpty) helper.body.shift(); @@ -4538,6 +4700,21 @@ export class ClassDynamicDispatch { } } + /** Each tracked class's bag initializer over a typed receiver (`this`), + * for the per-class field-creation helpers (trackClassFieldCreation), + * which create an instance's bag without the class-table dispatch. */ + private recordBagInitializers(lowerer: Lowerer): void { + for (const [className, initializers] of this.bagInitializers) { + const init = initializers[0]; + if (!init || !lowerer.classes.get(className)?.def.tracksOwnFields) continue; + const self = varRef("this", { kind: "object", className }, init.loc); + const value = withTypedReceiver(structuredClone(init.value), self); + if (mentionsLocal(value, "p.0")) continue; + // Lowerer stand-ins in unit tests may not carry the table. + lowerer.classBagInitializers?.set(className, value); + } + } + private normalizedDispatches(): IrFunction[] { return [ ...(this.propertyBag ? [this.propertyBag] : []), diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a765c91ee..66d3fd294 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3646,6 +3646,9 @@ export class Lowerer { .some((sf) => isDeclBoundaryFile(sf.fileName)); return this.declBoundaryMemo; } + /** Tracked classes' bag initializers over a typed `this` + * (class-dynamic-dispatch.ts recordBagInitializers). */ + readonly classBagInitializers = new Map(); constructor( readonly program: ts.Program, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e60a6600b..1140ced8c 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3745,6 +3745,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN }, "dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN }, "dyn.bagGet": { argTypes: [DYN, STRING, DYN], result: DYN }, + /** A class field's creation in its instance's property bag: adds the + * string key as a default data property when absent, answering false + * (nothing changed) when the bag needs the general defineProperty path. */ + "dyn.bagCreateField": { argTypes: [DYN, STRING], result: BOOL }, "dyn.bagSet": { argTypes: [DYN, STRING, DYN, DYN], result: VOID }, "dyn.objCreate": { argTypes: [DYN], result: DYN }, // Names a compiled class's prototype object (args[0]) after the class diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 907486fea..d26fb9d5a 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -16110,3 +16110,18 @@ void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule) { } scr_dyn_release(old); } + +/* A compiled class's field creation in its instance's property bag (the + * compiler's trackClassFieldCreation): an ordinary extensible bag gains + * `key` as a writable, enumerable, configurable data property when it + * does not have it yet. Answers false, changing nothing, for any other bag, + * whose creation takes the general defineProperty path. Calls only + * exported functions. */ +bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key); +bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key) { + if (!bag || bag->kind != SCR_DYN_OBJ || bag->non_extensible || bag->v.obj.source_access) + return false; + if (scr_dyn_obj_get(bag, key->data, key->len)) return true; + scr_dyn_obj_set(bag, key->data, key->len, scr_dyn_retain(scr_dyn_undefined())); + return true; +} From 756a93a8f20a6da11b1d1af8f083e7d8e9ae57c2 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 21:18:03 -0700 Subject: [PATCH 042/409] Exit the untyped class tables early for values that are not class capsules Co-Authored-By: Claude Opus 5.5 --- .../backend/llvm/checked-value-lifetimes.ts | 2 + .../compiler/src/backend/llvm/expr-dynamic.ts | 5 + .../lowering/class-dynamic-dispatch.ts | 112 +++++++++++++++++- packages/compiler/src/ir/ir.ts | 6 +- packages/runtime/src/scr_json.c | 9 ++ 5 files changed, 130 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts index 5f8119079..4169e86d8 100644 --- a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts @@ -5,6 +5,8 @@ export function preservesDynTest(test: string): boolean { switch (test) { case "truthy": + case "classCapsule": + case "funcKind": case "nullish": case "promise": case "buffer": diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 434435eae..07a538184 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -719,6 +719,10 @@ export function emitDynamicExpr( B.line( `${test} = call zeroext i1 @scr_dyn_typed_array_is(ptr ${d.name}, i32 ${BYTES_ELEM_NUM[e.bytesElem ?? "u8"]})`, ); + } else if (e.test === "classCapsule") { + host.declare(`declare zeroext i1 @scr_dyn_is_class_capsule(ptr)`); + test = B.tmp(); + B.line(`${test} = call zeroext i1 @scr_dyn_is_class_capsule(ptr ${d.name})`); } else if (e.test === "truthy") { host.declare(`declare zeroext i1 @scr_dyn_truthy(ptr)`); test = B.tmp(); @@ -839,6 +843,7 @@ export function emitDynamicExpr( undefined: DYN_KIND.UNDEF, null: DYN_KIND.NULL, bytes: DYN_KIND.BYTES, + funcKind: DYN_KIND.FUNC, }; test = oneOf([kindOf[e.test]!]); } diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index b5b585989..eb0529e76 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -394,6 +394,38 @@ export function classInstanceOf( }; } +/** `value` is the parameter itself, possibly through js.fromDyn. */ +function returnsParam(value: IrExpr, param: string): boolean { + if (value.kind === "varRef") return value.localId === param; + return ( + value.kind === "libCall" && + value.fn === "js.fromDyn" && + value.args.length === 1 && + value.args[0]!.kind === "varRef" && + value.args[0]!.localId === param + ); +} + +/** The kind a class-table branch requires of `param`: a capsule-key test + * (alone or guarding a conjunction) needs a capsule, a class-key test a + * function value; null for any other condition. */ +function branchKind(cond: IrExpr, param: string): "classCapsule" | "funcKind" | null { + if ( + cond.kind === "libCall" && + (cond.fn === "dyn.typedRefIs" || cond.fn === "dyn.typedRefKeyIs" || cond.fn === "dyn.classIs") && + cond.args[0]?.kind === "varRef" && + cond.args[0].localId === param + ) { + if (cond.fn === "dyn.classIs") return "funcKind"; + const key = cond.args[1]; + return key?.kind === "strLit" && key.value.startsWith("object:") ? "classCapsule" : null; + } + if (cond.kind === "ternary" && cond.else_.kind === "boolLit" && cond.else_.value === false) + return branchKind(cond.cond, param); + if (cond.kind === "logical" && cond.op === "&&") return branchKind(cond.left, param); + return null; +} + const READ_PROPERTIES = "%dyn.class.readProperties"; const READ_PROPERTIES_OR_EMPTY = "%dyn.class.readPropertiesOrEmpty"; @@ -4698,6 +4730,7 @@ export class ClassDynamicDispatch { fn.name === READ_PROPERTIES_OR_EMPTY, ); } + if (lowerer) this.guardClassTables(lowerer); } /** Each tracked class's bag initializer over a typed receiver (`this`), @@ -4715,6 +4748,63 @@ export class ClassDynamicDispatch { } } + /** Fast exits for the generated class tables: every class branch of + * the property-bag, normalization and static-bag helpers tests the + * value's capsule key (or class key), so a value of another kind (a + * plain object, the common case in untyped code) skips straight to the + * table's fallthrough instead of testing every class in the program. */ + private guardClassTables(lowerer: Lowerer): void { + const names = new Set([ + "%dyn.class.properties", + READ_PROPERTIES, + READ_PROPERTIES_OR_EMPTY, + "%dyn.class.normalize", + "%dyn.class.staticProperties", + ]); + for (const fn of lowerer.liftedFns) { + if (!names.has(fn.name) || fn.body.length < 2) continue; + const param = fn.params[0]?.localId; + if (param === undefined) continue; + const last = fn.body[fn.body.length - 1]!; + if (last.kind !== "return" || last.value === null || !returnsParam(last.value, param)) continue; + let test: "classCapsule" | "funcKind" | null = null; + let ok = true; + for (const stmt of fn.body.slice(0, -1)) { + if ( + stmt.kind === "assign" && + stmt.localId === param && + stmt.value.kind === "call" && + stmt.value.callee === "%dyn.class.normalize" + ) + continue; + const kind = stmt.kind === "if" && stmt.else_ === null ? branchKind(stmt.cond, param) : null; + if (kind === null || (test !== null && kind !== test)) { + ok = false; + break; + } + test = kind; + } + if (!ok || test === null) continue; + const first = fn.body[0]!; + if (first.kind === "if" && first.cond.kind === "dynTest" && first.cond.test === test) continue; + const loc = fn.loc; + fn.body.unshift({ + kind: "if", + cond: { + kind: "dynTest", + test, + negated: true, + value: varRef(param, DYN, loc), + type: BOOL, + loc, + }, + then: [structuredClone(last)], + else_: null, + loc, + }); + } + } + private normalizedDispatches(): IrFunction[] { return [ ...(this.propertyBag ? [this.propertyBag] : []), @@ -4786,10 +4876,17 @@ export class ClassDynamicDispatch { this.normalizationKey = key; const loc = this.normalization.loc; const value = varRef("value", DYN, loc); - this.normalization.body = entries.map(({ capsule, children }): IrStmt => { + // The checked receiver is unboxed once per capsule branch, not once + // per subclass test. + this.normalization.locals = this.normalization.locals.filter( + (local) => !local.id.startsWith("%normalized."), + ); + this.normalization.body = entries.map(({ capsule, children }, index): IrStmt => { const type: IrType = { kind: "object", className: capsule.def.name }; - const receiver: IrExpr = { kind: "dynCheck", value, type, loc }; - return { + const id = `%normalized.${index}`; + this.normalization!.locals.push({ id, name: id, type, mutable: false }); + const receiver: IrExpr = varRef(id, type, loc); + const branch: IrStmt = { kind: "if", cond: { kind: "libCall", @@ -4830,6 +4927,15 @@ export class ClassDynamicDispatch { else_: null, loc, }; + // The capsule test stays the branch condition; its body unboxes. + if (branch.kind === "if") + branch.then.unshift({ + kind: "varDecl", + localId: id, + init: { kind: "dynCheck", value, type, loc }, + loc, + }); + return branch; }); this.normalization.body.push({ kind: "return", value, loc }); } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 63286ff54..6d0760b81 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -3445,7 +3445,11 @@ export type IrExpr = | "array" | "truthy" | "error" - | "function"; + | "function" + /** The value is a capsule of a compiled class instance. */ + | "classCapsule" + /** The value is a boxed function or class value (exact kind test). */ + | "funcKind"; bytesElem?: IrBytesElem; negated?: true; value: IrExpr; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index d26fb9d5a..3e197d9e8 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -16125,3 +16125,12 @@ bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key) { scr_dyn_obj_set(bag, key->data, key->len, scr_dyn_retain(scr_dyn_undefined())); return true; } + +/* Whether a value is a capsule of a compiled class instance (its type key + * names an object type): the class tables' fast exit for every other value, + * including record and array capsules. Calls only libc. */ +bool scr_dyn_is_class_capsule(const ScrDyn *d); +bool scr_dyn_is_class_capsule(const ScrDyn *d) { + return d && d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.type_key_len > 7 && + memcmp(d->v.typed_ref.type_key, "object:", 7) == 0; +} From 604f647e30fc1a5dde3fa7c0fe124406a3e47eb0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 23:10:08 -0700 Subject: [PATCH 043/409] Update the normalization unit test for the hoisted receiver Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-dynamic-dispatch.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index b717452c2..1a21a6aa5 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -458,9 +458,11 @@ test("normalizes base capsules before dispatching each derived member once", () }); expect(dispatch.body.filter((statement) => statement.kind === "if")).toHaveLength(2); const normalize = liftedFns.find((helper) => helper.name === "%dyn.class.normalize")!; + // The capsule branch unboxes its receiver once, then tests each subclass. expect(normalize.body[0]).toMatchObject({ kind: "if", then: [ + { kind: "varDecl", init: { kind: "dynCheck", type: { kind: "object", className: "Base" } } }, { kind: "if", then: [ From da546c05b311fd5b81e5d5d79c52035e46951a1c Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 23:51:04 -0700 Subject: [PATCH 044/409] Format the class-table exits Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-dynamic-dispatch.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index eb0529e76..5fe14a8d8 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -412,7 +412,9 @@ function returnsParam(value: IrExpr, param: string): boolean { function branchKind(cond: IrExpr, param: string): "classCapsule" | "funcKind" | null { if ( cond.kind === "libCall" && - (cond.fn === "dyn.typedRefIs" || cond.fn === "dyn.typedRefKeyIs" || cond.fn === "dyn.classIs") && + (cond.fn === "dyn.typedRefIs" || + cond.fn === "dyn.typedRefKeyIs" || + cond.fn === "dyn.classIs") && cond.args[0]?.kind === "varRef" && cond.args[0].localId === param ) { @@ -4766,7 +4768,8 @@ export class ClassDynamicDispatch { const param = fn.params[0]?.localId; if (param === undefined) continue; const last = fn.body[fn.body.length - 1]!; - if (last.kind !== "return" || last.value === null || !returnsParam(last.value, param)) continue; + if (last.kind !== "return" || last.value === null || !returnsParam(last.value, param)) + continue; let test: "classCapsule" | "funcKind" | null = null; let ok = true; for (const stmt of fn.body.slice(0, -1)) { @@ -4777,7 +4780,8 @@ export class ClassDynamicDispatch { stmt.value.callee === "%dyn.class.normalize" ) continue; - const kind = stmt.kind === "if" && stmt.else_ === null ? branchKind(stmt.cond, param) : null; + const kind = + stmt.kind === "if" && stmt.else_ === null ? branchKind(stmt.cond, param) : null; if (kind === null || (test !== null && kind !== test)) { ok = false; break; @@ -4786,7 +4790,8 @@ export class ClassDynamicDispatch { } if (!ok || test === null) continue; const first = fn.body[0]!; - if (first.kind === "if" && first.cond.kind === "dynTest" && first.cond.test === test) continue; + if (first.kind === "if" && first.cond.kind === "dynTest" && first.cond.test === test) + continue; const loc = fn.loc; fn.body.unshift({ kind: "if", From 2f88f352816c22ac570af7720509cf2240efe1ff Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 01:56:34 -0500 Subject: [PATCH 045/409] Freeze records that other references still hold Object.freeze of a record that another reference may still hold stopped at compile time with SC2020 ("possibly-aliased value"), and freezing a record held in an untyped value threw SC2020 at run time. Node freezes the object itself: every later write through any reference throws a TypeError in strict mode code and leaves the object unchanged in sloppy-mode code. - A record a program may freeze keeps a frozen bit in its reference count word. A shape is freezable when a typed Object.freeze names it, or, in a program that freezes untyped values, when a value of it can be held as a live untyped reference. Only those shapes change: their release ignores the bit, and every write to them, keyed writes and deletes included, checks it first. - Object.freeze of a record held in an untyped value sets the bit and freezes the record's checked view, so Object.isFrozen, Object.isSealed and Object.isExtensible through that value answer for the frozen record. A typed Object.isFrozen of a record reads the bit. - Programs that never freeze a record compile to the same IR and LLVM. Corpus 7404 covers typed and untyped freezes, a recursive freeze through unknown, keyed writes and deletes, optional-field deletes, Object.assign, tuples, and frozen records that are released or collected as cycles, in strict mode code. 7405 covers the same writes in sloppy-mode code. Both match Node. Refs #1374 Co-authored-by: blurname <50226652+blurname@users.noreply.github.com> --- .../compiler/src/backend/llvm/lib-dispatch.ts | 13 + .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../compiler/src/backend/llvm/shapes.test.ts | 37 ++ packages/compiler/src/backend/llvm/shapes.ts | 35 +- .../src/backend/llvm/stack-objects.ts | 7 +- .../lowering/expressions/element-access.ts | 2 +- .../src/frontend/lowering/frozen-records.ts | 328 ++++++++++++++++++ .../src/frontend/lowering/lower-calls.ts | 24 +- .../src/frontend/lowering/lower-stmts.ts | 28 +- .../compiler/src/frontend/lowering/lowerer.ts | 10 +- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 12 + packages/compiler/src/ir/ir.ts | 4 + .../tests/corpus/7404-frozen-records.ts.json | 6 + .../7405-frozen-records-sloppy.cjs.json | 6 + packages/runtime/src/scr_cycle.c | 22 +- packages/runtime/src/scr_dyn.h | 7 + packages/runtime/src/scr_json.c | 71 +++- packages/runtime/src/scr_json.test.c | 13 + packages/runtime/src/scr_rc.h | 26 +- tests/corpus/7404-frozen-records.ts | 124 +++++++ tests/corpus/7405-frozen-records-sloppy.cjs | 34 ++ .../corpus-costs/7404-frozen-records.ts.json | 1 + .../7405-frozen-records-sloppy.cjs.json | 1 + 24 files changed, 776 insertions(+), 38 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/frozen-records.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7404-frozen-records.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7405-frozen-records-sloppy.cjs.json create mode 100644 tests/corpus/7404-frozen-records.ts create mode 100644 tests/corpus/7405-frozen-records-sloppy.cjs create mode 100644 tests/harness/corpus-costs/7404-frozen-records.ts.json create mode 100644 tests/harness/corpus-costs/7405-frozen-records-sloppy.cjs.json diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index cebe661d4..4f1a4fef8 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -770,6 +770,19 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { return { name: t, type: e.type }; } if (e.fn === "bytes.absent") return host.own({ name: "null", type: e.type }); + if (e.fn === "record.isFrozen") { + // The frozen bit of the record's rc word (scr_rc.h SCR_RC_FROZEN) is + // its second-highest bit; an immortal word sets the highest one too. + const value = host.emitReadReceiver(e.args[0]!); + const S = host.sizeType; + const rc = host.B.tmp(), + top = host.B.tmp(), + t = host.B.tmp(); + host.B.line(`${rc} = load ${S}, ptr ${value.name}`); + host.B.line(`${top} = lshr ${S} ${rc}, ${S === "i64" ? 62 : 30}`); + host.B.line(`${t} = icmp eq ${S} ${top}, 1`); + return { name: t, type: e.type }; + } const prefix = e.fn.slice(0, e.fn.indexOf(".")) as LibCallPrefix; // events.once builds typed listener adapters beside the emitter calls. if (e.fn === "events.once" || e.fn === "events.onceSignal") diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 4a9578e5e..6fbad7e11 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1014,6 +1014,8 @@ export const LIB_FN_SYMS: Record = { "class.descriptorHasValue": "scr_class_descriptor_has_value", "class.throwReadOnly": "scr_class_throw_read_only", "class.throwNotExtensible": "scr_class_throw_not_extensible", + "record.freeze": "scr_record_freeze", + "record.throwFrozen": "scr_record_throw_frozen", "dyn.nativeSetNew": "scr_dyn_native_set_new", "dyn.nativeSetIs": "scr_dyn_native_set_is", "dyn.nativeMapIs": "scr_dyn_native_map_is", diff --git a/packages/compiler/src/backend/llvm/shapes.test.ts b/packages/compiler/src/backend/llvm/shapes.test.ts index f7327a688..7af16ab83 100644 --- a/packages/compiler/src/backend/llvm/shapes.test.ts +++ b/packages/compiler/src/backend/llvm/shapes.test.ts @@ -14,6 +14,7 @@ import { type ShapeHost, vAdapters, } from "./shapes.js"; +import { mangleRecordRelease } from "../mangle.js"; import { NullableUnions } from "./nullable-unions.js"; import { InlineUnionFields } from "./inline-unions.js"; @@ -315,6 +316,42 @@ describe("emitted object releases", () => { root.indexOf("call void @scr_cyc_on_release(ptr %o)"), ); }); + + test("freezable records count their references without the frozen bit", () => { + const mod: IrModule = { + irVersion: 17, + sourceFile: "frozen.ts", + entry: "main", + functions: [], + records: [ + { id: "plain", fields: [{ name: "n", type: F64 }] }, + { id: "frozen", fields: [{ name: "n", type: F64 }], freezable: true }, + ], + }; + const body = (defs: string, name: string): string => + new RegExp(`^define internal void @${name}\\([^]*?^}`, "m").exec(defs)?.[0] ?? ""; + for (const [pointerBits, notFrozen, sharedBelow] of [ + [64, "-4611686018427387905", "-4611686018427387907"], + [32, "-1073741825", "-1073741827"], + ] as const) { + const { host } = declarationHost(pointerBits); + const defs = emitRecordShapes({ ...host, objectAudit: false }, mod).defs.join("\n"); + const S = pointerBits === 64 ? "i64" : "i32"; + const plain = body(defs, mangleRecordRelease("plain")); + expect(plain).toContain(`%shared = icmp ult ${S} %rcm2, -3`); + expect(plain).not.toContain("%refs"); + // The immortal word's count is ~FROZEN, which the compare sends to the + // immortal test, like counts 0 and 1. + const fast = body(defs, mangleRecordRelease("frozen")); + expect(fast).toContain(`%refs = and ${S} %rc, ${notFrozen}`); + expect(fast).toContain(`%rcm2 = sub ${S} %refs, 2`); + expect(fast).toContain(`%shared = icmp ult ${S} %rcm2, ${sharedBelow}`); + expect(fast).toContain(`%imm = icmp eq ${S} %rc, -1`); + const slow = body(defs, `${mangleRecordRelease("frozen")}_slow`); + expect(slow).toContain(`%refs = and ${S} %n, ${notFrozen}`); + expect(slow).toContain(`%dead = icmp eq ${S} %refs, 0`); + } + }); }); describe("live-object audit notes", () => { diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index d13f80a78..0997d0ace 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -1527,6 +1527,17 @@ export function destroyDefinitions( ]; } +/** The frozen bit of a freezable record's rc word (scr_rc.h SCR_RC_FROZEN): + * bit 62 on 64-bit targets, bit 30 on 32-bit ones. */ +function recordFrozenBit(sizeType: RcCount): bigint { + return 1n << (sizeType === "i64" ? 62n : 30n); +} + +/** `value` as a signed LLVM integer literal of `sizeType`. */ +function sizeLiteral(sizeType: RcCount, value: bigint): string { + return BigInt.asIntN(sizeType === "i64" ? 64 : 32, value).toString(); +} + /** Common NULL/immortal/decrement skeleton for ordinary object releases. * `freeBody` owns the zero-ref teardown and must leave the current block at * its end; traced objects also get the possible-cycle-root branch. @@ -1543,13 +1554,16 @@ export function releaseBody( bounded = true, immortal = false, chained = false, + frozen = false, ): string[] { const S = host.sizeType; if (bounded) host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); const destroy = `${fnName}_destroy`; + // A freezable record's count is the word without its frozen bit. + const notFrozen = sizeLiteral(S, ~recordFrozenBit(S)); const lines = [ ...(bounded ? destroyDefinitions(destroy, freeBody, chained) : []), - ...releaseFastPath(host, fnName, `${fnName}_slow`, traced, comment, immortal), + ...releaseFastPath(host, fnName, `${fnName}_slow`, traced, comment, immortal, S, frozen), `define internal void @${fnName}_slow(ptr %o) noinline ${FN_ATTRS} {${comment ? ` ; ${comment}` : ""}`, `entry:`, ` %isnull = icmp eq ptr %o, null`, @@ -1561,7 +1575,9 @@ export function releaseBody( `dec:`, ` %n = sub ${S} %rc, 1`, ` store ${S} %n, ptr %o`, - ` %dead = icmp eq ${S} %n, 0`, + ...(frozen + ? [` %refs = and ${S} %n, ${notFrozen}`, ` %dead = icmp eq ${S} %refs, 0`] + : [` %dead = icmp eq ${S} %n, 0`]), ` br i1 %dead, label %free, label %${traced ? "root" : "done"}`, `free:`, ...(traced ? cycleDeadLines(host, "teardown") : []), @@ -1591,9 +1607,14 @@ export function releaseFastPath( comment = "", immortal = false, count: RcCount = host.sizeType, + frozen = false, ): string[] { const S = count; if (immortal) return immortalRcBody(host, fnName, "void", comment); + // A freezable record compares its count without the frozen bit: an + // immortal word's count is then ~FROZEN, which the same compare sends to + // `slow`. + const notFrozen = ~recordFrozenBit(S); return [ `define internal void @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment} (fast path)` : ""}`, `entry:`, @@ -1601,8 +1622,13 @@ export function releaseFastPath( ` br i1 %isnull, label %done, label %check`, `check:`, ` %rc = load ${S}, ptr %o`, - ` %rcm2 = sub ${S} %rc, 2`, - ` %shared = icmp ult ${S} %rcm2, -3`, + ...(frozen + ? [ + ` %refs = and ${S} %rc, ${sizeLiteral(S, notFrozen)}`, + ` %rcm2 = sub ${S} %refs, 2`, + ` %shared = icmp ult ${S} %rcm2, ${sizeLiteral(S, notFrozen - 2n)}`, + ] + : [` %rcm2 = sub ${S} %rc, 2`, ` %shared = icmp ult ${S} %rcm2, -3`]), ` br i1 %shared, label %dec, label %slow`, `dec:`, ` %n = sub ${S} %rc, 1`, @@ -1810,6 +1836,7 @@ export function emitRecordShapes( refMembers.some((member) => needsBoundedRelease(member.type)), false, chained, + shape.freezable === true, ), ``, ); diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts index c7a58a7ea..ae2b904db 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -44,8 +44,9 @@ export type StackObjectFieldOwner = (fields: readonly StackObjectField[]) => voi * standalone scalar class whose constructor never lets `this` escape * (record-results.ts). The caller proves that every use of the value only * projects it. Record fields may own references; the frame releases them - * (stackObjectFields). Published shapes keep heap storage: their field - * guards treat the immortal header as a frozen object. */ + * (stackObjectFields). Published and freezable shapes keep heap storage: + * their field guards treat the immortal header as a frozen object, and a + * frozen record keeps its frozen bit in a counted header. */ export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackObjectSource { // A ThreadSlots key keeps its slot index for its lifetime and has its // values released when it is freed: it lives on the heap. @@ -53,6 +54,8 @@ export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackO return false; if (e.type.kind === "record" && host.publishedTypes?.recordGuarded(e.type.shapeId) === true) return false; + if (e.type.kind === "record" && host.recordsById.get(e.type.shapeId)?.freezable === true) + return false; // A producer with a result-slot entry builds its fresh result in the // caller's storage. if (e.kind === "call") return host.fnByName.has(resultEntryName(e.callee)); diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 0be9be81a..7a8fd646e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -1022,7 +1022,7 @@ function lowerGuardedRecordWrite( const value = stabilized(lowerer, lowerer.lowerExpr(expr.right), "%recordValue", receiver.stmts); const write = lowerer.withExpressionOverride(target.expression, receiver.present, () => lowerer.withExpressionOverride(target.argumentExpression, key, () => - lowerer.withExpressionOverride(expr.right, value, () => lowerElementWrite(lowerer, expr)), + lowerer.withExpressionOverride(expr.right, value, () => lowerer.lowerElementWrite(expr)), ), ); return { diff --git a/packages/compiler/src/frontend/lowering/frozen-records.ts b/packages/compiler/src/frontend/lowering/frozen-records.ts new file mode 100644 index 000000000..f61de39ec --- /dev/null +++ b/packages/compiler/src/frontend/lowering/frozen-records.ts @@ -0,0 +1,328 @@ +import * as ts from "../ts7/adapter.js"; +import { + BOOL, + VOID, + shapeHasAccessorSlots, + type IrExpr, + type IrFunction, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; +import { numLit, strLit, varRef } from "../../ir/build.js"; +import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; +import type { Lowerer } from "./lowerer.js"; +import { recordHasOwnHelper } from "./lower-calls.js"; +import { isStrictCode } from "./strict-mode.js"; + +/* Object.freeze of records that other references can still write. Only the + * shapes a program may freeze pay for it: their values keep a frozen bit in + * the rc word (scr_rc.h SCR_RC_FROZEN), their release ignores the bit, and + * every write to them checks it first (guardFrozenRecordWrites). A shape is + * freezable when a static Object.freeze names it, or, in a program that + * freezes untyped values, when a value of it or of a type reaching it can be + * held as a live untyped reference. */ + +type RecordType = Extract; +type RecordWrite = Extract; + +/** Object.freeze of a record: sets its frozen bit and answers the record. */ +export function lowerRecordFreeze( + lowerer: Lowerer, + target: IrExpr, + type: RecordType, + loc: SrcLoc, +): IrExpr { + const slot = lowerer.declareHiddenLocal("%freezeTarget", type); + const receiver = (): IrExpr => varRef(slot.id, type, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: slot.id, init: target, loc }, + { + kind: "exprStmt", + expr: { kind: "libCall", fn: "record.freeze", args: [receiver()], type: VOID, loc }, + loc, + }, + ], + result: receiver(), + type, + loc, + }; +} + +function isRecordWrite(s: IrStmt): s is RecordWrite { + return s.kind === "recordSet" || s.kind === "recordKeySet" || s.kind === "recordKeyDelete"; +} + +/** The key of a record write's source range: strict mode is lexical, so + * every write lowered from the same range shares its mode. */ +function writeKey(loc: SrcLoc): string { + return `${loc.file}\0${loc.start}\0${loc.end}`; +} + +/** Records the record writes of `stmt`, lowered from `node`, as sloppy-mode + * writes when `node` is sloppy-mode code: a write to a frozen record then + * leaves it unchanged instead of throwing. `deletes` says `stmt` is the + * absent-state write of `delete r.f`, which Node refuses only while the + * field is present. */ +export function markSloppyRecordWrites( + lowerer: Lowerer, + stmt: IrStmt, + node: ts.Node, + deletes = false, +): IrStmt { + if (deletes) lowerer.recordFieldDeletes.add(writeKey(stmt.loc)); + if (isStrictCode(node)) return stmt; + everyStmtList([stmt], { + stmt: (s) => { + if (s.kind === "recordSet" || s.kind === "recordKeySet" || s.kind === "recordKeyDelete") + lowerer.sloppyRecordWrites.add(writeKey(s.loc)); + return true; + }, + expr: () => true, + }); + return stmt; +} + +/** Whether a field write's value can be an optional field's absent state, + * which only a field write itself can hold. */ +function holdsAbsentState(e: IrExpr): boolean { + if (e.kind === "fieldAbsent") return true; + return e.kind === "ternary" && (holdsAbsentState(e.then) || holdsAbsentState(e.else_)); +} + +/** The shapes whose values the program may freeze. */ +function freezableShapes(lowerer: Lowerer, functions: IrFunction[]): Set { + const shapes = new Set(); + const live: IrType[] = []; + let dynFreeze = false; + for (const fn of functions) + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "libCall" && e.fn === "record.freeze") { + const target = e.args[0]; + if (target && target.type.kind === "record") shapes.add(target.type.shapeId); + } else if (e.kind === "libCall" && e.fn === "dyn.freeze") dynFreeze = true; + else if (e.kind === "dynFrom") live.push(e.value.type); + return true; + }, + stmt: () => true, + }); + if (!dynFreeze) return shapes; + // An untyped reference to a class instance, or to a record or array + // boxed live, reaches the values its fields and elements hold as further + // live references. Class instances also cross into untyped code through + // compiled callbacks, so every class is a root. + const stack = [...live]; + for (const info of lowerer.classes.values()) + for (const field of info.def.fields) stack.push(field.type); + const seen = new Set(); + while (stack.length > 0) { + const type = stack.pop()!; + switch (type.kind) { + case "record": { + if (seen.has(`record:${type.shapeId}`)) break; + seen.add(`record:${type.shapeId}`); + shapes.add(type.shapeId); + const shape = lowerer.shapes.get(type.shapeId); + for (const field of shape?.fields ?? []) stack.push(field.type); + if (shape?.indexValue) stack.push(shape.indexValue); + break; + } + case "union": + if (seen.has(`union:${type.unionId}`)) break; + seen.add(`union:${type.unionId}`); + for (const arm of lowerer.unions.get(type.unionId)?.arms ?? []) stack.push(arm); + break; + case "array": + case "set": + stack.push(type.elem); + break; + case "map": + stack.push(type.key, type.value); + break; + case "promise": + stack.push(type.inner); + break; + case "generator": + stack.push(type.yieldT, type.retT, type.nextT); + break; + case "func": + for (const param of type.params) stack.push(param); + stack.push(type.ret); + break; + default: + break; + } + } + return shapes; +} + +/** The guarded form of a write to a freezable record: the receiver and the + * operands are evaluated first, in source order, then the frozen bit decides + * between the write and `reject`. */ +function guardedWrite( + receiver: IrExpr, + decls: IrStmt[], + reject: IrStmt[], + store: IrStmt, + loc: SrcLoc, +): IrStmt { + return { + kind: "block", + body: [ + ...decls, + { + kind: "if", + cond: { kind: "libCall", fn: "record.isFrozen", args: [receiver], type: BOOL, loc }, + then: reject, + else_: [store], + loc, + }, + ], + loc, + }; +} + +/** Checks the frozen bit before every write to a record of a freezable + * shape. A write to a frozen record throws Node's TypeError in strict mode + * code; in sloppy-mode code it leaves the record unchanged. Deleting a key + * the record does not have succeeds either way. */ +export function guardFrozenRecordWrites(lowerer: Lowerer, functions: IrFunction[]): void { + const freezable = freezableShapes(lowerer, functions); + for (const id of freezable) { + const shape = lowerer.shapes.get(id); + if (shape) shape.freezable = true; + } + if (freezable.size === 0) return; + const lifted = lowerer.liftedFns.length; + for (const fn of functions) { + const local = (type: IrType, loc: SrcLoc): Extract => { + const id = `%frozenWrite.${fn.locals.length}`; + fn.locals.push({ id, name: id, type, mutable: false }); + return { kind: "varRef", localId: id, type, loc }; + }; + const guarded = new Set(); + fn.body = transformStmtList(fn.body, { + stmt: (statement) => { + if (!isRecordWrite(statement) || guarded.has(statement)) return statement; + if (!freezable.has(statement.shapeId)) return statement; + const sloppy = lowerer.sloppyRecordWrites.has(writeKey(statement.loc)); + const deletes = + statement.kind === "recordSet" && lowerer.recordFieldDeletes.has(writeKey(statement.loc)); + const loc = statement.loc; + const shapeId = statement.shapeId; + const receiver = local(statement.obj.type, loc); + const decls: IrStmt[] = [ + { kind: "varDecl", localId: receiver.localId, init: statement.obj, loc }, + ]; + const owner = strLit( + lowerer.shapes.get(shapeId)?.tuple ? "[object Array]" : "#", + loc, + ); + const thrown = (key: IrExpr, operation: number): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "record.throwFrozen", + args: [key, owner, numLit(operation, loc)], + type: VOID, + loc, + }, + loc, + }); + if (statement.kind === "recordSet") { + let value = statement.value; + if (!holdsAbsentState(value)) { + const slot = local(value.type, loc); + decls.push({ kind: "varDecl", localId: slot.localId, init: value, loc }); + value = slot; + } + const store: IrStmt = { + kind: "recordSet", + obj: receiver, + shapeId, + field: statement.field, + value, + loc, + }; + guarded.add(store); + const field = strLit(statement.field, loc); + // `delete r.f` of an optional field writes its absent state: Node + // only refuses it when the field is present. + const reject: IrStmt[] = sloppy + ? [] + : deletes + ? [ + { + kind: "if", + cond: lowerer.recordFieldPresent(receiver, shapeId, statement.field, loc), + then: [thrown(field, 2)], + else_: null, + loc, + }, + ] + : [thrown(field, 0)]; + return guardedWrite(receiver, decls, reject, store, loc); + } + const key = local(statement.key.type, loc); + decls.push({ kind: "varDecl", localId: key.localId, init: statement.key, loc }); + const named = key.type.kind === "string"; + const present: IrExpr = named + ? { + kind: "call", + callee: recordHasOwnHelper(lowerer, shapeId, loc), + args: [receiver, key], + type: BOOL, + loc, + } + : { kind: "boolLit", value: true, type: BOOL, loc }; + const text: IrExpr = named ? key : strLit("[key]", loc); + if (statement.kind === "recordKeySet") { + const value = local(statement.value.type, loc); + decls.push({ kind: "varDecl", localId: value.localId, init: statement.value, loc }); + const store: IrStmt = { + kind: "recordKeySet", + obj: receiver, + shapeId, + key, + value, + ...(statement.overflowOnly ? { overflowOnly: true as const } : {}), + loc, + }; + guarded.add(store); + const reject: IrStmt[] = sloppy + ? [] + : [ + { + kind: "if", + cond: present, + then: [thrown(text, 0)], + else_: [thrown(text, 1)], + loc, + }, + ]; + return guardedWrite(receiver, decls, reject, store, loc); + } + const store: IrStmt = { kind: "recordKeyDelete", obj: receiver, shapeId, key, loc }; + guarded.add(store); + const reject: IrStmt[] = sloppy + ? [] + : [{ kind: "if", cond: present, then: [thrown(text, 2)], else_: null, loc }]; + return guardedWrite(receiver, decls, reject, store, loc); + }, + expr: (expression) => expression, + }); + } + // The key-presence helpers the guards call. + for (const helper of lowerer.liftedFns.slice(lifted)) functions.push(helper); +} + +/** Whether a statically typed Object.freeze can freeze a value of `type`: + * a record whose shape has no accessor properties. */ +export function freezableRecordType(lowerer: Lowerer, type: IrType): type is RecordType { + if (type.kind !== "record") return false; + const shape = lowerer.shapes.get(type.shapeId); + return shape !== undefined && !shapeHasAccessorSlots(shape); +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index b595aaa43..e2ed5f9fe 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -245,6 +245,7 @@ import { import { fenceSymbolFieldCopy } from "./symbol-fields.js"; import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js"; import { lowerClassFieldDefine, lowerClassIntegrity } from "./class-attributes.js"; +import { freezableRecordType, lowerRecordFreeze } from "./frozen-records.js"; import { classStaticDataFor } from "./class-static-data.js"; import { lowerUrlFactory } from "./lower-url.js"; import { nullishValueUnitOf } from "./binding-analysis.js"; @@ -7206,7 +7207,7 @@ export function recordKeysArrayCall( * (a key is own exactly when Object.keys would list it — the two share * the test), everything else true. Unmatched keys probe the overflow * map when present, so explicit undefined values remain own keys. */ -function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): string { +export function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): string { const key = `obj.hasOwn:${shapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; @@ -8455,10 +8456,23 @@ function lowerObjectStaticCall( // the argument — no alias exists, so the frozen bit is unobservable // (writes through the Readonly result are compile errors, and no // other reference can write). Primitives pass through per ES2015. - // Aliased objects keep a fence: a later write through the original - // reference would need the runtime frozen bit (strict mode throws). + // Aliased records get the frozen bit of their rc word (frozen-records.ts); + // other aliased objects keep a fence: a later write through the original + // reference would need a runtime frozen bit (strict mode throws). if (member === "isFrozen" && call.arguments.nodes.length === 1) { - const value = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN); + const argNode = call.arguments.nodes[0]!; + if (tryLowerExpression(lowerer, argNode)?.type.kind === "record") { + const record = lowerer.lowerExpr(argNode); + if (record.type.kind === "record") + return { + kind: "libCall", + fn: "record.isFrozen", + args: [record], + type: BOOL, + loc: locOf(call), + }; + } + const value = lowerer.lowerExprExpecting(argNode, DYN); return { kind: "libCall", fn: "dyn.isFrozen", args: [value], type: BOOL, loc: locOf(call) }; } if (member === "freeze") { @@ -8508,6 +8522,8 @@ function lowerObjectStaticCall( if (fnIntegrity) return fnIntegrity; const integrity = lowerClassIntegrity(lowerer, call, "freeze", value); if (integrity) return integrity; + if (freezableRecordType(lowerer, value.type)) + return lowerRecordFreeze(lowerer, value, value.type, locOf(call)); lowerer.noLowering( "Object.freeze of a possibly-aliased value", call, diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 66ecea602..e66d0e855 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -105,6 +105,7 @@ import { requireSpecOf, } from "../program.js"; import { isStrictCode } from "./strict-mode.js"; +import { markSloppyRecordWrites } from "./frozen-records.js"; import { COMPOUND_ASSIGN_OPS, type CompoundOp, @@ -7160,7 +7161,11 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt if (obj.type.kind === "record") { const shape = lowerer.shapes.get(obj.type.shapeId); if (shape?.indexValue && shape.fields.length === 0 && !shape.tuple) { - return { kind: "recordKeyDelete", obj, shapeId: obj.type.shapeId, key: lowerKey(), loc }; + return markSloppyRecordWrites( + lowerer, + { kind: "recordKeyDelete", obj, shapeId: obj.type.shapeId, key: lowerKey(), loc }, + expr, + ); } // `delete r.f` of a declared OPTIONAL field (undefined-armed slot) // writes the field's ABSENT state: a monomorphic shape cannot remove @@ -7181,14 +7186,19 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt if (field) { const absent = lowerer.absentFieldValue(field.type, loc); if (absent) { - return { - kind: "recordSet", - obj, - shapeId: obj.type.shapeId, - field: field.name, - value: absent, - loc, - }; + return markSloppyRecordWrites( + lowerer, + { + kind: "recordSet", + obj, + shapeId: obj.type.shapeId, + field: field.name, + value: absent, + loc, + }, + expr, + true, + ); } } if (shape?.indexValue) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a765c91ee..ba6d1214f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -150,6 +150,7 @@ import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optio import { type ProtocolDispatch } from "./class-protocol-dispatch.js"; import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js"; import { guardClassAttributeWrites } from "./class-attributes.js"; +import { guardFrozenRecordWrites, markSloppyRecordWrites } from "./frozen-records.js"; import { applyHandleProperties } from "./handle-properties.js"; import { ClassProtocols } from "./class-protocols.js"; import { earlyReadVarSymbols } from "./var-hoisting.js"; @@ -3241,6 +3242,10 @@ export class Lowerer { // Stack of function contexts (bottom = the function being declared at // top level, top = the innermost nested function currently lowering). fnStack: FnCtx[] = []; + /** Source ranges of record writes in sloppy-mode code, and of the + * absent-state writes of `delete r.f` (markSloppyRecordWrites). */ + readonly sloppyRecordWrites = new Set(); + readonly recordFieldDeletes = new Set(); readonly liftedFns: IrFunction[] = []; /** The first lifted function of each name (liftedFnNamed), for the * liftedFns indexed so far. liftedFns only grows. */ @@ -7689,6 +7694,7 @@ export class Lowerer { finalizePrivateBrands(this, functions); trackClassFieldCreation(this, functions); guardClassAttributeWrites(this, functions); + guardFrozenRecordWrites(this, functions); applyHandleProperties(this, functions); // Globals typed by a class that never REGISTERED (a JS class whose @@ -12978,7 +12984,7 @@ export class Lowerer { } lowerElementWrite(expr: ts.BinaryExpression): IrStmt { - return lowerElementWrite(this, expr); + return markSloppyRecordWrites(this, lowerElementWrite(this, expr), expr); } lowerLiveWebValue(node: ts.Expression): IrExpr { @@ -13375,7 +13381,7 @@ export class Lowerer { } fieldSetStmt(target: FieldTarget, value: IrExpr, loc: SrcLoc, blame: ts.Node): IrStmt { - return fieldSetStmt(this, target, value, loc, blame); + return markSloppyRecordWrites(this, fieldSetStmt(this, target, value, loc, blame), blame); } lowerFieldCompound( diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 9be819dd3..0505129fb 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -687,6 +687,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "class.defineField", "class.throwReadOnly", "class.throwNotExtensible", + "record.throwFrozen", "dyn.toString", "dyn.valueOf", "string.fromCodePoint", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e60a6600b..1b2c1082d 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -271,6 +271,18 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** Throws Node's TypeError for a class field initializer on a * non-extensible instance: the field name. */ "class.throwNotExtensible": { argTypes: [STRING], result: VOID }, + /** Object.freeze of a record whose shape is freezable (frozen-records.ts): + * sets the frozen bit in its rc word (scr_record_freeze). The receiver is + * any record type. */ + "record.freeze": { argTypes: [null], result: VOID }, + /** Whether a record is frozen: its rc word's frozen bit, set by + * record.freeze and dyn.freeze of a capsule. Emitted inline. The receiver + * is any record type. */ + "record.isFrozen": { argTypes: [null], result: BOOL }, + /** Throws Node's TypeError for a write to a frozen record: the key, the + * object's text in the message ("#") and the operation (0 assign, + * 1 add, 2 delete). */ + "record.throwFrozen": { argTypes: [STRING, STRING, F64], result: VOID }, "dyn.nativeSetNew": { argTypes: [DYN], result: DYN }, "dyn.nativeSetIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeMapIs": { argTypes: [DYN], result: BOOL }, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 63286ff54..3211e48f3 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1685,6 +1685,10 @@ export interface IrRecordShape { * dynCheck), and JSON serialization appends overflow entries after the * declared fields in insertion order. */ indexValue?: IrType; + /** Values of this shape may be frozen (frozen-records.ts): their rc word + * can carry the frozen bit, which their release ignores, and every write + * to them checks it. */ + freezable?: true; /** Field names in FIRST-SEEN declaration order (the checker's property * order of the first ts.Type interned to this shape) — metadata, NOT * part of the interned identity: a later structurally-equal type with a diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7404-frozen-records.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7404-frozen-records.ts.json new file mode 100644 index 000000000..d85f2aab5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7404-frozen-records.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7404-frozen-records.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7405-frozen-records-sloppy.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7405-frozen-records-sloppy.cjs.json new file mode 100644 index 000000000..3d87d019b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7405-frozen-records-sloppy.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7405-frozen-records-sloppy.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 6dcd546b0..241164705 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -170,7 +170,8 @@ void scr_weak_dispose(void *object) { #include /* Every heap object's first member is `size_t rc`; the collector works on - * its count (SCR_RC_COUNT). */ + * its count (SCR_RC_COUNT), and tests it without a frozen record's bit + * (SCR_RC_REFS). */ #define SCR_RC(obj) SCR_RC_COUNT(obj) static void scr_cyc_oom(void) { @@ -1308,7 +1309,8 @@ static void scr_mg_visit(void *child, void *ctx) { scr_mg_skip_edge(child); return; } - size_t rc = --SCR_RC(child); + --SCR_RC(child); + size_t rc = SCR_RC_REFS(child); if (scr_cyc_color(h) == SCR_CYC_GRAY) { if (rc == 0) { scr_gray_referenced--; @@ -1336,7 +1338,7 @@ static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintp )) h->buf = scr_gray_comp; if (scr_white.n < SCR_CYC_GRAY_LIST_MAX) scr_cyc_push_gray(&scr_white, obj); - if (SCR_RC(obj) > 0) { + if (SCR_RC_REFS(obj) > 0) { scr_gray_referenced++; scr_comp_referenced++; } @@ -1408,7 +1410,7 @@ static void scr_scan_visit(void *child, void *ctx) { static void scr_scan(void *obj, uintptr_t depth) { ScrCycHdr *h = scr_cyc_hdr(obj); if (scr_cyc_color(h) != SCR_CYC_GRAY) return; - if (SCR_RC(obj) > 0) { + if (SCR_RC_REFS(obj) > 0) { scr_restore_from(obj); return; } @@ -1476,7 +1478,7 @@ static void scr_xg_destroy(void *obj) { } static SCR_CYC_COMPACT void scr_xg_release(void *obj) { - if (SCR_RC(obj) > 1) { + if (SCR_RC_REFS(obj) > 1) { SCR_RC(obj) -= 1; scr_cyc_on_release(obj); /* lost a reference: a possible cycle root */ return; @@ -1768,14 +1770,14 @@ static __attribute__((noinline)) size_t scr_yin_collect(void) { if (scr_cyc_gen(h) != SCR_CYC_YOUNG || !scr_cyc_is_buffered(h) || scr_cyc_buf_index(h) != k || scr_cyc_color(h) != SCR_CYC_BLACK) scr_trap("scriptc: corrupt young nursery entry\n"); - if (SCR_RC(v[k]) == 0 || SCR_RC(v[k]) < scr_yin_counts[k]) + if (SCR_RC_REFS(v[k]) == 0 || SCR_RC_REFS(v[k]) < scr_yin_counts[k]) scr_trap("scriptc: young object with uncounted references\n"); } #endif for (size_t k = 0; k < n; k++) { void *obj = v[k]; ScrCycHdr *h = scr_cyc_hdr(obj); - if (scr_cyc_color(h) == SCR_CYC_GRAY || SCR_RC(obj) <= scr_yin_counts[k]) continue; + if (scr_cyc_color(h) == SCR_CYC_GRAY || SCR_RC_REFS(obj) <= scr_yin_counts[k]) continue; scr_cyc_set_color(h, SCR_CYC_GRAY); scr_yin_push(&scr_yin_stack, obj); while (scr_yin_stack.n) { @@ -1799,7 +1801,7 @@ static __attribute__((noinline)) size_t scr_yin_collect(void) { scr_cyc_set_gen(h, SCR_CYC_YIN_PROMOTED); scr_cyc_clear_buffered(h); /* An outside root becomes a candidate (see "candidates" above). */ - if (SCR_RC(obj) > scr_yin_counts[k]) scr_cyc_on_release(obj); + if (SCR_RC_REFS(obj) > scr_yin_counts[k]) scr_cyc_on_release(obj); scr_yin_met(h->id, false); } else { scr_yin_met(h->id, true); @@ -2147,7 +2149,7 @@ static size_t scr_cyc_pass(unsigned gen_limit) { for (size_t j = start; j < end; j++) { void *obj = scr_white.v[j]; if (scr_cyc_color(scr_cyc_hdr(obj)) != SCR_CYC_GRAY) continue; - if (SCR_RC(obj) > 0) scr_restore_from(obj); + if (SCR_RC_REFS(obj) > 0) scr_restore_from(obj); else scr_white.v[kept++] = obj; } from = end; @@ -2176,7 +2178,7 @@ static size_t scr_cyc_pass(unsigned gen_limit) { * whitened only to be restored again. */ for (size_t i = 0; i < scr_cands.n; i++) { void *obj = scr_cands.v[i]; - if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_GRAY && SCR_RC(obj) > 0) scr_restore_from(obj); + if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_GRAY && SCR_RC_REFS(obj) > 0) scr_restore_from(obj); } for (size_t i = 0; i < scr_cands.n; i++) { void *obj = scr_cands.v[i]; diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index fdffd73cb..8468f8c0f 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1019,6 +1019,13 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops); void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *names, const size_t *lens, size_t count); bool scr_record_source_has(const void *rec); +/* Object.freeze of a record a program may freeze: sets SCR_RC_FROZEN in its + * rc word (immortal records are left as they are). */ +void scr_record_freeze(void *record); +/* Node's TypeError for a write to a frozen record: operation 0 assigns the + * existing property `key`, 1 adds it, 2 deletes it. `owner` is the object's + * text in the message ("#"). */ +void scr_record_throw_frozen(ScrStr *key, ScrStr *owner, double operation); /* The commit of a live record view converts the view into a new record * `from`, whose conversion keeps the view's undeclared properties, then * moves its fields into `to`: this moves the kept properties to `to` too, diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 798d53ae9..a542b5c24 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2293,6 +2293,55 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { return scr_dyn_retain(object); } +/* The native record a record capsule refers to, or NULL for any other + * value. */ +static void *scr_dyn_typed_record(const ScrDyn *d) { + if (d->kind != SCR_DYN_TYPED_REF || d->v.typed_ref.type_key_len < 7 || + memcmp(d->v.typed_ref.type_key, "record:", 7) != 0) + return NULL; + return d->v.typed_ref.ptr; +} + +/* Freezes the checked view of a frozen record: its fields, and every + * property added through the view. */ +static void scr_dyn_record_view_freeze(ScrDyn *view) { + view->non_extensible = true; + if (view->kind == SCR_DYN_OBJ) { + for (size_t i = 0; i < view->v.obj.len; i++) { + ScrDynEntry *entry = &view->v.obj.entries[i]; + entry->configurable = false; + if (!entry->accessor) entry->writable = false; + } + } else if (view->kind == SCR_DYN_ARR) { + view->v.arr.sealed = true; + view->v.arr.frozen = true; + if (view->v.arr.properties) scr_dyn_release(scr_dyn_freeze_data(view->v.arr.properties)); + } + if (view->symbol_properties) scr_dyn_release(scr_dyn_freeze_data(view->symbol_properties)); +} + +void scr_record_freeze(void *record) { + size_t *rc = record; + if (*rc != SIZE_MAX) *rc |= SCR_RC_FROZEN; +} + +void scr_record_throw_frozen(ScrStr *key, ScrStr *owner, double operation) { + ScrJsonBuf buffer; + scr_jb_init(&buffer); + if (operation == 1) { + scr_jb_puts(&buffer, "Cannot add property "); + scr_jb_write(&buffer, key->data, key->len); + scr_jb_puts(&buffer, ", object is not extensible"); + } else { + scr_jb_puts(&buffer, operation == 2 ? "Cannot delete property '" : "Cannot assign to read only property '"); + scr_jb_write(&buffer, key->data, key->len); + scr_jb_puts(&buffer, operation == 2 ? "' of " : "' of object '"); + scr_jb_write(&buffer, owner->data, owner->len); + if (operation != 2) scr_jb_puts(&buffer, "'"); + } + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); +} + /* A class instance's integrity state lives in its native layout, which only * statically typed calls can reach (the compiler adds the state to the * hierarchies they name). Through an untyped value the operation throws. */ @@ -2301,7 +2350,9 @@ static bool scr_dyn_class_integrity_fence(const ScrDyn *object, const char *oper ScrJsonBuf buffer; scr_jb_init(&buffer); scr_jb_puts(&buffer, operation); - scr_jb_puts(&buffer, " of a class instance held in an untyped value is not supported yet"); + scr_jb_puts(&buffer, scr_dyn_typed_record(object) + ? " of an object held in an untyped value is not supported yet" + : " of a class instance held in an untyped value is not supported yet"); ScrStr *message = scr_jb_finish(&buffer); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); scr_str_release(message); @@ -3628,12 +3679,16 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { fresh->rc = fresh_rc; scr_dyn_release(fresh); } + void *record = scr_dyn_typed_record(capsule); + if (record && scr_rc_frozen(record)) scr_dyn_record_view_freeze(capsule->v.typed_ref.materialized); return scr_dyn_retain(capsule->v.typed_ref.materialized); } void scr_dyn_typed_ref_commit(ScrDyn *d) { + /* A frozen record's view cannot have changed its fields. */ if (d && d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.commit && - d->v.typed_ref.materialized) { + d->v.typed_ref.materialized && + !(scr_dyn_typed_record(d) && scr_rc_frozen(d->v.typed_ref.ptr))) { d->v.typed_ref.commit(d->v.typed_ref.ptr, d->v.typed_ref.materialized); } @@ -15627,8 +15682,18 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt return scr_dyn_to_string_method_args(d, enc, what, NULL, 0); } +/* Freezing a record capsule freezes the native record, whose emitted writes + * then refuse to change it, and its checked view. */ +static ScrDyn *scr_dyn_freeze_record(ScrDyn *capsule, void *record) { + scr_record_freeze(record); + if (capsule->v.typed_ref.materialized) scr_dyn_record_view_freeze(capsule->v.typed_ref.materialized); + return scr_dyn_retain(capsule); +} + ScrDyn *scr_dyn_freeze(ScrDyn *value) { if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; + void *record = scr_dyn_typed_record(value); + if (record) return scr_dyn_freeze_record(value, record); if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(value); if (!table) return scr_dyn_freeze_data(value); @@ -15640,6 +15705,8 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) { ScrDyn *scr_dyn_freeze_data(ScrDyn *value) { if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; + void *record = scr_dyn_typed_record(value); + if (record) return scr_dyn_freeze_record(value, record); if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze_data(value->symbol_properties)); if (value->kind == SCR_DYN_ARR) { diff --git a/packages/runtime/src/scr_json.test.c b/packages/runtime/src/scr_json.test.c index 736eca540..a28759876 100644 --- a/packages/runtime/src/scr_json.test.c +++ b/packages/runtime/src/scr_json.test.c @@ -802,8 +802,21 @@ static void json_schema_union_members(void) { assert(test_opts_live == 0); } +/* Freezing a record sets the frozen bit beside its count; an immortal + * record keeps its word, which does not read as frozen. */ +static void record_frozen_bit(void) { + size_t record[2] = {3, 0}; + assert(!scr_rc_frozen(record)); + scr_record_freeze(record); + assert(scr_rc_frozen(record) && SCR_RC_REFS(record) == 3); + size_t immortal[2] = {SIZE_MAX, 0}; + scr_record_freeze(immortal); + assert(immortal[0] == SIZE_MAX && !scr_rc_frozen(immortal)); +} + int main(void) { json_string_boundaries(); + record_frozen_bit(); json_indent_ownership(); scr_init(); checked_storage(); diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 18df3a237..87eb2536a 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -45,14 +45,32 @@ typedef size_t ScrRcCount; /* Whether `obj`, an object of any kind, is published (immortal). */ #define SCR_RC_PUBLISHED(obj) (SCR_RC_COUNT(obj) >= SCR_RC_PUBLISHED_MIN) +/* Frozen records (Object.freeze). The rc word of a record a program may + * freeze (llvm/shapes.ts) carries SCR_RC_FROZEN beside its count, and that + * record's emitted release ignores the bit. On 64-bit targets the bit is in + * the high half, outside the count; on 32-bit targets it is the count's bit + * 30, which SCR_RC_REFS masks. An immortal word has the bit set too; it + * does not read as frozen. */ +#if SIZE_MAX > UINT32_MAX +#define SCR_RC_FROZEN ((size_t)1 << 62) +#define SCR_RC_REFS(obj) SCR_RC_COUNT(obj) +#else +#define SCR_RC_FROZEN ((size_t)1 << 30) +#define SCR_RC_REFS(obj) (SCR_RC_COUNT(obj) == SCR_RC_IMMORTAL ? SCR_RC_IMMORTAL : SCR_RC_COUNT(obj) & ~SCR_RC_FROZEN) +#endif +static inline bool scr_rc_frozen(const void *obj) { + size_t word = *(const size_t *)obj; + return word != SIZE_MAX && (word & SCR_RC_FROZEN) != 0; +} + /* The class id in the high half of `obj`'s rc word, or 0 for an object - * without one: the high half of any other object's word is 0, or 0xffffffff - * (immortal) or 0x80000000 (program-lifetime), and a class id is never one of - * these. */ + * without one: the high half of any other object's word is 0, 0xffffffff + * (immortal), 0x80000000 (program-lifetime) or 0x40000000 (a frozen record), + * and a class id is never one of these. */ static inline uint32_t scr_rc_class_id(const void *obj) { #if SIZE_MAX > UINT32_MAX uint32_t high = (uint32_t)(*(const size_t *)obj >> 32); - return high == UINT32_MAX || high == UINT32_C(0x80000000) ? 0 : high; + return high == UINT32_MAX || high == UINT32_C(0x80000000) || high == UINT32_C(0x40000000) ? 0 : high; #else (void)obj; return 0; diff --git a/tests/corpus/7404-frozen-records.ts b/tests/corpus/7404-frozen-records.ts new file mode 100644 index 000000000..acf9b393b --- /dev/null +++ b/tests/corpus/7404-frozen-records.ts @@ -0,0 +1,124 @@ +// Object.freeze of records that other references still hold: writes +// through any reference then throw Node's TypeError (this module is strict +// mode code; 7405 covers sloppy mode). Covers static freezes, freezes +// through untyped values, Object.isFrozen, keyed writes and deletes, +// optional-field deletes, Object.assign, tuples, and frozen records that are +// released or collected as cycles. +export {}; + +type Counter = { count: number; label: string }; +type Link = { name: string; next?: Link }; + +function report(e: unknown): void { + console.log((e as Error).name + ": " + (e as Error).message); +} + +function freezeUnknown(value: unknown): unknown { + return Object.freeze(value); +} + +function isFrozenUnknown(value: unknown): boolean { + return Object.isFrozen(value); +} + +function deepFreeze(value: unknown): unknown { + if (typeof value === "object" && value !== null) { + Object.freeze(value); + for (const key of Object.keys(value)) deepFreeze((value as Record)[key]); + } + return value; +} + +type Config = { name: string; limits: { max: number; min: number } }; + +function bump(c: Counter): void { + c.count += 1; +} + +function writes(): void { + const a: Counter = { count: 1, label: "a" }; + const alias = a; + console.log(Object.isFrozen(a), isFrozenUnknown(a)); + const frozen = Object.freeze(alias); + console.log(frozen === a, Object.isFrozen(a), isFrozenUnknown(alias)); + try { bump(a); } catch (e) { report(e); } + try { a.label = "b"; } catch (e) { report(e); } + console.log(a.count, a.label, JSON.stringify(a)); + + const viaUnknown: Counter = { count: 10, label: "u" }; + const back = freezeUnknown(viaUnknown); + console.log(back === viaUnknown, Object.isFrozen(viaUnknown)); + try { viaUnknown.count = 11; } catch (e) { report(e); } + console.log(viaUnknown.count); + + const dict: Record = { one: 1 }; + const dictAlias = dict; + Object.freeze(dictAlias); + try { dict["one"] = 2; } catch (e) { report(e); } + try { dict["two"] = 2; } catch (e) { report(e); } + try { delete dict["one"]; } catch (e) { report(e); } + delete dict["three"]; + console.log(JSON.stringify(dict), Object.isFrozen(dict)); + + const optional: { id: number; note?: string } = { id: 1, note: "n" }; + const optionalAlias = optional; + Object.freeze(optionalAlias); + try { delete optional.note; } catch (e) { report(e); } + console.log(JSON.stringify(optional)); + const bare: { id: number; note?: string } = { id: 2 }; + Object.freeze(bare); + delete bare.note; + console.log(JSON.stringify(bare)); + + const target: Counter = { count: 0, label: "t" }; + Object.freeze(target); + try { Object.assign(target, { count: 5, label: "x" }); } catch (e) { report(e); } + console.log(JSON.stringify(target)); + + const pair: [string, number] = ["p", 1]; + Object.freeze(pair); + try { pair[1] = 2; } catch (e) { report(e); } + console.log(pair[0], pair[1]); +} + +function deepFreezes(): void { + const config: Config = JSON.parse('{"name":"c","limits":{"max":2,"min":1}}'); + const limits = config.limits; + console.log(Object.isFrozen(config), Object.isFrozen(limits)); + deepFreeze(config); + console.log(Object.isFrozen(config), Object.isFrozen(limits), isFrozenUnknown(config.limits)); + try { config.limits.max = 3; } catch (e) { report(e); } + try { limits.min = 0; } catch (e) { report(e); } + try { config.name = "d"; } catch (e) { report(e); } + console.log(JSON.stringify(config)); +} + +function lifetimes(): void { + const list: Counter[] = []; + for (let i = 0; i < 4; i++) { + const item: Counter = { count: i, label: "x" + i }; + list.push(item); + if (i % 2 === 1) Object.freeze(item); + } + for (const item of list) { + try { item.count = item.count * 10; } catch (e) { console.log(item.label, (e as Error).message); } + } + console.log(JSON.stringify(list)); + const kept = list[1]!; + list.length = 0; + console.log(kept.count, Object.isFrozen(kept)); + + for (let round = 0; round < 3; round++) { + const first: Link = { name: "first" + round }; + const second: Link = { name: "second" + round, next: first }; + first.next = second; + Object.freeze(first); + Object.freeze(second); + try { first.name = "changed"; } catch (e) { report(e); } + console.log(first.next?.name, second.next?.name); + } +} + +writes(); +deepFreezes(); +lifetimes(); diff --git a/tests/corpus/7405-frozen-records-sloppy.cjs b/tests/corpus/7405-frozen-records-sloppy.cjs new file mode 100644 index 000000000..c0656b7b2 --- /dev/null +++ b/tests/corpus/7405-frozen-records-sloppy.cjs @@ -0,0 +1,34 @@ +// Writes to frozen records in sloppy-mode code leave them unchanged, where +// strict mode code throws (7404). The records are typed through JSDoc, so +// the writes are native record writes. + +/** @typedef {{ count: number, label: string }} Counter */ +/** @typedef {{ id: number, note?: string }} Note */ + +/** @param {Counter} c */ +function bump(c) { + c.count += 1; + c.label = c.label + "!"; +} + +/** @param {Note} n */ +function dropNote(n) { + delete n.note; +} + +/** @param {number} count @returns {Counter} */ +function counter(count) { + return { count: count, label: "c" + count }; +} + +const open = counter(1); +const frozen = Object.freeze(counter(2)); +bump(open); +bump(frozen); +console.log(JSON.stringify(open), JSON.stringify(frozen), Object.isFrozen(frozen)); + +/** @type {Note} */ +const note = { id: 1, note: "n" }; +Object.freeze(note); +dropNote(note); +console.log(JSON.stringify(note)); diff --git a/tests/harness/corpus-costs/7404-frozen-records.ts.json b/tests/harness/corpus-costs/7404-frozen-records.ts.json new file mode 100644 index 000000000..765134581 --- /dev/null +++ b/tests/harness/corpus-costs/7404-frozen-records.ts.json @@ -0,0 +1 @@ +{ "plain": 4725, "san": 55833 } diff --git a/tests/harness/corpus-costs/7405-frozen-records-sloppy.cjs.json b/tests/harness/corpus-costs/7405-frozen-records-sloppy.cjs.json new file mode 100644 index 000000000..c7cd5ec8f --- /dev/null +++ b/tests/harness/corpus-costs/7405-frozen-records-sloppy.cjs.json @@ -0,0 +1 @@ +{ "plain": 3175, "san": 37248 } From b7d187f889290a7549ef4aeadb6d86bf365e5d7d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 02:10:57 -0500 Subject: [PATCH 046/409] Keep inline caches for bag helpers with class-table exits --- packages/compiler/src/ir/js-ic.test.ts | 61 ++++++++++++++++++++++++++ packages/compiler/src/ir/js-ic.ts | 16 ++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index 1b566fd00..f71a1e9db 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -176,3 +176,64 @@ test("a function holds a site when it calls a classified helper or dispatches a expect(hasJsIcSite({ ...invoker, name: "%init.0" }, sites)).toBe(false); expect(hasJsIcSite({ ...caller, name: "%init.0" }, sites)).toBe(true); }); + +test("bag helpers that exit early for values no class branch matches are still the receiver", () => { + /** `if (!(v)) return `: the class tables' early exit. */ + const exit = (test: "classCapsule" | "funcKind", value: IrExpr, negated = true): IrStmt => ({ + kind: "if", + cond: { + kind: "dynTest", + test, + ...(negated ? { negated: true as const } : {}), + value: ref("v"), + type: BOOL, + loc, + }, + then: [ret(value)], + else_: null, + loc, + }); + const guarded = (name: string, first: IrStmt): IrFunction => + helper(name, ["v"], [first, capsuleBranch("v"), ret(ref("v"))]); + const readBag = guarded("%dyn.class.readPropertiesOrEmpty", exit("classCapsule", ref("v"))); + const writeBag = guarded("%dyn.class.properties", exit("classCapsule", ref("v"))); + const staticBag = guarded("%dyn.class.staticProperties", exit("funcKind", ref("v"))); + const getter = getHelper( + "%dyn.class.property.0", + "x", + [], + call(staticBag.name, call(readBag.name, ref("p.0"))), + ); + const setter = setHelper("%dyn.class.property.1", "y"); + // An exit that returns another value, or a test that is not negated, + // makes the bag differ from the receiver. + const otherExit = guarded( + "%dyn.class.other", + exit("classCapsule", { kind: "dynObjLit", type: DYN, loc } as IrExpr), + ); + const positive = guarded("%dyn.class.positive", exit("classCapsule", ref("v"), false)); + const otherGetter = getHelper("%dyn.class.property.2", "x", [], call(otherExit.name, ref("p.0"))); + const positiveGetter = getHelper( + "%dyn.class.property.3", + "x", + [], + call(positive.name, ref("p.0")), + ); + const sites = new JsIcSites({ + functions: [ + readBag, + writeBag, + staticBag, + otherExit, + positive, + getter, + setter, + otherGetter, + positiveGetter, + ], + }); + expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" }); + expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" }); + expect(sites.site("%dyn.class.property.2")).toBeNull(); + expect(sites.site("%dyn.class.property.3")).toBeNull(); +}); diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 316b3c958..752316db3 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -170,6 +170,18 @@ export class JsIcSites { ); } + /** An early exit for values a class table never matches: `if (!(value)) return `, with no else. A plain object takes it + * and gets itself back. */ + private kindExit(s: IrStmt, param: string): boolean { + if (s.kind !== "if" || s.else_ !== null || s.then.length !== 1) return false; + const { cond } = s; + if (cond.kind !== "dynTest" || cond.negated !== true || !isVar(cond.value, param)) return false; + if (cond.test !== "classCapsule" && cond.test !== "funcKind") return false; + const ret = s.then[0]!; + return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param); + } + /** A property-bag helper that returns its argument unless a capsule * branch is taken. */ private identityHelper(name: string): boolean { @@ -190,7 +202,9 @@ export class JsIcSites { const param = fn.params[0]!.localId; const last = fn.body[fn.body.length - 1]; identity = - fn.body.slice(0, -1).every((s) => this.capsuleBranch(s, param)) && + fn.body + .slice(0, -1) + .every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) && last?.kind === "return" && last.value !== null && this.bagOf(last.value, param); From 358d2df536fba442ce134015f5dfd2c510902b24 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 02:23:33 -0500 Subject: [PATCH 047/409] Keep package results that pass their argument through out of the declaration boundary A package function declared to return its own type parameter, which also types one of its parameters ((obj: T) => T), hands back the argument or something standing in for it, and the package may track that object's identity. Effect's Equal.byReference returns a proxy it keeps in a WeakSet. The declaration-typed boundary cast such a result to the argument's type and built a separate native record, so Equal.equals(a, b) on two by-reference objects compared them by value and printed true where Node prints false. The result's type comes from the caller's argument, not from data the package declares, so the projection no longer casts these calls. The package re-export coverage test now expects three dynamic statements: since the boundary, readRows's declared number result keeps its type, so count - 1 in 7004-package-declared-type-reexports is native arithmetic. --- .../compiler/src/frontend/decl-projection.ts | 82 +++++++++++++++++-- .../npm/cases/decl-pass-through/main.ts.json | 6 ++ .../npm/cases/decl-pass-through/main.ts | 16 ++++ .../node_modules/ref-marks/lib/index.d.ts | 8 ++ .../node_modules/ref-marks/lib/index.js | 18 ++++ .../node_modules/ref-marks/package.json | 1 + .../npm/cases/decl-pass-through/package.json | 1 + tests/harness/coverage.test.ts | 7 +- 8 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json create mode 100644 tests/fixtures/npm/cases/decl-pass-through/main.ts create mode 100644 tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts create mode 100644 tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js create mode 100644 tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json create mode 100644 tests/fixtures/npm/cases/decl-pass-through/package.json diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index 31d32834e..f2340e984 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -99,6 +99,70 @@ function declaredInPackage(symbol: ts.Symbol | undefined): boolean { return symbol.declarations.some((d) => isPackageDeclarationFile(ts.getSourceFileOfNode(d))); } +/** The parts of a call signature's declaration that pass-through detection + * reads, for the declaration kinds a resolved call signature has. */ +function signatureParts(node: ts.Node | undefined): { + typeParameters: readonly ts.TypeParameterDeclaration[]; + parameters: readonly ts.ParameterDeclaration[]; + type: ts.TypeNode | undefined; +} | null { + if (node === undefined) return null; + if (ts.isFunctionTypeNode(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isFunctionDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isMethodDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isMethodSignatureDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isCallSignatureDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + return null; +} + +/** A signature that returns one of its own type parameters which also types + * a parameter (`(obj: T) => T`) hands back the argument + * or something standing in for it, a proxy or a marked object, whose + * identity the package may track (Effect's `Equal.byReference`). Its result + * type comes from the caller's argument, not from data the package + * declares, so the boundary leaves it alone: converting it would build a + * separate native value. */ +function passesArgumentThrough(declaration: ts.Node | undefined): boolean { + const sig = signatureParts(declaration); + if (sig === null || sig.type === undefined || !ts.isTypeReferenceNode(sig.type)) return false; + const ret = sig.type; + if (!ts.isIdentifier(ret.typeName) || ret.typeArguments !== undefined) return false; + const name = ret.typeName.text; + if (!sig.typeParameters.some((tp) => tp.name.text === name)) return false; + return sig.parameters.some( + (param) => + param.type !== undefined && + ts.isTypeReferenceNode(param.type) && + ts.isIdentifier(param.type.typeName) && + param.type.typeName.text === name, + ); +} + function declaredInUserFile(symbol: ts.Symbol | undefined): boolean { if (symbol === undefined) return false; const decls = symbol.declarations; @@ -511,16 +575,16 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | if (ts.isCallExpression(d) || ts.isNewExpression(d)) { const callee = d.expression; const target = ts.isPropertyAccessExpression(callee) ? callee.name : callee; - produced = declaredInPackage(declSymbol(target)); - if (!produced) { - try { - const sig = declChecker.getResolvedSignature(d); - const decl = sig?.declaration; - produced = decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl)); - } catch { - produced = false; - } + let decl: ts.Node | undefined; + try { + decl = declChecker.getResolvedSignature(d)?.declaration; + } catch { + decl = undefined; } + produced = + (declaredInPackage(declSymbol(target)) || + (decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl)))) && + !passesArgumentThrough(decl); } else if (ts.isTaggedTemplateExpression(d)) { produced = declaredInPackage(declSymbol(d.tag)); } else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json new file mode 100644 index 000000000..1fd5083da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/decl-pass-through/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/decl-pass-through/main.ts b/tests/fixtures/npm/cases/decl-pass-through/main.ts new file mode 100644 index 000000000..a8168edc4 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/main.ts @@ -0,0 +1,16 @@ +// Package functions declared to return their argument's own type +// (`(obj: T) => T`) hand back the argument, or a proxy +// standing in for it, and the package tracks that object's identity: those +// results stay the package's objects. Reads of data the declarations +// describe (`point(): Point`) are still typed. +import { byReference, equals, mark, point } from "ref-marks"; + +const a = byReference({ n: 42 }); +const b = byReference({ n: 42 }); +console.log(equals(a, a), equals(a, b), a.n + b.n); +const m = mark({ n: 1 }); +const m2 = mark({ n: 1 }); +console.log(equals(m, m), equals(m, m2)); +const p = point(1, 2); +const q = point(1, 2); +console.log(equals(p, q), p.x + q.y); diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts new file mode 100644 index 000000000..aa0df42f4 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts @@ -0,0 +1,8 @@ +export interface Point { + x: number; + y: number; +} +export declare function byReference(obj: T): T; +export declare const mark: (obj: T) => T; +export declare function equals(a: unknown, b: unknown): boolean; +export declare function point(x: number, y: number): Point; diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js new file mode 100644 index 000000000..796efb19b --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js @@ -0,0 +1,18 @@ +const marked = new WeakSet(); +export function byReference(obj) { + const proxy = new Proxy(obj, {}); + marked.add(proxy); + return proxy; +} +export function mark(obj) { + marked.add(obj); + return obj; +} +export function equals(a, b) { + if (a === b) return true; + if (marked.has(a) || marked.has(b)) return false; + return JSON.stringify(a) === JSON.stringify(b); +} +export function point(x, y) { + return { x, y }; +} diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json new file mode 100644 index 000000000..5ae95f315 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json @@ -0,0 +1 @@ +{ "name": "ref-marks", "version": "1.0.0", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts" } diff --git a/tests/fixtures/npm/cases/decl-pass-through/package.json b/tests/fixtures/npm/cases/decl-pass-through/package.json new file mode 100644 index 000000000..4895e9aec --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/package.json @@ -0,0 +1 @@ +{ "name": "decl-pass-through-case", "private": true, "type": "module" } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 3ed562aea..e1831c609 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -561,13 +561,14 @@ test("types only a package's .d.ts declares stay static through re-exports and . // shape-kinds re-exports types its .d.ts entry imports; row-module's // runtime entry is an .mjs file. Without their declared types the // program's uses of Form, Kind and Row run in the dynamic tier (12 - // statements); with them only the packages' own JavaScript and one - // arithmetic on a JavaScript return value do (4). + // statements); with them only the packages' own JavaScript does (3). + // readRows's result keeps its declared number type, so `count - 1` is + // native arithmetic too. const { coverage } = analyze( join(repoRoot, "tests/corpus/7004-package-declared-type-reexports/main.ts"), ); expect(coverage.diagnostics).toEqual([]); - expect(coverage.stats.statementsDynamic).toBe(4); + expect(coverage.stats.statementsDynamic).toBe(3); }); test("any-typed checked-dynamic operators compile natively", () => { From a00d56e5181bffee04a66ab99246807f680b66f5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 02:26:57 -0500 Subject: [PATCH 048/409] Fix the int32 array CI failures --- packages/compiler/src/ir/int32-arrays.ts | 57 ++++++++++++++++-------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts index 0872fcddb..e67f51956 100644 --- a/packages/compiler/src/ir/int32-arrays.ts +++ b/packages/compiler/src/ir/int32-arrays.ts @@ -7,6 +7,8 @@ import { everyExprChild, everyStmtChild, everyStmtList, + mapExprChildren, + mapStmtChildren, transformExpr, transformStmt, transformStmtList, @@ -629,16 +631,34 @@ export function int32ArrayStores( return stores; } +const REPEATED_INDEX_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]); + +/** An index the inlined read may evaluate at each of its uses: a plain read, + * a literal, or integer arithmetic over them. Repeating it keeps the forms + * the backend derives from its operands at every use, such as the exact i64 + * of an int32 product, where a bound local holds only the double. */ +function repeatableIndex(e: IrExpr): boolean { + if (e.kind === "varRef" || e.kind === "numLit") return true; + if (e.kind === "unary") return e.op === "-" && repeatableIndex(e.operand); + return ( + e.kind === "bin" && + REPEATED_INDEX_OPS.has(e.op) && + repeatableIndex(e.left) && + repeatableIndex(e.right) + ); +} + /** Rewrite a function's candidates for the backend. A discarded pop uses * the candidate in place of its hidden local. An element-reader helper call * whose array is a candidate is inlined: the helper's returned ternary, with * the candidate for its array and the index for its index, so the read means - * the same. An index that is not a plain read or literal is bound to a fresh - * local first: in its own declaration just before the statement when the - * call is the whole value of a declaration or assignment (the form lowering - * gives `a[i]!`, and the one the backend reads without a box), otherwise in a - * sequence around the ternary. Selection keeps out indexes that write the - * array, so evaluating the index before the array read is unobservable. */ + * the same. An index that is not repeatable (repeatableIndex) is bound to a + * fresh local first: in its own declaration just before the statement when + * the call is the whole value of a declaration or assignment (the form + * lowering gives `a[i]!`, and the one the backend reads without a box), + * otherwise in a sequence around the ternary. Selection keeps out indexes + * that write the array, so evaluating the index before the array read is + * unobservable. */ function rewriteCandidates( fn: IrFunction, candidates: ReadonlySet, @@ -662,14 +682,13 @@ function rewriteCandidates( candidates.has(e.args[0]!.localId) ? e : null; - const simple = (e: IrExpr): boolean => e.kind === "varRef" || e.kind === "numLit"; // The statements of a list, with complex read indexes declared first. const hoist = (list: IrStmt[]): IrStmt[] => list.flatMap((s): IrStmt[] => { if (s.kind !== "varDecl" && s.kind !== "assign") return [s]; const value = s.kind === "varDecl" ? s.init : s.value; const call = readerCall(value); - if (!call || simple(call.args[1]!)) return [s]; + if (!call || repeatableIndex(call.args[1]!)) return [s]; const { decl, ref } = bind(call.args[1]!); const read: IrExpr = { ...call, args: [call.args[0]!, ref] }; if (s.kind === "varDecl") return [decl, { ...s, init: read }]; @@ -732,21 +751,23 @@ function rewriteCandidates( if (ret.kind !== "return" || !ret.value) return e; let index = call.args[1]!; const stmts: IrStmt[] = []; - if (!simple(index)) { + if (!repeatableIndex(index)) { const { decl, ref } = bind(index); stmts.push(decl); index = ref; } const operand = index; - const body = transformExpr(reparseExpr(ret.value), { - expr: (x) => { - if (x.kind !== "varRef") return x; - if (x.localId === arrayParam!.localId) return reparseExpr(array); - if (x.localId === indexParam!.localId) return reparseExpr(operand); - return x; - }, - stmt: (x) => x, - }); + // The arguments' copies are not searched again: an index may read a + // local that has the id of one of the helper's parameters. + const substituteExpr = (x: IrExpr): IrExpr => { + if (x.kind !== "varRef") return mapExprChildren(x, substituteExpr, substituteStmt); + if (x.localId === arrayParam!.localId) return reparseExpr(array); + if (x.localId === indexParam!.localId) return reparseExpr(operand); + return x; + }; + const substituteStmt = (x: IrStmt): IrStmt => + mapStmtChildren(x, substituteExpr, substituteStmt); + const body = substituteExpr(reparseExpr(ret.value)); // The ternary is a fresh copy (reparseExpr): it takes the call's location. const typed = body; typed.loc = e.loc; From 342cf3a063301e46a90e428a848dfd825aff80d8 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 00:29:22 -0700 Subject: [PATCH 049/409] Copy class-view property bags entry by entry Materializing the checked view of a JavaScript class instance copies its property bag through scr_dyn_copy_property_descriptors, which built a key string and a descriptor object per property and defined each through scr_dyn_define_property (about 2k instructions per property). Plain objects on both sides now copy each entry directly with the attributes, value or accessor pair and own-key order the definition gives; properties the target already has, non-extensible targets and other receivers keep the general path. Co-Authored-By: Claude Opus 5.5 --- .../7620-js-class-view-descriptors.js.json | 6 ++ packages/runtime/src/scr_json.c | 73 +++++++++++++-- packages/runtime/test/test_json.c | 86 ++++++++++++++++++ .../corpus/7620-js-class-view-descriptors.js | 91 +++++++++++++++++++ .../7620-js-class-view-descriptors.js.json | 1 + 5 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7620-js-class-view-descriptors.js.json create mode 100644 tests/corpus/7620-js-class-view-descriptors.js create mode 100644 tests/harness/corpus-costs/7620-js-class-view-descriptors.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7620-js-class-view-descriptors.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7620-js-class-view-descriptors.js.json new file mode 100644 index 000000000..2cb1ece22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7620-js-class-view-descriptors.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7620-js-class-view-descriptors.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 907486fea..3676dd059 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -13699,19 +13699,74 @@ ScrDyn *scr_dyn_object_rest(const ScrDyn *source, const ScrDyn *excluded) { /* Compiler-owned native class views retain every property and its flags, * including nonenumerable data, without invoking accessors. */ -ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) { - if (!src || src->kind == SCR_DYN_UNDEF) return scr_dyn_retain(target); - ScrDyn *keys = scr_dyn_obj_own_keys(src); - if (!keys) return NULL; - for (size_t i = 0; i < keys->v.arr.len; i++) { - ScrDyn *key = keys->v.arr.items[i]; - ScrDyn *descriptor = scr_dyn_own_descriptor(src, key->v.str); +static void scr_dyn_copy_property_descriptor(ScrDyn *target, const ScrDyn *src, + const ScrDynEntry *from) { + /* Exactly scr_dyn_define_property(target, key, own descriptor of key) + * when `target` has no such property and accepts new ones: the entry + * takes the source's attributes, value or accessor pair. */ + ScrDynEntry *prior = scr_dyn_find_entry(target, from->key, from->key_len); + if (prior || target->non_extensible) { + ScrStr *name = scr_str_new(from->key, from->key_len); + ScrDyn *key = scr_dyn_new_str(name); + ScrDyn *descriptor = scr_dyn_own_descriptor(src, name); ScrDyn *result = descriptor ? scr_dyn_define_property(target, key, descriptor) : NULL; scr_dyn_release(descriptor); scr_dyn_release(result); - if (scr_exc_pending()) break; + scr_dyn_release(key); + scr_str_release(name); + return; + } + /* The names whose definition may move into a native Error's slots + * (scr_error_sync_slots). */ + static const char *const slots[] = {"cause", "stack", "name", "message", "code"}; + bool slot = false; + for (size_t i = 0; i < sizeof slots / sizeof *slots && !slot; i++) + slot = from->key_len == strlen(slots[i]) && memcmp(from->key, slots[i], from->key_len) == 0; + bool accessor = from->accessor, writable = !accessor && from->writable; + bool enumerable = from->enumerable, configurable = from->configurable; + ScrDyn *value = scr_dyn_retain(accessor ? scr_dyn_undefined() : from->value); + ScrDyn *getter = accessor && from->getter ? scr_dyn_retain(from->getter) : NULL; + ScrDyn *setter = accessor && from->setter ? scr_dyn_retain(from->setter) : NULL; + ScrStr *name = slot ? scr_str_new(from->key, from->key_len) : NULL; + scr_dyn_obj_set(target, from->key, from->key_len, value); + ScrDynEntry *entry = &target->v.obj.entries[target->v.obj.len - 1]; + entry->accessor = accessor; + entry->getter = getter; + entry->setter = setter; + entry->writable = writable; + entry->enumerable = enumerable; + entry->configurable = configurable; + if (name) { + scr_error_sync_cause(target, name); + scr_str_release(name); + } +} + +ScrDyn *scr_dyn_copy_property_descriptors(ScrDyn *target, const ScrDyn *src) { + if (!src || src->kind == SCR_DYN_UNDEF) return scr_dyn_retain(target); + if (src->kind == SCR_DYN_OBJ && target->kind == SCR_DYN_OBJ && src != target && + src != scr_builtin_object_prototype && target != scr_builtin_error_prototype) { + /* Plain objects on both sides: no fence, proxy trap or accessor runs, + * so each property is copied from its entry without building the key + * and descriptor objects the general path goes through. */ + if (src->v.obj.len) scr_dyn_region_store(target, NULL); + size_t *order = scr_dyn_obj_key_order(src); + for (size_t i = 0; i < src->v.obj.len && !scr_exc_pending(); i++) + scr_dyn_copy_property_descriptor(target, src, &src->v.obj.entries[order ? order[i] : i]); + free(order); + } else { + ScrDyn *keys = scr_dyn_obj_own_keys(src); + if (!keys) return NULL; + for (size_t i = 0; i < keys->v.arr.len; i++) { + ScrDyn *key = keys->v.arr.items[i]; + ScrDyn *descriptor = scr_dyn_own_descriptor(src, key->v.str); + ScrDyn *result = descriptor ? scr_dyn_define_property(target, key, descriptor) : NULL; + scr_dyn_release(descriptor); + scr_dyn_release(result); + if (scr_exc_pending()) break; + } + scr_dyn_release(keys); } - scr_dyn_release(keys); if (!scr_exc_pending()) { ScrDyn *symbols = scr_dyn_get_own_property_symbols(src); if (!symbols) return NULL; diff --git a/packages/runtime/test/test_json.c b/packages/runtime/test/test_json.c index 2c072006f..865f1c757 100644 --- a/packages/runtime/test/test_json.c +++ b/packages/runtime/test/test_json.c @@ -551,6 +551,91 @@ static void test_class_values(void) { scr_str_release(name); } +/* The class-view copy (scr_dyn_copy_property_descriptors) gives each + * property of the bag the attributes, value or accessors, and own-key + * position that defining it from its own descriptor would. */ +static ScrDyn *define_from(ScrDyn *target, const char *key, ScrDyn *value, bool writable, + bool enumerable, bool configurable) { + ScrDyn *descriptor = scr_dyn_new_obj(); + scr_dyn_obj_set(descriptor, "value", 5, value); + scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(writable)); + scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(enumerable)); + scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(configurable)); + ScrStr *name = S(key); + ScrDyn *k = scr_dyn_new_str(name); + ScrDyn *result = scr_dyn_define_property(target, k, descriptor); + scr_dyn_release(k); + scr_str_release(name); + scr_dyn_release(descriptor); + return result; +} + +static const ScrDynEntry *entry_of(const ScrDyn *obj, const char *key) { + for (size_t i = 0; i < obj->v.obj.len; i++) + if (obj->v.obj.entries[i].key_len == strlen(key) && + memcmp(obj->v.obj.entries[i].key, key, strlen(key)) == 0) + return &obj->v.obj.entries[i]; + return NULL; +} + +static void test_copy_property_descriptors(void) { + ScrDyn *bag = scr_dyn_new_obj(); + ScrDyn *getter = scr_dyn_new_obj(); + scr_dyn_release(define_from(bag, "label", scr_dyn_new_num(1), true, true, true)); + scr_dyn_release(define_from(bag, "hidden", scr_dyn_new_num(2), false, false, true)); + scr_dyn_obj_set(bag, "7", 1, scr_dyn_new_num(7)); + scr_dyn_obj_set(bag, "3", 1, scr_dyn_new_num(3)); + /* An accessor entry (the getter object stands in for its function). */ + scr_dyn_obj_set(bag, "computed", 8, scr_dyn_retain(scr_dyn_undefined())); + ScrDynEntry *accessor = (ScrDynEntry *)entry_of(bag, "computed"); + accessor->accessor = true; + accessor->getter = scr_dyn_retain(getter); + accessor->writable = false; + accessor->enumerable = false; + accessor->configurable = false; + + ScrDyn *view = scr_dyn_new_obj(); + scr_dyn_release(scr_dyn_copy_property_descriptors(view, bag)); + check(!scr_exc_pending() && view->v.obj.len == 5, "copy keeps every bag property"); + ScrDyn *keys = scr_dyn_obj_own_keys(view); + const char *order[] = {"3", "7", "label", "hidden", "computed"}; + bool ordered = keys->v.arr.len == 5; + for (size_t i = 0; ordered && i < 5; i++) ordered = str_is(keys->v.arr.items[i]->v.str, order[i]); + check(ordered, "copy keeps the bag's own-key order"); + scr_dyn_release(keys); + const ScrDynEntry *hidden = entry_of(view, "hidden"); + check(hidden && !hidden->accessor && hidden->value->v.num == 2 && !hidden->writable && + !hidden->enumerable && hidden->configurable, + "copy keeps data attributes"); + const ScrDynEntry *copied = entry_of(view, "computed"); + check(copied && copied->accessor && copied->getter == getter && !copied->setter && + !copied->writable && !copied->enumerable && !copied->configurable && + copied->value->kind == SCR_DYN_UNDEF, + "copy keeps an accessor without calling it"); + + /* A property the target already has is redefined like defineProperty. */ + ScrDyn *existing = scr_dyn_new_obj(); + scr_dyn_obj_set(existing, "label", 5, scr_dyn_new_num(9)); + scr_dyn_release(scr_dyn_copy_property_descriptors(existing, bag)); + const ScrDynEntry *label = entry_of(existing, "label"); + check(!scr_exc_pending() && existing->v.obj.len == 5 && existing->v.obj.entries[0].key_len == 5 && + label->value->v.num == 1, + "copy redefines an existing property in place"); + + /* A target that takes no new properties throws as defineProperty does. */ + ScrDyn *closed = scr_dyn_new_obj(); + scr_dyn_release(scr_dyn_prevent_extensions(closed)); + ScrDyn *refused = scr_dyn_copy_property_descriptors(closed, bag); + check(!refused && scr_exc_pending() && closed->v.obj.len == 0, "copy onto a non-extensible object throws"); + scr_exc_clear(); + + scr_dyn_release(closed); + scr_dyn_release(existing); + scr_dyn_release(view); + scr_dyn_release(bag); + scr_dyn_release(getter); +} + static void test_primitive_owners(void) { ScrDyn *values[] = {scr_dyn_new_null(), scr_dyn_new_bool(false), scr_dyn_new_bool(true)}; const char *texts[] = {"null", "false", "true"}; @@ -579,6 +664,7 @@ int main(void) { test_class_values(); scr_init(); test_primitive_owners(); + test_copy_property_descriptors(); /* ── primitives ─────────────────────────────────────────────────── */ ScrDyn *d = parse_ok("null", "parse null"); diff --git a/tests/corpus/7620-js-class-view-descriptors.js b/tests/corpus/7620-js-class-view-descriptors.js new file mode 100644 index 000000000..a0dc42813 --- /dev/null +++ b/tests/corpus/7620-js-class-view-descriptors.js @@ -0,0 +1,91 @@ +// An instance of a JS class with public fields keeps its own properties in +// a property bag, and untyped code that calls a method on it sees a view +// built from that bag: data properties with their attributes, accessors, +// index-like keys (which come first, in ascending order), symbols, and +// properties added or removed after construction. Every refresh of the view +// must copy the bag exactly. +'use strict'; + +class Box { + label = 'box'; + count = 0; + constructor(n) { + this.count = n; + } + describe() { + return `${this.label}:${this.count}`; + } + bump() { + this.count++; + return this; + } +} + +function call(target, method, ...args) { + return target[method](...args); +} + +function put(target, key, value) { + target[key] = value; +} + +function drop(target, key) { + delete target[key]; +} + +function show(title, target) { + const lines = []; + for (const key of Reflect.ownKeys(target)) { + const d = Object.getOwnPropertyDescriptor(target, key); + const kind = 'value' in d ? `value=${String(d.value)} writable=${d.writable}` : `get=${typeof d.get} set=${typeof d.set}`; + lines.push(` ${String(key)}: ${kind} enumerable=${d.enumerable} configurable=${d.configurable}`); + } + console.log(`${title}\n${lines.join('\n')}`); +} + +const box = new Box(2); +Object.defineProperty(box, 'hidden', { value: 42, enumerable: false, writable: false, configurable: true }); +Object.defineProperty(box, 'computed', { + get() { + return this.count * 10; + }, + set(v) { + this.count = v / 10; + }, + enumerable: true, + configurable: true, +}); +Object.defineProperty(box, 'getterOnly', { get: () => 'g', enumerable: false, configurable: false }); +put(box, 2, 'two'); +put(box, 1, 'one'); +put(box, 'extra', 'x'); +const tag = Symbol('tag'); +put(box, tag, 'symbol'); + +console.log(call(box, 'describe')); +show('after construction', box); +console.log('keys', Object.keys(box).join(',')); + +call(box, 'bump'); +put(box, 'computed', 70); +drop(box, 'extra'); +put(box, 'later', true); +console.log(call(box, 'describe')); +show('after changes', box); +console.log('computed', box.computed, 'hidden', box.hidden, 'getterOnly', box.getterOnly); + +// Many views in a row see the same, current properties. +let total = 0; +for (let i = 0; i < 5; i++) { + call(box, 'bump'); + total += Object.keys(box).length; +} +console.log('total keys', total, call(box, 'describe')); + +const other = new Box(7); +console.log(call(other, 'describe'), Object.keys(other).join(',')); +try { + call(box, 'missing'); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} diff --git a/tests/harness/corpus-costs/7620-js-class-view-descriptors.js.json b/tests/harness/corpus-costs/7620-js-class-view-descriptors.js.json new file mode 100644 index 000000000..ef439d096 --- /dev/null +++ b/tests/harness/corpus-costs/7620-js-class-view-descriptors.js.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4500 } From 1edb2fe92d954ec6ef5fac0f4eb3d7163fc5a253 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 00:29:27 -0700 Subject: [PATCH 050/409] Keep fiber stacks across short event-loop sleeps Finished fiber stacks went back to a spare pool that the event loop unmapped whenever it was about to sleep. A server's loop sleeps between nearly every pair of requests, so each request that awaited mapped, faulted in and unmapped a fresh 512 KiB stack: on a Hono server mmap, munmap and the page faults were about a fifth of its CPU time. Spares now outlive short sleeps. At most once a second a sleep unmaps the spares that no fiber took since the previous trim, and a sleep longer than two seconds unmaps them all, so an idle program still returns the memory. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_async.c | 45 ++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 9e3365fbe..6afd174b7 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -72,15 +72,26 @@ void __lsan_unregister_root_region(const void *p, size_t size) __attribute__((we #if !defined(_WIN32) && !defined(__wasi__) -/* Keep stacks out of malloc's size classes. Reuse a bounded number during - * a burst of promise/generator work, then unmap the spares before the loop - * sleeps. This avoids both heap fragmentation and per-call mmap overhead. - * The sanitizer lane unmaps every finished stack immediately. */ +/* Keep stacks out of malloc's size classes. Reuse a bounded number of + * finished stacks, which avoids both heap fragmentation and per-call mmap + * overhead. A server's loop sleeps between nearly every pair of requests, + * so unmapping the spares at each sleep would map, fault in and unmap a + * stack for every request that awaits. Spares therefore outlive short + * sleeps: at most once per SCR_FIBER_TRIM_MS, a sleep unmaps the spares + * that no fiber took since the previous trim (the pool's low-water mark), + * and a sleep longer than twice that unmaps every spare, so an idle + * program gives the memory back as before. The sanitizer lane unmaps + * every finished stack immediately. */ #ifndef SCR_ASAN_FIBERS #define SCR_FIBER_SPARES 64 +#define SCR_FIBER_TRIM_MS 1000.0 static SCR_TL void *scr_fiber_spares[SCR_FIBER_SPARES]; static SCR_TL size_t scr_fiber_nspares; static SCR_TL bool scr_fiber_cleanup_registered; +/* The fewest spares the pool held since the last trim: the spares below + * this index went unused for the whole window. */ +static SCR_TL size_t scr_fiber_spares_low; +static SCR_TL double scr_fiber_trimmed_at; #endif static void scr_fiber_stacks_clear(void) { @@ -89,6 +100,29 @@ static void scr_fiber_stacks_clear(void) { (void)munmap(scr_fiber_spares[--scr_fiber_nspares], SCR_FIBER_STACK); scr_fiber_spares[scr_fiber_nspares] = NULL; } + scr_fiber_spares_low = 0; +#endif +} + +/* The loop is about to sleep for `wait` ms (see above). */ +static void scr_fiber_stacks_trim(double now, double wait) { +#ifndef SCR_ASAN_FIBERS + if (wait > 2 * SCR_FIBER_TRIM_MS) { + scr_fiber_stacks_clear(); + scr_fiber_trimmed_at = now; + return; + } + if (now - scr_fiber_trimmed_at < SCR_FIBER_TRIM_MS) return; + size_t unused = scr_fiber_spares_low; + for (size_t i = 0; i < unused; i++) (void)munmap(scr_fiber_spares[i], SCR_FIBER_STACK); + scr_fiber_nspares -= unused; + memmove(scr_fiber_spares, scr_fiber_spares + unused, scr_fiber_nspares * sizeof *scr_fiber_spares); + memset(scr_fiber_spares + scr_fiber_nspares, 0, unused * sizeof *scr_fiber_spares); + scr_fiber_spares_low = scr_fiber_nspares; + scr_fiber_trimmed_at = now; +#else + (void)now; + (void)wait; #endif } @@ -97,6 +131,7 @@ static void *scr_fiber_stack_new(void) { if (scr_fiber_nspares > 0) { void *stack = scr_fiber_spares[--scr_fiber_nspares]; scr_fiber_spares[scr_fiber_nspares] = NULL; + if (scr_fiber_nspares < scr_fiber_spares_low) scr_fiber_spares_low = scr_fiber_nspares; return stack; } if (!scr_fiber_cleanup_registered) { @@ -4599,7 +4634,7 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { /* An idle moment: a full pass the death rule deferred runs now. */ if (due > now && scr_cyc_collect_idle()) now = scr_now_ms(); #if !defined(_WIN32) && !defined(__wasi__) - if (due > now) scr_fiber_stacks_clear(); + if (due > now) scr_fiber_stacks_trim(now, due - now); #endif #ifdef SCR_WORKERS if (due > now) scr_sa_loop_waiting(true); From bcd506fc8b14c50ae7eb8ba96764d7d42091552e Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 00:29:28 -0700 Subject: [PATCH 051/409] Return ASCII header values as their own ByteString Converting a header value to a ByteString read it one UTF-16 code unit at a time through scr_str_char_code_at into a fresh buffer. An all-ASCII value, which nearly every header is, is already its own ByteString, so it is now returned as is. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_fetch.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 96e90e47c..549806f39 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -3287,6 +3287,10 @@ static bool sf_header_value_ok(const ScrStr *value) { } static ScrStr *sf_header_value_bytestring(ScrStr *value) { + /* An ASCII value is its own ByteString: each byte is one UTF-16 unit. */ + size_t ascii = 0; + while (ascii < value->len && (unsigned char)value->data[ascii] < 0x80) ascii++; + if (ascii == value->len) return scr_str_retain(value); size_t len = (size_t)scr_str_utf16_len(value); char *bytes = malloc(len ? len : 1); if (!bytes) sf_oom(); From b8d442e3ae9a2a14443e22f2226caef8492fd899 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:40:26 -0700 Subject: [PATCH 052/409] Run match and search with untyped patterns s.match(p) and s.search(p) with an untyped pattern now choose at run time, as Node does: a RegExp is used as is (with its lastIndex state), any other value builds new RegExp(value). The checked-dynamic receiver form previously threw 'method calls like path.match on a string are not supported yet' (hono's RegExpRouter matcher). Co-Authored-By: Claude Opus 5.5 --- .../lowering/containers/string-and-regexp.ts | 59 +++++++++++++++++-- .../corpus/7732-untyped-match-pattern.js.json | 6 ++ tests/corpus/7732-untyped-match-pattern.js | 15 +++++ .../7732-untyped-match-pattern.js.json | 1 + 4 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json create mode 100644 tests/corpus/7732-untyped-match-pattern.js create mode 100644 tests/harness/corpus-costs/7732-untyped-match-pattern.js.json diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 1947f0d75..d55f71ff0 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -16,6 +16,7 @@ import { type IrExpr, type IrStmt, type IrType, + REGEX, STRING, type SrcLoc, UNDEFINED_T, @@ -920,12 +921,15 @@ export function lowerRegexMethodCall( })(); if ((receiverKind === "string" || nullableStringRecv) && name === "match") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence - if (call.arguments.nodes.length !== 1) return null; + if (!arg0 || call.arguments.nodes.length !== 1) return null; + const checkedPattern = isCheckedPatternArgument(lowerer, arg0); + if (!checkedPattern && !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence const receiver = nullableStringRecv ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text) : lowerReceiver(); - const re = lowerRegexPatternArgument(lowerer, arg0); + const re = checkedPattern + ? lowerCheckedPatternArgument(lowerer, arg0) + : lowerRegexPatternArgument(lowerer, arg0); // RegExpMatchArray | null maps to the string[] | null union by // itself; intern it directly when the checker's spelling doesn't — // or when it maps to something WIDER (an optional-chain call node @@ -983,10 +987,13 @@ export function lowerRegexMethodCall( // 0, exactly Node. Never throws. if (receiverKind === "string" && name === "search") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence - if (call.arguments.nodes.length !== 1) return null; + if (!arg0 || call.arguments.nodes.length !== 1) return null; + const checkedPattern = isCheckedPatternArgument(lowerer, arg0); + if (!checkedPattern && !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence const receiver = lowerReceiver(); - const re = lowerRegexPatternArgument(lowerer, arg0); + const re = checkedPattern + ? lowerCheckedPatternArgument(lowerer, arg0) + : lowerRegexPatternArgument(lowerer, arg0); return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc }; } if (receiverKind === "string" && (name === "replace" || name === "replaceAll")) { @@ -1936,6 +1943,46 @@ function isRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean ); } +/** True when a `match` or `search` pattern is checker-untyped (`any` or + * `unknown`), so its runtime value decides the protocol. */ +function isCheckedPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean { + if (ts.isSpreadElement(node)) return false; + return (lowerer.typeOf(node).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; +} + +/** An untyped `match` or `search` pattern as a RegExp, chosen at run time as + * Node does: a RegExp is used as is (its lastIndex state included), and any + * other value builds `new RegExp(value)` (undefined is the empty pattern, + * other values convert with ToString). */ +function lowerCheckedPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { + const loc = locOf(node); + const value = lowerer.lowerExpr(node); + if (value.type.kind === "regex") return value; + const checked = lowerer.coerceInto(node, value, DYN); + const local = lowerer.declareHiddenLocal("%matchPattern", DYN); + const ref = varRef(local.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: checked, loc }], + result: { + kind: "ternary", + cond: { kind: "libCall", fn: "dyn.nativeRegexIs", args: [ref], type: BOOL, loc }, + then: lowerer.coerceInto(node, ref, REGEX), + else_: { + kind: "libCall", + fn: "regex.newChecked", + args: [ref, dynUndefinedExpr(loc)], + type: REGEX, + loc, + }, + type: REGEX, + loc, + }, + type: REGEX, + loc, + }; +} + /** The pattern of `match` or `search` as a RegExp: an undefined pattern is * `new RegExp(undefined)`, the empty pattern, as in Node. */ function lowerRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json new file mode 100644 index 000000000..f6355a2ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7732-untyped-match-pattern.js" + ], + "diags": [] +} diff --git a/tests/corpus/7732-untyped-match-pattern.js b/tests/corpus/7732-untyped-match-pattern.js new file mode 100644 index 000000000..2079bb8ef --- /dev/null +++ b/tests/corpus/7732-untyped-match-pattern.js @@ -0,0 +1,15 @@ +// String.prototype.match and search with an untyped pattern: a RegExp is used as is, +// any other value builds new RegExp(value), as hono's matcher does. +const matchers = { GET: [/^\/a\/([^/]+)$()|^\/b$()/, [0, 1], {}] }; +function match(method, path) { + const matcher = matchers[method] || matchers["ALL"]; + const m = path.match(matcher[0]); + if (!m) return null; + return [m.indexOf("", 1), m.length, m[1], m.index]; +} +console.log(JSON.stringify(match("GET", "/a/xy"))); +console.log(JSON.stringify(match("GET", "/b"))); +console.log(JSON.stringify(match("GET", "/c"))); +const g = { re: /o/g, s: "o.", n: null }; +function m2(s, k) { return s.match(g[k]); } +console.log(JSON.stringify(m2("foo boo", "re")), JSON.stringify(m2("go.o", "s")), JSON.stringify(m2("null", "n"))); diff --git a/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json b/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json new file mode 100644 index 000000000..2de3f6b94 --- /dev/null +++ b/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json @@ -0,0 +1 @@ +{ "plain": 3600, "san": 3300 } From be6e63735a3a2fe81616ec80b331fc7d62d167c5 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:40:25 -0700 Subject: [PATCH 053/409] Index strings through checker-any receivers An implicit-any package function specialized for a string argument lowered base?.[0] to the element-access refusal: the checker types the receiver any, so only the lowered string reached the string-index path. Hono's mergePath failed at startup. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/expressions/element-access.ts | 10 ++++++++-- .../corpus/7731-npm-any-string-index/main.ts.json | 6 ++++++ tests/corpus/7731-npm-any-string-index/main.ts | 9 +++++++++ .../node_modules/merge-path/index.d.ts | 2 ++ .../node_modules/merge-path/index.js | 10 ++++++++++ .../node_modules/merge-path/package.json | 1 + .../node_modules/merge-path/url.js | 10 ++++++++++ .../7731-npm-any-string-index/main.ts.json | 1 + 8 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json create mode 100644 tests/corpus/7731-npm-any-string-index/main.ts create mode 100644 tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts create mode 100644 tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js create mode 100644 tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json create mode 100644 tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js create mode 100644 tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 0be9be81a..0ea94ec7a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -566,8 +566,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } if (receiverIr?.kind !== "array") { + const receiver = tryLowerExpression(lowerer, expr.expression); { - const receiver = tryLowerExpression(lowerer, expr.expression); if ( receiver?.type.kind === "dyn" || (receiver && @@ -618,7 +618,13 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } } - if (receiverIr?.kind === "string") { + // A checker-`any` receiver that lowers to a string (an implicit-any + // package parameter specialized for a string argument) indexes the + // string the same way. + if ( + receiverIr?.kind === "string" || + (receiverIr === null && receiver?.type.kind === "string") + ) { // An indexed string property is absent for non-integer and missing // indexes. charAt has different coercion and out-of-range semantics. const recv = lowerer.lowerExpr(expr.expression); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json new file mode 100644 index 000000000..78fc40580 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7731-npm-any-string-index/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7731-npm-any-string-index/main.ts b/tests/corpus/7731-npm-any-string-index/main.ts new file mode 100644 index 000000000..21e7d1c10 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/main.ts @@ -0,0 +1,9 @@ +// An implicit-any package function specialized for a string argument indexes +// the string (`base?.[0]`), as hono's mergePath does. +import { App, mergePath } from "merge-path"; +console.log(new App().request("x")); +console.log(new App().request(new URL("http://h/p"))); +console.log(new App().basePath("/api").basePath("v1/")._basePath); +console.log(mergePath("/api/", "/"), mergePath("a", "b", "/c")); +console.log(new App().basePath("").basePath("").request("")); +console.log(mergePath("", "x"), mergePath("/", "/"), mergePath("//", "y/")); diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts new file mode 100644 index 000000000..aa5142307 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts @@ -0,0 +1,2 @@ +export declare const mergePath: (...paths: string[]) => string; +export declare class App { _basePath: string; request: (input: string | URL) => string; basePath(path: string): App; } diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js new file mode 100644 index 000000000..6a5c7c86b --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js @@ -0,0 +1,10 @@ +import { mergePath } from "./url.js"; +class App { + _basePath = "/"; + request = (input) => { + input = input.toString(); + return mergePath("/", input); + }; + basePath(path) { const s = new App(); s._basePath = mergePath(this._basePath, path); return s; } +} +export { App, mergePath }; diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json new file mode 100644 index 000000000..ddc3db4ef --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json @@ -0,0 +1 @@ +{ "name": "merge-path", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js new file mode 100644 index 000000000..e27469714 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js @@ -0,0 +1,10 @@ +/** +* Merge paths. +* @param {string[]} ...paths - The paths to merge. +* @returns {string} The merged path. +*/ +const mergePath = (base, sub, ...rest) => { + if (rest.length) sub = mergePath(sub, ...rest); + return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.[base.length - 1] === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`; +}; +export { mergePath }; diff --git a/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json b/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json new file mode 100644 index 000000000..9fe9ad101 --- /dev/null +++ b/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json @@ -0,0 +1 @@ +{ "plain": 6200, "san": 5800 } From fdcee191da193bbaed9f78d36aad1b70d29bb96a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:40:27 -0700 Subject: [PATCH 054/409] Give rebuilt property-bag stores their prototype Worklist passes that transform every function (the class-construction dispatch rewriting dyn.classBasePrototype) rebuild statement objects, so a recorded lazy bag store could be a detached copy. When the class gained prototype data later, only the copy was upgraded and the live store kept creating a bare {} bag: inherited methods read as undefined through the property bag (hono's SmartRouter.match once zod was in the program). Upgrades now also reach the stores as they stand in the generated bodies. Co-Authored-By: Claude Opus 5.5 --- .../lowering/class-dynamic-dispatch.ts | 28 +++++++++- .../7733-property-bag-late-prototype.js.json | 6 +++ .../7733-property-bag-late-prototype.js | 52 +++++++++++++++++++ .../7733-property-bag-late-prototype.js.json | 1 + 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json create mode 100644 tests/corpus/7733-property-bag-late-prototype.js create mode 100644 tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index e394718d1..6548d33d2 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1229,6 +1229,7 @@ export class ClassDynamicDispatch { } } } + let live: Map[]> | null = null; for (const plan of this.propertyReceivers(lowerer, false, false)) { const info = plan.info; const className = info.def.name; @@ -1309,7 +1310,11 @@ export class ClassDynamicDispatch { ); const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null => this.bagPrototype(lowerer, current, receiver, loc); - for (const existingInit of this.bagInitializers.get(className) ?? []) { + live ??= this.liveBagInitializers(); + for (const existingInit of [ + ...(this.bagInitializers.get(className) ?? []), + ...(live.get(className) ?? []), + ]) { if (existingInit.value.kind === "dynObjLit" && needsPrototype(info)) { const prototype = prototypeFor(info, existingInit.obj, existingInit.loc); if (prototype) { @@ -4440,6 +4445,27 @@ export class ClassDynamicDispatch { * A class with constructible subclasses that have prototype data selects * it through its `%class.bagPrototype` helper; any other class reads its * own prototype data. */ + /** The lazy bag stores as they stand in the generated bodies, by class. + * Worklist passes that transform every function rebuild statement + * objects, so a recorded initializer can be a detached copy; a store that + * gains its prototype must be updated where it runs. */ + private liveBagInitializers(): Map[]> { + const live = new Map[]>(); + const add = (stmt: IrStmt | undefined): void => { + if (stmt?.kind !== "if") return; + const store = stmt.then[0]; + if (store?.kind !== "fieldSet" || store.field !== PROPERTY_BAG) return; + const list = live.get(store.className); + if (list) list.push(store); + else live.set(store.className, [store]); + }; + // The complete dispatch guards each class's lazy store by its receiver. + for (const stmt of this.propertyBag?.body ?? []) if (stmt.kind === "if") add(stmt.then[0]); + for (const helper of this.typedPropertyBags.values()) add(helper.body[0]); + for (const helper of this.exactPropertyBags.values()) add(helper.body[0]); + return live; + } + private bagPrototype( lowerer: Lowerer, current: ClassInfo, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json new file mode 100644 index 000000000..2b9778666 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7733-property-bag-late-prototype.js" + ], + "diags": [] +} diff --git a/tests/corpus/7733-property-bag-late-prototype.js b/tests/corpus/7733-property-bag-late-prototype.js new file mode 100644 index 000000000..f88212b62 --- /dev/null +++ b/tests/corpus/7733-property-bag-late-prototype.js @@ -0,0 +1,52 @@ +// A class whose property bag is first created without prototype data, while a local +// class with an untyped base rewrites the shared bag dispatch: the store gains the +// prototype later, so inherited methods stay readable (hono's SmartRouter with zod). +function $constructor(name, initializer, params) { + const Parent = params?.Parent ?? Object; + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + function _(def) { + const inst = params?.Parent ? new Definition() : this; + inst.def = def; + initializer(inst, def); + return inst; + } + Object.defineProperty(_, "name", { value: name }); + return _; +} +class Smart { + name = "S"; + #inner = []; + constructor(init) { this.#inner = init.routers; } + add(x) { this.#inner.push(x); } + match(m) { + const r = this.#inner[0]; + this.match = r.match.bind(r); + this.name = `S + ${r.name}`; + return "first " + m; + } +} +class Inner { name = "Inner"; match(m) { return "inner " + m; } } +class App { + router; + constructor(options = {}) { + const { strict, ...rest } = options; + Object.assign(this, rest); + this.router = options.router ?? new Smart({ routers: [new Inner()] }); + } + dispatch(m) { return this.router.match(m); } +} +const app = new App(); +console.log(Object.keys(app.router).join(",")); +const $Err = $constructor("$Err", (inst, def) => { inst.issues = def.issues; }, { Parent: Error }); +const e = new $Err({ issues: [1] }); +console.log(Object.keys(e).join(","), e.issues.length); +const run = [(a) => a.dispatch("a"), (a) => a.dispatch("b")]; +for (const f of run) console.log(f(app), app.router.name); +const holder = JSON.parse("{}"); +holder.r = new Smart({ routers: [new Inner()] }); +console.log(typeof holder.r.match, "match" in holder.r, Object.keys(holder.r).join(",")); +console.log(holder.r.match("c"), holder.r.match("d"), holder.r.name); +const scratch = JSON.parse("{\"name\":\"x\",\"other\":1}"); +delete scratch.name; +console.log(Object.keys(scratch).join(",")); diff --git a/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json b/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json new file mode 100644 index 000000000..706d1a2a5 --- /dev/null +++ b/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 3900 } From e269262d7395019f0c47506f031914fcb7ef0d5d Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:40:29 -0700 Subject: [PATCH 055/409] Iterate untyped Headers values entries(), keys() and values() on a checked-dynamic Headers value threw 'is not a function'. They now return live native iterators over the sorted header list, re-read on every step with set-cookie values one by one, as Node's Headers iterator does (hono's Context copies response headers this way). Co-Authored-By: Claude Opus 5.5 --- .../7735-untyped-headers-iterators.js.json | 6 ++++ packages/runtime/src/scr_fetch.c | 36 ++++++++++++++++++- .../corpus/7735-untyped-headers-iterators.js | 23 ++++++++++++ .../7735-untyped-headers-iterators.js.json | 1 + 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json create mode 100644 tests/corpus/7735-untyped-headers-iterators.js create mode 100644 tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json new file mode 100644 index 000000000..7ca5d34da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7735-untyped-headers-iterators.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 96e90e47c..7cc2beef4 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -2491,11 +2491,44 @@ static bool sf_headers_sorted_entry(SfHeaders *h, size_t index, return found; } +/* One step of a live Headers iterator (entries, keys or values): the + * `*index`th entry of the current sorted list, as Node's iterator re-reads + * the list on every step. The result is { value, done }. */ +static ScrDyn *sf_headers_step(void *ptr, double *index, unsigned selection) { + ScrStr *name = NULL; + ScrStr *value = NULL; + bool found = sf_headers_sorted_entry(ptr, (size_t)*index, &name, &value); + ScrDyn *item; + if (found) { + (*index)++; + if (selection == 2) { + item = scr_dyn_new_arr(); + scr_dyn_arr_push(item, scr_dyn_new_str(name)); + scr_dyn_arr_push(item, scr_dyn_new_str(value)); + } else { + item = scr_dyn_new_str(selection == 1 ? name : value); + } + scr_str_release(name); + scr_str_release(value); + } else { + item = scr_dyn_retain(scr_dyn_undefined()); + } + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, item); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(!found)); + return result; +} + static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { SfHeaders *h = ptr; + if (strcmp(method, "entries") == 0 || strcmp(method, "keys") == 0 || + strcmp(method, "values") == 0) { + return scr_dyn_native_handle_iterator( + self, strcmp(method, "entries") == 0 ? 2 : strcmp(method, "keys") == 0 ? 1 : 0); + } if (strcmp(method, "get") == 0 || strcmp(method, "has") == 0) { if (argc < 1) { sf_type_error("Headers.get/has requires a header name"); @@ -5487,7 +5520,8 @@ static const ScrDynHandleOps sf_response_ops = { &sf_response_invoke, &sf_response_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_headers_ops = { "Headers", &sf_headers_retain_v, &sf_headers_release_v, - &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL}; + &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL, NULL, + &sf_headers_step}; static void sf_teardown(void) { while (sf_live) { diff --git a/tests/corpus/7735-untyped-headers-iterators.js b/tests/corpus/7735-untyped-headers-iterators.js new file mode 100644 index 000000000..40937fe5c --- /dev/null +++ b/tests/corpus/7735-untyped-headers-iterators.js @@ -0,0 +1,23 @@ +// Headers entries/keys/values on an untyped Headers value: live iterators over the +// sorted list, with set-cookie values one by one, as hono's Context copies headers. +const holder = JSON.parse("{}"); +holder.res = new Response("body", { headers: { "x-b": "2", "X-A": "1", "set-cookie": "s=1" } }); +holder.res.headers.append("set-cookie", "t=2"); +const headers = holder.res.headers; +for (const [k, v] of headers.entries()) console.log("entry", k, v); +console.log(JSON.stringify([...headers.keys()]), JSON.stringify([...headers.values()])); +const it = headers.entries(); +console.log(JSON.stringify(it.next())); +headers.append("a-first", "0"); +headers.append("z-last", "9"); +console.log(JSON.stringify(it.next()), JSON.stringify(it.next())); +const rest = []; +for (const entry of it) rest.push(entry.join("=")); +console.log(rest.join(" "), JSON.stringify(it.next())); +const copy = JSON.parse("{}"); +copy.res = new Response(null, { status: 204 }); +for (const [k, v] of headers.entries()) { + if (k === "set-cookie") continue; + copy.res.headers.set(k, v); +} +console.log(JSON.stringify([...copy.res.headers.entries()])); diff --git a/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json b/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json new file mode 100644 index 000000000..8836ebdf7 --- /dev/null +++ b/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 4000 } From b1214f76070fc34a45bb65f6c73ee13f41fb7f66 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 22 Oct 2026 22:40:28 -0700 Subject: [PATCH 056/409] Test catch bindings against class bindings e instanceof E where e is a catch binding and E a binding holding a class expression (var E = class extends Error {}) refused with "'instanceof' right-hand sides other than classes declared in the program". The binding holds exactly that class, so the test is the class's caught test (hono's SmartRouter fallback on UnsupportedPathError). Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/relational.ts | 90 ++++++++++++------- ...34-caught-instanceof-class-binding.js.json | 6 ++ .../7734-caught-instanceof-class-binding.js | 22 +++++ ...34-caught-instanceof-class-binding.js.json | 1 + 4 files changed, 85 insertions(+), 34 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json create mode 100644 tests/corpus/7734-caught-instanceof-class-binding.js create mode 100644 tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index b731e4912..9f06cc377 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -400,7 +400,24 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // (itself — no descendants can flow into the slot), so the answer // folds statically exactly like the named-target folds below. const rhsClassval = storedClassValueType(lowerer, expr.right); - if (rhsClassval?.kind === "classval" && !lowerer.caughtLocalOf(expr.left)) { + // A catch binding against a class held by a binding (`var E = class + // extends Error {}`): the binding holds exactly that class, so the test + // is the class's caught test, after the right operand's read. + const caught = lowerer.caughtLocalOf(expr.left); + const caughtTarget = + rhsClassval?.kind === "classval" && caught + ? lowerer.classes.get(rhsClassval.className) + : undefined; + if (caught && caughtTarget && !caughtTarget.localClass && !caughtTarget.def.runtime) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(expr.right), loc }], + result: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + type: BOOL, + loc, + }; + } + if (rhsClassval?.kind === "classval" && !caught) { const targetInfo = lowerer.classes.get(rhsClassval.className); if (!targetInfo) { // The type world names a class the lowering never registered @@ -551,39 +568,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // narrowing then types the branches; reads bridge through caughtRead. const caughtLocal = lowerer.caughtLocalOf(expr.left); if (caughtLocal) { - const caughtInstanceOf = (info: ClassInfo): IrExpr => { - if (!lowerer.inHierarchy(info)) { - const value = varRef(caughtLocal.id, CAUGHT, loc); - return { - kind: "ternary", - cond: { - kind: "caughtTest", - value, - test: "instanceof", - className: info.def.name, - type: BOOL, - loc, - }, - then: { kind: "boolLit", value: true, type: BOOL, loc }, - else_: classInstanceOf( - lowerer, - { kind: "caughtToDyn", value, type: DYN, loc }, - info, - loc, - ), - type: BOOL, - loc, - }; - } - return { - kind: "caughtTest", - value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc }, - test: "instanceof", - className: info.def.name, - type: BOOL, - loc, - }; - }; + const caughtInstanceOf = (info: ClassInfo): IrExpr => + caughtClassInstanceOf(lowerer, caughtLocal.id, info, loc); let test = caughtInstanceOf(target); // A thrown checked-dynamic object whose prototype chain reaches // Error.prototype (an ES5 constructor function inheriting from it). @@ -978,6 +964,42 @@ function writableClassValueTest( }; } +/** A catch binding tested against a program class: the runtime test against + * the class's preorder interval (false for non-hierarchy-object payloads). */ +function caughtClassInstanceOf( + lowerer: Lowerer, + caughtId: string, + info: ClassInfo, + loc: SrcLoc, +): IrExpr { + if (!lowerer.inHierarchy(info)) { + const value = varRef(caughtId, CAUGHT, loc); + return { + kind: "ternary", + cond: { + kind: "caughtTest", + value, + test: "instanceof", + className: info.def.name, + type: BOOL, + loc, + }, + then: { kind: "boolLit", value: true, type: BOOL, loc }, + else_: classInstanceOf(lowerer, { kind: "caughtToDyn", value, type: DYN, loc }, info, loc), + type: BOOL, + loc, + }; + } + return { + kind: "caughtTest", + value: { kind: "varRef", localId: caughtId, type: CAUGHT, loc }, + test: "instanceof", + className: info.def.name, + type: BOOL, + loc, + }; +} + /** A statically decided `instanceof` still reads its class after the left * operand: a class with an initialization flag throws there before its * declaration ran. */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json new file mode 100644 index 000000000..9ce0ed4f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7734-caught-instanceof-class-binding.js" + ], + "diags": [] +} diff --git a/tests/corpus/7734-caught-instanceof-class-binding.js b/tests/corpus/7734-caught-instanceof-class-binding.js new file mode 100644 index 000000000..55ee792ac --- /dev/null +++ b/tests/corpus/7734-caught-instanceof-class-binding.js @@ -0,0 +1,22 @@ +// A catch binding tested against a class held by a binding (var/const class expression), +// as hono's SmartRouter tests UnsupportedPathError. +var UnsupportedPathError = class extends Error {}; +class Strict { + add(p) { if (p.includes("*")) throw new UnsupportedPathError(); return p; } +} +class Loose { add(p) { return "loose " + p; } } +function pick(p) { + const routers = [new Strict(), new Loose()]; + for (const r of routers) { + try { + return r.add(p); + } catch (e) { + if (e instanceof UnsupportedPathError) continue; + throw e; + } + } +} +console.log(pick("/a"), pick("/b/*")); +try { throw new Error("x"); } catch (e) { console.log(e instanceof UnsupportedPathError, e instanceof Error); } +const u = new UnsupportedPathError("m"); +console.log(u instanceof UnsupportedPathError, u.message, u.name, u instanceof Error); diff --git a/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json b/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json new file mode 100644 index 000000000..71aa92bd3 --- /dev/null +++ b/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 3600 } From a474ff7e8d15ea44f608885fdd7a6afce64e451d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 02:43:57 -0500 Subject: [PATCH 057/409] Check for Error accessors after definitions over object-typed targets The reflection gate turned on only for an untyped or Error-shaped defineProperty target, so a helper taking `target: object` (or `{}`, `Object`, a weak type, or a type parameter) installed a message or name accessor that typed Error reads then skipped, returning the old slot value. The gate now turns on for any target an Error may flow into: an untyped or generic type, a type Error is assignable to, or an Error-shaped type, alone or in a union or intersection. --- .../src/frontend/lowering/lower-calls.ts | 27 +++++-- .../7263-error-accessor-object-helper.ts.json | 6 ++ .../7263-error-accessor-object-helper.ts | 54 +++++++++++++ .../7263-error-accessor-object-helper.ts.json | 1 + .../harness/error-accessor-reflection.test.ts | 77 +++++++++++++++++++ 5 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json create mode 100644 tests/corpus/7263-error-accessor-object-helper.ts create mode 100644 tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json create mode 100644 tests/harness/error-accessor-reflection.test.ts diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 54bf9a30d..909c8f27a 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -269,7 +269,7 @@ const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "cl /** `Object.defineProperty(target, ...)`, `Object.defineProperties`, * `Reflect.defineProperty`, and `target.__defineGetter__/__defineSetter__` - * over an untyped or Error-shaped target can install an accessor over an + * over a target that may hold an Error can install an accessor over an * error's `name` or `message` (error-methods.ts). */ function noteErrorAccessorReflection(lowerer: Lowerer, expr: ts.CallExpression): void { const callee = expr.expression; @@ -282,15 +282,28 @@ function noteErrorAccessorReflection(lowerer: Lowerer, expr: ts.CallExpression): ? callee.expression : undefined; if (!target) return; - const type = lowerer.checker.getTypeAtLocation(target); - if ( - type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown) || - (lowerer.checker.getPropertyOfType(type, "message") && - lowerer.checker.getPropertyOfType(type, "name")) - ) + const error = lowerer.checker.resolveName("Error", ts.SymbolFlags.Type, target, false); + const errorType = error ? lowerer.checker.getDeclaredTypeOfSymbol(error) : null; + if (mayHoldError(lowerer, lowerer.checker.getTypeAtLocation(target), errorType)) lowerer.errorAccessorReflection = locOf(expr); } +/** An untyped or generic type, a type Error is assignable to (`object`, + * `{}`, `Object`, `{ message?: string }`), or + * an Error-shaped type with both `name` and `message`, alone or in a + * union or intersection. */ +function mayHoldError(lowerer: Lowerer, type: ts.Type, errorType: ts.Type | null): boolean { + if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Instantiable)) + return true; + if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) + return ts.constituentTypes(type).some((part) => mayHoldError(lowerer, part, errorType)); + return ( + (lowerer.checker.getPropertyOfType(type, "message") !== undefined && + lowerer.checker.getPropertyOfType(type, "name") !== undefined) || + (errorType !== null && lowerer.checker.isTypeAssignableTo(errorType, type)) + ); +} + export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); if (lowerer.errorAccessorReflection === null) noteErrorAccessorReflection(lowerer, expr); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json new file mode 100644 index 000000000..8bf8f11da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7263-error-accessor-object-helper.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7263-error-accessor-object-helper.ts b/tests/corpus/7263-error-accessor-object-helper.ts new file mode 100644 index 000000000..8e8a4a532 --- /dev/null +++ b/tests/corpus/7263-error-accessor-object-helper.ts @@ -0,0 +1,54 @@ +// An accessor installed over an Error's own `message` and `name` by +// helpers whose target parameter is typed `object`, `{}` or generic: +// typed reads, writes and String() go through the accessors. +class ValidationError extends Error {} + +function defineLazyMessage(target: object, compute: () => string): void { + let cached: string | undefined; + Object.defineProperty(target, "message", { + get() { + cached ??= compute(); + return cached; + }, + set(value: string) { + cached = `set:${value}`; + }, + enumerable: true, + configurable: true, + }); +} + +function defineName(target: {}, name: string): void { + Object.defineProperty(target, "name", { get: () => name, configurable: true }); +} + +function defineFixedMessage(target: T, message: string): T { + Object.defineProperty(target, "message", { get: () => message, configurable: true }); + return target; +} + +let computed = 0; +const e = new ValidationError("orig"); +defineLazyMessage(e, () => { + computed++; + return "too small"; +}); +console.log(e.message, computed); +console.log(String(e), computed); +e.message = "typed"; +console.log(e.message, Object.keys(e).join()); + +const named = new ValidationError("bad input"); +defineName(named, "ZodError"); +console.log(named.name, String(named)); +const fixed = defineFixedMessage(new ValidationError("orig"), "fixed"); +console.log(fixed.message, String(fixed)); + +const plain = new Error("start"); +defineLazyMessage(plain, () => "computed"); +console.log(plain.message, `${plain}`); +try { + throw plain; +} catch (caught) { + console.log(caught instanceof Error && caught.message); +} diff --git a/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json b/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json new file mode 100644 index 000000000..b56ad904d --- /dev/null +++ b/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 4000 } diff --git a/tests/harness/error-accessor-reflection.test.ts b/tests/harness/error-accessor-reflection.test.ts new file mode 100644 index 000000000..5f50d8a66 --- /dev/null +++ b/tests/harness/error-accessor-reflection.test.ts @@ -0,0 +1,77 @@ +/* Reflection that can install an accessor over an error's `name` or + * `message`: a defineProperty call over a target that may hold an Error + * turns on the accessor checks of typed Error reads and writes + * (error-methods.ts), whatever the target parameter's declared type. */ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); + +async function emittedIr(source: string): Promise { + const key = createHash("sha256").update(source).digest("hex").slice(0, 16); + const outDir = join(cacheDir, `error-accessor-${key}`); + mkdirSync(outDir, { recursive: true }); + const file = join(outDir, "main.ts"); + writeFileSync(file, source); + const result = await compile(file, { + outPath: join(outDir, "main.ir"), + outDir, + outputKind: "ir", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFileSync(result.artifact.path, "utf8"); +} + +const program = ( + param: string, + define = "Object.defineProperty", +): string => `class E extends Error {} +function install${param} { + ${define}(target, "message", { get: () => "lazy", configurable: true }); +} +const e = new E("orig"); +install(e); +console.log(e.message); +`; + +const checksAccessors = async (source: string): Promise => + (await emittedIr(source)).includes("error.ownAccessor"); + +test.each([ + ["object", "(target: object)"], + ["{}", "(target: {})"], + ["Object", "(target: Object)"], + ["a weak type", "(target: { message?: string })"], + ["{ message: string }", "(target: { message: string })"], + ["a type parameter", "(target: T)"], + ["a constrained type parameter", "(target: T)"], + ["object | undefined", "(target: object | undefined)"], + ["Error", "(target: Error)"], + ["unknown", "(target: unknown)"], +])("a definition over %s checks for accessors", async (_, param) => { + expect(await checksAccessors(program(param))).toBe(true); +}); + +test("Reflect.defineProperty over object checks for accessors", async () => { + expect(await checksAccessors(program("(target: object)", "Reflect.defineProperty"))).toBe(true); +}); + +test("a definition over a type that cannot hold an Error leaves typed reads plain", async () => { + const source = `class E extends Error {} +class Box { + id = 0; +} +function install(target: Box) { + Object.defineProperty(target, "extra", { get: () => 1, configurable: true }); +} +install(new Box()); +const e = new E("orig"); +console.log(e.message); +`; + expect(await checksAccessors(source)).toBe(false); +}); From ac40773362d71f04c5564a7ba85ba4220ce90a3f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 02:54:19 -0500 Subject: [PATCH 058/409] Excuse date.fromTime in the attestation parity test date.fromTime makes a Date from the time value a Date local holds and reads no ambient state. The local's initializer is the Date expression the fences witness. --- tests/harness/surface-manifest.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/harness/surface-manifest.test.ts b/tests/harness/surface-manifest.test.ts index 2ff3c71c2..58958c102 100644 --- a/tests/harness/surface-manifest.test.ts +++ b/tests/harness/surface-manifest.test.ts @@ -558,6 +558,10 @@ describe("determinism attestation ↔ fence parity", () => { // witnessed Date method; assigning it to getTime would also deny // valueOf/toISOString/etc. under a getTime-only fence. "date.checkedValue", + // Makes a Date from the time value a Date local holds + // (date-values.ts) without reading ambient state. The local's + // initializer is the Date expression the fences witness. + "date.fromTime", ]); const missing = Object.keys(LIB_FN_SIGS).filter( (fn) => From b9275c2693597c3e79287fcbedc80f5d49814d5f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:05:19 -0500 Subject: [PATCH 059/409] buffer: fromArrayBuffer ranges, the deprecated constructor, Buffer as a value --- .../limitations/buffer-construction.mdx | 5 + .../ambient/scriptc-node-fallback.d.ts | 8 + .../compiler/src/backend/llvm/lib-shared.ts | 9 + .../src/frontend/lowering/containers/bytes.ts | 259 ++++++++++++++---- .../lowering/expressions/element-access.ts | 3 + .../lowering/expressions/identifiers.ts | 3 + .../lowering/expressions/property-access.ts | 45 +++ .../src/frontend/lowering/lower-calls.ts | 9 +- .../src/frontend/lowering/lower-classes.ts | 45 +-- .../src/frontend/lowering/lower-inspect.ts | 20 +- .../compiler/src/frontend/lowering/lowerer.ts | 5 + .../src/frontend/lowering/surfaces.ts | 3 + packages/compiler/src/frontend/program.ts | 5 + packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 9 + packages/compiler/src/ir/int32-slots.ts | 2 + packages/compiler/src/ir/runtime-features.ts | 21 +- .../7527-buffer-from-array-buffer.ts.json | 6 + .../7528-buffer-from-array-buffer.cjs.json | 6 + .../corpus/7529-buffer-constructor.cjs.json | 6 + .../corpus/7530-buffer-constructor.ts.json | 6 + .../main.cjs.json | 18 ++ .../main.cjs.json | 18 ++ .../tests/corpus/7533-buffer-value.cjs.json | 6 + .../7534-buffer-prototype-members.cjs.json | 6 + .../7535-inspect-function-properties.cjs.json | 6 + packages/runtime/src/scr_async.h | 4 + packages/runtime/src/scr_async_dyn.c | 16 ++ packages/runtime/src/scr_builtin_values.c | 214 +++++++++++++++ packages/runtime/src/scr_bytes.c | 20 +- packages/runtime/src/scr_bytes.h | 27 ++ packages/runtime/src/scr_bytes_dyn.c | 203 ++++++++++++++ packages/runtime/src/scr_bytes_io.c | 66 ++++- packages/runtime/src/scr_dyn.h | 4 + packages/runtime/src/scr_inspect.c | 40 ++- packages/runtime/src/scr_json.c | 22 +- tests/corpus/2570-buffer-arg-validation.cjs | 3 +- tests/corpus/7527-buffer-from-array-buffer.ts | 33 +++ .../corpus/7528-buffer-from-array-buffer.cjs | 31 +++ tests/corpus/7529-buffer-constructor.cjs | 80 ++++++ tests/corpus/7530-buffer-constructor.ts | 42 +++ .../main.cjs | 36 +++ .../node_modules/legacy-buf/index.d.ts | 3 + .../node_modules/legacy-buf/index.js | 15 + .../node_modules/legacy-buf/package.json | 5 + .../main.cjs | 10 + .../node_modules/legacy-buf/index.d.ts | 3 + .../node_modules/legacy-buf/index.js | 15 + .../node_modules/legacy-buf/package.json | 5 + tests/corpus/7533-buffer-value.cjs | 101 +++++++ .../corpus/7534-buffer-prototype-members.cjs | 29 ++ .../7535-inspect-function-properties.cjs | 29 ++ .../7527-buffer-from-array-buffer.ts.json | 1 + .../7528-buffer-from-array-buffer.cjs.json | 1 + .../7529-buffer-constructor.cjs.json | 1 + .../7530-buffer-constructor.ts.json | 1 + .../main.cjs.json | 1 + .../main.cjs.json | 1 + .../corpus-costs/7533-buffer-value.cjs.json | 1 + .../7534-buffer-prototype-members.cjs.json | 1 + .../7535-inspect-function-properties.cjs.json | 1 + tests/harness/differential.test.ts | 6 +- tests/harness/errors.test.ts | 49 ++++ 63 files changed, 1535 insertions(+), 117 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json create mode 100644 tests/corpus/7527-buffer-from-array-buffer.ts create mode 100644 tests/corpus/7528-buffer-from-array-buffer.cjs create mode 100644 tests/corpus/7529-buffer-constructor.cjs create mode 100644 tests/corpus/7530-buffer-constructor.ts create mode 100644 tests/corpus/7531-buffer-constructor-dependency/main.cjs create mode 100644 tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts create mode 100644 tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js create mode 100644 tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json create mode 100644 tests/corpus/7532-buffer-constructor-check-limit/main.cjs create mode 100644 tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts create mode 100644 tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js create mode 100644 tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json create mode 100644 tests/corpus/7533-buffer-value.cjs create mode 100644 tests/corpus/7534-buffer-prototype-members.cjs create mode 100644 tests/corpus/7535-inspect-function-properties.cjs create mode 100644 tests/harness/corpus-costs/7527-buffer-from-array-buffer.ts.json create mode 100644 tests/harness/corpus-costs/7528-buffer-from-array-buffer.cjs.json create mode 100644 tests/harness/corpus-costs/7529-buffer-constructor.cjs.json create mode 100644 tests/harness/corpus-costs/7530-buffer-constructor.ts.json create mode 100644 tests/harness/corpus-costs/7531-buffer-constructor-dependency/main.cjs.json create mode 100644 tests/harness/corpus-costs/7532-buffer-constructor-check-limit/main.cjs.json create mode 100644 tests/harness/corpus-costs/7533-buffer-value.cjs.json create mode 100644 tests/harness/corpus-costs/7534-buffer-prototype-members.cjs.json create mode 100644 tests/harness/corpus-costs/7535-inspect-function-properties.cjs.json diff --git a/docs/content/limitations/buffer-construction.mdx b/docs/content/limitations/buffer-construction.mdx index aca0c74ca..c88e01336 100644 --- a/docs/content/limitations/buffer-construction.mdx +++ b/docs/content/limitations/buffer-construction.mdx @@ -1,3 +1,8 @@ `Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects. Encoding arguments must be supported literals. +`Buffer.from(arrayBuffer, byteOffset, length)` is a view of the ArrayBuffer or SharedArrayBuffer, with Node's offset and length checks. Custom input `valueOf` hooks and opaque class or native-handle inputs throw an unsupported-operation error. +The deprecated `Buffer()` and `new Buffer()` take up to three arguments, without spreads, and follow Node: the first call made outside `node_modules` reports DEP0005 on the next tick, a number allocates zero-filled bytes, and other arguments convert as `Buffer.from` does. +In JavaScript, `Buffer` read as a value (the global or the buffer module's `Buffer`) is one function with Node's statics and `poolSize`. +Calling or constructing it through a stored value, reading its `prototype`, and its `copyBytesFrom` and `of` statics throw an unsupported-operation error. +`Buffer.prototype` members read as values are functions that run on Buffer receivers; inherited and symbol-keyed members throw an unsupported-operation error. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index b195c3fbb..e26faf956 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -917,6 +917,14 @@ interface Buffer< readBigUint64LE(offset?: number): bigint; } interface BufferConstructor { + /** @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead. */ + new (str: string, encoding?: BufferEncoding): Buffer; + /** @deprecated since v10.0.0 - Use `Buffer.alloc()` instead. */ + new (size: number): Buffer; + /** @deprecated since v10.0.0 - Use `Buffer.from(array)` instead. */ + new (array: ArrayLike): Buffer; + /** @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead. */ + new (arrayBuffer: ArrayBuffer | SharedArrayBuffer): Buffer; from(data: string, encoding?: BufferEncoding): Buffer; /* The ArrayBuffer form is the VIEW construction: x.buffer with an * optional byte offset/length shares x's storage (the subarray rule). */ diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 7dd4370c5..b92047e43 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -454,6 +454,7 @@ export const LIB_FN_SYMS: Record = { "buffer.poolFromDyn": "scr_buffer_from_dyn_pooled", "buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled", "buffer.poolFromDynOffset": "scr_buffer_from_dyn_offset_pooled", + "buffer.poolFromDynRange": "scr_buffer_from_dyn_range_pooled", "buffer.poolSize": "scr_buffer_pool_size", "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", @@ -479,6 +480,12 @@ export const LIB_FN_SYMS: Record = { "bytes.equalsChk": "scr_bytes_equals_chk", "bytes.compareChk": "scr_bytes_compare_chk", "buffer.newStringFail": "scr_buffer_new_string_fail", + "buffer.deprecation": "scr_buffer_deprecation", + "buffer.deprecationCount": "scr_buffer_deprecation_count", + "buffer.constructor": "scr_buffer_constructor_value", + "buffer.installValue": "scr_buffer_install_value", + "buffer.staticValue": "scr_buffer_static_value", + "buffer.prototypeMember": "scr_buffer_prototype_member", "fs.toUnixTimestamp": "scr_fs_to_unix_timestamp", "fs.existsChk": "scr_fs_exists_async", "fs.mkdtempSyncChk": "scr_fs_mkdtemp_sync_chk", @@ -1661,6 +1668,8 @@ export const USES_TIMERS_LIB_FNS = new Set([ "url.legacyParse", "url.legacyFormat", "url.legacyResolve", + // The deprecated Buffer constructor reports DEP0005 on the next tick. + "buffer.deprecation", // The wrappers schedule warnings and callbacks on the tick queue. "util.deprecate", "util.callbackify", diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 995cb177a..82c6f3052 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -15,6 +15,7 @@ import { type IrLibFn, type IrLocal, type IrParam, + type IrFunction, type IrStmt, type IrType, STRING, @@ -30,7 +31,8 @@ import { import { isJsSourceFile, locOf } from "../../program.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; import type { Lowerer } from "../lowerer.js"; -import { newFnCtx, own } from "../lowerer.js"; +import { ENTRY_NAME, newFnCtx, own } from "../lowerer.js"; +import { everyStmt } from "../../../ir/traverse.js"; import { buildBytesSortFn, closureKeyOrder } from "../lower-array-sort.js"; import { isSafeToDiscard } from "../expressions/evaluation-safety.js"; import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; @@ -1807,7 +1809,12 @@ function knownBufferProducer( * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; an * ArrayBuffer takes the second argument as its byte offset; other sources * ignore it. */ -function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr { +function dynBufferFromParts( + source: IrExpr, + encoding: IrExpr, + loc: SrcLoc, + length?: IrExpr, +): IrExpr { const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; const isString = (x: IrExpr): IrExpr => ({ kind: "dynTest", @@ -1848,20 +1855,48 @@ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrEx type: BYTES_U8, loc, }, - // An ArrayBuffer takes the second argument as its byte offset - // (scr_buffer_from_dyn_offset_pooled); other values convert with utf8. - else_: { - kind: "libCall", - fn: "buffer.poolFromDynOffset", - args: [source, encoding], - type: BYTES_U8, - loc, - }, + // An ArrayBuffer takes the second argument as its byte offset and the + // third as its length (scr_buffer_from_dyn_range_pooled); other values + // convert with utf8. + else_: length + ? { + kind: "libCall", + fn: "buffer.poolFromDynRange", + args: [source, encoding, length], + type: BYTES_U8, + loc, + } + : { + kind: "libCall", + fn: "buffer.poolFromDynOffset", + args: [source, encoding], + type: BYTES_U8, + loc, + }, type: BYTES_U8, loc, }; } +/** Buffer.from(arrayBuffer, byteOffset[, length]): each argument in order + * as a checked-dynamic value, read by the runtime as Node's + * fromArrayBuffer reads them. */ +function bufferFromArrayBufferRange( + lowerer: Lowerer, + args: readonly ts.Expression[], + loc: SrcLoc, + first?: IrExpr, +): IrExpr { + const operands = [0, 1, 2].map((i) => + i === 0 && first + ? first + : args[i] + ? lowerer.lowerExprExpecting(args[i]!, DYN) + : dynUndefinedExpr(loc), + ); + return { kind: "libCall", fn: "buffer.poolFromDynRange", args: operands, type: BYTES_U8, loc }; +} + /** Lowers `source` and `encNode` (or undefined) into hidden dyn locals, in * source order, and builds the result from their references. */ function withDynBufferArguments( @@ -1909,52 +1944,81 @@ function dynBufferFromRuntimeEncoding( ); } -/** The deprecated `new Buffer(arg[, encoding])` in a dependency's - * JavaScript, through the global or an alias of it (buffer-crc32 and yazl - * keep it as the fallback for runtimes without Buffer.alloc/Buffer.from). - * Node never emits DEP0005 for calls from node_modules, so the construction - * is exactly Node's: a number allocates zero-filled bytes (and throws - * ERR_INVALID_ARG_TYPE beside a string second argument), anything else - * converts as Buffer.from does. Null elsewhere: other callers owe the - * deprecation warning. */ -export function lowerNodeModulesBufferConstruct( +/** The deprecated `Buffer(arg[, encodingOrOffset[, length]])`, called or + * constructed through the global or an alias of it, as Node's Buffer + * function runs it: the arguments evaluate in order, DEP0005 is reported + * on the next tick (once, and never for a call made from node_modules, + * which only counts toward Node's 10000-call check), then a number + * allocates zero-filled bytes (and throws ERR_INVALID_ARG_TYPE beside a + * string second argument) and anything else converts as Buffer.from does. + * Null for spread arguments and more than three. */ +export function lowerBufferConstruct( lowerer: Lowerer, - expr: ts.NewExpression, + expr: ts.NewExpression | ts.CallExpression, ): IrExpr | null { - const sf = expr.getSourceFile(); - if (!isJsSourceFile(sf) || !/[\\/]node_modules[\\/]/.test(sf.fileName)) return null; + if (ts.isCallExpression(expr) && expr.questionDotToken) return null; const args = expr.arguments?.nodes ?? []; - if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null; + if (args.length > 3 || args.some(ts.isSpreadElement)) return null; if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Buffer") return null; const loc = locOf(expr); - const value = withDynBufferArguments( - lowerer, - lowerer.lowerExpr(args[0]!), - args[1], - loc, - (source, encoding) => ({ + const locals = [0, 1, 2].slice(0, Math.max(args.length, 2)).map((i) => { + const local = lowerer.declareHiddenLocal("%bufferArgument", DYN); + const node = args[i]; + const init = node === undefined ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(node, DYN); + return { local, init }; + }); + const [source, second, third] = locals.map(({ local }) => varRef(local.id, DYN, loc)) as [ + IrExpr, + IrExpr, + IrExpr | undefined, + ]; + const inNodeModules = /[\\/]node_modules[\\/]/.test(expr.getSourceFile().fileName); + const value: IrExpr = { + kind: "ternary", + cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc }, + then: { kind: "ternary", - cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc }, - then: { - kind: "ternary", - cond: { kind: "dynTest", test: "string", value: encoding, type: BOOL, loc }, - then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc }, - else_: { - kind: "libCall", - fn: "buffer.alloc", - args: [{ kind: "dynCheck", value: source, type: F64, loc }], - type: BYTES_U8, - loc, - }, + cond: { kind: "dynTest", test: "string", value: second, type: BOOL, loc }, + then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc }, + else_: { + kind: "libCall", + fn: "buffer.alloc", + args: [{ kind: "dynCheck", value: source, type: F64, loc }], type: BYTES_U8, loc, }, - else_: dynBufferFromParts(source, encoding, loc), type: BYTES_U8, loc, - }), - ); - return { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc }; + }, + else_: dynBufferFromParts(source, second, loc, third), + type: BYTES_U8, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + ...locals.map(({ local, init }): IrStmt => ({ + kind: "varDecl", + localId: local.id, + init, + loc, + })), + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: inNodeModules ? "buffer.deprecationCount" : "buffer.deprecation", + args: [], + type: VOID, + loc, + }, + loc, + }, + ], + result: { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc }, + type: BYTES_U8, + loc, + }; } /** Buffer.from(array or bytes, extra): Node copies an array-like source and @@ -2018,7 +2082,16 @@ function lowerBufferStaticValue( lowerer.typeOf(args[0]!).getSymbol(), ) ) { - return lowerArrayBufferView(lowerer, args, "u8", loc); + // A byte offset or length follows Node's fromArrayBuffer (its + // conversions and ERR_BUFFER_OUT_OF_BOUNDS), not the typed-array view. + if (args.length === 1) return lowerArrayBufferView(lowerer, args, "u8", loc); + return bufferFromArrayBufferRange(lowerer, args, loc); + } + // Buffer.from(value, byteOffset, length) over an untyped value: an + // ArrayBuffer's range, or the two-argument conversion of other values. + if (args.length === 3 && !args.some(ts.isSpreadElement)) { + const first = lowerer.lowerExpr(args[0]!); + if (first.type.kind === "dyn") return bufferFromArrayBufferRange(lowerer, args, loc, first); } // Buffer.from(x.buffer[, byteOffset[, length]]): a u8 VIEW sharing // x's storage (Node shares the ArrayBuffer; length is in BYTES). @@ -2445,11 +2518,101 @@ function lowerBufferStaticValue( const s = lowerer.lowerExprExpecting(args[0]!, STRING); return { kind: "libCall", fn: "buffer.isEncoding", args: [s], type: BOOL, loc }; } + // Any other single argument: Node answers false for non-strings. + if (args.length === 1 && !ts.isSpreadElement(args[0]!)) { + const value = lowerer.declareHiddenLocal("%bufferEncoding", DYN); + const ref = varRef(value.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: value.id, + init: lowerer.lowerExprExpecting(args[0]!, DYN), + loc, + }, + ], + result: { + kind: "logical", + op: "&&", + left: { kind: "dynTest", test: "string", value: ref, type: BOOL, loc }, + right: { + kind: "libCall", + fn: "buffer.isEncoding", + args: [{ kind: "dynCheck", value: ref, type: STRING, loc }], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } lowerer.noLowering( "Buffer.isEncoding with this argument shape", call, - "one string-typed argument is the lowered form", + "one argument is the lowered form", ); } return null; // of, copyBytesFrom, ... → the SC2020 member fence } + +/** `Buffer.prototype.` or `Buffer.prototype[key]` read in JavaScript + * (smart-buffer's `typeof Buffer.prototype[method]` probe): the member as + * scr_buffer_prototype_member answers it. Null for other expressions. */ +export function lowerBufferPrototypeMember( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression | ts.ElementAccessExpression, +): IrExpr | null { + if (expr.questionDotToken || !isJsSourceFile(expr.getSourceFile())) return null; + const receiver = expr.expression; + if ( + !ts.isPropertyAccessExpression(receiver) || + receiver.questionDotToken || + receiver.name.text !== "prototype" || + stdlibGlobalNameOf(lowerer, receiver.expression) !== "Buffer" + ) + return null; + const loc = locOf(expr); + const key: IrExpr = ts.isPropertyAccessExpression(expr) + ? { + kind: "dynFrom", + value: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + } + : lowerer.lowerExprExpecting(expr.argumentExpression, DYN); + return { kind: "libCall", fn: "buffer.prototypeMember", args: [key], type: DYN, loc }; +} + +const BUFFER_VALUE_FNS: ReadonlySet = new Set([ + "buffer.constructor", + "buffer.staticValue", + "buffer.prototypeMember", +]); + +/** A program that reads the Buffer function's value installs it in %main, + * so a Buffer's `.constructor` answers the same function. */ +export function installBufferValue(functions: IrFunction[]): void { + let found = false; + const visitor = { + expr: (e: IrExpr): boolean => { + if (e.kind === "libCall" && BUFFER_VALUE_FNS.has(e.fn)) found = true; + return !found; + }, + stmt: (): boolean => !found, + }; + for (const fn of functions) { + for (const s of fn.body) everyStmt(s, visitor); + if (found) break; + } + const main = functions.find((fn) => fn.name === ENTRY_NAME); + if (!found || !main) return; + main.body.unshift({ + kind: "exprStmt", + expr: { kind: "libCall", fn: "buffer.installValue", args: [], type: VOID, loc: main.loc }, + loc: main.loc, + }); +} diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 0ea94ec7a..24de38630 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -71,6 +71,7 @@ import { isClassCallback } from "../class-callbacks.js"; import { isKeyedFieldAccess } from "./keyed-field-targets.js"; import { lowerFieldCompoundValue } from "./updates.js"; import { readsHeldEvolvingArray } from "../lower-stmts.js"; +import { lowerBufferPrototypeMember } from "../containers/bytes.js"; /** `a[i]` reads. Only f64 indices into array receivers are modeled; JS * string-key element access (`a["length"]`) and string indexing (`s[0]` @@ -104,6 +105,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre const en = lowerEnumAccess(lowerer, expr); if (en) return en; } + const bufferMember = lowerBufferPrototypeMember(lowerer, expr); + if (bufferMember) return bufferMember; const cachedModule = lowerRequireCacheElement(lowerer, expr); if (cachedModule) return cachedModule; const namespaceElement = lowerModuleNamespaceElement(lowerer, expr); diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index ef0d8a2d1..1570ea709 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -744,6 +744,9 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; const nativeAlias = stdlibGlobalNameOf(lowerer, expr); if (nativeAlias === "Buffer" && expr.text !== "Buffer") { + // JavaScript reads the Buffer function's value (builtinGlobalValue). + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) + return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc }; return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc }; } if (lowerer.isStdlibGlobal(expr, "fetch")) { diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7b12297a8..607ad221b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -153,6 +153,7 @@ import { lowerStringCodesValue, } from "../lower-builtin-values.js"; import { lowerArrayFromValue } from "../containers/array-construction.js"; +import { lowerBufferPrototypeMember } from "../containers/bytes.js"; import { lowerNetAddressStaticRead } from "../builtins/net-address.js"; import { STATS_TYPE_TESTS } from "../builtins/filesystem.js"; import { arrayValueRead, arrayValueType } from "../array-values.js"; @@ -310,6 +311,10 @@ export function lowerPropertyAccessExpr( // Cloudflare probe) folds like `navigator.userAgent`. const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); if (navigator) return navigator; + { + const member = lowerBufferPrototypeMember(lowerer, expr); + if (member) return member; + } // A dgram socket's method read as a value in JavaScript (`emit: // socket.emit.bind(socket)`): the socket crosses as its checked-dynamic // handle, whose methods are function values that dispatch on their @@ -1769,6 +1774,28 @@ export function lowerIntrinsicProperty( return lowerNumberParserValue(lowerer, numberMember, locOf(expr)); if (lowerer.stdlibGlobalMember(expr, "Buffer") === "poolSize") return { kind: "libCall", fn: "buffer.poolSize", args: [], type: F64, loc: locOf(expr) }; + { + const member = lowerer.stdlibGlobalMember(expr, "Buffer"); + if (member === "name") + return { kind: "strLit", value: "Buffer", type: STRING, loc: locOf(expr) }; + if (member === "length") return numLit(3, locOf(expr)); + // JavaScript reads the statics from the Buffer function's value, so + // each has one identity and Node's name and length. + if ( + member !== null && + BUFFER_VALUE_STATICS.has(member) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) + ) { + const loc = locOf(expr); + return { + kind: "libCall", + fn: "buffer.staticValue", + args: [{ kind: "strLit", value: member, type: STRING, loc }], + type: DYN, + loc, + }; + } + } const netAddressStatic = lowerNetAddressStaticRead(lowerer, expr); if (netAddressStatic) return netAddressStatic; if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer") @@ -4173,6 +4200,8 @@ export function builtinFunctionValue(name: string, loc: SrcLoc): IrExpr { * null when it stays an identity token (JavaScript) or fenced. */ export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false): IrExpr | null { if (js && GLOBAL_FUNCTION_VALUES.has(canonical)) return builtinFunctionValue(canonical, loc); + if (js && canonical === "Buffer") + return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc }; if (GLOBAL_CONSTRUCTOR_VALUES.has(canonical)) { return { kind: "libCall", @@ -4300,6 +4329,22 @@ function fenceBuiltinConstructorStaticRead( return null; } +/** The Buffer statics the Buffer function's value holds + * (scr_buffer_constructor_value). */ +const BUFFER_VALUE_STATICS: ReadonlySet = new Set([ + "from", + "copyBytesFrom", + "of", + "alloc", + "allocUnsafe", + "allocUnsafeSlow", + "isBuffer", + "compare", + "isEncoding", + "concat", + "byteLength", +]); + /** Object.prototype members a plain record inherits, read as values. */ const OBJECT_PROTOTYPE_VALUE_READS: ReadonlySet = new Set([ "constructor", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 82f5a27c1..03d19f0de 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -119,7 +119,7 @@ import { } from "./containers/indexed-objects.js"; import { lowerTupleReadMethodCall } from "./containers/array-methods.js"; import { tryFuseIndexRead } from "./array-values.js"; -import { lowerBytesStaticCall } from "./containers/bytes.js"; +import { lowerBufferConstruct, lowerBytesStaticCall } from "./containers/bytes.js"; import { lowerRegexMethodCall, lowerStringIndexCall, @@ -335,6 +335,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression); if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration); } + // The deprecated Buffer(...) call (containers/bytes.ts). The checker + // types it `any`, so the bytes cross as a checked-dynamic value, which + // keeps the Buffer brand for inspection. + { + const buffer = lowerBufferConstruct(lowerer, expr); + if (buffer) return { kind: "dynFrom", value: buffer, type: DYN, loc }; + } // A package shim's runtime intrinsic (a bodiless `declare function` // named scriptcUnixDgram* in a shipped shim): a direct runtime call. if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) { diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 29afbb4fe..64fd1968d 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -46,7 +46,6 @@ import { DATE_T, DYN, F64, - bytesOf, type IrClassDef, type IrExpr, type IrFunction, @@ -115,8 +114,7 @@ import { isCjsRequireGlobal } from "./lower-node-module.js"; import { lowerArrayConstructor } from "./containers/array-construction.js"; import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js"; import { lowerObjectConstructor } from "./containers/indexed-objects.js"; -import { knownBufEncoding, lowerNodeModulesBufferConstruct } from "./containers/bytes.js"; -import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { knownBufEncoding, lowerBufferConstruct } from "./containers/bytes.js"; import { lowerSearchParamsNew } from "./builtins/url.js"; import { lowerTextCodecNew } from "./builtins/text-codecs.js"; import { @@ -9776,9 +9774,9 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean { export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); - // A dependency's deprecated `new Buffer(...)` fallback (containers/bytes.ts). + // The deprecated `new Buffer(...)` (containers/bytes.ts). { - const buffer = lowerNodeModulesBufferConstruct(lowerer, expr); + const buffer = lowerBufferConstruct(lowerer, expr); if (buffer) return buffer; } // new Function(...): Node's EvalError under @@ -10855,43 +10853,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // families carry pointed hints: each states WHY no honest static // lowering exists (or what to use instead). if (lowerer.isStdlibSymbol(symbol ?? undefined)) { - // The deprecated `new Buffer(string, encoding?)` ctor's string arm - // with a NON-STRING first argument and a string second: Node - // throws ERR_INVALID_ARG_TYPE synchronously (and DEP0005 never - // fires on this throwing path, so the compiled silence matches). - // Every other new Buffer form keeps the fence below — the - // constructing forms would owe the deprecation warning. - if ( - expr.expression.text === "Buffer" && - expr.arguments?.nodes.length === 2 && - !expr.arguments.nodes.some(ts.isSpreadElement) && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind !== "string" && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "string" - ) { - const first = lowerer.lowerExpr(expr.arguments.nodes[0]!); - if ( - first.kind === "unitLit" || - first.type.kind === "dyn" || - lowerer.dynConvertible(first.type) - ) { - const got: IrExpr = - first.type.kind === "dyn" ? first : { kind: "dynFrom", value: first, type: DYN, loc }; - // The encoding argument still evaluates in Node before the - // throw only via the ctor body's later reads — it does NOT - // observe it before throwing, so dropping it is exact for - // effect-free operands; effectful ones keep the fence. - const enc = lowerer.lowerExpr(expr.arguments.nodes[1]!); - if (enc.kind === "strLit" || isSafeToRepeat(enc)) { - return { - kind: "libCall", - fn: "buffer.newStringFail", - args: [got], - type: bytesOf("u8"), - loc, - }; - } - } - } const ctorHints: Record = { RegExp: "use a regex literal (/pattern/flags) — constructed regexes have no lowering", String: diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 5266d5c4e..7f0f3684a 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -14,7 +14,7 @@ import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import { unsignedHex } from "../../format-integer.js"; -import type { Lowerer } from "./lowerer.js"; +import { jsFuncNameOf, type Lowerer } from "./lowerer.js"; import { isJsSourceFile } from "../program.js"; import { BOOL, @@ -2416,6 +2416,12 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo } const decl = sym.declarations[0]; if (!decl) return null; + // A JavaScript function can hold own properties (`f.x = 1`, computed + // keys), which the runtime walk prints after its name. + if (isJsSourceFile(node.getSourceFile())) { + const type = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (type?.kind === "func" && lowerer.dynConvertible(type)) return null; + } if (ts.isFunctionDeclaration(decl) && decl.name) { return strLit(`[Function: ${decl.name.text}]`, loc); } @@ -2476,7 +2482,17 @@ export function lowerInspectValue( loc, ); } - const value = lowerer.lowerExpr(valueNode); + let value = lowerer.lowerExpr(valueNode); + // A JavaScript function renders through the runtime walk, which prints + // its own properties after its name. + if ( + value.type.kind === "func" && + isJsSourceFile(valueNode.getSourceFile()) && + lowerer.dynConvertible(value.type) + ) { + const name = jsFuncNameOf(valueNode); + value = { kind: "dynFrom", value, type: DYN, ...(name !== null ? { fnName: name } : {}), loc }; + } if (value.type.kind === "bytes") { // One runtime representation serves Buffer AND Uint8Array; their // renderings differ ( vs Uint8Array(1) [ 170 ]), so the diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index e36ffd09f..c4b13c9ab 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -151,6 +151,7 @@ import { type ProtocolDispatch } from "./class-protocol-dispatch.js"; import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js"; import { guardClassAttributeWrites } from "./class-attributes.js"; import { applyHandleProperties } from "./handle-properties.js"; +import { installBufferValue } from "./containers/bytes.js"; import { ClassProtocols } from "./class-protocols.js"; import { earlyReadVarSymbols } from "./var-hoisting.js"; import { recordTextCodecClass } from "../../ir/ir.js"; @@ -7698,6 +7699,7 @@ export class Lowerer { trackClassFieldCreation(this, functions); guardClassAttributeWrites(this, functions); applyHandleProperties(this, functions); + installBufferValue(functions); // Globals typed by a class that never REGISTERED (a JS class whose // collection fenced — Symbol-keyed fields, an unsupported base): the @@ -13870,6 +13872,9 @@ export class Lowerer { loc, }; } + // The buffer module's Buffer is the global constructor's value. + if (bi.module === "buffer" && bi.member === "Buffer") + return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc }; if (bi.module === "fs" && (bi.member === "realpathSync" || bi.member === "realpathSync.native")) return fsRealpathValue(this, bi.member === "realpathSync.native", loc); const fn = builtinModuleFnOf(this, bi.module, bi.member); diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 8b826304b..84d41e869 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -3445,6 +3445,9 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved); } if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { + // The buffer module's Buffer (`require("buffer").Buffer`, `buffer_1.Buffer`) + // is the global constructor. + if (bufferNamespaceMember(lowerer, expr)) return "Buffer"; if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null; // Through a detection chain the checker types the receiver as the // union of every arm; the selected arm (the global object) owns the diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 68b649b07..c9c41fceb 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1378,6 +1378,11 @@ function suppressedJsStrictness7(d: ts.Diagnostic): boolean { if (JS_ANY_OPERATOR_CODES.has(d.code)) { return ts.flattenDiagnosticMessageText(d, "\n").includes("'any'"); } + // The deprecated Buffer(...) call without `new`, which Node's Buffer + // function accepts (containers/bytes.ts lowers it). + if (d.code === 2348) { + return ts.flattenDiagnosticMessageText(d, "\n").includes("'BufferConstructor'"); + } return false; } diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 0239c1624..aead4b085 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -829,12 +829,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.poolFromDyn", "buffer.poolFromDynRef", "buffer.poolFromDynOffset", + "buffer.poolFromDynRange", // The checked-dynamic compare/equals validators: Node's argument // ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably. "buffer.compareChk", "bytes.equalsChk", "bytes.compareChk", "buffer.newStringFail", + // Inherited and symbol-keyed Buffer.prototype members throw SC2020. + "buffer.prototypeMember", "fs.toUnixTimestamp", "fs.existsChk", "fs.mkdtempChk", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 6a645785f..87ddda65d 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2988,6 +2988,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** poolFromDyn that also copies an array crossing by reference. */ "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 }, "buffer.poolFromDynOffset": { argTypes: [DYN, DYN], result: BYTES_U8 }, + /** Buffer.from(value, byteOffset, length): an ArrayBuffer's range with + * Node's fromArrayBuffer checks; other values as poolFromDynOffset. */ + "buffer.poolFromDynRange": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, "buffer.poolSize": { argTypes: [], result: F64 }, "buffer.setPoolSize": { argTypes: [F64], result: VOID }, /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like @@ -3022,6 +3025,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * always throws Node's ERR_INVALID_ARG_TYPE ("The \"string\" argument * must be of type string. Received ..."). Borrowed dyn; may-throw. */ "buffer.newStringFail": { argTypes: [DYN], result: BYTES_U8 }, + "buffer.deprecation": { argTypes: [], result: VOID }, + "buffer.deprecationCount": { argTypes: [], result: VOID }, + "buffer.constructor": { argTypes: [], result: DYN }, + "buffer.installValue": { argTypes: [], result: VOID }, + "buffer.staticValue": { argTypes: [STRING], result: DYN }, + "buffer.prototypeMember": { argTypes: [DYN], result: DYN }, /** fs._toUnixTimestamp(time) over a dyn value: numeric strings and * finite numbers coerce (negatives answer now/1000, Node's shape); * everything else throws Node's ERR_INVALID_ARG_TYPE. May-throw. */ diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index df9e3c0a5..be76cee7c 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -432,6 +432,8 @@ const DYN_READ_ONLY = new Set([ "error.new", "error.newOptions", "fs.utimesSync", + // Buffer.prototype members read as values: a lookup by key. + "buffer.prototypeMember", "insp.dyn", "insp.dynS", // The dynamic-tier operators run their dyn twins (scr_js_value.c); the diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 819cfbee0..89ed37e21 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -98,6 +98,8 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "process.setDeprecationFlag", "process.setEmitWarning", "punycode.warn", + // DEP0005 for the deprecated Buffer constructor. + "buffer.deprecation", "timers.immediatePromise", // The performance timeline (scr_perf.c, linked with this unit for its // MaxPerformanceEntryBufferExceededWarning). @@ -358,6 +360,10 @@ const SYMBOL_LIB_FNS: ReadonlySet = new Set([ "dyn.usingDispose", "dyn.usingMethod", "util.isDeepStrictEqual", + // The Buffer function value's Symbol(kIsEncodingSymbol) member + "buffer.constructor", + "buffer.staticValue", + "buffer.prototypeMember", ]); /** Entry points that link the querystring unit. */ @@ -839,6 +845,7 @@ const RUNTIME_FEATURE_RULES = { ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS, "util.promisifyDyn", + "buffer.prototypeMember", ], // Constant-key reads of Object.prototype member names answer the // inherited member through scr_dyn_invoke.c. @@ -867,10 +874,22 @@ const RUNTIME_FEATURE_RULES = { /** Buffer.prototype methods called on checked-dynamic values * (scr_bytes_dyn.c). */ dynBuffer: rule({ + // Buffer.prototype members read as values dispatch to the same entries. + fns: ["buffer.prototypeMember"], expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_METHODS.has(node.method), }), /** Builtin functions read as values (scr_builtin_values.c). */ - builtinValues: rule({ fns: ["buffer.byteLenValue", "buffer.allocValue", "fs.realpathValue"] }), + builtinValues: rule({ + fns: [ + "buffer.byteLenValue", + "buffer.allocValue", + "fs.realpathValue", + "buffer.constructor", + "buffer.installValue", + "buffer.staticValue", + "buffer.prototypeMember", + ], + }), /** The stream unit emits through the emitter unit, also when the * program constructs no stream (a pipeline of functions). */ /** scr_ecdh.c over the mbedTLS archive; DEP0031 reports through the diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json new file mode 100644 index 000000000..577923b2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7527-buffer-from-array-buffer.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7527-buffer-from-array-buffer.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json new file mode 100644 index 000000000..6ef4e3917 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7528-buffer-from-array-buffer.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7528-buffer-from-array-buffer.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json new file mode 100644 index 000000000..5b199cd4c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7529-buffer-constructor.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7529-buffer-constructor.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json new file mode 100644 index 000000000..1927980cc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7530-buffer-constructor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7530-buffer-constructor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json new file mode 100644 index 000000000..b760b018a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7531-buffer-constructor-dependency/main.cjs.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/corpus/7531-buffer-constructor-dependency/main.cjs" + ], + "diags": [ + { + "code": "SC1010", + "message": "the 'legacy-buf' module is not supported yet", + "loc": { + "file": "/tests/corpus/7531-buffer-constructor-dependency/main.cjs", + "start": 362, + "end": 392 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json new file mode 100644 index 000000000..e2f78ad2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7532-buffer-constructor-check-limit/main.cjs.json @@ -0,0 +1,18 @@ +{ + "order": [ + "/tests/corpus/7532-buffer-constructor-check-limit/main.cjs" + ], + "diags": [ + { + "code": "SC1010", + "message": "the 'legacy-buf' module is not supported yet", + "loc": { + "file": "/tests/corpus/7532-buffer-constructor-check-limit/main.cjs", + "start": 279, + "end": 309 + }, + "milestone": "M4", + "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported" + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json new file mode 100644 index 000000000..de7123599 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7533-buffer-value.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7533-buffer-value.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json new file mode 100644 index 000000000..03858cfce --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7534-buffer-prototype-members.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7534-buffer-prototype-members.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json new file mode 100644 index 000000000..9831d7def --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7535-inspect-function-properties.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7535-inspect-function-properties.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index fc0128294..8ea4877c6 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -252,6 +252,10 @@ void scr_process_set_emit_warning(ScrDyn *fn); * made from node_modules) — at once for a static ES import, which Node * links before any module evaluates, otherwise on the next tick. */ void scr_punycode_warn(bool immediate, bool report); +/* The deprecated Buffer() and new Buffer() called outside node_modules + * (scr_async_dyn.c): DEP0005 on the next tick, once, within Node's + * 10000-call check (scr_buffer_deprecation_count). */ +void scr_buffer_deprecation(void); /* scr_emit_warning once a 'warning' listener exists (scr_lib.c; set by the * listener registration): units that cannot link the dyn-async unit route * their warnings through it, and print the default report otherwise. */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index a6cd813de..909d9eb8a 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -948,6 +948,22 @@ void scr_punycode_warn(bool immediate, bool report) { scr_str_release(message); } +void scr_buffer_deprecation(void) { + /* Node's showFlaggedDeprecation: after 10000 calls the check stops. */ + if (scr_buffer_ctor_warned || ++scr_buffer_ctor_checks > 10000) return; + static const char text[] = + "Buffer() is deprecated due to security and usability issues. Please use the " + "Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead."; + ScrStr *message = scr_str_new(text, sizeof text - 1); + ScrStr *code = scr_str_new("DEP0005", 7); + ScrDyn *w = scr_warning_new(message, "DeprecationWarning", code); + scr_warning_emit_deferred(w); + scr_dyn_release(w); + scr_str_release(code); + scr_str_release(message); + scr_buffer_ctor_warned = true; +} + /* The console members' warnings. Node's emitWarning emits on * process.nextTick, so these queue in order and each rides one raw tick * on the shared tick queue; any left at exit are dropped with it. */ diff --git a/packages/runtime/src/scr_builtin_values.c b/packages/runtime/src/scr_builtin_values.c index ea240359a..f8dd2b79b 100644 --- a/packages/runtime/src/scr_builtin_values.c +++ b/packages/runtime/src/scr_builtin_values.c @@ -259,3 +259,217 @@ ScrDyn *scr_fs_realpath_value(const ScrDyn *path, const ScrDyn *options, bool na scr_str_release(resolved); return out; } + +/* ── Buffer as a value ─────────────────────────────────────────────────── + * The Buffer function read as a value in JavaScript (bn.js assigns + * `require('buffer').Buffer` to a variable and passes it to + * `allocate(ArrayType, size)`): one function per thread, named Buffer + * with length 3, whose own enumerable properties are Node's, in Node's + * order: poolSize, the statics and Symbol(kIsEncodingSymbol). The statics + * apply Node's argument handling. Calling or constructing Buffer through + * the value, reading its prototype, and the copyBytesFrom and of statics + * throw SC2020. poolSize is a data property the pool reads before each + * decision (scr_buffer_pool_refresh_hook). */ +static SCR_TL ScrDyn *scr_bv_buffer; + +#define SCR_BV_ARG(i) ((i) < argc ? args[i] : scr_dyn_undefined()) + +static ScrDyn *scr_bv_unsupported(const char *what) { + char message[160]; + int n = snprintf(message, sizeof message, "%s through a stored Buffer value is not supported yet", what); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); + return NULL; +} + +/* A Buffer result as a value: +1, or NULL with the exception pending. */ +static ScrDyn *scr_bv_bytes(ScrBytes *bytes) { + if (!bytes) return NULL; + ScrBytes *buffer = scr_bytes_as_buffer(bytes); + scr_bytes_release(bytes); + ScrDyn *value = scr_dyn_new_bytes(buffer); + scr_bytes_release(buffer); + return value; +} + +static ScrDyn *scr_bv_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Calling or constructing Buffer"); +} + +static ScrDyn *scr_bv_from(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *value = SCR_BV_ARG(0), *second = SCR_BV_ARG(1), *third = SCR_BV_ARG(2); + if (value->kind != SCR_DYN_STR) return scr_bv_bytes(scr_buffer_from_dyn_range_pooled(value, second, third)); + /* fromString: an encoding that is not a non-empty string means utf8. */ + if (second->kind == SCR_DYN_STR && second->v.str->len > 0) + return scr_bv_bytes(scr_buffer_from_str_checked(value->v.str, second->v.str)); + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *bytes = scr_buffer_from_str_checked(value->v.str, utf8); + scr_str_release(utf8); + return scr_bv_bytes(bytes); +} + +static ScrDyn *scr_bv_copy_bytes_from(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Buffer.copyBytesFrom"); +} + +static ScrDyn *scr_bv_of(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Buffer.of"); +} + +static ScrDyn *scr_bv_alloc(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + return scr_bv_bytes(scr_buffer_alloc_value(SCR_BV_ARG(0), SCR_BV_ARG(1), SCR_BV_ARG(2))); +} + +static ScrDyn *scr_bv_alloc_unsafe(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *size = SCR_BV_ARG(0); + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return NULL; + } + return scr_bv_bytes(scr_buffer_alloc_unsafe(size->v.num)); +} + +static ScrDyn *scr_bv_alloc_unsafe_slow(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *size = SCR_BV_ARG(0); + if (size->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("size", "of type number", size); + return NULL; + } + return scr_bv_bytes(scr_buffer_alloc_unsafe_slow(size->v.num)); +} + +static ScrDyn *scr_bv_is_buffer(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *value = SCR_BV_ARG(0); + return scr_dyn_new_bool(value->kind == SCR_DYN_BYTES && value->buffer); +} + +static ScrDyn *scr_bv_compare(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + double order = scr_buffer_compare_chk(SCR_BV_ARG(0), SCR_BV_ARG(1)); + return scr_exc_pending() ? NULL : scr_dyn_new_num(order); +} + +static ScrDyn *scr_bv_is_encoding(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *encoding = SCR_BV_ARG(0); + return scr_dyn_new_bool(encoding->kind == SCR_DYN_STR && encoding->v.str->len > 0 && + scr_bytes_is_encoding(encoding->v.str)); +} + +/* Buffer.concat(list, length): Node validates the list, answers an empty + * Buffer for an empty list before it reads the length, and otherwise + * validates the length as an offset. */ +static ScrDyn *scr_bv_concat(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + const ScrDyn *list = SCR_BV_ARG(0), *length = SCR_BV_ARG(1); + bool empty = list->kind == SCR_DYN_ARR && list->v.arr.len == 0; + if (length->kind == SCR_DYN_UNDEF || empty || list->kind != SCR_DYN_ARR) + return scr_bv_bytes(scr_buffer_concat_dyn(list)); + if (length->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("length", "of type number", length); + return NULL; + } + return scr_bv_bytes(scr_buffer_concat_len_dyn(list, length->v.num)); +} + +static ScrDyn *scr_bv_byte_length(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + double length = scr_buffer_byte_length_value((ScrDyn *)SCR_BV_ARG(0), (ScrDyn *)SCR_BV_ARG(1)); + return scr_exc_pending() ? NULL : scr_dyn_new_num(length); +} + +static ScrDyn *scr_bv_prototype(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_bv_unsupported("Buffer.prototype"); +} + +/* The pool's setting is the value's poolSize: a number as it is, another + * primitive through ToNumber, anything else NaN (no pooling). */ +static void scr_bv_pool_refresh(void) { + ScrDyn *table = scr_dyn_function_properties(scr_bv_buffer); + const ScrDyn *size = scr_dyn_obj_get(table, "poolSize", 8); + double setting = NAN; + if (size && size->kind == SCR_DYN_NUM) { + setting = size->v.num; + } else if (size && (size->kind == SCR_DYN_STR || size->kind == SCR_DYN_BOOL || size->kind == SCR_DYN_NULL || + size->kind == SCR_DYN_UNDEF)) { + if (!scr_dyn_number_coerce_js(size, &setting)) setting = NAN; + } + scr_dyn_release(table); + scr_buffer_pool_apply(setting); +} + +/* A static `Buffer.poolSize = n` write updates the value's property. */ +static void scr_bv_pool_publish(double size) { + ScrDyn *table = scr_dyn_function_properties(scr_bv_buffer); + scr_dyn_obj_set(table, "poolSize", 8, scr_dyn_new_num(size)); + scr_dyn_release(table); +} + +static void scr_bv_cleanup(void) { + scr_buffer_pool_refresh_hook = NULL; + scr_buffer_pool_publish_hook = NULL; + scr_dyn_release(scr_bv_buffer); + scr_bv_buffer = NULL; +} + +ScrDyn *scr_buffer_constructor_value(void) { + if (scr_bv_buffer) return scr_dyn_retain(scr_bv_buffer); + static const struct { const char *name; uint32_t length; ScrDynThunk thunk; } statics[] = { + {"from", 3, scr_bv_from}, + {"copyBytesFrom", 3, scr_bv_copy_bytes_from}, + {"of", 0, scr_bv_of}, + {"alloc", 3, scr_bv_alloc}, + {"allocUnsafe", 1, scr_bv_alloc_unsafe}, + {"allocUnsafeSlow", 1, scr_bv_alloc_unsafe_slow}, + {"isBuffer", 1, scr_bv_is_buffer}, + {"compare", 2, scr_bv_compare}, + {"isEncoding", 1, scr_bv_is_encoding}, + {"concat", 2, scr_bv_concat}, + {"byteLength", 2, scr_bv_byte_length}, + }; + double pool_size = scr_buffer_pool_size(); + scr_bv_buffer = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bv_call, 3, "native:Buffer", "Buffer"); + scr_atexit(scr_bv_cleanup); + ScrDyn *table = scr_dyn_function_properties(scr_bv_buffer); + ScrDyn *getter = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_bv_prototype, 0, "native:function", "get prototype"); + scr_dyn_obj_set_accessor(table, "prototype", 9, getter, NULL); + ScrDynEntry *entry = &table->v.obj.entries[table->v.obj.len - 1]; + entry->enumerable = entry->configurable = false; + scr_dyn_obj_set(table, "poolSize", 8, scr_dyn_new_num(pool_size)); + ScrDyn *is_encoding = NULL; + for (size_t i = 0; i < sizeof statics / sizeof statics[0]; i++) { + ScrDyn *fn = scr_dyn_new_func(scr_closure_new(NULL, 0), statics[i].thunk, statics[i].length, "native:function", + statics[i].name); + if (statics[i].thunk == scr_bv_is_encoding) is_encoding = scr_dyn_retain(fn); + scr_dyn_obj_set(table, statics[i].name, strlen(statics[i].name), fn); + } + scr_dyn_release(table); + ScrStr *description = scr_str_new("kIsEncodingSymbol", 17); + ScrSym *symbol = scr_sym_new(description); + scr_str_release(description); + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_sym_release(symbol); + scr_dyn_symbol_key_set(scr_bv_buffer, key, is_encoding); + scr_dyn_release(key); + scr_dyn_release(is_encoding); + scr_buffer_pool_refresh_hook = scr_bv_pool_refresh; + scr_buffer_pool_publish_hook = scr_bv_pool_publish; + return scr_dyn_retain(scr_bv_buffer); +} + +void scr_buffer_install_value(void) { scr_bytes_buffer_constructor_fn = scr_buffer_constructor_value; } + +ScrDyn *scr_buffer_static_value(const ScrStr *name) { + ScrDyn *buffer = scr_buffer_constructor_value(); + ScrDyn *member = scr_dyn_fn_get(buffer, name->data, name->len); + scr_dyn_release(buffer); + return member; +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 7bcfd0cfb..907c177dc 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -2787,16 +2787,30 @@ static SCR_TL bool scr_pool_registered; /* scr_buffer_pool_reset runs at context /* Buffer.poolSize >>> 1, kept with the setting. */ static SCR_TL size_t scr_pool_half = 32768; -double scr_buffer_pool_size(void) { return scr_pool_setting; } +SCR_TL void (*scr_buffer_pool_refresh_hook)(void) = NULL; +SCR_TL void (*scr_buffer_pool_publish_hook)(double size) = NULL; -void scr_buffer_set_pool_size(double size) { +double scr_buffer_pool_size(void) { + if (scr_buffer_pool_refresh_hook) scr_buffer_pool_refresh_hook(); + return scr_pool_setting; +} + +void scr_buffer_pool_apply(double size) { scr_pool_setting = size; double t = isfinite(size) ? fmod(trunc(size), 4294967296.0) : 0; if (t < 0) t += 4294967296.0; scr_pool_half = (size_t)((uint32_t)t >> 1); } -static size_t scr_pool_threshold(void) { return scr_pool_half; } +void scr_buffer_set_pool_size(double size) { + scr_buffer_pool_apply(size); + if (scr_buffer_pool_publish_hook) scr_buffer_pool_publish_hook(size); +} + +static size_t scr_pool_threshold(void) { + if (scr_buffer_pool_refresh_hook) scr_buffer_pool_refresh_hook(); + return scr_pool_half; +} /* Bytes left in the slab. Aligning can move the offset past the end of a * slab whose size is not a multiple of 8; nothing is left then. */ diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 9b382dda5..cca8364b1 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -388,6 +388,13 @@ ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc); * slab. A slab lives while any of its views does. All return +1 Buffers. */ double scr_buffer_pool_size(void); void scr_buffer_set_pool_size(double size); +/* The setting without publishing it (scr_buffer_pool_refresh_hook). */ +void scr_buffer_pool_apply(double size); +/* Set while a Buffer function value exists (scr_builtin_values.c): its + * poolSize property is the setting, which the refresh hook reads before + * each pool decision and the publish hook receives from static writes. */ +extern SCR_TL void (*scr_buffer_pool_refresh_hook)(void); +extern SCR_TL void (*scr_buffer_pool_publish_hook)(double size); ScrBytes *scr_buffer_alloc(double size); /* zeroed, never pooled */ ScrBytes *scr_buffer_alloc_unsafe(double size); /* pooled */ ScrBytes *scr_buffer_alloc_unsafe_slow(double size); /* never pooled */ @@ -398,6 +405,15 @@ ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second); +/* Buffer.from(value, byteOffset, length): an ArrayBuffer's view with + * Node's fromArrayBuffer checks; other values as the two-argument form. */ +ScrBytes *scr_buffer_from_dyn_range_pooled(const ScrDyn *value, const ScrDyn *second, + const ScrDyn *third); +/* The deprecated Buffer constructor's DEP0005 state (scr_bytes_io.c): a + * call from node_modules counts toward Node's 10000-call check. */ +extern SCR_TL bool scr_buffer_ctor_warned; +extern SCR_TL uint64_t scr_buffer_ctor_checks; +void scr_buffer_deprecation_count(void); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); /* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list @@ -406,6 +422,17 @@ ScrBytes *scr_buffer_concat_dyn(const struct ScrDyn *list); ScrBytes *scr_buffer_alloc_value(const struct ScrDyn *size, const struct ScrDyn *fill, const struct ScrDyn *encoding); struct ScrDyn *scr_fs_realpath_value(const struct ScrDyn *path, const struct ScrDyn *options, bool native); +/* The Buffer function as a value (scr_builtin_values.c), one per thread. +1. */ +struct ScrDyn *scr_buffer_constructor_value(void); +/* Installs that function as a Buffer's `.constructor` (the program's + * %main calls it when it reads the value): scr_bytes_buffer_constructor_fn. */ +void scr_buffer_install_value(void); +extern struct ScrDyn *(*scr_bytes_buffer_constructor_fn)(void); +/* A static of that value by name (`Buffer.alloc` read as a value). +1. */ +struct ScrDyn *scr_buffer_static_value(const ScrStr *name); +/* `Buffer.prototype[key]` (scr_bytes_dyn.c). +1, or NULL with the + * exception pending. */ +struct ScrDyn *scr_buffer_prototype_member(const struct ScrDyn *key); ScrBytes *scr_buffer_concat_len_dyn(const struct ScrDyn *list, double total); /* Drops the pool's slab reference and restores Buffer.poolSize (process * exit audits and library session resets). */ diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c index c492d43b1..a65cdf239 100644 --- a/packages/runtime/src/scr_bytes_dyn.c +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -592,3 +592,206 @@ ScrDyn *scr_dyn_invoke_buffer(ScrDyn *recv, ScrDyn *callee, const char *method, } return scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what); } + +/* A fixed-width read or write name (readUInt8, writeDoubleLE, ...). */ +static bool scr_bytes_dyn_num_method_name(const char *method) { + bool read = !strncmp(method, "read", 4); + if (!read && strncmp(method, "write", 5)) return false; + const char *rest = method + (read ? 4 : 5); + for (size_t at = 0; at < sizeof scr_bytes_dyn_names / sizeof scr_bytes_dyn_names[0]; at++) + if (!strcmp(rest, scr_bytes_dyn_names[at].name)) return true; + return false; +} + +/* ── Buffer.prototype members read as values ──────────────────────────── + * `Buffer.prototype[name]` (smart-buffer's typeof probe for the BigInt + * methods): an own member answers one function per name, with the name and + * length Node's function has; the parent and offset getters, read on the + * prototype itself, answer undefined; constructor is the Buffer value. + * Members inherited from Uint8Array.prototype and Object.prototype, symbol + * keys and object keys throw SC2020, and other keys answer undefined. The + * functions run the method on a Buffer receiver (`.call(buf, ...)`); other + * receivers throw SC2020. */ +static const struct { const char *key; const char *name; uint32_t length; } scr_buffer_proto_own[] = { + {"readBigUInt64LE", "readBigUInt64LE", 0}, + {"readBigUInt64BE", "readBigUInt64BE", 0}, + {"readBigUint64LE", "readBigUInt64LE", 0}, + {"readBigUint64BE", "readBigUInt64BE", 0}, + {"readBigInt64LE", "readBigInt64LE", 0}, + {"readBigInt64BE", "readBigInt64BE", 0}, + {"writeBigUInt64LE", "writeBigUInt64LE", 1}, + {"writeBigUInt64BE", "writeBigUInt64BE", 1}, + {"writeBigUint64LE", "writeBigUInt64LE", 1}, + {"writeBigUint64BE", "writeBigUInt64BE", 1}, + {"writeBigInt64LE", "writeBigInt64LE", 1}, + {"writeBigInt64BE", "writeBigInt64BE", 1}, + {"readUIntLE", "readUIntLE", 2}, + {"readUInt32LE", "", 1}, + {"readUInt16LE", "", 1}, + {"readUInt8", "", 1}, + {"readUIntBE", "readUIntBE", 2}, + {"readUInt32BE", "", 1}, + {"readUInt16BE", "", 1}, + {"readUintLE", "readUIntLE", 2}, + {"readUint32LE", "", 1}, + {"readUint16LE", "", 1}, + {"readUint8", "", 1}, + {"readUintBE", "readUIntBE", 2}, + {"readUint32BE", "", 1}, + {"readUint16BE", "", 1}, + {"readIntLE", "readIntLE", 2}, + {"readInt32LE", "", 1}, + {"readInt16LE", "", 1}, + {"readInt8", "", 1}, + {"readIntBE", "readIntBE", 2}, + {"readInt32BE", "", 1}, + {"readInt16BE", "", 1}, + {"writeUIntLE", "writeUIntLE", 3}, + {"writeUInt32LE", "writeUInt32LE", 1}, + {"writeUInt16LE", "writeUInt16LE", 1}, + {"writeUInt8", "writeUInt8", 1}, + {"writeUIntBE", "writeUIntBE", 3}, + {"writeUInt32BE", "writeUInt32BE", 1}, + {"writeUInt16BE", "writeUInt16BE", 1}, + {"writeUintLE", "writeUIntLE", 3}, + {"writeUint32LE", "writeUInt32LE", 1}, + {"writeUint16LE", "writeUInt16LE", 1}, + {"writeUint8", "writeUInt8", 1}, + {"writeUintBE", "writeUIntBE", 3}, + {"writeUint32BE", "writeUInt32BE", 1}, + {"writeUint16BE", "writeUInt16BE", 1}, + {"writeIntLE", "writeIntLE", 3}, + {"writeInt32LE", "writeInt32LE", 1}, + {"writeInt16LE", "writeInt16LE", 1}, + {"writeInt8", "writeInt8", 1}, + {"writeIntBE", "writeIntBE", 3}, + {"writeInt32BE", "writeInt32BE", 1}, + {"writeInt16BE", "writeInt16BE", 1}, + {"readFloatLE", "readFloatForwards", 0}, + {"readFloatBE", "readFloatBackwards", 0}, + {"readDoubleLE", "readDoubleForwards", 0}, + {"readDoubleBE", "readDoubleBackwards", 0}, + {"writeFloatLE", "writeFloatForwards", 1}, + {"writeFloatBE", "writeFloatBackwards", 1}, + {"writeDoubleLE", "writeDoubleForwards", 1}, + {"writeDoubleBE", "writeDoubleBackwards", 1}, + {"asciiSlice", "", 2}, + {"base64Slice", "", 2}, + {"base64urlSlice", "", 2}, + {"latin1Slice", "", 2}, + {"hexSlice", "", 2}, + {"ucs2Slice", "", 2}, + {"utf8Slice", "", 2}, + {"asciiWrite", "", 3}, + {"base64Write", "", 3}, + {"base64urlWrite", "", 3}, + {"latin1Write", "", 3}, + {"hexWrite", "", 3}, + {"ucs2Write", "", 3}, + {"utf8Write", "", 3}, + {"parent", NULL, 0}, + {"offset", NULL, 0}, + {"copy", "copy", 4}, + {"toString", "toString", 3}, + {"equals", "equals", 1}, + {"inspect", "inspect", 2}, + {"compare", "compare", 5}, + {"indexOf", "indexOf", 4}, + {"lastIndexOf", "lastIndexOf", 4}, + {"includes", "includes", 4}, + {"fill", "fill", 4}, + {"write", "write", 4}, + {"toJSON", "toJSON", 0}, + {"subarray", "subarray", 2}, + {"slice", "slice", 2}, + {"swap16", "swap16", 0}, + {"swap32", "swap32", 0}, + {"swap64", "swap64", 0}, + {"toLocaleString", "toString", 3}, +}; + +static const char *const scr_buffer_proto_inherited[] = { + "BYTES_PER_ELEMENT", "buffer", "byteLength", "byteOffset", "length", "entries", "keys", "values", + "at", "copyWithin", "every", "filter", "find", "findIndex", "findLast", "findLastIndex", + "forEach", "join", "map", "reverse", "reduce", "reduceRight", "set", "some", "sort", "toReversed", + "toSorted", "with", "__defineGetter__", "__defineSetter__", "hasOwnProperty", "__lookupGetter__", + "__lookupSetter__", "isPrototypeOf", "propertyIsEnumerable", "valueOf", "__proto__", +}; + +static SCR_TL ScrDyn *scr_buffer_proto_fns; + +static void scr_buffer_proto_cleanup(void) { + scr_dyn_release(scr_buffer_proto_fns); + scr_buffer_proto_fns = NULL; +} + +static ScrDyn *scr_buffer_proto_unsupported(const char *format, const char *name) { + char message[200]; + int n = snprintf(message, sizeof message, format, name); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); + return NULL; +} + +static ScrDyn *scr_buffer_proto_run(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc) { + if (!scr_bytes_dyn_buffer(recv)) + return scr_buffer_proto_unsupported("Buffer.prototype.%s on a receiver that is not a Buffer is not supported yet", + method); + const char *what = "Buffer.prototype method"; + /* The method-id cache slot of a call site, here a fresh one per call. */ + int32_t slot = -1; + int32_t *mid = &slot; + if (scr_bytes_dyn_num_method_name(method)) return scr_dyn_invoke_buffer_num(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "write")) return scr_dyn_invoke_buffer_write(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "fill")) return scr_dyn_invoke_buffer_fill(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "toString") || !strcmp(method, "toLocaleString")) + return scr_dyn_invoke_buffer_to_string(recv, NULL, "toString", mid, args, argc, what); + if (!strcmp(method, "indexOf") || !strcmp(method, "lastIndexOf") || !strcmp(method, "includes")) + return scr_dyn_invoke_buffer_search(recv, NULL, method, mid, args, argc, what); + if (!strcmp(method, "subarray") || !strcmp(method, "slice")) + return scr_dyn_invoke(recv, method, args, argc, what); + if (!strcmp(method, "inspect")) + return scr_buffer_proto_unsupported("Buffer.prototype.%s through a stored value is not supported yet", method); + return scr_dyn_invoke_buffer(recv, NULL, method, mid, args, argc, what); +} + +static ScrDyn *scr_buffer_proto_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *recv = scr_dyn_this_get(); + ScrDyn *result = scr_buffer_proto_run(recv, closure->fn, args, argc); + scr_dyn_release(recv); + return result; +} + +ScrDyn *scr_buffer_prototype_member(const ScrDyn *key) { + if (key->kind == SCR_DYN_SYMBOL) + return scr_buffer_proto_unsupported("Buffer.prototype members keyed by %s are not supported yet", "symbols"); + if (key->kind != SCR_DYN_STR) { + if (key->kind == SCR_DYN_NUM || key->kind == SCR_DYN_BOOL || key->kind == SCR_DYN_UNDEF || + key->kind == SCR_DYN_NULL || key->kind == SCR_DYN_BIGINT) + return scr_dyn_retain(scr_dyn_undefined()); + return scr_buffer_proto_unsupported("Buffer.prototype members keyed by %s are not supported yet", "objects"); + } + const ScrStr *name = key->v.str; + if (name->len == 11 && memcmp(name->data, "constructor", 11) == 0) return scr_buffer_constructor_value(); + for (size_t i = 0; i < sizeof scr_buffer_proto_inherited / sizeof scr_buffer_proto_inherited[0]; i++) { + const char *inherited = scr_buffer_proto_inherited[i]; + if (strlen(inherited) == name->len && memcmp(inherited, name->data, name->len) == 0) + return scr_buffer_proto_unsupported("Buffer.prototype.%s is not supported yet", inherited); + } + for (size_t i = 0; i < sizeof scr_buffer_proto_own / sizeof scr_buffer_proto_own[0]; i++) { + const char *own = scr_buffer_proto_own[i].key; + if (strlen(own) != name->len || memcmp(own, name->data, name->len) != 0) continue; + if (!scr_buffer_proto_own[i].name) return scr_dyn_retain(scr_dyn_undefined()); + if (!scr_buffer_proto_fns) { + scr_buffer_proto_fns = scr_dyn_new_obj_null_proto(); + scr_atexit(scr_buffer_proto_cleanup); + } + ScrDyn *fn = scr_dyn_obj_get(scr_buffer_proto_fns, own, name->len); + if (!fn) { + fn = scr_dyn_new_func(scr_closure_new((void *)own, 0), scr_buffer_proto_call, scr_buffer_proto_own[i].length, + "native:function", scr_buffer_proto_own[i].name); + scr_dyn_obj_set(scr_buffer_proto_fns, own, name->len, fn); + } + return scr_dyn_retain(fn); + } + return scr_dyn_retain(scr_dyn_undefined()); +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 137208938..c93c67cdc 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1299,13 +1299,16 @@ ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { return scr_buffer_concat_dyn_impl(list, total, true); } -/* Buffer.from(arrayBuffer, byteOffset) over a checked ArrayBuffer or - * SharedArrayBuffer, as Node's fromArrayBuffer: a Buffer from the offset to - * the end of the buffer that shares its memory. The offset converts with - * ToNumber, where a missing offset (NULL) or undefined and NaN are 0; an - * offset past the end throws ERR_BUFFER_OUT_OF_BOUNDS, and one at or below - * -1 V8's start-offset RangeError. NULL with the exception pending. */ -static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *offset_value) { +/* Buffer.from(arrayBuffer, byteOffset, length) over a checked ArrayBuffer + * or SharedArrayBuffer, as Node's fromArrayBuffer: a Buffer that shares its + * memory. The offset converts with ToNumber, where a missing offset (NULL) + * or undefined and NaN are 0; an offset past the end throws + * ERR_BUFFER_OUT_OF_BOUNDS, and one at or below -1 V8's start-offset + * RangeError. A missing length runs to the end; a given one converts with + * ToNumber, past the end throws ERR_BUFFER_OUT_OF_BOUNDS for "length", and + * zero, negative and NaN lengths are 0. NULL with the exception pending. */ +static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *offset_value, + const ScrDyn *length_value) { double offset = 0; if (offset_value && offset_value->kind != SCR_DYN_UNDEF) { if (!scr_dyn_number_coerce_js(offset_value, &offset)) return NULL; @@ -1313,11 +1316,26 @@ static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *of } ScrBytes *bytes = storage->v.handle.ptr; ScrBytes *owner = bytes->backing ? bytes->backing : bytes; - if ((double)scr_bytes_byte_len(owner) - offset < 0) { + double max_length = (double)scr_bytes_byte_len(owner) - offset; + if (max_length < 0) { static const char msg[] = "\"offset\" is outside of buffer bounds"; scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); return NULL; } + bool has_length = length_value && length_value->kind != SCR_DYN_UNDEF; + double length = max_length; + if (has_length) { + if (!scr_dyn_number_coerce_js(length_value, &length)) return NULL; + if (length > 0) { + if (length > max_length) { + static const char msg[] = "\"length\" is outside of buffer bounds"; + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); + return NULL; + } + } else { + length = 0; + } + } double start = trunc(offset); if (start < 0) { /* The offset as JavaScript prints it (-1.5, -1e+21, -Infinity). */ @@ -1331,7 +1349,7 @@ static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *of scr_throw_error(SCR_ERR_RANGE, message); return NULL; } - ScrBytes *out = scr_bytes_buffer_view(bytes, SCR_BYTES_U8, start, false, 0); + ScrBytes *out = scr_bytes_buffer_view(bytes, SCR_BYTES_U8, start, has_length, trunc(length)); if (out) out->is_buffer = true; return out; } @@ -1340,7 +1358,7 @@ ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); /* The second argument here is an encoding name, which as a byte offset * converts to NaN: offset 0. */ - if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL, NULL); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes); if (value->kind == SCR_DYN_ARR) { return scr_buffer_from_array_like(value, (double)value->v.arr.len); @@ -1386,7 +1404,7 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding return scr_buffer_from_bytes(value->v.bytes); /* A view of an ArrayBuffer shares its memory, so it never comes from the * pool. The second argument here is an encoding name: offset 0. */ - if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL, NULL); ScrBytes *out = scr_buffer_from_dyn(value, encoding); if (!out) return NULL; out->is_buffer = true; @@ -1422,12 +1440,32 @@ ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *s scr_str_release(utf8); return out; } - return scr_buffer_from_storage(value, second); + return scr_buffer_from_storage(value, second, NULL); +} + +/* Buffer.from(value, second, third): an ArrayBuffer or SharedArrayBuffer + * takes them as its byte offset and length; other values convert as + * scr_buffer_from_dyn_offset_pooled does, without the third argument. */ +ScrBytes *scr_buffer_from_dyn_range_pooled(const ScrDyn *value, const ScrDyn *second, + const ScrDyn *third) { + if (!scr_buffer_storage_is(value)) return scr_buffer_from_dyn_offset_pooled(value, second); + return scr_buffer_from_storage(value, second, third); +} + +/* The deprecated Buffer constructor's DEP0005 state, Node's + * bufferWarningAlreadyEmitted and nodeModulesCheckCounter: a call from + * node_modules only counts toward the 10000-call check, and + * scr_buffer_deprecation (scr_async_dyn.c) reports for the others. */ +SCR_TL bool scr_buffer_ctor_warned = false; +SCR_TL uint64_t scr_buffer_ctor_checks = 0; + +void scr_buffer_deprecation_count(void) { + if (!scr_buffer_ctor_warned) scr_buffer_ctor_checks++; } /* new Buffer(number, encoding) — the deprecated ctor's string arm with a - * non-string first argument: Node's exact ERR_INVALID_ARG_TYPE (the - * throwing path never fires DEP0005, so compiled silence matches). */ + * non-string first argument: Node's exact ERR_INVALID_ARG_TYPE, thrown + * after scr_buffer_deprecation has reported DEP0005. */ ScrBytes *scr_buffer_new_string_fail(const ScrDyn *got) { scr_dyn_arg_type_fail("string", "of type string", got); return NULL; diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 553b897d2..e93a9a0f0 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1328,6 +1328,10 @@ ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, si * for anonymous) and "length" (the boxed arity). Returns +1, or NULL when * the key answers nothing (the caller's undefined). Never throws. */ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len); +/* A function value's own property table, created on first use. +1. */ +ScrDyn *scr_dyn_function_properties(const ScrDyn *function); +/* The table a function value already has. +1, or NULL. */ +ScrDyn *scr_dyn_function_properties_existing(const ScrDyn *function); /* Object.defineProperties over dyn values (targets: OBJ and FUNC): each * descriptor's `value` becomes an own data property. OBJ targets preserve * writable/enumerable/configurable; FUNC targets still use plain properties. diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 1dce8d3c2..79b2564ff 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1600,7 +1600,45 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } else { ib_cstr(&out, "[Function (anonymous)]"); } - return ib_take(&out); + /* A function's own enumerable properties follow its base, as for an + * object (`[Function: f] { x: 1 }`), and past the depth limit it is + * `[Function]` (formatRaw). Classes keep the base. */ + ScrDyn *table = d->v.fn.class_obj ? NULL : scr_dyn_function_properties_existing(d); + bool shown = false; + for (size_t i = 0; table && i < table->v.obj.len && !shown; i++) shown = table->v.obj.entries[i].enumerable; + for (size_t i = 0; table && !shown && table->symbol_keys && i < table->symbol_keys->v.arr.len; i++) + shown = scr_insp_symbol_entry(table, i) != NULL; + if (!shown) { + scr_dyn_release(table); + return ib_take(&out); + } + /* Every value of one function shares its table: the table is the + * function's identity for the circular checks. */ + double circular = scr_insp_circ_check(table); + if (circular != 0) { + scr_dyn_release(table); + free(out.data); + return scr_insp_circular(circular); + } + if (recurse > depth) { + scr_dyn_release(table); + free(out.data); + return scr_str_new("[Function]", 10); + } + ScrStr *base = ib_take(&out); + char *cls = malloc(base->len + 1); + if (!cls) scr_trap("scriptc: out of memory\n"); + memcpy(cls, base->data, base->len); + cls[base->len] = '\0'; + scr_str_release(base); + size_t nseen = g_nseen; + g_pending_seen = table; + ScrStr *rendered = scr_insp_obj(table, recurse, depth, cls); + g_pending_seen = NULL; + if (g_nseen > nseen) g_nseen = nseen; + free(cls); + scr_dyn_release(table); + return rendered; } case SCR_DYN_HANDLE: { if (d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 615785687..c0154ab57 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2366,6 +2366,10 @@ static ScrDyn *scr_dyn_fn_existing_table(const ScrDyn *function) { return owner->props ? (ScrDyn *)scr_box_get_ref(owner->props) : NULL; } +ScrDyn *scr_dyn_function_properties_existing(const ScrDyn *function) { + return scr_dyn_fn_existing_table(function); +} + ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; } @@ -4200,6 +4204,9 @@ bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const Sc } if (scr_array_buffer_ctor && callee->v.fn.clo == scr_array_buffer_ctor->v.fn.clo) return scr_array_buffer_is(value); if (scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return value->kind == SCR_DYN_ARR || scr_dyn_ref_is_array(value); + /* The Buffer function value: Buffer instances, not other Uint8Arrays. */ + if (callee->v.fn.sig && strcmp(callee->v.fn.sig, "native:Buffer") == 0) + return value->kind == SCR_DYN_BYTES && value->buffer; for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) { if (scr_bytes_ctors[i] && callee->v.fn.clo == scr_bytes_ctors[i]->v.fn.clo) return scr_dyn_typed_array_is(value, (int)i); @@ -4262,6 +4269,7 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { return scr_bytes_instanceof_at(value, callee, NULL); } SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) = NULL; +ScrDyn *(*scr_bytes_buffer_constructor_fn)(void) = NULL; ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) { if (callee->kind == SCR_DYN_FUNC && scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) @@ -4282,6 +4290,14 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS ScrDyn *constructed = NULL; if (scr_dyn_global_construct_fn && scr_dyn_global_construct_fn(callee, args, &constructed)) return constructed; + /* The Buffer function value (scr_builtin_values.c): Node's construction + * reports DEP0005 by the caller's location, which a stored value does + * not know. */ + if (callee->kind == SCR_DYN_FUNC && callee->v.fn.sig && strcmp(callee->v.fn.sig, "native:Buffer") == 0) { + static const char message[] = "Calling or constructing Buffer through a stored Buffer value is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; + } ScrJsonBuf message; scr_jb_init(&message); scr_jb_write(&message, what->data, what->len); @@ -4311,7 +4327,9 @@ ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) { return scr_dyn_new_num((double)scr_bytes_elem_size(bytes->elem)); if (key->len == 11 && memcmp(key->data, "constructor", 11) == 0) { if (!value->buffer) return scr_bytes_constructor_for(bytes->elem); - /* Buffer retains its separate builtin identity. */ + /* A program that reads the Buffer function's value answers it; + * otherwise Buffer keeps its separate builtin identity token. */ + if (scr_bytes_buffer_constructor_fn) return scr_bytes_buffer_constructor_fn(); char token[64]; int length = snprintf(token, sizeof token, "[builtin %s]", value->buffer ? "Buffer" : scr_bytes_elem_name(bytes->elem)); ScrStr *name = scr_str_new(token, (size_t)length); @@ -5219,6 +5237,8 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { return (ScrDyn *)scr_box_get_ref(owner->props); } +ScrDyn *scr_dyn_function_properties(const ScrDyn *function) { return scr_dyn_fn_properties(function); } + ScrDyn *scr_dyn_class_inherit(ScrDyn *constructor, ScrDyn *base) { if (constructor->kind != SCR_DYN_FUNC || !constructor->v.fn.class_obj || (base->kind != SCR_DYN_FUNC && base->kind != SCR_DYN_UNDEF)) { static const char message[] = "Class inheritance requires native constructors"; diff --git a/tests/corpus/2570-buffer-arg-validation.cjs b/tests/corpus/2570-buffer-arg-validation.cjs index d49593a30..c0980e495 100644 --- a/tests/corpus/2570-buffer-arg-validation.cjs +++ b/tests/corpus/2570-buffer-arg-validation.cjs @@ -1,4 +1,3 @@ -// @no-deprecation // Buffer's invalid-argument ladders, Node-exact: Buffer.compare / // buf.compare / buf.equals over non-buffer values and bad offsets throw // ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE with Node's argument names and @@ -7,6 +6,8 @@ // real answers. Plus the deprecated `new Buffer(number, enc)` string-arm // rejection and the ERR_* codes on the write/swap/fill range ladders. 'use strict'; +// The deprecated constructor's DEP0005 report carries a process id. +process.noDeprecation = true; const show = (fn) => { try { console.log('ret', fn()); diff --git a/tests/corpus/7527-buffer-from-array-buffer.ts b/tests/corpus/7527-buffer-from-array-buffer.ts new file mode 100644 index 000000000..fb650e9f2 --- /dev/null +++ b/tests/corpus/7527-buffer-from-array-buffer.ts @@ -0,0 +1,33 @@ +// Buffer.from(arrayBuffer, byteOffset, length): Node's fromArrayBuffer over +// typed and untyped ArrayBuffers and SharedArrayBuffers: the offset and +// length conversions, the shared memory, zero and negative lengths, and +// ERR_BUFFER_OUT_OF_BOUNDS for an offset or length past the end. +const ab = new ArrayBuffer(6); +new Uint8Array(ab).set([1, 2, 3, 4, 5, 6]); +const offsets: unknown[] = [undefined, NaN, "1", 1.5, -1, 0, 6, 7, null, true]; +const lengths: unknown[] = [undefined, 0, -1, 2.5, "2", 6, 9, NaN, null]; +for (const o of offsets) { + for (const l of lengths) { + try { + const b = Buffer.from(ab, o as number, l as number); + console.log(String(o), String(l), b, b.byteOffset, b.length); + } catch (e) { + const err = e as NodeJS.ErrnoException; + console.log(String(o), String(l), err.name, err.code, err.message); + } + } +} +const typed = Buffer.from(ab, 2, 2); +typed[0] = 99; +console.log(new Uint8Array(ab)[2], Buffer.from(ab, 4).toString("hex"), Buffer.isBuffer(Buffer.from(ab, 1, 1))); +const shared = new SharedArrayBuffer(4); +console.log(Buffer.from(shared, 1, 2).length); +const loose: any = new ArrayBuffer(4); +const pairs: [unknown, unknown][] = [[1, 2], [4, undefined], [5, 1], [0, 5], ["2", "1"]]; +for (const [o, l] of pairs) { + try { + console.log(Buffer.from(loose, o as number, l as number).toString("hex")); + } catch (e) { + console.log((e as NodeJS.ErrnoException).code, (e as Error).message); + } +} diff --git a/tests/corpus/7528-buffer-from-array-buffer.cjs b/tests/corpus/7528-buffer-from-array-buffer.cjs new file mode 100644 index 000000000..e77b5d55b --- /dev/null +++ b/tests/corpus/7528-buffer-from-array-buffer.cjs @@ -0,0 +1,31 @@ +// Buffer.from(arrayBuffer, byteOffset, length) in JavaScript, with offsets +// and lengths of every kind and an ArrayBuffer held in an untyped slot. +const ab = new ArrayBuffer(6); +new Uint8Array(ab).set([1, 2, 3, 4, 5, 6]); +const offsets = [undefined, NaN, "1", 1.5, -1, 0, 6, 7, null, true]; +const lengths = [undefined, 0, -1, 2.5, "2", 6, 9, NaN, null]; +for (const o of offsets) { + for (const l of lengths) { + try { + const b = Buffer.from(ab, o, l); + console.log(String(o), String(l), b, b.byteOffset, b.length); + } catch (e) { + const err = e; + console.log(String(o), String(l), err.name, err.code, err.message); + } + } +} +const typed = Buffer.from(ab, 2, 2); +typed[0] = 99; +console.log(new Uint8Array(ab)[2], Buffer.from(ab, 4).toString("hex"), Buffer.isBuffer(Buffer.from(ab, 1, 1))); +const shared = new SharedArrayBuffer(4); +console.log(Buffer.from(shared, 1, 2).length); +const slots = { buffer: new ArrayBuffer(3) }; +new Uint8Array(slots.buffer).set([9, 8, 7]); +for (const args of [[1], [1, 1], [0, 0], [3, 0], [2, 5], [-0.5, 2]]) { + try { + console.log(args.join(","), Buffer.from(slots.buffer, args[0], args[1])); + } catch (e) { + console.log(args.join(","), e.code, e.message); + } +} diff --git a/tests/corpus/7529-buffer-constructor.cjs b/tests/corpus/7529-buffer-constructor.cjs new file mode 100644 index 000000000..3181cebec --- /dev/null +++ b/tests/corpus/7529-buffer-constructor.cjs @@ -0,0 +1,80 @@ +// The deprecated Buffer constructor, called and constructed, as Node's +// Buffer function runs it: the arguments evaluate in order, the first call +// reports DEP0005 on the next tick (once), a number allocates zero-filled +// bytes, and other values convert as Buffer.from does. +// The program re-runs itself and prints the child's output with process +// ids normalized. +"use strict"; +const { spawnSync } = require("node:child_process"); +const { Buffer: Imported } = require("buffer"); + +function child() { + process.on("warning", (w) => console.log("listener", w.name, w.code, w.message.length)); + const B = Buffer; + + const show = (label, fn) => { + try { + const b = fn(); + console.log(label, b, b.length, Buffer.isBuffer(b)); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } + }; + + // The throwing form still reports the warning, after the catch. + show("number+encoding", () => new Buffer(3, "utf8")); + show("size", () => new Buffer(4)); + show("fraction", () => new Buffer(2.7)); + show("zero", () => Buffer(0)); + show("negative", () => new Buffer(-1)); + show("nan", () => new Buffer(NaN)); + show("string", () => new Buffer("héllo")); + show("hex", () => new Buffer("6869", "hex")); + show("base64", () => Buffer("aGk=", "base64")); + show("empty encoding", () => new Buffer("x", "")); + show("bad encoding", () => new Buffer("x", "nope")); + show("array", () => new Buffer([1, 2, 300, -1])); + show("array-like", () => new Buffer({ length: 2, 0: 7, 1: 8 })); + show("buffer", () => new B(Buffer.from("copy"))); + show("typed", () => Buffer(new Uint16Array([1, 258]))); + const storage = new Uint8Array([10, 20, 30, 40, 50]).buffer; + show("arraybuffer", () => new Buffer(storage)); + show("offset", () => new Buffer(storage, 1)); + show("range", () => Buffer(storage, 1, 3)); + show("range past end", () => new Buffer(storage, 6)); + show("string third", () => new Buffer("abc", "latin1", 1)); + show("undefined", () => new Buffer(undefined)); + show("null", () => Buffer(null)); + show("none", () => new Buffer()); + show("imported", () => new Imported("imp")); + + console.log(Buffer(2), Buffer("ab").length, Buffer("hi").toString("hex"), new Buffer([5])); + console.log(Buffer.isBuffer(Buffer(1)), Buffer(1) instanceof Uint8Array, new Buffer(1) instanceof Uint8Array); + + // The view shares the ArrayBuffer's memory. + const view = new Buffer(storage, 2, 2); + new Uint8Array(storage)[2] = 99; + console.log(view); + + const order = []; + const arg = (v) => (order.push(v), v); + new Buffer(arg("a"), arg("utf8")); + Buffer(arg(2)); + console.log(order); + + console.log("sync end"); + process.nextTick(() => console.log("tick")); + setTimeout(() => { + new Buffer(1); + console.log("timer"); + }, 0); +} + +if (process.argv[2] === "child") { + child(); +} else { + const run = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7530-buffer-constructor.ts b/tests/corpus/7530-buffer-constructor.ts new file mode 100644 index 000000000..14eac61d5 --- /dev/null +++ b/tests/corpus/7530-buffer-constructor.ts @@ -0,0 +1,42 @@ +// The deprecated `new Buffer(...)` in TypeScript, over the forms +// @types/node declares: strings with an encoding, sizes, arrays and +// ArrayBuffers. The first call reports DEP0005 on the next tick. +// The program re-runs itself and prints the child's output with process +// ids normalized. +import { spawnSync } from "node:child_process"; + +function child(): void { + process.on("warning", (w) => console.log("listener", w.name, (w as { code?: string }).code)); + + function sized(n: number): Buffer { + return new Buffer(n); + } + + const encodings: BufferEncoding[] = ["utf8", "hex", "base64", "latin1"]; + for (const encoding of encodings) { + console.log(encoding, new Buffer("6869", encoding)); + } + console.log(sized(3), sized(0).length); + console.log(new Buffer([104, 105]).toString(), new Buffer(new Uint8Array([1, 2]))); + const storage = new ArrayBuffer(4); + const view = new Buffer(storage); + view[1] = 5; + console.log(new Uint8Array(storage), view.buffer === storage); + const text: string = "typed"; + console.log(new Buffer(text).toString("hex")); + try { + sized(-2); + } catch (e) { + console.log((e as Error).name, (e as { code?: string }).code); + } + console.log("sync end"); +} + +if (process.argv[2] === "child") { + child(); +} else { + const run = spawnSync(process.execPath, [process.argv[1]!, "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7531-buffer-constructor-dependency/main.cjs b/tests/corpus/7531-buffer-constructor-dependency/main.cjs new file mode 100644 index 000000000..75b130bc6 --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/main.cjs @@ -0,0 +1,36 @@ +// The deprecated Buffer constructor called from a dependency under +// node_modules: Node reports no DEP0005 for those calls, and the program's +// own first call reports it once, on the next tick. +// The program re-runs itself and prints the child's output with process +// ids normalized. +"use strict"; +const { spawnSync } = require("node:child_process"); +const legacy = require("legacy-buf"); + +function child() { + process.on("warning", (w) => console.log("listener", w.name, w.code)); + + console.log(legacy.construct("hi", "utf8"), legacy.construct(3), legacy.call([1, 2])); + const storage = new Uint8Array([1, 2, 3, 4]).buffer; + console.log(legacy.construct(storage, 1, 2), legacy.many(5)); + try { + legacy.construct(4, "hex"); + } catch (e) { + console.log(e.code); + } + process.nextTick(() => { + console.log("tick"); + console.log(new Buffer("own")); + console.log(Buffer(2), new Buffer([9])); + setTimeout(() => console.log("timer"), 0); + }); +} + +if (process.argv[2] === "child") { + child(); +} else { + const run = spawnSync(process.execPath, [process.argv[1], "child"], { encoding: "utf8" }); + console.log(`status: ${run.status}`); + console.log(run.stdout); + console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)")); +} diff --git a/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts new file mode 100644 index 000000000..ef314d625 --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.d.ts @@ -0,0 +1,3 @@ +export declare function construct(value: unknown, encodingOrOffset?: unknown, length?: unknown): Buffer; +export declare function call(value: unknown): Buffer; +export declare function many(count: number): number; diff --git a/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js new file mode 100644 index 000000000..062e4e41d --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/index.js @@ -0,0 +1,15 @@ +"use strict"; +// A dependency that keeps the deprecated Buffer constructor. +exports.construct = function (value, encodingOrOffset, length) { + if (length !== undefined) return new Buffer(value, encodingOrOffset, length); + if (encodingOrOffset !== undefined) return new Buffer(value, encodingOrOffset); + return new Buffer(value); +}; +exports.call = function (value) { + return Buffer(value); +}; +exports.many = function (count) { + let total = 0; + for (let i = 0; i < count; i++) total += new Buffer(1).length; + return total; +}; diff --git a/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json new file mode 100644 index 000000000..db2277b60 --- /dev/null +++ b/tests/corpus/7531-buffer-constructor-dependency/node_modules/legacy-buf/package.json @@ -0,0 +1,5 @@ +{ + "name": "legacy-buf", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/corpus/7532-buffer-constructor-check-limit/main.cjs b/tests/corpus/7532-buffer-constructor-check-limit/main.cjs new file mode 100644 index 000000000..c08690a83 --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/main.cjs @@ -0,0 +1,10 @@ +// Node checks the caller of the deprecated Buffer constructor for its first +// 10000 calls only: once a dependency under node_modules has made them, the +// program's own calls report no DEP0005. +"use strict"; +process.on("warning", (w) => console.log("listener", w.code)); +const legacy = require("legacy-buf"); + +console.log(legacy.many(10000)); +console.log(new Buffer("after"), Buffer(1)); +setTimeout(() => console.log("timer"), 0); diff --git a/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts new file mode 100644 index 000000000..ef314d625 --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.d.ts @@ -0,0 +1,3 @@ +export declare function construct(value: unknown, encodingOrOffset?: unknown, length?: unknown): Buffer; +export declare function call(value: unknown): Buffer; +export declare function many(count: number): number; diff --git a/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js new file mode 100644 index 000000000..062e4e41d --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/index.js @@ -0,0 +1,15 @@ +"use strict"; +// A dependency that keeps the deprecated Buffer constructor. +exports.construct = function (value, encodingOrOffset, length) { + if (length !== undefined) return new Buffer(value, encodingOrOffset, length); + if (encodingOrOffset !== undefined) return new Buffer(value, encodingOrOffset); + return new Buffer(value); +}; +exports.call = function (value) { + return Buffer(value); +}; +exports.many = function (count) { + let total = 0; + for (let i = 0; i < count; i++) total += new Buffer(1).length; + return total; +}; diff --git a/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json new file mode 100644 index 000000000..db2277b60 --- /dev/null +++ b/tests/corpus/7532-buffer-constructor-check-limit/node_modules/legacy-buf/package.json @@ -0,0 +1,5 @@ +{ + "name": "legacy-buf", + "version": "1.0.0", + "main": "index.js" +} diff --git a/tests/corpus/7533-buffer-value.cjs b/tests/corpus/7533-buffer-value.cjs new file mode 100644 index 000000000..3fd500ec2 --- /dev/null +++ b/tests/corpus/7533-buffer-value.cjs @@ -0,0 +1,101 @@ +// The Buffer function read as a value: one identity across the global, the +// buffer module and its aliases, Node's own enumerable members in Node's +// order, statics that apply Node's argument handling, instanceof, and a +// poolSize property the pool reads. +"use strict"; +const buffer = require("buffer"); +const nodeBuffer = require("node:buffer"); +const util = require("util"); + +// bn.js keeps the module's Buffer in a variable assigned under try. +var BnBuffer; +try { + BnBuffer = require("buffer").Buffer; +} catch (e) {} +const pass = (value) => value; +const Stored = pass(require("buffer").Buffer); +const Alias = Buffer; +const table = { Buffer: buffer.Buffer }; + +console.log(typeof Stored, Stored.name, Stored.length, Alias.name, Alias.length); +console.log(Stored === Buffer, Stored === globalThis.Buffer, Stored === buffer.Buffer, Stored === nodeBuffer.Buffer); +console.log(Stored === Alias, table.Buffer === Stored, Stored.constructor === Function); +console.log(Stored); +console.log(Object.keys(Stored)); +console.log(util.inspect({ nested: { Buffer: Stored } }, { depth: 1 })); + +// Statics through the value. +const show = (label, run) => { + try { + console.log(label, run()); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +}; +show("from string", () => Stored.from("héllo")); +show("from hex", () => Stored.from("6869", "hex")); +show("from empty encoding", () => Stored.from("x", "")); +show("from bad encoding", () => Stored.from("x", "nope")); +show("from array", () => Stored.from([1, 2, 300])); +const storage = new Uint8Array([1, 2, 3, 4, 5]).buffer; +show("from range", () => Stored.from(storage, 1, 3)); +show("from number", () => Stored.from(5)); +show("alloc", () => Stored.alloc(3, "ab")); +show("alloc fill", () => Stored.alloc(2, 7)); +show("alloc string size", () => Stored.alloc("3")); +show("allocUnsafe", () => Stored.allocUnsafe(4).fill(9)); +show("allocUnsafe negative", () => Stored.allocUnsafe(-1)); +show("allocUnsafe string", () => Stored.allocUnsafe("4")); +show("allocUnsafeSlow", () => Stored.allocUnsafeSlow(2).fill(1)); +show("isBuffer", () => [Stored.isBuffer(Buffer.alloc(1)), Stored.isBuffer(new Uint8Array(1)), Stored.isBuffer("x")]); +show("isEncoding", () => [Stored.isEncoding("UTF-8"), Stored.isEncoding(""), Stored.isEncoding("nope"), Stored.isEncoding(1)]); +show("compare", () => Stored.compare(Buffer.from("a"), Buffer.from("b"))); +show("compare bad", () => Stored.compare("a", Buffer.from("b"))); +show("concat", () => Stored.concat([Buffer.from("ab"), Buffer.from("cd")])); +show("concat length", () => Stored.concat([Buffer.from("ab"), Buffer.from("cd")], 3)); +show("concat empty", () => Stored.concat([], "x")); +show("concat bad length", () => Stored.concat([Buffer.from("a")], "x")); +show("concat bad list", () => Stored.concat("x")); +show("byteLength", () => [Stored.byteLength("héllo"), Stored.byteLength("aGk=", "base64"), Stored.byteLength(new Uint16Array(3))]); + +// Member values keep one identity, with Node's names and lengths. +const members = [ + [Buffer.from, Stored.from], + [Buffer.alloc, Stored.alloc], + [Buffer.allocUnsafe, Stored.allocUnsafe], + [Buffer.isBuffer, Stored.isBuffer], + [Buffer.compare, Stored.compare], + [Buffer.concat, Stored.concat], + [Buffer.byteLength, Stored.byteLength], + [Buffer.isEncoding, Stored.isEncoding], +]; +console.log(members.map(([a, b]) => `${a.name}/${a.length}/${a === b}`).join(" ")); +const isBuffer = require("buffer").Buffer.isBuffer; +console.log(isBuffer(Buffer.from("x")), isBuffer(null), typeof Buffer.allocUnsafe); + +// A bn.js-style allocator over Buffer or Array. +function allocate(ArrayType, size) { + if (ArrayType.allocUnsafe) return ArrayType.allocUnsafe(size); + return new ArrayType(size); +} +function toArrayLike(ArrayType, size) { + const made = allocate(ArrayType, size); + made.fill(4); + return made; +} +function toBuffer(size) { + return BnBuffer ? toArrayLike(BnBuffer, size) : null; +} +const made = toBuffer(3); +console.log(made, toArrayLike(Array, 2), made instanceof Stored, made instanceof Buffer); +console.log(new Uint8Array(2) instanceof Stored, Buffer.from("x") instanceof Alias, pass("x") instanceof Stored); + +// poolSize: one setting through the value and the static spelling. +console.log(Stored.poolSize, Buffer.poolSize); +Buffer.poolSize = 1024; +console.log(Stored.poolSize); +const small = Buffer.allocUnsafe(600); +console.log(small.buffer.byteLength, Buffer.allocUnsafe(400).buffer.byteLength); +Stored.poolSize = 2048; +console.log(Buffer.poolSize, Buffer.allocUnsafe(600).buffer.byteLength); +Stored.poolSize = 65536; diff --git a/tests/corpus/7534-buffer-prototype-members.cjs b/tests/corpus/7534-buffer-prototype-members.cjs new file mode 100644 index 000000000..2fd347373 --- /dev/null +++ b/tests/corpus/7534-buffer-prototype-members.cjs @@ -0,0 +1,29 @@ +// Buffer.prototype members read as values, as smart-buffer probes and +// calls them: own methods are functions with Node's names and lengths that +// run on a Buffer receiver, the parent and offset getters read on the +// prototype answer undefined, constructor is Buffer, and absent names are +// undefined. +"use strict"; +const buffer_1 = require("buffer"); + +function check(method) { + if (typeof buffer_1.Buffer.prototype[method] === "undefined") return `${method}: missing`; + return `${method}: ${typeof buffer_1.Buffer.prototype[method]}`; +} +console.log(["writeBigInt64BE", "readBigUInt64LE", "readUInt8", "nope", "parent", "offset"].map(check)); +console.log(Buffer.prototype.constructor === Buffer, Buffer.prototype["constructor"] === buffer_1.Buffer); +console.log(Buffer.prototype[0], Buffer.prototype[undefined], Buffer.prototype.toJSON === Buffer.prototype.toJSON); + +const names = ["readUInt8", "readUint8", "readFloatLE", "writeUInt16LE", "readUIntLE", "toString", "toLocaleString", "slice", "hexSlice"]; +console.log(names.map((n) => `${n}=${JSON.stringify(Buffer.prototype[n].name)}/${Buffer.prototype[n].length}`).join(" ")); + +const buf = Buffer.alloc(8); +function write(func, value, offset) { + return func.call(buf, value, offset); +} +console.log(write(Buffer.prototype.writeUInt16LE, 513, 0), write(Buffer.prototype.writeInt8, -2, 2), buf); +console.log(Buffer.prototype.readUInt16LE.call(buf, 0), Buffer.prototype.readInt8.call(buf, 2)); +console.log(Buffer.prototype.toString.call(buf, "hex", 0, 3), Buffer.prototype.toLocaleString.call(Buffer.from("hi"))); +console.log(Buffer.prototype.slice.call(buf, 0, 2), Buffer.prototype.equals.call(buf, Buffer.alloc(8))); +console.log(Buffer.prototype.fill.call(Buffer.alloc(3), 1), Buffer.prototype.indexOf.call(Buffer.from("abc"), "c")); +console.log(Buffer.prototype.write.call(buf, "zz", 4), buf, Buffer.prototype.toJSON.call(Buffer.from([1]))); diff --git a/tests/corpus/7535-inspect-function-properties.cjs b/tests/corpus/7535-inspect-function-properties.cjs new file mode 100644 index 000000000..5e3f8c6a6 --- /dev/null +++ b/tests/corpus/7535-inspect-function-properties.cjs @@ -0,0 +1,29 @@ +// util.inspect of functions with own enumerable properties, as Node's +// formatRaw prints them: the function's base then its keys (symbols +// included), `[Function]` past the depth limit, and the plain base when +// every own property is hidden. +"use strict"; +const util = require("util"); + +function named() {} +named.x = 1; +const arrow = () => {}; +arrow.a = "s"; +arrow[Symbol("k")] = 2; +Object.defineProperty(arrow, "hidden", { value: 3, enumerable: false }); +function hiddenOnly() {} +Object.defineProperty(hiddenOnly, "secret", { value: 1, enumerable: false }); +function big() {} +for (let i = 0; i < 8; i++) big["key" + i] = i; +function self() {} +self.me = self; + +const pass = (value) => value; + +console.log(named, arrow, hiddenOnly); +console.log(util.inspect(named, { depth: 0 })); +console.log(util.inspect(pass({ wrap: named }), { depth: 0 })); +console.log(util.inspect(pass([named, arrow]))); +console.log(util.inspect(big)); +console.log(util.inspect(self)); +console.log(util.inspect(pass({ a: { b: { f: big } } }))); diff --git a/tests/harness/corpus-costs/7527-buffer-from-array-buffer.ts.json b/tests/harness/corpus-costs/7527-buffer-from-array-buffer.ts.json new file mode 100644 index 000000000..593c181c3 --- /dev/null +++ b/tests/harness/corpus-costs/7527-buffer-from-array-buffer.ts.json @@ -0,0 +1 @@ +{ "plain": 4043, "san": 6242 } diff --git a/tests/harness/corpus-costs/7528-buffer-from-array-buffer.cjs.json b/tests/harness/corpus-costs/7528-buffer-from-array-buffer.cjs.json new file mode 100644 index 000000000..759dce51e --- /dev/null +++ b/tests/harness/corpus-costs/7528-buffer-from-array-buffer.cjs.json @@ -0,0 +1 @@ +{ "plain": 3823, "san": 5864 } diff --git a/tests/harness/corpus-costs/7529-buffer-constructor.cjs.json b/tests/harness/corpus-costs/7529-buffer-constructor.cjs.json new file mode 100644 index 000000000..408808da4 --- /dev/null +++ b/tests/harness/corpus-costs/7529-buffer-constructor.cjs.json @@ -0,0 +1 @@ +{ "plain": 5309, "san": 56270 } diff --git a/tests/harness/corpus-costs/7530-buffer-constructor.ts.json b/tests/harness/corpus-costs/7530-buffer-constructor.ts.json new file mode 100644 index 000000000..287d50fdc --- /dev/null +++ b/tests/harness/corpus-costs/7530-buffer-constructor.ts.json @@ -0,0 +1 @@ +{ "plain": 4282, "san": 5688 } diff --git a/tests/harness/corpus-costs/7531-buffer-constructor-dependency/main.cjs.json b/tests/harness/corpus-costs/7531-buffer-constructor-dependency/main.cjs.json new file mode 100644 index 000000000..568483d98 --- /dev/null +++ b/tests/harness/corpus-costs/7531-buffer-constructor-dependency/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 5222, "san": 6614 } diff --git a/tests/harness/corpus-costs/7532-buffer-constructor-check-limit/main.cjs.json b/tests/harness/corpus-costs/7532-buffer-constructor-check-limit/main.cjs.json new file mode 100644 index 000000000..3d262fe41 --- /dev/null +++ b/tests/harness/corpus-costs/7532-buffer-constructor-check-limit/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 4707, "san": 5678 } diff --git a/tests/harness/corpus-costs/7533-buffer-value.cjs.json b/tests/harness/corpus-costs/7533-buffer-value.cjs.json new file mode 100644 index 000000000..126c82265 --- /dev/null +++ b/tests/harness/corpus-costs/7533-buffer-value.cjs.json @@ -0,0 +1 @@ +{ "plain": 4874, "san": 10466 } diff --git a/tests/harness/corpus-costs/7534-buffer-prototype-members.cjs.json b/tests/harness/corpus-costs/7534-buffer-prototype-members.cjs.json new file mode 100644 index 000000000..624b7f12f --- /dev/null +++ b/tests/harness/corpus-costs/7534-buffer-prototype-members.cjs.json @@ -0,0 +1 @@ +{ "plain": 3944, "san": 5992 } diff --git a/tests/harness/corpus-costs/7535-inspect-function-properties.cjs.json b/tests/harness/corpus-costs/7535-inspect-function-properties.cjs.json new file mode 100644 index 000000000..58c48c344 --- /dev/null +++ b/tests/harness/corpus-costs/7535-inspect-function-properties.cjs.json @@ -0,0 +1 @@ +{ "plain": 3458, "san": 4785 } diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index 905f648dd..7f86c0dc5 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -148,9 +148,9 @@ function nodeOracleFile(file: string): string { } /** `// @no-deprecation` in the directive head: the Node oracle runs with - * --no-deprecation — for programs exercising deprecated-API ERROR paths - * (new Buffer's ctor ladder) whose DEP warnings carry a pid and can never - * byte-compare; the compiled runtime emits no process warnings at all. */ + * --no-deprecation, for programs whose DEP warnings carry a pid and cannot + * compare. The compiled program does not read the directive; a program + * whose compiled form reports the warning sets process.noDeprecation. */ function wantsNoDeprecation(file: string): boolean { return directiveHead(file).some((l) => /^\/\/ @no-deprecation\s*$/.test(l)); } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 319e7329f..39ecd9e79 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -2050,3 +2050,52 @@ console.log(words.length); expect(r.stdout).toBe(`${fence}${fence}1\n`); }); }); + +// The Buffer function read as a value models Node's statics; the forms it +// cannot answer as Node does throw SC2020: calling or constructing it +// through the value (Node reports DEP0005 by the caller's location), its +// prototype as a whole, copyBytesFrom and of, and Buffer.prototype members +// that are inherited, symbol-keyed or called on other receivers. +describe("Buffer value refusals", () => { + test("unmodeled Buffer value forms throw SC2020", async () => { + const r = await compileAndRun( + "buffer-value-refusals", + `const pass = (value) => value; +const Stored = pass(require("buffer").Buffer); +const runs = [ + () => Stored("x"), + () => new Stored(2), + () => Stored.prototype, + () => Stored.copyBytesFrom(new Uint8Array(2)), + () => Stored.of(1), + () => Buffer.prototype.map, + () => Buffer.prototype[Symbol.iterator], + () => Buffer.prototype.readUInt8.call(new Uint8Array(2), 0), +]; +for (const run of runs) { + try { + run(); + console.log("ran"); + } catch (e) { + console.log(e.name, e.code, e.message); + } +} +`, + "cjs", + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + [ + "Error SC2020 Calling or constructing Buffer through a stored Buffer value is not supported yet", + "Error SC2020 Calling or constructing Buffer through a stored Buffer value is not supported yet", + "Error SC2020 Buffer.prototype through a stored Buffer value is not supported yet", + "Error SC2020 Buffer.copyBytesFrom through a stored Buffer value is not supported yet", + "Error SC2020 Buffer.of through a stored Buffer value is not supported yet", + "Error SC2020 Buffer.prototype.map is not supported yet", + "Error SC2020 Buffer.prototype members keyed by symbols are not supported yet", + "Error SC2020 Buffer.prototype.readUInt8 on a receiver that is not a Buffer is not supported yet", + "", + ].join("\n"), + ); + }); +}); From 9c9fb6531a8857425049ffb700069ff008c3e730 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:07:29 -0500 Subject: [PATCH 060/409] Replay class objects with their base names, and restore the inspector after a refusal - The emitter journal that forked workers merge (#1919) records each class object entry whole, and the entries carry baseNameSym beside nameSym. The replay cast named only nameSym, which the native compiler refuses (SC1090), so the self-hosted LLVM emitter did not build. - An inspection refusal (the 'not supported yet' Error of insp.dyn) propagates out of the emitted inspect helpers before they close their frames and before util.inspect's layout options are restored. The open frames, their indentation and the custom layout then shaped every later console.log and util.inspect. A top-level render through helpers that can reach a checked-dynamic view, a generator or a class instance now runs in a lifted wrapper that takes insp.mark first and calls insp.unwind in a finally. A render under breakLength, compact or sorted restores the default layout in a finally the same way. - A non-enumerable accessor that %o shows, and any accessor of a checked-dynamic object, renders as [Getter], [Setter] or [Getter/Setter], as Node prints it, instead of its value slot. - Under %o, an array's non-enumerable own properties follow its named ones in brackets (`[ 1, 2, [length]: 2, foo: 'x', [hid]: 3 ]`), as Node prints them; corpus 6357 covers [length] ahead of named properties. --- packages/compiler/src/backend/llvm/emitter.ts | 2 +- .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../src/frontend/lowering/lower-inspect.ts | 174 +++++++++++++++--- .../compiler/src/ir/builtin-signatures.ts | 5 + packages/runtime/src/scr_inspect.c | 73 ++++++-- packages/runtime/src/scr_inspect.h | 4 + tests/corpus/6357-format-o-hidden.ts | 18 +- tests/harness/dyncheck.test.ts | 66 +++++++ 8 files changed, 306 insertions(+), 38 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 004277a24..7c6fd7a47 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -4859,7 +4859,7 @@ export class LlEmitter { return; } case "classObj": - addEntry(this.classObjs, key, value as { nameSym: string }); + addEntry(this.classObjs, key, value as { nameSym: string; baseNameSym: string | null }); return; case "retainWrapper": addEntry(this.nullableUnions.retainWrappers, key, value as string); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index dfc5327e2..fc4b1337b 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -663,6 +663,8 @@ export const LIB_FN_SYMS: Record = { "insp.moreItems": "scr_insp_more_items", "insp.end": "scr_insp_end", "insp.nestedBegin": "scr_insp_nested_begin", + "insp.mark": "scr_insp_mark", + "insp.unwind": "scr_insp_unwind", "insp.nestedEnd": "scr_insp_nested_end", // Circular references over cycle-capable composites (Node's // seen/circular machinery — none of these throw; refWrap borrows its diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index cda4d2972..349fec120 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -900,6 +900,112 @@ export function inspectExpr( recurse: IrExpr, depth: IrExpr, loc: SrcLoc, +): IrExpr { + const render = inspectExprBody(lowerer, t, value, recurse, depth, loc); + // A top-level render through emitted helpers that can reach a refusal + // (the 'not supported yet' Error of insp.dyn) unwinds the inspector to + // its state before the render on every exit: the Error propagates out of + // the helpers before they close their frames. + if ( + render.kind !== "call" || + recurse.kind !== "numLit" || + recurse.value !== 0 || + !inspectMayRefuse(lowerer, t) + ) + return render; + return withInspectUnwind(lowerer, render, t, loc); +} + +/** A top-level helper render (`call helper(v, r, d)`) through a lifted + * wrapper that takes insp.mark first and runs insp.unwind however the + * render ends: `try { return helper(v, r, d); } finally { insp.unwind(); }`. + * A seqExpr cannot hold the try. */ +function withInspectUnwind( + lowerer: Lowerer, + render: IrExpr & { kind: "call" }, + t: IrType, + loc: SrcLoc, +): IrExpr { + const key = `unwind:${render.callee}`; + let name = lowerer.inspectHelpers.get(key); + if (name === undefined) { + name = keyedHelperName(lowerer, "%util.inspUnwound", key); + lowerer.inspectHelpers.set(key, name); + const params = [ + { localId: "v.0", name: "v", type: t }, + { localId: "r.0", name: "r", type: F64 }, + { localId: "d.0", name: "d", type: F64 }, + ]; + const call = (fn: "insp.mark" | "insp.unwind"): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "libCall", fn, args: [], type: VOID, loc }, + loc, + }); + lowerer.liftedFns.push({ + name, + params, + returnType: STRING, + locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + body: [ + call("insp.mark"), + { + kind: "tryCatch", + tryBody: [ + { + kind: "return", + value: { + kind: "call", + callee: render.callee, + args: params.map((p) => varRef(p.localId, p.type, loc)), + type: STRING, + loc, + }, + loc, + }, + ], + catchBody: null, + catchLocalId: null, + finallyBody: [call("insp.unwind")], + loc, + }, + ], + loc, + }); + } + return { ...render, callee: name }; +} + +/** Whether inspecting a `t` value can end in an inspection refusal: `t` + * or a type reachable from it is a checked-dynamic value, a generator, a + * Dirent row or a class instance, which can render through a + * checked-dynamic view. */ +function inspectMayRefuse(lowerer: Lowerer, t: IrType): boolean { + const seen = new Set(); + const walk = (u: IrType): boolean => { + if (u.kind === "dyn" || u.kind === "generator") return true; + if (u.kind === "record") { + const shape = lowerer.shapes.get(u.shapeId); + if (shape && isDirentShape(shape)) return true; + } + // A class instance can render through its checked-dynamic view, which + // a helper built earlier may only learn at the end of lowering + // (classInspectsAddedProperties). + if (u.kind === "object") return true; + const k = typeKey(u); + if (seen.has(k)) return false; + seen.add(k); + return typeConstituents(lowerer, u).some(walk); + }; + return walk(t); +} + +function inspectExprBody( + lowerer: Lowerer, + t: IrType, + value: IrExpr, + recurse: IrExpr, + depth: IrExpr, + loc: SrcLoc, ): IrExpr { // A Dirent row renders through its dyn view: Node's `Dirent { ... }` // with the Symbol(type) key. @@ -2350,40 +2456,55 @@ function parseInspectOptions( /** A render under non-default breakLength/compact/sorted: the value is evaluated * first, the layout applies to this render only, and the defaults return - * afterwards. */ + * however the render ends, an inspection refusal included. The render runs + * in a lifted function, `(v) => { insp.layout(...); try { return render(v); } + * finally { insp.layout(80, 3, false); } }`, since a seqExpr cannot hold the + * try; `tag` names the render for the function's key. */ function withInspectLayout( lowerer: Lowerer, options: InspectOptions, value: IrExpr, + tag: string, render: (value: IrExpr) => IrExpr, loc: SrcLoc, ): IrExpr { if (options.breakLength === 80 && options.compact === 3 && !options.sorted) return render(value); - const input = lowerer.declareHiddenLocal("%inspectInput", value.type); - const text = lowerer.declareHiddenLocal("%inspectText", STRING); - const layout = (breakLength: number, compact: number, sorted: boolean): IrStmt => ({ - kind: "exprStmt", - expr: { - kind: "libCall", - fn: "insp.layout", - args: [numLit(breakLength, loc), numLit(compact, loc), boolLit(sorted, loc)], - type: VOID, + const key = `layout:${tag}:${typeKey(value.type)}:${options.breakLength}:${options.compact}:${options.sorted}:${options.depth}`; + let name = lowerer.inspectHelpers.get(key); + if (name === undefined) { + name = keyedHelperName(lowerer, "%util.inspLayout", key); + lowerer.inspectHelpers.set(key, name); + const layout = (breakLength: number, compact: number, sorted: boolean): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "insp.layout", + args: [numLit(breakLength, loc), numLit(compact, loc), boolLit(sorted, loc)], + type: VOID, + loc, + }, loc, - }, - loc, - }); - return { - kind: "seqExpr", - stmts: [ - { kind: "varDecl", localId: input.id, init: value, loc }, - layout(options.breakLength, options.compact, options.sorted), - { kind: "varDecl", localId: text.id, init: render(varRef(input.id, value.type, loc)), loc }, - layout(80, 3, false), - ], - result: varRef(text.id, STRING, loc), - type: STRING, - loc, - }; + }); + lowerer.liftedFns.push({ + name, + params: [{ localId: "v.0", name: "v", type: value.type }], + returnType: STRING, + locals: [{ id: "v.0", name: "v", type: value.type, mutable: false }], + body: [ + layout(options.breakLength, options.compact, options.sorted), + { + kind: "tryCatch", + tryBody: [{ kind: "return", value: render(varRef("v.0", value.type, loc)), loc }], + catchBody: null, + catchLocalId: null, + finallyBody: [layout(80, 3, false)], + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: name, args: [value], type: STRING, loc }; } /** `util.inspect(fn)` / `util.inspect(Cls)` over a DIRECT identifier @@ -2500,6 +2621,7 @@ export function lowerInspectValue( lowerer, options, value, + "typedArray", (input) => ({ kind: "libCall", fn: "insp.typedArray", @@ -2525,6 +2647,7 @@ export function lowerInspectValue( lowerer, options, value, + "dirDyn", (input) => ({ kind: "libCall", fn: "console.dirDyn", @@ -2539,6 +2662,7 @@ export function lowerInspectValue( lowerer, options, value, + "inspect", (input) => inspectExpr(lowerer, value.type, input, numLit(0, loc), numLit(depth, loc), loc), loc, ); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index a316b5dd5..6dc8a6826 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4414,6 +4414,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * `prefix inner` re-indented at the caller's level (+1). */ "insp.nestedBegin": { argTypes: [], result: VOID }, "insp.nestedEnd": { argTypes: [STRING, STRING], result: STRING }, + /** Around a top-level render that an inspection refusal can end: mark + * records the inspector's frame and nesting depth, unwind (in the + * render's finally) closes what the render left open above it. */ + "insp.mark": { argTypes: [], result: VOID }, + "insp.unwind": { argTypes: [], result: VOID }, /** node:string_decoder's utf8 StringDecoder (scr_string.c): the decoder * value is a one-field record whose f64 PACKS the pending partial * sequence (count + up to 3 raw bytes — Node buffers at most 3 for diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 9875a7077..e63c1cec1 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -646,6 +646,54 @@ ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner) { return ib_take(&out); } +/* The inspector's state at the start of a top-level render. A render that + * an inspection refusal ends leaves its compiled frames open: the Error + * propagates out of the emitted helpers before their scr_insp_end and + * scr_insp_nested_end calls. The emitted render takes a mark first and + * unwinds to it on every exit (lower-inspect.ts), so later renders start + * from the state the refused one found. */ +typedef struct { + size_t nframes; + size_t nsaved; + size_t nseen; + size_t indent; + double cur_depth; +} InspMark; + +static SCR_TL InspMark *g_marks; +static SCR_TL size_t g_nmarks; +static SCR_TL size_t g_marks_cap; + +void scr_insp_mark(void) { + if (g_nmarks == g_marks_cap) { + g_marks_cap = g_marks_cap ? g_marks_cap * 2 : 4; + g_marks = realloc(g_marks, g_marks_cap * sizeof(InspMark)); + if (!g_marks) insp_oom(); + } + g_marks[g_nmarks++] = (InspMark){g_nframes, g_nsaved, g_nseen, g_indent, g_cur_depth}; +} + +void scr_insp_unwind(void) { + InspMark m = g_marks[--g_nmarks]; + while (g_nframes > m.nframes) { + InspFrame *f = &g_frames[--g_nframes]; + for (size_t i = 0; i < f->len; i++) scr_str_release(f->items[i]); + free(f->items); + } + while (g_nsaved > m.nsaved) { + InspSaved *sv = &g_saved[--g_nsaved]; + free(g_seen); + g_seen = sv->seen; + g_seen_cap = sv->seen_cap; + memcpy(g_circ, sv->circ, sizeof g_circ); + g_ncirc = sv->ncirc; + } + if (g_nseen > m.nseen) g_nseen = m.nseen; + g_indent = m.indent; + g_cur_depth = m.cur_depth; + g_pending_seen = NULL; +} + /* ── string values (formatPrimitive's string half) ───────────────────── */ /* Byte offset of UTF-16 unit `units` — when the boundary splits an astral @@ -1422,7 +1470,7 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char for (size_t i = 0; i < d->v.obj.len; i++) { ScrDynEntry *ent = &d->v.obj.entries[i]; if (!insp_obj_shows(ent)) continue; - ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth); + ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; if (ent->enumerable) { insp_key_into(&eb, ent->key, ent->key_len); @@ -1443,7 +1491,7 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char ScrDynEntry *ent = scr_insp_symbol_entry(d, i); if (!ent) continue; ScrStr *key = scr_insp_dyn(d->symbol_keys->v.arr.items[i], recurse + 1, depth); - ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth); + ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; /* A hidden symbol key is bracketed like a hidden string key. */ if (!ent->enumerable) ib_char(&eb, '['); @@ -1635,10 +1683,11 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { case SCR_DYN_ARR: { size_t len = d->v.arr.len; /* Named and noncanonical numeric properties follow the elements as - * `key: value` entries (Node's array extras). */ + * `key: value` entries (Node's array extras); under showHidden the + * own `length` comes first, and non-enumerable properties show too. */ const ScrDyn *props = d->v.arr.properties; size_t nprops = 0; - for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += props->v.obj.entries[i].enumerable; + for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += insp_obj_shows(&props->v.obj.entries[i]); if (len == 0 && nprops == 0 && !g_show_hidden) return scr_str_new("[]", 2); if (recurse > depth) return scr_str_new("[Array]", 7); scr_insp_begin(recurse + 1); @@ -1672,9 +1721,9 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { scr_str_release(m); } if (g_show_hidden) { - /* The own non-enumerable `length`, after the items and before the - * other own properties. Node's grouping reads value[k] for every - * output position k, this one included. */ + /* The own non-enumerable `length`, after the items. Node's + * grouping reads value[k] for every output position k, this one + * included. */ char line[48]; int n = snprintf(line, sizeof line, "[length]: %zu", len); ScrStr *s = scr_str_new(line, (size_t)n); @@ -1685,10 +1734,13 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } for (size_t k = 0; props && k < props->v.obj.len; k++) { const ScrDynEntry *ent = &props->v.obj.entries[k]; - if (!ent->enumerable) continue; + if (!insp_obj_shows(ent)) continue; ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth); InspBuf eb = {0}; + /* formatProperty's hidden key: the key's rendering in brackets. */ + if (!ent->enumerable) ib_char(&eb, '['); insp_key_into(&eb, ent->key, ent->key_len); + if (!ent->enumerable) ib_char(&eb, ']'); ib_cstr(&eb, ": "); ib_bytes(&eb, val->data, val->len); scr_str_release(val); @@ -1702,9 +1754,8 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { entries++; } /* Node's grouping leaves out the last entry when there are more - * entries than maxArrayLength (the `... more items` entry when the - * array has no properties, or under showHidden the `[length]` - * entry). */ + * entries than maxArrayLength (100): the `... more items` entry, + * `[length]` under showHidden, or the last property. */ bool last_out = entries > 100; ScrStr *base = scr_str_new("", 0); ScrStr *b0 = scr_str_new("[", 1); diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index 9bf18a23c..f36335a3b 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -76,6 +76,10 @@ ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse, * caller's level (+1; borrows both). */ void scr_insp_nested_begin(void); ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner); +/* Mark the inspector's state before a top-level render, and return to the + * mark when the render ends, normally or by an inspection refusal. */ +void scr_insp_mark(void); +void scr_insp_unwind(void); /* The buf.read* / buf.write* numeric families on u8 bytes, Node-exact: * boundsError's ladder (non-integer offset → 'It must be an integer', a diff --git a/tests/corpus/6357-format-o-hidden.ts b/tests/corpus/6357-format-o-hidden.ts index 9e48ed4ba..44ed1d772 100644 --- a/tests/corpus/6357-format-o-hidden.ts +++ b/tests/corpus/6357-format-o-hidden.ts @@ -1,6 +1,7 @@ // util.format's %o over values reached through `unknown` renders with // showHidden, as Node does: arrays show their hidden `[length]` (with -// Node's grouping), at every depth, inside Maps and records too. +// Node's grouping), at every depth, inside Maps and records too, ahead of +// an array's named properties. import { format } from "node:util"; const cases: [string, unknown][] = [ ["ten", Array.from({ length: 10 }, (_, i) => i)], @@ -17,3 +18,18 @@ const cases: [string, unknown][] = [ ["date", [new Date(0)]], ]; for (const [k, v] of cases) console.log(k, format("%o", v)); +// The hidden `[length]` comes before an array's named properties, and the +// grid's last-entry rule counts both. +type Bag = Record; +const named = [1, 2]; +(named as unknown as Bag).foo = "x"; +const tagged = Array.from({ length: 100 }, (_, i) => i); +(tagged as unknown as Bag).tag = "t"; +const bare: number[] = []; +(bare as unknown as Bag).k = 1; +for (const [k, v] of [ + ["named", named], + ["tagged", tagged], + ["bare", bare], +] as [string, unknown][]) + console.log(k, format("%o", v)); diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index dab51a430..21e67d085 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -1311,6 +1311,72 @@ console.log("after"); expect(r.stderr).toContain(`Uncaught Error: ${refusal}`); }); + test("accessors reached through unknown render as Node's accessor tags", async () => { + const r = await compileAndRun( + "inspect-accessor-tags", + `import { format } from "node:util"; +const o: unknown = JSON.parse('{"x":1}'); +Object.defineProperty(o, "g", { get: () => 2, enumerable: true }); +Object.defineProperty(o, "h", { get: () => 3, enumerable: false }); +Object.defineProperty(o, "gs", { get: () => 4, set: (_v: number) => {}, enumerable: false }); +Object.defineProperty(o, "s", { set: (_v: number) => {}, enumerable: true }); +console.log(o); +console.log(format("%o", o)); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + [ + "{ x: 1, g: [Getter], s: [Setter] }", + "{", + " x: 1,", + " g: [Getter],", + " [h]: [Getter],", + " [gs]: [Getter/Setter],", + " s: [Setter]", + "}", + "", + ].join("\n"), + ); + }); + + test("a refused inspection leaves later renders at the default layout", async () => { + // The refusal's Error skips the call site's layout restore and the + // render's own exits; later renders must not keep the custom layout, + // the indentation, showHidden or open frames of the abandoned one. + const r = await compileAndRun( + "inspect-refusal-state", + `import { format, inspect } from "node:util"; +const p: unknown = Promise.resolve(1); +const rec: unknown = { a: [1], b: p }; +try { + inspect({ a: [1, 2, 3, 4, 5, 6, 7, 8], b: p }, { breakLength: 20 }); +} catch {} +try { + format("%o", rec); +} catch {} +const long: unknown = JSON.parse(JSON.stringify(Array.from({ length: 12 }, (_, i) => "item" + i))); +console.log(long); +console.log(format("%O", [1])); +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + [ + "[", + " 'item0', 'item1',", + " 'item2', 'item3',", + " 'item4', 'item5',", + " 'item6', 'item7',", + " 'item8', 'item9',", + " 'item10', 'item11'", + "]", + "[ 1 ]", + "", + ].join("\n"), + ); + }); + test("inspecting a compiled class with static fields through unknown refuses", async () => { // Node prints `[class S] { count: 3 }`; the class object does not // carry compiled static fields, so the inspector refuses rather than From 2f903f0255d59851f17784918a5cbe61233656fa Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 01:13:24 -0700 Subject: [PATCH 061/409] Read native handle methods as function values Methods of Web objects held in untyped code (Request, Response, Headers, ReadableStream, its reader, AbortSignal) answered member calls only by static name, so a property read such as hono's `raw[key]()` or `typeof req.json` saw undefined. A handle class now lists the methods its invoke answers; a read returns one shared function per method that calls invoke on its `this` and raises Node's ERR_INVALID_THIS TypeError (or a rejected promise for the async operations) on another receiver. The in operator sees the methods and the modeled properties. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_dyn.h | 23 +++++ packages/runtime/src/scr_fetch.c | 64 +++++++++++-- packages/runtime/src/scr_json.c | 87 ++++++++++++++++- tests/corpus/7740-handle-method-values.js | 110 ++++++++++++++++++++++ 4 files changed, 275 insertions(+), 9 deletions(-) create mode 100644 tests/corpus/7740-handle-method-values.js diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 553b897d2..5251c6f6c 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1472,6 +1472,19 @@ ScrDyn *scr_dyn_visit_view(const ScrDyn *capsule); * (scr_http_dyn_install / scr_net_dyn_install — emitted main() calls * them exactly when the unit is linked, the scr_net_install story), so * this always-linked core never references gated units. */ +/* A method a handle class answers through invoke, read as a value + * (`typeof req.json`, `raw[key]()`): the read answers one function per + * (tag, method) that calls invoke on its `this`. length is the method's + * Function.prototype.length; rejects marks a method whose call on another + * receiver answers a rejected promise instead of throwing (the async + * WebIDL operations: Body's json(), ReadableStream's cancel()). */ +typedef struct ScrDynHandleMethod { + const char *name; + uint32_t length; + bool rejects; + const char *invalid_this; /* NULL = the class's (ScrDynHandleOps) */ +} ScrDynHandleMethod; + typedef struct ScrDynHandleOps { const char *cls; /* Node's constructor name — error texts and dynCheck's "got ..." */ void *(*retain)(void *h); @@ -1537,6 +1550,16 @@ typedef struct ScrDynHandleOps { * stream unit builds the handle's Transform from these operations on its * first stream use. NULL = the class is not a stream. */ const struct ScrDynHandleTransform *transform; + /* The class's methods as values, terminated by a NULL name; NULL = a + * property read answers no methods (member calls still reach invoke). + * See ScrDynHandleMethod. */ + const ScrDynHandleMethod *methods; + /* The TypeError (code ERR_INVALID_THIS) a method value called on another + * receiver raises; NULL = WebIDL's "Illegal invocation". */ + const char *invalid_this; + /* A promise rejected with the pending exception (+1), for the rejects + * methods; the owning unit supplies it, since promises are a gated unit. */ + ScrDyn *(*rejected)(void); } ScrDynHandleOps; /* The operations a Transform handle gives the stream unit (scr_stream.c): diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 7cc2beef4..916fc6bf9 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -4990,7 +4990,7 @@ static ScrDyn *sf_request_get(void *ptr, const char *key, size_t len) { if (default_value) { ScrStr *s = scr_str_new(default_value, strlen(default_value)); ScrDyn *v = scr_dyn_new_str(s); scr_str_release(s); return v; } if (sf_name(key, len, "keepalive") || sf_name(key, len, "isReloadNavigation") || sf_name(key, len, "isHistoryNavigation")) return scr_dyn_new_bool(false); if (sf_name(key, len, "body") || sf_name(key, len, "bodyUsed") || sf_name(key, len, "headers")) return sf_response_get(r->payload, key, len); - return scr_dyn_undefined(); + return NULL; } static ScrDyn *sf_request_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { SfRequest *r = ptr; @@ -5495,33 +5495,73 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, return false; } +/* The members invoke answers, as values (ScrDynHandleMethod), with Node's + * lengths and the brand check each one runs on another receiver. */ +static const ScrDynHandleMethod sf_signal_methods[] = { + {"throwIfAborted", 0, false, + "Value of \"this\" must be of type AbortSignal"}, + {"addEventListener", 2, false}, + {"removeEventListener", 2, false}, {"dispatchEvent", 1, false}, + {NULL, 0, false}}; +static const ScrDynHandleMethod sf_stream_methods[] = { + {"cancel", 0, true}, {"getReader", 0, false}, {NULL, 0, false}}; +static const ScrDynHandleMethod sf_reader_methods[] = { + {"read", 0, true}, {"releaseLock", 0, false}, {"cancel", 0, true}, + {NULL, 0, false}}; +static const ScrDynHandleMethod sf_body_methods[] = { + {"json", 0, true}, {"text", 0, true}, {"bytes", 0, true}, + {"arrayBuffer", 0, true}, {NULL, 0, false}}; +static const ScrDynHandleMethod sf_headers_methods[] = { + {"append", 2, false}, {"delete", 1, false}, {"entries", 0, false}, + {"forEach", 1, false}, {"get", 1, false}, {"getSetCookie", 0, false}, + {"has", 1, false}, {"keys", 0, false}, {"set", 2, false}, + {"values", 0, false}, {NULL, 0, false}}; + +static ScrDyn *sf_rejected(void) { + ScrPromise *p = scr_promise_settled_ref(NULL, &scr_dyn_retain_v, + &scr_dyn_release_v, NULL); + ScrDyn *out = scr_dyn_new_promise(p); + scr_promise_release(p); + return out; +} + static const ScrDynHandleOps sf_signal_ops = { "AbortSignal", &sf_signal_retain_v, &sf_signal_release_v, - &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL}; + &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL, + .methods = sf_signal_methods, + .invalid_this = "Value of \"this\" must be of type EventTarget"}; static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_stream_ops = { "ReadableStream", &sf_stream_retain_v, &sf_stream_release_v, - &sf_stream_invoke, &sf_stream_get, &sf_no_set, NULL}; + &sf_stream_invoke, &sf_stream_get, &sf_no_set, NULL, + .methods = sf_stream_methods, + .invalid_this = "Value of \"this\" must be of type ReadableStream", + .rejected = &sf_rejected}; static const ScrDynHandleOps sf_reader_ops = { "ReadableStreamDefaultReader", &sf_reader_handle_retain_v, &sf_reader_handle_release_v, &sf_reader_invoke, &sf_reader_get, - &sf_no_set, NULL}; + &sf_no_set, NULL, .methods = sf_reader_methods, + .invalid_this = + "Value of \"this\" must be of type ReadableStreamDefaultReader", + .rejected = &sf_rejected}; static const ScrDynHandleOps sf_controller_ops = { "ReadableStreamDefaultController", &sf_stream_retain_v, &sf_stream_release_v, &sf_controller_invoke, &sf_controller_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_request_ops = { "Request", &sf_request_retain_v, &sf_request_release_v, - &sf_request_invoke, &sf_request_get, &sf_no_set, NULL}; + &sf_request_invoke, &sf_request_get, &sf_no_set, NULL, + .methods = sf_body_methods, .rejected = &sf_rejected}; static const ScrDynHandleOps sf_response_ops = { "Response", &sf_response_retain_v, &sf_response_release_v, - &sf_response_invoke, &sf_response_get, &sf_no_set, NULL}; + &sf_response_invoke, &sf_response_get, &sf_no_set, NULL, + .methods = sf_body_methods, .rejected = &sf_rejected}; static const ScrDynHandleOps sf_headers_ops = { "Headers", &sf_headers_retain_v, &sf_headers_release_v, &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL, NULL, - &sf_headers_step}; + &sf_headers_step, .methods = sf_headers_methods}; static void sf_teardown(void) { while (sf_live) { @@ -5572,9 +5612,17 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, const ScrDyn *valu (void)ptr; (void)key; (void)len; (void)value; return false; } +static const ScrDynHandleMethod sf_signal_methods[] = { + {"throwIfAborted", 0, false, + "Value of \"this\" must be of type AbortSignal"}, + {"addEventListener", 2, false}, + {"removeEventListener", 2, false}, {"dispatchEvent", 1, false}, + {NULL, 0, false}}; static const ScrDynHandleOps sf_signal_ops = { "AbortSignal", &sf_signal_retain_v, &sf_signal_release_v, - &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL }; + &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL, + .methods = sf_signal_methods, + .invalid_this = "Value of \"this\" must be of type EventTarget" }; static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL }; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 615785687..5fab5d26e 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4975,19 +4975,103 @@ ScrDyn *scr_dyn_promise_key_get(const ScrDyn *d, const ScrStr *k) { return method ? method : scr_dyn_retain(scr_dyn_undefined()); } +/* ── handle methods as values (ScrDynHandleMethod) ────────────────── + * One function per (tag, method), as Node's prototype methods are shared + * by every instance. The function calls invoke on its `this` when that is + * a handle of its tag, and otherwise raises the class's ERR_INVALID_THIS + * TypeError (or answers a promise rejected with it), as the WebIDL brand + * checks do. closure->fn holds the method entry, caps[0] the tag. */ +static SCR_TL ScrDyn **scr_dynh_method_values[SCR_DYNH_CAPACITY]; +static SCR_TL bool scr_dynh_method_values_registered; + +static void scr_dynh_method_values_cleanup(void) { + for (size_t tag = 0; tag < SCR_DYNH_CAPACITY; tag++) { + ScrDyn **values = scr_dynh_method_values[tag]; + if (!values) continue; + scr_dynh_method_values[tag] = NULL; + const ScrDynHandleMethod *methods = scr_dynh_ops[tag]->methods; + for (size_t i = 0; methods[i].name; i++) + if (values[i]) scr_dyn_release(values[i]); + free(values); + } +} + +static ScrDyn *scr_dynh_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + const ScrDynHandleMethod *method = closure->fn; + ScrDynHandleTag tag = (ScrDynHandleTag)scr_box_get_f64(closure->caps[0]); + const ScrDynHandleOps *ops = scr_dyn_handle_ops(tag); + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result; + if (self->kind == SCR_DYN_HANDLE && self->v.handle.tag == tag) { + result = ops->invoke(self->v.handle.ptr, self, method->name, args, argc, method->name); + } else { + const char *message = method->invalid_this ? method->invalid_this + : ops->invalid_this ? ops->invalid_this + : "Illegal invocation"; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, strlen(message), "ERR_INVALID_THIS"); + result = method->rejects && ops->rejected ? ops->rejected() : NULL; + } + scr_dyn_release(self); + return result; +} + +/* The method `key` of the handle's class as a function value (+1), or NULL + * when the class has no such method. */ +static ScrDyn *scr_dynh_method_value(ScrDynHandleTag tag, const ScrDynHandleOps *ops, const char *key, size_t len) { + const ScrDynHandleMethod *methods = ops->methods; + if (!methods || !ops->invoke) return NULL; + for (size_t i = 0; methods[i].name; i++) { + if (strlen(methods[i].name) != len || memcmp(methods[i].name, key, len) != 0) continue; + if (!scr_dynh_method_values[tag]) { + size_t count = i + 1; + while (methods[count].name) count++; + scr_dynh_method_values[tag] = calloc(count, sizeof(ScrDyn *)); + if (!scr_dynh_method_values[tag]) scr_trap("scriptc: out of memory\n"); + if (!scr_dynh_method_values_registered) { + scr_dynh_method_values_registered = true; + scr_atexit(scr_dynh_method_values_cleanup); + } + } + ScrDyn **slot = &scr_dynh_method_values[tag][i]; + if (!*slot) { + ScrClosure *closure = scr_closure_new((void *)&methods[i], 1); + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], (double)tag); + *slot = scr_dyn_new_func(closure, scr_dynh_method_call, methods[i].length, "", methods[i].name); + } + return scr_dyn_retain(*slot); + } + return NULL; +} + ScrDyn *scr_dyn_handle_key_get(const ScrDyn *d, const ScrStr *k) { - const ScrDynHandleOps *ops = scr_dyn_handle_ops(d->v.handle.tag); + ScrDynHandleTag tag = d->v.handle.tag; + const ScrDynHandleOps *ops = scr_dyn_handle_ops(tag); ScrDyn *r = ops->get(d->v.handle.ptr, k->data, k->len); if (scr_exc_pending()) { scr_dyn_release(r); return NULL; } + if (!r) r = scr_dynh_method_value(tag, ops, k->data, k->len); /* Unmodeled names answer undefined — the checked-dynamic tree's own-property stance * (real-but-unmodeled members throw "not supported yet" inside ops->get instead; * SEMANTICS.md documents the remainder). */ return r ? r : scr_dyn_retain(scr_dyn_undefined()); } +/* `key in handle`: the class's methods, and the properties its get models. */ +static bool scr_dyn_handle_in(const ScrDyn *d, const ScrStr *k) { + const ScrDynHandleOps *ops = scr_dyn_handle_ops(d->v.handle.tag); + const ScrDynHandleMethod *methods = ops->invoke ? ops->methods : NULL; + for (size_t i = 0; methods && methods[i].name; i++) + if (strlen(methods[i].name) == k->len && memcmp(methods[i].name, k->data, k->len) == 0) return true; + if (!methods) return false; + ScrDyn *value = ops->get(d->v.handle.ptr, k->data, k->len); + if (!value) return scr_dyn_object_proto_name(k); + scr_dyn_release(value); + return true; +} + /* ── the ambient receiver (scr_runtime.h's design note) ─────────────── * A strictly nested push/pop stack: firing sites bind the owner around * each listener call, dyn OBJ method dispatch binds the object, and the @@ -9196,6 +9280,7 @@ bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) { } if (v->kind == SCR_DYN_PROXY) return scr_dyn_proxy_has(v, key); if (v->kind == SCR_DYN_BYTES) return scr_dyn_bytes_in(v, key); + if (v->kind == SCR_DYN_HANDLE) return scr_dyn_handle_in(v, key); if (scr_dyn_has_key(v, key)) return true; if (scr_exc_pending()) return false; if (v->kind == SCR_DYN_OBJ) return scr_dyn_builtin_inherited(v, key); diff --git a/tests/corpus/7740-handle-method-values.js b/tests/corpus/7740-handle-method-values.js new file mode 100644 index 000000000..3694c204f --- /dev/null +++ b/tests/corpus/7740-handle-method-values.js @@ -0,0 +1,110 @@ +// Methods of native Web objects read as values in untyped code: hono's +// HonoRequest calls `raw[key]()` with a computed body-method name, and code +// probes `typeof x.json` or `"get" in headers`. Each method is one shared +// function that runs on its `this`; another receiver fails Node's brand check. +const report = (label, e) => console.log(label, e.name, e.message, e.code); +// An untyped holder, as values reach a JavaScript library. +const untyped = (value) => { + const holder = JSON.parse("{}"); + holder.value = value; + return holder.value; +}; + +async function bodies() { + const holder = JSON.parse("{}"); + holder.req = new Request("http://example.test/books", { method: "POST", body: '{"title":"Dune"}' }); + const cache = {}; + const cachedBody = (key) => { + const raw = holder.req; + if (cache[key]) return cache[key]; + return (cache[key] = raw[key]()); + }; + console.log(await cachedBody("json")); + for (const key of ["text", "arrayBuffer", "bytes"]) { + const res = new Response("abc"); + const value = await res[key](); + console.log(key, typeof value, value instanceof ArrayBuffer, value.length ?? value.byteLength); + } +} + +function shape() { + const req = JSON.parse("{}"); + req.value = new Request("http://example.test/"); + const r = req.value; + console.log(typeof r.json, typeof r.text, typeof r.blobby, typeof r.url); + console.log("json" in r, "url" in r, "toString" in r, "missing" in r); + const other = JSON.parse("{}"); + other.res = new Response("x"); + console.log(r.json === r.json, r.json === other.res.json, r.json === r.text); + console.log(r.json.name, r.json.length, r.arrayBuffer.name); + const box = { h: untyped(new Headers({ a: "1" })) }; + console.log(box.h.append.length, box.h.forEach.length, box.h.get.name, "has" in box.h, "nope" in box.h); + const name = "get"; + console.log(box.h[name]("a"), box.h["has"]("b")); + box.h["set"]("b", "2"); + const entries = []; + box.h["forEach"]((v, k) => entries.push(k + "=" + v)); + console.log(entries.join(",")); +} + +async function receivers() { + const box = { + h: untyped(new Headers({ a: "1" })), + req: untyped(new Request("http://example.test/", { method: "POST", body: "x" })), + }; + const get = box.h.get; + console.log(get.call(box.h, "a"), get.bind(box.h)("a"), get.apply(box.h, ["a"])); + try { + get("a"); + } catch (e) { + report("unbound get", e); + } + try { + get.call(box.req, "a"); + } catch (e) { + report("foreign get", e); + } + const text = box.req.text; + const pending = text(); + console.log("text returns", pending instanceof Promise); + await pending.then( + () => console.log("unexpected"), + (e) => report("unbound text", e), + ); + await text.call(box.h).catch((e) => report("foreign text", e)); + console.log(await text.call(box.req)); + const signal = { s: untyped(new AbortController().signal) }; + const throwIfAborted = signal.s.throwIfAborted; + try { + throwIfAborted(); + } catch (e) { + report("unbound throwIfAborted", e); + } + try { + signal.s.addEventListener.call(box.h, "abort", () => {}); + } catch (e) { + report("foreign addEventListener", e); + } +} + +async function streams() { + const box = { body: untyped(new Response("stream").body) }; + const key = "getReader"; + const reader = box.body[key](); + const read = reader.read; + const first = await read.call(reader); + console.log(first.done, new TextDecoder().decode(first.value)); + await read().catch((e) => report("unbound read", e)); + try { + box.body.getReader.call(reader); + } catch (e) { + report("foreign getReader", e); + } +} + +(async () => { + await bodies(); + shape(); + await receivers(); + await streams(); +})(); From e7bced94fd9082c9007d1af6f4cdefeb97f752f5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:20:04 -0500 Subject: [PATCH 062/409] Let LLVM merge the comparisons of a three-way string comparator --- .../compiler/src/backend/llvm/emit-module.ts | 6 +-- .../src/backend/llvm/string-lifetimes.test.ts | 4 +- tests/harness/string-compare-emission.test.ts | 47 +++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 tests/harness/string-compare-emission.test.ts diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 16a1b1601..3c0e91e20 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -1601,8 +1601,8 @@ export function helperDefs(emitter: LlEmitter): string[] { // order; other differing words decide by their first differing byte, // and strings shorter than a word by their first byte. Otherwise a // proven-ASCII string on either side orders by bytes (scr_str_cmp), and - // other pairs call scr_str_cmp_u16_bytes, which only reads the two - // strings, so LLVM can merge repeated comparisons of one pair. Callers + // other pairs call scr_str_cmp_u16_bytes. Both only read the two strings, + // so LLVM can merge repeated comparisons of one pair. Callers // only test the sign of the result. if (emitter.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) { const fn = "scr_str_cmp_u16"; @@ -1732,7 +1732,7 @@ export function helperDefs(emitter: LlEmitter): string[] { ` %anyascii = icmp slt i32 %cws, 0`, ` br i1 %anyascii, label %memorder, label %units`, `memorder:`, - ` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`, + ` %m = call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`, ` ret i32 %m`, `units:`, ` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`, diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts index f85addcf8..07ad31153 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts @@ -478,7 +478,9 @@ test("string comparisons decide identity, lengths and leading bytes before the r expect(units).toContain("%below = icmp ult i8 %bx, %by"); expect(units).toContain("%tailable = and i1 %samelen, %short"); expect(units).toContain("%anyascii = icmp slt i32 %cws, 0"); - expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)"); + expect(units).toContain( + "call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)", + ); expect(units).toContain( "call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)", ); diff --git a/tests/harness/string-compare-emission.test.ts b/tests/harness/string-compare-emission.test.ts new file mode 100644 index 000000000..2654f56ba --- /dev/null +++ b/tests/harness/string-compare-emission.test.ts @@ -0,0 +1,47 @@ +/* String comparison emission: the runtime calls behind a relational string + * operator only read the two strings, so LLVM merges the comparisons of one + * pair. A three-way comparator (`a < b ? -1 : a > b ? 1 : 0`) then compares + * its strings once, as a comparator that tests one operator does. */ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); + +async function emittedAsm(name: string, source: string): Promise { + const key = createHash("sha256").update(source).digest("hex").slice(0, 16); + const outDir = join(cacheDir, `string-compare-${key}`); + mkdirSync(outDir, { recursive: true }); + const file = join(outDir, `${name}.ts`); + writeFileSync(file, source); + const result = await compile(file, { + outPath: join(outDir, name), + outDir, + backend: "llvm", + outputKind: "asm", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFileSync(result.artifact.path, "utf8"); +} + +const program = (body: string): string => `function order(a: string, b: string): number { + return ${body}; +} +const words = process.argv.slice(2); +words.sort(order); +console.log(words.join(",")); +`; + +const compareCalls = (asm: string): number => + asm.match(/\bcall[a-z]*\s+(?:sc_str_cmp_u16|scr_str_cmp_u16_bytes|scr_str_cmp)\b/g)?.length ?? 0; + +test("a three-way string comparator compares its pair once", async () => { + const once = compareCalls(await emittedAsm("once", program("a < b ? -1 : 1"))); + const both = compareCalls(await emittedAsm("both", program("a < b ? -1 : a > b ? 1 : 0"))); + expect(once).toBeGreaterThan(0); + expect(both).toBe(once); +}); From 8d6bb60847dc0a25dd462fb34a6ea8897b79750e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:20:31 -0500 Subject: [PATCH 063/409] Test caught values against the class a class binding holds A class binding may hold a subclass of the class it was declared with. When the declared class has subclasses, a caught payload of that class is now tested against the class the binding holds, as for other operands, so a base-class instance no longer passes a test against a subclass. --- .../lowering/expressions/relational.ts | 42 +++++++++++++++++-- .../7734-caught-instanceof-class-binding.js | 20 +++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 9f06cc377..f4b9a59c8 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -401,18 +401,52 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // folds statically exactly like the named-target folds below. const rhsClassval = storedClassValueType(lowerer, expr.right); // A catch binding against a class held by a binding (`var E = class - // extends Error {}`): the binding holds exactly that class, so the test - // is the class's caught test, after the right operand's read. + // extends Error {}`). The binding holds the declared class or one of its + // subclasses. Without subclasses the test is the class's caught test, + // after the right operand's read; otherwise a payload of the declared + // class is tested against the class the binding holds. const caught = lowerer.caughtLocalOf(expr.left); const caughtTarget = rhsClassval?.kind === "classval" && caught ? lowerer.classes.get(rhsClassval.className) : undefined; if (caught && caughtTarget && !caughtTarget.localClass && !caughtTarget.def.runtime) { + const classValue = lowerer.lowerExpr(expr.right); + if (caughtTarget.subclasses.length === 0) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: classValue, loc }], + result: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + type: BOOL, + loc, + }; + } + if (classValue.type.kind !== "classval") + lowerer.badType(expr.right, lowerer.typeOf(expr.right)); + const held = lowerer.declareHiddenLocal("%instanceofClass", classValue.type); + const instance: IrType = { kind: "object", className: caughtTarget.def.name }; return { kind: "seqExpr", - stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(expr.right), loc }], - result: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + stmts: [{ kind: "varDecl", localId: held.id, init: classValue, loc }], + result: { + kind: "logical", + op: "&&", + left: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + right: { + kind: "instanceOfValue", + value: { + kind: "caughtNarrow", + value: varRef(caught.id, CAUGHT, loc), + type: instance, + loc, + }, + classValue: varRef(held.id, classValue.type, loc), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, type: BOOL, loc, }; diff --git a/tests/corpus/7734-caught-instanceof-class-binding.js b/tests/corpus/7734-caught-instanceof-class-binding.js index 55ee792ac..4332daab0 100644 --- a/tests/corpus/7734-caught-instanceof-class-binding.js +++ b/tests/corpus/7734-caught-instanceof-class-binding.js @@ -20,3 +20,23 @@ console.log(pick("/a"), pick("/b/*")); try { throw new Error("x"); } catch (e) { console.log(e instanceof UnsupportedPathError, e instanceof Error); } const u = new UnsupportedPathError("m"); console.log(u instanceof UnsupportedPathError, u.message, u.name, u instanceof Error); + +// A binding that holds a subclass of the class it was declared with: the +// test reads the class the binding holds at run time. +class BaseError extends Error {} +class DerivedError extends BaseError {} +class LeafError extends DerivedError {} +var Current = BaseError; +function caughtIs(value) { + try { + throw value; + } catch (e) { + return e instanceof Current; + } +} +const samples = () => [new BaseError("b"), new DerivedError("d"), new LeafError("l"), new Error("e"), "text"]; +console.log("base", samples().map(caughtIs).join(",")); +Current = DerivedError; +console.log("derived", samples().map(caughtIs).join(",")); +Current = LeafError; +console.log("leaf", samples().map(caughtIs).join(",")); From 3cbd1740a83de5d2b32a21863c5575853c142738 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 01:28:46 -0700 Subject: [PATCH 064/409] Call methods of bag-tracked class instances without building their views Untyped code calling a method on an instance of a compiled JavaScript class whose own properties live in a property bag materialized the instance's whole checked view on every call (copying the bag, boxing each field and attaching the prototype) only to read one member. Hono's Context declares its methods as arrow-function fields, so every c.header() or c.json() from middleware rebuilt an 18-property view. The materializer of such a class gives the view exactly the bag's string-keyed properties, the declared fields among them with their native slot values, and the class's prototype. The compiler now emits those parts as ScrDynTypedBagClassOps (bag, prototype, declared field names, field getter) and binds them to the class's capsules, and scr_dyn_invoke resolves a member that is an ordinary data property, own or inherited, from them, calling it with the capsule as this. Accessors, read-only fields, capsule prototype links and everything else still build the view. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/expr-context.ts | 3 + .../src/backend/llvm/expr-stream-bridges.ts | 182 ++++++++++++++---- .../7621-js-class-field-methods.js.json | 6 + packages/runtime/src/scr_dyn.h | 41 +++- packages/runtime/src/scr_dyn_invoke.c | 14 ++ packages/runtime/src/scr_json.c | 73 ++++++- tests/corpus/7621-js-class-field-methods.js | 96 +++++++++ .../7621-js-class-field-methods.js.json | 1 + 8 files changed, 379 insertions(+), 37 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json create mode 100644 tests/corpus/7621-js-class-field-methods.js create mode 100644 tests/harness/corpus-costs/7621-js-class-field-methods.js.json diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index df6dfc608..c6dc506f4 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -46,4 +46,7 @@ export interface LlStreamTypedRefAdapter { recordOps?: string; /** `@sym` of the ScrDynTypedClassOps table for class instance capsules. */ classOps?: string; + /** `@sym` of the ScrDynTypedBagClassOps table for capsules of bag-tracked + * class instances whose views inherit from prototype data. */ + bagClassOps?: string; } diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index ceaca3778..aee7be180 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -767,6 +767,7 @@ export function streamTypedRefMaterializeAdapter( // class view ops are published first. const opsMeta = isDynTypedRefType(t) ? host.classMeta.get(t.className) : undefined; if (opsMeta && liveClassOpsEligible(opsMeta)) adapter.classOps = `@${snapshot}_cops`; + else if (opsMeta && liveBagClassOpsEligible(opsMeta)) adapter.bagClassOps = `@${snapshot}_bops`; adapter.commit = `@${snapshot}_commit`; adapter.commit = host.streamTypedRefCommitAdapter(t, snapshot); const B = new BlockBuilder(); @@ -779,6 +780,7 @@ export function streamTypedRefMaterializeAdapter( ); } if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta); + if (adapter.bagClassOps !== undefined) liveBagClassOps(host, snapshot, meta, t); if (meta.hierarchy && meta.children.length) { const pre = B.tmp(); for (const line of classPreLines(meta, classStructSym(t.className), "%p", pre, host.sizeType)) @@ -850,11 +852,7 @@ export function streamTypedRefMaterializeAdapter( B.line(`call void @scr_dyn_release_v(ptr ${prototype})`); B.line(`call void @scr_dyn_release_v(ptr ${attached})`); } - for (const field of meta.def.fields.filter( - (f) => - isClassOwnEnumerableFieldName(f.name) && - !(meta.root.def.name === "%Error" && (f.name === "message" || f.name === "name")), - )) { + for (const field of classViewFields(meta)) { const afterField = meta.def.tracksOwnFields ? B.newLabel("field.after") : null; if (afterField) { host.declare(`declare ptr @scr_dyn_obj_get(ptr, ptr, ${host.sizeType})`); @@ -868,32 +866,7 @@ export function streamTypedRefMaterializeAdapter( B.condBr(present, yes, afterField); B.startBlock(yes); } - const { index, compressed } = classFieldIndex(meta, field.name, "compressed"); - const fieldPtr = B.tmp(); - let fieldValue = B.tmp(); - B.line( - `${fieldPtr} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`, - ); - if (host.int32Slots.isField(t.className, field.name)) { - const raw = B.tmp(); - B.line(`${raw} = load i32, ptr ${fieldPtr}`); - B.line(`${fieldValue} = sitofp i32 ${raw} to double`); - } else if (host.inlineUnions.classField(t.className, field.name)) - fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true); - else if (compressed) { - const idx = B.tmp(); - B.line(`${idx} = load i32, ptr ${fieldPtr}`); - for (const line of compactRefLoadLines(idx, fieldValue, `${fieldValue}.r`)) - B.line(line.trimStart()); - } else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`); - if (llFieldType(field.type, false) === "i8") { - const boolValue = B.tmp(); - B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`); - fieldValue = boolValue; - } - const nullable = host.nullableFields.get(t.className, field.name); - if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable); - const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue); + const { boxed, nullable, fieldValue } = classFieldDynValue(host, B, meta, t, field); // Attributes copied from the bag do not stop the view taking the value. const setter = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set"; if (withAttributes) @@ -1491,14 +1464,157 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM ); } +/** The checked-dynamic value a class instance's view holds for `field`, read + * from its native slot of `%p`: `boxed` (+1), and the nullable union + * `fieldValue` the caller releases when `nullable` is set. */ +function classFieldDynValue( + host: LlvmEmitterContext, + B: BlockBuilder, + meta: LlClassMeta, + t: Extract, + field: LlClassMeta["def"]["fields"][number], +): { boxed: string; nullable: NullableRefField | null; fieldValue: string } { + const { index, compressed } = classFieldIndex(meta, field.name, "compressed"); + const fieldPtr = B.tmp(); + let fieldValue = B.tmp(); + B.line( + `${fieldPtr} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`, + ); + if (host.int32Slots.isField(t.className, field.name)) { + const raw = B.tmp(); + B.line(`${raw} = load i32, ptr ${fieldPtr}`); + B.line(`${fieldValue} = sitofp i32 ${raw} to double`); + } else if (host.inlineUnions.classField(t.className, field.name)) + fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true); + else if (compressed) { + const idx = B.tmp(); + B.line(`${idx} = load i32, ptr ${fieldPtr}`); + for (const line of compactRefLoadLines(idx, fieldValue, `${fieldValue}.r`)) + B.line(line.trimStart()); + } else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`); + if (llFieldType(field.type, false) === "i8") { + const boolValue = B.tmp(); + B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`); + fieldValue = boolValue; + } + const nullable = host.nullableFields.get(t.className, field.name); + if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable); + return { boxed: host.streamTypedRefBoxValue(B, field.type, fieldValue), nullable, fieldValue }; +} + +/** The fields an instance's view takes from their native slots, in order: + * its own enumerable fields, except an Error's message and name. */ +function classViewFields(meta: LlClassMeta): LlClassMeta["def"]["fields"] { + return meta.def.fields.filter( + (f) => + isClassOwnEnumerableFieldName(f.name) && + !(meta.root.def.name === "%Error" && (f.name === "message" || f.name === "name")), + ); +} + +/** Classes whose instance views the materializer builds from the property + * bag and the prototype data alone (see liveBagClassOps): JavaScript classes + * tracking their own fields, outside the Error and runtime families, with no + * subclass whose view another materializer builds. */ +function liveBagClassOpsEligible(meta: LlClassMeta): boolean { + return ( + meta.def.tracksOwnFields === true && + (meta.def.prototypeDataHelper !== undefined || + meta.def.instancePrototypeHelper !== undefined) && + !meta.def.runtime && + !meta.root.def.runtime && + meta.root.def.name !== "%Error" && + !(meta.hierarchy && meta.children.length > 0) && + meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) && + !meta.def.fields.some( + (field) => + field.name === CLASS_ATTRIBUTES || + meta.def.fields.some((f) => f.name === classFieldAttributesName(field.name)), + ) + ); +} + +/** The hooks of a bag-tracked class instance capsule + * (ScrDynTypedBagClassOps). The materializer gives the instance's view + * exactly the bag's string-keyed properties, the values of the declared + * fields among them from their native slots, and the prototype these hooks + * return, so a method call resolves without building the view. */ +function liveBagClassOps( + host: LlvmEmitterContext, + snapshot: string, + meta: LlClassMeta, + t: Extract, +): void { + const struct = classStructSym(t.className); + const bag = new BlockBuilder(); + const slot = bag.tmp(), + value = bag.tmp(); + bag.line( + `${slot} = getelementptr inbounds %${struct}, ptr %p, i64 0, i32 ${classFieldIndex(meta, DYN_CLASS_PROPERTIES).index}`, + ); + bag.line(`${value} = load ptr, ptr ${slot}`); + bag.terminate(`ret ptr ${value}`); + const proto = new BlockBuilder(); + const prototype = proto.tmp(); + if (meta.def.instancePrototypeHelper) { + const receiver = proto.tmp(); + proto.line(`${receiver} = call ptr ${vAdapters(host.shapeHost, t).retain}(ptr %p)`); + proto.line( + `${prototype} = call ptr @${mangleFunction(meta.def.instancePrototypeHelper)}(ptr ${receiver})`, + ); + } else proto.line(`${prototype} = call ptr @${mangleFunction(meta.def.prototypeDataHelper!)}()`); + proto.terminate(`ret ptr ${prototype}`); + // Field `%i` of the view's field list, as the materializer boxes it. + const fields = classViewFields(meta); + const get = new BlockBuilder(); + const cases = fields.map((_, i) => get.newLabel(`field.${i}`)); + const none = get.newLabel("field.none"); + get.terminate( + `switch ${host.sizeType} %i, label %${none} [ ${cases.map((label, i) => `${host.sizeType} ${i}, label %${label}`).join(" ")} ]`, + ); + fields.forEach((field, i) => { + get.startBlock(cases[i]!); + const { boxed, nullable, fieldValue } = classFieldDynValue(host, get, meta, t, field); + if (nullable) { + host.declare(`declare void @scr_union_release(ptr)`); + get.line(`call void @scr_union_release(ptr ${fieldValue})`); + } + get.terminate(`ret ptr ${boxed}`); + }); + get.startBlock(none); + get.terminate(`ret ptr null`); + const names = fields.map((field) => `ptr ${host.internLiteral(field.name)}`); + host.resolveThunkDefs.push( + `define internal ptr @${snapshot}_field(ptr %p, ${host.sizeType} %i) ${FN_ATTRS} { ; view field of ${meta.def.name}`, + get.render(), + `}`, + ``, + `@${snapshot}_fnames = internal constant [${names.length} x ptr] ${names.length ? `[${names.join(", ")}]` : "zeroinitializer"}`, + ); + host.resolveThunkDefs.push( + `define internal ptr @${snapshot}_bag(ptr %p) ${FN_ATTRS} { ; property bag of ${meta.def.name}`, + bag.render(), + `}`, + ``, + `define internal ptr @${snapshot}_proto(ptr %p) ${FN_ATTRS} { ; view prototype of ${meta.def.name}`, + proto.render(), + `}`, + ``, + `@${snapshot}_bops = internal constant { ptr, ptr, ${host.sizeType}, ptr, ptr } { ptr @${snapshot}_bag, ptr @${snapshot}_proto, ${host.sizeType} ${fields.length}, ptr @${snapshot}_fnames, ptr @${snapshot}_field }`, + ``, + ); +} + /** The live view a capsule of this adapter binds: the runtime bind call * and its ops constant, with the view code a deferred receiver recipe - * records (1 array, 2 record, 3 class). */ + * records (1 array, 2 record, 3 class, 4 bag-tracked class). */ export function liveRefView( adapter: LlStreamTypedRefAdapter, -): { bind: string; ops: string; code: 1 | 2 | 3 } | null { +): { bind: string; ops: string; code: 1 | 2 | 3 | 4 } | null { if (adapter.classOps !== undefined) return { bind: "scr_dyn_typed_ref_bind_class", ops: adapter.classOps, code: 3 }; + if (adapter.bagClassOps !== undefined) + return { bind: "scr_dyn_typed_ref_bind_bag_class", ops: adapter.bagClassOps, code: 4 }; if (adapter.recordOps !== undefined) return { bind: "scr_dyn_typed_ref_bind_record", ops: adapter.recordOps, code: 2 }; if (adapter.arrayOps !== undefined) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json new file mode 100644 index 000000000..a7e5fdd14 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-js-class-field-methods.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7621-js-class-field-methods.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index fdffd73cb..b31457829 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -395,6 +395,25 @@ typedef struct ScrDynTypedClassOps { ScrStr *class_name; /* immortal constructor name; NULL for an anonymous class */ } ScrDynTypedClassOps; +/* A compiled JavaScript class whose instances record their own properties + * in a property bag and whose views inherit from the class's prototype + * data, emitted per class by the compiler. The view of such an instance is + * a fresh object holding exactly the bag's string-keyed properties, where + * each declared field among them takes its value from the native slot, and + * whose prototype is the class's. A method call can therefore resolve + * without building the view (scr_dyn_typed_ref_bag_method). + * bag: the instance's bag, borrowed; NULL or undefined when it has none; + * prototype: +1 prototype a fresh view of the instance takes; + * names: the declared fields whose values come from native slots; + * field: +1 view value of declared field `index`. */ +typedef struct ScrDynTypedBagClassOps { + ScrDyn *(*bag)(void *instance); + ScrDyn *(*prototype)(void *instance); + size_t count; + ScrStr *const *names; /* immortal field-name strings */ + ScrDyn *(*field)(void *instance, size_t index); +} ScrDynTypedBagClassOps; + struct ScrDyn { size_t rc; /* SIZE_MAX = immortal (unused for dyn; kept per convention) */ ScrDynKind kind; @@ -479,7 +498,9 @@ struct ScrDyn { * keep their ScrDynTypedRecordOps here instead, flagged by `record` * (scr_dyn_typed_ref_bind_record), and class instances their * ScrDynTypedClassOps, flagged by `klass` - * (scr_dyn_typed_ref_bind_class). NULL for other referents. */ + * (scr_dyn_typed_ref_bind_class), or their ScrDynTypedBagClassOps, + * flagged by `bag_class` (scr_dyn_typed_ref_bind_bag_class). NULL for + * other referents. */ const ScrDynTypedArrayOps *array; /* Both caches belong to this capsule. ReadableStream canonicalizes * capsules for repeated source references, so the refreshed dyn view @@ -488,11 +509,12 @@ struct ScrDyn { ScrDyn *materialized; ScrDynTypedCast *casts; /* One word keeps this, the largest payload, at its previous size. */ - uint32_t type_key_len : 28; + uint32_t type_key_len : 27; uint32_t traced : 1; /* native referent has a cycle header */ uint32_t observed : 1; /* native release reports this identity's disposal */ uint32_t record : 1; /* `array` holds ScrDynTypedRecordOps */ uint32_t klass : 1; /* `array` holds ScrDynTypedClassOps */ + uint32_t bag_class : 1; /* `array` holds ScrDynTypedBagClassOps */ } typed_ref; /* SCR_DYN_PROMISE: the retained promise. The boundary contract: it * settles with a dyn payload (SCR_EXC_REF ScrDyn fulfillment or a @@ -629,7 +651,9 @@ static inline const char *scr_dyn_class_name(const ScrDyn *d) { /* The element hooks of a live array capsule, or NULL (including for record * and class capsules, which keep their hooks in the same slot). */ static inline const ScrDynTypedArrayOps *scr_dyn_typed_ref_array_ops(const ScrDyn *d) { - return d->v.typed_ref.record || d->v.typed_ref.klass ? NULL : d->v.typed_ref.array; + return d->v.typed_ref.record || d->v.typed_ref.klass || d->v.typed_ref.bag_class + ? NULL + : d->v.typed_ref.array; } /* BORROWED member lookup on a SCR_DYN_OBJ; NULL when the key is absent. */ @@ -1029,6 +1053,17 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec); /* Attach the prototype member names of a class instance capsule * (idempotent), so `in` sees its methods and accessors. */ void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops); +/* Attach the bag and prototype hooks of a capsule of a bag-tracked class + * instance (idempotent). */ +void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops); +/* The method `key` that a call on a bag-tracked class instance capsule + * reads from its view, +1, when an ordinary data property answers it: an + * own one (a declared field's value from its native slot, or another bag + * property's value), or else one on the view's prototype chain. NULL when + * only the view can answer (the caller builds it), and also NULL, with the + * exception pending, when the prototype hook threw. Never runs user + * code. */ +ScrDyn *scr_dyn_typed_ref_bag_method(const ScrDyn *d, const char *key, size_t key_len); /* The constructor name of a class instance capsule's own class, for * util.inspect; NULL when the capsule is not a bound class instance, its * class is anonymous, or its prototype was replaced. */ diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 4039fda94..364017e05 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -879,6 +879,20 @@ static ScrDyn *scr_dyn_invoke_impl( scr_dyn_release(date_method); return result; } + /* A method of a bag-tracked class instance that its view holds as an + * ordinary data property, own or inherited: the call the view's + * dispatch below makes, without building the view. */ + size_t method_len = strlen(method); + ScrDyn *inherited = scr_dyn_typed_ref_bag_method(recv, method, method_len); + if (scr_exc_pending()) return NULL; + if (inherited && scr_dyn_is_callable(inherited)) { + scr_dyn_this_push_dyn(recv); + ScrDyn *result = scr_dyn_call(inherited, args, argc, what); + scr_dyn_this_pop(); + scr_dyn_release(inherited); + return result; + } + scr_dyn_release(inherited); ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv); if (scr_exc_pending()) { scr_dyn_release(materialized); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 907486fea..f0bd04d97 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3414,7 +3414,7 @@ static ScrDyn *scr_dyn_typed_ref_new( return scr_dyn_retain(value); } } - if (type_key_len >= (size_t)1 << 28) scr_trap("scriptc: typed reference key too long\n"); + if (type_key_len >= (size_t)1 << 27) scr_trap("scriptc: typed reference key too long\n"); ScrDyn *d = scr_dyn_alloc(SCR_DYN_TYPED_REF); d->v.typed_ref.ptr = retain(ptr); d->v.typed_ref.retain = retain; @@ -3437,6 +3437,7 @@ static ScrDyn *scr_dyn_typed_ref_new( d->v.typed_ref.observed = observed; d->v.typed_ref.record = false; d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = false; scr_typed_ref_slots[slot] = d; /* the empty slot that ended the probe */ scr_typed_ref_count++; return d; @@ -3652,6 +3653,7 @@ void scr_dyn_typed_ref_bind_array(ScrDyn *d, const ScrDynTypedArrayOps *ops) { d->v.typed_ref.array = ops; d->v.typed_ref.record = false; d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = false; } } @@ -3670,6 +3672,7 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops) { d->v.typed_ref.array = (const ScrDynTypedArrayOps *)(const void *)ops; d->v.typed_ref.record = true; d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = false; } } @@ -3678,9 +3681,75 @@ void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops) { d->v.typed_ref.array = (const ScrDynTypedArrayOps *)(const void *)ops; d->v.typed_ref.record = false; d->v.typed_ref.klass = true; + d->v.typed_ref.bag_class = false; } } +void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops) { + if (d && d->kind == SCR_DYN_TYPED_REF) { + d->v.typed_ref.array = (const ScrDynTypedArrayOps *)(const void *)ops; + d->v.typed_ref.record = false; + d->v.typed_ref.klass = false; + d->v.typed_ref.bag_class = true; + } +} + +/* The view's own string keys are the bag's (ScrDynTypedBagClassOps). An + * own data property answers as the view would hold it: a declared field + * from its native slot (the materializer's ordinary write of that value, + * which a read-only or accessor entry would refuse, so those take the + * view), another property with the bag's value. A key the bag lacks is + * looked up on the prototype the view has: the cached view's, which keeps + * prototype changes made through the view, or for a capsule without one, + * the prototype a fresh view takes. The walk is scr_dyn_obj_read_receiver's + * from the view's prototype on, answering only when it meets an ordinary + * data property before anything that needs more (an accessor, a function, + * array or capsule link, Object.prototype). */ +ScrDyn *scr_dyn_typed_ref_bag_method(const ScrDyn *d, const char *key, size_t key_len) { + if (d->kind != SCR_DYN_TYPED_REF || !d->v.typed_ref.bag_class || d->prototype) return NULL; + const ScrDynTypedBagClassOps *ops = (const ScrDynTypedBagClassOps *)(const void *)d->v.typed_ref.array; + void *instance = d->v.typed_ref.ptr; + const ScrDyn *bag = ops->bag(instance); + if (bag && bag->kind != SCR_DYN_UNDEF) { + if (bag->kind != SCR_DYN_OBJ) return NULL; + const ScrDynEntry *own = scr_dyn_find_entry(bag, key, key_len); + if (own) { + for (size_t i = 0; i < ops->count; i++) { + const ScrStr *name = ops->names[i]; + if (name->len != key_len || memcmp(name->data, key, key_len) != 0) continue; + return own->accessor || !own->writable ? NULL : ops->field(instance, i); + } + return own->accessor ? NULL : scr_dyn_retain(own->value); + } + } + ScrDyn *prototype; + const ScrDyn *cached = d->v.typed_ref.materialized; + if (cached) { + if (cached->kind != SCR_DYN_OBJ || !cached->prototype) return NULL; + prototype = scr_dyn_retain(cached->prototype); + } else { + prototype = ops->prototype(instance); + if (scr_exc_pending()) { + scr_dyn_release(prototype); + return NULL; + } + if (!prototype) return NULL; + } + ScrDyn *found = NULL; + for (const ScrDyn *current = prototype; current; current = current->prototype) { + if (current == scr_builtin_object_prototype || current->kind == SCR_DYN_FUNC || + current->kind == SCR_DYN_ARR || current->kind == SCR_DYN_TYPED_REF) + break; + if (current->kind != SCR_DYN_OBJ) continue; + ScrDynEntry *entry = scr_dyn_find_entry(current, key, key_len); + if (!entry) continue; + if (!entry->accessor) found = scr_dyn_retain(entry->value); + break; + } + scr_dyn_release(prototype); + return found; +} + static const ScrDynTypedClassOps *scr_typed_ref_class_ops(const ScrDyn *d) { if (!d->v.typed_ref.klass) return NULL; const ScrDynTypedClassOps *ops = (const ScrDynTypedClassOps *)(const void *)d->v.typed_ref.array; @@ -5049,6 +5118,8 @@ ScrDyn *scr_dyn_this_get(void) { scr_dyn_typed_ref_bind_record(e->dv, (const ScrDynTypedRecordOps *)r->ops); else if (r->view == 3) scr_dyn_typed_ref_bind_class(e->dv, (const ScrDynTypedClassOps *)r->ops); + else if (r->view == 4) + scr_dyn_typed_ref_bind_bag_class(e->dv, (const ScrDynTypedBagClassOps *)r->ops); } if (e->dv) return scr_dyn_retain(e->dv); /* An uninstalled tag never binds: a unit that fires without its dyn diff --git a/tests/corpus/7621-js-class-field-methods.js b/tests/corpus/7621-js-class-field-methods.js new file mode 100644 index 000000000..cd0a5be99 --- /dev/null +++ b/tests/corpus/7621-js-class-field-methods.js @@ -0,0 +1,96 @@ +// Untyped code calling methods on an instance of a JS class whose own +// properties live in a property bag: methods declared as arrow-function +// fields (own properties), prototype methods, prototype methods replaced or +// shadowed after construction, accessors on the prototype, and calls of +// members that are not functions. Each call must see what the instance +// holds at that moment and bind `this` to the instance. +'use strict'; + +class Context { + status = 200; + headers = {}; + label = 'ctx'; + header = (name, value) => { + this.headers[name] = value; + return this; + }; + json = (body) => `${this.status} ${JSON.stringify(body)} ${JSON.stringify(this.headers)}`; + constructor(label) { + this.label = label; + } + describe() { + return `${this.label}/${this.status}`; + } + get upper() { + return this.label.toUpperCase(); + } + shout() { + return this.upper + '!'; + } +} + +// Constant method names: the calls a framework's untyped code makes. +const calls = { + describe: (t) => t.describe(), + header: (t, name, value) => t.header(name, value), + json: (t, body) => t.json(body), + shout: (t) => t.shout(), + extra: (t, n) => t.extra(n), + status: (t) => t.status(), + missing: (t) => t.missing(), + upper: (t) => t.upper(), +}; + +function call(target, method, ...args) { + return calls[method](target, ...args); +} + +function put(target, key, value) { + target[key] = value; +} + +const c = new Context('first'); +console.log(call(c, 'describe')); +console.log(call(call(c, 'header', 'x-a', '1'), 'json', { ok: true })); +put(c, 'status', 404); +console.log(call(c, 'json', [1, 2]), call(c, 'describe'), call(c, 'shout')); + +// A field method replaced through untyped code, and an own property that +// shadows a prototype method. +put(c, 'json', (body) => `replaced ${body}`); +put(c, 'describe', function () { + return `own describe of ${this.label}`; +}); +console.log(call(c, 'json', 'x'), call(c, 'describe')); + +// A prototype method replaced after construction reaches every instance +// that does not shadow it. +const d = new Context('second'); +Context.prototype.describe = function () { + return `patched ${this.label}`; +}; +console.log(call(d, 'describe'), call(c, 'describe')); + +// A prototype method added after construction. +Context.prototype.extra = function (n) { + return `${this.label} extra ${n}`; +}; +console.log(call(d, 'extra', 3)); + +// Members that are not functions. +for (const name of ['status', 'missing', 'upper']) { + try { + call(d, name); + } catch (error) { + console.log(error instanceof TypeError, error.message); + } +} + +// Many calls in a row stay consistent with the instance's current state. +let out = ''; +for (let i = 0; i < 4; i++) { + call(d, 'header', `h${i}`, String(i)); + put(d, 'status', 200 + i); + out += call(d, 'json', i) + '\n'; +} +process.stdout.write(out); diff --git a/tests/harness/corpus-costs/7621-js-class-field-methods.js.json b/tests/harness/corpus-costs/7621-js-class-field-methods.js.json new file mode 100644 index 000000000..ef439d096 --- /dev/null +++ b/tests/harness/corpus-costs/7621-js-class-field-methods.js.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 4500 } From 5aef6597ab95b604ce3514c011ee49823aed68b3 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 01:28:47 -0700 Subject: [PATCH 065/409] Test the function tag before comparing a class constructor's key dyn.classIs went through the generic library call: retain the receiver, call scr_dyn_class_is_key, release it (and buffer it as a cycle candidate). A class's static property bag dispatch (%dyn.class.staticProperties) runs one probe per compiled class for every untyped property read, so this cost about 500 instructions per read in a Hono app. The probe now tests the value's function tag inline, as the capsule probes test theirs, so LLVM skips the chain for every other value, and borrows the receiver across a literal key. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/emitter.test.ts | 27 ++++++++++++++ .../compiler/src/backend/llvm/lib-dispatch.ts | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index d1fd62a83..4793663aa 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -459,6 +459,33 @@ test("class brand probes borrow a local only across an inert literal key", () => expect(computed).toContain("call ptr @scr_dyn_retain_v("); }); +test("class constructor probes test the function tag and borrow across a literal key", () => { + const dyn: IrType = { kind: "dyn" }; + const key: IrExpr = { kind: "strLit", value: "classval:Example", type: STRING, loc: receiverLoc }; + const probe: IrExpr = { + kind: "libCall", + fn: "dyn.classIs", + args: [ref(dyn), key], + type: BOOL, + loc: receiverLoc, + }; + const borrowed = work(probe, dyn); + expect(borrowed).not.toContain("call ptr @scr_dyn_retain_v("); + expect(borrowed).toContain("icmp ne ptr"); + expect(borrowed).toContain("icmp eq i32 %t"); + expect(borrowed).toMatch(/icmp eq i32 %t\d+, 8\b/); + expect(borrowed).toContain("call zeroext i1 @scr_dyn_class_is_key("); + const computed = work( + { + ...probe, + args: [ref(dyn), { kind: "toString", operand: ref(dyn), type: STRING, loc: receiverLoc }], + }, + dyn, + ); + expect(computed).toContain("call ptr @scr_dyn_retain_v("); + expect(computed).toContain("call zeroext i1 @scr_dyn_class_is_key("); +}); + test("class brand probes compare the program's key constant before the key text", () => { const dyn: IrType = { kind: "dyn" }; const key: IrExpr = { kind: "strLit", value: "object:Example", type: STRING, loc: receiverLoc }; diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index cebe661d4..eaf8f59f6 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -297,6 +297,41 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll B.line(`${result} = load i1, ptr ${slot}`); return { name: result, type: e.type }; } + if (e.fn === "dyn.classIs") { + // Only a class constructor's function value can match. Expose the + // function tag to LLVM so a chain of constructor probes (a static + // property bag's dispatch) costs one test for every other value, and + // borrow the receiver across a literal key as the capsule probes do. + const literal = e.args[1]!.kind === "strLit"; + const value = literal ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!); + const key = host.emitExpr(e.args[1]!); + const slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca i1`); + B.line(`store i1 false, ptr ${slot}`); + const present = B.tmp(); + const inspect = B.newLabel("ctor.inspect"); + const compare = B.newLabel("ctor.compare"); + const done = B.newLabel("ctor.done"); + B.line(`${present} = icmp ne ptr ${value.name}, null`); + B.condBr(present, inspect, done); + B.startBlock(inspect); + const kind = host.dynKind(value.name); + const fn = B.tmp(); + B.line(`${fn} = icmp eq i32 ${kind}, ${DYN_KIND.FUNC}`); + B.condBr(fn, compare, done); + B.startBlock(compare); + host.declare("declare zeroext i1 @scr_dyn_class_is_key(ptr, ptr)"); + const matches = B.tmp(); + B.line( + `${matches} = call zeroext i1 @scr_dyn_class_is_key(ptr ${value.name}, ptr ${key.name})`, + ); + B.line(`store i1 ${matches}, ptr ${slot}`); + B.br(done); + B.startBlock(done); + const result = B.tmp(); + B.line(`${result} = load i1, ptr ${slot}`); + return { name: result, type: e.type }; + } if (e.fn === "crypto.randomFillCb" || e.fn === "crypto.randomFillCbDyn") { host.usesTimers = true; const args = e.args.map((arg) => host.emitExpr(arg)); From 9e41d505b63d0b573c3d99b0ac165e64789c33a8 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:30:06 -0500 Subject: [PATCH 066/409] Declare the property-bag helpers in the runtime header scr_dyn_bag_create_field and scr_dyn_is_class_capsule are declared by the LLVM backend; their prototypes move from scr_json.c into scr_dyn.h, which llvm-runtime-abi.test.ts checks the backend's declares against. --- packages/runtime/src/scr_dyn.h | 2 ++ packages/runtime/src/scr_json.c | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 553b897d2..f7a71538f 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -935,6 +935,8 @@ ScrDyn *scr_dyn_new_obj_null_proto(void); ScrDyn *scr_dyn_obj_create(ScrDyn *prototype); ScrDyn *scr_dyn_obj_create_with_properties(ScrDyn *prototype, ScrDyn *descriptors); ScrDyn *scr_dyn_get_prototype_for_identity(ScrDyn *object); +bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key); +bool scr_dyn_is_class_capsule(const ScrDyn *d); ScrDyn *scr_dyn_get_prototype(ScrDyn *object); ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object); ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 615785687..290ae4982 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -16215,7 +16215,6 @@ void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule) { * does not have it yet. Answers false, changing nothing, for any other bag, * whose creation takes the general defineProperty path. Calls only * exported functions. */ -bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key); bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key) { if (!bag || bag->kind != SCR_DYN_OBJ || bag->non_extensible || bag->v.obj.source_access) return false; @@ -16227,7 +16226,6 @@ bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key) { /* Whether a value is a capsule of a compiled class instance (its type key * names an object type): the class tables' fast exit for every other value, * including record and array capsules. Calls only libc. */ -bool scr_dyn_is_class_capsule(const ScrDyn *d); bool scr_dyn_is_class_capsule(const ScrDyn *d) { return d && d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.type_key_len > 7 && memcmp(d->v.typed_ref.type_key, "object:", 7) == 0; From 8bb9ee8c5784f4e0d7ec8c7e07b3887c1c2c90de Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:38:18 -0500 Subject: [PATCH 067/409] Check generateKeyPair arguments and RSA parameters as Node does --- .../limitations/builtin-apis/crypto.mdx | 2 + .../src/frontend/lowering/builtins/crypto.ts | 10 +- .../7445-crypto-key-pair-options.mjs.json | 6 + .../7446-crypto-rsa-key-pair-errors.ts.json | 6 + packages/runtime/src/scr_pki.c | 473 ++++++++++++------ tests/corpus/7445-crypto-key-pair-options.mjs | 105 ++++ .../corpus/7446-crypto-rsa-key-pair-errors.ts | 72 +++ .../7445-crypto-key-pair-options.mjs.json | 1 + .../7446-crypto-rsa-key-pair-errors.ts.json | 1 + 9 files changed, 526 insertions(+), 150 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json create mode 100644 tests/corpus/7445-crypto-key-pair-options.mjs create mode 100644 tests/corpus/7446-crypto-rsa-key-pair-errors.ts create mode 100644 tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json create mode 100644 tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 971065408..5d7ff0e56 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -29,6 +29,8 @@ Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused. Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`. RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. + `generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback. + RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, the `raw-public`, `raw-private`, and `raw-seed` encodings, and `paramEncoding: 'explicit'` throw a coded SC2020 error. The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1. Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 47e66f9b2..6123ee0a3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -1455,14 +1455,10 @@ function lowerPkiCall( expr, `${supported} is supported`, ); - // generateKeyPair(type, callback): Node reads a function in the options - // position as the callback. - const shifted = fn === "crypto.generateKeyPairCb" && args.length === 2; + // generateKeyPair(type, callback): the runtime reads a function in the + // options position as the callback, as Node does. const values: IrExpr[] = []; - for (let i = 0; i < max; i++) { - const node = shifted ? (i === 1 ? undefined : i === 2 ? args[1] : args[i]) : args[i]; - values.push(dynArg(lowerer, node, loc)); - } + for (let i = 0; i < max; i++) values.push(dynArg(lowerer, args[i], loc)); if (fn === "crypto.generateKeyPairCb") return { kind: "libCall", fn, args: values, type: VOID, loc }; const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json new file mode 100644 index 000000000..5d57afbef --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7445-crypto-key-pair-options.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7445-crypto-key-pair-options.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json new file mode 100644 index 000000000..f055ddab9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7446-crypto-rsa-key-pair-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7446-crypto-rsa-key-pair-errors.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index 54aadc06b..888e0eae9 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -68,14 +68,25 @@ static ScrStr *sp_received(const ScrDyn *got, bool range) { } /* ERR_INVALID_ARG_VALUE for an option ("The property 'options.type' is - * invalid. Received 'spki'"). */ -static void sp_property_invalid(const char *name, const ScrDyn *got) { + * invalid. Received 'spki'"), with another reason than "is invalid". */ +static void sp_property_invalid_because(const char *name, const ScrDyn *got, const char *reason) { ScrStr *received = sp_received(got, false); - scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "The property '%s' is invalid. Received %.*s", name, + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "The property '%s' %s. Received %.*s", name, reason, (int)received->len, received->data); scr_str_release(received); } +static void sp_property_invalid(const char *name, const ScrDyn *got) { + sp_property_invalid_because(name, got, "is invalid"); +} + +/* ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS ("The selected key encoding pkcs1 + * can only be used for RSA keys."). */ +static void sp_incompatible(const char *encoding, const char *reason) { + scr_pki_throw_fmt(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", "The selected key encoding %s %s.", encoding, + reason); +} + static void sp_unsupported_input(void) { sp_throw(SCR_ERR_ERROR, "ERR_OSSL_UNSUPPORTED", "error:1E08010C:DECODER routines::unsupported"); } @@ -989,13 +1000,11 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { return NULL; } if (pkcs1 && !sp_is_rsa(k)) { - sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", - "The selected key encoding pkcs1 can only be used for RSA keys."); + sp_incompatible("pkcs1", "can only be used for RSA keys"); return NULL; } if (sec1 && !sp_is_ec(k)) { - sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", - "The selected key encoding sec1 can only be used for EC keys."); + sp_incompatible("sec1", "can only be used for EC keys"); return NULL; } unsigned char *der = malloc(SP_DER_MAX); @@ -1175,132 +1184,292 @@ static void sp_key_install(void) { /* ── generateKeyPairSync ──────────────────────────────────────────────── */ /* A key in the encoding an options.publicKeyEncoding/privateKeyEncoding - * object asks for, or the KeyObject when it is undefined. Consumes `k`. */ + * object asks for, or the KeyObject when it is undefined or null. + * Consumes `k`. */ static ScrDyn *sp_encoded(SpKey *k, const ScrDyn *encoding) { - if (sp_absent(encoding)) return sp_key_box(k); + if (sp_absent(encoding) || encoding->kind == SCR_DYN_NULL) return sp_key_box(k); ScrDyn *result = sp_export(k, encoding); sp_key_release(k); return result; } -static bool sp_check_encoding_plain(const ScrDyn *encoding, const char *name, bool private_key); - -static bool sp_check_encoding(const ScrDyn *options, const char *name, bool private_key) { - ScrDyn *encoding = sp_plain(sp_opt(options, name)); - if (!encoding) return false; - bool ok = sp_check_encoding_plain(encoding, name, private_key); - scr_dyn_release(encoding); - return ok; +/* validateObject: an object other than null or an array (typeof + * "object"); false with ERR_INVALID_ARG_TYPE pending. */ +static bool sp_check_object(const ScrDyn *v, const char *name) { + if (v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_ARR && scr_dyn_is_object(v)) return true; + scr_dyn_arg_type_fail(name, "of type object", v); + return false; } -static bool sp_check_encoding_plain(const ScrDyn *encoding, const char *name, bool private_key) { - if (sp_absent(encoding)) return true; - char full[64]; - snprintf(full, sizeof full, "options.%s", name); - if (!sp_is_object(encoding)) { - sp_property_invalid(full, encoding); +/* validateUint32 for an option: a number, then an integer, then the + * range; -0 reads as 0. False with the error pending. */ +static bool sp_check_uint32(const ScrDyn *v, const char *name, double *out) { + if (v->kind != SCR_DYN_NUM) { + scr_dyn_prop_type_fail(name, "of type number", v); return false; } - const ScrDyn *format = sp_opt(encoding, "format"); - if (!sp_is_str(format, "pem") && !sp_is_str(format, "der") && !sp_is_str(format, "jwk")) { - snprintf(full, sizeof full, "options.%s.format", name); - sp_property_invalid(full, format); - return false; - } - if (sp_is_str(format, "jwk")) return true; - const ScrDyn *type = sp_opt(encoding, "type"); - bool valid = private_key ? sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || sp_is_str(type, "sec1") - : sp_is_str(type, "pkcs1") || sp_is_str(type, "spki"); - if (!valid) { - snprintf(full, sizeof full, "options.%s.type", name); - sp_property_invalid(full, type); + double x = v->v.num; + bool integer = isfinite(x) && trunc(x) == x; + if (!integer || x < 0 || x > 4294967295.0) { + ScrStr *received = sp_received(v, true); + scr_pki_throw_fmt(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", + "The value of \"%s\" is out of range. It must be %s. Received %.*s", name, + integer ? ">= 0 && <= 4294967295" : "an integer", (int)received->len, received->data); + scr_str_release(received); return false; } + *out = x + 0.0; return true; } -/* Generates the private key for (type, options), validated in Node's - * order; NULL with the error pending. */ -static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { +static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const ScrDyn *type, bool private_key, + const char **refusal); + +/* Node's parseKeyEncoding for options.publicKeyEncoding or + * options.privateKeyEncoding of a `type` key: true when the encoding is + * undefined, null or valid, false with the error pending. A valid raw or + * encrypted encoding, which this runtime does not write, sets *refusal + * when it is unset. */ +static bool sp_check_encoding(const ScrDyn *options, const char *key, const ScrDyn *type, bool private_key, + const char **refusal) { + ScrDyn *enc = sp_plain(sp_opt(options, key)); + if (!enc) return false; + char name[64]; + snprintf(name, sizeof name, "options.%s", key); + bool ok = sp_check_encoding_plain(enc, name, type, private_key, refusal); + scr_dyn_release(enc); + return ok; +} + +static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const ScrDyn *type, bool private_key, + const char **refusal) { + if (sp_absent(enc) || enc->kind == SCR_DYN_NULL) return true; + if (!scr_dyn_is_object(enc)) { + sp_property_invalid(name, enc); + return false; + } + if (!sp_check_object(enc, "options")) return false; + /* Node's isPublic: true for a public key encoding of a non-empty type, + * false for a private key encoding, and unset otherwise. */ + bool is_public = !private_key && type->v.str->len > 0; + char full[96]; + const ScrDyn *format = sp_opt(enc, "format"); + const ScrDyn *kind = sp_opt(enc, "type"); + bool raw_public = sp_is_str(format, "raw-public"); + bool raw_private = sp_is_str(format, "raw-private") || sp_is_str(format, "raw-seed"); + bool jwk = sp_is_str(format, "jwk"); + bool der = sp_is_str(format, "der"); + if ((!raw_public && !raw_private && !jwk && !der && !sp_is_str(format, "pem")) || (raw_public && private_key) || + (raw_private && is_public)) { + snprintf(full, sizeof full, "%s.format", name); + sp_property_invalid(full, format); + return false; + } + bool pkcs1 = false, sec1 = false; + if (raw_public || raw_private) { + if (!sp_absent(kind) && + !(raw_public && (sp_is_str(kind, "uncompressed") || sp_is_str(kind, "compressed")))) { + snprintf(full, sizeof full, "%s.type", name); + sp_property_invalid(full, kind); + return false; + } + } else { + pkcs1 = sp_is_str(kind, "pkcs1"); + sec1 = !is_public && sp_is_str(kind, "sec1"); + bool valid = (jwk && sp_absent(kind)) || pkcs1 || sec1 || (!private_key && sp_is_str(kind, "spki")) || + (!is_public && sp_is_str(kind, "pkcs8")); + if (!valid) { + snprintf(full, sizeof full, "%s.type", name); + sp_property_invalid(full, kind); + return false; + } + if (pkcs1 && !sp_is_str(type, "rsa")) { + sp_incompatible("pkcs1", "can only be used for RSA keys"); + return false; + } + if (sec1 && !sp_is_str(type, "ec")) { + sp_incompatible("sec1", "can only be used for EC keys"); + return false; + } + } + bool encrypted = false; + if (!is_public) { + const ScrDyn *cipher = sp_opt(enc, "cipher"); + const ScrDyn *passphrase = sp_opt(enc, "passphrase"); + encrypted = !sp_absent(cipher) && cipher->kind != SCR_DYN_NULL; + if (raw_private) { + if (encrypted || !sp_absent(passphrase)) { + sp_incompatible("raw format", "does not support encryption"); + return false; + } + } else if (encrypted) { + if (cipher->kind != SCR_DYN_STR) { + snprintf(full, sizeof full, "%s.cipher", name); + sp_property_invalid(full, cipher); + return false; + } + if (der && (pkcs1 || sec1)) { + sp_incompatible(pkcs1 ? "pkcs1" : "sec1", "does not support encryption"); + return false; + } + if (passphrase->kind != SCR_DYN_STR && passphrase->kind != SCR_DYN_BYTES && !scr_buffer_storage_is(passphrase)) { + snprintf(full, sizeof full, "%s.passphrase", name); + sp_property_invalid(full, passphrase); + return false; + } + } else if (!sp_absent(passphrase)) { + snprintf(full, sizeof full, "%s.cipher", name); + sp_property_invalid_because(full, cipher, "is required when a passphrase is specified"); + return false; + } + } + if (!*refusal && (raw_public || raw_private)) *refusal = "generateKeyPair with a raw key encoding"; + if (!*refusal && encrypted) *refusal = "encrypted private key export"; + return true; +} + +/* The key generateKeyPair makes, as sp_keygen_check reads it. */ +typedef struct { + enum { SP_GEN_RSA, SP_GEN_EC, SP_GEN_ED25519 } kind; + double bits, exponent; /* RSA */ + const SpCurve *curve; /* EC */ +} SpKeyGen; + +/* The key types Node generates that this runtime does not, apart from + * rsa-pss, dsa and dh: Node's named key pairs. */ +static const char *const sp_other_named_types[] = { + "ed448", + "x25519", + "x448", + "ml-dsa-44", + "ml-dsa-65", + "ml-dsa-87", + "ml-kem-512", + "ml-kem-768", + "ml-kem-1024", + "slh-dsa-sha2-128f", + "slh-dsa-sha2-128s", + "slh-dsa-sha2-192f", + "slh-dsa-sha2-192s", + "slh-dsa-sha2-256f", + "slh-dsa-sha2-256s", + "slh-dsa-shake-128f", + "slh-dsa-shake-128s", + "slh-dsa-shake-192f", + "slh-dsa-shake-192s", + "slh-dsa-shake-256f", + "slh-dsa-shake-256s", +}; + +/* createJob's checks for generateKeyPair(Sync)(type, options) in Node's + * order: the type, the options object, the public and private key + * encodings, then the options of the type. False with the error pending. + * The forms this runtime does not generate throw a coded SC2020 error + * after all of these checks. */ +static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen *gen) { if (type->kind != SCR_DYN_STR) { scr_dyn_arg_type_fail("type", "of type string", type); - return NULL; + return false; } - bool rsa = sp_is_str(type, "rsa"); - bool ec = sp_is_str(type, "ec"); - bool ed = sp_is_str(type, "ed25519"); - static const char *const known[] = {"rsa-pss", "dsa", "ed448", "x25519", "x448", "dh"}; - bool other = false; - for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) other = other || sp_is_str(type, known[i]); - if (!rsa && !ec && !ed && !other) { - ScrStr *received = sp_received(type, false); - scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", - "The argument 'type' must be a supported key type. Received %.*s", (int)received->len, - received->data); - scr_str_release(received); - return NULL; + if (!sp_absent(options) && !sp_check_object(options, "options")) return false; + const char *refusal = NULL; + if (!sp_check_encoding(options, "publicKeyEncoding", type, false, &refusal) || + !sp_check_encoding(options, "privateKeyEncoding", type, true, &refusal)) + return false; + bool pss = sp_is_str(type, "rsa-pss"); + if (sp_is_str(type, "rsa") || pss) { + if (!sp_check_object(options, "options")) return false; + gen->kind = SP_GEN_RSA; + if (!sp_check_uint32(sp_opt(options, "modulusLength"), "options.modulusLength", &gen->bits)) return false; + const ScrDyn *exponent = sp_opt(options, "publicExponent"); + gen->exponent = 65537; + if (!sp_absent(exponent) && exponent->kind != SCR_DYN_NULL && + !sp_check_uint32(exponent, "options.publicExponent", &gen->exponent)) + return false; + if (pss && !refusal) refusal = "generateKeyPairSync for this key type"; + } else if (sp_is_str(type, "dsa") || sp_is_str(type, "dh")) { + if (!sp_check_object(options, "options")) return false; + if (!refusal) refusal = "generateKeyPairSync for this key type"; + } else if (sp_is_str(type, "ec")) { + if (!sp_check_object(options, "options")) return false; + gen->kind = SP_GEN_EC; + const ScrDyn *curve_name = sp_opt(options, "namedCurve"); + if (curve_name->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("options.namedCurve", "of type string", curve_name); + return false; + } + const ScrDyn *param = sp_opt(options, "paramEncoding"); + if (sp_is_str(param, "explicit")) { + if (!refusal) refusal = "generateKeyPair with paramEncoding 'explicit'"; + } else if (!sp_absent(param) && param->kind != SCR_DYN_NULL && !sp_is_str(param, "named")) { + sp_property_invalid("options.paramEncoding", param); + return false; + } + gen->curve = NULL; + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_is_str(curve_name, sp_curves[i].name) || + (sp_curves[i].nist && sp_is_str(curve_name, sp_curves[i].nist))) + gen->curve = &sp_curves[i]; + if (!gen->curve) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); + return false; + } + } else if (sp_is_str(type, "ed25519")) { + gen->kind = SP_GEN_ED25519; + } else { + bool named = false; + for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) + named = named || sp_is_str(type, sp_other_named_types[i]); + if (!named) { + ScrStr *received = sp_received(type, false); + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", + "The argument 'type' must be a supported key type. Received %.*s", (int)received->len, + received->data); + scr_str_release(received); + return false; + } + if (!refusal) refusal = "generateKeyPairSync for this key type"; } - /* The ed25519 options are optional. */ - if (!sp_is_object(options) && !(ed && sp_absent(options))) { - scr_dyn_arg_type_fail("options", "of type object", options); - return NULL; - } - if (other) { - sp_not_supported("generateKeyPairSync for this key type"); - return NULL; + if (refusal) { + sp_not_supported(refusal); + return false; } + return gen->kind != SP_GEN_ED25519 || sp_ed() != NULL; +} + +/* OpenSSL's error for an RSA modulus length or public exponent it does not + * generate, as Node reports it, or NULL. */ +static const char *sp_rsa_keygen_failure(double bits, double exponent) { + if (bits < 512) return "error:1C8000AB:Provider routines::key size too small"; + if (exponent == 0) return "Key generation job failed"; + if (bits > 2147483647.0) return "error:02000078:rsa routines::key size too small"; + if (exponent == 1 || fmod(exponent, 2) == 0) return "error:020000B2:rsa routines::pub exponent out of range"; + return NULL; +} + +/* Generates the key `gen` describes: NULL with the error pending, and + * *job_error set for the errors generateKeyPair passes to its callback. */ +static SpKey *sp_keygen_run(const SpKeyGen *gen, bool *job_error) { + *job_error = false; SpKey *k = sp_key_alloc(true); - if (ed) { - if (!sp_check_encoding(options, "publicKeyEncoding", false) || - !sp_check_encoding(options, "privateKeyEncoding", true)) { - sp_key_release(k); - return NULL; - } - const ScrPkiEd25519 *ed = sp_ed(); - if (!ed) { - sp_key_release(k); - return NULL; - } + if (gen->kind == SP_GEN_ED25519) { k->ed25519 = true; sp_random(NULL, k->ed_seed, 32); - ed->public_key(k->ed_pub, k->ed_seed); + scr_pki_ed25519->public_key(k->ed_pub, k->ed_seed); return k; } - if (rsa) { - const ScrDyn *bits = sp_opt(options, "modulusLength"); - if (bits->kind != SCR_DYN_NUM) { + if (gen->kind == SP_GEN_RSA) { + const char *failure = sp_rsa_keygen_failure(gen->bits, gen->exponent); + if (failure) { sp_key_release(k); - scr_dyn_prop_type_fail("options.modulusLength", "of type number", bits); + *job_error = true; + scr_pki_throw_fmt(SCR_ERR_ERROR, NULL, "%s", failure); return NULL; } - double b = bits->v.num; - /* validateUint32: an integer first, then the range. */ - bool integer = isfinite(b) && trunc(b) == b; - if (!integer || b < 0 || b > 4294967295.0) { - sp_key_release(k); - ScrStr *received = sp_received(bits, true); - scr_pki_throw_fmt(SCR_ERR_RANGE, "ERR_OUT_OF_RANGE", - "The value of \"options.modulusLength\" is out of range. It must be %s. Received %.*s", - integer ? ">= 0 && <= 4294967295" : "an integer", (int)received->len, received->data); - scr_str_release(received); - return NULL; - } - const ScrDyn *exp = sp_opt(options, "publicExponent"); - double e = 65537; - if (!sp_absent(exp)) { - if (exp->kind != SCR_DYN_NUM) { - sp_key_release(k); - scr_dyn_prop_type_fail("options.publicExponent", "of type number", exp); - return NULL; - } - e = exp->v.num; - } - if (!sp_check_encoding(options, "publicKeyEncoding", false) || - !sp_check_encoding(options, "privateKeyEncoding", true)) { - sp_key_release(k); - return NULL; - } - if (b < 512 || b > 16384 || e < 3 || e > 2147483647.0 || e != (double)(int64_t)e || ((int64_t)e & 1) == 0 || + /* mbedTLS generates even modulus lengths from 1024 to 8192 bits and, + * past the checks above, odd exponents up to INT_MAX. */ + double b = gen->bits, e = gen->exponent; + if (b < MBEDTLS_RSA_GEN_KEY_MIN_BITS || b > MBEDTLS_MPI_MAX_BITS || fmod(b, 2) != 0 || e > 2147483647.0 || mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_RSA)) != 0 || mbedtls_rsa_gen_key(mbedtls_pk_rsa(k->pk), sp_random, NULL, (unsigned)b, (int)e) != 0) { sp_key_release(k); @@ -1309,29 +1478,8 @@ static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { } return k; } - const ScrDyn *curve_name = sp_opt(options, "namedCurve"); - if (curve_name->kind != SCR_DYN_STR) { - sp_key_release(k); - scr_dyn_prop_type_fail("options.namedCurve", "of type string", curve_name); - return NULL; - } - if (!sp_check_encoding(options, "publicKeyEncoding", false) || - !sp_check_encoding(options, "privateKeyEncoding", true)) { - sp_key_release(k); - return NULL; - } - const SpCurve *curve = NULL; - for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) - if (sp_is_str(curve_name, sp_curves[i].name) || - (sp_curves[i].nist && sp_is_str(curve_name, sp_curves[i].nist))) - curve = &sp_curves[i]; - if (!curve) { - sp_key_release(k); - sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); - return NULL; - } if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) != 0 || - mbedtls_ecp_gen_key(curve->id, mbedtls_pk_ec(k->pk), sp_random, NULL) != 0) { + mbedtls_ecp_gen_key(gen->curve->id, mbedtls_pk_ec(k->pk), sp_random, NULL) != 0) { sp_key_release(k); sp_unsupported_input(); return NULL; @@ -1339,17 +1487,14 @@ static SpKey *sp_generate(const ScrDyn *type, const ScrDyn *options) { return k; } -/* generateKeyPairSync(type, options): { publicKey, privateKey }, each a - * KeyObject or the encoding the options ask for. */ -ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { - if (options->kind == SCR_DYN_TYPED_REF) { - ScrDyn *value = scr_dyn_typed_ref_materialize(options); - if (!value) return NULL; - ScrDyn *result = scr_crypto_generate_key_pair_sync(type, value); - scr_dyn_release(value); - return result; - } - SpKey *k = sp_generate(type, options); +/* The { publicKey, privateKey } pair for (type, options), each a + * KeyObject or the encoding the options ask for; NULL with the error + * pending, and *job_error as sp_keygen_run sets it. */ +static ScrDyn *sp_key_pair(const ScrDyn *type, const ScrDyn *options, bool *job_error) { + *job_error = false; + SpKeyGen gen; + if (!sp_keygen_check(type, options, &gen)) return NULL; + SpKey *k = sp_keygen_run(&gen, job_error); if (!k) return NULL; SpKey *pub = sp_public_of(k); if (!pub) { @@ -1373,6 +1518,16 @@ ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { return pair; } +/* generateKeyPairSync(type, options). */ +ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options) { + ScrDyn *plain = sp_plain(options); + if (!plain) return NULL; + bool job_error; + ScrDyn *pair = sp_key_pair(type, plain, &job_error); + scr_dyn_release(plain); + return pair; +} + /* ── sign and verify ──────────────────────────────────────────────────── */ typedef struct { @@ -1931,12 +2086,44 @@ static void sp_key_pair_deliver(ScrClosure *self) { scr_dyn_release(callback); } -/* generateKeyPair(type, options, callback): argument errors throw, and the - * callback receives (null, publicKey, privateKey) in a later macrotask. */ +/* Calls the callback with the error alone, as Node does for a key + * generation that fails. */ +static void sp_key_pair_fail(ScrClosure *self) { + ScrDyn *callback = sp_cap(self, 0); + ScrDyn *error = sp_cap(self, 1); + ScrDyn *result = scr_dyn_call(callback, &error, 1, "callback"); + if (result) scr_dyn_release(result); + scr_dyn_release(error); + scr_dyn_release(callback); +} + +/* generateKeyPair(type, options, callback): a function in the options + * position is the callback. Argument errors throw; the callback receives + * (null, publicKey, privateKey), or the error of a key generation that + * fails, in a later macrotask. */ void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback) { - ScrDyn *pair = scr_crypto_generate_key_pair_sync(type, options); - if (!pair) return; - ScrClosure *c = scr_closure_new((void *)sp_key_pair_deliver, 2); + if (options->kind == SCR_DYN_FUNC) { + callback = options; + options = scr_dyn_undefined(); + } + if (callback->kind != SCR_DYN_FUNC) { + scr_dyn_arg_type_fail("callback", "of type function", callback); + return; + } + ScrDyn *plain = sp_plain(options); + if (!plain) return; + bool job_error; + ScrDyn *pair = sp_key_pair(type, plain, &job_error); + scr_dyn_release(plain); + if (!pair && !job_error) return; + void *deliver = (void *)sp_key_pair_deliver; + if (!pair) { + ScrCaught *caught = scr_exc_take(); + pair = scr_caught_to_dyn(caught); + scr_caught_release(caught); + deliver = (void *)sp_key_pair_fail; + } + ScrClosure *c = scr_closure_new(deliver, 2); c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); scr_box_set_ref(c->caps[0], scr_dyn_retain(callback)); c->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); diff --git a/tests/corpus/7445-crypto-key-pair-options.mjs b/tests/corpus/7445-crypto-key-pair-options.mjs new file mode 100644 index 000000000..86138e860 --- /dev/null +++ b/tests/corpus/7445-crypto-key-pair-options.mjs @@ -0,0 +1,105 @@ +// generateKeyPairSync and generateKeyPair check their arguments in Node's +// order: the callback, the type, the options object, the public and then +// the private key encoding, and then the options of the key type. The +// encodings follow Node's parseKeyEncoding rules for formats, types, +// ciphers and passphrases. +import * as crypto from "node:crypto"; + +function attempt(label, run) { + try { + const result = run(); + console.log(label, result === undefined ? "returned" : Object.keys(result)); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +const ec = (options) => () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", ...options }); +const rsa = (options) => () => crypto.generateKeyPairSync("rsa", { modulusLength: 1024, ...options }); + +// The type, then the options object, then the encodings, then the rest. +attempt("number type", () => crypto.generateKeyPairSync(5, 7)); +attempt("unknown type, bad encoding", () => crypto.generateKeyPairSync("foo", { publicKeyEncoding: 5 })); +attempt("unknown type, number options", () => crypto.generateKeyPairSync("foo", 5)); +attempt("unknown type, null options", () => crypto.generateKeyPairSync("foo", null)); +attempt("unknown type, array options", () => crypto.generateKeyPairSync("foo", [])); +attempt("unknown type", () => crypto.generateKeyPairSync("foo")); +attempt("upper-case types", () => crypto.generateKeyPairSync("RSA", {})); +attempt("upper-case Ed25519", () => crypto.generateKeyPairSync("Ed25519")); +attempt("upper-case ML-DSA", () => crypto.generateKeyPairSync("ML-DSA-44")); +attempt("unknown type, pkcs1", () => crypto.generateKeyPairSync("foo", { publicKeyEncoding: { format: "pem", type: "pkcs1" } })); +attempt("rsa, bad modulusLength and encoding", () => crypto.generateKeyPairSync("rsa", { modulusLength: "x", publicKeyEncoding: 5 })); +attempt("rsa, bad modulusLength and private encoding", () => crypto.generateKeyPairSync("rsa", { modulusLength: "x", privateKeyEncoding: 5 })); +attempt("rsa, negative modulusLength and format", () => crypto.generateKeyPairSync("rsa", { modulusLength: -1, publicKeyEncoding: { format: "x" } })); +attempt("rsa, bad exponent and encoding", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1024, publicExponent: "x", publicKeyEncoding: 5 })); +attempt("rsa-pss, bad encoding", () => crypto.generateKeyPairSync("rsa-pss", { publicKeyEncoding: 5 })); +attempt("rsa-pss, bad modulusLength", () => crypto.generateKeyPairSync("rsa-pss", { modulusLength: "x" })); +attempt("dsa, bad encoding", () => crypto.generateKeyPairSync("dsa", { publicKeyEncoding: 5 })); +attempt("dsa, no options", () => crypto.generateKeyPairSync("dsa")); +attempt("dh, number options", () => crypto.generateKeyPairSync("dh", 5)); +attempt("ec, no curve and bad encoding", () => crypto.generateKeyPairSync("ec", { publicKeyEncoding: 5 })); +attempt("ec, unknown curve and bad encoding", () => crypto.generateKeyPairSync("ec", { namedCurve: "nope", publicKeyEncoding: 5 })); +attempt("ec, number curve", () => crypto.generateKeyPairSync("ec", { namedCurve: 5 })); +attempt("ec, lower-case NIST name", () => crypto.generateKeyPairSync("ec", { namedCurve: "p-256" })); +attempt("ec, unknown curve and paramEncoding", () => crypto.generateKeyPairSync("ec", { namedCurve: "nope", paramEncoding: "bogus" })); +attempt("ec, paramEncoding bogus", ec({ paramEncoding: "bogus" })); +attempt("ec, paramEncoding named", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", paramEncoding: "named" })); +attempt("ec, paramEncoding null", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", paramEncoding: null })); +attempt("ed25519, no options", () => crypto.generateKeyPairSync("ed25519")); +attempt("ed25519, null options", () => crypto.generateKeyPairSync("ed25519", null)); +attempt("ed25519, number options", () => crypto.generateKeyPairSync("ed25519", 5)); +attempt("ed25519, bad encoding", () => crypto.generateKeyPairSync("ed25519", { publicKeyEncoding: 5 })); + +// Encodings. +attempt("both encodings invalid", rsa({ publicKeyEncoding: { format: "x" }, privateKeyEncoding: 5 })); +attempt("array encoding", rsa({ publicKeyEncoding: [] })); +attempt("null encodings", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: null, privateKeyEncoding: null })); +attempt("der without type", rsa({ publicKeyEncoding: { format: "der" } })); +attempt("pem without type", rsa({ privateKeyEncoding: { format: "pem" } })); +attempt("jwk without type", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", publicKeyEncoding: { format: "jwk" }, privateKeyEncoding: { format: "jwk" } })); +attempt("jwk with bad type", rsa({ publicKeyEncoding: { format: "jwk", type: "bogus" } })); +attempt("jwk with pkcs1 for ec", ec({ publicKeyEncoding: { format: "jwk", type: "pkcs1" } })); +attempt("public sec1", rsa({ publicKeyEncoding: { format: "pem", type: "sec1" } })); +attempt("public pkcs8", rsa({ publicKeyEncoding: { format: "pem", type: "pkcs8" } })); +attempt("private spki", rsa({ privateKeyEncoding: { format: "pem", type: "spki" } })); +attempt("private sec1 for rsa", rsa({ privateKeyEncoding: { format: "pem", type: "sec1" } })); +attempt("public pkcs1 for ec", ec({ publicKeyEncoding: { format: "pem", type: "pkcs1" } })); +attempt("private pkcs1 for ec", ec({ privateKeyEncoding: { format: "der", type: "pkcs1" } })); +attempt("public pkcs1 for ed25519", () => crypto.generateKeyPairSync("ed25519", { publicKeyEncoding: { format: "pem", type: "pkcs1" } })); +attempt("private sec1 for ed25519", () => crypto.generateKeyPairSync("ed25519", { privateKeyEncoding: { format: "pem", type: "sec1" } })); +attempt("raw-public as private", ec({ privateKeyEncoding: { format: "raw-public" } })); +attempt("raw-private as public", ec({ publicKeyEncoding: { format: "raw-private" } })); +attempt("raw-seed as public", ec({ publicKeyEncoding: { format: "raw-seed" } })); +attempt("raw-public with bad type", ec({ publicKeyEncoding: { format: "raw-public", type: "x" } })); +attempt("raw-private with a type", ec({ privateKeyEncoding: { format: "raw-private", type: "pkcs8" } })); +attempt("raw-private with a passphrase", ec({ privateKeyEncoding: { format: "raw-private", passphrase: "x" } })); +attempt("raw-seed with a cipher", ec({ privateKeyEncoding: { format: "raw-seed", cipher: "aes-128-cbc" } })); +attempt("number cipher", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: 5 } })); +attempt("passphrase without cipher", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", passphrase: "x" } })); +attempt("passphrase with null cipher", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: null, passphrase: "x" } })); +attempt("der pkcs1 with a cipher", rsa({ privateKeyEncoding: { format: "der", type: "pkcs1", cipher: "aes-128-cbc", passphrase: "x" } })); +attempt("der sec1 with a cipher", ec({ privateKeyEncoding: { format: "der", type: "sec1", cipher: "aes-128-cbc", passphrase: "x" } })); +attempt("number passphrase", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: "aes-128-cbc", passphrase: 5 } })); +attempt("missing passphrase", rsa({ privateKeyEncoding: { format: "pem", type: "pkcs8", cipher: "aes-128-cbc" } })); +attempt("encoding errors before the modulusLength", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1.5, privateKeyEncoding: { format: "pem", type: "pkcs8", passphrase: "x" } })); + +// generateKeyPair checks its callback first, reads a function in the +// options position as the callback, and throws argument errors. +attempt("async, options as the callback", () => crypto.generateKeyPair("rsa", { modulusLength: 1024 })); +attempt("async, undefined callback", () => crypto.generateKeyPair("rsa", { modulusLength: 1024 }, undefined)); +attempt("async, number callback", () => crypto.generateKeyPair("rsa", { modulusLength: 1024 }, 5)); +attempt("async, number type without callback", () => crypto.generateKeyPair(5, {})); +attempt("async, unknown type", () => crypto.generateKeyPair("foo", undefined, () => console.log("never"))); +attempt("async, ec without options", () => crypto.generateKeyPair("ec", () => console.log("never"))); +attempt("async, bad encoding", () => crypto.generateKeyPair("ec", { namedCurve: "P-256", publicKeyEncoding: { format: "pem", type: "pkcs1" } }, () => console.log("never"))); +attempt("async, shifted callback", () => + crypto.generateKeyPair("ed25519", (err, publicKey, privateKey) => { + console.log("shifted callback", err, publicKey.asymmetricKeyType, privateKey.type); + crypto.generateKeyPair( + "ed25519", + (err2, publicKey2) => console.log("options callback", err2, publicKey2.type), + () => console.log("never"), + ); + }), +); +console.log("returned"); diff --git a/tests/corpus/7446-crypto-rsa-key-pair-errors.ts b/tests/corpus/7446-crypto-rsa-key-pair-errors.ts new file mode 100644 index 000000000..ca5416bcd --- /dev/null +++ b/tests/corpus/7446-crypto-rsa-key-pair-errors.ts @@ -0,0 +1,72 @@ +// RSA key generation parameters: publicExponent's validateUint32 checks +// and its 65537 default, and the errors OpenSSL reports for modulus +// lengths and exponents it does not generate, in OpenSSL's order. +// generateKeyPairSync throws those errors and generateKeyPair passes them +// to its callback, alone, in a later macrotask. +import * as crypto from "node:crypto"; + +type RsaOptions = { modulusLength: number; publicExponent?: unknown }; + +function attempt(label: string, options: RsaOptions): void { + try { + const { publicKey } = crypto.generateKeyPairSync("rsa", options as crypto.RSAKeyPairKeyObjectOptions); + const details = publicKey.asymmetricKeyDetails!; + console.log(label, details.modulusLength, details.publicExponent); + } catch (e) { + const error = e as NodeJS.ErrnoException; + console.log(label, "throws", error.name, error.code, error.message); + } +} + +attempt("exponent 1.5", { modulusLength: 1024, publicExponent: 1.5 }); +attempt("exponent NaN", { modulusLength: 1024, publicExponent: NaN }); +attempt("exponent Infinity", { modulusLength: 1024, publicExponent: Infinity }); +attempt("exponent -1", { modulusLength: 1024, publicExponent: -1 }); +attempt("exponent 2 ** 32", { modulusLength: 1024, publicExponent: 2 ** 32 }); +attempt("exponent string", { modulusLength: 1024, publicExponent: "3" }); +attempt("exponent bigint", { modulusLength: 1024, publicExponent: 3n }); +attempt("exponent null", { modulusLength: 1024, publicExponent: null }); +attempt("exponent undefined", { modulusLength: 1024, publicExponent: undefined }); +attempt("exponent 3", { modulusLength: 1024, publicExponent: 3 }); +attempt("exponent 17", { modulusLength: 1024, publicExponent: 17 }); +attempt("exponent 0", { modulusLength: 1024, publicExponent: 0 }); +attempt("exponent -0", { modulusLength: 1024, publicExponent: -0 }); +attempt("exponent 1", { modulusLength: 1024, publicExponent: 1 }); +attempt("exponent 2", { modulusLength: 1024, publicExponent: 2 }); +attempt("exponent 4", { modulusLength: 1024, publicExponent: 4 }); +attempt("exponent 2 ** 31", { modulusLength: 1024, publicExponent: 2 ** 31 }); +attempt("modulusLength 0", { modulusLength: 0 }); +attempt("modulusLength -0", { modulusLength: -0 }); +attempt("modulusLength 100", { modulusLength: 100 }); +attempt("modulusLength 511", { modulusLength: 511 }); +attempt("modulusLength 2 ** 31", { modulusLength: 2 ** 31 }); +attempt("modulusLength 2 ** 32 - 1", { modulusLength: 2 ** 32 - 1 }); +attempt("modulusLength 100, exponent 4", { modulusLength: 100, publicExponent: 4 }); +attempt("modulusLength 100, exponent 0", { modulusLength: 100, publicExponent: 0 }); +attempt("modulusLength 2 ** 31, exponent 0", { modulusLength: 2 ** 31, publicExponent: 0 }); +attempt("modulusLength 2 ** 31, exponent 4", { modulusLength: 2 ** 31, publicExponent: 4 }); + +function later(label: string, options: RsaOptions, next: () => void): void { + try { + crypto.generateKeyPair("rsa", options as crypto.RSAKeyPairKeyObjectOptions, (...args: unknown[]) => { + const error = args[0] as Error | null; + console.log(label, "callback", args.length, error === null ? null : [error.name, error.message]); + next(); + }); + console.log(label, "returned"); + } catch (e) { + console.log(label, "throws", (e as Error).message); + next(); + } +} + +later("async exponent 4", { modulusLength: 1024, publicExponent: 4 }, () => + later("async modulusLength 100", { modulusLength: 100 }, () => + later("async exponent 0", { modulusLength: 1024, publicExponent: 0 }, () => + later("async exponent 1.5", { modulusLength: 1024, publicExponent: 1.5 }, () => + later("async exponent 3", { modulusLength: 1024, publicExponent: 3 }, () => console.log("done")), + ), + ), + ), +); +process.nextTick(() => console.log("tick")); diff --git a/tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json b/tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json new file mode 100644 index 000000000..273d0c784 --- /dev/null +++ b/tests/harness/corpus-costs/7445-crypto-key-pair-options.mjs.json @@ -0,0 +1 @@ +{ "plain": 3596, "san": 5499 } diff --git a/tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json b/tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json new file mode 100644 index 000000000..ed5d25bcc --- /dev/null +++ b/tests/harness/corpus-costs/7446-crypto-rsa-key-pair-errors.ts.json @@ -0,0 +1 @@ +{ "plain": 7539, "san": 19613 } From 91e15f34267365c7464c61b4ff42a2e67dce2c77 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:40:52 -0500 Subject: [PATCH 068/409] Run child_process and http2 events in Node's AsyncLocalStorage contexts child_process and http2 events ran with no store, or with the context of whichever socket dispatch delivered them. Node runs them in the context of the handle or request that produces them. child_process: a child, its piped stdout, stderr and stdin, and its IPC channel keep the context of the spawn() or fork() call. 'spawn', 'error', 'exit', 'close', stdout's and stderr's 'data' and 'end', and the channel's 'message' and 'disconnect' run in it, in the POSIX and the Windows implementations. stdin's 'drain' and 'error' run in the context of the latest write() call, its 'finish' in the context of its end() call, and a send() callback in the context of its send() call. http2: a session keeps the context it was created in, the http2.connect() call for a client and the server's listen() context for a server session, and the session's socket callbacks and deferred emits run in it. A client stream keeps the context of its request() call, and the frames that drive a stream's events (DATA, WINDOW_UPDATE, RST_STREAM, and a client's HEADERS) and its deferred emits run in the stream's context. A session remembers the stream its last lookup by id found, so the context lookup and the frame's own handling share one search. A server stream and its compatibility request and response run with no store, as Node's do. An accepted h2c socket's own events run with no store, as an accepted net socket's do, so a server.close() that waits for its sessions calls back with no store; a TLS server's sockets keep the listen() context, as before. Corpus 7385 to 7389. --- .../builtin-apis/asynclocalstorage.mdx | 4 +- .../generated/node-v24-internal.json | 61 ++++- internal/compatibility/static-support.json | 2 +- .../7385-als-child-process-events.ts.json | 6 + .../7386-als-child-fork-events/main.mjs.json | 6 + .../corpus/7387-als-http2-events.mjs.json | 6 + .../7388-als-http2-compat-events.mjs.json | 6 + .../7389-als-http2-secure-events.mjs.json | 6 + packages/runtime/src/scr_child.c | 221 +++++++++++++++++- packages/runtime/src/scr_http2.c | 95 +++++++- tests/corpus/7385-als-child-process-events.ts | 104 +++++++++ .../7386-als-child-fork-events/main.mjs | 25 ++ .../7386-als-child-fork-events/worker.mjs | 4 + tests/corpus/7387-als-http2-events.mjs | 91 ++++++++ tests/corpus/7388-als-http2-compat-events.mjs | 49 ++++ tests/corpus/7389-als-http2-secure-events.mjs | 94 ++++++++ .../7385-als-child-process-events.ts.json | 1 + .../7386-als-child-fork-events/main.mjs.json | 1 + .../7387-als-http2-events.mjs.json | 1 + .../7388-als-http2-compat-events.mjs.json | 1 + .../7389-als-http2-secure-events.mjs.json | 1 + 21 files changed, 766 insertions(+), 19 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json create mode 100644 tests/corpus/7385-als-child-process-events.ts create mode 100644 tests/corpus/7386-als-child-fork-events/main.mjs create mode 100644 tests/corpus/7386-als-child-fork-events/worker.mjs create mode 100644 tests/corpus/7387-als-http2-events.mjs create mode 100644 tests/corpus/7388-als-http2-compat-events.mjs create mode 100644 tests/corpus/7389-als-http2-secure-events.mjs create mode 100644 tests/harness/corpus-costs/7385-als-child-process-events.ts.json create mode 100644 tests/harness/corpus-costs/7386-als-child-fork-events/main.mjs.json create mode 100644 tests/harness/corpus-costs/7387-als-http2-events.mjs.json create mode 100644 tests/harness/corpus-costs/7388-als-http2-compat-events.mjs.json create mode 100644 tests/harness/corpus-costs/7389-als-http2-secure-events.mjs.json diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx index 016b712f6..ce77d57f5 100644 --- a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -6,5 +6,7 @@ Stream events and callbacks run with the stores of the code that triggers them, as in Node. Stream steps that Node defers with `process.nextTick` see the context that scheduled them, and `stream.finished` and `stream.pipeline` callbacks see the context of their call. `fs` read and write streams and `zlib` streams run their events and callbacks in the context that created them. - Events of `child_process` children and of `http2` sessions and streams do not yet run in the contexts Node gives them. + Events of `child_process` children, their standard streams and their IPC channels see the context of the `spawn()` or `fork()` call; stdin's `'finish'` and `send()` callbacks see the context of their call. + A client `http2` session's events see the context of its `http2.connect()` call, and a client stream's events the context of its `request()` call. + An `http2` server's `'session'` and `'stream'` events and the compatibility API's request handler see the context of its `listen()` call; a server stream's own events, and its compatibility request's and response's, run with no store, as in Node. `AsyncResource` has no lowering yet. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index b5542944d..58a707f92 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1394,7 +1394,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "chapter" @@ -1451,7 +1456,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1531,7 +1541,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1565,7 +1580,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1599,7 +1619,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1633,7 +1658,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1667,7 +1697,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" @@ -1978,6 +2013,11 @@ "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs", "tests/harness/node-test.test.ts" ] }, @@ -2547,7 +2587,12 @@ "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", - "tests/corpus/7384-als-stream-events.mjs" + "tests/corpus/7384-als-stream-events.mjs", + "tests/corpus/7385-als-child-process-events.ts", + "tests/corpus/7386-als-child-fork-events/main.mjs", + "tests/corpus/7387-als-http2-events.mjs", + "tests/corpus/7388-als-http2-compat-events.mjs", + "tests/corpus/7389-als-http2-secure-events.mjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 640345a02..a711bcf3c 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -429,7 +429,7 @@ { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs", "tests/corpus/7385-als-child-process-events.ts", "tests/corpus/7386-als-child-fork-events/main.mjs", "tests/corpus/7387-als-http2-events.mjs", "tests/corpus/7388-als-http2-compat-events.mjs", "tests/corpus/7389-als-http2-secure-events.mjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json new file mode 100644 index 000000000..aac67defb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7385-als-child-process-events.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7385-als-child-process-events.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json new file mode 100644 index 000000000..a334cf0aa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7386-als-child-fork-events/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7386-als-child-fork-events/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json new file mode 100644 index 000000000..272024d1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7387-als-http2-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7387-als-http2-events.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json new file mode 100644 index 000000000..8a64f395e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7388-als-http2-compat-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7388-als-http2-compat-events.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json new file mode 100644 index 000000000..aec8e2d2a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7389-als-http2-secure-events.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7389-als-http2-secure-events.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index ec685656d..19633f8dd 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -1404,6 +1404,10 @@ struct ScrChildStream { size_t n_data, cap_data; ScrChildStreamEndL *end_ls; size_t n_end, cap_end; + /* The AsyncLocalStorage context of the spawn() call that created the + * stream (+1, NULL outside a store): its events run in it, as Node's + * pipe handles run theirs. */ + ScrAlsCtx *als; struct ScrChildStream *next; /* the service registry (+1) */ }; @@ -1432,6 +1436,11 @@ struct ScrChildWriter { size_t n_finish, cap_finish; ScrChildErrEntry *err_ls; size_t n_write_err, cap_write_err; + /* AsyncLocalStorage contexts (+1 each, NULL outside a store): the + * latest write() call's, for 'drain' and 'error' (the spawn() call's + * until the first write), and the end() call's, for 'finish'. */ + ScrAlsCtx *als; + ScrAlsCtx *end_als; struct ScrChildWriter *next; /* the service registry (+1) */ }; @@ -1478,6 +1487,9 @@ struct ScrChild { ScrChildWriter *in_stream; ScrDyn *out_dyn, *err_dyn, *in_dyn; ScrIpc *ipc; + /* The AsyncLocalStorage context of the spawn() call (+1, NULL outside + * a store): 'spawn', 'error', 'exit' and 'close' run in it. */ + ScrAlsCtx *als; struct ScrChild *next; /* the pending registry */ }; @@ -1536,6 +1548,7 @@ void scr_child_release(ScrChild *c) { scr_child_stream_release(c->err_stream); scr_child_writer_release(c->in_stream); scr_ipc_release(c->ipc); + scr_als_ctx_release(c->als); if (c->proc != NULL) CloseHandle(c->proc); free(c); } @@ -1568,6 +1581,7 @@ void scr_child_stream_release(ScrChildStream *s) { if (s->h != NULL) CloseHandle(s->h); scr_str_release(s->encoding); free(s->capture_data); + scr_als_ctx_release(s->als); free(s); } } @@ -1587,6 +1601,7 @@ static ScrChildStream *scr_child_stream_new(HANDLE h) { ScrChildStream *s = calloc(1, sizeof *s); if (!s) scr_child_oom(); s->rc = 1; + s->als = scr_als_capture(); s->h = h; if (h == NULL) { s->eof = true; @@ -1633,8 +1648,16 @@ static ScrStr *scr_child_stream_capture_value(ScrChildStream *s) { : scr_str_new((const char *)s->capture_data, s->capture_len); } -static void scr_child_stream_emit(ScrChildStream *s, ScrBytes *bytes, - ScrStr *text) { +static void scr_child_stream_emit_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text); + +/* 'data' runs in the spawn() call's context. */ +static void scr_child_stream_emit(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_emit_in(s, bytes, text); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_emit_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { size_t nd = s->n_data; if (nd == 0) return; ScrChildStreamDataL *snap = malloc(nd * sizeof *snap); @@ -1676,7 +1699,16 @@ static void scr_child_stream_emit(ScrChildStream *s, ScrBytes *bytes, free(snap); } +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end); + +/* A stream's 'end' runs in the spawn() call's context. */ static void scr_child_stream_finish(ScrChildStream *s, bool fire_end) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_finish_in(s, fire_end); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end) { if (fire_end && s->encoding != NULL && s->n_data > 0) { ScrStr *tail = scr_strdec_end(s->encoding, s->dec_pending); s->dec_pending = 0; @@ -1907,6 +1939,8 @@ void scr_child_writer_release(ScrChildWriter *w) { free(w->buf); scr_str_release(w->err_msg); scr_child_writer_drop_listeners(w); + scr_als_ctx_release(w->als); + scr_als_ctx_release(w->end_als); free(w); } } @@ -1922,6 +1956,7 @@ static ScrChildWriter *scr_child_writer_new(HANDLE h) { ScrChildWriter *w = calloc(1, sizeof *w); if (!w) scr_child_oom(); w->rc = 1; + w->als = scr_als_capture(); w->h = h; w->writable = h != NULL; w->destroyed = h == NULL; @@ -2068,6 +2103,8 @@ static void scr_child_writer_pump(ScrChildWriter *w) { static bool scr_child_writer_write(ScrChildWriter *w, const void *data, size_t len) { + scr_als_ctx_release(w->als); + w->als = scr_als_capture(); if (!w->writable) { scr_child_writer_fail(w, "write after end", "ERR_STREAM_WRITE_AFTER_END"); return false; @@ -2098,6 +2135,8 @@ bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data) { void scr_child_writer_end(ScrChildWriter *w) { if (w->ending || w->finish_pending || w->finished || w->destroyed) return; + scr_als_ctx_release(w->end_als); + w->end_als = scr_als_capture(); w->writable = false; w->ending = true; scr_child_writer_pump(w); @@ -2165,7 +2204,16 @@ void scr_child_writer_on_error(ScrChildWriter *w, ScrClosure *cb /*moves*/, w->err_ls[w->n_write_err++] = (ScrChildErrEntry){cb, fn}; } +static void scr_child_writer_fire_drain_in(ScrChildWriter *w); + +/* 'drain' runs in the latest write() call's context. */ static void scr_child_writer_fire_drain(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_drain_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_drain_in(ScrChildWriter *w) { w->drain_pending = false; size_t n = w->n_drain; if (n == 0) return; @@ -2192,7 +2240,16 @@ static void scr_child_writer_fire_drain(ScrChildWriter *w) { free(snap); } +static void scr_child_writer_fire_finish_in(ScrChildWriter *w); + +/* 'finish' runs in the end() call's context. */ static void scr_child_writer_fire_finish(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->end_als); + scr_child_writer_fire_finish_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_finish_in(ScrChildWriter *w) { w->finish_pending = false; w->finished = true; w->destroyed = true; @@ -2211,7 +2268,16 @@ static void scr_child_writer_fire_finish(ScrChildWriter *w) { scr_child_writer_drop_zero_listeners(w); } +static void scr_child_writer_fire_error_in(ScrChildWriter *w); + +/* 'error' runs in the latest write() call's context. */ static void scr_child_writer_fire_error(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_error_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_error_in(ScrChildWriter *w) { w->error_pending = false; if (w->n_write_err == 0) { fflush(stdout); @@ -2305,6 +2371,7 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, ScrChild *c = calloc(1, sizeof(ScrChild)); if (!c) scr_child_oom(); c->rc = 1; + c->als = scr_als_capture(); /* Piped slots: the pipe NOW; a pipe failure degrades the slot to NUL * and the stream husk answers eof (fd exhaustion — nothing real). */ @@ -2703,7 +2770,16 @@ static bool scr_child_close_ready(const ScrChild *c) { scr_ipc_close_ready(c->ipc); } +static void scr_child_fire_close_in(ScrChild *c); + +/* 'close' runs in the spawn() call's context. */ static void scr_child_fire_close(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_fire_close_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_fire_close_in(ScrChild *c) { c->close_done = true; bool has_code = c->state == SCR_CHILD_SPAWN_FAILED ? true : c->has_code; double code = c->state == SCR_CHILD_SPAWN_FAILED @@ -2767,7 +2843,16 @@ bool scr_children_wait(double max_wait_ms) { /* Fires one settled child's terminal event — the POSIX arm's settle, * verbatim (streams drain first so 'end' precedes 'exit'; spawn * failures fire 'error' or die unhandled). */ +static void scr_child_settle_in(ScrChild *c); + +/* A child's terminal events run in its spawn() call's context. */ static void scr_child_settle(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_settle_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_settle_in(ScrChild *c) { if (c->reffed) { c->reffed = false; scr_children_reffed_n--; @@ -3815,6 +3900,10 @@ struct ScrChildStream { size_t n_data, cap_data; ScrChildStreamEndL *end_ls; size_t n_end, cap_end; + /* The AsyncLocalStorage context of the spawn() call that created the + * stream (+1, NULL outside a store): its events run in it, as Node's + * pipe handles run theirs. */ + ScrAlsCtx *als; struct ScrChildStream *next; /* the service registry (+1) */ }; @@ -3839,6 +3928,11 @@ struct ScrChildWriter { size_t n_finish, cap_finish; ScrChildErrEntry *err_ls; size_t n_write_err, cap_write_err; + /* AsyncLocalStorage contexts (+1 each, NULL outside a store): the + * latest write() call's, for 'drain' and 'error' (the spawn() call's + * until the first write), and the end() call's, for 'finish'. */ + ScrAlsCtx *als; + ScrAlsCtx *end_als; struct ScrChildWriter *next; /* the service registry (+1) */ }; @@ -3889,6 +3983,9 @@ struct ScrChild { ScrChildWriter *in_stream; ScrDyn *out_dyn, *err_dyn, *in_dyn; ScrIpc *ipc; + /* The AsyncLocalStorage context of the spawn() call (+1, NULL outside + * a store): 'spawn', 'error', 'exit' and 'close' run in it. */ + ScrAlsCtx *als; struct ScrChild *next; /* the pending registry */ }; @@ -4085,6 +4182,7 @@ void scr_child_release(ScrChild *c) { scr_child_stream_release(c->err_stream); scr_child_writer_release(c->in_stream); scr_ipc_release(c->ipc); + scr_als_ctx_release(c->als); free(c); } } @@ -4137,6 +4235,7 @@ void scr_child_stream_release(ScrChildStream *s) { if (s->fd >= 0) close(s->fd); scr_str_release(s->encoding); free(s->capture_data); + scr_als_ctx_release(s->als); free(s); } } @@ -4159,6 +4258,7 @@ static ScrChildStream *scr_child_stream_new(int fd) { ScrChildStream *s = calloc(1, sizeof *s); if (!s) scr_child_oom(); s->rc = 1; + s->als = scr_als_capture(); s->fd = fd; if (fd < 0) { s->eof = true; @@ -4213,8 +4313,16 @@ static ScrStr *scr_child_stream_capture_value(ScrChildStream *s) { : scr_str_new((const char *)s->capture_data, s->capture_len); } -static void scr_child_stream_emit_posix(ScrChildStream *s, ScrBytes *bytes, - ScrStr *text) { +static void scr_child_stream_emit_posix_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text); + +/* 'data' runs in the spawn() call's context. */ +static void scr_child_stream_emit_posix(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_emit_posix_in(s, bytes, text); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_emit_posix_in(ScrChildStream *s, ScrBytes *bytes, ScrStr *text) { size_t nd = s->n_data; if (nd == 0) return; ScrChildStreamDataL *snap = malloc(nd * sizeof *snap); @@ -4264,7 +4372,16 @@ static void scr_child_stream_emit_posix(ScrChildStream *s, ScrBytes *bytes, /* EOF/error tail: 'end' fires over a snapshot, everything drops, the fd * closes (deleting its kqueue filter), and the liveness/fallback counters * settle. The caller unlinks from the registry and releases its ref. */ +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end); + +/* A stream's 'end' runs in the spawn() call's context. */ static void scr_child_stream_finish(ScrChildStream *s, bool fire_end) { + ScrAlsCtx *prev = scr_als_dispatch_enter(s->als); + scr_child_stream_finish_in(s, fire_end); + scr_als_dispatch_leave(prev); +} + +static void scr_child_stream_finish_in(ScrChildStream *s, bool fire_end) { if (fire_end && s->encoding != NULL && s->n_data > 0) { ScrStr *tail = scr_strdec_end(s->encoding, s->dec_pending); s->dec_pending = 0; @@ -4572,6 +4689,8 @@ void scr_child_writer_release(ScrChildWriter *w) { free(w->buf); scr_str_release(w->err_msg); scr_child_writer_drop_listeners(w); + scr_als_ctx_release(w->als); + scr_als_ctx_release(w->end_als); free(w); } } @@ -4587,6 +4706,7 @@ static ScrChildWriter *scr_child_writer_new(int fd) { ScrChildWriter *w = calloc(1, sizeof *w); if (!w) scr_child_oom(); w->rc = 1; + w->als = scr_als_capture(); w->fd = fd; w->writable = fd >= 0; w->destroyed = fd < 0; @@ -4720,6 +4840,8 @@ static void scr_child_writer_pump(ScrChildWriter *w) { static bool scr_child_writer_write(ScrChildWriter *w, const void *data, size_t len) { + scr_als_ctx_release(w->als); + w->als = scr_als_capture(); if (!w->writable) { scr_child_writer_fail(w, "write after end", "ERR_STREAM_WRITE_AFTER_END"); return false; @@ -4756,6 +4878,8 @@ bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data) { void scr_child_writer_end(ScrChildWriter *w) { if (w->ending || w->finish_pending || w->finished || w->destroyed) return; + scr_als_ctx_release(w->end_als); + w->end_als = scr_als_capture(); w->writable = false; w->ending = true; scr_child_writer_pump(w); @@ -4823,7 +4947,16 @@ void scr_child_writer_on_error(ScrChildWriter *w, ScrClosure *cb /*moves*/, w->err_ls[w->n_write_err++] = (ScrChildErrEntry){cb, fn}; } +static void scr_child_writer_fire_drain_in(ScrChildWriter *w); + +/* 'drain' runs in the latest write() call's context. */ static void scr_child_writer_fire_drain(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_drain_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_drain_in(ScrChildWriter *w) { w->drain_pending = false; size_t n = w->n_drain; if (n == 0) return; @@ -4850,7 +4983,16 @@ static void scr_child_writer_fire_drain(ScrChildWriter *w) { free(snap); } +static void scr_child_writer_fire_finish_in(ScrChildWriter *w); + +/* 'finish' runs in the end() call's context. */ static void scr_child_writer_fire_finish(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->end_als); + scr_child_writer_fire_finish_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_finish_in(ScrChildWriter *w) { w->finish_pending = false; w->finished = true; w->destroyed = true; @@ -4869,7 +5011,16 @@ static void scr_child_writer_fire_finish(ScrChildWriter *w) { scr_child_writer_drop_zero_listeners(w); } +static void scr_child_writer_fire_error_in(ScrChildWriter *w); + +/* 'error' runs in the latest write() call's context. */ static void scr_child_writer_fire_error(ScrChildWriter *w) { + ScrAlsCtx *prev = scr_als_dispatch_enter(w->als); + scr_child_writer_fire_error_in(w); + scr_als_dispatch_leave(prev); +} + +static void scr_child_writer_fire_error_in(ScrChildWriter *w) { w->error_pending = false; if (w->n_write_err == 0) { fflush(stdout); @@ -4973,6 +5124,7 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode, ScrChild *c = calloc(1, sizeof(ScrChild)); if (!c) scr_child_oom(); c->rc = 1; + c->als = scr_als_capture(); /* Piped slots: pipe(2) NOW, both ends cloexec (the file action's dup2 * clears the flag on the child's copy — every other descriptor closes @@ -5489,7 +5641,16 @@ static bool scr_child_close_ready(const ScrChild *c) { scr_ipc_close_ready(c->ipc); } +static void scr_child_fire_close_in(ScrChild *c); + +/* 'close' runs in the spawn() call's context. */ static void scr_child_fire_close(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_fire_close_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_fire_close_in(ScrChild *c) { c->close_done = true; bool has_code = c->state == SCR_CHILD_SPAWN_FAILED ? true : c->has_code; double code = c->state == SCR_CHILD_SPAWN_FAILED @@ -5527,7 +5688,16 @@ static void scr_children_closing_poll(void) { * ordering runs first: consumer-owning piped streams drain to EOF/EAGAIN * so their 'end' events fire BEFORE 'exit' (Node-exact — verified: data*, * end, then exit for every output shape). */ +static void scr_child_settle_in(ScrChild *c); + +/* A child's terminal events run in its spawn() call's context. */ static void scr_child_settle(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_settle_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_settle_in(ScrChild *c) { if (c->reffed) { c->reffed = false; scr_children_reffed_n--; @@ -5695,7 +5865,16 @@ void scr_child_on_spawn(ScrChild *c, ScrClosure *cb) { /* Success is observable on the next loop pass, before any data or exit * event. Detaching the snapshot keeps listeners registered by a callback * from participating in an event that has already started. */ +static void scr_child_fire_spawn_in(ScrChild *c); + +/* 'spawn' runs in the spawn() call's context. */ static void scr_child_fire_spawn(ScrChild *c) { + ScrAlsCtx *prev = scr_als_dispatch_enter(c->als); + scr_child_fire_spawn_in(c); + scr_als_dispatch_leave(prev); +} + +static void scr_child_fire_spawn_in(ScrChild *c) { c->spawn_done = true; ScrClosure **callbacks = c->spawn_cbs; size_t count = c->n_spawn; @@ -5901,6 +6080,7 @@ typedef struct { typedef struct { ScrClosure *cb; ScrIpcSendFn fn; + ScrAlsCtx *ctx; /* the send() call's context (+1): the callback runs in it */ } ScrIpcSendEntry; struct ScrIpc { @@ -5928,6 +6108,9 @@ struct ScrIpc { ScrIpcSendEntry *send_cbs; size_t n_send, cap_send; ScrStr *send_error; + /* The context the channel was created in (+1): 'message' and + * 'disconnect' run in it. */ + ScrAlsCtx *als; struct ScrIpc *next; }; @@ -5963,8 +6146,12 @@ static void scr_ipc_release(ScrIpc *ipc) { for (size_t i = 0; i < ipc->n_pending; i++) scr_dyn_release(ipc->pending[i]); for (size_t i = 0; i < ipc->n_message; i++) scr_closure_release(ipc->message_ls[i].cb); for (size_t i = 0; i < ipc->n_disconnect; i++) scr_closure_release(ipc->disconnect_ls[i].cb); - for (size_t i = 0; i < ipc->n_send; i++) scr_closure_release(ipc->send_cbs[i].cb); + for (size_t i = 0; i < ipc->n_send; i++) { + scr_closure_release(ipc->send_cbs[i].cb); + scr_als_ctx_release(ipc->send_cbs[i].ctx); + } scr_str_release(ipc->send_error); + scr_als_ctx_release(ipc->als); free(ipc->buffer); free(ipc->pending); free(ipc->message_ls); @@ -6084,6 +6271,7 @@ static ScrIpc *scr_ipc_new(ScrChildStream *reader, ScrChildWriter *writer) { ScrIpc *ipc = calloc(1, sizeof(*ipc)); if (!ipc) scr_trap("out of memory"); ipc->rc = 1; + ipc->als = scr_als_capture(); ipc->connected = true; ipc->reader = reader; ipc->writer = writer; @@ -6114,7 +6302,7 @@ static void scr_ipc_add_send(ScrIpc *ipc, ScrClosure *cb, ScrIpcSendFn fn) { ipc->send_cbs = realloc(ipc->send_cbs, ipc->cap_send * sizeof(*ipc->send_cbs)); if (!ipc->send_cbs) scr_trap("out of memory"); } - ipc->send_cbs[ipc->n_send++] = (ScrIpcSendEntry){cb, fn}; + ipc->send_cbs[ipc->n_send++] = (ScrIpcSendEntry){cb, fn, scr_als_capture()}; } static bool scr_ipc_send(ScrIpc *ipc, ScrStr *json, ScrClosure *cb, @@ -6198,7 +6386,16 @@ static void scr_ipc_remove_message_once(ScrIpc *ipc, ScrClosure *cb) { } } +static void scr_ipc_fire_messages_in(ScrIpc *ipc); + +/* 'message' runs in the channel's creation context. */ static void scr_ipc_fire_messages(ScrIpc *ipc) { + ScrAlsCtx *prev = scr_als_dispatch_enter(ipc->als); + scr_ipc_fire_messages_in(ipc); + scr_als_dispatch_leave(prev); +} + +static void scr_ipc_fire_messages_in(ScrIpc *ipc) { while (ipc->n_pending > 0 && ipc->n_message > 0 && !scr_exc_pending()) { ScrDyn *message = ipc->pending[0]; memmove(ipc->pending, ipc->pending + 1, @@ -6235,16 +6432,28 @@ static void scr_ipc_fire_send_callbacks(ScrIpc *ipc) { error = scr_error_new(0, ipc->send_error); scr_error_set_code(error, "ERR_IPC_CHANNEL_CLOSED"); } + ScrAlsCtx *prev = scr_als_dispatch_enter(snapshot[i].ctx); if (!scr_exc_pending()) snapshot[i].fn(snapshot[i].cb, error); else scr_error_release(error); + scr_als_dispatch_leave(prev); scr_closure_release(snapshot[i].cb); + scr_als_ctx_release(snapshot[i].ctx); } free(snapshot); scr_str_release(ipc->send_error); ipc->send_error = NULL; } +static void scr_ipc_fire_disconnect_in(ScrIpc *ipc); + +/* 'disconnect' runs in the channel's creation context. */ static void scr_ipc_fire_disconnect(ScrIpc *ipc) { + ScrAlsCtx *prev = scr_als_dispatch_enter(ipc->als); + scr_ipc_fire_disconnect_in(ipc); + scr_als_dispatch_leave(prev); +} + +static void scr_ipc_fire_disconnect_in(ScrIpc *ipc) { if (!ipc->disconnect_pending || ipc->disconnect_emitted) return; ipc->disconnect_pending = false; ipc->disconnect_emitted = true; diff --git a/packages/runtime/src/scr_http2.c b/packages/runtime/src/scr_http2.c index 4c5267e6a..55d880c01 100644 --- a/packages/runtime/src/scr_http2.c +++ b/packages/runtime/src/scr_http2.c @@ -678,6 +678,10 @@ struct ScrH2Stream { int64_t send_window; ScrNetLs data_ls, end_ls, close_ls, err_ls, resp_ls, aborted_ls, ready_ls, trailers_ls, wanttrailers_ls, push_ls, frameerr_ls; + /* The AsyncLocalStorage context of the request() call that created a + * client stream (+1, NULL outside a store); NULL on server streams, + * whose own events Node runs with no store. */ + ScrAlsCtx *als; }; /* The server-side ctx (the ScrHttpSrvCtx shape; `proto` FIRST — the @@ -757,6 +761,14 @@ struct ScrH2Session { bool error_emitted; ScrNetLs stream_ls, close_ls, err_ls, connect_ls, goaway_ls, remote_settings_ls, local_settings_ls; + /* The stream the last lookup by id found (borrowed from `streams`): + * the frames of one stream arrive together, and a frame's context + * lookup and its handling ask for the same stream. */ + ScrH2Stream *last_found; + /* The AsyncLocalStorage context the session was created in (+1, NULL + * outside a store): the connect() call's for a client session, the + * server's listen() context for a server session. */ + ScrAlsCtx *als; }; /* ── refcounts + the RC audit ────────────────────────────────────────── */ @@ -838,6 +850,7 @@ static void scr_h2_stream_free(ScrH2Stream *st) { } free(st->wbuf.data); scr_http2_session_release(st->sess); + scr_als_ctx_release(st->als); free(st); #ifdef SCR_RC_AUDIT scr_h2_streams_live--; @@ -864,6 +877,7 @@ static void scr_h2_session_free(ScrH2Session *s) { scr_h2_dyntab_free(&s->dec_table); if (s->srv) scr_h2_srv_ctx_release(s->srv); if (s->sock) scr_net_sock_release(s->sock); + scr_als_ctx_release(s->als); free(s); #ifdef SCR_RC_AUDIT scr_h2_sessions_live--; @@ -1011,8 +1025,9 @@ static void scr_h2_send_headers(ScrH2Session *s, int32_t stream_id, const ScrH2B /* ── stream bookkeeping ──────────────────────────────────────────────── */ static ScrH2Stream *scr_h2_find_stream(ScrH2Session *s, int32_t id) { + if (s->last_found != NULL && s->last_found->id == id) return s->last_found; for (size_t i = 0; i < s->nstreams; i++) { - if (s->streams[i]->id == id) return s->streams[i]; + if (s->streams[i]->id == id) return s->last_found = s->streams[i]; } return NULL; } @@ -1023,6 +1038,7 @@ static ScrH2Stream *scr_h2_stream_new(ScrH2Session *sess, int32_t id) { st->rc = 1; st->id = id; st->sess = scr_http2_session_retain(sess); + if (sess->client) st->als = scr_als_capture(); st->send_window = sess->peer_init_window ? (int64_t)sess->peer_init_window : SCR_H2_DEFAULT_WINDOW; #ifdef SCR_RC_AUDIT scr_h2_streams_live++; @@ -1039,6 +1055,7 @@ static ScrH2Stream *scr_h2_stream_new(ScrH2Session *sess, int32_t id) { /* Drop the session's ref on a settled stream (its 'close' is queued or * emitted — the vector entry is what kept it discoverable by id). */ static void scr_h2_session_forget_stream(ScrH2Session *s, ScrH2Stream *st) { + if (s->last_found == st) s->last_found = NULL; for (size_t i = 0; i < s->nstreams; i++) { if (s->streams[i] == st) { memmove(s->streams + i, s->streams + i + 1, (s->nstreams - i - 1) * sizeof *s->streams); @@ -1150,6 +1167,10 @@ static bool scr_h2_proto_pending(void) { return scr_h2_emits_head < scr_h2_emits static void scr_h2_proto_sweep(void) { while (scr_h2_emits_head < scr_h2_emits_len) { ScrH2Emit e = scr_h2_emits[scr_h2_emits_head++]; + ScrAlsCtx *prev = scr_als_dispatch_enter( + e.kind == SCR_H2_EMIT_STREAM_CLOSE || e.kind == SCR_H2_EMIT_STREAM_DRAIN + ? ((ScrH2Stream *)e.h)->als + : ((ScrH2Session *)e.h)->als); switch (e.kind) { case SCR_H2_EMIT_STREAM_CLOSE: { ScrH2Stream *st = (ScrH2Stream *)e.h; @@ -1227,6 +1248,7 @@ static void scr_h2_proto_sweep(void) { break; } } + scr_als_dispatch_leave(prev); scr_h2_emit_release(&e); if (scr_exc_pending()) return; } @@ -1380,8 +1402,29 @@ static void scr_h2_fire_settings(ScrNetLs *ls, ScrH2Session *s, const ScrH2Setti } /* One complete frame. Returns false when the session tore down. */ +static bool scr_h2_on_frame_in(ScrH2Session *s, uint8_t type, uint8_t flags, int32_t stream_id, + const uint8_t *p, size_t n); + +/* A frame that drives an open stream's events (DATA, WINDOW_UPDATE, + * RST_STREAM, and a client's HEADERS, CONTINUATION and PUSH_PROMISE) runs + * in that stream's context; other frames, a server stream's opening + * HEADERS included, in the session's, which the socket callbacks below + * enter. */ static bool scr_h2_on_frame(ScrH2Session *s, uint8_t type, uint8_t flags, int32_t stream_id, const uint8_t *p, size_t n) { + bool stream_frame = type == SCR_H2_F_DATA || type == SCR_H2_F_WINDOW_UPDATE || + type == SCR_H2_F_RST_STREAM || + (s->client && (type == SCR_H2_F_HEADERS || type == SCR_H2_F_CONTINUATION || + type == SCR_H2_F_PUSH_PROMISE)); + ScrH2Stream *st = stream_frame && stream_id != 0 ? scr_h2_find_stream(s, stream_id) : NULL; + ScrAlsCtx *prev = st != NULL ? scr_als_dispatch_enter(st->als) : NULL; + bool ok = scr_h2_on_frame_in(s, type, flags, stream_id, p, n); + if (st != NULL) scr_als_dispatch_leave(prev); + return ok; +} + +static bool scr_h2_on_frame_in(ScrH2Session *s, uint8_t type, uint8_t flags, int32_t stream_id, + const uint8_t *p, size_t n) { /* An open header block admits only its CONTINUATIONs. */ if (s->hblock_active && type != SCR_H2_F_CONTINUATION) { goto proto_err; @@ -1629,7 +1672,16 @@ proto_err: return false; } +static void scr_h2_on_data_in(void *ctx, const char *buf, size_t n); + +/* The socket's callbacks run in the session's context. */ static void scr_h2_on_data(void *ctx, const char *buf, size_t n) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + scr_h2_on_data_in(ctx, buf, n); + scr_als_dispatch_leave(prev); +} + +static void scr_h2_on_data_in(void *ctx, const char *buf, size_t n) { ScrH2Session *s = ctx; if (s->destroyed) return; /* append to the reassembly buffer */ @@ -1680,7 +1732,15 @@ done: scr_http2_session_release(s); } +static void scr_h2_on_eof_in(void *ctx); + static void scr_h2_on_eof(void *ctx) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + scr_h2_on_eof_in(ctx); + scr_als_dispatch_leave(prev); +} + +static void scr_h2_on_eof_in(void *ctx) { ScrH2Session *s = ctx; if (s->destroyed) return; /* The peer is done sending. After a graceful GOAWAY exchange this is @@ -1688,7 +1748,15 @@ static void scr_h2_on_eof(void *ctx) { scr_h2_session_abort_streams(s); } +static void scr_h2_on_closed_in(void *ctx); + static void scr_h2_on_closed(void *ctx) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + scr_h2_on_closed_in(ctx); + scr_als_dispatch_leave(prev); +} + +static void scr_h2_on_closed_in(void *ctx) { ScrH2Session *s = ctx; s->destroyed = true; scr_h2_session_abort_streams(s); @@ -1700,7 +1768,16 @@ static void scr_h2_on_closed(void *ctx) { } } +static bool scr_h2_on_err_in(void *ctx, ScrStr *msg); + static bool scr_h2_on_err(void *ctx, ScrStr *msg) { + ScrAlsCtx *prev = scr_als_dispatch_enter(((ScrH2Session *)ctx)->als); + bool consumed = scr_h2_on_err_in(ctx, msg); + scr_als_dispatch_leave(prev); + return consumed; +} + +static bool scr_h2_on_err_in(void *ctx, ScrStr *msg) { ScrH2Session *s = ctx; if (s->err_ls.n == 0 && (s->srv == NULL || s->srv->session_error_ls.n == 0)) { return false; /* the socket's error story applies */ @@ -1716,6 +1793,7 @@ static ScrH2Session *scr_h2_session_new(bool client) { ScrH2Session *s = calloc(1, sizeof *s); if (!s) scr_h2_oom(); s->rc = 1; + s->als = scr_als_capture(); s->client = client; s->preface_done = client; /* clients never RECEIVE the magic */ s->next_stream_id = 1; @@ -2287,6 +2365,8 @@ static void scr_h2_upgrade_hook(void *ctx, ScrClosure *cb /*moves*/, void *fn, b * 'request' listeners (this = the net server, the http/1 shape). */ static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2Headers *h, bool end_stream, ScrNetLs *listeners) { + /* The compat pair's own events follow the stream's context. */ + ScrAlsCtx *prev = scr_als_dispatch_enter(st->als); ScrHttpReq *req = scr_http_h2_req_new(s->sock, st); for (size_t i = 0; i < h->n; i++) { ScrStr *name = h->names[i]; @@ -2301,6 +2381,7 @@ static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2 scr_http_h2_req_header(req, name, value); } ScrHttpRes *res = scr_http_h2_res_new(s->sock, st); + scr_als_dispatch_leave(prev); scr_http_res_set_req(res, req); /* res.req — the compat pair's backref */ st->compat_req = scr_http_req_retain(req); st->compat_res = scr_http_res_retain(res); @@ -2342,7 +2423,7 @@ static void scr_h2_srv_ctx_settle(void *p) { static void scr_h2_on_connection(void *ctx, ScrNetSocket *sock) { ScrH2SrvCtx *srv = (ScrH2SrvCtx *)ctx; - ScrH2Session *sess = scr_h2_session_new(false); + ScrH2Session *sess = scr_h2_session_new(false); /* in the listen() context */ sess->srv = scr_h2_srv_ctx_retain(srv); sess->sock = scr_net_sock_retain(sock); sess->next_stream_id = 2; /* server-initiated ids are even (push, unused) */ @@ -2354,6 +2435,14 @@ static void scr_h2_on_connection(void *ctx, ScrNetSocket *sock) { scr_h2_emit_push(SCR_H2_EMIT_SERVER_SESSION, scr_http2_session_retain(sess)); } +/* An accepted h2c connection: the socket's own events run with no store, + * as an accepted net socket's do (a TLS server's sockets keep the listen() + * context); the session keeps the listen() context the accept runs in. */ +static void scr_h2_on_plain_connection(void *ctx, ScrNetSocket *sock) { + scr_net_sock_set_context(sock, NULL); + scr_h2_on_connection(ctx, sock); +} + ScrNetServer *scr_http2_create_server(void) { scr_http2_install(); ScrNetServer *s = scr_net_create_server(NULL, NULL); @@ -2361,7 +2450,7 @@ ScrNetServer *scr_http2_create_server(void) { if (!ctx) scr_h2_oom(); ctx->proto = SCR_NET_PROTO_H2; ctx->rc = 1; - scr_net_server_set_native_conn(s, &scr_h2_on_connection, ctx, &scr_h2_srv_ctx_free_v); + scr_net_server_set_native_conn(s, &scr_h2_on_plain_connection, ctx, &scr_h2_srv_ctx_free_v); scr_net_server_set_http_ctx(s, ctx); scr_net_server_set_proto_settle(s, &scr_h2_srv_ctx_settle); return s; diff --git a/tests/corpus/7385-als-child-process-events.ts b/tests/corpus/7385-als-child-process-events.ts new file mode 100644 index 000000000..54fd80c3a --- /dev/null +++ b/tests/corpus/7385-als-child-process-events.ts @@ -0,0 +1,104 @@ +// AsyncLocalStorage contexts of node:child_process events. A child's +// 'spawn', 'error', 'exit' and 'close' events and its stdout and stderr +// 'data' and 'end' events run in the context of the spawn() call, +// whatever context registered the listener or killed the child. stdin's +// 'finish' runs in the context of the end() call, and its 'drain' and +// 'error' in the context of the write() call. Lines are sorted at the end: +// the stdout and stderr events of one child may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import { spawn } from "node:child_process"; + +const store = new AsyncLocalStorage(); +const lines: string[] = []; +const log = (what: string): void => { + lines.push(`${what}: ${store.getStore() ?? "none"}`); +}; +const step = (fn: (done: () => void) => void): Promise => + new Promise((resolve) => fn(resolve)); + +await step((done) => { + const child = store.run("spawn", () => spawn("/bin/sh", ["-c", "printf out; printf err >&2"])); + store.run("listen", () => { + child.on("spawn", () => log("spawn event")); + child.stdout!.on("data", () => log("stdout data")); + child.stdout!.on("end", () => log("stdout end")); + child.stderr!.on("data", () => log("stderr data")); + child.on("exit", () => log("exit")); + child.on("close", () => { + log("close"); + done(); + }); + }); +}); + +await step((done) => { + const child = store.run("spawn-cat", () => spawn("/bin/cat")); + child.stdout!.on("data", () => log("cat data")); + store.run("stdin-write", () => { + child.stdin!.write("x"); + }); + child.stdin!.on("finish", () => log("stdin finish")); + store.run("stdin-end", () => { + child.stdin!.end(); + }); + child.on("close", () => { + log("cat close"); + done(); + }); +}); + +await step((done) => { + const child = store.run("spawn-missing", () => spawn("/nonexistent/command")); + child.on("error", () => log("spawn error")); + child.on("close", () => { + log("missing close"); + done(); + }); +}); + +await step((done) => { + const child = store.run("spawn-sleep", () => spawn("/bin/sleep", ["5"])); + child.on("exit", () => log("killed exit")); + child.on("close", () => { + log("killed close"); + done(); + }); + setTimeout(() => store.run("kill", () => child.kill()), 20); +}); + +// A write that fills the pipe: 'drain' runs in the write() call's context. +await step((done) => { + const child = store.run("spawn-slow", () => + spawn("/bin/sh", ["-c", "sleep 0.2; cat > /dev/null"]), + ); + child.stdin!.on("drain", () => { + log("stdin drain"); + child.stdin!.end(); + }); + child.on("close", () => { + log("slow close"); + done(); + }); + store.run("large-write", () => { + child.stdin!.write(Buffer.alloc(1 << 22, 97)); + }); +}); + +// A write to a child that closed its stdin: 'error' runs in the write() +// call's context. +await step((done) => { + const child = store.run("spawn-closed", () => spawn("/bin/sh", ["-c", "exec 0<&-; sleep 0.1"])); + child.stdin!.on("error", () => log("stdin error")); + child.on("close", () => { + log("closed close"); + done(); + }); + setTimeout(() => { + store.run("late-write", () => { + child.stdin!.write(Buffer.alloc(1 << 20, 97)); + }); + }, 50); +}); + +lines.sort(); +for (const line of lines) console.log(line); diff --git a/tests/corpus/7386-als-child-fork-events/main.mjs b/tests/corpus/7386-als-child-fork-events/main.mjs new file mode 100644 index 000000000..18b410643 --- /dev/null +++ b/tests/corpus/7386-als-child-fork-events/main.mjs @@ -0,0 +1,25 @@ +// AsyncLocalStorage contexts of a forked child's IPC events. The child's +// 'message', 'disconnect', 'exit' and 'close' events run in the context of +// the fork() call, whatever context registered the listener; a send() +// callback runs in the context of the send() call. +import { AsyncLocalStorage } from "node:async_hooks"; +import childProcess from "node:child_process"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +const child = store.run("fork", () => + childProcess.fork(new URL("./worker.mjs", import.meta.url), []), +); +store.run("listen", () => { + child.on("message", (message) => log(`message ${JSON.stringify(message)}`)); +}); +child.on("disconnect", () => log("disconnect")); +store.run("send", () => child.send("hi", () => log("send callback"))); +setTimeout(() => store.run("send-bye", () => child.send("bye")), 50); +child.on("exit", () => log("exit")); +child.on("close", () => { + log("close"); + for (const line of lines) console.log(line); +}); diff --git a/tests/corpus/7386-als-child-fork-events/worker.mjs b/tests/corpus/7386-als-child-fork-events/worker.mjs new file mode 100644 index 000000000..79f4bfdc6 --- /dev/null +++ b/tests/corpus/7386-als-child-fork-events/worker.mjs @@ -0,0 +1,4 @@ +process.on("message", (m) => { + process.send({ echo: m }); + if (m === "bye") process.disconnect(); +}); diff --git a/tests/corpus/7387-als-http2-events.mjs b/tests/corpus/7387-als-http2-events.mjs new file mode 100644 index 000000000..4dd17916e --- /dev/null +++ b/tests/corpus/7387-als-http2-events.mjs @@ -0,0 +1,91 @@ +// AsyncLocalStorage contexts of node:http2 events. A client session's +// 'connect' and 'close' run in the context of the http2.connect() call, +// and a client stream's 'response', 'data', 'end' and 'close' in the +// context of the request() call. A server's 'session' and 'stream' events +// run in the context of its listen() call; a server stream's own 'data', +// 'end' and 'close' events run with no store, as does a server.close() +// callback that waits for the sessions. Lines are sorted at the end: +// events on the two sides may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import http2 from "node:http2"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +/** @returns {Promise} */ +function requestScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => http2.createServer()); + server.on("session", () => log("server session")); + server.on("stream", (stream) => { + log("server stream"); + stream.on("data", () => log("server stream data")); + stream.on("end", () => { + log("server stream end"); + store.run("respond", () => { + stream.respond({ ":status": 200 }); + stream.end("ok"); + }); + }); + }); + store.run("listen", () => + server.listen(0, () => { + log("listening"); + const client = store.run("connect", () => + http2.connect(`http://localhost:${server.address().port}`), + ); + client.on("connect", () => log("client connect")); + client.on("close", () => log("client session close")); + const req = store.run("request", () => client.request({ ":method": "POST", ":path": "/" })); + req.on("response", () => log("client response")); + req.on("data", () => log("client data")); + req.on("end", () => { + log("client end"); + store.run("close", () => { + client.close(); + server.close(() => { + log("server close callback"); + resolve(); + }); + }); + }); + store.run("write", () => { + req.write("x"); + req.end(); + }); + }), + ); + }); +} + +/** @returns {Promise} */ +function resetScenario() { + return new Promise((resolve) => { + const server = store.run("reset-create", () => http2.createServer()); + server.on("stream", (stream) => { + log("reset server stream"); + stream.on("close", () => log("reset server stream close")); + store.run("reset", () => stream.close(http2.constants.NGHTTP2_CANCEL)); + }); + store.run("reset-listen", () => + server.listen(0, () => { + const client = store.run("reset-connect", () => + http2.connect(`http://localhost:${server.address().port}`), + ); + const req = store.run("reset-request", () => client.request({ ":path": "/" })); + req.on("close", () => { + log("reset client close"); + client.close(); + server.close(() => resolve()); + }); + req.end(); + }), + ); + }); +} + +await requestScenario(); +await resetScenario(); +lines.sort(); +for (const line of lines) console.log(line); diff --git a/tests/corpus/7388-als-http2-compat-events.mjs b/tests/corpus/7388-als-http2-compat-events.mjs new file mode 100644 index 000000000..da44dc378 --- /dev/null +++ b/tests/corpus/7388-als-http2-compat-events.mjs @@ -0,0 +1,49 @@ +// AsyncLocalStorage contexts of the node:http2 compatibility API. The +// createServer() request handler runs in the context of the server's +// listen() call. The request's 'end' and the response's 'finish' and +// 'close' follow the server stream, which runs with no store, whatever +// context calls res.end(). A client stream's 'response', 'data' and +// 'close' run in the context of the request() call, and the client +// session's 'close' in the context of the http2.connect() call. Lines are +// sorted at the end: events on the two sides may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import http2 from "node:http2"; + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +const server = store.run("create", () => + http2.createServer((req, res) => { + log("request handler"); + req.resume(); + req.on("end", () => { + log("request end"); + res.on("finish", () => log("response finish")); + res.on("close", () => log("response close")); + store.run("respond", () => res.end("done")); + }); + }), +); +store.run("listen", () => + server.listen(0, () => { + const client = store.run("connect", () => + http2.connect(`http://localhost:${server.address().port}`), + ); + client.on("close", () => { + log("client session close"); + server.close(() => { + lines.sort(); + for (const line of lines) console.log(line); + }); + }); + const req = store.run("request", () => client.request({ ":method": "POST", ":path": "/x" })); + req.on("response", () => log("client response")); + req.on("data", () => log("client data")); + req.on("close", () => { + log("client stream close"); + store.run("close", () => client.close()); + }); + store.run("write", () => req.end("body")); + }), +); diff --git a/tests/corpus/7389-als-http2-secure-events.mjs b/tests/corpus/7389-als-http2-secure-events.mjs new file mode 100644 index 000000000..0772b5267 --- /dev/null +++ b/tests/corpus/7389-als-http2-secure-events.mjs @@ -0,0 +1,94 @@ +// AsyncLocalStorage contexts of node:http2 events over TLS +// (http2.createSecureServer and an https: authority). They follow corpus +// 7387's rules, with one difference that comes from the transport: a TLS +// server's connections run in the context of its listen() call, so a +// server.close() callback that waits for the sessions runs there too. +// Lines are sorted at the end: events on the two sides may interleave. +import { AsyncLocalStorage } from "node:async_hooks"; +import fs from "node:fs"; +import http2 from "node:http2"; + +const key = fs.readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const cert = fs.readFileSync("tests/fixtures/server/certs/localhost.pem"); +const ca = fs.readFileSync("tests/fixtures/server/certs/ca.pem", "utf8"); + +const store = new AsyncLocalStorage(); +const lines = []; +const log = (what) => lines.push(`${what}: ${store.getStore() ?? "none"}`); + +/** @returns {Promise} */ +function requestScenario() { + return new Promise((resolve) => { + const server = store.run("create", () => http2.createSecureServer({ key, cert })); + server.on("session", () => log("server session")); + server.on("stream", (stream) => { + log("server stream"); + stream.on("data", () => log("server stream data")); + stream.on("end", () => { + log("server stream end"); + store.run("respond", () => { + stream.respond({ ":status": 200 }); + stream.end("ok"); + }); + }); + }); + store.run("listen", () => + server.listen(0, () => { + log("listening"); + const client = store.run("connect", () => + http2.connect(`https://localhost:${server.address().port}`, { ca }), + ); + client.on("connect", () => log("client connect")); + client.on("close", () => log("client session close")); + const req = store.run("request", () => client.request({ ":method": "POST", ":path": "/" })); + req.on("response", () => log("client response")); + req.on("data", () => log("client data")); + req.on("end", () => { + log("client end"); + store.run("close", () => { + client.close(); + server.close(() => { + log("server close callback"); + resolve(); + }); + }); + }); + store.run("write", () => { + req.write("x"); + req.end(); + }); + }), + ); + }); +} + +/** @returns {Promise} */ +function resetScenario() { + return new Promise((resolve) => { + const server = store.run("reset-create", () => http2.createSecureServer({ key, cert })); + server.on("stream", (stream) => { + log("reset server stream"); + stream.on("close", () => log("reset server stream close")); + store.run("reset", () => stream.close(http2.constants.NGHTTP2_CANCEL)); + }); + store.run("reset-listen", () => + server.listen(0, () => { + const client = store.run("reset-connect", () => + http2.connect(`https://localhost:${server.address().port}`, { ca }), + ); + const req = store.run("reset-request", () => client.request({ ":path": "/" })); + req.on("close", () => { + log("reset client close"); + client.close(); + server.close(() => resolve()); + }); + req.end(); + }), + ); + }); +} + +await requestScenario(); +await resetScenario(); +lines.sort(); +for (const line of lines) console.log(line); diff --git a/tests/harness/corpus-costs/7385-als-child-process-events.ts.json b/tests/harness/corpus-costs/7385-als-child-process-events.ts.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7385-als-child-process-events.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } diff --git a/tests/harness/corpus-costs/7386-als-child-fork-events/main.mjs.json b/tests/harness/corpus-costs/7386-als-child-fork-events/main.mjs.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7386-als-child-fork-events/main.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } diff --git a/tests/harness/corpus-costs/7387-als-http2-events.mjs.json b/tests/harness/corpus-costs/7387-als-http2-events.mjs.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7387-als-http2-events.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } diff --git a/tests/harness/corpus-costs/7388-als-http2-compat-events.mjs.json b/tests/harness/corpus-costs/7388-als-http2-compat-events.mjs.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7388-als-http2-compat-events.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } diff --git a/tests/harness/corpus-costs/7389-als-http2-secure-events.mjs.json b/tests/harness/corpus-costs/7389-als-http2-secure-events.mjs.json new file mode 100644 index 000000000..a806a3ed9 --- /dev/null +++ b/tests/harness/corpus-costs/7389-als-http2-secure-events.mjs.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 9000 } From 945a519036530065664e07e85754ff3a7dd5cc6f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:49:51 -0500 Subject: [PATCH 069/409] Match Node for typed pipeline callbacks and rl.cursor on non-terminal interfaces --- .../builtin-apis/streams-and-readline.mdx | 4 +- .../frontend/lowering/builtins/readline.ts | 51 +++++++- .../lowering/expressions/narrowing.ts | 3 + .../src/frontend/lowering/lower-stream.ts | 24 +++- .../compiler/src/frontend/lowering/lowerer.ts | 3 +- .../7500-pipeline-callback-undefined.ts.json | 6 + .../7501-pipeline-callback-undefined.mjs.json | 6 + .../7502-readline-cursor-undefined.ts.json | 6 + .../7500-pipeline-callback-undefined.ts | 120 ++++++++++++++++++ .../7501-pipeline-callback-undefined.mjs | 22 ++++ .../corpus/7502-readline-cursor-undefined.ts | 28 ++++ .../7500-pipeline-callback-undefined.ts.json | 1 + .../7501-pipeline-callback-undefined.mjs.json | 1 + .../7502-readline-cursor-undefined.ts.json | 1 + 14 files changed, 268 insertions(+), 8 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json create mode 100644 tests/corpus/7500-pipeline-callback-undefined.ts create mode 100644 tests/corpus/7501-pipeline-callback-undefined.mjs create mode 100644 tests/corpus/7502-readline-cursor-undefined.ts create mode 100644 tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json create mode 100644 tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json create mode 100644 tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 166fc3402..45a784742 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -18,7 +18,7 @@ Stages may take the `{ signal }` argument. With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`. `@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`). - In typed code, the callback of a pipeline with an async function destination receives `null` instead of `undefined` when there is no error. + In typed code, a pipeline callback with an async function or async generator destination receives `null` for no error when its error parameter cannot hold `undefined` (@types/node declares it `NodeJS.ErrnoException | null`), and `undefined` when it can, as in Node. `Readable.from` accepts `objectMode` and `highWaterMark` options. The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options. Their callbacks may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream. @@ -43,7 +43,7 @@ The output's `'resize'` event does not redraw the line. The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end. In JavaScript, a `'history'` listener receives a copy of the history; its changes are written back when the listener returns, but the copy is not `rl.history` itself. - `rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node, except that `rl.cursor` of an interface that is not a terminal throws instead of answering `undefined`. + `rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node; `rl.cursor` is `undefined` on an interface that is not a terminal. `rl[Symbol.dispose]()` closes the interface, and `instanceof` with `readline.Interface` or `readlinePromises.Interface` answers whether the interface came from that module, as in Node. In JavaScript, the input and output can be any expression that evaluates to a stream, such as a function's result or an object's member; a value that is not a stream is unsupported. In JavaScript, `question()` and `setPrompt()` accept a value that is not a string: when such a prompt is written, an interface that is not a terminal throws `ERR_INVALID_ARG_TYPE` as Node does (the `readline/promises` question rejects with it) and the question stays pending, and a terminal interface writes `String(value)`. diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index 846b9a926..aca209017 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -6,9 +6,11 @@ import { DYN, F64, type IrExpr, + type IrType, type IrStmt, type SrcLoc, STRING, + UNDEFINED_T, VOID, arrayOf, BYTES_U8, @@ -54,6 +56,21 @@ export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean }); } +/** `rl.cursor`: @types/node declares a number, but an interface that is + * not a terminal answers undefined, so the read keeps its + * `number | undefined` union. */ +export function isOptionalReadlineCursorRead( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): boolean { + return ( + expr.name.text === "cursor" && + !expr.questionDotToken && + isReadlineTyped(lowerer, expr.expression) && + lowerer.isStdlibMember(expr) + ); +} + /** The runtime's listener event numbers (scr_readline.c). */ const RL_EVENTS: Record = { line: 0, @@ -1341,7 +1358,7 @@ export function lowerReadlineCreateCall( /** Property reads on an Interface: `history` (the history option itself, * or the interface's own empty array), `historySize`, `historyIndex`, * `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `terminal` and - * `cursor` (a non-terminal interface's undefined cursor throws SC2020). + * `cursor` (undefined on a non-terminal interface). * Null for other receivers. */ export function lowerReadlineProperty( lowerer: Lowerer, @@ -1373,10 +1390,38 @@ export function lowerReadlineProperty( loc, }; } - const reads: Record = { + if (name === "cursor") { + // `number | undefined`: a non-terminal interface has no cursor + const recv = lowerer.lowerExprExpecting(access.expression, F64); + const type: IrType = { kind: "union", unionId: lowerer.unions.intern([F64, UNDEFINED_T]) }; + const local = recv.kind === "varRef" ? null : lowerer.declareHiddenLocal("%rlCursor", F64); + const id: IrExpr = local ? { kind: "varRef", localId: local.id, type: F64, loc } : recv; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "libCall", fn: "rl.isTerminal", args: [id], type: BOOL, loc }, + then: lowerer.coerceToExpected( + { kind: "libCall", fn: "rl.cursor", args: [id], type: F64, loc }, + type, + ), + else_: lowerer.coerceToExpected( + { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, + type, + ), + type, + loc, + }; + if (!local) return result; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: recv, loc }], + result, + type, + loc, + }; + } + const reads: Record = { line: ["rl.line", STRING], terminal: ["rl.isTerminal", BOOL], - cursor: ["rl.cursor", F64], }; const read = reads[name]; if (read !== undefined) { diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index fa8ad686f..01cab9901 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -33,6 +33,7 @@ import { isOptionalProcessVersionsRead, } from "../builtins/process.js"; import { requestGeneralStreamSurface } from "../general-streams.js"; +import { isOptionalReadlineCursorRead } from "../builtins/readline.js"; import { literalValues } from "../../literal-values.js"; import { literalUnionArm } from "../../union-discriminants.js"; import { checkedArmExtraction } from "../coercions/unions.js"; @@ -617,6 +618,8 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node)) return expr; if (isOptionalProcessVersionsRead(lowerer, node)) return expr; + if (ts.isPropertyAccessExpression(node) && isOptionalReadlineCursorRead(lowerer, node)) + return expr; // Represented fields and index values can include undefined even when // checker inference or lib.d.ts declares strings (RegExp captures). diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index cf9f19b9d..687418350 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -2906,6 +2906,22 @@ class StreamArgTypeThrow extends Error { } } +/** Whether a checked-dynamic pipeline callback can take undefined as its + * error: a callback that is already checked-dynamic, one without + * parameters, and one whose first parameter (or rest element) type holds + * undefined. */ +function callbackErrorAdmitsUndefined(lowerer: Lowerer, cb: IrExpr): boolean { + if (cb.kind !== "dynFrom" || cb.value.type.kind !== "func") return true; + const fnT = cb.value.type; + const first = fnT.params[0]; + if (first === undefined) return true; + const p0 = + fnT.rest === true && fnT.params.length === 1 && first.kind === "array" ? first.elem : first; + if (p0.kind === "dyn" || p0.kind === "jsval" || p0.kind === "undefinedT") return true; + if (p0.kind !== "union") return false; + return lowerer.unions.get(p0.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false; +} + /** The finished/pipeline completion callback: an inline function lowers * through the option-callback machinery (leading `this`, an `Error | * null` prefix — Node binds the stream and passes the error); any other @@ -3367,12 +3383,16 @@ export function lowerStreamModuleCall( // an async generator) reaches the callback through the // checked-dynamic path. const cb = lowerer.lowerExprExpecting(args[args.length - 1]!, DYN); - // SCR_PIPE_TYPED_CB: typed code's callback takes null for no error + // SCR_PIPE_TYPED_CB: a typed callback whose error parameter cannot + // hold undefined takes null for no error; others take undefined. const stagedArgs = [...staged.args]; const flagsArg = stagedArgs[3]!; if (flagsArg.kind !== "numLit") throw new InternalCompilerError("pipeline flags not a literal"); - if (!isJsSourceFile(ts.getSourceFileOfNode(call))) + if ( + !isJsSourceFile(ts.getSourceFileOfNode(call)) && + !callbackErrorAdmitsUndefined(lowerer, cb) + ) stagedArgs[3] = numLit(flagsArg.value | 128, loc); return staged.wrap({ kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a765c91ee..a17522333 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -496,7 +496,7 @@ import { lowerBuiltinExtraProperty } from "./builtins/properties.js"; import { registerPromisifiedBuiltinDecl } from "./builtins/callable-aliases.js"; import { lowerExecFileAsyncCall } from "./builtins/child-process-async.js"; import { lowerStringDecoderMethodCall, lowerTextCodecCall } from "./builtins/text-codecs.js"; -import { lowerReadlineMethodCall } from "./builtins/readline.js"; +import { isOptionalReadlineCursorRead, lowerReadlineMethodCall } from "./builtins/readline.js"; import { lowerDiagnosticsChannelMethodCall, lowerDiagnosticsChannelProperty, @@ -5547,6 +5547,7 @@ export class Lowerer { if (isIndexedRead(e)) return true; if (ts.isPropertyAccessExpression(e) && isRecordIndexPropertyRead(e)) return true; if (ts.isPropertyAccessExpression(e) && isOptionalProcessStreamProperty(this, e)) return true; + if (ts.isPropertyAccessExpression(e) && isOptionalReadlineCursorRead(this, e)) return true; if (isOptionalProcessVersionsRead(this, e)) return true; if (ts.isPropertyAccessExpression(e) && isSettledIteratorValueRead(this, e)) return true; if (ts.isPropertyAccessExpression(e) && isOsConstantsSymbol(symbolOf(e.expression), this)) diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json new file mode 100644 index 000000000..fe65b63f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7500-pipeline-callback-undefined.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7500-pipeline-callback-undefined.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json new file mode 100644 index 000000000..8d52980fe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7501-pipeline-callback-undefined.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7501-pipeline-callback-undefined.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json new file mode 100644 index 000000000..6fd23042e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7502-readline-cursor-undefined.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7502-readline-cursor-undefined.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7500-pipeline-callback-undefined.ts b/tests/corpus/7500-pipeline-callback-undefined.ts new file mode 100644 index 000000000..e1503ff6b --- /dev/null +++ b/tests/corpus/7500-pipeline-callback-undefined.ts @@ -0,0 +1,120 @@ +// pipeline() with an async function or async generator destination: a +// typed callback whose error parameter admits undefined receives +// undefined when the pipeline succeeds, as in Node, and the error when it +// fails. +import { pipeline, Readable } from "node:stream"; + +function source(): Readable { + return Readable.from(["a", "b", "c"]); +} + +async function join(src: AsyncIterable): Promise { + let s = ""; + for await (const c of src) s += c; + return s; +} + +function optional(): Promise { + return new Promise((resolve) => { + pipeline(source(), join, (err?: Error | null, value?: string) => { + console.log("optional", err, err === undefined, value); + resolve(); + }); + }); +} + +function declared(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function (src: AsyncIterable) { + let n = 0; + for await (const c of src) n += c.length; + return n; + }, + (err: Error | null | undefined, value?: number) => { + console.log("declared", err, typeof err, value); + resolve(); + }, + ); + }); +} + +function unknownParam(): Promise { + return new Promise((resolve) => { + pipeline(source(), join, (err: unknown, value: unknown) => { + console.log("unknown", err, value); + resolve(); + }); + }); +} + +function rest(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function* (src: AsyncIterable) { + for await (const c of src) yield c.toUpperCase(); + }, + async function (src: AsyncIterable) { + for await (const c of src) void c; + }, + (...args: unknown[]) => { + console.log("rest", args.length, args); + resolve(); + }, + ); + }); +} + +function generatorDestination(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function* (src: AsyncIterable) { + for await (const c of src) yield c; + }, + (err: Error | null | undefined, value: unknown) => { + console.log("generator", err, value); + resolve(); + }, + ); + }); +} + +function named(): Promise { + return new Promise((resolve) => { + function done(err: Error | null | undefined, value: string | undefined): void { + console.log("named", err, value); + resolve(); + } + pipeline(source(), join, done); + }); +} + +function failing(): Promise { + return new Promise((resolve) => { + pipeline( + source(), + async function (src: AsyncIterable) { + for await (const c of src) if (c === "b") throw new Error("boom at " + c); + return "never"; + }, + (err?: Error | null, value?: string) => { + console.log("failing", err?.message, value); + resolve(); + }, + ); + }); +} + +async function main(): Promise { + await optional(); + await declared(); + await unknownParam(); + await rest(); + await generatorDestination(); + await named(); + await failing(); +} +void main(); diff --git a/tests/corpus/7501-pipeline-callback-undefined.mjs b/tests/corpus/7501-pipeline-callback-undefined.mjs new file mode 100644 index 000000000..c51738e77 --- /dev/null +++ b/tests/corpus/7501-pipeline-callback-undefined.mjs @@ -0,0 +1,22 @@ +// pipeline() with an async function destination in JavaScript: the +// callback receives undefined and the destination's value on success. +import { pipeline, Readable } from "node:stream"; + +pipeline( + Readable.from(["x", "y"]), + async function (src) { + const out = []; + for await (const c of src) out.push(c); + return out.join("-"); + }, + (err, value) => { + console.log("callback", err, typeof err, value); + pipeline( + Readable.from(["z"]), + async function (src) { + for await (const c of src) void c; + }, + (...args) => console.log("no value", args), + ); + }, +); diff --git a/tests/corpus/7502-readline-cursor-undefined.ts b/tests/corpus/7502-readline-cursor-undefined.ts new file mode 100644 index 000000000..abac985cb --- /dev/null +++ b/tests/corpus/7502-readline-cursor-undefined.ts @@ -0,0 +1,28 @@ +// rl.cursor answers undefined on an interface that is not a terminal and +// the cursor position on a terminal interface. +import * as readline from "node:readline"; +import { PassThrough, Writable } from "node:stream"; + +const input = new PassThrough(); +const rl = readline.createInterface({ input }); +const c = rl.cursor; +console.log("cursor", c, typeof rl.cursor, rl.cursor === undefined); +console.log("arithmetic", rl.cursor + 1, Number.isNaN(rl.cursor * 2)); +console.log("fallback", rl.cursor ?? -1, `${rl.cursor}`); + +const interfaces = [rl]; +function first(): readline.Interface { + return interfaces[0]!; +} +console.log("call receiver", first().cursor); +rl.close(); + +const sink = new Writable({ + write(_chunk: Buffer, _enc, cb) { + cb(); + }, +}); +const terminal = readline.createInterface({ input: new PassThrough(), output: sink, terminal: true }); +terminal.write("abc"); +console.log("terminal", terminal.cursor, typeof terminal.cursor, terminal.cursor + 1); +terminal.close(); diff --git a/tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json b/tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json new file mode 100644 index 000000000..122c66b27 --- /dev/null +++ b/tests/harness/corpus-costs/7500-pipeline-callback-undefined.ts.json @@ -0,0 +1 @@ +{ "plain": 3783, "san": 4994 } diff --git a/tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json b/tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json new file mode 100644 index 000000000..d9c00a7de --- /dev/null +++ b/tests/harness/corpus-costs/7501-pipeline-callback-undefined.mjs.json @@ -0,0 +1 @@ +{ "plain": 2457, "san": 3243 } diff --git a/tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json b/tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json new file mode 100644 index 000000000..7a36db8f3 --- /dev/null +++ b/tests/harness/corpus-costs/7502-readline-cursor-undefined.ts.json @@ -0,0 +1 @@ +{ "plain": 2592, "san": 3421 } From fb3806ce42a7717c84e4ce4e285bf62caf86d35a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 03:54:26 -0500 Subject: [PATCH 070/409] Search TypedArrays, read stdin through 'readable', and assign constructor-function globals - TypedArray indexOf, lastIndexOf and includes on every element kind, typed and checked-dynamic: strict equality (SameValueZero for includes) and fromIndex as ToIntegerOrInfinity. A Buffer reached through a Uint8Array value keeps Buffer's search. - process.stdin 'readable' with read() and read(size), including sizes above the read-ahead mark and input a once('readable') listener leaves for a later 'data' listener. for await over process.stdin yields Buffers. - Module bindings holding `new F()` of a constructor function can be read and reassigned from other functions. --- .../compiler/src/backend/llvm/expr-bytes.ts | 16 +++ .../src/frontend/lowering/containers/bytes.ts | 49 +++++++- .../src/frontend/lowering/lower-modules.ts | 1 - .../frontend/lowering/object-factory-new.ts | 31 +++-- packages/compiler/src/ir/ir.ts | 7 ++ packages/compiler/src/ir/validate.ts | 4 + .../corpus/7680-typed-array-search.ts.json | 6 + .../7681-typed-array-search-dynamic.mjs.json | 6 + .../7682-constructor-function-globals.js.json | 6 + packages/runtime/src/scr_bytes.c | 44 +++++++ packages/runtime/src/scr_bytes.h | 2 + packages/runtime/src/scr_dyn_invoke.c | 15 +++ packages/runtime/src/scr_events.c | 115 ++++++++++++++++-- tests/corpus/7680-typed-array-search.ts | 63 ++++++++++ .../7681-typed-array-search-dynamic.mjs | 28 +++++ .../7682-constructor-function-globals.js | 55 +++++++++ .../event-loop/stdin-for-await-buffers.mjs | 10 ++ .../event-loop/stdio-readable-large-size.mjs | 9 ++ .../event-loop/stdio-readable-once-rest.mjs | 12 ++ .../event-loop/stdio-readable-size.mjs | 18 +++ tests/fixtures/event-loop/stdio-readable.mjs | 19 +++ .../7680-typed-array-search.ts.json | 1 + .../7681-typed-array-search-dynamic.mjs.json | 1 + .../7682-constructor-function-globals.js.json | 1 + tests/harness/event-loop-cases.ts | 49 ++++++++ 25 files changed, 543 insertions(+), 25 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json create mode 100644 tests/corpus/7680-typed-array-search.ts create mode 100644 tests/corpus/7681-typed-array-search-dynamic.mjs create mode 100644 tests/corpus/7682-constructor-function-globals.js create mode 100644 tests/fixtures/event-loop/stdin-for-await-buffers.mjs create mode 100644 tests/fixtures/event-loop/stdio-readable-large-size.mjs create mode 100644 tests/fixtures/event-loop/stdio-readable-once-rest.mjs create mode 100644 tests/fixtures/event-loop/stdio-readable-size.mjs create mode 100644 tests/fixtures/event-loop/stdio-readable.mjs create mode 100644 tests/harness/corpus-costs/7680-typed-array-search.ts.json create mode 100644 tests/harness/corpus-costs/7681-typed-array-search-dynamic.mjs.json create mode 100644 tests/harness/corpus-costs/7682-constructor-function-globals.js.json diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index 41ddf2b9d..2a368f0e2 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -936,6 +936,22 @@ export function emitBytesIntrinsic( B.line(`${t} = fcmp one double ${idx.name}, ${f64Lit(-1)}`); return { name: t, type: e.type }; } + case "indexOfElem": + case "lastIndexOfElem": + case "includesElem": { + const includes = method === "includesElem"; + const idx = call( + "scr_bytes_ta_index_of", + "double (ptr, double, double, i1 zeroext, i1 zeroext, i1 zeroext)", + `ptr ${r.name}, double ${args[0]!.name}, double ${args[1]?.name ?? f64Lit(0)}, i1 ${args.length > 1}, i1 ${method !== "lastIndexOfElem"}, i1 ${includes}`, + false, + false, + ); + if (!includes) return idx; + const t = B.tmp(); + B.line(`${t} = fcmp one double ${idx.name}, ${f64Lit(-1)}`); + return { name: t, type: e.type }; + } case "fillElem": // Per-element TypedArray fill (non-u8): slice-style index // defaults, never throws; the receiver comes back +1. diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 995cb177a..9cf68da9f 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -907,9 +907,10 @@ function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { * (shared view), toReversed, copyWithin, set(src, * offset?), and the u8-only Buffer surface: toString(enc?) plus the * whole numeric read/write family (fixed widths BE/LE and the - * variable-width read/writeUIntLE quartet — BUF_NUM_METHODS). Everything - * else the lib declares (fill, indexOf, reverse, ...) falls through to - * the SC2020 member fence. Null when this isn't a bytes method call. */ + * variable-width read/writeUIntLE quartet — BUF_NUM_METHODS), and + * TypedArray fill, indexOf, lastIndexOf and includes. Everything else the + * lib declares (reverse, sort, ...) falls through to the SC2020 member + * fence. Null when this isn't a bytes method call. */ export function lowerBytesMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -1101,6 +1102,43 @@ export function lowerBytesMethodCall( loc, }; } + if ( + (name === "indexOf" || name === "lastIndexOf" || name === "includes") && + !(() => { + const symbol = lowerer.checker.getSymbolAtLocation(access.name); + return ( + symbol !== undefined && + symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration.parent) && + declaration.parent.name.text === "Buffer", + ) + ); + })() + ) { + // TypedArray search: strict equality (includes: SameValueZero) with a + // relative fromIndex. Buffer's same-named methods keep their byte, + // string and Buffer needles. + if (nArgs < 1 || nArgs > 2 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.${name} with ${nArgs} arguments on typed arrays`, call); + } + const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); + const args = call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, F64)); + const method = + name === "indexOf" + ? "indexOfElem" + : name === "lastIndexOf" + ? "lastIndexOfElem" + : "includesElem"; + return { + kind: "bytesIntrinsic", + method, + receiver, + args, + type: name === "includes" ? BOOL : F64, + loc, + }; + } if (name === "set") { if (nArgs < 1 || nArgs > 2) { lowerer.noLowering(`.set with ${nArgs} arguments on typed arrays`, call); @@ -1210,9 +1248,8 @@ export function lowerBytesMethodCall( // The Buffer-declared comparison/search/mutation surface. All of // these resolve against the Buffer interface (the checker keeps most // off plain typed arrays; where the lib DOES declare a same-named - // TypedArray member — indexOf, includes, fill — the semantics differ, - // so only Buffer-declared resolutions lower and u8 receivers gate the - // rest). + // TypedArray member — indexOf, includes, fill — the semantics differ: + // those resolutions lowered above, and u8 receivers gate the rest). const declOnBuffer = (() => { const nameSym = lowerer.checker.getSymbolAtLocation(access.name); return ( diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 5238333ad..af2ed4a2c 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -2361,7 +2361,6 @@ export function collectGlobals( // References from separately-declared functions cascade to // their own per-site runtime fences. const factoryType = - !isLet && isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts index bcb675692..c672f82e2 100644 --- a/packages/compiler/src/frontend/lowering/object-factory-new.ts +++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts @@ -6,10 +6,14 @@ import { bindingNeverReassigned } from "./binding-analysis.js"; import { implicitDefaultInstance } from "./generic-functions.js"; import { type FnSig } from "./call-signatures.js"; -/** An ordinary constructor's explicit object return replaces its receiver. - * Without observing this or new.target, it can use the function's call ABI. */ -function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { - if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return false; +/** The JavaScript function declaration (or never-reassigned function + * expression binding) `new` calls, when it is an ordinary constructor: not + * a generator or async function. */ +function jsConstructorFunctionOf( + lowerer: Lowerer, + expr: ts.NewExpression, +): ts.FunctionDeclaration | ts.FunctionExpression | null { + if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return null; // This probe also sees ordinary classes during global collection. Leave // their deferred diagnostics for the class declaration's lowering. const wasCollecting = lowerer.collecting; @@ -20,7 +24,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { } finally { lowerer.collecting = wasCollecting; } - if (!symbol) return false; + if (!symbol) return null; const owner = symbol.declarations.find( (node) => (ts.isFunctionDeclaration(node) && node.body !== undefined) || @@ -45,7 +49,15 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { declaration.asteriskToken || declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ) - return false; + return null; + return declaration; +} + +/** An ordinary constructor's explicit object return replaces its receiver. + * Without observing this or new.target, it can use the function's call ABI. */ +function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { + const declaration = jsConstructorFunctionOf(lowerer, expr); + if (!declaration?.body) return false; const objectReturn = (expression: ts.Expression): boolean => ts.isObjectLiteralExpression(expression) || ts.isArrayLiteralExpression(expression) || @@ -81,8 +93,13 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { /** Collection must register the shared result before reaching factory * bodies: those bodies can reference later globals in the same module. */ +/** The storage of a JavaScript module binding initialized with `new` of an + * ordinary function: a factory's result and a constructor function's + * receiver (`const head = new Segment()`) are both checked-dynamic + * values, which functions declared elsewhere in the module read through + * the global. */ export function objectFactoryGlobalType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null { - return isObjectFactoryNew(lowerer, expr) ? DYN : null; + return jsConstructorFunctionOf(lowerer, expr) !== null ? DYN : null; } export function objectFactorySignature(lowerer: Lowerer, expr: ts.NewExpression): FnSig | null { diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 6d0760b81..a26cb638f 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -2484,6 +2484,13 @@ export type IrBytesIntrinsicMethod = * (same observable number-fill result; Buffer's throwing offset * validation). */ | "fillElem" + /** TypedArray.prototype.indexOf / lastIndexOf / includes on any + * receiver: [f64 searchElement, f64 fromIndex?] → f64 index (-1 when + * absent) or bool; strict equality (includes: SameValueZero), a present + * fromIndex counts from the end when negative, never throws. */ + | "indexOfElem" + | "lastIndexOfElem" + | "includesElem" | "copy" | "swap16" | "swap32" diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 02825f0de..77909fb52 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -2196,6 +2196,10 @@ function validateFunction( fillNum: { argTypes: [F64, F64, F64], minArgs: 1, result: BYTES_U8 }, // Per-element TypedArray fill (any elem — the non-u8 fill path). fillElem: { argTypes: [F64, F64, F64], minArgs: 1, result: bytesOf(recv.elem) }, + // TypedArray search (any elem). + indexOfElem: { argTypes: [F64, F64], minArgs: 1, result: F64 }, + lastIndexOfElem: { argTypes: [F64, F64], minArgs: 1, result: F64 }, + includesElem: { argTypes: [F64, F64], minArgs: 1, result: BOOL }, fillStr: { argTypes: [STRING, STRING, F64, F64], minArgs: 2, result: BYTES_U8 }, copy: { argTypes: [BYTES_U8, F64, F64, F64], minArgs: 1, result: F64 }, swap16: { argTypes: [], minArgs: 0, result: BYTES_U8 }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json new file mode 100644 index 000000000..d93d9afdc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7680-typed-array-search.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json new file mode 100644 index 000000000..ab91940c0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7681-typed-array-search-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json new file mode 100644 index 000000000..26d05338c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7682-constructor-function-globals.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index 7bcfd0cfb..26f1a57a5 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -3300,6 +3300,50 @@ double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd) return scr_bytes_index_of(b, &needle, off, 1, fwd); } +/* TypedArray.prototype.indexOf and lastIndexOf (strict equality, so NaN + * never matches) and includes (`same_value_zero`: NaN matches NaN). `from` + * is the fromIndex argument when `has_from` (ToIntegerOrInfinity, negative + * counts from the end), else the default: 0, or len - 1 searching + * backwards. Returns the element index, or -1. Never throws. */ +double scr_bytes_ta_index_of(const ScrBytes *b, double v, double from, bool has_from, bool forward, + bool same_value_zero) { + // A Buffer viewed as a Uint8Array keeps Buffer's own search methods, whose + // byte needle and byteOffset rules differ (an absent offset is NaN there). + if (scr_bytes_is_buffer(b)) return scr_bytes_index_of_num(b, v, has_from ? from : NAN, forward); + size_t len = b->len; + if (len == 0) return -1; + double k = has_from ? (from != from ? 0 : trunc(from)) : forward ? 0 : (double)len - 1; + bool nan = v != v; + if (nan && !same_value_zero) return -1; + bool bytes = (b->elem == SCR_BYTES_U8 || b->elem == SCR_BYTES_U8C) && !b->shared; + if (forward) { + if (k >= (double)len) return -1; + if (k < 0) k = k + (double)len < 0 ? 0 : k + (double)len; + size_t start = (size_t)k; + if (bytes) { + if (!(v >= 0 && v <= 255 && v == trunc(v))) return -1; + const uint8_t *hit = memchr(b->data + start, (int)v, len - start); + return hit ? (double)(hit - b->data) : -1; + } + for (size_t i = start; i < len; i++) { + double e = scr_bytes_load(b, i); + if (e == v || (nan && e != e)) return (double)i; + } + return -1; + } + if (k < 0) { + k += (double)len; + if (k < 0) return -1; + } else if (k > (double)len - 1) { + k = (double)len - 1; + } + for (size_t i = (size_t)k + 1; i-- > 0;) { + double e = scr_bytes_load(b, i); + if (e == v || (nan && e != e)) return (double)i; + } + return -1; +} + /* fill's shared core over a normalized byte pattern. nargs counts the * present offset/end args. An EMPTY pattern zero-fills when zero_ok * (Node's '' string) and is the constant TypeError otherwise (an empty diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 9b382dda5..13b31b183 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -499,6 +499,8 @@ ScrEmitter *scr_emitter_set_max_chk(ScrEmitter *em, const ScrDyn *n); void scr_emitter_set_default_max_chk(const ScrDyn *n, const ScrStr *name); double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, double align, bool fwd); double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd); +double scr_bytes_ta_index_of(const ScrBytes *b, double v, double from, bool has_from, bool forward, + bool same_value_zero); ScrBytes *scr_bytes_fill(ScrBytes *b, const ScrBytes *pattern, double nargs, double offset, double end); ScrBytes *scr_bytes_fill_num(ScrBytes *b, double v, double nargs, double offset, double end); ScrBytes *scr_bytes_fill_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc, diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 4039fda94..d57ca24ac 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1704,6 +1704,21 @@ static ScrDyn *scr_dyn_invoke_impl( scr_str_release(joined); return out; } + if (!recv->buffer && + ((mid == DYN_M_indexOf) || (mid == DYN_M_lastIndexOf) || (mid == DYN_M_includes))) { + /* TypedArray search: strict equality (includes: SameValueZero), so a + * needle that is not a number matches nothing; fromIndex counts as + * given whenever it is passed, undefined included. */ + double from = 0; + if (argc > 1) { + if (!scr_dyn_number_coerce_js(args[1], &from)) return NULL; + } + bool includes = mid == DYN_M_includes; + double found = argc > 0 && args[0]->kind == SCR_DYN_NUM + ? scr_bytes_ta_index_of(bytes, args[0]->v.num, from, argc > 1, mid != DYN_M_lastIndexOf, includes) + : -1; + return includes ? scr_dyn_new_bool(found >= 0) : scr_dyn_new_num(found); + } if (dyn_bytes_proto_real(mid)) { dyn_throw_unsupported(scr_bytes_elem_name(bytes->elem), method); return NULL; diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index 3391f54b1..bc554a445 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -390,6 +390,13 @@ static SCR_TL bool scr_stdin_resumed = false; static SCR_TL bool scr_stdin_read_eof = false; static SCR_TL ScrBytes *scr_stdin_buffer; static SCR_TL size_t scr_stdin_read_demand; +/* 'readable' listeners (paused mode, as Node's once one is added): input + * stays buffered for read(), and the event fires on the next tick when + * input arrives while a read wanted more (`need`), and once at EOF. */ +static SCR_TL ScrStdinEndL *scr_stdin_readable = NULL; +static SCR_TL size_t scr_stdin_nreadable = 0, scr_stdin_readable_cap = 0; +static SCR_TL bool scr_stdin_readable_need = false; +static SCR_TL bool scr_stdin_readable_queued = false; #ifdef SCR_STDIO_EXTENDED /* process.stdin.setEncoding(enc): the canonical encoding name (NULL until @@ -475,6 +482,78 @@ void scr_stdin_on_end(ScrClosure *cb /*moves*/, bool once) { scr_stdin_nend++; } +static void scr_stdin_finish(bool fire_end); + +static void scr_stdin_emit_readable(void) { + scr_stdin_readable_queued = false; + size_t length = scr_stdin_buffer ? scr_stdin_buffer->len : 0; + if (scr_stdin_eof || scr_stdin_destroyed || (!length && !scr_stdin_read_eof)) return; + size_t n = scr_stdin_nreadable; + ScrStdinEndL *snap = malloc((n ? n : 1) * sizeof *snap); + if (!snap) scr_events_oom(); + for (size_t i = 0; i < n; i++) { + snap[i] = scr_stdin_readable[i]; + scr_closure_retain(snap[i].cb); + } + for (size_t i = 0; i < n; i++) { + if (snap[i].once) { + for (size_t j = 0; j < scr_stdin_nreadable; j++) { + if (scr_stdin_readable[j].cb == snap[i].cb) { + scr_closure_release(scr_stdin_readable[j].cb); + memmove(scr_stdin_readable + j, scr_stdin_readable + j + 1, + (scr_stdin_nreadable - j - 1) * sizeof *scr_stdin_readable); + scr_stdin_nreadable--; + break; + } + } + } + if (!scr_exc_pending() && !scr_stdin_eof && !scr_stdin_destroyed) + ((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb); + scr_closure_release(snap[i].cb); + } + free(snap); + length = scr_stdin_buffer ? scr_stdin_buffer->len : 0; + /* Another event is due once the buffer reaches the 64 KiB mark, or the + * larger size a listener's read(size) asked for. */ + scr_stdin_readable_need = + !scr_stdin_read_eof && (length <= 65536 || length < scr_stdin_read_demand); +} + +static void scr_stdin_queue_readable(void) { + if (scr_stdin_readable_queued || !scr_stdin_nreadable) return; + scr_stdin_readable_queued = true; + scr_next_tick_raw(scr_stdin_emit_readable); +} + +/* Adding a 'readable' listener starts reading ahead to the 64 KiB mark; a + * buffer that already holds input fires the event at once (next tick). */ +static void scr_stdin_on_readable(ScrClosure *cb /*moves*/, bool once) { + if (scr_stdin_eof || scr_stdin_destroyed) { + scr_closure_release(cb); + return; + } + if (scr_stdin_nreadable == scr_stdin_readable_cap) { + scr_stdin_readable_cap = scr_stdin_readable_cap ? scr_stdin_readable_cap * 2 : 2; + scr_stdin_readable = realloc(scr_stdin_readable, scr_stdin_readable_cap * sizeof *scr_stdin_readable); + if (!scr_stdin_readable) scr_events_oom(); + } + scr_stdin_readable[scr_stdin_nreadable].cb = cb; + scr_stdin_readable[scr_stdin_nreadable].once = once; + scr_stdin_nreadable++; + scr_stdin_readable_need = true; + if (scr_stdin_read_demand < 65536) scr_stdin_read_demand = 65536; + if (scr_stdin_buffer && scr_stdin_buffer->len) scr_stdin_queue_readable(); +} + +/* The input ended: with 'readable' listeners the event fires once more and + * 'end' waits for the read that finds nothing left; otherwise 'end' fires + * once the buffer is drained. */ +static void scr_stdin_reached_eof(void) { + scr_stdin_read_eof = true; + if (scr_stdin_nreadable) scr_stdin_queue_readable(); + else if (!scr_stdin_buffer) scr_stdin_finish(true); +} + void scr_stdin_on_error(ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) { if (scr_stdin_eof || scr_stdin_destroyed) { scr_closure_release(cb); @@ -528,6 +607,10 @@ static void scr_stdin_drop_listeners(void) { for (size_t i = 0; i < scr_stdin_ndata; i++) scr_closure_release(scr_stdin_data[i].cb); for (size_t i = 0; i < scr_stdin_nend; i++) scr_closure_release(scr_stdin_end[i].cb); for (size_t i = 0; i < scr_stdin_nerr; i++) scr_closure_release(scr_stdin_err[i].cb); + for (size_t i = 0; i < scr_stdin_nreadable; i++) scr_closure_release(scr_stdin_readable[i].cb); + free(scr_stdin_readable); + scr_stdin_readable = NULL; + scr_stdin_nreadable = scr_stdin_readable_cap = 0; free(scr_stdin_data); free(scr_stdin_end); free(scr_stdin_err); @@ -627,7 +710,8 @@ static void scr_stdin_deliver(ScrBytes *chunk); /* A chunk that arrived (moves): delivered, or buffered while paused or * without a consumer. */ static void scr_stdin_arrived(ScrBytes *chunk) { - if (scr_stdin_paused || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) { + if (scr_stdin_paused || scr_stdin_nreadable || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) { + if (scr_stdin_nreadable && scr_stdin_readable_need) scr_stdin_queue_readable(); if (scr_stdin_buffer) { size_t before = scr_stdin_buffer->len; ScrBytes *joined = scr_bytes_new(SCR_BYTES_U8, (double)(before + chunk->len)); @@ -645,7 +729,11 @@ static void scr_stdin_arrived(ScrBytes *chunk) { static void scr_stdin_service(void) { if (!scr_stdin_watching()) return; - if (!scr_stdin_paused && scr_stdin_buffer) { + /* Buffered input goes to a consumer: 'data' listeners, a resumed + * stream, or a parked for-await read. After the last 'readable' listener + * it stays buffered until one arrives, as in Node. */ + if (!scr_stdin_paused && !scr_stdin_nreadable && scr_stdin_buffer && + (scr_stdin_ndata > 0 || scr_stdin_resumed || scr_stdin_waiter != NULL)) { ScrBytes *chunk = scr_stdin_buffer; scr_stdin_buffer = NULL; scr_stdin_deliver(chunk); @@ -653,7 +741,7 @@ static void scr_stdin_service(void) { return; } if (scr_stdin_read_eof) { - if (!scr_stdin_buffer) scr_stdin_finish(true); + if (!scr_stdin_buffer && !scr_stdin_nreadable) scr_stdin_finish(true); return; } if (scr_stdin_fed) { @@ -662,10 +750,7 @@ static void scr_stdin_service(void) { bool ended = false; ScrBytes *chunk = scr_stdin_fed_take(&ended); if (chunk) scr_stdin_arrived(chunk); - else if (ended) { - scr_stdin_read_eof = true; - if (!scr_stdin_buffer) scr_stdin_finish(true); - } + else if (ended) scr_stdin_reached_eof(); return; } #ifdef _WIN32 @@ -727,8 +812,7 @@ static void scr_stdin_service(void) { return; } if (n == 0) { - scr_stdin_read_eof = true; - if (!scr_stdin_buffer) scr_stdin_finish(true); + scr_stdin_reached_eof(); return; } ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n); @@ -802,6 +886,8 @@ static void scr_stdin_deliver(ScrBytes *chunk) { #endif ScrPromise *p = scr_stdin_waiter; scr_stdin_waiter = NULL; + /* Node's stdin yields Buffers. */ + chunk->is_buffer = true; scr_promise_fulfill_ref(p, scr_bytes_retain(chunk), scr_bytes_retain_v, scr_bytes_release_v, NULL); scr_promise_release(p); @@ -878,6 +964,7 @@ static void scr_stdio_data_text(ScrClosure *cb, ScrStr *text) { } #endif static void scr_stdio_end(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); } +static void scr_stdio_readable(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); } static void scr_stdio_error(ScrClosure *cb, ScrStr *message) { ScrError *error = scr_error_new(SCR_ERR_ERROR, message); ScrDyn *arg = scr_dyn_from_error(error); @@ -985,6 +1072,7 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) { if (scr_stdin_read_eof && wanted > available) wanted = available; if (wanted == 0 || wanted > available) { if (!scr_stdin_eof && !scr_stdin_destroyed) { + scr_stdin_readable_need = true; scr_stdin_read_demand = wanted > 65536 ? wanted : 65536; if (scr_stdin_read_eof) scr_next_tick_raw(scr_stdio_finish_read); } @@ -995,6 +1083,7 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) { scr_bytes_release(scr_stdin_buffer); scr_stdin_buffer = rest; scr_stdin_read_demand = 65536; + if (!rest && !scr_stdin_read_eof) scr_stdin_readable_need = true; scr_stdin_deliver(chunk); ScrDyn *result = scr_dyn_new_chunk(chunk); scr_bytes_release(chunk); @@ -1150,12 +1239,14 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc bool data = arg->kind == SCR_DYN_STR && arg->v.str->len == 4 && memcmp(event, "data", 4) == 0; bool end = arg->kind == SCR_DYN_STR && arg->v.str->len == 3 && memcmp(event, "end", 3) == 0; bool error = arg->kind == SCR_DYN_STR && arg->v.str->len == 5 && memcmp(event, "error", 5) == 0; + bool readable = stream->fd == 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 8 && + memcmp(event, "readable", 8) == 0; #ifdef SCR_STDIO_EXTENDED bool resize = stream->fd != 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 6 && memcmp(event, "resize", 6) == 0; - if ((!data && !end && !error && !resize) || (stream->fd != 0 && !error && !resize)) { + if ((!data && !end && !error && !readable && !resize) || (stream->fd != 0 && !error && !resize)) { #else - if ((!data && !end && !error) || (stream->fd != 0 && !error)) { + if ((!data && !end && !error && !readable) || (stream->fd != 0 && !error)) { #endif result = scr_stdio_refusal("event"); goto done; @@ -1175,6 +1266,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc if (data) scr_stdin_on_data(scr_stdio_callback(listener, (void *)scr_stdio_data), scr_stdio_data, once); #endif if (end) scr_stdin_on_end(scr_stdio_callback(listener, (void *)scr_stdio_end), once); + if (readable) scr_stdin_on_readable(scr_stdio_callback(listener, (void *)scr_stdio_readable), once); if (error) scr_stdin_on_error(scr_stdio_callback(listener, (void *)scr_stdio_error), scr_stdio_error, once); } else { #define REMOVE_STDIO_LISTENER(list, count, fire) \ @@ -1187,6 +1279,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc } if (data) { REMOVE_STDIO_LISTENER(scr_stdin_data, scr_stdin_ndata, scr_stdio_data); } if (end) { REMOVE_STDIO_LISTENER(scr_stdin_end, scr_stdin_nend, scr_stdio_end); } + if (readable) { REMOVE_STDIO_LISTENER(scr_stdin_readable, scr_stdin_nreadable, scr_stdio_readable); } if (error) { REMOVE_STDIO_LISTENER(scr_stdin_err, scr_stdin_nerr, scr_stdio_error); } #undef REMOVE_STDIO_LISTENER } diff --git a/tests/corpus/7680-typed-array-search.ts b/tests/corpus/7680-typed-array-search.ts new file mode 100644 index 000000000..353736e09 --- /dev/null +++ b/tests/corpus/7680-typed-array-search.ts @@ -0,0 +1,63 @@ +// TypedArray.prototype.indexOf, lastIndexOf and includes on every numeric +// element kind: strict equality (NaN never found) for indexOf/lastIndexOf, +// SameValueZero (NaN found, -0 equals 0) for includes, and fromIndex as +// ToIntegerOrInfinity, counted from the end when negative. +const bytes = new Uint8Array([1, 2, 10, 3, 10, 255]); +console.log("u8", bytes.indexOf(10), bytes.indexOf(10, 3), bytes.indexOf(10, 5), bytes.lastIndexOf(10)); +console.log("u8 from", bytes.indexOf(10, -2), bytes.indexOf(10, -100), bytes.indexOf(1, Infinity), bytes.indexOf(10, 2.9)); +console.log("u8 last", bytes.lastIndexOf(10, 3), bytes.lastIndexOf(10, -3), bytes.lastIndexOf(1, -100), bytes.lastIndexOf(255, 99)); +console.log("u8 values", bytes.indexOf(256 + 10), bytes.indexOf(-246), bytes.indexOf(10.5), bytes.includes(255), bytes.includes(0)); +console.log("u8 includes from", bytes.includes(1, 1), bytes.includes(255, -1), bytes.includes(2, -5)); + +const clamped = new Uint8ClampedArray([0, 300, -5, 127.5]); +console.log("u8c", clamped.indexOf(255), clamped.indexOf(0, 1), clamped.lastIndexOf(0), clamped.indexOf(128)); + +const signed = new Int8Array([-1, 127, -128, 0]); +console.log("i8", signed.indexOf(-1), signed.indexOf(255), signed.indexOf(-128), signed.lastIndexOf(0)); + +const shorts = new Int16Array([5, -300, 5, 7]); +const ushorts = new Uint16Array([65535, 1, 65535]); +console.log("16", shorts.indexOf(-300), shorts.lastIndexOf(5), ushorts.indexOf(-1), ushorts.lastIndexOf(65535, 1)); + +const ints = new Int32Array([-2147483648, 0, 2147483647, 0]); +const uints = new Uint32Array([4294967295, 3]); +console.log("32", ints.indexOf(2147483647), ints.lastIndexOf(0), uints.indexOf(4294967295), uints.includes(-1)); + +const floats = new Float64Array([1.5, NaN, -0, 0, Infinity]); +console.log("f64", floats.indexOf(NaN), floats.lastIndexOf(NaN), floats.includes(NaN), floats.indexOf(0), floats.indexOf(-0)); +console.log("f64 more", floats.lastIndexOf(0), floats.indexOf(Infinity), floats.includes(-Infinity), floats.includes(NaN, 2)); + +const singles = new Float32Array([1.1, 0.5, NaN]); +console.log("f32", singles.indexOf(1.1), singles.indexOf(Math.fround(1.1)), singles.indexOf(0.5), singles.includes(NaN), singles.indexOf(NaN)); + +const empty = new Float64Array(0); +console.log("empty", empty.indexOf(0), empty.lastIndexOf(0), empty.includes(0), empty.includes(0, -1)); + +// Views search their own window of the buffer. +const buffer = new ArrayBuffer(16); +const whole = new Uint8Array(buffer); +whole.set([9, 9, 1, 2, 9, 3, 9, 9]); +const view = new Uint8Array(buffer, 2, 4); +const sub = whole.subarray(3, 6); +console.log("views", view.indexOf(9), view.lastIndexOf(9), view.indexOf(1), sub.indexOf(9), sub.includes(1), sub.lastIndexOf(3)); +const words = new Uint16Array(buffer, 2, 3); +console.log("word view", words.indexOf(0x0201), words.length); + +// Shared memory searches the same way. +const shared = new Int32Array(new SharedArrayBuffer(16)); +shared[1] = 7; +shared[3] = 7; +console.log("shared", shared.indexOf(7), shared.lastIndexOf(7), shared.includes(0), shared.indexOf(7, 2)); + +// A loop that finds every occurrence, as a line splitter does. +const text = new TextEncoder().encode("one\ntwo\n\nthree\n"); +const lines: number[] = []; +for (let at = text.indexOf(10); at !== -1; at = text.indexOf(10, at + 1)) lines.push(at); +console.log("lines", lines.join(",")); + +// A Buffer passed as a Uint8Array keeps Buffer's search: a number needle +// is taken as a byte, and a NaN byteOffset searches the whole buffer. +function searchView(view: Uint8Array): string { + return [view.indexOf(97 + 256), view.lastIndexOf(98, NaN), view.includes(353), view.indexOf(98, -1)].join(","); +} +console.log(searchView(Buffer.from("abcab")), searchView(new Uint8Array([97, 98, 99, 97, 98]))); diff --git a/tests/corpus/7681-typed-array-search-dynamic.mjs b/tests/corpus/7681-typed-array-search-dynamic.mjs new file mode 100644 index 000000000..643ba0cfa --- /dev/null +++ b/tests/corpus/7681-typed-array-search-dynamic.mjs @@ -0,0 +1,28 @@ +// TypedArray search on untyped values (parameters of untyped functions and +// object members): needles that are not numbers are never found (strict +// equality), fromIndex may be undefined, a string or a fraction, and +// Buffers keep their own indexOf, which wraps number needles and takes +// strings. +function views(bytes, floats, shorts) { + return { bytes, floats, shorts }; +} +const { bytes, floats, shorts } = views( + new Uint8Array([1, 2, 10, 3, 10]), + new Float64Array([0.5, NaN, -0]), + new Int16Array([-1, 300, -1]), +); +console.log(bytes.indexOf(10), bytes.indexOf(10, 3), bytes.lastIndexOf(10), bytes.includes(3), bytes.includes(4)); +console.log(bytes.lastIndexOf(1, undefined), bytes.lastIndexOf(10), bytes.indexOf(10, undefined), bytes.includes(1, undefined)); +console.log(bytes.indexOf(10, '3'), bytes.indexOf(10, 2.9), bytes.indexOf(10, -1), bytes.lastIndexOf(10, -2), bytes.indexOf(10, null)); +console.log(bytes.indexOf('10'), bytes.includes('1'), bytes.indexOf(10n), bytes.indexOf(), bytes.includes(), bytes.indexOf(266)); +console.log(floats.indexOf(NaN), floats.includes(NaN), floats.indexOf(0), floats.lastIndexOf(-0), floats.includes(0.5, 1)); +console.log(shorts.indexOf(-1), shorts.lastIndexOf(-1), shorts.indexOf(300, -2), shorts.includes(65535)); +const holder = { view: bytes.subarray(2) }; +console.log(holder.view.indexOf(10), holder.view.lastIndexOf(10), holder.view.includes(1)); +const buffer = Buffer.from([1, 2, 10, 3, 10]); +const boxed = [buffer]; +console.log(boxed[0].indexOf(10), boxed[0].indexOf(266), boxed[0].indexOf('\n'), boxed[0].includes(Buffer.from([3, 10]))); +console.log(boxed[0].subarray(3).indexOf(10), Buffer.isBuffer(boxed[0].subarray(3)), boxed[0].lastIndexOf(10, -2)); +let count = 0; +for (let at = bytes.indexOf(10); at !== -1; at = bytes.indexOf(10, at + 1)) count++; +console.log('count', count); diff --git a/tests/corpus/7682-constructor-function-globals.js b/tests/corpus/7682-constructor-function-globals.js new file mode 100644 index 000000000..58292e4ac --- /dev/null +++ b/tests/corpus/7682-constructor-function-globals.js @@ -0,0 +1,55 @@ +// Module bindings that hold `new F()` of an ordinary constructor function +// (const, let and var), read and reassigned by functions declared elsewhere +// in the module, such as the head and tail of a linked list. +'use strict'; +function Segment(prev) { + this.prev = prev; + this.next = null; + this.data = new Array(4); + this.pos = 0; +} +const head = new Segment(); +let current = head; +var spare = new Segment(head); + +function append(value) { + if (current.pos === current.data.length) { + current = current.next || (current.next = new Segment(current)); + } + current.data[current.pos++] = value; +} + +function collect() { + const out = []; + for (let node = head; node; node = node.next) out.push(node.data.slice(0, node.pos).join('')); + return out.join('|'); +} + +function reset() { + current = head; + for (let node = head; node; node = node.next) node.pos = 0; +} + +for (const ch of 'abcdefghij') append(ch); +console.log(collect(), current === head, current.prev === head.next ? 'two' : 'more'); +reset(); +append('z'); +console.log(collect(), current === head, head.next !== null); +console.log(spare.prev === head, spare instanceof Segment, typeof spare.data); +spare = new Segment(spare); +console.log(spare.prev.prev === head); + +function Counter(start) { + this.value = start; +} +Counter.prototype.bump = function () { + return ++this.value; +}; +let counter = new Counter(10); +function bumpTwice() { + counter.bump(); + return counter.bump(); +} +console.log(bumpTwice(), counter.value); +counter = new Counter(0); +console.log(bumpTwice()); diff --git a/tests/fixtures/event-loop/stdin-for-await-buffers.mjs b/tests/fixtures/event-loop/stdin-for-await-buffers.mjs new file mode 100644 index 000000000..d64ae460a --- /dev/null +++ b/tests/fixtures/event-loop/stdin-for-await-buffers.mjs @@ -0,0 +1,10 @@ +// for await over process.stdin yields Buffers: Buffer.isBuffer holds and +// indexOf takes a byte and a start offset (only chunking-independent facts +// are printed). +const chunks = []; +for await (const chunk of process.stdin) chunks.push(chunk); +console.log('buffers', chunks.every((chunk) => Buffer.isBuffer(chunk))); +const all = Buffer.concat(chunks); +const newlines = []; +for (let at = all.indexOf(10); at !== -1; at = all.indexOf(10, at + 1)) newlines.push(at); +console.log('newlines', newlines.join(','), all.subarray(newlines[0] + 1).indexOf('#')); diff --git a/tests/fixtures/event-loop/stdio-readable-large-size.mjs b/tests/fixtures/event-loop/stdio-readable-large-size.mjs new file mode 100644 index 000000000..f1f44edce --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-large-size.mjs @@ -0,0 +1,9 @@ +// read(size) above the 64 KiB read-ahead mark: 'readable' keeps firing as +// input arrives until the requested size is buffered. +const input = process.stdin; +const sizes = []; +input.on('readable', () => { + let chunk; + while ((chunk = input.read(150000)) !== null) sizes.push(chunk.length); +}); +input.on('end', () => console.log(sizes.join(','))); diff --git a/tests/fixtures/event-loop/stdio-readable-once-rest.mjs b/tests/fixtures/event-loop/stdio-readable-once-rest.mjs new file mode 100644 index 000000000..81eae144b --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-once-rest.mjs @@ -0,0 +1,12 @@ +// A once('readable') listener that reads part of the input: the rest stays +// buffered for a 'data' listener added afterwards. +const input = process.stdin; +input.once('readable', () => { + const head = input.read(3); + console.log('head', head === null ? null : head.toString('ascii')); + setTimeout(() => { + const rest = []; + input.on('data', (chunk) => rest.push(chunk.toString('ascii'))); + input.on('end', () => console.log('rest', rest.join(''))); + }, 50); +}); diff --git a/tests/fixtures/event-loop/stdio-readable-size.mjs b/tests/fixtures/event-loop/stdio-readable-size.mjs new file mode 100644 index 000000000..9ff76dafe --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-size.mjs @@ -0,0 +1,18 @@ +// Fixed-size reads of stdin: read(n) answers null until n bytes are +// buffered, and the rest once the input ended. +const input = process.stdin; +const chunks = []; +const counts = []; +input.on('readable', () => { + let reads = 0; + let chunk; + while ((chunk = input.read(4)) !== null) { + chunks.push(chunk.toString('ascii')); + reads++; + } + counts.push(reads); +}); +input.on('end', () => { + console.log(chunks.join('|')); + console.log(counts.join(',')); +}); diff --git a/tests/fixtures/event-loop/stdio-readable.mjs b/tests/fixtures/event-loop/stdio-readable.mjs new file mode 100644 index 000000000..324a4b57b --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable.mjs @@ -0,0 +1,19 @@ +// process.stdin in paused mode through 'readable': each event drains the +// buffer with read(), one more event follows EOF, and 'end' comes after the +// read that finds the buffer empty. +const input = process.stdin; +const events = []; +let text = ''; +input.on('readable', () => { + let reads = 0; + let chunk; + while ((chunk = input.read()) !== null) { + text += String(chunk); + reads++; + } + events.push(`readable ${reads}`); +}); +input.on('end', () => { + console.log(events.join(', ')); + console.log(JSON.stringify(text), input.readableLength); +}); diff --git a/tests/harness/corpus-costs/7680-typed-array-search.ts.json b/tests/harness/corpus-costs/7680-typed-array-search.ts.json new file mode 100644 index 000000000..37f8b6e89 --- /dev/null +++ b/tests/harness/corpus-costs/7680-typed-array-search.ts.json @@ -0,0 +1 @@ +{ "plain": 2204, "san": 3060 } diff --git a/tests/harness/corpus-costs/7681-typed-array-search-dynamic.mjs.json b/tests/harness/corpus-costs/7681-typed-array-search-dynamic.mjs.json new file mode 100644 index 000000000..b6fcd5eff --- /dev/null +++ b/tests/harness/corpus-costs/7681-typed-array-search-dynamic.mjs.json @@ -0,0 +1 @@ +{ "plain": 1649, "san": 958 } diff --git a/tests/harness/corpus-costs/7682-constructor-function-globals.js.json b/tests/harness/corpus-costs/7682-constructor-function-globals.js.json new file mode 100644 index 000000000..90fcede92 --- /dev/null +++ b/tests/harness/corpus-costs/7682-constructor-function-globals.js.json @@ -0,0 +1 @@ +{ "plain": 1623, "san": 962 } diff --git a/tests/harness/event-loop-cases.ts b/tests/harness/event-loop-cases.ts index 40bc7988d..1b75defaa 100644 --- a/tests/harness/event-loop-cases.ts +++ b/tests/harness/event-loop-cases.ts @@ -34,6 +34,55 @@ export const eventLoopCases: EventLoopCase[] = [ fixture: "stdio-paused-exit.mjs", script: { writes: [], end: false }, }, + { + title: "stdio values: 'readable' drains with read(), then fires once more at EOF", + fixture: "stdio-readable.mjs", + script: { + writes: [ + { delayMs: 0, data: "alpha " }, + { delayMs: 60, data: "beta\n" }, + ], + end: true, + }, + }, + { + title: "stdio values: 'readable' with read(size) holds a partial tail until EOF", + fixture: "stdio-readable-size.mjs", + script: { writes: [{ delayMs: 0, data: "abcdefghij" }], end: true }, + }, + { + title: "stdio values: input a once('readable') listener leaves stays buffered for 'data'", + fixture: "stdio-readable-once-rest.mjs", + script: { writes: [{ delayMs: 0, data: "abcdefgh" }], end: true }, + }, + { + title: "stdio values: 'readable' with a read(size) above the read-ahead mark", + fixture: "stdio-readable-large-size.mjs", + script: { + writes: [ + { delayMs: 0, data: "a".repeat(70000) }, + { delayMs: 30, data: "b".repeat(70000) }, + { delayMs: 30, data: "c".repeat(60000) }, + ], + end: true, + }, + }, + { + title: "stdio values: 'readable' on an immediately ended pipe", + fixture: "stdio-readable.mjs", + script: { writes: [], end: true }, + }, + { + title: "for-await: process.stdin yields Buffers", + fixture: "stdin-for-await-buffers.mjs", + script: { + writes: [ + { delayMs: 0, data: "#one\nabcd\n" }, + { delayMs: 40, data: "#two\nefgh\n" }, + ], + end: true, + }, + }, { title: "read-all: one instant write, then EOF", fixture: "stdin-read-all.ts", From 03807c0c3574d395c53905aa4b8983ef8ec11d09 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 04:11:54 -0500 Subject: [PATCH 071/409] Run private members and super calls over foreign receivers like Node A JavaScript class's prototype method called with a receiver that is not an instance of its class (`C.prototype.m.call(other)`, or an object that inherits the method) now behaves as in Node when it uses private members or calls super methods, instead of throwing "expected C" for the receiver. - Private fields keep their brand check: reading or writing `this.#x` throws "Cannot read private member #x from an object whose class did not declare it" (or the write message), and `#x in this` answers false. - `this.#m()` brand-checks the receiver through a new `call` use of the private brand helpers and throws "Receiver must be an instance of class C" (V8's wording: the class's own identifier, "anonymous" without one, and "Cannot read properties of undefined (reading 'C')" for a nullish receiver). It also applies to private method calls on other checked-dynamic values. - `super.m(...)` looks the method up on the base class's complete prototype object and calls it with the receiver, as `super` reads [[HomeObject]].[[Prototype]]. Methods that use `super` in other positions, static methods with private names, and classes whose base chain includes a function-local, generic, mixin, Date or runtime-provided class keep the native receiver check. The receiver-independent method lowering and the prototype reflection now mark their frames as `this` boundaries, so lowering one class's members on top of another method's frames (a super call reflecting the base) never reaches the caller's `this`. --- .../frontend/lowering/class-method-values.ts | 50 ++++++- .../src/frontend/lowering/class-prototypes.ts | 3 + .../src/frontend/lowering/lower-calls.ts | 44 ++++++ .../src/frontend/lowering/lower-classes.ts | 14 +- .../src/frontend/lowering/private-brands.ts | 50 ++++++- ...1-class-methods-foreign-receivers.mjs.json | 6 + .../7621-class-methods-foreign-receivers.mjs | 130 ++++++++++++++++++ ...1-class-methods-foreign-receivers.mjs.json | 1 + 8 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json create mode 100644 tests/corpus/7621-class-methods-foreign-receivers.mjs create mode 100644 tests/harness/corpus-costs/7621-class-methods-foreign-receivers.mjs.json diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 6e8256513..af2ef51d5 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -378,9 +378,17 @@ function reflectedMethodValue( !isJsSourceFile(ts.getSourceFileOfNode(declaration)) || info.localClass !== undefined || lowerer.errorHierarchyClassOf(info.def.name); + // `super.m(...)` in a prototype method reads the base class's prototype, + // which lowerSuperMethodCall answers over the call-site receiver when the + // base has a prototype object. Other uses of `super` keep the native + // receiver. + const superCalls = !staticMethod && info.base !== null && prototypeChainViewable(info.base); ts.walkPreorder(member, (node) => { - if (ts.isPrivateIdentifier(node) || node.kind === ts.SyntaxKind.SuperKeyword) + if (node.kind === ts.SyntaxKind.SuperKeyword && !(superCalls && isSuperMethodCallee(node))) needsNativeReceiver = true; + // Private members on a static method's receiver (the class value) keep + // the native receiver; prototype methods brand-check theirs. + if (staticMethod && ts.isPrivateIdentifier(node)) needsNativeReceiver = true; }); if (needsNativeReceiver) return null; // The method reads itself as a value inside its own body: it is the @@ -403,7 +411,11 @@ function reflectedMethodValue( loc: locOf(member), }; } + // The method may be lowered on top of an unrelated caller's frames (a + // `super` call in another class's method reflects this class): its `this` + // is its own receiver, never an enclosing function's. const context = newFnCtx(false, null, null, DYN); + context.thisBoundary = true; const previousClass = lowerer.currentClass; lowerer.currentClass = info; lowerer.fnStack.push(context); @@ -441,6 +453,42 @@ function reflectedMethodValue( } } +/** Whether `base` and every class above it have a complete prototype object + * (reflectClassPrototype): module-level program classes that neither extend + * Date nor a runtime-provided class. */ +function prototypeChainViewable(base: ClassInfo): boolean { + for (let c: ClassInfo | null = base; c; c = c.base) + if ( + c.decl === null || + c.localClass !== undefined || + c.generic !== undefined || + c.genericInstance !== undefined || + c.mixinInstance !== undefined || + c.callableBase !== undefined || + c.extendsDate !== undefined || + c.def.runtime || + c.builtinEmitter || + c.builtinStream || + c.builtinError || + c.builtinEventTarget + ) + return false; + return true; +} + +/** `super` as the receiver of a called member: `super.m(...)`. */ +function isSuperMethodCallee(node: ts.Node): boolean { + const access = node.parent; + return ( + access !== undefined && + ts.isPropertyAccessExpression(access) && + access.expression === node && + access.parent !== undefined && + ts.isCallExpression(access.parent) && + access.parent.expression === access + ); +} + /** Complete adapters after all native class bodies and instantiations exist. */ export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunction[]): void { const byName = new Map(functions.map((fn) => [fn.name, fn])); diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index cb2ab2280..116150465 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -400,7 +400,10 @@ export function reflectClassPrototype( writable, ]), ); + // The members lower on top of whatever frames requested the view (a + // `super` call in a subclass's method); their `this` is their own receiver. const context = newFnCtx(false, null, null, DYN); + context.thisBoundary = true; lowerer.fnStack.push(context); try { for (const member of info.decl.members.nodes) { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 82f5a27c1..d80f342c2 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,3 +1,4 @@ +import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js"; import { keyedHelperName } from "./stable-names.js"; import { perKeyOrder } from "./key-order-witness.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; @@ -547,6 +548,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ) { return lowerer.lowerSuperMethodCall(expr, expr.expression); } + // `value.#m(...)` on a checked-dynamic value (`this` in a JavaScript + // class's receiver-independent method): the private method of the + // brand-checked receiver. + if ( + ts.isPropertyAccessExpression(expr.expression) && + ts.isPrivateIdentifier(expr.expression.name) && + !expr.expression.questionDotToken + ) { + const privateCall = lowerCheckedPrivateMethodCall(lowerer, expr, expr.expression); + if (privateCall) return privateCall; + } const consoleMember = lowerer.consoleCallMember(expr); const consoleMethod = consoleMember === null ? lowerer.consoleMethodMember(expr) : null; @@ -11286,3 +11298,35 @@ function lowerShimIntrinsicCall( ); return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc }; } + +/** `value.#m(args)` where `value` lowers to a checked-dynamic value and the + * declaration-level class whose body declares `#m` declares it as a method: + * the method called on the brand-checked receiver + * (checkedPrivateMethodReceiver). Null for any other private member call. */ +function lowerCheckedPrivateMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (!ts.isPrivateIdentifier(access.name)) return null; + const name = access.name.text; + const owner = privateNameClass(lowerer, access.name); + const found = owner ? lowerer.findMethodOn(owner, name) : null; + if (!owner || !found || found.declarer !== owner || found.sig.abstract === true) return null; + const probed = tryLowerExpression(lowerer, access.expression); + if (!probed || !checkedDynamicOperand(probed)) return null; + const value = checkedDynamicOperand(lowerer.lowerExpr(access.expression)); + if (!value) return null; + const loc = locOf(call); + const callee = `%${owner.def.name}.${name}`; + lowerer.noteEdge(callee); + const receiver = checkedPrivateMethodReceiver(lowerer, owner, name, value, loc); + const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, loc, call); + return { kind: "call", callee, args: [receiver, ...args], type: found.sig.ret, loc }; +} + +/** A checked-dynamic value, or the dynamic value under a checked extraction. */ +function checkedDynamicOperand(expr: IrExpr): IrExpr | null { + if (expr.type.kind === "dyn") return expr; + return expr.kind === "dynCheck" && expr.value.type.kind === "dyn" ? expr.value : null; +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 29afbb4fe..db8f35f95 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -181,7 +181,7 @@ import { construction, constructionPrefix } from "./js-class-fields.js"; import { lowerObjectFactoryNew } from "./object-factory-new.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerInstanceConstructorNew } from "./class-instance-constructor.js"; -import { classPrototypeData } from "./class-prototypes.js"; +import { classPrototypeData, reflectClassPrototype } from "./class-prototypes.js"; import { constructionEscape } from "./construction-escapes.js"; import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js"; import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js"; @@ -9384,8 +9384,16 @@ export function lowerSuperMethodCall( if (!thisLocal) lowerer.unsupported("SC1080", access); const loc = locOf(call); const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc }; - if (lowerer.prototypeMethodAccesses.has(access.name.text)) { - const prototype = classPrototypeData(lowerer, cls.base, loc); + // A method whose `this` is the call-site receiver (a JavaScript class's + // receiver-independent method value, reflectedClassMethodValue) looks the + // method up on the base class's complete prototype, as `super` reads + // [[HomeObject]].[[Prototype]], and calls it with that receiver. + const dynamicThis = thisRef.type.kind === "dyn"; + if (dynamicThis || lowerer.prototypeMethodAccesses.has(access.name.text)) { + const prototype = + dynamicThis && cls.base.decl + ? reflectClassPrototype(lowerer, cls.base, loc, classValueRef(lowerer, cls.base, access)) + : classPrototypeData(lowerer, cls.base, loc); if (prototype) { const callee: IrExpr = { kind: "dynKeyGet", diff --git a/packages/compiler/src/frontend/lowering/private-brands.ts b/packages/compiler/src/frontend/lowering/private-brands.ts index c7881bce1..92bee4fdd 100644 --- a/packages/compiler/src/frontend/lowering/private-brands.ts +++ b/packages/compiler/src/frontend/lowering/private-brands.ts @@ -17,8 +17,9 @@ import type { Lowerer } from "./lowerer.js"; import type { ClassInfo } from "./lower-classes.js"; /** How a brand helper uses its receiver: `in` answers `#x in v`, `read` and - * `write` return the receiver as the declaring class for a field access. */ -type BrandUse = "in" | "read" | "write"; + * `write` return the receiver as the declaring class for a field access, and + * `call` for a private method call. */ +type BrandUse = "in" | "read" | "write" | "call"; const PREFIX = "%private."; @@ -214,6 +215,35 @@ export function checkedPrivateValue( return { kind: "call", callee: helper, args: [receiver], type: resultType, loc }; } +/** `value.#m(...)` on a checked-dynamic `value` in a declaration-level class + * `info` that declares the private method `#m`: the receiver as an instance + * of `info` when the value is one, else Node's TypeError ("Receiver must be + * an instance of class C"). */ +export function checkedPrivateMethodReceiver( + lowerer: Lowerer, + info: ClassInfo, + name: string, + receiver: IrExpr, + loc: SrcLoc, +): IrExpr { + const className = info.def.name; + const resultType: IrType = { kind: "object", className }; + const helper = `${PREFIX}call:${className}:${name}`; + if (!lowerer.valueHelpers.has(helper)) { + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "value.0", name: "value", type: DYN }], + returnType: resultType, + locals: [{ id: "value.0", name: "value", type: DYN, mutable: false }], + // finalizePrivateBrands fills the body. + body: [], + loc: helperLoc(loc), + }); + lowerer.valueHelpers.set(helper, helper); + } + return { kind: "call", callee: helper, args: [receiver], type: resultType, loc }; +} + /** A plain assignment, a destructuring target, or a for-in/of target writes * the field. Compound assignments and updates read it first, and Node * reports the failed read. */ @@ -358,6 +388,22 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]) }, { kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }, ); + } else if (use === "call") { + // V8 names the class by its own identifier (not a name inferred from + // a binding), "anonymous" without one, also for a nullish receiver. + const owner = lowerer.classes.get(className); + const identifier = owner?.decl?.name; + const shown = identifier !== undefined ? identifier.text : "anonymous"; + for (const unit of ["undefined", "null"] as const) { + body.push({ + kind: "if", + cond: { kind: "dynTest", test: unit, value, type: BOOL, loc }, + then: [typeError(strLit(`Cannot read properties of ${unit} (reading '${shown}')`, loc))], + else_: null, + loc, + }); + } + body.push(typeError(strLit(`Receiver must be an instance of class ${shown}`, loc))); } else { const verb = use === "read" ? "reading" : "setting"; const action = use === "read" ? "Cannot read properties" : "Cannot set properties"; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json new file mode 100644 index 000000000..8f50b09a3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7621-class-methods-foreign-receivers.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7621-class-methods-foreign-receivers.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/7621-class-methods-foreign-receivers.mjs b/tests/corpus/7621-class-methods-foreign-receivers.mjs new file mode 100644 index 000000000..beb046ea4 --- /dev/null +++ b/tests/corpus/7621-class-methods-foreign-receivers.mjs @@ -0,0 +1,130 @@ +// Prototype methods called with a receiver that is not an instance of +// their class (`C.prototype.m.call(other)`): `#private` members throw +// Node's TypeErrors for such a receiver, and `super.m()` looks the method up +// on the base class's prototype and calls it with the receiver. +function report(label, run) { + try { + console.log(label, run()); + } catch (error) { + console.log(label, error.constructor.name, error.message); + } +} + +class Counter { + #count = 0; + #step = 1; + constructor(name) { + this.name = name; + } + read() { + return this.#count; + } + bump() { + this.#count += this.#step; + return this.#count; + } + reset(value) { + this.#count = value; + return this.name; + } + #label() { + return `${this.name}#${this.#count}`; + } + label() { + return this.#label(); + } + owns(value) { + return #count in value; + } + ownsSelf() { + return #count in this; + } +} + +const counter = new Counter("c"); +console.log(counter.bump(), counter.bump(), counter.read(), counter.label()); +console.log(counter.owns(counter), counter.owns({}), counter.ownsSelf(), counter.reset(7), counter.read()); + +const plain = { name: "plain" }; +report("read", () => Counter.prototype.read.call(plain)); +report("bump", () => Counter.prototype.bump.call(plain)); +report("reset", () => Counter.prototype.reset.call(plain, 3)); +report("label", () => Counter.prototype.label.call(plain)); +report("ownsSelf", () => Counter.prototype.ownsSelf.call(plain)); +report("owns primitive", () => Counter.prototype.owns.call(counter, 5)); +report("label undefined", () => Counter.prototype.label.call(undefined)); +report("label null", () => Counter.prototype.label.call(null)); +report("label number", () => Counter.prototype.label.call(4)); +report("read other instance", () => Counter.prototype.read.call(new Counter("d"))); +report("label other instance", () => Counter.prototype.label.call(new Counter("e"))); + +const Anonymous = class { + #m() { + return 1; + } + call() { + return this.#m(); + } +}; +const Named = class Inner { + #m() { + return 2; + } + call() { + return this.#m(); + } +}; +report("anonymous", () => Anonymous.prototype.call.call({})); +report("named", () => Named.prototype.call.call({})); +console.log(new Anonymous().call(), new Named().call()); + +class Shape { + constructor(name) { + this.name = name; + } + describe() { + return `shape ${this.name}`; + } + area() { + return 0; + } +} +class Square extends Shape { + describe() { + return `square: ${super.describe()} area ${this.area()}`; + } + area() { + return this.size * this.size; + } + both() { + return [super.describe(), super.area()].join("|"); + } +} +class Tiny extends Square { + #tag = "t"; + describe() { + return `tiny<${super.describe()}>`; + } + tag() { + return `${this.#tag}:${super.both()}`; + } +} + +const square = new Square("s"); +square.size = 3; +console.log(square.describe(), square.both()); +const tiny = new Tiny("t"); +tiny.size = 1; +console.log(tiny.describe(), tiny.tag()); +const foreign = { + name: "f", + size: 2, + area() { + return 40; + }, +}; +report("square describe", () => Square.prototype.describe.call(foreign)); +report("square both", () => Square.prototype.both.call(foreign)); +report("tiny describe", () => Tiny.prototype.describe.call(foreign)); +report("tiny tag", () => Tiny.prototype.tag.call(foreign)); +report("square describe on tiny", () => Square.prototype.describe.call(tiny)); diff --git a/tests/harness/corpus-costs/7621-class-methods-foreign-receivers.mjs.json b/tests/harness/corpus-costs/7621-class-methods-foreign-receivers.mjs.json new file mode 100644 index 000000000..bb2ade1c2 --- /dev/null +++ b/tests/harness/corpus-costs/7621-class-methods-foreign-receivers.mjs.json @@ -0,0 +1 @@ +{ "plain": 6415, "san": 10107 } From fb200c1240de049c2b0ec56540e27437d8f8ead9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 04:27:14 -0500 Subject: [PATCH 072/409] Reuse the bundled declaration files' diagnostics across builds --- .../src/frontend/ts7/lib-diagnostics.test.ts | 221 ++++++++++++ .../src/frontend/ts7/lib-diagnostics.ts | 324 ++++++++++++++++++ .../compiler/src/frontend/ts7/program-host.ts | 35 +- packages/compiler/src/frontend/ts7/session.ts | 30 ++ packages/compiler/src/index.ts | 21 ++ packages/compiler/src/native/cli.ts | 17 +- 6 files changed, 645 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts create mode 100644 packages/compiler/src/frontend/ts7/lib-diagnostics.ts diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts new file mode 100644 index 000000000..f041a6cb5 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts @@ -0,0 +1,221 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, afterEach, expect, test } from "vitest"; +import type { SourceFile } from "./ast-types.js"; +import { + reusedLibDiagnostics, + setLibDiagnosticsStore, + type LibDiagnosticsStore, +} from "./lib-diagnostics.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; +import type { Ts7CheckedFileDiagnosticData, Ts7SessionProgram } from "./session.js"; +import { closeSourceParser, parseSourceFile } from "./source-parser-node.js"; + +const dir = mkdtempSync(join(tmpdir(), "scriptc-lib-diagnostics-")); +const lib = join(dir, "lib.es5.d.ts"); +const fallback = join(dir, "scriptc-node-fallback.d.ts"); +const libText = "declare var NaN: number;\ninterface Array { length: number }\n"; +const fallbackText = "export {};\ndeclare global {\n var process: { argv: string[] };\n}\n"; +writeFileSync(lib, libText); +writeFileSync(fallback, fallbackText); +const main = join(dir, "main.ts"); +const util = join(dir, "\u{1F600}.ts"); +const other = join(dir, "�.ts"); + +afterEach(() => setLibDiagnosticsStore(null)); +afterAll(() => { + closeSourceParser(); + rmSync(dir, { recursive: true, force: true }); +}); + +function memoryStore(): LibDiagnosticsStore & { entries: Map } { + const entries = new Map(); + return { + entries, + read: (key) => entries.get(key) ?? null, + write: (key, text) => void entries.set(key, text), + }; +} + +function diagnostic(fileName: string | undefined, pos: number, code = 2322): Ts7DiagnosticData { + return { fileName, pos, end: pos + 1, code, category: 1, text: `at ${pos}` }; +} + +/** A session program whose full-check and checked-file answers are given; it + * records which requests it was asked. */ +function sessionProgram(answers: { + semantic: Map; + global?: Ts7DiagnosticData[]; + checked?: boolean; +}): Ts7SessionProgram & { asked: string[] } { + const asked: string[] = []; + const all = [...answers.semantic.values()].flat(); + return { + asked, + getCompilerOptions: () => ({ strict: true }), + diagnosticData: (method: string, file?: string) => { + asked.push(file === undefined ? method : `${method} ${file}`); + if (method === "getGlobalDiagnostics") return answers.global ?? []; + return file === undefined ? all : (answers.semantic.get(file) ?? []); + }, + checkedFileDiagnosticData: (files: readonly string[]): Ts7CheckedFileDiagnosticData | null => { + asked.push(`checked ${files.join(",")}`); + if (answers.checked === false) return null; + return { files: files.map((file) => answers.semantic.get(file) ?? []), global: [] }; + }, + } as unknown as Ts7SessionProgram & { asked: string[] }; +} + +/** The program's files, parsed: the supplied two and the given others. */ +function files(texts: Record): (name: string) => SourceFile | undefined { + const parsed = new Map([ + [lib, parseSourceFile(lib, libText, "ts")], + [fallback, parseSourceFile(fallback, fallbackText, "ts")], + ]); + for (const [name, text] of Object.entries(texts)) + parsed.set(name, parseSourceFile(name, text, "ts")); + return (name) => parsed.get(name); +} + +const names = [lib, fallback, main]; +const supplied = (name: string): boolean => name === lib || name === fallback; +const moduleFile = files({ [main]: "export const n: number = 'x';\n" }); + +test("without a store the full check answers", () => { + const program = sessionProgram({ semantic: new Map() }); + expect(reusedLibDiagnostics(program, names, supplied, moduleFile)).toBeNull(); + expect(program.asked).toEqual([]); +}); + +test("a stored answer stands in for checking the supplied files, merged by file name", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([ + [lib, [diagnostic(lib, 1)]], + [fallback, []], + [main, [diagnostic(main, 5), diagnostic(main, 9)]], + ]); + const first = sessionProgram({ semantic }); + const full = reusedLibDiagnostics(first, names, supplied, moduleFile); + // The first build runs the full check and stores the supplied files' answers. + expect(first.asked).toEqual([ + "getGlobalDiagnostics", + "getSemanticDiagnostics", + "checked ", + `getSemanticDiagnostics ${lib}`, + `getSemanticDiagnostics ${fallback}`, + ]); + expect(store.entries.size).toBe(1); + const second = sessionProgram({ semantic }); + const reused = reusedLibDiagnostics(second, names, supplied, moduleFile); + expect(second.asked).toEqual([`checked ${main}`]); + // The files' diagnostics in the order of their names (lib.es5.d.ts, main.ts), as the full answer. + expect(reused).toEqual({ + semantic: [diagnostic(lib, 1), diagnostic(main, 5), diagnostic(main, 9)], + global: [], + }); + expect(full?.semantic).toEqual(reused?.semantic); +}); + +test("files merge in code point order, which differs from UTF-16 order", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const all = [lib, fallback, util, other]; + const sources = files({ [util]: "export {};\n", [other]: "export {};\n" }); + const semantic = new Map([ + [util, [diagnostic(util, 1)]], + [other, [diagnostic(other, 2)]], + ]); + reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); + const reused = reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); + // U+FFFD sorts before U+1F600 by code point (its surrogates sort after it by UTF-16 unit). + expect(reused?.semantic).toEqual([diagnostic(other, 2), diagnostic(util, 1)]); +}); + +test("a full check with global diagnostics is not stored", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const program = sessionProgram({ + semantic: new Map([[main, [diagnostic(main, 1)]]]), + global: [diagnostic(undefined, 0, 2318)], + }); + expect(reusedLibDiagnostics(program, names, supplied, moduleFile)?.global).toEqual([ + diagnostic(undefined, 0, 2318), + ]); + expect(store.entries.size).toBe(0); +}); + +test("scripts reuse the answer unless they declare a global the supplied files declare", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + // A script declaring only new names: it stores the answer, and a later one reuses it. + const fresh = files({ + [main]: "const total = 1;\nfunction run() {}\nif (total) { var later = 2; }\n", + }); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, fresh); + expect(store.entries.size).toBe(1); + const again = sessionProgram({ semantic }); + expect(reusedLibDiagnostics(again, names, supplied, fresh)).not.toBeNull(); + expect(again.asked).toEqual([`checked ${main}`]); + // Scripts declaring a supplied global, at the top level, in a block, in a + // `declare global` of a supplied module, or an ambient module: the full check. + for (const text of [ + "interface Array { extra: T }\n", + "if (true) { var NaN = 1; }\n", + "declare var process: unknown;\n", + 'declare module "fs" {}\n', + ]) { + const program = sessionProgram({ semantic }); + const result = reusedLibDiagnostics(program, names, supplied, files({ [main]: text })); + expect(program.asked[0], text).not.toBe(`checked ${main}`); + if (result !== null) expect(program.asked[0]).toBe("getGlobalDiagnostics"); + } + expect(store.entries.size).toBe(1); +}); + +test("a program whose script declares a supplied global does not store its answer", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const program = sessionProgram({ semantic: new Map([[main, []]]) }); + reusedLibDiagnostics(program, names, supplied, files({ [main]: "declare var NaN: number;\n" })); + expect(store.entries.size).toBe(0); +}); + +test("modules that can declare something global, and servers without the method, ask as before", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); + expect(store.entries.size).toBe(1); + for (const text of [ + "export {};\ndeclare global { var extra: number }\n", + 'export {};\ndeclare module "./other" { const extra: number }\n', + "export as namespace Lib;\nexport const n = 1;\n", + ]) { + const program = sessionProgram({ semantic }); + expect(reusedLibDiagnostics(program, names, supplied, files({ [main]: text }))).toBeNull(); + expect(program.asked).toEqual([]); + } + const old = sessionProgram({ semantic, checked: false }); + expect(reusedLibDiagnostics(old, names, supplied, moduleFile)).toBeNull(); + // Nor does a server without the method store an answer it could not use. + const fresh = memoryStore(); + setLibDiagnosticsStore(fresh, "checker"); + reusedLibDiagnostics(sessionProgram({ semantic, checked: false }), names, supplied, moduleFile); + expect(fresh.entries.size).toBe(0); +}); + +test("a supplied file that changes is a different entry", () => { + const store = memoryStore(); + setLibDiagnosticsStore(store, "checker"); + const semantic = new Map([[main, []]]); + reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); + writeFileSync(fallback, fallbackText + "// edited\n"); + const program = sessionProgram({ semantic }); + reusedLibDiagnostics(program, names, supplied, moduleFile); + expect(program.asked[0]).toBe("getGlobalDiagnostics"); + expect(store.entries.size).toBe(2); + writeFileSync(fallback, fallbackText); +}); diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts new file mode 100644 index 000000000..827b337bd --- /dev/null +++ b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts @@ -0,0 +1,324 @@ +/* The semantic diagnostics of the declaration files scriptc supplies (the + * TypeScript libs and scriptc's own declaration files), reused across + * builds. Checking them is most of the full check of a small program. When + * no other file of a program can change what checking those files reports + * (every one is a module without global or module augmentations, or a + * script declaring no global name the supplied files declare), the answer + * depends only on the checker, the compiler options and the files + * themselves, so a stored answer for the same three stands in for checking + * them again: the server checks only the program's other files + * (getDiagnosticsOfCheckedFiles), and the semantic diagnostics merge in the + * order the full check sorts them, by file name. An answer is stored only + * when the full check reports no global diagnostics: the global diagnostics + * of checking the other files are then the full check's. */ +import { createHash } from "node:crypto"; +import { statSync } from "node:fs"; +import type { Node, SourceFile } from "./ast-types.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; +import type { Ts7SessionProgram } from "./session.js"; +import { NodeFlags, SyntaxKind } from "./enums.js"; +import { walkPreorder } from "./ast.js"; + +/** Key-value storage for reused diagnostics (best effort: failures miss). */ +export interface LibDiagnosticsStore { + read(key: string): string | null; + write(key: string, text: string): void; +} + +const SCHEMA = "scriptc.lib-diagnostics.v1"; + +let configured: { store: LibDiagnosticsStore; checker: string } | null = null; + +/** Has later builds of this process reuse the diagnostics of the supplied + * declaration files through `store`, for the checker identified by + * `checker` (its executable's identity), or stop reusing them (null). */ +export function setLibDiagnosticsStore(store: LibDiagnosticsStore | null, checker = ""): void { + configured = store === null ? null : { store, checker }; +} + +/** A checker executable's identity for the stored diagnostics' key: its + * path and file identity, or the path alone when it cannot be read. */ +export function checkerExecutableIdentity(path: string): string { + try { + const info = statSync(path); + return JSON.stringify([path, info.size, info.mtimeMs, info.ino]); + } catch { + return JSON.stringify([path]); + } +} + +/** The program's semantic and global diagnostics as the full check reports + * them, using stored diagnostics for the supplied declaration files; null + * when they cannot be reused for this program (then the caller asks for + * them as before). `supplied` tells whether a program file is one of the + * supplied declaration files. */ +export function reusedLibDiagnostics( + program: Ts7SessionProgram, + fileNames: readonly string[], + supplied: (fileName: string) => boolean, + sourceFile: (fileName: string) => SourceFile | undefined, +): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + if (configured === null || process.env["SCRIPTC_LIB_DIAGNOSTICS"] === "0") return null; + const { store, checker } = configured; + const libs: string[] = []; + const own: string[] = []; + for (const name of fileNames) (supplied(name) ? libs : own).push(name); + if (libs.length === 0) return null; + // The global names each of the program's scripts declares; a module that + // declares nothing global contributes none. + const scripts: Set[] = []; + for (const name of own) { + const file = sourceFile(name); + if (file === undefined) return null; + if (file.externalModuleIndicator !== undefined) { + if (!declaresNothingGlobal(file)) return null; + continue; + } + const names = isJsFileName(name) ? null : scriptGlobalNames(file); + if (names === null) return null; + scripts.push(names); + } + // A script's globals cannot change what the supplied files report unless + // one of them is also a global the supplied files declare. + const collides = (globals: ReadonlySet): boolean => + scripts.some((names) => [...names].some((name) => globals.has(name))); + // The options without the configuration file's path: it names where the + // project is, which checking the supplied files does not read (the files a + // project's location brings in are part of the key). + const options = { ...program.getCompilerOptions(), configFilePath: undefined }; + const key = entryKey(checker, options, libs); + if (key === null) return null; + const stored = readEntry(store, key, libs); + if (stored !== null && !collides(stored.globals)) { + const checked = program.checkedFileDiagnosticData(own); + if (checked === null) return null; + const byFile = new Map(stored.files); + own.forEach((name, index) => byFile.set(name, checked.files[index] ?? [])); + return { semantic: inFileOrder(byFile), global: checked.global }; + } + // The full check, as before. Its answers for the supplied files are stored + // when it reports no global diagnostics and the program's scripts declare + // none of the supplied files' globals (so nothing of the program reached + // what those files report). + const global = program.diagnosticData("getGlobalDiagnostics"); + const semantic = program.diagnosticData("getSemanticDiagnostics"); + // A server that cannot check only some files could never use the answer. + if (global.length === 0 && stored === null && program.checkedFileDiagnosticData([]) !== null) { + const globals = suppliedGlobalNames(libs, sourceFile); + if (globals !== null && !collides(globals)) { + const files: Record = {}; + for (const name of libs) files[name] = program.diagnosticData("getSemanticDiagnostics", name); + try { + store.write(key, JSON.stringify({ schema: SCHEMA, globals: [...globals], files })); + } catch { + // Best effort: a failed write is a later miss. + } + } + } + return { semantic, global }; +} + +/** Whether a file cannot add to or change the global declarations the + * supplied files declare: a module with no `declare global`, no module + * augmentation and no UMD `export as namespace` declaration. */ +function declaresNothingGlobal(file: SourceFile): boolean { + if (file.externalModuleIndicator === undefined) return false; + if (file.moduleAugmentations.length > 0) return false; + for (const statement of file.statements.nodes) { + if (statement.kind === SyntaxKind.NamespaceExportDeclaration) return false; + // `declare global` (a module declaration named by the identifier `global`). + if ( + statement.kind === SyntaxKind.ModuleDeclaration && + statement.name?.kind === SyntaxKind.Identifier && + statement.name.text === "global" + ) + return false; + } + return true; +} + +function isJsFileName(name: string): boolean { + return /\.[cm]?jsx?$/.test(name); +} + +/** The names a script file declares in the global scope: its top-level + * declarations, and the `var` and function declarations of its top-level + * blocks; null when it declares an ambient module (`declare module "x"`), + * which can merge with one the supplied files declare. Over-approximates: + * a name more only makes the stored answer apply less often. */ +function scriptGlobalNames(file: SourceFile): Set | null { + const names = new Set(); + let ambientModule = false; + walkPreorder(file, (node) => { + if (node === file) return undefined; + if (node.kind === SyntaxKind.ModuleDeclaration && node.name?.kind === SyntaxKind.StringLiteral) + ambientModule = true; + const topLevel = node.parent === file; + if (topLevel || node.kind === SyntaxKind.FunctionDeclaration) addDeclaredNames(node, names); + else if ( + node.kind === SyntaxKind.VariableDeclarationList && + (node.flags & NodeFlags.BlockScoped) === 0 + ) + for (const declaration of node.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + // Functions, classes and namespaces have scopes of their own. + return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; + }); + return ambientModule ? null : names; +} + +/** The global names the supplied files declare: the declarations of their + * scripts (as scriptGlobalNames finds them, with ambient modules as their + * quoted names) and those of the `declare global` blocks of their modules; + * null when one of them is not available. */ +function suppliedGlobalNames( + libs: readonly string[], + sourceFile: (fileName: string) => SourceFile | undefined, +): Set | null { + const names = new Set(); + for (const name of libs) { + const file = sourceFile(name); + if (file === undefined) return null; + if (file.externalModuleIndicator === undefined) { + walkPreorder(file, (node) => { + if (node === file) return undefined; + if ( + node.kind === SyntaxKind.ModuleDeclaration && + node.name?.kind === SyntaxKind.StringLiteral + ) + names.add(JSON.stringify(node.name.text ?? "")); + if (node.parent === file || node.kind === SyntaxKind.FunctionDeclaration) + addDeclaredNames(node, names); + else if ( + node.kind === SyntaxKind.VariableDeclarationList && + (node.flags & NodeFlags.BlockScoped) === 0 + ) + for (const declaration of node.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; + }); + continue; + } + for (const statement of file.statements.nodes) { + if ( + statement.kind !== SyntaxKind.ModuleDeclaration || + statement.name?.kind !== SyntaxKind.Identifier || + statement.name.text !== "global" + ) + continue; + for (const inner of statement.body?.statements?.nodes ?? []) addDeclaredNames(inner, names); + } + } + return names; +} + +const GLOBAL_SCOPE_BOUNDARIES = new Set([ + SyntaxKind.FunctionDeclaration, + SyntaxKind.FunctionExpression, + SyntaxKind.ArrowFunction, + SyntaxKind.MethodDeclaration, + SyntaxKind.Constructor, + SyntaxKind.GetAccessor, + SyntaxKind.SetAccessor, + SyntaxKind.ClassDeclaration, + SyntaxKind.ClassExpression, + SyntaxKind.ModuleDeclaration, +]); + +/** The names a statement declares in the scope it is in. */ +function addDeclaredNames(node: Node, names: Set): void { + switch (node.kind) { + case SyntaxKind.VariableStatement: + for (const declaration of node.declarationList?.declarations?.nodes ?? []) + addBindingNames(declaration.name, names); + return; + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + if (node.name?.kind === SyntaxKind.Identifier) names.add(node.name.text ?? ""); + return; + } +} + +function addBindingNames(name: Node | undefined, names: Set): void { + if (name === undefined) return; + if (name.kind === SyntaxKind.Identifier) { + names.add(name.text ?? ""); + return; + } + // Object and array binding patterns. + for (const element of name.elements?.nodes ?? []) addBindingNames(element.name, names); +} + +/** The stored diagnostics' key: the checker, the compiler options and the + * supplied files in program order with each one's file identity; null when + * one of them cannot be read. */ +function entryKey(checker: string, options: unknown, libs: readonly string[]): string | null { + const files: unknown[] = []; + for (const name of libs) { + try { + const info = statSync(name); + files.push([name, info.size, info.mtimeMs, info.ino]); + } catch { + return null; + } + } + return createHash("sha256") + .update(JSON.stringify({ schema: SCHEMA, checker, options, files })) + .digest("hex"); +} + +function readEntry( + store: LibDiagnosticsStore, + key: string, + libs: readonly string[], +): { files: Map; globals: Set } | null { + let text: string | null; + try { + text = store.read(key); + } catch { + return null; + } + if (text === null) return null; + let entry: { schema?: unknown; globals?: unknown; files?: Record }; + try { + entry = JSON.parse(text) as typeof entry; + } catch { + return null; + } + if (entry.schema !== SCHEMA || entry.files === undefined || !Array.isArray(entry.globals)) + return null; + const files = new Map(); + for (const name of libs) { + const list = entry.files[name]; + if (!Array.isArray(list)) return null; + files.set(name, list); + } + return { files, globals: new Set(entry.globals.filter((name) => typeof name === "string")) }; +} + +/** Each file's diagnostics (already in the checker's order) in the order the + * checker sorts files: by file name, compared by code point. */ +function inFileOrder(byFile: ReadonlyMap): Ts7DiagnosticData[] { + const names = [...byFile.keys()].sort(compareCodePoints); + const out: Ts7DiagnosticData[] = []; + for (const name of names) for (const d of byFile.get(name)!) out.push(d); + return out; +} + +function compareCodePoints(a: string, b: string): number { + let i = 0; + let j = 0; + while (i < a.length && j < b.length) { + const x = a.codePointAt(i)!; + const y = b.codePointAt(j)!; + if (x !== y) return x < y ? -1 : 1; + i += x > 0xffff ? 2 : 1; + j += y > 0xffff ? 2 : 1; + } + return i < a.length ? 1 : j < b.length ? -1 : 0; +} diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 639086e94..ec143e401 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -46,6 +46,10 @@ import { trackedRealpath, } from "../input-tracker.js"; import { packageShimDeclarationText } from "../package-shims.js"; +import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.js"; +import { reusedLibDiagnostics } from "./lib-diagnostics.js"; +import { ts7Diagnostic } from "./diagnostic.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; /** The compiler options our createProgram accepts: TS7's CompilerOptions * shape (numeric enums for target/module/moduleResolution — the enums module @@ -393,6 +397,22 @@ export class Ts7Program { return this.project.program.getSourceFileMetadata(resolve(fileName)); } + /** The semantic and global diagnostics with the stored diagnostics of the + * declaration files scriptc supplies (lib-diagnostics.ts), or null. */ + reusedLibDiagnostics(): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { + this.snapshot.ensureActive(); + const program = this.project.program; + const declarations = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]); + return reusedLibDiagnostics( + program, + this.getSourceFileNames(), + (name) => + declarations.has(tsgoPath(name)) || + program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true, + (name) => this.getSourceFile(name), + ); + } + isSourceFileDefaultLibrary(file: SourceFile): boolean { return this.project.program.isSourceFileDefaultLibrary(file); } @@ -423,9 +443,20 @@ export class Ts7Program { * package dropped the aggregate). Order mirrors 5.9.3: config, options/ * program, global, syntactic, semantic. */ export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[] { + const config = program.getConfigFileParsingDiagnostics(); + const options = program.getProgramDiagnostics(); + const reused = program.reusedLibDiagnostics(); + if (reused !== null) + return [ + ...config, + ...options, + ...reused.global.map(ts7Diagnostic), + ...program.getSyntacticDiagnostics(), + ...reused.semantic.map(ts7Diagnostic), + ]; return [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getProgramDiagnostics(), + ...config, + ...options, ...program.getGlobalDiagnostics(), ...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics(), diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index d3f2ae9fe..082bb79a6 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -55,6 +55,13 @@ interface ProgramQuery { snapshot?: number; project?: string; file?: SemanticDocument; + files?: string[]; +} + +/** The answer of getDiagnosticsOfCheckedFiles, as the server sends it. */ +export interface Ts7CheckedFileDiagnosticData { + files: Ts7DiagnosticData[][]; + global: Ts7DiagnosticData[]; } /** Platform-independent session over an already connected transport. The @@ -344,6 +351,29 @@ export class Ts7SessionProgram { result.push(ts7Diagnostic(data)); return result; } + /** The diagnostics of `method` for `file`, or for the whole program, as + * the server sends them. */ + diagnosticData(method: string, file?: SemanticDocument): Ts7DiagnosticData[] { + return this.session.request(method, this.query(file)) ?? []; + } + /** The semantic diagnostics of each of `files` and the global diagnostics, + * after the server checks only those files (getDiagnosticsOfCheckedFiles), + * or null when the server does not answer that method. */ + checkedFileDiagnosticData(files: readonly string[]): Ts7CheckedFileDiagnosticData | null { + try { + return this.session.request("getDiagnosticsOfCheckedFiles", { + ...this.query(), + files: [...files], + }); + } catch (error) { + if ( + error instanceof Error && + error.message.includes('unknown API method "getDiagnosticsOfCheckedFiles"') + ) + return null; + throw error; + } + } getSyntacticDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSyntacticDiagnostics", file); } diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 4f7b93cfd..586561632 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -9,6 +9,7 @@ import { } from "./library/prepare.js"; import { analyzeWithFrontend } from "./frontend/analysis.js"; import { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js"; +import { setLibDiagnosticsStore } from "./frontend/ts7/lib-diagnostics.js"; import { analyzeWithVerdictCache, fileVerdictStore } from "./coverage/verdict-cache.js"; import { coverageCompilerIdentity } from "./coverage/verdict-startup.js"; import { commentOnlyReplay } from "./coverage/verdict-semantic.js"; @@ -1161,9 +1162,28 @@ async function prepareExecutableInputInChild( * `requestPath` as `compileTracked` would in-process, and writes what it prepared, without the LLVM * text, to `resultPath`. */ +/** Has the build reuse the diagnostics of the declaration files scriptc + * supplies (lib-diagnostics.ts) through the build cache, when there is one and + * the checker is tsc-ts. */ +async function configureLibDiagnostics(): Promise { + let checker: string | undefined; + try { + checker = checkerCacheIdentity(); + } catch { + // A missing checker fails where the build starts it. + checker = undefined; + } + const root = checker === undefined ? null : await prepareBuildCacheRoot(buildCacheRoot()); + setLibDiagnosticsStore( + root === null ? null : fileVerdictStore(join(root, "lib-diagnostics-v1")), + checker ?? "", + ); +} + export async function runFrontendProcess(requestPath: string, resultPath: string): Promise { const request = JSON.parse(await readFile(requestPath, "utf8")) as FrontendProcessRequest; clearCompileSessionCaches(); + await configureLibDiagnostics(); setProvenanceSources(request.provenance); const frontendInputs = new FrontendInputTracker(); const warnings: CompileWarningSink = { warnings: [], sourceTexts: null }; @@ -1226,6 +1246,7 @@ async function compileTracked( const timing = compilationTiming(); const timingStart = Date.now(); entryPath = resolve(entryPath); + await configureLibDiagnostics(); const outputKind = opts.outputKind ?? "exe"; if ( (opts.backend !== undefined && opts.backend !== "llvm") || diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index 21047b929..44d5eb9f0 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -16,7 +16,11 @@ import { createNativeTs7Api } from "../frontend/ts7/native-api.js"; import { resolveProvenanceSourcesWithParser } from "../frontend/provenance-core.js"; import { setCompilerReleaseVersion } from "../library/sidecar.js"; import { NativeCompiler } from "./compiler.js"; -import { openNativeCache } from "./cache.js"; +import { NATIVE_STRING_BYTES, openNativeCache } from "./cache.js"; +import { + checkerExecutableIdentity, + setLibDiagnosticsStore, +} from "../frontend/ts7/lib-diagnostics.js"; import { loadNativeToolchain } from "./toolchain.js"; import { buildSanitizedRuntime } from "./sanitizer.js"; import { runCompilerTask } from "./task.js"; @@ -53,6 +57,17 @@ async function main(): Promise { if (toolchain.declarationsRoot !== undefined) setDeclarationRoot(toolchain.declarationsRoot); setCompilerReleaseVersion(toolchain.compilerVersion); cache = openNativeCache((path, harden) => compilerNativePrivateDirectory(path, harden)); + if (cache !== null) { + const store = cache; + setLibDiagnosticsStore( + { + read: (key) => + store.read("lib-diagnostics", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null, + write: (key, text) => store.write("lib-diagnostics", key, text), + }, + checkerExecutableIdentity(toolchain.ts7Executable), + ); + } compiler = new NativeCompiler(toolchain, cache); return compiler; }; From 58d3603549065404f6b832c4f16b35d93409c82e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 04:33:56 -0500 Subject: [PATCH 073/409] Accept a callable Proxy as the generateKeyPair callback generateKeyPair tested its callback, and a function in the options position, by the checked-dynamic kind, so a Proxy whose target is a function threw ERR_INVALID_ARG_TYPE. Node checks typeof, which answers "function" for such a Proxy. Both tests now ask whether the value is callable. --- packages/runtime/src/scr_pki.c | 4 ++-- tests/corpus/7445-crypto-key-pair-options.mjs | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index 888e0eae9..a1a5e97c2 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -2102,11 +2102,11 @@ static void sp_key_pair_fail(ScrClosure *self) { * (null, publicKey, privateKey), or the error of a key generation that * fails, in a later macrotask. */ void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback) { - if (options->kind == SCR_DYN_FUNC) { + if (scr_dyn_is_callable(options)) { callback = options; options = scr_dyn_undefined(); } - if (callback->kind != SCR_DYN_FUNC) { + if (!scr_dyn_is_callable(callback)) { scr_dyn_arg_type_fail("callback", "of type function", callback); return; } diff --git a/tests/corpus/7445-crypto-key-pair-options.mjs b/tests/corpus/7445-crypto-key-pair-options.mjs index 86138e860..2e8f7e8b3 100644 --- a/tests/corpus/7445-crypto-key-pair-options.mjs +++ b/tests/corpus/7445-crypto-key-pair-options.mjs @@ -97,7 +97,19 @@ attempt("async, shifted callback", () => console.log("shifted callback", err, publicKey.asymmetricKeyType, privateKey.type); crypto.generateKeyPair( "ed25519", - (err2, publicKey2) => console.log("options callback", err2, publicKey2.type), + (err2, publicKey2) => { + console.log("options callback", err2, publicKey2.type); + // A callable Proxy is a function, in either position. + const proxied = new Proxy( + (err3, publicKey3) => { + console.log("proxy callback", err3, publicKey3.type); + crypto.generateKeyPair("ed25519", proxiedOptions); + }, + {}, + ); + const proxiedOptions = new Proxy((err4, publicKey4) => console.log("proxy options callback", err4, publicKey4.type), {}); + crypto.generateKeyPair("ed25519", {}, proxied); + }, () => console.log("never"), ); }), From 0e0458f29234d104d43c55a753a82703459545d0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 01:55:25 -0700 Subject: [PATCH 074/409] Keep literal key order through self copies and conversions that build no records A literal that starts by spreading a value of its own type, such as `{ ...current, ...patch, updatedAt }`, made the key-order witness give up on the type: the copy's source order was the type's own order, seeded with the declared table, so it conflicted with the literals that build the values. While no checked conversion builds the type, every value has the one chosen order and the copy adds no witness. Checked conversions also counted as builders of records they cannot create: a class instance's field records, a checked function's record parameters, a boxed primitive (the undefined a dynamic construction passes for a missing argument), and the result of a typed call of a method the program may replace on its instance, which without a replacement is the method's own native value boxed and checked back. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/class-callbacks.ts | 13 +- .../lowering/key-order-witness.test.ts | 118 +++++++++++++++++ .../frontend/lowering/key-order-witness.ts | 125 +++++++++++++++--- packages/compiler/src/ir/traverse.ts | 14 +- .../main.ts.json | 7 + .../corpus/7737-key-order-self-spread.ts.json | 6 + .../main.ts | 106 +++++++++++++++ .../registry.js | 21 +++ tests/corpus/7737-key-order-self-spread.ts | 34 +++++ .../main.ts.json | 1 + .../7737-key-order-self-spread.ts.json | 1 + 11 files changed, 421 insertions(+), 25 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json create mode 100644 tests/corpus/7736-key-order-replaceable-method-result/main.ts create mode 100644 tests/corpus/7736-key-order-replaceable-method-result/registry.js create mode 100644 tests/corpus/7737-key-order-self-spread.ts create mode 100644 tests/harness/corpus-costs/7736-key-order-replaceable-method-result/main.ts.json create mode 100644 tests/harness/corpus-costs/7737-key-order-self-spread.ts.json diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index 0bda0068e..f92f42a80 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -156,6 +156,13 @@ export function classCallbackValue( }; } +/** Notes a checked conversion of a replaceable method's result for the + * key-order witness: the result is a value the method or its replacement + * returns, not a record built from a dynamic value. */ +function methodResult(lowerer: Lowerer, checked: IrExpr): IrExpr { + return checked.kind === "dynCheck" ? lowerer.keyOrder.methodResult(checked) : checked; +} + export function classCallbackCall( lowerer: Lowerer, receiver: IrExpr, @@ -197,7 +204,7 @@ export function classCallbackCall( // generator arm does not box) keeps its native type: the unchanged // method's result stays as it is, and a replacement's dynamic result // takes the checked conversion into that type. - const converted = lowerer.coerceToExpected(call, fallback.type); + const converted = methodResult(lowerer, lowerer.coerceToExpected(call, fallback.type)); if (!typeEquals(converted.type, fallback.type)) { lowerer.pushDiag( unsupportedDiag( @@ -234,7 +241,9 @@ export function classCallbackCall( loc, }; answer = - fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type); + fallback.type.kind === "void" + ? result + : methodResult(lowerer, lowerer.coerceToExpected(result, fallback.type)); } // Read after observedSlot built the prototype data and its originals. const originalId = observed ? original?.originalId() : undefined; diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts index c85902c96..dc9149119 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -700,3 +700,121 @@ test("a spread reads its source's order right after the literal evaluates the so const snapshot = (prologue[2] as IrStmt & { kind: "varDecl" }).localId; expect(orderIndexOf(lit)).toMatchObject({ kind: "varRef", localId: snapshot }); }); + +/** `{ a, b }` with a declared table `["a", "b"]` and a literal `{ b, a }` + * plus `{ ...value, a }` of the same shape, beside `extra`. */ +function selfCopy(extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt[]): string[] { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false }; + f.run([ + decl("v", f.literal(ab, ["b", "a"])), + observe(f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }])), + ...extra(f, ab), + ]); + return f.order(ab)!; +} + +test("a literal that starts by spreading its own shape takes the other literals' order", () => { + expect(selfCopy(() => [])).toEqual(["b", "a"]); +}); + +test("a self copy of a shape a checked conversion builds cannot be ordered statically", () => { + const parsed = (ab: IrType): IrExpr => ({ + kind: "dynCheck", + value: ref("j", { kind: "dyn" }), + type: ab, + loc, + }); + expect(selfCopy((_, ab) => [observe(parsed(ab))])).toEqual(["a", "b"]); +}); + +test("a replaceable method's checked result builds no record", () => { + expect( + selfCopy((f, ab) => { + const result: IrExpr = { kind: "dynCheck", value: ref("r", { kind: "dyn" }), type: ab, loc }; + return [observe(f.notes.methodResult(result))]; + }), + ).toEqual(["b", "a"]); +}); + +test("a replaceable method's result keeps its witness through an IR rewrite", () => { + expect( + selfCopy((f, ab) => { + const result = f.notes.methodResult({ + kind: "dynCheck", + value: ref("r", { kind: "dyn" }), + type: ab, + loc, + }); + const identity = { expr: (e: IrExpr): IrExpr => e, stmt: (st: IrStmt): IrStmt => st }; + observeRecordLiteralRebuilds((from, to) => f.notes.follow(from, to)); + try { + const rebuilt = transformExpr(result, identity); + expect(rebuilt).not.toBe(result); + return [observe(rebuilt)]; + } finally { + observeRecordLiteralRebuilds(null); + } + }), + ).toEqual(["b", "a"]); +}); + +test("a checked conversion of a boxed primitive builds no record", () => { + expect( + selfCopy((_, ab) => { + const missing: IrExpr = { + kind: "dynFrom", + value: { kind: "unitLit", unit: "undefined", type: { kind: "undefinedT" }, loc }, + type: { kind: "dyn" }, + loc, + }; + return [observe({ kind: "dynCheck", value: missing, type: ab, loc })]; + }), + ).toEqual(["b", "a"]); +}); + +test("a checked conversion into a class builds none of its fields' records", () => { + expect( + selfCopy((f, ab) => { + (f.lowerer.classes as Map).set("Holder", { + def: { fields: [{ name: "value", type: ab }] }, + }); + const holder: IrType = { kind: "object", className: "Holder" }; + const checked: IrExpr = { + kind: "dynCheck", + value: ref("h", { kind: "dyn" }), + type: holder, + loc, + }; + return [{ kind: "exprStmt", expr: checked, loc }]; + }), + ).toEqual(["b", "a"]); +}); + +test("a checked function passes its record parameters out, and its callbacks' back in", () => { + const callable = (params: IrType[]): IrType => ({ + kind: "func", + params, + ret: { kind: "string" }, + }); + const checkedAs = (type: IrType): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "dynCheck", value: ref("fn", { kind: "dyn" }), type, loc }, + loc, + }); + expect(selfCopy((_, ab) => [checkedAs(callable([ab]))])).toEqual(["b", "a"]); + // A callback parameter receives values the JavaScript function builds. + expect(selfCopy((_, ab) => [checkedAs(callable([callable([ab])]))])).toEqual(["a", "b"]); + // So does a container parameter, which the function can write into. + expect(selfCopy((_, ab) => [checkedAs(callable([{ kind: "array", elem: ab }]))])).toEqual([ + "a", + "b", + ]); +}); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts index aa84c68c2..2f573e676 100644 --- a/packages/compiler/src/frontend/lowering/key-order-witness.ts +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -28,7 +28,19 @@ * build keeps today's table, since another table would make values * parsed in today's order keep a source view; a shape object literals * also build takes the literals' order. Their order is unknown, so a - * copy out of such a shape cannot be ordered statically; + * copy out of such a shape cannot be ordered statically. A conversion + * builds only the records it can create from a JavaScript value: none + * for a boxed primitive or a class instance (a native instance whose + * fields its own writes fill), none for a checked function's record + * parameters, and none for the checked result of a typed call of a + * method the program may replace on its instance. Without a replacement + * that result is the method's own native value, boxed and checked back + * unchanged; a replacement's result keeps its own order in its record + * source, and only copies out of it follow the chosen table, as they + * followed today's; + * - a literal that starts by spreading a value of its own shape copies + * that value's order. While no checked conversion builds the shape, + * every value has the one chosen order, so the copy adds no witness; * - a write to an optional key that may be absent appends it in Node. A * key no construction creates ("late-only") goes after every key a * construction can create; any other moving write goes after every key. @@ -112,6 +124,7 @@ export type KeyOrderSegment = export class KeyOrderNotes { private readonly literals = new Map(); private readonly copies = new Map(); + private readonly methodResults = new Set(); /** Shapes a frontend order surface (keys, values/entries, inspect, * index-signature merges) walks. */ readonly observed = new Set(); @@ -128,13 +141,21 @@ export class KeyOrderNotes { return node; } + /** The checked result of a typed call of a method the program may + * replace on its instance (class-callbacks.ts classCallbackCall). */ + methodResult(node: IrExpr): IrExpr { + this.methodResults.add(node); + return node; + } + /** A pass rebuilt `from` as `to` (traverse.ts mapExprChildren): the - * rebuilt literal is the same construction, so it keeps the witness. */ + * rebuilt node is the same construction, so it keeps the witness. */ follow(from: IrExpr, to: IrExpr): void { const segments = this.literals.get(from); if (segments !== undefined) this.literals.set(to, segments); const source = this.copies.get(from); if (source !== undefined) this.copies.set(to, source); + if (this.methodResults.has(from)) this.methodResults.add(to); } observe(type: IrType): void { @@ -148,6 +169,10 @@ export class KeyOrderNotes { copyOf(node: IrExpr): string | undefined { return this.copies.get(node); } + + isMethodResult(node: IrExpr): boolean { + return this.methodResults.has(node); + } } const isArrayIndex = (name: string): boolean => { @@ -201,6 +226,23 @@ function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean { ); } +const PRIMITIVE_KINDS = new Set([ + "f64", + "bigint", + "string", + "bool", + "undefinedT", + "nullT", + "symbol", +]); + +/** A checked conversion of a boxed primitive (the `undefined` a dynamic + * construction passes for a missing argument) answers that primitive or + * throws: it builds no record. */ +function primitiveSource(node: IrExpr & { kind: "dynCheck" }): boolean { + return node.value.kind === "dynFrom" && PRIMITIVE_KINDS.has(node.value.value.type.kind); +} + /** Chooses each in-scope record shape's enumeration table from its * witnesses. Runs once retained reachability has settled the historical * tables and derived metadata, before the order-snapshotting helper bodies @@ -239,35 +281,53 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio /** Every record shape a type can hold, through fields, arms, elements, * callables, promises and class fields. */ /** Each walk shares one `seen` set per result set, so every type graph - * is walked once however many roots reach it. */ - const shapesIn = (type: IrType, out: Set, seen: Set): void => { - const visit = (t: IrType): boolean => { + * is walked once however many roots reach it. + * + * `produced` walks only what a checked conversion into the type can + * build. It answers an existing native instance for a class type (a plain + * object cannot acquire a class brand; only %Error rebuilds), so the + * records the instance's fields hold come from the class's own writes and + * its checked field setters, each a witness of its own. A checked + * function's parameters receive native values: a record parameter is + * passed out, never built, though a callback among them receives values + * built from JavaScript again. */ + const shapesIn = (type: IrType, out: Set, seen: Set, produced = false): void => { + const visit = (t: IrType, built: boolean): boolean => { + const mark = built ? "" : "-"; if (t.kind === "record") { - if (seen.has("r:" + t.shapeId)) return true; - seen.add("r:" + t.shapeId); - out.add(t.shapeId); + if (seen.has(mark + "r:" + t.shapeId)) return true; + seen.add(mark + "r:" + t.shapeId); + if (built) out.add(t.shapeId); const shape = shapes.get(t.shapeId); if (shape) { - for (const f of shape.fields) visit(f.type); - if (shape.indexValue) visit(shape.indexValue); + // A record passed out can be written through: its fields take + // values JavaScript builds. + for (const f of shape.fields) visit(f.type, true); + if (shape.indexValue) visit(shape.indexValue, true); } return true; } if (t.kind === "union") { - if (seen.has("u:" + t.unionId)) return true; - seen.add("u:" + t.unionId); - for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm); + if (seen.has(mark + "u:" + t.unionId)) return true; + seen.add(mark + "u:" + t.unionId); + for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm, built); return true; } if (t.kind === "object") { + if (produced && t.className !== "%Error") return true; if (seen.has("c:" + t.className)) return true; seen.add("c:" + t.className); - for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type); + for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type, true); return true; } - return everyTypeChild(t, visit); + if (produced && t.kind === "func") { + for (const param of t.params) visit(param, !built); + return visit(t.ret, built); + } + // Containers passed out stay writable: their elements count as built. + return everyTypeChild(t, (child) => visit(child, true)); }; - visit(type); + visit(type, true); }; const opaqueShapes = new Set(); const opaqueSeen = new Set(); @@ -280,7 +340,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const declaredSeen = new Set(); const producedBy = (type: IrType, opaque: boolean): void => { if (opaque) shapesIn(type, opaqueShapes, opaqueSeen); - else shapesIn(type, runtimeShapes, runtimeSeen); + else shapesIn(type, runtimeShapes, runtimeSeen, true); }; // ── collection ────────────────────────────────────────────────────── @@ -388,7 +448,12 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio return; case "dynCheck": if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen); - else if (!orderPreservingClone(e)) producedBy(e.type, false); + // Conversions that build no record: a boxed primitive, and a + // replaceable method's result, which is the value the method + // returns (boxed and checked back unchanged) or what a replacement + // installed on the instance returns. + else if (!orderPreservingClone(e) && !primitiveSource(e) && !notes.isMethodResult(e)) + producedBy(e.type, false); return; case "dynDestrCheck": case "caughtCheck": @@ -649,6 +714,29 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio for (const c of f.copies) deps.add(c.source); return [...deps]; }; + /** `{ ...value, ...rest }` that starts with a spread of a value of the + * literal's own shape holding every key: it creates its keys in the + * value's order, and the later segments only overwrite them. With no + * checked conversion building the shape, every value enumerates in the + * one table this pass chooses, so the copy agrees with any choice and + * adds no edge. */ + const copiesItself = ( + id: string, + f: ShapeFacts, + table: readonly string[], + segments: readonly KeyOrderSegment[], + ): boolean => { + const first = segments[0]; + return ( + first !== undefined && + first.kind === "spread" && + first.shapeId === id && + !first.maybeAbsent && + !f.runtime && + segments.every((s) => s.kind !== "opaque") && + table.every((k) => !maybeAbsent(id, k)) + ); + }; /** One shape's order given its sources' current orders. */ const resolveOne = (id: string, current: (source: string) => Resolved): Resolved => { const shape = shapes.get(id); @@ -666,6 +754,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio const edges: [string, string][] = []; if (f.pinned) chain(table, edges); for (const segments of f.literals) { + if (copiesItself(id, f, table, segments)) continue; const sequences = literalSequences(segments, keySet, current); if (sequences === null) return "unknown"; for (const seq of sequences) chain(seq, edges); diff --git a/packages/compiler/src/ir/traverse.ts b/packages/compiler/src/ir/traverse.ts index 251904351..80425a056 100644 --- a/packages/compiler/src/ir/traverse.ts +++ b/packages/compiler/src/ir/traverse.ts @@ -473,9 +473,10 @@ export function everyStmtChild( throw new Error("unhandled IR node"); } -/** Called with each record literal mapExprChildren rebuilds and its - * replacement, so a side table keyed by node identity (the lowering's - * key-order witness notes) follows the literal into the rewritten tree. */ +/** Called with each record literal and checked conversion mapExprChildren + * rebuilds and its replacement, so a side table keyed by node identity (the + * lowering's key-order witness notes) follows the node into the rewritten + * tree. */ let recordLiteralRebuilt: ((from: IrExpr, to: IrExpr) => void) | null = null; /** Installs (or with null removes) the record-literal rebuild observer. */ @@ -504,8 +505,11 @@ export function mapExprChildren( return node; case "libCall": return { ...node, args: mapOf(node.args, expr) }; - case "dynCheck": - return { ...node, value: expr(node.value) }; + case "dynCheck": { + const rebuilt: IrExpr = { ...node, value: expr(node.value) }; + if (recordLiteralRebuilt !== null) recordLiteralRebuilt(node, rebuilt); + return rebuilt; + } case "strEq": return { ...node, left: expr(node.left), right: expr(node.right) }; case "fieldGet": diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json new file mode 100644 index 000000000..006d4a8c7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7736-key-order-replaceable-method-result/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7736-key-order-replaceable-method-result/registry.js", + "/tests/corpus/7736-key-order-replaceable-method-result/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json new file mode 100644 index 000000000..97f2ca7bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7737-key-order-self-spread.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7737-key-order-self-spread.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7736-key-order-replaceable-method-result/main.ts b/tests/corpus/7736-key-order-replaceable-method-result/main.ts new file mode 100644 index 000000000..5ebbfa761 --- /dev/null +++ b/tests/corpus/7736-key-order-replaceable-method-result/main.ts @@ -0,0 +1,106 @@ +import { Registry, describe, titleIndex } from "./registry.js"; + +// Records keep the key order their object literals write when values of +// the same type also pass through checked conversions that build none of +// them: +// - a typed method the program may replace on its instance (the +// computed-key write in registry.js makes every method name replaceable) +// returns the records it stores, checked back from the guarded call; +// - an index spec from JavaScript, checked into a type whose function +// field takes a Book, receives Books but never builds one. +// JSON, Object.keys and spreads of those records follow insertion order, +// not the interface's declared order. +interface Book { + id: string; + title: string; + author: string; + createdAt: string; + updatedAt: string; +} + +type Draft = { title: string; author: string }; + +interface IndexSpec { + name: string; + key: (row: T) => string; +} + +class Repository { + readonly #rows = new Map(); + + insert(row: T): T { + this.#rows.set(row.id, row); + return row; + } + + require(id: string): T { + const row = this.#rows.get(id); + if (row === undefined) throw new Error(`missing ${id}`); + return row; + } + + find(id: string): T | undefined { + return this.#rows.get(id); + } + + all(): T[] { + return [...this.#rows.values()]; + } +} + +class Service { + readonly #books = new Repository(); + readonly #index: IndexSpec = titleIndex; + #clock = 0; + + #now(): string { + this.#clock++; + return `t${this.#clock}`; + } + + create(draft: Draft, id: string): Book { + const at = this.#now(); + return this.#books.insert({ ...draft, id, createdAt: at, updatedAt: at }); + } + + update(id: string, patch: Partial): Book { + const current = this.#books.require(id); + return this.#books.insert({ ...current, ...patch, updatedAt: this.#now() }); + } + + get(id: string): Book { + return this.#books.require(id); + } + + maybe(id: string): Book | undefined { + return this.#books.find(id); + } + + list(): Book[] { + return this.#books.all(); + } + + indexed(): string[] { + return this.list().map((book) => `${this.#index.name}=${this.#index.key(book)}`); + } +} + +const registry = new Registry(); +console.log(describe(registry)); + +const service = new Service(); +console.log(JSON.stringify(service.create({ title: "Dune", author: "Herbert" }, "b1"))); +console.log(JSON.stringify(service.create({ title: "Earthsea", author: "Le Guin" }, "b2"))); +console.log(JSON.stringify(service.update("b1", { title: "Dune Messiah" }))); +console.log(Object.keys(service.get("b2")).join(",")); +const found = service.maybe("b1"); +if (found !== undefined) console.log(JSON.stringify(found)); +console.log(JSON.stringify(service.list())); +const copy = { ...service.get("b1") }; +console.log(Object.keys(copy).join(",")); +console.log(service.indexed().join(" ")); +try { + service.get("missing"); +} catch (error) { + console.log((error as Error).message); +} diff --git a/tests/corpus/7736-key-order-replaceable-method-result/registry.js b/tests/corpus/7736-key-order-replaceable-method-result/registry.js new file mode 100644 index 000000000..d6da1edc5 --- /dev/null +++ b/tests/corpus/7736-key-order-replaceable-method-result/registry.js @@ -0,0 +1,21 @@ +// Installs one handler per method name with a computed-key write, the way +// router classes attach `get`, `post`, ... in their constructor. The +// compiler cannot tell which names such a write replaces. +const METHODS = ["get", "post"]; + +export class Registry { + constructor() { + METHODS.forEach((method) => { + this[method] = (...args) => `${method}:${args.length}`; + }); + } +} + +export function describe(registry) { + return `${registry.get(1, 2)} ${registry.post()}`; +} + +export const titleIndex = { + name: "title", + key: (row) => row.title.toLowerCase(), +}; diff --git a/tests/corpus/7737-key-order-self-spread.ts b/tests/corpus/7737-key-order-self-spread.ts new file mode 100644 index 000000000..5577f7531 --- /dev/null +++ b/tests/corpus/7737-key-order-self-spread.ts @@ -0,0 +1,34 @@ +// A literal that starts by spreading a value of its own type copies that +// value's keys in their order, then overwrites some of them. It must not +// stop the type from taking the order its other literals write: the +// records below enumerate as created (title before id), not in the +// interface's declared order. +interface Book { + id: string; + title: string; + tags: string[]; + updatedAt: string; + version: number; +} + +type Draft = { title: string; tags: string[] }; + +function create(draft: Draft, id: string): Book { + return { ...draft, id, updatedAt: "t1", version: 1 }; +} + +function update(current: Book, patch: Partial): Book { + return { ...current, ...patch, updatedAt: "t2", version: current.version + 1 }; +} + +function touch(current: Book): Book { + return { ...current, version: current.version + 1 }; +} + +const book = create({ title: "Dune", tags: ["sf"] }, "b1"); +console.log(JSON.stringify(book)); +const updated = update(book, { title: "Dune Messiah" }); +console.log(JSON.stringify(updated)); +console.log(Object.keys(touch(updated)).join(",")); +console.log(Object.keys(update(updated, { tags: [] })).join(" ")); +for (const key in touch(book)) console.log(key); diff --git a/tests/harness/corpus-costs/7736-key-order-replaceable-method-result/main.ts.json b/tests/harness/corpus-costs/7736-key-order-replaceable-method-result/main.ts.json new file mode 100644 index 000000000..6a79d2c04 --- /dev/null +++ b/tests/harness/corpus-costs/7736-key-order-replaceable-method-result/main.ts.json @@ -0,0 +1 @@ +{ "plain": 10900, "san": 17500 } diff --git a/tests/harness/corpus-costs/7737-key-order-self-spread.ts.json b/tests/harness/corpus-costs/7737-key-order-self-spread.ts.json new file mode 100644 index 000000000..de3f40849 --- /dev/null +++ b/tests/harness/corpus-costs/7737-key-order-self-spread.ts.json @@ -0,0 +1 @@ +{ "plain": 3800, "san": 6100 } From 72736211b1b36712c305443ff8d61040a366d963 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 01:55:31 -0700 Subject: [PATCH 075/409] Assign private fields through untyped receivers with logical assignment `other.#x ??= value` (and `||=`, `&&=`) on a receiver JavaScript leaves untyped read and wrote an ordinary property named "#x" instead of the brand-checked private field, so the field kept its old value. Hono's RegExpRouter numbers its parameter nodes with `next.#varIndex ??= context.varIndex++`; without the stored index its regular expression lost every parameter capture, and `c.req.param("id")` answered undefined. Co-Authored-By: Claude Opus 5.5 --- .../expressions/nullish-assignment.ts | 4 +- ...private-logical-assignment-untyped.js.json | 6 ++ ...7738-private-logical-assignment-untyped.js | 56 +++++++++++++++++++ ...private-logical-assignment-untyped.js.json | 1 + 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json create mode 100644 tests/corpus/7738-private-logical-assignment-untyped.js create mode 100644 tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json diff --git a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts index 81b3b9d69..9664f591d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts +++ b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts @@ -100,7 +100,9 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp write = (value) => eventAccessorWrite(receiver, name, value, lowerer, loc); } else if (ts.isPropertyAccessExpression(target)) { const probed = tryLowerExpression(lowerer, target.expression); - if (probed?.type.kind === "dyn") { + // `value.#x ??= v` on a checked-dynamic value names the brand-checked + // field (fieldTarget), never a property called "#x". + if (probed?.type.kind === "dyn" && !ts.isPrivateIdentifier(target.name)) { const receiver = save(probed, "%shortCircuitReceiver"); const key: IrExpr = { kind: "strLit", value: target.name.text, type: STRING, loc }; storage = DYN; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json new file mode 100644 index 000000000..4865d3206 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7738-private-logical-assignment-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7738-private-logical-assignment-untyped.js" + ], + "diags": [] +} diff --git a/tests/corpus/7738-private-logical-assignment-untyped.js b/tests/corpus/7738-private-logical-assignment-untyped.js new file mode 100644 index 000000000..b0ceaea86 --- /dev/null +++ b/tests/corpus/7738-private-logical-assignment-untyped.js @@ -0,0 +1,56 @@ +// Logical assignments to a private field of another instance whose type +// JavaScript leaves open, the way a router trie numbers its parameter +// nodes on first use (`next.#varIndex ??= context.varIndex++`). They read +// and write the brand-checked field, and evaluate the right side only +// when they store it. +class Node { + #varIndex; + #label = ""; + #count = 0; + #children = Object.create(null); + + child(key, context) { + let next = this.#children[key]; + if (!next) next = this.#children[key] = new Node(); + next.#varIndex ??= context.varIndex++; + next.#label ||= `node:${key}`; + next.#count &&= next.#count + 1; + next.#count ||= 1; + return next; + } + + describe() { + const kind = typeof this.#varIndex === "number" ? "param" : "static"; + return `${kind} ${this.#varIndex} ${this.#label} ${this.#count}`; + } + + static reset(node, other) { + other.#label &&= ""; + other.#varIndex ??= -1; + return [node.#label, other.#label, other.#varIndex]; + } + + static peek(value) { + return value.#varIndex ??= 99; + } +} + +const context = { varIndex: 0 }; +const root = new Node(); +const a = root.child("a", context); +const b = root.child("b", context); +const again = root.child("a", context); +console.log(a === again, context.varIndex); +console.log(root.describe()); +console.log(a.describe()); +console.log(b.describe()); +console.log(JSON.stringify(Node.reset(root, a))); +console.log(a.describe()); + +for (const value of [{}, null]) { + try { + Node.peek(value); + } catch (error) { + console.log(error instanceof TypeError, error.message); + } +} diff --git a/tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json b/tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json new file mode 100644 index 000000000..d61b17a5d --- /dev/null +++ b/tests/harness/corpus-costs/7738-private-logical-assignment-untyped.js.json @@ -0,0 +1 @@ +{ "plain": 5500, "san": 8800 } From d1f3eda7326b81ca84b9fcdb6a29f574aa14f54a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 04:50:06 -0500 Subject: [PATCH 076/409] Freeze arrays, Maps and Sets that other references still hold Object.freeze of an array that another reference may still hold stopped at compile time with SC2020, and freezing an array, Map or Set held in an untyped value threw SC2020 at run time. Node freezes the object itself: a frozen array refuses every later write through any reference with the TypeError of the operation's first write, and a frozen Map or Set only stops being extensible, since its entries are not properties. - An array a program may freeze keeps a frozen bit in its storage flags. An array type is freezable when a typed Object.freeze names it, or, in a program that freezes untyped values, when a value of it can be held as a live untyped reference. The writes of those types test the bit with the guard published arrays already use; other array types are unchanged. - The guard's errors follow Node for every operation: splice, fill, copyWithin, sort and reverse throw at their first write, and do not throw when they would write nothing. Published arrays get the same errors, and a statement splice of a published array is now guarded. - A Map or Set records that it is not extensible, for Object.freeze, seal and preventExtensions, typed or untyped. Object.isFrozen, isSealed and isExtensible report it, and its entries stay writable. - Writes to a frozen array through an untyped value are refused with the same errors, as are writes to frozen checked arrays and objects, which now name the property and the object as Node does. - Programs that neither freeze nor publish arrays compile to the same IR and LLVM. Corpus 7406 covers typed and untyped freezes of arrays, Maps and Sets, every array write on frozen arrays of zero, one and three elements, comparator sorts, nested arrays, and frozen arrays that are released, and matches Node. Refs #1374 Co-authored-by: blurname <50226652+blurname@users.noreply.github.com> --- packages/compiler/src/backend/llvm/common.ts | 3 + packages/compiler/src/backend/llvm/emitter.ts | 63 +++++++- .../src/backend/llvm/expr-containers.ts | 39 ++++- .../compiler/src/backend/llvm/lib-dispatch.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 4 + .../compiler/src/backend/publication.test.ts | 31 ++++ packages/compiler/src/backend/publication.ts | 27 +++- .../src/frontend/lowering/frozen-records.ts | 62 +++++--- .../src/frontend/lowering/lower-calls.ts | 57 ++++++- .../compiler/src/frontend/lowering/lowerer.ts | 9 +- .../compiler/src/ir/builtin-signatures.ts | 10 ++ .../src/ir/function-references.test.ts | 1 + packages/compiler/src/ir/ir.ts | 4 + .../corpus/7406-frozen-containers.ts.json | 6 + packages/runtime/src/scr_array.c | 122 +++++++++++++++ packages/runtime/src/scr_array.h | 22 ++- packages/runtime/src/scr_dyn_invoke.c | 35 +++-- packages/runtime/src/scr_json.c | 142 +++++++++++++----- packages/runtime/src/scr_map.c | 6 + packages/runtime/src/scr_map.h | 8 + packages/runtime/src/scr_publish.c | 58 +------ packages/runtime/src/scr_rc.h | 8 +- tests/corpus/7406-frozen-containers.ts | 132 ++++++++++++++++ .../7406-frozen-containers.ts.json | 1 + 24 files changed, 690 insertions(+), 161 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7406-frozen-containers.ts.json create mode 100644 tests/corpus/7406-frozen-containers.ts create mode 100644 tests/harness/corpus-costs/7406-frozen-containers.ts.json diff --git a/packages/compiler/src/backend/llvm/common.ts b/packages/compiler/src/backend/llvm/common.ts index ac4e28708..cca1b603b 100644 --- a/packages/compiler/src/backend/llvm/common.ts +++ b/packages/compiler/src/backend/llvm/common.ts @@ -29,6 +29,9 @@ export const SCR_ARR_FIELD = { * (SCR_ARR_STATES in scr_array.h). */ export const SCR_ARR_STATES = 16; +/** The `spare` flag of a frozen %ScrArr (SCR_ARR_FROZEN in scr_array.h). */ +export const SCR_ARR_FROZEN = 32; + /** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a * size-type count. */ export function loadArrCount( diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index c0cc9fef2..a66e00736 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -184,7 +184,7 @@ import { } from "./string-slices.js"; import { LazyCaptures } from "./lazy-captures.js"; import { StackCaptures } from "./stack-captures.js"; -import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD } from "./common.js"; +import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD, SCR_ARR_FROZEN } from "./common.js"; import { ffiExtendsNarrowIntegers } from "../targets.js"; import { emitLiteralExpr, @@ -6264,6 +6264,41 @@ export class LlEmitter { return ok; } + /** Branches on "the array `arr` is frozen" (SCR_ARR_FROZEN in its storage + * flags, scr_array.h), or published too when `published`; like + * emitPublishedTest, leaves the builder in the cold block and answers the + * continuation label. */ + private emitFrozenArrayTest(arr: string, published: boolean): string { + const B = this.B; + const sparep = B.tmp(), + spare = B.tmp(), + bit = B.tmp(); + let frozen = B.tmp(); + B.line( + `${sparep} = getelementptr inbounds %ScrArr, ptr ${arr}, i32 0, i32 ${SCR_ARR_FIELD.spare}`, + ); + B.line(`${spare} = load i16, ptr ${sparep}`); + B.line(`${bit} = and i16 ${spare}, ${SCR_ARR_FROZEN}`); + B.line(`${frozen} = icmp ne i16 ${bit}, 0`); + if (published) { + const rc = B.tmp(), + immortal = B.tmp(), + either = B.tmp(); + B.line(`${rc} = load ${this.sizeType}, ptr ${arr}`); + B.line(`${immortal} = icmp eq ${this.sizeType} ${rc}, -1`); + B.line(`${either} = or i1 ${frozen}, ${immortal}`); + frozen = either; + } + this.declare(`declare i1 @llvm.expect.i1(i1, i1)`); + const expected = B.tmp(); + B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${frozen}, i1 false)`); + const cold = B.newLabel("arr.frozen"), + ok = B.newLabel("arr.ok"); + B.condBr(expected, cold, ok); + B.startBlock(cold); + return ok; + } + private publishedNamesSym: string | null = null; /** The class-name table the hierarchy field guards index by preorder. */ @@ -6328,15 +6363,29 @@ export class LlEmitter { op: number, index = f64Lit(0), count = f64Lit(0), + third: string | null = null, ): void { - if (this.publishedTypes?.containerGuarded(arrType) !== true) return; + const published = this.publishedTypes; + if (published?.containerGuarded(arrType) !== true) return; const B = this.B; - const ok = this.emitPublishedTest(arr); - this.declare(`declare zeroext i1 @scr_throw_published_array(i32, ptr, double, double)`); + const guards = published.arrayGuards(arrType); + const ok = guards.frozen + ? this.emitFrozenArrayTest(arr, guards.published) + : this.emitPublishedTest(arr); const threw = B.tmp(); - B.line( - `${threw} = call zeroext i1 @scr_throw_published_array(i32 ${op}, ptr ${arr}, double ${index}, double ${count})`, - ); + if (third === null) { + this.declare(`declare zeroext i1 @scr_throw_published_array(i32, ptr, double, double)`); + B.line( + `${threw} = call zeroext i1 @scr_throw_published_array(i32 ${op}, ptr ${arr}, double ${index}, double ${count})`, + ); + } else { + this.declare( + `declare zeroext i1 @scr_throw_published_array_range(i32, ptr, double, double, double)`, + ); + B.line( + `${threw} = call zeroext i1 @scr_throw_published_array_range(i32 ${op}, ptr ${arr}, double ${index}, double ${count}, double ${third})`, + ); + } const unwind = B.newLabel("pub.throw"); B.condBr(threw, unwind, ok); B.startBlock(unwind); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 1ba5da818..93cc615a4 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -257,6 +257,16 @@ function emitDiscarded(host: LlvmEmitterContext, e: IrExpr): void { const start = host.emitExpr(e.args[0]!); const count = e.args[1] ? host.emitExpr(e.args[1]).name : F64_INF; const values = items.map((item) => host.emitExpr(item)); + // Before the values move into the buffer: a throw releases them with the + // frame. + host.emitPublishedArrayGuard( + r.name, + e.receiver.type, + 10, + start.name, + count, + f64Lit(values.length), + ); if (acc === "ref") values.forEach((value) => host.toHeap(value)); let buffer = "null"; if (values.length > 0) { @@ -978,7 +988,25 @@ export function emitArrIntrinsic( case "fill": case "fillUndefined": { const args = e.args.map((arg) => host.emitExpr(arg)); - host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + // fill's range is its last two arguments; copyWithin's target, + // start and end are its three. + if (e.method === "copyWithin") + host.emitPublishedArrayGuard( + r.name, + e.receiver.type, + 9, + args[0]!.name, + args[1]!.name, + args[2]!.name, + ); + else + host.emitPublishedArrayGuard( + r.name, + e.receiver.type, + 8, + args[args.length - 2]!.name, + args[args.length - 1]!.name, + ); if (host.checkedFills.has(e)) { // A new int32 array with checked stores, filled with a value that // may not be an int32: widen it first then. @@ -1042,7 +1070,7 @@ export function emitArrIntrinsic( // removes to the end (+Infinity, the slice convention). const start = host.emitExpr(e.args[0]!); const cnt = e.args[1] ? host.emitExpr(e.args[1]).name : F64_INF; - host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + host.emitPublishedArrayGuard(r.name, e.receiver.type, 10, start.name, cnt, f64Lit(0)); host.declare(`declare ptr @scr_arr_splice(ptr, double, double)`); const t = B.tmp(); B.line(`${t} = call ptr @scr_arr_splice(ptr ${r.name}, double ${start.name}, double ${cnt})`); @@ -1052,7 +1080,12 @@ export function emitArrIntrinsic( const start = host.emitExpr(e.args[0]!); const count = host.emitExpr(e.args[1]!); const items = emitBorrowedInput(host, e.args[2]!); - host.emitPublishedArrayGuard(r.name, e.receiver.type, 7); + if (host.publishedTypes?.containerGuarded(e.receiver.type) === true) { + host.declare(`declare double @scr_arr_len(ptr)`); + const n = B.tmp(); + B.line(`${n} = call double @scr_arr_len(ptr ${items.name})`); + host.emitPublishedArrayGuard(r.name, e.receiver.type, 10, start.name, count.name, n); + } host.declare(`declare ptr @scr_arr_splice_insert(ptr, double, double, ptr)`); const t = B.tmp(); B.line( diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 4f1a4fef8..cc8953fc4 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -891,6 +891,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "blob": case "wasm": case "array": + case "collection": case "record": case "disposable": case "punycode": diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 6fbad7e11..f8e59e7b7 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1016,6 +1016,10 @@ export const LIB_FN_SYMS: Record = { "class.throwNotExtensible": "scr_class_throw_not_extensible", "record.freeze": "scr_record_freeze", "record.throwFrozen": "scr_record_throw_frozen", + "array.freeze": "scr_arr_freeze", + "array.isFrozen": "scr_arr_is_frozen", + "collection.preventExtensions": "scr_map_prevent_extensions", + "collection.isExtensible": "scr_map_is_extensible", "dyn.nativeSetNew": "scr_dyn_native_set_new", "dyn.nativeSetIs": "scr_dyn_native_set_is", "dyn.nativeMapIs": "scr_dyn_native_map_is", diff --git a/packages/compiler/src/backend/publication.test.ts b/packages/compiler/src/backend/publication.test.ts index 149ea4bb9..7b3e5f5cc 100644 --- a/packages/compiler/src/backend/publication.test.ts +++ b/packages/compiler/src/backend/publication.test.ts @@ -85,3 +85,34 @@ test("published types close over fields, containers and whole hierarchies", () = computePublishedTypes({ ...withStores, functions: withStores.functions.slice(1) }), ).toBeNull(); }); + +test("freezable array types carry the array write guard without a publish", () => { + const numbers = arrayOf(F64); + const push: IrStmt = { + kind: "exprStmt", + expr: { + kind: "arrIntrinsic", + method: "push", + receiver: ref("a", numbers), + args: [{ kind: "numLit", value: 1, type: F64, loc }], + type: F64, + loc, + }, + loc, + }; + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [fn("main", []), fn("pushNumber", [push])], + freezableArrays: ["array"], + }; + const guarded = computePublishedTypes(mod)!; + expect(guarded.containerGuarded(numbers)).toBe(true); + expect(guarded.arrayGuards(numbers)).toEqual({ published: false, frozen: true }); + expect(guarded.containerGuarded(arrayOf(STRING))).toBe(false); + // A frozen array's push throws, so the function may throw. + expect(computeMayThrow(mod).fns.has("pushNumber")).toBe(true); + const { freezableArrays: _frozen, ...plain } = mod; + expect(computePublishedTypes(plain)).toBeNull(); +}); diff --git a/packages/compiler/src/backend/publication.ts b/packages/compiler/src/backend/publication.ts index af65fa39a..7f7918fb1 100644 --- a/packages/compiler/src/backend/publication.ts +++ b/packages/compiler/src/backend/publication.ts @@ -46,6 +46,10 @@ export class PublishedTypes { readonly records = new Set(); /** typeKeys of array, map and set types. */ readonly containers = new Set(); + /** typeKeys of the array types the program may freeze + * (IrModule.freezableArrays): a frozen array refuses writes like a + * published one, so their stores carry the same guard. */ + readonly frozenArrays = new Set(); /** Constructor stores into a `this` nothing else can reach yet * (immutability.ts): never a published target, so never guarded. */ privateStores: ReadonlySet = new Set(); @@ -64,10 +68,16 @@ export class PublishedTypes { } containerGuarded(t: IrType): boolean { - return ( - (t.kind === "array" || t.kind === "map" || t.kind === "set") && - this.containers.has(typeKey(t)) - ); + if (t.kind !== "array" && t.kind !== "map" && t.kind !== "set") return false; + const key = typeKey(t); + return this.containers.has(key) || this.frozenArrays.has(key); + } + + /** Whether an array of type `t` may be published, and whether it may be + * frozen: the two states its write guard tests. */ + arrayGuards(t: IrType): { published: boolean; frozen: boolean } { + const key = typeKey(t); + return { published: this.containers.has(key), frozen: this.frozenArrays.has(key) }; } } @@ -82,7 +92,13 @@ export function computePublishedTypes(mod: IrModule): PublishedTypes | null { }; for (const fn of mod.functions) everyStmtList(fn.body, { expr: visit, stmt: (_s: IrStmt) => true }); - if (roots.length === 0) return null; + const frozenArrays = mod.freezableArrays ?? []; + if (roots.length === 0 && frozenArrays.length === 0) return null; + if (roots.length === 0) { + const frozen = new PublishedTypes(); + for (const key of frozenArrays) frozen.frozenArrays.add(key); + return frozen; + } const classes = new Map((mod.classes ?? []).map((c) => [c.name, c])); const children = new Map(); @@ -95,6 +111,7 @@ export function computePublishedTypes(mod: IrModule): PublishedTypes | null { const records = new Map((mod.records ?? []).map((r) => [r.id, r])); const unions = new Map((mod.unions ?? []).map((u) => [u.id, u])); const out = new PublishedTypes(); + for (const key of frozenArrays) out.frozenArrays.add(key); out.privateStores = computeConstructionFacts(mod).privateStores; const seen = new Set(); const stack = [...roots]; diff --git a/packages/compiler/src/frontend/lowering/frozen-records.ts b/packages/compiler/src/frontend/lowering/frozen-records.ts index f61de39ec..30eb9e455 100644 --- a/packages/compiler/src/frontend/lowering/frozen-records.ts +++ b/packages/compiler/src/frontend/lowering/frozen-records.ts @@ -3,6 +3,7 @@ import { BOOL, VOID, shapeHasAccessorSlots, + typeKey, type IrExpr, type IrFunction, type IrStmt, @@ -15,24 +16,31 @@ import type { Lowerer } from "./lowerer.js"; import { recordHasOwnHelper } from "./lower-calls.js"; import { isStrictCode } from "./strict-mode.js"; -/* Object.freeze of records that other references can still write. Only the - * shapes a program may freeze pay for it: their values keep a frozen bit in - * the rc word (scr_rc.h SCR_RC_FROZEN), their release ignores the bit, and - * every write to them checks it first (guardFrozenRecordWrites). A shape is +/* Object.freeze of records and arrays that other references can still + * write. Only the types a program may freeze pay for it. A record keeps its + * frozen bit in the rc word (scr_rc.h SCR_RC_FROZEN), which its release + * ignores, and every write to it checks the bit first + * (guardFrozenWrites). An array keeps it in its storage flags + * (scr_array.h SCR_ARR_FROZEN), which the backend's array write guards test + * for the array types the module lists (IrModule.freezableArrays). A type is * freezable when a static Object.freeze names it, or, in a program that * freezes untyped values, when a value of it or of a type reaching it can be * held as a live untyped reference. */ type RecordType = Extract; type RecordWrite = Extract; +type FreezeFn = "record.freeze" | "array.freeze" | "collection.preventExtensions"; -/** Object.freeze of a record: sets its frozen bit and answers the record. */ -export function lowerRecordFreeze( +/** Object.freeze of a record or an array, or Object.freeze, seal or + * preventExtensions of a Map or Set: `fn` sets the value's bit, and the + * call answers the value. */ +export function lowerNativeFreeze( lowerer: Lowerer, + fn: FreezeFn, target: IrExpr, - type: RecordType, loc: SrcLoc, ): IrExpr { + const type = target.type; const slot = lowerer.declareHiddenLocal("%freezeTarget", type); const receiver = (): IrExpr => varRef(slot.id, type, loc); return { @@ -41,7 +49,7 @@ export function lowerRecordFreeze( { kind: "varDecl", localId: slot.id, init: target, loc }, { kind: "exprStmt", - expr: { kind: "libCall", fn: "record.freeze", args: [receiver()], type: VOID, loc }, + expr: { kind: "libCall", fn, args: [receiver()], type: VOID, loc }, loc, }, ], @@ -92,9 +100,14 @@ function holdsAbsentState(e: IrExpr): boolean { return e.kind === "ternary" && (holdsAbsentState(e.then) || holdsAbsentState(e.else_)); } -/** The shapes whose values the program may freeze. */ -function freezableShapes(lowerer: Lowerer, functions: IrFunction[]): Set { +/** The record shapes and array types (typeKeys) whose values the program + * may freeze. */ +function freezableTypes( + lowerer: Lowerer, + functions: IrFunction[], +): { shapes: Set; arrays: Set } { const shapes = new Set(); + const arrays = new Set(); const live: IrType[] = []; let dynFreeze = false; for (const fn of functions) @@ -103,13 +116,16 @@ function freezableShapes(lowerer: Lowerer, functions: IrFunction[]): Set if (e.kind === "libCall" && e.fn === "record.freeze") { const target = e.args[0]; if (target && target.type.kind === "record") shapes.add(target.type.shapeId); + } else if (e.kind === "libCall" && e.fn === "array.freeze") { + const target = e.args[0]; + if (target && target.type.kind === "array") arrays.add(typeKey(target.type)); } else if (e.kind === "libCall" && e.fn === "dyn.freeze") dynFreeze = true; else if (e.kind === "dynFrom") live.push(e.value.type); return true; }, stmt: () => true, }); - if (!dynFreeze) return shapes; + if (!dynFreeze) return { shapes, arrays }; // An untyped reference to a class instance, or to a record or array // boxed live, reaches the values its fields and elements hold as further // live references. Class instances also cross into untyped code through @@ -135,7 +151,14 @@ function freezableShapes(lowerer: Lowerer, functions: IrFunction[]): Set seen.add(`union:${type.unionId}`); for (const arm of lowerer.unions.get(type.unionId)?.arms ?? []) stack.push(arm); break; - case "array": + case "array": { + const key = typeKey(type); + if (seen.has(key)) break; + seen.add(key); + arrays.add(key); + stack.push(type.elem); + break; + } case "set": stack.push(type.elem); break; @@ -156,7 +179,7 @@ function freezableShapes(lowerer: Lowerer, functions: IrFunction[]): Set break; } } - return shapes; + return { shapes, arrays }; } /** The guarded form of a write to a freezable record: the receiver and the @@ -186,15 +209,18 @@ function guardedWrite( } /** Checks the frozen bit before every write to a record of a freezable - * shape. A write to a frozen record throws Node's TypeError in strict mode - * code; in sloppy-mode code it leaves the record unchanged. Deleting a key - * the record does not have succeeds either way. */ -export function guardFrozenRecordWrites(lowerer: Lowerer, functions: IrFunction[]): void { - const freezable = freezableShapes(lowerer, functions); + * shape, and lists the freezable array types, whose writes the backend + * guards (IrModule.freezableArrays). A write to a frozen record throws + * Node's TypeError in strict mode code; in sloppy-mode code it leaves the + * record unchanged. Deleting a key the record does not have succeeds either + * way. */ +export function guardFrozenWrites(lowerer: Lowerer, functions: IrFunction[]): void { + const { shapes: freezable, arrays } = freezableTypes(lowerer, functions); for (const id of freezable) { const shape = lowerer.shapes.get(id); if (shape) shape.freezable = true; } + for (const key of arrays) lowerer.freezableArrays.add(key); if (freezable.size === 0) return; const lifted = lowerer.liftedFns.length; for (const fn of functions) { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index e2ed5f9fe..819f90534 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -245,7 +245,7 @@ import { import { fenceSymbolFieldCopy } from "./symbol-fields.js"; import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js"; import { lowerClassFieldDefine, lowerClassIntegrity } from "./class-attributes.js"; -import { freezableRecordType, lowerRecordFreeze } from "./frozen-records.js"; +import { freezableRecordType, lowerNativeFreeze } from "./frozen-records.js"; import { classStaticDataFor } from "./class-static-data.js"; import { lowerUrlFactory } from "./lower-url.js"; import { nullishValueUnitOf } from "./binding-analysis.js"; @@ -8422,6 +8422,22 @@ function lowerObjectStaticCall( locOf(call), ); if (fnIntegrity) return fnIntegrity; + // A Map or Set has no own properties: freeze, seal and preventExtensions + // all only make it not extensible. + if (value.type.kind === "map" || value.type.kind === "set") { + if (member === "preventExtensions" || member === "seal") + return lowerNativeFreeze(lowerer, "collection.preventExtensions", value, locOf(call)); + const extensible: IrExpr = { + kind: "libCall", + fn: "collection.isExtensible", + args: [value], + type: BOOL, + loc: locOf(call), + }; + return member === "isExtensible" + ? extensible + : { kind: "unary", op: "!", operand: extensible, type: BOOL, loc: locOf(call) }; + } if (member === "preventExtensions" || member === "seal") { const integrity = lowerClassIntegrity(lowerer, call, member, value); if (integrity) return integrity; @@ -8461,15 +8477,36 @@ function lowerObjectStaticCall( // reference would need a runtime frozen bit (strict mode throws). if (member === "isFrozen" && call.arguments.nodes.length === 1) { const argNode = call.arguments.nodes[0]!; - if (tryLowerExpression(lowerer, argNode)?.type.kind === "record") { - const record = lowerer.lowerExpr(argNode); - if (record.type.kind === "record") + const probedKind = tryLowerExpression(lowerer, argNode)?.type.kind; + if ( + probedKind === "record" || + probedKind === "array" || + probedKind === "map" || + probedKind === "set" + ) { + const native = lowerer.lowerExpr(argNode); + const loc = locOf(call); + if (native.type.kind === "record" || native.type.kind === "array") return { kind: "libCall", - fn: "record.isFrozen", - args: [record], + fn: native.type.kind === "record" ? "record.isFrozen" : "array.isFrozen", + args: [native], type: BOOL, - loc: locOf(call), + loc, + }; + if (native.type.kind === "map" || native.type.kind === "set") + return { + kind: "unary", + op: "!", + operand: { + kind: "libCall", + fn: "collection.isExtensible", + args: [native], + type: BOOL, + loc, + }, + type: BOOL, + loc, }; } const value = lowerer.lowerExprExpecting(argNode, DYN); @@ -8523,7 +8560,11 @@ function lowerObjectStaticCall( const integrity = lowerClassIntegrity(lowerer, call, "freeze", value); if (integrity) return integrity; if (freezableRecordType(lowerer, value.type)) - return lowerRecordFreeze(lowerer, value, value.type, locOf(call)); + return lowerNativeFreeze(lowerer, "record.freeze", value, locOf(call)); + if (value.type.kind === "array") + return lowerNativeFreeze(lowerer, "array.freeze", value, locOf(call)); + if (value.type.kind === "map" || value.type.kind === "set") + return lowerNativeFreeze(lowerer, "collection.preventExtensions", value, locOf(call)); lowerer.noLowering( "Object.freeze of a possibly-aliased value", call, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ba6d1214f..95deb648b 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -150,7 +150,7 @@ import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optio import { type ProtocolDispatch } from "./class-protocol-dispatch.js"; import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js"; import { guardClassAttributeWrites } from "./class-attributes.js"; -import { guardFrozenRecordWrites, markSloppyRecordWrites } from "./frozen-records.js"; +import { guardFrozenWrites, markSloppyRecordWrites } from "./frozen-records.js"; import { applyHandleProperties } from "./handle-properties.js"; import { ClassProtocols } from "./class-protocols.js"; import { earlyReadVarSymbols } from "./var-hoisting.js"; @@ -3246,6 +3246,8 @@ export class Lowerer { * absent-state writes of `delete r.f` (markSloppyRecordWrites). */ readonly sloppyRecordWrites = new Set(); readonly recordFieldDeletes = new Set(); + /** typeKeys of the array types the program may freeze (IrModule.freezableArrays). */ + readonly freezableArrays = new Set(); readonly liftedFns: IrFunction[] = []; /** The first lifted function of each name (liftedFnNamed), for the * liftedFns indexed so far. liftedFns only grows. */ @@ -7694,7 +7696,7 @@ export class Lowerer { finalizePrivateBrands(this, functions); trackClassFieldCreation(this, functions); guardClassAttributeWrites(this, functions); - guardFrozenRecordWrites(this, functions); + guardFrozenWrites(this, functions); applyHandleProperties(this, functions); // Globals typed by a class that never REGISTERED (a JS class whose @@ -7803,6 +7805,9 @@ export class Lowerer { ...(this.threadSlotKeys.size > 0 ? { threadSlotKeys: [...this.threadSlotKeys].sort() } : {}), + ...(this.freezableArrays.size > 0 + ? { freezableArrays: [...this.freezableArrays].sort() } + : {}), ...(isNodeEsmFile(this.entry, this.program) ? {} : { commonJsEntry: true }), ...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}), }; diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 1b2c1082d..717f7ed8a 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -283,6 +283,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * object's text in the message ("#") and the operation (0 assign, * 1 add, 2 delete). */ "record.throwFrozen": { argTypes: [STRING, STRING, F64], result: VOID }, + /** Object.freeze of an array (scr_arr_freeze): sets its frozen bit, which + * the write guards of IrModule.freezableArrays test, and isFrozen reads. + * The receiver is any array type. */ + "array.freeze": { argTypes: [null], result: VOID }, + "array.isFrozen": { argTypes: [null], result: BOOL }, + /** Object.freeze, seal or preventExtensions of a Map or Set + * (scr_map_prevent_extensions), which only makes it not extensible: its + * entries stay writable. The receiver is any map or set type. */ + "collection.preventExtensions": { argTypes: [null], result: VOID }, + "collection.isExtensible": { argTypes: [null], result: BOOL }, "dyn.nativeSetNew": { argTypes: [DYN], result: DYN }, "dyn.nativeSetIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeMapIs": { argTypes: [DYN], result: BOOL }, diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 26539b17a..91e1d5f05 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -219,6 +219,7 @@ const MODULE_FIELDS: Record>, Role> = { "wasmUntranslated[].sha256": "other", "wasmUntranslated[].reason": "other", "threadSlotKeys[]": "other", + "freezableArrays[]": "other", }; const loc = { file: "references.ts", start: 0, end: 0 }; diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 3211e48f3..df23f0360 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1056,6 +1056,10 @@ export interface IrModule { * each one's hierarchy carries a slot word (the slots.* intrinsics; * backend/llvm/thread-slots.ts). */ threadSlotKeys?: string[]; + /** typeKeys of the array types whose values the program may freeze + * (frontend frozen-records.ts): their writes test the array's frozen bit + * (SCR_ARR_FROZEN) and throw Node's TypeError when it is set. */ + freezableArrays?: string[]; /** The entry module is CommonJS under Node's classification. Node runs a * CommonJS main synchronously and drains process.nextTick callbacks * before promise jobs at the first checkpoint; an ES-module main's diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7406-frozen-containers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7406-frozen-containers.ts.json new file mode 100644 index 000000000..34fb3ba0e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7406-frozen-containers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7406-frozen-containers.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 91328a1e8..af810483f 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -1689,6 +1689,126 @@ void scr_arr_release(ScrArr *a) { double scr_arr_len(ScrArr *a) { return SCR_ARR_LEN_F64(a->len); } +/* ── frozen arrays ────────────────────────────────────────────────────── + * Node's TypeErrors for writes to a frozen array, shared by the emitted + * guards of published and frozen arrays and the checked-dynamic tier. */ +static void scr_frozen_throw(const char *before, double index, const char *after) { + char text[64]; + size_t n = scr_f64_to_str(index, text); + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, before); + scr_jb_write(&buffer, text, n); + scr_jb_puts(&buffer, after); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); +} + +/* A relative array index argument (fill, copyWithin, splice): ToIntegerOrInfinity + * of `value`, counted from the end when negative, clamped to [0, len]. */ +static size_t scr_frozen_relative_index(double value, size_t len) { + if (value != value) return 0; + double n = trunc(value); + if (n < 0) return n + (double)len <= 0 ? 0 : (size_t)(n + (double)len); + return n >= (double)len ? len : (size_t)n; +} + +static void scr_frozen_read_only(double index) { + scr_frozen_throw("Cannot assign to read only property '", index, "' of object '[object Array]'"); +} + +static void scr_frozen_not_extensible(double index) { + scr_frozen_throw("Cannot add property ", index, ", object is not extensible"); +} + +static void scr_frozen_undeletable(double index) { + scr_frozen_throw("Cannot delete property '", index, "' of [object Array]"); +} + +/* Each case finds the operation's first write in the order the spec makes + * its writes. */ +bool scr_array_frozen_error(int op, size_t len, double x, double y, double z) { + switch (op) { + case 0: + if (x >= 0 && x < (double)len && x == (double)(size_t)x) scr_frozen_read_only(x); + else scr_frozen_not_extensible(x); + return true; + case 1: + if (y == 0) break; + scr_frozen_not_extensible((double)len); + return true; + case 2: + case 6: + if (len == 0) break; + if (op == 6 && len > 1) scr_frozen_read_only(0); + else scr_frozen_undeletable((double)(len - 1)); + return true; + case 3: break; + case 4: + if (y == 0) break; + scr_frozen_not_extensible(len ? (double)len + y - 1 : 0); + return true; + case 5: + if (!(x >= 0 && x < (double)len && x == (double)(size_t)x)) return false; + scr_frozen_undeletable(x); + return true; + case 7: + if (len < 2) return false; + scr_frozen_read_only(0); + return true; + case 8: { + size_t k = scr_frozen_relative_index(x, len), final = scr_frozen_relative_index(y, len); + if (k >= final) return false; + scr_frozen_read_only((double)k); + return true; + } + case 9: { + size_t to = scr_frozen_relative_index(x, len), from = scr_frozen_relative_index(y, len); + size_t final = scr_frozen_relative_index(z, len); + if (final <= from || len <= to) return false; + size_t count = final - from < len - to ? final - from : len - to; + /* An overlapping copy toward the end writes its last index first. */ + if (from < to && to < from + count) to += count - 1; + scr_frozen_read_only((double)to); + return true; + } + case 10: { + size_t start = scr_frozen_relative_index(x, len); + double count = y != y ? 0 : trunc(y); + size_t removed = count <= 0 ? 0 : count >= (double)(len - start) ? len - start : (size_t)count; + size_t items = (size_t)z; + if (items < removed) { + /* Elements after the removed ones move down, then the tail goes. */ + if (start < len - removed) scr_frozen_read_only((double)(start + items)); + else scr_frozen_undeletable((double)(len - 1)); + return true; + } + if (items > removed) { + /* Elements after the removed ones move up, past the end first. */ + if (len - removed > start) scr_frozen_not_extensible((double)(len - removed + items - 1)); + else if (start < len) scr_frozen_read_only((double)start); + else scr_frozen_not_extensible((double)start); + return true; + } + if (items > 0) { + scr_frozen_read_only((double)start); + return true; + } + break; + } + default: + return false; + } + static const char length[] = "Cannot assign to read only property 'length' of object '[object Array]'"; + scr_throw_error_msg(SCR_ERR_TYPE, length, sizeof length - 1); + return true; +} + +void scr_arr_freeze(ScrArr *a) { + if (a->rc != SIZE_MAX) a->spare |= SCR_ARR_FROZEN; +} + +bool scr_arr_is_frozen(const ScrArr *a) { return scr_arr_frozen(a); } + bool scr_arr_has(const ScrArr *a, double i) { size_t idx; if (!scr_arr_valid_index(i, &idx)) return scr_arr_prop_get(a, i, NULL); @@ -4990,6 +5110,8 @@ bool scr_arr_sort_dense(ScrArr *a, ScrClosure *f, uint32_t arity, bool owned, in if (a->rc == SIZE_MAX || elem == SCR_ELEM_CREF || !scr_arr_dense_values(a)) return false; size_t n = a->len; if (n < 2) return true; + /* A frozen array takes the snapshot path, whose write-back throws. */ + if (scr_arr_frozen(a)) return false; if (n > SIZE_MAX / 2 / sizeof(uint64_t)) scr_arr_oom(); ScrSortCompare c = scr_sort_compare_for(a, f, arity, order, key); /* Nothing else can reach a private copy: its own storage is sorted, and diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 4666750ea..d0c0ea1a8 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -128,7 +128,8 @@ typedef struct ScrArr { * header. Kept beside `elem` so the RC fast paths test it with one load. */ uint8_t cyc; /* The low SCR_ARR_OPS_SHIFT bits: storage flags (SCR_ARR_INLINE_SLOTS, - * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT, SCR_ARR_STATES). The rest: the + * SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT, SCR_ARR_STATES) + * and SCR_ARR_FROZEN. The rest: the * registry id of the element type's ops (scr_arr_ops), 0 for every kind * but SCR_ELEM_REF and SCR_ELEM_CREF. */ uint16_t spare; @@ -204,6 +205,10 @@ _Static_assert(sizeof(ScrArr) == sizeof(size_t) + 16 + sizeof(void *), #define SCR_ARR_UNSHIFTED 4u #define SCR_ARR_HAS_EXT 8u #define SCR_ARR_STATES 16u +/* Object.freeze of the array (scr_arr_freeze). Emitted guards test it + * before the writes of array types a program may freeze; the runtime's own + * writes assume it is clear. */ +#define SCR_ARR_FROZEN 32u /* An array starts without state bytes. Without them, every index below * `len` is a present value in dense storage: len <= cap, there are no @@ -214,6 +219,19 @@ _Static_assert(sizeof(ScrArr) == sizeof(size_t) + 16 + sizeof(void *), * states (scr_arr_add_states), which it keeps. */ static inline bool scr_arr_has_states(const ScrArr *a) { return (a->spare & SCR_ARR_STATES) != 0; } +static inline bool scr_arr_frozen(const ScrArr *a) { return (a->spare & SCR_ARR_FROZEN) != 0; } +/* Sets SCR_ARR_FROZEN; immortal arrays are left as they are. */ +void scr_arr_freeze(ScrArr *a); +bool scr_arr_is_frozen(const ScrArr *a); +/* Throws Node's TypeError for the first write that the array operation + * `op` makes to a frozen array of length `len`, or answers false when the + * operation writes nothing. op: 0 index write (x = index), 1 push (y = + * argument count), 2 pop, 3 length write, 4 unshift (y = argument count), + * 5 delete (x = index), 6 shift, 7 reverse or sort, 8 fill (x = start, y = + * end), 9 copyWithin (x = target, y = start, z = end), 10 splice (x = + * start, y = delete count, z = item count). */ +bool scr_array_frozen_error(int op, size_t len, double x, double y, double z); + /* The start of the dense slots' allocation: `data` less any head room. */ static inline uint64_t *scr_arr_block(const ScrArr *a) { return (a->spare & SCR_ARR_HEAD_ROOM) ? a->data - (size_t)a->data[-1] : a->data; @@ -236,7 +254,7 @@ static inline struct ScrArrExt *scr_arr_ext_of(const ScrArr *a) { * Id 0 is no ops; ids are assigned on each descriptor's first array, in * any thread. A program with more element types than the field holds keeps * the ops of the rest in the array's ext (SCR_ARR_OPS_OVERFLOW). */ -#define SCR_ARR_OPS_SHIFT 5 +#define SCR_ARR_OPS_SHIFT 6 #define SCR_ARR_OPS_OVERFLOW (UINT16_MAX >> SCR_ARR_OPS_SHIFT) extern const ScrArrOps *scr_arr_ops_table[SCR_ARR_OPS_OVERFLOW]; /* By registry id: 1 when the descriptor has SCR_ARR_OPS_RELEASE_NOTHING. diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 4039fda94..727e3e835 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -740,6 +740,8 @@ static ScrDyn *dyn_live_array_method(ScrDyn *recv, int mid, ScrDyn *const *args, if (!ops) return NULL; ScrArr *arr = recv->v.typed_ref.ptr; size_t len = arr->len; + /* A frozen array's view refuses the write. */ + if (scr_arr_frozen(arr) && ((mid == DYN_M_push) || (mid == DYN_M_pop))) return NULL; if ((mid == DYN_M_push) && argc == 1 && ops->set) { if (!ops->set(arr, (double)len, args[0])) return NULL; *handled = true; @@ -1098,16 +1100,20 @@ static ScrDyn *scr_dyn_invoke_impl( return NULL; } } - if (recv->v.arr.frozen && ((mid == DYN_M_push) || - (mid == DYN_M_pop) || (mid == DYN_M_shift) || - (mid == DYN_M_unshift) || (mid == DYN_M_splice))) { - static const char message[] = "Cannot assign to read only property 'length'"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + /* A frozen array throws at the operation's first write, as in Node. */ + int frozen_op = !recv->v.arr.frozen ? -1 + : (mid == DYN_M_push) ? 1 + : (mid == DYN_M_pop) ? 2 + : (mid == DYN_M_shift) ? 6 + : (mid == DYN_M_unshift) ? 4 + : (mid == DYN_M_splice) ? 10 + : ((mid == DYN_M_sort) || (mid == DYN_M_reverse)) ? 7 + : -1; + double items = (mid == DYN_M_splice) ? (argc > 2 ? (double)(argc - 2) : 0) : (double)argc; + if (frozen_op >= 0 && + scr_array_frozen_error(frozen_op, len, (double)splice_start, frozen_op == 10 ? (double)splice_removed : items, items)) return NULL; - } - if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !(mid == DYN_M_shift)) || - (recv->v.arr.frozen && (((mid == DYN_M_sort) && len > 1) || - ((mid == DYN_M_reverse) && len > 1)))) { + if ((grows && recv->non_extensible) || (shrinks && recv->v.arr.sealed && !(mid == DYN_M_shift))) { static const char message[] = "Cannot modify a sealed or frozen array"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; @@ -1376,11 +1382,7 @@ static ScrDyn *scr_dyn_invoke_impl( if (!dyn_arr_length_unchanged(recv, len)) return NULL; size_t start = dyn_rel_index(startD, len); size_t end = dyn_rel_index(endD, len); - if (recv->v.arr.frozen && start < end) { - static const char message[] = "Cannot assign to read only array index"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - return NULL; - } + if (recv->v.arr.frozen && scr_array_frozen_error(8, len, (double)start, (double)end, 0)) return NULL; ScrDyn *fill = argc ? args[0] : scr_dyn_undefined(); if (start < end) scr_dyn_region_store(recv, fill); for (size_t i = start; i < end; i++) { @@ -1409,11 +1411,8 @@ static ScrDyn *scr_dyn_invoke_impl( size_t end = dyn_rel_index(endD, len); size_t count = end > start ? end - start : 0; if (count > len - target) count = len - target; - if (recv->v.arr.frozen && count > 0) { - static const char message[] = "Cannot assign to read only array index"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + if (recv->v.arr.frozen && scr_array_frozen_error(9, len, (double)target, (double)start, (double)end)) return NULL; - } if (start < target && target < start + count) { for (size_t i = count; i > 0; i--) { ScrDyn *old = recv->v.arr.items[target + i - 1]; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index a542b5c24..13bd4d066 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2302,8 +2302,32 @@ static void *scr_dyn_typed_record(const ScrDyn *d) { return d->v.typed_ref.ptr; } -/* Freezes the checked view of a frozen record: its fields, and every - * property added through the view. */ +/* The native array an array capsule refers to, or NULL for any other + * value. */ +static ScrArr *scr_dyn_typed_array(const ScrDyn *d) { + if (d->kind != SCR_DYN_TYPED_REF || d->v.typed_ref.type_key_len < 6 || + memcmp(d->v.typed_ref.type_key, "array<", 6) != 0) + return NULL; + return d->v.typed_ref.ptr; +} + +/* The native Map or Set a handle holds, or NULL for any other value. */ +static ScrMap *scr_dyn_native_collection(const ScrDyn *d) { + if (d->kind != SCR_DYN_HANDLE || (d->v.handle.tag != SCR_DYNH_MAP && d->v.handle.tag != SCR_DYNH_SET)) + return NULL; + return d->v.handle.ptr; +} + +/* Whether a capsule's native record or array is frozen. */ +static bool scr_dyn_typed_ref_frozen(const ScrDyn *d) { + void *record = scr_dyn_typed_record(d); + if (record) return scr_rc_frozen(record); + ScrArr *array = scr_dyn_typed_array(d); + return array && scr_arr_frozen(array); +} + +/* Freezes the checked view of a frozen record or array: its fields or + * elements, and every property added through the view. */ static void scr_dyn_record_view_freeze(ScrDyn *view) { view->non_extensible = true; if (view->kind == SCR_DYN_OBJ) { @@ -2350,9 +2374,9 @@ static bool scr_dyn_class_integrity_fence(const ScrDyn *object, const char *oper ScrJsonBuf buffer; scr_jb_init(&buffer); scr_jb_puts(&buffer, operation); - scr_jb_puts(&buffer, scr_dyn_typed_record(object) - ? " of an object held in an untyped value is not supported yet" - : " of a class instance held in an untyped value is not supported yet"); + scr_jb_puts(&buffer, scr_dyn_typed_record(object) ? " of an object held in an untyped value is not supported yet" + : scr_dyn_typed_array(object) ? " of an array held in an untyped value is not supported yet" + : " of a class instance held in an untyped value is not supported yet"); ScrStr *message = scr_jb_finish(&buffer); scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); scr_str_release(message); @@ -2400,6 +2424,11 @@ static ScrDyn *scr_dyn_fn_existing_table(const ScrDyn *function) { ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; } + ScrMap *collection = scr_dyn_native_collection(object); + if (collection) { + scr_map_prevent_extensions(collection); + return scr_dyn_retain(object); + } if (scr_dyn_class_integrity_fence(object, "Object.preventExtensions")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(object); if (table) { @@ -2419,6 +2448,8 @@ bool scr_dyn_is_extensible(ScrDyn *object) { if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isExtensible"); return false; } bool answer; if (scr_dyn_typed_ref_view_answer(object, scr_dyn_is_extensible_view, &answer)) return answer; + ScrMap *collection = scr_dyn_native_collection(object); + if (collection) return scr_map_is_extensible(collection); if (scr_dyn_plain_fn(object)) { ScrDyn *table = scr_dyn_fn_existing_table(object); answer = !table || !table->non_extensible; @@ -2431,6 +2462,11 @@ bool scr_dyn_is_extensible(ScrDyn *object) { ScrDyn *scr_dyn_seal(ScrDyn *object) { if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.seal"); return NULL; } + ScrMap *collection = scr_dyn_native_collection(object); + if (collection) { + scr_map_prevent_extensions(collection); + return scr_dyn_retain(object); + } if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL; if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties)); ScrDyn *table = scr_dyn_fn_integrity_table(object); @@ -2455,6 +2491,8 @@ bool scr_dyn_is_sealed(const ScrDyn *object) { if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isSealed"); return false; } bool answer; if (scr_dyn_typed_ref_view_answer((ScrDyn *)object, scr_dyn_is_sealed, &answer)) return answer; + ScrMap *collection = scr_dyn_native_collection(object); + if (collection) return !scr_map_is_extensible(collection); if (object->symbol_properties && !scr_dyn_is_sealed(object->symbol_properties)) return false; if (scr_dyn_plain_fn(object)) { ScrDyn *table = scr_dyn_fn_existing_table(object); @@ -3679,16 +3717,14 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) { fresh->rc = fresh_rc; scr_dyn_release(fresh); } - void *record = scr_dyn_typed_record(capsule); - if (record && scr_rc_frozen(record)) scr_dyn_record_view_freeze(capsule->v.typed_ref.materialized); + if (scr_dyn_typed_ref_frozen(capsule)) scr_dyn_record_view_freeze(capsule->v.typed_ref.materialized); return scr_dyn_retain(capsule->v.typed_ref.materialized); } void scr_dyn_typed_ref_commit(ScrDyn *d) { - /* A frozen record's view cannot have changed its fields. */ + /* A frozen record's or array's view cannot have changed its contents. */ if (d && d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.commit && - d->v.typed_ref.materialized && - !(scr_dyn_typed_record(d) && scr_rc_frozen(d->v.typed_ref.ptr))) { + d->v.typed_ref.materialized && !scr_dyn_typed_ref_frozen(d)) { d->v.typed_ref.commit(d->v.typed_ref.ptr, d->v.typed_ref.materialized); } @@ -3927,6 +3963,8 @@ static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) { static bool scr_typed_ref_array_set(ScrDyn *d, const ScrStr *key, const ScrDyn *value) { const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(d); if (!ops || !ops->set) return false; + /* A frozen array's view refuses the write. */ + if (scr_arr_frozen(d->v.typed_ref.ptr)) return false; /* Numeric properties live in the native array's property table, which * holds values of the element type. */ double number; @@ -9230,6 +9268,26 @@ bool scr_dyn_in(const ScrDyn *v, const ScrStr *key) { return false; } +/* Node's TypeErrors for assigning a read-only property and deleting a + * non-configurable one of a checked object: the object reads as + * #, or [object Object] without a prototype. */ +static void scr_dyn_throw_property(const ScrDyn *object, const char *key, size_t key_len, bool deleting) { + const char *name = scr_dyn_class_name(object); + ScrJsonBuf buffer; + scr_jb_init(&buffer); + scr_jb_puts(&buffer, deleting ? "Cannot delete property '" : "Cannot assign to read only property '"); + scr_jb_write(&buffer, key, key_len); + scr_jb_puts(&buffer, deleting ? "' of " : "' of object '"); + if (object->null_proto) scr_jb_puts(&buffer, "[object Object]"); + else { + scr_jb_puts(&buffer, "#<"); + scr_jb_puts(&buffer, name ? name : "Object"); + scr_jb_puts(&buffer, ">"); + } + if (!deleting) scr_jb_puts(&buffer, "'"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); +} + static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, const ScrDyn *receiver) { for (size_t i = 0; i < recv->v.obj.len; i++) { ScrDynEntry *entry = &recv->v.obj.entries[i]; @@ -9245,8 +9303,7 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con return; } if (entry->accessor || !entry->writable) { - static const char msg[] = "Cannot assign to read only property"; - scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + scr_dyn_throw_property(receiver, key->data, key->len, false); return; } } @@ -9273,8 +9330,7 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con return; } if (inherited->accessor || !inherited->writable) { - static const char message[] = "Cannot assign to read only property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_dyn_throw_property(receiver, key->data, key->len, false); scr_dyn_release(table); return; } @@ -9354,8 +9410,7 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { if (recv->kind == SCR_DYN_ARR) { if (key->len == 6 && memcmp(key->data, "length", 6) == 0) { if (recv->v.arr.frozen) { - static const char message[] = "Cannot assign to read only property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_array_frozen_error(3, recv->v.arr.len, 0, 0, 0); return; } double number; @@ -9374,8 +9429,7 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { while (recv->v.arr.len < length) scr_dyn_arr_push_hole(recv); while (recv->v.arr.len > length) { if (recv->v.arr.sealed && scr_dyn_arr_has_index(recv, recv->v.arr.len - 1)) { - static const char message[] = "Cannot delete non-configurable property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_array_frozen_error(5, recv->v.arr.len, (double)(recv->v.arr.len - 1), 0, 0); return; } scr_dyn_release(recv->v.arr.items[--recv->v.arr.len]); @@ -9393,14 +9447,13 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { } if (is_index && idx < 4294967295ULL) { if (scr_dyn_array_prototype_element_fence(recv)) return; - if (recv->v.arr.frozen && idx < recv->v.arr.len) { - static const char message[] = "Cannot assign to read only property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + if (recv->v.arr.frozen && idx < recv->v.arr.len && scr_dyn_arr_has_index(recv, idx)) { + scr_array_frozen_error(0, recv->v.arr.len, (double)idx, 0, 0); return; } if (recv->non_extensible && !scr_dyn_arr_has_index(recv, idx)) { - static const char message[] = "Cannot add property, object is not extensible"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + /* An absent index, a hole included, is a property to add. */ + scr_array_frozen_error(0, 0, (double)idx, 0, 0); return; } if (idx > recv->v.arr.len + 1000000) { @@ -11229,10 +11282,7 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { if (recv->kind == SCR_DYN_OBJ) { ScrDynEntry *entry = scr_dyn_entry(recv, key); if (entry && !entry->configurable) { - if (strict) { - static const char message[] = "Cannot delete non-configurable property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - } + if (strict) scr_dyn_throw_property(recv, key->data, key->len, true); return; } scr_json_delete_member(recv, key); @@ -11244,10 +11294,7 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { for (size_t i = 0; i < key->len; i++) index = index * 10 + (size_t)(key->data[i] - '0'); if (!scr_dyn_arr_has_index(recv, index)) return; if (recv->v.arr.sealed) { - if (strict) { - static const char message[] = "Cannot delete non-configurable property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - } + if (strict) scr_array_frozen_error(5, recv->v.arr.len, (double)index, 0, 0); return; } if (!recv->v.arr.presence) { @@ -15682,18 +15729,29 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt return scr_dyn_to_string_method_args(d, enc, what, NULL, 0); } -/* Freezing a record capsule freezes the native record, whose emitted writes - * then refuse to change it, and its checked view. */ -static ScrDyn *scr_dyn_freeze_record(ScrDyn *capsule, void *record) { - scr_record_freeze(record); - if (capsule->v.typed_ref.materialized) scr_dyn_record_view_freeze(capsule->v.typed_ref.materialized); - return scr_dyn_retain(capsule); +/* Freezing a record or array capsule freezes the native value, whose + * emitted writes then refuse to change it, and its checked view. A Map or + * Set handle only stops being extensible: its entries stay writable. Answers + * NULL for every other value. */ +static ScrDyn *scr_dyn_freeze_native(ScrDyn *value) { + void *record = scr_dyn_typed_record(value); + ScrArr *array = record ? NULL : scr_dyn_typed_array(value); + ScrMap *collection = scr_dyn_native_collection(value); + if (collection) { + scr_map_prevent_extensions(collection); + return scr_dyn_retain(value); + } + if (!record && !array) return NULL; + if (record) scr_record_freeze(record); + else scr_arr_freeze(array); + if (value->v.typed_ref.materialized) scr_dyn_record_view_freeze(value->v.typed_ref.materialized); + return scr_dyn_retain(value); } ScrDyn *scr_dyn_freeze(ScrDyn *value) { if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; - void *record = scr_dyn_typed_record(value); - if (record) return scr_dyn_freeze_record(value, record); + ScrDyn *native = scr_dyn_freeze_native(value); + if (native) return native; if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(value); if (!table) return scr_dyn_freeze_data(value); @@ -15705,8 +15763,8 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) { ScrDyn *scr_dyn_freeze_data(ScrDyn *value) { if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; - void *record = scr_dyn_typed_record(value); - if (record) return scr_dyn_freeze_record(value, record); + ScrDyn *native = scr_dyn_freeze_native(value); + if (native) return native; if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze_data(value->symbol_properties)); if (value->kind == SCR_DYN_ARR) { @@ -15737,6 +15795,8 @@ bool scr_dyn_is_frozen(const ScrDyn *value) { if (scr_dyn_class_reflection_fence(value)) return false; bool answer; if (scr_dyn_typed_ref_view_answer((ScrDyn *)value, scr_dyn_is_frozen, &answer)) return answer; + ScrMap *collection = scr_dyn_native_collection(value); + if (collection) return !scr_map_is_extensible(collection); if (value->symbol_properties && !scr_dyn_is_frozen(value->symbol_properties)) return false; if (scr_dyn_plain_fn(value)) { ScrDyn *table = scr_dyn_fn_existing_table(value); diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 1a6a3fc00..1cb77b0f6 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -988,6 +988,12 @@ double scr_map_size(const ScrMap *m) { return (double)m->nlive; } * table. Regrowth is amortized against the insertions that need it. * A map without entries has empty tables already: clearing it does no * work and keeps its storage for the next fill. */ +void scr_map_prevent_extensions(ScrMap *m) { + if (m->rc != SIZE_MAX) m->flags |= SCR_MAP_NOT_EXTENSIBLE; +} + +bool scr_map_is_extensible(const ScrMap *m) { return (m->flags & SCR_MAP_NOT_EXTENSIBLE) == 0; } + void scr_map_clear(ScrMap *m) { size_t walked = m->nentries; if (walked == 0) return; diff --git a/packages/runtime/src/scr_map.h b/packages/runtime/src/scr_map.h index d1ca387bf..a581cc1c7 100644 --- a/packages/runtime/src/scr_map.h +++ b/packages/runtime/src/scr_map.h @@ -116,6 +116,10 @@ typedef struct ScrMapOps { * whose object has a null prototype (Object.groupBy results). Compiled * code sets and tests the byte; the runtime never copies it. */ #define SCR_MAP_NULL_PROTO 1u +/* Object.freeze, Object.seal or Object.preventExtensions of a Map or Set: + * it has no own properties, so it is then frozen, sealed and not + * extensible at once. Its entries stay writable, as in Node. */ +#define SCR_MAP_NOT_EXTENSIBLE 2u typedef struct ScrMap { size_t rc; /* SIZE_MAX = immortal (unused for maps; kept per convention) */ @@ -272,6 +276,10 @@ void scr_map_release_v(void *m); void scr_map_trace_v(void *m, ScrTraceVisit visit, void *ctx); double scr_map_size(const ScrMap *m); /* live entries (Map.size) */ +/* Object.freeze, seal and preventExtensions of a Map or Set + * (SCR_MAP_NOT_EXTENSIBLE); immortal maps are left as they are. */ +void scr_map_prevent_extensions(ScrMap *m); +bool scr_map_is_extensible(const ScrMap *m); void scr_map_clear(ScrMap *m); bool scr_map_has_f64(const ScrMap *m, double key); diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 4767122dd..afbe4d6af 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -527,62 +527,12 @@ void scr_throw_published_field(const char *prop, const char *owner) { scr_pub_throw("Cannot assign to read only property '%s' of object '#<%s>'", prop, owner); } -static void scr_pub_index_text(double index, char *out, size_t size) { - char buf[64]; - size_t n = scr_f64_to_str(index, buf); - if (n > size - 1) n = size - 1; - memcpy(out, buf, n); - out[n] = 0; +bool scr_throw_published_array(int op, const ScrArr *a, double index, double n) { + return scr_array_frozen_error(op, a->len, index, n, 0); } -/* op: 0 index write, 1 push (n = argument count), 2 pop, 3 length write, - * 4 unshift (n = argument count), 5 delete, 6 shift, 7 other in-place - * rewrites (splice, reverse, fill, copyWithin, sort). Returns false when - * Node's operation would not write the frozen array at all (it then is not - * an error: e.g. reversing an empty array). */ -bool scr_throw_published_array(int op, const ScrArr *a, double index, double n) { - static const char length[] = "Cannot assign to read only property 'length' of object '[object Array]'"; - char text[64]; - size_t len = a->len; - switch (op) { - case 0: - scr_pub_index_text(index, text, sizeof text); - if (index >= 0 && index < (double)len && index == (double)(size_t)index) - scr_pub_throw("Cannot assign to read only property '%s' of object '[object Array]'%s", text, ""); - else scr_pub_throw("Cannot add property %s, object is not extensible%s", text, ""); - return true; - case 1: - if (n == 0) break; - scr_pub_index_text((double)len, text, sizeof text); - scr_pub_throw("Cannot add property %s, object is not extensible%s", text, ""); - return true; - case 2: - case 6: - if (len == 0) break; - if (op == 6 && len > 1) { - scr_pub_throw("Cannot assign to read only property '0' of object '[object Array]'%s%s", "", ""); - return true; - } - scr_pub_index_text((double)(len - 1), text, sizeof text); - scr_pub_throw("Cannot delete property '%s' of [object Array]%s", text, ""); - return true; - case 3: break; - case 4: - if (n == 0) break; - scr_pub_index_text(len ? (double)len + n - 1 : 0, text, sizeof text); - scr_pub_throw("Cannot add property %s, object is not extensible%s", text, ""); - return true; - case 5: - scr_pub_index_text(index, text, sizeof text); - scr_pub_throw("Cannot delete property '%s' of [object Array]%s", text, ""); - return true; - default: - if (len == 0) return false; - scr_pub_throw("Cannot assign to read only property '0' of object '[object Array]'%s%s", "", ""); - return true; - } - scr_throw_error_msg(SCR_ERR_TYPE, length, sizeof length - 1); - return true; +bool scr_throw_published_array_range(int op, const ScrArr *a, double x, double y, double z) { + return scr_array_frozen_error(op, a->len, x, y, z); } void scr_throw_published_collection(bool set) { diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 87eb2536a..ccb74514d 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -743,12 +743,14 @@ void scr_pub_dyn_fn(void *v, ScrPub *ctx); void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem); void scr_pub_map(void *v, ScrPub *ctx, ScrPubFn key, ScrPubFn val); void scr_publish_refuse(const char *message); -/* Writes into published objects (emitted guards): Node's frozen-object - * TypeErrors. scr_throw_published_array answers false (and throws nothing) - * when Node's operation would not write the array. */ +/* Writes into published or frozen objects (emitted guards): Node's + * frozen-object TypeErrors. The array guards answer false (and throw + * nothing) when Node's operation would not write the array; their ops are + * scr_array_frozen_error's (scr_array.h). */ struct ScrArr; SCR_COLD void scr_throw_published_field(const char *prop, const char *owner); bool scr_throw_published_array(int op, const struct ScrArr *a, double index, double n); +bool scr_throw_published_array_range(int op, const struct ScrArr *a, double x, double y, double z); SCR_COLD void scr_throw_published_collection(bool set); /* Young-in nursery (scr_cycle.c built with SCR_YIN, linked by programs diff --git a/tests/corpus/7406-frozen-containers.ts b/tests/corpus/7406-frozen-containers.ts new file mode 100644 index 000000000..450cbf8cb --- /dev/null +++ b/tests/corpus/7406-frozen-containers.ts @@ -0,0 +1,132 @@ +// Object.freeze of arrays, Maps and Sets that other references still hold. +// A frozen array refuses every write through any reference with Node's +// TypeError for the operation's first write (this module is strict mode +// code). A frozen Map or Set only stops being extensible: its entries stay +// writable, and Object.isFrozen, isSealed and isExtensible report it. +export {}; + +function report(e: unknown): void { + console.log((e as Error).name + ": " + (e as Error).message); +} + +function freezeUnknown(value: unknown): unknown { + return Object.freeze(value); +} + +function writeAny(value: any): void { + value[0] = 7; +} + +function pushAny(value: any): void { + value.push(8); +} + +function integrity(value: unknown): string { + return [Object.isFrozen(value), Object.isSealed(value), Object.isExtensible(value)].join(" "); +} + +function arrays(): void { + const a: number[] = [1, 2, 3]; + const alias = a; + console.log(Object.isFrozen(a), integrity(a)); + Object.freeze(alias); + console.log(Object.isFrozen(a), integrity(a)); + try { a[0] = 9; } catch (e) { report(e); } + try { a[5] = 9; } catch (e) { report(e); } + try { a.length = 0; } catch (e) { report(e); } + try { a.push(4); } catch (e) { report(e); } + try { a.pop(); } catch (e) { report(e); } + try { a.shift(); } catch (e) { report(e); } + try { a.unshift(0); } catch (e) { report(e); } + try { a.splice(0, 1); } catch (e) { report(e); } + try { a.splice(1, 0, 7); } catch (e) { report(e); } + try { a.reverse(); } catch (e) { report(e); } + try { a.sort(); } catch (e) { report(e); } + try { a.fill(0, 1); } catch (e) { report(e); } + try { a.copyWithin(1, 0); } catch (e) { report(e); } + console.log(a.join(), a.length); + + const one: string[] = ["x"]; + Object.freeze(one); + console.log(one.reverse().join(), one.sort().join(), one.copyWithin(0, 1).join()); + try { one.splice(0, 1); } catch (e) { report(e); } + try { one.shift(); } catch (e) { report(e); } + const none: string[] = []; + Object.freeze(none); + console.log(none.reverse().length, none.fill("y").length); + try { none.pop(); } catch (e) { report(e); } + try { none.splice(0, 0, "z"); } catch (e) { report(e); } + + let calls = 0; + const byValue = (x: number, y: number): number => { + calls++; + return x - y; + }; + const unsorted: number[] = [3, 1, 2]; + Object.freeze(unsorted); + try { unsorted.sort(byValue); } catch (e) { report(e); } + const single: number[] = [5]; + Object.freeze(single); + console.log(unsorted.join(), calls > 0, single.sort(byValue).join()); + + const open: number[] = [4, 5]; + open.push(6); + open[0] = 1; + console.log(open.join(), Object.isFrozen(open)); +} + +function throughUnknown(): void { + const a: number[] = [1, 2]; + console.log(integrity(a)); + const back = freezeUnknown(a); + console.log(back === a, Object.isFrozen(a), integrity(a)); + try { a.push(3); } catch (e) { report(e); } + try { a[1] = 5; } catch (e) { report(e); } + try { writeAny(a); } catch (e) { report(e); } + try { pushAny(a); } catch (e) { report(e); } + console.log(a.join()); + + const rows: number[][] = [[1], [2]]; + freezeUnknown(rows[1]); + rows[0]!.push(10); + try { rows[1]!.push(20); } catch (e) { report(e); } + console.log(JSON.stringify(rows), Object.isFrozen(rows), Object.isFrozen(rows[1])); +} + +function collections(): void { + const m = new Map([["x", 1]]); + console.log(integrity(m)); + const frozen = Object.freeze(m); + m.set("y", 2); + m.delete("x"); + console.log(frozen === m, Object.isFrozen(m), Object.isSealed(m), Object.isExtensible(m), m.size); + const s = new Set([1]); + freezeUnknown(s); + s.add(2); + console.log(integrity(s), s.size); + const sealed = new Set(); + Object.seal(sealed); + const fresh = new Map(); + Object.preventExtensions(fresh); + console.log(Object.isFrozen(sealed), Object.isExtensible(fresh), integrity(new Map())); +} + +function lifetimes(): void { + const kept: string[][] = []; + for (let round = 0; round < 3; round++) { + const row = ["r" + round, "s" + round]; + if (round !== 1) Object.freeze(row); + kept.push(row); + } + for (const row of kept) { + try { row[0] = "changed"; } catch (e) { console.log(row[1], (e as Error).message); } + } + console.log(JSON.stringify(kept)); + kept.length = 0; + console.log(kept.length); +} + +arrays(); +throughUnknown(); +collections(); +lifetimes(); diff --git a/tests/harness/corpus-costs/7406-frozen-containers.ts.json b/tests/harness/corpus-costs/7406-frozen-containers.ts.json new file mode 100644 index 000000000..30bfa4b73 --- /dev/null +++ b/tests/harness/corpus-costs/7406-frozen-containers.ts.json @@ -0,0 +1 @@ +{ "plain": 3146, "san": 3439 } From ec833b9a02cf0476e7995778b1716d16d4b0582f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 02:57:19 -0700 Subject: [PATCH 077/409] Record the handle method values fixture's baseline and cost Co-Authored-By: Claude Opus 5.5 --- .../tests/corpus/7740-handle-method-values.js.json | 6 ++++++ .../harness/corpus-costs/7740-handle-method-values.js.json | 1 + 2 files changed, 7 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7740-handle-method-values.js.json create mode 100644 tests/harness/corpus-costs/7740-handle-method-values.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7740-handle-method-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7740-handle-method-values.js.json new file mode 100644 index 000000000..ab7f75639 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7740-handle-method-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7740-handle-method-values.js" + ], + "diags": [] +} diff --git a/tests/harness/corpus-costs/7740-handle-method-values.js.json b/tests/harness/corpus-costs/7740-handle-method-values.js.json new file mode 100644 index 000000000..d18e4dddd --- /dev/null +++ b/tests/harness/corpus-costs/7740-handle-method-values.js.json @@ -0,0 +1 @@ +{ "plain": 9800, "san": 12000 } From 952e01d2ed21c7420c0305ca1d9025e246961750 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:03:12 -0500 Subject: [PATCH 078/409] Stream request bodies in the Node server adapter --- docs/content/docs/dependencies.mdx | 2 +- .../ambient/scriptc-node-fallback.d.ts | 1 + .../compiler/shims/hono-node-server/v2.ts | 167 ++++++++++++++++-- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/lower-server.ts | 5 +- .../compiler/src/ir/builtin-signatures.ts | 3 +- ...287-http-held-request-backpressure.ts.json | 6 + packages/runtime/src/scr_http.c | 44 +++-- packages/runtime/src/scr_net.c | 21 ++- packages/runtime/src/scr_net.h | 1 + .../7287-http-held-request-backpressure.ts | 106 +++++++++++ .../npm/cases/hono-node-server/main.ts | 105 ++++++++++- ...287-http-held-request-backpressure.ts.json | 1 + 13 files changed, 425 insertions(+), 38 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json create mode 100644 tests/corpus/7287-http-held-request-backpressure.ts create mode 100644 tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 3098dcad4..14130b234 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -92,7 +92,7 @@ scriptc implements a few packages itself and compiles that implementation in pla - graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying. - An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there. -`@hono/node-server` 2.x is built in. `serve`, `createAdaptorServer`, `getRequestListener` and `RequestError` serve a fetch handler such as Hono's `app.fetch` over `node:http` with native `Request` and `Response` objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies are read before the handler runs. Known differences: +`@hono/node-server` 2.x is built in. `serve`, `createAdaptorServer`, `getRequestListener` and `RequestError` serve a fetch handler such as Hono's `app.fetch` over `node:http` with native `Request` and `Response` objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies stream to the handler as it reads them, and a body the handler leaves unread is drained after the response, up to 64 MiB or 500 ms, as the package does. Known differences: - Only HTTP/1.1 servers are declared: the `createServer` and `serverOptions` options for `node:https` and `node:http2`, the `websocket` option and `upgradeWebSocket` aren't declared, so using one is a type error at build time. Subpath modules such as `@hono/node-server/serve-static` compile from the package's own files. - The fetch handler must return a `Response` or a promise of one; the handler's second argument holds `incoming` and `outgoing`. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index b195c3fbb..09a3fdd8e 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -5561,6 +5561,7 @@ declare module "http" { readonly trailersDistinct: { [name: string]: string[] | undefined }; readonly rawTrailers: string[]; resume(): void; + pause(): void; destroy(): void; setTimeout(msecs: number, callback?: () => void): this; /* setEncoding('utf8'): 'data' delivers strings (other real encodings diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts index 94a9c8d85..c46813c7b 100644 --- a/packages/compiler/shims/hono-node-server/v2.ts +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -3,10 +3,12 @@ // node:http. The published package swaps the global Request and Response // for lightweight objects and reparents prototypes, which only pays off // under V8; this port builds native Request objects from the incoming -// message and writes the handler's Response the way the package's -// listener does (header record, content length for bodies that complete -// within the first reads, streamed bodies, error responses), so a client -// sees the same status lines, headers and bodies. +// message, with a body streamed from it as the app reads it, and writes +// the handler's Response the way the package's listener does (header +// record, content length for bodies that complete within the first reads, +// streamed bodies, error responses), so a client sees the same status +// lines, headers and bodies. A body the app leaves unread is drained after +// the response, within the package's limits. // // HTTP/1.1 only: the http2/https server options and WebSocket upgrades are // not declared. A Response that is still exactly its constructor arguments @@ -136,27 +138,125 @@ function requestHeaders(incoming: IncomingMessage): Headers { return headers; } -function readIncoming(incoming: IncomingMessage): Promise { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - incoming.on("data", (chunk: Buffer) => { - chunks.push(chunk); - }); - incoming.on("end", () => resolve(chunks.length === 1 ? chunks[0]! : Buffer.concat(chunks))); - incoming.on("error", reject); +/** What the listener knows about the incoming body: whether it ended + * (the package's `readableEnded`) and whether a drain started. */ +type IncomingState = { ended: boolean; draining: boolean }; + +/** The request body as the package streams it: each pull reads one chunk + * of the incoming message, which pauses between pulls, so the socket + * holds what the app has not read yet. The stream pulls once on its own + * when it is constructed; the package builds its stream only when the app + * reads the body, so until the app's first read a drain discards the + * chunks instead of pausing for them. */ +function incomingBody(incoming: IncomingMessage, state: IncomingState): ReadableStream { + let pulls = 0; + let listening = false; + let settled = false; + let wake: (() => void) | undefined = undefined; + const woken = (): void => { + const resolve = wake; + wake = undefined; + if (resolve !== undefined) resolve(); + }; + return new ReadableStream({ + pull(controller) { + pulls++; + return new Promise((resolve) => { + if (settled) { + resolve(); + return; + } + wake = resolve; + if (listening) { + incoming.resume(); + return; + } + listening = true; + incoming.on("data", (chunk: Buffer) => { + if (state.draining && pulls === 1) return; + incoming.pause(); + if (settled) return; + controller.enqueue(chunk); + woken(); + }); + incoming.on("end", () => { + if (settled) return; + settled = true; + controller.close(); + woken(); + }); + incoming.on("error", (err: Error) => { + if (settled) return; + settled = true; + controller.error(err); + woken(); + }); + }); + }, }); } -async function newRequest( +function newRequest( incoming: IncomingMessage, defaultHostname: string | undefined, -): Promise { + state: IncomingState, +): Request { const method = normalizeIncomingMethod(incoming.method); const url = requestUrl(incoming, defaultHostname); const headers = requestHeaders(incoming); if (method === "GET" || method === "HEAD") return new Request(url, { method, headers }); - const body = await readIncoming(incoming); - return new Request(url, { method, headers, body }); + return new Request(url, { + method, + headers, + body: incomingBody(incoming, state), + duplex: "half", + }); +} + +const DRAIN_TIMEOUT_MS = 500; +const MAX_DRAIN_BYTES = 64 * 1024 * 1024; + +/** The package's drain of a body the app left unread: read and discard + * up to MAX_DRAIN_BYTES or DRAIN_TIMEOUT_MS, then close the connection + * once its writes are out. */ +function drainIncoming(incoming: IncomingMessage, state: IncomingState): void { + const socket = incoming.socket; + if (socket.destroyed || state.draining) return; + state.draining = true; + let bytesRead = 0; + let done = false; + const cleanup = (): void => { + if (done) return; + done = true; + clearTimeout(timer); + }; + const forceClose = (): void => { + if (done) return; + cleanup(); + if (!socket.destroyed) socket.destroySoon(); + }; + const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS); + timer.unref(); + incoming.on("data", (chunk: Buffer) => { + if (done) return; + bytesRead += chunk.length; + if (bytesRead > MAX_DRAIN_BYTES) forceClose(); + }); + incoming.on("end", cleanup); + incoming.on("error", cleanup); + incoming.resume(); +} + +/** The package's close handler cleanup: an unread body drains two timer + * turns after the response closes. */ +function drainAfterClose(incoming: IncomingMessage, state: IncomingState): void { + if (state.ended) return; + setTimeout(() => { + if (!state.ended) + setTimeout(() => { + drainIncoming(incoming, state); + }); + }); } /** The package's outgoing header record: Headers order (lowercase, @@ -427,6 +527,14 @@ async function respond( await writeResponse(res, outgoing, head); } +/** A Response the package writes from its cache without waiting: still + * exactly its constructor arguments, with no body or a string or bytes + * body. */ +function immediateCacheable(res: Response): boolean { + const light = scriptcResponseLight(res); + return light[0] !== "0" && Number(light[2]!) >= -1; +} + function handleFetchError(e: unknown): Response { return emptyResponse(e instanceof Error && e.name === "TimeoutError" ? 504 : 500); } @@ -439,13 +547,38 @@ async function handleRequest( ): Promise { let req: Request | undefined = undefined; let res: Response | Promise | undefined = undefined; + const state: IncomingState = { ended: false, draining: false }; + let needsBodyCleanup = false; + let closeHandlerAttached = false; + const ensureCloseHandler = (): void => { + if (req === undefined || closeHandlerAttached) return; + closeHandlerAttached = true; + if (needsBodyCleanup) + outgoing.on("close", () => { + drainAfterClose(incoming, state); + }); + }; try { - req = await newRequest(incoming, options.hostname); + req = newRequest(incoming, options.hostname, state); + needsBodyCleanup = + options.autoCleanupIncoming !== false && + !(incoming.method === "GET" || incoming.method === "HEAD"); + if (needsBodyCleanup) + incoming.on("end", () => { + state.ended = true; + }); res = fetchCallback(req, { incoming, outgoing }); + if (!(res instanceof Promise) && immediateCacheable(res)) { + if (needsBodyCleanup) + outgoing.once("finish", () => { + if (!state.ended) drainIncoming(incoming, state); + }); + } else ensureCloseHandler(); } catch (e) { if (res !== undefined) return handleResponseError(e, outgoing); const errorHandler = options.errorHandler; if (errorHandler !== undefined) { + ensureCloseHandler(); const handled = await errorHandler( req !== undefined ? e : e instanceof RequestError ? e : toRequestError(e), ); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8c7d44cdb..937f9ff6b 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1319,6 +1319,7 @@ export const LIB_FN_SYMS: Record = { "http.reqPipeClient": "scr_http_req_pipe_client", "http.reqPipeSock": "scr_http_req_pipe_sock", "http.reqResume": "scr_http_req_resume", + "http.reqPause": "scr_http_req_pause", "http.reqDestroy": "scr_http_req_destroy", "http.reqSetEncoding": "scr_http_req_set_encoding", "http.resSetHeader": "scr_http_res_set_header", diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index d9746e1f6..cb79accea 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -7581,7 +7581,7 @@ function lowerHttpReqMethodCall( loc, }; } - if (name === "resume" || name === "destroy") { + if (name === "resume" || name === "pause" || name === "destroy") { requireStatementPosition(lowerer, call, `req.${name}()`); if (args.length !== 0) { lowerer.noLowering( @@ -7591,7 +7591,8 @@ function lowerHttpReqMethodCall( ); } const receiver = coerceToHandle(lowerer, access.expression, HTTPREQ_T); - const fn: IrLibFn = name === "resume" ? "http.reqResume" : "http.reqDestroy"; + const fn: IrLibFn = + name === "resume" ? "http.reqResume" : name === "pause" ? "http.reqPause" : "http.reqDestroy"; return { kind: "libCall", fn, args: [receiver], type: VOID, loc }; } if (name === "setEncoding") { diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 8d2de2a87..2b12c15ab 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1835,7 +1835,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * connection (+1, the same handle net.connect would give); * sockRemoteAddress answers `string | undefined` (NULL after the fd * closed, Node's destroyed-socket undefined; a dual-stack accept of an - * IPv4 peer reads "::ffff:a.b.c.d" like Node). reqResume/reqDestroy and + * IPv4 peer reads "::ffff:a.b.c.d" like Node). reqResume/reqPause/reqDestroy and * the req error/close listener slots complete the IncomingMessage * surface portless's client responses use; resDestroy/resOnClose and * resWriteHeadPairs ([k0,v0,k1,v1,...] — the env.pairs helper's flat @@ -1902,6 +1902,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "http.reqPipeClient": { argTypes: [HTTPREQ_T, HTTPCLIENTREQ_T], result: VOID }, "http.reqPipeSock": { argTypes: [HTTPREQ_T, NETSOCKET_T], result: VOID }, "http.reqResume": { argTypes: [HTTPREQ_T], result: VOID }, + "http.reqPause": { argTypes: [HTTPREQ_T], result: VOID }, "http.reqDestroy": { argTypes: [HTTPREQ_T], result: VOID }, "http.reqOnError": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID }, "http.reqOnClose": { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json new file mode 100644 index 000000000..3a4358d78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7287-http-held-request-backpressure.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7287-http-held-request-backpressure.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index cb88297bf..29eb52ef7 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -239,10 +239,11 @@ struct ScrHttpReq { bool close_queued; bool close_emitted; /* settled: err/close listeners dropped */ bool join_dup; /* joinDuplicateHeaders: repeated names read joined ", " */ - /* pause(): delivery holds — arrived body bytes buffer in pend (the - * parser keeps consuming; memory is the buffer, a documented bound) - * and 'end' defers (end_pending) until resume() drains through the - * emit queue, never the resuming stack. */ + /* pause(): delivery holds — arrived body bytes buffer in pend and + * 'end' defers (end_pending) until resume() drains through the emit + * queue, never the resuming stack. A held HTTP/1 server request stops + * its socket's reads once pend reaches SCR_HTTP_REQ_HOLD_BYTES + * (sock_held), as Node's readStop does. */ bool paused; bool end_pending; bool drain_queued; @@ -257,6 +258,7 @@ struct ScrHttpReq { bool received; /* the whole body arrived (req.complete) */ bool body_seen; /* body bytes arrived */ bool destroyed; /* req.destroy()/the teardown ran — req.destroyed */ + bool sock_held; /* this request stopped its socket's reads */ ScrNetLs aborted_ls; /* req.on('aborted') — the h2 compat event */ }; @@ -330,7 +332,10 @@ void scr_http_req_release(ScrHttpReq *r) { scr_http_client_release(r->pipe_client); scr_net_sock_release(r->pipe_sock); free(r->pend); - if (r->sock) scr_net_sock_release(r->sock); + if (r->sock) { + if (r->sock_held) scr_net_sock_hold(r->sock, false); + scr_net_sock_release(r->sock); + } if (r->h2_stream) scr_http_h2_ops->release(r->h2_stream); #ifdef SCR_RC_AUDIT scr_http_live--; @@ -638,6 +643,22 @@ static bool scr_http_req_holding(const ScrHttpReq *r) { return r->paused || (r->gated && !r->flowing); } +/* IncomingMessage's readableHighWaterMark (its socket's, 64 KiB): a held + * request buffers this many body bytes before its socket stops reading. */ +#define SCR_HTTP_REQ_HOLD_BYTES 65536u + +/* Stops the socket's reads while an HTTP/1 server request holds a full + * buffer, and restarts them once it flows, ends or is dumped, as Node's + * readStop when push() returns false. Client responses and h2 compat + * requests keep reading. */ +static void scr_http_req_update_hold(ScrHttpReq *r) { + bool hold = r->sock != NULL && r->status == -1 && !r->http2 && !r->ended && + scr_http_req_holding(r) && r->pend_len >= SCR_HTTP_REQ_HOLD_BYTES; + if (hold == r->sock_held) return; + r->sock_held = hold; + scr_net_sock_hold(r->sock, hold); +} + static void scr_http_req_queue_drain(ScrHttpReq *r) { if (r->drain_queued) return; r->drain_queued = true; @@ -650,6 +671,7 @@ static void scr_http_req_flow(ScrHttpReq *r) { r->paused = false; r->flowing = true; if (r->pend_len > 0 || r->end_pending) scr_http_req_queue_drain(r); + scr_http_req_update_hold(r); } /* A 'data' listener or pipe() starts the flow unless pause() stopped it, @@ -658,9 +680,7 @@ static void scr_http_req_consume(ScrHttpReq *r) { if (r->gated && !r->paused && !r->flowing) scr_http_req_flow(r); } -/* resume(): releases what pause() or the initial paused state held. This - * parser always consumes body bytes from the socket (SEMANTICS.md); held - * bytes buffer in memory. */ +/* resume(): releases what pause() or the initial paused state held. */ void scr_http_req_resume(ScrHttpReq *r) { if (!scr_http_req_holding(r)) return; scr_http_req_flow(r); @@ -676,6 +696,7 @@ static void scr_http_req_dump(ScrHttpReq *r) { r->pend_len = r->pend_cap = 0; r->paused = false; r->flowing = true; + scr_http_req_update_hold(r); } /* pause(): delivery holds until resume() (scr_http_req_deliver buffers; @@ -759,6 +780,7 @@ static void scr_http_req_finish(ScrHttpReq *r, bool fire) { return; } r->ended = true; + scr_http_req_update_hold(r); r->end_consumed = fire && (r->data_ls.n > 0 || r->pipe_res || r->pipe_client || r->pipe_sock); if (fire) scr_net_fire0_this(&r->end_ls, r, SCR_DYNH_HTTP_REQ); scr_net_ls_drop(&r->data_ls); @@ -2305,6 +2327,7 @@ static void scr_http_proto_sweep(void) { req->pend_len = req->pend_cap = 0; if (held_len > 0) scr_http_req_deliver(req, held, held_len); free(held); + scr_http_req_update_hold(req); if (!scr_exc_pending() && !req->paused && req->end_pending) { req->end_pending = false; scr_http_req_finish(req, true); @@ -2758,8 +2781,8 @@ static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n) { if (!r || n == 0 || r->ended) return; r->body_seen = true; if (scr_http_req_holding(r)) { - /* paused: the parser keeps consuming (memory is the buffer — a - * documented bound), delivery waits for the drain */ + /* held: delivery waits for the drain; a full buffer stops the + * socket's reads */ if (r->pend_len + n > r->pend_cap) { size_t cap = r->pend_cap ? r->pend_cap : 4096; while (cap < r->pend_len + n) cap *= 2; @@ -2769,6 +2792,7 @@ static void scr_http_req_deliver(ScrHttpReq *r, const char *data, size_t n) { } memcpy(r->pend + r->pend_len, data, n); r->pend_len += n; + scr_http_req_update_hold(r); return; } bool piped = r->pipe_res != NULL || r->pipe_client != NULL || r->pipe_sock != NULL; diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 29a4af393..4d5a37f77 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -556,6 +556,10 @@ struct ScrNetSocket { * unconsumed stream). resume() clears user_paused and sets flowing. */ bool user_paused; bool flowing; + /* scr_net_sock_hold: a protocol consumer (an HTTP/1 server request + * holding unread body bytes) stops reads without touching the user's + * pause state */ + bool hold; /* EOF arrived while the socket buffered for a consumer it does not have * yet: 'end' waits until one drains the buffer (Node's readable state) */ bool eof_pending; @@ -978,7 +982,7 @@ static ScrNetSocket *scr_net_sock_new(void) { #define SCR_NET_READ_AHEAD 65536u static void scr_net_sock_update_read(ScrNetSocket *s) { - bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused && + bool want = s->fd >= 0 && !s->connecting && !s->rd_eof && !s->user_paused && !s->hold && (s->data_ls.n > 0 || s->pipe_dst != NULL || s->native_data != NULL || (s->tunnel != NULL && !s->tunnel->send_only) /* the proxy's CONNECT answer */ || s->flowing /* resume(): flow (and discard sans listeners) */ || @@ -1303,7 +1307,7 @@ static void scr_net_sock_read(ScrNetSocket *s) { if (s->tunnel != NULL) return; /* readiness feeds the tunnel pump instead */ if (s->tops && !s->t_est) return; /* readiness feeds the handshake pump instead */ for (;;) { - if (s->fd < 0 || s->user_paused) return; + if (s->fd < 0 || s->user_paused || s->hold) return; bool flowing = s->data_ls.n > 0 || s->pipe_dst || s->native_data || s->flowing; /* buffered (peeked/unshifted) bytes first — they precede the * kernel's in the stream; a TLS engine drains them through its bio */ @@ -3102,6 +3106,15 @@ ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s) { return scr_net_sock_retain(s); } +/* Stops (hold) or restarts reads for a protocol consumer that has + * buffered as much as it accepts, as Node's readStop when push() returns + * false. Separate from pause()/resume(), which stay the user's. */ +void scr_net_sock_hold(ScrNetSocket *s, bool hold) { + if (s->hold == hold) return; + s->hold = hold; + scr_net_sock_update_read(s); +} + /* socket.resume(): flowing mode — a consumer even with no 'data' * listener (arrived bytes discard, so 'end' can be reached, Node's * resumed-but-unconsumed stream). Buffered bytes deliver from the next @@ -5271,7 +5284,7 @@ static bool scr_net_flags_pending(void) { * plaintext inside a transport, or unshifted bytes in the receive * buffer: the poller can't re-signal them, the sweep must deliver */ bool consumers = (s->data_ls.n > 0 || s->pipe_dst || s->native_data || s->flowing) && - !s->user_paused; + !s->user_paused && !s->hold; if (consumers && s->fd >= 0) { if (s->tops && s->t_est && s->tops->pending && s->tops->pending(s->tctx)) return true; if (!s->tops && s->rlen > s->rhead) return true; @@ -5377,7 +5390,7 @@ static void scr_net_sweep(void) { while (sock) { ScrNetSocket *next = sock->next; /* may unregister below */ scr_net_sock_retain(sock); /* callbacks may drop every other ref */ - if (sock->fd >= 0 && !sock->user_paused && + if (sock->fd >= 0 && !sock->user_paused && !sock->hold && (sock->data_ls.n > 0 || sock->pipe_dst || sock->native_data || sock->flowing) && ((sock->tops && sock->t_est && sock->tops->pending && sock->tops->pending(sock->tctx)) || (!sock->tops && sock->rlen > sock->rhead))) { diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index ffa9b2f72..6df6945ea 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -232,6 +232,7 @@ void scr_net_sock_destroy(ScrNetSocket *s); * counters/flags the compat surface reads. */ ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s); /* +1: chaining */ ScrNetSocket *scr_net_sock_resume(ScrNetSocket *s); /* +1: chaining */ +void scr_net_sock_hold(ScrNetSocket *s, bool hold); /* protocol read hold (not pause) */ ScrNetSocket *scr_net_sock_set_nodelay(ScrNetSocket *s, bool enable); /* +1: chaining */ void scr_net_sock_set_keepalive(ScrNetSocket *s, bool enable, double delay_ms); void scr_net_sock_destroy_soon(ScrNetSocket *s); diff --git a/tests/corpus/7287-http-held-request-backpressure.ts b/tests/corpus/7287-http-held-request-backpressure.ts new file mode 100644 index 000000000..1953b8ec6 --- /dev/null +++ b/tests/corpus/7287-http-held-request-backpressure.ts @@ -0,0 +1,106 @@ +// A server request that holds its body (nothing reads it yet, or its +// reader paused it) stops reading its socket once its buffer is full, so +// a client that keeps uploading is held back by TCP. Each client writes +// 64 KiB chunks, one after the previous write's callback, and stops when +// the response arrives. An unread body followed by a pipelined request is +// discarded once the response finishes, and the second request is served. +import * as http from "node:http"; +import * as net from "node:net"; +import type { AddressInfo } from "node:net"; + +const MiB = 1024 * 1024; + +function upload(port: number, path: string): Promise { + return new Promise((resolve) => { + const socket = net.connect(port, "127.0.0.1"); + const frame = Buffer.concat([Buffer.from("10000\r\n"), Buffer.alloc(65536, 98), Buffer.from("\r\n")]); + let sent = 0; + let sentBeforeResponse = -1; + let response = ""; + const writeNext = (): void => { + if (sentBeforeResponse >= 0 || socket.destroyed || sent >= 256 * MiB) return; + sent += 65536; + socket.write(frame, () => { + writeNext(); + }); + }; + socket.on("connect", () => { + socket.write(`POST ${path} HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\n`); + writeNext(); + }); + socket.on("data", (chunk: Buffer) => { + response += chunk.toString("latin1"); + if (sentBeforeResponse < 0 && response.includes("\r\n\r\n")) { + sentBeforeResponse = sent; + socket.destroy(); + resolve(sentBeforeResponse); + } + }); + socket.on("error", () => {}); + }); +} + +function pipelined(port: number): Promise { + return new Promise((resolve) => { + const socket = net.connect(port, "127.0.0.1"); + const body = "c".repeat(300000); + let response = ""; + socket.on("connect", () => { + socket.write( + `POST /unread HTTP/1.1\r\nHost: localhost\r\nContent-Length: ${body.length}\r\n\r\n${body}` + + "GET /second HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n", + ); + }); + socket.on("data", (chunk: Buffer) => { + response += chunk.toString("latin1"); + }); + socket.on("end", () => { + const lines = response.split("\r\n").filter((line) => line.startsWith("HTTP/") || line.startsWith("served")); + resolve(lines); + }); + }); +} + +let pausedRequest: http.IncomingMessage | undefined; + +const server = http.createServer((req, res) => { + if (req.url === "/unconsumed") { + setTimeout(() => { + res.writeHead(413, { "content-type": "text/plain" }); + res.end("unconsumed"); + }, 300); + return; + } + if (req.url === "/paused") { + let seen = 0; + req.on("data", (chunk: Buffer) => { + seen += chunk.length; + if (pausedRequest === undefined) { + pausedRequest = req; + req.pause(); + setTimeout(() => { + res.writeHead(413, { "content-type": "text/plain" }); + res.end(`paused after ${seen > 0}`); + req.resume(); + }, 300); + } + }); + return; + } + res.writeHead(200, { "content-type": "text/plain" }); + res.end(`served ${req.url}`); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as AddressInfo).port; + void (async () => { + const before = process.memoryUsage().rss; + const unconsumed = await upload(port, "/unconsumed"); + console.log("unconsumed: client stopped before 32 MiB:", unconsumed < 32 * MiB); + const paused = await upload(port, "/paused"); + console.log("paused: client stopped before 32 MiB:", paused < 32 * MiB); + console.log("memory grew under 64 MiB:", process.memoryUsage().rss - before < 64 * MiB); + for (const line of await pipelined(port)) console.log(line); + server.close(() => console.log("closed")); + })(); +}); diff --git a/tests/fixtures/npm/cases/hono-node-server/main.ts b/tests/fixtures/npm/cases/hono-node-server/main.ts index b47dae8e3..3cca4aa44 100644 --- a/tests/fixtures/npm/cases/hono-node-server/main.ts +++ b/tests/fixtures/npm/cases/hono-node-server/main.ts @@ -2,9 +2,11 @@ // serving a fetch handler: a raw HTTP client prints each exchange's status // line, wire-order headers (Date dropped) and body, covering buffered and // streamed bodies, header forms, cookies, request bodies, URL handling, -// HEAD, empty statuses, handler errors, and a client that disconnects -// during a streamed body. +// HEAD, empty statuses, handler errors, a client that disconnects during +// a streamed body, and request bodies the handler reads, leaves unread or +// rejects while the upload is still arriving. import * as http from "node:http"; +import * as net from "node:net"; import type { AddressInfo } from "node:net"; import { getRequestListener, RequestError, serve } from "@hono/node-server"; @@ -282,7 +284,104 @@ function startSecond(): void { void (async () => { console.log(await exchange(port, { method: "GET", path: "/ok" })); console.log(await exchange(port, { method: "GET", path: "/fail" })); - second.close(() => console.log("second closed")); + second.close(() => { + console.log("second closed"); + startUploads(); + }); })(); }); } + +// Request bodies stream to the handler. /size reads the whole body. The +// other routes leave it unread: /unread answers at once (the package's +// cached-response path) and /unread-async through a promise, and /reject +// answers 413 after 300 ms while the client keeps uploading, so the server +// must stop reading the socket. +const MiB = 1024 * 1024; + +interface UploadResult { + status: string; + body: string; + sentBeforeResponse: number; +} + +/** A chunked POST over a raw socket: 64 KiB chunks, each written after + * the previous write's callback, until `limit` bytes are out (then the + * last chunk, when `finish` is set) or the response arrives. */ +function chunkedUpload(port: number, path: string, limit: number, finish: boolean): Promise { + return new Promise((resolve) => { + const socket = net.connect(port, "127.0.0.1"); + const frame = Buffer.concat([Buffer.from("10000\r\n"), Buffer.alloc(65536, 97), Buffer.from("\r\n")]); + let sent = 0; + let sentBeforeResponse = -1; + let response = ""; + let settled = false; + const settle = (): void => { + if (settled) return; + settled = true; + const headEnd = response.indexOf("\r\n\r\n"); + const status = response.slice(0, response.indexOf("\r\n")); + resolve({ status, body: headEnd < 0 ? "" : response.slice(headEnd + 4), sentBeforeResponse }); + socket.destroy(); + }; + const writeNext = (): void => { + if (sentBeforeResponse >= 0 || socket.destroyed) return; + if (sent >= limit) { + if (finish) socket.write("0\r\n\r\n"); + return; + } + sent += 65536; + socket.write(frame, () => { + writeNext(); + }); + }; + socket.on("connect", () => { + socket.write(`POST ${path} HTTP/1.1\r\nHost: 127.0.0.1\r\nTransfer-Encoding: chunked\r\n\r\n`); + writeNext(); + }); + socket.on("data", (chunk: Buffer) => { + response += chunk.toString("latin1"); + if (sentBeforeResponse < 0 && response.includes("\r\n\r\n")) sentBeforeResponse = sent; + const match = /content-length: (\d+)/i.exec(response); + const headEnd = response.indexOf("\r\n\r\n"); + if (match !== null && headEnd >= 0 && response.length - headEnd - 4 >= Number(match[1])) settle(); + }); + socket.on("error", () => settle()); + socket.on("close", () => settle()); + }); +} + +function startUploads(): void { + const server = serve( + { + fetch: (request: Request): Response | Promise => { + const url = new URL(request.url); + if (url.pathname === "/size") + return request.arrayBuffer().then((body) => new Response(`size ${body.byteLength}`)); + if (url.pathname === "/reject") + return delay(300).then(() => new Response("too large", { status: 413 })); + if (url.pathname === "/unread-async") return Promise.resolve(new Response("unread async")); + return new Response(`unread ${url.pathname}`); + }, + port: 0, + hostname: "127.0.0.1", + }, + (info: AddressInfo) => { + void (async () => { + const read = await chunkedUpload(info.port, "/size", 3 * MiB, true); + console.log(`### chunked POST /size\n${read.status}\n${read.body}\n`); + console.log(await exchange(info.port, { method: "POST", path: "/unread", body: "x".repeat(300000) })); + console.log(await exchange(info.port, { method: "GET", path: "/after-unread" })); + console.log(await exchange(info.port, { method: "POST", path: "/unread-async", body: "y".repeat(300000) })); + console.log(await exchange(info.port, { method: "GET", path: "/after-unread-async" })); + const before = process.memoryUsage().rss; + const rejected = await chunkedUpload(info.port, "/reject", 512 * MiB, false); + const grown = process.memoryUsage().rss - before; + console.log(`### chunked POST /reject\n${rejected.status}\n${rejected.body}`); + console.log("client stopped before 32 MiB:", rejected.sentBeforeResponse >= 0 && rejected.sentBeforeResponse < 32 * MiB); + console.log("server memory grew under 64 MiB:", grown < 64 * MiB); + server.close(() => console.log("uploads closed")); + })(); + }, + ); +} diff --git a/tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json b/tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json new file mode 100644 index 000000000..9622e52d1 --- /dev/null +++ b/tests/harness/corpus-costs/7287-http-held-request-backpressure.ts.json @@ -0,0 +1 @@ +{ "plain": 5600, "san": 5600 } From ed1786869b4fb76d8a9f21de32b4cf57d4bf6886 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:06:48 -0500 Subject: [PATCH 079/409] Record a class's bag initializer only when its class-table branches agree A per-class field-creation helper creates an instance's property bag with the class's recorded initializer. A class can carry several: three.js's Material bag is created with Material's own prototype for Material instances and through the per-instance prototype dispatch for subclass instances, and the first one was recorded. MeshStandardMaterial instances then got bags with Material's prototype, and writes to their own fields (roughness, metalness, color) through setValues stored into the bag. The initializer is now recorded only when every branch's initializer is the same; other classes create their bags through the class tables. --- .../lowering/class-dynamic-dispatch.test.ts | 50 +++++++++++++++++++ .../lowering/class-dynamic-dispatch.ts | 17 +++++++ 2 files changed, 67 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index 1a21a6aa5..bbdf271c0 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -560,3 +560,53 @@ test("class branches read their exact class's bag, also for classes with subclas then: [{ kind: "return", value: { kind: "dynObjLit", fields: [] } }], }); }); + +test("records a class's bag initializer only when all its class-table branches agree", () => { + const { lowerer } = context(); + const classBagInitializers = new Map(); + Object.assign(lowerer, { classBagInitializers }); + for (const name of ["Material", "Texture"]) + lowerer.classes.set(name, { def: { name, tracksOwnFields: true } } as unknown as ClassInfo); + const initializer = ( + className: string, + prototype: string, + start: number, + ): Extract => { + const at = { ...loc, start }; + return { + kind: "fieldSet", + obj: { + kind: "dynCheck", + value: { kind: "varRef", localId: "p.0", type: DYN, loc: at }, + type: { kind: "object", className }, + loc: at, + }, + className, + field: "%bag", + value: { + kind: "libCall", + fn: "dyn.objCreate", + args: [{ kind: "call", callee: prototype, args: [], type: DYN, loc: at }], + type: DYN, + loc: at, + }, + loc: at, + }; + }; + const dispatch = new ClassDynamicDispatch(); + const tables = (dispatch as unknown as { bagInitializers: Map }) + .bagInitializers; + // A base class whose own instances get its prototype and whose subclass + // instances get theirs through a per-instance dispatch has no single bag. + tables.set("Material", [ + initializer("Material", "%prototype.data.Material", 1), + initializer("Material", "%class.bagPrototype:Material", 2), + ]); + // Branches that differ only in source location agree. + tables.set("Texture", [ + initializer("Texture", "%prototype.data.Texture", 3), + initializer("Texture", "%prototype.data.Texture", 4), + ]); + dispatch.finalize(lowerer); + expect([...classBagInitializers.keys()]).toEqual(["Texture"]); +}); diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index a1bc41782..ab3ebb34e 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -982,6 +982,11 @@ function initializeBag(lowerer: Lowerer, className: string, self: IrExpr, loc: S ]; } +/** `value` as JSON without its source locations. */ +function withoutLocations(value: IrExpr): string { + return JSON.stringify(value, (key, field: unknown) => (key === "loc" ? undefined : field)); +} + /** Whether `value` reads local `id`. */ function mentionsLocal(value: IrExpr, id: string): boolean { return JSON.stringify(value).includes(`"localId":${JSON.stringify(id)}`); @@ -4903,6 +4908,18 @@ export class ClassDynamicDispatch { const self = varRef("this", { kind: "object", className }, init.loc); const value = withTypedReceiver(structuredClone(init.value), self); if (mentionsLocal(value, "p.0")) continue; + // The class-table branches can give the class's instances different + // bags (a base class's own prototype for its instances, the + // instance's class prototype for subclass instances); only a class + // whose branches all agree has one initializer for every instance. + const shape = withoutLocations(value); + if ( + initializers.some( + (other) => + withoutLocations(withTypedReceiver(structuredClone(other.value), self)) !== shape, + ) + ) + continue; // Lowerer stand-ins in unit tests may not carry the table. lowerer.classBagInitializers?.set(className, value); } From eb2fa3c829fff0433b890cf92bc41690d63bde6e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:14:51 -0500 Subject: [PATCH 080/409] Fire spawn listeners for a child that exits before its first poll pass A child spawned from a listener that runs during a poll pass, such as a 'close' listener, can exit before the next pass fires its 'spawn' listeners. The same pass's waitpid loop then settled it with 'spawn' never emitted. Settling an exited child now fires its pending 'spawn' listeners first, before its stream data and 'exit', as Node emits them. --- .../7640-child-spawn-before-fast-exit.ts.json | 6 ++++++ packages/runtime/src/scr_child.c | 10 +++++++--- .../7640-child-spawn-before-fast-exit.ts | 20 +++++++++++++++++++ .../7640-child-spawn-before-fast-exit.ts.json | 1 + 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7640-child-spawn-before-fast-exit.ts.json create mode 100644 tests/corpus/7640-child-spawn-before-fast-exit.ts create mode 100644 tests/harness/corpus-costs/7640-child-spawn-before-fast-exit.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7640-child-spawn-before-fast-exit.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7640-child-spawn-before-fast-exit.ts.json new file mode 100644 index 000000000..0b82f6091 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7640-child-spawn-before-fast-exit.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7640-child-spawn-before-fast-exit.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index df7543500..a70b10894 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -2780,7 +2780,8 @@ static void scr_child_settle(ScrChild *c) { scr_children_reffed_n--; } if (c->state == SCR_CHILD_EXITED) { - scr_child_stream_drain_now(c->out_stream); + if (!c->spawn_done) scr_child_fire_spawn(c); + if (!scr_exc_pending()) scr_child_stream_drain_now(c->out_stream); if (!scr_exc_pending()) scr_child_stream_drain_now(c->err_stream); if (!scr_exc_pending()) scr_ipc_settle(c->ipc); } @@ -5607,9 +5608,12 @@ static void scr_child_settle(ScrChild *c) { /* Streams first, settled after: an exit listener registered from a * draining 'data'/'end' callback still fires below, like Node's. The * SPAWN-FAILURE path skips the drain — Node fires 'error' first and - * the streams' immediate EOF delivers 'end' on the next turn. */ + * the streams' immediate EOF delivers 'end' on the next turn. A child + * spawned during this pass can exit before the next pass fires 'spawn'; + * Node emits 'spawn' before any data or 'exit', so it fires here first. */ if (c->state == SCR_CHILD_EXITED) { - scr_child_stream_drain_now(c->out_stream); + if (!c->spawn_done) scr_child_fire_spawn(c); + if (!scr_exc_pending()) scr_child_stream_drain_now(c->out_stream); if (!scr_exc_pending()) scr_child_stream_drain_now(c->err_stream); if (!scr_exc_pending()) scr_ipc_settle(c->ipc); } diff --git a/tests/corpus/7640-child-spawn-before-fast-exit.ts b/tests/corpus/7640-child-spawn-before-fast-exit.ts new file mode 100644 index 000000000..9c232d96c --- /dev/null +++ b/tests/corpus/7640-child-spawn-before-fast-exit.ts @@ -0,0 +1,20 @@ +// A child spawned from a 'close' listener that exits before the listener +// returns still emits 'spawn' before 'exit'. +import { spawn } from "node:child_process"; + +function busyWait(ms: number): void { + const end = Date.now() + ms; + while (Date.now() < end) {} +} + +const events: string[] = []; +spawn("sh", ["-c", "exit 0"], { stdio: "ignore" }).on("close", () => { + const child = spawn("sh", ["-c", "exit 4"], { stdio: "ignore" }); + child.on("spawn", () => events.push("spawn")); + child.on("exit", (code) => events.push(`exit ${code}`)); + child.on("close", (code) => { + events.push(`close ${code}`); + console.log(events.join(", ")); + }); + busyWait(300); +}); diff --git a/tests/harness/corpus-costs/7640-child-spawn-before-fast-exit.ts.json b/tests/harness/corpus-costs/7640-child-spawn-before-fast-exit.ts.json new file mode 100644 index 000000000..b56ad904d --- /dev/null +++ b/tests/harness/corpus-costs/7640-child-spawn-before-fast-exit.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 4000 } From 2c44d3e3abe229cab1062e29b23dbe1bfc781948 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:15:45 -0500 Subject: [PATCH 081/409] Stop reading stdin at the read-ahead mark while 'readable' listeners hold it - With 'readable' listeners the stream stays paused, as in Node: 'data' listeners, resume() and a parked for-await read no longer keep the loop reading once the held input reaches the 64 KiB mark (or a larger read(size)); reading resumes when a read drains it. - Held input is a list of chunks with a running length, so an arrival appends without copying what is already held, and read() joins only the bytes it returns. - New event-loop case: 'readable', 'data' and resume() with no read() for 400 ms while 1 MiB arrives. --- packages/runtime/src/scr_events.c | 123 +++++++++++++----- .../stdio-readable-backpressure.mjs | 27 ++++ tests/harness/event-loop-cases.ts | 11 ++ 3 files changed, 128 insertions(+), 33 deletions(-) create mode 100644 tests/fixtures/event-loop/stdio-readable-backpressure.mjs diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c index bc554a445..6e0bf5146 100644 --- a/packages/runtime/src/scr_events.c +++ b/packages/runtime/src/scr_events.c @@ -388,7 +388,12 @@ static SCR_TL bool scr_stdin_destroyed = false; static SCR_TL bool scr_stdin_paused = false; static SCR_TL bool scr_stdin_resumed = false; static SCR_TL bool scr_stdin_read_eof = false; -static SCR_TL ScrBytes *scr_stdin_buffer; +/* Input held for read() and later consumers, as Node's BufferList: the + * chunks in arrival order and their total length, so an arrival appends + * without copying what is already held. */ +static SCR_TL ScrBytes **scr_stdin_held = NULL; +static SCR_TL size_t scr_stdin_nheld = 0, scr_stdin_held_cap = 0; +static SCR_TL size_t scr_stdin_buffered = 0; static SCR_TL size_t scr_stdin_read_demand; /* 'readable' listeners (paused mode, as Node's once one is added): input * stays buffered for read(), and the event fires on the next tick when @@ -398,6 +403,66 @@ static SCR_TL size_t scr_stdin_nreadable = 0, scr_stdin_readable_cap = 0; static SCR_TL bool scr_stdin_readable_need = false; static SCR_TL bool scr_stdin_readable_queued = false; +/* Appends an arrived chunk (moves) to the held input. */ +static void scr_stdin_hold(ScrBytes *chunk) { + if (chunk->len == 0) { + scr_bytes_release(chunk); + return; + } + if (scr_stdin_nheld == scr_stdin_held_cap) { + scr_stdin_held_cap = scr_stdin_held_cap ? scr_stdin_held_cap * 2 : 4; + scr_stdin_held = realloc(scr_stdin_held, scr_stdin_held_cap * sizeof *scr_stdin_held); + if (!scr_stdin_held) scr_events_oom(); + } + scr_stdin_held[scr_stdin_nheld++] = chunk; + scr_stdin_buffered += chunk->len; +} + +/* The first n held bytes as one chunk (+1), 0 < n <= scr_stdin_buffered. + * A whole first chunk moves out without a copy; otherwise the bytes are + * joined once, and a partly taken chunk keeps its rest. */ +static ScrBytes *scr_stdin_take(size_t n) { + ScrBytes *first = scr_stdin_held[0]; + size_t used = 0; + ScrBytes *out; + if (n == first->len) { + out = first; + used = 1; + } else if (n < first->len) { + out = scr_bytes_slice(first, 0, (double)n); + scr_stdin_held[0] = scr_bytes_slice(first, (double)n, (double)first->len); + scr_bytes_release(first); + } else { + out = scr_bytes_new(SCR_BYTES_U8, (double)n); + size_t at = 0; + while (at < n) { + ScrBytes *chunk = scr_stdin_held[used]; + size_t part = chunk->len <= n - at ? chunk->len : n - at; + memcpy(out->data + at, chunk->data, part); + at += part; + if (part == chunk->len) { + scr_bytes_release(chunk); + used++; + } else { + scr_stdin_held[used] = scr_bytes_slice(chunk, (double)part, (double)chunk->len); + scr_bytes_release(chunk); + } + } + } + memmove(scr_stdin_held, scr_stdin_held + used, (scr_stdin_nheld - used) * sizeof *scr_stdin_held); + scr_stdin_nheld -= used; + scr_stdin_buffered -= n; + return out; +} + +static void scr_stdin_drop_held(void) { + for (size_t i = 0; i < scr_stdin_nheld; i++) scr_bytes_release(scr_stdin_held[i]); + free(scr_stdin_held); + scr_stdin_held = NULL; + scr_stdin_nheld = scr_stdin_held_cap = 0; + scr_stdin_buffered = 0; +} + #ifdef SCR_STDIO_EXTENDED /* process.stdin.setEncoding(enc): the canonical encoding name (NULL until * set) and the StringDecoder's packed pending partial sequence, so a @@ -486,7 +551,7 @@ static void scr_stdin_finish(bool fire_end); static void scr_stdin_emit_readable(void) { scr_stdin_readable_queued = false; - size_t length = scr_stdin_buffer ? scr_stdin_buffer->len : 0; + size_t length = scr_stdin_buffered; if (scr_stdin_eof || scr_stdin_destroyed || (!length && !scr_stdin_read_eof)) return; size_t n = scr_stdin_nreadable; ScrStdinEndL *snap = malloc((n ? n : 1) * sizeof *snap); @@ -512,7 +577,7 @@ static void scr_stdin_emit_readable(void) { scr_closure_release(snap[i].cb); } free(snap); - length = scr_stdin_buffer ? scr_stdin_buffer->len : 0; + length = scr_stdin_buffered; /* Another event is due once the buffer reaches the 64 KiB mark, or the * larger size a listener's read(size) asked for. */ scr_stdin_readable_need = @@ -542,7 +607,7 @@ static void scr_stdin_on_readable(ScrClosure *cb /*moves*/, bool once) { scr_stdin_nreadable++; scr_stdin_readable_need = true; if (scr_stdin_read_demand < 65536) scr_stdin_read_demand = 65536; - if (scr_stdin_buffer && scr_stdin_buffer->len) scr_stdin_queue_readable(); + if (scr_stdin_buffered) scr_stdin_queue_readable(); } /* The input ended: with 'readable' listeners the event fires once more and @@ -551,7 +616,7 @@ static void scr_stdin_on_readable(ScrClosure *cb /*moves*/, bool once) { static void scr_stdin_reached_eof(void) { scr_stdin_read_eof = true; if (scr_stdin_nreadable) scr_stdin_queue_readable(); - else if (!scr_stdin_buffer) scr_stdin_finish(true); + else if (!scr_stdin_buffered) scr_stdin_finish(true); } void scr_stdin_on_error(ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) { @@ -595,10 +660,15 @@ bool scr_stdin_ended(void) { return scr_stdin_eof || scr_stdin_destroyed; } * alive (a paused Node stdin with only those never flows — the process * exits with them registered). */ static bool scr_stdin_watching(void) { - return !scr_stdin_eof && !scr_stdin_destroyed && - ((!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) || - scr_stdin_waiter != NULL || - (!scr_stdin_read_eof && scr_stdin_read_demand > (scr_stdin_buffer ? scr_stdin_buffer->len : 0))); + if (scr_stdin_eof || scr_stdin_destroyed) return false; + /* 'readable' listeners keep the stream paused, as in Node, where + * resume() and 'data' listeners do not make it flow: it reads ahead only + * while a read wants more than is held (the 64 KiB mark, or a larger + * read(size)), and stops until something drains it. */ + if (scr_stdin_nreadable) return !scr_stdin_read_eof && scr_stdin_read_demand > scr_stdin_buffered; + return (!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) || + scr_stdin_waiter != NULL || + (!scr_stdin_read_eof && scr_stdin_read_demand > scr_stdin_buffered); } bool scr_stdin_pending(void) { return scr_stdin_watching(); } @@ -676,8 +746,7 @@ static void scr_stdin_finish(bool fire_end) { void scr_stdin_destroy(void) { if (scr_stdin_destroyed) return; scr_stdin_destroyed = true; - scr_bytes_release(scr_stdin_buffer); - scr_stdin_buffer = NULL; + scr_stdin_drop_held(); scr_stdin_read_demand = 0; scr_stdin_settle_done(); scr_stdin_drop_listeners(); @@ -712,15 +781,7 @@ static void scr_stdin_deliver(ScrBytes *chunk); static void scr_stdin_arrived(ScrBytes *chunk) { if (scr_stdin_paused || scr_stdin_nreadable || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) { if (scr_stdin_nreadable && scr_stdin_readable_need) scr_stdin_queue_readable(); - if (scr_stdin_buffer) { - size_t before = scr_stdin_buffer->len; - ScrBytes *joined = scr_bytes_new(SCR_BYTES_U8, (double)(before + chunk->len)); - memcpy(joined->data, scr_stdin_buffer->data, before); - memcpy(joined->data + before, chunk->data, chunk->len); - scr_bytes_release(scr_stdin_buffer); - scr_bytes_release(chunk); - scr_stdin_buffer = joined; - } else scr_stdin_buffer = chunk; + scr_stdin_hold(chunk); return; } scr_stdin_deliver(chunk); @@ -732,16 +793,15 @@ static void scr_stdin_service(void) { /* Buffered input goes to a consumer: 'data' listeners, a resumed * stream, or a parked for-await read. After the last 'readable' listener * it stays buffered until one arrives, as in Node. */ - if (!scr_stdin_paused && !scr_stdin_nreadable && scr_stdin_buffer && + if (!scr_stdin_paused && !scr_stdin_nreadable && scr_stdin_buffered && (scr_stdin_ndata > 0 || scr_stdin_resumed || scr_stdin_waiter != NULL)) { - ScrBytes *chunk = scr_stdin_buffer; - scr_stdin_buffer = NULL; + ScrBytes *chunk = scr_stdin_take(scr_stdin_buffered); scr_stdin_deliver(chunk); scr_bytes_release(chunk); return; } if (scr_stdin_read_eof) { - if (!scr_stdin_buffer && !scr_stdin_nreadable) scr_stdin_finish(true); + if (!scr_stdin_buffered && !scr_stdin_nreadable) scr_stdin_finish(true); return; } if (scr_stdin_fed) { @@ -1058,7 +1118,7 @@ static void scr_stdio_queue_write_error(ScrStdio *stream, ScrDyn *callback, int } static void scr_stdio_finish_read(void) { - if (scr_stdin_read_eof && !scr_stdin_buffer && !scr_stdin_eof && !scr_stdin_destroyed) + if (scr_stdin_read_eof && !scr_stdin_buffered && !scr_stdin_eof && !scr_stdin_destroyed) scr_stdin_finish(true); } @@ -1067,7 +1127,7 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) { if (!all && (size->kind != SCR_DYN_NUM || !isfinite(size->v.num) || size->v.num < 0 || size->v.num > 1073741824 || floor(size->v.num) != size->v.num)) return scr_stdio_refusal("read size"); - size_t available = scr_stdin_buffer ? scr_stdin_buffer->len : 0; + size_t available = scr_stdin_buffered; size_t wanted = all ? available : (size_t)size->v.num; if (scr_stdin_read_eof && wanted > available) wanted = available; if (wanted == 0 || wanted > available) { @@ -1078,10 +1138,8 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) { } return scr_dyn_new_null(); } - ScrBytes *chunk = scr_bytes_slice(scr_stdin_buffer, 0, (double)wanted); - ScrBytes *rest = wanted < available ? scr_bytes_slice(scr_stdin_buffer, (double)wanted, (double)available) : NULL; - scr_bytes_release(scr_stdin_buffer); - scr_stdin_buffer = rest; + ScrBytes *chunk = scr_stdin_take(wanted); + bool rest = scr_stdin_buffered > 0; scr_stdin_read_demand = 65536; if (!rest && !scr_stdin_read_eof) scr_stdin_readable_need = true; scr_stdin_deliver(chunk); @@ -1339,7 +1397,7 @@ static ScrDyn *scr_stdio_get(void *ptr, const char *key, size_t length) { return dimension < 0 ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(dimension); } if (stream->fd == 0 && length == 14 && memcmp(key, "readableLength", 14) == 0) - return scr_dyn_new_num(scr_stdin_buffer ? (double)scr_stdin_buffer->len : 0); + return scr_dyn_new_num((double)scr_stdin_buffered); if (stream->fd == 0 && length == 5 && memcmp(key, "isRaw", 5) == 0) return scr_process_is_tty(0) ? scr_dyn_new_bool(scr_stdio_raw) : scr_dyn_retain(scr_dyn_undefined()); if (stream->fd == 0 && length == 9 && memcmp(key, "destroyed", 9) == 0) return scr_dyn_new_bool(scr_stdin_destroyed); @@ -1624,8 +1682,7 @@ static void scr_events_cleanup_atexit(void) { if (scr_context_is_main()) scr_sig_cleanup(); scr_stdin_settle_done(); scr_stdin_drop_listeners(); - scr_bytes_release(scr_stdin_buffer); - scr_stdin_buffer = NULL; + scr_stdin_drop_held(); if (scr_stdio_initialized) scr_stdio_cleanup(); } diff --git a/tests/fixtures/event-loop/stdio-readable-backpressure.mjs b/tests/fixtures/event-loop/stdio-readable-backpressure.mjs new file mode 100644 index 000000000..df2afb18f --- /dev/null +++ b/tests/fixtures/event-loop/stdio-readable-backpressure.mjs @@ -0,0 +1,27 @@ +// 'readable', 'data' and resume() together, with no read() for a while: +// 'readable' keeps the stream paused, so it stops reading at its read-ahead +// mark until a read drains it, and 'data' sees what read() returns. +const input = process.stdin; +let draining = false; +let peak = 0; +let dataBytes = 0; +let readBytes = 0; +const drain = () => { + let chunk; + while ((chunk = input.read()) !== null) readBytes += chunk.length; +}; +input.on('data', (chunk) => { + dataBytes += chunk.length; +}); +input.on('readable', () => { + peak = Math.max(peak, input.readableLength); + if (draining) drain(); +}); +input.resume(); +setTimeout(() => { + peak = Math.max(peak, input.readableLength); + console.log('held below 128 KiB while not draining', peak <= 131072); + draining = true; + drain(); +}, 400); +input.on('end', () => console.log('read', readBytes, 'data', dataBytes)); diff --git a/tests/harness/event-loop-cases.ts b/tests/harness/event-loop-cases.ts index 1b75defaa..50a7d2b85 100644 --- a/tests/harness/event-loop-cases.ts +++ b/tests/harness/event-loop-cases.ts @@ -67,6 +67,17 @@ export const eventLoopCases: EventLoopCase[] = [ end: true, }, }, + { + title: "stdio values: 'readable' with 'data' and resume() holds input at the read-ahead mark", + fixture: "stdio-readable-backpressure.mjs", + script: { + writes: Array.from({ length: 16 }, (_, i) => ({ + delayMs: i === 0 ? 0 : 5, + data: String.fromCharCode(97 + i).repeat(65536), + })), + end: true, + }, + }, { title: "stdio values: 'readable' on an immediately ended pipe", fixture: "stdio-readable.mjs", From 47490f0bca3cd9657ae6d7fb1f3b6b7f76a13c56 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:16:33 -0500 Subject: [PATCH 082/409] Revert "Reuse the bundled declaration files' diagnostics across builds (#1981)" --- .../src/frontend/ts7/lib-diagnostics.test.ts | 221 ------------ .../src/frontend/ts7/lib-diagnostics.ts | 324 ------------------ .../compiler/src/frontend/ts7/program-host.ts | 35 +- packages/compiler/src/frontend/ts7/session.ts | 30 -- packages/compiler/src/index.ts | 21 -- packages/compiler/src/native/cli.ts | 17 +- 6 files changed, 3 insertions(+), 645 deletions(-) delete mode 100644 packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts delete mode 100644 packages/compiler/src/frontend/ts7/lib-diagnostics.ts diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts deleted file mode 100644 index f041a6cb5..000000000 --- a/packages/compiler/src/frontend/ts7/lib-diagnostics.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterAll, afterEach, expect, test } from "vitest"; -import type { SourceFile } from "./ast-types.js"; -import { - reusedLibDiagnostics, - setLibDiagnosticsStore, - type LibDiagnosticsStore, -} from "./lib-diagnostics.js"; -import type { Ts7DiagnosticData } from "./session-schema.generated.js"; -import type { Ts7CheckedFileDiagnosticData, Ts7SessionProgram } from "./session.js"; -import { closeSourceParser, parseSourceFile } from "./source-parser-node.js"; - -const dir = mkdtempSync(join(tmpdir(), "scriptc-lib-diagnostics-")); -const lib = join(dir, "lib.es5.d.ts"); -const fallback = join(dir, "scriptc-node-fallback.d.ts"); -const libText = "declare var NaN: number;\ninterface Array { length: number }\n"; -const fallbackText = "export {};\ndeclare global {\n var process: { argv: string[] };\n}\n"; -writeFileSync(lib, libText); -writeFileSync(fallback, fallbackText); -const main = join(dir, "main.ts"); -const util = join(dir, "\u{1F600}.ts"); -const other = join(dir, "�.ts"); - -afterEach(() => setLibDiagnosticsStore(null)); -afterAll(() => { - closeSourceParser(); - rmSync(dir, { recursive: true, force: true }); -}); - -function memoryStore(): LibDiagnosticsStore & { entries: Map } { - const entries = new Map(); - return { - entries, - read: (key) => entries.get(key) ?? null, - write: (key, text) => void entries.set(key, text), - }; -} - -function diagnostic(fileName: string | undefined, pos: number, code = 2322): Ts7DiagnosticData { - return { fileName, pos, end: pos + 1, code, category: 1, text: `at ${pos}` }; -} - -/** A session program whose full-check and checked-file answers are given; it - * records which requests it was asked. */ -function sessionProgram(answers: { - semantic: Map; - global?: Ts7DiagnosticData[]; - checked?: boolean; -}): Ts7SessionProgram & { asked: string[] } { - const asked: string[] = []; - const all = [...answers.semantic.values()].flat(); - return { - asked, - getCompilerOptions: () => ({ strict: true }), - diagnosticData: (method: string, file?: string) => { - asked.push(file === undefined ? method : `${method} ${file}`); - if (method === "getGlobalDiagnostics") return answers.global ?? []; - return file === undefined ? all : (answers.semantic.get(file) ?? []); - }, - checkedFileDiagnosticData: (files: readonly string[]): Ts7CheckedFileDiagnosticData | null => { - asked.push(`checked ${files.join(",")}`); - if (answers.checked === false) return null; - return { files: files.map((file) => answers.semantic.get(file) ?? []), global: [] }; - }, - } as unknown as Ts7SessionProgram & { asked: string[] }; -} - -/** The program's files, parsed: the supplied two and the given others. */ -function files(texts: Record): (name: string) => SourceFile | undefined { - const parsed = new Map([ - [lib, parseSourceFile(lib, libText, "ts")], - [fallback, parseSourceFile(fallback, fallbackText, "ts")], - ]); - for (const [name, text] of Object.entries(texts)) - parsed.set(name, parseSourceFile(name, text, "ts")); - return (name) => parsed.get(name); -} - -const names = [lib, fallback, main]; -const supplied = (name: string): boolean => name === lib || name === fallback; -const moduleFile = files({ [main]: "export const n: number = 'x';\n" }); - -test("without a store the full check answers", () => { - const program = sessionProgram({ semantic: new Map() }); - expect(reusedLibDiagnostics(program, names, supplied, moduleFile)).toBeNull(); - expect(program.asked).toEqual([]); -}); - -test("a stored answer stands in for checking the supplied files, merged by file name", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const semantic = new Map([ - [lib, [diagnostic(lib, 1)]], - [fallback, []], - [main, [diagnostic(main, 5), diagnostic(main, 9)]], - ]); - const first = sessionProgram({ semantic }); - const full = reusedLibDiagnostics(first, names, supplied, moduleFile); - // The first build runs the full check and stores the supplied files' answers. - expect(first.asked).toEqual([ - "getGlobalDiagnostics", - "getSemanticDiagnostics", - "checked ", - `getSemanticDiagnostics ${lib}`, - `getSemanticDiagnostics ${fallback}`, - ]); - expect(store.entries.size).toBe(1); - const second = sessionProgram({ semantic }); - const reused = reusedLibDiagnostics(second, names, supplied, moduleFile); - expect(second.asked).toEqual([`checked ${main}`]); - // The files' diagnostics in the order of their names (lib.es5.d.ts, main.ts), as the full answer. - expect(reused).toEqual({ - semantic: [diagnostic(lib, 1), diagnostic(main, 5), diagnostic(main, 9)], - global: [], - }); - expect(full?.semantic).toEqual(reused?.semantic); -}); - -test("files merge in code point order, which differs from UTF-16 order", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const all = [lib, fallback, util, other]; - const sources = files({ [util]: "export {};\n", [other]: "export {};\n" }); - const semantic = new Map([ - [util, [diagnostic(util, 1)]], - [other, [diagnostic(other, 2)]], - ]); - reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); - const reused = reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, sources); - // U+FFFD sorts before U+1F600 by code point (its surrogates sort after it by UTF-16 unit). - expect(reused?.semantic).toEqual([diagnostic(other, 2), diagnostic(util, 1)]); -}); - -test("a full check with global diagnostics is not stored", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const program = sessionProgram({ - semantic: new Map([[main, [diagnostic(main, 1)]]]), - global: [diagnostic(undefined, 0, 2318)], - }); - expect(reusedLibDiagnostics(program, names, supplied, moduleFile)?.global).toEqual([ - diagnostic(undefined, 0, 2318), - ]); - expect(store.entries.size).toBe(0); -}); - -test("scripts reuse the answer unless they declare a global the supplied files declare", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const semantic = new Map([[main, []]]); - // A script declaring only new names: it stores the answer, and a later one reuses it. - const fresh = files({ - [main]: "const total = 1;\nfunction run() {}\nif (total) { var later = 2; }\n", - }); - reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, fresh); - expect(store.entries.size).toBe(1); - const again = sessionProgram({ semantic }); - expect(reusedLibDiagnostics(again, names, supplied, fresh)).not.toBeNull(); - expect(again.asked).toEqual([`checked ${main}`]); - // Scripts declaring a supplied global, at the top level, in a block, in a - // `declare global` of a supplied module, or an ambient module: the full check. - for (const text of [ - "interface Array { extra: T }\n", - "if (true) { var NaN = 1; }\n", - "declare var process: unknown;\n", - 'declare module "fs" {}\n', - ]) { - const program = sessionProgram({ semantic }); - const result = reusedLibDiagnostics(program, names, supplied, files({ [main]: text })); - expect(program.asked[0], text).not.toBe(`checked ${main}`); - if (result !== null) expect(program.asked[0]).toBe("getGlobalDiagnostics"); - } - expect(store.entries.size).toBe(1); -}); - -test("a program whose script declares a supplied global does not store its answer", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const program = sessionProgram({ semantic: new Map([[main, []]]) }); - reusedLibDiagnostics(program, names, supplied, files({ [main]: "declare var NaN: number;\n" })); - expect(store.entries.size).toBe(0); -}); - -test("modules that can declare something global, and servers without the method, ask as before", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const semantic = new Map([[main, []]]); - reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); - expect(store.entries.size).toBe(1); - for (const text of [ - "export {};\ndeclare global { var extra: number }\n", - 'export {};\ndeclare module "./other" { const extra: number }\n', - "export as namespace Lib;\nexport const n = 1;\n", - ]) { - const program = sessionProgram({ semantic }); - expect(reusedLibDiagnostics(program, names, supplied, files({ [main]: text }))).toBeNull(); - expect(program.asked).toEqual([]); - } - const old = sessionProgram({ semantic, checked: false }); - expect(reusedLibDiagnostics(old, names, supplied, moduleFile)).toBeNull(); - // Nor does a server without the method store an answer it could not use. - const fresh = memoryStore(); - setLibDiagnosticsStore(fresh, "checker"); - reusedLibDiagnostics(sessionProgram({ semantic, checked: false }), names, supplied, moduleFile); - expect(fresh.entries.size).toBe(0); -}); - -test("a supplied file that changes is a different entry", () => { - const store = memoryStore(); - setLibDiagnosticsStore(store, "checker"); - const semantic = new Map([[main, []]]); - reusedLibDiagnostics(sessionProgram({ semantic }), names, supplied, moduleFile); - writeFileSync(fallback, fallbackText + "// edited\n"); - const program = sessionProgram({ semantic }); - reusedLibDiagnostics(program, names, supplied, moduleFile); - expect(program.asked[0]).toBe("getGlobalDiagnostics"); - expect(store.entries.size).toBe(2); - writeFileSync(fallback, fallbackText); -}); diff --git a/packages/compiler/src/frontend/ts7/lib-diagnostics.ts b/packages/compiler/src/frontend/ts7/lib-diagnostics.ts deleted file mode 100644 index 827b337bd..000000000 --- a/packages/compiler/src/frontend/ts7/lib-diagnostics.ts +++ /dev/null @@ -1,324 +0,0 @@ -/* The semantic diagnostics of the declaration files scriptc supplies (the - * TypeScript libs and scriptc's own declaration files), reused across - * builds. Checking them is most of the full check of a small program. When - * no other file of a program can change what checking those files reports - * (every one is a module without global or module augmentations, or a - * script declaring no global name the supplied files declare), the answer - * depends only on the checker, the compiler options and the files - * themselves, so a stored answer for the same three stands in for checking - * them again: the server checks only the program's other files - * (getDiagnosticsOfCheckedFiles), and the semantic diagnostics merge in the - * order the full check sorts them, by file name. An answer is stored only - * when the full check reports no global diagnostics: the global diagnostics - * of checking the other files are then the full check's. */ -import { createHash } from "node:crypto"; -import { statSync } from "node:fs"; -import type { Node, SourceFile } from "./ast-types.js"; -import type { Ts7DiagnosticData } from "./session-schema.generated.js"; -import type { Ts7SessionProgram } from "./session.js"; -import { NodeFlags, SyntaxKind } from "./enums.js"; -import { walkPreorder } from "./ast.js"; - -/** Key-value storage for reused diagnostics (best effort: failures miss). */ -export interface LibDiagnosticsStore { - read(key: string): string | null; - write(key: string, text: string): void; -} - -const SCHEMA = "scriptc.lib-diagnostics.v1"; - -let configured: { store: LibDiagnosticsStore; checker: string } | null = null; - -/** Has later builds of this process reuse the diagnostics of the supplied - * declaration files through `store`, for the checker identified by - * `checker` (its executable's identity), or stop reusing them (null). */ -export function setLibDiagnosticsStore(store: LibDiagnosticsStore | null, checker = ""): void { - configured = store === null ? null : { store, checker }; -} - -/** A checker executable's identity for the stored diagnostics' key: its - * path and file identity, or the path alone when it cannot be read. */ -export function checkerExecutableIdentity(path: string): string { - try { - const info = statSync(path); - return JSON.stringify([path, info.size, info.mtimeMs, info.ino]); - } catch { - return JSON.stringify([path]); - } -} - -/** The program's semantic and global diagnostics as the full check reports - * them, using stored diagnostics for the supplied declaration files; null - * when they cannot be reused for this program (then the caller asks for - * them as before). `supplied` tells whether a program file is one of the - * supplied declaration files. */ -export function reusedLibDiagnostics( - program: Ts7SessionProgram, - fileNames: readonly string[], - supplied: (fileName: string) => boolean, - sourceFile: (fileName: string) => SourceFile | undefined, -): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { - if (configured === null || process.env["SCRIPTC_LIB_DIAGNOSTICS"] === "0") return null; - const { store, checker } = configured; - const libs: string[] = []; - const own: string[] = []; - for (const name of fileNames) (supplied(name) ? libs : own).push(name); - if (libs.length === 0) return null; - // The global names each of the program's scripts declares; a module that - // declares nothing global contributes none. - const scripts: Set[] = []; - for (const name of own) { - const file = sourceFile(name); - if (file === undefined) return null; - if (file.externalModuleIndicator !== undefined) { - if (!declaresNothingGlobal(file)) return null; - continue; - } - const names = isJsFileName(name) ? null : scriptGlobalNames(file); - if (names === null) return null; - scripts.push(names); - } - // A script's globals cannot change what the supplied files report unless - // one of them is also a global the supplied files declare. - const collides = (globals: ReadonlySet): boolean => - scripts.some((names) => [...names].some((name) => globals.has(name))); - // The options without the configuration file's path: it names where the - // project is, which checking the supplied files does not read (the files a - // project's location brings in are part of the key). - const options = { ...program.getCompilerOptions(), configFilePath: undefined }; - const key = entryKey(checker, options, libs); - if (key === null) return null; - const stored = readEntry(store, key, libs); - if (stored !== null && !collides(stored.globals)) { - const checked = program.checkedFileDiagnosticData(own); - if (checked === null) return null; - const byFile = new Map(stored.files); - own.forEach((name, index) => byFile.set(name, checked.files[index] ?? [])); - return { semantic: inFileOrder(byFile), global: checked.global }; - } - // The full check, as before. Its answers for the supplied files are stored - // when it reports no global diagnostics and the program's scripts declare - // none of the supplied files' globals (so nothing of the program reached - // what those files report). - const global = program.diagnosticData("getGlobalDiagnostics"); - const semantic = program.diagnosticData("getSemanticDiagnostics"); - // A server that cannot check only some files could never use the answer. - if (global.length === 0 && stored === null && program.checkedFileDiagnosticData([]) !== null) { - const globals = suppliedGlobalNames(libs, sourceFile); - if (globals !== null && !collides(globals)) { - const files: Record = {}; - for (const name of libs) files[name] = program.diagnosticData("getSemanticDiagnostics", name); - try { - store.write(key, JSON.stringify({ schema: SCHEMA, globals: [...globals], files })); - } catch { - // Best effort: a failed write is a later miss. - } - } - } - return { semantic, global }; -} - -/** Whether a file cannot add to or change the global declarations the - * supplied files declare: a module with no `declare global`, no module - * augmentation and no UMD `export as namespace` declaration. */ -function declaresNothingGlobal(file: SourceFile): boolean { - if (file.externalModuleIndicator === undefined) return false; - if (file.moduleAugmentations.length > 0) return false; - for (const statement of file.statements.nodes) { - if (statement.kind === SyntaxKind.NamespaceExportDeclaration) return false; - // `declare global` (a module declaration named by the identifier `global`). - if ( - statement.kind === SyntaxKind.ModuleDeclaration && - statement.name?.kind === SyntaxKind.Identifier && - statement.name.text === "global" - ) - return false; - } - return true; -} - -function isJsFileName(name: string): boolean { - return /\.[cm]?jsx?$/.test(name); -} - -/** The names a script file declares in the global scope: its top-level - * declarations, and the `var` and function declarations of its top-level - * blocks; null when it declares an ambient module (`declare module "x"`), - * which can merge with one the supplied files declare. Over-approximates: - * a name more only makes the stored answer apply less often. */ -function scriptGlobalNames(file: SourceFile): Set | null { - const names = new Set(); - let ambientModule = false; - walkPreorder(file, (node) => { - if (node === file) return undefined; - if (node.kind === SyntaxKind.ModuleDeclaration && node.name?.kind === SyntaxKind.StringLiteral) - ambientModule = true; - const topLevel = node.parent === file; - if (topLevel || node.kind === SyntaxKind.FunctionDeclaration) addDeclaredNames(node, names); - else if ( - node.kind === SyntaxKind.VariableDeclarationList && - (node.flags & NodeFlags.BlockScoped) === 0 - ) - for (const declaration of node.declarations?.nodes ?? []) - addBindingNames(declaration.name, names); - // Functions, classes and namespaces have scopes of their own. - return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; - }); - return ambientModule ? null : names; -} - -/** The global names the supplied files declare: the declarations of their - * scripts (as scriptGlobalNames finds them, with ambient modules as their - * quoted names) and those of the `declare global` blocks of their modules; - * null when one of them is not available. */ -function suppliedGlobalNames( - libs: readonly string[], - sourceFile: (fileName: string) => SourceFile | undefined, -): Set | null { - const names = new Set(); - for (const name of libs) { - const file = sourceFile(name); - if (file === undefined) return null; - if (file.externalModuleIndicator === undefined) { - walkPreorder(file, (node) => { - if (node === file) return undefined; - if ( - node.kind === SyntaxKind.ModuleDeclaration && - node.name?.kind === SyntaxKind.StringLiteral - ) - names.add(JSON.stringify(node.name.text ?? "")); - if (node.parent === file || node.kind === SyntaxKind.FunctionDeclaration) - addDeclaredNames(node, names); - else if ( - node.kind === SyntaxKind.VariableDeclarationList && - (node.flags & NodeFlags.BlockScoped) === 0 - ) - for (const declaration of node.declarations?.nodes ?? []) - addBindingNames(declaration.name, names); - return GLOBAL_SCOPE_BOUNDARIES.has(node.kind) ? "skip" : undefined; - }); - continue; - } - for (const statement of file.statements.nodes) { - if ( - statement.kind !== SyntaxKind.ModuleDeclaration || - statement.name?.kind !== SyntaxKind.Identifier || - statement.name.text !== "global" - ) - continue; - for (const inner of statement.body?.statements?.nodes ?? []) addDeclaredNames(inner, names); - } - } - return names; -} - -const GLOBAL_SCOPE_BOUNDARIES = new Set([ - SyntaxKind.FunctionDeclaration, - SyntaxKind.FunctionExpression, - SyntaxKind.ArrowFunction, - SyntaxKind.MethodDeclaration, - SyntaxKind.Constructor, - SyntaxKind.GetAccessor, - SyntaxKind.SetAccessor, - SyntaxKind.ClassDeclaration, - SyntaxKind.ClassExpression, - SyntaxKind.ModuleDeclaration, -]); - -/** The names a statement declares in the scope it is in. */ -function addDeclaredNames(node: Node, names: Set): void { - switch (node.kind) { - case SyntaxKind.VariableStatement: - for (const declaration of node.declarationList?.declarations?.nodes ?? []) - addBindingNames(declaration.name, names); - return; - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - if (node.name?.kind === SyntaxKind.Identifier) names.add(node.name.text ?? ""); - return; - } -} - -function addBindingNames(name: Node | undefined, names: Set): void { - if (name === undefined) return; - if (name.kind === SyntaxKind.Identifier) { - names.add(name.text ?? ""); - return; - } - // Object and array binding patterns. - for (const element of name.elements?.nodes ?? []) addBindingNames(element.name, names); -} - -/** The stored diagnostics' key: the checker, the compiler options and the - * supplied files in program order with each one's file identity; null when - * one of them cannot be read. */ -function entryKey(checker: string, options: unknown, libs: readonly string[]): string | null { - const files: unknown[] = []; - for (const name of libs) { - try { - const info = statSync(name); - files.push([name, info.size, info.mtimeMs, info.ino]); - } catch { - return null; - } - } - return createHash("sha256") - .update(JSON.stringify({ schema: SCHEMA, checker, options, files })) - .digest("hex"); -} - -function readEntry( - store: LibDiagnosticsStore, - key: string, - libs: readonly string[], -): { files: Map; globals: Set } | null { - let text: string | null; - try { - text = store.read(key); - } catch { - return null; - } - if (text === null) return null; - let entry: { schema?: unknown; globals?: unknown; files?: Record }; - try { - entry = JSON.parse(text) as typeof entry; - } catch { - return null; - } - if (entry.schema !== SCHEMA || entry.files === undefined || !Array.isArray(entry.globals)) - return null; - const files = new Map(); - for (const name of libs) { - const list = entry.files[name]; - if (!Array.isArray(list)) return null; - files.set(name, list); - } - return { files, globals: new Set(entry.globals.filter((name) => typeof name === "string")) }; -} - -/** Each file's diagnostics (already in the checker's order) in the order the - * checker sorts files: by file name, compared by code point. */ -function inFileOrder(byFile: ReadonlyMap): Ts7DiagnosticData[] { - const names = [...byFile.keys()].sort(compareCodePoints); - const out: Ts7DiagnosticData[] = []; - for (const name of names) for (const d of byFile.get(name)!) out.push(d); - return out; -} - -function compareCodePoints(a: string, b: string): number { - let i = 0; - let j = 0; - while (i < a.length && j < b.length) { - const x = a.codePointAt(i)!; - const y = b.codePointAt(j)!; - if (x !== y) return x < y ? -1 : 1; - i += x > 0xffff ? 2 : 1; - j += y > 0xffff ? 2 : 1; - } - return i < a.length ? 1 : j < b.length ? -1 : 0; -} diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index ec143e401..639086e94 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -46,10 +46,6 @@ import { trackedRealpath, } from "../input-tracker.js"; import { packageShimDeclarationText } from "../package-shims.js"; -import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.js"; -import { reusedLibDiagnostics } from "./lib-diagnostics.js"; -import { ts7Diagnostic } from "./diagnostic.js"; -import type { Ts7DiagnosticData } from "./session-schema.generated.js"; /** The compiler options our createProgram accepts: TS7's CompilerOptions * shape (numeric enums for target/module/moduleResolution — the enums module @@ -397,22 +393,6 @@ export class Ts7Program { return this.project.program.getSourceFileMetadata(resolve(fileName)); } - /** The semantic and global diagnostics with the stored diagnostics of the - * declaration files scriptc supplies (lib-diagnostics.ts), or null. */ - reusedLibDiagnostics(): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null { - this.snapshot.ensureActive(); - const program = this.project.program; - const declarations = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]); - return reusedLibDiagnostics( - program, - this.getSourceFileNames(), - (name) => - declarations.has(tsgoPath(name)) || - program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true, - (name) => this.getSourceFile(name), - ); - } - isSourceFileDefaultLibrary(file: SourceFile): boolean { return this.project.program.isSourceFileDefaultLibrary(file); } @@ -443,20 +423,9 @@ export class Ts7Program { * package dropped the aggregate). Order mirrors 5.9.3: config, options/ * program, global, syntactic, semantic. */ export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[] { - const config = program.getConfigFileParsingDiagnostics(); - const options = program.getProgramDiagnostics(); - const reused = program.reusedLibDiagnostics(); - if (reused !== null) - return [ - ...config, - ...options, - ...reused.global.map(ts7Diagnostic), - ...program.getSyntacticDiagnostics(), - ...reused.semantic.map(ts7Diagnostic), - ]; return [ - ...config, - ...options, + ...program.getConfigFileParsingDiagnostics(), + ...program.getProgramDiagnostics(), ...program.getGlobalDiagnostics(), ...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics(), diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index 082bb79a6..d3f2ae9fe 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -55,13 +55,6 @@ interface ProgramQuery { snapshot?: number; project?: string; file?: SemanticDocument; - files?: string[]; -} - -/** The answer of getDiagnosticsOfCheckedFiles, as the server sends it. */ -export interface Ts7CheckedFileDiagnosticData { - files: Ts7DiagnosticData[][]; - global: Ts7DiagnosticData[]; } /** Platform-independent session over an already connected transport. The @@ -351,29 +344,6 @@ export class Ts7SessionProgram { result.push(ts7Diagnostic(data)); return result; } - /** The diagnostics of `method` for `file`, or for the whole program, as - * the server sends them. */ - diagnosticData(method: string, file?: SemanticDocument): Ts7DiagnosticData[] { - return this.session.request(method, this.query(file)) ?? []; - } - /** The semantic diagnostics of each of `files` and the global diagnostics, - * after the server checks only those files (getDiagnosticsOfCheckedFiles), - * or null when the server does not answer that method. */ - checkedFileDiagnosticData(files: readonly string[]): Ts7CheckedFileDiagnosticData | null { - try { - return this.session.request("getDiagnosticsOfCheckedFiles", { - ...this.query(), - files: [...files], - }); - } catch (error) { - if ( - error instanceof Error && - error.message.includes('unknown API method "getDiagnosticsOfCheckedFiles"') - ) - return null; - throw error; - } - } getSyntacticDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSyntacticDiagnostics", file); } diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 586561632..4f7b93cfd 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -9,7 +9,6 @@ import { } from "./library/prepare.js"; import { analyzeWithFrontend } from "./frontend/analysis.js"; import { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js"; -import { setLibDiagnosticsStore } from "./frontend/ts7/lib-diagnostics.js"; import { analyzeWithVerdictCache, fileVerdictStore } from "./coverage/verdict-cache.js"; import { coverageCompilerIdentity } from "./coverage/verdict-startup.js"; import { commentOnlyReplay } from "./coverage/verdict-semantic.js"; @@ -1162,28 +1161,9 @@ async function prepareExecutableInputInChild( * `requestPath` as `compileTracked` would in-process, and writes what it prepared, without the LLVM * text, to `resultPath`. */ -/** Has the build reuse the diagnostics of the declaration files scriptc - * supplies (lib-diagnostics.ts) through the build cache, when there is one and - * the checker is tsc-ts. */ -async function configureLibDiagnostics(): Promise { - let checker: string | undefined; - try { - checker = checkerCacheIdentity(); - } catch { - // A missing checker fails where the build starts it. - checker = undefined; - } - const root = checker === undefined ? null : await prepareBuildCacheRoot(buildCacheRoot()); - setLibDiagnosticsStore( - root === null ? null : fileVerdictStore(join(root, "lib-diagnostics-v1")), - checker ?? "", - ); -} - export async function runFrontendProcess(requestPath: string, resultPath: string): Promise { const request = JSON.parse(await readFile(requestPath, "utf8")) as FrontendProcessRequest; clearCompileSessionCaches(); - await configureLibDiagnostics(); setProvenanceSources(request.provenance); const frontendInputs = new FrontendInputTracker(); const warnings: CompileWarningSink = { warnings: [], sourceTexts: null }; @@ -1246,7 +1226,6 @@ async function compileTracked( const timing = compilationTiming(); const timingStart = Date.now(); entryPath = resolve(entryPath); - await configureLibDiagnostics(); const outputKind = opts.outputKind ?? "exe"; if ( (opts.backend !== undefined && opts.backend !== "llvm") || diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index 44d5eb9f0..21047b929 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -16,11 +16,7 @@ import { createNativeTs7Api } from "../frontend/ts7/native-api.js"; import { resolveProvenanceSourcesWithParser } from "../frontend/provenance-core.js"; import { setCompilerReleaseVersion } from "../library/sidecar.js"; import { NativeCompiler } from "./compiler.js"; -import { NATIVE_STRING_BYTES, openNativeCache } from "./cache.js"; -import { - checkerExecutableIdentity, - setLibDiagnosticsStore, -} from "../frontend/ts7/lib-diagnostics.js"; +import { openNativeCache } from "./cache.js"; import { loadNativeToolchain } from "./toolchain.js"; import { buildSanitizedRuntime } from "./sanitizer.js"; import { runCompilerTask } from "./task.js"; @@ -57,17 +53,6 @@ async function main(): Promise { if (toolchain.declarationsRoot !== undefined) setDeclarationRoot(toolchain.declarationsRoot); setCompilerReleaseVersion(toolchain.compilerVersion); cache = openNativeCache((path, harden) => compilerNativePrivateDirectory(path, harden)); - if (cache !== null) { - const store = cache; - setLibDiagnosticsStore( - { - read: (key) => - store.read("lib-diagnostics", key, NATIVE_STRING_BYTES)?.toString("utf8") ?? null, - write: (key, text) => store.write("lib-diagnostics", key, text), - }, - checkerExecutableIdentity(toolchain.ts7Executable), - ); - } compiler = new NativeCompiler(toolchain, cache); return compiler; }; From a8484aea07e84bef367990532c70ba607d171943 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:22:01 -0500 Subject: [PATCH 083/409] Address review feedback on request-body streaming --- .../compiler/shims/hono-node-server/v2.ts | 88 +++++++++++-------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/packages/compiler/shims/hono-node-server/v2.ts b/packages/compiler/shims/hono-node-server/v2.ts index c46813c7b..a4dcc6805 100644 --- a/packages/compiler/shims/hono-node-server/v2.ts +++ b/packages/compiler/shims/hono-node-server/v2.ts @@ -142,56 +142,72 @@ function requestHeaders(incoming: IncomingMessage): Headers { * (the package's `readableEnded`) and whether a drain started. */ type IncomingState = { ended: boolean; draining: boolean }; -/** The request body as the package streams it: each pull reads one chunk - * of the incoming message, which pauses between pulls, so the socket - * holds what the app has not read yet. The stream pulls once on its own - * when it is constructed; the package builds its stream only when the app - * reads the body, so until the app's first read a drain discards the - * chunks instead of pausing for them. */ +/** The incoming message's readableHighWaterMark: Readable.toWeb queues + * this many bytes before it pauses the message. */ +const BODY_HIGH_WATER_MARK = 64 * 1024; + +/** The request body as the package streams it: Readable.toWeb over the + * incoming message, read one chunk per pull. Chunks queue until + * BODY_HIGH_WATER_MARK bytes wait unread, then the message pauses until a + * pull takes them, so the socket holds the rest. The stream pulls once on + * its own when it is constructed; the package builds its stream only when + * the app reads the body, so until the app's first read a drain discards + * the chunks instead of queueing them. */ function incomingBody(incoming: IncomingMessage, state: IncomingState): ReadableStream { let pulls = 0; let listening = false; - let settled = false; + let ended = false; + let failure: Error | undefined = undefined; + const queue: Buffer[] = []; + let queued = 0; let wake: (() => void) | undefined = undefined; const woken = (): void => { const resolve = wake; wake = undefined; if (resolve !== undefined) resolve(); }; + const listen = (): void => { + listening = true; + incoming.on("data", (chunk: Buffer) => { + if (state.draining && pulls === 1) return; + queue.push(chunk); + queued += chunk.length; + if (queued >= BODY_HIGH_WATER_MARK) incoming.pause(); + woken(); + }); + incoming.on("end", () => { + ended = true; + woken(); + }); + incoming.on("error", (err: Error) => { + failure = err; + woken(); + }); + }; return new ReadableStream({ - pull(controller) { + async pull(controller) { pulls++; - return new Promise((resolve) => { - if (settled) { - resolve(); - return; - } - wake = resolve; - if (listening) { - incoming.resume(); - return; - } - listening = true; - incoming.on("data", (chunk: Buffer) => { - if (state.draining && pulls === 1) return; - incoming.pause(); - if (settled) return; + if (!listening) listen(); + for (;;) { + const chunk = queue.shift(); + if (chunk !== undefined) { + queued -= chunk.length; + if (queued < BODY_HIGH_WATER_MARK) incoming.resume(); controller.enqueue(chunk); - woken(); - }); - incoming.on("end", () => { - if (settled) return; - settled = true; + return; + } + if (failure !== undefined) { + controller.error(failure); + return; + } + if (ended) { controller.close(); - woken(); + return; + } + await new Promise((resolve) => { + wake = resolve; }); - incoming.on("error", (err: Error) => { - if (settled) return; - settled = true; - controller.error(err); - woken(); - }); - }); + } }, }); } From 61bae8c5e077099b4fa0f00be254c2df0d395d8d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:30:24 -0500 Subject: [PATCH 084/409] Keep package-created values of interfaces extending package interfaces dynamic The declaration boundary projected a user-declared interface as plain data even when it extends a package interface. effect's Request.tagged constructors create such values (interface Double extends Request.Request) with a prototype, so the boundary cast threw "expected a plain object (this value has a prototype)". A user interface now projects only when every interface it extends is user-declared too; other values keep their JavaScript-world typing, so the package finds its prototype members when the value goes back to it. --- .../compiler/src/frontend/decl-projection.ts | 17 +++++++++++++++-- .../cases/types-boundary-prototype/main.ts.json | 6 ++++++ .../npm/cases/types-boundary-prototype/main.ts | 13 +++++++++++++ .../node_modules/reqtag/index.d.ts | 8 ++++++++ .../node_modules/reqtag/index.js | 15 +++++++++++++++ .../node_modules/reqtag/package.json | 1 + .../cases/types-boundary-prototype/package.json | 1 + 7 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json create mode 100644 tests/fixtures/npm/cases/types-boundary-prototype/main.ts create mode 100644 tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts create mode 100644 tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js create mode 100644 tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json create mode 100644 tests/fixtures/npm/cases/types-boundary-prototype/package.json diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index f2340e984..eaf7486df 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -252,6 +252,16 @@ class TypePrinter { return props.length > 0 && props.every((p) => String(p.escapedName).startsWith("__@")); } + /** Whether an interface type and every type it extends are declared in + * user files. */ + private userInterface(type: ts.Type, seen = new Set()): boolean { + if (!declaredInUserFile(type.getSymbol())) return false; + const target = type.isTypeReference() ? type.getTarget() : type; + if (target === undefined || !target.isClassOrInterface() || seen.has(target)) return true; + seen.add(target); + return this.checker.getBaseTypes(target).every((base) => this.userInterface(base, seen)); + } + private printObject(type: ts.Type, depth: number): string | null { const checker = this.checker; if (checker.isArrayType(type)) { @@ -280,8 +290,11 @@ class TypePrinter { if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Function | ts.SymbolFlags.Method)) return null; // Interfaces: only user-declared ones describe plain data reliably - // (package and lib interfaces often describe class instances). - if (symbol.flags & ts.SymbolFlags.Interface && !declaredInUserFile(symbol)) return null; + // (package and lib interfaces often describe class instances), and + // only when every interface they extend is user-declared too: a user + // interface extending a package one (effect's Request.Request) + // inherits members the package's own constructors create. + if (symbol.flags & ts.SymbolFlags.Interface && !this.userInterface(type)) return null; } if ( checker.getCallSignatures(type).length > 0 || diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json new file mode 100644 index 000000000..2abe1bd8f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/types-boundary-prototype/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/types-boundary-prototype/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/main.ts b/tests/fixtures/npm/cases/types-boundary-prototype/main.ts new file mode 100644 index 000000000..6645535e4 --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/main.ts @@ -0,0 +1,13 @@ +// A user interface extending a package interface types values the +// package's constructors create with a prototype (effect's Request.tagged +// pattern): they stay JavaScript values, so the package still finds its +// prototype members when the value goes back to it. +import { run, tagged, type Req } from "reqtag"; + +interface Double extends Req { + readonly _tag: "Double"; + readonly value: number; +} +const Double = tagged("Double"); +const request = Double({ value: 21 }); +console.log(run(request), request.value * 2, request._tag); diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts new file mode 100644 index 000000000..731b6a07f --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.d.ts @@ -0,0 +1,8 @@ +export declare const ReqTypeId: unique symbol; +export interface Req { + readonly [ReqTypeId]: { readonly _A: A }; +} +export declare function tagged & { readonly _tag: string }>( + tag: R["_tag"], +): (args: Omit) => R; +export declare function run(request: Req): string; diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js new file mode 100644 index 000000000..196bf555e --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/index.js @@ -0,0 +1,15 @@ +// Requests share a prototype that carries a symbol-keyed marker and a +// method, as effect's Request.tagged constructors do. +export const ReqTypeId = Symbol.for("reqtag/Req"); +const proto = { + [ReqTypeId]: true, + describe() { + return `${this._tag}(${this.value})`; + }, +}; +export function tagged(tag) { + return (args) => Object.assign(Object.create(proto), args, { _tag: tag }); +} +export function run(request) { + return request[ReqTypeId] === true ? request.describe() : "not a request"; +} diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json new file mode 100644 index 000000000..e461981ee --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/node_modules/reqtag/package.json @@ -0,0 +1 @@ +{ "name": "reqtag", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/fixtures/npm/cases/types-boundary-prototype/package.json b/tests/fixtures/npm/cases/types-boundary-prototype/package.json new file mode 100644 index 000000000..ac2988811 --- /dev/null +++ b/tests/fixtures/npm/cases/types-boundary-prototype/package.json @@ -0,0 +1 @@ +{ "name": "types-boundary-prototype-case", "private": true, "type": "module" } From 40c3f69b4091590c9840002157b1114ffb5e66cf Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:31:21 -0500 Subject: [PATCH 085/409] Read an AbortSignal's EventTarget methods as EventTarget's own An AbortSignal answered addEventListener, removeEventListener and dispatchEvent read as values with functions of its own, which only ran on AbortSignal handles: calling one on a plain EventTarget threw ERR_INVALID_THIS. In Node they are EventTarget.prototype's methods, shared with every target. The signal's property read now answers the EventTarget method values (scr_event_target_method_value), which run on any EventTarget receiver, AbortSignal included, so signal.addEventListener === new EventTarget().addEventListener holds. The signal's own method table keeps throwIfAborted. --- packages/runtime/src/scr_event_target.c | 4 ++++ packages/runtime/src/scr_event_target.h | 5 +++++ packages/runtime/src/scr_fetch.c | 22 ++++++++-------------- tests/corpus/7740-handle-method-values.js | 16 ++++++++++++++++ 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/packages/runtime/src/scr_event_target.c b/packages/runtime/src/scr_event_target.c index 7c8a465a6..597eeca9b 100644 --- a/packages/runtime/src/scr_event_target.c +++ b/packages/runtime/src/scr_event_target.c @@ -1054,6 +1054,10 @@ static ScrDyn *scr_et_method_value(const char *key, size_t len, bool target) { return NULL; } +ScrDyn *scr_event_target_method_value(const char *key, size_t len) { + return scr_et_method_value(key, len, true); +} + static ScrDyn *scr_event_target_value(const ScrEvent *ev) { return ev->target ? scr_dyn_retain(ev->target) : scr_dyn_new_null(); } diff --git a/packages/runtime/src/scr_event_target.h b/packages/runtime/src/scr_event_target.h index e787cdbdb..e1ff5d0da 100644 --- a/packages/runtime/src/scr_event_target.h +++ b/packages/runtime/src/scr_event_target.h @@ -56,6 +56,11 @@ void scr_et_core_drop(ScrEventTargetCore *core); ScrDyn *scr_et_add_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); ScrDyn *scr_et_remove_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); ScrDyn *scr_et_dispatch_event(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *const *args, size_t argc); +/* EventTarget.prototype's method `key` (addEventListener, + * removeEventListener, dispatchEvent) as a function value (+1), the one + * every EventTarget answers, AbortSignal included: it runs on any target + * its `this` is. NULL for another name. */ +ScrDyn *scr_event_target_method_value(const char *key, size_t len); /* Dispatches an event handle (borrowed) without the argument checks. */ void scr_et_dispatch(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *event); /* `on` attributes (Node's defineEventHandler): get answers +1 (null diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 916fc6bf9..c8847f335 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -713,7 +713,8 @@ static ScrDyn *sf_signal_get(void *ptr, const char *key, size_t len) { : scr_dyn_retain(scr_dyn_undefined()); } if (sf_name(key, len, "onabort")) return scr_et_handler_get(&s->core, "abort"); - return NULL; + /* Inherited from EventTarget: the function every target shares. */ + return scr_event_target_method_value(key, len); } static bool sf_signal_set(void *ptr, const char *key, size_t len, @@ -5496,13 +5497,10 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, } /* The members invoke answers, as values (ScrDynHandleMethod), with Node's - * lengths and the brand check each one runs on another receiver. */ + * lengths and the brand check each one runs on another receiver. The + * signal's EventTarget methods are EventTarget's own (sf_signal_get). */ static const ScrDynHandleMethod sf_signal_methods[] = { - {"throwIfAborted", 0, false, - "Value of \"this\" must be of type AbortSignal"}, - {"addEventListener", 2, false}, - {"removeEventListener", 2, false}, {"dispatchEvent", 1, false}, - {NULL, 0, false}}; + {"throwIfAborted", 0, false}, {NULL, 0, false}}; static const ScrDynHandleMethod sf_stream_methods[] = { {"cancel", 0, true}, {"getReader", 0, false}, {NULL, 0, false}}; static const ScrDynHandleMethod sf_reader_methods[] = { @@ -5529,7 +5527,7 @@ static const ScrDynHandleOps sf_signal_ops = { "AbortSignal", &sf_signal_retain_v, &sf_signal_release_v, &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL, .methods = sf_signal_methods, - .invalid_this = "Value of \"this\" must be of type EventTarget"}; + .invalid_this = "Value of \"this\" must be of type AbortSignal"}; static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL}; @@ -5613,16 +5611,12 @@ static bool sf_no_set(void *ptr, const char *key, size_t len, const ScrDyn *valu return false; } static const ScrDynHandleMethod sf_signal_methods[] = { - {"throwIfAborted", 0, false, - "Value of \"this\" must be of type AbortSignal"}, - {"addEventListener", 2, false}, - {"removeEventListener", 2, false}, {"dispatchEvent", 1, false}, - {NULL, 0, false}}; + {"throwIfAborted", 0, false}, {NULL, 0, false}}; static const ScrDynHandleOps sf_signal_ops = { "AbortSignal", &sf_signal_retain_v, &sf_signal_release_v, &sf_signal_invoke, &sf_signal_get, &sf_signal_set, NULL, .methods = sf_signal_methods, - .invalid_this = "Value of \"this\" must be of type EventTarget" }; + .invalid_this = "Value of \"this\" must be of type AbortSignal" }; static const ScrDynHandleOps sf_abort_controller_ops = { "AbortController", &sf_signal_retain_v, &sf_signal_release_v, &sf_abort_controller_invoke, &sf_abort_controller_get, &sf_no_set, NULL }; diff --git a/tests/corpus/7740-handle-method-values.js b/tests/corpus/7740-handle-method-values.js index 3694c204f..dffcba306 100644 --- a/tests/corpus/7740-handle-method-values.js +++ b/tests/corpus/7740-handle-method-values.js @@ -85,6 +85,22 @@ async function receivers() { } catch (e) { report("foreign addEventListener", e); } + // The signal's EventTarget methods are EventTarget's own: one function + // shared with every target, which runs on any EventTarget receiver. + const target = untyped(new EventTarget()); + const add = signal.s.addEventListener; + console.log(add === target.addEventListener, signal.s.dispatchEvent === target.dispatchEvent); + console.log(add.name, add.length, "addEventListener" in signal.s, "throwIfAborted" in target); + const heard = (event) => console.log("heard", event.type, event.target === target); + add.call(target, "ping", heard); + console.log(signal.s.dispatchEvent.call(target, new Event("ping"))); + signal.s.removeEventListener.call(target, "ping", heard); + target.dispatchEvent(new Event("ping")); + try { + throwIfAborted.call(target); + } catch (e) { + report("target throwIfAborted", e); + } } async function streams() { From 967f2ec85dcd408bea5d24e2e425aafd3ea248ac Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:55:39 -0500 Subject: [PATCH 086/409] Compile minified npm entries and untyped regex arguments Refs #1777 npm packages are no longer refused up front for minified JavaScript. A bundle whose interop helpers lost their names is still refused by the bundle recognizers, and a package whose attempt fails is still refused on its own. A `var` read in a later declarator of the statement that stores it (`var a = {...}, b = {...a}`) runs after the store. A read in a function body is now located at the body's own statement instead of the body, so JavaScript `var` bindings read after their store in a function keep their type instead of gaining undefined. `replace` and `replaceAll` with an untyped search value and a callback call the regex callback replacement for a RegExp and the string-search helper otherwise. That helper now accepts untyped callback parameters, passes undefined to parameters after the subject, and converts number, boolean and untyped results with ToString. In JavaScript, `||` and `&&` over primitives of different types whose result tsc types `any` evaluate in the checked domain. --- docs/content/docs/dependencies.mdx | 2 +- packages/compiler/src/cli/usage.ts | 6 +- .../lowering/containers/string-replacement.ts | 64 +++++++++---- .../frontend/lowering/expressions/binary.ts | 18 ++++ .../src/frontend/lowering/var-hoisting.ts | 43 ++++++--- packages/compiler/src/frontend/npm-static.ts | 26 ++---- .../7543-var-list-declarator-order.js.json | 6 ++ ...545-string-regex-checked-arguments.js.json | 6 ++ .../7546-minified-esm-rules/main.ts.json | 6 ++ .../corpus/7543-var-list-declarator-order.js | 66 ++++++++++++++ .../7545-string-regex-checked-arguments.js | 89 +++++++++++++++++++ .../npm/cases/7546-minified-esm-rules/main.ts | 22 +++++ .../node_modules/gtminimark/dist/index.d.ts | 1 + .../npm/node_modules/gtminimark/dist/index.js | 1 + .../npm/node_modules/gtminimark/package.json | 8 ++ .../7543-var-list-declarator-order.js.json | 1 + ...545-string-regex-checked-arguments.js.json | 1 + tests/harness/library-mode.test.ts | 26 +++--- tests/harness/npm-static.test.ts | 9 ++ tests/library-mode/npm-refuse/lib-minified.ts | 4 +- tests/library-mode/npm-static/lib.ts | 2 +- .../npm-static/node_modules/mathkit/index.js | 2 +- 22 files changed, 340 insertions(+), 69 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7543-var-list-declarator-order.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7545-string-regex-checked-arguments.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts.json create mode 100644 tests/corpus/7543-var-list-declarator-order.js create mode 100644 tests/corpus/7545-string-regex-checked-arguments.js create mode 100644 tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts create mode 100644 tests/fixtures/npm/node_modules/gtminimark/dist/index.d.ts create mode 100644 tests/fixtures/npm/node_modules/gtminimark/dist/index.js create mode 100644 tests/fixtures/npm/node_modules/gtminimark/package.json create mode 100644 tests/harness/corpus-costs/7543-var-list-declarator-order.js.json create mode 100644 tests/harness/corpus-costs/7545-string-regex-checked-arguments.js.json diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index 14130b234..f02dcfbf0 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -53,7 +53,7 @@ Type-only imports follow Node.js's type stripping. `import type` and `export typ scriptc compiles a package automatically when it: - ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and -- ships readable JavaScript: unminified, or minified by a bundler whose helpers keep their names. +- ships JavaScript that isn't a bundle whose bundler-interop helpers, such as `__commonJS`, have lost their names. Minified JavaScript compiles like unminified JavaScript. The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use. diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index 23e57c66f..c4d07656a 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -77,9 +77,9 @@ Options: --npm-static compile the named npm packages' shipped JS statically as program modules (repeatable; "auto" opts in every eligible - direct import: own .d.ts, unminified JS, no build-transform - markers; "pkg..." also names every package pkg depends - on). Packages that fail preflight are refused with a + direct import: own .d.ts, no build-transform markers; + "pkg..." also names every package pkg depends on). + Packages that fail preflight are refused with a diagnostic. Eligible dependencies compile automatically. --provenance-sources EXPERIMENTAL: compile npm dependencies from their diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 780c77d0f..156f74869 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -2,6 +2,7 @@ import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { BOOL, + DYN, F64, REGEX, STRING, @@ -14,7 +15,7 @@ import { type IrType, type SrcLoc, } from "../../../ir/ir.js"; -import { numLit, strLit, varRef } from "../../../ir/build.js"; +import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import { locOf } from "../../program.js"; import type { Lowerer } from "../lowerer.js"; @@ -65,14 +66,14 @@ export function lowerStringReplacement( const search = argument(args[0], false); const replacement = argument(args[1], true); if (search.type.kind === "dyn" || replacement.type.kind === "dyn") { - if (replacement.type.kind === "func") { - lowerer.noLowering(`string .${method} with checked search values and callbacks`, call); - } // Evaluate all inputs once before choosing the search protocol. Regexes // retain their native pattern; plain values use the string-search helper. + // A typed callback receives the regex match's arguments or the plain + // search's (match, offset, subject). const values = [receiver, search, replacement]; const locals = values.map((value) => lowerer.declareHiddenLocal("%replaceArg", value.type)); const refs = locals.map((local) => varRef(local.id, local.type, loc)); + const callback = replacement.type.kind === "func"; const checkedReplacement = replacement.type.kind === "dyn" ? lowerer.coerceInto(args[1] ?? call, refs[2]!, STRING) @@ -91,14 +92,27 @@ export function lowerStringReplacement( ? { kind: "ternary", cond: { kind: "libCall", fn: "dyn.nativeRegexIs", args: [refs[1]!], type: BOOL, loc }, - then: { - kind: "regexIntrinsic", - method, - receiver: refs[0]!, - args: [lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), checkedReplacement], - type: STRING, - loc, - }, + then: callback + ? { + kind: "libCall", + fn: "regex.replaceCallback", + args: [ + refs[0]!, + lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), + lowerer.coerceToExpected(refs[2]!, DYN), + { kind: "boolLit", value: method === "replaceAll", type: BOOL, loc }, + ], + type: STRING, + loc, + } + : { + kind: "regexIntrinsic", + method, + receiver: refs[0]!, + args: [lowerer.coerceInto(args[0] ?? call, refs[1]!, REGEX), checkedReplacement], + type: STRING, + loc, + }, else_: plain, type: STRING, loc, @@ -148,11 +162,17 @@ function lowerStringReplacementValues( if (callback.kind !== "func") { throw new InternalCompilerError(`string .${method} replacement of type ${callback.kind}`); } + // A checked parameter receives the boxed argument (undefined after the + // subject), and a checked, number or boolean result converts with + // ToString. const full = [STRING, F64, STRING]; + const ret = callback.ret.kind; if ( - callback.ret.kind !== "string" || - callback.params.length > full.length || - !callback.params.every((type, index) => typeEquals(type, full[index]!)) + (ret !== "string" && ret !== "dyn" && ret !== "f64" && ret !== "bool") || + !callback.params.every( + (type, index) => + type.kind === "dyn" || (index < full.length && typeEquals(type, full[index]!)), + ) ) { lowerer.noLowering( `string .${method} callback signature`, @@ -175,13 +195,21 @@ function lowerStringReplacementValues( const chunksType = arrayOf(STRING); const chunks = varRef("chunks.0", chunksType, loc); const builder = stringBuilder(loc, chunks); - const value: IrExpr = { + const result: IrExpr = { kind: "callValue", callee: replace, - args: [needle, match, subject].slice(0, callback.params.length), - type: STRING, + args: callback.params.map((type, index) => { + const arg = [needle, match, subject][index]; + if (arg === undefined) return dynUndefinedExpr(loc); + return type.kind === "dyn" ? lowerer.coerceToExpected(arg, DYN) : arg; + }), + type: callback.ret, loc, }; + const value: IrExpr = + ret === "dyn" + ? { kind: "libCall", fn: "dyn.toStringCoerce", args: [result], type: STRING, loc } + : lowerer.ensureString(result, call); const stop: IrStmt = { kind: "break", loc }; const body: IrStmt[] = [ builder.declare("chunks.0", { kind: "arrayLit", elems: [], type: chunksType, loc }), diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 58f6e16b5..17aef7b85 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -2841,6 +2841,24 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE loc, }; } + // A JavaScript default that tsc types `any` (`e.title || null` in an + // instance of a function with untyped parameters, where `e.title` is a + // string): the checked domain, as for union operands above. + if ( + target === null && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && + lowerer.dynConvertible(left.type) && + lowerer.dynConvertible(right.type) + ) { + return { + kind: "logical", + op: op === ts.SyntaxKind.AmpersandAmpersandToken ? "&&" : "||", + left: lowerer.coerceInto(expr.left, left, DYN), + right: lowerer.coerceInto(expr.right, right, DYN), + type: DYN, + loc, + }; + } lowerer.unsupported("SC1042", expr); } return { diff --git a/packages/compiler/src/frontend/lowering/var-hoisting.ts b/packages/compiler/src/frontend/lowering/var-hoisting.ts index 3f950f3ca..1a60e7fda 100644 --- a/packages/compiler/src/frontend/lowering/var-hoisting.ts +++ b/packages/compiler/src/frontend/lowering/var-hoisting.ts @@ -194,6 +194,15 @@ function referencesBySymbol( return out; } +/** Where a binding is first stored unconditionally: the scope statement's + * index, and for a declarator of a `var` statement or a `for` initializer, + * the list and the declarator's end. */ +interface Settle { + index: number; + list: ts.VariableDeclarationList | null; + end: number; +} + class ScopeAnalysis { private readonly js: boolean; /** Whether a function declaration's references all settle, by scope @@ -214,8 +223,10 @@ class ScopeAnalysis { } /** The index of the scope statement that first stores `symbol` - * unconditionally, or Infinity. */ - settleIndex(scope: Scope, symbol: ts.Symbol): number { + * unconditionally (Infinity when none does), and, when that statement + * declares it in a `var` statement or a `for` initializer, the end of the + * storing declarator: later declarators of the list run after it. */ + settleIndex(scope: Scope, symbol: ts.Symbol): Settle { const statements = statementsOf(scope); const checker = this.lowerer.checker; for (let i = 0; i < statements.length; i++) { @@ -234,7 +245,7 @@ class ScopeAnalysis { (d.initializer !== undefined || loop) && boundIdentifiersOf(d.name).some((n) => checker.getSymbolAtLocation(n) === symbol) ) - return i; + return { index: i, list: loop ? null : list, end: d.end }; } } else if ( ts.isExpressionStatement(s) && @@ -243,20 +254,21 @@ class ScopeAnalysis { ts.isIdentifier(s.expression.left) && checker.getSymbolAtLocation(s.expression.left) === symbol ) { - return i; + return { index: i, list: null, end: -1 }; } } - return Infinity; + return { index: Infinity, list: null, end: -1 }; } /** Whether `ref` (inside `scope`, or in another module) runs only after - * the statement at index `settle`. `read`: `ref` reads the binding - * itself, so tsc vouches for one in the scope's own code. */ - settles(scope: Scope, settle: number, ref: ts.Identifier, read = true): boolean { + * the store `settle` describes. `read`: `ref` reads the binding itself, + * so tsc vouches for one in the scope's own code. */ + settles(scope: Scope, settle: Settle, ref: ts.Identifier, read = true): boolean { if (ts.getSourceFileOfNode(ref) !== this.sf) return true; const statements = statementsOf(scope); let deferred: ts.Node | null = null; let top: ts.Node | null = null; + const container: ts.Node | undefined = ts.isSourceFile(scope) ? scope : scope.body; for (let n: ts.Node = ref; n.parent !== undefined && n !== scope; n = n.parent) { if ( ts.isFunctionLike(n) || @@ -265,18 +277,25 @@ class ScopeAnalysis { ts.isClassStaticBlockDeclaration(n) ) deferred = n; - if (n.parent === scope || (!ts.isSourceFile(scope) && n.parent === scope.body)) top = n; + // The scope's own statement, not the function body that holds it. + if (n.parent === container) top = n; } if (top === null) return false; if (deferred === null && read && !this.js) return true; if (deferred !== null && ts.isFunctionDeclaration(deferred) && deferred.name !== undefined) return this.functionSettles(scope, settle, deferred); const index = statements.indexOf(top as ts.Statement); - return index > settle || (index === settle && settlesWithin(top, ref)); + return ( + index > settle.index || + (index === settle.index && + (settlesWithin(top, ref) || + // A later declarator of the storing list (`var a = {}, b = { ...a }`). + (settle.list !== null && isInside(ref, settle.list) && ref.pos >= settle.end))) + ); } - private functionSettles(scope: Scope, settle: number, fn: ts.FunctionDeclaration): boolean { - const key = `${fn.pos}:${settle}`; + private functionSettles(scope: Scope, settle: Settle, fn: ts.FunctionDeclaration): boolean { + const key = `${fn.pos}:${settle.index}:${settle.end}`; const known = this.functionMemo.get(key); if (known !== undefined) return known; this.functionMemo.set(key, true); diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index 800d02eff..0d6c8ec8e 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -614,27 +614,13 @@ export function clearProgramBuiltinAliasCache(): void { * but the bundler-emitted CJS export rewrite (npm-static-rewrite.ts) * types the getter-table shapes and the offender attribution degrades a * package whose surface still breaks its consumers — so those bundles are - * worth ATTEMPTING: eligible when the .d.ts and unminified criteria hold, - * gracefully per-package-degraded when the attempt fails. */ + * worth ATTEMPTING: eligible when the .d.ts criteria hold, gracefully + * per-package-degraded when the attempt fails. Minified JS is attempted + * too: a whitespace-minified dist is ordinary JavaScript, and an + * identifier-minified bundle whose interop helpers lost their names is + * refused by the bundle recognizers, which name the reason. */ const TRANSFORM_MARKERS = ["__webpack_require__"]; -/** Unminified-JS heuristic over an entry source: at least two lines and - * an average line length under 200 characters (minified dists are one - * enormous line; readable code averages well under 100). */ -function looksUnminified(source: string): boolean { - const lines = source.split("\n"); - if (lines.length < 2) return false; - return source.length / lines.length <= 200; -} - -/** A whitespace/syntax-minified bundler dist whose interop helpers keep - * their names (`var __toESM=…,__toCommonJS=mod=>…`): the recognizers - * verify those shapes and refuse the package precisely when they cannot, - * so the minification heuristic need not refuse it up front. */ -function namedBundlerHelpers(source: string): boolean { - return /\b__(?:toCommonJS|commonJS|toESM)\d*\s*=\s*(?:\(|[A-Za-z_$][\w$]*\s*=>)/.test(source); -} - function hasTransformMarkers(source: string): boolean { return TRANSFORM_MARKERS.some((m) => source.includes(m)); } @@ -676,8 +662,6 @@ export function npmStaticIneligibleReason( if (jsEntry === null) return "no runtime JS entry resolves"; const source = trackedReadFile(jsEntry); if (source === null) return `its runtime entry ${jsEntry} cannot be read`; - if (!looksUnminified(source) && !namedBundlerHelpers(source)) - return "its shipped JS looks minified"; if (hasTransformMarkers(source)) { return "its shipped JS carries build-transform markers (bundled/transpiled dist)"; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7543-var-list-declarator-order.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7543-var-list-declarator-order.js.json new file mode 100644 index 000000000..d193e84ed --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7543-var-list-declarator-order.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7543-var-list-declarator-order.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7545-string-regex-checked-arguments.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7545-string-regex-checked-arguments.js.json new file mode 100644 index 000000000..03b3197c4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7545-string-regex-checked-arguments.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7545-string-regex-checked-arguments.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts.json new file mode 100644 index 000000000..d026fb402 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7543-var-list-declarator-order.js b/tests/corpus/7543-var-list-declarator-order.js new file mode 100644 index 000000000..8fae06ff9 --- /dev/null +++ b/tests/corpus/7543-var-list-declarator-order.js @@ -0,0 +1,66 @@ +// A `var` read in a later declarator of the statement that stores it runs +// after the store; a read in the same or an earlier declarator sees undefined. +var base = { a: 1, b: "x" }, + more = { ...base, c: 2 }, + keys = Object.keys({ ...more }).join(","), + first = base.a + 1; +console.log(JSON.stringify(more), keys, first); + +function describeBase() { + return base.b + more.c; +} +var rules = { space: /\s+/, word: /\w+/ }, + merged = { ...rules, digit: /\d/ }, + described = describeBase(); +console.log(Object.keys(merged).join(","), merged.word.test("ab"), described); + +var table = { one: 1, two: 2 }, + lookup = function (key) { + return table[key]; + }, + arrow = (key) => ({ ...table })[key]; +console.log(lookup("one"), arrow("two")); + +var readLater = function () { + return later; + }, + early = readLater(), + later = { k: 1 }, + after = readLater().k; +console.log(early, after); + +var self = self ? 1 : 2; +console.log(self); + +for (var i = 0, j = i + 10; i < 2; i++) console.log(i, j); + +function nested() { + var inner = [1, 2, 3], + copy = [...inner, 4], + total = copy.reduce((sum, n) => sum + n, 0); + return { copy, total }; +} +console.log(JSON.stringify(nested())); + +function earlyInFunction() { + show("before"); + var point = { n: 1 }, + next = { m: point.n + 1 }; + show("after"); + function show(when) { + console.log(when, typeof point, point === undefined ? "none" : point.n, next === undefined ? "none" : next.m); + } + var check = () => console.log("check", items === undefined); + check(); + var items = [1]; + check(); +} +earlyInFunction(); + +function loopLists() { + for (var k = 0, sum = k; k < 3; k++) sum += k; + var doubled = sum * 2, + again = doubled + 1; + return [sum, doubled, again]; +} +console.log(JSON.stringify(loopLists())); diff --git a/tests/corpus/7545-string-regex-checked-arguments.js b/tests/corpus/7545-string-regex-checked-arguments.js new file mode 100644 index 000000000..a7ace9cdb --- /dev/null +++ b/tests/corpus/7545-string-regex-checked-arguments.js @@ -0,0 +1,89 @@ +// String search, match and replace with untyped pattern values: a RegExp +// is used as is, and any other value is converted as Node converts it. +const rules = JSON.parse('{"other":{}}'); +rules.other.space = /\s+/g; +rules.other.word = /o+/; +rules.other.text = "l"; +rules.other.dot = "."; +rules.other.none = undefined; +rules.other.nul = null; +rules.other.one = /(l)(o)/; +rules.other.many = /l+/g; +const t = " hello world"; + +console.log( + t.search(rules.other.word), + t.search(rules.other.text), + t.search(rules.other.none), + t.search(rules.other.nul), + t.search(rules.other.space), + "a.b".search(rules.other.dot), +); +console.log(rules.other.space.lastIndex); + +let conversions = 0; +rules.other.obj = { + toString() { + conversions++; + return "w"; + }, +}; +console.log(t.search(rules.other.obj), t.match(rules.other.obj)[0], conversions); + +const all = t.match(rules.other.many); +console.log(JSON.stringify(all), rules.other.many.lastIndex); +const pair = t.match(rules.other.one); +console.log(pair[0], pair[1], pair[2], pair.index, pair.length, pair.input === t); +const literal = t.match(rules.other.dot); +console.log(literal[0], literal.index, literal.length); +console.log(JSON.stringify(t.match(rules.other.none)), t.match(rules.other.zz)); +rules.other.zz = "zz"; +console.log(t.match(rules.other.zz)); +function first(text, re) { + const m = text.match(re); + return m ? m[0] : "none"; +} +console.log(first("abc", rules.other.dot), first("", rules.other.dot)); + +console.log(t.replace(rules.other.space, (m) => "[" + m.length + "]")); +console.log(t.replace(rules.other.word, (m, offset) => m.toUpperCase() + offset)); +console.log(t.replace(rules.other.text, (m, offset, subject) => "<" + m + offset + subject.length + ">")); +console.log(t.replaceAll(rules.other.text, (m) => m.toUpperCase())); +console.log(t.replaceAll(rules.other.space, () => "_")); +console.log(t.replace(rules.other.one, (m, p1, p2, offset) => p1 + "|" + p2 + "|" + offset)); +console.log(t.replace(rules.other.text, (m, offset, subject, extra) => typeof extra)); +console.log(t.replace(rules.other.text, (m) => m.length)); +try { + t.replaceAll(rules.other.word, (m) => m); +} catch (e) { + console.log(e.constructor.name, e.message); +} + +function tabs(text, width) { + let column = 0; + let out = ""; + for (const ch of text) { + if (ch === "\t") { + const pad = 4 - (column % 4); + out += " ".repeat(pad); + column += pad; + } else { + out += ch; + column++; + } + } + return out + width; +} +class Lexer { + constructor(table) { + this.rules = table; + } + indent(line, width) { + return line.replace(this.rules.leading, (w) => tabs(w, width)); + } + firstText(line) { + return line.search(this.rules.nonSpace); + } +} +const lexer = new Lexer({ leading: /^( {1,4}| {0,3}\t)/, nonSpace: /[^ ]/ }); +console.log(JSON.stringify(lexer.indent("\tabc", 2)), lexer.firstText(" x")); diff --git a/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts b/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts new file mode 100644 index 000000000..0c94acb0d --- /dev/null +++ b/tests/fixtures/npm/cases/7546-minified-esm-rules/main.ts @@ -0,0 +1,22 @@ +// A one-line minified ES module. Its module-level var statement spreads +// earlier declarators into later ones, its tokenizer reads regexes through +// an untyped rules table (search, match and replace with callbacks), and +// one function receives both typed and untyped link objects. +import { render } from "gtminimark"; + +const doc = [ + "# Title \\*one\\*", + "## Sub", + '[a](/x "T\\*")', + "[b](/y)", + '[r]: /ref "Ref"', + "[n]: /none", + "[R]", + "[n]", + "[z]", + "\tcode *em* line", + " three spaces *x* and *y*", + "plain words here", +].join("\n"); +console.log(render(doc)); +console.log(render(doc, { pedantic: true })); diff --git a/tests/fixtures/npm/node_modules/gtminimark/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtminimark/dist/index.d.ts new file mode 100644 index 000000000..295430b86 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminimark/dist/index.d.ts @@ -0,0 +1 @@ +export declare function render(src: string, options?: { pedantic?: boolean }): string; diff --git a/tests/fixtures/npm/node_modules/gtminimark/dist/index.js b/tests/fixtures/npm/node_modules/gtminimark/dist/index.js new file mode 100644 index 000000000..c5e1daa31 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminimark/dist/index.js @@ -0,0 +1 @@ +var m = { heading: /^(#{1,6}) +(.*)$/, link: /^\[([^\]]*)\]\(([^ )]*)(?: "([^"]*)")?\)/, ref: /^\[([^\]]*)\]$/, lead: /^( {1,4}| {0,3}\t)/, nonSpace: /[^ ]/, punct: /\\([\\*_\[\]])/g, word: /\w+/g, def: /^\[([^\]]*)\]: (\S+)(?: "([^"]*)")?$/ },B = { ...m, emph: /\*([^*]+)\*/g },P = { ...B, word: /[a-z]+/g },K = { normal: { other: m, inline: B }, pedantic: { other: m, inline: P, strict: !0 } };function X(a, e) { let t = 0, n = ""; for (let i of a) if (i === "\t") { let r = 4 - t % 4; n += " ".repeat(r), t += r } else n += i, t++; return n + e; }function Se(a, e, t, n) { let r = e.href, o = e.title || null; n.links++; return { type: "link", raw: t, href: r, title: o, text: a[1] }; }class T {constructor() { this.state = { links: 0 }; }heading(s) { let c = s.match(this.rules.other.heading); return c ? "h" + c[1].length + ":" + c[2].replace(this.rules.other.punct, "$1") : null; }indent(s, w) { return s.replace(this.rules.other.lead, W => X(W, w)); }first(s) { return s.search(this.rules.other.nonSpace); }emph(s) { return s.replace(this.rules.inline.emph, (W, x) => "" + x + ""); }words(s) { let c = s.match(this.rules.inline.word); return c ? c.length : 0; }link(s) { let c = this.rules.other.link.exec(s); if (!c) return null; let r = c[2], o = ""; o = c[3] ? c[3] : ""; return Se(c, { href: r && r.replace(this.rules.other.punct, "$1"), title: o && o.replace(this.rules.other.punct, "$1") }, c[0], this.state); }ref(s, l) { let c = this.rules.other.ref.exec(s); if (!c) return null; let o = l[c[1].toLowerCase()]; return o ? Se(c, o, c[0], this.state) : { type: "text", raw: c[0] }; }}class L {constructor(e) { this.options = e || {}; this.links = {}; this.tokenizer = new T(); this.tokenizer.rules = this.options.pedantic ? K.pedantic : K.normal; }define(s) { let c = s.match(this.tokenizer.rules.other.def); if (c) this.links[c[1].toLowerCase()] = { href: c[2], title: c[3] || "" }; return !!c; }line(s) {let t = this.tokenizer, h = t.heading(s);if (h) return h;let k = t.link(s);if (k) return JSON.stringify(k);let f = t.ref(s, this.links);if (f) return JSON.stringify(f);return t.first(s) + "|" + JSON.stringify(t.indent(s, t.first(s))) + "|" + t.emph(s) + "|" + t.words(s);}}export function render(src, options) {let l = new L(options), out = [];for (let s of src.split("\n")) if (!l.define(s)) out.push(l.line(s));return out.join("\n") + "\nlinks " + l.tokenizer.state.links;} diff --git a/tests/fixtures/npm/node_modules/gtminimark/package.json b/tests/fixtures/npm/node_modules/gtminimark/package.json new file mode 100644 index 000000000..ff188c682 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminimark/package.json @@ -0,0 +1,8 @@ +{ + "name": "gtminimark", + "version": "1.0.0", + "description": "A one-line minified ES module: a module-level var statement whose later declarators spread earlier ones, regexes read through an untyped rules table, and a function called with typed and untyped link objects", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/harness/corpus-costs/7543-var-list-declarator-order.js.json b/tests/harness/corpus-costs/7543-var-list-declarator-order.js.json new file mode 100644 index 000000000..cc47657bd --- /dev/null +++ b/tests/harness/corpus-costs/7543-var-list-declarator-order.js.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 5900 } diff --git a/tests/harness/corpus-costs/7545-string-regex-checked-arguments.js.json b/tests/harness/corpus-costs/7545-string-regex-checked-arguments.js.json new file mode 100644 index 000000000..83b82ee18 --- /dev/null +++ b/tests/harness/corpus-costs/7545-string-regex-checked-arguments.js.json @@ -0,0 +1 @@ +{ "plain": 4900, "san": 4600 } diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index 0d4052952..db4fabb3e 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -50,8 +50,8 @@ * no-teachings default is pinned by K5/K7 over * the teachings-free traps profile * K13 npm-posture bare npm specifiers are static-or-refuse: an - * eligible package (own .d.ts, unminified JS, - * no transform markers) compiles statically + * eligible package (own .d.ts, no transform + * markers) compiles statically * into the library graph, its own npm deps * judged by the same bar; every ineligible one * refuses SC4020 naming the failed bar — never @@ -746,11 +746,11 @@ describe("K9: library-mode refusals", () => { * synthesized per test like K9's. Emission never engages — SC4020 is a * frontend verdict — so one lane suffices. */ let npmRefusalCounter = 0; -async function npmRefusal( +async function compileNpmFixture( entryFile: string, exports: unknown[], profilePatch: Record = {}, -): Promise<{ code: string; message: string; hint?: string; note?: string; file: string }[]> { +): Promise>> { const outDir = join(cacheDir, `npm-refusal-${npmRefusalCounter++}`); mkdirSync(outDir, { recursive: true }); const profile = { @@ -770,7 +770,15 @@ async function npmRefusal( }; const profilePath = join(outDir, "profile.json"); writeFileSync(profilePath, JSON.stringify(profile)); - const result = await compileLibrary({ profilePath, outDir }); + return compileLibrary({ profilePath, outDir }); +} + +async function npmRefusal( + entryFile: string, + exports: unknown[], + profilePatch: Record = {}, +): Promise<{ code: string; message: string; hint?: string; note?: string; file: string }[]> { + const result = await compileNpmFixture(entryFile, exports, profilePatch); expect(result.ok).toBe(false); if (result.ok) throw new Error("unreachable"); return result.diagnostics.map((d) => ({ @@ -795,13 +803,11 @@ describe("K13: library-mode npm refusals (SC4020, static-or-refuse)", () => { expect(diags[0]!.file.endsWith("lib-markers.ts")).toBe(true); }); - test("minified shipped JS refuses", async () => { - const diags = await npmRefusal("lib-minified.ts", [ + test("minified shipped JS compiles", async () => { + const result = await compileNpmFixture("lib-minified.ts", [ { export: "f", symbol: "kx_f", params: ["f64", "f64"], returns: "f64" }, ]); - expect(diags.map((d) => d.code)).toEqual(["SC4020"]); - expect(diags[0]!.message).toContain("'slimmed'"); - expect(diags[0]!.message).toContain("looks minified"); + expect(result.ok ? [] : result.diagnostics.map((d) => d.message)).toEqual([]); }); test("no own .d.ts refuses by name — never the generic import fence", async () => { diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 7ee76307f..11e729f60 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -1490,6 +1490,15 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { ]); }, 180_000); + // 7546: a one-line minified ES module: a var statement whose later + // declarators spread earlier ones, regexes read through an untyped rules + // table, and a function instantiated with typed and untyped arguments. + test("the minified module 7546-minified-esm-rules compiles statically and matches Node", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/7546-minified-esm-rules/main.ts"), [ + "gtminimark", + ]); + }, 180_000); + // A whitespace/syntax-minified bundle whose helpers keep their names is // judged by the recognizers, not refused up front as minified. test("--npm-static=auto opts a minified bundle with named helpers in", () => { diff --git a/tests/library-mode/npm-refuse/lib-minified.ts b/tests/library-mode/npm-refuse/lib-minified.ts index 9e5c16e75..492cd5369 100644 --- a/tests/library-mode/npm-refuse/lib-minified.ts +++ b/tests/library-mode/npm-refuse/lib-minified.ts @@ -1,5 +1,5 @@ -// SC4020 fixture: slimmed ships a one-long-line dist — minified JS fails -// the eligibility bar. +// slimmed ships a one-long-line dist: minified JS compiles like any other +// eligible package. import { pick } from "slimmed"; export function f(a: number, b: number): number { diff --git a/tests/library-mode/npm-static/lib.ts b/tests/library-mode/npm-static/lib.ts index 27107f29d..15bad17a3 100644 --- a/tests/library-mode/npm-static/lib.ts +++ b/tests/library-mode/npm-static/lib.ts @@ -1,5 +1,5 @@ // Library-mode npm posture fixture (static-or-refuse): a bare specifier -// naming an ELIGIBLE package (own .d.ts, unminified shipped JS, no +// naming an ELIGIBLE package (own .d.ts, no // build-transform markers) compiles statically as part of the library // graph — no flag, no island — and the package's own top-level require // ("mathdep") joins through the same bar. The node:path import alongside diff --git a/tests/library-mode/npm-static/node_modules/mathkit/index.js b/tests/library-mode/npm-static/node_modules/mathkit/index.js index f9f9242c3..eadfefba9 100644 --- a/tests/library-mode/npm-static/node_modules/mathkit/index.js +++ b/tests/library-mode/npm-static/node_modules/mathkit/index.js @@ -1,6 +1,6 @@ "use strict"; // A vendored fake package for the library-mode npm posture: eligible by -// every bar (own .d.ts, unminified CJS, no build-transform markers), with +// every bar (own .d.ts, no build-transform markers), with // its OWN npm dep edge (mathdep) so the transitive fixpoint is exercised. const steps = require("mathdep"); From d9544bb71d9f908cf82f99e32a7102c3e3b0b7c5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 05:57:37 -0500 Subject: [PATCH 087/409] Release EventTarget's shared method values at exit when only a signal read them The EventTarget method values are cached for the program, and exit released them only when an EventTarget, an event or a listener had registered the unit's teardown. A program that read an AbortSignal's addEventListener and never added a listener or made a target left the cached functions live, and RC-audit builds exited with 99. Caching a method value now registers the teardown too. --- .../corpus/7741-abort-signal-listener-value.js.json | 6 ++++++ packages/runtime/src/scr_event_target.c | 3 +++ tests/corpus/7741-abort-signal-listener-value.js | 10 ++++++++++ .../7741-abort-signal-listener-value.js.json | 1 + 4 files changed, 20 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json create mode 100644 tests/corpus/7741-abort-signal-listener-value.js create mode 100644 tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json new file mode 100644 index 000000000..c556e243e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7741-abort-signal-listener-value.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7741-abort-signal-listener-value.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_event_target.c b/packages/runtime/src/scr_event_target.c index 597eeca9b..9ca91879a 100644 --- a/packages/runtime/src/scr_event_target.c +++ b/packages/runtime/src/scr_event_target.c @@ -1043,6 +1043,9 @@ static ScrDyn *scr_et_method_value(const char *key, size_t len, bool target) { const char *name = scr_et_method_names[i]; if (strlen(name) != len || memcmp(name, key, len) != 0) continue; if (!scr_et_method_values[i]) { + /* An AbortSignal answers these without the unit ever installing: + * exit releases the cached function either way. */ + scr_et_register_teardown(); ScrClosure *clo = scr_closure_new(NULL, 1); clo->caps[0] = scr_box_new(SCR_BOX_F64); scr_box_set_f64(clo->caps[0], (double)i); diff --git a/tests/corpus/7741-abort-signal-listener-value.js b/tests/corpus/7741-abort-signal-listener-value.js new file mode 100644 index 000000000..1a8a201d7 --- /dev/null +++ b/tests/corpus/7741-abort-signal-listener-value.js @@ -0,0 +1,10 @@ +// An AbortSignal's EventTarget methods read as values, in a program that +// never adds a listener or makes an EventTarget of its own: the shared +// functions are released at exit like every other value. +const holder = JSON.parse("{}"); +holder.controller = new AbortController(); +const signal = holder.controller.signal; +const add = signal.addEventListener; +console.log(typeof add, add.name, add.length, signal.dispatchEvent.name); +console.log("removeEventListener" in signal, signal.removeEventListener.length); +console.log(signal.aborted); diff --git a/tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json b/tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json new file mode 100644 index 000000000..e028a5bb1 --- /dev/null +++ b/tests/harness/corpus-costs/7741-abort-signal-listener-value.js.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 7500 } From ba5d73e90b3289a52a31d75095da4b7d50daad50 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 06:20:02 -0500 Subject: [PATCH 088/409] Recognize esbuild's __commonJS helper under a minified name Refs #1777 An identifier-minified esbuild bundle renames the __commonJS helper (var j=(u,t)=>()=>(t||u((t={exports:{}}).exports,t),t.exports)). The recognizer already compared the helper's shape independently of its parameter names, but only used a renamed match to refuse the package. A stable top-level declaration with the helper's shape now splits the bundle's chunks like the named helper, so lru-cache's CommonJS build is no longer refused. --- docs/content/docs/dependencies.mdx | 2 +- .../src/frontend/lowering/lower-calls.ts | 27 +++++++++++++++++++ .../npm-static-bundled-chunks.test.ts | 11 ++++---- .../src/frontend/npm-static-bundled-chunks.ts | 20 ++++++-------- .../src/frontend/npm-static-rewrite-syntax.ts | 14 ++++++++-- packages/compiler/src/frontend/npm-static.ts | 5 ++-- packages/compiler/src/frontend/program.ts | 16 +++++++++-- .../main.cjs.json | 7 +++++ .../main.cjs.json | 8 ++++++ .../7547-minified-cjs-chunks/main.ts.json | 6 +++++ .../lib.cjs | 10 +++++++ .../main.cjs | 19 +++++++++++++ .../7549-cjs-minified-esmodule-stamp/main.cjs | 7 +++++ .../module-stamped.cjs | 3 +++ .../stamped.cjs | 5 ++++ .../cases/7547-minified-cjs-chunks/main.ts | 12 +++++++++ .../node_modules/gtminicache/dist/index.d.ts | 7 +++++ .../node_modules/gtminicache/dist/index.js | 1 + .../npm/node_modules/gtminicache/package.json | 7 +++++ .../main.cjs.json | 1 + .../main.cjs.json | 1 + tests/harness/npm-static.test.ts | 9 +++++++ 22 files changed, 173 insertions(+), 25 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts.json create mode 100644 tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs create mode 100644 tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs create mode 100644 tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs create mode 100644 tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs create mode 100644 tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs create mode 100644 tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts create mode 100644 tests/fixtures/npm/node_modules/gtminicache/dist/index.d.ts create mode 100644 tests/fixtures/npm/node_modules/gtminicache/dist/index.js create mode 100644 tests/fixtures/npm/node_modules/gtminicache/package.json create mode 100644 tests/harness/corpus-costs/7548-cjs-receiverless-builtin-calls/main.cjs.json create mode 100644 tests/harness/corpus-costs/7549-cjs-minified-esmodule-stamp/main.cjs.json diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index f02dcfbf0..a9c5ae138 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -53,7 +53,7 @@ Type-only imports follow Node.js's type stripping. `import type` and `export typ scriptc compiles a package automatically when it: - ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and -- ships JavaScript that isn't a bundle whose bundler-interop helpers, such as `__commonJS`, have lost their names. Minified JavaScript compiles like unminified JavaScript. +- ships JavaScript that isn't a webpack bundle. Minified JavaScript compiles like unminified JavaScript. The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use. diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 436d4f1d4..672801e9e 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -306,6 +306,22 @@ function mayHoldError(lowerer: Lowerer, type: ts.Type, errorType: ts.Type | null ); } +/** The `f` of a `(0, f)` callee (a numeric left operand, so dropping it + * changes only the call's receiver), or null. */ +function receiverlessCalleeOf(callee: ts.Expression): ts.Expression | null { + let e = callee; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if ( + !ts.isBinaryExpression(e) || + e.operatorToken.kind !== ts.SyntaxKind.CommaToken || + !ts.isNumericLiteral(e.left) + ) + return null; + let right = e.right; + while (ts.isParenthesizedExpression(right)) right = right.expression; + return right; +} + export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); if (lowerer.errorAccessorReflection === null) noteErrorAccessorReflection(lowerer, expr); @@ -329,6 +345,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const json = lowerJsonRequireCall(lowerer, expr); if (json) return json; + // `(0, ns.member)(...)` over a builtin module (tsc's and esbuild's call + // without a receiver): Node's module functions do not read `this`, so it + // is the member call. + { + const member = receiverlessCalleeOf(expr.expression); + if (member !== null && ts.isPropertyAccessExpression(member)) { + const builtin = lowerer.lowerNamespaceBuiltinCall(expr, member); + if (builtin) return builtin; + } + } + // eval(...) and Function(...): Node's EvalError under // --disallow-code-generation-from-strings (code-generation.ts). { diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts index 0779db3ce..dc238c622 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts @@ -276,13 +276,14 @@ export default require_f(); ).toBeNull(); }); -test("an identifier-minified bundle names its lost helper names", () => { - // esbuild --minify output: the helper shapes survive, their names do not. - const result = split( +test("an identifier-minified bundle splits through its helper's shape", () => { + // esbuild --minify output: the helper shape survives, its name does not. + const answer = expectSameBehavior( `var c=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);` + - `var x=c((t,m)=>{m.exports={v:1}});console.log(x().v);`, + `var x=c((t,m)=>{m.exports={v:1}});var y=c(t=>{t.w=x().v+1});` + + `console.log(x().v,y().w,x()===x());`, ); - expect(result).toEqual({ degrade: expect.stringContaining("identifier-minified") as string }); + expect(answer.chunks.map((c) => basename(c.path))).toEqual(["index.js.x.cjs", "index.js.y.cjs"]); }); test("an ES-module bundle of CommonJS chunks names the chunk interop", () => { diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts index 37a74f2be..66c25403b 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts @@ -525,7 +525,7 @@ export function splitBundledChunks( } } } - const helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS"); + let helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS"); // Every name a recognizer relies on is written nowhere and declared // once at the top level; `Object` itself is never rebound. @@ -552,18 +552,14 @@ export function splitBundledChunks( ); }; if (helperNames.length === 0) { - // The helper's shape under another name: an identifier-minified bundle, - // whose other interop helpers lost their names as well. - for (const [name, t] of top) { + // An identifier-minified bundle: the helper's shape under another name. + helperNames = [...top].flatMap(([name, t]) => { const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined; - if (init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null) - return { - degrade: - "its shipped JS is an identifier-minified bundle whose bundler-interop helpers " + - "(__commonJS and the rest) have lost the names the recognizers verify", - }; - } - return null; + return init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null + ? [name] + : []; + }); + if (helperNames.length === 0) return null; } const forms = new Map(); for (const name of helperNames) { diff --git a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts index c48420625..917041603 100644 --- a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts @@ -260,11 +260,21 @@ function definePropertyExportOf( value = getterReturnOf(p); } } - const esModuleStamp = - name === "__esModule" && value !== null && value.kind === ts.SyntaxKind.TrueKeyword; + const esModuleStamp = name === "__esModule" && value !== null && isTrueLiteral(value); return { name, value, esModuleStamp }; } +/** `true`, or `!0`, its minified spelling. */ +function isTrueLiteral(expr: ts.Expression): boolean { + return ( + expr.kind === ts.SyntaxKind.TrueKeyword || + (ts.isPrefixUnaryExpression(expr) && + expr.operator === ts.SyntaxKind.ExclamationToken && + ts.isNumericLiteral(expr.operand) && + expr.operand.text === "0") + ); +} + /** Space-pads [start, end) of `text` preserving newlines — the recognized * statement disappears while every original offset survives. */ function padSpan(text: string, start: number, end: number): string { diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index 0d6c8ec8e..eed02c95c 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -616,9 +616,8 @@ export function clearProgramBuiltinAliasCache(): void { * package whose surface still breaks its consumers — so those bundles are * worth ATTEMPTING: eligible when the .d.ts criteria hold, gracefully * per-package-degraded when the attempt fails. Minified JS is attempted - * too: a whitespace-minified dist is ordinary JavaScript, and an - * identifier-minified bundle whose interop helpers lost their names is - * refused by the bundle recognizers, which name the reason. */ + * too: a minified dist is ordinary JavaScript, and the bundle recognizers + * know esbuild's __commonJS helper by its shape under any name. */ const TRANSFORM_MARKERS = ["__webpack_require__"]; function hasTransformMarkers(source: string): boolean { diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 68b649b07..932ac7054 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1436,8 +1436,20 @@ function requiresOf7( return out; } +/** `true`, or `!0`, its minified spelling. */ +function isTrueLiteral(expr: ts.Expression): boolean { + return ( + expr.kind === ts.SyntaxKind.TrueKeyword || + (ts.isPrefixUnaryExpression(expr) && + expr.operator === ts.SyntaxKind.ExclamationToken && + ts.isNumericLiteral(expr.operand) && + expr.operand.text === "0") + ); +} + /** The exact `Object.defineProperty(exports|module.exports, "__esModule", - * { value: true })` interop stamp (shared by preflight and the no-op lowering). */ + * { value: true })` interop stamp (shared by preflight and the no-op lowering); + * `{ value: !0 }` is the same stamp. */ export function isEsModuleStamp(expr: ts.Expression): boolean { if (!ts.isCallExpression(expr) || expr.questionDotToken !== undefined) return false; const callee = expr.expression; @@ -1471,7 +1483,7 @@ export function isEsModuleStamp(expr: ts.Expression): boolean { ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "value" && - p.initializer.kind === ts.SyntaxKind.TrueKeyword + isTrueLiteral(p.initializer) ); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs.json new file mode 100644 index 000000000..f3b8ba419 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs", + "/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs.json new file mode 100644 index 000000000..08adae885 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs", + "/tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs", + "/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts.json new file mode 100644 index 000000000..212551ccd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs b/tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs new file mode 100644 index 000000000..248e66270 --- /dev/null +++ b/tests/corpus/7548-cjs-receiverless-builtin-calls/lib.cjs @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: !0 }); +exports.label = exports.describe = void 0; +const node_util_1 = require("node:util"); +const node_path_1 = require("node:path"); +function describe(name, size) { + return (0, node_util_1.format)("%s holds %d (%s)", name, size, (0, node_path_1.basename)("/a/b/c.txt")); +} +exports.describe = describe; +exports.label = "lib"; diff --git a/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs b/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs new file mode 100644 index 000000000..6fd0b78a9 --- /dev/null +++ b/tests/corpus/7548-cjs-receiverless-builtin-calls/main.cjs @@ -0,0 +1,19 @@ +"use strict"; +// tsc and esbuild output: the __esModule stamp spelled { value: !0 }, and +// calls of builtin module functions without a receiver, (0, ns.member)(...). +const lib = require("./lib.cjs"); +const dc = require("node:diagnostics_channel"); +const util = require("node:util"); +const os = require("node:os"); +const path = require("node:path"); +console.log(lib.label, lib.describe("cache", 3)); +const metrics = (0, dc.channel)("cache:metrics"); +console.log(metrics.name, metrics.hasSubscribers); +const seen = []; +dc.subscribe("cache:metrics", (message) => seen.push(message.key)); +metrics.publish({ key: "a" }); +console.log(seen.join(","), metrics.hasSubscribers); +const tracing = (0, dc.tracingChannel)("cache"); +console.log(tracing.hasSubscribers); +console.log((0, util.inspect)({ a: [1, 2] }), (0, util.isDeepStrictEqual)([1], [1])); +console.log((0, path.join)("a", "b"), typeof (0, os.platform)()); diff --git a/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs b/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs new file mode 100644 index 000000000..c619a847a --- /dev/null +++ b/tests/corpus/7549-cjs-minified-esmodule-stamp/main.cjs @@ -0,0 +1,7 @@ +"use strict"; +// Minifiers spell `true` as `!0`: Object.defineProperty(exports, +// "__esModule", { value: !0 }) is the same interop stamp as { value: true }. +const stamped = require("./stamped.cjs"); +const moduleStamped = require("./module-stamped.cjs"); +console.log(stamped.answer, stamped.twice(4), moduleStamped.label); +console.log(typeof stamped.twice, moduleStamped.label.length); diff --git a/tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs b/tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs new file mode 100644 index 000000000..96a2aae7b --- /dev/null +++ b/tests/corpus/7549-cjs-minified-esmodule-stamp/module-stamped.cjs @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(module.exports, "__esModule", { value: !0 }); +module.exports.label = "module"; diff --git a/tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs b/tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs new file mode 100644 index 000000000..cd3b9c400 --- /dev/null +++ b/tests/corpus/7549-cjs-minified-esmodule-stamp/stamped.cjs @@ -0,0 +1,5 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: !0 }); +exports.answer = void 0; +exports.answer = 42; +exports.twice = (n) => n * 2; diff --git a/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts b/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts new file mode 100644 index 000000000..ad2303f47 --- /dev/null +++ b/tests/fixtures/npm/cases/7547-minified-cjs-chunks/main.ts @@ -0,0 +1,12 @@ +// An identifier-minified esbuild CommonJS bundle (lru-cache's shape): its +// __commonJS helper and chunk parameters lost their names, the chunks +// stamp __esModule with { value: !0 }, and they call node:util and +// node:diagnostics_channel functions in the (0, ns.member)(...) form. +import { Cache } from "gtminicache"; + +const cache = new Cache(2); +cache.set("a", 1).set("b", 2); +cache.get("a"); +cache.set("c", 3); +console.log(cache.keys().join(","), cache.get("b"), cache.get("c")); +console.log(cache.describe("cache")); diff --git a/tests/fixtures/npm/node_modules/gtminicache/dist/index.d.ts b/tests/fixtures/npm/node_modules/gtminicache/dist/index.d.ts new file mode 100644 index 000000000..5d12cad7e --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminicache/dist/index.d.ts @@ -0,0 +1,7 @@ +export declare class Cache { + constructor(max: number); + set(key: K, value: V): this; + get(key: K): V | undefined; + keys(): K[]; + describe(label: string): string; +} diff --git a/tests/fixtures/npm/node_modules/gtminicache/dist/index.js b/tests/fixtures/npm/node_modules/gtminicache/dist/index.js new file mode 100644 index 000000000..31c6deef9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminicache/dist/index.js @@ -0,0 +1 @@ +"use strict";var n=(s,e)=>()=>(e||s((e={exports:{}}).exports,e),e.exports);var u=n(i=>{"use strict";Object.defineProperty(i,"__esModule",{value:!0});i.metrics=void 0;var o=require("node:diagnostics_channel");i.metrics=(0,o.channel)("gtminicache:metrics")});var d=n(r=>{"use strict";Object.defineProperty(r,"__esModule",{value:!0});r.describe=a;var l=require("node:util");function a(s,e){return(0,l.format)("%s holds %d",s,e)}});Object.defineProperty(exports,"__esModule",{value:!0});exports.Cache=void 0;var h=u(),_=d(),c=class{#e=new Map;#t;constructor(e){this.#t=e}set(e,t){for(this.#e.delete(e),this.#e.set(e,t);this.#e.size>this.#t;)this.#e.delete(this.#e.keys().next().value);return h.metrics.hasSubscribers&&h.metrics.publish({key:e}),this}get(e){let t=this.#e.get(e);return t!==void 0&&(this.#e.delete(e),this.#e.set(e,t)),t}keys(){return[...this.#e.keys()]}describe(e){return(0,_.describe)(e,this.#e.size)}};exports.Cache=c; diff --git a/tests/fixtures/npm/node_modules/gtminicache/package.json b/tests/fixtures/npm/node_modules/gtminicache/package.json new file mode 100644 index 000000000..3f57e1ae3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/gtminicache/package.json @@ -0,0 +1,7 @@ +{ + "name": "gtminicache", + "version": "1.0.0", + "description": "tsc-style CommonJS modules bundled by esbuild 0.27.0 with --bundle --platform=node --format=cjs --minify: the __commonJS helper and the chunk parameters lose their names, the __esModule stamps read { value: !0 }, and calls into node:util and node:diagnostics_channel use the (0, ns.member)(...) form", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" +} diff --git a/tests/harness/corpus-costs/7548-cjs-receiverless-builtin-calls/main.cjs.json b/tests/harness/corpus-costs/7548-cjs-receiverless-builtin-calls/main.cjs.json new file mode 100644 index 000000000..8a603b2f4 --- /dev/null +++ b/tests/harness/corpus-costs/7548-cjs-receiverless-builtin-calls/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 12300, "san": 13600 } diff --git a/tests/harness/corpus-costs/7549-cjs-minified-esmodule-stamp/main.cjs.json b/tests/harness/corpus-costs/7549-cjs-minified-esmodule-stamp/main.cjs.json new file mode 100644 index 000000000..d00a49ec8 --- /dev/null +++ b/tests/harness/corpus-costs/7549-cjs-minified-esmodule-stamp/main.cjs.json @@ -0,0 +1 @@ +{ "plain": 16000, "san": 21200 } diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 11e729f60..388da2138 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -1499,6 +1499,15 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { ]); }, 180_000); + // 7547: an identifier-minified esbuild CommonJS bundle: the __commonJS + // helper and chunk parameters renamed, { value: !0 } stamps, and + // (0, ns.member)(...) calls of builtin module functions. + test("the identifier-minified bundle 7547-minified-cjs-chunks compiles statically and matches Node", async () => { + await expectNodeParity(join(fixturesRoot, "npm/cases/7547-minified-cjs-chunks/main.ts"), [ + "gtminicache", + ]); + }, 180_000); + // A whitespace/syntax-minified bundle whose helpers keep their names is // judged by the recognizers, not refused up front as minified. test("--npm-static=auto opts a minified bundle with named helpers in", () => { From efc351f202d1636dc455b73a2408a59c400c22ad Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 06:22:00 -0500 Subject: [PATCH 089/409] Read emitter analyses from one recorded walk of the module The LLVM emitter's constructor runs many analyses that each walk every function body to collect facts from a few node kinds. IrNodeIndex (ir/node-index.ts) walks the bodies once, before those analyses, and records each body's nodes in everyStmtList's order with a kind code per node and the kinds present per function. An analysis names the kinds it reads and receives exactly those nodes, in the same order, without recursing through the tree; a function the index does not hold (a rewritten copy) is walked as before. The index also lists the distinct type objects of the module, which the boxed-union and runtime-feature scans read instead of walking every expression's type tree. These read the index: walk borrows, appended array literals, constant callbacks and the rebinding census, int32 slots (dynamic field stores, call facts, the box census, candidate writes and the range walks), initializer bindings, stack observations, the may-throw call-graph census, published types and construction facts, final fields, stable globals, reference writes (private arrays, call targets, definitions, fresh constructors), boxed unions and runtime features. Analyses whose walks track their position (statement lists, nesting, ordered dataflow) keep their own walks. Callers outside the emitter pass no index and walk as before. The emitted LLVM IR is unchanged. packages/compiler/native/cli.profdata.gz is refreshed with scripts/refresh-cli-profile.mjs on this change, so the native compiler's optimization profile covers the rewritten analyses. --- packages/compiler/native/cli.profdata.gz | Bin 1634316 -> 1701590 bytes packages/compiler/src/backend/immutability.ts | 50 ++-- .../backend/llvm/appended-array-literals.ts | 70 +++--- .../src/backend/llvm/constant-callbacks.ts | 122 ++++++---- packages/compiler/src/backend/llvm/emitter.ts | 35 ++- .../src/backend/llvm/nullable-unions.ts | 14 +- .../compiler/src/backend/llvm/receivers.ts | 1 + .../src/backend/llvm/reference-writes.ts | 149 ++++++------ .../compiler/src/backend/llvm/walk-borrows.ts | 86 +++---- packages/compiler/src/backend/may-throw.ts | 38 +++- packages/compiler/src/backend/publication.ts | 27 ++- .../src/backend/stack-observations.ts | 93 +++++--- .../src/ir/callback-specialization.ts | 39 ++-- packages/compiler/src/ir/final-fields.ts | 30 +-- .../compiler/src/ir/function-references.ts | 3 + .../compiler/src/ir/initializer-bindings.ts | 40 ++-- packages/compiler/src/ir/int32-slots.ts | 166 +++++++++++--- packages/compiler/src/ir/node-index.ts | 214 ++++++++++++++++++ packages/compiler/src/ir/runtime-features.ts | 26 ++- packages/compiler/test/node-index.test.ts | 157 +++++++++++++ 20 files changed, 1006 insertions(+), 354 deletions(-) create mode 100644 packages/compiler/src/ir/node-index.ts create mode 100644 packages/compiler/test/node-index.test.ts diff --git a/packages/compiler/native/cli.profdata.gz b/packages/compiler/native/cli.profdata.gz index aa0e9c36beac0cdc3528321a8a654ede3118b4b4..b5f1e079f522bd1acd58a9d574272a2e272a8f2a 100644 GIT binary patch literal 1701590 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

3f&^%Su_N3Dd0UnlS_1D@h_lKMb~=U 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Πa4C|bS%#n)oWXbk!{G^BhZ+{=%NV^P(rx8umH4T}IfH1_zAz9p$#Dut zVU$8_?EYP?`MbBq=!GJv0#d!d=aux{E37-cFdzNPFi+TOrF!DrM}uAlAQX|oWSLh) zT!EJXc#xWOaL(m>mn}Yh)LW_3bo&Fzu>?s`JVmlR&9Z*clEf{(^z%}Ii*VVtIonL% z^Ax(^V-NIAlqg{erEm;Gb11MVymLgFg0E@FklF09J-KOYy&b=xZ=UKm>~2Scmqh_1 zd6~q>xm6vrzUa`f7`jt2Ls6F@Y7FPE9`mJtsowZ^o3y_dlyb&amaXC5&qi(Cm>r3z zXdo%Y3l!yKdBPhWcFt?Z4}NsMR&*Ja2hDWyHlm0SO&XCp&xgJLhAB*8GLkRILnFH5 z1!DyuQI57I;SqiGUT5%oXM^7n!5wo{Zm|GUi7CiEOZ1-J4sIooZ(iqu=se2Th;SDR ziNYt_?+;p%Yb<%kxY=Sdo6Kgj(Y@(7o$+WiUQIgVv53z5X45cR<-ad~+cw*|L#F<1 zRGelMCn3_bM3JjZqq^?7oEcLOWIXa7?>WHo^{~-%dNi((H^0V(9EES3ZAa;>*>9Xj zQ;UwzU?nBhBfA{3NRlGQQ6$DYpS|Ai3I4_FX6QD!NIt)%NFno}qB$_u`yJAXeAZ`0 zh`b+XySA~`*YnP!A#ys=kMlXM1;mWB%q)*AIq(uks!p%db$xMO9c=6V*3QZZqjdPH zz-xHD_d>;Y>bdaq|BdG#wDRtCx#9Q2?}y(HzaM@-{C@cT@caLk_wQ9b!tD`$Km7ik ze}Bt6mU}XuA2dGR>-rb8{K8tt=XYKxXuS*j`wMGc43nu)`r+MrFZ_P^{qXzY_w!3R z=h-IMmMb-vJkpF(e+l4w8XYEf^p?|vbWe(SJ9KD@ySK-cl0LXgibr+{sovkPh3>0g zUmNP@Y zgPxKk6ikquBqn1lu6IhZMIJv=|CHo&wjIkNH@p#pe9ElUZ5obqv`)=Pm)oYLc)UBP zN=kZ)JGqza$#SKa*t=!DH*A&Og?)7N9BsQt>-?^#V3#Q16q<7qc8=}H#gZ+)Sfd|f zk>hci#Bj!Yo}T0x*vd!MTfaXf1TJEZ?c0*|2VdBOF6|Yu_%Sq%#yE#FD<##FlCE;I zW4e;jU3t6ah-JU+|1zSW2TEfP_!xXp)Sh#BRXt9HP9>qVyc$_*EUrB_0iPSJdK&67 z+V}j)mwfoltgx0=7?dL^=GmTjm$SEaB*qipHdzmg2F=&MqlVZ>&-n z@#4>rN6CEKdkso{fPbnh3UQj?IfkKljA1a`@A^PvHP!QoKi?MfSVHpJoe1ZOs3D~} z2)dlItZ7vF9!20W9ea^@Of37R)W9B-DmwYO!p_j{+u)2F0-9xe%c`N z=)z61|8%RQJ6KMUB$1|_60Y?8@Uef6#b3z2t5+kBcjoH>jU4^I#Dc?5PiR#s!jk>B z6}H$HUaY$9%^;!Cy%(7zA=BevMa9W70!J%O0)|E}p`%M9E<~7C+B&{_YT613ap8(N zP)RgH;NIvsm_p-;ec^A24hcz*7KD2Dli&hd__-0q~DN z;_nAI6JV76O4~x78Yir)kgGQb=qV&maiT&AoWjHKKNg){xYG8j@RRFo<3#_Qb24TW z7Goq_;3N!(8JE`8so$}U7{AaJd8U@XPVJ33oRW^zFAR6|k`>t{r;Bo1MUh31`^TGo z#?!H%Wyc}2sl%$1Iz~|HU+N~?C*r9(#A3AOpAqM;@KHSPkcVZY4ogWN+|uO|MtTh$ z^=Vh-=O&bmdoJva>&Fn~ah_o@ilD$$55ExSGZIcnk{u0^{H+4xSOBMnva@feZVGB1ct{145ws5 zSpQGOr^<}mlz>{|?>WBOpPZ)siiYa?V|ZU*`rO0$>rTHl(qayTjYU_zN7w0BW&4F{ z+mUyylJCv_YWXYQ%!|%$ug`6-T$=stoH+EYJpz6!gHbHYDwNYHIp<$#OWQl1$hNW^ z(^*O%1^A?HpV?_!^x3bm9iGsQs!TDG$V(K%D~fozMz3eRj@ZyB`Z3RbyuP(PH}Pm~ z&jCi?BB$;sQ-q_9*Jk08rLGxLR;t`FJ;jqEq^69LU2cb46w9T%{ng+5tJbqG)J}O({d=PNS~Ye1f8PM@`)HTHZlw16&}i5UtWqzA z$R~QHFFgUCN4tqmwF{@J@#xlqj`OxU27XoRU81ozBXr$(xLNxi>Y^opwQZ`wwoNVY z_Y*qZ%u_zJ@3-7EhuDt}*_xkSKd0a3BL1SzBRMDnO0=CZdJ^rhZT-L(R=wFsU!t)z zgK-o|a2!jsoZr^)s`b7?O6z?5VcVJArEXmxf~<5s0{gxqFxkoRoI(m}XlHLg=Qk@J z-8Hvq-T0bj^x^8b2WHIiYPvz~&_Ai3)8?z5y2 zcozR!dg79=!PlrtQ0**Vhi+)r&WQDn`3}OYQzhES5xx%eeJdYM#(mFyscNE!)$wBg z$@iaGc3=AXSl{nR0oWO>9e;Y=D?Lja(G@=T+e6x!t({u^>Z;E8$49CnpcdS>RsQ&e z>fK9CpN!VZc|;A=bglY_Tbq6V857}4w*vu%@`2+E9ZdKI9m1zrE2s*g&!K?OEmht@oB z9)+qBA2{`A-0!wwolk8|qLB6JN`zm}O`|Bnk(x1Fc6E0Skws7U;ptsvPjZIjz0WXl z5;tJRXiu>x!z;)8g5KYox92zREm(Qg_G*_IRR9jtzl(At$0ggPAFJ-XkR88bv)>v) zNq1m8#YwEhI4P2>(|<P6#4_bBsu{3N`z1FRt0D zq85~O5wlSlS8R#vpQzR2bKS|4XKR_ce|8S8Gh1bhtVfpv%T?A#lxSB2{wUB4962rn&T9O zfDr+wcNiqQviO9A7t&sr-OR zfD(oDUU{(ToTEG5v{hPi39nX9|2{BAk~o301jBjbCHdV4enS;0FYERXi~TKZUWTwr z--bd_6iZW-*Mvea{#$03)xeq17j{k&k#=dslI?$P%MKV5@aBdja0JFUNsc7RL(eE& zlXA<8*H+(Hs+vHrf9+eR%~Oxb$JExA`B^QkiB*r#{l=vLqZC>!i#}jIQ-3v5#NNK{ ziVDkL)bCLwOXCDVlLSFw9Ld7w{-3%-+Bib5xnu34ru_c%OACTuzi^5qMN0ziBxDwD z@Rrc+N+s;OYVV1TIgJ`BfUkx$j&QiL(z^+slwoq8jEvNJTL+H961PObS@NwYZJPI4 zu%x}s*rO|3A4iL0dw}b*B*8f(SIRKCy__z)1W$%5c~F(eaVy5eEf4EZ4-|!}$k)_K z^ScnJYLIVDgXjJ$d=JxqgK|%9(VEGvl>LpVRV$C2t*^`A1j*4D>9v0`B;jX`I+X5H zUWEo09g$zgzTlC^ewy$on$$Y@PeAC*8|CPck}0RAq|5EH+}`aKCCe?RcRv5m_Uu_4*r1PS#0rjKq+F6Z^XIchc5`JwPQaU#u= zq(s8CSe$O&4edo0j1pIvx!0-kR zjIa!@xtPiU{E)#8LSAvHJmQiDCQ^tK3~*c?ajXtrxcBU%Ew^>68kyY|ggv^%W9FlO z=o`>@mS#wfp%|9Id5VW(?}{4j^-|}+Vvn$&eU}`YzOkCVUg_02+_Ee=7~IK|3N7G_ zOdWb_#fFrA?ie&l+yK+jQeA@`V?Xka(xiGXBxb7x6tgmqGXlX7G$v8WhireIe{NyK zIKVavl%d|vGX1HF`hf#|>4MtYI#g(@>hf27>55lc_0M&*cH-UY^9R6pL%)BF&akVd^Zaf& zJk?otKG&uAD*WmjKHTLov*tS>p`ZJe(dLgn7qu6p$*BF+ z2mhyrw{NW~)BDtRcwb%pei(^q-q-sb;tIQq2Nk2&*J$y;S9}#(9q0S9ZR(Yo53c(D ze*Q--pBiQPdRDsn{nb>taljn7U!e3kwL>0>*5=nKR?DAwwF7*v^5?}zf=>&zBDDEJ zioeIxR*rR|iGA$!?zN}x))kW!MG`c{(FDozEQJwJO!iV)Tt}yfCiWrKmJf`(iH1-S z|K-_jSCuL5cCtrIZj~X8O!6LbZ8MFE`Jhg1Gumw2*IJt*Lg7|ziCPf?4+^58xU0Xf zZ5~#e2wf{otfH;A={{*9v*|JWDc68E%YTMUPlI#~NYJ^mM@UKSoh)R^butc(Uf6wL z(P*9NyktI1dfK?T&Fl}?X|#I%$(s7jd^>J#hO4b0COcS(kOW1NF(*SbizvMzu1Q-9 z^b!!kU@bBy2JgAoaV=l&e-wY4iBN;5BfjEr#W18gtS41POku_q-> zwhF0Pa-WQjt{OtuaW$rQNO)W4tBfp;4_bs2qxx3RQET#hcHgX%ugQSlW}UQxP8w>y zdTE&MErr`5Oh&Ux5sh5wV4pj1c~*lmNUNwguG zHYhe;o|ai>;~ zv_mx!=FI1P?DeqqPjzioJ$U2#Hq|JJCuxyj36fCew*R(d?$)&wP##$xFnMPQxpnP! zYo9XP+2QB_pnfbL($UiG@gAvlxNq-Hd;i-_NRzw1bMWw&iCxU-wAku_KIhr~`>;R{os(*HBDLlrSav`A z#QI}r)GC2aORf!IQlxQjJRdPtaLL|N0a8oX;H)$`-Q!+8uiVIiFEmal6+jIRgzx>r zc7Q$lYPo0E*VVsqg$W$XNQxwiJiG#B&U0}C?H|WIRPy7}Xmqv+a`TWBaL!4QJS|hl z-~Oj>t#))|4UqBdd&IxlI(T+I>zCi-XEj5#4VX<%`=>QJJp2Zwf7}NnD4N1)f?;`{ z_do9Q&_18bs_xJ0ooScsKSZDSY(-`TeR-m!J186@MS_z!L3IB7Mula$UGE>_zyKj5 z@?{D!n-qIYm(Nbl*@5b^*%GKO45?r^uMjeiu_MltN3NBu7~SUq*O#`2_3T2W&jDXi zTJ%2QXU_N=Vqe`jqBGV3ZJcb*k*zL|OAuwRFvq-CSuufA*ulv!_b(auNA~rYPBGdP zQSR+kbAavDs{pS97;G!}?Zc!2JP2TTI0$+#gr@=FeTG^FG84`z@b)&mX;UeWxT1l9 zrK|zg+io_ShT6s5UHIP1Y6lg#z9Vk5D9KJi1PO z#XzhV=H$tC)jN>Ozin714*f0FUaB;GY~^nBey08$k0}%<@+>ZjjB}M~RM$P1Gh^z3 z@3r0I0cHyk+wxiO!oJB>Ipj$))Bf8UPtWf<2ECYw2{b7wxX8;s3!|Ysy{qK zX|cbI%Ch%AKB?8f{=qLMvQCa+FeipFq!a2P?bPM#AG+r;N80DNX*qS_PPB9keCSmm zNrn&vi4`d14YtDYPZtntR!5-Pcm6r=CJHTwN7+w|?y&OZ*MmDc#aR|3I7wzmCx*|z zbY=5~$??&-@;2q;nraR^p0XcX*NS?+Un&1O3bT>|FhBr&=yQ>L&{RZS>Gx&R@Tyj<-*kvvTFA4VC;wLCyhn zobv8@-415hRrUz*c={V^FFzUbme&Nk{i*gZ3A=+HPnW!MfRJQP5S4(((|1H@Q%LCd z$90`MKW+afVpD^4Uj+#zOHc&K5{I7ay0>%8*zBA?9vt60Dyp0>xHGEb~G1a)Zr zZAFrB^vH6irnr-BLh4Yj$KT8>cXFG}OZSvpy&wU7;dZrU2dZZzQi|!Zc#?g3OxLq1 zr_1G<#u1A}?>@D|#VD9U@+{5EPCxgtWt!_{JlX!?(ePWzZmFW4(i*|%hW|4?qRD+)t$9LLK9!@*nZa~ha6DO?Ii zm}c51UZ77V?9#oViZd98Q7lH_Bu6oRCXn{3h2gT!Vtb}NV#(gJtp=knIpO~$w-2YM zq)QHolyO$1WSUo~vVSI*S$%Mp-?j*(*gWz+HjnS+H-AKWwzrw~H%cs>{|!+~zlD8v zlyt%6${6k-8HpAtR#a$NfFAA|_pullJZ9N5FI8-4PA%zQT5e-7%fb${PTiwFrE5f+ zZFhV-YQVe_A=r8u4r6f+V`y2B^tOHOwS&9LA>zvUtanTy*Ec4_{x;jby>5joe~b*^ ziX_M8>J&stk!gl=vea#x>zT)&IqWq+W;;JNNd0@>OZHL}<03|*U710F$`x4-lUbZ2 z2#nO*m6;AwlMdF~urvXtm`!u-zwdex>%T5hcTjkJuqh1TloX!Ba2yVfwd$3_wWamG zeu@25>yq0iS3rYfZjiwtN`y!#G($7as^_M>vv{CjemIYcctI8RJGEd4HQ#$>Pc;V_ zMB2lXuiC$D{@&sv!-DUfN|=+92*D|kEZjT&9Hb^4oYRL^+Do6kQU7gT{|Fb!(io33 zB+F1dM)=*L`6@1OE94aMx_w;w|L{`!Q7$PGqbU-jFpgt+j_~uLspo$zr-(P~n^!Nm z`k6a~6Mzya%Ou88PK6`%P5>^uM@K4Qxd`k1ZbYZ^+2v1%>|1}0eaOYN*5_D;)Fv{>4`&4fb#IPwXW=PMo@XPSB!h z7Uy`95Je}gz@m5Nv6XX*{?uOa`8iE?R4k|0qrJt_6oKJ1$NGEltgBvno)@8~_H4C3 zyXz!be=xc+IL=>x3v^>p6jD?B2(H1hr%RK0bGoeG7=zBpk9kO&?oxk$OFaVcwA!qV zN{7Y^_#XU(t@e5?`S;diXd`d4o&t&@-e4q;QIbeHg(cU2t$jSLLlJa1p_g^}u+ ztd~ld?hDH49^33h!{U?arutD}X-r`WT9KSi25xv)QO^LbR5|;JpyMQ2T?hAz$Ox~N!;}YlypVn`ta9^P&&TgGzUGYOFYRCV zY&vOxD~NL&496LU!C01~FwE~n{d0ysy}q&+pF5#+^_IvZ(FlLhoJM1u!zB++aeHJ} ztE`k%PfB`kk4qNPZlBYU-3vCfIH5c`EDDL{sYQdQMv4gf`;r)=+J9p|Ip+I}pT5zx zDDgajvn0h)97f>&UM(i5H7coF!XA6KgFV+0S9PT~Niw9&3b;hce&K>{7R_sm@3U{| zcc9EUbVww3d^;)4fzy=3nIVn*=n>P#n>$JrO~~VdKPJTA^NMrDAh+9BJdsr4{aU$J z$bRcwpDhu0@(RNVq=d7=?c>7mV9>;6(K@AkXD?c&Ue{3#f(23FaY`f}w7eP}b#WOfZ zkrc;b6f7b=-S@wF(RxK3wYS^y&5%Vm(8^||j@OQqRIlEMafHZl3PCdh)3N&0{x9EL z9p6}e^M!W7dl>w?(ewWna>d-8eD$ma7Sjnk^~3ya3x7oC%t{1|>cGexrm(m`5CY*4 zQ#0JM)Fnd{Qr)v7OO~k-UpW$;&V2WiC{4Ohb>*>4T~uGGudUb~)dkqhU(EskzV3j} zD)-)T?eh9oj5(zAPEl@#=C&-Hu+ORAxk2(5!B3HsBFo{pK;oF}H!MF_rA-Chk^hrD za%m>}S9`Q9N)9mhQY7W@jLejK$zFHO?si$}q9-LIz4owyk*A^-M568y2hxJDL2HHj z+5S@E50N|01zlJRBF9Ps$+5Id!-e&#!+C4LT(R@)%f9RI ztrL#s*baeQR8f?}o#k|UQao86xtA=;DZ^yf>)#G=4tOfr5{`m%Z?=KLq@{>33+_vP zn04wC@gHx;92xwVea8CLKNa~P=#4W)5($=a5>8Qe2E1|hwndvlLchPM8=C-pK!d;S zZ~JdeA3ELRas~a>g*OhWf^j&{3o^w@W8E!RKY4n6w%>szsvGFH1E~^$KK-NW#_UUL4q@K1Wn->PEtJ2<9_zWn(AKPN?Fgt$xVCPkFxr` zl8nyyWIpOYgENfJDOGPznx}PgN~&~2*?+Zpw=pq0)t6rSjlG>-UF9pLKjv%F@#+@k zD)qTm>eA%NUwqGU5g(II#%lN^ z7Wa*qHIHX#_y)_}SR{D)Pmb$yJ6+V&S8nGs+M|D{eALeWn;fHQH)5f-T~WQKsQFb~ zn(o^!_@UM9wA21rO+T97Izq!&#Lt8mefgxcj0pH$%hd0>sgA@=Z~ESwyuz1?M|}0$ zyT|8zzn5CCKVSC!JzlME-GRREdT4;J{2$7xe$!{Fi{@s&^2e&b|FmB{ii%F7-1MbA z)%+Kz??tNbJy*rw02EdZ!W1SS%~s`rQ=#q(6-m+&RRU;M{R}oIKlWC zpv6V|X|cJn7LTc?#XG8N@wKX2JhFlo|5{dyPXjy#^q+E(T0Hm zE&q>ws(m5-Yi<6`W@_<<8Cpz%@vs=w|6x%7kG=(_=ufox18R&{`~bMgvs%1(8Yq9N7LORN#pxa`e(7PY{8@EGyK&!kkG6k4`=pj%l}2gt zkPI!J1j3I_2i8y#e19!~E056P7T|j#0N&=-;+6nE^@J8Py@9;%3d#rg!!BC<3y8wu#9*G#Rv5kdU6F+e|fw5yiCH=BX^5@5$;T6}7+w){%(YVrS^ zTD%z??HUX4D}Z0~0Y1=5i^~9f5X5;~2XJeEhk^KzZ$GNVYXF|o1pJ-T;`4z2D**ow z57Od4D{JwO&9(ep3;2KTBW-z|!TfeOU5iHo{>uQ50sOyY*WyKh{|nk`@dUvCJ%GP6 z0agGe0JgRW&i^8S|5YiVe}VcO0{Q<995t>6__stYz5?zxeFt2!B>{XHoGN+&oB)~y z&NVp!o(bl+G=QrD48ufU2lC(#fG+@i1Yj5pRspyZz{uE%yD0M-z>5I}-NOVg9K!m+t88WSh>gNT!my4d@`&O2LKx4im;rA1 zq89HOqQzeVN2@P`3Qq?(3E;h;g8zWiQhx)yKPouh2yj1u`vPnQI0@jU07n_%S3rM< zS3(j1h7pK=1O2_#E$x7P6kwXxzVO+@V19_!&Ic_{Y4aZs)}z$NKt2gt{0YD>0G#R2 z;x+&?Kwo*QjTTP@@;4P=C%`no4FHa47976^!lh?|0s81wE&oaZkD@>B0pkY*nrsf{ z+dW|X3{KYKulj@U>8{0-0RL{X;CpD$zThg{Th+n%1@qY%aMrB@z<+`H&H)5qN5H?v zK%i9s_mLt2z6Nf@e|JU8@8>RS@k8ge&({U~w`XecAQ{+WK>vNW7Z|^Q|K9<}(P_1{ zcy~1|o(OPvt-oZ`J}{vi0_~9i@Q0v1-T?S@fPVn(wF%(Y0A39yq=Boo@+9KCmjCMk z|3`xf_Ptb~kAwc+0<_1qzCd39?Xej+`u+{vq%Q*SaNt;*1pFxm@KwN%T>$?N;9J1a zc_+~SFM#=OGPtME3v9%;2bcnm&>sc*_h*l3@%!M=?l6E?0(p=Ga3bh$^Fe>;0_N)q zK%p29>R$@rq5v-i`tKz$-!A}o6u^6czC0b^t^jWa^Yt`<2Lh}N0P7FXKkI_^{99oC za|?`b0pQV3GiM}{|Ny11h^f*x4`$l0`Q{%-vr

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

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

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

    G)uZx3(^uP<+jQ?h!{vH(sy+2B1vHV zzIm+mOS68i1Y}f6XS}S)4}#ZF=_ZXgYt_ZKKY{m(3NFTX7}tlktE$=8!19Y@ed(#^ zeBa;jqc1H$_EuX$^nS?Bbs9vsf#|;=VO~3Em`#O*77|4FgXpFZ?Sl3)??Uvm5G_G; ze~1<<`_dyI`ij<_4i8xKVAa#g-&G)K*q92543q+rTmZi@j+5yqMAo?m)zsEFs!gkN= z%i|jkyAAq(h9(UD&)xKcOOrZ{0E^t9Ad4K%VU$QIxFSg_Cj2^ZTis`4QC{p9Rt-)^ z3#fF8n*M`P$}b!b`Yg`nUSQlh7Og2Fc1D7y``(_VRxT;qWcoCd~Wnv4b4(Ura22|)ER$_ z%jol8o-c$p^Stk0)E1--&wGFT6XShOJmh`;&LUq`g(>5G^L17Wn@{!S5>uWFuHt|< zMs=uap21y?etrE6=&Yv#c2uu=@Yxz?Lv00 zUi|S@-*#yH0AYkAo=6W~#t{FPHqabjis!v$m8vftP&t4IN-`5T3GdLhBWt>W=gL1# z-_LxfNr26!T}f92hhnz#0;|vrPGJ(Sm>uc1$rnZ~`mWxl=c<8~SPT)u>6#1;J=0b_ zztb=reH=q^IF0cP%i%adQ+~%WcI~|XAR>u|p6mOPr|upMb_%A$9~^jlS>YDJvQS#}n?gg<4VSXSJaGv^SR5X`_*6_`*i+8G|)D z<$*M@GANcdTNP(FS+*+Az6;6Ng5~o#z%69BwBdR^-DUxavqSe{NZ0$K8L*$C&?JeD+EUN zqYr%$bMpCkFtjvjfT1HI$*N%v9H6TCr&fj+wX-R+skA#*8;|wDZyvNyPxjwp%Nb^x zFpD-h`^u|ltUE4js0{YcU&ICuT`_?}bn@*ygQuQ+)bKafzk_(r7!fB)Gc=08;YkwU6mJbe+w3+ETD zcGKRoc=yhpt1hsU8J6K#hQwIEbFj7AMXUpDP;*pAkNWSxHw+8F`2NgiRNy3b zV_5vVcgMP)Pu4KO`s&rn+49oS^R4mmuXn3parw#^fKyUsS>ahh;0Q%D%ZkHkw`Dh& z{NZ!NtD9l~vvkVn8yXCO>0HIPg8W}f2r-CGmkez*-0s=`;=ALUGGGB*6BCHHBuHGu zX_e<$g}JrC?FT2;T4@A#wZ>>6JoSUU3p|UA(>g+J&^HXev;jdI!rTjiL7#wF0IEP$ zznT-F|KSxm0TB-hksclO_yL$qsoZQoGiVF?r#7oW96?_yD@ z$0mjRuDI|*DW@MQ{=)L-9m_d?UtG}Zo`)l9LBNaF-gNGiQ-LWdbGADflYnU1-nmxgV|_JZQ8r&T`C*9N}VxG3|6 z@befZ@O#BA1An}j4}}hE{bFC!$i8!H;TPKV#u?qcVSDZ7*`1wz+Xsce!{j+zm6JJS zh8FJ7!rL@v^opry{d0$=vL7Gjp~Zxg-iE!ui*Nhl?Y!Z(Bi``e+P?KW9I|DYsHL|{ zOMfJ!)on+#Um7A`PnNO0S^gYMdloJ~vS!U!VVHO&XoiZmbS)M3dOPYf74?`WNT zLUN>?q^qaes!rdxxdblIWag)xe}M+aS)NgGMigWcb%7=x3JnOFgQ~%V;7MTGXt>KW z`ov3Zoh+!PEdi^EXDLR;Se{ZT{P4uTPmSpKb4<-bj?q{s40mmfG@bnB=;rD1-&1K1 zr6BKzaTJT=EKU#@#o`^ggY9mUvU1Jg z_Ozrc)83W_SD8615!B>QpO#JC4p7c}J;|+)uXf)7`pO>x`wAnH3@vdCL*d-{|7vFB z-iT`uCBDm4$o0TfV(Y^Y&zeW~On>{X5N=gjiWe0X;|Pp{Ef~}^qI%gdPwL&9(}#?! zhmJdgBnWewEpv!7!wE-hL}YrHCuiH$jN3MVJGTLR`W3t%Ezk^sYjE=Wo@`}q6FZW{~w^-a(?kqdF* zr8wMkWKi}pW*VrB6vPE&o4to1rI~q=#|4TLc$^X1-0{WOtrMqJgG{>RT^cupc>Yic z4d?V?iU)n;XoTnJu*FaB=n%vrIY#9!FS+az%~3o&15eXd(lf|$H`-J4)fZYOpGJFI z#TbUBm|WYABu7vroJ5nKym=R5P+f7XC;P5>=dD-4@LO15Rlm{w%ywE*30f3*Ld0mA zTzKKc_B~ZWe1E@tPrwjhBvgGSXwqtAEF3-7^WtPCXU+Ob1=2ATFro7KeM!IX-Z?)R zzTkJsSk$>t@O0_hbo-d3ccSTBXCBOV|=ea%dlsN%9L-hBW$&+ui z+J2|2Z#e7@Z+O1BE;uU-ER4w@d-`0adiI|SHgLP23&5%o-rJAev#mmgMd~pn)Bfnk z6X(pj|LOP#Gzl0OEx_c>oMhjLZyS_>lUmqad*7plz)UHyTQo+ypW90Ne|>}njYn`k zZMA6#I2EW2F$J(hEv}HGKNXUWn__}JPp(@ z;yu(nfkFJp0F7P~8(zM`{jLS{-Bm4Y3GrWR5F2|tEV(oIN@5LAyjKn3#Zw=Wr1plM zgLF`P^(Si=t$*y(!djYSyoDwl_*}ExbD!3rr?i>oshf171^pU0+h9YCi)f*+o}|rT z?i{CtneHZt<_kIhz($;ZfavQc-+p#?h=mUzlqt385!N49$#)%Vr_Cc55$g&H>$UR0 z2MqN>d>5_}qSGedb-NX`z<_nUI#iBr(E2@9i{Kt;JvTw~bhL2<2tbX&`w75aExZz; zl_pb&g*RRs$iNv7ux->p8wh_u-#=mW{Vp#a?RiXG`Iy(hy7@o-`c>iPXQS`RZek3L}v%%|0d__Uf4&NrZ+fRn?06|_CC>RnogjtYlX z`ue6rhmI(tJHnLukL8r#h%Mwi8Le$tY4Me%eJkA_qTco8t8~#I!Ui}F2_k>gtFTzG z_a-ej5F<9zyFj5pYvF{L@ap+UA=44{s@9?7rhzVJ(4nL30X{d5bRT;$!t|C^kb0{g z;rk`!mg1h9q~9il`5YYk-Jyjo^dVm)JXyl&>DF!{LzQyMXT?>}_Ao~H-t7^tzcC@^ z!5Do=l*|60qj>P{O+Fwl>Rq5hqI!hwulEBTI&|pJp`+{o?AsPa_}zjcrl-5j)lrL`6Th?S7L8WIF4DCmrP&T!A% z>CmA=hmI1&`Y0(dg8lCzy-G_2yS1>q$v(UY8zevy9~FM@cu`K4jul`?NQyFI2s#xUM}l)bJ~UZ zUU8of!s}M03ffN+L+no~DqqvCoaMJl1>e`|d}7g1S+9Z;#aUzd7@t+35OzKvulMf~ zMbC1U-zSQB|Il3DpWIX^@8_9%cdeN9RYPy8n;QDPnc_i3J14HF=VajCYGLoM8uk8m z1G*eTM_EAUGM+!(PEyIVe;u8_EfTux8$NyCSdNfT#P3R)e<`c`gQDiEe>v+n`uUN* z4=fGn_q=uJ&{5XV&gdH!c0JYCRUJA%QT*s>3?7F1uDoNC(Km$UYxRwR_cy>_WE>P0 zb$`^Xz#cx3Zj|g<`N4#l^dk*4dX^SCB(tcR%~J^4oIw+eIYY@175e}oL%B3=-->Hr zR&+7m2)C^?*V@ULt%ueTKl2hN1sjvO|i0*H~icbI0=jA@!uYM;ly%e0yxIA3`LMQK`NsH7)Yhl=YcZy@(Vn#NpO7t9hf}ahicK|VSuHlJnN@jS5P4jcSoQk#>zBSs?4Jml z__3Ev0aKdlSvjNn<42PjjUE^8myUP#CmGJ1nl3mTsWz*_Y0r|J7Mr!Z#gQpEr8IM< z-6kmxM?1D^?|Du8CV!@LKsrhg6FQiDLez8k|I*HHD6ESy6e-{9Vu~kUrG<4d#S`B; z-ETsfMn0=B!&B{-H%7Mn`WEd;?*g2OlCB621?*N>+MH^$I_-kwOifdyiHd#U`pxJ5 z+*GFu*i>1ya5R)!V#r*~@+|L@_GX*oO|>V>18+StgiN!-i4sH76iJg})87`n@%;UQ z@wS_+`HW>GUG&ChzNfdVOaH?^)%Rz&^7lQMDzT!V@Fb;jFn`q_*;f{NTI~42W_E#X z>c|2;7IaxVPf3(W2^1qyvRPIfPP;9;X7hV59D4QE1khdYF!_cs;O8j;?iYD~KkDgG z{gknZv$=n13uKAKX_A#Oj3QN#CAie*j41x~G0z8sZX4P2FTkkPubm=$fX!V-@JN>*ppv7q>aDIH+vdyUlw)p#Ff)7xmVX5coI`%7PYl9{1F7y(n zDlA2*BBSC0bz-Y$^esKA@kR^kojn92zT;b=9!V%W7J=Rn)r)a z^Cw>hV-!{(6^_C%vm@O$d4}mwi+1n7QVH-THRF6k7~uIPV|ed{&i8T;Ie)^*y)&CZ zt1HxD9JUO_Ov|J|h?vT9xVUTJ(OXu0^-)3tM9mG1@eNV`M+($;^P&-Vj7Gl=GaTcC6=2Q+fYpnKs zIB;*yEi*yq9TceZDg=)cILk{I!{6wI9myGemfK#;@HGM-EsQ>fxvBtAG?V*P{ zJa8*oJeENbW{2V&WVcOFBKb%J!WP$#kWEf3ru-y^ zNf-8cq~GZXJRh#}ymh*^_4sJOR-G~gWUEw*RW@^y!pXEOOEgE$dcyhsE61Kfj!*aq z=EV^C`3&)0D0D00`)IalgXh8a8+SW8p$}47jHOtD#CbCJJQat}IrnG>sr5}LHsVE3 zd``#Szy6L)f+Pu?V>ycCaF$^i)-MSyycfxhZS=VMU8-vz3J&3W`?Jj%1c93~?UqS` zQ|V^63)zmzliuj}x^ia{U~g(^$I&lqTw%-4eVGJ8vL(HmZaj^!_W@4LLl zedG;0z3eT}PR;+&mT$bwb3cuT-l6Rh!T%s^1l8Je_DZ|i#GYyZ_kwtD`q<>l7xau6e|eIJ(+u&&RF?VA~@(h{Iao7 zA;2-nwvXus^8X(PWgO;y%Ig0{!>gV{hpINCb_5lB@G7awBq_^`Ai`Yts&6YHxbD|H zBX+%LIXfl{>AK2LGDXO|q(}-ZUH_zMc%3NR_oDqh_zv}bxbgm4N{3f)x+v-TYaUZ) z>;Kt1>i8zEe^Q}BOL2E+ST1*$+~Mvk#TgbNmr|&a79XyIvElAEbhr*1FkCmb;jY7F zFa}M2pYq;o`(E!Zm$YTTFYll4ef7ECldk5~MN`xXsf7cJi8t{YjnQl|8V7Z+F^oOm zK9JlyC$;$_y*FogrBPnGXIqzWcwp6|>IDkc-e@bUF;bk_D)8nk^M2XTVaw$pa_z0) z2hR9{5_mxu!<9Fk!jo5V!f#B_OliM5-+9TE7%io@T3C}-W3^aHpK3ek`Knx*$vZcl zRk!5MXLCaQHI9XOWpg8tP};V{_6iBSQ9o>5m}CHHH)62|K`X%-ei z<(0!0&*F^{(FU8JxLrdn^*mjN)kW$FqfUix+?6cnB=dAbJ`oNgj(aV*UljPdb8RBZfvf<{McELN6f zDXlTnnT}^0Bz)tCq8XV_zUAeJ>W!r1qMuFeaXNdJtf)T-dUT`4ni2Do$1Hmj`etYD zJ-s14+~+usqiKrc8J5v$XmYr3_z8Gi)JG~Y4kgrWGm(DY4SCT>Y=P8jHL?_~w`i@L ziQJR`NAN0|h;@L|;&mk9VWVl^9Wxe}A2wMdBI5;{L8~)aD6?QOF}&WdOohOILr;aE z_M=L-qG>AfY1LO;wO(Xa#CIkqW$#4|*TQq4q%t|uuGGlsNJ63gD`%`|UMzKaMLB8{ z%!WP@miTT4R;SmPbS9Hvq4_BnT5>g31|-cie4Q4aqX}Pj9Aj<7=9R1(JkM%4Ua!|P ztX`LDuDu^Nt6e49q@xL6X(JBjtc9X-S}js9v_?H|r7Wz~%4$$An7_}@v$bnhc_;l3 z$%{jkN*qrt{V7`sa*$AzIO0=+Xy^(Ncy{uJSpruR_+7%k3k3cffo~=7Ed;)tz?TyE zLIR&m;Nu8;pg|4GfQBhx!pDGk>h>YnY z*qX(djgdm#n5bBzO{i;(j0|75;V;driJ2Cp)8O?(>$~bgGHABqs{7n-o-de2euzZ6 z`?xq6ayEH#M)%xs=5hEC^i;yZZ;gNXS1%+o%>c6uI3{NpLxWy%f~8qZOe{Iq(5b~? z>XF0ggkn1mmmDwz?eDlIdS=Yb>KR_oC+_sq%usg;;I}plL{+7SAL!IOAocJ(FT*C(`XrsMsL*X$T9XgUwJ?NOc;G?PPavS^QWGbr|?0`YE3$` zK(Q<(Nz!=ZHME|hbqu*)e`hBc)9;ns8+R*ViFrWLXOBvzp05xm zZ5hUpxH4-pntRl@S_N}_-03Ov`R*O#{7^^T9hybI1~q;-p4#`@33Hmn6&lb_<`xx} z({ePW)o^-B$E1p2cr2JLm2M&B#l3`ocYd>TPL$m5SPjK!wMMhiXw|di5=nk6o7QfcRhTP11B?$fW%R~%(eA2BTJxmyv@Xw0S zgT+&me%OVNx|&{*D*;L(X6!f63ibxI7@eFXw4OscmDIpV@}iMwW}77PeX~CY_+6 z^cJ&b>Z&qy&2=ZdTVzn`Is2VL9^wB|^?AGR)2P1-Hjdd9UIZxRHb8ig&_4;A$ghd; z<|2OcqW)o*6Mvj-^6I2kTKirk& zdZ@n>V#W?#ICTK}W4|0*DnJ(|X?T-~7EC%l$4lMehY}R=I+! zRj(6%8M&bHrR9ZFuTdmN(PBB@!f1cqq7S19F0^d^pDHLTV`Gj)pij7%BE_G)>U*0lgHm6!dGx0;3*f|FMDq;7Gs#T$4l zW6^U~juXs+!EB;#jre0+sjN>!N`7YTz~v0y-jI~~_TBzfW>=|-if}DPqzDVNPOBBP zCcR0gF&`W=rH9@tc!cLUg3rn&T|5KZ7&URY10Agjfjh8Hn*b~bkJzA%S{2}`Q$ZS7 zY_=P~uc5ux0vw1!4^K?=!kRcV0DDIGzYGNs)^un_6%RXy(Xa$(gU>}3+zQ(NJ*NOZ zSB($g*Frl1YXC&O#R3?gQaJ$33~P!F_P%?v;+^q&A{}SL1C;)1ywB=DRA_X%d=-Qg z(aEp#!YgWSm;q=Heq|&FVT-1~gFPbi5L>h{F-Dx@)-QbULlE)LA|OT;hG zMSoQ;`m0K(@+&oAPwD{SB~gAbV#lp`yD=25R1MgZKJ~%l+$+CQ6ZQOV=ELu>q0dp_ zzbUV2b;~3Mh9Ar8&Av{`Ke`)kaXS0ERN=y#vs0HrpGZCNcG5r9{!H2fRDO7(y(i1r zOEk`cu#7}K>6F3k)*2^rYhWTF{B#P7du z7oDZ^hu-2aAf4?gZGQrM{@ePPQSG*rt1VHFeB2DTufq9HVK?J->jUKnD(~)|J$840 z>xVvmd_WQsNIx4&U@m*Y@c=?^F@%70AuZiT5v3;~J&& zac_xq&Lh!Zpw}*?!reQA%2_sva#!8Oek{E%SR;SHu9lEH^nurVmG*-P|Itlz|JA%c z-v{>Z|D}IXf8^yRKti8$a~$aNAJrGsulh)g!#=h4+gZKIXqP#I>?&=4?GgogX7hY5 zrBnM1YG2apu5aHo*58kHJuyZiy`<`!lD2 zg+8isp54WGDs{J^;!F()VYdv*hu!LoZAYBBXm|W;r%D*f9(E9_fmcX{Gav*(GdGPF^6vXE({kaQ8*<#!|;2Mva>Em}7c(x)6Wg15et z|1WdHftDW;addZJJxI^&g~u;|n8%~_YP9~41Ij&tr=K>$iaY?uZ$v%Zf8_2E=`lft zrwek`;3E{YL<57;y$umwbSS1SglHcIihf=Af%bbK|Iz+}cv#MQLAg^?@%eXKiTp(V z-;>CnO=|ggAV{VsQbX+*y4Ca_4Q>$Kb%?g^^Jr-bbnchWI>FWZCiXpf;fAMxwPhB0K zk>zufmoooJC$)T7&dGS2+$S)vk89(><7rHfFA0Cha(y<9^YjoG^TXH0@~?7|A8D=c zQsvFrqowZFA7|?-UTK^sr1$=h^je=$x?iT?f3*G}l`n%8>MMOJ!OO+=#ntj#E}hG( zyX7&x->3^nXM1La`Qznc`WJCC|2@v*JTBv1fi(6HUMg=pHQW4K#~8_H9T;9ibF_x# z8IENr4S9>wtSs_Ac-xP15nT%XBY*ibYvQat&%b}~guMKDs=}W)&Mto@-%}Xnq;LG1 zzP zFT5#1;DwX=USIq1A>FgS{u(Vdq_|fLdw-l}Mz+kfJmrmAP2g)ech%0ywV%E6-33*? zzlX|Qif49GlshA<)o?VgwNkvU|K*akpQm_d2f6Gn)+s;xsfODPofnC`>4m?wrKL26 zW|8&!MJHbC1#VM{v5{|j-I9FME3-YfdCix>l~D}0iC&4sC7vdwbsD2!w3N-WX6e7# z3S??UjDju`)5PHrZimyM2XOg!Mj3q7V->tVJ@yM=>CsjI>!4TS;jpA8^yuAi{B#)_ zRUIdWLjz0Tv+jZLr%0=B06Y>0mPdX&2Jo5CKKeU-11`g%&9?@a1qWb!y2F$@=j8NQ zC1RBlIdnZ1IxWT$s}d-WHV*<2of4Y~;Z}*7f!ugF?TLaTkF$Qw`{;>I$+%tWos@(u5czQW0f70rIdQ~UV zj`Zagh3tRb>|f~E5HzStadSlS3px zJW#m7%9~l97g!CY)Bd}?d;3~_^Y}0HOvXLV<33vVy(bC&6PG-eDrXNbN%v&GQug|; zNvjt$8l6$#1)+Jv9lyLu@%nC^G={0p2a0k>Xv9C|LMw zsO9TIa#_&X#~&~E_mKWcH|2}QT5l2b9H%krIG%cFXtiX})DD@IZhpxm0n^K+%ujEu zxvgPu^V3%W`&jgAC&kiwiek<(ETdy6i%w@DSJSgdCR#3b8k$AckBW#- z9F8R}Rn!wqy~5u=H*SbF#z*uPLVHD-1RLG|SnXRWo?^)bOK0_HSasoVjQzxa%xW%& zy{UazNXvEu<8q~LPU7N?;gxzNR%uGdQyNB~!@EuYdQxCi(#;e~UrbH@g%)N%xA9PH zzqums`{1hr(MFpsrjNnG8nq0qF=@<#X>j~eW6Jk^NH-=}-uE#3iZjL;RY!_Eh?BG$ zuVD@GLf`mSkr8GgPMD{eG%#PTOehyNz@57ziDF$bn>ZvLNl(s&7IxdgUO{`4E^$39 z-qBGS+QJB0t3Yeojan8s<@~E)XQ!8T!$O&P2!LpJ|5!+;&@N^g*V{*lL#3{=;$_H| z_N#T9*Y__k`5#8hvJ9uuX|xo>>sX5X52J^Fj3!7vD%Hjwo$Y1&BS}k9L)0P-BExeW zO9^I<612$;V#&-uEg8}#XHZ#k;gYz7=a4r}cNczcPAYz7iQ=G^m{xhI+S~uRa_0BR zv(Xe64V8Zm>NdLn2eF3#wek=bykiEO@7jtyvR)#2X*2jo(&Vg*1!u? zP2huzJM?mG$(5!lVhuQ1E4vW4Q}=lj3`)7l0l=ptCmM_L|Au9>oCb97>+XRK4Eb)0 z+Zkji?o#}b{q<`~yCk1p&Y8Wxy;UCfe>8_}Z|@N0|NKNa`q9`vb$?H54DmLjxrbnB z5D_WV?;8;pA9rHOt%Iev`v*w1$d}8i-3pFc)xrL{SNVK@I#AzuF2%qcdlHS(8(Ilb z#_0G6bK_`>(6@24H72=FTGp@Rv|D}qg`ifV2oavbYMfpqoo~LlpN?}ed??Vxe&W)+ zYcn#VVNiR~jeQzD#T$(}jn-l@POEog|FF{4RAt-@%g1_X-{(KBJ8D(9y$>5@ju;~S z7ACEybQ+$~(Y&7Hv^sKJc6O?9OBeJW z_6>!XM~4j;nRVHsg+2zG)@r3qdRD8o8nu5HZ#k~(+wGZMy`V8UFC*=p)j_W(O)num znNImHmN)Atil;fVPM|7W-yIy+$QFcV)#h#=ssKjY$NzZT`inPe3}%SVi5S63nJh+) zUf_f|!yIK3XU!pQ`aPGxr$abVg0DO-G4@M^YL2qiMRB<&8W)z;noL%U#%Q4h)s!9i z=@UUoJB}|9jrePYr9kN|%4lEYLAdh9_~H6!KVf-4@iZ$1p`XiX6KlV8uHc}3d(fb9 zjL82ETFV*CF_DqN`^P~W#|t)Ne2gve{oh7&X0KcreK+{a3~G4O>y{t1pVQr2d$SVI zj|iW$H+|69H;9O*=NYH;YOmt#k@WRi;loiQdpdO;fB!Ew##Y~G?q*;aqo5T`I*Unb ztkhI^HsriIIH!asCw=7UC*91&VqR&Jm)@?`+rH}YSNflJmPkGJ4mVqE0vhRRwUnWy zU=?gav{`6pi-;HE$TjGWM9VkX&EaKl`&R9V?Xycr_PJ)g&Z=Q4M$fWb=~Hb7C2rhD z346XH$Y`{6zN*vW(8q4Kb;wjmk477NRU*wA!x*~9#YDFhdiIL239XH`a3Oy5PfrK7 zuXNixCo!A}{A6lEKYPzvue^7BnLWi{0afa=7!1ul`7&ERce zP_uRS7DkKw+mijXL>VYXV=%>7`kk?5>vTD)kw3X=+EC(uQ$ZM>62#JhGhC^hGdwwd zqwQ18hdb76i~3pdGf&3EchU`d3&pWk)~qoaDMLh5Y-Enok1H=p@JkxNUfQ7S(yNrm zpVT79pZ+r8WBcI-91m`RH%uMFa6E`;x0VsnZ$scZ0xw9kM+FG{Hc^bu68JF!Cl5W$ zA@CUl{vCmjCGf!n9!lUWfs-o>mn+=4-pi|8Cj z(JZZ}IX%PibgD)4cW``?AW^}-v3F3fJ=MRQ%%<(c!8mQDEVR|cX^j?xIVQGW*WR_y zEk2hwGy2{0mVhOR3@b)JDd#P52zduO6+}RE7Z1`y@GUW#dh6mSHlrS;OWB9{U^B;W z?K_5E*}@Ky?!`EZma#H4W1$&x=IHNoQ~teOU82~KMJp6+))#n+w^+0$!EDhu+pHh% z1Wzv~%;ME| zpQSj1cp5gM_lU;jp3{v56YaZ}ber-Bn$*Kt~k<%dj+-@WqO zWXU~sotnzyQu5B^NVmE~+T^8kXR_TiyP@gognS~oleDyC)-gJkGHMw?Lz9{E1^k%m zB@a_)$~1e?3C+tDX;Vyk5zQu;4Sgak@!br($)cgGjEP~bR&v=ax1<--?Uw9)FNU8e zn7YkLY;uxE@`)32Fo=yK2hhkF)XJJ3FVzhDrGQ7jYU<=q-KC{m3^r?MGs{{nI))}Y zHL05zksmYcQ%2-y`jsy#&+9~n=sL<^6XIebdkdklQH`TwV{GyIOs@hfJsNy53mV@( zgK2JqjdE>R(53d4Hiv1@%pb=Q9GdPU@|kv3=kG_X^eU7(E#Ge%qcqs2F>_2*R78Au zx9O|vrby1B1SGxjR?W57t~$lsb*1#0v5wVIdQPjM^$g2vwPcQ6{|zL;L5Z?B*KQbP zdla)Y=|rV>>KxN*7(=8G9p5}wuo`yWTc@CLxrH7x+I6?KVpKnK^Z6LeIXzaz$IMTz?u3&VvxR0zI;kbM7uJfVCH z&F^76EO3WPHCU@^<;3H0Q+x=h>fnv%Thd=Ph+l^9!u2LDZaVW9uuvX8+<5?A-mn-( zY*~K5KLq1nkX+$ zk|XUQ9Pvqi(mn)RW{>{6#@1h#qc&lIXq!M8DWlcMYADvIA9X8J$?jFp`k*Rpk?=z; zfv4BD59xMpx!uuf%;t$Z(4>@0IVp`dMnt0_8Lc&h^)f|9#C2x71ts zS<29|Z$^N?W>e;j*>G+xGtzB~+B_=6l+}y8M$p;S)bv+!D|)7Tp%5@`)*h>YnY*qWQV3+DLRad8ph(eHOdx9Vn$6}oMjP-=#D zR>&l^r!;w-^GQ*9%V)G5kMlT>^Ei+5IFIu|?SPA6S`2TZ|<4b8(EJ zb)3FcOfQ>R0LzZ#-l5GdzQCTmuyWFVe-R)4_zGHB8IyYQX^l4Uf}UkGR!(E4wYj&? z%EO&*?C8WfSTN=18N> zn0SDQ+}Uz$=?Dy_PN`q*S$uqV9T+Hb>r#r8vKqCNmDV%7mbT_7{n(Il1C;9rqkgkr zEjnqBsjc*G8cwgL^$eraGJ366m+FS{(LaKuJ0$rtrE} z^SmzA8P6`e0Nmsw<--+wwMPYp|9M>Gj3-{sSuJO%ZL=Bs)sGaSgy{HI@y2)|PHG+d zo@A7++P{7}sY}2YXeD3ds2$p*_?f*6>^RZ|?ZOc~3~Lm$jFqB|W;1Oy#6`yR$$6^;J)vWQH(GyO=Np7U z(jj{I9|02Cb;th0&P&mUtkQ*Cujd(-X7xInXLYHIAOpj~O4g&l?D4HSKjNFAB{oi^ zrJ$(9Yty2`BZao{?@M8(GaX0dT{Sxdwb}E0lvdkE_G^)Ar>`*PPhH|uu2vg4mNRO3 zi(u4~gAYp;>FOo7M$B|fvyZE!Q!i5Jov=QW9~oYAD$TTJBcRi(ewlYgf@w>PQY z<$>Qe$#Tt5G|h07M#E}YR-;Mv2vUEzbz_@^7tig!zurCec!Q(|6su!d9mi@JEzj#& zhV&o|UZFldM5?sCwl|qnfcm<4M&BvT{6Yve4Sc`R6q~zpkzy-2bLJw=ZprZm5h_L04hHU!X@|!YqO*mL2wz z=iq%g6=6|&)<})n*$e~5qq|N~^5ww8iGCBKrV-gacl*;A+9qe_zxKRy2Y+cfCcpjN zY~pDnuQlp9BRPa6-vo4qC)cwCdmZC1RgUCNDx;zYle8RdWdyxh%j&3zYvVQ?u#ERA zM)cXcVD_VZwfTtRL2e?DYBGHY`loin8l`Q5eUxv`9=lqg8hXx4wuUkmtyM=;6m8M) zZL*mwZ8&rTzYtx$>H_% z;H+9!*=u+ifu*!U;(V7flS7S0lD$))W8^Tt@`UfuKD_0!m*nfUR-@IbGh5BPrg5<` zou=DY1*6HU-pR0)o*RBI2vDODm8zOMjo-Eqhpgm@g|P98jU7lO@j?F#TzXRK{q+%^v>Ak zO0CM~2)@2&@6eK{=${TwJ@sW6ogp@HVX;-bO)y3^k4;=5i_7}$!nY&3Rq|Q=fqrNg z^7Jwum-uXyCkZ_613INg!qJLw0SUT};1`mRBVE$zYlZD0>3>B6swDVXCHPqh|H)e? zoV|miw0kI=q6-&;q4d}Ot$ZO>APrOcRXqLG1 zUa%R=;8~*gzeL|3Jt$;-{Ibi9=RxiN&+_$C+t>X%&*v4N^iCHteUjHq3OJVEn0q>? z09q6k*+3G-Qmi2&E-ca*5#3TS$Ji{b`^5@t`;Mtzpz`)0@|MIC@D2y`W2tmb@hmV) z1bG#jLgc{l5ABjy!Am;291iPvCJepkC$eu$>cOi95z)rT$bNARVr)%%Ma51`*ikd_ z(XL>L;_0lI_EEbgFBPYfj-@rCUyQqpRvgQUuMx+^L`JfDmUT=o_B>NST+&T~BGrmww1zlil+YkTh_r;+1Zza! z?`KqNb2MKwHJaz`R*OTW8Ld`TaGYG!zU4o&&}uJEMZOM3{cELQWl*IO$DO^DSCGK7Nc8h+73C-#pMBNEQL;9{%c@{v&|sNxsNoKB95yOstD^Ziw@z z!EJ<=5D>6Y#lWVI7vBT?cNibkZY&_4NxKT~rOyZBxdW!DAMs0$e6pZ3gygO^l)2<~ zJM!k);lBb{9MT&=`b&xG@ww##xjfSz_MJb&@K4wX!g1UP^dIl@#`0%{L^@@z?aAj) zDW5(0`*G**=hBX*cR3sY{jx1FUrcXBzU){(iRiJ@eX($A;oHcu z@Nv@}aIqBWli@-7$dP)aUF7z`^hh8INNJ+nNcoHOP<)1+#nYP%2lAivuP_lFG8|I= zBmZhT@o#`ynSUr;q`!qE{B@RJQt>3WeW7qz(r6diaki}t*(3dN*FLgCraSUSO0TfO zbyuWM%6qoY;jWO(`gF<`f^m$mm}!2@GW2tlllr59{Tz*nij9wm65jt9Z5`9tcBVtV zm4!36@b;GQU3yVk?{E~*)~?+a<)7qfb|JpD*(}7x#n@!dv(Gy}1%s*mtM0h$+vDWA zL1+cHo442su8Gs=Sl(!4Xp?@_PaPXiZ^;EjcnX!reMU~{pEp(}Tgc~uQf(OQbu@D!zCSys=JSJgbc4ifEZfVdaU90eEDxE5hUk((iV z!ds`&vu2JK1T)1n?fAUozoDl>D!Mp=QKeBny`7(na()W!+(_qr!Z#E?XQ5Vo>A3mh z;X~i|6e%<*o=Xx;G(+ilt(7*Ciy+0EPPCdk+TYg4+j!YK2LuzNWqFgvWYw_bodb(3 zQl`TkH8wvRyrO8{6cry;w{Wx7CL}f}3`292&d3NlhJ1@INCHar1RSxQ(96llIJxC( z?O46@w~%i(=aH1*!_ltZITcO4b7_L9Vs)VV|YEq zFsxPT9jRhL|4my%`ipQWx-)mo%{JzkT3Nz-sX98CU3XsePuj04viH~;ZO}6eEATwS zSuK>R%`s!jZO3Yf5s1=NosN!nrRNtKPl-HInfjGx&cx~jlU1)}S!1)%OjpWvY~_O* zX4K+zBB1NbOr0D*?;bN!{gwRfZaUgR@fwYmHVZu2{1uaYF}kxuIQ4c$?{?Cc(`h*k zr>8X>MNt&TQDn2Sdp+=Ep`e!VLU0td_Zj~tOVaflq8EShyoKciv%u(dblA!Ed;aXR zR!xqo^GRgtAGtH8{@f!5f#j-9HsXv|5P_3h!$~}!NP{Fd>&W3`mCo61LTFZSVN=7f zeqGx(_DZUE5spK@?j3(&G|HoM{-P_HyudQn!~$m0Us(N1ZT|MDEawS7I!nYkKTOC% zu;bjL#%E3qVEnhC_Q7d{v)?EB$|V?#@|Tl~(rv8ho8qJYjRe{U;^iYvI4a#;px3U{_$d{i-rv zj#0Tjk`1rVh>zNrqF%Us7a|x)zn(W||2t=cGSeM&L^bJhPJ(QZFYs%B~1Ut=6lK{4i4T_GwMxHU3sJ%g`1L zdHZxhBY;mJuJd;`w(sEx&h+YB)hUwq`s-+(qjeOUIJW1QRI_^XK3F^grPlPK9F4Q| z>8tw#6|lS#OKvf?`bKj%18Xu`1*_gfF=lPqsz*Do?=c{R+#PeyOFUX6T}m#!-L*^^ zV@GPuE6UNW<>XnL%A(Qo4ABXh#$wV~d83xMTKFEf8b9#O8|ELPaP(J{SPiO0q-4Iv z0DkL*LzL~^y_8KZy;L^GfVX4EX8k=N_2e?~o>Yz@=aS8Gi<`1J2K$s>HLkp5#j)sE zGc5?L*~GJd7jHSP>)Y)~6{noUe+ub})5lRWSF!5VHWec(GFZqd67=bq+@ahCk&WAMQz=Xid zIl;*dilv-%Sj8lYFgX!Pnpio(oefCItr_{9Os?}KAexS>1 zO#K~2!d3New2)tT(`xjBg||?Ypko{EyQNLJ@K%&)D2o%YsM9bEa9CP4zO-gv{?vV8 ziZC#GhGl89(acb$DHmFDHC6^B{hQO?%H+h4fsUqg4s_o)Q?iw!EhfsOqji*-V#w}& z<2*PgP%BN=VUFtK?zSkHw*)adm$;gpHEOj2t+Dcg*)rqIv%`5(ylYa6fYP3ha6HfT z^nBc(*;D`0NcqAz#cG>A{N=d)c-8wD6;YkVtkW5F46kD}Hz%yUH_E%U zHw`t)6Hq0AEBzHB0&-B6*4pbbQRO>u#N`5^i`(-gPy zL1(w*w)Wnn)2z6&!o}{}QKI%1ky|g%dQKcDHtWor-cxe88OZzpK3>pfyP?!rS4kFuFY#0ApYLj- zMV?b-@>WkyYT=S}!v1I|M>}i6vDi5p=S{BmTxb}X6aGjDgRPl`4l052XSi3Dp_ z9}KwbJ|fqdNE;gO{{k3|FNl)-)_&&q)CW)^Fyl8$?<<301houSn^Zn#*`YW2);L-Y zDH~9_#fuOl>AjE3df`Jl3U;*-qv6MZhg836vm$PFSbo;}bHo$;C;pvuDOzvQQzwi` zyR#oD{r4guFn;H{NV5$0wYxK}`+9o9#OLJ!e&CP)W9g|&7E$dR#A*(f*>WgWKB7Lt zw!9BSz`O2iA5+v1f=${lHr))4eC%x8Qc1&Uu=>NLq^WSJ57>n@HSC4=JK3^`P1%SP z>52Z+c8edw=j^pb(uTlcnct)@1?J_}!x);|R~(PeW3=Rcc!pJYKo4@N7oJmBQ!5%g z6_6D0YY6onq2|GaZ4+Gn?-=&Z_0(&@>)!s zBcoxWB<9ptywuK9#ko`A)g`Zfs3Kl%yE&44L^>+Eg2mp5Tow84aP!zdZHOuq@#Lcm zw|(VqyXozL(vZ(zJ+D>o*)i^~R6?qs_6e*3ZXihV{(v(l9Xl*9p6(DKwcl^e7dD-j z_!93G6a--I@I?Xh_vF__mW>ZdRJ{_4!5OF}O{)(8aeh{zfHtd)48=^VO>EaeDPdC+ z(AYh*!Hi~nTq)b6G0Ge-zNF*C)--uza+ceA&!#?rUs*CZD$aWD@?YM2s8^6t*U{3h zJ@NC+A*T!fzMko@SB|i@Nr{M^;Y2zk9R3rwO$0iyt8I4pGpefBD`-0!hCPlQlY~r~ zu*&W6fIhpMLhn^R=h^#9qtlGtDS1^jZ* zv~~(U%X#SjH$y5YeaC1+b;%9dpepvZ)a}IL$r8>;6e9bth4hmB(ddwz&g>Td?&aVA4o40BmJ)O~i)do|JR_pyeZduo>qQ9;+3VJCWrSkz@d5P2@65B0e z_NKGl^gyILUy#N*{N)u{_~cl4XmI z$_(dxbauF~_>q2=m%oCTv{PMX-e8#7gxZV}*mZ(H%tA5OUa5>d)VskjE=IML z8##EaRV6#rH+J6N)-?+9sqLAGyf9+^#+kXZzslG|A-m@59?I#BqVWxr{+L;#d?=dSVYS_N_mxW4tM$G_=5+6?l5dl z!tYC{{K*gVwUz2h!?*9yubydXTR8|LeLsQM8a3j&6%~79*pKZ&=lryU4O`_G8sxy` z0$60vvGR5nwQL`w_M$vCiTID{NP42p;O&#+jb~%h(#b~qaX2}jTs(hA6Ctpu%+>CL zxOZMn&(uwmW%yrZ6{VaTmC_PX;2(f0jQ0IjNE-}+uSrg5n0(h{%qwW|7e7>WGl>|@ z-|Z)e54~>;IG;GWrSR{`!pWi>-*oxi;Sja?R51AgnR6q1V#F{JHu;raS9nOHTb`-vqc>Nbwjn-}_?w`@h=ueNG4cR^JiTVK|23H7abs{-Eb@0M{ zV(F_}*1eRoS|8?L>9FWL==N!G7MCJvV!OZU&vowo{7vh^;)dEY z0V8+6C~RJ8+&q}O@Md`Vby>|Vd$)<4{dBqAq2u=Suh)Ei7-zhc?aS%%G6CE2y2V53*BED|?_YS^Q>)PmHCbA`^WX~iBma%xXS6Lmqj>rUd`4Xo zoP0-@4$b`!dp55Da%TQj{a&-R!%m%JkzU4L$bK{6`mJgvt#I(kg%xL?)7~!LH>`(T ze3wz_Hm&PlR7_pC$#TUDQAO6(hDXwrqpkZ+_o63)B;dn!hTZ}{^Q%Ty!c$+859QCE z%o+JF|8kI%cfV65+Gyq8qlhiy8#Tl3`=1qVmCZaB_c`}QPdUr&NqOVUo4Iz3D?W4M zPv-B9ecSMDm2%%hDku$&(5;io7g=_VPYqR29GptDY|7>*)XrneA)3PA)eU7kyiVNN zV=FB`@BHvBx4SwoUiAj8zCRoau=!b~6l_UUP|)8RHp@6ta@sd%EG%#8=Sel-CH>@6 zH)Jf^b9dV24)fVW#^f-%2&Qfoy~DSx^<&o4y`D%5Wm)`8{<9+aSjzmahwwu1>mwP7 zBR?Mt2b{L%ISlb1iG6PqnS>++pPT-VeeXd)&%tuXv>CGi+Y8aACtiJ0c)^1lnzkH+ zrmN4JS{~YtC*1olf4v|*sIh>TAFktfl&pQrm|AK zA2mcHsZdewbzEk#&1-Pqrs0H9M^pp?`IwX zSOkmxocgd>6Es8%NcDOsUqtx^fA)X8o@c#b9rsc&9W#&niGv`~{G6rk1%kd~Cc`Mh zuA!feI6{>uxdS_Gl6TRT-0WEDze-^R-QXD6u;}*Dap92k^8<<_&xafJpkUZ3-u&eiOSQd{z%`T4F zfwnHoZ1I|TG6r&=QuHWqzcXGf^R$-IE?g~-khu0 zv^F9Pis2>EF;RgLwEsD~{y#T+TPk;X;gK9j-x|Yqy1NKGVzBxoJ3|sV-*q;6&4LA! zj0U83;J@Ro&0LN^i5bdFrzF#S1Ss{;ME%iL>h#1l_BD&v>8glx9fN@`8r_aw4wo!a zW~s+9&NB$ok2~O~{$t>AZS$`u2D|*jV#;zBerTIdS3pma-$>eGg2k{qWVc5JTLW@; z?{=S0&6zoreqX;*x98`=-;~^RJqBMslQDZRx|?(?-Qy5BBsMgeGe7LD-ds&f5bgyh#6d{P>z%*n3e> zh^?#tA5&2F)VWa4El%e9KLl~V==_8W#PBxdS< z9=-ibD948miDM9&3M(B<+z!PPDi16EX-fP7zPT2ypsjw*byQjktGzLNT=qIAPNt~G zKnqOHw|!cFTWIOY*cdY_s$tFOVvs~AFIFozkbw-kS9m6U*f`1}QsOzq_UA;Nv%?*U z3T&_r?m|0DukN9yGWI%2hqO>qa=L%w)aP#-(?_CN#aZ<%XmMl#XeM z829xKylptZa&y?iRvjSKcA>bnL4 z!Hya}5vL8lZ(NZ92N`}dytDcB#-qsc9wjy6SALq0X(8NmX5mE}&A!J#>XGIJJ+|1! zrc6+K>YGwGHC>;{i8~(YE3&4<*{Bs2fy%4We8!?C7Ce3Qk9z zpuLUMD#iz-M1+29z9#Phrp z-=J4jL99BO;E#;WoZNN^7CH{vN|%z{2wmHv0ZtNoAs*DT|2c|Zov55g@%1v8vRiAd z0Ip9Uloc+sr5v60g4%Aj-s>IhRqAF~(!ur7Vh7T`{M!}mA^&{=K-;|k`jL6y0H;wC zU=F!fNKEO(XUh?STx~5pCe*=he$lP3K4$lVAcs8yJq|!`r$UyURy<9vkL@!(&{Be%(g5#@fUA1K!w)?X79~A!WJQ7o;jFiVpr5a@!00T?Ps#rx~FKE+`X)Hu@%@F#{@_ zT>IT?#XPZ19_MhUoA5ljgqJ|KlI)7uEp3^mbtB=6hXxTaWsI3yR{t{7Vok# z>}70EbuAf?43<{ZQ(Hk)$h2_H2p)KYi^l;XV0@%(t;3;h6~A4e?Rx6y1^;M7wLcA< zq@+KKkc%XAF46tln3TFWozlos9i-;OOhnVyx6Kpv=vZz9&o*yb`$Y)aq(w2k{JB?o zx)CO^lO8F<^u1TUg*Klpl#Ruk&aqGKykwu`ci<__J}IlGan7i_^<_2faOBic;Bzt* zY+Bd=HrL4#9{2S;y71g)2+@{?$mm_7#Lh*{Q9VVOAl*L6B7yn->kjVlGw=Ae(B;V! z(PlUEI7xn%>%a7&tb}18r(|iCU98u;H1`M?Q*69Blaxb(Og$T3S?%2zkOF;`>>S_g zS(BU27~Zpj4FdsEab57?S=5!}iF0Yrj}T)5 zYLPyCx`-#oI{O`En)OuvfNEqYhF;=};{*~rQ!4VaG(l;*4Ax>3B*ez5y9T_?#kR>( z@RQ<<$dr{AYu}cX4s?upLDArCph+V*&he&W{5ISOIlxWQzDMAUG}71nAnBM$I4-z#9S@jZdI2*kxx`(@LXD@iqtPEwo_0** z8^ziwROjOBBtA)+iA?9HwG7$05)`|f_Iyx`vRXKtbd?abWzRv&V{Nnb(szc82DKSh`t=jKN0vNwUc$nCVArU<)Jv6 z_8hJ!?(|!%;V9Zo@D@yTE7?JYplUf(Z4bqlKwWwn>sN;+uFIDD7U())&+!ix49ZXl zh5W40>=%fHSw9&iAl=9ZKnGC$)jah5l*#_YH!1Pc6}>IoP(D?x010CURIZhy zDBcE6n2J=R29=5k6(@sK=!BK$G!nN5DbM7js7ls}`Jb z16I|Z=UY@XaxpT46osFg-L6q_EZJf$91oyO_ijc8phw0DLi{ba^}7B&a1LpAAw#KVn|uFRac2lH2Kf= zp{b$G7A}$8yS7%LM#XdWtCC>)bLe@_(Y&n0b2_MW_#rHC7WVTiW0;A*S${>A--hYUfji0rU<&lU>@93(4 zZX#)uI^CjxNAvpnA#aRwD%{HPU!b7lt6@x`tq=@gbU=WF1VCAD@_LuF)zG)3kOOo; z6d2pm5p=O|x}mf_>cW}-t_P;bw$@n#c+$@y^76SvfhD3sH!H(7xj#c^?|vYQj#8uU zQ;UgyfXT|tT+ruiisT0<9HEj9d1+KGOe;D{X{LDC<(8DfPEDh?3NJ1;)+Ed&9_WDG zR0a);;h0iUuR!GP5mVPfUWajM;>#I!iTd;xL8Og)(u$rAkfZ$E-W?d#CNB*sYvCSA z@cJe|yRJ~kJviGa2q0XHQY*Y#9%wXu_R=H~)yCg8WL1bz_=EO_a+}TXw~hTN_bPf3 zp(_Rq{&xf5FQ(W`j1(v(s;CbJoj)Hi^Up|V(U&FE$t9j_aJ&E6pZhZ7Z7%rqHy%78 zc7vXuv|@b;Hgp^SvF~E>pJ=DxWeph@9Ha>;@5C4B3oVbB(;hyXUiA1$OA(G@=XS{Z z7C6Nk2^a6Wm7+PH!`}wiL@wWK@%Ve1FiTH;jcPhV(!SOHDDm#Ie#iJ7q5zth0YG3B z$`Q>HB>d>By}h*?tz4|(?WU~l<8MSlRCnA@f z+`lx)2b;N5SYzxs)()TL{bUz~nhz{!Yn}icrIv1ZDs_%0vy*bn$f8O= zy+rrR84pfha{rWe^>yA|V>LI1=rJ<^3zS*9Vhw=yUVfhA=j61HYfETt{wUXH{ZpTU zJ3cCqh8dgOn+v@xtld!SX|CTB;gnn9_Fy+14~&Db&u*Rxzp=)1|29&Lpg4f|pmS-j zjjzxkdCz57Y*G7{5hCG5Er_(kOz_`Wf1I65)cFQhOdoa+@7{PT#nZiX4ADNfRog+b zts43Vte^3N2-EU*XF~*!0CpRNjIe=L%Ah}2Js?xJr+HrXK;m!x&W~Xi4EQ&(`fmwk z7}rNko0Ejb))uzcftcd6(R(n_PRXZ&r9*vl5{mlUyD6Cfkw&9BrI=CIs{O?)a zS}()v((WuO7~UiStV#{*KE6s>NgBqk&)+qg4v(=wr-sGLbCaMT4z+hS2KN-;JJ++O^D1+>+C8jXb@YZUz!2rCknUDJPln%3quW${^Nc6_ zjsLJ2+5t2#2O;BN{61@t;4YgQ8wY6XGh;F%g?#Y33%belH#J|0R@pHdl16_u>~?W? zfDMSyQPXA0{^xc&i4IYa%gxXgTeM0hv42@ZVU|u%j+@MucW-da)GPqus$AZDN|4qq z6$pAp@6YMoSQ1!=NZ=5sw2l_nLz0GZ*M|BZ|G6dMJU~3@v#o2ADLV2=$3yt|R?5(3 zgumV_`_>r;d`HPufG>jX0P5Ct`7o&?Z%qHzkc5Va(+I|HgT5}{uj>>QrG>jx-_lx0){~Y5|>GA=2NxQ@8$u|B5mGgA?EC|5f~0H=Bhw&fHZ%SfMLtET_##~a1dRg3s` zhK6TjrV)HGiXt4)j|qZ@90lK2amF**!})|E+j%!@<&0Z=QM}vK*GO@KcI1SCDVF$+ z>^mvA(-I39MN1BIK+tw^)xN#vKL@ZhjmNyhNI`d!q-g3Bb?y+7{}26Z7t8(bPplW+ z+rN`Cv7fXeOuHsfLnw>g?MQIJ2-oU$2nT|UWJq;R7Z3kAx{&mJttJDwldw<63nW`zDO1#J}+C{jH* zXW;4q|8v$7Z^lOnI&y&K>m3}^TQ?g~pO%|JWu3a*@`K|{u?KoUsePO9A$Wa z|J&~74{iUw@@D}behI-)-P~ShpBm&*lpUH2aT{#KV0N#yGB)%8P7xv$wXWSaOV~Z4 zOQ5&KE;+BA7;=x*3s}l$Dbx`6ayrlx%~>qM?EXivFwwUaIr)vO97$$iT7`Fv*M+~a z6m${KIw9uYH{u;76JL-MH6e1*6Z^Z#gl8EjQ#zAI~N{&^C9;|uo=SbTc?SFp7RwDXQ^rMJg6((a zqNzf`LeKMXNqdGOfAr!s26+Ln-EFWUM&n=O0i)u$W1ZgRfE}ghL7!$r7@tmw5^f!6 zy}#cgZijv7z-QIAi-eNQJ2`KYj9n{H+(H`&$iV^iAC%ydFuF*liv`RhukN7Hqx2_#d{Z!7LkC@A&z%i zn8ZZ#e0&E?Rg29hNoGA$<85gG{4HLt4a)*1EChpzdQekVN%yvV*B?+1`2<=hYy}Ju z;);@|yEb;+S;6Uz+IG!L642Jq%V!S3Gdl^|5kPV_W}sO#T60nrru@dX+DSQKE>C_p1b zv=mW)^{qBFkU}UTY=0omz#Iw-aeToY%lfI4BAcu9$F30b30zvD3Lw?Sw@m)KXjFXm z`W>S-2^={mU5+rJZ@WKS{wTShTv&b`VWiFswCIF@6bZbTj~!FgTNk-cqFY-GHrZei zI8_yB5o`7RYeu|M{M7XV0Q*Q+bwLlO8hyt?`AhRjQOZ!jW{AX(j0s&`rs999W&)&7 z8h{1kgQQ<&o7)Ad-7M{lk2-O7#hr}tx3k+-#e8AB;dPd8=1>7o+X1{SoKD!0C6((Y ztuWh;IpAY%7J4n~I`EFpLMHix=gpz$a)G$-aY#xGayZ3IhCs9$DmUxh_>K=||IuUq ziROexqZ1Kx>>)QJOmSx!k+hSH0D6(Xbbu4pc)O+^y2pSxl$b15fMs$hx(S_w@ zDEKpCix_LbFLzgub&qq=dsE4CY>oie=Kz$`Enpt%r>ts60v#~3^HDAl*zh@0c+?We zHqunh$iLeO`Asw^@=I+OJB%XX#BjVd)I*tad{jn_y22zHiB-7+9;etd8*F4ak) zCbrH}H*o+R^?F_Saru4FZHd^Kh`C(=d+HvP_fI@i5_jrHiMgwngSi;R{bj2H*6s#o zA$Y7Nt%vdA_gU2|DHChwgvwQw%YGrYl{^9|M$zcjxn5S#@x>xc_mZ#r;B^2k@qiI_ zT%Xz3Yh2;xBZN#p+@dmk9ol|Cpr}BcX50XcAvPKv#T+I@h?EBcYgcc11a?Lsv17$e zo9oTOi;CtV_cE;jy5KW`hInb{?(wU<*$N{>g8=&5IZKG-{Y>zma&G#w2pBI6@1_5@ z8tUN1ll-9?d9*WH@jl$sc_FB~-*SD{$7d5&lhWUa(Q!b;2oJeW?f>tdKHOheEfDik z0)lDb0A}R2Os3kblkFC8eOn*iPkt%~aOu~*K-P^_1oz+aVZ99~=$Jg2 zfIt+)vMe4Xws^4}k#M>e6rNIK6sO&zO?@M2GitucHh8Vtu4q|O@+v}lHb2gYGX+#O zWb@JDur0Z|REe$#((4v(MYj>B?;MgB>M@N?lJ)afG%N|TN38nY2BS_6E5*qFFh|yI zg?j|`hDVz=uK%EiPKJZHbP=>06M&&%&c}xGWODttEWHq9tHP3{%ofHPj<$ zN)&1u$^&j0@{;H)Xr~2UOY>|<2J!T85Zl2^*DUhF9qwkw_@r&Q0V^0Cz=C$%11j?> z)jIuiq?tiajSDdyX6el}$T>yreL<*iU+R$mD>_QrHN34XIr4LYSgJ_z{ld4{Z$)P4 z849zP271X7&w(+S3V^_;>OrU|yF#$c>~sKNbxMe$_*j&t9srGDA48hnO4@S^k$1p< zy=&pYPe`Jpt`6Z*f?GZ-z+X*H)7+~+RNL;{iEjBijt(g!CamZjr>R!^xIHYt5v~@A zYHMK=4S?pc0Z?v6BboElO_)ac`;E+A;e%bCTCMup@`e%2375hd z;x_;#t+7t@OTg%lHU^VdPl2#_p&i%4mPuMvG{^Kc;!GVfw;U^hh=sDgclv7;zbjPfmN|Hr@>Yp)S2thnEpFRi@1Fj^M@Is%PCy_ zb&2qM!UJgB!~?>rmj~a5LEIj|)0lnt7TM-I z9-4^X+}nD~f`61-rhKB&Irt6EFogvl5{xFq+8>!m9CK(r867x09{0O~@Z-ih>p;7B zC?ar}Wf3_Z*CaW%`$%w?lIU7O7ug};9_%sTmqK-l`%DMru3=ns5!_?D@+JJbmZ@;y z?X3iJ@Ycte>FV69RlhtfRuQt}sYN^LYPVbj$-ctqw^W18^Idz3y`fjEiT++>3n+u$-8(?}JD z?90C>{~l_15FgxGHe%i0WvHY8upKBgRugUiv4+~VZD7ZNz~H1dOawB`6!64|#?-qu z3$Y5M{wNk*>KQdCyjU`So%6!;6lyYU)8L1w)Lgp@;;tmAIp$=f7nt$Jkl83XKj1+g zh|8Imof@OZ#Er*9K}OaK@J7_xbjt6dZSh5|QE)4GU|d|PAVuZJ;E0mIZfsBbGoYlJ zgmP6eh5Y6}+>>{=si#$c7!+Qi2M(W-WR|lV!B{L^j7I%+k8(xm##Gn^@(N$Po7e+9 zqCg`8Cb@7ia1xeJ#UeM4A0T-CphDC)u-Br9)@&9|b7w7~5-ZHw_=s#%N8-8w8r?wf z^WoO})$w?D{+$CG4+ZX_uWnl;+;DUkjsTI}2wQ_3vr*0qr|ztLzQpSi z&adXY0_SROq=8+IK7&yd>*kdAa5eMMG92|sBB}NQ!hgt>-YdOhFymN#YC zuI-LUYO^woCfpB>gk!86%`jdM(0Vae>wZCXus78qOnVJOlV;m4n=K#av~wwV&4!WB ze^T}FKHNw3JD0k9uDfTHGxzb|-u#@13B*T^W>+THfx=y# zCdjOxztzg=a63!6kiLvN-mx4-%9I_W6K|-VclySBC+Y5ZqtxaX0PO%E;iDhmR?d|y zI4?9%j_3f);Xp=!qBG`j^prJ{oKr!G{pcmKbsZwn;v-nN zVU`WK6zG2R9>Xnckeb~=uV*#h#`G#htfTli1MW>0^C}}NXkSXI=y)4wdCmgpbw!{1 znZ)dOtRn7w{1+NXx7jpj=5D6lZR832XII+u0<&m|GTai!7a^tCGd3JJ`BY@}rB=6_ zeTE~Y9f4o|P0ra?+H*cK_dqN zb)JSojIHUR4h8O&GFRNDMR!_zt0sBV_O8k`uyx!v51;k&APZ)Z(Z4QTjz@=imaoHw0A5pml*N{ zB*k+MHE~Lbtv9~vu@GKmIw^28T6Adj!o3-le5DY@oyAzqLW1y9?6%dAulawN1r5@Q zRH!tg1TY=fwLbjg$8MBgGo#g`cC)2rc6c90l}bH|WHO8Re?Q+$Dl7mKD!w7AkSXsp01vc)}JSbE2b zLWkuUMC|<{=Vi9C#HToJ$|-F7JM?s>rYpEXE^3L4?9O$;G7d)I7v$M<C-lOWW7n%ae_`Cq`z5b?3 zCVWS8$-TqsGciDMJ~e_LPSm+jj#e!3?s3~dYBNv0BTB}MUtZQaQBSwWBP~qiYtP<% z3K{tGI4~&x3FQv&*e}VS_n0297r*DR84-_?*UHy5$=fQPU6X13PZ75jM(k@&Hgn;E zp%>i7V;E0=?kx(@{u!^r;`z|CIh#19s@V*l z8P&I~alFk=m!_0dT@0K?3>H&q3Cq7_xC~9Q7Tq zi*$*zqQ6(oi}tp>8sn&^dx$=&00y{zFBSg7-FMb80`7ipaRdQ;$;~0eGEgBn|C;yw zJcK2iO8Cv9N$~ynARLPZ3ze=p7@`x#- z)?~zYghbWuP#~h5h1moXeM*fe`?6=1&0qebrMUHhVV8BY-sS!R?P00SIq9Ga|Iyc! z>h`vjo=s0n1!Ld;;vD(S7CjIdzz3^2HxioJo>xmKm`wdrURM0Ca5wE&Jm`}0V|A*L z?#Uk$?)8j-oIX#&r(0(um45%sCx({lOrQcaf;jeXV4Up-bkXC+LGGk4YZaJVT`LiQ z;?a1tLr`p05JbQO{H_S0@OhG@yEKeAw^ak-JA}DfDS66@F+dLM<<^{%Z1wrK z3M;TJwQ>7^dF0cFcOAXiAC7hxjY(Lh?Qw0{dL~(T!be!3XJ5&v`~>y+?(6oHF)PQ` zxsID%9{87k!9z(Q;#MTTm#)ocKXlld_ED}z-Ek*bG_7*xjc%ZwRr}C;wUipB6P`gO z^O=`V`Vm|1v$^k&KIK(iMwlz_JOsgsTA(Rs-lpDDfi=7p=D~5qRHR1lwn3cS{QjQO zX52=o{uV?@__0A=O|DrU7Sk(N$I0GapqQb_Dij|<7OCcl2z-5{`Tb95-EK1~*dbBw@9F^=}`w9^y^G})gzSgNEI=_xLs*G_dYb7#&uycM1)G*MFa za}<^JsjXUpcGsuksP4>E{{|B(!|5ikRFPX0Z~bQuIbBuWoZHlKly3VvqvA3Jrp@p8 z`TKgoos?xmO;}_ADJ={@53fKt<%X09ZEnZ$(^VLY9%h@TK1HE_Re=AK-%m6`N}9S5 z-;2_AbX`@c*Q2YRW7iMDZWvY(s5O(}o@er6Qzl!=W7cKInc6iIrHY=!S`e)@jm`6PdsmUJ=-B0AoGfrJgj6rgL+f7?+XCPsW%*};SZ_K}wos2S|d-{9x z_TwG1w@E%YV4He+G2pRr=0d;2r^%r6u_cP-$mvP<)|Je}F^Fu= ze(0qr$a*q-k(l(@;@LvF?Y@#iqXS(Xiw|@#&TN}pYnWdn+`BOUObQ{CJkWf%bdu3l zD@DzgO!W);g2yr``7CrxKXW|^4)C41Kb>1k|??dw30@w6T77$dm_E9;fafER`;sKutsU3a%S^%;oK_h7T;ok1D}MsVAQPKK(pwR!mhMyLIW#jw$stiQ&&?zk5ovuSr?+eQ})W z^{tQ~PEHScD1lgy8RQMnAB4+}PKCb3!vE+jm$Zs^jH^WNd1>WiMO&ScnyH%3z= zPUafskGCLReR^5LcKSZ1ySZk(q1|M$>GX3v z;v|9tv7#~weGHT?(iqo_JW2%!E7b1|!d5Yyx5Lzm82b3NtEdf8NoqMRMxd_F{86GT47fF*}iRv4fB0 z7VT{6IEH1*ry_aj@vYt7Sp^X3&XQ`qx4XlSlD~w?hC2Fg(CvJmsbbO>VyuBEX`Lpo z&k2(_##3~p*y7p+z?Ea`(^u&)3pR|Ts-ISt|Dng+IZtil-&vdLV-!zIhTgXU@QwwV z{G{3$3Bu9CtCRV|eQV{h#}MmL2jC$x4FBW&kk{A4d2%F2=L!s^qa)70yq>;tgBlaR z3zdPN@qB7tB-Vk~12#0+895qOP`8qbh^aAT+~1Pq$=w5pqTnq~D64e@-xQDG$sTHN&AdAo}NhgReP7Z$WtUlr#_dK(M`}f4_jDXWYHDAsR6rlX zmsp24hK=vdCa-*U?7ubL=JW>UXWHkc#`DHe8TIAxDs%{i)6D;CDpVkK>A(H@a5T)J zJmYpof#`t2k|@k#7SwZ|^EZ}QL&dMt4+CN(9?&l$?Sy{16Ca$Nk26~=qy|Sge%2bv z%@E*9{0{ptwrg|Zx5wJgEMdErK{-<3bYBra2EK%Ttbjml zH}q}?$@~~REa(zvu^F9GX%Y2m%mgbx56h`%B-)XScqny!-cxzRw2l})RC$`16gLON zes7=%Q~9T&z@;o;eo7|o`LosHm_$orl_)6_~^*vxY%5CYGlcmlIR~0DkZduy7Oikd)&-eGKvAK0uA^w* zY}RTQAXe!!wEcn1-%r%O;?uHsjPZScCH;Ewigs-IfK$A48RkK)fUN-03T%Wr%`XK& z)SX4~_L&^dbUu^fdlbADlFK|!Dsfqz+!=NymYj_6DkzGd%sO7j&VQIce0>Jb(x&+q zmc_Upp_TZ#V#6}ciC}cREi*3RAtvqwjgwc!7#v7oMc#Q}u8O)8_{$(avj09_>+}#O z(20}!JaIZF(&KKV8hBkIR+p{oZivKOJ z{so8eUCDp3IY^;M>LXTVX;mda4U2}oD!F(Is%UAqTmCRdXmZh%M0n7dhuztWRoh^) zg#z#1&B&ux2=1Edn7^M3*Rcw1@`+bH)n9CQ5;sb}fpt1MggQoQt9dB*t}Dy~+3r+dJ!rbCcyb(f!d&zj-;qy4svc}3b-bX2-=ds9`Bp`y|x#I*Bz z&t_8 zvsOwZNqg8wRZPqi!vbRD-FV`0sjzCbWw!z=)^01$l#1iQyav0O6R`bA<)LPG%TVYK zS;cyaz2dcX_j!_{hFI&pNK+FhqWBzoCFMp*$j3l`SGkT#1+40iVX%jJ5CLuufhT%yuT?)W~E zRm5X#nb7AJv0hm95sIOldpTT#S0s`Q1l7g*vq7gjC$6-rf~ z=rm0qva6f){Kcr}GxM{iq<)FVsa^Rhy^b41 zympmU`=#@3vvb_)1=3n1Fny|w|CxCV~MUz8TVNjZ%f-X;KEGn+89yNTcUejmT4 zUx&G~r0@3bBI}ji19J(tapPzHrBw8%$&HbL1-#A`uxcGKd}Pp@p|p&q%EgVIx=m@n zteXD}Kd!^YJen3l>^J_}h+K2G&zAT$tq6AFx|Qx$=533)bEf4ixviNbMhK8VuBX4m zkL^1D<(;M_no+0??nfswY^EqG49GDm)2ngMx>{<%<>0uvibXv`M|C8&B!c&uw4=HE z3Y10Q^bu`-xYfnw>e;_3ViYT zN0v6~5xN6L!!1bt!MWnM3qvbL7h2d4IsY-pwI;5kkQJD@d${L z$La45TG4f^JJK%17Mcq5;mbm%1>5Ok>#$@9K?uoEGB!FeR$=?BIP;`)k8=I=y$0Nk z5=qg!z&)?X=ty@$+v=GFqVE&g{k_QcD`k}^+ovd$^&_@fRyksP7xiaG6`5T*i}eVF zJ)@`R%!p_0kkd70<1)s&YKV)vMC)I-Uy6;l@*%f4?{()-1Q+d1n8w{;1sv^%cq-N* zv*~=|GBo6O)!I7Yr8)`-uZKR;KD-lFSdsck9L-o?z~A}{808{CgppuTwf#mXcp=ZYj=ce5ZnwG`#%w55YBYD8y%V#YWA@jdjV+j_HM%n$Zb#pw{d`|iOTbL+9kTUgrAIUE7mZ4RO)3<;e$^4!Ma){4_ePeKS zAX2@p#{z+7TkwKOtHMFOd?Kr6}cX-N$j!K>036leHryL}SJ2ROV3$*=U2LJ=_gqt|qA2 z(I0YMA!ccAS1W4gRriC81w|Z##pw zEmsi?UwyjRJK}#ZKeD=nYtA38&@_ewwIGrV=T^arH0In! z*6-nY6{X~}KashCEgE>xvtvB=ZXfnJ9&0+qEdB<6E>5l8&nHQ&&FCDkWz|x}rID zItIP+T~csoM4r|*99PXQ8erX-*G{$$8;l~HOij7smhuR^0DJKOQf!e>WUB(!;ZE3N zR7y!#yV>ejI#x96Kx1qdx9xbJJxTFd?eHl} zezcd-!rQX4s}w&KE-C#}#KWc@E zFMr$__%oNDXEb4-<1L9VDjt_*4fxx}>rb+^Rj|K8I_GO!8^A-;vmqEp%yT^1C#vow zvY_=35Xjk9dB=x%;-u?MBHEhr9)e?yO3lVwZO= ziA~cK=0E=HC*&%_3H-vJMEENK4o4S~@2yri|7lDOyi8!3G`*nz>ef85EH?9Z7K>7x zA00`F47<&4-MZ?K59toVOTCkl1XJ;CnF!I-sLd7QiWHF7 zm8cV^3T*9upeTW9l*8(h(#8}$-`lc)4M?(NhhUP}0M;gQX@TVpQWb;Fx;*)%?kmB1 zy(W5n|J@v(YGCRP{D@EDk8VFJ8`7lK+4}>Ywsm?n$m)Y6Q=TgrvOdEC?BPu0Bk_Dw zxC4Jb=Bp2O{$x*$%RhDn)pXJ%zYEp+Ox6^*_F%X+{Yf}Q7resK-yi~o-6^`I{}0M5 zn-y0*=KKZ<;M~muJ)EyI!*I?UkYaMGh_AOa;ZwlyJ1@)zz6=y=N{)vy08AQ)27&$^VRX#reM z)lYtigFaaqYQ0&Ypq)UuuFWgT5$km(i>%UuIcv+lMnH@Sz!Z!=kpeyB$Bx&R0Tk;P z*c}vGi*fPe)9t^{HNdZCxOv~O={MWOzj~6V7U}OOdv-R>t72knck~e#7!D3B;Ip$(UL~4T#3KqI~1m{N%!cD;7-U zB5ck)kmbi}NpN(f{nK88_89xe6x-MZvNsjy*llcM>LjqYkBCh=*@hhhT?#cNrC_~W zz>9?jv(DA%#HXmBG{oOKgt8}0$!BnB;8|tZYj<$5l@j1yt}_r|v@-W!wKH0E8&f#5 zVWsFiug#>^YN%wPmq}J7xtSy>AJ!b&tG3h`vKsu|xdA@*RogyCjgxJ0`UOO~`#6PZ zIJ+Cyz%8M;VZ=}HnTTo{tkA#{f!~ehXbjQ}pv9;y&)z1VKiwSaX1HSdN|9?}So4Jq zzckL})uiFt4os3hx%2LicC6knRqel2^{a&I*8W0^dcNaoON!!Sz7CXB?kvJB|mx;Ccd)!m)D&G#~~hv`J^|+ zTpOK2pSxPv`?V9o$x0$-9V~kxQbq9nDLy-=Y1}1h(v=nJn6##RTK1#!m#!xej9Qhp zC*0sW&R=xa!1h|eN;rz7J!8q5m6u#;hc?%XP4IIjBrIfvy=p9YX_7_^Qpbh@gtC|w zvHse>5BSnRs_H`5z80yOCwG&6PHEMVi=`n053nVVj*mv%X#^*~s&qlWP?xjKSF8fU zX;%Z3Lt0hm?<%Q|y%&bYr>HR$aIQN(YP>0p+#MJ4nSNrR*_f)_(I-l=FdQtw>$;rE z;T#na%C;ADCws?Or@;)A2{%;ZaCdJc!H4$>D|8W4TfaV0$CL2ec(#~LlU-e}CBXo{ zB|on@qrjKj96`S~5Dml1$E^9iFF5xD2&l_XYT18Yqby>&R>e&fqIavD?7$VhF}J2r zGqX27abbKI<0sqXw~?gYm|5iKQ;j;!`t2D-Q9DBVYZp9OY3vk$|`91kARLZyX<>AV%k; zXMdzXwQ0m@{;b%jZ5)9+1=;SFgnOX9LmgIQd#$NzgwJ~?AzJrnz$e7(mz3MbeXtA$ zAS7ylfGXyeD2noh)ziRyeOV#D@W0)ETAki-zI48vJg69Yt7=yC@C?r2xte8}C@;Ln z$U{6A1}@xdKfQ%;Y*Tb#PY~eJ3zD|fmh=~ZQrRF%4_qc0wH2^QSCnz?dqeZjG|X(p zy+*Zu2_OpxpZc-H86h$Qa+)`^UxuGpmQEO&XE2+t)%wRg!&$|xyN2{c9P!<+=^(q5`7zB4Y+jxH^XZ6VkLiZb z@mo*uzF#LuofI>m7v(Mav0@`xpJFQqT_>kj|T@Ji2Mgb_7(Sf@E%K0 zntM-BAX0hO%l0>R|Eybs1lJW*Nx5$^7dp9rt=sfaHeEZHI8iZ`)Ch20+oP3$4+YYl zJh72d$-T$;(5@d*c$&^&j>&bggNLxa(G-Eb@e<`p5HIb;(=t0y2?d-&UDnHPmSOxO zdM!!`o$tx}N+Vd{3F19DBFOWuw(o4yQ$(2TJ`CBv%A@$U4>|Tkz!hcxSx}#YUbh#ObA#Bff%dgG3C{5a7gt}BEL!t zaW$G?8-E*7p>1Qb&9POg~kh$ypA#T;d~4#R&y6Xb2(s-1N*kCFkzOhS>-ZzCTjYk*G^KC`89Vx zSyAc{LAlpn#xC@2cixj*Ox(}crNq;cJHH0X9n#1xd#Ev|pk@USc3%m8&ro#h1s3xl zTQtJ`&RslB`;+z2DfUY&|J|yc1E#vbT;g4;^;NF{rb+1aI{)9YwM{r4c(tBKwsfQX zc_Gp~pGrIyPK;(=>v7JmULCPIh3H77U)o`gN|6}-lVnTcUCIDBy^BPJY;;WQQ<&neSGw=)<8oyiBxaRBXSK`6x7>?9;D|>31e!5+^McC#aAwMs^uTfFP|^}S?RKnD zeS%hh*YrA8LMHu5cYjP~Fr$CR))xF#DD849@NtWQu0L$lu&g7)rR8+D`lNZ+bA2vAycL4uUQi0nmeo-2r|i_f z{=Gsl_{GGznM>k-52WMyO4o`#nG)|2@y(0!gxBN3ZSbHb{_VGphfU%#RwXTEC46vq z$bz}+C%DOB0WKK{kiJ;yM?NJ24^dgcbPQr&v&^mU5^P|g%gKOI#Z5!CK0;*Vr+_wz zXaBOx>r~0y0n;Q&9%qBDlLWtrM71Wovuc`xThCQr8HH^ zKI$(0tFB%|y4t)Uo@V+MQ!r}q)<>qZ8wsSl8y84#P{`kx$@det{f0fwIq(FFeJ(*3uz%rr&A5zkxEbN_3@=NK z4--R;PJ`8l!oa*SF4$}k)Sfy&AGL`=8b1SeZ;)UwugqW`^K8inCJbU1i~WHudDAp} zCf<8IX0$}?V~5}6-BeNlo!}PERK?Wia7y9yB=%DTYkeK*KA?Zm)kS6lbuox;aGYk-pI6E8l|!{1F3D?6EAGsQ=)Bv{qpQIKVA z|FnDBb0f~haZ!XB#KjQ4tvJ#5+}j@%Y{C5JY+9>+zt4dmDE>hW+1zLAh=6Qoz7DTz zi=vl>;-ftSH5(JvhblHKUJ2bIn5+)#ivP5||5B|MEGE7hpSRRtV~!6T4z3dasMCiY zI+q&Svesj03Tr)tG?}5NA<=c%bNqO8vP8I&%n_L!Pl5UFSq=739!%fPg`RxYH_DlM z9r>O0BzNJeS87gV50LMtF53-W0=#VmBP}ak-rLMg)OKdKW|+<*uKbobXA1`gi zsGJA&0xLot=~gFQ#$MAcm;z_OUKvwszFgKx%S|JztooA@6kIxU@i~sVlumWQr$h2v za%~l8S<$5+{M|qY_T(!{{^X@5-tUlcZhOc?@*%qZ$1rUCGB@G-H=g@eGwz)JeRff-JFoq)x3I9SpcvUwKpBQ+jXPxQ#TMug4D05IL7&QS{{(dW$i?>c-DNOJytQu=k!&n5{1l)I@L5zGeBl6-#Y<*Y)?w?gPQD8a-H3A3VRgkn- z&kgx%lJWxmCFHS=E#;OA+;`#Miijtq_qS=V9Ad&JWVuiEKeHNp{#XOtzaKN5YS6{w z!QnW(3cU7!HO=(z^-HM+dt&NTFIPBR%MeN853Ue^Xzkm#2m_!K?YI00vme7~M8ekC z%&ZBat6sH1!R7rt0a!uqjNz?ToW_AQR+J6^o#?xsHqk0vk3JtNw3)O_2A zH&FvOwQf}D<4{d#i9R^|jdg-XkI&#s?GvI8_NDkYk?(#(N%xKW^56PEPvrVsUY{7X z3t8-*=|AbQcL1QnY|wm-3C-zz&R3@qdBL?)_jiP9fD;%?uwC0dl(n8*=O6mhJ=PVi z1A1F>B?2Ca0l@NQyUn%~(Mxo^0{-CRO0T+p`n0C&>H z!M4QAMKDYifMM(M3WHUQ)5gWyk+M_AC$+n;Gc;p-;l`W@U3j16Xx+(2X-VA4YF>>0 zVYy6eG>5E0UNLqwf%wexx$pwuGs&p(Xf_Rx3hwjBTkuiInJeHwB&JUeT3VocctJ+~#QTbq5*#3?wd|MfGx?Y+SS%q_Yil;msqDF$0iki+c1<4(s!4nM}vghldoxuB%>fp&r zz`5_hXfcM3Brb+YZ4Z+o)cP}iRz)D$rp}EssRqo77Ovtq@ybre{KV$~RPr;CjZZz- z^8&Jr07}-1o8CxAyW)=dG3_1&G)YXR{Q6%=r~D%r_8V5@xWK!<0L3Ks)s&1)wQ-i= zAs(2P=cL*UFi&xq9#lQ&vW=pU?6Na?VTinWHp5UU;rM&RXC0`?aj#03KWTW%+LiY1 zvC_cL>*5;T+ruNNF3}1RD@q5MUf}qUjsenL6)ecK4|GqN^1qtBLh?ZFlG4J^KSKP8N$YJwFX z+qT`w;raKJnYQ(v?}7A(vG+JXr@wY%PARIi$dm?u7%b&9{yDM4-;D_B7~pH(Vnkd- zW40dNy!=a7*5d8iIe^4&YMAQS^p;PHEikEn!O5vf(bm*s*l1K>x2nstBnx*5>5&RG z9`$0w(;U)tDq4X;WS<|h3H{cfP2@ddIzK2*=!{=_*rp|^F9#ZO4lyA zaNV_QN>EvY()8lqAbg6bq;Z7g{n2UXTRbp*bF=*D=_c_tfqjDzRTYNyH0X%^c!xQ4 z^hv0VvS8|&E)Io005N6(ZxFBCs!0g#4WSWjzq(0R46AENPHK2!>}{eTn3@l+k}Yh5 zI%m-QiHV9+%ci5YZ8wQNJ!LJ+HArzQ`jX2E@W6Y906v3`fH1an=fh*W>j z*1cmUkFf&#l#gH`xOM*5B%d$bK=#z=Ob)ot+7ote|25_TLi+WTEk6{y9t@Lz)^v_t zsoEL%^{O*}^642w&cLlDbks-tyvscR&#yDy3G9E)oYh(sfmn!Ihd;3!0GT^z?6JMn zP4$}H`-}UiRgq!h%`DagG$Z% zuWZTeHB>V4xlj8^yDlkU#dXywu%{8B_- z2~*-c5{UgQ9b%RGFf#ZzkglK(QMv+c<3F}p*g76G%Xd+TJzg+?H|k|^w|nsVo=D;| z&VQ7x#X0;G0?)rgc(8R@{5xw3Y5CN*9M0%)DrQ+NIO}qfDz$ln$=j!PVBBL968UPr zUbA^A)+SWrY zSyx$HbH*jcue?6nKdwnTV@X*(wZnkd&X8n{(Z*(9R*R-!37$gaU!+_IEYZ~}S*cTt zn@X3j%WwwgBuyUXa*Mx{If_=$9rs4nVa9qd9Rar9--@Jn98k~NPVc_vznh0LYanTS z-PxQItb*0LQ`%MD0I$sqlZL+Go~sbDw$ZU{_I01G^D0FADGYvZGb=y^E6aC7CB71o z>J&|KEwMQ<7i7x9`W@9>E-H1)=cAA=Z+ZitvxP;q73bn64G0#n_sL5URJ4m9JWi(- z(rHkQbVINVOD7ThLgOtooTs z_QxU!;g2bs_>cr>JV)_g@eS3{47Exc4!e2(*o@9Pdy0KZ;HHYc8${rSkD|9{ z5)PRc(6h#7q^%|Nxf}aUcKyxCg-q>aw|$vdFXTNQwQZnbV%~|qT2nDPlR$|r`_%+CEeLMbeojgAA`z@1bcYYdmBab$} zYf)cn?yrmqSDpHvM!&CnNOU&Z@;GTsHx{MSbyd-&`8{{>Q!40-J|lYMQL)C!_R_4j z8HZcS!qHy%D7?T`u9Blh8gzJ ztm0vw3cS&3lFwCZ_fen%JJgRUW<@NMtey*uQzeow_F-LlroY+?Jd-jzCB{EYsq#~> z9lM+Bgt6W2R|f|8mv^xd8?O=vAkW?TE+@nk zo}%E?5+B+TO6};hUBD*ods4+Ya-b>BNBboE^B#fB?t7>iefPrXZ$(;pO*86!FH@Ly zuv!~`B8e6yYiTvb&BymhbMg3P&_G2sYPK-ZIY!Z?P4hO=BMMJB*FD{;A$tt9jz~s1 z8`QbVpCyZw*DRjI{cIRJd1F$)*$Z~ktP#o=-sP7heP}v69s3O+dR81MC<3r^WO^OW zRtli@bvsJb5*>3WdT)rU>{6MiX;wU|iDzdi$^8`>GDbPE~K^{&zV=O zks4vZfszmkc!@^>>xEMLoY^mgO9NOP5#D@=p-MR)ZUXVeofvEVzdC zKTRA+R4dsb^U-WpP+76BxZlEyHh=qKonIcO)kesOTMN|Xbz6;Ij6r^MU}D1^!r(2n zw`0m5@Na86wc50TD$Cp8NWbT(+oyS34XHMN6h)XBbQ!3ZKlvxsBz?S6x!+?ao7K&l z$Vh&b9fc1vtA@raBickuISXbn19z~4&jfM;KW^=x|w&^ zpA)wYlr-|!N$btM z`nklplE5zYI9A^&>dZcKQpLitTN_r2Po75|7^6(7#lCw?lPnaC#{Cj&2py0K$eL~b zK5uq4=Oj1oWlvuT1A~s(Ml>f6&;m`Ns%q!zj&T?omE`bUOMMSth@*ALpl_TpblW

    Ck_bn>O+Ax8LNTK$g02zz|R-wkxD;BNzwX#W^M^!J6}M(3AS6TR%w$6f6F!~*E9 zzoFuke89TP-1XOFItlU*W&ZL+f9wnw@Ux`VVrUXo>cm+!qn~=C}dZz}SR!3#O zu#muQu?G@@hu%ykYF=p?4#5Q`DYR%xVqg}yzt45#@Y=87SQ*>Q~7cEv(t06lT9YGn&M&XcSXXqno{%j z2jZ=#z;hXjD5q!i|GI4+DX$$ihEsG1COOyGq-5)byq}+?6&vCz?ng-v&~KP2A&Sp{ z)6EotN8ci4YYC)eG69{;rMY|kV9a@d%}in6;;6)mMsbP2ubsG2)^fvOS$gXm6}0gd z?W*NoU%%aQvoOS z2%*vTu@U9Xq9nLOt67f7h-L3NZIV+wnEyF%mG$%nTJNj&%8+=A6DDHqCw0$bBc7ynUE2vYzzDr`C+{3?LSAQB;9skQjb+Y{oE?#(|TD#N60II@XxC% zq)`&3MS}IdE6l6dMSVll$wvIw%zOtB~7hcVeE^-gO3fGHC+?G zx|-&)WoYWzy!YGX91Ox@$>NH_0H##9Tr1=noLefqoQA%B8l0OT!Jc>m_^J#5ju}h* zR!x~*ijp%Ozq3<*hA5iEMGym7XL-Tu>?sXFUTE_PV()Vo=G0pk$y%M(2#~o!@~Wox z6D|I+VZV5zV(8%qbV|ky{O>O|vXX#V)}0Kj-fLi^E2z8}#?{`7eKM1hU#ze-^>X;` zR`%-6X4VmvxQZ$X{3YsE(0BIvC6G_!CA#zvnG{gY2SWi4s6})zs%zs};>HA@Ooe9h zUH#FM&m&cQ;Xm&76nb#@kxA<%yQ|ctQ0a>5@RHk8(zj(cl#r-5yrN@4j#ke>2TRpo=UH$^Qlj z{F5z(SE;mkZ6j!gk7K>U7jG_7N_X_5_nU!2Tb*W=d$_ciQu%wOQt)Yl;Az5|8X8e@ z9FGB+(gB}(h}xg_flw`3LgC2IY9s!xlSy2gtTl~ZS8B|`dCB|ZlY&mL6Y`LaEF{v$ zrcA1rYa8RuYtpSFooIqP$Eb4`yh`-$q zVge3V{9TXT+2NSY#FiMXk+v)Ub+8-bl;pQf3K)OCmkh_x@om7$fInIDA9>}lSB|Z; zGNoyx0&;qTauxpkcZ~LX82wrg>TKeF*Vlrm=1YvE$07KF&_uFMw1@I>)c)fjJM~!f z$H#LKNhj+vwXYp`|DQF%sRP(}2Q?qu)3|#-XtRnWbo1!qdvgjtw4EQ{v=vse3E43y zTQ=N}$K}TU=qEsao+B3qr2yT6aa-;!CdgzwNE)hbO|xJuhS4@8EYr9N=@htjfB4S^ zn%1I9h@Eb z?su=9oX!ee|CO#xeVW6mD@5wZSkkBb36KNOt1SHjakmQgb-CNxn@nW$Wxvls<+h|N z)j8%}EO!fwNt&xe48VpCCYi8yA6+;h?LNeW1x5^__Bh0;oL@YUFvV?kW1aOOl(qjCoTAZ6`)sk0KGiXHWhx(a zjH`gG$%rK}@^21+?eG=2ig5>U+fJH8@jOc8=W<7N>9eB0+&Zt_TNE{@sXS-I-g4H( zcD>+R8zrYY4O03l5NH(w|IjR-ZmEPnahUq*TEQBcmpEhUZ_@mdljk`qmRYM` ztRseRE3Ro)65zHlaRC-p#wO>mvIA5Nowb`l;tk~8&7k3E=2ZO1aKdMJdPtZQ(Rzn9 z6w`p6hTdk~-?1d~s5DO4i0{g7gl3c|NUtlE_Me5Ji$4g*DpE2^-nYo_G)Qk*bWfO_ z!y)s=vEqm-R?%r;^TE~Nxth|5UCr2^6n658_DwDrSU1CkrO)?}ZxXZGez6@hasOE+ z)GbFR5hpax%7{j*lElgIKz%%6 z`4EB!%$E&i;qe)=9)`2oFYW!|K5~4SA}s$=`zP}D0txK;9cXzr=&OFQr}-XqMEsR* zwuRc|j|YkY7qd9~Mi(!&81G8ss@>g>$#(N{FtGaT(6k?z4ueG#3Y$8|7X8#>`Eebb zv6e9G?LO)09TyD_x7XdJNt5A8+(N^8ZuCl}aak8zdiy5uJH;;w*!zI4U8u;On8Je?BB2*!$U{Z=1kVZ?T*d;?^F}-#Gx4VqN@A{=ZXbu4{MO zG>OU(mD)>Y;!tL6r=l*{u#kzpJtDSL=(|E-gBqWUJf4eH^6Sy2QOGx^Kwn;RHqUCj zR&@FE6X*d}8{M7uHph_>oO>^+e_(Z9-?2o|yxIseZU2Un;2oMH zOX>yyd}u{}<~}$D$@zlhI-(?#S@L}2V2IVPpo(wdDr}!8F|n6ILAhxXdL$>Ae3moQ zXbys>b4)P%F<51o2+ngc*<#{~lAUZIZ|=L|{9b68GtQkh$>beJd>^RwD+MCYt&RT# z0PKV4gAbt>`D&pXikM-eJ0ZyEI_vyB)@ zn}@#jB~Lu#d<`H3y-{X;*%Sowj~uO=$B|0 z2qo>|VZhzoG`LziV((?L!}ZKvK%_Y`=1~WF>-PcD)t~Gq?)dc$V|NQE58A$OM-Wrb z;DB(;NlJPP(V>C6a^Z!>6JNHtc_*EM!*7Atx&LSh3>#+v$qLEhtdX2Q9Rrh5ACn(i z5~Q27qgAXYegeqXRAithxJWfe+vNOM+HkxrbRbzf$o83EOtBX9dYz9GHTo0={>4fo z3BRTa9o0&~E$*BoIjj@)pNv8hE;b`V-Uk_#wGG{;TImV8DC`fhv4OE@jyy^CbqwW(t&luTk($@}Sx z3tx$26DbV>qM0ZX^1myBNi1o@;4sF;2tN-7=~vb-fF zF2#=3JoxhurNkDEAnp8x^5Rtf{dsop7c)Vijt5WaJgc*WOZVa((`Aii#6}uWO+qRW z9bQaW6WtSRIc=^Dj^Hn>D8t9fihT4S~yXTx~-O&4+pzRtbJF`iNTl;N{ zYo_2$m%jz)DotN=8qN2*M*Hb~@Gqj^lyv_rWvy3S)yeMRd;zrAaiHvt$$UD$CZ|ov z8=epzMyfa;bIp0I@&Zp_DDSg^8C*!^umJDi5F7*x|NOI|&kFhEYn z%aXN|Xln;y907xQbc0-5?ms^I6fklf^=od*$*%6n{GpBA8GYA?!v4(+$HSS$o`ssp zT-Roh?y2}ztc2%sPV$R8p42Oha^4o7t};UT?8>y8G58%^)+xbeEs#Qc2Kv{qkk=eB z`OyVRvgw@a5F!12mCWW5XWFnMn*`UJtt;_m!MXc$0r^Rf78%9mqCbty;MSWX#IvkS zE+KQ1^C5-RGspy4b{m<%X~GPY3tPjN{p&J+a};s-fDT5;jx z<971z;A^|&iN^UC%wCZ_lk^WGYivp%nqJE;zs$C==#Gxmxcuu8 zfR-*R*Nrudsse%3+M4!IuX@mt9wjp!tmv_ac(Q8*J;IU8Z4UW{pAKo_nvnG7mDL7I zR2|3L8~;_xHVNC?Q$T!0{h~0UBWj*aa@LWo-5(^$HZC1|yViLXs~#I%+QbhLoCAE|GCiMpQ=|}9S+$)AfNIt;UFIBFKf3C8JyG?$@`YpH?{Yt>mTMqK2UzE>Cp_)8VMZMo&7_L_OV>3+B)fxjq# z4@Z3?p8C>ICUn*{9&%%TMcbdOI~`c?fAbvBU?{lM!UZ4E#Yg*4GoUj`YtST+eu76l zv{qEHSWTuK4Z1~9uyt~=om+jkX0O)_qUaiG7mV#U3EsIeg|B{$CpRdyv)2#$WR=SL zb6*0ss!YMHM-6h+ToQHVPS$TeO(hkWgJb$PcL%QO7i#3rl6Tv#>J*X++`({33hJbF z!J#p|UB50Z(}?BoO>ENVt5uh1l0kg>ZTmbFVE1xlzc0%{^~^l$tN=Cf(nGB$sO!+< zd9%KElJ7ebfWt|pW`}fxK=%*NK25uWv)e2nT=HjcJ~Y;<4)zKdh7q~tfH$udby`4U@|B$L zx3Wc;9i4O$QBoTGY!Bu*x8|b-GlCd8EeN1Wn zS3fR<=Gc?UBru=iL&0)8QE`0h%eBeK`kTx>{?4Jwx-@KZ&mzR_o6v=us?%@!W#=d)9zrc$qBH~(KTnh zpRGA*#fCjJBGAk(k*9ifIT$oYYHV9cvG$0PhQEmvJ)Y0s1(dv@zqU%DxW)C~K!dl( z=LM#K037}?s-B=?Q{cEmuLJU7n((aA&U{kpyxOE+%slq|tPOvK1`$f*xHF1RAV(-^c-&U^s z^ks4GP5-N(`IQ%%KYOh9JY`nCWa@0#_E2Qh#YF&!_uGiHHsSyS2O{zz^0iH1M(RLh zwADd3FYPp_4vbX^P2Pd$OA?opN2dsUlb^yiMG%b_w5$-1vV4e8Ui5)SBl{)PtBTZ3uq)QtA#*PF-GSKQlWs`;9)9b=zl<%f(^y#l7^%UVM&M5(2KbG{ zuO6)Vw61;OYX5*^lu#&e=nyUGBZLerBq%`^2M; z`Af>N;6*LB7>lr>A8|3x9N;q?bw2`aqAW>%h7YCQ=dH?R>xvCGc?waN5uoYeQuK+U z#7C&eB>k0{$F&2>hn#)Zpk+hgW?+}@PYUb_iWsqS|30B6RELjhIR~GKl)x5ArG*hn zt5T({(uea5csT1AXE3T--ZPKpN(+btm2u`~(uj@_8cQ z#>!J6fi&yq++MHJhJ1Q;6a2IBT$$$ud$UeG}Dwm&AqLssHecFgehb zQgeG=x>;F**f{FQRtps$zDB)BxYZ?;zOB@DQBP#ea~VMaP`K0V*OqmQkf_EmE!hG672qY%0;bwaO&bCev*>J?q80-E6IAX z=p;UF;mO$wcq{5+=)C>XFQSm~8kWiicUV2Hvr>#~(_upzHsF5-_17KKrPZ{}@OL1I zq{#3T+2M4yT(%d3{&2oqQ?B*Z)WeCiE3nSC;MX>(F40S5xeDjJEapP?f%4GPM4Q$4 zNB^gcB(0E4~-KrZxk zKEo22IuFamRI6`&RLR%AwhH(cnsXH<-$wu2RK@l;NaQ8|BYW{g`&)A24IgvGTk!=? zg5_P0tRsdibKHrOAWfQFdUbkZGOTbIS7*5&@cZ0EvgXxciSul3w7|LLqd%}~3HVWh zO=*TX2R(H8WHg$$&3IZTan3uIb+rM$aoT6#*0OBJ_e#{}0U|JaAze<(Z6ilpW0DFJ zV*)p`kQH{eH*9$j6OnvIBIn8rGkaEtg}x>dxeE1fZ_3a6De-89Lu<{Ei89sFi%ubE zZb<*YOymDJI`>DW`~Uwtgi7dyRIE~|8w~-bfPqTWg4i4ZY#`5sA(V$5^LiqpGu1NYh8&0~omWYg3b%f72|Kg|MZc*R$ z;I6#Ppn@gezCUucIIq$|y;r5ascqZeZ|7OuH`X1>Qyr>4&*`nH{v-I-v4p5Pa|R)QAnxGhhPn$vfGd<~sd_z{dFr^B-t5 z>zK-_x_T`jOowuJ&y8>Ems|l2OLxJ%YlWcF10k5{$ajd)PYHt5A2~FJv*GG077Cj5 z?u1d6=Sa!I^s+_FG|Opc?KJM2`>+6Ze95>|Gii6l_v_gB@W#Ax*f zO5QA`{SY(oeoco|cZQabmHLvcq0p=7bsZvB-M53Ycdy$GS&zne%u@3+ztHlo($^1> zZVB@$XK~fNwRq_(d$4RBpMk!RpnLdLVvfe3gAYSl)CxLpuYTYawuj7+4g_!Jkp5Qu z1mYhtjJ2*ZH-bRAd{#gqD$}FGo;F+kQ-JRh?4qd44&Lfhk0}`NV1iToNa!;cIauKN z7bSsUH+H~I8dy*`f>uRK&r$?y4tz;xxpYK(87htNk*2CY;5rSc$wQtxWqvoBrNCB= zL0kywVxqK9z(3|La3340z{brRyEY`^c@HcrE`XcC71ran@koJ%+G6zp+WdMW#tYp( z_W%cr;is(422F02it1|dR&|rEA7!zJJq6E0vhjG6DtD5VhwOCNVb0YpNmo7L1+W9l zeDqD}!54=W1g^W7;LEz)d0W39*6@TJe|$zVzNrpA@f6Lvu}*xi`=}{a^4HH;xqDw& zVJqA$TTEse#mE;s`-W(Qpv&9f@-IIXQw&I(c4M@QiFKITH0fw!KQNUm*!A8GY7Fof z{!v1l*5(*zM3-e2u_RBwXp?_LLwja)9;&9N_eh`_xUlszLl@xG+ox4I=Bo`&71+Ly zgq)^1lG*+86h@vjXdQEJNw?&VaW$0}4=qW7tUa5>ZoSCl6Tdl-m*QMQ+k?P_^YG?A z%P@(EFpv35CpmGRvGS&UAtRadbc(L|R3gLlDI(>RbHJY+{GwydtXGt_(0mVee+-<; zSqG+VHy2@3c7g!51^E41VF+uBScZ#PIL@PqIOh=P|MPq)eXS zu~AT|AlATv_I;flU--pHISx;KcL#+{s2?b$^ysgtKRA-Ir4Un5X-71 zD{YdF;Tk`%R(;``7hQn=bi!M2yy9(q!>h|VOw-@kF}79iSov~=uT>j(bWYOyGD|QW zen1Eas857dUim`l@t2C48|vWy{x+U?LZ0sMn#2uGD5E&9%S#n0xRBU%isSf+?+f%} z*x8b_g6XsTtv7$kc8kq+i}#s!cb*@aVVoK*y}W6m=qZVYi9{dNx{DLsocZqE?UJ5< z3A8i0We#b+xuqZ{Ro<;E!@E!Sp}ZPnm`Wq#^10oOG~UJ;bVID~JCTo-5^k0$XS`y%Ik7)SW4C0jYEP)gd|5m))JBD+|jH|F8p=?qP^Mk!;_L@THfT1$- z|1x+6b3&2vT(`@>bT|BN^il^WyBS#)V?Hnb*M^*0WsmpI043;;`(iYxuwYdVhCc?z zKSN_WvXVi67Z~}q!#ZneaF^?yJk(ZoNVP;&eT|peKmCE!nsf@6 zy58)Z$6UF5LNK;{rR0~I=7+ietIx`o(txaQB#f8rnjYbA8Pa!9g(KL?x!kJ9w_5wn zaN_F=9yM^a1?tnr@0#>Mi4R9IH~DcyuOhq95iW1>S7_nq6NVF~^RZyl9u_pVa4NIp z`FmN}P7Tp=r3ixe9^h-_rv+KgzEk>r9$U`h{Br?LX)9EAe=}xve_zonwM83GXT=U# z;N^vhyAJ0p4S~k7Wo35g0_jw)8r7^tC`UWYVs&{!xLsw*pcYQ|Vz_E~ms%KOY0;Ml zlJ@Wd4AVgSRJeEc+~6OOVpYakUOrKFAy21TWoO_~Lq~A9TRLFOe+H|6o&YA6A@7t3 zo`sC}5LDehgdxA$N>$J~$H=Af_^$-YIqs5_0ENT4S@OolXtH5(^s;_Z(lb@A%Y`_V zWfvp)deL1MLdF5D2juTwGQY#jz(Q4%;Cv97vVHaYl4@W=IW|8yE%8Z(_^C{ao6BXV z+(B}oY=fCZ&?fl&O}ORUlO*`;i0UOlozSK)&-3qQC$|w8b#R{Vo5e6kY2xSn32kJ} z?LX%Ak-TTX`ILRLBCN2ae{K1o>dHi7H$*ZF>@*_=d*d1E#Sp}LY1qDTo{1{$^(p@B zcY^HSI+Isl!3r1Vq;D+(sGqc4gKXHWj^?FOLXd3kMlRjFI6|@Wv%FnZm|Q&xQwSRR zQx{(g)7Ffj>s$?O1T877CD~WB`EyftV4-O%wPiPdjgKun4n>ncU9l7R!jyi1JwU-` zh{ZXfXzAdrN%~_b0%kfK@lj=L$&#H+^_Yw&GB=1YE+UBT?Q+ha17y~(60}pOZd;uO zjWePJ^C2K{vwUhDS4*JYS)$KxX>1%%f#)h?DGWAUm>~apOrO$wZ~1m8;Bz+aYkD0! zpE9Yz{iw}P{jBC2Q5zUeqK^rtwOwAuF+W=3a6Zj$ArwTX~T&T;P& zh?t~9tDjA)Gk#FzKHV(4s~h3#E0&K@6vvi>zQy9ATOTsl0!7%BsaKM=b3GE^8FuN` zS3%5#{-y+#0@p!KYMs+l1tQ~Cd9Pt5uF?bq#@mAhhmKzufi(qhSaC8R!5(>`b#JvJ z$%c1$=h{1~&LdLX(73QIZ|``e&he>&k}W93js4`r3di|1BX#EZ(-KHmP=abQsd%il zQO9{>OoFi-O8!<(yE7>8DDGdftu&gK7OO|ye?xTuMK;YU8P+Lfhq+!$wetUJ-p2

    kkAOr9)7=d{eQ+9gj?btEgY_*{9$?~+C#1hk`bK?UC2a|376nZbM$ zn8tbXHYMv2)OAZs#XK?kYeEcr3C?FX@ZDc2HIR!BwsH^NnjTe}S8BacRd6zI78ZF! zYH8S=+?q4SLA+jhAJD@?HD}2h7J+T&Aq&weq|aZ_U=zI*e#sa*qhsEJ@O8r&`rkc? zX!`VvfphI7WTEYVinZfh&If_pofj?4 zRSx=BW7otduFEKsIj}TMBu7Umvl*`q`SKJ!5W0HPYgSfvlGfHniJuOSeRCYCU{+5< zL?l|o^j%D%UccdMRSwl>QarVvH(;?x@Z}+0PoRqX`7aaul8N%T2XaiDK0_I$6pJ0x z523Ruq1HG0Z!iOPZiF^A+!izqea1s*_rK+&Vhm=;uT|vxy35#h9EWEh@csAiS=Szl ze4m`Stm1S|Blp!2_CVP;fvwAnrBLyvJ{EDFlo!65G{1EFH_v6)1Ee8BzQ3}Bs0yc+ zpsgGNIuLpQrkU^w55?fg9%(>fe^2Tsk4eOr@HwCbvzOU|ZLrtkrrETBbDK%YHu5*8 zX2>S`TxSqnlJl=nEdSpy&9xFIgY<*(Z#y{V{gp;?wgs2N>6V<6sL1CFke#k0Qo`9~ zsrITQJPXomgqMf+r#}mjt9@qg!dn^gr5vOuqQ6s7t&FOB0>5QB6fZnb*3RE0-GvZh z-#QP&Rzz~GF~~-phcs2kbUU%L>$O~v!mj{hXjwOzqJRhAPHB5FEAXlx)_b85jNi?# zIp&P|eS6_B;F_xNTDO(HYGyzMcJzf%VqJTde_dRR^4)109qie|uMFutGPAYY`IM8o zJSguTb1AwDG>bJr74oC5JhS?dfoZQFrqvC5CoHfSev7d7G@S9W-t5mZsT8g~3|f67 z@5B*C#XLq^Adjz;?haP|5C^mKle8=&SN{D^39#nT1IN z%J**rQ5nE+XU5@4nq_|q>e~pkJ88KCwM)p@9C&V+2)n>PLf^{<YqQZ=NB1hxYq5s5$fWGnoZYV8cq9d z_F`SsGgCOePhRD5*%(=+4?}ejP1oAWlKfICeY4bk@ci7;pNAA{8>Q|cPly&<1hn2S58V! zl8ZpmvwCfKd=TP~=WI}>3KOF{!|>6{PTllyer>J*%Zn&Pk=P&Y5b-Dqbbv3Vu=%k( zJ)l06XkP3S zY`}OwENJ^mvcPR@8;K{pesX+HB&q`xZX}RHIG} zrXFpTckf@4!!Pk;p=zwiGhuLni#`UV4eN}HiLw)u{N_8*Y0Z+|9ENsrNuHGJBkzz$ zgfTuS$No8R6%*kWu1iG#T|t)uW*o0vLw>~lh<6H8pkRYi_f$RUTjvvoNer+b=yAtv zv>az33sk5liSuIeL>FKy`?`ZsO^o|_b-!?Spj z4uX}d)&qlEFM$a5bG>m3x;miaLKN7YCB!mWbvUi$*E&e;6I{lzi-688|A`M0{tnH` z;%XihA-Sx@FWLqt_6P9SY(Tr{e{t$2o_4}7oWSL6 z98xs1WI-KNXw_Pfk`eibM(*Qn#(~XK zyVm~)k1zJI`83Xyp`+Dc`8ujYyGG^r^Lkk4BGu%T2WKgD)v=Q1k+Y>dZ^gFfCDWxV zN`2CGH7})#Nq9vXpZ}pcCGP~m@Y5Udh0DvXp+%~=R>fA%cnCMps4tcPB_^G|TGeVoOk4TDaU(u)h; zI`%QAy!4K zl1~rOsb3f8uB<@F{)_PeX}CBG{<#VBc4FmOT(4$-d*m(Y5W)989kXqVrnn?Bu?4!Mubi5XcfBP0?k!IVwsohKYFwq;&K&J_2djd@%%?DXU?1S* zn4&q-RgV`1>W16}ec?RoBI(F2TG=*(I%4%ZC|^d@UeZy!(!~=QnGlJtt=Jg#`;x@k zTn!8N>=9k+c^t8d-En0%y`ga3(STxFK+`60!O6~h*OMz{Z5Fm2qbh&2vlzue;Ls5#Ngoh}{EUe=B3HhM*UK8(GWe)-~yk6+^adG`@>yIj_ZkObbDmobY=sGE2e5*CN{(Tbs zram`y=WtNvJ!VtH{ZowdHqh|+nCO&UxahDWt>`D8>;m2yFKA6Irxt&xNy*YBAmTLQ zmcF6e`xd`V9|jM@9(kcJbF_GjRJ}K1^4?h1rjIWMdVbye6utKGX6<{|>6>=61bYNr zuM)re3#n@ABCNrM`@jqfn@*%Niu0<)Bku^TcD37|k8v_%q-E0O@~QTVv|A5T1UGjP z9oj}kIh#nU7nCI1eM5pWH7H|ym=jM6a>dKz=fy0+g zajurvCUK5c!_w~P;@~1`BA@QzKd7%b&988nl{2t2R$}#}HxE%2Fnhn&(SC__->Wsd zI#sBT4jl2UxE$}<5x8r()^es%sdoRy@0Ki)Rirz*Hy*U(P}e0TTn;14J{!$jDMJry zG*E~q9cRpkh@%ZJ64<>6IpAmhT6{F75p=~9eo<=}k?@q&!ubc%`_wzGB1J?bgtlLh zB?c^&4N%IYxVla^1lgw}WYjfQ64O$JjqzBZpV6ztF6R^Gwv;$7rxFXk3ZAu49Ahl| ziQ|(ml%MxJ>wHTc^}Dvv#0riZ+&yeXy!Z*zMxBMe(xVAV&06!R(7tmhU!JBrL6+X_ z68*48j{7)6{?$iHE@yu{9fe2x&p(dTl>NAk1KZ{@4UIyqN>`HmJoeRw+%@M8A6kYZ zY~-WHg}$^ZlmNCcEiEHV&&nNBxjdD3y&;6@)EpmvujG)qci~qbr@-C;7Ae09X3HGV1jSkwJ@VM$ z=$9EJ#LLhBx3UCmNpDfbhx@>z;g#4epdLuG!x-XaUrO=|krO9P}D@v|qKlQ0r{t4a_*r!_a(La%x3`m8tRwS}e^J^aE+j{&NIz`2G5HM%**Qx2_n;K^ z_pWXV>(iKCw0SGX`8(xN!c+6nGBo<@;&0;@sKDRnrPAv2u`b_Gdt3k0nkg@fvwZfu zVX(WCHli5S=C)W2=^54k72~W$30-Msrmnlca9Fpr#0E}F@G`p(0hs8{Y%FG@B*n=M z-k^?}T6n@>zx^#C8qv)PZd1KOk>PvUJAGu1mV}~qE}5KIDSo)25OhtNckTE#vTQ#VPTUK( zT(w^Ok8MD)vO0S? zr;}9LP9Nh>UoLD)HPcziL;rAi9v4#pZc}zDbR(3NH0T;l_aKd{fxvw=$FW7%CsXpH z?@-0Y3z<*DZ>u=M2^`< zOgAd7@1G`|FJ|$F>*y^)=jw+RB5v#OXvO}SN0*BE)UTq)RjP?ERf8@V{`c5uNsl=M z=45Pyp&S+iEykw8gMD5FX4$u4f_&%TPXn{B{>%miz*H7nG`X@}6XoERH*)5eSjq6N zib>gPB+n}HZowC8B{0rgC*TwD{vgoAozeeOm1FlW^IBz^-cHlj^CzC~ayH;(=Asp@640M9cP`s57BxkXd<$;O(lWdkEUg$)J2VU=M;`QQF|Os?OFaQl!_!GlV9?*>B2 zxi@WJ{AL=evn2;pFDZ&^DDXrgk}^0s+krEr2?UoCEX^3l(21^viD)h7t?$?;30>ME z)}r{L%MOtbC`@upP?KEZSxCcfqL}8>0@)iEB^a)3W5BY*siTNenw!{9tRrd_ zTPZHKkidTwdJR`=P97ZhuoYp&Xm^sNF%tg*cTXU`7jDiP7>)_=p@&U-X~|I2J~JWN!fB%{?8VyXtwPs&x=y;&B%WF)>&J$ z@*{uebU6B0oNImKZCGf34kP^tl5^qOq$6YXE-qMKE{z&v_%BFEJ7CRe6xWm;?#-bV zF|M0n1;3D>dpsU7-VG7p9VM&a&&OKX06)790VDR5fr-KcXS+p{xuC{(?+~a+mu&|@=YOsJ!XnOhIl(HMtiPKS0t9O>lis}P` zR9V8$7ZgTa_|KQvHX#(juX9f1g_&klS;<*nE4>&FN=^D5@-dYbVhw}QFH)mbxpPkh z25P!o4NAf1FM8HfzD|;<|Nf!y@|IERNU&~s%8*+OiMxl1-l#4VUMDCHk;8T&EJMrFEcnrO!1=cfGE%k;AcZTn$_%9?1 z3_LL$^gNG{6M1C~YJY~V_h^Lj^TBdZ=6WpE@Rbks3@`YTDk2rloHJ1=5S`*betiT^ z4(ng4j5QcC?%PT-f*VNg((1tcniS5|3f3A35^&(?ZuxB@{^=;5YjevTv*f5TpM3%J z;n2byQx!CVWb)4@aVS&UDS zGH`KGcR({SG9irI=mrked36c89Z3GTZXCD|d?~gtfelg``zV#y{o3eNj+Hl*&& zujHstR{ogYq}NH7>C1wq8)b?Ygp$UJl+9g$Mmi?6^D*;rL>j^!+IGGJpHNbVIJ_x| z_dp)>&*%4$BW<3giH8V?Pr;p%9}$^JmTIsr6w-VPhW!T8w06lg6#ZFXBOa|>SiAoY zT$=hH*ooBI;t6HsDUIB}e)auF zyLRfNJ*Y)@37bQyZ+Xa^E}qTbr{E&u4#9FQw%Z{Ufc|%m0&R7|&09 z)O(8!?VIx`eNEFUM1%{|^b5&B8=y9`W~WO0?ZWZbqtF0rV3^Jg=FZ!)kFzFSO+wt% z8AxU8GNah@B8-#}qv$an6Z||anh#d$1=0mv#`VAY0tdD_=vXm5_ZBjo%^>MH63BCU zN2U<%9#5^}9}zplle%qUFvJ+Cv&)-F33>J4AcN9FDv={A&L?&`>eKJlsOk#$k|C(?*JSJD1~FH4`Mz{DJz*6} zvX=M9NFLZM@KeRsBZTsL=iwTIoff6w>#xKqo?a1gsIxhRncu;HH6VBRWay0Ie@`xJ7d&$m@Eo7!Se)Tm#P#s<8dhYD z2BCH-Xrj{FY#umL<~Khbco`PCD~u(IM@j{{aZ#td;iSPRKm63WsLlzOz2TAHNG!pn zPaMxzi1a%XB_O~J-L~@p9LRwG$hCO7Mj_ta@(AbjQc4|wxeJzh1)$Y!9JB5-e*^yL z$&YrEWg?N*L^%*p`&ZocTDoCZzm8Ux7;M*dhy@Wv9jRm zLsN;RU!nLqDfGie*_vIw_xS*h(rq$Nf|aU@dnRK$PlpIgiEl*f=O0u3qV0=l?IiE0 zP7U53MhlYOWT*yS^x>Or!ejrLOpf1(l$vz^Qhxh-b0>v74}*vaiCZi#xYoW^Uk3BB znNkV)opu2x*N|XvdQuEO%Ud`xA}%3TjJd8Yiqkye}mF)JcN3Cg6M5{=R{+*0Ln# zo^uRuyCI87t5!^mR=0y7J=Uc~&z&vW;wF0@*Su_mEpr*J;9gXa){WB}lvHb$D*5>ZN)uJ(reX(d5 z@(%;yBB_85n)&Q_-C)ZiR|2J z-ji&&T7c$j72qQ#c~`a-c~<@@`_ zPgjeQ`=0NZKCY3JX(pAc=%-w&f6w}b^5VTH>mL&QlNq%C7DF(Ljb8-ztNazIa(`+q z2YoDvm0bHWzj`cwBr( zsxMD+ju#=H5zGMKq(WzD2+0t0x$3F56z<{2KXaJCxxPbM?-~5IdB96)4!6=2FWedf zj#W~;NuEst8L-s$uz2NIM+I!ZVvx!ua0ArZ#&;je*eQqf++}Uu*JEYFeq+m_zc0xix4a zqb!7-!0Ua%z5Op|gSxJafu^(3n30{U4o`eHGd`&vjXuM8Kb1ueCYOmFjk&nQbcesQN(>WpM!Hz@k3(53Z)9!ve|(7$wNqmWvw z2chQmTGz7#B4e0Bj$o9^WhthUh?LEs)?2R<*2sv@DK1&2i=`;zh0ecenf6>sj(r-A zmc=0hy2$b0vIP-zFPY=EL%uhXR+TTsy(4%~R58WjBlsk+2d2?vX z?nqpwOF4R~OR%)|5Ux4mpk;kH6U`9?b|};rfg)4l&`uxup zO3A+3zW&C+SwwJw<>HI%W!b`kApIqfI&a|23PZO@O?LKfO!OHCuCL!)?BE%K#9uQq zM1vda4+sCN3NEnSj)}gtf33h8p8t;GFga0<^_!169(+6nT|8z$iVl}vi$8?;_Mo7R zxzyXJj8`O&{!RHHYa3Bn8=ZQ@X0@GGWlU^_GQ1Fs6q8U+*1=i%nR9jEW{rjBKG4St z(dYz=;MaKaAwi@%^d68U+u3|xAWD??_`?5alKNGaXvstPT9FL{ofiEi1U z+SZEt%y_mD(dGh1JZTgCBi#+Cqs!v9lVom)jOPhu5mOgGFxIW?BO*sj>lBZFfBg1D z@Z0Gnr(N?2qgj7Op!-(d?4}$5Y+l^9g{Gn8OP_SZv|nw31b-PU(yfvnTzRX{Y#I$e zfUx{`WEVz_im({hYg`~HsAu_MH)ACY+s;F>F2+K9&$~6J9t9zaMqor0eBw){Hc^%A zqL(Cd=1aik(iM#*I3TxUy)ypu2i3g@saNnkkh7nji|Jbc9>anHL=e;FH2CUHzD~2c zA_WC1idRR}eB;R`TZJhhYG81(77dYD2jZ9A7R(tC6F=+K3RE6!s9<{#s`Oqo@r-yr zTSbs)qt4j(L5j13uv;sFvJ3LP{*+dG<_tyw`WDRT|Wi=d=__TH4vp16nmzVm6BjgkBsgc z(#!k*ZU2MI%{6@%{I!S@0=fiV#H{NJpRP<9gUy5k?yT*;>bjl8@xc35*tZ(Wq6$ z;Q&-Bn+E}&kUyIQiF857#Ww=uroX$-vnHs!P=qEk>&aWWdJ_vwa3pkmUEw4QbDm$r=YQAmT?dcbI&OB>Kj$<8vPsvGQ(yJ5WQ zusib2UA`~WLn=Ey3y#hK>r|k&6&?rp%`8WvtnMtsYB5KW?|<=ANLM{`$%^deB$$4) zbJe-w8;2FPUVPJIIVJhAlEv;9|4Neg>e^K2u%c(6S{E_hl^gQOa^W(ozLnOU64#sx zgZusuhJ|-(N-1}|I8I1(PS~oudJhY3q^u@XPe>`AyRgYE?urt|@3|XSYtk%&IgTq! z9zBv;yG_#5U1L+xeGgvQew4J6Bj_HI#64Cvz8%d@;&DUec5+GkrsV|SzgsFxllpxe z3;lKCUwf9@(CPDQ3tKl~OK-s8E4OfRU4Ge{+4za07ZY5Eihf9`$}hufzzy6|Dh>o( zpf2o1P|crV^k8TCHiu_rXb;d)|5AvLy5e-PJ#fB`l3Z0iupmX7J1Nqv&$-Ox7uXr{Hh}PO9wUcw5$5T-klY6_TtniW;+;RcEahT(YT3Zac62_&dsHBmG3)W`@?!WtmGY{(V3K;8X4+Xtp{c zs?&5a-dlAow%TECM2bCdWOdfEyuJMb==tyS%~H|6IXCp9t9P710fQ*!W0P3MkGOq9 z({9x0W8>Qn@+8;KtGe9T$bVk*gbY6BABAYCxhqJW10iUn@=gR)PIOf86S>Z9=QplU zd8%edH*>mf?G=Izm6HB{t^V&MoR*LI@{3qHdgTr%xBlR#49)t)W>15N1Q6u`^|)Vi z4eeN;g9VuldGO*-#0RyczUywNcqIrj9}gF%;Ha=|ZV8Vde9{r*{q~#wt z@zBQAgWfAAMRVMwqglIY3kS1UFAaf3m?o=X68NX^LnZ!msn8 zt%wD3>kNGPUUuJn4@P1H4euU!0>i6Z51-=+QESd}%hrMpbTO(1l24I_9SoQIOtu+X z<5%@i<}bZPkDOyTL(bUvDY{40-r0eZ+Y}il8D_COvftw0hL^-{ycl`{b564D9*=B` z@ra)m$4YjNsKkyOEDp{LG##S1pLoN=o?8$TKh83WTdTzh^IPuMx>B|ak6*6Wi`4Fj zN?mrXv=M;T)gYAo2hdVa`PO347;ismzUB^#GCR2U(ahczjuY zD;gQS6@wD^jGc=qh_9eH!c0bj#!QzfI0lrqm_bR|Yc0Q&&B z#Sj=_Rpu+(L#D}0=V!NAxDE_?hBnU`!f}fM!!@R^-nm2rK`LUK#EW)36(^BCEum#% z$)NE-(g|HL_jBCy&Q z!qE{gWG_Q-wXNgZ1Rdrcu!i@A=uqKMEFm?ZT->}-8lPaNHC?MQbGiGC#kdoRVIMwv z#z|nB_2&=e`ZWyxFbI#a09(;?SN|;4Djf*1srA6czEazj3REq7QiGEiK#1%1vkfDM zP6S%w@yOD54#G6LUBHi%&SdJp;){I)0)5i!t68$7Rr9O1jN2Av4bB(-dh7Tl;KJ^A ziyS=&dTxX6tF0Q)bo;nNOIVlOm7>bRr3lffzJQWJbV-hQ?)xm&>9SBxs)Kfl;;^vh zZVKylaVN(rIg4RPQdRWs#>Uf=JlHtBnTN@-kekM4+t@#il8#k)7JWzXIc;C2?)kS2UB z5KlKB{CM9fgB5+@jY2a1&Xf;`-_CG~+bT&xiEz6rflDVv?<_BmwZUa3u@La!p71Gc zuH~%~>a0iw!sT12Ag*$G&9Le_tD=u@0VopFj9e6@jxF)H<2qrGwD2a@MUq+Hs+jFs zq@)0`WnJ3}n*yd#^ez`9-lOB#Gja8E-6#W7bMj-B$foG7hV+fc#yw@o$ps&WAjHuF`2XT6Xra(|J|+mUl~j z1ie*#ir$IHv+R@^6n-WmCv(_qu8cPOsdrvwTk>(gEMtYewn!^cE zX)6HS$ns=Pqr27Ujq0@uXXO}WQ-#>#G3%%SrsejsU!JQLr@4X`d?jKcQH^01Z6I(C zm%hF^$a~TOu^$KSC21?o^2=MK9g_yb0Z{=HjFxn?;Fo87$+WbTW>R0Xv;_mc4-k$p z9^i`>)+8GsC`^4KSTGkg#4044F@e9tab)1CAPcnb zd`dTER3PlQt)jVy{=Yj6ibD2Yp#r-NxEo#PTS$`ZZ;iS-es@)Hoqv}S6c}0`eKQHW zli))xJqQOI&IHtX%Ct7MHd~Z|g8)+BC)zvDUXH_o_36E@xIDoQ*jzoaP0X^!*39XAn zTzonDVa4?*Er8PUaCu0)%xh`vcFELyNE`8TlIV_JA`O3ubuqIBK3HFlcA0V1T85tX zW~Lr<{+1P+bCCep^9fQd|1uIQ3AWb+^?n1gP%l;a=lf%V?Cp#kZ)Nn!g=avjZkr+j2l)6LQ#5@b1hhl8vHfn;kjZk;@6IH& zU37E4`}^A2(HwOs|_{Bv*z$>Gg+Vc9SDf>Si}^A|tnwmJgi4YjDK` zFP3G}=?2j>JS^}oOTIW65B=Otn);TNS{(9Q0uOl6gqQ7{w+dpA#yeHTsonBrq@GeZ zn*dyxz`%3RC8Wg$jyhVflRFh54;tQu3lTLX;1Xrq*Pw^@(B?a{Ip|H!9A6~;HBx-- z2)x%??4<9|K+2W-w?LLd>EU7OiieBxZT~@Z5vb2W^=4AF5P3loc+_7ATv=%Dts0K| zim{d|s~^KM%fCjA>hg=IKRZ#OD5X~`eG&0vfl>XUT)g}*VRYc)bYn*nYW->p^9jU* zxgraV*M1doU=4FjUOq7kgsMZNTPRwzU>)k%mU+ zq>N7?rvTrV5~ZPie4h%D(P4s>i)!T4jXPfUbBv3<_~1>6N&V=EVoNIHy_!^zs}R0^ zFB$veFhqjBxrN5~2MM&oSjmV-&>R4=9nn7|nRm0i+WTAyGXPngjOSI9&EgH?zaVOQ zNkfOv^9y=hUdX^7AK4?`?&nXv*vbGNlh4K~3%ZSdh)WshmgX~jmS!Qa?i+gQ`yxrl zMO&tjw%zQe(Vj5NeVH0}ZYyC8t8=W7D}I6{rIuVff@|6&!uEYB&n%dil{>-%eO-KAjYtGx8U9;tmLfM@r?Y1o68uPEz)Y zNBH~%25Jx+PF&_qks1<>m#heso886%cYPZo2u1tgpS@rb79l!>fdRnO<1k6|-BD^F z#B&gE-e55x+J=;<#@*T4LvDYg0_NoZZ0-1e6rGDd)BhjEjgX`zmCCKV+$(a4S$%~{ zZc!?i6_tui7K^cwL@AVOn2E}zSQ46maUX~Sm1yMmK#ZrC8cJMNvZmn*YB`=T}hDaKl&I_70EUuV&S$!R@R9k zI|=CFh}dz8#Fn&^pBRc#InLl0G;6RB)QpRvpf--%zAC0jw~po+wo` zz$3vkh-=gZ)mr}BwUXoekg{yq`lfo~i6gR7 z^#u#9*#zhn*4@~S;_orA&)<7irRhJ&^%_pN{lu;nqgr>VwTl^>k!ZntFm&HgI%d62 z`jqlbIl+_R1Fld2p#*8LtXP2Ka3#on;Ro-#O>)pFosQT0V_@HQ{0egV>V9O7Z*TjI zVKwewIzH}Cfa#3{Il&(+>s<1swjV(uw=B>Cdl%onF#!iuC;}h@SvlN)on#<;y(p>k zdhm@X?|^G`-AT`j&P2SuyhfeJO;Er^1>VrH#(Fzq%X{2)#_S)-T1|<*+e(WsmU3m| z&){SFc15c}M&_)h9lKYx{1=3?~MPcQGvEIEh1R!7YNB>L0YHNRU*zSG=gHp{YK zRjJU$Q?T?La0%cB(^a}d7DEB0{~)+HhiYF=Q$mvOClk3+4nz*rco3VQb~BuC^K(D< z-~~ea)?LMmVfl}RyIxLH$6uo8ZF=S@)Lf5FDX7piP`-1{(Ng8-#rkKNIu2I$Sn3YP;_P6nK57JFBA9odHford0O}ihXREGBB|R z!zlFPeJ2;u9J?t_0ddk`$S+B~0@^`qqTF>}sr!UwJ1%%5ffAKz&iduV5?~_AXyLZ= zV>$?211Gd-7L}CLd?70nxVr7~wLA?7fPYm*4W)y%n{{-?&}QrD0fsWFf;2Oz+4pS-!qfLQ)JJQw#0r0`<^Pb{;f_^J+%E$r_w>aJ0Esk<}XOvld9!m>px8t zVS0}dP+A?CM{bZ_?VrtjW+ZeU6{-(S(<#5_7+V(Zvo02jCKmv-3)u^43@*a!h!Uac z(F9U#`yi-qzl~AXQl4X;H_k)IM&#kC0dXN_M%Da8=sj~gih>92@mwIZszw;AocHu? zHnRH3M^sF=I}I?UVs^ku)0H-_Sw7EG1*F#S*KZ4@civ?%#T&JuD0$>x+I8`Uufo?@ zr3hjHL$hO|-%fF39&VJSwP!4}hx|!IUhGR~4)`7ETxt4K&;Kvq{PJCOmnpi|H-_7e zWvTDbao~|Tv0GF47Lgv-E%pXfi380DV<@`~SK=^QCd9F|_bR`m!ZEcWj+gp*w?3|E zuh*Kg>U{ggCZNhad{XVoAMOWb82|XV}@DrIil2=u3IA{II zt}|iVW*et>D}j+9W;2GO4TH(0iu z^Pl~Rn|`v#wbv!IBqi^w6yg>R{hx(Xx=l7wCoM)p@G8^skS*=(UAS0CO?R-l;N{(Vj8guR=Qc z2KM#r0q~12>?6qkco1@mW1A0D4t#-IwK9=Q&t7ED1lT~BM^Mzora1QBus2Yqqa^sD zqTX>L!K5gn@Hkh`TEInZlX^Cc%D?=6o*dYt)S8d+1Y2#ZUl^*rZ{K1djoA`zcGRl3 z#|C`LNN`03>3yPUG)?xMk*dAuX`>c(F_jpSV6p6+FuYZKjgd^NckUA&dDVv8Gj%-U zubv+`?GMsZMiNRW^2KQMO;3Q1wnfRG*dyvIQGg=ay8iE*qZ?~JB%QzAOUAkrV2^{e?p-OMwaHALOlK-4 z_@x@TCukJ;WzSA1(RDOPd9LcVYT_pzyVONuE78xK;b&(|Gx)wxyulK3{pGn+yw#hi z(@~bse(;>?&@hvDHE_B%5v{||0$Z&*cBPq#q)l6YXZ@@C)VZtp}D*^cQHLE|OD&A*^VzTQ!G1+1| zg)a^{OCNl_btY?gnqEQyfYvm`f~@KiNsHggRCs{N9ZI&h)^1B5Y1h6Sxd#_pw<8l) zwId>29$WICxA6K$He3@)^z*1SKA7lZc1>J6T8x2=T_P~>BW_Oc(anju&B>xgfiQS+ zbBH^Fx2kt=^Bi*hYi``vWSY^-5O#n<{qjL9I@zfbByOgG^baaCdiKg8cfXKz!f}Rr z`{9Q1jDh6qVK#V{i#;Ub-F1*-bURw}GV8=0$alq*A8HE0I%(Y{-i#5qeVcjr<}Ex) zeJ_0nxdNx&m;&c*B15lAuE1>CFs5@n!Vn`V8#b6UFNrWhLh@DpA)lt$?N;g*ucT$| z5?+?jcd|pyW-?N~Pz^J8f^BlZ{L+E1sU}87%0bXW3XDh)bUTp-NsIbWiWOS~#3jX^ z?@!#x;hW7Qh_DZW$mb-94MT}Z8y0TrDN&*d0-=|-wL`xGVw*GQ`kG66&~wXYn zdypu$U)+B0J`J(*;33W1_)0t5WK5@T$t(PT!ZAgN1#USQz&K2UJV?m4?*Id5hA}Tr z!iKX2Fhx%wXWi0qwJACeft`g)`%LFnvnmoi!+S70 z*|wLWCJ9C3wr-Ykby!pwz$jZ`r*rE1Jdu4zBGitGA&i>F4Cn}2$IWrnlA`gcZ)64; zK*+M=_4b`(J56X7Ar`*!?Z~---Y5VVzLA%aHf?ACS%5FAgp3$Pt4)WYklC}l`}r1| zuzpq>C1xp-3F+l)bg}7)Qd)ZvTQ~TzDCQV_R$=G(JzhU0K2!}PcuHCDWcNvJHu>#mxCxbkkPe{wW)fttBnca4^I@9zP*P>$ zAhZKSyeVtXYWa=3Yqgtn+wYIJ$!T72YhGaf%ZY09Sy+z>)+c3Q+_Q+c<18y%f-@Py ze^g~rw=Wq_i!75Jf&sd5g^s%J*4+j+xxH5NuCNVr9btVn^-#bB`96;2EQwt)uL=2N z6s;Qpb!F#ztIiPA&VtDW81gnR-le!aVpHyTfEA)xZ2O$^XG4dML4I)fd!4=jLsh5f z%|phHy}S^=XJ+7sSfbt@O2m<^F~;9}X5ZqwXIpXqMZJCd0ru883qy1csjqtM2-cml zw4=rCo}qeH3g`dz)K`{Mgxh11zJ{9y!vC2U(`@zbHS1nBfgTHhBBA-Xa_9>WsfqJ+ z(#IF{K8ev&&Wx%=_@&+7h+zgNc)ys~2A+DVoQJ0D{)t_;#@uMlHly_;<_}iW(he}l zzRxg(6ZPQ(XS*+2<@N@IkjS%#TE(VQ8gD;}gXH2P5q-3OM;_1cNtv6mE#T8kyHOG! z`_eg1eM)Gcpuy#$r-h67YREOaXsgZ_3V+j)%4N9PG;^B-z3HpOatxTL8;VsK!$H!| ztvqo!8C$g{h7sI3(GnITOzqlbCtNRUS3u32chgW_^<-{a0zF2L-WsA5g!G-@rZ0cB z{FBu73N1csn?=CvN}Mti0#ddGVrP3W7$41@n=Vi*-pPMIeukA#-1_ znue)jrb4j-@x8QBgtBjm;EO5XKRPlmRE6v@xg%Y^(UH!_*w~46m$sm)k*&Yi3GlC0 z*2!otn~3G>mu_AgC8S>qsOBLC>-2v*`g!4SO})L0f9I3i0#(O)-*%KWzTO>3sXf8N z5mw5w`~0vTgC~8}G(qH6j2~T1K-4 zfZVKh_h#ipVrFxHW@Il*w>gZlr>}F^abHo%Wy=C_)q{yYUXiG?kXraZb;(6=Lu`pw za?n)rmUS|Pm(xz`Dt(_74K8&l|99qu79@ahP+4C7Nx`s;#^Wy4;|_*?Q2cwbo$Iz}Bh zMFEX4&Md8w-q+>bZrjJcXw)E%9@mYrkVeaqoOZfJz%NN1HR?e&>+dRx1OIa_sOk!& zmAZmt^5kdE-G*qraRFaFisqjBl0ItILb}G>LfXh67WI)|lU^d}xIApEQj{Ywnv&Qd z)AYxCwQ7zgab5j^F3xuk4Ov-T=T8>C=jlH}_B|q^G{c)QY;%Pds&Js~Vde$Y+r9kY zFUFtFmd%(#As2ghu|Fuk-?p5rhaUVI9LaM23F4Ov?Y-l`W0`e)z8ax6`xcp^#RMvAOhP@rlIi_x;-g=O z(e5`GwbFPuDr{MsJ6W&%J?I+WELa6(bZA)(g??c-K-9Xou;KDt@R%M9c+YV99cpG# z-l5V>N%+E^MAc02ul1XaX`;TB`5kp35GjKmMSSHX^qIscYY%&r;xMIMJKiqTYY5pk zCp*chAI*y(z7Dd}6$s$W*ac*TX!lGRW>iSQ%%Wjmbdk>5Qb*cv7z&frMPnF+ayKEn z11U?!eMa&h>_BOS?4|~mS3={vg9DC}7P4a~w=bB$1?d0skj;g6=&QX2u>rRx!6Sq3 zlrDIwEIm;}g(#|Yz-bfjHeJ54Pf^g?a(J<+PFZqq%k12dI@4kC8xNQ!?|{{05r`h4 zQyWpD-c`VAT49H@21Q{ohLB^|4q^6SozLLtoVNQR>3_3 z4&~|}g6rME>ktD5woCqzZ1~v^N-T5&QFe15g9XBT%brQ#09mh8KCmTvFs7B7za_8h zQv*M2`Zy2RZ56N5%y}sTXi!#FLpC4d`9_Q~l5*$0hfUyUCdGw4n-GWUR%;G9TC`BO zcOph-$*-h_dPg(5E0>N=ila-pC{%wtFm$a z4G!=_l>J%Ir`4^%(YH;G05>SwR#BfEJyMzK~?@K6bVon&UwpNh8>@3c{ z=#BfKHdU;k^R+x9kj^{`d9QKruLU!4Dx}}>fiEozJPQi#B*p5()Y1W9g912cgJBq* zo8_0Zc&bMit&H6NbdoLD%;)+?f@x&~r7miCK5!uw((4#r7gX#W-^HTOHSit)Il~c! zRs}SZALnEzg-Hr^K&euDeDh#XmsJNgfAuP{%jOu_RdVqey2MvwrDX_|pFq19F+jbe zL3?9i+j82ocsxaN(xNt?#=@4Y0(;BIn8ge$Bmhd77=3p%VU40+LkT)mg;Vf2@gG&* zjl2L=wW$sT=a4_FNxc4p))MZ$fXa`?hxAPHH_1^ZB+#!y5_AkGXh-@(MlghIJL$Jo zUEAb2B+!Qy)Ts6W(W16Y=QS#$=o{ne?>EJW|Jmv%gr_%(W8Hlw;cFZN9*9i!7Kpjg zjAu-wm1C{qa8NavRI-KyhWETG_S5gI4_J(6mRc|H=qgdLH9s$$;RY9G)P;TU$BA`! zjx3H@!o7daL;^=(%ugGmM8^j=CNWS1$%36<4H0R1}`aBT0VLO_=7jQw}aQbD}8{1?^TI91+Y1(`o#M+ti^c_>$h?t z(kQHKnmy$Aga7wMf{ZlJxlgJo+2Ewquxz#varHfA(rlm)(yJ($*~FQ9k%&TW6#zG; zw|(Xxw_V&$R{}r1XI>~mQ9WcbS@{&Zdq)UrmfZ$fH&^y+YaqF``9l4Z4UTlG`pRJy zrA77kFsa(3I-QIEk$N95iZA(rCX2!0LkE|zqwitqbE)<01I9JsyK z{5J2T!Nv}6SvMYQ9piBiN~lEUI>jNIl%)h^!cuBm(!lPE+|G_u5Sz64^d|CDSkJhf zYCGNp5w%@MmcSnKC1!75vY1OQSgZlp?pZ<|JxjO~4$UvyVly}B!T_zk&m175A*y*$ee1WAGXqDP&ZO)z+SoPFg;gTi{6qglHP3Ucv8 z%09hmi&wjpBhRo8(d)0>kA}H^(feI7fiK8DO-p?oJCcNHEw7@ymgy(O$ua@+5j);X z&$!fj+lDGa8|}SCBFK|bVfNGGl!I5qr{0ufZ@Ee1`b;pB$^ypLPzpEBqu6ZR>m3{b zE4W@e*@7J*hHe-sCyccDqIbb*_c848Et1sWZ*%AWxxrZ7kXXNH4Bk)i(LObs(8%rG zn?P^p)MJL%#|xLO{qf+w>!uXc{t>)GCR)#_WC5JRF8)d?EYlS2QlDK7yluE>;l{Ez zVpr|C+?8gddp`)Jjhejfp=gSzxy66|ihS5uCiySCo&7=4BYkn-%u(D~ROQ|!I{mjz(sXX~di|6;us8@R& zif-YMcyF_er1D}iiml!Nv!nxN;)+e@Db(l#T~c?soC&tXNPshQ+2Zz*(p`R zQI~4gBT3iBxOS0G126Ru1-T7O)RMY>1q5U%tvC8x^$YPfzpf2g{j=@6eYdW69!1Yu zgQKe*fm;%526fIEd&D4u&*729z28LWi4h!n6M?s*2YC9^z@@4Bl-k8{=ZsZsU8U2` z6HP)j$8vU>0I80k7NI4v-ZG5An@RUdHPTd)sG0vE|w(pUHAUpjR!|Ciad zzB-5+htbndoxatf6|qFWT_`XrS>gti+=t#$-j-G!(R#LymP_iJZ}aRvQu4N$6ncvI zUUg}gg%E~T)L^}-OCbNt=GHfdDWdRRJcDktrhq6T)#o80;8jH$X9z)ZvB%CT{op#U zy(ik)z;j#waKg3oyW&-W*@lNW{YRkdO4m33!6NSN<$1a-b3f0wv{d45cV})4 ze^Q0)dc?%J&-qS9J1_A9mAK75D6Z`kqUA4Q&jZmQT*>>m`PeC`#Y<*VqSm=Z*C9|N#w2WGJef26guwd(ovh|Ef{OJo<(BX9Sy?w=i>3rg4=!_UVS4{>rGNm%?9be zru(IO6;X~Iw7F2J$>J{lx`x5K@s!yQd%^d#quKkCPxUGV;+1yb8tCktdCZoXiiyz# zII!QvNgxKZPDxvqnTda4p_E-0k4N=iYesI1Wt&BUy(=)EPUGZ9dQ_q~{EMs23B;;j zvZ4c#^g+=|2C^+>u@bj&=HK;X>v0w8f6Va|5T$X?P~hEZEN$|>h`vwa zMmhId_C5Tz-RcB?-3nl`#_(a=abSZKSC}*YOGmPYpmaq1E3toPLu~u0n7H#D^z|=A zFbyN?&4Mpik|W#z$Zf7LwTKkxgsE{({Wgkuh9({u=(7UQ5T}?yWXncaF4jlN z+#+i`8t|D+*gwjTow}28-Oe9RJ-$wq4^@z8KGl?o6PIZY{Q0&9zK~lf2N3_N6l=PL zNRlUM(v2%&r;;cAlt6wrW%&;vUMhwwYuLRy3*pY^qG^~EYtN>Km!gU+rj&k;2`pGAuI z;K76O6p*X znC-cM`N16=de^r4^9%8}eyvSNdwK)atgQ$SsLHCpOQxeg{ShINeVt2-e|MQX;$ z*-lzFkZx4z#cUyLxo47+6@Jr-tG`5}*Ek!eaFDtty5VQ7cB;^@>B)0O|U zJr(DF1hgrTUbC0N(ZIJj09TUvzo9FDdn@V*!Jz|x8d-s7#z zeDrBpdZC$uD79{08uFQK1=B6T>?zXw3ve*7>KLOY_qN>x2Y%@iDnXk!MK7NI!6iN? z%5I=|&Ruc_AEt2F$1AnlDfuv)}SxC=$mJ-vn-9py2n>b=(B=>{4J zvo1Zs6!N|mW~Bg`ePfB-sem#nil<=j5-=*Mm3Pa2ShqpQBaFnP&k)of4<6HMJKbEc z(-wNlxz9Centv1Re-6jJ-Bm`j8zal#jPYV*USjXPPTZ;lOyLjWm5|?1 z-p@W!<%GWYY=nl9Z=8iSKPbggIxz@&Tfi+klI+l+m0uF@ZDRsgi&D#L-!k2{+BXW* z+?Q}eQzjx_%2%jH3^8NQ1Wz%=}INMJ>1@ad`OkGws2Jq&0mJ zBk#o?KHj+kn0o_Pv&$!`??MAtdOoKl(rnl$T+vl*e1Kn?S9SoaYooz=mOI2nHoW*! zRtY{ADT@f`-e;lCY`RZl`~e^@LOaYdJd>2*JL`F`VWHAwM*S;McdUf>bNDSEWG=Oj zN*1=*E>Zq%`^o1+Un9TW&I=gnV-=2n#o)E1Mj2tNzuG=K57lR9zQXMJV;yQj)b53l z?KVimB&FZ(G$a`x=>AuxMBtcoELV9H=>lcwd&sEai%Tvfm11KFnhrH1S)h(nB|p}2 z=xgXJCm#fz-t7pc@1EaUI3i2wE!PWqDQhdqlcy#QS~MhSB>`|ARx`obtN(0_3P>!u zSD_ue$D=zN@p??k&H+dRwlBnI&9yyrfNOgm9)f$Qx{EZ4LT0QT1dE>}Ke zmO;KAyU53$d~S66Wc+T9)1K9!#qZ?^n+^U8!uYpW$sbHs?s_VFNFj-rTBfm$0IBJ1 zK=8Q2qz)SU+-7-Qg7clPYO2Cn8jKPip#|LOeeE~Hdm=6>!ClNhbkdtL6G+4lZ)0H2 zSFo=7m(S>ojHs$Qa@WwGu?78W(Scn_iRLHY1aJ> zq$cI)G_&QKS1{hAvR~ol;Q!2#8w4||Q^R+Ul@unQ$Ih4T!FH}f_NAg>KlAubte>HD zVg*b1P(+B?OID2;4{C_bJ!CgUxDY~2#^^y#O0UjLI{%PJ#qXy0d`*-W4m@R7r*{Hn zyr2SfllXu$Tc_9Mv01{ zTjcK)a5zl8AvPiIDm|*gxmAb9_P7@m9#0qYb17+)C1dD?!e{EhKUf`mo3(PHt|Fh1 zCSp`IaHFhi$}IofzOG_bX2F(N)8BK|>G!GB6p`aIOc|L~2fNBht5!G9agu9(TV*sB z&FL3Rao=xVS~%O_u-_{O|8M>L`}OZ@HHpM?)6k&f@h)~fj`w_!y{RB#qcLjIHDrp$ zo2HXQX8Z}eKvhCh2JY>AX&T@;JyL5{fd|E4!?57_ohS%y9{6aDx&0b@lhaHmc;yTl z?_tx2ajVx=DMCE}LF3Jb`lgeynPGIANk2M^!ylL13!05@)UP_kSskk&x+B@Uhf0Dg z+WuaPrz}=qc9yQ2NeKQ%U#N{C&pu{w68;sjV^ddUJ6qaRije7|RQVrl?!IMpCm)uR zTv}Ge5!)T^fXa%|<7uK-<{bj_q1DQHKY1VRg_+bmQJwLsO(&3j$`Gt8Gb9!o%`Bpl zoWYO>vU%l>UujIqT~!hN17j`td=&bfw8@m{Rl~p^zm82*jTioVo@l2vg*Z}O4>9

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

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

+ + + + + ); +}); + export function NodeCompatibilityMatrix() { const [snapshot, setSnapshot] = useState(null); const [loadError, setLoadError] = useState(false); @@ -202,15 +180,16 @@ export function NodeCompatibilityMatrix() { const [staticFilter, setStaticFilter] = useState<"all" | Status>("all"); const [expanded, setExpanded] = useState>(() => new Set()); - const rows = snapshot?.rows ?? []; - const chapters = snapshot?.chapters ?? []; + const rows = useMemo(() => snapshot?.rows ?? [], [snapshot]); + const chapters = useMemo(() => snapshot?.chapters ?? [], [snapshot]); const verificationBases = snapshot?.verificationBases; const apiRowCount = useMemo(() => rows.filter((row) => row.scope === "api").length, [rows]); const chapterTitles = useMemo(() => new Map(chapters.map((item) => [item.slug, item.title])), [chapters]); const base = `https://nodejs.org/docs/v${snapshot?.nodeVersion ?? "24.22.0"}/api/`; - const staticCounts = useMemo(() => statusCounts(rows, "static"), [rows]); + const staticCounts = useMemo(() => statusCounts(rows), [rows]); const visibleStatuses = useMemo(() => statuses.filter((status) => staticCounts[status] > 0), [staticCounts]); const tree = useMemo(() => buildTree(rows), [rows]); + const texts = useMemo(() => searchTexts(rows, chapterTitles), [rows, chapterTitles]); useEffect(() => { const controller = new AbortController(); @@ -231,36 +210,23 @@ export function NodeCompatibilityMatrix() { return () => controller.abort(); }, []); - const needle = query.trim().toLocaleLowerCase(); - const hasResultFilter = needle !== "" || staticFilter !== "all"; - const rowMatches = (row: Row) => { - if (staticFilter !== "all" && row.static.status !== staticFilter) return false; - if (!needle) return true; - return [chapterTitles.get(row.chapter), row.kind, row.name, row.signature, row.apiSymbol, row.static.detail] - .join(" ") - .toLocaleLowerCase() - .includes(needle); - }; + // The search field and the status select update at once; the table + // follows the deferred values, so input stays responsive while the rows + // re-render. + const deferredQuery = useDeferredValue(query); + const deferredStatic = useDeferredValue(staticFilter); + const needle = deferredQuery.trim().toLocaleLowerCase(); + const hasResultFilter = needle !== "" || deferredStatic !== "all"; - const resultTree = useMemo( - () => hasResultFilter ? filterTree(tree, rowMatches) : tree, - // rowMatches closes over these filter values. - // eslint-disable-next-line react-hooks/exhaustive-deps - [hasResultFilter, needle, staticFilter, tree], + const result = useMemo( + () => hasResultFilter ? filterTree(tree, rowMatcher(texts, needle, deferredStatic)) : { nodes: tree, matched: nodeCount(tree) }, + [hasResultFilter, needle, deferredStatic, texts, tree], ); const visible = useMemo( - () => flattenTree(resultTree, expanded, hasResultFilter), - [expanded, hasResultFilter, resultTree], - ); - const matchedCount = useMemo( - () => hasResultFilter ? tree.reduce((count, node) => { - const visit = (item: TreeNode): number => (rowMatches(item.row) ? 1 : 0) + item.children.reduce((sum, child) => sum + visit(child), 0); - return count + visit(node); - }, 0) : nodeCount(tree), - // rowMatches closes over these filter values. - // eslint-disable-next-line react-hooks/exhaustive-deps - [hasResultFilter, needle, staticFilter, tree], + () => flattenTree(result.nodes, expanded, hasResultFilter), + [expanded, hasResultFilter, result], ); + const matchedCount = result.matched; const changeQuery = (value: string) => setQuery(value); const resetFilters = () => { @@ -268,12 +234,12 @@ export function NodeCompatibilityMatrix() { setStaticFilter("all"); setExpanded(new Set()); }; - const toggle = (id: string) => setExpanded((current) => { + const toggle = useCallback((id: string) => setExpanded((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; - }); + }), []); useEffect(() => { if (!snapshot) return; @@ -316,8 +282,8 @@ export function NodeCompatibilityMatrix() { {visible.length.toLocaleString()} visible · {matchedCount.toLocaleString()} {hasResultFilter ? "matching" : "API"} rows {!hasResultFilter ? ( - - + + ) : null} @@ -335,33 +301,19 @@ export function NodeCompatibilityMatrix() { {visible.map(({ node, level }) => { const row = node.row; - const hasChildren = node.children.length > 0; - const open = hasResultFilter || expanded.has(row.id); - const href = `${base}${row.chapter}.html${row.anchor ? `#${row.anchor}` : ""}`; - const isRoot = level === 1; - const displayName = isRoot ? chapterTitles.get(row.chapter) ?? row.label : row.label; return ( - - - - - + 0} + open={hasResultFilter || expanded.has(row.id)} + filtering={hasResultFilter} + displayName={level === 1 ? chapterTitles.get(row.chapter) ?? row.label : row.label} + href={`${base}${row.chapter}.html${row.anchor ? `#${row.anchor}` : ""}`} + verification={verificationBases?.[row.static.verification]} + onToggle={toggle} + /> ); })} diff --git a/docs/src/lib/compatibility-tree.ts b/docs/src/lib/compatibility-tree.ts new file mode 100644 index 000000000..4044bbc8d --- /dev/null +++ b/docs/src/lib/compatibility-tree.ts @@ -0,0 +1,157 @@ +// The compatibility matrix's data model: the ledger rows as a tree, and the +// search and filter passes over it. Kept free of React so the passes can be +// tested directly. + +export type Status = + | "supported" + | "partial" + | "refused" + | "not-implemented" + | "by-design" + | "unreviewed" + | "not-applicable"; +export type VerificationBasis = + | "test-backed" + | "explicit-refusal" + | "verified-absence" + | "declared-gap" + | "registry-gap" + | "architectural-policy" + | "unreviewed" + | "not-applicable" + | "derived"; +export type Verification = { label: string; detail: string }; +export type Tier = { status: Status; detail: string; verification: VerificationBasis }; +export type NodeStability = { index: string; level: number; text: string; inherited: boolean } | null; +export type Row = { + id: string; + chapter: string; + kind: string; + name: string; + signature: string; + label: string; + apiSymbol: string; + depth: number; + scope: "api" | "documentation" | "metadata" | "configuration"; + anchor: string; + nodeStability: NodeStability; + static: Tier; +}; +export type Chapter = { slug: string; title: string; entries: number; apiEntries: number }; +export type Snapshot = { + nodeVersion: string; + verificationBases: Record; + chapters: Chapter[]; + rows: Row[]; +}; +export type TreeNode = { row: Row; children: TreeNode[] }; +export type VisibleNode = { node: TreeNode; level: number }; + +export const statuses = ["supported", "partial", "not-implemented", "refused", "by-design", "unreviewed", "not-applicable"] as const; + +export function statusCounts(rows: Row[]): Record { + return rows.reduce>( + (counts, row) => { + if (row.scope !== "api" || row.depth === 0) return counts; + counts[row.static.status] += 1; + return counts; + }, + { + supported: 0, + partial: 0, + refused: 0, + "not-implemented": 0, + "by-design": 0, + unreviewed: 0, + "not-applicable": 0, + }, + ); +} + +/** Node's all.json is preorder with an explicit heading depth. */ +export function buildTree(rows: Row[]): TreeNode[] { + const roots: TreeNode[] = []; + const stack: TreeNode[] = []; + for (const row of rows) { + const node: TreeNode = { row, children: [] }; + while (stack.length > row.depth) stack.pop(); + const parent = stack[row.depth - 1]; + if (row.depth === 0 || parent === undefined) roots.push(node); + else parent.children.push(node); + stack[row.depth] = node; + stack.length = row.depth + 1; + } + return roots; +} + +/** The lowercase text a search matches against, built once per row: the + * chapter title, kind, name, signature, API symbol and support detail. */ +export function searchTexts(rows: Row[], chapterTitles: ReadonlyMap): Map { + return new Map( + rows.map((row) => [ + row.id, + [chapterTitles.get(row.chapter), row.kind, row.name, row.signature, row.apiSymbol, row.static.detail] + .join(" ") + .toLocaleLowerCase(), + ]), + ); +} + +/** The rows a status filter and a lowercase search needle keep. */ +export function rowMatcher( + texts: ReadonlyMap, + needle: string, + status: "all" | Status, +): (row: Row) => boolean { + return (row) => { + if (status !== "all" && row.static.status !== status) return false; + return needle === "" || (texts.get(row.id) ?? "").includes(needle); + }; +} + +/** The matching rows with their ancestors, and the number of matching + * rows, in one pass. */ +export function filterTree(nodes: TreeNode[], matches: (row: Row) => boolean): { nodes: TreeNode[]; matched: number } { + let matched = 0; + const visit = (items: TreeNode[]): TreeNode[] => { + const filtered: TreeNode[] = []; + for (const node of items) { + const children = visit(node.children); + const own = matches(node.row); + if (own) matched += 1; + if (own || children.length > 0) filtered.push({ row: node.row, children }); + } + return filtered; + }; + return { nodes: visit(nodes), matched }; +} + +export function flattenTree(nodes: TreeNode[], expanded: ReadonlySet, forceExpanded: boolean): VisibleNode[] { + const visible: VisibleNode[] = []; + const visit = (items: TreeNode[], level: number) => { + for (const node of items) { + visible.push({ node, level }); + if (node.children.length > 0 && (forceExpanded || expanded.has(node.row.id))) { + visit(node.children, level + 1); + } + } + }; + visit(nodes, 1); + return visible; +} + +export function nodeCount(nodes: TreeNode[]): number { + return nodes.reduce((count, node) => count + 1 + nodeCount(node.children), 0); +} + +export function expandableIds(nodes: TreeNode[]): Set { + const ids = new Set(); + const visit = (items: TreeNode[]) => { + for (const node of items) { + if (node.children.length > 0) ids.add(node.row.id); + visit(node.children); + } + }; + visit(nodes); + return ids; +} diff --git a/docs/test/compatibility-matrix.test.mjs b/docs/test/compatibility-matrix.test.mjs new file mode 100644 index 000000000..abd3df945 --- /dev/null +++ b/docs/test/compatibility-matrix.test.mjs @@ -0,0 +1,115 @@ +import assert from "node:assert/strict"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { test } from "node:test"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import ts from "typescript"; +import { + buildTree, + expandableIds, + filterTree, + flattenTree, + nodeCount, + rowMatcher, + searchTexts, + statusCounts, +} from "../src/lib/compatibility-tree.ts"; + +const row = (id, depth, label, status, detail = "") => ({ + id, + chapter: "zlib", + kind: depth === 0 ? "module" : "method", + name: label, + signature: label, + label, + apiSymbol: label, + depth, + scope: "api", + anchor: "", + nodeStability: null, + static: { status, detail, verification: "test-backed" }, +}); +const rows = [ + row("root", 0, "zlib", "partial"), + row("a", 1, "zlib.gzipSync()", "supported", "Lowered with Node's options"), + row("b", 1, "zlib.Gzip", "partial"), + row("b1", 2, "gzip.flush()", "supported"), + row("c", 1, "zlib.zstdCompress()", "not-implemented"), +]; +const titles = new Map([["zlib", "Zlib"]]); + +test("the tree follows the rows' heading depths", () => { + const tree = buildTree(rows); + assert.equal(tree.length, 1); + assert.deepEqual(tree[0].children.map((node) => node.row.id), ["a", "b", "c"]); + assert.deepEqual(tree[0].children[1].children.map((node) => node.row.id), ["b1"]); + assert.equal(nodeCount(tree), 5); + assert.deepEqual([...expandableIds(tree)].sort(), ["b", "root"]); +}); + +test("filtering keeps matching rows with their ancestors and counts the matches once", () => { + const tree = buildTree(rows); + const texts = searchTexts(rows, titles); + const byText = filterTree(tree, rowMatcher(texts, "flush", "all")); + assert.equal(byText.matched, 1); + assert.deepEqual(flattenTree(byText.nodes, new Set(), true).map(({ node, level }) => `${node.row.id}@${level}`), ["root@1", "b@2", "b1@3"]); + const byDetail = filterTree(tree, rowMatcher(texts, "node's options", "all")); + assert.deepEqual(flattenTree(byDetail.nodes, new Set(), true).map(({ node }) => node.row.id), ["root", "a"]); + const byStatus = filterTree(tree, rowMatcher(texts, "", "supported")); + assert.equal(byStatus.matched, 2); + const both = filterTree(tree, rowMatcher(texts, "gzip", "partial")); + assert.equal(both.matched, 1); + assert.equal(filterTree(tree, rowMatcher(texts, "brotli", "all")).nodes.length, 0); +}); + +test("collapsed rows hide their children until expanded", () => { + const tree = buildTree(rows); + assert.deepEqual(flattenTree(tree, new Set(), false).map(({ node }) => node.row.id), ["root"]); + assert.deepEqual(flattenTree(tree, new Set(["root"]), false).map(({ node }) => node.row.id), ["root", "a", "b", "c"]); + assert.deepEqual(flattenTree(tree, new Set(["root", "b"]), false).map(({ node }) => node.row.id), ["root", "a", "b", "b1", "c"]); +}); + +test("status counts cover API rows below the chapters", () => { + const counts = statusCounts(rows); + assert.equal(counts.supported, 2); + assert.equal(counts.partial, 1); + assert.equal(counts["not-implemented"], 1); +}); + +/** The matrix component, compiled for Node from its TSX source. */ +async function loadMatrix() { + const docs = new URL("../", import.meta.url); + const source = await readFile(new URL("src/components/node-compatibility-matrix.tsx", docs), "utf8"); + const { outputText } = ts.transpileModule(source, { + compilerOptions: { jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, + }); + const lib = pathToFileURL(fileURLToPath(new URL("src/lib/compatibility-tree.ts", docs))).href; + const dir = new URL("node_modules/.cache/compatibility-matrix-test/", docs); + await mkdir(dir, { recursive: true }); + const file = new URL("node-compatibility-matrix.mjs", dir); + await writeFile(file, outputText.replace('"@/lib/compatibility-tree"', JSON.stringify(lib))); + return import(file.href); +} + +test("badges render without their tooltips until hovered or focused", async () => { + const { StatusBadge, StabilityBadge } = await loadMatrix(); + const status = renderToStaticMarkup( + createElement(StatusBadge, { + status: "partial", + detail: "Lowered with Node's options", + verification: { label: "Test-backed", detail: "Corpus programs cover it." }, + }), + ); + assert.ok(status.includes('tabindex="0"'), status); + assert.ok(status.includes('title="Lowered with Node's options'), status); + assert.ok(!status.includes('role="tooltip"'), status); + assert.ok(!status.includes("aria-describedby"), status); + const stability = renderToStaticMarkup( + createElement(StabilityBadge, { stability: { index: "2", level: 2, text: "Stable", inherited: false } }), + ); + assert.ok(stability.includes('tabindex="0"'), stability); + assert.ok(!stability.includes('role="tooltip"'), stability); + const legend = renderToStaticMarkup(createElement(StatusBadge, { status: "supported" })); + assert.ok(!legend.includes("tabindex"), legend); +}); From ccd45c748f91d060ec0631c53df93f34bac383dc Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 12:21:50 -0500 Subject: [PATCH 136/409] Address the Node island report review findings --- packages/compiler/src/cli/command.ts | 15 +++- packages/compiler/src/diagnostics/envelope.ts | 5 +- packages/compiler/src/diagnostics/render.ts | 27 ++++++- packages/compiler/src/executable/prepare.ts | 6 +- packages/compiler/src/frontend/analysis.ts | 4 +- .../compiler/src/frontend/pipeline-native.ts | 4 +- packages/compiler/src/frontend/pipeline.ts | 78 ++++++++++++------- packages/compiler/src/index.ts | 9 ++- packages/compiler/src/native/compiler.ts | 3 +- .../node-islands-config/main.ts.json | 6 -- .../node-islands-config/main.ts | 4 + .../node-islands-config/package.json | 0 tests/diagnostics/node-islands-config/main.ts | 3 - .../node-islands-config/main.ts.txt | 35 --------- tests/harness/coverage.test.ts | 28 +++++++ 15 files changed, 141 insertions(+), 86 deletions(-) delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json create mode 100644 tests/coverage-fixtures/node-islands-config/main.ts rename tests/{diagnostics => coverage-fixtures}/node-islands-config/package.json (100%) delete mode 100644 tests/diagnostics/node-islands-config/main.ts delete mode 100644 tests/harness/__snapshots__/node-islands-config/main.ts.txt diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts index 2d15cbbb3..a660d339a 100644 --- a/packages/compiler/src/cli/command.ts +++ b/packages/compiler/src/cli/command.ts @@ -19,7 +19,7 @@ import { } from "../node-islands/policy.js"; import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js"; import { buildEnvelope } from "../diagnostics/envelope.js"; -import { renderWarnings } from "../diagnostics/render.js"; +import { renderConfigWarnings, renderWarnings } from "../diagnostics/render.js"; import { isPerformanceCode, renderPerformanceWarnings } from "../diagnostics/dynamic-tier.js"; import { isNativeOptimization } from "../backend/optimization.js"; import { setProvenanceSources } from "../frontend/provenance-registry.js"; @@ -609,14 +609,21 @@ async function main(args: string[], host: CliHost): Promise { // warning names likely-hot functions that use untyped values. `run` // keeps stderr for the program's own output. const color = process.stderr.isTTY ?? false; - const divergences = warnings.filter((d) => !isPerformanceCode(d.code)); + const config = warnings.filter((d) => d.code === "SC8004"); + const divergences = warnings.filter((d) => !isPerformanceCode(d.code) && d.code !== "SC8004"); const performance = warnings.filter((d) => isPerformanceCode(d.code)); - if (divergences.length > 0) + if (config.length > 0) + process.stderr.write( + renderConfigWarnings(config, result.sourceTexts ?? new Map(), { color }) + "\n", + ); + if (divergences.length > 0) { + if (config.length > 0) process.stderr.write("\n"); process.stderr.write( renderWarnings(divergences, result.sourceTexts ?? new Map(), { color }) + "\n", ); + } if (performance.length > 0) { - if (divergences.length > 0) process.stderr.write("\n"); + if (divergences.length > 0 || config.length > 0) process.stderr.write("\n"); process.stderr.write(renderPerformanceWarnings(performance) + "\n"); } } diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts index ff08c3968..ad2e31507 100644 --- a/packages/compiler/src/diagnostics/envelope.ts +++ b/packages/compiler/src/diagnostics/envelope.ts @@ -334,7 +334,10 @@ export function locateDiagnostic( return { code: diag.code, category: categoryOf(diag), - severity: isDivergenceCode(diag.code) || isPerformanceCode(diag.code) ? "warning" : "error", + severity: + isDivergenceCode(diag.code) || isPerformanceCode(diag.code) || diag.code === "SC8004" + ? "warning" + : "error", scope, file: diag.loc.file, line: start.line, diff --git a/packages/compiler/src/diagnostics/render.ts b/packages/compiler/src/diagnostics/render.ts index e5a75c6bc..16fe3f0bf 100644 --- a/packages/compiler/src/diagnostics/render.ts +++ b/packages/compiler/src/diagnostics/render.ts @@ -102,14 +102,12 @@ export function renderDiagnostic( frame.push( c(DIM, ` ${" ".repeat(gutterWidth)} | `) + " ".repeat(caretCol) + - c(/^SC6\d{3}$/.test(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)), + c(isWarningCode(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)), ); emitLine(lineNum + 1); } - // The divergence band (SC6xxx) is the one warning severity: the program - // compiles, and the site can behave differently from Node. - const warning = /^SC6\d{3}$/.test(diag.code); + const warning = isWarningCode(diag.code); out.push( `${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${warning ? c(YELLOW, "warning") : c(RED, "error")} ${c(BOLD, diag.code)}: ${diag.message}`, ); @@ -154,6 +152,27 @@ export function renderDiagnostics( /** Divergence warnings after a successful build: the rendered sites plus a * one-line count, on stderr. */ +/** The warning severities: the divergence band (SC6xxx), where the program + * compiles and the site can behave differently from Node, and unknown + * settings in the project's scriptc configuration (SC8004), which builds do + * not use. */ +export function isWarningCode(code: string): boolean { + return /^SC6\d{3}$/.test(code) || code === "SC8004"; +} + +/** A build's SC8004 warnings: the unknown scriptc settings it ignored. */ +export function renderConfigWarnings( + diags: readonly ScrDiagnostic[], + sourceTextByFile: Map, + opts: RenderOptions = {}, +): string { + const n = diags.length; + return ( + renderDiagnostics([...diags], sourceTextByFile, opts) + + `\n\n${n} warning${n === 1 ? "" : "s"}: the build does not use ${n === 1 ? "this setting" : "these settings"}; scriptc coverage reports Node islands once they are valid.` + ); +} + export function renderWarnings( diags: readonly ScrDiagnostic[], sourceTextByFile: Map, diff --git a/packages/compiler/src/executable/prepare.ts b/packages/compiler/src/executable/prepare.ts index 1771f98f5..027bd6ca6 100644 --- a/packages/compiler/src/executable/prepare.ts +++ b/packages/compiler/src/executable/prepare.ts @@ -122,6 +122,10 @@ export function prepareExecutableModule( mod: lowered.module!, sourceTexts, stats: lowered.stats, - warnings: [...(lowered.divergences ?? []), ...(lowered.performance ?? [])], + warnings: [ + ...(fe.warnings ?? []), + ...(lowered.divergences ?? []), + ...(lowered.performance ?? []), + ], }; } diff --git a/packages/compiler/src/frontend/analysis.ts b/packages/compiler/src/frontend/analysis.ts index 403cb70b7..848306b66 100644 --- a/packages/compiler/src/frontend/analysis.ts +++ b/packages/compiler/src/frontend/analysis.ts @@ -38,7 +38,9 @@ export function analyzeWithFrontend( } ffi = loaded.profile; } - const fe = createFrontend(entryPath, opts.npmStatic, opts.externalTypes); + const fe = createFrontend(entryPath, opts.npmStatic, opts.externalTypes, undefined, { + nodeIslands: true, + }); try { const emptyStats = { statementsTotal: 0, diff --git a/packages/compiler/src/frontend/pipeline-native.ts b/packages/compiler/src/frontend/pipeline-native.ts index 511fd62f2..f2df52608 100644 --- a/packages/compiler/src/frontend/pipeline-native.ts +++ b/packages/compiler/src/frontend/pipeline-native.ts @@ -1,4 +1,4 @@ -import { runFrontend, type Frontend } from "./pipeline.js"; +import { runFrontend, type Frontend, type FrontendOptions } from "./pipeline.js"; import { loadProgram } from "./program.js"; import { FrontendServices, type ComptimeEvaluator } from "./services.js"; import { createNativeTs7Api } from "./ts7/native-api.js"; @@ -13,6 +13,7 @@ export function runNativeFrontend( externalTypes?: Readonly>, evaluateComptime?: ComptimeEvaluator, libraryNpmStatic: readonly string[] = [], + options: FrontendOptions = {}, ): Frontend { const services = new FrontendServices( (options) => createNativeTs7Api({ ...options, executable }), @@ -31,6 +32,7 @@ export function runNativeFrontend( npmStatic, externalTypes, libraryNpmStatic, + options, ); return { ...frontend, diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 1021202cc..4e6a1c0f5 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -115,18 +115,30 @@ export interface Frontend { /** The Node island detector's input (report-only): the program's * modules with their pre-scan markers, package import edges, and how * each refused package was refused. Call before dispose. */ - nodeIslandScan?: (opts: { userJs: boolean }) => NodeIslandScan; + nodeIslandScan?: ((opts: { userJs: boolean }) => NodeIslandScan) | undefined; + /** Diagnostics that do not fail a build: unknown settings in the + * project's scriptc configuration (SC8004), which builds do not use. */ + warnings?: ScrDiagnostic[]; /** Releases the frontend's resources (the spawned tsgo server). Call * exactly once, after the last lower(). */ dispose: () => void; } /** Hosts own transport lifetimes; shared compiler stages own the frontend. */ +/** What a frontend computes beyond the build's own needs. */ +export interface FrontendOptions { + /** The Node island report (coverage): the project's scriptc settings and + * the detector's scan of the program and its npm packages. Builds do not + * read either. */ + nodeIslands?: boolean; +} + export type FrontendFactory = ( entryPath: string, npmStatic?: readonly string[] | "auto" | "lib", externalTypes?: Readonly>, libraryNpmStatic?: readonly string[], + options?: FrontendOptions, ) => Frontend; /** Adds the program modules a literal `import()` reaches to `scanned`, @@ -426,6 +438,7 @@ export function runFrontend( npmStatic?: readonly string[] | "auto" | "lib", externalTypes?: Readonly>, libraryNpmStatic: readonly string[] = [], + options: FrontendOptions = {}, ): Frontend { // A preflight or package probe may throw after its host has opened. Track // only live loads, releasing each immediately on normal fallback; this @@ -465,6 +478,7 @@ export function runFrontend( externalTypes, [...new Set(names)], transitive, + options, ); } catch (error) { for (const load of active) { @@ -486,6 +500,7 @@ function loadFrontend( externalTypes?: Readonly>, libraryNpmStatic: readonly string[] = [], transitive: Set = new Set(), + options: FrontendOptions = {}, ): Frontend { // Resolver package/workspace metadata is intentionally shared across the // several load attempts of ONE auto-detection fixpoint, but never across @@ -632,7 +647,7 @@ function loadFrontend( } const dropping = [...reasons.keys()].filter((p) => effective.has(p)); if (dropping.length === 0) break; - scanDroppedPackages(load, dropping, droppedScan); + if (options.nodeIslands === true) scanDroppedPackages(load, dropping, droppedScan); for (const p of dropping) { effective.delete(p); statuses.push({ package: p, status: "refused", detail: reasons.get(p)! }); @@ -684,13 +699,15 @@ function loadFrontend( probe.dispose(); if (probeDiags.some((d) => d.code === "SC0001")) dropWithNote(p); } - scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); + if (options.nodeIslands === true) + scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); load.dispose(); load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes }); preflight = checkPreflight(load); if (preflight.some((d) => d.code === "SC0001") && effective.size > 0) { for (const p of [...effective]) dropWithNote(p); - scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); + if (options.nodeIslands === true) + scanDroppedPackages(load, lastResortDropped.splice(0), droppedScan); load.dispose(); load = loadProgram(entryPath, { npmStatic: [...effective], externalTypes }); preflight = checkPreflight(load); @@ -711,12 +728,16 @@ function loadFrontend( } const finalLoad = load; - // An unknown key or value in the project's scriptc configuration fails - // the build before anything else (SC8004). + // An unknown key or value in the project's scriptc configuration makes + // the Node island report unavailable (SC8004). The settings configure + // only that report: a build warns about them and goes on. const projectConfig = readProjectConfig(entryPath); const configDiagnostics = projectConfig.diagnostics; + const report = options.nodeIslands === true; return { - preflight: configDiagnostics.length > 0 ? [...configDiagnostics, ...preflight] : preflight, + preflight: + report && configDiagnostics.length > 0 ? [...configDiagnostics, ...preflight] : preflight, + ...(!report && configDiagnostics.length > 0 ? { warnings: configDiagnostics } : {}), entryText: () => finalLoad.entry.text, entryExports: () => entryFunctionExports(finalLoad.entry), // The contract scans the PROGRAM's source files, not the runtime @@ -779,26 +800,29 @@ function loadFrontend( }, npmStatic: statuses, npmImportSites: npmSites, - nodeIslandScan: ({ userJs }) => { - const files = new Set([finalLoad.entry, ...finalLoad.moduleOrder]); - addDynamicImportGraph(finalLoad, files); - // Package edges matter only when the program has npm packages: one - // that loads, one that was refused, or one judged and dropped. - const npm = - statuses.length > 0 || - droppedScan.modules.length > 0 || - [...files].some((sf) => sf.fileName.includes("/node_modules/")); - const scanned = scanFiles(finalLoad.program, [...files], { - userJs, - dropped: false, - edges: npm, - }); - return { - modules: [...droppedScan.modules, ...scanned.modules], - edges: [...droppedScan.edges, ...scanned.edges], - refusals, - }; - }, + nodeIslandScan: + options.nodeIslands !== true + ? undefined + : ({ userJs }) => { + const files = new Set([finalLoad.entry, ...finalLoad.moduleOrder]); + addDynamicImportGraph(finalLoad, files); + // Package edges matter only when the program has npm packages: one + // that loads, one that was refused, or one judged and dropped. + const npm = + statuses.length > 0 || + droppedScan.modules.length > 0 || + [...files].some((sf) => sf.fileName.includes("/node_modules/")); + const scanned = scanFiles(finalLoad.program, [...files], { + userJs, + dropped: false, + edges: npm, + }); + return { + modules: [...droppedScan.modules, ...scanned.modules], + edges: [...droppedScan.edges, ...scanned.edges], + refusals, + }; + }, dispose: finalLoad.dispose, }; } diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 41f5cd619..14bdebf3c 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -412,8 +412,13 @@ export async function analyzeCached( return result; } -const nodeFrontend: FrontendFactory = (entry, npmStatic, externalTypes, libraryNpmStatic) => - runFrontend(entry, loadProgram, npmStatic, externalTypes, libraryNpmStatic); +const nodeFrontend: FrontendFactory = ( + entry, + npmStatic, + externalTypes, + libraryNpmStatic, + options, +) => runFrontend(entry, loadProgram, npmStatic, externalTypes, libraryNpmStatic, options); /** The checker's part of the executable cache key (`checkerCacheIdentity`), as the CLI's startup route computes it. */ function checkerField(): { checker?: string } { diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts index a37d76f80..8f88356e9 100644 --- a/packages/compiler/src/native/compiler.ts +++ b/packages/compiler/src/native/compiler.ts @@ -108,7 +108,7 @@ export class NativeCompiler { private readonly cache: NativeCache | null = null, ) { const evaluator = toolchain.comptimeExecutable; - this.frontend = (entry, npmStatic, externalTypes, libraryNpmStatic) => + this.frontend = (entry, npmStatic, externalTypes, libraryNpmStatic, options) => runNativeFrontend( entry, toolchain.ts7Executable, @@ -119,6 +119,7 @@ export class NativeCompiler { : (source, timeout) => evaluateNativeComptime(source, timeout, toolchain.ts7Executable, evaluator), libraryNpmStatic, + options, ); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json deleted file mode 100644 index 6b86e7f9a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/node-islands-config/main.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/node-islands-config/main.ts" - ], - "diags": [] -} diff --git a/tests/coverage-fixtures/node-islands-config/main.ts b/tests/coverage-fixtures/node-islands-config/main.ts new file mode 100644 index 000000000..5bacea88a --- /dev/null +++ b/tests/coverage-fixtures/node-islands-config/main.ts @@ -0,0 +1,4 @@ +// Unknown values in scriptc.packages or scriptc.nodeIslands are SC8004: a +// build warns about them and goes on (it does not use the settings), and +// coverage reports no Node islands until they are valid. +console.log("configured"); diff --git a/tests/diagnostics/node-islands-config/package.json b/tests/coverage-fixtures/node-islands-config/package.json similarity index 100% rename from tests/diagnostics/node-islands-config/package.json rename to tests/coverage-fixtures/node-islands-config/package.json diff --git a/tests/diagnostics/node-islands-config/main.ts b/tests/diagnostics/node-islands-config/main.ts deleted file mode 100644 index cc7b3735f..000000000 --- a/tests/diagnostics/node-islands-config/main.ts +++ /dev/null @@ -1,3 +0,0 @@ -// An unknown value in scriptc.packages or scriptc.nodeIslands is SC8004: -// the configuration the project asked for is never silently ignored. -console.log("configured"); diff --git a/tests/harness/__snapshots__/node-islands-config/main.ts.txt b/tests/harness/__snapshots__/node-islands-config/main.ts.txt deleted file mode 100644 index 03cc714aa..000000000 --- a/tests/harness/__snapshots__/node-islands-config/main.ts.txt +++ /dev/null @@ -1,35 +0,0 @@ -node-islands-config/package.json:6:7 - error SC8004: package.json 'scriptc.packages.encoding' has the unknown value "unloded" - - 5 | "packages": { - 6 | "encoding": "unloded", - | ^ - 7 | "debug": { "run": "island" } - - hint: use one of "unloaded", "node", "native", "auto", or an object with run, evaluate, share, marshal, schedule - -node-islands-config/package.json:7:18 - error SC8004: package.json 'scriptc.packages.debug.run' has the unknown value "island" - - 6 | "encoding": "unloded", - 7 | "debug": { "run": "island" } - | ^ - 8 | }, - - hint: use one of "native", "node", "auto" - -node-islands-config/package.json:10:7 - error SC8004: package.json 'scriptc.nodeIslands.auto' must be true or false, not "yes" - - 9 | "nodeIslands": { - 10 | "auto": "yes", - | ^ - 11 | "mode": "auto" - - hint: use a boolean - -node-islands-config/package.json:11:7 - error SC8004: package.json 'scriptc.nodeIslands.mode' is not a known Node island setting - - 10 | "auto": "yes", - 11 | "mode": "auto" - | ^ - 12 | } - - hint: the settings are auto, modules, node, nodeVersion, packaging, speculativeStart \ No newline at end of file diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 2c34a69e1..8dd7173ad 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import { expect, test } from "vitest"; import { analyze, + compile, coverageEnvelope, coveragePasses, renderCoverage, @@ -871,6 +872,33 @@ test("Node islands (report-only): one package per class, gap islands first", asy expect(coveragePasses(coverage, "node-islands")).toBe(false); }); +test("unknown scriptc settings warn in a build and make the island report unavailable", async () => { + const entry = fixture("node-islands-config/main.ts"); + const outDir = mkdtempSync(join(tmpdir(), "scriptc-island-config-")); + try { + const built = await compile(entry, { + outputKind: "ir", + outPath: join(outDir, "main.ir.json"), + outDir, + }); + expect(built.ok).toBe(true); + if (!built.ok) return; + expect((built.warnings ?? []).map((d) => [d.code, d.message])).toEqual([ + ["SC8004", `package.json 'scriptc.packages.encoding' has the unknown value "unloded"`], + ["SC8004", `package.json 'scriptc.packages.debug.run' has the unknown value "island"`], + ["SC8004", `package.json 'scriptc.nodeIslands.auto' must be true or false, not "yes"`], + ["SC8004", "package.json 'scriptc.nodeIslands.mode' is not a known Node island setting"], + ]); + } finally { + rmSync(outDir, { recursive: true, force: true }); + } + const { coverage } = analyze(entry); + expect(coverage.nodeIslands).toBeUndefined(); + expect(renderCoverage({ ...coverage, file: "main.ts" }, { root: repoRoot })).toContain( + "not analyzable: the project's scriptc configuration has 4 unknown settings", + ); +}); + test("Node island policy from the command line: native, forced, off", () => { const entry = fixture("node-islands/main.ts"); const forced = analyze(entry, { From 4d706302292d78adc1f31be3e39f9434f6e53ecb Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 12:30:31 -0500 Subject: [PATCH 137/409] Keep a keyboard-focused badge's tooltip open when the pointer leaves Hover and keyboard focus now open the tooltip separately, as the CSS-only tooltip's hover and focus-visible rules did, so moving the pointer off a focused badge no longer unmounts its tooltip or drops aria-describedby. Escape still dismisses it. --- .../components/node-compatibility-matrix.tsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/src/components/node-compatibility-matrix.tsx b/docs/src/components/node-compatibility-matrix.tsx index bcc5ece61..af2435668 100644 --- a/docs/src/components/node-compatibility-matrix.tsx +++ b/docs/src/components/node-compatibility-matrix.tsx @@ -55,27 +55,34 @@ const stabilityStyles: Record = { /** A badge's tooltip, mounted only while the badge is hovered or has * keyboard focus (the :focus-visible rule the CSS-only tooltip used), and - * named by aria-describedby while open. Escape dismisses it. */ + * named by aria-describedby while open. Hover and focus are tracked apart, + * so moving the pointer away keeps a keyboard-focused badge's tooltip. + * Escape dismisses it until the next hover or focus. */ function BadgeTooltip({ tooltipClass, children, badge }: { tooltipClass: string; children: ReactNode; badge: ReactNode }) { - const [open, setOpen] = useState(false); + const [hovered, setHovered] = useState(false); + const [focused, setFocused] = useState(false); + const open = hovered || focused; const id = useId(); - const show = useCallback(() => setOpen(true), []); - const hide = useCallback(() => setOpen(false), []); + const enter = useCallback(() => setHovered(true), []); + const leave = useCallback(() => setHovered(false), []); const focus = useCallback((event: FocusEvent) => { - if (event.currentTarget.matches(":focus-visible")) setOpen(true); + if (event.currentTarget.matches(":focus-visible")) setFocused(true); }, []); + const blur = useCallback(() => setFocused(false), []); const key = useCallback((event: KeyboardEvent) => { - if (event.key === "Escape") setOpen(false); + if (event.key !== "Escape") return; + setHovered(false); + setFocused(false); }, []); return ( {badge} From cda584260f660821d30912ea7ff911de8dda65de Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 09:52:26 -0700 Subject: [PATCH 138/409] Run typed arrays that the checker types any on their native bytes A JavaScript binding referenced by its own initializer's inference collapses to `any` in the checker while it still lowers to a Uint8Array: hono's etag digest declares `const nextChunk = new Uint8Array(new ArrayBuffer(f(chunk)))` beside `chunk = nextChunk`. The typed-array lowerings key on the checker type and declined such a receiver, and the dynamic paths required a dyn value, so `nextChunk.set(...)` and `nextChunk.byteLength` threw SC1090/SC2020 at run time and multi-chunk etag digests failed. Method calls and property reads on such a receiver now box it by reference (the box shares the native storage, so writes stay visible) and dispatch on the runtime kind. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/property-access.ts | 19 ++++++ .../src/frontend/lowering/lower-calls.ts | 8 +++ .../7748-npm-any-bytes-receiver/main.ts.json | 6 ++ .../7748-npm-any-bytes-receiver/main.ts | 33 ++++++++++ .../node_modules/chunk-buffers/index.d.ts | 1 + .../node_modules/chunk-buffers/index.js | 18 ++++++ .../node_modules/chunk-buffers/package.json | 1 + .../node_modules/etag-digest/index.d.ts | 1 + .../node_modules/etag-digest/index.js | 60 +++++++++++++++++++ .../node_modules/etag-digest/package.json | 1 + .../7748-npm-any-bytes-receiver/main.ts.json | 1 + 11 files changed, 149 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-npm-any-bytes-receiver/main.ts.json create mode 100644 tests/corpus/7748-npm-any-bytes-receiver/main.ts create mode 100644 tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.d.ts create mode 100644 tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.js create mode 100644 tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/package.json create mode 100644 tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.d.ts create mode 100644 tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.js create mode 100644 tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/package.json create mode 100644 tests/harness/corpus-costs/7748-npm-any-bytes-receiver/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 7b12297a8..b0c7a66bb 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1499,6 +1499,25 @@ export function lowerPropertyAccessExpr( ) { return afterReceiver(recvLowered, dynUndefinedExpr(loc), loc); } + // A typed array under a checker `any` (a JS binding its own initializer's + // inference refers to: hono's etag digest `nextChunk`) reads through its + // box, as the method-call twin dispatches. The box shares the native + // storage, so it needs no fresh-result restriction. + if ( + recvLowered.type.kind === "bytes" && + (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Any) !== 0 + ) { + return lowerer.maybeNarrow( + { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(recvLowered, DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + }, + expr, + ); + } lowerer.unsupported( "SC1090", expr, diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 672801e9e..6779cde82 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -3755,6 +3755,14 @@ function lowerDynReceiverMethodCall( // 'unknown' slot does. if (checkerUntyped && DYN_HANDLE_KINDS.has(recv.type.kind)) recv = lowerer.coerceToExpected(recv, DYN); + // A typed array whose checker type collapsed to `any` (a JS binding + // referenced in its own initializer's inference — hono's etag digest + // `const nextChunk = new Uint8Array(new ArrayBuffer(f(chunk)))` beside + // `chunk = nextChunk`) still lowered to its native bytes. The typed + // lowerings key on the checker type and decline it, so it boxes by + // reference (the box shares the native storage, so writes through the + // call stay visible) and the call dispatches on the runtime kind. + if (checkerUntyped && recv.type.kind === "bytes") recv = lowerer.coerceToExpected(recv, DYN); // The last-resort retry (boxTypedTemporary — every other lowering // already declined): a checker-`any` receiver whose value is a FRESH // typed result of an earlier step in the chain (`out.split("\n")` diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-npm-any-bytes-receiver/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-npm-any-bytes-receiver/main.ts.json new file mode 100644 index 000000000..89db88211 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-npm-any-bytes-receiver/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7748-npm-any-bytes-receiver/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7748-npm-any-bytes-receiver/main.ts b/tests/corpus/7748-npm-any-bytes-receiver/main.ts new file mode 100644 index 000000000..58301765a --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/main.ts @@ -0,0 +1,33 @@ +// Typed arrays the checker types `any` in package JavaScript: hono's etag +// digest (hono/dist/middleware/etag/digest.js) merges multi-chunk bodies +// through `nextChunk.set(...)`, where `nextChunk`'s initializer depends on +// `chunk`, which is assigned `nextChunk`. Method calls and property reads on +// such values run on the native bytes, as in Node. +import { generateDigest } from "etag-digest"; +import { grow } from "chunk-buffers"; + +const enc = new TextEncoder(); +const sha1 = (body: Uint8Array): Promise => crypto.subtle.digest({ name: "SHA-1" }, body); + +function chunked(parts: string[]): ReadableStream { + let i = 0; + return new ReadableStream({ + pull(controller): void { + if (i < parts.length) controller.enqueue(enc.encode(parts[i++]!)); + else controller.close(); + }, + }); +} + +async function main(): Promise { + console.log("single", await generateDigest(chunked([JSON.stringify({ books: 400 })]), sha1)); + console.log("chunked", await generateDigest(chunked(["alpha\n", "beta\n", "gamma\n", "delta\n"]), sha1)); + const large = ["x".repeat(200000), "y".repeat(100000), "z".repeat(5)]; + console.log("large", await generateDigest(chunked(large), sha1)); + console.log("exact", await generateDigest(chunked(["a".repeat(262144), "b"]), sha1)); + console.log("empty", await generateDigest(chunked([]), sha1)); + console.log("null", await generateDigest(null, sha1)); + console.log(grow([new Uint8Array([1, 2, 3]), new Uint8Array([4]), new Uint8Array([5])])); +} + +await main(); diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.d.ts b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.d.ts new file mode 100644 index 000000000..26fe364a0 --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.d.ts @@ -0,0 +1 @@ +export declare function grow(parts: Uint8Array[]): Uint8Array | undefined; diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.js b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.js new file mode 100644 index 000000000..a0a1fea79 --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/index.js @@ -0,0 +1,18 @@ +// `next` is referenced by its own initializer's inference (through +// `chunk = next`), so the checker types it `any` while it is a Uint8Array. +export function grow(parts) { + let chunk; + for (const value of parts) { + if (!chunk) chunk = value.slice(0); + else { + const next = new Uint8Array(new ArrayBuffer(Math.max(8, chunk.byteLength * 2))); + next.set(chunk.subarray(0, 2)); + next.set([9], 2); + next[3] = next.byteLength; + console.log(next.length, next[0], next.subarray(1, 3), next.at(-1), next.byteOffset); + console.log(next.slice(2, 4), next.buffer.byteLength, next.BYTES_PER_ELEMENT); + chunk = next; + } + } + return chunk; +} diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/package.json b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/package.json new file mode 100644 index 000000000..74e354d34 --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/chunk-buffers/package.json @@ -0,0 +1 @@ +{ "name": "chunk-buffers", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.d.ts b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.d.ts new file mode 100644 index 000000000..79d9dc396 --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.d.ts @@ -0,0 +1 @@ +export declare const generateDigest: (stream: ReadableStream | null, generator: (body: Uint8Array) => ArrayBuffer | Promise) => Promise; diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.js b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.js new file mode 100644 index 000000000..bf438a5ea --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/index.js @@ -0,0 +1,60 @@ +// hono/dist/middleware/etag/digest.js (hono 4.13.13) +const mergeBuffers = (buffer1, buffer2) => { + if (!buffer1) return buffer2; + const merged = new Uint8Array(new ArrayBuffer(buffer1.byteLength + buffer2.byteLength)); + merged.set(new Uint8Array(buffer1), 0); + merged.set(buffer2, buffer1.byteLength); + return merged; +}; +const CHUNK_SIZE = 262144; +const generateDigest = async (stream, generator) => { + if (!stream) return null; + let result = void 0; + let chunk; + let chunkLength = 0; + const digest = async (body) => { + result = await generator(mergeBuffers(result, body)); + }; + const reader = stream.getReader(); + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + let offset = 0; + while (offset < value.byteLength) { + const remaining = value.byteLength - offset; + if (chunkLength === 0 && remaining >= CHUNK_SIZE) { + await digest(value.subarray(offset, offset + CHUNK_SIZE)); + offset += CHUNK_SIZE; + continue; + } + const requiredLength = chunkLength + remaining; + if (requiredLength < CHUNK_SIZE) { + if (!chunk) chunk = value.slice(offset); + else { + if (chunk.byteLength < requiredLength) { + const nextChunk = new Uint8Array(new ArrayBuffer(Math.min(CHUNK_SIZE, Math.max(requiredLength, chunk.byteLength * 2)))); + nextChunk.set(chunk.subarray(0, chunkLength)); + chunk = nextChunk; + } + chunk.set(value.subarray(offset), chunkLength); + } + chunkLength = requiredLength; + break; + } + const length = CHUNK_SIZE - chunkLength; + if (chunk?.byteLength !== CHUNK_SIZE) { + const nextChunk = new Uint8Array(/* @__PURE__ */ new ArrayBuffer(CHUNK_SIZE)); + if (chunk) nextChunk.set(chunk.subarray(0, chunkLength)); + chunk = nextChunk; + } + chunk.set(value.subarray(offset, offset + length), chunkLength); + await digest(chunk); + chunkLength = 0; + offset += length; + } + } + if (chunk && chunkLength > 0) await digest(chunk.subarray(0, chunkLength)); + if (!result) return null; + return Array.prototype.map.call(new Uint8Array(result), (x) => x.toString(16).padStart(2, "0")).join(""); +}; +export { generateDigest }; diff --git a/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/package.json b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/package.json new file mode 100644 index 000000000..f15495eed --- /dev/null +++ b/tests/corpus/7748-npm-any-bytes-receiver/node_modules/etag-digest/package.json @@ -0,0 +1 @@ +{ "name": "etag-digest", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/harness/corpus-costs/7748-npm-any-bytes-receiver/main.ts.json b/tests/harness/corpus-costs/7748-npm-any-bytes-receiver/main.ts.json new file mode 100644 index 000000000..197263cad --- /dev/null +++ b/tests/harness/corpus-costs/7748-npm-any-bytes-receiver/main.ts.json @@ -0,0 +1 @@ +{ "plain": 4600, "san": 7300 } From f14997776b804884e9d71be36c690c772e44ab93 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 09:52:30 -0700 Subject: [PATCH 139/409] Box TextEncoder and TextDecoder records as checked-dynamic handles A codec stored in untyped storage (hono's StreamingApi keeps `this.encoder = new TextEncoder()` in a JavaScript class field) had no dynamic representation and threw SC1101 at run time. The codec records now cross 'unknown' slots as handles of two new tags that hold the record itself, so identity and a decoder's streaming state survive the round trip, and checked casts unbox the same record. The handles answer encode, encodeInto and decode (with options objects), the encoding, fatal and ignoreBOM properties, method values with Node's ERR_INVALID_THIS, instanceof, String() tags and JSON's {}. The unit scr_text_codec_dyn.c links only into programs that convert a codec to or from an untyped value (the textCodecDyn runtime feature, found with the dgram carrier walk, now shared). Co-Authored-By: Claude Opus 5.5 --- .../builtin-apis/text-encoding.mdx | 1 + .../generated/node-v24-internal.json | 20 +- internal/compatibility/static-support.json | 4 +- .../src/backend/executable-features.ts | 1 + packages/compiler/src/backend/library-pack.ts | 1 + .../src/backend/llvm/dyn-handle-tags.test.ts | 8 +- packages/compiler/src/backend/llvm/dyn.ts | 122 ++++++++++ .../compiler/src/backend/llvm/expr-dynamic.ts | 6 +- .../src/backend/llvm/expr-stream-bridges.ts | 14 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 4 + .../compiler/src/backend/native/executable.ts | 1 + .../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 + .../lowering/expressions/relational.ts | 16 ++ packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/analysis.ts | 7 +- .../compiler/src/ir/builtin-signatures.ts | 4 + packages/compiler/src/ir/ir.ts | 16 ++ packages/compiler/src/ir/runtime-features.ts | 110 +++++++-- packages/compiler/src/native/cli.ts | 1 + packages/compiler/src/native/prepare.ts | 1 + .../compiler/src/native/sanitizer.test.ts | 1 + .../7747-text-codec-untyped-slot.js.json | 6 + .../7749-text-codec-unknown-roundtrip.ts.json | 6 + .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_bytes.h | 12 + packages/runtime/src/scr_dyn.h | 2 + packages/runtime/src/scr_text_codec_dyn.c | 215 ++++++++++++++++++ tests/corpus/7747-text-codec-untyped-slot.js | 69 ++++++ .../7749-text-codec-unknown-roundtrip.ts | 22 ++ .../7747-text-codec-untyped-slot.js.json | 1 + .../7749-text-codec-unknown-roundtrip.ts.json | 1 + .../self-hosting-native-toolchain.test.ts | 1 + .../harness/self-hosting-runtime-pack.test.ts | 1 + 39 files changed, 650 insertions(+), 37 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json create mode 100644 packages/runtime/src/scr_text_codec_dyn.c create mode 100644 tests/corpus/7747-text-codec-untyped-slot.js create mode 100644 tests/corpus/7749-text-codec-unknown-roundtrip.ts create mode 100644 tests/harness/corpus-costs/7747-text-codec-untyped-slot.js.json create mode 100644 tests/harness/corpus-costs/7749-text-codec-unknown-roundtrip.ts.json diff --git a/docs/content/limitations/builtin-apis/text-encoding.mdx b/docs/content/limitations/builtin-apis/text-encoding.mdx index bffd24424..e20cfd943 100644 --- a/docs/content/limitations/builtin-apis/text-encoding.mdx +++ b/docs/content/limitations/builtin-apis/text-encoding.mdx @@ -1,6 +1,7 @@ - **Text encoding:** `TextEncoder` and `TextDecoder` can be stored in fields, arguments, return values, arrays, and closures. Encoding supports UTF-8, including `encodeInto` into a Uint8Array or Buffer view. Codec properties expose the canonical encoding and decoder flags. + Codecs held in `any` or `unknown` storage keep their identity and streaming state, and dynamic code can call `encode`, `encodeInto` and `decode`, with options objects, and read their properties. Decoding accepts recognized WHATWG labels, including runtime labels and omitted or undefined encodings. Literal constructor options support `fatal` and `ignoreBOM`; literal decode options support streaming UTF-8, UTF-16, and single-byte encodings. Other legacy streaming encodings, runtime options objects, inspection, JSON conversion, and module-scope declarations using `var` or lacking an initializer are unsupported. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index ba8692b49..f2b528d7a 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -52194,6 +52194,7 @@ "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/corpus/6331-jose-root-entry/main.ts", + "tests/corpus/7747-text-codec-untyped-slot.js", "tests/corpus/text-codec-values/main.ts", "tests/corpus/webassembly-api.mjs", "tests/corpus/webassembly-dylink/main.mjs", @@ -53346,7 +53347,8 @@ "tests": [ "tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -53397,7 +53399,8 @@ "tests": [ "tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -101277,6 +101280,7 @@ "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/6428-promisify-callback-functions.ts", + "tests/corpus/7747-text-codec-untyped-slot.js", "tests/corpus/node24-deep-equality-properties.js", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality.ts", @@ -101805,7 +101809,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -101926,7 +101931,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -102036,7 +102042,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" @@ -102065,7 +102072,8 @@ "tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", - "tests/corpus/3133-text-decoder-streaming.ts" + "tests/corpus/3133-text-decoder-streaming.ts", + "tests/corpus/7747-text-codec-untyped-slot.js" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 100882c1a..5950be0a5 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -332,11 +332,11 @@ { "chapter": "buffer", "signatures": ["`buf.readBigInt64BE([offset])`", "`buf.readBigInt64LE([offset])`", "`buf.readBigUInt64BE([offset])`", "`buf.readBigUInt64LE([offset])`", "`buf.writeBigInt64BE(value[, offset])`", "`buf.writeBigInt64LE(value[, offset])`", "`buf.writeBigUInt64BE(value[, offset])`", "`buf.writeBigUInt64LE(value[, offset])`"], "status": "supported", "evidence": ["tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"] }, { "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" }, { "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] }, - { "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts"] }, + { "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/7747-text-codec-untyped-slot.js"] }, { "chapter": "globals", "signatures": ["Class: `Blob`", "Class: `File`"], "status": "partial", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5286-blob-readers.ts", "tests/corpus/5287-blob-inspect.ts", "tests/corpus/5288-blob-validation.js", "tests/corpus/5290-blob-dynamic.js"] }, { "chapter": "buffer", "signatures": ["Class: `Blob`", "Class: `File`"], "status": "partial", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5288-blob-validation.js", "tests/fixtures/node-types/blob-file.ts"] }, { "chapter": "buffer", "signatures": ["`blob.arrayBuffer()`", "`blob.bytes()`", "`blob.text()`", "`blob.slice([start[, end[, type]]])`", "`blob.size`", "`type` Type: {string}", "`name` Type: {string}", "`lastModified` Type: {number}"], "status": "supported", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5286-blob-readers.ts", "tests/corpus/5288-blob-validation.js", "tests/corpus/5289-blob-read-order.ts", "tests/corpus/5290-blob-dynamic.js", "tests/fixtures/node-types/blob-file.ts"] }, - { "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts"] }, + { "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/7747-text-codec-untyped-slot.js"] }, { "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", "tests/corpus/2880-cjs-module-graph/main.cjs", "tests/corpus/2923-cjs-factory-export/main.cjs"] }, { "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths"], "status": "partial", "evidence": ["tests/corpus/2858-import-meta-resolve/main.mjs", "tests/corpus/2860-require-resolve/main.cjs"] }, { "chapter": "modules", "symbols": ["cache", "children", "id", "isPreloading", "loaded", "parent", "path", "paths", "Module"], "status": "partial", "evidence": ["tests/corpus/2880-cjs-module-graph/main.cjs"] }, diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 32318d53e..a9076af22 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -50,6 +50,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { dgram: features.dgram, dnsPromises: features.dnsPromises, dgramDyn: features.dgramDyn, + textCodecDyn: features.textCodecDyn, watch: features.fsWatch, foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []), nodeTest: features.nodeTest, diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index e9f52f7a0..79be316db 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -54,6 +54,7 @@ export async function compilePackedLibrary( dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts index c015b85d0..8c3e5de80 100644 --- a/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts +++ b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import { expect, test } from "vitest"; import { DYN_HANDLE_KINDS } from "../../ir/ir.js"; -import { DYN_HANDLE_TAG_NUM } from "./dyn.js"; +import { DYN_HANDLE_TAG_NUM, TEXT_CODEC_TAG_NUM } from "./dyn.js"; // SCR_DYNH_TAGS in scr_dyn.h lists the ScrDynHandleTag values by name, each // with an explicit value, so changes that add tags edit different lines. The @@ -39,3 +39,9 @@ test("the backend's handle tag numbers match scr_dyn.h", () => { expect(values.get(tag!), `${kind} (${tag})`).toBe(num); } }); + +test("the backend's codec handle tag numbers match scr_dyn.h", () => { + const values = new Map(tags.map((tag) => [tag.name, tag.value])); + expect(values.get("SCR_DYNH_TEXT_DECODER")).toBe(TEXT_CODEC_TAG_NUM.TextDecoder); + expect(values.get("SCR_DYNH_TEXT_ENCODER")).toBe(TEXT_CODEC_TAG_NUM.TextEncoder); +}); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 86626d304..3987c52c2 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -43,6 +43,8 @@ import { isDynTypedRefType, isRefCounted, isUnitType, + recordTextCodecClass, + textCodecRecordClass, typeKey, } from "../../ir/ir.js"; import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js"; @@ -94,6 +96,17 @@ export const DYN_HANDLE_TAG_NUM: Record = { cryptoHmac: 53, }; +/** The handle tags of TextEncoder and TextDecoder records boxed as dyn + * (scr_dyn.h; scr_text_codec_dyn.c boxes and unboxes them). */ +export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder", number> = { + TextDecoder: 76, + TextEncoder: 77, +}; + +/** The decoder record fields scr_text_decoder_to_dyn reads, in its + * parameter order. */ +const TEXT_DECODER_FIELDS = ["%TextDecoder", "%fatal", "%ignoreBOM", "%state"] as const; + /** Handle kinds whose runtime unit boxes them through its own function, * which installs the tag's ops on first use (scr_crypto.h). Programs that * never box one of these values do not link its dynamic members. */ @@ -927,6 +940,21 @@ export class LlDyn { const name = `sc_dm_${this.dynMatchers.size}`; this.dynMatchers.set(matcherKey, name); this.host.journalAllocated?.(name, "dyn.dynMatchHelper", [t, preserveRefs]); + const codec = textCodecRecordClass(t, (id) => this.host.recordsById.get(id)); + if (codec !== null) { + // A codec matches only its own handle box. + const B = new BlockBuilder(); + const matched = this.handleTagMatches(B, TEXT_CODEC_TAG_NUM[codec]); + B.terminate(`ret i1 ${matched}`); + this.defs.push( + `define internal zeroext i1 @${name}(ptr %d) ${FN_ATTRS} { ; matches ${key}`, + B.render(), + `}`, + ``, + ); + this.host.journalBuilt?.(); + return name; + } const recursive = this.isRecursiveShape(t); const wrapped = recursive || this.holdsRecursiveShape(t); const B = new BlockBuilder(); @@ -1643,6 +1671,24 @@ export class LlDyn { this.host.journalBuilt?.(); return name; } + const codec = textCodecRecordClass(t, (id) => host.recordsById.get(id)); + if (codec !== null) { + // The record the codec's handle box holds (+1), or the path TypeError. + host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`); + const checked = B.tmp(); + B.line( + `${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`, + ); + B.terminate(`ret ptr ${checked}`); + this.defs.push( + `define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`, + B.render(), + `}`, + ``, + ); + this.host.journalBuilt?.(); + return name; + } if (isRefCounted(t) && t.kind !== "dyn") { host.declare(`declare ptr @scr_dyn_typed_ref_unbox(ptr)`); if (t.kind === "object" && t.className === "%Error") { @@ -2671,6 +2717,77 @@ export class LlDyn { return name; } + /** `i1`: %d is a handle box with tag `tag`. */ + private handleTagMatches(B: BlockBuilder, tag: number): string { + const kd = this.kindOf(B, "%d"); + const isHandle = B.tmp(); + B.line(`${isHandle} = icmp eq i32 ${kd}, ${DYN_KIND.HANDLE}`); + const lHandle = B.newLabel("dm.handle"); + const lDone = B.newLabel("dm.handle.done"); + const entry = B.currentLabel(); + B.condBr(isHandle, lHandle, lDone); + B.startBlock(lHandle); + const tagPtr = B.tmp(); + const loaded = B.tmp(); + B.line( + `${tagPtr} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(24, 20)} ; ->v.handle.tag`, + ); + B.line(`${loaded} = load i32, ptr ${tagPtr}`); + const same = B.tmp(); + B.line(`${same} = icmp eq i32 ${loaded}, ${tag}`); + B.br(lDone); + B.startBlock(lDone); + const matched = B.tmp(); + B.line(`${matched} = phi i1 [ false, %${entry} ], [ ${same}, %${lHandle} ]`); + return matched; + } + + /** A TextEncoder or TextDecoder record boxes as its handle (+1), which + * holds the record by reference. A decoder passes the byte offsets of the + * fields its boxed decode reads. */ + private textCodecToDyn( + B: BlockBuilder, + t: IrType & { kind: "record" }, + shape: IrRecordShape, + codec: "TextEncoder" | "TextDecoder", + ): void { + const host = this.host; + const rc = vAdapters(host, t); + const r = B.tmp(); + if (codec === "TextEncoder") { + host.declare(`declare ptr @scr_text_encoder_to_dyn(ptr, ptr, ptr)`); + B.line( + `${r} = call ptr @scr_text_encoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release})`, + ); + } else { + const struct = mangleRecordStruct(t.shapeId); + const base = host.nullableUnions.fieldBase(t.shapeId); + const start = B.tmp(); + B.line(`${start} = ptrtoint ptr %v to ${this.S}`); + const offsets = TEXT_DECODER_FIELDS.map((field) => { + const index = shape.fields.findIndex((f) => f.name === field); + if (index < 0) + throw new InternalCompilerError(`llvm emitter bug: TextDecoder record lacks ${field}`); + const p = B.tmp(); + const at = B.tmp(); + const offset = B.tmp(); + B.line( + `${p} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${index + base} ; .${llvmCommentText(field)}`, + ); + B.line(`${at} = ptrtoint ptr ${p} to ${this.S}`); + B.line(`${offset} = sub ${this.S} ${at}, ${start}`); + return `${this.S} ${offset}`; + }); + host.declare( + `declare ptr @scr_text_decoder_to_dyn(ptr, ptr, ptr, ${this.S}, ${this.S}, ${this.S}, ${this.S})`, + ); + B.line( + `${r} = call ptr @scr_text_decoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`, + ); + } + B.terminate(`ret ptr ${r}`); + } + /* ── toDynHelper (walkers.ts, ported) ─────────────────────────── */ /** `sc_td_( v) -> ptr` — build a fresh dyn value from a @@ -2846,6 +2963,11 @@ export class LlDyn { const shape = host.recordsById.get(t.shapeId); if (!shape) throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`); + const codec = recordTextCodecClass(shape); + if (codec !== null) { + this.textCodecToDyn(B, t, shape, codec); + break; + } const struct = mangleRecordStruct(t.shapeId); const base = host.nullableUnions.fieldBase(t.shapeId); const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base])); diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 07a538184..26905a7ef 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -18,6 +18,7 @@ import { type IrExpr, type IrType, isDynTypedRefType, + textCodecRecordClass, isRefCounted, isUnitType, typeEquals, @@ -86,7 +87,10 @@ export function emitDynamicExpr( // Bytes already box their shared mutable storage directly. A second // capsule would split identity between checked and native views. const errorRef = v.type.kind === "object" && host.shapeHost.isErrorClass(v.type.className); - if (!errorRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) { + // A TextEncoder or TextDecoder record boxes as its own handle, which + // keeps the record's identity (toDynHelper). + const codecRef = textCodecRecordClass(v.type, (id) => host.recordsById.get(id)) !== null; + if (!errorRef && !codecRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) { if (v.type.kind === "union") { const adapter = host.liveDynUnionRefAdapter(v.type); const boxed = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index aee7be180..a2f7208ec 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -16,6 +16,7 @@ import { KEY_ORDER_FIELD, shapeKeyOrders, isDynTypedRefType, + textCodecRecordClass, isRefCounted, shapeHasAccessorSlots, typeKey, @@ -597,12 +598,21 @@ export function liveDynUnionRefAdapter( if (!union) { throw new InternalCompilerError(`llvm emitter bug: live dyn ref of unknown union ${t.unionId}`); } + // Codec records box as their own handles, by reference, through toDynHelper. + const codecArm = (arm: IrType): boolean => + textCodecRecordClass(arm, (id) => host.recordsById.get(id)) !== null; const mutableArms = union.arms .map((arm, tag) => ({ arm, tag })) .filter( - ({ arm }) => arm.kind !== "bytes" && (streamTypedRefEligible(arm) || isDynTypedRefType(arm)), + ({ arm }) => + arm.kind !== "bytes" && + !codecArm(arm) && + (streamTypedRefEligible(arm) || isDynTypedRefType(arm)), ); - if (mutableArms.length === 0 && !union.arms.some((arm) => arm.kind === "bytes")) { + if ( + mutableArms.length === 0 && + !union.arms.some((arm) => arm.kind === "bytes" || codecArm(arm)) + ) { throw new InternalCompilerError(`llvm emitter bug: live dyn ref of immutable union ${key}`); } diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index af32c6936..7b411f2bb 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -560,6 +560,7 @@ export const LIB_FN_SYMS: Record = { "text.decoderEncoding": "scr_text_decoder_encoding", "text.decoderName": "scr_text_decoder_name", "text.encodeInto": "scr_text_encode_into", + "text.codecIs": "scr_text_codec_is", "buffer.isAscii": "scr_buffer_is_ascii", "buffer.isUtf8": "scr_buffer_is_utf8", "buffer.transcode": "scr_buffer_transcode", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index e018037fd..c073bb9a6 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -81,6 +81,9 @@ export interface NativeLinkFeatures { dnsPromises: boolean; /** dgram sockets as untyped values: scr_dgram_dyn.c. */ dgramDyn: boolean; + /** TextEncoder and TextDecoder records as untyped values: + * scr_text_codec_dyn.c. */ + textCodecDyn: boolean; watch: boolean; foreignFfi: boolean; nodeTest: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 61de82e72..53e160984 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -220,6 +220,10 @@ export interface CcOptions { * scr_dgram_dyn.c over the dgram unit, so typed dgram binaries keep * their exact link line. */ dgramDyn?: boolean; + /** A TextEncoder or TextDecoder record converts to or from an untyped + * value: compiles scr_text_codec_dyn.c, so typed codec binaries keep + * their exact link line. */ + textCodecDyn?: boolean; /** The program uses fs.watch (moduleUsesFsWatch on the IR): compiles * scr_watch.c into the binary — the net gating precedent, so watch-free diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 233b9cac2..1578a0c80 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -302,6 +302,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.dgram, "scr_dgram.c"), unit((u) => u.opts.dgram && u.opts.dnsPromises, "scr_dns_promises.c"), 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"), unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"), unit((u) => u.opts.nodeTest, "scr_test.c"), diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 44c3d6290..9846e8d89 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -118,6 +118,7 @@ const features = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index e3bc999a7..e2b24f8ca 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -54,6 +54,7 @@ const BASE: NativeLinkFeatures = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index b48cf6739..02acb5462 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -61,6 +61,7 @@ export async function warmNativeCaches( dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 5cf8c6588..e066c0412 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -58,6 +58,7 @@ const native: EarlyExecutableNativeFeatures = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 7b7febc06..96922ca13 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -92,6 +92,7 @@ export interface EarlyExecutableNativeFeatures { dgram: boolean; dnsPromises: boolean; dgramDyn: boolean; + textCodecDyn: boolean; watch: boolean; foreignFfi: boolean; nodeTest: boolean; @@ -235,6 +236,7 @@ const BOOLEAN_NATIVE_KEYS = [ "dgram", "dnsPromises", "dgramDyn", + "textCodecDyn", "watch", "foreignFfi", "nodeTest", diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index f1590e9cc..1651bf0d9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -338,6 +338,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc type: BOOL, loc, }; + // A codec in untyped storage is its record's handle box. + const codec = (["TextEncoder", "TextDecoder"] as const).find((name) => + lowerer.isStdlibGlobal(expr.right, name), + ); + if (codec && lowerer.mapTypeOf(lowerer.typeOf(expr.left))?.kind === "dyn") { + return { + kind: "libCall", + fn: "text.codecIs", + args: [ + lowerer.lowerExprExpecting(expr.left, DYN), + { kind: "boolLit", value: codec === "TextDecoder", type: BOOL, loc }, + ], + type: BOOL, + loc, + }; + } if (lowerer.isStdlibGlobal(expr.right, "ReadableStream")) { const value = lowerer.lowerExprExpecting(expr.left, DYN); return { kind: "libCall", fn: "fetch.streamIs", args: [value], type: BOOL, loc }; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 4f7b93cfd..ae93598d6 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -623,6 +623,7 @@ async function compileExecutableNative( dgram: features.dgram, dnsPromises: features.dnsPromises, dgramDyn: features.dgramDyn, + textCodecDyn: features.textCodecDyn, watch: features.watch, foreignFfi: features.foreignFfi, ...(features.workers ? { workers: true } : {}), diff --git a/packages/compiler/src/ir/analysis.ts b/packages/compiler/src/ir/analysis.ts index 70ec381a1..ee1cee6be 100644 --- a/packages/compiler/src/ir/analysis.ts +++ b/packages/compiler/src/ir/analysis.ts @@ -4,6 +4,7 @@ import { BYTES_ELEMENT_NAME, DYN_HANDLE_KINDS, RUNTIME_STREAM_CLASSES, + recordTextCodecClass, typeEquals, type IrExpr, type IrLibFn, @@ -141,8 +142,10 @@ export function dynDesc( return "string"; case "bool": return "boolean"; - case "record": - return recordsById.get(t.shapeId)?.tuple ? "array" : "object"; + case "record": { + const shape = recordsById.get(t.shapeId); + return shape?.tuple ? "array" : ((shape && recordTextCodecClass(shape)) ?? "object"); + } case "array": return "array"; case "nullT": diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 1be0f95cd..9f5b3334f 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4852,6 +4852,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "text.decoderEncoding": { argTypes: [DYN], result: F64 }, "text.decoderName": { argTypes: [F64], result: STRING }, "text.encodeInto": { argTypes: [DYN, DYN], result: DYN }, + /** `value instanceof TextDecoder` (true) or `TextEncoder` (false) on a + * checked-dynamic value: a codec record's handle box (scr_text_codec_dyn.c). + * Borrowed; never throws. */ + "text.codecIs": { argTypes: [DYN, BOOL], result: BOOL }, "buffer.isAscii": { argTypes: [DYN], result: BOOL }, "buffer.isUtf8": { argTypes: [DYN], result: BOOL }, "buffer.transcode": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 }, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 878088d65..29dda102d 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1745,6 +1745,18 @@ export function recordTextCodecClass(shape: IrRecordShape): "TextEncoder" | "Tex return null; } +/** The codec class of a TextEncoder or TextDecoder record type, else null. + * Such a record crosses an `unknown` slot as a native handle that holds the + * record itself (scr_text_codec_dyn.c), never as a copy or a field view. */ +export function textCodecRecordClass( + t: IrType, + getRecord: (shapeId: string) => IrRecordShape | undefined, +): "TextEncoder" | "TextDecoder" | null { + if (t.kind !== "record") return null; + const shape = getRecord(t.shapeId); + return shape ? recordTextCodecClass(shape) : null; +} + /** Object-literal ACCESSOR properties (`{ get x() {...}, set x(v) {...} }`) * live on the shape as reserved '%'-fields holding closures: `%get:x` a * `() => T` invoked once per property READ (side effects and all — JS's @@ -4273,6 +4285,7 @@ export function canConvertToDyn( visiting: Set = new Set(), ): boolean { if (isJsonSafeType(t, getRecord, getUnion)) return true; + if (textCodecRecordClass(t, getRecord) !== null) return true; // numeric typed arrays and boxable functions are dyn kinds the walker boxes // ANYWHERE (bytes and functions held by identity), including nested // in records/arrays/unions. isJsonSafeType rejects them, but dynFrom @@ -4319,6 +4332,7 @@ export function canConvertToDyn( // either its result or a promise of it. canBoxDynComposite(a, getRecord, getUnion, visiting) || isDynTypedRefType(a) || + textCodecRecordClass(a, getRecord) !== null || a.kind === "classval" || DYN_HANDLE_KINDS.has(a.kind) || (a.kind === "func" && canBoxFuncIntoDyn(a, getRecord, getUnion, visiting)) || @@ -4499,6 +4513,8 @@ function canDynCheckToAt( visiting.delete(key); return result; } + // A codec handle unboxes to the record it holds. + if (textCodecRecordClass(t, getRecord) !== null) return true; if (t.kind === "record") { const key = typeKey(t); if (visiting.has(key)) return false; diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 4ad3dcc5c..e1e7e28be 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -2,6 +2,7 @@ import type { IrClassDef, IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } import type { IrLibFn } from "./builtin-signatures.js"; import { isDirentShape, + recordTextCodecClass, RUNTIME_EMITTER_CLASS, RUNTIME_EVENT_CLASS, RUNTIME_EVENT_TARGET_CLASS, @@ -415,7 +416,10 @@ interface TypeTables { unions: Map; classes: Map; /** mentionsDgramSocket's carriers, computed on first use. */ - dgramCarriers?: DgramCarriers; + dgramCarriers?: TypeCarriers; + /** mentionsTextCodec's and mentionsTextDecoder's carriers. */ + textCodecCarriers?: TypeCarriers; + textDecoderCarriers?: TypeCarriers; } /** Whether boxing a value of type `t` as dyn boxes a promise, which goes @@ -479,30 +483,42 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set): boolean } } -/** The record shapes, unions and classes whose values carry a dgram socket - * (directly or through other members), computed once per module. */ -interface DgramCarriers { +/** The record shapes, unions and classes whose values carry a leaf type + * (directly or through other members), computed once per module and leaf. */ +interface TypeCarriers { records: Set; unions: Set; anyClass: boolean; } -/** Whether `t` carries a dgram socket, given the carrier sets so far: - * structural through functions (parameters and result), promises and - * collections; named records, unions and class instances by the sets. */ -function carriesDgram(t: IrType, carriers: DgramCarriers): boolean { +/** The leaf a carrier set tracks: a dgram socket, a codec record. */ +type CarrierLeaf = (t: IrType, tables: TypeTables) => boolean; + +/** Whether `t` carries a leaf, given the carrier sets so far: structural + * through functions (parameters and result), promises and collections; + * named records, unions and class instances by the sets. */ +function carriesLeaf( + t: IrType, + carriers: TypeCarriers, + leaf: CarrierLeaf, + tables: TypeTables, +): boolean { + if (leaf(t, tables)) return true; switch (t.kind) { - case "dgramSocket": - return true; case "func": - return t.params.some((p) => carriesDgram(p, carriers)) || carriesDgram(t.ret, carriers); + return ( + t.params.some((p) => carriesLeaf(p, carriers, leaf, tables)) || + carriesLeaf(t.ret, carriers, leaf, tables) + ); case "promise": - return carriesDgram(t.inner, carriers); + return carriesLeaf(t.inner, carriers, leaf, tables); case "array": case "set": - return carriesDgram(t.elem, carriers); + return carriesLeaf(t.elem, carriers, leaf, tables); case "map": - return carriesDgram(t.key, carriers) || carriesDgram(t.value, carriers); + return ( + carriesLeaf(t.key, carriers, leaf, tables) || carriesLeaf(t.value, carriers, leaf, tables) + ); case "record": return carriers.records.has(t.shapeId); case "union": @@ -515,25 +531,29 @@ function carriesDgram(t: IrType, carriers: DgramCarriers): boolean { } } -/** The module's dgram carriers: a fixpoint over its records, unions and +/** The module's carriers of `leaf`: a fixpoint over its records, unions and * class fields. */ -function dgramCarriers(tables: TypeTables): DgramCarriers { - const carriers: DgramCarriers = { records: new Set(), unions: new Set(), anyClass: false }; +function leafCarriers(tables: TypeTables, leaf: CarrierLeaf): TypeCarriers { + const carriers: TypeCarriers = { records: new Set(), unions: new Set(), anyClass: false }; + const carries = (t: IrType): boolean => carriesLeaf(t, carriers, leaf, tables); for (let changed = true; changed;) { changed = false; for (const [id, shape] of tables.records) - if (!carriers.records.has(id) && shape.fields.some((f) => carriesDgram(f.type, carriers))) { + if ( + !carriers.records.has(id) && + (leaf({ kind: "record", shapeId: id }, tables) || shape.fields.some((f) => carries(f.type))) + ) { carriers.records.add(id); changed = true; } for (const [id, def] of tables.unions) - if (!carriers.unions.has(id) && def.arms.some((a) => carriesDgram(a, carriers))) { + if (!carriers.unions.has(id) && def.arms.some(carries)) { carriers.unions.add(id); changed = true; } if ( !carriers.anyClass && - [...tables.classes.values()].some((c) => c.fields.some((f) => carriesDgram(f.type, carriers))) + [...tables.classes.values()].some((c) => c.fields.some((f) => carries(f.type))) ) { carriers.anyClass = true; changed = true; @@ -542,13 +562,42 @@ function dgramCarriers(tables: TypeTables): DgramCarriers { return carriers; } +const dgramLeaf: CarrierLeaf = (t) => t.kind === "dgramSocket"; + /** Whether converting a value of type `t` to or from dyn converts a dgram * socket, which goes through scr_dgram_dyn.c: the socket itself, or a * function, collection, record, union, promise or class instance that * carries one (boxed functions convert their parameters too). */ function mentionsDgramSocket(t: IrType, tables: TypeTables): boolean { - tables.dgramCarriers ??= dgramCarriers(tables); - return carriesDgram(t, tables.dgramCarriers); + tables.dgramCarriers ??= leafCarriers(tables, dgramLeaf); + return carriesLeaf(t, tables.dgramCarriers, dgramLeaf, tables); +} + +const textCodecLeaf: CarrierLeaf = (t, tables) => { + if (t.kind !== "record") return false; + const shape = tables.records.get(t.shapeId); + return shape !== undefined && recordTextCodecClass(shape) !== null; +}; + +const textDecoderLeaf: CarrierLeaf = (t, tables) => { + if (t.kind !== "record") return false; + const shape = tables.records.get(t.shapeId); + return shape !== undefined && recordTextCodecClass(shape) === "TextDecoder"; +}; + +/** Whether converting a value of type `t` to or from dyn converts a + * TextEncoder or TextDecoder record, which boxes it as a handle of + * scr_text_codec_dyn.c (the same carrier walk as dgram sockets). */ +function mentionsTextCodec(t: IrType, tables: TypeTables): boolean { + tables.textCodecCarriers ??= leafCarriers(tables, textCodecLeaf); + return carriesLeaf(t, tables.textCodecCarriers, textCodecLeaf, tables); +} + +/** The TextDecoder half: its boxed decode runs scr_text_decode_stream, + * which reaches the legacy decoders. */ +function mentionsTextDecoder(t: IrType, tables: TypeTables): boolean { + tables.textDecoderCarriers ??= leafCarriers(tables, textDecoderLeaf); + return carriesLeaf(t, tables.textDecoderCarriers, textDecoderLeaf, tables); } /** The libCall fns served by the OPTIONAL events unit (scr_events.c): @@ -794,6 +843,15 @@ const RUNTIME_FEATURE_RULES = { (node.kind === "dynFrom" && mentionsDgramSocket(node.value.type, tables())) || (node.kind === "dynCheck" && mentionsDgramSocket(node.type, tables())), }), + /** A TextEncoder or TextDecoder record converts to or from an untyped + * value (its handle box, or a boxed function, record or collection that + * carries one): scr_text_codec_dyn.c. */ + textCodecDyn: rule({ + fns: ["text.codecIs"], + expr: (node, tables) => + (node.kind === "dynFrom" && mentionsTextCodec(node.value.type, tables())) || + (node.kind === "dynCheck" && mentionsTextCodec(node.type, tables())), + }), /** node:dns/promises: scr_dns_promises.c over the dgram unit. */ dnsPromises: rule({ fns: ["dns.promisesLookup", "dns.promisesLookupService"] }), dynAsync: rule({ @@ -942,7 +1000,12 @@ const RUNTIME_FEATURE_RULES = { prefixes: ["insp.", "console.", "test."], libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!), }), - legacyTextDecoder: rule({ fns: [...LEGACY_TEXT_DECODER_LIB_FNS] }), + legacyTextDecoder: rule({ + fns: [...LEGACY_TEXT_DECODER_LIB_FNS], + expr: (node, tables) => + (node.kind === "dynFrom" && mentionsTextDecoder(node.value.type, tables())) || + (node.kind === "dynCheck" && mentionsTextDecoder(node.type, tables())), + }), /** The program calls mock.timers members: its timer calls go through * scr_test.c's mock-aware entry points. */ mockTimers: rule({ prefixes: ["test.mockTimers", "test.ctxMockTimers"] }), @@ -1056,6 +1119,7 @@ function noRuntimeFeatures(): RuntimeFeatures { dc: false, dgram: false, dgramDyn: false, + textCodecDyn: false, dnsPromises: false, dynAsync: false, dynBuiltinClass: false, diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index 21047b929..a35ce811f 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -122,6 +122,7 @@ async function main(): Promise { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index f7f399f80..a238029dc 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -239,6 +239,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "dgram", "dnsPromises", "dgramDyn", + "textCodecDyn", "watch", "foreignFfi", "nodeTest", diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 4c031a8e7..33a4c19b9 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -114,6 +114,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json new file mode 100644 index 000000000..23efb7db6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-text-codec-untyped-slot.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7747-text-codec-untyped-slot.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json new file mode 100644 index 000000000..dd9f068f3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-text-codec-unknown-roundtrip.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7749-text-codec-unknown-roundtrip.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 89b6eaf19..0ab467236 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -148,6 +148,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_dns_promises.c", all("dgram", "dnsPromises")], // Sockets as untyped values run over the dgram unit. ["scr_dgram_dyn.c", all("dgram", "dgramDyn")], + // TextEncoder and TextDecoder records boxed as untyped values. + ["scr_text_codec_dyn.c", "textCodecDyn"], ["scr_watch.c", "watch"], ["scr_ffi_queue.c", "foreignFfi"], ["scr_test.c", "nodeTest"], diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 9b382dda5..2e4b44409 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -357,6 +357,18 @@ ScrStr *scr_text_decode(const ScrBytes *b); double scr_text_decoder_encoding(const ScrDyn *label); ScrStr *scr_text_decoder_name(double encoding); ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination); +/* TextEncoder and TextDecoder records as checked-dynamic handles + * (scr_text_codec_dyn.c, linked by the textCodecDyn runtime feature). The + * boxes borrow the record and retain it through `retain`; a decoder also + * names the byte offsets of its encoding (f64), fatal and ignoreBOM (i8) + * and state (dyn) fields. from_dyn answers the retained record or throws + * the path-annotated TypeError and answers NULL. */ +ScrDyn *scr_text_encoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *)); +ScrDyn *scr_text_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *), + size_t encoding, size_t fatal, size_t ignore_bom, size_t state); +void *scr_text_codec_from_dyn(const ScrDyn *d, bool decoder, const struct ScrDynPath *path, + const char *want); +bool scr_text_codec_is(const ScrDyn *d, bool decoder); bool scr_buffer_is_ascii(const ScrDyn *input); bool scr_buffer_is_utf8(const ScrDyn *input); ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const ScrDyn *to); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 6f0c28371..740e33a09 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -200,6 +200,8 @@ typedef enum { X(SHARED_ARRAY_BUFFER, 37) \ X(SIGN, 67) /* node:crypto Sign (scr_pki.c) */ \ X(STDIO, 27) /* stable process stdin/stdout/stderr values */ \ + X(TEXT_DECODER, 76) /* compiled TextDecoder record (scr_text_codec_dyn.c) */ \ + X(TEXT_ENCODER, 77) /* compiled TextEncoder record (scr_text_codec_dyn.c) */ \ X(URL, 28) /* native WHATWG URL */ \ X(VERIFY, 68) /* node:crypto Verify (scr_pki.c) */ \ X(WASM_GLOBAL, 61) /* WebAssembly.Global */ \ diff --git a/packages/runtime/src/scr_text_codec_dyn.c b/packages/runtime/src/scr_text_codec_dyn.c new file mode 100644 index 000000000..5102bc558 --- /dev/null +++ b/packages/runtime/src/scr_text_codec_dyn.c @@ -0,0 +1,215 @@ +/* WHATWG TextEncoder and TextDecoder instances as checked-dynamic values + * (SCR_DYNH_TEXT_ENCODER, SCR_DYNH_TEXT_DECODER). A codec is a compiled + * record (type-mapper.ts): the handle holds that record by reference, so + * a codec stored in an untyped slot and read back keeps its identity and + * a decoder keeps its streaming state. The compiler passes the record's + * reference-count entry points and, for decoders, the byte offsets of the + * encoding, fatal, ignoreBOM and state fields when it boxes one. A unit + * of its own so programs that never box a codec link none of it + * (native-toolchain gating: the textCodecDyn runtime feature). */ +#include "scr_platform.h" +#include "scr_runtime.h" + +#include + +typedef struct { + void *(*retain)(void *); + void (*release)(void *); + size_t encoding, fatal, ignore_bom, state; +} ScrTextCodecLayout; + +/* A program has one record shape per codec class, so one layout each. */ +static SCR_TL ScrTextCodecLayout scr_tcd_encoder_layout, scr_tcd_decoder_layout; + +static void *scr_tcd_encoder_retain(void *rec) { return scr_tcd_encoder_layout.retain(rec); } +static void scr_tcd_encoder_release(void *rec) { scr_tcd_encoder_layout.release(rec); } +static void *scr_tcd_decoder_retain(void *rec) { return scr_tcd_decoder_layout.retain(rec); } +static void scr_tcd_decoder_release(void *rec) { scr_tcd_decoder_layout.release(rec); } + +static void scr_tcd_codec_not_a_function(const char *what) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); +} + +/* Boxes an owned string (+1 box; the string's reference moves in). */ +static ScrDyn *scr_tcd_str(ScrStr *text) { + ScrDyn *boxed = scr_dyn_new_str(text); + scr_str_release(text); + return boxed; +} + +static ScrDyn *scr_tcd_codec_arg(ScrDyn *const *args, size_t argc, size_t index) { + return index < argc ? args[index] : scr_dyn_undefined(); +} + +static double scr_tcd_decoder_encoding(const void *rec) { + double encoding; + memcpy(&encoding, (const char *)rec + scr_tcd_decoder_layout.encoding, sizeof encoding); + return encoding; +} + +static bool scr_tcd_decoder_flag(const void *rec, size_t offset) { + return ((const unsigned char *)rec)[offset] != 0; +} + +static ScrDyn *scr_tcd_encoder_invoke(void *rec, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + (void)rec; + (void)self; + if (strcmp(method, "encode") == 0) { + /* encode(input = ""): any other input converts with ToString. */ + ScrDyn *input = scr_tcd_codec_arg(args, argc, 0); + ScrStr *text = + input->kind == SCR_DYN_UNDEF ? scr_str_new("", 0) : scr_dyn_string_coerce_js(input); + if (!text) return NULL; + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *bytes = scr_bytes_from_str(text, utf8); + scr_str_release(utf8); + scr_str_release(text); + /* A fresh Uint8Array, not a Buffer. */ + bytes->is_buffer = false; + ScrDyn *result = scr_dyn_new_bytes(bytes); + scr_bytes_release(bytes); + return result; + } + if (strcmp(method, "encodeInto") == 0) + return scr_text_encode_into(scr_tcd_codec_arg(args, argc, 0), + scr_tcd_codec_arg(args, argc, 1)); + scr_tcd_codec_not_a_function(what); + return NULL; +} + +static ScrDyn *scr_tcd_encoder_get(void *rec, const char *key, size_t len) { + (void)rec; + if (len == 8 && memcmp(key, "encoding", 8) == 0) return scr_tcd_str(scr_str_new("utf-8", 5)); + return NULL; +} + +/* decode(input, options): the options object's `stream` member, read + * with JS truthiness; undefined and null mean no options. */ +static bool scr_tcd_decoder_stream_option(const ScrDyn *options, bool *stream) { + *stream = false; + if (options->kind == SCR_DYN_UNDEF || options->kind == SCR_DYN_NULL) return true; + if (options->kind != SCR_DYN_OBJ && options->kind != SCR_DYN_ARR && + options->kind != SCR_DYN_FUNC && options->kind != SCR_DYN_TYPED_REF && + options->kind != SCR_DYN_HANDLE) { + scr_dyn_arg_type_fail("options", "of type object", options); + return false; + } + ScrDyn *value = scr_dyn_obj_read(options, "stream", 6); + if (!value) return false; + *stream = scr_dyn_truthy(value); + scr_dyn_release(value); + return !scr_exc_pending(); +} + +static ScrDyn *scr_tcd_decoder_invoke(void *rec, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, const char *what) { + (void)self; + if (strcmp(method, "decode") != 0) { + scr_tcd_codec_not_a_function(what); + return NULL; + } + ScrBytes *input = scr_bytes_buffer_source(scr_tcd_codec_arg(args, argc, 0)); + if (!input) return NULL; + bool stream; + if (!scr_tcd_decoder_stream_option(scr_tcd_codec_arg(args, argc, 1), &stream)) { + scr_bytes_release(input); + return NULL; + } + const ScrTextCodecLayout *layout = &scr_tcd_decoder_layout; + ScrDyn *state; + memcpy(&state, (const char *)rec + layout->state, sizeof state); + ScrStr *text = scr_text_decode_stream(state, input, scr_tcd_decoder_encoding(rec), + scr_tcd_decoder_flag(rec, layout->fatal), + scr_tcd_decoder_flag(rec, layout->ignore_bom), stream); + scr_bytes_release(input); + return text ? scr_tcd_str(text) : NULL; +} + +static ScrDyn *scr_tcd_decoder_get(void *rec, const char *key, size_t len) { + if (len == 8 && memcmp(key, "encoding", 8) == 0) + return scr_tcd_str(scr_text_decoder_name(scr_tcd_decoder_encoding(rec))); + if (len == 5 && memcmp(key, "fatal", 5) == 0) + return scr_dyn_new_bool(scr_tcd_decoder_flag(rec, scr_tcd_decoder_layout.fatal)); + if (len == 9 && memcmp(key, "ignoreBOM", 9) == 0) + return scr_dyn_new_bool(scr_tcd_decoder_flag(rec, scr_tcd_decoder_layout.ignore_bom)); + return NULL; +} + +static bool scr_tcd_codec_set(void *rec, const char *key, size_t len, const ScrDyn *value) { + (void)rec; + (void)key; + (void)len; + (void)value; + return false; +} + +static const ScrDynHandleMethod scr_tcd_encoder_methods[] = { + {.name = "encode", .length = 0}, + {.name = "encodeInto", .length = 2}, + {.name = NULL}, +}; + +static const ScrDynHandleMethod scr_tcd_decoder_methods[] = { + {.name = "decode", .length = 0}, + {.name = NULL}, +}; + +static const ScrDynHandleOps scr_tcd_encoder_ops = { + .cls = "TextEncoder", + .to_string_tag = "TextEncoder", + .retain = scr_tcd_encoder_retain, + .release = scr_tcd_encoder_release, + .invoke = scr_tcd_encoder_invoke, + .get = scr_tcd_encoder_get, + .set = scr_tcd_codec_set, + .json_empty_object = true, + .methods = scr_tcd_encoder_methods, + .invalid_this = "Value of \"this\" must be of type TextEncoder", +}; + +static const ScrDynHandleOps scr_tcd_decoder_ops = { + .cls = "TextDecoder", + .to_string_tag = "TextDecoder", + .retain = scr_tcd_decoder_retain, + .release = scr_tcd_decoder_release, + .invoke = scr_tcd_decoder_invoke, + .get = scr_tcd_decoder_get, + .set = scr_tcd_codec_set, + .json_empty_object = true, + .methods = scr_tcd_decoder_methods, + .invalid_this = "Value of \"this\" must be of type TextDecoder", +}; + +ScrDyn *scr_text_encoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *)) { + if (!scr_tcd_encoder_layout.retain) { + scr_tcd_encoder_layout.retain = retain; + scr_tcd_encoder_layout.release = release; + scr_dyn_handle_install(SCR_DYNH_TEXT_ENCODER, &scr_tcd_encoder_ops); + } + return scr_dyn_new_handle(rec, SCR_DYNH_TEXT_ENCODER); +} + +ScrDyn *scr_text_decoder_to_dyn(void *rec, void *(*retain)(void *), void (*release)(void *), + size_t encoding, size_t fatal, size_t ignore_bom, size_t state) { + if (!scr_tcd_decoder_layout.retain) { + scr_tcd_decoder_layout = (ScrTextCodecLayout){retain, release, encoding, fatal, ignore_bom, state}; + scr_dyn_handle_install(SCR_DYNH_TEXT_DECODER, &scr_tcd_decoder_ops); + } + return scr_dyn_new_handle(rec, SCR_DYNH_TEXT_DECODER); +} + +void *scr_text_codec_from_dyn(const ScrDyn *d, bool decoder, const ScrDynPath *path, + const char *want) { + return scr_dyn_handle_unbox(d, decoder ? SCR_DYNH_TEXT_DECODER : SCR_DYNH_TEXT_ENCODER, path, + want); +} + +bool scr_text_codec_is(const ScrDyn *d, bool decoder) { + return d->kind == SCR_DYN_HANDLE && + d->v.handle.tag == (decoder ? SCR_DYNH_TEXT_DECODER : SCR_DYNH_TEXT_ENCODER); +} diff --git a/tests/corpus/7747-text-codec-untyped-slot.js b/tests/corpus/7747-text-codec-untyped-slot.js new file mode 100644 index 000000000..cdf8345da --- /dev/null +++ b/tests/corpus/7747-text-codec-untyped-slot.js @@ -0,0 +1,69 @@ +// TextEncoder and TextDecoder instances held in untyped storage (hono's +// StreamingApi keeps `this.encoder = new TextEncoder()` in a JavaScript +// class field) and called from dynamic code, with their identity, streaming +// state, properties, method values and argument errors. +class StreamingApi { + /** @type {any} */ encoder; + /** @type {any} */ decoder; + /** @type {any} */ latin; + /** @type {any} */ strict; + constructor() { + this.encoder = new TextEncoder(); + this.decoder = new TextDecoder(); + this.latin = new TextDecoder("latin1"); + this.strict = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + } + write(input) { + if (typeof input === "string") input = this.encoder.encode(input); + return input; + } +} + +const show = (error) => console.log(error.name, error.code); +const api = new StreamingApi(); +console.log(api.write("héllo 😀")); +console.log(api.encoder.encoding, typeof api.encoder, api.encoder === api.encoder); +console.log(String(api.encoder), JSON.stringify(api.encoder), JSON.stringify({ d: api.decoder })); +console.log(api.encoder.encode(), api.encoder.encode(42), api.encoder.encode(undefined)); +const dest = new Uint8Array(4); +console.log(api.encoder.encodeInto("héllo", dest), dest); +try { + api.encoder.encodeInto(1, dest); +} catch (error) { + show(error); +} + +console.log(api.decoder.encoding, api.decoder.fatal, api.decoder.ignoreBOM); +console.log(api.latin.encoding, api.strict.fatal, api.strict.ignoreBOM); +const euro = api.encoder.encode("€uro"); +const head = api.decoder.decode(euro.subarray(0, 2), { stream: true }); +console.log(JSON.stringify(head), api.decoder.decode(euro.subarray(2))); +console.log(api.latin.decode(new Uint8Array([0xe9, 0x41]))); +console.log(JSON.stringify(api.strict.decode(new Uint8Array([0xef, 0xbb, 0xbf, 0x41])))); +try { + api.strict.decode(new Uint8Array([0xff])); +} catch (error) { + show(error); +} +try { + api.decoder.decode("text"); +} catch (error) { + show(error); +} +console.log(JSON.stringify(api.decoder.decode()), JSON.stringify(api.decoder.decode(undefined, null))); + +const encode = api.encoder.encode; +try { + encode("x"); +} catch (error) { + console.log(error.name, error.code, error.message); +} +console.log(typeof api.encoder.encode, api.encoder.encode.length, api.encoder.encodeInto.length); +console.log(api.decoder.decode.length, encode === api.encoder.encode); +console.log(api.encoder.encode.call(api.encoder, "ab")); +console.log("encode" in api.encoder, "decode" in api.decoder, api.encoder.missing); + +console.log(api.encoder instanceof TextEncoder, api.decoder instanceof TextDecoder); +console.log(api.encoder instanceof TextDecoder, JSON.parse("{}") instanceof TextEncoder); +const codecs = [api.encoder, api.decoder]; +console.log(codecs.indexOf(api.decoder), codecs.includes(api.encoder)); diff --git a/tests/corpus/7749-text-codec-unknown-roundtrip.ts b/tests/corpus/7749-text-codec-unknown-roundtrip.ts new file mode 100644 index 000000000..14c35d2bb --- /dev/null +++ b/tests/corpus/7749-text-codec-unknown-roundtrip.ts @@ -0,0 +1,22 @@ +// TypeScript codecs crossing 'unknown' slots: boxed by reference and +// checked back, keeping identity and a decoder's streaming state. +const holder: { codec: unknown; decoder: unknown } = { + codec: new TextEncoder(), + decoder: new TextDecoder("utf-16le"), +}; + +const encoder = holder.codec as TextEncoder; +console.log(encoder.encode("typed"), encoder === (holder.codec as TextEncoder)); +const decoder = holder.decoder as TextDecoder; +const bytes = new Uint8Array([0x68, 0x00, 0x69]); +console.log(JSON.stringify(decoder.decode(bytes, { stream: true }))); +console.log(JSON.stringify(decoder.decode(new Uint8Array([0x00]))), decoder.encoding); + +function store(value: unknown): unknown[] { + return [value, value]; +} +const pair = store(encoder); +console.log(pair[0] === pair[1], pair[0] === holder.codec); + +const maybe: unknown = Math.random() > 2 ? 1 : new TextDecoder(); +if (typeof maybe === "object" && maybe !== null) console.log((maybe as TextDecoder).encoding); diff --git a/tests/harness/corpus-costs/7747-text-codec-untyped-slot.js.json b/tests/harness/corpus-costs/7747-text-codec-untyped-slot.js.json new file mode 100644 index 000000000..a537f9ede --- /dev/null +++ b/tests/harness/corpus-costs/7747-text-codec-untyped-slot.js.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 6500 } diff --git a/tests/harness/corpus-costs/7749-text-codec-unknown-roundtrip.ts.json b/tests/harness/corpus-costs/7749-text-codec-unknown-roundtrip.ts.json new file mode 100644 index 000000000..aa8658007 --- /dev/null +++ b/tests/harness/corpus-costs/7749-text-codec-unknown-roundtrip.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3600 } diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index fb15e80a4..c5476328e 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -41,6 +41,7 @@ const features: NativeLinkFeatures = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index 57857d642..64ddd0213 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -42,6 +42,7 @@ const base: NativeLinkFeatures = { dgram: false, dnsPromises: false, dgramDyn: false, + textCodecDyn: false, watch: false, foreignFfi: false, nodeTest: false, From 934d254a417f48df2264f26c2409104e6ce398d2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 12:44:35 -0500 Subject: [PATCH 140/409] Keep the inline-cache record list within the shape budget --- packages/runtime/src/scr_js.h | 11 ++++++---- packages/runtime/src/scr_js_object.test.c | 26 +++++++++++++---------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/runtime/src/scr_js.h b/packages/runtime/src/scr_js.h index 6fa20fdb8..1d874b31e 100644 --- a/packages/runtime/src/scr_js.h +++ b/packages/runtime/src/scr_js.h @@ -195,10 +195,13 @@ bool scr_js_truthy(ScrJsVal value); * index and which keys an object lacks. * * Each property site of compiled dynamic-tier code owns one zeroed, - * thread-local ScrJsIc. The code at the site is fixed: it tests entry 0 - * inline and calls a probe for everything else. Probes never run user - * code: they answer from plain data properties or return a miss (NULL / - * false) with nothing done, and the site then runs its generic path. + * thread-local ScrJsIc with static storage: the reset that frees a + * thread's shapes (at thread exit, and when a library session ends) clears + * every record that holds entries. The code at the site is fixed: it tests + * entry 0 inline and calls a probe for everything else. Probes never run + * user code: they answer from plain data properties or return a miss + * (NULL / false) with nothing done, and the site then runs its generic + * path. * * load entry: shape = receiver shape; target = NULL for an own property * (slot indexes the receiver's entries), else the shape of diff --git a/packages/runtime/src/scr_js_object.test.c b/packages/runtime/src/scr_js_object.test.c index a14aa9ff6..6b6934dea 100644 --- a/packages/runtime/src/scr_js_object.test.c +++ b/packages/runtime/src/scr_js_object.test.c @@ -24,11 +24,17 @@ static ScrDyn *object_with(const ScrStr *key, double n) { return object; } +/* Cache records have static storage, as the compiled sites' do: the reset + * at exit clears every record that holds entries. Each check starts from + * an empty record. */ +static ScrJsIc load_ic; +static ScrJsIc store_ic; + /* Whether a load probe answers `object.key` and returns `n`. */ static bool probe_reads(ScrDyn *object, const ScrStr *key, double n) { - ScrJsIc ic; - memset(&ic, 0, sizeof ic); - ScrDyn *value = scr_js_load_probe(object, key, &ic); + ScrJsIc *ic = &load_ic; + memset(ic, 0, sizeof *ic); + ScrDyn *value = scr_js_load_probe(object, key, ic); if (!value) return false; assert(value->kind == SCR_DYN_NUM && value->v.num == n); scr_dyn_release(value); @@ -66,14 +72,13 @@ static void budget_bounds_shapes(size_t key_len, size_t budget) { /* A store probe that would add a new shape leaves the add to the generic * store; one that replaces through an existing shape still runs. */ - ScrJsIc ic; - memset(&ic, 0, sizeof ic); + memset(&store_ic, 0, sizeof store_ic); ScrStr *added = scr_str_new("added", 5); ScrDyn *value = scr_dyn_new_num(8); - assert(!scr_js_store_probe(object, added, value, &ic)); + assert(!scr_js_store_probe(object, added, value, &store_ic)); assert(object->v.obj.len == 1); - memset(&ic, 0, sizeof ic); - assert(scr_js_store_probe(object, first, value, &ic)); + memset(&store_ic, 0, sizeof store_ic); + assert(scr_js_store_probe(object, first, value, &store_ic)); assert(probe_reads(object, first, 8)); scr_dyn_release(value); scr_str_release(added); @@ -90,10 +95,9 @@ static void immortal_objects_stay_unshaped(void) { object->rc = SIZE_MAX; assert(!probe_reads(object, key, 3)); assert(object->v.obj.shape == NULL); - ScrJsIc ic; - memset(&ic, 0, sizeof ic); + memset(&store_ic, 0, sizeof store_ic); ScrDyn *value = scr_dyn_new_num(4); - assert(!scr_js_store_probe(object, key, value, &ic)); + assert(!scr_js_store_probe(object, key, value, &store_ic)); assert(object->v.obj.shape == NULL); scr_dyn_release(value); scr_str_release(key); From c016b97acb4a67cabd18527c2635de773834c144 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 12:54:17 -0500 Subject: [PATCH 141/409] Fix CI on typed-array unions --- .../src/frontend/lowering/containers/bytes.ts | 5 ++ .../src/frontend/lowering/indexed-unions.ts | 49 +++++++++++++---- .../src/frontend/lowering/lower-inspect.ts | 13 ++++- .../compiler/src/frontend/lowering/lowerer.ts | 3 ++ .../test/typed-array-union-emission.test.ts | 52 +++++++++++++++++++ tests/diagnostics/index-signature-values.ts | 4 -- .../index-signature-values.ts.txt | 15 ++---- 7 files changed, 115 insertions(+), 26 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index c804d62e1..1c5513506 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -961,6 +961,11 @@ function lowerUnionBytesMethodCall( name: string, ): IrExpr | null { if (isJsSourceFile(ts.getSourceFileOfNode(call))) return null; + // A Buffer arm's methods differ from a typed array's (slice aliases, fill + // checks its range, searches take strings and wrap numbers), so such a + // union narrows to its arms first. + const receiverName = lowerer.checker.typeToString(lowerer.typeOf(access.expression)); + if (/(^|\| )(NonShared)?Buffer\b/.test(receiverName)) return null; const nodes = call.arguments.nodes; if (nodes.some(ts.isSpreadElement)) return null; const loc = locOf(call); diff --git a/packages/compiler/src/frontend/lowering/indexed-unions.ts b/packages/compiler/src/frontend/lowering/indexed-unions.ts index aa93871c9..774f3aef3 100644 --- a/packages/compiler/src/frontend/lowering/indexed-unions.ts +++ b/packages/compiler/src/frontend/lowering/indexed-unions.ts @@ -131,10 +131,6 @@ function dispatchHelper( const returning = (value: IrExpr, loc: SrcLoc): IrStmt[] => [{ kind: "return", value, loc }]; -/** Safe-read helpers mapped to their numeric twins, for the fusion of an - * immediate ToNumber (tryLowerNumericIndexRead). */ -const numericTwins = new WeakMap>(); - /** `u[i]` read as an element or undefined (an invalid index), as the * typed-array and number-array reads answer. */ export function lowerIndexedUnionRead( @@ -164,9 +160,7 @@ export function lowerIndexedUnionRead( loc, ); const numeric = indexedUnionNumberHelper(lowerer, receiver.type, arms, loc); - let twins = numericTwins.get(lowerer); - if (!twins) numericTwins.set(lowerer, (twins = new Map())); - twins.set(name, numeric); + lowerer.indexedUnionNumericTwins.set(name, numeric); return { kind: "call", callee: name, args: [receiver, index], type: resultT, loc }; } @@ -221,7 +215,7 @@ function indexedUnionNumberHelper( /** The numeric twin of an indexed-union read helper call, or null. */ export function indexedUnionNumericTwin(lowerer: Lowerer, call: IrExpr): IrExpr | null { if (call.kind !== "call") return null; - const twin = numericTwins.get(lowerer)?.get(call.callee); + const twin = lowerer.indexedUnionNumericTwins.get(call.callee); return twin === undefined ? null : { ...call, callee: twin, type: F64 }; } @@ -479,12 +473,49 @@ function inlineDispatch(receiver: IrExpr, operands: readonly IrExpr[]): boolean ); } -/** Whether evaluating `e` can rebind one of the plain reads `reads`. */ +/** Expressions that run no program code: reads, literals and operators + * over primitive values. */ +const CODE_FREE_EXPRS = new Set([ + "numLit", + "boolLit", + "strLit", + "unitLit", + "varRef", + "unary", + "logical", + "ternary", + "toBool", + "strEq", + "strCmp", + "arrayGet", + "arrayHas", + "arrayState", + "bytesIntrinsic", + "fieldGet", + "recordGet", + "unionNarrow", + "unionIsTag", + "unionDisc", + "upcast", + "nullish", +]); +const PRIMITIVE_OPERANDS = new Set(["f64", "bool", "string", "bigint"]); + +/** Whether evaluating `e` can rebind one of the plain reads `reads`: by + * assigning it, or by running program code (a call, a getter, a + * conversion) that could assign a captured binding. */ function rebinds(e: IrExpr, reads: readonly IrExpr[]): boolean { const ids = new Set(reads.flatMap((r) => (r.kind === "varRef" ? [r.localId] : []))); let found = false; const expr = (x: IrExpr): boolean => { if ((x.kind === "assignExpr" || x.kind === "incDec") && ids.has(x.localId)) found = true; + else if (x.kind === "bin") { + if (!PRIMITIVE_OPERANDS.has(x.left.type.kind) || !PRIMITIVE_OPERANDS.has(x.right.type.kind)) + found = true; + } else if (x.kind === "call") { + // The union dispatch helpers only read and store elements. + if (!x.callee.startsWith("%union.")) found = true; + } else if (!CODE_FREE_EXPRS.has(x.kind)) found = true; return !found && everyExprChild(x, expr, stmt); }; const stmt = (x: IrStmt): boolean => { diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index d914faa5d..fc52ed555 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -948,7 +948,13 @@ export function inspectExpr( } // A Uint8Array, a Buffer and a DataView share the u8 representation // and print differently: the view's own flags pick. - return { kind: "libCall", fn: "insp.bytes", args: [value, recurse, depth], type: STRING, loc }; + return { + kind: "libCall", + fn: "insp.bytes", + args: [value, recurse, depth], + type: STRING, + loc, + }; case "dyn": return { kind: "libCall", fn: "insp.dyn", args: [value, recurse, depth], type: STRING, loc }; case "generator": @@ -2510,7 +2516,10 @@ export function lowerInspectValue( value, (input) => ({ kind: "libCall", - fn: value.type.kind === "bytes" && value.type.elem === "u8" ? "insp.bytes" : "insp.typedArray", + fn: + value.type.kind === "bytes" && value.type.elem === "u8" + ? "insp.bytes" + : "insp.typedArray", args: [input, numLit(0, loc), numLit(options.depth, loc)], type: STRING, loc, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3c74cfbad..090c957ad 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2896,6 +2896,9 @@ export class Lowerer { /** util.inspect's per-type traversal helpers (%util.insp.N), interned * by typeKey — the assertHelpers pattern with its own namespace. */ readonly inspectHelpers = new Map(); + /** Indexed-union safe-read helpers mapped to their numeric twins, for the + * fusion of an immediate ToNumber (tryLowerNumericIndexRead). */ + readonly indexedUnionNumericTwins = new Map(); private readonly unionLiteralOwners = new Map< string, { owners: Map | null; shapes: number; unions: number } diff --git a/packages/compiler/test/typed-array-union-emission.test.ts b/packages/compiler/test/typed-array-union-emission.test.ts index 743788af0..5aaf846ed 100644 --- a/packages/compiler/test/typed-array-union-emission.test.ts +++ b/packages/compiler/test/typed-array-union-emission.test.ts @@ -54,3 +54,55 @@ test("a typed-array union read through a computed receiver goes through one help // The receiver is a call: evaluated once, then dispatched by the helper. expect(body(llvm, "first")).toContain("@sc_bf__x25_union_getNumber"); }); + +const stores = ` +type Wide = Uint8Array | Uint16Array; +const pick = (wide: boolean): Wide => (wide ? new Uint16Array(4) : new Uint8Array(4)); +function fill(xs: Wide): void { + for (let i = 0; i < xs.length; i++) xs[i] = i + 1; +} +function rebind(): void { + let u = pick(false); + let i = 0; + const swap = (): number => { + u = pick(true); + i = 2; + return 3; + }; + u[i] = swap(); + console.log(u, i); +} +fill(pick(process.argv.length > 9)); +rebind(); +`; + +test("a typed-array union store runs in place unless its value can rebind the receiver or index", async () => { + const llvm = await emitLlvm(stores); + expect(body(llvm, "fill")).not.toContain("@sc_bf__x25_union_idxSet"); + // The call may assign the captured receiver and index, which the store + // read before it: the helper evaluates them first. + expect(body(llvm, "rebind")).toContain("@sc_bf__x25_union_idxSet"); +}); + +test("typed-array methods on a union with a Buffer arm need a narrowing first", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-typed-array-unions-")); + try { + const entry = join(dir, "main.ts"); + await writeFile( + entry, + `import { Buffer } from "node:buffer"; +const u: Buffer | Uint16Array = process.argv.length > 9 ? new Uint16Array(2) : Buffer.alloc(2); +console.log(u.slice(1)); +`, + ); + const result = await compile(entry, { + outDir: dir, + outPath: join(dir, "main.ll"), + outputKind: "llvm", + }); + expect(result.ok).toBe(false); + expect(JSON.stringify(result.diagnostics)).toContain("slice"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/tests/diagnostics/index-signature-values.ts b/tests/diagnostics/index-signature-values.ts index 6c53b141c..1f3a01ba5 100644 --- a/tests/diagnostics/index-signature-values.ts +++ b/tests/diagnostics/index-signature-values.ts @@ -8,10 +8,6 @@ const bySymbol: { [s: symbol]: number } = {}; console.log(bySymbol); -// Storage is supported, but composite typed-array inspection is not. -const buffers: Record = {}; -console.log(buffers); - // Dual signatures whose value types intern DIFFERENTLY cannot share the // one store (tsc requires only assignability, but the store gives one // answer per key). diff --git a/tests/harness/__snapshots__/index-signature-values.ts.txt b/tests/harness/__snapshots__/index-signature-values.ts.txt index bbbed7fe2..ea36a9c76 100644 --- a/tests/harness/__snapshots__/index-signature-values.ts.txt +++ b/tests/harness/__snapshots__/index-signature-values.ts.txt @@ -7,18 +7,11 @@ index-signature-values.ts:8:7 - error SC2006: values of type '{ [s: symbol]: num hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering -index-signature-values.ts:13:13 - error SC1090: console.log of '{ [key: string]: Uint8Array }' values (Uint8Array and Buffer values inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)) are not supported yet +index-signature-values.ts:14:7 - error SC2006: values of type '{ [k: string]: string | number; [n: number]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') - 12 | const buffers: Record = {}; - 13 | console.log(buffers); - | ^~~~~~~ - 14 | - -index-signature-values.ts:18:7 - error SC2006: values of type '{ [k: string]: string | number; [n: number]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown') - - 17 | // answer per key). - 18 | const dual: { [k: string]: string | number; [n: number]: number } = {}; + 13 | // answer per key). + 14 | const dual: { [k: string]: string | number; [n: number]: number } = {}; | ^~~~ - 19 | console.log(dual); + 15 | console.log(dual); hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering \ No newline at end of file From 0af86e2e3ef7862ca6d91b8917ff6ccf36735fd1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 12:58:05 -0500 Subject: [PATCH 142/409] Version two-counter loops over typed arrays A loop with two counters closing in on each other, such as `for (let i = lo, j = hi; i < j; i++, j--)`, checked the bounds of every typed-array element it read or wrote at either counter. Within the body the low counter only rises and stays below the high one, which only falls, so every such index lies between the two starts. Typed-array loop versions now cover these loops. The guard tests that both starts are exact integers, that the low start is at least 0, and that the high start is below the length of each view the loop indexes at a counter. The fast copy then reads and writes those elements without checks, and the checked copy runs whenever the guard fails. In the fast copy the guard also stands for the loop's own start test. The exact-integer test of a start is now one function, shared by the typed-array version guards and the dense array two-counter guard. The dense guard, which compared the round trip with `fcmp oeq`, now also leaves a -0 start to the checked loop. --- .../backend/llvm/bytes-loop-versions.test.ts | 77 ++++++++++++++- .../src/backend/llvm/bytes-loop-versions.ts | 97 ++++++++++++------- .../src/backend/llvm/counted-loops.ts | 33 +++++++ .../src/backend/llvm/dense-array-loops.ts | 31 ++---- .../src/backend/llvm/emit-statements.ts | 11 ++- packages/compiler/src/backend/llvm/emitter.ts | 30 +++--- .../7981-typed-array-paired-loops.ts.json | 6 ++ tests/corpus/7981-typed-array-paired-loops.ts | 57 +++++++++++ .../7981-typed-array-paired-loops.ts.json | 1 + 9 files changed, 265 insertions(+), 78 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7981-typed-array-paired-loops.ts.json create mode 100644 tests/corpus/7981-typed-array-paired-loops.ts create mode 100644 tests/harness/corpus-costs/7981-typed-array-paired-loops.ts.json diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index 34b75861c..de71a6ef0 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -11,7 +11,7 @@ import { type IrType, } from "../../ir/ir.js"; import { analyzeIntegerRanges } from "../../ir/integer-ranges.js"; -import { matchIntegerCountedForLoop } from "../../ir/integer-loops.js"; +import { matchIntegerCountedForLoop, matchIntegerPairedForLoop } from "../../ir/integer-loops.js"; import { findInitializedByteLoopBindings } from "./byte-windows.js"; import { matchBytesLoopVersion, type BytesLoopVersion } from "./bytes-loop-versions.js"; import { emitLlvmModule } from "./emitter.js"; @@ -623,3 +623,78 @@ test("while loop counters below a string's length version its reads", () => { expect(version.bounded.map((c) => c.slack)).toEqual([0]); expect(version.fixed).toEqual([]); }); + +test("a two-counter loop versions the typed arrays it indexes at either counter", () => { + // for (let i = 0, j = k; i < j; i++, j--) { const t = out[i]; out[i] = out[j]; out[j] = t; } + const loop: IrStmt & { kind: "for" } = { + kind: "for", + init: { + kind: "block", + body: [ + { kind: "varDecl", localId: "i", init: num(0), loc }, + { kind: "varDecl", localId: "j", init: ref("k"), loc }, + ], + loc, + }, + cond: { kind: "bin", op: "<", left: ref("i"), right: ref("j"), type: BOOL, loc }, + update: { + kind: "block", + body: [ + { kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }, + { kind: "assign", localId: "j", value: bin("-", ref("j"), num(1)), loc }, + ], + loc, + }, + body: [ + { kind: "varDecl", localId: "t", init: get(ref("out", f64s), ref("i")), loc }, + set(ref("out", f64s), ref("i"), get(ref("out", f64s), ref("j"))), + set(ref("out", f64s), ref("j"), ref("t")), + // Another index stays checked. + set(ref("src", f64s), bin("+", ref("i"), num(1)), num(0)), + ], + loc, + }; + const kernel = fn([loop], [local("t", F64)]); + const ranges = analyzeIntegerRanges(kernel); + const locals = new Map(kernel.locals.map((l) => [l.id, l])); + const paired = matchIntegerPairedForLoop(loop, locals, ranges)!; + expect(paired).not.toBeNull(); + const version = matchBytesLoopVersion( + loop, + null, + { + locals, + invariantGlobals: new Set(), + captured: new Set(), + ranges, + initialized: findInitializedByteLoopBindings(kernel).get(loop) ?? new Set(), + proven: new Set(), + workers: false, + owned: new Set(), + scalarStrings: new Set(), + }, + paired, + )!; + expect(version.paired?.loop).toBe(paired); + expect(version.paired?.receivers.map((r) => version.receivers[r]!.ref)).toEqual([ + ref("out", f64s), + ]); + expect(version.bounds.size).toBe(4); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + kernel, + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m)); + expect(guard).toMatch(/icmp sge i64 %\S+, 0/); + expect(guard).toMatch(/icmp slt i64 %\S+, %\S+/); + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + // The fast copy keeps only the `src[i + 1]` check, and no second guard. + expect(fast.match(/bytes\.index\.invalid\d+:/g)).toHaveLength(1); + expect(fast).not.toContain("counted.fast"); +}); diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index 704b6723a..2013d03e4 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -1,11 +1,15 @@ import { F64, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js"; import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js"; -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { exactInteger, widenInteger } from "./integer-values.js"; -import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; +import { + emitCountedLoopLimit, + emitCountedLoopSafety, + emitExactIntegerTest, +} from "./counted-loops.js"; import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; import { f64Lit } from "./common.js"; import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js"; @@ -79,6 +83,10 @@ export interface BytesLoopVersion { guarded: ReadonlySet; /** The counted loop whose iteration space the affine checks cover. */ counted: IntegerCountedForLoop | null; + /** A two-counter loop (`for (let i = lo, j = hi; i < j; i++, j--)`) and + * the receivers it indexes at either counter. Both counters stay between + * the two starts, so the guard requires `lo >= 0` and `hi < length`. */ + paired: { loop: IntegerPairedForLoop; receivers: number[] } | null; /** The guard also requires every counter value within [-bound, bound]. */ counterBound: number | null; /** Integer ranges of affine index operands that hold in the unchecked @@ -153,6 +161,7 @@ export function matchBytesLoopVersion( loop: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, facts: BytesLoopFacts, + paired: IntegerPairedForLoop | null = null, ): BytesLoopVersion | null { const written = new Set(); const temps = new Map(); @@ -438,6 +447,9 @@ export function matchBytesLoopVersion( string, { check: BoundedCheck; nodes: { node: Node; access: WhileAccess }[] } >(); + const pairedCounters = + paired !== null && counted === null ? new Set([paired.low.localId, paired.high.localId]) : null; + const pairedNodes = new Map(); const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { const ref = root(array); if (!ref) return; @@ -448,6 +460,20 @@ export function matchBytesLoopVersion( else owned.set(receiver, [node]); } if (facts.proven.has(node)) return; + // An element at either counter of a two-counter loop. + if ( + pairedCounters !== null && + width === 1 && + ref.type.kind === "bytes" && + index.kind === "varRef" && + pairedCounters.has(index.localId) + ) { + const receiver = receiverOf(ref); + const nodes = pairedNodes.get(receiver); + if (nodes) nodes.push(node); + else pairedNodes.set(receiver, [node]); + return; + } const shape = counter !== null ? affine(index) : null; if (shape && shape.counter !== 0) { const operands = new Set(); @@ -543,11 +569,16 @@ export function matchBytesLoopVersion( Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length), ); const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS); + const pairedKept = [...pairedNodes.entries()].slice( + 0, + Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length - boundedKept.length), + ); if ( fixedKept.length === 0 && affineKept.length === 0 && boundedKept.length === 0 && - ownedKept.length === 0 + ownedKept.length === 0 && + pairedKept.length === 0 ) return null; const bounds = new Set(); @@ -616,6 +647,10 @@ export function matchBytesLoopVersion( if (access.index !== access.ref) narrow(access.index, { min: 0, max: SAFE }); } } + for (const [receiver, nodes] of pairedKept) { + used.add(receiver); + for (const node of nodes) bounds.add(node); + } const counterChecks: CounterCheck[] = []; for (const [id, min] of counterMins) { const info = whileAccesses!.counters.get(id)!; @@ -658,6 +693,10 @@ export function matchBytesLoopVersion( counters: counterChecks, guarded, counted: affineKept.length > 0 ? counted : null, + paired: + paired !== null && pairedKept.length > 0 + ? { loop: paired, receivers: pairedKept.map(([receiver]) => renumber.get(receiver)!) } + : null, counterBound: needsBound ? bound : null, ranges, }; @@ -1143,6 +1182,25 @@ export function emitBytesLoopVersionGuard( conditions.push(both); } } + if (version.paired !== null) { + // Both counters stay within [lo, hi] (exact integers below 2^53): every + // access at either one is in bounds when `lo >= 0` and `hi < length`. + const { loop } = version.paired; + const [low, high] = [loop.low.start, loop.high.start].map((expr) => { + const tested = emitExactIntegerTest(host, host.emitExpr(expr).name, -SAFE, SAFE); + conditions.push(tested.ok); + return tested.integer; + }); + const nonNegative = B.tmp(); + B.line(`${nonNegative} = icmp sge i64 ${low}, 0`); + conditions.push(nonNegative); + for (const receiver of version.paired.receivers) { + // Lengths are at most 2^53 elements: a signed comparison is exact. + const below = B.tmp(); + B.line(`${below} = icmp slt i64 ${high}, ${lengths[receiver]}`); + conditions.push(below); + } + } let valid = "true"; for (const cond of conditions) { if (valid === "true") valid = cond; @@ -1156,39 +1214,6 @@ export function emitBytesLoopVersionGuard( else B.condBr(valid, fast, slow); } -/** Tests that a double is an exact integer within [min, max] (not -0; NaN - * fails the ordered comparisons) and gives its i64. The integer is frozen, - * so it is a fixed value even when the test fails. */ -function emitExactIntegerTest( - host: LlvmEmitterContext, - value: string, - min: number, - max: number, -): { ok: string; integer: string } { - const B = host.B; - const low = B.tmp(), - high = B.tmp(), - inRange = B.tmp(), - raw = B.tmp(), - integer = B.tmp(), - back = B.tmp(), - bits = B.tmp(), - backBits = B.tmp(), - exact = B.tmp(), - ok = B.tmp(); - B.line(`${low} = fcmp oge double ${value}, ${f64Lit(min)}`); - B.line(`${high} = fcmp ole double ${value}, ${f64Lit(max)}`); - B.line(`${inRange} = and i1 ${low}, ${high}`); - B.line(`${raw} = fptosi double ${value} to i64`); - B.line(`${integer} = freeze i64 ${raw}`); - B.line(`${back} = sitofp i64 ${integer} to double`); - B.line(`${bits} = bitcast double ${value} to i64`); - B.line(`${backBits} = bitcast double ${back} to i64`); - B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); - B.line(`${ok} = and i1 ${inRange}, ${exact}`); - return { ok, integer }; -} - function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string { if (scale === 1) return value; const scaled = host.B.tmp(); diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts index ca2dd6b92..1ff4c6580 100644 --- a/packages/compiler/src/backend/llvm/counted-loops.ts +++ b/packages/compiler/src/backend/llvm/counted-loops.ts @@ -151,3 +151,36 @@ function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string { B.line(`${ok} = and i1 ${inRange}, ${exact}`); return ok; } + +/** Tests that a double is an exact integer within [min, max] (not -0; NaN + * fails the ordered comparisons) and gives its i64. The integer is frozen, + * so it is a fixed value even when the test fails. */ +export function emitExactIntegerTest( + host: LlvmEmitterContext, + value: string, + min: number, + max: number, +): { ok: string; integer: string } { + const B = host.B; + const low = B.tmp(), + high = B.tmp(), + inRange = B.tmp(), + raw = B.tmp(), + integer = B.tmp(), + back = B.tmp(), + bits = B.tmp(), + backBits = B.tmp(), + exact = B.tmp(), + ok = B.tmp(); + B.line(`${low} = fcmp oge double ${value}, ${f64Lit(min)}`); + B.line(`${high} = fcmp ole double ${value}, ${f64Lit(max)}`); + B.line(`${inRange} = and i1 ${low}, ${high}`); + B.line(`${raw} = fptosi double ${value} to i64`); + B.line(`${integer} = freeze i64 ${raw}`); + B.line(`${back} = sitofp i64 ${integer} to double`); + B.line(`${bits} = bitcast double ${value} to i64`); + B.line(`${backBits} = bitcast double ${back} to i64`); + B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`); + B.line(`${ok} = and i1 ${inRange}, ${exact}`); + return { ok, integer }; +} diff --git a/packages/compiler/src/backend/llvm/dense-array-loops.ts b/packages/compiler/src/backend/llvm/dense-array-loops.ts index 384c31b0c..b806939b5 100644 --- a/packages/compiler/src/backend/llvm/dense-array-loops.ts +++ b/packages/compiler/src/backend/llvm/dense-array-loops.ts @@ -36,8 +36,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { loadArrCount, SCR_ARR_FIELD } from "./common.js"; -import { emitCountedLoopLimit } from "./counted-loops.js"; -import { f64Lit } from "./common.js"; +import { emitCountedLoopLimit, emitExactIntegerTest } from "./counted-loops.js"; export const INT32_MAX = 2147483647; @@ -453,28 +452,10 @@ function emitPairedDenseGuard( header: (array: string, field: number) => string, ): string { const B = host.B; - const integer = (expr: IrExpr, min: number, max: number): { value: string; ok: string } => { - const start = host.emitExpr(expr); - const low = B.tmp(), - high = B.tmp(), - raw = B.tmp(), - value = B.tmp(), - back = B.tmp(), - exact = B.tmp(), - inRange = B.tmp(), - ok = B.tmp(); - B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(min)}`); - B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(max)}`); - B.line(`${inRange} = and i1 ${low}, ${high}`); - B.line(`${raw} = fptosi double ${start.name} to i64`); - B.line(`${value} = freeze i64 ${raw}`); - B.line(`${back} = sitofp i64 ${value} to double`); - B.line(`${exact} = fcmp oeq double ${back}, ${start.name}`); - B.line(`${ok} = and i1 ${inRange}, ${exact}`); - return { value, ok }; - }; - const low = integer(pair.lowStart, 0, INT32_MAX - 1); - const high = integer(pair.highStart, -1, INT32_MAX - 1); + const start = (expr: IrExpr, min: number, max: number) => + emitExactIntegerTest(host, host.emitExpr(expr).name, min, max); + const low = start(pair.lowStart, 0, INT32_MAX - 1); + const high = start(pair.highStart, -1, INT32_MAX - 1); let valid = B.tmp(); B.line(`${valid} = and i1 ${low.ok}, ${high.ok}`); for (const ref of loop.arrays.values()) { @@ -486,7 +467,7 @@ function emitPairedDenseGuard( both = B.tmp(); B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`); B.line(`${wide} = zext i32 ${filled} to i64`); - B.line(`${covered} = icmp slt i64 ${high.value}, ${wide}`); + B.line(`${covered} = icmp slt i64 ${high.integer}, ${wide}`); B.line(`${both} = and i1 ${valid}, ${covered}`); valid = both; } diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index aef5ccf30..55e31219c 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -1051,15 +1051,17 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { LOOPS.of(emitter).unaliasedArrays, ) : countedLoop; - const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null; + const version = versionable + ? emitter.matchBytesLoopVersion(s, versionCounted, versionCounted ? null : pairedLoop) + : null; if (version) { emitVersionedLoop(emitter, s, version); break; } let countedJoin: string | null = null; if ( - (countedLoop?.guarded && !LOOPS.of(emitter).provenCountedGuards.has(s)) || - pairedLoop?.guarded + (countedLoop?.guarded || pairedLoop?.guarded) && + !LOOPS.of(emitter).provenCountedGuards.has(s) ) { const safe = countedLoop ? emitCountedLoopSafety(emitter, countedLoop) @@ -2094,7 +2096,8 @@ function emitVersionedLoop( if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); if (version.ranges.size > 0) emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); - if (version.counted?.guarded) LOOPS.of(emitter).provenCountedGuards.add(s); + if (version.counted?.guarded || version.paired?.loop.guarded) + LOOPS.of(emitter).provenCountedGuards.add(s); emitter.emitStmt(s); B.br(done); } finally { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 4aa9b65ad..ad59c3b1b 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -60,7 +60,7 @@ import { captureFreeFunctions, observesStacks } from "../stack-observations.js"; import { findInitializedByteLoopBindings } from "./byte-windows.js"; import { matchBytesLoopVersion } from "./bytes-loop-versions.js"; import { globalSlotAlias } from "./bytes-alias.js"; -import type { IntegerCountedForLoop } from "../../ir/integer-loops.js"; +import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js"; import { materializeSplitPiece, type SplitSpan, @@ -4263,6 +4263,7 @@ export class LlEmitter { matchBytesLoopVersion( s: IrStmt & { kind: "for" | "while" }, counted: IntegerCountedForLoop | null, + paired: IntegerPairedForLoop | null = null, ): ReturnType { const fn = LOOPS.of(this).bytesLoopFunction; if (!fn) return null; @@ -4272,17 +4273,22 @@ export class LlEmitter { const invariantGlobals = new Set(this.stableGlobals); for (const global of this.initializerBindings.get(fn.name) ?? []) invariantGlobals.add(global.id); - return matchBytesLoopVersion(s, counted, { - locals: this.currentLocals, - invariantGlobals, - captured: this.captureIds, - ranges: this.integerRanges, - initialized, - proven: this.bytesBounds, - workers: this.mod.workers === true, - owned: this.bytesOwned, - scalarStrings: new Set(this.stringSlices.keys()), - }); + return matchBytesLoopVersion( + s, + counted, + { + locals: this.currentLocals, + invariantGlobals, + captured: this.captureIds, + ranges: this.integerRanges, + initialized, + proven: this.bytesBounds, + workers: this.mod.workers === true, + owned: this.bytesOwned, + scalarStrings: new Set(this.stringSlices.keys()), + }, + paired, + ); } /** The alias tag of a whole-slot load of a binding (module globals only). */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7981-typed-array-paired-loops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7981-typed-array-paired-loops.ts.json new file mode 100644 index 000000000..71fa483a8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7981-typed-array-paired-loops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7981-typed-array-paired-loops.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7981-typed-array-paired-loops.ts b/tests/corpus/7981-typed-array-paired-loops.ts new file mode 100644 index 000000000..d13eecbd6 --- /dev/null +++ b/tests/corpus/7981-typed-array-paired-loops.ts @@ -0,0 +1,57 @@ +// Two-counter loops over typed arrays (`for (let i = lo, j = hi; i < j; +// i++, j--)`), with starts inside the arrays and outside them: past the +// end, negative, fractional and NaN. Views over one buffer, other element +// types, inclusive conditions and steps other than one. + +function reverse(a: Int32Array, lo: number, hi: number): string { + for (let i = lo, j = hi; i < j; i++, j--) { + const t = a[i]!; + a[i] = a[j]!; + a[j] = t; + } + return Array.from(a).join(); +} + +function mirror(a: Float64Array, b: Float64Array, hi: number): number { + let sum = 0; + for (let i = 0, j = hi; i <= j; i++, j--) { + b[i] = a[j]! * 2; + sum += a[i]! - b[j]!; + } + return sum; +} + +function skipping(a: Uint8ClampedArray, hi: number): string { + for (let i = 0, j = hi; i < j; i += 2, j -= 3) { + a[i] = a[j]! + 200; + a[j] = i - 10; + } + return Array.from(a).join(); +} + +function palindrome(text: Uint16Array, n: number): boolean { + for (let i = 0, j = n - 1; i < j; i++, j--) if (text[i] !== text[j]) return false; + return true; +} + +console.log(reverse(new Int32Array([1, 2, 3, 4, 5, 6]), 0, 5)); +console.log(reverse(new Int32Array([1, 2, 3, 4, 5, 6]), 1, 3)); +console.log(reverse(new Int32Array([1, 2, 3, 4]), 0, 6)); +console.log(reverse(new Int32Array([1, 2, 3, 4]), -2, 2)); +console.log(reverse(new Int32Array([1, 2, 3, 4]), 0.5, 3.5)); +console.log(reverse(new Int32Array([1, 2, 3, 4]), 0, NaN)); +console.log(reverse(new Int32Array([1, 2, 3, 4]), 3, 0)); + +const buffer = new ArrayBuffer(48); +const whole = new Float64Array(buffer); +for (let k = 0; k < whole.length; k++) whole[k] = k + 0.5; +const tail = new Float64Array(buffer, 16, 3); +console.log(mirror(whole, tail, 4), Array.from(whole).join()); +console.log(mirror(tail, tail, 2), Array.from(whole).join()); +console.log(mirror(tail, whole, 7), Array.from(whole).join()); + +console.log(skipping(new Uint8ClampedArray([10, 20, 30, 40, 50, 60, 70, 80]), 7)); +console.log(skipping(new Uint8ClampedArray([10, 20, 30]), 9)); + +const word = new Uint16Array([114, 97, 99, 101, 99, 97, 114]); +console.log(palindrome(word, 7), palindrome(word, 6), palindrome(word, 9), palindrome(word, 0)); diff --git a/tests/harness/corpus-costs/7981-typed-array-paired-loops.ts.json b/tests/harness/corpus-costs/7981-typed-array-paired-loops.ts.json new file mode 100644 index 000000000..c93369880 --- /dev/null +++ b/tests/harness/corpus-costs/7981-typed-array-paired-loops.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 4500 } From 8b6d3329f4666d641fcbebaf8be501ee4b6d0131 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 13:00:50 -0500 Subject: [PATCH 143/409] Hold an element assignment's receiver against its index's effects When the index assigns the receiver's binding (`a[(a = b, 0)] = c[0]!`), holding the index alone left the receiver read at the store, after the index ran, so the value landed in the replacement array. The receiver is now held whenever the index or the value could change what it reads. --- .../compiler/src/frontend/lowering/array-values.ts | 14 ++++++++++---- tests/corpus/7642-element-assignment-order.ts | 8 ++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index 9f23a860b..f191ad942 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -291,16 +291,22 @@ export function arrayValueStore( // JavaScript evaluates them first (`out[n++] = src[n]` reads src[1]), so // either may need its value held before the value runs. if (value.kind !== "varRef") { - const effects = valueEffects(lowerer, value); - const hold = (e: IrExpr, name: string): IrExpr => { + const valueFx = valueEffects(lowerer, value); + const hold = (e: IrExpr, name: string, effects: typeof valueFx): IrExpr => { if (unaffectedBy(e, effects) && !(effects.opaque && readsMutableLocal(lowerer, e))) return e; const temp = lowerer.declareHiddenLocal(name, e.type); prefix.push({ kind: "varDecl", localId: temp.id, init: e, loc }); return varRef(temp.id, temp.type, loc); }; - arr = hold(arr, "%arrayTarget"); - index = hold(index, "%arrayIndex"); + // The receiver runs before the index and the value, so the index's + // effects can change it too (`a[(a = b, 0)] = c[0]!`). + const indexFx = valueEffects(lowerer, index); + arr = hold(arr, "%arrayTarget", { + writes: new Set([...valueFx.writes, ...indexFx.writes]), + opaque: valueFx.opaque || indexFx.opaque, + }); + index = hold(index, "%arrayIndex", valueFx); } if (value.kind !== "varRef") { const temp = lowerer.declareHiddenLocal("%arrayValue", value.type); diff --git a/tests/corpus/7642-element-assignment-order.ts b/tests/corpus/7642-element-assignment-order.ts index f2f2fae7b..e020d05fc 100644 --- a/tests/corpus/7642-element-assignment-order.ts +++ b/tests/corpus/7642-element-assignment-order.ts @@ -35,3 +35,11 @@ function index(): number { const c: (number | undefined)[] = [0, 0]; c[index()] = optional[1]; console.log(c, reads); + +// The index assigns the receiver's binding: the store uses the receiver +// evaluated before it. +let receiver: (number | undefined)[] = [0, 0]; +const original = receiver; +const replacement: (number | undefined)[] = [7, 7]; +receiver[((receiver = replacement), 1)] = optional[1]; +console.log(original, replacement, receiver === replacement); From e3b42a311d988a04ad38cd805045adc616eec8b3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 13:10:16 -0500 Subject: [PATCH 144/409] Add --target-cpu to compile x86-64 programs for a microarchitecture level Release builds compile for the x86-64 baseline, so LLVM vectorizes with SSE2 only. `--target-cpu=` compiles the program's code for a higher level: every emitted function carries the level as its `target-cpu`, so LLVM can use AVX2, BMI2, FMA or AVX-512. The executable then runs only on CPUs of that level. - `native` resolves to the highest level the build machine supports, read from /proc/cpuinfo on Linux and `sysctl machdep.cpu` on macOS. - The level is part of the executable cache key; `baseline` and builds without the option produce the same output and keys as before. - The runtime pack keeps its baseline code. - Targets other than x86-64 refuse a level (SC3002). The CLI's cached bootstrap route leaves builds with the option to the full compiler. - `bench:runtime` takes `--baseline-target-cpu` and `--candidate-target-cpu`. --- docs/content/docs/cli.mdx | 40 ++++++ packages/cli/src/bootstrap.ts | 3 + packages/cli/test/target-cpu.test.ts | 66 ++++++++++ .../compiler/src/backend/llvm/emit-module.ts | 23 +++- .../compiler/src/backend/llvm/emitter.test.ts | 10 ++ packages/compiler/src/backend/llvm/emitter.ts | 7 ++ .../compiler/src/backend/target-cpu.test.ts | 51 ++++++++ packages/compiler/src/backend/target-cpu.ts | 119 ++++++++++++++++++ packages/compiler/src/cli/command.ts | 29 ++++- packages/compiler/src/cli/usage.ts | 7 ++ packages/compiler/src/compile-types.ts | 7 ++ .../src/executable/executable-cache.test.ts | 2 + .../src/executable/executable-cache.ts | 5 + packages/compiler/src/index.ts | 11 ++ packages/compiler/src/native/prepare.ts | 2 + scripts/bench-runtime.mjs | 24 +++- 16 files changed, 395 insertions(+), 11 deletions(-) create mode 100644 packages/cli/test/target-cpu.test.ts create mode 100644 packages/compiler/src/backend/target-cpu.test.ts create mode 100644 packages/compiler/src/backend/target-cpu.ts diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 8b0353620..9fa5e9447 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -135,6 +135,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
--detached-exit
On Linux, when the program calls process.exit(), hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.
+
--target-cpu <baseline|x86-64-v2|x86-64-v3|x86-64-v4|native>
+
+
--profile-generate <pattern>
On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where %p expands to the process ID; the LLVM_PROFILE_FILE environment variable overrides the pattern at run time. A relative pattern resolves from the current working directory. Instrumented programs run slower. Valid only for executable builds with --optimization=release or speed.
@@ -228,6 +231,43 @@ $ scriptc build app.ts --optimization=speed --profile-use app.profdata Train on workloads like the ones the program will run: code the training never reached is optimized as without a profile. The profile can be committed and reused as the program changes. Functions whose control flow changed since the profile was collected lose their profile, and the rest keep theirs, so regenerate it when much of the program has changed. [Build parallelism](/docs/how-it-works#build-parallelism) describes what a profile adds to build time. +## Target CPU + +By default, x86-64 executables run on any x86-64 CPU, so the compiler uses only the baseline instruction set (SSE2). `--target-cpu` compiles the program's code for a higher x86-64 microarchitecture level. Each level includes the ones below it: + +
On x86-64 targets, compile the program's code for an x86-64 microarchitecture level. The default, baseline, runs on any x86-64 CPU. A higher level lets LLVM use more instructions, and the executable then runs only on CPUs that support that level. native uses the highest level the build machine supports. Valid for executable, assembly, and object output on x86-64 targets. See Target CPU.
+
+ {hasChildren ? ( + + ) : } +
+ + {displayName} + +
+ {row.kind}{row.apiSymbol && row.apiSymbol !== row.signature && !isRoot ? ` · ${row.apiSymbol}` : ""} +
+
+
+
-
- {hasChildren ? ( - - ) : } -
- - {displayName} - -
- {row.kind}{row.apiSymbol && row.apiSymbol !== row.signature && !isRoot ? ` · ${row.apiSymbol}` : ""} -
-
-
-
+ + + + + + + + + + + + + + + + + + + + +
LevelAdds
x86-64-v2SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT
x86-64-v3AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE
x86-64-v4AVX-512 (F, BW, CD, DQ, VL)
+ +```console +$ scriptc build app.ts --target-cpu=x86-64-v3 -o app +``` + +Loops over typed arrays and numbers gain the most, because LLVM vectorizes them with wider registers. Some loops run slower at a higher level, so measure the program on the machines it will run on. + +An executable built for a level runs only on CPUs that support that level. On an older CPU, it crashes with an illegal-instruction signal (`SIGILL`) when it reaches an instruction the CPU lacks. A named level is portable to every machine of that level. `native` selects the highest level the build machine supports, so use it only for executables that run on the machine that built them or on identical machines. + +The level applies to the program's own code. The runtime pack keeps its baseline code. Builds for different levels never share cached artifacts, and `baseline` builds are the same as builds without the option. + ## LLVM backend scriptc generates LLVM IR and compiles it to native code. Executable builds link the result with the matching precompiled runtime pack. Use --emit=llvm to inspect the generated code. diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts index 242a7c6f7..f14866a9c 100644 --- a/packages/cli/src/bootstrap.ts +++ b/packages/cli/src/bootstrap.ts @@ -70,6 +70,8 @@ async function tryFastPath(): Promise { values["profile-generate"] !== undefined || values["profile-use"] !== undefined || values.sparse || + // The full compiler resolves `native` against the build host. + values["target-cpu"] !== undefined || (values["external-types"] ?? []).length > 0 ) return null; @@ -200,6 +202,7 @@ async function coverageFastPath( values.emit !== undefined || values.strip || values["detached-exit"] || + values["target-cpu"] !== undefined || values.lib || values.profile !== undefined || values.ffi !== undefined || diff --git a/packages/cli/test/target-cpu.test.ts b/packages/cli/test/target-cpu.test.ts new file mode 100644 index 000000000..c5852fb40 --- /dev/null +++ b/packages/cli/test/target-cpu.test.ts @@ -0,0 +1,66 @@ +import { execFile } from "node:child_process"; +import { createRequire } from "node:module"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { hostTargetCpu } from "../../compiler/src/backend/target-cpu.js"; + +const execFileAsync = promisify(execFile); +const require = createRequire(import.meta.url); +const repoRoot = join(import.meta.dirname, "../../.."); +const cliEntry = join(repoRoot, "packages/cli/src/main.ts"); +const bootstrapEntry = join(repoRoot, "packages/cli/dist/bootstrap.js"); +const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs"); +const host = hostTargetCpu(); +const runsV3 = host === "x86-64-v3" || host === "x86-64-v4"; + +const program = `const a = new Int32Array(4096); +for (let i = 0; i < a.length; i++) a[i] = (i * 2654435761) | 0; +let s = 0; +for (let r = 0; r < 64; r++) for (let i = 0; i < a.length; i++) s = (s + a[i]!) | 0; +console.log(s); +`; + +test.runIf(process.platform === "linux" && process.arch === "x64")( + "--target-cpu compiles the program for the level without mixing cache variants", + async () => { + const dir = await mkdtemp(join("/tmp", "scriptc-target-cpu-")); + const entry = join(dir, "main.ts"); + const outPath = join(dir, "program"); + const llvmPath = join(dir, "main.ll"); + const env = { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") }; + const build = async (args: string[], bootstrap = false): Promise => { + await execFileAsync( + process.execPath, + [ + ...(bootstrap ? [bootstrapEntry] : ["--import", tsxLoader, cliEntry]), + "build", + entry, + "-o", + outPath, + ...args, + ], + { env, maxBuffer: 4 * 1024 * 1024 }, + ); + return readFile(llvmPath, "utf8"); + }; + try { + await writeFile(entry, program); + const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`; + const baseline = await build([]); + expect(baseline).not.toContain('"target-cpu"'); + expect(await build(["--target-cpu=baseline"])).toBe(baseline); + const v3 = await build(["--target-cpu=x86-64-v3"]); + expect(v3).toMatch(/^attributes #0 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m); + if (runsV3) expect((await execFileAsync(outPath)).stdout).toBe(expected); + // The bootstrap's cached route builds without a level. + expect(await build([], true)).toBe(baseline); + expect((await execFileAsync(outPath)).stdout).toBe(expected); + await expect(build(["--target-cpu=haswell"])).rejects.toThrow(/unknown target CPU "haswell"/); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + 240_000, +); diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 16a1b1601..e5258e583 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -64,6 +64,7 @@ import { literalEqualityDefinition } from "./string-literal-equality.js"; import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js"; import { emitLibraryIdentityLines } from "../library-identity-markers.js"; import { InternalCompilerError } from "../../errors.js"; +import { targetCpuAttribute } from "../target-cpu.js"; /** The function bodies emitted in `chunks` contiguous ranges, each by its * own emitter (LlEmitter.chunkEmitter), then merged in function order: the @@ -191,6 +192,20 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st } } +/** The attribute groups of the emitted functions: #0 for every function, + * #1 for coroutine bodies and #2 for out-of-line record clones. Each carries + * the build's `--target-cpu` level. */ +function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean): string[] { + const cpu = targetCpuAttribute(emitter.targetCpu); + return [ + `attributes #0 = { sanitize_address${cpu} }`, + ...(emitter.usesCoroutines + ? [`attributes #1 = { sanitize_address presplitcoroutine${cpu} }`] + : []), + ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address${cpu} }`] : []), + ]; +} + export function emitModuleParts(emitter: LlEmitter): string[] { // Function bodies first (the literal/unit/fn-value tables fill as they // emit), then the file assembles around them — the runtime ABI’s order. @@ -972,9 +987,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // symbols specified by the library IR instead. for (const line of emitter.emitLibDefs(globals, globalReleaseLines, stamps)) out.push(line); out.push(...emitter.inlineRcTail(flushedDecls)); - out.push(`attributes #0 = { sanitize_address }`); - if (emitter.usesCoroutines) out.push(`attributes #1 = { sanitize_address presplitcoroutine }`); - if (hasNoInlineRecordClone) out.push(`attributes #2 = { noinline sanitize_address }`); + out.push(...functionAttributes(emitter, hasNoInlineRecordClone)); out.push(``); out.push(...emitter.fieldAliasMetadata()); if (emitter.debug !== null) out.push(emitter.debug.render()); @@ -1122,9 +1135,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] { // sanitize_address is inert under the plain pipeline; the sanitized // lane's -fsanitize=address link activates instrumentation over the // emitted functions too (the runtime TUs get theirs from clang). - `attributes #0 = { sanitize_address }`, - ...(emitter.usesCoroutines ? [`attributes #1 = { sanitize_address presplitcoroutine }`] : []), - ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address }`] : []), + ...functionAttributes(emitter, hasNoInlineRecordClone), ``, ); if (emitter.mod.workers) { diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index e6334ab4c..7dffc9183 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -202,6 +202,16 @@ test("detached exit defines the runtime opt-in symbol only when requested", () = expect(detached.replace("@scr_detached_exit = constant i8 1\n", "")).toBe(plain); }); +test("a target CPU level reaches every function attribute group and nothing else", () => { + const module = moduleFor([call()]); + const plain = emitLlvmModule(module); + expect(plain).not.toContain("target-cpu"); + const v3 = emitLlvmModule(module, { targetCpu: "x86-64-v3" }); + expect(v3).toMatch(/^attributes #0 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m); + expect(v3.replaceAll(' "target-cpu"="x86-64-v3"', "")).toBe(plain); + expect(emitLlvmModule(module, { targetCpu: "baseline" })).toBe(plain); +}); + test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => { for (const boxed of [false, true]) { const module = moduleFor(Array.from({ length: 4 }, call), { boxed }); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 4aa9b65ad..2e46f60b9 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1,3 +1,4 @@ +import type { TargetCpu } from "../target-cpu.js"; import { relieveHeapPressure } from "../../heap-pressure.js"; import { wasmBoundsChecks } from "./wasm-modules.js"; import { jsTruthy } from "./jsval.js"; @@ -482,6 +483,10 @@ export interface LlvmTargetOptions { * store the instance's address over 8 in 32 bits (classes.ts * compactRefLoadLines). Elsewhere they keep 8-byte pointers. */ lowRegion?: boolean; + /** The x86-64 level the program's code may require (`--target-cpu`): + * every emitted function carries it as its `target-cpu`. Unset, code + * runs on any x86-64 CPU. */ + targetCpu?: TargetCpu; } /** LlvmTargetOptions.lowRegion for an executable: Linux targets without @@ -1048,6 +1053,7 @@ export class LlEmitter { readonly emitLibraryIdentity: boolean; readonly runtimeAbiMarker: boolean; readonly detachedExit: boolean; + readonly targetCpu: TargetCpu | undefined; readonly wasmBoundsChecks: boolean; /** Interned string literals: UTF-8 text → { symbol, byte length } — * first-use order, the runtime ABI’s determinism discipline. */ @@ -1630,6 +1636,7 @@ export class LlEmitter { this.emitLibraryIdentity = options.emitLibraryIdentity !== false; this.runtimeAbiMarker = options.runtimeAbiMarker === true; this.detachedExit = options.detachedExit === true; + this.targetCpu = options.targetCpu; this.wasmBoundsChecks = options.wasmBoundsChecks === true || wasmBoundsChecks({ diff --git a/packages/compiler/src/backend/target-cpu.test.ts b/packages/compiler/src/backend/target-cpu.test.ts new file mode 100644 index 000000000..428caee32 --- /dev/null +++ b/packages/compiler/src/backend/target-cpu.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from "vitest"; +import { + isTargetCpuOption, + targetCpuAttribute, + targetCpuField, + targetCpuKeyParts, + targetCpuTargetRefusal, + x86Level, +} from "./target-cpu.js"; + +const V2 = ["cx16", "lahf_lm", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3"]; +const V3 = ["avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "abm", "movbe", "xsave"]; +const V4 = ["avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"]; +const level = (flags: string[]) => x86Level((flag) => flags.includes(flag)); + +test("a host's level is the highest one whose features and lower levels' features it has", () => { + expect(level([])).toBe("baseline"); + expect(level(V2)).toBe("x86-64-v2"); + expect(level([...V2, ...V3])).toBe("x86-64-v3"); + expect(level([...V2, ...V3, ...V4])).toBe("x86-64-v4"); + // AVX-512 without BMI2 is still v2. + expect(level([...V2, ...V3.filter((f) => f !== "bmi2"), ...V4])).toBe("x86-64-v2"); + expect(level([...V3, ...V4])).toBe("baseline"); +}); + +test("the baseline adds no key part, attribute or option field", () => { + for (const cpu of [undefined, "baseline"] as const) { + expect(targetCpuKeyParts(cpu)).toEqual([]); + expect(targetCpuAttribute(cpu)).toBe(""); + expect(targetCpuField(cpu)).toEqual({}); + expect(targetCpuTargetRefusal(cpu, "linux", "arm64")).toBeNull(); + } + expect(targetCpuKeyParts("x86-64-v3")).toEqual(["target-cpu-x86-64-v3"]); + expect(targetCpuAttribute("x86-64-v4")).toBe(' "target-cpu"="x86-64-v4"'); + expect(targetCpuField("x86-64-v2")).toEqual({ targetCpu: "x86-64-v2" }); +}); + +test("a level is refused for targets that are not x86-64", () => { + expect(targetCpuTargetRefusal("x86-64-v3", "linux", "x64")).toBeNull(); + expect(targetCpuTargetRefusal("x86-64-v3", "darwin", "arm64")).toBe( + "--target-cpu=x86-64-v3 requires an x86-64 target", + ); + expect(targetCpuTargetRefusal("x86-64-v3", "wasi", "x64")).not.toBeNull(); +}); + +test("the option accepts the levels and native", () => { + for (const value of ["baseline", "x86-64-v2", "x86-64-v3", "x86-64-v4", "native"]) + expect(isTargetCpuOption(value)).toBe(true); + for (const value of ["x86-64", "haswell", "", undefined]) + expect(isTargetCpuOption(value)).toBe(false); +}); diff --git a/packages/compiler/src/backend/target-cpu.ts b/packages/compiler/src/backend/target-cpu.ts new file mode 100644 index 000000000..228652ab3 --- /dev/null +++ b/packages/compiler/src/backend/target-cpu.ts @@ -0,0 +1,119 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { nativeCodegenTarget } from "./targets.js"; + +/** The x86-64 microarchitecture levels an executable may require + * (`--target-cpu`). `baseline` is the x86-64 baseline every release build + * targets; the other levels are LLVM's `x86-64-v2` to `x86-64-v4`. */ +export type TargetCpu = "baseline" | "x86-64-v2" | "x86-64-v3" | "x86-64-v4"; + +/** The values `--target-cpu` accepts: a level, or `native` for the highest + * level the build host supports. */ +export const TARGET_CPU_OPTIONS = ["baseline", "x86-64-v2", "x86-64-v3", "x86-64-v4", "native"]; + +export function isTargetCpuOption(value: unknown): value is TargetCpu | "native" { + return typeof value === "string" && TARGET_CPU_OPTIONS.includes(value); +} + +/** The CPU features each level adds to the one below it, as Linux names + * them in /proc/cpuinfo (`pni` is SSE3, `abm` includes LZCNT). */ +const LEVEL_FLAGS: readonly [TargetCpu, readonly string[]][] = [ + ["x86-64-v2", ["cx16", "lahf_lm", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3"]], + ["x86-64-v3", ["avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "abm", "movbe", "xsave"]], + ["x86-64-v4", ["avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"]], +]; + +/** The macOS `machdep.cpu` spellings of the same features. */ +const DARWIN_FLAGS: Readonly> = { + cx16: "CX16", + lahf_lm: "LAHF", + popcnt: "POPCNT", + pni: "SSE3", + sse4_1: "SSE4.1", + sse4_2: "SSE4.2", + ssse3: "SSSE3", + avx: "AVX1.0", + avx2: "AVX2", + bmi1: "BMI1", + bmi2: "BMI2", + f16c: "F16C", + fma: "FMA", + abm: "LZCNT", + movbe: "MOVBE", + xsave: "XSAVE", + avx512f: "AVX512F", + avx512bw: "AVX512BW", + avx512cd: "AVX512CD", + avx512dq: "AVX512DQ", + avx512vl: "AVX512VL", +}; + +/** The highest level whose features (and every lower level's) are all in + * `has`. */ +export function x86Level(has: (flag: string) => boolean): TargetCpu { + let level: TargetCpu = "baseline"; + for (const [next, flags] of LEVEL_FLAGS) { + if (!flags.every(has)) break; + level = next; + } + return level; +} + +/** The highest level the build host supports, or null when the host is not + * an x86-64 Linux or macOS machine. */ +export function hostTargetCpu( + platform: string = process.platform, + arch: string = process.arch, +): TargetCpu | null { + if (arch !== "x64") return null; + try { + if (platform === "linux") { + const info = readFileSync("/proc/cpuinfo", "utf8"); + const line = /^flags\s*:(.*)$/m.exec(info); + if (!line) return null; + const flags = new Set(line[1]!.trim().split(/\s+/)); + return x86Level((flag) => flags.has(flag)); + } + if (platform === "darwin") { + const text = execFileSync( + "sysctl", + ["-n", "machdep.cpu.features", "machdep.cpu.leaf7_features", "machdep.cpu.extfeatures"], + { encoding: "utf8" }, + ); + const flags = new Set(text.trim().split(/\s+/)); + return x86Level((flag) => flags.has(DARWIN_FLAGS[flag]!)); + } + } catch { + return null; + } + return null; +} + +/** Cache-key parts for a target level; empty for the baseline so existing + * keys are unchanged. */ +export function targetCpuKeyParts(cpu: TargetCpu | undefined): string[] { + return cpu === undefined || cpu === "baseline" ? [] : [`target-cpu-${cpu}`]; +} + +/** The function attribute that makes LLVM generate code for the level, or + * "" for the baseline. */ +export function targetCpuAttribute(cpu: TargetCpu | undefined): string { + return cpu === undefined || cpu === "baseline" ? "" : ` "target-cpu"="${cpu}"`; +} + +/** The options field for a level: omitted for the baseline. */ +export function targetCpuField(cpu: TargetCpu | undefined): { targetCpu?: TargetCpu } { + return cpu === undefined || cpu === "baseline" ? {} : { targetCpu: cpu }; +} + +/** The error for a level on a target that is not x86-64, or null. */ +export function targetCpuTargetRefusal( + cpu: TargetCpu | undefined, + platform: string, + architecture: string = nativeCodegenTarget()?.architecture ?? process.arch, +): string | null { + if (cpu === undefined || cpu === "baseline") return null; + if (architecture !== "x64" || platform === "wasi") + return `--target-cpu=${cpu} requires an x86-64 target`; + return null; +} diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts index 3285aa7fa..57b15349e 100644 --- a/packages/compiler/src/cli/command.ts +++ b/packages/compiler/src/cli/command.ts @@ -15,6 +15,13 @@ import { buildEnvelope } from "../diagnostics/envelope.js"; import { renderWarnings } from "../diagnostics/render.js"; import { isPerformanceCode, renderPerformanceWarnings } from "../diagnostics/dynamic-tier.js"; import { isNativeOptimization } from "../backend/optimization.js"; +import { + hostTargetCpu, + isTargetCpuOption, + TARGET_CPU_OPTIONS, + targetCpuField, + type TargetCpu, +} from "../backend/target-cpu.js"; import { setProvenanceSources } from "../frontend/provenance-registry.js"; import type { CliHost, NativeCacheWarmProfile } from "./host.js"; import { resolveOutputOptions } from "./output-options.js"; @@ -96,6 +103,7 @@ async function main(args: string[], host: CliHost): Promise { values.strip || values["windows-subsystem"] !== undefined || values["detached-exit"] || + values["target-cpu"] !== undefined || values["profile-generate"] !== undefined || values["profile-use"] !== undefined || values.sparse || @@ -154,6 +162,7 @@ async function main(args: string[], host: CliHost): Promise { values.sanitize || values["windows-subsystem"] !== undefined || values["detached-exit"] || + values["target-cpu"] !== undefined || values["profile-generate"] !== undefined || values["profile-use"] !== undefined || (values["npm-static"] ?? []).length > 0 || @@ -210,6 +219,7 @@ async function main(args: string[], host: CliHost): Promise { values.ffi !== undefined || values["windows-subsystem"] !== undefined || values["detached-exit"] || + values["target-cpu"] !== undefined || values["profile-generate"] !== undefined || values["profile-use"] !== undefined || (values["npm-static"] ?? []).length > 0 || @@ -218,7 +228,7 @@ async function main(args: string[], host: CliHost): Promise { values["explain-dynamic"] !== undefined ) { fail( - "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only", + "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--target-cpu/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only", ); } const profilePath = resolve(profileArg); @@ -261,6 +271,9 @@ async function main(args: string[], host: CliHost): Promise { if (command === "coverage" && values.strip) { fail(`--strip is a build/run option\n\n${USAGE}`); } + if (command === "coverage" && values["target-cpu"] !== undefined) { + fail(`--target-cpu is a build/run option\n\n${USAGE}`); + } if (command === "coverage" && values["detached-exit"]) { fail(`--detached-exit is a build/run option\n\n${USAGE}`); } @@ -377,6 +390,19 @@ async function main(args: string[], host: CliHost): Promise { if (windowsSubsystem !== undefined && command === "coverage") { fail(`--windows-subsystem is only supported for executable builds\n\n${USAGE}`); } + const targetCpuOption = values["target-cpu"]; + if (targetCpuOption !== undefined && !isTargetCpuOption(targetCpuOption)) { + fail( + `unknown target CPU "${targetCpuOption}" (supported: ${TARGET_CPU_OPTIONS.join(", ")})\n\n${USAGE}`, + ); + } + let targetCpu: TargetCpu | undefined = targetCpuOption === "native" ? undefined : targetCpuOption; + if (targetCpuOption === "native") { + const level = hostTargetCpu(); + if (level === null) + fail(`--target-cpu=native needs an x86-64 Linux or macOS build host; name a level instead`); + targetCpu = level; + } const output = command === "coverage" ? null @@ -498,6 +524,7 @@ async function main(args: string[], host: CliHost): Promise { ...(values.strip ? { strip: true } : {}), ...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}), ...(values["detached-exit"] ? { detachedExit: true } : {}), + ...targetCpuField(targetCpu), // The instrumented program resolves a relative pattern against its own // working directory; resolve it here, where the user wrote it. ...(values["profile-generate"] === undefined diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index c4d07656a..01afbdd41 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -54,6 +54,12 @@ Options: a helper process so the parent sees the exit sooner. The helper keeps the executable mapped and its memory charged for up to ~100 ms after exit (opt in; --emit=exe only) + --target-cpu + x86-64: compile the program's code for a CPU level + (default: baseline, which runs on any x86-64 CPU). The + executable then requires that level: v3 adds AVX2, BMI2 + and FMA, v4 adds AVX-512. native uses the build host's + level. The runtime keeps its baseline code --profile-generate Linux: instrument the executable for profile-guided optimization. Each run writes a raw profile to the @@ -123,6 +129,7 @@ export const CLI_OPTIONS = { strip: { type: "boolean", default: false }, "windows-subsystem": { type: "string" }, "detached-exit": { type: "boolean", default: false }, + "target-cpu": { type: "string" }, "profile-generate": { type: "string" }, "profile-use": { type: "string" }, sparse: { type: "boolean", default: false }, diff --git a/packages/compiler/src/compile-types.ts b/packages/compiler/src/compile-types.ts index 9ecd75974..c50931c6a 100644 --- a/packages/compiler/src/compile-types.ts +++ b/packages/compiler/src/compile-types.ts @@ -1,3 +1,4 @@ +import type { TargetCpu } from "./backend/target-cpu.js"; import type { WindowsSubsystem } from "./backend/targets.js"; import type { NativeLinkInfo } from "./backend/native-link-info.js"; import type { NativeOptimization } from "./backend/optimization.js"; @@ -36,6 +37,12 @@ export interface CompileBaseOptions { * executable mapped and the memory charged for up to ~100 ms after exit. * Executables only; no effect on other platforms. */ detachedExit?: boolean; + /** The x86-64 microarchitecture level the executable's code may require + * (`x86-64-v2` to `x86-64-v4`). The program's functions are compiled for + * that level, and the executable then runs only on CPUs that support it. + * Unset or `baseline`, the code runs on any x86-64 CPU. x86-64 targets + * only; the runtime pack keeps its baseline code. */ + targetCpu?: TargetCpu; /** Instrument the executable for profile-guided optimization. Each run * writes a raw profile to this path pattern (`%p` expands to the process * id; `LLVM_PROFILE_FILE` overrides the pattern at run time). Linux diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 5cf8c6588..923730e17 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -235,6 +235,8 @@ test("early executable keys isolate compile modes, paths, implementation, checke { ...f.options, optimization: "dev" }, { ...f.options, optimization: "speed" }, { ...f.options, detachedExit: true }, + { ...f.options, targetCpu: "x86-64-v3" }, + { ...f.options, targetCpu: "x86-64-v4" }, { ...f.options, profile: "profile-generate:/tmp/%p.profraw" }, { ...f.options, profile: "profile-use:digest" }, { ...f.options, checker: "tsc-ts-executable-identity" }, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 7b7febc06..343b03430 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -18,6 +18,7 @@ import { compilerReleaseVersion } from "../library/sidecar.js"; import { nativeArtifactDependenciesStillMatch } from "../backend/native/artifact-stamps.js"; import { type NativeArtifactDependency } from "../backend/native/contracts.js"; import { optimizationKeyParts } from "../backend/optimization.js"; +import { type TargetCpu, targetCpuKeyParts } from "../backend/target-cpu.js"; import { privateSiblingPath } from "../backend/build-cache.js"; import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js"; import { @@ -138,6 +139,8 @@ export interface EarlyExecutableCacheOptions { windowsSubsystem?: "gui"; /** Omitted unless --detached-exit is set. */ detachedExit?: true; + /** Omitted for the x86-64 baseline (`--target-cpu`). */ + targetCpu?: TargetCpu; /** Omitted without --profile-generate/--profile-use; otherwise the * pattern, or the digest of the profile's bytes. */ profile?: string; @@ -279,6 +282,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string { ...(options.strip ? ["strip"] : []), ...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []), ...(options.detachedExit ? ["detached-exit"] : []), + ...targetCpuKeyParts(options.targetCpu), ...(options.profile !== undefined ? [options.profile] : []), ...(options.checker !== undefined ? [`checker:${options.checker}`] : []), ...(options.youngNursery ? ["young-nursery"] : []), @@ -321,6 +325,7 @@ function routeKey(options: Omit] [--baseline=] [--workloads=a,b] [--runs=15] [--warmup=2] [--timeout=120] [--layouts=1] [--json=] [--baseline-optimization=release] [--candidate-optimization=release] + [--baseline-target-cpu=] [--candidate-target-cpu=] [--no-lock] [--no-node] [--keep] Each checkout must have a built CLI (packages/cli/dist/bootstrap.js) and native @@ -69,7 +72,10 @@ Workload names are the file names in benchmarks/runtime/workloads/*.json; for ex --baseline-optimization/--candidate-optimization pick each contender's --optimization posture (default release). The same checkout may be both contenders, e.g. --baseline=. --candidate-optimization=speed compares speed with -release. cold_build_ms records each contender's first, uncached build.`); +release. --baseline-target-cpu/--candidate-target-cpu pass --target-cpu to each +contender's builds, e.g. --baseline=. --candidate-target-cpu=x86-64-v3 compares +x86-64-v3 code with the baseline. cold_build_ms records each contender's first, +uncached build.`); process.exit(0); } const runs = Number(values.runs); @@ -428,7 +434,7 @@ function revision(root) { }); return r.stdout.trim() + (dirty.stdout.trim() ? "+dirty" : ""); } -function build(label, root, workload, layout, optimization) { +function build(label, root, workload, layout, optimization, targetCpu) { const cli = cliFor(root); const out = join(work, label, layout === 0 ? workload.name : `${workload.name}.layout${layout}`); mkdirSync(dirname(out), { recursive: true }); @@ -444,7 +450,15 @@ function build(label, root, workload, layout, optimization) { const entry = join(suiteRoot, workload.entry); const result = run( process.execPath, - [cli, "build", entry, `--optimization=${optimization}`, "-o", out], + [ + cli, + "build", + entry, + `--optimization=${optimization}`, + ...(targetCpu === undefined ? [] : [`--target-cpu=${targetCpu}`]), + "-o", + out, + ], { env, timeout: 900_000, @@ -467,6 +481,7 @@ const contenders = [ label: "baseline", root: resolve(values.baseline), optimization: values["baseline-optimization"], + targetCpu: values["baseline-target-cpu"], }, ] : []), @@ -474,6 +489,7 @@ const contenders = [ label: "candidate", root: resolve(values.candidate), optimization: values["candidate-optimization"], + targetCpu: values["candidate-target-cpu"], }, ]; const report = { @@ -580,7 +596,7 @@ try { entry.results[c.label] = result; result.binaries = []; for (let layout = 0; layout < layouts && result.status === undefined; layout++) { - const built = build(c.label, c.root, w, layout, c.optimization); + const built = build(c.label, c.root, w, layout, c.optimization, c.targetCpu); result.build_ms += Math.round(built.buildMs); if (layout === 0) result.cold_build_ms = Math.round(built.buildMs); if (!built.ok) { From 26678e3c72385b44fe784ffea31c6a3f7fe2318c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 13:20:59 -0500 Subject: [PATCH 145/409] Keep class methods native when element writes store data under their names --- benchmarks/runtime/README.md | 1 + .../workloads/keyed-option-writes.json | 6 ++ .../runtime/workloads/keyed-option-writes.ts | 76 ++++++++++++++++++ .../compiler/src/contracts/int32-elements.ts | 3 +- .../frontend/lowering/builtins/webassembly.ts | 5 +- .../lowering/class-method-mutations.ts | 53 +++++++++++-- .../lowering/containers/array-pipelines.ts | 4 +- .../lowering/containers/iterator-helpers.ts | 18 +++-- .../frontend/lowering/generic-functions.ts | 6 +- .../lowering/instantiation-path.test.ts | 12 ++- .../frontend/lowering/instantiation-path.ts | 6 +- .../compiler/src/frontend/lowering/lowerer.ts | 58 +++++++++----- .../lowering/site-dependencies.test.ts | 14 ++-- .../frontend/lowering/site-dependencies.ts | 15 +++- .../frontend/lowering/string-index-bounds.ts | 23 +++++- packages/compiler/src/frontend/npm-static.ts | 59 +++++++------- packages/compiler/src/frontend/services.ts | 9 ++- .../src/frontend/ts7/semantic-model.ts | 4 +- .../src/frontend/ts7/source-parser.ts | 4 +- .../compiler/test/keyed-method-writes.test.ts | 79 +++++++++++++++++++ 20 files changed, 361 insertions(+), 94 deletions(-) create mode 100644 benchmarks/runtime/workloads/keyed-option-writes.json create mode 100644 benchmarks/runtime/workloads/keyed-option-writes.ts create mode 100644 packages/compiler/test/keyed-method-writes.test.ts diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 4f152e4bb..f7c21571a 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -16,6 +16,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | +| `keyed-option-writes` | cpu | class methods whose names are also keys of option records written through element accesses: `enabled[name] = true` over a union of literal names and `totals["count"] = n` | | `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe | | `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs | | `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes | diff --git a/benchmarks/runtime/workloads/keyed-option-writes.json b/benchmarks/runtime/workloads/keyed-option-writes.json new file mode 100644 index 000000000..802911e7d --- /dev/null +++ b/benchmarks/runtime/workloads/keyed-option-writes.json @@ -0,0 +1,6 @@ +{ + "entry": "workloads/keyed-option-writes.ts", + "args": [ + "4" + ] +} diff --git a/benchmarks/runtime/workloads/keyed-option-writes.ts b/benchmarks/runtime/workloads/keyed-option-writes.ts new file mode 100644 index 000000000..95cdbb86b --- /dev/null +++ b/benchmarks/runtime/workloads/keyed-option-writes.ts @@ -0,0 +1,76 @@ +// Class methods whose names are also keys of option records written through +// element accesses: `enabled[name] = true` over a union of literal names and +// `totals["count"] = n`. The writes store booleans and numbers into plain +// records, so the methods keep their native calls. +type Step = "scale" | "offset" | "clamp"; + +const enabled: Record = { scale: false, offset: false, clamp: false }; +function enable(name: Step): void { + enabled[name] = true; +} + +const totals: Record = {}; + +class Transform { + private readonly factor: number; + private readonly shift: number; + constructor(factor: number, shift: number) { + this.factor = factor; + this.shift = shift; + } + scale(x: number): number { + return enabled.scale ? x * this.factor : x; + } + offset(x: number): number { + return enabled.offset ? x + this.shift : x; + } + clamp(x: number): number { + if (!enabled.clamp) return x; + return x < 0 ? 0 : x > 1000 ? 1000 : x; + } +} + +class Counter { + private seen = 0; + private sum = 0; + add(value: number): void { + this.seen++; + this.sum += value; + } + count(): number { + return this.seen; + } + mean(): number { + return this.seen === 0 ? 0 : this.sum / this.seen; + } +} + +const scale = Number(process.argv[2] ?? "1"); +const steps: Step[] = ["scale", "offset", "clamp"]; +for (const step of steps) enable(step); + +const transforms: Transform[] = []; +for (let i = 0; i < 16; i++) transforms.push(new Transform(1 + (i % 5) * 0.25, (i % 7) - 3)); +const counters: Counter[] = []; +for (let i = 0; i < 8; i++) counters.push(new Counter()); + +let seed = 12345; +let checksum = 0; +const rounds = Math.floor(2_000_000 * scale); +for (let i = 0; i < rounds; i++) { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0; + const t = transforms[seed & 15]!; + const value = t.clamp(t.offset(t.scale((seed >>> 8) % 1200))); + const counter = counters[(seed >>> 4) & 7]!; + counter.add(value); + checksum += value; + if ((i & 65535) === 0) totals["count"] = counter.count(); +} + +let mean = 0; +let seen = 0; +for (const counter of counters) { + mean += counter.mean(); + seen += counter.count(); +} +console.log("seen", seen, "count", totals["count"], "mean", (mean / counters.length).toFixed(4), "checksum", checksum); diff --git a/packages/compiler/src/contracts/int32-elements.ts b/packages/compiler/src/contracts/int32-elements.ts index ca61a8e49..67446f2d1 100644 --- a/packages/compiler/src/contracts/int32-elements.ts +++ b/packages/compiler/src/contracts/int32-elements.ts @@ -709,7 +709,8 @@ export class Int32ElementContracts { const findChainsIn = (st: IrStmt): boolean => everyStmtChild(st, findChains, findChainsIn); for (const st of fn.body) findChainsIn(st); // Parameters whose callers are not all visible hold opaque arrays. - if (this.valueEscaped.has(fn.name) || this.runtimeVirtual(fn.name)) + const runtimeVirtual = this.runtimeVirtual; + if (this.valueEscaped.has(fn.name) || runtimeVirtual(fn.name)) for (const p of fn.params) if (this.holds(p.type)) { const why = this.valueEscaped.has(fn.name) diff --git a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts index 8e5c3a048..f33ebe9fe 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts @@ -403,7 +403,10 @@ export function lowerWebAssemblyValueOf( recv: IrExpr, loc: IrExpr["loc"], ): IrExpr | null { - const ctx = { checker: lowerer.checker, isStdlibFile: lowerer.isStdlibFile }; + const ctx = { + checker: lowerer.checker, + isStdlibFile: (sf: ts.SourceFile) => lowerer.isStdlibFile(sf), + }; if (!isWebAssemblyType(lowerer.typeOf(receiver), ctx)) return null; lowerer.usesWebAssembly = true; return { kind: "libCall", fn: "wasm.valueOf", args: [recv], type: DYN, loc }; diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index fd23b6db8..d75f1d499 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -36,9 +36,20 @@ export function collectClassMethodMutations( const computedWrites: { key: ts.Expression | undefined; receiver: ts.Expression | undefined; + /** The written element access, when the write has one. */ + target?: ts.ElementAccessExpression; site: ts.Node; }[] = []; const propertyWrites: { target: ts.PropertyAccessExpression; site: ts.Node }[] = []; + /** Writes and deletes of a method name through an element access with a + * known key (`o["m"] = f`, `o[k] = f` with `k: "m" | "n"`), judged like + * the named writes in propertyWrites. */ + const keyedWrites: { + name: string; + receiver: ts.Expression; + target: ts.ElementAccessExpression; + site: ts.Node; + }[] = []; const reflection: ts.PropertyAccessExpression[] = []; const globals: ts.Identifier[] = []; const jsAssignments: (ts.PropertyAccessExpression | ts.ElementAccessExpression)[] = []; @@ -242,13 +253,32 @@ export function collectClassMethodMutations( sameBinding(receiver, keys.arguments.nodes[0]!) ); }; - const markKey = (key: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => { + /** A write of the known key `name`: through an element access, a write + * to a method name is judged like a named write (keyedWrites). */ + const markKeyed = ( + name: string, + site: ts.Node, + receiver?: ts.Expression, + target?: ts.ElementAccessExpression, + ): void => { + if (receiver && target && methods.has(name) && name !== "prototype" && name !== "__proto__") { + receiverFields.add(name); + keyedWrites.push({ name, receiver, target, site }); + } else markName(name, site); + }; + const markKey = ( + key: ts.Expression, + site: ts.Node, + receiver?: ts.Expression, + target?: ts.ElementAccessExpression, + ): void => { const value = unwrap(key); - if (ts.isStringLiteralLike(value)) markName(value.text, site); - else if (ts.isNumericLiteral(value)) markName(String(Number(value.text)), site); + if (ts.isStringLiteralLike(value)) markKeyed(value.text, site, receiver, target); + else if (ts.isNumericLiteral(value)) + markKeyed(String(Number(value.text)), site, receiver, target); // The key keeps its assertions: `slots[at as number]` and `slots[at!]` // write a number key whatever the type of `at`. - else computedWrites.push({ key, receiver, site }); + else computedWrites.push(target ? { key, receiver, target, site } : { key, receiver, site }); }; const markFields = (value: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => { value = unwrap(value); @@ -275,7 +305,7 @@ export function collectClassMethodMutations( propertyWrites.push({ target, site }); } else markName(name, site, arrow); } else if (ts.isElementAccessExpression(target)) - markKey(target.argumentExpression, site, target.expression); + markKey(target.argumentExpression, site, target.expression, target); else if (ts.isArrayLiteralExpression(target)) { for (const element of target.elements.nodes) { if (ts.isSpreadElement(element)) markTarget(element.expression, site); @@ -575,7 +605,7 @@ export function collectClassMethodMutations( if (!methodOwnerCanInhabit(target.expression, name)) continue; lowerer.prototypeMethodAccesses.set(name, site); } - for (const { key, receiver, site } of computedWrites) { + for (const { key, receiver, target, site } of computedWrites) { // A class method declared with a well-known symbol key is not a write; // any other write with one can replace that method. const symbolName = key ? wellKnownMethodName(unwrap(key)) : null; @@ -592,7 +622,7 @@ export function collectClassMethodMutations( const type = key ? lowerer.typeOf(key) : undefined; const names = type && (literalValues(type) ?? (key ? literalLoopKeys(unwrap(key)) : null)); if (names) { - for (const name of names) markName(String(name), site); + for (const name of names) markKeyed(String(name), site, receiver, target); continue; } if (key && receiver && preservesExistingMethod(key, receiver, site)) continue; @@ -614,6 +644,15 @@ export function collectClassMethodMutations( unknownKey = site; } } + lowerer.checker.prefetchClassCollection( + keyedWrites.flatMap((write) => [write.target, write.receiver]), + [], + ); + for (const { name, receiver, target, site } of keyedWrites) { + if (lowerer.prototypeMethodAccesses.has(name) || functionFree(lowerer.typeOf(target))) continue; + if (!methodOwnerCanInhabit(receiver, name)) continue; + lowerer.prototypeMethodAccesses.set(name, site); + } if (numericKey) { for (const name of [...methods, ...arrowFields]) { if (String(Number(name)) === name) markName(name, numericKey); diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index 2f839a20b..ff7da5fd7 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1019,7 +1019,9 @@ class FusedLoop { private callee(arg: IrExpr & { type: FuncType }): Callee { const known = this.callees.find((c) => c.arg === arg); if (known) return known; - const target = this.inlineTarget(arg); + // Called as a plain function: the loop is not the target's receiver. + const inlineTarget = this.inlineTarget; + const target = inlineTarget(arg); let callee: Callee; if (target && arg.kind === "closure") { const captures = new Map(); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index ba5d0684d..f6580c70e 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -1182,10 +1182,10 @@ function bindSource( /** Where chain code allocates its locals: a lifted helper function's own * frame, or hidden locals of the function being lowered (a fused for-of). */ -interface ChainFrame { - local(base: string, type: IrType, mutable: boolean): string; +abstract class ChainFrame { + abstract local(base: string, type: IrType, mutable: boolean): string; /** `id = id + 1` over a number local. */ - bump(id: string): IrStmt; + abstract bump(id: string): IrStmt; } /** `id = id + 1` over a number local. */ @@ -1206,11 +1206,13 @@ function bumpLocal(id: string, loc: SrcLoc): IrStmt { } /** Hidden locals of the function being lowered. */ -class HostFrame implements ChainFrame { +class HostFrame extends ChainFrame { constructor( private readonly lowerer: Lowerer, private readonly loc: SrcLoc, - ) {} + ) { + super(); + } local(base: string, type: IrType, mutable: boolean): string { const local = this.lowerer.declareHiddenLocal(`%${base}`, type); local.mutable = mutable; @@ -1223,11 +1225,13 @@ class HostFrame implements ChainFrame { /** The locals and parameters of a lifted helper function under * construction. */ -export class LiftedFrame implements ChainFrame { +export class LiftedFrame extends ChainFrame { readonly locals: IrLocal[] = []; readonly params: IrParam[] = []; private n = 0; - constructor(private readonly loc: SrcLoc) {} + constructor(private readonly loc: SrcLoc) { + super(); + } local(base: string, type: IrType, mutable: boolean): string { const id = `${base}.${this.n++}`; this.locals.push({ id, name: base, type, mutable }); diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 222cb89f9..7896da578 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -645,8 +645,7 @@ function extendsParameter( info: GenericFnInfo, ): boolean { const params = new Set(); - const fn = info.decl as { parameters?: { nodes: readonly ts.ParameterDeclaration[] } }; - for (const param of fn.parameters?.nodes ?? []) { + for (const param of info.decl.parameters.nodes) { const symbol = ts.isIdentifier(param.name) ? lowerer.checker.getSymbolAtLocation(param.name) : undefined; @@ -672,8 +671,7 @@ function declaredInside(node: ts.Node, owner: ts.Node): boolean { * return gives anything else (or nothing). Nested functions and classes keep * their own returns. */ function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLikeDeclaration | null { - const fn = info.decl as ts.Node & { body?: ts.Node }; - const body = fn.body; + const body = info.decl.body; if (!body) return null; const classOf = (node: ts.Expression): ts.ClassLikeDeclaration | null => { let expr = node; diff --git a/packages/compiler/src/frontend/lowering/instantiation-path.test.ts b/packages/compiler/src/frontend/lowering/instantiation-path.test.ts index a31617106..267b402bc 100644 --- a/packages/compiler/src/frontend/lowering/instantiation-path.test.ts +++ b/packages/compiler/src/frontend/lowering/instantiation-path.test.ts @@ -1,19 +1,23 @@ import { expect, test } from "vitest"; +import type * as ts from "../ts7/adapter.js"; import { extendInstantiationPath, MAX_INSTANTIATION_RECURSION } from "./instantiation-path.js"; +/** A stand-in declaration: paths compare declarations by identity only. */ +const node = (): ts.Node => ({}) as ts.Node; + test("independent declarations and roots do not consume a recursive budget", () => { - const declaration = {}; + const declaration = node(); for (let i = 0; i < MAX_INSTANTIATION_RECURSION * 2; i++) expect(extendInstantiationPath(null, declaration)).not.toBeNull(); let path = extendInstantiationPath(null, declaration)!; for (let i = 0; i < MAX_INSTANTIATION_RECURSION * 2; i++) - path = extendInstantiationPath(path, {})!; + path = extendInstantiationPath(path, node())!; expect(extendInstantiationPath(path, declaration)).not.toBeNull(); }); test("bounds fresh recursive demands across intervening declarations", () => { - const first = {}; - const second = {}; + const first = node(); + const second = node(); let path = extendInstantiationPath(null, first)!; for (let i = 1; i < MAX_INSTANTIATION_RECURSION; i++) { path = extendInstantiationPath(path, second)!; diff --git a/packages/compiler/src/frontend/lowering/instantiation-path.ts b/packages/compiler/src/frontend/lowering/instantiation-path.ts index d79760988..b4a8375f6 100644 --- a/packages/compiler/src/frontend/lowering/instantiation-path.ts +++ b/packages/compiler/src/frontend/lowering/instantiation-path.ts @@ -1,7 +1,9 @@ +import type * as ts from "../ts7/adapter.js"; + /** Queued bodies preserve their demand ancestry. Independent concrete uses * do not consume a recursion budget; only fresh instances on one path do. */ export interface InstantiationPath { - declaration: object; + declaration: ts.Node; parent: InstantiationPath | null; } @@ -9,7 +11,7 @@ export const MAX_INSTANTIATION_RECURSION = 100; export function extendInstantiationPath( parent: InstantiationPath | null, - declaration: object, + declaration: ts.Node, ): InstantiationPath | null { let repetitions = 0; for (let path = parent; path; path = path.parent) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3c74cfbad..ae2734ad4 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3694,7 +3694,7 @@ export class Lowerer { positionOf: (node) => this.positions.of(node), shapes: this.shapes, unions: this.unions, - classNamer: this.classNamer, + classNamer: (decl) => this.classNamer(decl), resolveTypeParam: this.typeParamResolver, resolveTypeParamTs: this.typeParamTsResolver, isErasing: () => this.erasingDepth > 0, @@ -3775,8 +3775,8 @@ export class Lowerer { }, mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened), classImplementedProtocol: (type) => this.classImplementedProtocol(type), - isStdlibFile: this.isStdlibFile, - isNpmFile: this.isNpmFile, + isStdlibFile: (sf) => this.isStdlibFile(sf), + isNpmFile: (sf) => this.isNpmFile(sf), // The declaration-typed boundary's generated types (decl-projection.ts) // describe data, like an explicitly mapped external type surface. isExternalTypeFile: (sf) => @@ -8002,16 +8002,24 @@ export class Lowerer { lower: () => IrFunction | null; } >(); - const bodyRoots = (...roots: (ts.Node | undefined | null)[]): ts.Node[] => - roots.filter((root) => root !== undefined && root !== null); - const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] => - bodyRoots(...decl.parameters.nodes.map((param) => param.initializer), decl.body); - const classCtorRoots = (info: ClassInfo): ts.Node[] => - bodyRoots( - ...(info.ctor?.parameters?.nodes ?? []).map((param) => param.initializer), - info.ctor?.body, - ...info.fieldOrder.map((field) => field.initializer), - ); + // Roots are collected with plain pushes: spread arguments into a rest + // parameter pass the nodes as dynamic values. + const addRoot = (roots: ts.Node[], root: ts.Node | undefined | null): void => { + if (root !== undefined && root !== null) roots.push(root); + }; + const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] => { + const roots: ts.Node[] = []; + for (const param of decl.parameters.nodes) addRoot(roots, param.initializer); + addRoot(roots, decl.body); + return roots; + }; + const classCtorRoots = (info: ClassInfo): ts.Node[] => { + const roots: ts.Node[] = []; + for (const param of info.ctor?.parameters?.nodes ?? []) addRoot(roots, param.initializer); + addRoot(roots, info.ctor?.body); + for (const field of info.fieldOrder) addRoot(roots, field.initializer); + return roots; + }; const classMemberRoots = (info: ClassInfo): ts.Node[] => { const roots = classCtorRoots(info); for (const { member } of this.classMethodMembers(info)) roots.push(...functionRoots(member)); @@ -8188,7 +8196,8 @@ export class Lowerer { register(`%${cName}.set:${prop}`, [], () => this.throwingSetterFn(info, prop)); } }; - for (const name of collectionEdges) this.onEdge(name); + const onEdge = this.onEdge; + for (const name of collectionEdges) onEdge(name); // Generic instances queued by the bodies above lower here (an instance // body fires edges of its own and can queue further instances of // either kind — function instances and class instantiations drain to @@ -8323,7 +8332,8 @@ export class Lowerer { // profile-mapped exports are called from outside the graph, so they // seed the worklist beside the init bodies. Unknown names are inert // (the export-map resolution reports them as SC4002 later). - for (const root of extraRoots ?? []) this.onEdge?.(root); + const rootEdge = this.onEdge; + for (const root of extraRoots ?? []) rootEdge?.(root); const shuffle = lowerShuffle(); const drainUnits = (): void => { while (queue.length > 0) { @@ -8441,7 +8451,9 @@ export class Lowerer { /** Reachability hook (see emitReachable): fires when lowering resolves a * reference to a lowerable body. */ noteEdge(name: string): void { - if (this.onEdge) this.onEdge(name); + // Called as a plain function: the instance is not its receiver. + const onEdge = this.onEdge; + if (onEdge) onEdge(name); } /** A subclass constructor's super() edge: the base constructor runs, but @@ -11827,12 +11839,15 @@ export class Lowerer { /** Method-like members with their collected method-map names. Body * lowering omits abstract declarations; ABI analysis must include them - * because an abstract-typed call selects the same virtual slot. */ - *classMethodMembers( + * because an abstract-typed call selects the same virtual slot. An array, + * not a generator: a generator's yielded values are dynamic values, which + * would make every syntax node one. */ + classMethodMembers( info: ClassInfo, includeAbstract = false, - ): Generator<{ mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }> { - if (!info.decl) return; // builtin error classes: runtime-provided bodies + ): { mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }[] { + const members: { mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }[] = []; + if (!info.decl) return members; // builtin error classes: runtime-provided bodies for (const member of info.decl.members.nodes) { const fnLike = ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member) @@ -11856,8 +11871,9 @@ export class Lowerer { : `${ts.isGetAccessor(fnLike) ? "get" : "set"}:${baseName}`; const signature = info.methods.get(mName); if (!signature || (!fnLike.body && !(includeAbstract && signature.abstract))) continue; - yield { mName, member: fnLike }; + members.push({ mName, member: fnLike }); } + return members; } lowerClassCtor(info: ClassInfo): IrFunction { diff --git a/packages/compiler/src/frontend/lowering/site-dependencies.test.ts b/packages/compiler/src/frontend/lowering/site-dependencies.test.ts index ca5affcbe..0580a52ee 100644 --- a/packages/compiler/src/frontend/lowering/site-dependencies.test.ts +++ b/packages/compiler/src/frontend/lowering/site-dependencies.test.ts @@ -1,9 +1,13 @@ import { expect, test } from "vitest"; +import type * as ts from "../ts7/adapter.js"; import { SiteDependencies } from "./site-dependencies.js"; +/** A stand-in node fact: facts are compared by identity. */ +const node = (): ts.Node => ({}) as ts.Node; + test("a changed fact makes the sites that read it stale", () => { const dependencies = new SiteDependencies(3); - const a = {}; + const a = node(); const b = "key"; dependencies.begin(0); dependencies.read(a); @@ -20,8 +24,8 @@ test("a changed fact makes the sites that read it stale", () => { test("a visit that changes a fact makes every reader of its facts stale", () => { const dependencies = new SiteDependencies(3); - const shared = {}; - const own = {}; + const shared = node(); + const own = node(); dependencies.begin(0); dependencies.read(shared); dependencies.end(false); @@ -37,7 +41,7 @@ test("a visit that changes a fact makes every reader of its facts stale", () => test("reads outside a visit are not recorded", () => { const dependencies = new SiteDependencies(1); - const fact = {}; + const fact = node(); dependencies.read(fact); dependencies.changed(fact); expect(dependencies.isStale(0)).toBe(false); @@ -45,7 +49,7 @@ test("reads outside a visit are not recorded", () => { test("a site visited again is listed once per fact", () => { const dependencies = new SiteDependencies(3); - const fact = {}; + const fact = node(); for (const site of [0, 2, 1, 2, 0]) { dependencies.begin(site); dependencies.read(fact); diff --git a/packages/compiler/src/frontend/lowering/site-dependencies.ts b/packages/compiler/src/frontend/lowering/site-dependencies.ts index da8531e48..868531353 100644 --- a/packages/compiler/src/frontend/lowering/site-dependencies.ts +++ b/packages/compiler/src/frontend/lowering/site-dependencies.ts @@ -1,3 +1,5 @@ +import type * as ts from "../ts7/adapter.js"; + /** The facts each site of a fixed-point scan read on its last visit, so that * a pass visits again only the sites whose facts changed since. * @@ -13,14 +15,19 @@ * that with a pass over every site once a pass changes nothing, and resumes * when that pass changes a fact: a fact this tracking misses costs passes, * not a different result. */ +/** A fact a site reads: a symbol, a syntax node, or the name of a derived + * fact. A typed union keeps nodes and symbols native values; as `unknown` + * every syntax node would become a dynamic value. */ +export type SiteFact = ts.Node | ts.Symbol | string; + export class SiteDependencies { /** Each fact's entry: the visit that last read it, so that a visit lists a * fact once, followed by the sites that read it, ascending. */ - private readonly readers = new Map(); + private readonly readers = new Map(); private readonly stale: Uint8Array; private site = -1; private visit = 0; - private facts: unknown[] = []; + private facts: SiteFact[] = []; constructor(siteCount: number) { this.stale = new Uint8Array(siteCount); @@ -46,7 +53,7 @@ export class SiteDependencies { } /** The site being visited read `fact`. */ - read(fact: unknown): void { + read(fact: SiteFact): void { if (this.site < 0) return; const entry = this.readers.get(fact); if (entry === undefined) { @@ -72,7 +79,7 @@ export class SiteDependencies { } /** `fact` changed: every site that read it is visited again. */ - changed(fact: unknown): void { + changed(fact: SiteFact): void { const entry = this.readers.get(fact); if (entry !== undefined) for (let i = 1; i < entry.length; i++) this.stale[entry[i]!] = 1; } diff --git a/packages/compiler/src/frontend/lowering/string-index-bounds.ts b/packages/compiler/src/frontend/lowering/string-index-bounds.ts index af1b00ec6..6418aed76 100644 --- a/packages/compiler/src/frontend/lowering/string-index-bounds.ts +++ b/packages/compiler/src/frontend/lowering/string-index-bounds.ts @@ -182,11 +182,28 @@ export class StringIndexBounds { private readonly identifiers = new Map>(); constructor( - private readonly symbolAt: (node: ts.Node) => ts.Symbol | null, - private readonly declarationOf: (symbol: ts.Symbol) => ts.Node | undefined, - private readonly isString: (node: ts.Expression) => boolean, + private readonly lookupSymbol: (node: ts.Node) => ts.Symbol | null, + private readonly lookupDeclaration: (symbol: ts.Symbol) => ts.Node | undefined, + private readonly stringTyped: (node: ts.Expression) => boolean, ) {} + // The lookups are called as plain functions: the instance is not their + // receiver. + private symbolAt(node: ts.Node): ts.Symbol | null { + const lookup = this.lookupSymbol; + return lookup(node); + } + + private declarationOf(symbol: ts.Symbol): ts.Node | undefined { + const lookup = this.lookupDeclaration; + return lookup(symbol); + } + + private isString(node: ts.Expression): boolean { + const test = this.stringTyped; + return test(node); + } + /** True when `read` (an element access on a string receiver) is proven * to name an existing code unit. */ inBounds(read: ts.ElementAccessExpression): boolean { diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index eed02c95c..b00e14aa0 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -254,13 +254,18 @@ function aliasedPackageName(pkgDir: string): string | null { export function npmStaticTransformPkgJson(pkg: Record): void { delete pkg["types"]; delete pkg["typings"]; - const transform = (v: unknown): void => { - if (v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) transform(item); - return; - } - const obj = v as Record; + const transform = (v: unknown): unknown => { + if (v === null || typeof v !== "object") return v; + if (Array.isArray(v)) return v.map(transform); + // The result is a new object, built in key order; the first entry of a + // key wins. + const entries: [string, unknown][] = []; + const seen = new Set(); + const add = (key: string, value: unknown): void => { + if (seen.has(key)) return; + seen.add(key); + entries.push([key, value]); + }; // 0. The "node" condition HOISTS: Node's runtime resolution matches it // (yaml's `{ types, node: "./dist/index.js", default: "./browser/ // index.js" }` runs the node dist; the browser dist is a DIFFERENT @@ -270,24 +275,18 @@ export function npmStaticTransformPkgJson(pkg: Record): void { // under import+require (either matcher finds it there), an object's // entries splice inline — earlier real keys keep winning, exactly // Node's first-match-in-key-order rule. - if (obj["node"] !== undefined) { - const entries = Object.entries(obj); - for (const k of Object.keys(obj)) delete obj[k]; - for (const [k, val] of entries) { - if (k !== "node") { - if (!(k in obj)) obj[k] = val; - continue; - } - const inner: Record = - typeof val === "string" - ? { import: val, require: val } - : val !== null && typeof val === "object" && !Array.isArray(val) - ? (val as Record) - : {}; - for (const [ik, iv] of Object.entries(inner)) { - if (!(ik in obj)) obj[ik] = iv; - } + for (const [key, value] of Object.entries(v as Record)) { + if (key !== "node") { + add(key, value); + continue; } + const inner: Record = + typeof value === "string" + ? { import: value, require: value } + : value !== null && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; + for (const [innerKey, innerValue] of Object.entries(inner)) add(innerKey, innerValue); } // 1. The types claims go AFTER the hoist: a node object's own // "types" key (uuid's `node: { types: "./dist/index.d.ts", default: @@ -295,15 +294,17 @@ export function npmStaticTransformPkgJson(pkg: Record): void { // in place tsgo follows it, maps the hidden declaration to its // same-stem JS (dist/index.js, the browser build) and disagrees with // this resolver's runtime answer. - delete obj["types"]; - delete obj["typings"]; - for (const value of Object.values(obj)) transform(value); + return Object.fromEntries( + entries + .filter(([key]) => key !== "types" && key !== "typings") + .map(([key, value]): [string, unknown] => [key, transform(value)]), + ); }; - if (pkg["exports"] !== undefined) transform(pkg["exports"]); + if (pkg["exports"] !== undefined) pkg["exports"] = transform(pkg["exports"]); // The "imports" field maps "#" specifiers with the same condition objects // (chalk 5's `"#supports-color": { node, default }`), so Node's "node" // target hoists there too. - if (pkg["imports"] !== undefined) transform(pkg["imports"]); + if (pkg["imports"] !== undefined) pkg["imports"] = transform(pkg["imports"]); } /** npmStaticTransformPkgJson over a package.json TEXT (the fs shadow's diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts index 19728d6ce..14b366bfa 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -52,8 +52,10 @@ export class FrontendServices { } private sourceParser(): Ts7SourceParser { this.ensureOpen(); + // Called as a plain function: the instance is not its receiver. + const createApi = this.createApi; return (this.parser ??= new Ts7SourceParser( - (options) => this.createApi({ ...options, collectTiming: false }), + (options) => createApi({ ...options, collectTiming: false }), this.cwd, )); } @@ -62,9 +64,10 @@ export class FrontendServices { } evaluateComptime(source: string, timeoutMs: number): unknown { this.ensureOpen(); - if (this.comptimeEvaluator === undefined) + const evaluate = this.comptimeEvaluator; + if (evaluate === undefined) throw new Error("this compiler host does not provide compile-time evaluation"); - return this.comptimeEvaluator(source, timeoutMs); + return evaluate(source, timeoutMs); } createProgramHost(options?: Ts7HostOptions): Ts7Host { this.ensureOpen(); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index d3d5fe61a..1a6560348 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -64,7 +64,9 @@ export interface SemanticQuery { export class SemanticProgram { constructor(private readonly sourceFile: (path: string) => AstNode | undefined) {} getSourceFile(path: string): AstNode | undefined { - return this.sourceFile(path); + // Called as a plain function: the instance is not its receiver. + const sourceFile = this.sourceFile; + return sourceFile(path); } } diff --git a/packages/compiler/src/frontend/ts7/source-parser.ts b/packages/compiler/src/frontend/ts7/source-parser.ts index f9029774c..bbb6cae7a 100644 --- a/packages/compiler/src/frontend/ts7/source-parser.ts +++ b/packages/compiler/src/frontend/ts7/source-parser.ts @@ -42,7 +42,9 @@ export class Ts7SourceParser { } private connect(): Ts7Api { - return this.createApi({ + // Called as a plain function: the instance is not its receiver. + const createApi = this.createApi; + return createApi({ cwd: this.cwd, fs: { readFile: (path) => this.files.get(tsgoPath(path)) ?? null, diff --git a/packages/compiler/test/keyed-method-writes.test.ts b/packages/compiler/test/keyed-method-writes.test.ts new file mode 100644 index 000000000..407530945 --- /dev/null +++ b/packages/compiler/test/keyed-method-writes.test.ts @@ -0,0 +1,79 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; +import { everyStmtList } from "../src/ir/traverse.js"; + +async function lowerSource(source: string): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-keyed-writes-")); + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + const outPath = join(dir, "main.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 deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +/** Calls in `run` that go through a dynamic value. */ +function dynamicCalls(mod: IrModule): number { + const fn = mod.functions.find((f) => f.name === "run"); + if (!fn) throw new Error("no run function"); + let n = 0; + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "dynCall" || e.kind === "dynInvoke") n++; + return true; + }, + stmt: () => true, + }); + return n; +} + +const shape = ` +type Step = "scale" | "clamp"; +class Transform { + scale(x: number): number { + return x * 2; + } + clamp(x: number): number { + return x < 0 ? 0 : x; + } +} +`; + +test("element writes of data under method names keep the methods native", async () => { + const mod = await lowerSource(`${shape} +const enabled: Record = { scale: false, clamp: false }; +function enable(name: Step): void { + enabled[name] = true; +} +const totals: Record = {}; +function run(t: Transform): number { + enable("scale"); + totals["clamp"] = 1; + return t.clamp(t.scale(3)); +} +console.log(run(new Transform())); +`); + expect(dynamicCalls(mod)).toBe(0); +}); + +test("element writes through untyped receivers can still replace a method", async () => { + const mod = await lowerSource(`${shape} +function replace(target: object, name: Step): void { + (target as any)[name] = (x: number) => x + 100; +} +function run(t: Transform): number { + replace(t, "scale"); + return t.clamp(t.scale(3)); +} +console.log(run(new Transform())); +`); + expect(dynamicCalls(mod)).toBeGreaterThan(0); +}); From 285a92354e998dcb5a329f43f80bb158b4847211 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 13:21:29 -0500 Subject: [PATCH 146/409] Wake TransformStream pulls waiting on a static fetch body --- packages/runtime/src/scr_fetch.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 9792f8bed..03de9eed2 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -3227,8 +3227,17 @@ static void sft_set_backpressure(SfTransform *t, bool backpressure) { } } +/* After an enqueue: whether a read is still waiting. A fetch upload's read + * was just satisfied by the chunk, and it reads again through the next + * pull, as a reader's next read() does. */ static bool sft_readable_wants_chunk(const SfStream *r) { - return sf_stream_has_consumer(r); + return r->collector || (r->reader && r->reader->pending_head); +} + +/* The readable side is closed or errored: a pull still waiting on + * bp_change can no longer receive a chunk, so complete it. */ +static void sft_finish_pull(SfTransform *t) { + if (t->bp_change && scr_promise_is_pending(t->bp_change)) scr_promise_fulfill_void(t->bp_change); } static bool sft_readable_can_enqueue(const SfStream *r) { @@ -3334,6 +3343,7 @@ static void sf_transform_step(SfTransform *t, int op) { } else { sf_stream_close(r); } + sft_finish_pull(t); if (error) sf_reject_promise_reason(t->finish, error); else scr_promise_fulfill_void(t->finish); if (t->in_flight_close) sfw_finish_close(t, error); @@ -3355,6 +3365,7 @@ static void sf_transform_step(SfTransform *t, int op) { : scr_dyn_retain(scr_dyn_undefined()); bool readable_errored = r->error != NULL; if (!readable_errored) sf_stream_error(r, reason); + sft_finish_pull(t); /* The sink abort algorithm's finish promise, which a * readable.cancel() after the abort answers too: rejected with the * readable side's stored error when it had already errored, else @@ -3395,6 +3406,7 @@ static void sf_transform_step(SfTransform *t, int op) { } else { sfw_error_if_needed(t, reason ? reason : scr_dyn_undefined()); if (t->t_backpressure) sft_set_backpressure(t, false); + sft_finish_pull(t); scr_promise_fulfill_void(t->finish); } scr_dyn_release(reason); From 26d2c8bae194b6f6c0bf84907341c2cd9c2cc1e6 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 10:33:57 -0700 Subject: [PATCH 147/409] Iterate untyped arrays without the generic iterator protocol for...of, spread and destructuring over an untyped array opened the iterator by calling the builtin values() function through scr_dyn_call with the array as `this`, and stepped it through the general element path: retain the source, compute the live length, read the element with hole handling, release the source. An array whose iterator method is the builtin values() now gets the native iterator that call returns directly. A step over a plain array whose index is an own element, or past the end, answers without the general path; no getter can run there. Holes, keys(), entries(), typed array capsules and every other source keep the general path. zod 3, z.array(Order).parse over 500 orders: 187.3M -> 182.2M instructions per round (-2.7%). Co-Authored-By: Claude Opus 5.5 --- .../7794-untyped-array-iteration.js.json | 6 ++ packages/runtime/src/scr_json.c | 23 +++++ tests/corpus/7794-untyped-array-iteration.js | 86 +++++++++++++++++++ .../7794-untyped-array-iteration.js.json | 1 + 4 files changed, 116 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7794-untyped-array-iteration.js.json create mode 100644 tests/corpus/7794-untyped-array-iteration.js create mode 100644 tests/harness/corpus-costs/7794-untyped-array-iteration.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7794-untyped-array-iteration.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7794-untyped-array-iteration.js.json new file mode 100644 index 000000000..5ffe60f53 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7794-untyped-array-iteration.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7794-untyped-array-iteration.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index eed8a9da2..a00a18d82 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -5719,6 +5719,23 @@ static void scr_native_iterator_release(void *ptr) { * next() calls still receive a fresh, independently mutable result object. */ static ScrDyn *scr_native_iterator_value(ScrNativeIterator *iterator, bool *done_out) { ScrDyn *source = iterator->source; + /* An own element of an untyped array, or its end: no getter can run, so + * the general path below is not needed. */ + if (source && source->kind == SCR_DYN_ARR && iterator->kind == 0 && !iterator->array_like && + iterator->selection == 0) { + if (iterator->index >= (double)source->v.arr.len) { + iterator->source = NULL; + scr_dyn_release(source); + *done_out = true; + return scr_dyn_retain(scr_dyn_undefined()); + } + if (scr_dyn_arr_has_index(source, (size_t)iterator->index)) { + size_t index = (size_t)iterator->index; + iterator->index++; + *done_out = false; + return scr_dyn_retain(source->v.arr.items[index]); + } + } /* An inherited indexed getter can advance this same iterator recursively. * Keep the source alive even if that nested call exhausts the iterator. */ if (source) scr_dyn_retain(source); @@ -6863,6 +6880,12 @@ ScrDyn *scr_dyn_iterator(const ScrDyn *source, const ScrStr *spell) { scr_dyn_release(method); return scr_dyn_not_iterable(source, spell); } + /* An array whose iterator method is the builtin values(): what calling + * it with the array as `this` returns, without the call. */ + if (source->kind == SCR_DYN_ARR && method == scr_iterator_functions[1]) { + scr_dyn_release(method); + return scr_native_iterator_new((ScrDyn *)source); + } ScrDyn *iterator = scr_iterator_call(method, source, "value[Symbol.iterator]"); scr_dyn_release(method); if (!iterator) return NULL; diff --git a/tests/corpus/7794-untyped-array-iteration.js b/tests/corpus/7794-untyped-array-iteration.js new file mode 100644 index 000000000..ba7fe0810 --- /dev/null +++ b/tests/corpus/7794-untyped-array-iteration.js @@ -0,0 +1,86 @@ +"use strict"; +// for...of, spread and destructuring over untyped arrays: elements are read +// live, so pushes and truncation during the loop show, holes read as +// undefined, an own Symbol.iterator replaces the builtin one, and keys() +// and entries() keep their own shapes, as in Node. + +/** @param {any} list */ +function collect(list) { + const out = []; + for (const item of list) out.push(item); + return out; +} + +/** @param {any} list */ +function growWhileIterating(list) { + const seen = []; + for (const item of list) { + seen.push(item); + if (item === 2) list.push(99); + if (seen.length > 10) break; + } + return seen; +} + +/** @param {any} list */ +function shrinkWhileIterating(list) { + const seen = []; + for (const item of list) { + seen.push(item); + list.length = 2; + } + return seen; +} + +const parsed = JSON.parse('[1, "two", {"three": 3}, [4], null, true]'); +console.log(JSON.stringify(collect(parsed))); +console.log(JSON.stringify(collect([]))); +console.log(JSON.stringify(growWhileIterating(JSON.parse("[1, 2, 3]")))); +console.log(JSON.stringify(shrinkWhileIterating(JSON.parse("[1, 2, 3, 4, 5]")))); + +/** @type {any} */ +const holes = [1, , 3]; +holes.length = 5; +console.log(collect(holes).map((x) => String(x)).join(",")); + +/** @type {any} */ +const custom = JSON.parse("[1, 2, 3]"); +custom[Symbol.iterator] = function* () { + yield "custom"; + yield this.length; +}; +console.log(JSON.stringify(collect(custom))); + +/** @type {any} */ +const data = JSON.parse('[10, 20, 30]'); +console.log(JSON.stringify([...data]), JSON.stringify([...data.keys()]), JSON.stringify([...data.entries()])); +const [first, , third = "default", fourth = "missing"] = data; +console.log(first, third, fourth); + +let total = 0; +for (const value of data) { + if (value === 20) continue; + total += value; +} +console.log(total); + +/** @param {any} list */ +function nested(list) { + const pairs = []; + for (const a of list) for (const b of list) pairs.push(`${a}${b}`); + return pairs.join(" "); +} +console.log(nested(JSON.parse('["x", "y"]'))); + +/** @param {any} list */ +function earlyReturn(list) { + for (const item of list) if (item > 1) return item; + return -1; +} +console.log(earlyReturn(JSON.parse("[0, 1, 2, 3]")), earlyReturn(JSON.parse("[0]"))); + +try { + console.log(collect(null)); +} catch (e) { + console.log(e instanceof TypeError, e.message); +} diff --git a/tests/harness/corpus-costs/7794-untyped-array-iteration.js.json b/tests/harness/corpus-costs/7794-untyped-array-iteration.js.json new file mode 100644 index 000000000..6c25b05fd --- /dev/null +++ b/tests/harness/corpus-costs/7794-untyped-array-iteration.js.json @@ -0,0 +1 @@ +{ "plain": 3100, "san": 3700 } From 0a98779dd979c1bc2a0d2a18dbda6fd4d1e8944b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 13:49:22 -0500 Subject: [PATCH 148/409] Convert arrays and records that hold text codecs to unknown The recursive boxability check rejected TextEncoder and TextDecoder records, so an array or record holding a codec could not convert to 'unknown' (SC1101), although the converter boxes a codec as its own handle at any depth. Codec records are now boxable there, and a live view of a record or array boxes a codec field or element as its handle too, instead of a view of the codec's internal fields. --- .../src/backend/llvm/expr-stream-bridges.ts | 8 +++++++- packages/compiler/src/ir/ir.ts | 3 ++- tests/corpus/7749-text-codec-unknown-roundtrip.ts | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index a2f7208ec..70a8dcdee 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -722,7 +722,13 @@ export function streamTypedRefBoxValue( B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${value})`); return boxed; } - if (t.kind === "bytes" || (!streamTypedRefEligible(t) && !isDynTypedRefType(t))) { + // A codec record boxes as its own handle, by reference (toDynHelper), as + // it does outside a live view. + if ( + t.kind === "bytes" || + textCodecRecordClass(t, (id) => host.recordsById.get(id)) !== null || + (!streamTypedRefEligible(t) && !isDynTypedRefType(t)) + ) { const valueTy = host.llType(t); B.line(`${boxed} = call ptr @${host.dyn.toDynHelper(t)}(${valueTy} ${value})`); return boxed; diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 29dda102d..03b323cd5 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -4412,7 +4412,8 @@ function canBoxDynCompositeAt( case "record": { const shape = getRecord(t.shapeId); if (!shape) return false; - if (recordTextCodecClass(shape) !== null) return false; + // A codec boxes as its own handle wherever it sits (toDynHelper). + if (recordTextCodecClass(shape) !== null) return true; // Recursive shapes answer coinductively, like isJsonSafeType. if (visiting.has(t.shapeId)) return true; visiting.add(t.shapeId); diff --git a/tests/corpus/7749-text-codec-unknown-roundtrip.ts b/tests/corpus/7749-text-codec-unknown-roundtrip.ts index 14c35d2bb..b7c18b3ef 100644 --- a/tests/corpus/7749-text-codec-unknown-roundtrip.ts +++ b/tests/corpus/7749-text-codec-unknown-roundtrip.ts @@ -20,3 +20,17 @@ console.log(pair[0] === pair[1], pair[0] === holder.codec); const maybe: unknown = Math.random() > 2 ? 1 : new TextDecoder(); if (typeof maybe === "object" && maybe !== null) console.log((maybe as TextDecoder).encoding); + +// Codecs inside arrays and records cross as their handles too. +const codecs: TextEncoder[] = [encoder, new TextEncoder()]; +const asUnknown: unknown = codecs; +const back = asUnknown as TextEncoder[]; +console.log(back.length, back[0] === encoder, back[1]!.encode("x")); +const streaming = new TextDecoder(); +const nested: { name: string; decoder: TextDecoder } = { name: "nested", decoder: streaming }; +const boxed: unknown = nested; +const unboxed = boxed as { name: string; decoder: TextDecoder }; +console.log(unboxed.name, unboxed.decoder === streaming); +console.log(JSON.stringify(unboxed.decoder.decode(new Uint8Array([0xe2, 0x82]), { stream: true }))); +console.log(JSON.stringify(streaming.decode(new Uint8Array([0xac])))); +console.log(JSON.stringify(boxed)); From c81ee69da78cb380b55a71e86a7633721b5221fa Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 10:34:07 -0700 Subject: [PATCH 149/409] Rule out global constructor names by their first byte A global constructor read as a value in untyped code (zod's `data instanceof Map`, `Set`, `Date` in getParsedType) resolves its name at run time: Function, then the Error constructors, then the other globals, each by strcmp. Every miss in the Error and global tables cost a call, about a dozen per read. Each table entry is now skipped unless its first byte matches the name's, before the strcmp. The tables, their order and the values they return are unchanged. zod 3, z.array(Order).parse over 500 orders: 187.3M -> 183.2M instructions per round (-2.2%). Co-Authored-By: Claude Opus 5.5 --- .../7795-untyped-global-constructors.js.json | 6 ++++ packages/runtime/src/scr_dyn_invoke.c | 2 +- packages/runtime/src/scr_json.c | 5 +++- .../7795-untyped-global-constructors.js | 29 +++++++++++++++++++ .../7795-untyped-global-constructors.js.json | 1 + 5 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7795-untyped-global-constructors.js.json create mode 100644 tests/corpus/7795-untyped-global-constructors.js create mode 100644 tests/harness/corpus-costs/7795-untyped-global-constructors.js.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7795-untyped-global-constructors.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7795-untyped-global-constructors.js.json new file mode 100644 index 000000000..48eb29e33 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7795-untyped-global-constructors.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7795-untyped-global-constructors.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 364017e05..2a797b626 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -1972,7 +1972,7 @@ static bool dyn_global_construct(const ScrDyn *callee, const ScrDyn *args, ScrDy static ScrDyn *dyn_global_ctor(const char *name) { for (size_t i = 0; i < DYN_GLOBAL_CTORS; i++) { - if (strcmp(dyn_global_ctor_names[i], name) != 0) continue; + if (dyn_global_ctor_names[i][0] != name[0] || strcmp(dyn_global_ctor_names[i], name) != 0) continue; if (!dyn_global_ctors_registered) { dyn_global_ctors_registered = true; scr_atexit(dyn_global_ctors_cleanup); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index eed8a9da2..c63b97145 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1911,7 +1911,10 @@ static ScrDyn *scr_error_ctor_call(ScrClosure *closure, ScrDyn *const *args, siz ScrDyn *scr_dyn_error_constructor(const char *name) { for (size_t i = 0; i < SCR_ERROR_CTORS; i++) { - if (!scr_error_ctor_names[i] || strcmp(scr_error_ctor_names[i], name) != 0) continue; + /* The first byte rules out most names without a call. */ + if (!scr_error_ctor_names[i] || scr_error_ctor_names[i][0] != name[0] || + strcmp(scr_error_ctor_names[i], name) != 0) + continue; if (!scr_error_ctors_registered) { scr_error_ctors_registered = true; scr_atexit(scr_error_ctors_cleanup); diff --git a/tests/corpus/7795-untyped-global-constructors.js b/tests/corpus/7795-untyped-global-constructors.js new file mode 100644 index 000000000..0b464b7b2 --- /dev/null +++ b/tests/corpus/7795-untyped-global-constructors.js @@ -0,0 +1,29 @@ +"use strict"; +// Global constructors read as values in untyped code: instanceof tests, +// identity comparisons and constructor names, as zod's type sniffing uses +// them, answer as in Node for every global the lookup knows. + +/** @param {unknown} data */ +function kind(data) { + if (typeof Map !== "undefined" && data instanceof Map) return "map"; + if (typeof Set !== "undefined" && data instanceof Set) return "set"; + if (typeof Date !== "undefined" && data instanceof Date) return "date"; + if (data instanceof TypeError) return "type-error"; + if (data instanceof RangeError) return "range-error"; + if (data instanceof Error) return "error"; + if (data instanceof Promise) return "promise"; + if (Array.isArray(data)) return "array"; + return typeof data; +} + +const values = [new Map(), new Set([1]), new Date(0), new TypeError("t"), new RangeError("r"), new SyntaxError("s"), new Error("e"), Promise.resolve(1), [1], { a: 1 }, "s", 1, null]; +for (const value of values) console.log(kind(value)); + +const ctors = [Map, Set, Date, Error, TypeError, RangeError, SyntaxError, ReferenceError, EvalError, URIError, Object, Promise, WeakMap, WeakSet, RegExp]; +console.log(ctors.map((ctor) => ctor.name).join(",")); +console.log(ctors.every((ctor, i) => ctors.indexOf(ctor) === i)); +const again = [Map, Set, Date, Error, TypeError]; +console.log(again.every((ctor, i) => ctor === ctors[i])); +/** @type {any} */ +const err = new TypeError("x"); +console.log(err.constructor === TypeError, err.constructor === Error, err instanceof Error); diff --git a/tests/harness/corpus-costs/7795-untyped-global-constructors.js.json b/tests/harness/corpus-costs/7795-untyped-global-constructors.js.json new file mode 100644 index 000000000..05dfbf1a6 --- /dev/null +++ b/tests/harness/corpus-costs/7795-untyped-global-constructors.js.json @@ -0,0 +1 @@ +{ "plain": 1200, "san": 1200 } From 693e33742ac334e3778872f3478d4f70aafd18b1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 13:59:28 -0500 Subject: [PATCH 150/409] Support the raw-public and raw-private key formats EC and Ed25519 keys read and write Node 24's raw formats: - generateKeyPair(Sync) encodings: raw-public gives the EC point, uncompressed or, with type "compressed", compressed, or the Ed25519 public key; raw-private gives the EC scalar at the curve's size or the Ed25519 seed. - KeyObject.export({ format: "raw-public" | "raw-private" | "raw-seed" }), with Node's check of the point type, and Node's error for a passphrase with any format other than PEM and DER, which now also covers JWK. - createPublicKey and createPrivateKey of { key, format, asymmetricKeyType, namedCurve }, in Node's argument order, with compressed EC points, and createPublicKey of a raw private key. As in Node, raw-seed and every raw format of an RSA, RSA-PSS, DSA or DH key throw ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, which generateKeyPair passes to its callback. Invalid key data throws ERR_INVALID_ARG_VALUE. Raw X25519, X448, Ed448 and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error. --- .../limitations/builtin-apis/crypto.mdx | 4 +- .../corpus/7449-crypto-raw-keys.mjs.json | 6 + packages/runtime/src/scr_pki.c | 252 +++++++++++++++--- tests/corpus/7449-crypto-raw-keys.mjs | 120 +++++++++ .../7449-crypto-raw-keys.mjs.json | 1 + 5 files changed, 352 insertions(+), 31 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json create mode 100644 tests/corpus/7449-crypto-raw-keys.mjs create mode 100644 tests/harness/corpus-costs/7449-crypto-raw-keys.mjs.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index e4b555806..964ca4ec9 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -33,9 +33,11 @@ Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`. RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. `generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback. - RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, the `raw-public`, `raw-private`, and `raw-seed` encodings, and `paramEncoding: 'explicit'` throw a coded SC2020 error. + RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error. The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1. Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. + EC and Ed25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points. + Raw X25519, X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error. They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. `crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm. Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json new file mode 100644 index 000000000..48bf2dfcc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7449-crypto-raw-keys.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7449-crypto-raw-keys.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index a1a5e97c2..94a0000e7 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -5,8 +5,9 @@ * asymmetricKeyDetails, export(), equals()), crypto.sign and crypto.verify, * and the Sign and Verify objects of createSign and createVerify. Keys read * and write PEM and DER as PKCS#1, SEC1, PKCS#8 and SubjectPublicKeyInfo, - * and JWK; the EC curves are prime256v1, secp384r1, secp521r1 and - * secp256k1, and the Ed25519 arithmetic is scr_ed25519.c. Argument checks + * JWK, and the raw-public and raw-private encodings; the EC curves are + * prime256v1, secp384r1, secp521r1 and secp256k1, and the Ed25519 + * arithmetic is scr_ed25519.c. Argument checks * and errors follow Node 24's lib/internal/crypto/keys.js, keygen.js and * sig.js. Encrypted keys and other key types throw a coded SC2020 error. * All arguments are borrowed. */ @@ -878,6 +879,8 @@ static ScrBytes *sp_source_bytes(const ScrDyn *v, const ScrDyn *encoding) { return NULL; } +static SpKey *sp_key_from_raw(const ScrDyn *input, const ScrDyn *data, const ScrDyn *format, bool private_input); + static const char sp_key_input_types[] = "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, or URL"; @@ -929,6 +932,14 @@ static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { SpKey *k = sp_key_from_jwk(data, private_key); return k ? sp_key_box(k) : NULL; } + if (sp_is_str(format, "raw-public") || sp_is_str(format, "raw-private") || sp_is_str(format, "raw-seed")) { + SpKey *k = sp_key_from_raw(input, data, format, private_key); + if (!k || private_key || !k->private_key) return k ? sp_key_box(k) : NULL; + SpKey *pub = sp_public_of(k); + sp_key_release(k); + if (!pub) sp_unsupported_input(); + return pub ? sp_key_box(pub) : NULL; + } if (!sp_absent(format) && !sp_is_str(format, "pem") && !sp_is_str(format, "der")) { sp_property_invalid("key.format", format); return NULL; @@ -959,6 +970,183 @@ ScrDyn *scr_crypto_create_private_key(ScrDyn *key) { return sp_create_key(key, t ScrDyn *scr_crypto_create_public_key(ScrDyn *key) { return sp_create_key(key, false); } +/* ── raw keys ─────────────────────────────────────────────────────────── */ + +/* A namedCurve as Node reads it: the OpenSSL short name or the NIST name, + * matched exactly. NULL for other names. */ +static const SpCurve *sp_curve_named(const ScrDyn *name) { + for (size_t i = 0; i < sizeof sp_curves / sizeof sp_curves[0]; i++) + if (sp_is_str(name, sp_curves[i].name) || (sp_curves[i].nist && sp_is_str(name, sp_curves[i].nist))) + return &sp_curves[i]; + return NULL; +} + +static void sp_incompatible_key_type(void) { + sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", + "The selected key encoding is incompatible with the key type"); +} + +static void sp_invalid_key_data(void) { sp_throw(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid key data"); } + +/* A key's raw encoding: an EC public point, compressed when asked, or the + * private scalar at the curve's size, or an Ed25519 public key or seed. + * raw-seed, and every encoding of an RSA key, throw + * ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS. NULL with the error pending. */ +static ScrDyn *sp_export_raw(SpKey *k, bool private_part, bool seed, bool compressed) { + if (seed || sp_is_rsa(k)) { + sp_incompatible_key_type(); + return NULL; + } + if (sp_is_ed(k)) return sp_dyn_buffer(private_part ? k->ed_seed : k->ed_pub, 32); + mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); + const mbedtls_ecp_group *grp = &ec->MBEDTLS_PRIVATE(grp); + unsigned char buf[1 + 2 * 66]; + size_t n = (grp->nbits + 7) / 8; + int rc = private_part ? mbedtls_mpi_write_binary(&ec->MBEDTLS_PRIVATE(d), buf, n) + : mbedtls_ecp_point_write_binary( + grp, &ec->MBEDTLS_PRIVATE(Q), + compressed ? MBEDTLS_ECP_PF_COMPRESSED : MBEDTLS_ECP_PF_UNCOMPRESSED, &n, buf, sizeof buf); + ScrDyn *out = rc == 0 ? sp_dyn_buffer(buf, n) : NULL; + mbedtls_platform_zeroize(buf, sizeof buf); + if (!out) sp_unsupported_input(); + return out; +} + +/* Node's named key types that this runtime does not generate or read: + * the other OKP curves and the post-quantum types. */ +static const char *const sp_other_named_types[] = { + "ed448", + "x25519", + "x448", + "ml-dsa-44", + "ml-dsa-65", + "ml-dsa-87", + "ml-kem-512", + "ml-kem-768", + "ml-kem-1024", + "slh-dsa-sha2-128f", + "slh-dsa-sha2-128s", + "slh-dsa-sha2-192f", + "slh-dsa-sha2-192s", + "slh-dsa-sha2-256f", + "slh-dsa-sha2-256s", + "slh-dsa-shake-128f", + "slh-dsa-shake-128s", + "slh-dsa-shake-192f", + "slh-dsa-shake-192s", + "slh-dsa-shake-256f", + "slh-dsa-shake-256s", +}; + +/* createPrivateKey / createPublicKey of { key, format: "raw-public" | + * "raw-private" | "raw-seed", asymmetricKeyType, namedCurve }: an EC point + * (uncompressed or compressed) or scalar, or an Ed25519 public key or + * seed. The argument checks are Node's prepareAsymmetricKey order, then + * the key type, the seed format, the curve and the data. NULL with the + * error pending. */ +static SpKey *sp_key_from_raw(const ScrDyn *input, const ScrDyn *data, const ScrDyn *format, bool private_input) { + bool raw_public = sp_is_str(format, "raw-public"); + bool seed = sp_is_str(format, "raw-seed"); + if (private_input && raw_public) { + sp_property_invalid("key.format", format); + return NULL; + } + if (data->kind != SCR_DYN_BYTES && !scr_buffer_storage_is(data)) { + scr_dyn_arg_type_fail("key.key", "an instance of ArrayBuffer, Buffer, TypedArray, or DataView", data); + return NULL; + } + const ScrDyn *type = sp_opt(input, "asymmetricKeyType"); + if (type->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("key.asymmetricKeyType", "of type string", type); + return NULL; + } + bool ec = sp_is_str(type, "ec"); + bool ed = sp_is_str(type, "ed25519"); + const ScrDyn *curve_name = sp_opt(input, "namedCurve"); + if (ec && curve_name->kind != SCR_DYN_STR) { + scr_dyn_prop_type_fail("key.namedCurve", "of type string", curve_name); + return NULL; + } + if (sp_is_str(type, "rsa") || sp_is_str(type, "rsa-pss") || sp_is_str(type, "dsa") || sp_is_str(type, "dh")) { + sp_incompatible_key_type(); + return NULL; + } + bool other = false; + for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) + other = other || sp_is_str(type, sp_other_named_types[i]); + if (!ec && !ed && !other) { + ScrStr *s = type->v.str; + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid asymmetricKeyType: %.*s", (int)s->len, s->data); + return NULL; + } + bool ml = !ec && !ed && type->v.str->len > 3 && memcmp(type->v.str->data, "ml-", 3) == 0; + if (seed && !ml) { + sp_incompatible_key_type(); + return NULL; + } + if (other) { + sp_not_supported("raw keys of this asymmetricKeyType"); + return NULL; + } + const SpCurve *curve = NULL; + if (ec && !(curve = sp_curve_named(curve_name))) { + sp_throw(SCR_ERR_TYPE, "ERR_CRYPTO_INVALID_CURVE", "Invalid EC curve name"); + return NULL; + } + ScrBytes *bytes = scr_bytes_buffer_source(data); + if (!bytes) return NULL; + const unsigned char *p = bytes->data; + size_t len = (size_t)scr_bytes_byte_len(bytes); + SpKey *k = sp_key_alloc(!raw_public); + bool ok = false; + if (ed) { + k->ed25519 = true; + if (len == 32 && raw_public) { + memcpy(k->ed_pub, p, 32); + ok = true; + } else if (len == 32) { + const ScrPkiEd25519 *eds = sp_ed(); + if (!eds) { + sp_key_release(k); + scr_bytes_release(bytes); + return NULL; + } + memcpy(k->ed_seed, p, 32); + eds->public_key(k->ed_pub, k->ed_seed); + ok = true; + } + } else if (raw_public && len == 1 && p[0] == 0) { + sp_key_release(k); + scr_bytes_release(bytes); + sp_not_supported("EC public keys at the point at infinity"); + return NULL; + } else if (mbedtls_pk_setup(&k->pk, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)) == 0) { + mbedtls_ecp_keypair *kp = mbedtls_pk_ec(k->pk); + mbedtls_ecp_group *grp = &kp->MBEDTLS_PRIVATE(grp); + ok = mbedtls_ecp_group_load(grp, curve->id) == 0; + if (ok && raw_public) { + ok = mbedtls_ecp_point_read_binary(grp, &kp->MBEDTLS_PRIVATE(Q), p, len) == 0 && + mbedtls_ecp_check_pubkey(grp, &kp->MBEDTLS_PRIVATE(Q)) == 0; + } else if (ok) { + ok = len == (grp->nbits + 7) / 8 && mbedtls_mpi_read_binary(&kp->MBEDTLS_PRIVATE(d), p, len) == 0; + if (ok && mbedtls_ecp_check_privkey(grp, &kp->MBEDTLS_PRIVATE(d)) != 0) { + sp_key_release(k); + scr_bytes_release(bytes); + sp_not_supported("EC private keys outside 1 to n - 1"); + return NULL; + } + ok = ok && mbedtls_ecp_mul(grp, &kp->MBEDTLS_PRIVATE(Q), &kp->MBEDTLS_PRIVATE(d), &grp->G, sp_random, NULL) == 0; + } + } + scr_bytes_release(bytes); + if (!ok) { + sp_key_release(k); + sp_invalid_key_data(); + return NULL; + } + return k; +} + /* ── export ───────────────────────────────────────────────────────────── */ /* export({ format, type }) per Node's parseKeyEncoding: format "pem", @@ -975,11 +1163,37 @@ static ScrDyn *sp_export(SpKey *k, const ScrDyn *options) { } static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { + const ScrDyn *format = sp_opt(options, "format"); + /* A private key's passphrase applies only to PEM and DER. */ + if (k->private_key && !sp_absent(sp_opt(options, "passphrase")) && !sp_is_str(format, "pem") && + !sp_is_str(format, "der")) { + ScrStr *text = scr_dyn_string_coerce_js(format); + if (!text) return NULL; + scr_pki_throw_fmt(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", + "The selected key encoding %.*s does not support encryption.", (int)text->len, text->data); + scr_str_release(text); + return NULL; + } + if (!k->private_key && sp_is_str(format, "raw-public")) { + const ScrDyn *type = sp_opt(options, "type"); + bool compressed = sp_is_str(type, "compressed"); + /* Only an EC point has a compressed form. */ + if (sp_is_ec(k) && !sp_absent(type) && !compressed && !sp_is_str(type, "uncompressed")) { + ScrStr *received = sp_received(type, false); + scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", + "The property 'options.type' must be one of: 'compressed', 'uncompressed'. Received %.*s", + (int)received->len, received->data); + scr_str_release(received); + return NULL; + } + return sp_export_raw(k, false, false, sp_is_ec(k) && compressed); + } + if (k->private_key && (sp_is_str(format, "raw-private") || sp_is_str(format, "raw-seed"))) + return sp_export_raw(k, true, sp_is_str(format, "raw-seed"), false); if (options == NULL || !sp_is_object(options)) { scr_dyn_arg_type_fail("options", "of type object", options ? options : scr_dyn_undefined()); return NULL; } - const ScrDyn *format = sp_opt(options, "format"); if (sp_is_str(format, "jwk")) return sp_export_jwk(k); bool pem = sp_is_str(format, "pem"); if (!pem && !sp_is_str(format, "der")) { @@ -1227,7 +1441,7 @@ static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const S /* Node's parseKeyEncoding for options.publicKeyEncoding or * options.privateKeyEncoding of a `type` key: true when the encoding is - * undefined, null or valid, false with the error pending. A valid raw or + * undefined, null or valid, false with the error pending. A valid * encrypted encoding, which this runtime does not write, sets *refusal * when it is unset. */ static bool sp_check_encoding(const ScrDyn *options, const char *key, const ScrDyn *type, bool private_key, @@ -1323,7 +1537,6 @@ static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const S return false; } } - if (!*refusal && (raw_public || raw_private)) *refusal = "generateKeyPair with a raw key encoding"; if (!*refusal && encrypted) *refusal = "encrypted private key export"; return true; } @@ -1335,31 +1548,6 @@ typedef struct { const SpCurve *curve; /* EC */ } SpKeyGen; -/* The key types Node generates that this runtime does not, apart from - * rsa-pss, dsa and dh: Node's named key pairs. */ -static const char *const sp_other_named_types[] = { - "ed448", - "x25519", - "x448", - "ml-dsa-44", - "ml-dsa-65", - "ml-dsa-87", - "ml-kem-512", - "ml-kem-768", - "ml-kem-1024", - "slh-dsa-sha2-128f", - "slh-dsa-sha2-128s", - "slh-dsa-sha2-192f", - "slh-dsa-sha2-192s", - "slh-dsa-sha2-256f", - "slh-dsa-sha2-256s", - "slh-dsa-shake-128f", - "slh-dsa-shake-128s", - "slh-dsa-shake-192f", - "slh-dsa-shake-192s", - "slh-dsa-shake-256f", - "slh-dsa-shake-256s", -}; /* createJob's checks for generateKeyPair(Sync)(type, options) in Node's * order: the type, the options object, the public and private key @@ -1502,14 +1690,18 @@ static ScrDyn *sp_key_pair(const ScrDyn *type, const ScrDyn *options, bool *job_ sp_unsupported_input(); return NULL; } + /* An encoding the key type has no form for (a raw encoding of an RSA + * key, raw-seed) fails the job, as in Node. */ ScrDyn *public_value = sp_encoded(pub, sp_opt(options, "publicKeyEncoding")); if (!public_value) { sp_key_release(k); + *job_error = true; return NULL; } ScrDyn *private_value = sp_encoded(k, sp_opt(options, "privateKeyEncoding")); if (!private_value) { scr_dyn_release(public_value); + *job_error = true; return NULL; } ScrDyn *pair = scr_dyn_new_obj(); diff --git a/tests/corpus/7449-crypto-raw-keys.mjs b/tests/corpus/7449-crypto-raw-keys.mjs new file mode 100644 index 000000000..0795dbb0b --- /dev/null +++ b/tests/corpus/7449-crypto-raw-keys.mjs @@ -0,0 +1,120 @@ +// The raw-public, raw-private and raw-seed key formats: generateKeyPair +// encodings, KeyObject.export and createPublicKey/createPrivateKey with +// asymmetricKeyType and namedCurve, for EC keys (uncompressed and +// compressed points, scalars at the curve's size) and Ed25519 keys, and +// Node's errors for the other key types and formats. +import * as crypto from "node:crypto"; + +function describe(value) { + if (Buffer.isBuffer(value)) return `Buffer(${value.length})`; + if (value instanceof crypto.KeyObject) return `${value.type} ${value.asymmetricKeyType} ${JSON.stringify(value.asymmetricKeyDetails)}`; + if (value && typeof value === "object") return Object.entries(value).map(([key, field]) => `${key}=${describe(field)}`).join(" "); + return String(value); +} + +function attempt(label, run) { + try { + console.log(label, describe(run())); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +// generateKeyPair encodings +for (const namedCurve of ["P-256", "P-384", "P-521", "secp256k1"]) { + attempt(`ec ${namedCurve} raw-public`, () => crypto.generateKeyPairSync("ec", { namedCurve, publicKeyEncoding: { format: "raw-public" } })); + attempt(`ec ${namedCurve} compressed`, () => crypto.generateKeyPairSync("ec", { namedCurve, publicKeyEncoding: { format: "raw-public", type: "compressed" } })); + attempt(`ec ${namedCurve} raw-private`, () => crypto.generateKeyPairSync("ec", { namedCurve, privateKeyEncoding: { format: "raw-private" } })); +} +attempt("ec raw-seed", () => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", privateKeyEncoding: { format: "raw-seed" } })); +attempt("ed25519 raw", () => + crypto.generateKeyPairSync("ed25519", { publicKeyEncoding: { format: "raw-public" }, privateKeyEncoding: { format: "raw-private" } }), +); +attempt("ed25519 raw-seed", () => crypto.generateKeyPairSync("ed25519", { privateKeyEncoding: { format: "raw-seed" } })); +attempt("rsa raw-public", () => crypto.generateKeyPairSync("rsa", { modulusLength: 1024, publicKeyEncoding: { format: "raw-public" } })); + +// export +const ec = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const ed = crypto.generateKeyPairSync("ed25519"); +const rsa = crypto.generateKeyPairSync("rsa", { modulusLength: 1024 }); +const point = Buffer.from(ec.publicKey.export({ format: "raw-public" })); +const compressed = Buffer.from(ec.publicKey.export({ format: "raw-public", type: "compressed" })); +const scalar = Buffer.from(ec.privateKey.export({ format: "raw-private" })); +const jwk = ec.privateKey.export({ format: "jwk" }); +console.log("ec point", point.length, point[0], point.subarray(1, 33).toString("base64url") === jwk.x, point.subarray(33).toString("base64url") === jwk.y); +console.log("ec compressed", compressed.length, compressed[0] === 2 + (point[64] & 1), compressed.subarray(1).equals(point.subarray(1, 33))); +console.log("ec scalar", scalar.length, scalar.toString("base64url") === jwk.d); +console.log("ec uncompressed", Buffer.from(ec.publicKey.export({ format: "raw-public", type: "uncompressed" })).equals(point)); +const edPublic = Buffer.from(ed.publicKey.export({ format: "raw-public" })); +const edSeed = Buffer.from(ed.privateKey.export({ format: "raw-private" })); +const edJwk = ed.privateKey.export({ format: "jwk" }); +console.log("ed25519", edPublic.toString("base64url") === edJwk.x, edSeed.toString("base64url") === edJwk.d); +attempt("ed25519 raw-public with a type", () => ed.publicKey.export({ format: "raw-public", type: "x" })); +attempt("ec bad point type", () => ec.publicKey.export({ format: "raw-public", type: "x" })); +attempt("ec null point type", () => ec.publicKey.export({ format: "raw-public", type: null })); +attempt("private raw-public", () => ec.privateKey.export({ format: "raw-public" })); +attempt("public raw-private", () => ec.publicKey.export({ format: "raw-private" })); +attempt("ec raw-seed", () => ec.privateKey.export({ format: "raw-seed" })); +attempt("ed25519 raw-seed", () => ed.privateKey.export({ format: "raw-seed" })); +attempt("rsa raw-public", () => rsa.publicKey.export({ format: "raw-public" })); +attempt("rsa raw-private", () => rsa.privateKey.export({ format: "raw-private" })); +for (const format of [undefined, 5, "jwk", "raw-private", "raw-seed"]) + attempt(`passphrase with ${format}`, () => ec.privateKey.export({ format, passphrase: "x" })); + +// import +const pub = (key) => () => crypto.createPublicKey(key); +const priv = (key) => () => crypto.createPrivateKey(key); +attempt("ec point", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("ec compressed point", pub({ key: compressed, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "prime256v1" })); +attempt("ec scalar", priv({ key: scalar, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("ec scalar as public", pub({ key: scalar, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +const reread = crypto.createPublicKey({ key: compressed, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" }); +console.log("ec reread", reread.equals(ec.publicKey), Buffer.from(reread.export({ format: "raw-public" })).equals(point)); +const rereadPrivate = crypto.createPrivateKey({ key: scalar, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" }); +console.log("ec private reread", crypto.createPublicKey(rereadPrivate).equals(ec.publicKey)); +attempt("ed25519 public", pub({ key: edPublic, format: "raw-public", asymmetricKeyType: "ed25519" })); +attempt("ed25519 seed", priv({ key: edSeed, format: "raw-private", asymmetricKeyType: "ed25519" })); +console.log("ed25519 reread", crypto.createPublicKey({ key: edSeed, format: "raw-private", asymmetricKeyType: "ed25519" }).equals(ed.publicKey)); + +// fixed keys: the EC scalar 1 is the generator, and an Ed25519 seed of +// zeros has a known public key +const one = Buffer.concat([Buffer.alloc(31), Buffer.from([1])]); +const generator = crypto.createPrivateKey({ key: one, format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" }); +console.log("generator", Buffer.from(crypto.createPublicKey(generator).export({ format: "raw-public", type: "compressed" })).toString("hex")); +const zeros = crypto.createPrivateKey({ key: Buffer.alloc(32), format: "raw-private", asymmetricKeyType: "ed25519" }); +console.log("zero seed", Buffer.from(crypto.createPublicKey(zeros).export({ format: "raw-public" })).toString("hex")); +const message = Buffer.from("raw keys"); +console.log("zero seed signature", crypto.sign(null, message, zeros).toString("hex").slice(0, 32)); + +// import errors +attempt("raw-public to createPrivateKey", priv({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("string data", pub({ key: "abc", format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("no asymmetricKeyType", pub({ key: point, format: "raw-public" })); +attempt("number asymmetricKeyType", pub({ key: point, format: "raw-public", asymmetricKeyType: 5 })); +attempt("no namedCurve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec" })); +attempt("unknown asymmetricKeyType", pub({ key: point, format: "raw-public", asymmetricKeyType: "nope" })); +attempt("Ed25519 spelling", pub({ key: edPublic, format: "raw-public", asymmetricKeyType: "Ed25519" })); +attempt("unknown curve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "nope" })); +attempt("lower-case NIST curve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "p-256" })); +attempt("other curve", pub({ key: point, format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-384" })); +attempt("short point", pub({ key: point.subarray(0, 64), format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("off-curve point", pub({ key: Buffer.concat([Buffer.from([4]), Buffer.alloc(64, 1)]), format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("bad compressed x", pub({ key: Buffer.concat([Buffer.from([2]), Buffer.alloc(32, 0xff)]), format: "raw-public", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("short scalar", priv({ key: scalar.subarray(0, 31), format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("long scalar", priv({ key: Buffer.concat([Buffer.alloc(1), scalar]), format: "raw-private", asymmetricKeyType: "ec", namedCurve: "P-256" })); +attempt("ec raw-seed", priv({ key: scalar, format: "raw-seed", asymmetricKeyType: "ec", namedCurve: "nope" })); +attempt("ed25519 raw-seed", priv({ key: edSeed, format: "raw-seed", asymmetricKeyType: "ed25519" })); +attempt("short ed25519 key", pub({ key: edPublic.subarray(0, 31), format: "raw-public", asymmetricKeyType: "ed25519" })); +attempt("short ed25519 seed", priv({ key: edSeed.subarray(0, 31), format: "raw-private", asymmetricKeyType: "ed25519" })); +for (const asymmetricKeyType of ["rsa", "rsa-pss", "dsa", "dh"]) + attempt(`${asymmetricKeyType} raw`, pub({ key: edPublic, format: "raw-public", asymmetricKeyType })); + +// generateKeyPair passes an encoding the key type has no form for to its +// callback +crypto.generateKeyPair("ed25519", { privateKeyEncoding: { format: "raw-seed" } }, (...args) => { + console.log("async raw-seed", args.length, args[0].code); + crypto.generateKeyPair("ec", { namedCurve: "P-384", publicKeyEncoding: { format: "raw-public", type: "compressed" } }, (err, publicKey, privateKey) => { + console.log("async raw-public", err, describe(publicKey), describe(privateKey)); + }); +}); +console.log("returned"); diff --git a/tests/harness/corpus-costs/7449-crypto-raw-keys.mjs.json b/tests/harness/corpus-costs/7449-crypto-raw-keys.mjs.json new file mode 100644 index 000000000..c8b39ec8a --- /dev/null +++ b/tests/harness/corpus-costs/7449-crypto-raw-keys.mjs.json @@ -0,0 +1 @@ +{ "plain": 7592, "san": 12217 } From c24ae2d7e921eb70d92d435f287ddde12d1123f4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 14:11:21 -0500 Subject: [PATCH 151/409] Keep caches for property helpers that answer plain values first Property helpers now start with an exit for values no class table matches: if the receiver is neither a class capsule nor a function, they run the generic read or write on it directly. The inline-cache recognizer accepted only capsule branches and the subclass normalization before a helper's generic tail, so these helpers stopped being cache sites. It now also accepts that exit when its operation is the one the site caches: a read or write of the same key on the receiver. --- packages/compiler/src/ir/js-ic.test.ts | 54 ++++++++++++++++++++ packages/compiler/src/ir/js-ic.ts | 69 ++++++++++++++++++++++---- 2 files changed, 114 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index 026600cc3..f5391b83e 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -371,3 +371,57 @@ test("bag helpers that exit early for values no class branch matches are still t expect(sites.site("%dyn.class.property.2")).toBeNull(); expect(sites.site("%dyn.class.property.3")).toBeNull(); }); + +test("helpers that answer plain values first with the same operation are cache sites", () => { + const negated = (test: "classCapsule" | "funcKind"): IrExpr => ({ + kind: "dynTest", + test, + negated: true, + value: ref("p.0"), + type: BOOL, + loc, + }); + const exit = (body: IrStmt[], test: IrExpr = negated("classCapsule")): IrStmt => ({ + kind: "if", + cond: test, + then: [{ kind: "if", cond: negated("funcKind"), then: body, else_: null, loc }], + else_: null, + loc, + }); + const store = (key: string): IrStmt[] => [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.keySet", + args: [ref("p.0"), str(key), ref("p.1")], + type: VOID, + loc, + }, + loc, + }, + ret(null), + ]; + const load = (key: string): IrStmt[] => [ + ret({ kind: "dynKeyGet", value: ref("p.0"), key: str(key), type: DYN, loc }), + ]; + const sites = sitesOf( + setHelper("%dyn.class.property.0", "x", [exit(store("x")), capsuleBranch("p.0")]), + getHelper("%dyn.class.property.1", "x", [exit(load("x")), capsuleBranch("p.0")]), + // An exit for another key, or one plain values do not take. + setHelper("%dyn.class.property.2", "x", [exit(store("y"))]), + setHelper("%dyn.class.property.3", "x", [ + exit(store("x"), { + kind: "dynTest", + test: "classCapsule", + value: ref("p.0"), + type: BOOL, + loc, + }), + ]), + ); + expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "set", key: "x" }); + expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "get", key: "x" }); + expect(sites.site("%dyn.class.property.2")).toBeNull(); + expect(sites.site("%dyn.class.property.3")).toBeNull(); +}); diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 58c568bbd..48019636a 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -61,17 +61,68 @@ export class JsIcSites { if (fn.params.some((p) => p.type.kind !== "dyn")) return null; const receiver = fn.params[0]?.localId; if (receiver === undefined) return null; + const second = fn.params[1]?.localId; + const exits: JsIcSite[] = []; let i = 0; - while ( - i < fn.body.length && - (this.capsuleBranch(fn.body[i]!, receiver) || this.rebinding(fn.body[i]!, receiver)) - ) - i++; + for (; i < fn.body.length; i++) { + const s = fn.body[i]!; + if (this.capsuleBranch(s, receiver) || this.rebinding(s, receiver)) continue; + const exit = this.plainExit(s, receiver, second); + if (exit === null) break; + exits.push(exit); + } const tail = fn.body.slice(i); - if (fn.params.length === 1) return this.getTail(tail, receiver); - if (fn.params.length !== 2) return null; - const second = fn.params[1]!.localId; - return this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second); + let site: JsIcSite | null; + if (fn.params.length === 1) site = this.getTail(tail, receiver); + else if (fn.params.length !== 2 || second === undefined) return null; + else site = this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second); + if (site === null || site.kind === "keyed") return exits.length === 0 ? site : null; + // An exit answers plain values with the operation the site caches. + return exits.every((exit) => exit.kind === site.kind && exit.key === site.key) ? site : null; + } + + /** `if (!classCapsule(receiver)) { [if (!funcKind(receiver))] OP }`, no + * else: an exit that runs the helper's generic operation directly on a + * value no class table matches. OP is `return [js.fromDyn](dynKeyGet(R, + * K))` or `dyn.keySet(R, K, value); return`, with R the receiver (or a + * bag of it). */ + private plainExit(s: IrStmt, receiver: string, value: string | undefined): JsIcSite | null { + const negated = (e: IrExpr, test: "classCapsule" | "funcKind"): boolean => + e.kind === "dynTest" && e.test === test && e.negated === true && isVar(e.value, receiver); + if (s.kind !== "if" || s.else_ !== null || !negated(s.cond, "classCapsule")) return null; + let body = s.then; + const inner = body[0]; + if ( + body.length === 1 && + inner?.kind === "if" && + inner.else_ === null && + negated(inner.cond, "funcKind") + ) + body = inner.then; + const [first, ret] = body; + if (body.length === 1 && first?.kind === "return") { + const read = unwrapFromDyn(first.value); + if ( + read?.kind !== "dynKeyGet" || + read.key.kind !== "strLit" || + !this.bagOf(read.value, receiver) + ) + return null; + return { kind: "get", key: read.key.value }; + } + if ( + body.length !== 2 || + first?.kind !== "exprStmt" || + ret?.kind !== "return" || + ret.value !== null + ) + return null; + const call = first.expr; + if (call.kind !== "libCall" || call.fn !== "dyn.keySet" || call.args.length !== 3) return null; + const [bag, key, stored] = call.args; + if (!bag || !this.bagOf(bag, receiver) || key?.kind !== "strLit") return null; + if (value === undefined || !isVar(stored, value)) return null; + return { kind: "set", key: key.value }; } /** `varDecl bag = BAG(receiver); return [js.fromDyn](bag === receiver ? From c2963628343abb47685249cedaa2613bb612c888 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 14:21:21 -0500 Subject: [PATCH 152/409] Park typed collection iterators on the entry they read An early exit from a loop over an iterator-typed Map or Set method parks the iterator with a second yield of the current entry. That yield read the entry again by its index, which aborted with a map iteration error when the loop had deleted the entry. Each step now reads the entry once into a local that both yields answer. `entries()` takes the typed walk only for a slot whose yield type is the key and value pair record. Other yield types, such as `IterableIterator` or ``, keep the checked adapter instead of failing to compile. --- .../frontend/lowering/iterator-adapters.ts | 28 +++++++++- .../7746-key-order-iterable-iterator.ts | 51 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index a8b4ea496..9f2b1e31f 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -84,6 +84,26 @@ function collectionIteratorSlotValue( collT.kind === "map" ? { kind: "mapIntrinsic", method: m, receiver: coll, args: [index], type: t, loc } : { kind: "setIntrinsic", method: "iterKey", receiver: coll, args: [index], type: t, loc }; + // entries() answers [key, value] pairs: only a slot whose yield type is + // that pair as a record of the two fields takes the typed walk; a tuple + // or other pair type keeps the checked adapter. + if (method === "entries") { + const pair = + expected.yieldT.kind === "record" ? lowerer.shapes.get(expected.yieldT.shapeId) : undefined; + const field = (name: string): IrType | undefined => + pair?.fields.find((f) => f.name === name)?.type; + const first = field("0"); + const second = field("1"); + if ( + pair === undefined || + pair.fields.length !== 2 || + first === undefined || + second === undefined || + !typeEquals(first, keyT) || + !typeEquals(second, valueT) + ) + return null; + } const entry = (): IrExpr => method === "keys" || (collT.kind === "set" && method === "values") ? read("iterKey", keyT) @@ -99,9 +119,13 @@ function collectionIteratorSlotValue( loc, }; if (!typeEquals(entry().type, expected.yieldT)) return null; + // The entry is read once per step and kept: the parking yield of an + // early exit answers it again rather than rereading a slot the loop may + // have deleted. + const current = varRef("current.0", expected.yieldT, loc); const suspend = (): IrStmt => ({ kind: "exprStmt", - expr: { kind: "yieldExpr", value: entry(), type: resumeType(expected.nextT), loc }, + expr: { kind: "yieldExpr", value: current, type: resumeType(expected.nextT), loc }, loc, }); const finish: IrStmt = { @@ -120,6 +144,7 @@ function collectionIteratorSlotValue( { id: "native.0", name: "native", type: DYN, mutable: false }, { id: "index.0", name: "index", type: F64, mutable: true }, { id: "resumed.0", name: "resumed", type: BOOL, mutable: true }, + { id: "current.0", name: "current", type: expected.yieldT, mutable: false }, ], body: [ { @@ -173,6 +198,7 @@ function collectionIteratorSlotValue( loc, }, setResumed(false, loc), + { kind: "varDecl", localId: "current.0", init: entry(), loc }, { kind: "tryCatch", tryBody: [suspend(), setResumed(true, loc)], diff --git a/tests/corpus/7746-key-order-iterable-iterator.ts b/tests/corpus/7746-key-order-iterable-iterator.ts index 13f27ee83..d9d4ea15a 100644 --- a/tests/corpus/7746-key-order-iterable-iterator.ts +++ b/tests/corpus/7746-key-order-iterable-iterator.ts @@ -56,3 +56,54 @@ const ordered = [...newest(listed)]; console.log(JSON.stringify(ordered), ordered[0] === listed[1]); const step = newest(listed).next(); console.log(step.done, JSON.stringify(step.value)); + +// Deleting the current entry before leaving a loop over an iterator-typed +// method: the iterator parks after that entry and continues with the next. +class Store { + readonly rows = new Map(); + readonly tags = new Set(); + values(): IterableIterator { + return this.rows.values(); + } + keys(): IterableIterator { + return this.rows.keys(); + } + tagValues(): IterableIterator { + return this.tags.values(); + } + anyPairs(): IterableIterator { + return this.rows.entries(); + } + unknownPairs(): IterableIterator { + return this.rows.entries(); + } + tagAny(): IterableIterator { + return this.tags.entries(); + } +} +const store = new Store(); +for (const [k, v] of [["a", 1], ["b", 2], ["c", 3]] as const) store.rows.set(k, v); +for (const t of ["x", "y", "z"]) store.tags.add(t); +console.log(JSON.stringify([...store.anyPairs()]), JSON.stringify([...store.unknownPairs()])); +console.log(JSON.stringify([...store.tagAny()])); +const values = store.values(); +for (const v of values) { + if (v === 2) { + store.rows.delete("b"); + break; + } +} +console.log([...values].join(), [...store.rows.keys()].join()); +for (const k of store.keys()) { + store.rows.delete(k); + break; +} +console.log([...store.rows.keys()].join()); +const tags = store.tagValues(); +for (const t of tags) { + if (t === "y") { + store.tags.delete("y"); + break; + } +} +console.log([...tags].join(), [...store.tags].join()); From 7cdfe16c9b3849f3bd75efcb1e352ecae0561eac Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 14:24:38 -0500 Subject: [PATCH 153/409] Cap the Linux test step's memory and spread memory-heavy side files The Linux test shards run their suites in a transient systemd scope with MemoryMax=13G and no swap, below the runner's 16 GB. A process tree that grows past the cap loses its largest process to the kernel, and the job log names the failing test, where the runner itself ran out of memory and left no log. Hosts without systemd run the suites directly. The side-queue balancer also takes a table of memory-heavy files, with their measured peak. They run whole in the cost-balanced task, and each goes to the shard with the least memory-heavy work so far, after the time-balanced files. decl-boundary.test.ts, which peaks at about 1.8 GB alone and had no time measurement, so vitest's file sharding put it in shard 11, is the first entry. It now runs in shard 1 (plain) and 6 (san). --- .github/workflows/ci.yml | 7 +++++ scripts/ci-test-plan.mjs | 36 ++++++++++++++++++++++--- tests/harness/ci-test-plan.test.ts | 43 +++++++++++++++++++++++++++--- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3bf0e770..5ab1d9331 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -216,9 +216,16 @@ jobs: # Start the corpus immediately beside a serial side queue. One shared # worker budget covers both processes; file and case shard axes stay # separate so every fixture still runs exactly once in each lane. + # The tests run in a transient scope capped below the runner's 16 GB: + # past 13 GB the kernel kills the scope's largest process, so the job + # log names the test instead of the runner itself running out of memory. - name: Tests (corpus and integration suites) run: | ulimit -c 0 + if [ -d /run/systemd/system ]; then + exec sudo --preserve-env systemd-run --scope --quiet -p MemoryMax=13G -p MemorySwapMax=0 \ + --uid="$(id -u)" --gid="$(id -g)" -- env PATH="$PATH" HOME="$HOME" node scripts/ci-test.mjs + fi node scripts/ci-test.mjs tests_macos: diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 2b76b52d6..184f04d1c 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -42,22 +42,38 @@ export const testCosts = JSON.parse( readFileSync(new URL("./ci-test-costs.json", import.meta.url), "utf8"), ); +// Side-queue files whose process tree, run alone, peaks far above a plain +// vitest run (about 1 GB): megabytes at the peak, measured with CI's settings. +// They run beside the corpus task's three workers, so no two of them share a +// shard while there are shards without one. +export const memoryHeavyFiles = { + "tests/harness/decl-boundary.test.ts": 1838, +}; + // The shard (1-based) of each measured side-queue file and isolated suite: // longest first to the shard with the least work so far, counting about a // second per file for vitest to import it. Ties go to the highest shard, so the // longest files start from the last shard while the cost-balanced corpus and // three.js cases (tests/harness/shard.ts) place their longest from the first. -// An isolated suite without a measurement weighs as much as the median -// measured file. Files without a measurement are left to vitest's --shard, -// which splits them by count. +// An isolated suite or memory-heavy file without a measurement weighs as much +// as the median measured file. Memory-heavy files are placed after the others, +// each on the shard with the least memory-heavy work so far and, among those, +// the least work. Files without a +// measurement are left to vitest's --shard, which splits them by count. export function sideOwners(flavor, count) { const costs = testCosts[flavor]; const known = Object.values(costs).sort((a, b) => a - b); const median = known.length === 0 ? 0 : known[Math.floor(known.length / 2)]; const weight = (file) => (costs[file] ?? median) + 1000; - const files = [...new Set([...Object.keys(costs), ...isolatedFiles.map(([, file]) => file)])]; + const heavy = Object.keys(memoryHeavyFiles).sort( + (a, b) => memoryHeavyFiles[b] - memoryHeavyFiles[a] || (a < b ? -1 : a > b ? 1 : 0), + ); + const files = [ + ...new Set([...Object.keys(costs), ...isolatedFiles.map(([, file]) => file)]), + ].filter((file) => !(file in memoryHeavyFiles)); files.sort((a, b) => weight(b) - weight(a) || (a < b ? -1 : a > b ? 1 : 0)); const loads = new Array(count).fill(0); + const memory = new Array(count).fill(0); const owners = new Map(); for (const file of files) { let lightest = count - 1; @@ -66,6 +82,18 @@ export function sideOwners(flavor, count) { loads[lightest] += weight(file); owners.set(file, lightest + 1); } + for (const file of heavy) { + let lightest = count - 1; + for (let shard = count - 2; shard >= 0; shard--) + if ( + memory[shard] < memory[lightest] || + (memory[shard] === memory[lightest] && loads[shard] < loads[lightest]) + ) + lightest = shard; + memory[lightest] += memoryHeavyFiles[file]; + loads[lightest] += weight(file); + owners.set(file, lightest + 1); + } return owners; } diff --git a/tests/harness/ci-test-plan.test.ts b/tests/harness/ci-test-plan.test.ts index 34d61346f..9ff730ddf 100644 --- a/tests/harness/ci-test-plan.test.ts +++ b/tests/harness/ci-test-plan.test.ts @@ -11,6 +11,8 @@ import { caseShardedSideFiles, isolatedFiles, leakSuppressions, + memoryHeavyFiles, + sideOwners, testCosts, } from "../../scripts/ci-test-plan.mjs"; import { runCiTestPlan } from "../../scripts/ci-test.mjs"; @@ -40,20 +42,27 @@ test.for([1, 3, 5, 10, 20])( file, ).toHaveLength(everyShard ? count : 1); } - // Each measured file runs on exactly one shard, outside vitest's file shard. + // Each measured or memory-heavy file runs on exactly one shard, outside + // vitest's file shard. const isolated = new Set(isolatedFiles.map(([, file]) => file)); const balanced = plans.flatMap((plan) => plan.side .filter((task) => task.name === "cost-balanced tests") .flatMap((task) => task.args.slice(2)), ); - const measured = Object.keys(testCosts[flavor]).filter((file) => !isolated.has(file)); + const measured = [ + ...new Set([...Object.keys(testCosts[flavor]), ...Object.keys(memoryHeavyFiles)]), + ].filter((file) => !isolated.has(file)); expect(measured.length).toBeGreaterThan(0); expect(balanced.toSorted()).toEqual(measured.toSorted()); for (const plan of plans) { expect(plan.corpus.args).toEqual(["test", ...corpusFiles]); expect(plan.side[0]!.args).toContain(`--shard=${plan.corpus.env.SCRIPTC_TEST_SHARD}`); - for (const file of [...fileExclusions, ...Object.keys(testCosts[flavor])]) + for (const file of [ + ...fileExclusions, + ...Object.keys(testCosts[flavor]), + ...Object.keys(memoryHeavyFiles), + ]) expect(plan.side[0]!.args).toContain(`--exclude=${file}`); const cache = plan.side.find((task) => task.name === "native-cache correctness"); expect(Boolean(cache)).toBe(flavor === "plain"); @@ -103,6 +112,26 @@ test("the side queue balances measured files across the twenty Linux shards", () } }); +test("memory-heavy side files run whole in the cost-balanced task, on shards of their own", () => { + for (const flavor of ["plain", "san"] as const) { + const owners = sideOwners(flavor, 20); + const heavy = Object.keys(memoryHeavyFiles); + const shards = heavy.map((file) => owners.get(file)); + expect(shards.every((shard) => shard !== undefined)).toBe(true); + expect(new Set(shards).size).toBe(Math.min(heavy.length, 20)); + for (const file of heavy) { + const plan = ciTestPlan({ shard: `${owners.get(file)}/20`, flavor }); + const balanced = plan.side.find((task) => task.name === "cost-balanced tests")!; + expect(balanced.args).toContain(file); + // The file-sharded task leaves it out on every shard. + for (let shard = 1; shard <= 20; shard++) + expect(ciTestPlan({ shard: `${shard}/20`, flavor }).side[0]!.args).toContain( + `--exclude=${file}`, + ); + } + } +}); + test.for([1, 2, 4, 8])( "CI overlaps reserved lanes within a budget of %i workers", async (workers) => { @@ -265,7 +294,13 @@ test("pull requests run the full suite on Linux and the darwin suite on macOS", ); expect(coreStep).toBeGreaterThan(-1); expect(coreStep).toBeLessThan(linux.steps.length - 1); - expect(linux.steps.at(-1)!.run).toBe("ulimit -c 0\nnode scripts/ci-test.mjs\n"); + // Linux runs the suites in a memory-capped scope where systemd manages the + // runner, and directly elsewhere. + const linuxRun = linux.steps.at(-1)!.run as string; + expect(linuxRun.startsWith("ulimit -c 0\n")).toBe(true); + expect(linuxRun).toContain("systemd-run --scope --quiet -p MemoryMax=13G -p MemorySwapMax=0"); + expect(linuxRun).toMatch(/-- env PATH="\$PATH" HOME="\$HOME" node scripts\/ci-test\.mjs\n/); + expect(linuxRun.endsWith("fi\nnode scripts/ci-test.mjs\n")).toBe(true); expect(macos.steps.at(-1)!.run).toBe("node scripts/ci-test.mjs"); for (const job of [linux, macos]) expect(job.strategy.matrix.flavor).toEqual(["plain", "san"]); }); From 89d122574f708d0bede331b8b21a9fd3505fbd35 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 14:27:20 -0500 Subject: [PATCH 154/409] Run Test262 tests that include testTypedArray.js --- tests/harness/test262-runner.test.ts | 20 + tests/harness/test262.test.ts | 69 +++ tests/test262/README.md | 4 +- tests/test262/run.ts | 6 +- tests/test262/support.mjs | 12 +- tests/test262/test-typed-array.js | 313 +++++++++++++ tests/test262/upstream.json | 13 + .../test262/vendor/harness/testTypedArray.js | 431 ++++++++++++++++++ .../prototype/byteOffset/return-byteoffset.js | 31 ++ .../return-abrupt-from-begin-symbol.js | 24 + .../byteoffset-is-negative-zero-sab.js | 23 + .../is-infinity-throws-rangeerror.js | 27 ++ .../undefined-newtarget-throws.js | 27 ++ ...mericindex-desc-not-configurable-throws.js | 39 ++ 14 files changed, 1032 insertions(+), 7 deletions(-) create mode 100644 tests/test262/test-typed-array.js create mode 100644 tests/test262/vendor/harness/testTypedArray.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js create mode 100644 tests/test262/vendor/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js diff --git a/tests/harness/test262-runner.test.ts b/tests/harness/test262-runner.test.ts index 6f6df35d4..3ddf3ca84 100644 --- a/tests/harness/test262-runner.test.ts +++ b/tests/harness/test262-runner.test.ts @@ -101,6 +101,26 @@ test("the property helper only admits direct inline descriptor checks", () => { } }); +test("the typed array helper is admitted without the %TypedArray% intrinsic", () => { + const text = source( + "includes: [testTypedArray.js]", + "testWithTypedArrayConstructors(function(TA) { assert.sameValue(new TA(1).length, 1); });", + ); + expect(exclusion(text, metadata(text), "strict")).toBeUndefined(); + const prepared = prepare(text, false, undefined, "strict", ["testTypedArray.js"]); + expect(prepared).toContain("function testWithTypedArrayConstructors("); + expect(prepared).not.toContain("Object.getPrototypeOf(Int8Array)"); + for (const body of [ + "assert.sameValue(typeof TypedArray, 'function');", + "testWithTypedArrayConstructors(function(TA) { assert.sameValue(Object.getPrototypeOf(TA), TypedArray); });", + ]) { + const named = source("includes: [testTypedArray.js]", body); + expect(exclusion(named, metadata(named), "strict")).toBe("harness:TypedArray-intrinsic"); + } + const other = source("includes: [testTypedArray.js, detachArrayBuffer.js]"); + expect(exclusion(other, metadata(other), "strict")).toBe("harness-includes:detachArrayBuffer.js"); +}); + test("receiver-bound this is admitted without adapting script-level this", () => { for (const [body, expected] of [ ["class C { read() { return this.value; } }", undefined], diff --git a/tests/harness/test262.test.ts b/tests/harness/test262.test.ts index a36fa2cdf..f1b616578 100644 --- a/tests/harness/test262.test.ts +++ b/tests/harness/test262.test.ts @@ -19,6 +19,10 @@ const upstreamHarness = ["assert.js", "sta.js"] .map((name) => readFileSync(join(vendorRoot, "harness", name), "utf8")) .join("\n"); const upstreamPropertyHelper = readFileSync(join(vendorRoot, "harness/propertyHelper.js"), "utf8"); +const upstreamTypedArrayHelper = readFileSync( + join(vendorRoot, "harness/testTypedArray.js"), + "utf8", +); function runUpstream( source: string, @@ -29,6 +33,8 @@ function runUpstream( runInContext(upstreamHarness, context, { timeout: 5000 }); if (includes.includes("propertyHelper.js")) runInContext(upstreamPropertyHelper, context, { timeout: 5000 }); + if (includes.includes("testTypedArray.js")) + runInContext(upstreamTypedArrayHelper, context, { timeout: 5000 }); runInContext(variant === "strict" ? `"use strict";\n${source}` : source, context, { timeout: 5000, }); @@ -52,6 +58,8 @@ async function runUpstreamAsync( runInContext(upstreamHarness, context, { timeout: 5000 }); if (includes.includes("propertyHelper.js")) runInContext(upstreamPropertyHelper, context, { timeout: 5000 }); + if (includes.includes("testTypedArray.js")) + runInContext(upstreamTypedArrayHelper, context, { timeout: 5000 }); runInContext(variant === "strict" ? `"use strict";\n${source}` : source, context, { timeout: 5000, }); @@ -375,6 +383,67 @@ if (propertyControls.length > 0) } }); +const typedArrayControls = shardSelect( + [ + { + name: "every constructor with array, array-like and buffer arguments", + status: "pass", + source: + "testWithTypedArrayConstructors(function(TA, makeCtorArg) { var sample = new TA(makeCtorArg([40, 41, 42])); assert.sameValue(sample.length, 3); assert.sameValue(sample[2], 42); }, null, ['passthrough', 'arraylike', 'arraybuffer']);", + }, + { + name: "failing element assertion", + status: "fail", + source: + "testWithTypedArrayConstructors(function(TA, makeCtorArg) { var sample = new TA(makeCtorArg([40, 41, 42])); assert.sameValue(sample[2], 43); });", + }, + { + name: "failure in one constructor", + status: "fail", + source: + "testWithTypedArrayConstructors(function(TA) { assert(TA !== Int16Array, 'Int16Array'); });", + }, + { + name: "passthrough factories visit each constructor once", + status: "pass", + source: + "var count = 0; testWithTypedArrayConstructors(function(TA, makeCtorArg) { assert.sameValue(new TA(makeCtorArg(4)).length, 4); count++; }, null, ['passthrough']); assert.sameValue(count, typedArrayConstructors.length);", + }, + { + name: "explicit constructor list", + status: "pass", + source: + "var names = []; testWithTypedArrayConstructors(function(TA) { names.push(TA.name); }, [Uint8Array, Float64Array], ['passthrough']); assert.compareArray(names, ['Uint8Array', 'Float64Array']);", + }, + { + name: "float constructor classification", + status: "pass", + source: + "assert(isFloatTypedArrayConstructor(Float32Array)); assert(!isFloatTypedArrayConstructor(Int8Array)); assert.sameValue(floatTypedArrayConstructorPrecision(Float64Array), 'double');", + }, + { + name: "unknown argument factory feature", + status: "fail", + source: "testWithTypedArrayConstructors(function() {}, null, ['unknown']);", + }, + ], + (item) => `typed-array:${item.name}`, +); + +if (typedArrayControls.length > 0) + describe(`Test262 typed array helper contract${shardSuffix()}`, () => { + for (const control of typedArrayControls) { + test(control.name, async () => { + const includes = ["compareArray.js", "testTypedArray.js"]; + if (control.status === "fail") + expect(() => runUpstream(control.source, "strict", includes)).toThrow(); + else expect(() => runUpstream(control.source, "strict", includes)).not.toThrow(); + const result = await runSource(control.source, { sanitize, includes }); + expect(result, JSON.stringify(result, null, 2)).toMatchObject({ status: control.status }); + }); + } + }); + const asyncControls = shardSelect( [ { diff --git a/tests/test262/README.md b/tests/test262/README.md index 46465645f..2f8a0ed3e 100644 --- a/tests/test262/README.md +++ b/tests/test262/README.md @@ -28,11 +28,11 @@ Pass the resulting directory as `--root` and use `--filter` to survey an API fam ## Execution profile and limits -The current profile is `static-adapted-v5`. It adapts strict scripts to standalone modules and sloppy scripts to CommonJS entries. Preparation supplies host assertions and a completion marker while leaving the upstream test body unchanged. Each program executes in a fresh native process. Conservative syntax checks reject known global-script dependencies, but cannot establish that every adaptation preserves the original script's semantics. Reports are local investigation artifacts, not published compatibility assessments. +The current profile is `static-adapted-v6`. It adapts strict scripts to standalone modules and sloppy scripts to CommonJS entries. Preparation supplies host assertions and a completion marker while leaving the upstream test body unchanged. Each program executes in a fresh native process. Conservative syntax checks reject known global-script dependencies, but cannot establish that every adaptation preserves the original script's semantics. Reports are local investigation artifacts, not published compatibility assessments. The unmodified upstream assertion helper currently reaches scriptc's JavaScript function-expando refusal. Test262 explicitly permits host implementations of harness functions in its [interpretation rules](https://github.com/tc39/test262/blob/7ab7fafa0003f73fc85c1b95d88094d33f7eb8bd/INTERPRETING.md#host-defined-functions). `harness.ts` supplies `assert`, `assert.sameValue`, `assert.notSameValue`, `assert.compareArray`, `assert.throws` for selected Error constructors, `$DONE`, and `Test262Error`. Scalar SameValue and scalar array-element comparisons use the statically compiled `node:assert/strict` implementation. Contract tests compare successful and failing assertions with the original Test262 helpers under Node, including NaN, signed zero, null/undefined, differing primitive types, and premature termination. -Reference-equality assertions terminate with a distinct harness-refusal result: conversion at a native function boundary can copy references, so this adapter cannot faithfully compare every object, array, or function identity. Tests cannot catch this refusal and accidentally pass. Sparse array comparisons are not yet handled faithfully across that boundary. The adapted `propertyHelper.js` admits direct `verifyProperty` calls with an inline descriptor literal or `undefined`; reference-valued comparisons still refuse, while other property helpers and descriptor expressions remain excluded. Assertion aliases, mutation, reflection, additional assertion methods, and other `includes` helpers remain excluded. +Reference-equality assertions terminate with a distinct harness-refusal result: conversion at a native function boundary can copy references, so this adapter cannot faithfully compare every object, array, or function identity. Tests cannot catch this refusal and accidentally pass. Sparse array comparisons are not yet handled faithfully across that boundary. The adapted `propertyHelper.js` admits direct `verifyProperty` calls with an inline descriptor literal or `undefined`; reference-valued comparisons still refuse, while other property helpers and descriptor expressions remain excluded. The adapted `testTypedArray.js` keeps the upstream constructor lists, argument factories and `testWith*` helpers. It omits the `TypedArray` (%TypedArray%) binding, so tests that name it are excluded. Its resizable and immutable buffer factories run only where `ArrayBuffer.prototype` provides `resize` and `transferToImmutable`, as the upstream feature checks intend. Contract tests compare its successful and failing uses with the upstream helper under Node. Assertion aliases, mutation, reflection, additional assertion methods, and other `includes` helpers remain excluded. Raw tests, module variants, agents, resolution negatives, and runtime negatives remain excluded. Parse negatives pass only when a source-matched syntax or vetted JavaScript early-error diagnostic appears; TypeScript type errors and harness-only failures never satisfy a negative expectation. `$262`, dynamic evaluation, observable script-global state, and other unsupported host requirements are excluded as well. The runtime and compiler continue to enforce their existing refusal boundaries for admitted source. diff --git a/tests/test262/run.ts b/tests/test262/run.ts index b798b3f21..2ad02100b 100644 --- a/tests/test262/run.ts +++ b/tests/test262/run.ts @@ -4,7 +4,7 @@ import { parseArgs } from "node:util"; import { shardSelect } from "../harness/shard.js"; import { runSource, type Outcome } from "./execute.js"; import { - assertThrowsSource, directory, exclusion, harnessSource, propertyHelperSource, matchesExpectation, matchesParseNegative, metadata, pin, sha256, snapshotDigest, summarize, + assertThrowsSource, directory, exclusion, harnessSource, propertyHelperSource, typedArrayHelperSource, matchesExpectation, matchesParseNegative, metadata, pin, sha256, snapshotDigest, summarize, testPaths, variants, vendorRoot, verifyVendor, } from "./support.mjs"; @@ -104,8 +104,8 @@ Other variants remain visible as exclusions.`); const report = { schema: "scriptc.test262.v1", revision: pin.commit, snapshotSha256: pin.snapshotSha256, - profile: "static-adapted-v5", - harnessSha256: sha256(harnessSource + assertThrowsSource + propertyHelperSource), + profile: "static-adapted-v6", + harnessSha256: sha256(harnessSource + assertThrowsSource + propertyHelperSource + typedArrayHelperSource), requestedBackend: backend, sanitize: process.env.SCRIPTC_SAN === "1", host: `${process.platform}-${process.arch}`, node: process.version, compilerVersion: JSON.parse(readFileSync(join(directory, "../../packages/compiler/package.json"), "utf8")).version, diff --git a/tests/test262/support.mjs b/tests/test262/support.mjs index e56612457..52739f91a 100644 --- a/tests/test262/support.mjs +++ b/tests/test262/support.mjs @@ -12,6 +12,8 @@ export const completion = "__scriptc_test262_complete__"; export const harnessSource = readFileSync(join(directory, "harness.ts"), "utf8"); export const assertThrowsSource = readFileSync(join(directory, "assert-throws.js"), "utf8"); export const propertyHelperSource = readFileSync(join(directory, "property-helper.js"), "utf8"); +export const typedArrayHelperSource = readFileSync(join(directory, "test-typed-array.js"), "utf8"); +const adaptedIncludes = new Set(["compareArray.js", "propertyHelper.js", "testTypedArray.js"]); export const expectations = JSON.parse(readFileSync(join(directory, "expectations.json"), "utf8")); export const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex"); @@ -131,11 +133,12 @@ export function exclusion(source, meta, variant) { } if (variant !== "strict" && variant !== "sloppy") return `execution:${variant}`; if (meta.flags.some((flag) => flag.startsWith("CanBlock"))) return "host:agents"; - const unsupportedIncludes = meta.includes.filter((name) => name !== "compareArray.js" && name !== "propertyHelper.js"); + const unsupportedIncludes = meta.includes.filter((name) => !adaptedIncludes.has(name)); if (unsupportedIncludes.length) return `harness-includes:${unsupportedIncludes.join(",")}`; const sf = ts.createSourceFile("test.js", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS); let reason; const propertyHelper = meta.includes.includes("propertyHelper.js"); + const typedArrayHelper = meta.includes.includes("testTypedArray.js"); const forbidden = new Set(["$262", "$DONE", "$DONOTEVALUATE", "globalThis", "eval", "Function", "print", "process", "require"]); if (variant === "sloppy") for (const name of ["module", "exports", "__dirname", "__filename"]) forbidden.add(name); if (meta.flags.includes("async")) forbidden.delete("$DONE"); @@ -170,6 +173,9 @@ export function exclusion(source, meta, variant) { 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"; + } else if (typedArrayHelper && ts.isIdentifier(node) && node.text === "TypedArray") { + // The adapted testTypedArray.js leaves out %TypedArray%. + reason = "harness:TypedArray-intrinsic"; } else if (propertyHelper && ts.isIdentifier(node)) { if (unsupportedPropertyHelpers.has(node.text)) reason = "harness:propertyHelper-surface"; else if (node.text === "verifyProperty") { @@ -188,7 +194,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 end = asyncTest ? "" : `;console.log(${JSON.stringify(marker)});\n`; - const helpers = includes.includes("propertyHelper.js") ? `${propertyHelperSource}\n` : ""; + const helpers = + (includes.includes("propertyHelper.js") ? `${propertyHelperSource}\n` : "") + + (includes.includes("testTypedArray.js") ? `${typedArrayHelperSource}\n` : ""); if (variant === "sloppy") return `const { ${names} } = require("./harness.ts");\n${helpers}${source}\n${end}`; return `"use strict";\nimport { ${names} } from "./harness.ts";\n${helpers}${source}\n${end}`; } diff --git a/tests/test262/test-typed-array.js b/tests/test262/test-typed-array.js new file mode 100644 index 000000000..f7ee73c84 --- /dev/null +++ b/tests/test262/test-typed-array.js @@ -0,0 +1,313 @@ +// Copyright (C) 2015 André Bargull. All rights reserved. +// This adaptation of Test262's testTypedArray.js is governed by the BSD license in vendor/LICENSE. +// It keeps the upstream constructor lists and argument factories. The host +// assert.js is replaced by harness.ts, so isPrimitive is defined here, and +// Test262Error is a class that needs `new`. Factories bind through a +// closure, and feature checks use typeof (and tolerate an ArrayBuffer +// without a reachable prototype, which then has no resizable or immutable +// factories). %TypedArray% (`TypedArray`) is left out: tests that name it +// are excluded. + +function isPrimitive(value) { + return !value || (typeof value !== "object" && typeof value !== "function"); +} + +var floatArrayConstructors = [ + Float64Array, + Float32Array +]; + +var nonClampedIntArrayConstructors = [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array +]; + +var intArrayConstructors = nonClampedIntArrayConstructors.concat([Uint8ClampedArray]); + +// Float16Array is a newer feature +// adding it to this list unconditionally would cause implementations lacking it to fail every test which uses it +if (typeof Float16Array !== "undefined") { + floatArrayConstructors.push(Float16Array); +} + +var bigIntArrayConstructors = []; +if (typeof BigInt64Array !== "undefined") { + bigIntArrayConstructors.push(BigInt64Array); +} +if (typeof BigUint64Array !== "undefined") { + bigIntArrayConstructors.push(BigUint64Array); +} + +var typedArrayConstructors = floatArrayConstructors.concat(intArrayConstructors); + +var allTypedArrayConstructors = typedArrayConstructors.concat(bigIntArrayConstructors); + +function makePassthrough(TA, primitiveOrIterable) { + return primitiveOrIterable; +} + +function makeArray(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable); + // Only values between 0 and 2**53 - 1 inclusive can get mapped into TA contents. + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return Array.from({ length: n }, function() { return "0"; }); + } + return Array.from(primitiveOrIterable); +} + +function makeArrayLike(TA, primitiveOrIterable) { + var arr = makeArray(TA, primitiveOrIterable); + if (isPrimitive(arr)) return arr; + var obj = { length: arr.length }; + for (var i = 0; i < obj.length; i++) obj[i] = arr[i]; + return obj; +} + +var makeIterable; +if (typeof Symbol !== "undefined") { + makeIterable = function makeIterable(TA, primitiveOrIterable) { + var src = makeArray(TA, primitiveOrIterable); + if (isPrimitive(src)) return src; + var obj = {}; + obj[Symbol.iterator] = function() { return src[Symbol.iterator](); }; + return obj; + }; +} + +function makeArrayBuffer(TA, primitiveOrIterable) { + var arr = makeArray(TA, primitiveOrIterable); + if (isPrimitive(arr)) return arr; + return new TA(arr).buffer; +} + +var makeResizableArrayBuffer, makeGrownArrayBuffer, makeShrunkArrayBuffer, makeImmutableArrayBuffer; +if (typeof ArrayBuffer.prototype === "object" && typeof ArrayBuffer.prototype.resize === "function") { + var copyIntoArrayBuffer = function(destBuffer, srcBuffer) { + var destView = new Uint8Array(destBuffer); + var srcView = new Uint8Array(srcBuffer); + for (var i = 0; i < srcView.length; i++) destView[i] = srcView[i]; + return destBuffer; + }; + + makeResizableArrayBuffer = function makeResizableArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return new ArrayBuffer(n, { maxByteLength: n * 2 }); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var resizable = new ArrayBuffer(byteLength, { maxByteLength: byteLength * 2 }); + return copyIntoArrayBuffer(resizable, fixed); + }; + + makeGrownArrayBuffer = function makeGrownArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + var grown = new ArrayBuffer(Math.floor(n / 2), { maxByteLength: n }); + grown.resize(n); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var grown = new ArrayBuffer(Math.floor(byteLength / 2), { maxByteLength: byteLength }); + grown.resize(byteLength); + return copyIntoArrayBuffer(grown, fixed); + }; + + makeShrunkArrayBuffer = function makeShrunkArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + var shrunk = new ArrayBuffer(n * 2, { maxByteLength: n * 2 }); + shrunk.resize(n); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var shrunk = new ArrayBuffer(byteLength * 2, { maxByteLength: byteLength * 2 }); + copyIntoArrayBuffer(shrunk, fixed); + shrunk.resize(byteLength); + return shrunk; + }; +} +if (typeof ArrayBuffer.prototype === "object" && typeof ArrayBuffer.prototype.transferToImmutable === "function") { + makeImmutableArrayBuffer = function makeImmutableArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return (new ArrayBuffer(n)).transferToImmutable(); + } + var mutable = makeArrayBuffer(TA, primitiveOrIterable); + return mutable.transferToImmutable(); + }; +} + +var typedArrayCtorArgFactories = [makePassthrough, makeArray, makeArrayLike]; +if (makeIterable) typedArrayCtorArgFactories.push(makeIterable); +typedArrayCtorArgFactories.push(makeArrayBuffer); +if (makeResizableArrayBuffer) typedArrayCtorArgFactories.push(makeResizableArrayBuffer); +if (makeGrownArrayBuffer) typedArrayCtorArgFactories.push(makeGrownArrayBuffer); +if (makeShrunkArrayBuffer) typedArrayCtorArgFactories.push(makeShrunkArrayBuffer); +if (makeImmutableArrayBuffer) typedArrayCtorArgFactories.push(makeImmutableArrayBuffer); + + +function ctorArgFactoryMatchesSome(argFactory, features) { + for (var i = 0; i < features.length; ++i) { + switch (features[i]) { + case "passthrough": + if (argFactory === makePassthrough) return true; + break; + case "arraylike": + if (argFactory === makeArray || argFactory === makeArrayLike) return true; + break; + case "iterable": + if (argFactory === makeIterable) return true; + break; + case "arraybuffer": + if ( + argFactory === makeArrayBuffer || + argFactory === makeResizableArrayBuffer || + argFactory === makeGrownArrayBuffer || + argFactory === makeShrunkArrayBuffer || + argFactory === makeImmutableArrayBuffer + ) { + return true; + } + break; + case "resizable": + if ( + argFactory === makeResizableArrayBuffer || + argFactory === makeGrownArrayBuffer || + argFactory === makeShrunkArrayBuffer + ) { + return true; + } + break; + case "immutable": + if (argFactory === makeImmutableArrayBuffer) return true; + break; + default: + throw new Test262Error("unknown feature: " + features[i]); + } + } + return false; +} + + +function bindArgFactory(argFactory, constructor) { + return function(primitiveOrIterable) { + return argFactory(constructor, primitiveOrIterable); + }; +} + +function testWithAllTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || allTypedArrayConstructors; + var ctorArgFactories = typedArrayCtorArgFactories; + if (includeArgFactories) { + ctorArgFactories = []; + for (var i = 0; i < typedArrayCtorArgFactories.length; ++i) { + if (ctorArgFactoryMatchesSome(typedArrayCtorArgFactories[i], includeArgFactories)) { + ctorArgFactories.push(typedArrayCtorArgFactories[i]); + } + } + } + if (excludeArgFactories) { + ctorArgFactories = ctorArgFactories.slice(); + for (var i = ctorArgFactories.length - 1; i >= 0; --i) { + if (ctorArgFactoryMatchesSome(ctorArgFactories[i], excludeArgFactories)) { + ctorArgFactories.splice(i, 1); + } + } + } + if (ctorArgFactories.length === 0) { + throw new Test262Error("no arg factories match include " + includeArgFactories + " and exclude " + excludeArgFactories); + } + for (var k = 0; k < ctorArgFactories.length; ++k) { + var argFactory = ctorArgFactories[k]; + for (var i = 0; i < ctors.length; ++i) { + var constructor = ctors[i]; + var boundArgFactory = bindArgFactory(argFactory, constructor); + try { + f(constructor, boundArgFactory); + } catch (e) { + e.message += " (Testing with " + constructor.name + " and " + argFactory.name + ".)"; + throw e; + } + } + } +} + +function testWithTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || typedArrayConstructors; + testWithAllTypedArrayConstructors(f, ctors, includeArgFactories, excludeArgFactories); +} + +function testWithBigIntTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || [BigInt64Array, BigUint64Array]; + testWithAllTypedArrayConstructors(f, ctors, includeArgFactories, excludeArgFactories); +} + +var nonAtomicsFriendlyTypedArrayConstructors = floatArrayConstructors.concat([Uint8ClampedArray]); +function testWithNonAtomicsFriendlyTypedArrayConstructors(f, includeArgFactories, excludeArgFactories) { + testWithAllTypedArrayConstructors( + f, + nonAtomicsFriendlyTypedArrayConstructors, + includeArgFactories, + excludeArgFactories + ); +} + +function testWithAtomicsFriendlyTypedArrayConstructors(f, includeArgFactories, excludeArgFactories) { + testWithAllTypedArrayConstructors( + f, + [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array, + ], + includeArgFactories, + excludeArgFactories + ); +} + +function testTypedArrayConversions(byteConversionValues, fn) { + var values = byteConversionValues.values; + var expected = byteConversionValues.expected; + + testWithTypedArrayConstructors(function(TA) { + var name = TA.name.slice(0, -5); + + return values.forEach(function(value, index) { + var exp = expected[name][index]; + var initial = 0; + if (exp === 0) { + initial = 1; + } + fn(TA, value, exp, initial); + }); + }, null, ["passthrough"]); +} + +function isFloatTypedArrayConstructor(arg) { + return floatArrayConstructors.indexOf(arg) !== -1; +} + +function floatTypedArrayConstructorPrecision(FA) { + if (typeof Float16Array !== "undefined" && FA === Float16Array) { + return "half"; + } else if (FA === Float32Array) { + return "single"; + } else if (FA === Float64Array) { + return "double"; + } else { + throw new Error("Malformed test - floatTypedArrayConstructorPrecision called with non-float TypedArray"); + } +} diff --git a/tests/test262/upstream.json b/tests/test262/upstream.json index 1dc97ba20..025576e72 100644 --- a/tests/test262/upstream.json +++ b/tests/test262/upstream.json @@ -169,6 +169,12 @@ "test/built-ins/String/prototype/trim/15.5.4.20-2-47.js", "test/built-ins/String/prototype/trim/15.5.4.20-2-50.js", "test/built-ins/String/prototype/trim/15.5.4.20-2-6.js", + "test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js", + "test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js", + "test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js", + "test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js", + "test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js", + "test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js", "test/language/arguments-object/func-decl-args-trailing-comma-null.js", "test/language/arguments-object/func-expr-args-trailing-comma-undefined.js", "test/language/expressions/addition/S11.6.1_A4_T1.js", @@ -214,6 +220,7 @@ "harness/assert.js": "206e274ca325eb8a652e3911c3fbd090e2480d11ed7579dc17a5d17a2360ed48", "harness/propertyHelper.js": "08bc29114d804c4f24f906bac950ee4dc9c93c659d93258e7b44a54283ba9bb8", "harness/sta.js": "1930c54af79455c484799f43e9a28e2b2f15c40d0917c9941ca54e26db243f35", + "harness/testTypedArray.js": "51eb50e53e6215f31f14d64d249bea2d08ad943c77835f63ba52ec694de2189a", "test/annexB/language/function-code/function-redeclaration-block.js": "8d4a3f259dabceeee101959aac117a19f5bfe9e374e1178b45f45a1142195d9e", "test/annexB/language/function-code/if-decl-else-decl-b-func-existing-fn-no-init.js": "ccd95922fd6c784012f7299445f0859db04812232a2266dd90070b026244bbf5", "test/built-ins/Array/prototype/at/returns-item-relative-index.js": "71f53cf86f1387101b19b6ae9939d47ced34497d3cee12f954eef454f912ba88", @@ -377,6 +384,12 @@ "test/built-ins/String/prototype/trim/15.5.4.20-2-47.js": "3e1a745af465177a96a29601e56299afc044060dbc2ac3061a59032be451cfde", "test/built-ins/String/prototype/trim/15.5.4.20-2-50.js": "38b84997faa99e422408917eeac7f3575bd6064b429ea390a9d041fd77fce532", "test/built-ins/String/prototype/trim/15.5.4.20-2-6.js": "3d5857f24117f65f07a70edd0d254dfa07f593c76effed94500847ccf8564c06", + "test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js": "04cd6e31c8223319bbc6f5e5630ee476708a3897ead70e9cec34d40fb3ebd062", + "test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js": "51fd475317d19c863a85c26f1ffa2070b10abf123b49a3243bb3829218b0b7a9", + "test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js": "96741e885f333125077884cb59c23c297fad61a4834941a5ac38720227094d7d", + "test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js": "ea340740e8fb3227b19fff023508c38d3bd6b2ff45f7a623d1e4460fad9b32c1", + "test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js": "19d2ebe0d3db51eb9f978b9b2462bb38ff5cf4702f32e3163d8be46e851d0f8f", + "test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js": "711cf2873e49c9d48b3246f8dd067380ac902d286907b620735f5f4dbd36c943", "test/language/arguments-object/func-decl-args-trailing-comma-null.js": "80aecea8edd0d7bac1ddcfcd6cb97dc9a513b25a9283bb9cedd21fd77dd64d50", "test/language/arguments-object/func-expr-args-trailing-comma-undefined.js": "cf39515a9031db4c81db231a157516bdc97617c392e4ef28890b2ae74777a99e", "test/language/expressions/addition/S11.6.1_A4_T1.js": "12928eb319dcf0cc785e399580effb0b7495709296dce9c0a58e8408616a959f", diff --git a/tests/test262/vendor/harness/testTypedArray.js b/tests/test262/vendor/harness/testTypedArray.js new file mode 100644 index 000000000..a6418c2f7 --- /dev/null +++ b/tests/test262/vendor/harness/testTypedArray.js @@ -0,0 +1,431 @@ +// Copyright (C) 2015 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +description: | + Collection of functions used to assert the correctness of TypedArray objects. +defines: + - floatArrayConstructors + - nonClampedIntArrayConstructors + - intArrayConstructors + - typedArrayConstructors + - TypedArray + - testWithAllTypedArrayConstructors + - testWithTypedArrayConstructors + - testWithBigIntTypedArrayConstructors + - nonAtomicsFriendlyTypedArrayConstructors + - testWithAtomicsFriendlyTypedArrayConstructors + - testWithNonAtomicsFriendlyTypedArrayConstructors + - testTypedArrayConversions +---*/ + +var floatArrayConstructors = [ + Float64Array, + Float32Array +]; + +var nonClampedIntArrayConstructors = [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array +]; + +var intArrayConstructors = nonClampedIntArrayConstructors.concat([Uint8ClampedArray]); + +// Float16Array is a newer feature +// adding it to this list unconditionally would cause implementations lacking it to fail every test which uses it +if (typeof Float16Array !== "undefined") { + floatArrayConstructors.push(Float16Array); +} + +var bigIntArrayConstructors = []; +if (typeof BigInt64Array !== "undefined") { + bigIntArrayConstructors.push(BigInt64Array); +} +if (typeof BigUint64Array !== "undefined") { + bigIntArrayConstructors.push(BigUint64Array); +} + +/** + * Array containing every non-bigint typed array constructor. + */ +var typedArrayConstructors = floatArrayConstructors.concat(intArrayConstructors); + +/** + * Array containing every typed array constructor, including those with bigint values. + */ +var allTypedArrayConstructors = typedArrayConstructors.concat(bigIntArrayConstructors); + +/** + * The %TypedArray% intrinsic constructor function. + */ +var TypedArray = Object.getPrototypeOf(Int8Array); + +function makePassthrough(TA, primitiveOrIterable) { + return primitiveOrIterable; +} + +function makeArray(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable); + // Only values between 0 and 2**53 - 1 inclusive can get mapped into TA contents. + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return Array.from({ length: n }, function() { return "0"; }); + } + return Array.from(primitiveOrIterable); +} + +function makeArrayLike(TA, primitiveOrIterable) { + var arr = makeArray(TA, primitiveOrIterable); + if (isPrimitive(arr)) return arr; + var obj = { length: arr.length }; + for (var i = 0; i < obj.length; i++) obj[i] = arr[i]; + return obj; +} + +var makeIterable; +if (typeof Symbol !== "undefined" && Symbol.iterator) { + makeIterable = function makeIterable(TA, primitiveOrIterable) { + var src = makeArray(TA, primitiveOrIterable); + if (isPrimitive(src)) return src; + var obj = {}; + obj[Symbol.iterator] = function() { return src[Symbol.iterator](); }; + return obj; + }; +} + +function makeArrayBuffer(TA, primitiveOrIterable) { + var arr = makeArray(TA, primitiveOrIterable); + if (isPrimitive(arr)) return arr; + return new TA(arr).buffer; +} + +var makeResizableArrayBuffer, makeGrownArrayBuffer, makeShrunkArrayBuffer, makeImmutableArrayBuffer; +if (ArrayBuffer.prototype.resize) { + var copyIntoArrayBuffer = function(destBuffer, srcBuffer) { + var destView = new Uint8Array(destBuffer); + var srcView = new Uint8Array(srcBuffer); + for (var i = 0; i < srcView.length; i++) destView[i] = srcView[i]; + return destBuffer; + }; + + makeResizableArrayBuffer = function makeResizableArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return new ArrayBuffer(n, { maxByteLength: n * 2 }); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var resizable = new ArrayBuffer(byteLength, { maxByteLength: byteLength * 2 }); + return copyIntoArrayBuffer(resizable, fixed); + }; + + makeGrownArrayBuffer = function makeGrownArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + var grown = new ArrayBuffer(Math.floor(n / 2), { maxByteLength: n }); + grown.resize(n); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var grown = new ArrayBuffer(Math.floor(byteLength / 2), { maxByteLength: byteLength }); + grown.resize(byteLength); + return copyIntoArrayBuffer(grown, fixed); + }; + + makeShrunkArrayBuffer = function makeShrunkArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + var shrunk = new ArrayBuffer(n * 2, { maxByteLength: n * 2 }); + shrunk.resize(n); + } + var fixed = makeArrayBuffer(TA, primitiveOrIterable); + var byteLength = fixed.byteLength; + var shrunk = new ArrayBuffer(byteLength * 2, { maxByteLength: byteLength * 2 }); + copyIntoArrayBuffer(shrunk, fixed); + shrunk.resize(byteLength); + return shrunk; + }; +} +if (ArrayBuffer.prototype.transferToImmutable) { + makeImmutableArrayBuffer = function makeImmutableArrayBuffer(TA, primitiveOrIterable) { + if (isPrimitive(primitiveOrIterable)) { + var n = Number(primitiveOrIterable) * TA.BYTES_PER_ELEMENT; + if (!(n >= 0 && n < 9007199254740992)) return primitiveOrIterable; + return (new ArrayBuffer(n)).transferToImmutable(); + } + var mutable = makeArrayBuffer(TA, primitiveOrIterable); + return mutable.transferToImmutable(); + }; +} + +var typedArrayCtorArgFactories = [makePassthrough, makeArray, makeArrayLike]; +if (makeIterable) typedArrayCtorArgFactories.push(makeIterable); +typedArrayCtorArgFactories.push(makeArrayBuffer); +if (makeResizableArrayBuffer) typedArrayCtorArgFactories.push(makeResizableArrayBuffer); +if (makeGrownArrayBuffer) typedArrayCtorArgFactories.push(makeGrownArrayBuffer); +if (makeShrunkArrayBuffer) typedArrayCtorArgFactories.push(makeShrunkArrayBuffer); +if (makeImmutableArrayBuffer) typedArrayCtorArgFactories.push(makeImmutableArrayBuffer); + +/** + * @typedef {"passthrough" | "arraylike" | "iterable" | "arraybuffer" | "resizable" | "immutable"} typedArrayArgFactoryFeature + */ + +/** + * A predicate for testing whether a TypedArray argument factory from this file + * matches any of the provided features. + * + * @param {Function} argFactory + * @param {typedArrayArgFactoryFeature[]} features + * @returns {boolean} + */ +function ctorArgFactoryMatchesSome(argFactory, features) { + for (var i = 0; i < features.length; ++i) { + switch (features[i]) { + case "passthrough": + if (argFactory === makePassthrough) return true; + break; + case "arraylike": + if (argFactory === makeArray || argFactory === makeArrayLike) return true; + break; + case "iterable": + if (argFactory === makeIterable) return true; + break; + case "arraybuffer": + if ( + argFactory === makeArrayBuffer || + argFactory === makeResizableArrayBuffer || + argFactory === makeGrownArrayBuffer || + argFactory === makeShrunkArrayBuffer || + argFactory === makeImmutableArrayBuffer + ) { + return true; + } + break; + case "resizable": + if ( + argFactory === makeResizableArrayBuffer || + argFactory === makeGrownArrayBuffer || + argFactory === makeShrunkArrayBuffer + ) { + return true; + } + break; + case "immutable": + if (argFactory === makeImmutableArrayBuffer) return true; + break; + default: + throw Test262Error("unknown feature: " + features[i]); + } + } + return false; +} + +/** + * Callback for testing a typed array constructor. + * + * @callback typedArrayConstructorCallback + * @param {Function} TypedArrayConstructor the constructor object to test with + * @param {Function} [TypedArrayConstructorArgFactory] a function for making + * a TypedArrayConstructor argument from a primitive (usually a number) or + * iterable (usually an array) + */ + +/** + * Calls the provided function with (typedArrayCtor, typedArrayCtorArgFactory) + * pairs, where typedArrayCtor is Uint8Array/Int8Array/BigInt64Array/etc. and + * typedArrayCtorArgFactory is a function for mapping a primitive (usually a + * number) or iterable (usually an array) into a value suitable as the first + * argument of typedArrayCtor (an Array, arraylike, iterable, or ArrayBuffer). + * + * @param {typedArrayConstructorCallback} f - the function to call + * @param {Array} [constructors] - an explicit list of TypedArray constructors + * @param {typedArrayArgFactoryFeature[]} [includeArgFactories] - for selecting + * initial constructor argument factory functions, rather than starting with + * all argument factories + * @param {typedArrayArgFactoryFeature[]} [excludeArgFactories] - for excluding + * constructor argument factory functions, after an initial selection + */ +function testWithAllTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || allTypedArrayConstructors; + var ctorArgFactories = typedArrayCtorArgFactories; + if (includeArgFactories) { + ctorArgFactories = []; + for (var i = 0; i < typedArrayCtorArgFactories.length; ++i) { + if (ctorArgFactoryMatchesSome(typedArrayCtorArgFactories[i], includeArgFactories)) { + ctorArgFactories.push(typedArrayCtorArgFactories[i]); + } + } + } + if (excludeArgFactories) { + ctorArgFactories = ctorArgFactories.slice(); + for (var i = ctorArgFactories.length - 1; i >= 0; --i) { + if (ctorArgFactoryMatchesSome(ctorArgFactories[i], excludeArgFactories)) { + ctorArgFactories.splice(i, 1); + } + } + } + if (ctorArgFactories.length === 0) { + throw Test262Error("no arg factories match include " + includeArgFactories + " and exclude " + excludeArgFactories); + } + for (var k = 0; k < ctorArgFactories.length; ++k) { + var argFactory = ctorArgFactories[k]; + for (var i = 0; i < ctors.length; ++i) { + var constructor = ctors[i]; + var boundArgFactory = argFactory.bind(undefined, constructor); + try { + f(constructor, boundArgFactory); + } catch (e) { + e.message += " (Testing with " + constructor.name + " and " + argFactory.name + ".)"; + throw e; + } + } + } +} + +/** + * Calls the provided function with (typedArrayCtor, typedArrayCtorArgFactory) + * pairs, where typedArrayCtor is Uint8Array/Int8Array/BigInt64Array/etc. and + * typedArrayCtorArgFactory is a function for mapping a primitive (usually a + * number) or iterable (usually an array) into a value suitable as the first + * argument of typedArrayCtor (an Array, arraylike, iterable, or ArrayBuffer). + * + * typedArrayCtor will not be BigInt64Array or BigUint64Array unless one or both + * of those are explicitly provided. + * + * @param {typedArrayConstructorCallback} f - the function to call + * @param {Array} [constructors] - an explicit list of TypedArray constructors + * @param {typedArrayArgFactoryFeature[]} [includeArgFactories] - for selecting + * initial constructor argument factory functions, rather than starting with + * all argument factories + * @param {typedArrayArgFactoryFeature[]} [excludeArgFactories] - for excluding + * constructor argument factory functions, after an initial selection + */ +function testWithTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || typedArrayConstructors; + testWithAllTypedArrayConstructors(f, ctors, includeArgFactories, excludeArgFactories); +} + +/** + * Calls the provided function for every BigInt typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call + * @param {Array} [constructors] - an explicit list of TypedArray constructors + * @param {typedArrayArgFactoryFeature[]} [includeArgFactories] - for selecting + * initial constructor argument factory functions, rather than starting with + * all argument factories + * @param {typedArrayArgFactoryFeature[]} [excludeArgFactories] - for excluding + * constructor argument factory functions, after an initial selection + */ +function testWithBigIntTypedArrayConstructors(f, constructors, includeArgFactories, excludeArgFactories) { + var ctors = constructors || [BigInt64Array, BigUint64Array]; + testWithAllTypedArrayConstructors(f, ctors, includeArgFactories, excludeArgFactories); +} + +var nonAtomicsFriendlyTypedArrayConstructors = floatArrayConstructors.concat([Uint8ClampedArray]); +/** + * Calls the provided function for every non-"Atomics Friendly" typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call for each typed array constructor. + * @param {Array} selected - An optional Array with filtered typed arrays + * @param {typedArrayArgFactoryFeature[]} [includeArgFactories] - for selecting + * initial constructor argument factory functions, rather than starting with + * all argument factories + * @param {typedArrayArgFactoryFeature[]} [excludeArgFactories] - for excluding + * constructor argument factory functions, after an initial selection + */ +function testWithNonAtomicsFriendlyTypedArrayConstructors(f, includeArgFactories, excludeArgFactories) { + testWithAllTypedArrayConstructors( + f, + nonAtomicsFriendlyTypedArrayConstructors, + includeArgFactories, + excludeArgFactories + ); +} + +/** + * Calls the provided function for every "Atomics Friendly" typed array constructor. + * + * @param {typedArrayConstructorCallback} f - the function to call for each typed array constructor. + * @param {Array} selected - An optional Array with filtered typed arrays + * @param {typedArrayArgFactoryFeature[]} [includeArgFactories] - for selecting + * initial constructor argument factory functions, rather than starting with + * all argument factories + * @param {typedArrayArgFactoryFeature[]} [excludeArgFactories] - for excluding + * constructor argument factory functions, after an initial selection + */ +function testWithAtomicsFriendlyTypedArrayConstructors(f, includeArgFactories, excludeArgFactories) { + testWithAllTypedArrayConstructors( + f, + [ + Int32Array, + Int16Array, + Int8Array, + Uint32Array, + Uint16Array, + Uint8Array, + ], + includeArgFactories, + excludeArgFactories + ); +} + +/** + * Helper for conversion operations on TypedArrays, the expected values + * properties are indexed in order to match the respective value for each + * TypedArray constructor + * @param {Function} fn - the function to call for each constructor and value. + * will be called with the constructor, value, expected + * value, and a initial value that can be used to avoid + * a false positive with an equivalent expected value. + */ +function testTypedArrayConversions(byteConversionValues, fn) { + var values = byteConversionValues.values; + var expected = byteConversionValues.expected; + + testWithTypedArrayConstructors(function(TA) { + var name = TA.name.slice(0, -5); + + return values.forEach(function(value, index) { + var exp = expected[name][index]; + var initial = 0; + if (exp === 0) { + initial = 1; + } + fn(TA, value, exp, initial); + }); + }, null, ["passthrough"]); +} + +/** + * Checks if the given argument is one of the float-based TypedArray constructors. + * + * @param {constructor} ctor - the value to check + * @returns {boolean} + */ +function isFloatTypedArrayConstructor(arg) { + return floatArrayConstructors.indexOf(arg) !== -1; +} + +/** + * Determines the precision of the given float-based TypedArray constructor. + * + * @param {constructor} ctor - the value to check + * @returns {string} "half", "single", or "double" for Float16Array, Float32Array, and Float64Array respectively. + */ +function floatTypedArrayConstructorPrecision(FA) { + if (typeof Float16Array !== "undefined" && FA === Float16Array) { + return "half"; + } else if (FA === Float32Array) { + return "single"; + } else if (FA === Float64Array) { + return "double"; + } else { + throw new Error("Malformed test - floatTypedArrayConstructorPrecision called with non-float TypedArray"); + } +} diff --git a/tests/test262/vendor/test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js b/tests/test262/vendor/test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js new file mode 100644 index 000000000..cbca8c3ec --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArray/prototype/byteOffset/return-byteoffset.js @@ -0,0 +1,31 @@ +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-get-%typedarray%.prototype.byteoffset +description: > + Return value from [[ByteOffset]] internal slot +info: | + 22.2.3.3 get %TypedArray%.prototype.byteOffset + + ... + 6. Let offset be the value of O's [[ByteOffset]] internal slot. + 7. Return size. +includes: [testTypedArray.js] +features: [TypedArray] +---*/ + +testWithTypedArrayConstructors(function(TA, makeCtorArg) { + var ta1 = new TA(); + assert.sameValue(ta1.byteOffset, 0, "Regular typedArray"); + + var offset = 4 * TA.BYTES_PER_ELEMENT; + + var buffer1 = makeCtorArg(8); + var ta2 = new TA(buffer1, offset); + assert.sameValue(ta2.byteOffset, offset, "TA(buffer, offset)"); + + var buffer2 = makeCtorArg(8); + var sample = new TA(buffer2, offset); + var ta3 = new TA(sample); + assert.sameValue(ta3.byteOffset, 0, "TA(typedArray)"); +}, null, ["arraybuffer"]); diff --git a/tests/test262/vendor/test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js b/tests/test262/vendor/test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js new file mode 100644 index 000000000..1e54741d3 --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArray/prototype/subarray/return-abrupt-from-begin-symbol.js @@ -0,0 +1,24 @@ +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-%typedarray%.prototype.subarray +description: Return abrupt from ToInteger(begin), begin is symbol +info: | + 22.2.3.27 %TypedArray%.prototype.subarray( begin , end ) + + ... + 7. Let relativeBegin be ? ToInteger(begin). + ... +includes: [testTypedArray.js] +features: [Symbol, TypedArray] +---*/ + +var s = Symbol("1"); + +testWithTypedArrayConstructors(function(TA, makeCtorArg) { + var sample = new TA(makeCtorArg(0)); + + assert.throws(TypeError, function() { + sample.subarray(s); + }); +}); diff --git a/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js new file mode 100644 index 000000000..b4bf00149 --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/buffer-arg/byteoffset-is-negative-zero-sab.js @@ -0,0 +1,23 @@ +// Copyright (C) 2015 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: pending +description: > + TypedArray's [[ByteOffset]] internal slot is always a positive number, test with negative zero. +info: | + %TypedArray% ( buffer [ , byteOffset [ , length ] ] ) + + ... + 6. Let offset be ? ToInteger(byteOffset). + 7. If offset < 0, throw a RangeError exception. + 8. If offset is -0, let offset be +0. + ... +includes: [testTypedArray.js] +features: [SharedArrayBuffer, TypedArray] +---*/ + +testWithTypedArrayConstructors(function(TAConstructor) { + var typedArray = new TAConstructor(new SharedArrayBuffer(8), -0); + assert.sameValue(typedArray.byteOffset, +0); +}, null, ["passthrough"]); diff --git a/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js new file mode 100644 index 000000000..c04f79f76 --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/length-arg/is-infinity-throws-rangeerror.js @@ -0,0 +1,27 @@ +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-typedarray-length +description: > + Throws a RangeError if length is a Infinity value +info: | + 22.2.4.2 TypedArray ( length ) + + This description applies only if the TypedArray function is called with at + least one argument and the Type of the first argument is not Object. + + ... + 4. Let numberLength be ? ToNumber(length). + 5. Let elementLength be ToLength(numberLength). + 6. If SameValueZero(numberLength, elementLength) is false, throw a RangeError + exception. + ... +includes: [testTypedArray.js] +features: [TypedArray] +---*/ + +testWithTypedArrayConstructors(function(TA) { + assert.throws(RangeError, function() { + new TA(Infinity); + }); +}, null, ["passthrough"]); diff --git a/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js new file mode 100644 index 000000000..e96b6d1d4 --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/ctors/typedarray-arg/undefined-newtarget-throws.js @@ -0,0 +1,27 @@ +// Copyright (C) 2016 the V8 project authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-typedarray-typedarray +description: > + Throws a TypeError if NewTarget is undefined. +info: | + 22.2.4.3 TypedArray ( typedArray ) + + This description applies only if the TypedArray function is called with at + least one argument and the Type of the first argument is Object and that + object has a [[TypedArrayName]] internal slot. + + ... + 2. If NewTarget is undefined, throw a TypeError exception. + ... +includes: [testTypedArray.js] +features: [TypedArray] +---*/ + +testWithTypedArrayConstructors(function(TA, makeCtorArg) { + var typedArray = new TA(makeCtorArg(4)); + + assert.throws(TypeError, function() { + TA(typedArray); + }); +}); diff --git a/tests/test262/vendor/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js new file mode 100644 index 000000000..898cf1abc --- /dev/null +++ b/tests/test262/vendor/test/built-ins/TypedArrayConstructors/internals/DefineOwnProperty/key-is-numericindex-desc-not-configurable-throws.js @@ -0,0 +1,39 @@ +// Copyright (C) 2021 Alexey Shvayka. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-integer-indexed-exotic-objects-defineownproperty-p-desc +description: > + Throws TypeError for valid index & non-configurable descriptor. +info: | + [[DefineOwnProperty]] ( P, Desc ) + + [...] + 3. If Type(P) is String, then + a. Let numericIndex be ! CanonicalNumericIndexString(P). + b. If numericIndex is not undefined, then + [...] + ii. If Desc has a [[Configurable]] field and if Desc.[[Configurable]] is false, return false. +includes: [testTypedArray.js] +features: [align-detached-buffer-semantics-with-web-reality, TypedArray] +---*/ + +testWithTypedArrayConstructors(function(TA, makeCtorArg) { + var sample = new TA(makeCtorArg([0])); + + assert.throws(TypeError, function() { + Object.defineProperty(sample, "0", { + configurable: false, + }); + }, "partial descriptor"); + + assert.throws(TypeError, function() { + Object.defineProperty(sample, "0", { + value: 42, + writable: true, + enumerable: true, + configurable: false, + }); + }, "complete descriptor"); + + assert.sameValue(sample[0], 0, "side effect check"); +}, null, ["passthrough"]); From 46b00f69c97629ba4df012f30583a3d5262e1b66 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 14:30:29 -0500 Subject: [PATCH 155/409] Give glibc's per-thread heaps huge pages Blocks above 512 bytes come from glibc, which serves each worker thread from a 64 MiB heap of its own and makes it accessible with mprotect a page-rounded step at a time. Every 2 MiB range of a heap is first touched while part of it is still inaccessible, so the heaps never get transparent huge pages: on a 16-checker type check of VS Code they hold about 0.9 GB on small pages, close to half of the process's page faults. Once a worker thread has allocated 16 MiB of large blocks, the heap of each large block it gets is made accessible whole and advised MADV_HUGEPAGE, once per heap. glibc's calloc still clears only what lies below its own high-water mark, so no page is touched early. glibc's mmap and trim thresholds are set to 4 MiB and 8 MiB, so that the largest blocks get mappings of their own and a heap's free top goes back to the system: with huge pages, a heap otherwise keeps resident memory it never writes. The brk heap policy's own thresholds take precedence once it switches on. The policy is off unless THP is in madvise mode and malloc is glibc's, and when the glibc.malloc.hugetlb tunable is set or the system forbids overcommit. A heap is advised only when /proc/self/maps shows anonymous mappings covering exactly its range. Library archives leave their embedder's heaps alone. --- packages/runtime/src/scr_alloc.c | 101 ++++++++++++++++++++++++++ packages/runtime/src/scr_alloc.test.c | 53 ++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 3a36f672e..f3d6c0893 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -567,7 +567,108 @@ static _Atomic(uintptr_t) scr_sa_heap_page; /* the page size, read with the mode * These loads run only on blocks too large for the small allocator, next to * a glibc malloc call: plain loads on x86-64, LDAR (or LDAPR) on arm64. */ +#if defined(SCR_WORKERS) && !defined(SCR_LIB) +/* glibc's per-thread heaps (worker executables; library archives run on + * their embedder's threads and leave its heaps alone). glibc serves a thread's + * blocks from a heap of its own: a 64 MiB mapping, aligned to its size, + * that it makes accessible with mprotect as it grows, a page-rounded step + * at a time, so every 2 MiB range is first touched while part of it is + * still inaccessible and gets small pages. Once a thread has allocated + * SCR_SA_THREAD_HUGE_AFTER bytes of large blocks (threads with few keep + * small pages), the heap of each large block it gets is made accessible + * whole and advised huge pages, once per heap; glibc's calloc still clears + * only what lies below its own high-water mark, so this touches nothing. + * glibc's mmap and trim thresholds are fixed at SCR_SA_THREAD_MMAP_THRESHOLD + * and SCR_SA_THREAD_TRIM_THRESHOLD: lower than the maxima glibc's adaptive + * thresholds reach, so that blocks of 4 MiB and more get mappings of their + * own and a heap's free top goes back once it passes 8 MiB, while huge + * pages keep resident memory that a heap would otherwise leave untouched. + * The brk heap policy below sets its own (higher) values when the main heap + * grows past SCR_SA_HEAP_HUGE_AFTER, and those take precedence. The + * policy is off unless THP is in madvise mode, when the glibc.malloc.hugetlb + * tunable is set (glibc then advises its heaps itself, or sizes them by + * hugetlbfs pages), when the system forbids overcommit (making a heap + * accessible would commit all of it), and when malloc is not glibc's (a + * sanitizer or a preloaded allocator: blocks have no glibc chunk header). + * A heap is only advised when /proc/self/maps shows the anonymous mappings + * of exactly its range. */ +#define SCR_SA_GLIBC_HEAP ((uintptr_t)64 << 20) /* glibc's heap size and alignment */ +#define SCR_SA_THREAD_HUGE_AFTER ((size_t)16 << 20) +#define SCR_SA_THREAD_MMAP_THRESHOLD (4 << 20) +#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 */ +static _Atomic(int) scr_sa_thread_heaps; /* 0 undecided, 1 advised, 2 off */ +extern void *__libc_malloc(size_t); + +/* Whether [heap, heap + SCR_SA_GLIBC_HEAP) is covered exactly by anonymous + * mappings, as a glibc heap is (its accessible part and the rest). */ +static bool scr_sa_glibc_heap_mapped(uintptr_t heap) { + FILE *f = fopen("/proc/self/maps", "r"); + if (!f) return false; + char line[512]; + uintptr_t next = heap, end = heap + SCR_SA_GLIBC_HEAP; + while (next < end && fgets(line, sizeof line, f)) { + unsigned long lo, hi, inode; + char path[2] = {0}; + if (sscanf(line, "%lx-%lx %*s %*s %*s %lu %1s", &lo, &hi, &inode, path) < 3) continue; + if (hi <= next) continue; + if (lo != next || inode != 0 || path[0]) break; + next = hi; + } + fclose(f); + return next == end; +} + +static void scr_sa_thread_heap_note(void *p, size_t n) { + if (scr_sa_thread_large_bytes < SCR_SA_THREAD_HUGE_AFTER) { + scr_sa_thread_large_bytes += n; + return; + } + int mode = atomic_load_explicit(&scr_sa_thread_heaps, memory_order_relaxed); + if (mode == 2) return; + if (mode == 0) { + int undecided = 0; + const char *tunables = getenv("GLIBC_TUNABLES"); + FILE *f = fopen("/proc/sys/vm/overcommit_memory", "r"); + int overcommit = f ? fgetc(f) : '0'; + if (f) fclose(f); + /* Loaded through volatile: the compiler takes two declared functions + * to have different addresses. */ + void *(*volatile used)(size_t) = malloc, *(*volatile own)(size_t) = __libc_malloc; + bool glibc_malloc = used == own; + mode = glibc_malloc && scr_sa_thp_mode() == 'm' && !(tunables && strstr(tunables, "glibc.malloc.hugetlb")) && + overcommit != '2' + ? 1 + : 2; + if (atomic_compare_exchange_strong(&scr_sa_thread_heaps, &undecided, mode) && mode == 1 && + atomic_load_explicit(&scr_sa_heap_state, memory_order_relaxed) != 1) { + (void)mallopt(M_MMAP_THRESHOLD, SCR_SA_THREAD_MMAP_THRESHOLD); + (void)mallopt(M_TRIM_THRESHOLD, SCR_SA_THREAD_TRIM_THRESHOLD); + /* The brk heap policy marks itself on before it sets its values: if it + * did so meanwhile, its values are set again. */ + if (atomic_load(&scr_sa_heap_state) == 1) { + (void)mallopt(M_MMAP_THRESHOLD, SCR_SA_HEAP_MMAP_THRESHOLD); + (void)mallopt(M_TRIM_THRESHOLD, SCR_SA_HEAP_TRIM_THRESHOLD); + } + } + mode = atomic_load_explicit(&scr_sa_thread_heaps, memory_order_relaxed); + if (mode != 1) return; + } + /* glibc's chunk header: NON_MAIN_ARENA (4) set, IS_MMAPPED (2) clear. */ + if ((((const size_t *)p)[-1] & 6) != 4) return; + 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_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); +} +#endif + static void scr_sa_large_note(void *p, size_t n) { +#if defined(SCR_WORKERS) && !defined(SCR_LIB) + if (p) scr_sa_thread_heap_note(p, n); +#endif int state = atomic_load_explicit(&scr_sa_heap_state, memory_order_relaxed); if (state == 2 || !p) return; uintptr_t top = (uintptr_t)sbrk(0); diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index e20c0733b..6ccb1495b 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -343,6 +343,56 @@ static void *reclaim_arena(void *arg) { return NULL; } +#if defined(__linux__) && defined(__GLIBC__) +/* A thread whose large blocks pass 16 MiB gets them from a glibc heap of + * its own, which is then advised huge pages where THP is in madvise mode. */ +enum { HEAP_BLOCKS = 320, HEAP_BLOCK = 64 << 10 }; +static void *heap_blocks[HEAP_BLOCKS]; +static char heap_flags[256]; +static void *thread_heap_allocate(void *arg) { + (void)arg; + for (unsigned i = 0; i < HEAP_BLOCKS; i++) { + heap_blocks[i] = scr_mem_alloc(HEAP_BLOCK); + assert(heap_blocks[i] && !owned(heap_blocks[i])); + ((unsigned char *)heap_blocks[i])[0] = (unsigned char)i; + } + vm_flags(heap_blocks[HEAP_BLOCKS - 1], heap_flags, sizeof heap_flags); + for (unsigned i = 0; i < HEAP_BLOCKS; i++) { + assert(((unsigned char *)heap_blocks[i])[0] == (unsigned char)i); + scr_mem_free(heap_blocks[i]); + } + scr_sa_thread_park(); + return NULL; +} + +static void check_thread_heap_huge_pages(void) { + pthread_t t; + assert(pthread_create(&t, NULL, thread_heap_allocate, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + char mode[96] = {0}; + FILE *thp = fopen("/sys/kernel/mm/transparent_hugepage/enabled", "r"); + if (thp) { + size_t n = fread(mode, 1, sizeof mode - 1, thp); + mode[n] = '\0'; + fclose(thp); + } + FILE *f = fopen("/proc/sys/vm/overcommit_memory", "r"); + int overcommit = f ? fgetc(f) : '0'; + if (f) fclose(f); + /* ThreadSanitizer replaces malloc, which turns the policy off. */ + bool glibc_malloc = true; +#if defined(__SANITIZE_THREAD__) + glibc_malloc = false; +#elif defined(__has_feature) +#if __has_feature(thread_sanitizer) + glibc_malloc = false; +#endif +#endif + if (glibc_malloc && strstr(mode, "[madvise]") && !getenv("GLIBC_TUNABLES") && overcommit != '2') + assert(strstr(heap_flags, " hg")); +} +#endif + static void check_threads(void) { void *mine = scr_mem_alloc(48); assert(owned(mine)); @@ -1047,6 +1097,9 @@ int main(void) { setenv("SCR_SA_DISCARD_DELAY_MS", "100", 1); setenv("SCR_SA_DISCARD_DELAY_MAX_MS", "0", 1); check_threads(); +#if defined(__linux__) && defined(__GLIBC__) + check_thread_heap_huge_pages(); +#endif #ifdef __linux__ check_trim(); check_release_frontiers(); From 19d6736b01b65bfaa75f3be92f1bcd49c0acc74d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:04:56 -0500 Subject: [PATCH 156/409] Look up exports whose names start with "__" by their escaped key TypeScript keys a symbol table entry whose name starts with `__` with an extra `_` (`__x` is stored as `___x`; `symbolName` removes it). Native ESM link checking looked an import up by its own spelling, so a program that imports such an export compiled to Node's startup SyntaxError: // dep.ts export const __x = 1; // main.ts import { __x } from "./dep.ts"; // The requested module './dep.ts' // does not provide an export named '__x' `escapedSymbolName`, the inverse of `symbolName`, gives a name's table key. Link checking, external type lookups, CommonJS export lookups and the `this` check of ES module exports now look names up through it. `Object.keys` over a module namespace skipped every key starting with `__` to drop TypeScript's internal names, so it also dropped these exports; it now skips only internal names and lists exports by their own names. Closes #1582. Co-authored-by: thekevinbrown <1531750+thekevinbrown@users.noreply.github.com> --- .../frontend/lowering/cjs-exports-object.ts | 2 +- .../src/frontend/lowering/lower-calls.ts | 6 ++++-- .../compiler/src/frontend/lowering/lowerer.ts | 4 ++-- packages/compiler/src/frontend/program.ts | 2 +- .../src/frontend/ts7/semantic-model.test.ts | 6 ++++++ .../src/frontend/ts7/semantic-model.ts | 6 ++++++ .../src/frontend/ts7/semantic-types.ts | 2 +- .../corpus/7306-dunder-exports/main.ts.json | 10 ++++++++++ tests/corpus/7306-dunder-exports/dep.ts | 19 ++++++++++++++++++ tests/corpus/7306-dunder-exports/facade.ts | 2 ++ tests/corpus/7306-dunder-exports/main.ts | 20 +++++++++++++++++++ tests/corpus/7306-dunder-exports/star.ts | 4 ++++ tests/corpus/7306-dunder-exports/table.cjs | 5 +++++ .../7306-dunder-exports/main.ts.json | 1 + 14 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json create mode 100644 tests/corpus/7306-dunder-exports/dep.ts create mode 100644 tests/corpus/7306-dunder-exports/facade.ts create mode 100644 tests/corpus/7306-dunder-exports/main.ts create mode 100644 tests/corpus/7306-dunder-exports/star.ts create mode 100644 tests/corpus/7306-dunder-exports/table.cjs create mode 100644 tests/harness/corpus-costs/7306-dunder-exports/main.ts.json diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index ae8cce4ca..5e5f24e32 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -381,7 +381,7 @@ function esModuleExportIgnoresThis( depth: number, ): boolean { const moduleSym = lowerer.checker.getSymbolAtLocation(sf); - const exported = moduleSym?.getExports().get(name as ts.__String); + const exported = moduleSym?.getExports().get(ts.escapedSymbolName(name)); if (exported !== undefined) { const target = exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 31276263d..5366ca9d0 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -8777,11 +8777,13 @@ function lowerObjectStaticCall( const moduleSymbol = source ? lowerer.checker.getSymbolAtLocation(source) : undefined; const names: string[] = []; moduleSymbol?.getExports().forEach((symbol, key) => { + // Internal names (`__export`) start with `__`; an exported name that + // does is keyed with an extra `_` (escapedSymbolName). const name = String(key); - if (name.startsWith("__") || name === "export=") return; + if ((name.startsWith("__") && !name.startsWith("___")) || name === "export=") return; const target = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - if (target.flags & ts.SymbolFlags.Value) names.push(name); + if (target.flags & ts.SymbolFlags.Value) names.push(ts.symbolName(symbol)); }); names.sort(); const loc = locOf(call); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index fff4a043b..7faba254f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -4219,7 +4219,7 @@ export class Lowerer { // exact mappings, while star exports resolve to the mapped declaration // owner through externalTypeSpecifiersByFile. const moduleSymbol = this.checker.getSymbolAtLocation(sf); - const exported = moduleSymbol?.getExports().get(exportName as ts.__String); + const exported = moduleSymbol?.getExports().get(ts.escapedSymbolName(exportName)); return this.externalTypeSpecifierOfSymbol(exported, seenSymbols, seenExports); } @@ -4362,7 +4362,7 @@ export class Lowerer { cjsModuleExportSymbol(sf: ts.SourceFile, name: string): ts.Symbol | undefined { const moduleSym = this.checker.getSymbolAtLocation(sf); const exports = moduleSym?.getExports(); - const direct = exports?.get(name as ts.__String); + const direct = exports?.get(ts.escapedSymbolName(name)); if (direct) return direct; // tsgo represents `module.exports = { ... }` as one `export=` symbol // and does not duplicate the object literal's properties into the diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 6d8ffe4c0..e92251554 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -6437,7 +6437,7 @@ function analyzeEsmNamedImportLinks( const nextSeen = new Set(seenModules); nextSeen.add(dep); const module = checker.getSymbolAtLocation(dep); - const exported = module?.getExports().get(name as ts.__String); + const exported = module?.getExports().get(ts.escapedSymbolName(name)); if (exported !== undefined) { let resolved = exported; const seenSymbols = new Set(); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.test.ts b/packages/compiler/src/frontend/ts7/semantic-model.test.ts index 9d0f61a4e..bbcd53d8b 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.test.ts @@ -28,6 +28,7 @@ import { SemanticSnapshot, SemanticType, symbolNameOf, + escapedSymbolName, } from "./semantic-model.js"; import type { __String } from "./ast-types.js"; import { @@ -313,6 +314,11 @@ test("symbol tables keep escaped keys, display names, canonical context and cach const exports = parent.getExports(); expect(symbolNameOf(members.get("___x" as __String))).toBe("__x"); expect(symbolNameOf(exports.get("__x" as __String))).toBe("__x"); + // A name that starts with `__` is keyed with an extra `_`. + expect(symbolNameOf(members.get(escapedSymbolName("__x")))).toBe("__x"); + expect(escapedSymbolName("x")).toBe("x"); + expect(escapedSymbolName("_x")).toBe("_x"); + expect(escapedSymbolName("___y")).toBe("____y"); expect(parent.getMembers()).toBe(members); expect(parent.getExports()).toBe(exports); expect(parent.getParent()).toBe(first.symbol(symbol(2))); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index d3d5fe61a..405f4775b 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -454,6 +454,12 @@ export function symbolName(symbol: SemanticSymbol): string { return name.startsWith("___") ? name.slice(1) : name; } +/** The key of the name `name` in a symbol table (`getExports()`, `getMembers()`): the name, with + * an extra `_` when it starts with `__`. The inverse of `symbolName`. */ +export function escapedSymbolName(name: string): __String { + return (name.startsWith("__") ? `_${name}` : name) as __String; +} + /** `symbolName(symbol)`, or undefined without a symbol. */ export function symbolNameOf(symbol: SemanticSymbol | null | undefined): string | undefined { return symbol === undefined || symbol === null ? undefined : symbolName(symbol); diff --git a/packages/compiler/src/frontend/ts7/semantic-types.ts b/packages/compiler/src/frontend/ts7/semantic-types.ts index ea43e3885..5a597ae4e 100644 --- a/packages/compiler/src/frontend/ts7/semantic-types.ts +++ b/packages/compiler/src/frontend/ts7/semantic-types.ts @@ -30,7 +30,7 @@ export type { SemanticSignature as Signature, SemanticSymbol as Symbol, } from "./semantic-model.js"; -export { symbolName, symbolNameOf } from "./semantic-model.js"; +export { escapedSymbolName, symbolName, symbolNameOf } from "./semantic-model.js"; export type { SemanticIndexInfo as IndexInfo, SemanticTypePredicate as TypePredicate, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json new file mode 100644 index 000000000..fd14c0dfe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7306-dunder-exports/main.ts.json @@ -0,0 +1,10 @@ +{ + "order": [ + "/tests/corpus/7306-dunder-exports/dep.ts", + "/tests/corpus/7306-dunder-exports/star.ts", + "/tests/corpus/7306-dunder-exports/facade.ts", + "/tests/corpus/7306-dunder-exports/table.cjs", + "/tests/corpus/7306-dunder-exports/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7306-dunder-exports/dep.ts b/tests/corpus/7306-dunder-exports/dep.ts new file mode 100644 index 000000000..6d54eb365 --- /dev/null +++ b/tests/corpus/7306-dunder-exports/dep.ts @@ -0,0 +1,19 @@ +export const __x = 1; +export const ___y = "three"; +export let __count = 0; +export function __bump(): number { + __count++; + return __count; +} +export class __Point { + a: number; + b: number; + constructor(a: number, b: number) { + this.a = a; + this.b = b; + } + sum(): number { + return this.a + this.b; + } +} +export const plain = "plain"; diff --git a/tests/corpus/7306-dunder-exports/facade.ts b/tests/corpus/7306-dunder-exports/facade.ts new file mode 100644 index 000000000..89ab6eb32 --- /dev/null +++ b/tests/corpus/7306-dunder-exports/facade.ts @@ -0,0 +1,2 @@ +export { __x, __x as __alias, ___y as renamed } from "./dep.ts"; +export * from "./star.ts"; diff --git a/tests/corpus/7306-dunder-exports/main.ts b/tests/corpus/7306-dunder-exports/main.ts new file mode 100644 index 000000000..415bc4a77 --- /dev/null +++ b/tests/corpus/7306-dunder-exports/main.ts @@ -0,0 +1,20 @@ +// Exports whose names start with "__": named imports of a const, a function, +// a class and a live let, a name with three underscores, aliased re-exports, +// names reached through `export *`, namespace member reads and keys, and +// CommonJS exports read through require. +import { __x, ___y, __count, __bump, __Point, plain } from "./dep.ts"; +import { __alias, renamed, __starred, __twice } from "./facade.ts"; +import * as facade from "./facade.ts"; +import * as depNs from "./dep.ts"; +import { createRequire } from "node:module"; + +console.log(__x, ___y, plain); +console.log(__count, __bump(), __bump(), __count); +console.log(new __Point(2, 3).sum()); +console.log(__alias, renamed, __starred, __twice(21)); +console.log(facade.__x, facade.__starred, facade.__twice(2)); +console.log(Object.keys(depNs).join(",")); + +const require = createRequire(import.meta.url); +const table = require("./table.cjs"); +console.log(table.__a, table.__hello("dunder"), table.b); diff --git a/tests/corpus/7306-dunder-exports/star.ts b/tests/corpus/7306-dunder-exports/star.ts new file mode 100644 index 000000000..6ca21d73d --- /dev/null +++ b/tests/corpus/7306-dunder-exports/star.ts @@ -0,0 +1,4 @@ +export const __starred = "from export *"; +export function __twice(n: number): number { + return n * 2; +} diff --git a/tests/corpus/7306-dunder-exports/table.cjs b/tests/corpus/7306-dunder-exports/table.cjs new file mode 100644 index 000000000..e775925ff --- /dev/null +++ b/tests/corpus/7306-dunder-exports/table.cjs @@ -0,0 +1,5 @@ +exports.__a = 10; +exports.__hello = function (name) { + return "hello " + name; +}; +exports.b = 20; diff --git a/tests/harness/corpus-costs/7306-dunder-exports/main.ts.json b/tests/harness/corpus-costs/7306-dunder-exports/main.ts.json new file mode 100644 index 000000000..0477ef4d5 --- /dev/null +++ b/tests/harness/corpus-costs/7306-dunder-exports/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 3600 } From e75174aeea3d01e31ce4d8f2b3f2f2adeba679bd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:13:03 -0500 Subject: [PATCH 157/409] Compile string-key destructuring of any values and cast-guarded Buffer aliases Co-authored-by: patlux <4481570+patlux@users.noreply.github.com> Co-authored-by: jlucaso1 <55464917+jlucaso1@users.noreply.github.com> --- .../lowering/expressions/identifiers.ts | 11 ++++ .../frontend/lowering/expressions/typeof.ts | 6 ++ .../frontend/lowering/expressions/updates.ts | 4 ++ .../src/frontend/lowering/lower-exprs.ts | 4 ++ .../src/frontend/lowering/lower-stmts.ts | 9 ++- .../compiler/src/frontend/lowering/lowerer.ts | 4 ++ .../src/frontend/lowering/surfaces.ts | 32 ++++++++++ .../7309-globalthis-buffer-aliases.ts.json | 6 ++ .../7310-any-destructure-string-keys.ts.json | 6 ++ .../any-destructure-unspelled-keys.ts.json | 6 ++ .../globalthis-buffer-alias-values.ts.json | 6 ++ .../corpus/7309-globalthis-buffer-aliases.ts | 31 ++++++++++ .../7310-any-destructure-string-keys.ts | 60 +++++++++++++++++++ .../any-destructure-unspelled-keys.ts | 12 ++++ .../globalthis-buffer-alias-values.ts | 12 ++++ .../any-destructure-unspelled-keys.ts.txt | 27 +++++++++ .../globalthis-buffer-alias-values.ts.txt | 17 ++++++ .../7309-globalthis-buffer-aliases.ts.json | 1 + .../7310-any-destructure-string-keys.ts.json | 1 + 19 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json create mode 100644 tests/corpus/7309-globalthis-buffer-aliases.ts create mode 100644 tests/corpus/7310-any-destructure-string-keys.ts create mode 100644 tests/diagnostics/any-destructure-unspelled-keys.ts create mode 100644 tests/diagnostics/globalthis-buffer-alias-values.ts create mode 100644 tests/harness/__snapshots__/any-destructure-unspelled-keys.ts.txt create mode 100644 tests/harness/__snapshots__/globalthis-buffer-alias-values.ts.txt create mode 100644 tests/harness/corpus-costs/7309-globalthis-buffer-aliases.ts.json create mode 100644 tests/harness/corpus-costs/7310-any-destructure-string-keys.ts.json diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index ef0d8a2d1..b1116647a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -744,6 +744,17 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; const nativeAlias = stdlibGlobalNameOf(lowerer, expr); if (nativeAlias === "Buffer" && expr.text !== "Buffer") { + // An alias through a cast of globalThis carries the cast's static type, + // which the identity token below does not have. + const aliasSymbol = identifierReadSymbol(lowerer, expr); + if (aliasSymbol && lowerer.castGlobalAliases.has(aliasSymbol)) { + lowerer.unsupported( + "SC1090", + expr, + "the Buffer constructor read through a cast of globalThis as a value", + "test it in a condition, read its typeof, or call its members", + ); + } return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc }; } if (lowerer.isStdlibGlobal(expr, "fetch")) { diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index cc3ac06fb..fbbc068b9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -88,6 +88,12 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc if (builtinClassRefOf(lowerer, expr.expression) !== null) { return { kind: "strLit", value: "function", type: STRING, loc }; } + // The Buffer constructor and its aliases (`const B = globalThis.Buffer`, + // `(globalThis as { Buffer?: B }).Buffer`): a value read of an alias is an + // identity token, so its typeof is decided here. + if (stdlibGlobalNameOf(lowerer, expr.expression) === "Buffer") { + return { kind: "strLit", value: "function", type: STRING, loc }; + } if (ts.isPropertyAccessExpression(expr.expression)) { const member = expr.expression; const prototype = member.expression; diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 04ca23a96..7cc77a2db 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -191,6 +191,10 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio operatorUnsupported(lowerer, expr, "+", [raw.type]); } case ts.SyntaxKind.ExclamationToken: { + // `!runtimeBuffer` over an alias of the Buffer global: Node always + // defines Buffer (the condition twin is lowerCondition's). + if (lowerer.isStdlibGlobal(expr.operand, "Buffer")) + return { kind: "boolLit", value: false, type: BOOL, loc }; // `!x` is ToBoolean-then-negate: f64/string operands go through toBool. const operand = lowerer.ensureBool( deferredSlotOr( diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 2bde5f2aa..c1c5ef8c8 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1087,6 +1087,10 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr { lowerer.isStdlibGlobal(e.expression, "Object") ) return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) }; + // `if (runtimeBuffer)` over an alias of the Buffer global (a + // capability probe): Node always defines Buffer. + if (lowerer.isStdlibGlobal(e, "Buffer")) + return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) }; if (ts.isPropertyAccessExpression(e)) { const presence = lowerPromiseThenPresence(lowerer, e, { kind: "boolLit", diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e66d0e855..e11f79c25 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -8749,13 +8749,20 @@ function destrSpellingOf(rhs: ts.Expression): string | null { return base === null ? null : `${base}.${e.name.text}`; } // `table[key]` and `list[0]`; V8 prints a numeric key in its canonical - // number form (`o[1.50]` reads "o[1.5]"). + // number form (`o[1.50]` reads "o[1.5]"), and a string key as a property + // name with its text decoded (`o["a.b"]` and `o[\`a.b\`]` read "o.a.b"). + // The message is a C string, so a key holding NUL keeps the refusal. if (ts.isElementAccessExpression(e) && !e.questionDotToken) { const base = destrSpellingOf(e.expression); const key = e.argumentExpression; if (base === null) return null; if (ts.isIdentifier(key)) return `${base}[${key.text}]`; if (ts.isNumericLiteral(key)) return `${base}[${String(Number(key.text))}]`; + if ( + (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) && + !key.text.includes("\0") + ) + return `${base}.${key.text}`; return null; } if (ts.isCallExpression(e) && !e.questionDotToken) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index fff4a043b..441b313f0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2937,6 +2937,10 @@ export class Lowerer { * checks resolve through this map (stdlibGlobalNameOf), declarations * emit nothing. */ readonly stdlibGlobalAliases = new Map(); + /** The stdlibGlobalAliases bound through a cast of the global object + * (`(globalThis as { Buffer?: B }).Buffer`). Their static type is the + * cast's, so their value reads are refused (identifiers.ts). */ + readonly castGlobalAliases = new Set(); /** CJS export-table ACCESSORS (`module.exports = { get path() {...} }`), * lifted lazily as module-level functions and interned per accessor * declaration: member reads call the getter (lower-exprs). */ diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 44564f2f1..ed615b276 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -6,6 +6,20 @@ function unparenthesized(e: ts.Expression): ts.Expression { while (ts.isParenthesizedExpression(e)) e = e.expression; return e; } + +/** `e` without parentheses and type-only wrappers (as, satisfies, type + * assertions, non-null assertions), which change no runtime value. */ +function withoutTypeCasts(e: ts.Expression): ts.Expression { + while ( + ts.isParenthesizedExpression(e) || + ts.isAsExpression(e) || + ts.isSatisfiesExpression(e) || + ts.isTypeAssertion(e) || + ts.isNonNullExpression(e) + ) + e = e.expression; + return e; +} import { UNSUPPORTED } from "../../diagnostics/diagnostic.js"; import { BOOL, @@ -3440,6 +3454,17 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved); } if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { + // A cast of the global object (`(globalThis as { Buffer?: B }).Buffer`, + // `(globalThis as any).Buffer`) changes only its static type: the + // member is the global object's own. + const uncast = withoutTypeCasts(expr.expression); + if (uncast !== unparenthesized(expr.expression)) { + if (!ts.isIdentifier(uncast) || stdlibGlobalNameOf(lowerer, uncast) !== "globalThis") + return null; + const member = lowerer.checker.getPropertyOfType(lowerer.typeOf(uncast), expr.name.text); + if (!member || !lowerer.isStdlibSymbol(member)) return null; + return ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member); + } if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null; // Through a detection chain the checker types the receiver as the // union of every arm; the selected arm (the global object) owns the @@ -3572,6 +3597,13 @@ export function stdlibGlobalAliasDecl( const symbol = lowerer.checker.getSymbolAtLocation(nameNode); if (!symbol) return false; lowerer.stdlibGlobalAliases.set(symbol, name); + const access = init !== undefined ? unparenthesized(init) : undefined; + if ( + access !== undefined && + ts.isPropertyAccessExpression(access) && + withoutTypeCasts(access.expression) !== unparenthesized(access.expression) + ) + lowerer.castGlobalAliases.add(symbol); return true; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json new file mode 100644 index 000000000..94d458e11 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7309-globalthis-buffer-aliases.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7309-globalthis-buffer-aliases.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json new file mode 100644 index 000000000..45a9f62ff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7310-any-destructure-string-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7310-any-destructure-string-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json new file mode 100644 index 000000000..f38d71f47 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/any-destructure-unspelled-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/any-destructure-unspelled-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json new file mode 100644 index 000000000..4f6fe0c8c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/globalthis-buffer-alias-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/globalthis-buffer-alias-values.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7309-globalthis-buffer-aliases.ts b/tests/corpus/7309-globalthis-buffer-aliases.ts new file mode 100644 index 000000000..39d896463 --- /dev/null +++ b/tests/corpus/7309-globalthis-buffer-aliases.ts @@ -0,0 +1,31 @@ +// Aliases of the global Buffer constructor read through a cast of +// globalThis, the capability-probe spelling of code that also runs where +// Buffer is absent: typed and `as any` casts, `if`, ternary, typeof and `!` +// guards, member calls through the alias, and typeof of the alias. +type RuntimeBuffer = { + byteLength(value: string, encoding: "utf8"): number; + from(value: string, encoding: "utf8"): Uint8Array; + isBuffer(value: unknown): boolean; +}; + +const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer; +const anyBuffer = (globalThis as any).Buffer; +const text = "héllo wörld"; + +if (runtimeBuffer) { + console.log("typed", runtimeBuffer.byteLength(text, "utf8")); +} else { + console.log("typed: no Buffer"); +} +if (typeof anyBuffer === "function") { + console.log("any", anyBuffer.byteLength(text, "utf8")); +} +const bytes = runtimeBuffer ? runtimeBuffer.from(text, "utf8") : undefined; +console.log("from", bytes ? bytes.length : -1); +console.log("isBuffer", runtimeBuffer ? runtimeBuffer.isBuffer(bytes) : false); +console.log("typeof", typeof runtimeBuffer, typeof anyBuffer, !runtimeBuffer); + +function utf8Length(value: string): number { + return runtimeBuffer ? runtimeBuffer.byteLength(value, "utf8") : value.length; +} +console.log("function", utf8Length("✓✓"), utf8Length("abc")); diff --git a/tests/corpus/7310-any-destructure-string-keys.ts b/tests/corpus/7310-any-destructure-string-keys.ts new file mode 100644 index 000000000..4ab24ec77 --- /dev/null +++ b/tests/corpus/7310-any-destructure-string-keys.ts @@ -0,0 +1,60 @@ +// Destructuring an `any` source read through a string-literal key +// (`const { value } = events["a.b"]`): reads, renames, defaults, rest, a +// nested pattern, the assignment form, a template-literal key, chained +// string keys, and V8's TypeError for undefined and null sources, whose +// message spells the key as a property name with its text decoded. +const events: any = { + "a.b": { value: 1, label: "first", extra: true, nested: { deep: "d" } }, + "x-y": { value: 2 }, + outer: { "in.ner": { value: 3 } }, + "nul": null, +}; + +const { value } = events["a.b"]; +const { value: renamed, missing = "fallback", ...rest } = events["a.b"]; +const { + nested: { deep }, +} = events["a.b"]; +console.log(value, renamed, missing, JSON.stringify(rest), deep); + +let assigned: unknown; +({ value: assigned } = events[`x-y`]); +const { value: chained } = events["outer"]["in.ner"]; +console.log(assigned, chained); + +function attempt(label: string, f: () => unknown): void { + try { + console.log(label, f()); + } catch (e) { + console.log(label, e instanceof TypeError, (e as Error).message); + } +} +attempt("missing", () => { + const { value } = events["no.such"]; + return value; +}); +attempt("null", () => { + const { value } = events["nul"]; + return value; +}); +attempt("quotes", () => { + const { value } = events["it's \"quoted\""]; + return value; +}); +attempt("escapes", () => { + const { value } = events["tab\there\nnext"]; + return value; +}); +attempt("accents", () => { + const { value } = events["été ✓"]; + return value; +}); +attempt("template", () => { + const { value } = events[`plain`]; + return value; +}); +attempt("assignment", () => { + let target: unknown; + ({ value: target } = events["gone"]); + return target; +}); diff --git a/tests/diagnostics/any-destructure-unspelled-keys.ts b/tests/diagnostics/any-destructure-unspelled-keys.ts new file mode 100644 index 000000000..6680de854 --- /dev/null +++ b/tests/diagnostics/any-destructure-unspelled-keys.ts @@ -0,0 +1,12 @@ +// Destructuring an `any` source keeps SC1031 where the source has no +// compiled spelling for V8's TypeError: a key computed by an expression, an +// optional element read, a template key with a substitution, and a string key +// holding NUL (the message is a C string). String-literal keys compile +// (tests/corpus/7310-any-destructure-string-keys.ts). +const events: any = { ab: { value: 1 } }; +const k = "a"; +const { value: computed } = events["a" + "b"]; +const { value: optional } = events?.["ab"]; +const { value: template } = events[`${k}b`]; +const { value: nul } = events["a\0b"]; +console.log(computed, optional, template, nul); diff --git a/tests/diagnostics/globalthis-buffer-alias-values.ts b/tests/diagnostics/globalthis-buffer-alias-values.ts new file mode 100644 index 000000000..97b49f920 --- /dev/null +++ b/tests/diagnostics/globalthis-buffer-alias-values.ts @@ -0,0 +1,12 @@ +// An alias of the Buffer global read through a cast of globalThis compiles +// in conditions, typeof and member calls +// (tests/corpus/7309-globalthis-buffer-aliases.ts). Its other value uses +// keep a named SC1090 refusal: the alias has the cast's static type. +type RuntimeBuffer = { from(value: string, encoding: "utf8"): Uint8Array }; +const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer; +function keep(value: unknown): unknown { + return value; +} +const bytes = runtimeBuffer && runtimeBuffer.from("abc", "utf8"); +keep(runtimeBuffer); +keep(bytes); diff --git a/tests/harness/__snapshots__/any-destructure-unspelled-keys.ts.txt b/tests/harness/__snapshots__/any-destructure-unspelled-keys.ts.txt new file mode 100644 index 000000000..872b717cf --- /dev/null +++ b/tests/harness/__snapshots__/any-destructure-unspelled-keys.ts.txt @@ -0,0 +1,27 @@ +any-destructure-unspelled-keys.ts:8:29 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 7 | const k = "a"; + 8 | const { value: computed } = events["a" + "b"]; + | ^~~~~~~~~~~~~~~~~ + 9 | const { value: optional } = events?.["ab"]; + +any-destructure-unspelled-keys.ts:9:29 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 8 | const { value: computed } = events["a" + "b"]; + 9 | const { value: optional } = events?.["ab"]; + | ^~~~~~~~~~~~~~ + 10 | const { value: template } = events[`${k}b`]; + +any-destructure-unspelled-keys.ts:10:29 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 9 | const { value: optional } = events?.["ab"]; + 10 | const { value: template } = events[`${k}b`]; + | ^~~~~~~~~~~~~~~ + 11 | const { value: nul } = events["a\0b"]; + +any-destructure-unspelled-keys.ts:11:24 - error SC1031: destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call) is not supported yet + + 10 | const { value: template } = events[`${k}b`]; + 11 | const { value: nul } = events["a\0b"]; + | ^~~~~~~~~~~~~~ + 12 | console.log(computed, optional, template, nul); \ No newline at end of file diff --git a/tests/harness/__snapshots__/globalthis-buffer-alias-values.ts.txt b/tests/harness/__snapshots__/globalthis-buffer-alias-values.ts.txt new file mode 100644 index 000000000..f55423db9 --- /dev/null +++ b/tests/harness/__snapshots__/globalthis-buffer-alias-values.ts.txt @@ -0,0 +1,17 @@ +globalthis-buffer-alias-values.ts:10:15 - error SC1090: the Buffer constructor read through a cast of globalThis as a value is not supported yet + + 9 | } + 10 | const bytes = runtimeBuffer && runtimeBuffer.from("abc", "utf8"); + | ^~~~~~~~~~~~~ + 11 | keep(runtimeBuffer); + + hint: test it in a condition, read its typeof, or call its members + +globalthis-buffer-alias-values.ts:11:6 - error SC1090: the Buffer constructor read through a cast of globalThis as a value is not supported yet + + 10 | const bytes = runtimeBuffer && runtimeBuffer.from("abc", "utf8"); + 11 | keep(runtimeBuffer); + | ^~~~~~~~~~~~~ + 12 | keep(bytes); + + hint: test it in a condition, read its typeof, or call its members \ No newline at end of file diff --git a/tests/harness/corpus-costs/7309-globalthis-buffer-aliases.ts.json b/tests/harness/corpus-costs/7309-globalthis-buffer-aliases.ts.json new file mode 100644 index 000000000..f1c2cc30b --- /dev/null +++ b/tests/harness/corpus-costs/7309-globalthis-buffer-aliases.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 3400 } diff --git a/tests/harness/corpus-costs/7310-any-destructure-string-keys.ts.json b/tests/harness/corpus-costs/7310-any-destructure-string-keys.ts.json new file mode 100644 index 000000000..df39abc08 --- /dev/null +++ b/tests/harness/corpus-costs/7310-any-destructure-string-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 1500, "san": 3900 } From 97b64fae1cc66582b2e0ffe13008d817acfc4966 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:13:36 -0500 Subject: [PATCH 158/409] Turn the thread-heap huge-page policy off under memory tagging --- packages/runtime/src/scr_alloc.c | 26 +++++++++++++++++++++++--- packages/runtime/src/scr_alloc.test.c | 6 +++++- packages/runtime/src/scr_alloc.test.ts | 20 ++++++++++++++------ 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index f3d6c0893..a27df378a 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -568,6 +568,9 @@ static _Atomic(uintptr_t) scr_sa_heap_page; /* the page size, read with the mode * a glibc malloc call: plain loads on x86-64, LDAR (or LDAPR) on arm64. */ #if defined(SCR_WORKERS) && !defined(SCR_LIB) +#if defined(__aarch64__) +#include +#endif /* glibc's per-thread heaps (worker executables; library archives run on * their embedder's threads and leave its heaps alone). glibc serves a thread's * blocks from a heap of its own: a 64 MiB mapping, aligned to its size, @@ -588,8 +591,11 @@ static _Atomic(uintptr_t) scr_sa_heap_page; /* the page size, read with the mode * policy is off unless THP is in madvise mode, when the glibc.malloc.hugetlb * tunable is set (glibc then advises its heaps itself, or sizes them by * hugetlbfs pages), when the system forbids overcommit (making a heap - * accessible would commit all of it), and when malloc is not glibc's (a - * sanitizer or a preloaded allocator: blocks have no glibc chunk header). + * accessible would commit all of it), when malloc is not glibc's (a + * sanitizer or a preloaded allocator: blocks have no glibc chunk header), + * and when memory tagging is on (arm64 MTE, the glibc.mem.tagging tunable): + * a chunk header then carries another tag than the block's pointer, and + * reading it through that pointer faults. * A heap is only advised when /proc/self/maps shows the anonymous mappings * of exactly its range. */ #define SCR_SA_GLIBC_HEAP ((uintptr_t)64 << 20) /* glibc's heap size and alignment */ @@ -601,6 +607,20 @@ static _Thread_local uintptr_t scr_sa_thread_heap; /* the heap this thread advis static _Atomic(int) scr_sa_thread_heaps; /* 0 undecided, 1 advised, 2 off */ extern void *__libc_malloc(size_t); +/* Whether glibc tags heap memory: the glibc.mem.tagging tunable is set to a + * nonzero value, or (arm64) the kernel checks tags for this process. */ +static bool scr_sa_mem_tagging(const char *tunables) { + const char *t = tunables ? strstr(tunables, "glibc.mem.tagging=") : NULL; + if (t && t[sizeof "glibc.mem.tagging=" - 1] != '0') return true; +#if defined(__aarch64__) + /* PR_GET_TAGGED_ADDR_CTRL (56); PR_MTE_TCF_MASK is bits 1-2. A kernel + * without the call returns -1: no tag checks. */ + long ctrl = prctl(56, 0, 0, 0, 0); + if (ctrl > 0 && (ctrl & (3L << 1))) return true; +#endif + return false; +} + /* Whether [heap, heap + SCR_SA_GLIBC_HEAP) is covered exactly by anonymous * mappings, as a glibc heap is (its accessible part and the rest). */ static bool scr_sa_glibc_heap_mapped(uintptr_t heap) { @@ -638,7 +658,7 @@ static void scr_sa_thread_heap_note(void *p, size_t n) { void *(*volatile used)(size_t) = malloc, *(*volatile own)(size_t) = __libc_malloc; bool glibc_malloc = used == own; mode = glibc_malloc && scr_sa_thp_mode() == 'm' && !(tunables && strstr(tunables, "glibc.malloc.hugetlb")) && - overcommit != '2' + overcommit != '2' && !scr_sa_mem_tagging(tunables) ? 1 : 2; if (atomic_compare_exchange_strong(&scr_sa_thread_heaps, &undecided, mode) && mode == 1 && diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index 6ccb1495b..dfdd9bf5c 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -388,7 +388,11 @@ static void check_thread_heap_huge_pages(void) { glibc_malloc = false; #endif #endif - if (glibc_malloc && strstr(mode, "[madvise]") && !getenv("GLIBC_TUNABLES") && overcommit != '2') + /* With glibc's memory tagging tunable set the heap is left alone: under + * arm64 MTE, reading its chunk headers through block pointers faults. */ + const char *tunables = getenv("GLIBC_TUNABLES"); + if (tunables && strstr(tunables, "glibc.mem.tagging=3")) assert(!strstr(heap_flags, " hg")); + else if (glibc_malloc && strstr(mode, "[madvise]") && !tunables && overcommit != '2') assert(strstr(heap_flags, " hg")); } #endif diff --git a/packages/runtime/src/scr_alloc.test.ts b/packages/runtime/src/scr_alloc.test.ts index 187ffce51..0ceda33cd 100644 --- a/packages/runtime/src/scr_alloc.test.ts +++ b/packages/runtime/src/scr_alloc.test.ts @@ -11,13 +11,20 @@ const exec = promisify(execFile); // UBSan-only build is the one that exercises it; the ASan build pins the // system-allocator bypass the sanitizer lane relies on. // Worker executables (-DSCR_WORKERS) give each script thread its own arena; -// the ThreadSanitizer build checks the cross-thread handoff for races. +// the ThreadSanitizer build checks the cross-thread handoff for races. The +// last build runs with glibc's memory tagging tunable set, which turns the +// thread-heap huge-page policy off. test.each([ - ["size-class", ["-fsanitize=undefined"]], - ["system", ["-fsanitize=address,undefined"]], - ["size-class workers", ["-fsanitize=undefined", "-DSCR_WORKERS", "-pthread"]], - ["size-class workers", ["-fsanitize=thread", "-DSCR_WORKERS", "-pthread"]], -])("small-object allocator contracts hold (%s, %j)", async (mode, sanitize) => { + ["size-class", ["-fsanitize=undefined"], {}], + ["system", ["-fsanitize=address,undefined"], {}], + ["size-class workers", ["-fsanitize=undefined", "-DSCR_WORKERS", "-pthread"], {}], + ["size-class workers", ["-fsanitize=thread", "-DSCR_WORKERS", "-pthread"], {}], + [ + "size-class workers", + ["-fsanitize=undefined", "-DSCR_WORKERS", "-pthread"], + { GLIBC_TUNABLES: "glibc.mem.tagging=3" }, + ], +])("small-object allocator contracts hold (%s, %j, %j)", async (mode, sanitize, extraEnv) => { const scratch = await mkdtemp(join(tmpdir(), "scriptc-alloc-")); try { const binary = join(scratch, "alloc"); @@ -41,6 +48,7 @@ test.each([ ASAN_OPTIONS: "halt_on_error=1", TSAN_OPTIONS: "halt_on_error=1", UBSAN_OPTIONS: "halt_on_error=1", + ...extraEnv, }, }); expect(result.stdout).toBe(`allocator checks passed: ${mode}\n`); From fa80286f9b164b01921457f14983953f8d7f5845 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:21:21 -0500 Subject: [PATCH 159/409] Lower borrowed typed array set calls and keep method calls direct after dictionary stores Co-authored-by: ainslec <201870+ainslec@users.noreply.github.com> Co-authored-by: MathurAditya724 <57684218+MathurAditya724@users.noreply.github.com> --- .../lowering/class-method-mutations.ts | 17 ++- .../src/frontend/lowering/containers/bytes.ts | 128 +++++++++++++++--- .../src/frontend/lowering/lower-calls.ts | 3 +- .../test/class-method-mutations.test.ts | 32 +++++ .../7940-typed-array-prototype-set.mjs.json | 6 + .../7941-typed-array-prototype-set.ts.json | 6 + ...7942-dictionary-store-method-calls.ts.json | 6 + .../corpus/7940-typed-array-prototype-set.mjs | 40 ++++++ .../corpus/7941-typed-array-prototype-set.ts | 21 +++ .../7942-dictionary-store-method-calls.ts | 45 ++++++ 10 files changed, 282 insertions(+), 22 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json create mode 100644 tests/corpus/7940-typed-array-prototype-set.mjs create mode 100644 tests/corpus/7941-typed-array-prototype-set.ts create mode 100644 tests/corpus/7942-dictionary-store-method-calls.ts diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 035c2c2a5..a07a4d3e1 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -125,11 +125,13 @@ export function collectClassMethodMutations( }; // Whether no function value can inhabit `type`. Primitives qualify, and so // do non-callable object types a function is not assignable to: one that - // requires a property functions lack, or a weak (all-optional) type that + // requires a property functions lack, a weak (all-optional) type that // shares no property with functions (TypeScript's weak-type rule rejects - // callable sources too). - const functionFree = (type: ts.Type): boolean => { - if (type.isUnionType()) return ts.constituentTypes(type).every(functionFree); + // callable sources too), or a dictionary with no properties whose index + // signatures hold function-free values (functions have no index + // signature). `seen` stops recursive dictionary types. + const functionFree = (type: ts.Type, seen: Set = new Set()): boolean => { + if (type.isUnionType()) return ts.constituentTypes(type).every((t) => functionFree(t, seen)); if ( (type.flags & (ts.TypeFlags.StringLike | @@ -149,7 +151,12 @@ export function collectClassMethodMutations( ) return false; const properties = lowerer.checker.getPropertiesOfType(type); - if (properties.length === 0) return false; + if (properties.length === 0) { + if (seen.has(type)) return true; + seen.add(type); + const indexes = lowerer.checker.getIndexInfosOfType(type); + return indexes.length > 0 && indexes.every((index) => functionFree(index.type, seen)); + } const required = properties.filter( (property) => (property.flags & ts.SymbolFlags.Optional) === 0, ); diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 995cb177a..36e7ecd22 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1,5 +1,5 @@ import { keyedHelperName } from "../stable-names.js"; -import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js"; +import { boolLit, dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../../ir/build.js"; import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js"; import * as ts from "../../ts7/adapter.js"; import { @@ -903,6 +903,116 @@ function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { return lowerArrayBufferView(lowerer, args, "dv", locOf(expr)); } +/** `receiver.set(src, offset?)` on a lowered typed-array receiver. */ +function bytesSetIntrinsic( + lowerer: Lowerer, + receiver: IrExpr, + argNodes: readonly ts.Expression[], + loc: SrcLoc, +): IrExpr { + const src = lowerer.lowerExpr(argNodes[0]!); + const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn"; + const args = [src.type.kind === "bytes" ? src : lowerer.coerceInto(argNodes[0]!, src, DYN)]; + if (argNodes.length === 2) + args.push( + lowerOptionalArgument(lowerer, argNodes[1]!, F64, { + kind: "numLit", + value: 0, + type: F64, + loc, + }), + ); + return { kind: "bytesIntrinsic", method, receiver, args, type: VOID, loc }; +} + +/** `Uint8Array.prototype.set.call(target, src, offset?)`, through any typed + * array constructor's prototype (they share %TypedArray%.prototype.set). A + * typed-array target lowers like `target.set(src, offset)`. Any other + * target is checked at run time: a typed array calls its `set`, and + * anything else throws Node's TypeError "this is not a typed array.", + * after every argument is evaluated. */ +export function lowerBytesPrototypeSetCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if ( + call.questionDotToken || + access.questionDotToken || + access.name.text !== "call" || + !ts.isPropertyAccessExpression(access.expression) + ) + return null; + const method = access.expression; + const proto = method.expression; + if ( + method.questionDotToken || + method.name.text !== "set" || + !ts.isPropertyAccessExpression(proto) || + proto.questionDotToken || + proto.name.text !== "prototype" || + !ts.isIdentifier(proto.expression) || + !lowerer.isStdlibMember(method) + ) + return null; + const ctor = lowerer.resolveValueSymbol(proto.expression); + if (!ctor || !lowerer.isStdlibSymbol(ctor) || !own(BYTES_CTORS, ts.symbolName(ctor))) return null; + const argNodes = call.arguments.nodes; + if (argNodes.length < 2 || argNodes.length > 3 || argNodes.some(ts.isSpreadElement)) return null; + const loc = locOf(call); + const target = lowerer.lowerExpr(argNodes[0]!); + if (target.type.kind === "bytes") + return bytesSetIntrinsic(lowerer, target, argNodes.slice(1), loc); + const slot = (value: IrExpr): { decl: IrStmt; read: IrExpr } => { + const local = lowerer.declareHiddenLocal("%setArg", DYN); + return { + decl: { kind: "varDecl", localId: local.id, init: value, loc }, + read: varRef(local.id, DYN, loc), + }; + }; + const slots = [ + slot(lowerer.coerceInto(argNodes[0]!, target, DYN)), + ...argNodes.slice(1).map((arg) => slot(lowerer.lowerExprExpecting(arg, DYN))), + ]; + const isTypedArray: IrExpr = { + kind: "libCall", + fn: "util.typeIs", + args: [slots[0]!.read, { kind: "strLit", value: "isTypedArray", type: STRING, loc }], + type: BOOL, + loc, + }; + return { + kind: "seqExpr", + stmts: [ + ...slots.map((s) => s.decl), + { + kind: "if", + cond: { kind: "unary", op: "!", operand: isTypedArray, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: nodeThrowExpr(1, "", "this is not a typed array.", VOID, loc), + loc, + }, + ], + else_: null, + loc, + }, + ], + result: { + kind: "dynInvoke", + recv: slots[0]!.read, + method: "set", + calleeName: ts.getTextOfNode(method), + args: slots.slice(1).map((s) => s.read), + type: DYN, + loc, + }, + type: DYN, + loc, + }; +} + /** Method calls on typed-array/Buffer receivers: slice (copy), subarray * (shared view), toReversed, copyWithin, set(src, * offset?), and the u8-only Buffer surface: toString(enc?) plus the @@ -1106,21 +1216,7 @@ export function lowerBytesMethodCall( lowerer.noLowering(`.set with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const src = lowerer.lowerExpr(call.arguments.nodes[0]!); - const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn"; - const args = [ - src.type.kind === "bytes" ? src : lowerer.coerceInto(call.arguments.nodes[0]!, src, DYN), - ]; - if (nArgs === 2) - args.push( - lowerOptionalArgument(lowerer, call.arguments.nodes[1]!, F64, { - kind: "numLit", - value: 0, - type: F64, - loc, - }), - ); - return { kind: "bytesIntrinsic", method, receiver, args, type: VOID, loc }; + return bytesSetIntrinsic(lowerer, receiver, call.arguments.nodes, loc); } if (name === "toString") { // Buffer's toString(encoding?) — utf8 by default. A 0-arg toString diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 31276263d..c071d5a83 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -119,7 +119,7 @@ import { } from "./containers/indexed-objects.js"; import { lowerTupleReadMethodCall } from "./containers/array-methods.js"; import { tryFuseIndexRead } from "./array-values.js"; -import { lowerBytesStaticCall } from "./containers/bytes.js"; +import { lowerBytesPrototypeSetCall, lowerBytesStaticCall } from "./containers/bytes.js"; import { lowerRegexMethodCall, lowerStringIndexCall, @@ -1579,6 +1579,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.lowerTestMethodCall(expr, expr.expression) ?? lowerer.lowerTimeoutMethodCall(expr, expr.expression) ?? lowerArrayPrototypeBorrowCall(lowerer, expr, expr.expression) ?? + lowerBytesPrototypeSetCall(lowerer, expr, expr.expression) ?? lowerObjectOwnPrototypeCall(lowerer, expr, expr.expression) ?? lowerObjectPrototypeCall(lowerer, expr, expr.expression) ?? lowerStringPrototypeCall(lowerer, expr, expr.expression) ?? diff --git a/packages/compiler/test/class-method-mutations.test.ts b/packages/compiler/test/class-method-mutations.test.ts index b508a3949..4871dda2b 100644 --- a/packages/compiler/test/class-method-mutations.test.ts +++ b/packages/compiler/test/class-method-mutations.test.ts @@ -135,3 +135,35 @@ test("a reflection call with a well-known symbol key keeps checked dispatch", as const callers = await callersOf(`${range}${write}`, "%Range.sym:iterator"); expect(callers.filter((name) => !name.startsWith("%method.value:"))).toEqual([]); }); + +const dictionaries = ` +class Counter { + n = 0; + add(): number { return ++this.n; } +} +interface Settings { groups: Record> } +interface Tree { [key: string]: Tree } +function addGroup(settings: Settings, name: string): void { + settings.groups[name] = {}; +} +function grow(tree: Tree, name: string): void { + tree[name] = {}; +} +const settings: Settings = { groups: {} }; +addGroup(settings, "colours"); +grow({}, "a"); +const counter = new Counter(); +console.log(counter.add(), Object.keys(settings.groups)); +`; + +test("a store into a dictionary of dictionaries leaves method calls direct", async () => { + expect(await callersOf(dictionaries, "%dyn.class.properties")).toEqual([]); +}); + +test("a store into a dictionary whose values can be functions keeps checked dispatch", async () => { + const source = dictionaries.replace( + "Record>", + "Record | (() => number)>", + ); + expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json new file mode 100644 index 000000000..3c59407b3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7940-typed-array-prototype-set.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7940-typed-array-prototype-set.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json new file mode 100644 index 000000000..8fbec6a90 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7941-typed-array-prototype-set.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7941-typed-array-prototype-set.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json new file mode 100644 index 000000000..f04478490 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7942-dictionary-store-method-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7942-dictionary-store-method-calls.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7940-typed-array-prototype-set.mjs b/tests/corpus/7940-typed-array-prototype-set.mjs new file mode 100644 index 000000000..4f1b2301d --- /dev/null +++ b/tests/corpus/7940-typed-array-prototype-set.mjs @@ -0,0 +1,40 @@ +// Uint8Array.prototype.set borrowed through .call on untyped receivers. +let cached = null; +function memory() { + if (cached === null) cached = new Uint8Array(4); + return cached; +} +function copy(target, source, offset) { + return Uint8Array.prototype.set.call(target, source, offset); +} +console.log(copy(memory(), new Uint8Array([3, 4]), 1)); +console.log(String(memory())); +copy(memory(), [9], undefined); +console.log(String(memory())); + +// Every typed array shares %TypedArray%.prototype.set. +const wide = new Int16Array(3); +copy(wide, [1, -2], 1); +console.log(String(wide)); +Float64Array.prototype.set.call(memory(), new Uint8Array([7]), 3); +console.log(String(memory())); + +const order = []; +const arg = (label, value) => { + order.push(label); + return value; +}; +for (const target of [{}, [0, 0], "ab", null, new DataView(new ArrayBuffer(2))]) { + order.length = 0; + try { + Uint8Array.prototype.set.call(arg("target", target), arg("source", [1]), arg("offset", 0)); + console.log("set"); + } catch (e) { + console.log(e instanceof TypeError, e.message, order.join(",")); + } +} +try { + copy(memory(), [1, 2], 3); +} catch (e) { + console.log(e instanceof RangeError, e.message); +} diff --git a/tests/corpus/7941-typed-array-prototype-set.ts b/tests/corpus/7941-typed-array-prototype-set.ts new file mode 100644 index 000000000..db880e38a --- /dev/null +++ b/tests/corpus/7941-typed-array-prototype-set.ts @@ -0,0 +1,21 @@ +// Uint8Array.prototype.set borrowed through .call on typed receivers. +const target = new Uint8Array(4); +Uint8Array.prototype.set.call(target, new Uint8Array([3, 4]), 1); +console.log(String(target)); +Uint8Array.prototype.set.call(target, [5]); +console.log(String(target)); + +const wide = new Int32Array(3); +Uint8Array.prototype.set.call(wide, new Uint8Array([200, 100]), 1); +console.log(String(wide)); + +function fill(view: Uint8Array, bytes: number[], at: number): void { + Uint8Array.prototype.set.call(view, bytes, at); +} +fill(target, [8, 9], 2); +console.log(String(target)); +try { + fill(target, [1, 2, 3], 2); +} catch (e) { + console.log(e instanceof RangeError, (e as Error).message); +} diff --git a/tests/corpus/7942-dictionary-store-method-calls.ts b/tests/corpus/7942-dictionary-store-method-calls.ts new file mode 100644 index 000000000..0056c26f6 --- /dev/null +++ b/tests/corpus/7942-dictionary-store-method-calls.ts @@ -0,0 +1,45 @@ +// Stores into dictionaries whose values cannot be functions leave class +// methods in place. +interface Data { + items: string[]; +} +class Store { + data: Data = { items: [] }; + get(): Data { + return this.data; + } +} +class Counter { + n = 0; + add(): number { + return ++this.n; + } +} +interface Settings { + groups: Record>; +} +interface Tree { + [key: string]: Tree; +} +function addGroup(settings: Settings, name: string): void { + settings.groups[name] = {}; +} +function grow(tree: Tree, name: string): Tree { + tree[name] = {}; + return tree[name]!; +} +const settings: Settings = { groups: {} }; +addGroup(settings, "colours"); +addGroup(settings, "fonts"); +settings.groups["colours"]!["fg"] = "red"; +console.log(Object.keys(settings.groups).join(","), JSON.stringify(settings.groups)); +const tree: Tree = {}; +grow(grow(tree, "a"), "b"); +console.log(JSON.stringify(tree)); +const store = new Store(); +store.get().items.push("a"); +store.get().items.push("b"); +const counter = new Counter(); +let total = 0; +for (let i = 0; i < 1000; i++) total += counter.add(); +console.log(store.data.items.length, total, typeof counter.add); From b630efd450f066dabd3013e9d48b40a2b87ae0d1 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 13:41:29 -0700 Subject: [PATCH 160/409] Report inferred shapes for untyped npm JavaScript (shape inference, stage 1) SCRIPTC_DEBUG_SHAPES=1 runs a whole-program allocation-site points-to analysis over opted-in npm packages' JavaScript and the user files that call into them, and prints which object and array literals a native record would represent exactly, the inferred class field types, the census of writes and reflection on untracked receivers, and the hot functions' parameter and return types. Report only: nothing reaches the lowering, and the analysis runs only when the variable is set. Design: results/design-shape-inference/DESIGN.md (scriptc-perf). Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/program.ts | 5 + .../src/frontend/shape-inference/analyze.ts | 2864 +++++++++++++++++ .../src/frontend/shape-inference/domain.ts | 153 + .../src/frontend/shape-inference/index.ts | 21 + .../src/frontend/shape-inference/report.ts | 374 +++ .../compiler/test/shape-inference.test.ts | 166 + 6 files changed, 3583 insertions(+) create mode 100644 packages/compiler/src/frontend/shape-inference/analyze.ts create mode 100644 packages/compiler/src/frontend/shape-inference/domain.ts create mode 100644 packages/compiler/src/frontend/shape-inference/index.ts create mode 100644 packages/compiler/src/frontend/shape-inference/report.ts create mode 100644 packages/compiler/test/shape-inference.test.ts diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 6d8ffe4c0..a1f440648 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -140,6 +140,7 @@ import { } from "./type-only-imports.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; import { planDeclProjection, type DeclProjection } from "./decl-projection.js"; +import { reportShapes } from "./shape-inference/index.js"; import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js"; import { containsJsx, @@ -1260,6 +1261,10 @@ export function loadProgram( npmStaticPackages.length > 0, projector, ); + // Shape inference, stage S1: a report only (SCRIPTC_DEBUG_SHAPES). + const shapes = process.env["SCRIPTC_DEBUG_SHAPES"]; + if (shapes !== undefined && shapes !== "" && shapes !== "0" && npmStaticPackages.length > 0) + reportShapes(load.program, shapes); return { ...load, services, diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts new file mode 100644 index 000000000..55e0e0107 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -0,0 +1,2864 @@ +/* Shape inference (results/design-shape-inference/DESIGN.md), stage S1: + * a whole-program allocation-site points-to analysis over the JavaScript of + * opted-in npm packages and the user files that call into them. + * + * Stage S1 is report-only: the analysis runs when SCRIPTC_DEBUG_SHAPES is + * set and prints its plan (report.ts). Nothing it computes reaches the + * lowering yet, so every program compiles exactly as before. + * + * The analysis (DESIGN.md §4): + * - Andersen-style inclusion constraints, solved with a worklist. Locals + * and parameters are one value node per binding (flow-insensitive in S1); + * heap fields are one node per (atom, name); arrays one element node. + * - Calls resolve by the callee's atoms: functions, class constructors, + * methods by the receiver's class chain (inherited members, getters and + * setters, `this.constructor`), closures stored in fields, `bind`, and + * builtin summaries for the common library surface. + * - Escape: an atom escapes when it reaches code the analysis cannot see + * (a ⊤ callee, a builtin without a summary, a store into a ⊤ object). + * Escaped functions run with ⊤ arguments; everything reachable from an + * escaped atom escapes. + * - Census: writes and reflection whose receiver may be ⊤ apply to every + * escaped atom (§5.4); the report uses them to decide which allocation + * sites could become native records (§5.5). */ +import * as ts from "../ts7/adapter.js"; +import { isJsSourceFile } from "../program.js"; +import { npmStaticPackageOfPath } from "../npm-static.js"; +import { AtomTable, MODELED_GLOBALS, STRING_METHOD_RESULTS, type Atom } from "./domain.js"; + +type Node = ts.Node; + +export interface ValueNode { + readonly id: number; + readonly atoms: Set; + readonly succ: Set; + /** Constraints to run for each atom that arrives. */ + readonly onAtom: ((atom: Atom) => void)[]; + /** Every atom that arrives escapes. */ + escapeSink: string | null; +} + +export interface FunctionInfo { + readonly node: Node; + /** The analysis context: "" or a method's receiver (`C`). */ + readonly ctx: string; + /** The function whose clone the context is (bindings inside it are per context). */ + readonly root: Node | null; + /** Bodies are walked on first call: code that never runs adds no facts. */ + walked: boolean; + readonly label: string; + readonly params: number[]; + /** Index of a rest parameter, or -1. */ + readonly rest: number; + readonly thisNode: number; + readonly ret: number; + readonly isArrow: boolean; + readonly isAsync: boolean; + readonly isGenerator: boolean; + /** Reads `arguments` other than through `...arguments` forwarding. */ + usesArguments: boolean; + /** `f(...arguments)` forwarding: one node per argument position. */ + argPos: number[] | null; + /** The class whose member this function is (method, accessor, constructor). */ + readonly ownerClass: Node | null; + readonly inPackage: boolean; +} + +export interface LiteralSite { + readonly atom: Atom; + readonly node: Node; + readonly kind: "object" | "array"; + /** Static keys in creation order; null when a spread or computed key makes them unknown. */ + readonly keys: string[] | null; + readonly hasAccessor: boolean; + readonly hasProto: boolean; + readonly hasComputedKey: boolean; + readonly spreads: number[]; + readonly inPackage: boolean; +} + +export interface Op { + readonly kind: + | "read" + | "write" + | "computedRead" + | "computedWrite" + | "delete" + | "freeze" + | "seal" + | "defineProperty" + | "setPrototypeOf" + | "assign" + | "proxy" + | "lengthWrite"; + readonly recv: number; + readonly name: string | null; + readonly node: Node; +} + +export interface CallEdge { + readonly caller: FunctionInfo | null; + readonly callee: FunctionInfo; + readonly inLoop: boolean; +} + +export interface ShapeAnalysis { + readonly atoms: AtomTable; + readonly nodes: ValueNode[]; + readonly functions: Map; + readonly sites: LiteralSite[]; + readonly ops: Op[]; + readonly calls: CallEdge[]; + readonly escaped: Map; + readonly topWriteNames: Map; + readonly topComputedWrites: Node[]; + /** Fields per atom: name → node id. */ + readonly fields: Map>; + readonly elements: Map; + readonly top: Atom; + readonly files: readonly ts.SourceFile[]; + readonly packageFiles: number; + readonly checker: ts.TypeChecker; + readonly budgetExceeded: boolean; + readonly steps: number; + readonly topCause: ReadonlyMap; + readonly nodeLabel: ReadonlyMap; +} + +/** Array.prototype members: other names on an array are own properties. */ +const ARRAY_METHODS: ReadonlySet = new Set([ + "at", + "concat", + "copyWithin", + "entries", + "every", + "fill", + "filter", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flat", + "flatMap", + "forEach", + "includes", + "indexOf", + "join", + "keys", + "lastIndexOf", + "map", + "pop", + "push", + "reduce", + "reduceRight", + "reverse", + "shift", + "slice", + "some", + "sort", + "splice", + "toLocaleString", + "toReversed", + "toSorted", + "toSpliced", + "toString", + "unshift", + "values", + "with", +]); +const MAX_STEPS = 20_000_000; + +function list(arr: { readonly nodes: readonly T[] } | undefined): readonly T[] { + return arr?.nodes ?? []; +} + +function isStatic(node: Node): boolean { + const mods = (node as { modifiers?: { nodes: readonly Node[] } }).modifiers; + return list(mods).some((m) => m.kind === ts.SyntaxKind.StaticKeyword); +} + +function unparen(node: ts.Expression): ts.Expression { + let n = node; + for (;;) { + if (ts.isParenthesizedExpression(n)) n = n.expression; + else if (ts.isNonNullExpression(n)) n = n.expression; + else if (ts.isAsExpression(n) || ts.isSatisfiesExpression(n)) n = n.expression; + else return n; + } +} + +function propertyNameText(name: Node): string | null { + if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) return name.text; + if (ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) return name.text; + if (ts.isNumericLiteral(name)) return String(Number(name.text)); + if (ts.isComputedPropertyName(name)) { + const e = unparen(name.expression); + if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return e.text; + if (ts.isNumericLiteral(e)) return String(Number(e.text)); + } + return null; +} + +function shortFile(fileName: string): string { + const norm = fileName.split("\\").join("/"); + const nm = norm.lastIndexOf("/node_modules/"); + if (nm >= 0) { + const rest = norm.slice(nm + "/node_modules/".length).split("/"); + const pkgLen = rest[0]?.startsWith("@") ? 2 : 1; + const pkg = rest.slice(0, pkgLen).join("/"); + return `${pkg}/${rest[rest.length - 1]}`; + } + return norm.slice(norm.lastIndexOf("/") + 1); +} + +export function locLabel(node: Node): string { + const sf = node.getSourceFile(); + const { line } = ts.getLineAndCharacterOfPosition(sf, node.getStart(sf)); + return `${shortFile(sf.fileName)}:${line + 1}`; +} + +/** Whether a source file belongs to an opted-in package's JavaScript. */ +export function isPackageJsFile(sf: ts.SourceFile): boolean { + return isJsSourceFile(sf) && npmStaticPackageOfPath(sf.fileName) !== null; +} + +function isLoop(node: Node): boolean { + return ( + ts.isForStatement(node) || + ts.isForOfStatement(node) || + ts.isForInStatement(node) || + ts.isWhileStatement(node) || + ts.isDoStatement(node) + ); +} + +const LOOP_CALLBACK_METHODS: ReadonlySet = new Set([ + "map", + "forEach", + "filter", + "some", + "every", + "find", + "findIndex", + "findLast", + "findLastIndex", + "reduce", + "reduceRight", + "flatMap", + "sort", +]); + +export function analyzeShapes(program: ts.Program, checker: ts.TypeChecker): ShapeAnalysis { + return new Analyzer(program, checker).run(); +} + +class Analyzer { + readonly atoms = new AtomTable(); + readonly nodes: ValueNode[] = []; + readonly functions = new Map(); + readonly sites: LiteralSite[] = []; + private readonly siteAtoms = new Set(); + readonly ops: Op[] = []; + readonly calls: CallEdge[] = []; + readonly escaped = new Map(); + readonly topWriteNames = new Map(); + readonly topComputedWrites: Node[] = []; + readonly fields = new Map>(); + readonly elements = new Map(); + readonly top: Atom; + private readonly bindings = new Map(); + private readonly exprMemo = new Map(); + /** Current analysis context and the function whose clone it is. */ + private ctx = ""; + private ctxRoot: Node | null = null; + private readonly classInfo = new Map(); + private readonly boundCalls = new Set(); + private readonly escapedFunctionCalls = new Set(); + private readonly worklist: [number, Atom][] = []; + private steps = 0; + private budgetExceeded = false; + private currentFn: FunctionInfo | null = null; + private loopDepth = 0; + private files: ts.SourceFile[] = []; + private packageFiles = 0; + private readonly prims = new Map(); + /** Per call site: the calling function and whether the site is in a loop + * (constraints fire during solving, after the walk has moved on). */ + private readonly siteCtx = new Map(); + private forceLoop = false; + + noteSite(site: Node): void { + if (!this.siteCtx.has(site)) + this.siteCtx.set(site, { caller: this.currentFn, inLoop: this.loopDepth > 0 }); + } + + edgeFor(site: Node, callee: FunctionInfo): void { + const ctx = this.siteCtx.get(site); + this.calls.push({ + caller: ctx?.caller ?? null, + callee, + inLoop: (ctx?.inLoop ?? false) || this.forceLoop, + }); + } + + constructor( + private readonly program: ts.Program, + private readonly checker: ts.TypeChecker, + ) { + this.top = this.atoms.intern("top", () => ({ kind: "top", label: "⊤" })); + } + + run(): ShapeAnalysis { + for (const sf of this.program.getSourceFiles()) { + if (sf.isDeclarationFile) continue; + const name = sf.fileName.split("\\").join("/"); + const inPackage = isPackageJsFile(sf); + if (!inPackage && name.includes("/node_modules/")) continue; + this.files.push(sf); + if (inPackage) this.packageFiles++; + } + for (const sf of this.files) this.checker.prefetchSourceFile(sf); + for (const sf of this.files) this.moduleBody(sf); + this.finish(); + return { + atoms: this.atoms, + nodes: this.nodes, + functions: this.functions, + sites: this.sites, + ops: this.ops, + calls: this.calls, + escaped: this.escaped, + topWriteNames: this.topWriteNames, + topComputedWrites: this.topComputedWrites, + fields: this.fields, + elements: this.elements, + top: this.top, + files: this.files, + packageFiles: this.packageFiles, + checker: this.checker, + budgetExceeded: this.budgetExceeded, + steps: this.steps, + topCause: this.topCause, + nodeLabel: this.nodeLabel, + }; + } + + // ---- value nodes ------------------------------------------------------- + + newNode(): number { + const id = this.nodes.length; + this.nodes.push({ id, atoms: new Set(), succ: new Set(), onAtom: [], escapeSink: null }); + return id; + } + + /** Debugging (SCRIPTC_DEBUG_SHAPES_WHY): where each node's ⊤ came from. */ + readonly topCause = new Map(); + readonly nodeLabel = new Map(); + private cause: number | null = null; + private readonly traceTop = process.env["SCRIPTC_DEBUG_SHAPES_WHY"] !== undefined; + + add(node: number, atom: Atom): void { + const n = this.nodes[node]!; + if (n.atoms.has(atom)) return; + n.atoms.add(atom); + if (this.traceTop && atom === this.top && !this.topCause.has(node)) + this.topCause.set( + node, + this.cause ?? + (new Error().stack ?? "") + .split("\n") + .slice(2, 4) + .map((l) => l.trim()) + .join(" < "), + ); + this.worklist.push([node, atom]); + } + + edge(from: number, to: number): void { + if (from === to) return; + const n = this.nodes[from]!; + if (n.succ.has(to)) return; + n.succ.add(to); + const saved = this.cause; + this.cause = from; + for (const a of n.atoms) this.add(to, a); + this.cause = saved; + } + + on(node: number, fn: (atom: Atom) => void): void { + const n = this.nodes[node]!; + n.onAtom.push(fn); + for (const a of [...n.atoms]) fn(a); + } + + sink(node: number, reason: string): void { + const n = this.nodes[node]!; + if (n.escapeSink !== null) return; + n.escapeSink = reason; + for (const a of [...n.atoms]) this.escape(a, reason); + } + + constNode(atom: Atom): number { + const n = this.newNode(); + this.add(n, atom); + return n; + } + + prim(name: string): Atom { + let a = this.prims.get(name); + if (a === undefined) { + a = this.atoms.intern(`prim:${name}`, () => ({ kind: "prim", label: name })); + this.prims.set(name, a); + } + return a; + } + + builtin(path: string, receiver?: Atom): Atom { + const key = receiver === undefined ? `builtin:${path}` : `builtin:${path}@${receiver.id}`; + return this.atoms.intern(key, () => ({ kind: "builtin", label: path, path, receiver })); + } + + solve(): void { + while (this.worklist.length > 0) { + if (++this.steps > MAX_STEPS) { + this.budgetExceeded = true; + return; + } + const [id, atom] = this.worklist.pop()!; + const n = this.nodes[id]!; + this.cause = id; + for (const s of n.succ) this.add(s, atom); + this.cause = null; + for (const f of n.onAtom) f(atom); + if (n.escapeSink !== null) this.escape(atom, n.escapeSink); + } + } + + // ---- heap ---------------------------------------------------------------- + + field(atom: Atom, name: string): number { + let m = this.fields.get(atom); + if (m === undefined) { + m = new Map(); + this.fields.set(atom, m); + } + let id = m.get(name); + if (id === undefined) { + id = this.newNode(); + m.set(name, id); + if (this.traceTop) this.nodeLabel.set(id, `${atom.label}.${name}`); + const reason = this.escaped.get(atom); + if (reason !== undefined) { + this.sink(id, reason); + if (this.topComputedWrites.length > 0 || this.topWriteNames.has(name)) + this.add(id, this.top); + } + } + return id; + } + + element(atom: Atom): number { + let id = this.elements.get(atom); + if (id === undefined) { + id = this.newNode(); + this.elements.set(atom, id); + const reason = this.escaped.get(atom); + if (reason !== undefined) { + this.sink(id, reason); + if (this.topComputedWrites.length > 0) this.add(id, this.top); + } + } + return id; + } + + escape(atom: Atom, reason: string): void { + if (atom.kind === "prim" || atom.kind === "top") return; + if (atom.kind === "builtin" && atom.receiver === undefined) return; + if (this.escaped.has(atom)) return; + this.escaped.set(atom, reason); + const via = `via ${atom.label}`; + const fm = this.fields.get(atom); + if (fm !== undefined) { + for (const [name, id] of fm) { + this.sink(id, via); + if (this.topComputedWrites.length > 0 || this.topWriteNames.has(name)) + this.add(id, this.top); + } + } + const el = this.elements.get(atom); + if (el !== undefined) { + this.sink(el, via); + if (this.topComputedWrites.length > 0) this.add(el, this.top); + } + if (atom.receiver !== undefined) this.escape(atom.receiver, via); + if (atom.kind === "function") { + const info = this.infoOfAtom(atom); + if (info !== undefined) this.escapeFunction(info, via); + } + if ((atom.kind === "instance" || atom.kind === "class") && atom.node !== undefined) { + // Unknown code may call any member with any arguments. + for (const member of this.classMembers(atom.node, atom.kind === "class")) { + const info = this.functionInfo(member, this.ownerOf(member), this.receiverCtx(atom)); + this.add(info.thisNode, atom); + this.escapeFunction(info, via); + } + } + if (atom.kind === "promise") this.sink(this.field(atom, "value"), via); + } + + /** A function unknown code may call: ⊤ arguments and receiver; its + * results escape. */ + escapeFunction(info: FunctionInfo, reason: string): void { + if (this.escapedFunctionCalls.has(info)) return; + this.escapedFunctionCalls.add(info); + this.activate(info); + for (const p of info.params) this.add(p, this.top); + if (!info.isArrow && info.ownerClass === null) this.add(info.thisNode, this.top); + this.sink(info.ret, reason); + } + + /** Property names that untyped code writes on receivers that may be ⊤ (census). */ + noteTopWrite(name: string | null, node: Node): void { + if (name === null) { + const first = this.topComputedWrites.length === 0; + this.topComputedWrites.push(node); + if (first) { + for (const atom of [...this.escaped.keys()]) { + for (const id of (this.fields.get(atom) ?? new Map()).values()) + this.add(id, this.top); + const el = this.elements.get(atom); + if (el !== undefined) this.add(el, this.top); + } + } + return; + } + if (this.topWriteNames.has(name)) return; + this.topWriteNames.set(name, node); + for (const atom of [...this.escaped.keys()]) { + const id = this.fields.get(atom)?.get(name); + if (id !== undefined) this.add(id, this.top); + } + } + + // ---- classes --------------------------------------------------------------- + + /** The base class declaration of `decl`, via the checker; null at a + * builtin base or none; undefined when unresolved. */ + baseClass(decl: Node): Node | null | undefined { + const heritage = list((decl as ts.ClassLikeDeclaration).heritageClauses).find( + (c) => c.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]; + if (heritage === undefined) return null; + const e = unparen(heritage.expression); + if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e)) return undefined; + let d = this.resolve(ts.isIdentifier(e) ? e : e.name); + // `var C = class C { … }` (bundler output) + if ( + d !== undefined && + ts.isVariableDeclaration(d) && + d.initializer !== undefined && + ts.isClassExpression(unparen(d.initializer)) + ) + d = unparen(d.initializer); + if (d !== undefined && ts.isClassLikeDeclaration(d)) return d; + if (d !== undefined && d.getSourceFile().isDeclarationFile) return null; + return undefined; + } + + classChain(decl: Node): Node[] { + const out: Node[] = []; + let cur: Node | null | undefined = decl; + while (cur && out.length < 64 && !out.includes(cur)) { + out.push(cur); + cur = this.baseClass(cur); + } + return out; + } + + /** Method, accessor and constructor declarations of a class chain. */ + classMembers(decl: Node, statics: boolean): Node[] { + const out: Node[] = []; + for (const c of this.classChain(decl)) { + for (const m of list((c as ts.ClassLikeDeclaration).members)) { + if (isStatic(m) !== statics) continue; + if ( + ts.isMethodDeclaration(m) || + ts.isGetAccessorDeclaration(m) || + ts.isSetAccessorDeclaration(m) + ) + out.push(m); + else if (ts.isPropertyDeclaration(m) && m.initializer !== undefined) { + const init = unparen(m.initializer); + if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) out.push(init); + } + } + } + return out; + } + + lookupMember( + decl: Node, + name: string, + statics: boolean, + ): { + kind: "method" | "getter" | "setter" | "both"; + get?: Node; + set?: Node; + method?: Node; + } | null { + for (const c of this.classChain(decl)) { + let get: Node | undefined; + let set: Node | undefined; + for (const m of list((c as ts.ClassLikeDeclaration).members)) { + if (isStatic(m) !== statics) continue; + const mn = (m as { name?: Node }).name; + if (mn === undefined || propertyNameText(mn) !== name) continue; + if (ts.isMethodDeclaration(m)) return { kind: "method", method: m }; + if (ts.isGetAccessorDeclaration(m)) get = m; + if (ts.isSetAccessorDeclaration(m)) set = m; + if (ts.isPropertyDeclaration(m)) return null; // an own field: data + } + if (get || set) + return { + kind: get && set ? "both" : get ? "getter" : "setter", + ...(get ? { get } : {}), + ...(set ? { set } : {}), + }; + } + return null; + } + + // ---- symbols ----------------------------------------------------------------- + + resolve(id: Node): Node | undefined { + let sym = this.checker.getSymbolAtLocation(id); + if (sym === undefined) return undefined; + if (sym.flags & ts.SymbolFlags.Alias) { + try { + sym = this.checker.getAliasedSymbol(sym); + } catch { + return undefined; + } + } + return sym.valueDeclaration ?? sym.declarations[0]; + } + + binding(decl: Node): number { + const key = this.ctxKey(decl); + let id = this.bindings.get(key); + if (id === undefined) { + id = this.newNode(); + this.bindings.set(key, id); + if (this.traceTop) + this.nodeLabel.set( + id, + `binding ${(decl as { name?: { text?: string } }).name?.text ?? "?"}@${locLabel(decl)}${this.ctx !== "" ? ` [${this.ctx}]` : ""}`, + ); + } + return id; + } + + /** Bindings and expressions inside the function being cloned belong to + * the clone's context; everything else to the default context. */ + ctxKey(node: Node): string { + const root = this.ctxRoot; + const inside = + root !== null && + node.getSourceFile() === root.getSourceFile() && + node.pos >= root.pos && + node.end <= root.end; + return `${inside ? this.ctx : ""}|${this.nodeKey(node)}`; + } + + receiverCtx(atom: Atom): string { + return atom.kind === "instance" || atom.kind === "class" ? `C${atom.id}` : ""; + } + + infoOfAtom(atom: Atom): FunctionInfo | undefined { + if (atom.node === undefined) return undefined; + return this.functions.get(`${atom.ctx ?? ""}|${this.nodeKey(atom.node)}`); + } + + // ---- functions --------------------------------------------------------------- + + functionInfo(node: Node, ownerClass: Node | null, ctxArg = ""): FunctionInfo { + // A nested closure walked inside a clone shares the clone's context. + const inherit = + ctxArg === "" && + this.ctxRoot !== null && + node.getSourceFile() === this.ctxRoot.getSourceFile() && + node.pos >= this.ctxRoot.pos && + node.end <= this.ctxRoot.end; + const ctx = ctxArg !== "" ? ctxArg : inherit ? this.ctx : ""; + const root = ctxArg !== "" ? node : inherit ? this.ctxRoot : null; + const fkey = `${ctx}|${this.nodeKey(node)}`; + const existing = this.functions.get(fkey); + if (existing !== undefined) return existing; + const savedCtx = this.ctx; + const savedRoot = this.ctxRoot; + this.ctx = ctx; + this.ctxRoot = root; + const fn = node as ts.FunctionLikeDeclaration; + const isArrow = ts.isArrowFunction(node); + const params: number[] = []; + let rest = -1; + for (const p of list(fn.parameters)) { + const id = this.binding(p); + if (p.dotDotDotToken !== undefined) rest = params.length; + params.push(id); + } + const mods = list((fn as { modifiers?: { nodes: readonly Node[] } }).modifiers); + const sf = node.getSourceFile(); + const nameNode = (fn as { name?: Node }).name; + const name = nameNode !== undefined ? (propertyNameText(nameNode) ?? "") : null; + const owner = ownerClass !== null ? this.className(ownerClass) : null; + const label = `${owner !== null ? `${owner}.` : ""}${name ?? (ts.isConstructorDeclaration(node) ? "constructor" : "")}@${locLabel(node)}`; + const info: FunctionInfo = { + node, + ctx, + root, + walked: false, + label: ctx === "" ? label : `${label} [${this.ctxLabel(ctx)}]`, + params, + rest, + thisNode: isArrow && this.currentFn !== null ? this.currentFn.thisNode : this.newNode(), + ret: this.newNode(), + isArrow, + isAsync: mods.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword), + isGenerator: (fn as { asteriskToken?: Node }).asteriskToken !== undefined, + usesArguments: false, + argPos: null, + ownerClass, + inPackage: isPackageJsFile(sf), + }; + this.functions.set(fkey, info); + this.ctx = savedCtx; + this.ctxRoot = savedRoot; + return info; + } + + /** Walks a function's body the first time it can run. */ + activate(info: FunctionInfo): void { + if (info.walked) return; + info.walked = true; + if (ts.isSourceFile(info.node) || ts.isClassLikeDeclaration(info.node)) return; + const fn = info.node as ts.FunctionLikeDeclaration; + const savedCtx = this.ctx; + const savedRoot = this.ctxRoot; + const saved = this.currentFn; + const savedLoop = this.loopDepth; + this.ctx = info.ctx; + this.ctxRoot = info.root; + this.currentFn = info; + this.loopDepth = 0; + for (const p of list(fn.parameters)) { + if (p.initializer !== undefined) this.edge(this.expr(p.initializer), this.binding(p)); + if (p.dotDotDotToken !== undefined) { + const arr = this.atoms.intern(`rest:${this.nodes.length}`, () => ({ + kind: "array", + label: `rest@${locLabel(p)}`, + node: p, + })); + this.add(this.binding(p), arr); + } + this.bindPattern(p.name, this.binding(p)); + } + const body = fn.body; + if (body !== undefined) { + if (ts.isBlock(body)) this.stmts(list(body.statements)); + else this.edge(this.expr(body as ts.Expression), info.ret); + } + this.currentFn = saved; + this.loopDepth = savedLoop; + this.ctx = savedCtx; + this.ctxRoot = savedRoot; + if (info.usesArguments) { + for (const p of info.params) this.add(p, this.top); + } + } + + /** The atom of a function node (memoized by its info). */ + fnAtom(node: Node, ownerClass: Node | null, ctx = ""): Atom { + const info = this.functionInfo(node, ownerClass, ctx); + return this.atoms.intern(`fn:${info.ret}`, () => ({ + kind: "function", + label: info.label, + node, + ctx: info.ctx, + })); + } + + ctxLabel(ctx: string): string { + if (ctx.startsWith("C")) return `this=${this.atoms.atoms[Number(ctx.slice(1))]?.label ?? ctx}`; + return ctx; + } + + className(decl: Node): string { + const own = (decl as ts.ClassLikeDeclaration).name?.text; + if (own !== undefined) return own; + const p = decl.parent; + if (p !== undefined && ts.isVariableDeclaration(p) && ts.isIdentifier(p.name)) + return p.name.text; + return ``; + } + + classAtoms(decl: Node): { cls: Atom; inst: Atom } { + const name = this.className(decl); + const cls = this.atoms.intern(`class:${this.nodeKey(decl)}`, () => ({ + kind: "class", + label: `class ${name}`, + node: decl, + })); + const inst = this.atoms.intern(`inst:${this.nodeKey(decl)}`, () => ({ + kind: "instance", + label: name, + node: decl, + })); + this.ensureClass(decl); + return { cls, inst }; + } + + private readonly nodeKeys = new Map(); + nodeKey(node: Node): number { + let k = this.nodeKeys.get(node); + if (k === undefined) { + k = this.nodeKeys.size; + this.nodeKeys.set(node, k); + } + return k; + } + + /** Registers a class: member functions, field initializers (as a pseudo + * function run at construction) and static initializers. */ + ensureClass(decl: Node): { initFn: FunctionInfo; staticInit: FunctionInfo } { + const existing = this.classInfo.get(decl); + if (existing !== undefined) return existing; + const c = decl as ts.ClassLikeDeclaration; + const label = this.className(decl); + const mk = (suffix: string): FunctionInfo => ({ + node: decl, + ctx: "", + root: null, + walked: true, + label: `${label}.${suffix}@${locLabel(decl)}`, + params: [], + rest: -1, + thisNode: this.newNode(), + ret: this.newNode(), + isArrow: false, + isAsync: false, + isGenerator: false, + usesArguments: false, + argPos: null, + ownerClass: decl, + inPackage: isPackageJsFile(decl.getSourceFile()), + }); + const entry = { initFn: mk(""), staticInit: mk("") }; + this.classInfo.set(decl, entry); + const cls = this.atoms.intern(`class:${this.nodeKey(decl)}`, () => ({ + kind: "class", + label: `class ${label}`, + node: decl, + })); + this.add(entry.staticInit.thisNode, cls); + const saved = this.currentFn; + for (const m of list(c.members)) { + const statics = isStatic(m); + if ( + ts.isMethodDeclaration(m) || + ts.isGetAccessorDeclaration(m) || + ts.isSetAccessorDeclaration(m) || + ts.isConstructorDeclaration(m) + ) { + continue; + } + if (ts.isPropertyDeclaration(m)) { + const name = propertyNameText(m.name); + const init = statics ? entry.staticInit : entry.initFn; + this.currentFn = init; + const value = + m.initializer !== undefined + ? this.exprIn(m.initializer, decl) + : this.constNode(this.prim("undefined")); + if (name !== null) this.storeConstraint(init.thisNode, name, value, m); + continue; + } + if (ts.isClassStaticBlockDeclaration(m)) { + this.currentFn = entry.staticInit; + this.stmts(list(m.body.statements)); + } + } + this.currentFn = saved; + return entry; + } + + /** An initializer expression: arrow functions in field initializers take + * the instance as `this` and belong to the class. */ + exprIn(e: ts.Expression, ownerClass: Node): number { + const u = unparen(e); + if (ts.isArrowFunction(u) || ts.isFunctionExpression(u)) { + const n = this.newNode(); + this.add(n, this.fnAtom(u, ts.isArrowFunction(u) ? null : ownerClass)); + return n; + } + return this.expr(e); + } + + /** `new C(args)` on a class atom: the instance, the field initializers of + * every class in the chain, and the constructor (or the implicit one). */ + construct( + decl: Node, + args: number[], + spread: boolean, + dst: number, + site: Node, + inst?: Atom, + ): void { + const atoms = this.classAtoms(decl); + const instance = inst ?? atoms.inst; + this.add(dst, instance); + this.callConstructorChain(decl, instance, args, spread, site); + } + + callConstructorChain( + decl: Node, + instance: Atom, + args: number[], + spread: boolean, + site: Node, + ): void { + const key = `ctor:${this.nodeKey(decl)}:${instance.id}:${this.nodeKey(site)}`; + if (this.boundCalls.has(key)) return; + this.boundCalls.add(key); + const entry = this.ensureClass(decl); + this.add(entry.initFn.thisNode, instance); + const ctor = list((decl as ts.ClassLikeDeclaration).members).find((m) => + ts.isConstructorDeclaration(m), + ); + if (ctor !== undefined) { + const info = this.functionInfo(ctor, decl, this.receiverCtx(instance)); + this.activate(info); + this.add(info.thisNode, instance); + this.bindArgs(info, args, spread); + this.edgeFor(site, info); + return; + } + const base = this.baseClass(decl); + if (base) this.callConstructorChain(base, instance, args, spread, site); + } + + bindArgs(info: FunctionInfo, args: number[], spread: boolean): void { + if (info.argPos !== null) { + for (let i = 0; i < info.argPos.length; i++) { + if (spread || i >= info.argPos.length - 1) { + for (const a of args.slice(i)) this.edge(a, info.argPos[i]!); + if (spread) this.add(info.argPos[i]!, this.top); + } else if (i < args.length) this.edge(args[i]!, info.argPos[i]!); + else this.add(info.argPos[i]!, this.prim("undefined")); + } + } + if (spread) { + for (const p of info.params) this.add(p, this.top); + for (const a of args) this.sink(a, `spread into ${info.label}`); + return; + } + for (let i = 0; i < info.params.length; i++) { + if (i === info.rest) { + const restAtoms = [...this.nodes[info.params[i]!]!.atoms]; + for (const ra of restAtoms) + for (let j = i; j < args.length; j++) this.edge(args[j]!, this.element(ra)); + break; + } + if (i < args.length) this.edge(args[i]!, info.params[i]!); + else this.add(info.params[i]!, this.prim("undefined")); + } + } + + /** Call of a function atom with a receiver node. */ + callFunction( + info: FunctionInfo, + thisNode: number | null, + args: number[], + spread: boolean, + dst: number, + site: Node, + ): void { + // `() => Object.create(null)` helpers: each call site gets its own + // dictionary, as if the helper were inlined. + if (this.isFactory(info.node)) { + const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({ + kind: "dictionary", + label: `Object.create@${locLabel(site)}`, + node: site, + })); + this.add(dst, d); + return; + } + this.activate(info); + const fnNode = info.node; + const key = `call:${info.ctx}|${this.nodeKey(fnNode)}:${this.nodeKey(site)}:${thisNode ?? -1}:${dst}`; + if (this.boundCalls.has(key)) return; + this.boundCalls.add(key); + if (thisNode !== null && !info.isArrow) this.edge(thisNode, info.thisNode); + this.bindArgs(info, args, spread); + if (info.isAsync) { + const p = this.promiseAtom(fnNode, `async ${info.label}`); + this.awaitInto(info.ret, this.field(p, "value")); + this.add(dst, p); + } else if (info.isGenerator) { + this.add(dst, this.top); + } else this.edge(info.ret, dst); + this.edgeFor(site, info); + } + + private readonly factories = new Map(); + isFactory(node: Node): boolean { + let r = this.factories.get(node); + if (r === undefined) { + r = false; + const fn = node as ts.FunctionLikeDeclaration; + let e: Node | undefined = fn.body; + if (e !== undefined && ts.isBlock(e)) { + const st = e.statements.nodes; + e = st.length === 1 && ts.isReturnStatement(st[0]!) ? st[0]!.expression : undefined; + } + if (e !== undefined && !ts.isSourceFile(node)) { + const u = unparen(e as ts.Expression); + r = + ts.isCallExpression(u) && + u.expression.getText() === "Object.create" && + u.arguments.nodes.length === 1 && + u.arguments.nodes[0]!.kind === ts.SyntaxKind.NullKeyword; + } + this.factories.set(node, r); + } + return r; + } + + promiseAtom(site: Node, label: string): Atom { + return this.atoms.intern(`promise:${this.nodeKey(site)}`, () => ({ + kind: "promise", + label: `Promise@${label}`, + node: site, + })); + } + + /** dst ⊇ awaited(src). */ + awaitInto(src: number, dst: number): void { + this.on(src, (a) => { + if (a.kind === "promise") this.edge(this.field(a, "value"), dst); + else this.add(dst, a); + }); + } + + syntheticArray(site: Node, tag: string): Atom { + return this.atoms.intern(`arr:${tag}:${this.nodeKey(site)}`, () => ({ + kind: "array", + label: `${tag}[]@${locLabel(site)}`, + node: site, + })); + } + + // ---- statements ---------------------------------------------------------------- + + moduleBody(sf: ts.SourceFile): void { + const info: FunctionInfo = { + node: sf, + ctx: "", + root: null, + walked: true, + label: ``, + params: [], + rest: -1, + thisNode: this.constNode(this.prim("undefined")), + ret: this.newNode(), + isArrow: false, + isAsync: false, + isGenerator: false, + usesArguments: false, + argPos: null, + ownerClass: null, + inPackage: isPackageJsFile(sf), + }; + this.functions.set(`|${this.nodeKey(sf)}`, info); + this.currentFn = info; + this.stmts(list(sf.statements)); + this.currentFn = null; + } + + stmts(stmts: readonly Node[]): void { + for (const s of stmts) this.stmt(s); + } + + stmt(s: Node): void { + if (ts.isVariableStatement(s)) { + for (const d of list(s.declarationList.declarations)) this.varDecl(d); + return; + } + if (ts.isExpressionStatement(s)) { + this.expr(s.expression); + return; + } + if (ts.isReturnStatement(s)) { + if (s.expression !== undefined && this.currentFn !== null) + this.edge(this.expr(s.expression), this.currentFn.ret); + return; + } + if (ts.isFunctionDeclaration(s)) { + const b = this.binding(s); + const saved = this.currentFn; + this.currentFn = null; + this.add(b, this.fnAtom(s, null)); + this.currentFn = saved; + return; + } + if (ts.isClassDeclaration(s)) { + const b = this.binding(s); + this.add(b, this.classAtoms(s).cls); + return; + } + if (isLoop(s)) { + if (ts.isForOfStatement(s) || ts.isForInStatement(s)) { + const src = this.expr(s.expression); + const target = this.newNode(); + if (ts.isForInStatement(s)) this.add(target, this.prim("string")); + else this.iterateInto(src, target, s); + const init = s.initializer; + if (ts.isVariableDeclarationList(init)) { + for (const d of list(init.declarations)) { + this.edge(target, this.binding(d)); + this.bindPattern(d.name, this.binding(d)); + } + } else this.assignTo(init as ts.Expression, target); + } + this.loopDepth++; + if (ts.isForStatement(s)) { + if (s.initializer !== undefined) { + if (ts.isVariableDeclarationList(s.initializer)) + for (const d of list(s.initializer.declarations)) this.varDecl(d); + else this.expr(s.initializer as ts.Expression); + } + if (s.condition !== undefined) this.expr(s.condition); + if (s.incrementor !== undefined) this.expr(s.incrementor); + } + if (ts.isWhileStatement(s) || ts.isDoStatement(s)) this.expr(s.expression); + const body = (s as { statement?: Node }).statement; + if (body !== undefined) this.stmt(body); + this.loopDepth--; + return; + } + if (ts.isTryStatement(s)) { + this.stmts(list(s.tryBlock.statements)); + if (s.catchClause !== undefined) { + const v = s.catchClause.variableDeclaration; + if (v !== undefined) { + this.add(this.binding(v), this.top); + this.bindPattern(v.name, this.binding(v)); + } + this.stmts(list(s.catchClause.block.statements)); + } + if (s.finallyBlock !== undefined) this.stmts(list(s.finallyBlock.statements)); + return; + } + if (ts.isThrowStatement(s)) { + this.sink(this.expr(s.expression), `thrown at ${locLabel(s)}`); + return; + } + if ( + ts.isImportDeclaration(s) || + ts.isExportDeclaration(s) || + ts.isInterfaceDeclaration(s) || + ts.isTypeAliasDeclaration(s) + ) + return; + if (ts.isExportAssignment(s)) { + this.expr(s.expression); + return; + } + // Blocks, ifs, switches, labels: walk children, evaluating expressions. + ts.forEachChild(s, (child) => { + if (ts.isExpression(child)) this.expr(child); + else this.stmt(child); + return undefined; + }); + } + + varDecl(d: ts.VariableDeclaration): void { + const b = this.binding(d); + if (d.initializer !== undefined) this.edge(this.expr(d.initializer), b); + this.bindPattern(d.name, b); + } + + /** Destructuring: binding elements read from the source node. */ + bindPattern(name: Node, src: number): void { + if (ts.isIdentifier(name)) return; + if (ts.isObjectBindingPattern(name)) { + for (const el of list(name.elements)) { + const b = this.binding(el); + if (el.dotDotDotToken !== undefined) { + this.add(b, this.top); + continue; + } + const keyNode = el.propertyName ?? el.name; + const key = keyNode !== undefined ? propertyNameText(keyNode) : null; + if (key !== null) this.loadConstraint(src, key, b, el); + else this.add(b, this.top); + if (el.initializer !== undefined) this.edge(this.expr(el.initializer), b); + if (el.name !== undefined) this.bindPattern(el.name, b); + } + return; + } + if (ts.isArrayBindingPattern(name)) { + let i = 0; + for (const el of list(name.elements)) { + if (ts.isBindingElement(el)) { + const b = this.binding(el); + if (el.dotDotDotToken !== undefined) this.add(b, this.top); + else this.iterateInto(src, b, el, i); + if (el.initializer !== undefined) this.edge(this.expr(el.initializer), b); + if (el.name !== undefined) this.bindPattern(el.name, b); + } + i++; + } + } + } + + /** Element values of an iterable. */ + iterateInto(src: number, dst: number, site: Node, _index?: number): void { + this.on(src, (a) => { + if (a.kind === "array" || a.kind === "collection") this.edge(this.element(a), dst); + else if (a.kind === "prim" && a.label === "string") this.add(dst, this.prim("string")); + else if (a.kind === "prim" || a.kind === "object" || a.kind === "dictionary") { + /* not iterable (a TypeError): no value. Literals with a + Symbol.iterator member have a computed key; they are G sites. */ + } else { + this.add(dst, this.top); + if (a.kind !== "top") this.escape(a, `iterated at ${locLabel(site)}`); + } + }); + } + + // ---- expressions --------------------------------------------------------------- + + expr(e: ts.Expression): number { + const key = this.ctxKey(e); + const memo = this.exprMemo.get(key); + if (memo !== undefined) return memo; + const id = this.exprUncached(e); + this.exprMemo.set(key, id); + if (this.traceTop && !this.nodeLabel.has(id)) + this.nodeLabel.set( + id, + `expr ${e.getText().slice(0, 40).replace(/\s+/g, " ")}@${locLabel(e)}`, + ); + return id; + } + + private exprUncached(e: ts.Expression): number { + const u = unparen(e); + if (u !== e) return this.expr(u); + switch (e.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: + case ts.SyntaxKind.TemplateExpression: + if (ts.isTemplateExpression(e)) + for (const span of list(e.templateSpans)) this.expr(span.expression); + return this.constNode(this.prim("string")); + case ts.SyntaxKind.NumericLiteral: + return this.constNode(this.prim("number")); + case ts.SyntaxKind.BigIntLiteral: + return this.constNode(this.prim("bigint")); + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + return this.constNode(this.prim("boolean")); + case ts.SyntaxKind.NullKeyword: + return this.constNode(this.prim("null")); + case ts.SyntaxKind.RegularExpressionLiteral: + return this.constNode(this.builtin("RegExp.instance")); + case ts.SyntaxKind.ThisKeyword: + return this.currentFn !== null + ? this.currentFn.thisNode + : this.constNode(this.prim("undefined")); + case ts.SyntaxKind.SuperKeyword: + return this.constNode(this.top); + } + if (ts.isIdentifier(e)) return this.identifier(e); + if (ts.isObjectLiteralExpression(e)) return this.objectLiteral(e); + if (ts.isArrayLiteralExpression(e)) return this.arrayLiteral(e); + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { + const n = this.newNode(); + this.add(n, this.fnAtom(e, null)); + if (ts.isFunctionExpression(e) && e.name !== undefined) + this.add(this.binding(e), this.fnAtom(e, null)); + return n; + } + if (ts.isClassExpression(e)) { + const n = this.newNode(); + this.add(n, this.classAtoms(e).cls); + return n; + } + if (ts.isPropertyAccessExpression(e)) { + this.noteSite(e); + const recv = this.expr(e.expression); + const name = e.name.text; + const dst = this.newNode(); + this.loadConstraint(recv, name, dst, e); + this.ops.push({ kind: "read", recv, name, node: e }); + return dst; + } + if (ts.isElementAccessExpression(e)) { + const recv = this.expr(e.expression); + const key = unparen(e.argumentExpression); + const dst = this.newNode(); + const name = + ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key) ? key.text : null; + if (name !== null) { + this.loadConstraint(recv, name, dst, e); + this.ops.push({ kind: "read", recv, name, node: e }); + } else { + this.expr(e.argumentExpression); + this.computedLoad(recv, dst, e); + this.ops.push({ kind: "computedRead", recv, name: null, node: e }); + } + return dst; + } + if (ts.isCallExpression(e)) return this.call(e); + if (ts.isNewExpression(e)) return this.newExpr(e); + if (ts.isBinaryExpression(e)) return this.binary(e); + if (ts.isConditionalExpression(e)) { + this.expr(e.condition); + const n = this.newNode(); + this.edge(this.expr(e.whenTrue), n); + this.edge(this.expr(e.whenFalse), n); + return n; + } + if (ts.isAwaitExpression(e)) { + const n = this.newNode(); + this.awaitInto(this.expr(e.expression), n); + return n; + } + if (ts.isTypeOfExpression(e)) { + this.expr(e.expression); + return this.constNode(this.prim("string")); + } + if (ts.isVoidExpression(e)) { + this.expr(e.expression); + return this.constNode(this.prim("undefined")); + } + if (ts.isDeleteExpression(e)) { + const target = unparen(e.expression); + if (ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) { + const recv = this.expr(target.expression); + const name = ts.isPropertyAccessExpression(target) ? target.name.text : null; + this.ops.push({ kind: "delete", recv, name, node: e }); + } + return this.constNode(this.prim("boolean")); + } + if (ts.isPrefixUnaryExpression(e) || ts.isPostfixUnaryExpression(e)) { + const operand = e.operand; + this.expr(operand); + if ( + e.operator === ts.SyntaxKind.PlusPlusToken || + e.operator === ts.SyntaxKind.MinusMinusToken + ) + this.assignTo(operand, this.constNode(this.prim("number"))); + if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken) + return this.constNode(this.prim("boolean")); + return this.constNode(this.prim("number")); + } + if (ts.isSpreadElement(e)) return this.expr(e.expression); + if (ts.isTaggedTemplateExpression(e)) { + this.expr(e.tag); + return this.constNode(this.top); + } + if (ts.isYieldExpression(e)) { + if (e.expression !== undefined) + this.sink(this.expr(e.expression), `yielded at ${locLabel(e)}`); + return this.constNode(this.top); + } + if (ts.isMetaProperty(e)) return this.constNode(this.top); + // Anything else: evaluate children, answer ⊤. + ts.forEachChild(e, (child) => { + if (ts.isExpression(child)) this.expr(child); + return undefined; + }); + return this.constNode(this.top); + } + + identifier(id: ts.Identifier): number { + if (id.text === "undefined") return this.constNode(this.prim("undefined")); + if (id.text === "arguments") { + const fn = this.enclosingNonArrow(); + if (fn !== null) { + const parent = id.parent; + const forwarding = parent !== undefined && ts.isSpreadElement(parent); + if (!forwarding) fn.usesArguments = true; + const arr = this.syntheticArray(id, "arguments"); + for (const p of fn.params) this.edge(p, this.element(arr)); + return this.constNode(arr); + } + } + const decl = this.resolve(id); + if (decl === undefined) return this.globalNode(id.text); + if (decl.getSourceFile().isDeclarationFile) return this.globalNode(id.text); + if (!this.files.includes(decl.getSourceFile())) return this.constNode(this.top); + if (ts.isFunctionDeclaration(decl)) { + const b = this.binding(decl); + const saved = this.currentFn; + this.currentFn = null; + this.add(b, this.fnAtom(decl, null)); + this.currentFn = saved; + return b; + } + if (ts.isClassDeclaration(decl) || ts.isClassExpression(decl)) { + const b = this.binding(decl); + this.add(b, this.classAtoms(decl).cls); + return b; + } + if (ts.isFunctionExpression(decl)) return this.binding(decl); + if ( + ts.isImportSpecifier(decl) || + ts.isImportClause(decl) || + ts.isNamespaceImport(decl) || + ts.isImportEqualsDeclaration(decl) + ) + return this.constNode(this.top); + if (ts.isSourceFile(decl) || ts.isModuleDeclaration(decl)) return this.namespaceNode(decl); + return this.narrowed(id, decl, this.binding(decl)); + } + + /** Flow narrowing at guarded references (S1 handles the guards hot code + * uses: `x instanceof C` and calls of predicate functions whose body is + * `… && x instanceof C`, in the true arm of an `if`, `?:` or `&&`). The + * narrowed value keeps the guard's class arms and ⊤. */ + narrowed(id: ts.Identifier, decl: Node, value: number): number { + let child: Node = id; + let p = id.parent; + while (p !== undefined && !ts.isFunctionLike(p) && !ts.isSourceFile(p)) { + let cond: ts.Expression | undefined; + if (ts.isIfStatement(p) && p.thenStatement === child) cond = p.expression; + else if (ts.isConditionalExpression(p) && p.whenTrue === child) cond = p.condition; + else if ( + ts.isBinaryExpression(p) && + p.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && + p.right === child + ) + cond = p.left; + if (cond !== undefined) { + const cls = this.guardClass(cond, decl); + if (cls !== null) { + const out = this.newNode(); + this.on(value, (a) => { + if (a.kind === "top") this.add(out, a); + else if ( + cls === "Promise" + ? a.kind === "promise" + : a.kind === "instance" && + a.node !== undefined && + this.classChain(a.node).includes(cls) + ) + this.add(out, a); + }); + return out; + } + } + child = p; + p = p.parent; + } + return value; + } + + /** The class a condition proves `decl`'s value an instance of, if any. */ + guardClass(cond: ts.Expression, decl: Node): Node | "Promise" | null { + const e = unparen(cond); + if (ts.isBinaryExpression(e)) { + if (e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) + return this.guardClass(e.left, decl) ?? this.guardClass(e.right, decl); + if (e.operatorToken.kind !== ts.SyntaxKind.InstanceOfKeyword) return null; + const l = unparen(e.left); + if (!ts.isIdentifier(l) || this.resolve(l) !== decl) return null; + return this.instanceofTarget(e.right); + } + if ( + ts.isCallExpression(e) && + ts.isIdentifier(unparen(e.expression)) && + e.arguments.nodes.length === 1 + ) { + const arg = unparen(e.arguments.nodes[0]!); + if (!ts.isIdentifier(arg) || this.resolve(arg) !== decl) return null; + const fnDecl = this.resolve(unparen(e.expression)); + let fn: Node | undefined = fnDecl; + if (fn !== undefined && ts.isVariableDeclaration(fn) && fn.initializer !== undefined) + fn = unparen(fn.initializer); + if ( + fn === undefined || + !(ts.isArrowFunction(fn) || ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn)) + ) + return null; + const param = fn.parameters.nodes[0]; + if (param === undefined || !ts.isIdentifier(param.name)) return null; + let body: Node | undefined = fn.body; + if (body !== undefined && ts.isBlock(body)) { + const stmts = body.statements.nodes; + body = + stmts.length === 1 && ts.isReturnStatement(stmts[0]!) ? stmts[0]!.expression : undefined; + } + if (body === undefined) return null; + return this.predicateClass(body as ts.Expression, param); + } + return null; + } + + /** `… && p instanceof C` in a predicate body over parameter `p`. */ + predicateClass(e: ts.Expression, param: Node): Node | "Promise" | null { + const u = unparen(e); + if (!ts.isBinaryExpression(u)) return null; + if (u.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) + return this.predicateClass(u.right, param) ?? this.predicateClass(u.left, param); + if (u.operatorToken.kind !== ts.SyntaxKind.InstanceOfKeyword) return null; + const l = unparen(u.left); + if (!ts.isIdentifier(l) || this.resolve(l) !== param) return null; + return this.instanceofTarget(u.right); + } + + instanceofTarget(right: ts.Expression): Node | "Promise" | null { + const r = unparen(right); + if (!ts.isIdentifier(r)) return null; + const d = this.resolve(r); + if (d === undefined || d.getSourceFile().isDeclarationFile) + return r.text === "Promise" ? "Promise" : null; + return ts.isClassLikeDeclaration(d) ? d : null; + } + + private readonly namespaces = new Map(); + /** `import * as ns`: an object whose fields are the module's exports. */ + namespaceNode(mod: Node): number { + const existing = this.namespaces.get(mod); + if (existing !== undefined) return existing; + const n = this.newNode(); + this.namespaces.set(mod, n); + const atom = this.atoms.intern(`ns:${this.nodeKey(mod)}`, () => ({ + kind: "object", + label: `namespace ${shortFile((mod as ts.SourceFile).fileName ?? "")}`, + node: mod, + })); + this.add(n, atom); + const sym = this.checker.getSymbolAtLocation(mod); + if (sym !== undefined) { + for (const ex of this.checker.getExportsOfModule(sym)) { + let target = ex; + if (target.flags & ts.SymbolFlags.Alias) { + try { + target = this.checker.getAliasedSymbol(target); + } catch { + continue; + } + } + const d = target.valueDeclaration ?? target.declarations[0]; + if (d === undefined) continue; + const value = this.declValue(d); + if (value !== null) this.edge(value, this.field(atom, String(ex.escapedName))); + } + } + return n; + } + + declValue(d: Node): number | null { + if (d.getSourceFile().isDeclarationFile || !this.files.includes(d.getSourceFile())) return null; + if (ts.isFunctionDeclaration(d)) { + const b = this.binding(d); + const saved = this.currentFn; + this.currentFn = null; + this.add(b, this.fnAtom(d, null)); + this.currentFn = saved; + return b; + } + if (ts.isClassDeclaration(d)) { + const b = this.binding(d); + this.add(b, this.classAtoms(d).cls); + return b; + } + if (ts.isVariableDeclaration(d) || ts.isBindingElement(d) || ts.isParameter(d)) + return this.binding(d); + if (ts.isSourceFile(d)) return this.namespaceNode(d); + return null; + } + + globalNode(name: string): number { + if (MODELED_GLOBALS.has(name)) return this.constNode(this.builtin(name)); + if (name === "NaN" || name === "Infinity") return this.constNode(this.prim("number")); + return this.constNode(this.top); + } + + enclosingNonArrow(): FunctionInfo | null { + const fn = this.currentFn; + if (fn === null) return null; + if (!fn.isArrow) return fn; + // An arrow's `arguments` is its enclosing function's. + let p: Node | undefined = fn.node.parent; + while (p !== undefined) { + if (ts.isFunctionLike(p) && !ts.isArrowFunction(p)) + return ( + this.functions.get(this.ctxKey(p)) ?? this.functions.get(`|${this.nodeKey(p)}`) ?? null + ); + p = p.parent; + } + return null; + } + + objectLiteral(e: ts.ObjectLiteralExpression): number { + const keys: string[] = []; + let keysKnown = true; + let hasAccessor = false; + let hasProto = false; + let hasComputedKey = false; + const spreads: number[] = []; + const inPackage = isPackageJsFile(e.getSourceFile()); + const label = (): string => + `{${keys.slice(0, 6).join(",")}${keys.length > 6 ? ",…" : ""}${keysKnown ? "" : ",…"}}@${locLabel(e)}`; + // keys first, so the label is complete + for (const p of list(e.properties)) { + if (ts.isSpreadAssignment(p)) { + keysKnown = false; + continue; + } + const name = (p as { name?: Node }).name; + const key = name !== undefined ? propertyNameText(name) : null; + if (key === null) { + hasComputedKey = true; + keysKnown = false; + continue; + } + if (key === "__proto__" && ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) + hasProto = true; + if (!keys.includes(key)) keys.push(key); + } + const atom = this.atoms.intern(`obj:${this.nodeKey(e)}`, () => ({ + kind: "object", + label: label(), + node: e, + })); + const n = this.newNode(); + this.add(n, atom); + for (const p of list(e.properties)) { + if (ts.isSpreadAssignment(p)) { + const src = this.expr(p.expression); + spreads.push(src); + this.copyFields(src, atom, p); + continue; + } + if (ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) { + hasAccessor = true; + const info = this.functionInfo(p, null); + this.activate(info); + this.add(info.thisNode, atom); + const key = propertyNameText(p.name); + if (key !== null && ts.isGetAccessorDeclaration(p)) + this.edge(info.ret, this.field(atom, key)); + continue; + } + const name = (p as { name?: Node }).name; + const key = name !== undefined ? propertyNameText(name) : null; + if (name !== undefined && ts.isComputedPropertyName(name)) this.expr(name.expression); + let value: number; + if (ts.isPropertyAssignment(p)) value = this.expr(p.initializer); + else if (ts.isShorthandPropertyAssignment(p)) { + const sym = this.checker.getShorthandAssignmentValueSymbol(p); + const d = sym?.valueDeclaration ?? sym?.declarations[0]; + const id = p.name as ts.Identifier; + value = d !== undefined ? (this.declValue(d) ?? this.identifier(id)) : this.identifier(id); + } else if (ts.isMethodDeclaration(p)) { + const fa = this.fnAtom(p, null); + this.add(this.infoOfAtom(fa)!.thisNode, atom); + value = this.constNode(fa); + } else continue; + if (key !== null) this.edge(value, this.field(atom, key)); + else this.edge(value, this.anyField(atom)); + } + if (!this.siteAtoms.has(atom) && this.siteAtoms.add(atom)) + this.sites.push({ + atom, + node: e, + kind: "object", + keys: keysKnown ? keys : null, + hasAccessor, + hasProto, + hasComputedKey, + spreads, + inPackage, + }); + return n; + } + + /** Values written under unknown keys. */ + anyField(atom: Atom): number { + return this.field(atom, "*"); + } + + copyFields(src: number, dstAtom: Atom, site: Node): void { + this.on(src, (a) => { + if (a.kind === "top") { + this.edge(this.constNode(this.top), this.anyField(dstAtom)); + return; + } + if (a.kind === "prim" || a.kind === "builtin") return; + if (a.kind === "array") { + this.edge(this.element(a), this.anyField(dstAtom)); + return; + } + // Copy every own field the source has, now and later. + const fm = this.fields.get(a); + const copied = new Set(); + const copy = (): void => { + for (const [name, id] of this.fields.get(a) ?? new Map()) { + if (copied.has(name)) continue; + copied.add(name); + this.edge(id, this.field(dstAtom, name)); + } + }; + copy(); + if (fm === undefined) this.fields.set(a, new Map()); + this.lateCopies.push(copy); + if (a.kind === "instance" || a.kind === "dictionary" || a.kind === "collection") + this.edge(this.constNode(this.top), this.anyField(dstAtom)); + void site; + }); + } + + /** Field copies re-run after solving rounds (fields created later). */ + private readonly lateCopies: (() => void)[] = []; + + arrayLiteral(e: ts.ArrayLiteralExpression): number { + const atom = this.atoms.intern(`arr:${this.nodeKey(e)}`, () => ({ + kind: "array", + label: `[]@${locLabel(e)}`, + node: e, + })); + const n = this.newNode(); + this.add(n, atom); + const el = this.element(atom); + let hasHole = false; + for (const item of list(e.elements)) { + if (ts.isOmittedExpression(item)) { + hasHole = true; + continue; + } + if (ts.isSpreadElement(item)) this.iterateInto(this.expr(item.expression), el, item); + else this.edge(this.expr(item), el); + } + if (!this.siteAtoms.has(atom) && this.siteAtoms.add(atom)) + this.sites.push({ + atom, + node: e, + kind: "array", + keys: null, + hasAccessor: false, + hasProto: hasHole, + hasComputedKey: false, + spreads: [], + inPackage: isPackageJsFile(e.getSourceFile()), + }); + return n; + } + + binary(e: ts.BinaryExpression): number { + const op = e.operatorToken.kind; + if (op === ts.SyntaxKind.EqualsToken) { + const value = this.expr(e.right); + this.assignTo(e.left, value); + return value; + } + if ( + op === ts.SyntaxKind.BarBarEqualsToken || + op === ts.SyntaxKind.QuestionQuestionEqualsToken || + op === ts.SyntaxKind.AmpersandAmpersandEqualsToken + ) { + const n = this.newNode(); + this.edge(this.expr(e.left), n); + const value = this.expr(e.right); + this.edge(value, n); + this.assignTo(e.left, value); + return n; + } + if (op >= ts.SyntaxKind.FirstCompoundAssignment && op <= ts.SyntaxKind.LastCompoundAssignment) { + this.expr(e.left); + this.expr(e.right); + const result = this.constNode( + op === ts.SyntaxKind.PlusEqualsToken ? this.top : this.prim("number"), + ); + if (op === ts.SyntaxKind.PlusEqualsToken) { + this.nodes[result]!.atoms.clear(); + this.add(result, this.prim("string")); + this.add(result, this.prim("number")); + } + this.assignTo(e.left, result); + return result; + } + if ( + op === ts.SyntaxKind.BarBarToken || + op === ts.SyntaxKind.QuestionQuestionToken || + op === ts.SyntaxKind.AmpersandAmpersandToken + ) { + const n = this.newNode(); + this.edge(this.expr(e.left), n); + this.edge(this.expr(e.right), n); + return n; + } + if (op === ts.SyntaxKind.CommaToken) { + this.expr(e.left); + return this.expr(e.right); + } + const l = this.expr(e.left); + const r = this.expr(e.right); + switch (op) { + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + case ts.SyntaxKind.LessThanToken: + case ts.SyntaxKind.LessThanEqualsToken: + case ts.SyntaxKind.GreaterThanToken: + case ts.SyntaxKind.GreaterThanEqualsToken: + case ts.SyntaxKind.InstanceOfKeyword: + case ts.SyntaxKind.InKeyword: + if (op === ts.SyntaxKind.InKeyword) + this.ops.push({ kind: "computedRead", recv: r, name: null, node: e }); + return this.constNode(this.prim("boolean")); + case ts.SyntaxKind.PlusToken: { + const n = this.newNode(); + const strOrNum = (a: Atom): void => { + this.add( + n, + a.kind === "prim" && a.label === "number" ? this.prim("number") : this.prim("string"), + ); + if ( + a.kind === "prim" && + (a.label === "boolean" || a.label === "null" || a.label === "undefined") + ) + this.add(n, this.prim("number")); + if (a.kind === "top") this.add(n, this.prim("number")); + }; + this.on(l, strOrNum); + this.on(r, strOrNum); + return n; + } + default: + return this.constNode(this.prim("number")); + } + } + + assignTo(target: ts.Expression, value: number): void { + const t = unparen(target); + if (ts.isIdentifier(t)) { + const decl = this.resolve(t); + if (decl === undefined || decl.getSourceFile().isDeclarationFile) { + this.sink(value, `global write at ${locLabel(t)}`); + return; + } + this.edge(value, this.binding(decl)); + return; + } + if (ts.isPropertyAccessExpression(t)) { + const recv = this.expr(t.expression); + this.storeConstraint(recv, t.name.text, value, t); + this.ops.push({ + kind: t.name.text === "length" ? "lengthWrite" : "write", + recv, + name: t.name.text, + node: t, + }); + return; + } + if (ts.isElementAccessExpression(t)) { + const recv = this.expr(t.expression); + const key = unparen(t.argumentExpression); + if (ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) { + this.storeConstraint(recv, key.text, value, t); + this.ops.push({ kind: "write", recv, name: key.text, node: t }); + } else { + this.expr(t.argumentExpression); + this.computedStore(recv, value, t); + this.ops.push({ kind: "computedWrite", recv, name: null, node: t }); + } + return; + } + if (ts.isObjectLiteralExpression(t)) { + for (const p of list(t.properties)) { + if (ts.isPropertyAssignment(p)) { + const key = propertyNameText(p.name); + const n = this.newNode(); + if (key !== null) this.loadConstraint(value, key, n, p); + else this.add(n, this.top); + this.assignTo(p.initializer, n); + } else if (ts.isShorthandPropertyAssignment(p)) { + const n = this.newNode(); + const id = p.name as ts.Identifier; + this.loadConstraint(value, id.text, n, p); + this.assignTo(id, n); + } + } + return; + } + if (ts.isArrayLiteralExpression(t)) { + for (const el of list(t.elements)) { + if (ts.isOmittedExpression(el)) continue; + const n = this.newNode(); + this.iterateInto(value, n, el); + this.assignTo(ts.isSpreadElement(el) ? el.expression : el, n); + } + } + } + + // ---- loads and stores ------------------------------------------------------------ + + loadConstraint(recv: number, name: string, dst: number, site: Node): void { + this.on(recv, (a) => this.loadAtom(a, name, dst, site)); + } + + loadAtom(a: Atom, name: string, dst: number, site: Node): void { + switch (a.kind) { + case "top": + this.add(dst, this.top); + return; + case "prim": + if (name === "length" && a.label === "string") this.add(dst, this.prim("number")); + else if (a.label === "string" || a.label === "number" || a.label === "boolean") + this.add(dst, this.builtin(`${a.label}.${name}`, a)); + return; + case "instance": { + const decl = a.node!; + if (name === "constructor") { + this.add(dst, this.classAtoms(decl).cls); + return; + } + const member = this.lookupMember(decl, name, false); + if (member?.kind === "method") + this.add( + dst, + this.fnAtom(member.method!, this.ownerOf(member.method!), this.receiverCtx(a)), + ); + if (member !== null && member.get !== undefined) { + const g = this.functionInfo(member.get, this.ownerOf(member.get), this.receiverCtx(a)); + this.activate(g); + this.add(g.thisNode, a); + this.edge(g.ret, dst); + this.edgeFor(site, g); + return; + } + if (this.classChain(decl).some((c) => this.baseClass(c) === undefined)) + this.add(dst, this.top); + this.edge(this.field(a, name), dst); + return; + } + case "class": { + const decl = a.node!; + if (name === "prototype") { + this.add(dst, this.top); + return; + } + if (name === "name") { + this.add(dst, this.prim("string")); + return; + } + const member = this.lookupMember(decl, name, true); + if (member?.kind === "method") + this.add( + dst, + this.fnAtom(member.method!, this.ownerOf(member.method!), this.receiverCtx(a)), + ); + if (member?.get !== undefined) { + const g = this.functionInfo(member.get, this.ownerOf(member.get), this.receiverCtx(a)); + this.activate(g); + this.add(g.thisNode, a); + this.edge(g.ret, dst); + return; + } + // static fields of the class and its bases + for (const c of this.classChain(decl)) { + const cls = this.classAtoms(c).cls; + this.edge(this.field(cls, name), dst); + } + return; + } + case "function": + if (name === "call" || name === "apply" || name === "bind") + this.add(dst, this.builtin(`function.${name}`, a)); + else if (name === "prototype") this.add(dst, this.top); + else if (name === "name") this.add(dst, this.prim("string")); + else if (name === "length") this.add(dst, this.prim("number")); + else this.edge(this.field(a, name), dst); + return; + case "array": + if (name === "length") this.add(dst, this.prim("number")); + else if (/^\d+$/.test(name)) this.edge(this.element(a), dst); + else if (ARRAY_METHODS.has(name)) this.add(dst, this.builtin(`array.${name}`, a)); + else this.edge(this.field(a, name), dst); + return; + case "object": + this.edge(this.field(a, name), dst); + this.edge(this.anyField(a), dst); + if (name === "constructor" || name === "hasOwnProperty" || name === "toString") + this.add(dst, this.builtin(`object.${name}`, a)); + return; + case "dictionary": + this.edge(this.anyField(a), dst); + this.edge(this.field(a, name), dst); + return; + case "promise": + if (name === "then" || name === "catch" || name === "finally") + this.add(dst, this.builtin(`promise.${name}`, a)); + return; + case "collection": + if (name === "size") this.add(dst, this.prim("number")); + else this.add(dst, this.builtin(`collection.${name}`, a)); + return; + case "builtin": + this.loadBuiltin(a, name, dst); + return; + } + void site; + } + + ownerOf(member: Node): Node | null { + const p = member.parent; + return p !== undefined && ts.isClassLikeDeclaration(p) ? p : null; + } + + loadBuiltin(a: Atom, name: string, dst: number): void { + const path = a.path ?? ""; + if ( + path === "Number" && + (name === "MAX_SAFE_INTEGER" || + name === "MIN_SAFE_INTEGER" || + name === "EPSILON" || + name === "MAX_VALUE" || + name === "MIN_VALUE" || + name === "POSITIVE_INFINITY" || + name === "NEGATIVE_INFINITY" || + name === "NaN") + ) + this.add(dst, this.prim("number")); + else if (path === "Math" && /^[A-Z0-9_]+$/.test(name)) this.add(dst, this.prim("number")); + else if (path === "Symbol") this.add(dst, this.prim("symbol")); + else if (path === "Error.instance" || path === "error") { + if (name === "message" || name === "stack" || name === "name") + this.add(dst, this.prim("string")); + else this.add(dst, this.top); + } else if ( + a.receiver === undefined && + MODELED_GLOBALS.has(path.split(".")[0]!) && + path.split(".").length < 4 + ) + this.add(dst, this.builtin(`${path}.${name}`)); + else if (path === "RegExp.instance") this.add(dst, this.builtin(`regexp.${name}`, a)); + else this.add(dst, this.top); + } + + storeConstraint(recv: number, name: string, value: number, site: Node): void { + this.on(recv, (a) => { + switch (a.kind) { + case "top": + this.noteTopWrite(name, site); + this.sink(value, `stored into ⊤.${name} at ${locLabel(site)}`); + return; + case "instance": { + const member = this.lookupMember(a.node!, name, false); + if (member?.set !== undefined) { + const s = this.functionInfo(member.set, this.ownerOf(member.set), this.receiverCtx(a)); + this.activate(s); + this.add(s.thisNode, a); + if (s.params[0] !== undefined) this.edge(value, s.params[0]); + return; + } + this.edge(value, this.field(a, name)); + return; + } + case "class": + case "function": + case "object": + case "dictionary": + this.edge(value, this.field(a, name)); + return; + case "array": + if (/^\d+$/.test(name)) this.edge(value, this.element(a)); + return; + case "prim": + return; + default: + this.sink(value, `stored into ${a.label} at ${locLabel(site)}`); + } + }); + } + + computedLoad(recv: number, dst: number, site: Node): void { + this.on(recv, (a) => { + switch (a.kind) { + case "top": + this.add(dst, this.top); + return; + case "array": + case "collection": + this.edge(this.element(a), dst); + return; + case "prim": + if (a.label === "string") this.add(dst, this.prim("string")); + return; + case "object": + case "dictionary": + case "instance": + case "class": + case "function": { + // every field, now and later + const seen = new Set(); + const copy = (): void => { + for (const [name, id] of this.fields.get(a) ?? new Map()) { + if (seen.has(name)) continue; + seen.add(name); + this.edge(id, dst); + } + }; + copy(); + this.lateCopies.push(copy); + if (a.kind === "instance" || a.kind === "class") this.add(dst, this.top); + return; + } + default: + this.add(dst, this.top); + } + void site; + }); + } + + computedStore(recv: number, value: number, site: Node): void { + this.on(recv, (a) => { + switch (a.kind) { + case "top": + this.noteTopWrite(null, site); + this.sink(value, `stored into ⊤[k] at ${locLabel(site)}`); + return; + case "array": + this.edge(value, this.element(a)); + return; + case "prim": + return; + default: + this.edge(value, this.anyField(a)); + } + }); + } + + // ---- calls --------------------------------------------------------------------------- + + args(e: ts.CallExpression | ts.NewExpression): { args: number[]; spread: boolean } { + const out: number[] = []; + let spread = false; + for (const a of list(e.arguments)) { + if (ts.isSpreadElement(a)) { + const inner = unparen(a.expression); + if (ts.isIdentifier(inner) && inner.text === "arguments") { + // `f(...arguments)`: forward the enclosing function's parameters. + const fn = this.enclosingNonArrow(); + if (fn !== null && out.length === 0) { + if (fn.argPos === null) fn.argPos = [0, 1, 2, 3, 4, 5].map(() => this.newNode()); + out.push(...fn.argPos); + continue; + } + } + spread = true; + out.push(this.expr(a.expression)); + continue; + } + out.push(this.expr(a)); + } + return { args: out, spread }; + } + + call(e: ts.CallExpression): number { + this.noteSite(e); + const callee = unparen(e.expression); + const dst = this.newNode(); + if (callee.kind === ts.SyntaxKind.SuperKeyword) { + const { args, spread } = this.args(e); + const fn = this.enclosingNonArrow(); + const owner = fn?.ownerClass ?? null; + const base = owner !== null ? this.baseClass(owner) : undefined; + if (fn !== null && base) { + this.on(fn.thisNode, (inst) => { + if (inst.kind === "instance") this.callConstructorChain(base, inst, args, spread, e); + }); + } else if (base === undefined) + for (const a of args) this.sink(a, `super(...) at ${locLabel(e)}`); + return dst; + } + if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) { + const recvExpr = unparen(callee.expression); + const { args, spread } = this.args(e); + if (recvExpr.kind === ts.SyntaxKind.SuperKeyword && ts.isPropertyAccessExpression(callee)) { + // super.m(...): the base class member with the current receiver. + const fn = this.enclosingNonArrow(); + const owner = fn?.ownerClass ?? null; + const base = owner !== null ? this.baseClass(owner) : undefined; + if (fn !== null && base) { + const member = this.lookupMember(base, callee.name.text, isStatic(fn.node)); + if (member?.method !== undefined) { + this.callFunction( + this.functionInfo(member.method, this.ownerOf(member.method), fn.ctx), + fn.thisNode, + args, + spread, + dst, + e, + ); + } + } + return dst; + } + const recv = this.expr(recvExpr); + let name: string | null = null; + if (ts.isPropertyAccessExpression(callee)) name = callee.name.text; + else { + const k = unparen(callee.argumentExpression); + if (ts.isStringLiteral(k) || ts.isNoSubstitutionTemplateLiteral(k)) name = k.text; + else this.expr(callee.argumentExpression); + } + if (name !== null) this.ops.push({ kind: "read", recv, name, node: callee }); + // Per receiver atom: the member it resolves to runs with that atom as `this`. + const member = name; + this.on(recv, (a) => { + const fnNode = this.newNode(); + const self = this.constNode(a); + if (member !== null) this.loadAtom(a, member, fnNode, callee); + else this.computedLoad(self, fnNode, callee); + this.invoke(fnNode, self, args, spread, dst, e); + }); + return dst; + } + const calleeNode = this.expr(callee); + const { args, spread } = this.args(e); + this.invoke(calleeNode, null, args, spread, dst, e); + return dst; + } + + invoke( + callee: number, + recv: number | null, + args: number[], + spread: boolean, + dst: number, + site: ts.CallExpression, + ): void { + const undefNode = recv === null ? this.constNode(this.prim("undefined")) : recv; + this.on(callee, (a) => { + switch (a.kind) { + case "function": + this.callFunction(this.infoOfAtom(a)!, undefNode, args, spread, dst, site); + return; + case "builtin": + this.callBuiltin(a, args, spread, dst, site, recv); + return; + case "class": + return; // calling a class throws + case "prim": + return; + default: + for (const x of args) this.sink(x, `argument of ⊤ call at ${locLabel(site)}`); + if (recv !== null && a.kind === "top") + this.sink(recv, `receiver of ⊤ call at ${locLabel(site)}`); + this.add(dst, this.top); + } + }); + } + + newExpr(e: ts.NewExpression): number { + this.noteSite(e); + const dst = this.newNode(); + const callee = this.expr(e.expression); + const { args, spread } = this.args(e); + this.on(callee, (a) => { + switch (a.kind) { + case "class": + this.construct(a.node!, args, spread, dst, e); + return; + case "function": { + // An ES5 constructor function: one instance atom per function. + const inst = this.atoms.intern(`fninst:${this.nodeKey(a.node!)}`, () => ({ + kind: "object", + label: `new ${a.label}`, + node: a.node, + })); + this.add(dst, inst); + const info = this.infoOfAtom(a); + if (info !== undefined) { + this.activate(info); + this.add(info.thisNode, inst); + this.bindArgs(info, args, spread); + this.edge(info.ret, dst); + } + return; + } + case "builtin": + this.constructBuiltin(a.path ?? "", args, dst, e); + return; + default: + for (const x of args) this.sink(x, `argument of ⊤ constructor at ${locLabel(e)}`); + this.add(dst, this.top); + } + }); + return dst; + } + + constructBuiltin(path: string, args: number[], dst: number, site: Node): void { + switch (path) { + case "Map": + case "Set": + case "WeakMap": + case "WeakSet": { + const c = this.atoms.intern(`coll:${this.nodeKey(site)}`, () => ({ + kind: "collection", + label: `${path}@${locLabel(site)}`, + node: site, + })); + this.add(dst, c); + if (args[0] !== undefined) this.iterateInto(args[0], this.element(c), site); + return; + } + case "Error": + case "TypeError": + case "RangeError": + case "SyntaxError": + this.add(dst, this.builtin("Error.instance")); + return; + case "Promise": { + const p = this.promiseAtom(site, locLabel(site)); + this.add(this.field(p, "value"), this.top); + this.add(dst, p); + if (args[0] !== undefined) { + const resolver = this.constNode(this.builtin("promise.resolver")); + this.on(args[0], (f) => { + if (f.kind === "function") + this.callFunction( + this.infoOfAtom(f)!, + null, + [resolver, resolver], + false, + this.newNode(), + site, + ); + }); + } + return; + } + case "Date": + this.add(dst, this.builtin("Date.instance")); + return; + case "RegExp": + this.add(dst, this.builtin("RegExp.instance")); + return; + case "Array": { + this.add(dst, this.syntheticArray(site, "Array")); + return; + } + default: + for (const x of args) this.sink(x, `argument of new ${path} at ${locLabel(site)}`); + this.add(dst, this.top); + } + } + + /** Builtin call summaries; anything unmodeled makes its arguments escape. */ + callBuiltin( + a: Atom, + args: number[], + spread: boolean, + dst: number, + site: ts.CallExpression, + recvNode: number | null, + ): void { + const path = a.path ?? ""; + const recv = a.receiver; + const arg = (i: number): number => args[i] ?? this.constNode(this.prim("undefined")); + const prim = (p: string): void => this.add(dst, this.prim(p)); + const callback = (i: number, cbArgs: number[], ret: number | null): void => { + if (args[i] === undefined) return; + this.on(args[i]!, (f) => { + if (f.kind === "function") { + const saved = this.forceLoop; + this.forceLoop = LOOP_CALLBACK_METHODS.has(path.slice(path.indexOf(".") + 1)); + this.callFunction(this.infoOfAtom(f)!, null, cbArgs, false, ret ?? this.newNode(), site); + this.forceLoop = saved; + } else if (f.kind === "top" || f.kind === "builtin") { + for (const x of cbArgs) this.sink(x, `argument of ⊤ callback at ${locLabel(site)}`); + if (ret !== null) this.add(ret, this.top); + } + }); + }; + const escapeAll = (): void => { + for (const x of args) this.sink(x, `argument of ${path} at ${locLabel(site)}`); + this.add(dst, this.top); + }; + // Methods of builtin receivers. + if (recv !== undefined) { + const method = path.slice(path.indexOf(".") + 1); + if (path.startsWith("array.")) { + const el = this.element(recv); + const num = this.constNode(this.prim("number")); + const recvN = this.constNode(recv); + switch (method) { + case "push": + case "unshift": + for (const x of args) { + if (spread) this.iterateInto(x, el, site); + else this.edge(x, el); + } + prim("number"); + return; + case "pop": + case "shift": + case "at": + this.edge(el, dst); + this.add(dst, this.prim("undefined")); + return; + case "find": + case "findLast": + callback(0, [el, num, recvN], null); + this.edge(el, dst); + this.add(dst, this.prim("undefined")); + return; + case "forEach": + callback(0, [el, num, recvN], null); + prim("undefined"); + return; + case "some": + case "every": + case "includes": + callback(0, [el, num, recvN], null); + prim("boolean"); + return; + case "findIndex": + case "findLastIndex": + case "indexOf": + case "lastIndexOf": + callback(0, [el, num, recvN], null); + prim("number"); + return; + case "join": + case "toString": + prim("string"); + return; + case "map": + case "flatMap": { + const out = this.syntheticArray(site, method); + const outEl = this.element(out); + if (method === "map") callback(0, [el, num, recvN], outEl); + else { + const r = this.newNode(); + callback(0, [el, num, recvN], r); + this.iterateInto(r, outEl, site); + } + this.add(dst, out); + return; + } + case "filter": + case "slice": + case "concat": + case "reverse": + case "sort": + case "splice": + case "toSorted": + case "toReversed": + case "flat": { + if (method === "filter") callback(0, [el, num, recvN], null); + if (method === "sort" || method === "toSorted") callback(0, [el, el], null); + if (method === "reverse" || method === "sort") { + this.add(dst, recv); + return; + } + const out = this.syntheticArray(site, method); + this.edge(el, this.element(out)); + if (method === "concat") + for (const x of args) this.iterateInto(x, this.element(out), site); + if (method === "splice") for (const x of args.slice(2)) this.edge(x, el); + this.add(dst, out); + return; + } + case "reduce": + case "reduceRight": { + const acc = this.newNode(); + if (args[1] !== undefined) this.edge(args[1], acc); + else this.edge(el, acc); + callback(0, [acc, el, num, recvN], acc); + this.edge(acc, dst); + return; + } + case "keys": + case "values": + case "entries": { + const out = this.syntheticArray(site, method); + if (method === "values") this.edge(el, this.element(out)); + else if (method === "keys") this.add(this.element(out), this.prim("number")); + else { + const pair = this.syntheticArray(site, "entry"); + this.add(this.element(pair), this.prim("number")); + this.edge(el, this.element(pair)); + this.add(this.element(out), pair); + } + this.add(dst, out); + return; + } + case "fill": + for (const x of args) this.edge(x, el); + this.add(dst, recv); + return; + } + escapeAll(); + this.escape(recv, `array.${method} at ${locLabel(site)}`); + return; + } + if (path.startsWith("string.") || path.startsWith("number.") || path.startsWith("boolean.")) { + const r = STRING_METHOD_RESULTS.get(method); + if (r === "array") { + const out = this.syntheticArray(site, method); + this.add(this.element(out), this.prim("string")); + this.add(this.element(out), this.prim("undefined")); + this.add(dst, out); + this.add(dst, this.prim("null")); + } else if (r !== undefined) prim(r); + else if (method === "replace" || method === "replaceAll") prim("string"); + else escapeAll(); + if (method === "replace" || method === "replaceAll") + callback(1, [this.constNode(this.prim("string"))], null); + return; + } + if (path.startsWith("regexp.")) { + if (method === "test") prim("boolean"); + else if (method === "exec") { + const out = this.syntheticArray(site, "exec"); + this.add(this.element(out), this.prim("string")); + this.add(dst, out); + prim("null"); + } else escapeAll(); + return; + } + if (path.startsWith("promise.")) { + const out = this.promiseAtom(site, locLabel(site)); + const value = this.field(recv, "value"); + const ret = this.newNode(); + if (method === "then") { + callback(0, [value], ret); + callback(1, [this.constNode(this.top)], ret); + } else if (method === "catch") { + callback(0, [this.constNode(this.top)], ret); + this.edge(value, ret); + } else { + callback(0, [], null); + this.edge(value, ret); + } + this.awaitInto(ret, this.field(out, "value")); + this.add(dst, out); + return; + } + if (path.startsWith("collection.")) { + const el = this.element(recv); + switch (method) { + case "add": + case "set": + for (const x of args) this.edge(x, el); + this.add(dst, recv); + return; + case "get": + this.edge(el, dst); + prim("undefined"); + return; + case "has": + case "delete": + prim("boolean"); + return; + case "clear": + prim("undefined"); + return; + case "forEach": + callback(0, [el, el], null); + prim("undefined"); + return; + case "keys": + case "values": + case "entries": { + const out = this.syntheticArray(site, method); + this.edge(el, this.element(out)); + if (method === "entries") { + const pair = this.syntheticArray(site, "entry"); + this.edge(el, this.element(pair)); + this.nodes[this.element(out)]!.atoms.clear(); + this.add(this.element(out), pair); + } + this.add(dst, out); + return; + } + } + escapeAll(); + return; + } + if (path.startsWith("function.")) { + if (recv.kind !== "function" || recv.node === undefined) { + escapeAll(); + return; + } + if (method === "bind") { + const info = this.infoOfAtom(recv); + if (info !== undefined && args[0] !== undefined && !info.isArrow) + this.edge(args[0], info.thisNode); + if (info !== undefined && args.length > 1) { + this.activate(info); + for (const p of info.params) this.add(p, this.top); + for (const x of args.slice(1)) this.sink(x, `bound argument at ${locLabel(site)}`); + } + this.add(dst, recv); + return; + } + if (method === "call") { + this.callFunction( + this.infoOfAtom(recv)!, + args[0] ?? this.constNode(this.prim("undefined")), + args.slice(1), + spread, + dst, + site, + ); + return; + } + // apply: arguments from an array + const info = this.infoOfAtom(recv); + if (info !== undefined) { + this.activate(info); + if (args[0] !== undefined && !info.isArrow) this.edge(args[0], info.thisNode); + for (const p of info.params) + if (args[1] !== undefined) this.iterateInto(args[1], p, site); + this.edge(info.ret, dst); + } + return; + } + if (path.startsWith("object.")) { + if (method === "hasOwnProperty") prim("boolean"); + else if (method === "toString") prim("string"); + else escapeAll(); + return; + } + escapeAll(); + return; + } + // `Object.prototype.m.call(o, …)`-style calls of builtin prototype methods. + if (/^(Object|Array|String|Function)\.prototype\.\w+\.(call|apply)$/.test(path)) { + const m = path.split(".")[2]!; + if (m === "hasOwnProperty" || m === "propertyIsEnumerable" || m === "isPrototypeOf") + prim("boolean"); + else if (m === "toString") prim("string"); + else escapeAll(); + return; + } + switch (path) { + case "Object.keys": + case "Object.getOwnPropertyNames": { + const out = this.syntheticArray(site, "keys"); + this.add(this.element(out), this.prim("string")); + this.add(dst, out); + return; + } + case "Object.values": + case "Object.entries": { + const out = this.syntheticArray(site, path === "Object.values" ? "values" : "entries"); + const vals = this.newNode(); + if (args[0] !== undefined) this.computedLoad(args[0], vals, site); + if (path === "Object.values") this.edge(vals, this.element(out)); + else { + const pair = this.syntheticArray(site, "entry"); + this.add(this.element(pair), this.prim("string")); + this.edge(vals, this.element(pair)); + this.add(this.element(out), pair); + } + this.add(dst, out); + return; + } + case "Object.freeze": + case "Object.seal": + case "Object.preventExtensions": + this.ops.push({ + kind: path === "Object.freeze" ? "freeze" : "seal", + recv: arg(0), + name: null, + node: site, + }); + this.edge(arg(0), dst); + return; + case "Object.isFrozen": + case "Object.isSealed": + case "Object.isExtensible": + case "Array.isArray": + case "Number.isInteger": + case "Number.isNaN": + case "Number.isFinite": + case "Number.isSafeInteger": + case "isNaN": + case "isFinite": + prim("boolean"); + return; + case "Object.assign": { + this.ops.push({ kind: "assign", recv: arg(0), name: null, node: site }); + this.on(arg(0), (t) => { + for (const s of args.slice(1)) this.copyFields(s, t, site); + if (t.kind === "top") + for (const s of args.slice(1)) + this.sink(s, `Object.assign into ⊤ at ${locLabel(site)}`); + }); + this.edge(arg(0), dst); + return; + } + case "Object.defineProperty": + case "Object.defineProperties": + case "Reflect.defineProperty": + this.ops.push({ kind: "defineProperty", recv: arg(0), name: null, node: site }); + this.sink(arg(2), `property descriptor at ${locLabel(site)}`); + this.sink(arg(1), `property descriptor at ${locLabel(site)}`); + this.edge(arg(0), dst); + return; + case "Object.setPrototypeOf": + case "Reflect.setPrototypeOf": + this.ops.push({ kind: "setPrototypeOf", recv: arg(0), name: null, node: site }); + this.sink(arg(1), `prototype at ${locLabel(site)}`); + this.edge(arg(0), dst); + return; + case "Object.create": { + const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({ + kind: "dictionary", + label: `Object.create@${locLabel(site)}`, + node: site, + })); + this.add(dst, d); + return; + } + case "Object.fromEntries": { + const d = this.atoms.intern(`dict:${this.nodeKey(site)}`, () => ({ + kind: "dictionary", + label: `fromEntries@${locLabel(site)}`, + node: site, + })); + const pairs = this.newNode(); + if (args[0] !== undefined) this.iterateInto(args[0], pairs, site); + const vals = this.newNode(); + this.iterateInto(pairs, vals, site); + this.edge(vals, this.anyField(d)); + this.add(dst, d); + return; + } + case "Object.getPrototypeOf": + case "Reflect.getPrototypeOf": + this.add(dst, this.top); + return; + case "Object.is": + prim("boolean"); + return; + case "Array.from": + case "Array.of": { + const out = this.syntheticArray(site, path); + if (path === "Array.of") for (const x of args) this.edge(x, this.element(out)); + else if (args[1] !== undefined) { + const els = this.newNode(); + if (args[0] !== undefined) this.iterateInto(args[0], els, site); + callback(1, [els, this.constNode(this.prim("number"))], this.element(out)); + } else if (args[0] !== undefined) this.iterateInto(args[0], this.element(out), site); + this.add(dst, out); + return; + } + case "JSON.parse": + this.add(dst, this.top); + return; + case "JSON.stringify": + prim("string"); + prim("undefined"); + return; + case "String": + case "encodeURIComponent": + case "decodeURIComponent": + case "encodeURI": + case "decodeURI": + prim("string"); + return; + case "Number": + case "parseInt": + case "parseFloat": + case "Number.parseInt": + case "Number.parseFloat": + prim("number"); + return; + case "Boolean": + prim("boolean"); + return; + case "Symbol": + case "Symbol.for": + prim("symbol"); + return; + case "BigInt": + prim("bigint"); + return; + case "Promise.resolve": { + const p = this.promiseAtom(site, locLabel(site)); + if (args[0] !== undefined) this.awaitInto(args[0], this.field(p, "value")); + else this.add(this.field(p, "value"), this.prim("undefined")); + this.add(dst, p); + return; + } + case "Promise.reject": { + const p = this.promiseAtom(site, locLabel(site)); + this.add(dst, p); + if (args[0] !== undefined) this.sink(args[0], `rejection at ${locLabel(site)}`); + return; + } + case "Promise.all": + case "Promise.allSettled": + case "Promise.race": + case "Promise.any": { + const p = this.promiseAtom(site, locLabel(site)); + const items = this.newNode(); + if (args[0] !== undefined) this.iterateInto(args[0], items, site); + if (path === "Promise.all") { + const out = this.syntheticArray(site, "all"); + this.awaitInto(items, this.element(out)); + this.add(this.field(p, "value"), out); + } else if (path === "Promise.race" || path === "Promise.any") + this.awaitInto(items, this.field(p, "value")); + else this.add(this.field(p, "value"), this.top); + this.add(dst, p); + return; + } + case "promise.resolver": + for (const x of args) this.sink(x, `promise resolution at ${locLabel(site)}`); + return; + } + if (path.startsWith("console.")) { + prim("undefined"); + return; + } + if (path.startsWith("Math.")) { + prim("number"); + return; + } + if (path === "Date.now") { + prim("number"); + return; + } + if (path.startsWith("Date.instance") || path === "Error.instance") { + escapeAll(); + return; + } + void recvNode; + escapeAll(); + } + + /** Re-run deferred field copies until nothing new flows. */ + finish(): void { + for (;;) { + const before = this.steps; + for (const c of this.lateCopies) c(); + this.solve(); + if (this.steps === before || this.budgetExceeded) return; + } + } +} diff --git a/packages/compiler/src/frontend/shape-inference/domain.ts b/packages/compiler/src/frontend/shape-inference/domain.ts new file mode 100644 index 000000000..36978d4c9 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/domain.ts @@ -0,0 +1,153 @@ +/* Shape inference (results/design-shape-inference/DESIGN.md), stage S1: + * the abstract domain of the allocation-site points-to analysis. + * + * An abstract value is a set of atoms. An atom names a set of runtime + * values: a primitive kind, an allocation site (object or array literal, or + * a value a builtin creates at one call site), the instances of one class, + * one class value, one function (closure site), a builtin object, or ⊤ — + * every value the analysis does not track. Value nodes hold atom sets and + * flow into each other along inclusion edges; the solver (analyze.ts) + * attaches load, store and call constraints that fire as atoms arrive. */ +import type * as ts from "../ts7/adapter.js"; + +export type AtomKind = + | "prim" + | "top" + | "object" + | "array" + | "instance" + | "class" + | "function" + | "builtin" + | "promise" + | "collection" + | "dictionary"; + +export interface Atom { + readonly id: number; + readonly kind: AtomKind; + /** Short display name, e.g. `string`, `ZodString`, `{status,value}@types.js:804`. */ + readonly label: string; + /** The syntax that creates the atom: the literal, class or function + * declaration, or the builtin call. Undefined for prims, ⊤ and builtins. */ + readonly node?: ts.Node | undefined; + /** builtin atoms: the dotted path (`Object.keys`, `array.push`). */ + readonly path?: string | undefined; + /** method atoms of builtin receivers: the receiver atom. */ + readonly receiver?: Atom | undefined; + /** function atoms: the analysis context of the function's clone (a + * method's receiver class), "" for the context-insensitive default. */ + readonly ctx?: string | undefined; +} + +export class AtomTable { + readonly atoms: Atom[] = []; + private readonly byKey = new Map(); + + intern(key: string, make: (id: number) => Omit): Atom { + let atom = this.byKey.get(key); + if (atom === undefined) { + const id = this.atoms.length; + atom = { id, ...make(id) }; + this.atoms.push(atom); + this.byKey.set(key, atom); + } + return atom; + } +} + +/** Object.prototype members: a read of one of these names on a plain + * object resolves on the prototype, never to an absent own key. */ +export const OBJECT_PROTOTYPE_MEMBERS: ReadonlySet = new Set([ + "constructor", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", + "toString", + "valueOf", + "__defineGetter__", + "__defineSetter__", + "__lookupGetter__", + "__lookupSetter__", + "__proto__", +]); + +/** Global names the analysis models as builtin objects. Any other global is ⊤. */ +export const MODELED_GLOBALS: ReadonlySet = new Set([ + "Object", + "Array", + "JSON", + "Math", + "Number", + "String", + "Boolean", + "Symbol", + "Promise", + "Map", + "Set", + "WeakMap", + "WeakSet", + "Error", + "TypeError", + "RangeError", + "SyntaxError", + "Date", + "RegExp", + "console", + "Reflect", + "parseInt", + "parseFloat", + "isNaN", + "isFinite", + "encodeURIComponent", + "decodeURIComponent", + "encodeURI", + "decodeURI", + "BigInt", +]); + +/** Results of string (and number) methods, by name. Anything else is ⊤. */ +type MethodResult = "string" | "number" | "boolean" | "array"; +export const STRING_METHOD_RESULTS: ReadonlyMap = new Map< + string, + MethodResult +>([ + ["at", "string"], + ["charAt", "string"], + ["charCodeAt", "number"], + ["codePointAt", "number"], + ["concat", "string"], + ["endsWith", "boolean"], + ["includes", "boolean"], + ["indexOf", "number"], + ["lastIndexOf", "number"], + ["localeCompare", "number"], + ["match", "array"], + ["matchAll", "array"], + ["normalize", "string"], + ["padEnd", "string"], + ["padStart", "string"], + ["repeat", "string"], + ["replace", "string"], + ["replaceAll", "string"], + ["search", "number"], + ["slice", "string"], + ["split", "array"], + ["startsWith", "boolean"], + ["substring", "string"], + ["substr", "string"], + ["toLowerCase", "string"], + ["toUpperCase", "string"], + ["toLocaleLowerCase", "string"], + ["toLocaleUpperCase", "string"], + ["toString", "string"], + ["trim", "string"], + ["trimEnd", "string"], + ["trimStart", "string"], + ["valueOf", "string"], + ["test", "boolean"], + ["toFixed", "string"], + ["toPrecision", "string"], + ["toExponential", "string"], +]); diff --git a/packages/compiler/src/frontend/shape-inference/index.ts b/packages/compiler/src/frontend/shape-inference/index.ts new file mode 100644 index 000000000..88c2100ee --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/index.ts @@ -0,0 +1,21 @@ +/* Shape inference for untyped npm JavaScript + * (results/design-shape-inference/DESIGN.md). Stage S1: report only. */ +import type * as ts from "../ts7/adapter.js"; +import { analyzeShapes } from "./analyze.js"; +import { buildShapeReport } from "./report.js"; + +export { analyzeShapes } from "./analyze.js"; +export { buildShapeReport } from "./report.js"; + +/** SCRIPTC_DEBUG_SHAPES: `1` reports package code in hot functions, + * `verbose` every package site, `all` user files too, any other value only + * files whose path contains it. Writes to stderr; changes nothing else. */ +export function reportShapes(program: ts.Program, filter: string): void { + const start = performance.now(); + const analysis = analyzeShapes(program, program.getTypeChecker()); + const report = buildShapeReport(analysis, filter); + const ms = (performance.now() - start).toFixed(0); + process.stderr.write( + `[shapes] ${ms} ms\n${report.lines.map((l) => `[shapes] ${l}`).join("\n")}\n`, + ); +} diff --git a/packages/compiler/src/frontend/shape-inference/report.ts b/packages/compiler/src/frontend/shape-inference/report.ts new file mode 100644 index 000000000..c18d9eb56 --- /dev/null +++ b/packages/compiler/src/frontend/shape-inference/report.ts @@ -0,0 +1,374 @@ +/* Shape inference stage S1: the plan the analysis would hand the lowering, + * as a report (DESIGN.md §4.5, §5.5). Nothing here changes compilation. + * + * Per allocation site of package code, the verdict: + * P local — a native record would be exact: no reflection, no added + * keys, no computed writes, and its objects never escape; + * P escaping — exact as a record behind identity-keeping capsules: it + * escapes, but the census leaves every later operation + * answerable (fields untyped code may write become dyn); + * G — keeps today's dynamic object, with the rule that decided. + * Plus class field types, the census, and the hot functions' value types. */ +import * as ts from "../ts7/adapter.js"; +import { OBJECT_PROTOTYPE_MEMBERS, type Atom } from "./domain.js"; +import { locLabel, type FunctionInfo, type LiteralSite, type ShapeAnalysis } from "./analyze.js"; + +export interface SiteVerdict { + readonly site: LiteralSite; + readonly label: string; + readonly verdict: "P-local" | "P-escaping" | "G"; + readonly reasons: string[]; + /** P-escaping: fields untyped code may write (they become dyn slots). */ + readonly writableByTop: string[]; + readonly freezable: boolean; + readonly brand: boolean; +} + +export interface ShapeReport { + readonly sites: SiteVerdict[]; + readonly hot: Set; + readonly lines: string[]; +} + +const MAX_ATOMS_SHOWN = 6; + +function fieldKeys(fields: ReadonlyMap | undefined): string[] { + return fields === undefined ? [] : [...fields.keys()]; +} + +function fieldEntries(fields: ReadonlyMap | undefined): [string, number][] { + return fields === undefined ? [] : [...fields.entries()]; +} + +export function describeValue(analysis: ShapeAnalysis, node: number): string { + const atoms = [...analysis.nodes[node]!.atoms]; + if (atoms.length === 0) return "∅"; + const labels = [...new Set(atoms.map((a) => a.label))]; + labels.sort((a, b) => (a === "⊤" ? 1 : b === "⊤" ? -1 : a.localeCompare(b))); + const shown = labels.slice(0, MAX_ATOMS_SHOWN).join(" | "); + if (labels.length <= MAX_ATOMS_SHOWN) return shown; + return `${shown} | +${labels.length - MAX_ATOMS_SHOWN}${labels.includes("⊤") ? " | ⊤" : ""}`; +} + +function atomsOf(analysis: ShapeAnalysis, node: number): Set { + return analysis.nodes[node]!.atoms; +} + +/** Functions reached from call sites in loops (or loop callbacks), and + * everything they call: the static hotness heuristic (DESIGN.md §8.1). */ +export function hotFunctions(analysis: ShapeAnalysis): Set { + const hot = new Set(); + const callees = new Map(); + const work: FunctionInfo[] = []; + for (const c of analysis.calls) { + let l = callees.get(c.caller); + if (l === undefined) callees.set(c.caller, (l = [])); + l.push(c.callee); + if (c.inLoop && !hot.has(c.callee)) { + hot.add(c.callee); + work.push(c.callee); + } + } + while (work.length > 0) { + const f = work.pop()!; + for (const g of callees.get(f) ?? []) { + if (!hot.has(g)) { + hot.add(g); + work.push(g); + } + } + } + return hot; +} + +export function buildShapeReport(analysis: ShapeAnalysis, filter: string): ShapeReport { + const lines: string[] = []; + const all = filter === "all" || filter === "verbose"; + const verbose = filter === "verbose"; + const fileFilter = filter !== "1" && !all ? filter : null; + const wanted = (n: ts.Node, inPackage: boolean): boolean => { + if (fileFilter !== null) return n.getSourceFile().fileName.includes(fileFilter); + return all || inPackage; + }; + + // Census facts: operations whose receiver may be ⊤ reach every escaped atom. + + // Index ops by the site atoms they can reach directly. + const byAtom = new Map(); + const topOps: typeof analysis.ops = []; + for (const o of analysis.ops) { + const atoms = atomsOf(analysis, o.recv); + for (const a of atoms) { + if (a.kind !== "object" && a.kind !== "array") continue; + let l = byAtom.get(a); + if (l === undefined) byAtom.set(a, (l = [])); + l.push(o); + } + if (atoms.has(analysis.top)) topOps.push(o); + } + + // Key-order groups: sites with the same key set and different orders need brands. + const orders = new Map>(); + for (const s of analysis.sites) { + if (s.kind !== "object" || s.keys === null) continue; + const set = [...s.keys].sort().join(","); + let o = orders.get(set); + if (o === undefined) orders.set(set, (o = new Set())); + o.add(s.keys.join(",")); + } + + const verdicts: SiteVerdict[] = []; + for (const s of analysis.sites) { + if (!s.inPackage && !all && fileFilter === null) continue; + const reasons: string[] = []; + const escapedWhy = analysis.escaped.get(s.atom); + const escapes = escapedWhy !== undefined; + if (s.hasAccessor) reasons.push("accessor in literal"); + if (s.hasProto) reasons.push(s.kind === "array" ? "hole in literal" : "__proto__ in literal"); + if (s.hasComputedKey) reasons.push("computed key in literal"); + for (const sp of s.spreads) + if (atomsOf(analysis, sp).has(analysis.top)) reasons.push("spread of ⊤"); + const direct = byAtom.get(s.atom) ?? []; + const ops = escapes ? [...direct, ...topOps] : direct; + const keys = new Set(s.keys ?? []); + const fieldsOf = analysis.fields.get(s.atom); + const knownKeys = + s.keys !== null ? keys : new Set([...fieldKeys(fieldsOf)].filter((k) => k !== "*")); + let freezable = false; + const seen = new Set(); + const note = (r: string): void => { + if (!seen.has(r)) { + seen.add(r); + reasons.push(r); + } + }; + for (const o of ops) { + const viaTop = !atomsOf(analysis, o.recv).has(s.atom); + const where = `${locLabel(o.node)}${viaTop ? " via ⊤" : ""}`; + switch (o.kind) { + case "delete": + case "defineProperty": + case "setPrototypeOf": + case "seal": + case "proxy": + note(`${o.kind} at ${where}`); + break; + case "assign": + note(`Object.assign target at ${where}`); + break; + case "freeze": + freezable = true; + break; + case "computedWrite": + if (!viaTop) + note(s.kind === "array" ? `index write at ${where}` : `computed write at ${where}`); + break; + case "lengthWrite": + if (s.kind === "array") note(`length write at ${where}`); + break; + case "write": + if (s.kind === "object" && !viaTop && o.name !== null && !knownKeys.has(o.name)) + note(`adds key '${o.name}' at ${where}`); + break; + case "read": + if ( + s.kind === "object" && + !underPromiseGuard(o.node) && + escapes && + o.name !== null && + !viaTop && + !knownKeys.has(o.name) && + !OBJECT_PROTOTYPE_MEMBERS.has(o.name) + ) + note(`reads absent key '${o.name}' at ${where} (escapes)`); + break; + default: + break; + } + } + let brand = false; + if (s.kind === "object" && s.keys !== null) { + const o = orders.get([...s.keys].sort().join(",")); + if (o !== undefined && o.size > 1) brand = true; + } + const writableByTop: string[] = []; + if (escapes) { + if (analysis.topComputedWrites.length > 0) writableByTop.push("*"); + else for (const k of knownKeys) if (analysis.topWriteNames.has(k)) writableByTop.push(k); + } + const verdict: SiteVerdict["verdict"] = + reasons.length > 0 ? "G" : escapes ? "P-escaping" : "P-local"; + const label = s.atom.label; + if (escapes) reasons.push(`escapes: ${escapedWhy}`); + verdicts.push({ site: s, label, verdict, reasons, writableByTop, freezable, brand }); + } + + const hot = hotFunctions(analysis); + const hotNodes = new Set([...hot].map((f) => f.node)); + + // ---- print ---- + const pkgFns = [...analysis.functions.values()].filter((f) => f.inPackage); + lines.push( + `analyzed ${analysis.files.length} files (${analysis.packageFiles} package JS), ${analysis.functions.size} functions (${pkgFns.length} in packages), ${analysis.atoms.atoms.length} atoms, ${analysis.nodes.length} value nodes, ${analysis.steps} steps${analysis.budgetExceeded ? " — BUDGET EXCEEDED, results incomplete" : ""}`, + ); + const topNames = [...analysis.topWriteNames.entries()].map( + ([n, node]) => `${n} (${locLabel(node)})`, + ); + lines.push( + `census: writes on ⊤ receivers: ${topNames.length === 0 ? "none" : topNames.slice(0, 20).join(", ")}${topNames.length > 20 ? ` +${topNames.length - 20}` : ""}`, + ); + lines.push( + `census: computed writes on ⊤ receivers: ${analysis.topComputedWrites.length === 0 ? "none" : analysis.topComputedWrites.slice(0, 8).map(locLabel).join(", ")}`, + ); + const topReflect = topOps.filter( + (o) => + o.kind !== "read" && + o.kind !== "computedRead" && + o.kind !== "write" && + o.kind !== "computedWrite", + ); + lines.push( + `census: reflection on ⊤ receivers: ${topReflect.length === 0 ? "none" : topReflect.map((o) => `${o.kind} (${locLabel(o.node)})`).join(", ")}`, + ); + const escapedPkg = [...analysis.escaped.keys()].filter( + (a) => + a.node !== undefined && + wanted(a.node, a.node.getSourceFile().fileName.includes("/node_modules/")), + ); + lines.push( + `escaped atoms: ${analysis.escaped.size} (${escapedPkg.length} shown below where relevant)`, + ); + const firstEscapes = [...analysis.escaped.entries()] + .filter(([, why]) => !why.startsWith("via ")) + .slice(0, 12); + for (const [atom, why] of firstEscapes) lines.push(` escape root: ${atom.label}: ${why}`); + + const counts = { "P-local": 0, "P-escaping": 0, G: 0 }; + const hotCounts = { "P-local": 0, "P-escaping": 0, G: 0 }; + const inHot = (n: ts.Node): boolean => { + let p: ts.Node | undefined = n.parent; + while (p !== undefined) { + if (hotNodes.has(p)) return true; + p = p.parent; + } + return false; + }; + for (const v of verdicts) { + counts[v.verdict]++; + if (inHot(v.site.node)) hotCounts[v.verdict]++; + } + lines.push( + `sites: ${verdicts.length} (P local ${counts["P-local"]}, P escaping ${counts["P-escaping"]}, G ${counts.G}); in hot functions: P local ${hotCounts["P-local"]}, P escaping ${hotCounts["P-escaping"]}, G ${hotCounts.G}`, + ); + for (const v of verdicts) { + if (!wanted(v.site.node, v.site.inPackage)) continue; + if (!verbose && !inHot(v.site.node)) continue; + const extra: string[] = []; + if (v.brand) extra.push("needs brand (key order)"); + if (v.freezable) extra.push("freezable"); + if (v.writableByTop.length > 0) extra.push(`⊤-writable: ${v.writableByTop.join(",")}`); + const fields = analysis.fields.get(v.site.atom); + const ftypes = + v.site.kind === "array" + ? `elem: ${analysis.elements.has(v.site.atom) ? describeValue(analysis, analysis.elements.get(v.site.atom)!) : "∅"}` + : fieldEntries(fields) + .filter(([k]) => k !== "*") + .map(([k, id]) => `${k}: ${describeValue(analysis, id)}`) + .join("; "); + lines.push( + `site ${v.label}${inHot(v.site.node) ? " [hot]" : ""}: ${v.verdict}${v.reasons.length > 0 ? ` — ${v.reasons.join("; ")}` : ""}${extra.length > 0 ? ` [${extra.join("; ")}]` : ""}`, + ); + if (ftypes.length > 0) lines.push(` ${ftypes}`); + } + + // Class fields. + for (const atom of analysis.atoms.atoms) { + if (atom.kind !== "instance" || atom.node === undefined) continue; + const inPackage = atom.node.getSourceFile().fileName.includes("/node_modules/"); + if (!wanted(atom.node, inPackage)) continue; + const fm = analysis.fields.get(atom); + if (fm === undefined || fm.size === 0) continue; + const esc = analysis.escaped.get(atom); + const parts = [...fm.entries()].map(([k, id]) => { + const w = + esc !== undefined && + !k.startsWith("#") && + (analysis.topComputedWrites.length > 0 || analysis.topWriteNames.has(k)) + ? " (⊤-writable)" + : ""; + return `${k}: ${describeValue(analysis, id)}${w}`; + }); + lines.push( + `class ${atom.label}${esc !== undefined ? ` (escapes: ${esc})` : ""}: ${parts.join("; ")}`, + ); + } + + // Hot functions. + const hotPkg = [...hot].filter((info) => wanted(info.node, info.inPackage)); + lines.push(`hot functions: ${hotPkg.length} (clones per receiver class counted separately)`); + for (const info of hotPkg) { + const n = info.node; + const params = info.params.map((p, i) => `p${i}=${describeValue(analysis, p)}`).join(", "); + const flags = [ + info.usesArguments ? "uses arguments" : "", + analysis.escaped.size > 0 && [...analysis.escaped.keys()].some((a) => a.node === n) + ? "escapes" + : "", + ].filter((x) => x !== ""); + lines.push( + `fn ${info.label}${flags.length > 0 ? ` [${flags.join(", ")}]` : ""}: this=${describeValue(analysis, info.thisNode)}${params.length > 0 ? `, ${params}` : ""}, ret=${describeValue(analysis, info.ret)}`, + ); + } + const why = process.env["SCRIPTC_DEBUG_SHAPES_WHY"]; + if (why !== undefined) { + for (const info of analysis.functions.values()) { + if (!info.label.startsWith(why)) continue; + for (const [what, node] of [ + ["ret", info.ret], + ...info.params.map((p, i) => [`p${i}`, p] as const), + ] as const) { + if (!analysis.nodes[node]!.atoms.has(analysis.top)) continue; + lines.push(`why ⊤ in ${info.label} ${what}:`); + let cur: number | string | undefined = node; + for (let i = 0; i < 40 && typeof cur === "number"; i++) { + lines.push(` ${analysis.nodeLabel.get(cur) ?? `node ${cur}`}`); + cur = analysis.topCause.get(cur); + } + if (typeof cur === "string") lines.push(` seed: ${cur}`); + } + } + } + return { sites: verdicts, hot, lines }; +} + +/** A read inside the true arm of a promise test (`isAsync(x)`, + * `x instanceof Promise`): flow-sensitive narrowing removes the record + * arms there. Stage S1's locals are flow-insensitive, so the report + * recognizes the guard syntactically. */ +function underPromiseGuard(node: ts.Node): boolean { + let child: ts.Node = node; + let p = node.parent; + while (p !== undefined && !ts.isFunctionLike(p)) { + let cond: ts.Expression | undefined; + if (ts.isIfStatement(p) && p.thenStatement === child) cond = p.expression; + if (ts.isConditionalExpression(p) && p.whenTrue === child) cond = p.condition; + if (cond !== undefined && isPromiseTest(cond)) return true; + child = p; + p = p.parent; + } + return false; +} + +function isPromiseTest(e: ts.Expression): boolean { + if (ts.isParenthesizedExpression(e)) return isPromiseTest(e.expression); + if (ts.isBinaryExpression(e)) { + if (e.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) + return ts.isIdentifier(e.right) && e.right.text === "Promise"; + if (e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) + return isPromiseTest(e.left) || isPromiseTest(e.right); + return false; + } + if (ts.isCallExpression(e) && ts.isIdentifier(e.expression)) + return /^is(Async|Promise|Thenable)$/.test(e.expression.text); + return false; +} diff --git a/packages/compiler/test/shape-inference.test.ts b/packages/compiler/test/shape-inference.test.ts new file mode 100644 index 000000000..3cd6e809d --- /dev/null +++ b/packages/compiler/test/shape-inference.test.ts @@ -0,0 +1,166 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import { compile } from "../src/index.js"; + +// A package shaped like a validation library: schema classes with a virtual +// `_parse`, per-field context and result literals, a class holding the +// context, and an output object built with computed keys. +const packageSource = ` +export class Status { + constructor() { this.value = "valid"; } + dirty() { if (this.value === "valid") this.value = "dirty"; } +} +class LazyPath { + constructor(parent, data, key) { this.parent = parent; this.data = data; this.key = key; } + get path() { return [this.key]; } +} +export class Base { + constructor(def) { this._def = def; } + ctxOf(input) { return { common: input.parent.common, data: input.data, parent: input.parent }; } + parse(data) { + const root = { common: { issues: [] }, data, parent: null }; + const result = this._parse({ data, parent: root }); + return result.value; + } +} +export class Str extends Base { + _parse(input) { + const status = new Status(); + if (typeof input.data !== "string") status.dirty(); + return { status: status.value, value: input.data }; + } +} +export class Obj extends Base { + _parse(input) { + const ctx = this.ctxOf(input); + const pairs = []; + for (const key of Object.keys(this._def.shape)) { + const field = this._def.shape[key]; + pairs.push({ key, value: field._parse(new LazyPath(ctx, ctx.data[key], key)) }); + } + const out = {}; + for (const pair of pairs) out[pair.key] = pair.value.value; + return { status: "valid", value: out }; + } +} +export const str = () => new Str({ checks: [] }); +export const obj = (shape) => new Obj({ shape }); +export function leak(value) { globalThis.leaked = value; return value; } +export function reflect() { const o = { a: 1 }; delete o.a; return o; } +`; + +const packageTypes = ` +export declare class Base { parse(data: unknown): unknown; } +export declare class Str extends Base {} +export declare class Obj extends Base {} +export declare const str: () => Str; +export declare const obj: (shape: Record) => Obj; +export declare function leak(value: T): T; +export declare function reflect(): object; +`; + +const mainSource = ` +import { leak, obj, reflect, str } from "shapes-fixture"; +const schema = obj({ a: str(), b: str() }); +const input: unknown = JSON.parse('{"a":"x","b":"y"}'); +console.log(JSON.stringify(schema.parse(input))); +console.log(JSON.stringify(leak({ kept: 1 })), JSON.stringify(reflect())); +`; + +let dir: string; +let saved: string | undefined; +beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "scriptc-shapes-")); + const pkg = join(dir, "node_modules", "shapes-fixture"); + await mkdir(pkg, { recursive: true }); + await writeFile( + join(pkg, "package.json"), + JSON.stringify({ + name: "shapes-fixture", + version: "1.0.0", + type: "module", + main: "index.js", + types: "index.d.ts", + }), + ); + await writeFile(join(pkg, "index.js"), packageSource); + await writeFile(join(pkg, "index.d.ts"), packageTypes); + await writeFile( + join(dir, "package.json"), + JSON.stringify({ type: "module", dependencies: { "shapes-fixture": "1.0.0" } }), + ); + await writeFile(join(dir, "main.ts"), mainSource); + saved = process.env["SCRIPTC_DEBUG_SHAPES"]; +}); +afterEach(async () => { + if (saved === undefined) delete process.env["SCRIPTC_DEBUG_SHAPES"]; + else process.env["SCRIPTC_DEBUG_SHAPES"] = saved; + vi.restoreAllMocks(); + await rm(dir, { recursive: true, force: true }); +}); + +async function report(): Promise { + process.env["SCRIPTC_DEBUG_SHAPES"] = "verbose"; + const lines: string[] = []; + vi.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array) => { + const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"); + for (const l of text.split("\n")) if (l.startsWith("[shapes] ")) lines.push(l.slice(9)); + return true; + }); + const entry = join(dir, "main.ts"); + await compile(entry, { + outDir: dir, + outPath: join(dir, "main.ir.json"), + outputKind: "ir", + npmStatic: ["shapes-fixture"], + }); + return lines; +} + +function site(lines: string[], label: RegExp): string { + const line = lines.find((l) => l.startsWith("site ") && label.test(l)); + if (line === undefined) throw new Error(`no site ${label}:\n${lines.join("\n")}`); + return line; +} + +test("S1 report: per-field temporaries are provable records, dynamic objects stay G", async () => { + const lines = await report(); + // result literals of both _parse implementations, the pair literal, the + // context and the root context never escape: native records would be exact + // the leaf schema's result: in a function called from a loop (hot) + expect(site(lines, /^site \{status,value\}@shapes-fixture\/index\.js:23 \[hot\]/)).toMatch( + /: P-local/, + ); + expect(site(lines, /^site \{status,value\}@shapes-fixture\/index\.js:36/)).toMatch(/: P-local/); + expect(site(lines, /^site \{key,value\}@/)).toMatch(/: P-local/); + expect(site(lines, /^site \{common,data,parent\}@shapes-fixture\/index\.js:12/)).toMatch( + /: P-local/, + ); + expect(site(lines, /^site \[\]@shapes-fixture\/index\.js:29/)).toMatch(/: P-local/); + // the output object is built with computed keys + expect(site(lines, /^site \{\}@shapes-fixture\/index\.js:34/)).toMatch(/: G — computed write/); + // a deleted key and a value stored into the global object + expect(site(lines, /^site \{a\}@/)).toMatch(/: G — delete at/); + expect(site(lines, /^site \{kept\}@main\.ts/)).toMatch(/: P-escaping — escapes: stored into ⊤/); + // receiver-typed dispatch: the field's validators are the schema classes + expect(lines.find((l) => l.startsWith("fn Str._parse@"))).toMatch(/p0=LazyPath/); + expect(lines.find((l) => l.startsWith("class LazyPath"))).toMatch( + /parent: \{common,data,parent\}@shapes-fixture\/index\.js:12;/, + ); + expect(lines.find((l) => l.startsWith("class Str"))).toMatch(/_def: \{checks\}@/); +}); + +test("S1 report is off by default and changes nothing", async () => { + delete process.env["SCRIPTC_DEBUG_SHAPES"]; + const writes = vi.spyOn(process.stderr, "write"); + const entry = join(dir, "main.ts"); + await compile(entry, { + outDir: dir, + outPath: join(dir, "main.ir.json"), + outputKind: "ir", + npmStatic: ["shapes-fixture"], + }); + expect(writes.mock.calls.some(([c]) => String(c).includes("[shapes]"))).toBe(false); +}); From e0d0e03eed999bcbc4564986632659937cecd030 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:47:49 -0500 Subject: [PATCH 161/409] Support Symbol.toStringTag class fields and getters --- .../symbol-fields.mdx | 1 + .../src/backend/llvm/expr-stream-bridges.ts | 71 +++++++++--- .../lowering/class-dynamic-dispatch.ts | 84 +++++++++++++- .../lowering/class-method-mutations.ts | 14 ++- .../frontend/lowering/class-to-primitive.ts | 103 ++++++++++++++++-- .../frontend/lowering/cycle-initialization.ts | 10 +- .../src/frontend/lowering/lower-calls.ts | 6 +- .../src/frontend/lowering/lower-classes.ts | 5 +- .../src/frontend/lowering/lower-inspect.ts | 87 ++++++++++++++- .../src/frontend/lowering/symbol-fields.ts | 49 +++++++++ .../src/ir/function-references.test.ts | 6 + .../compiler/src/ir/function-references.ts | 1 + packages/compiler/src/ir/ir.ts | 9 +- packages/compiler/src/ir/validate.ts | 25 ++++- ...751-class-well-known-symbol-fields.ts.json | 6 + packages/runtime/src/scr_dyn.h | 8 ++ packages/runtime/src/scr_inspect.c | 30 ++++- packages/runtime/src/scr_json.c | 71 +++++++++++- packages/runtime/src/scr_symbol.c | 5 + .../7751-class-well-known-symbol-fields.ts | 76 +++++++++++++ ...751-class-well-known-symbol-fields.ts.json | 1 + 21 files changed, 620 insertions(+), 48 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7751-class-well-known-symbol-fields.ts.json create mode 100644 tests/corpus/7751-class-well-known-symbol-fields.ts create mode 100644 tests/harness/corpus-costs/7751-class-well-known-symbol-fields.ts.json diff --git a/docs/content/limitations/types-and-class-definitions/symbol-fields.mdx b/docs/content/limitations/types-and-class-definitions/symbol-fields.mdx index baff4d190..45659b493 100644 --- a/docs/content/limitations/types-and-class-definitions/symbol-fields.mdx +++ b/docs/content/limitations/types-and-class-definitions/symbol-fields.mdx @@ -1,4 +1,5 @@ - **Symbol fields:** Instance fields require a stable module-level `Symbol()` or `Symbol.for()` binding with a literal description or constant string binding. + A `[Symbol.toStringTag]` field or getter with a string value sets the tag that `String()`, `Object.prototype.toString` and `util.inspect` print; fields keyed by other well-known symbols are unsupported. Reassigned keys, runtime-computed descriptions, distinct symbols with the same description in one class, and copying symbol fields with rest bindings or `Object.assign` are unsupported. JavaScript object spreads copy represented own symbol fields. Equal registry keys share a field across modules. diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 70a8dcdee..f4532b21a 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -20,6 +20,7 @@ import { isRefCounted, shapeHasAccessorSlots, typeKey, + type IrClassDef, } from "../../ir/ir.js"; import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js"; import { BlockBuilder } from "./blocks.js"; @@ -277,9 +278,7 @@ export function streamTypedRefCommitAdapter( ); }); } - const symbols = new Map( - (meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol.globalId]), - ); + const symbols = new Map((meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol])); const fields = meta.def.fields.filter( (field) => (isClassOwnEnumerableFieldName(field.name) || symbols.has(field.name)) && @@ -299,13 +298,15 @@ export function streamTypedRefCommitAdapter( const inline = host.inlineUnions.classField(t.className, field.name); const fieldTy = int32 ? "i32" : llFieldType(field.type, inline); const checkTy = host.llType(field.type); - const symbolGlobal = symbols.get(field.name); - if (symbolGlobal) { + const symbolKey = symbols.get(field.name); + if (symbolKey) { host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); + const keyLines = symbolKeyLines(host, symbolKey, `f${index}_keyraw`); lines.push( - ` %f${index}_keyraw = load ptr, ptr @${mangleGlobal(symbolGlobal)}`, + ...keyLines.load, ` %f${index}_key = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %f${index}_keyraw)`, + ...keyLines.release, ); if (field.type.kind === "symbol") { // A base constructor can mutate the instance before this @@ -333,7 +334,7 @@ export function streamTypedRefCommitAdapter( lines.push( ` %${raw} = call ptr @scr_dyn_obj_get(ptr %d, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`, ); - if (meta.def.tracksOwnFields && !symbolGlobal) + if (meta.def.tracksOwnFields && !symbolKey) lines.push( ` %f${index}_present = icmp ne ptr %${raw}, null`, ` br i1 %f${index}_present, label %f${index}_read, label %${after}`, @@ -344,7 +345,7 @@ export function streamTypedRefCommitAdapter( ` %${undef} = call ptr @scr_dyn_undefined()`, ` %${input} = select i1 %${missing}, ptr %${undef}, ptr %${raw}`, ` %${next} = call ${field.type.kind === "bool" ? "zeroext " : ""}${checkTy} @${host.dyn.dynCheckHelper(field.type)}(ptr %${input}, ptr null)`, - ...(symbolGlobal ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []), + ...(symbolKey ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []), ...host.pendingTestLines(`%${pending}`), ` br i1 %${pending}, label %done, label %${store}`, `${store}:`, @@ -499,16 +500,18 @@ export function streamTypedRefCommitAdapter( ? `declare void @scr_dyn_symbol_key_placeholder(ptr, ptr)` : `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`, ); - [...symbols.values()].forEach((globalId, index) => + [...symbols.values()].forEach((symbol, index) => { + const keyLines = symbolKeyLines(host, symbol, `bag_symraw${index}`); lines.push( - ` %bag_symraw${index} = load ptr, ptr @${mangleGlobal(globalId)}`, + ...keyLines.load, ` %bag_sym${index} = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %bag_symraw${index})`, + ...keyLines.release, meta.def.tracksOwnFields ? ` call void @scr_dyn_symbol_key_placeholder(ptr %bag, ptr %bag_sym${index})` : ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`, ` call void @scr_dyn_release_v(ptr %bag_sym${index})`, - ), - ); + ); + }); const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES); lines.push( ` %bag_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${index}`, @@ -705,6 +708,27 @@ export function liveDynUnionRefAdapter( return sym; } +/** The raw ScrSym of a class's symbol field key into `%name`: a module + * key is borrowed from its global, a well-known one comes from the + * runtime's registry (+1, released by the `release` lines once the key + * value holds it). */ +function symbolKeyLines( + host: LlvmEmitterContext, + symbol: NonNullable[number], + name: string, +): { load: string[]; release: string[] } { + if ("globalId" in symbol) + return { load: [` %${name} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`], release: [] }; + host.declare("declare ptr @scr_sym_well_known(ptr)"); + host.declare("declare void @scr_sym_release(ptr)"); + return { + load: [ + ` %${name} = call ptr @scr_sym_well_known(ptr ${host.internLiteral(symbol.wellKnown)})`, + ], + release: [` call void @scr_sym_release(ptr %${name})`], + }; +} + export function streamTypedRefBoxValue( host: LlvmEmitterContext, B: BlockBuilder, @@ -929,8 +953,10 @@ export function streamTypedRefMaterializeAdapter( B.line(`${fieldValue} = sitofp i32 ${raw} to double`); } else if (inline) fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true); else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`); - B.line(`${rawKey} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`); + const keyLines = symbolKeyLines(host, symbol, rawKey.slice(1)); + for (const line of keyLines.load) B.line(line.trim()); const key = host.streamTypedRefBoxValue(B, SYMBOL_T, rawKey); + for (const line of keyLines.release) B.line(line.trim()); const afterSymbol = meta.def.tracksOwnFields || field.type.kind === "symbol" ? B.newLabel("symbol.after") @@ -1473,9 +1499,26 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM // The constructor's name, as util.inspect prints before the fields. const jsName = meta.def.jsName ?? meta.def.name.replace(/^%/, ""); const className = jsName === "" ? "null" : host.internLiteral(jsName); + // Its Symbol.toStringTag getter's string, for Object.prototype.toString + // and util.inspect. + let stringTag = "null"; + if (meta.def.stringTagHelper) { + // The runtime lends the instance; the helper takes its own reference. + stringTag = `@${snapshot}_ctag`; + const retain = vAdapters(host.shapeHost, { kind: "object", className: meta.def.name }).retain; + host.resolveThunkDefs.push( + `define internal ptr ${stringTag}(ptr %p) ${FN_ATTRS} { ; Symbol.toStringTag of ${meta.def.name}`, + `entry:`, + ` %owned = call ptr ${retain}(ptr %p)`, + ` %tag = call ptr @${mangleFunction(meta.def.stringTagHelper)}(ptr %owned)`, + ` ret ptr %tag`, + `}`, + ``, + ); + } host.resolveThunkDefs.push( `@${names} = internal constant [${members.length} x ptr] [${literals.join(", ")}]`, - `@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className} }`, + `@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className}, ptr ${stringTag} }`, ``, ); } diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index abf03811e..ad5f4a620 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1,4 +1,5 @@ import { primitiveConstructorOr } from "./lower-exprs.js"; +import { TO_STRING_TAG_FIELD, wellKnownSymbolOfField } from "./symbol-fields.js"; import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js"; import { constructedValue } from "./constructor-returns.js"; @@ -26,6 +27,7 @@ import { isUnitType, typeEquals, typeKey, + type IrClassDef, type IrExpr, type IrFunction, type IrParam, @@ -153,6 +155,41 @@ const STREAM_NUM_PROPERTIES = new Set([ const streamProperty = (name: string): boolean => STREAM_BOOL_PROPERTIES.has(name) || STREAM_NUM_PROPERTIES.has(name); +/** `(instance) => string`, the string a class's inherited + * Symbol.toStringTag getter answers, which the runtime reads for + * Object.prototype.toString and util.inspect of an instance seen as + * `unknown`. None for a class without a string getter, or whose own tag + * field shadows it. */ +function stringTagHelper(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrFunction | null { + if (info.fields.has(TO_STRING_TAG_FIELD)) return null; + const getter = findMethodOn(lowerer, info, "get:sym:toStringTag"); + if (getter?.sig.ret.kind !== "string") return null; + const type: IrType = { kind: "object", className: info.def.name }; + const owner = getter.declarer.def.name; + return { + name: `%class.stringTag:${info.def.name}`, + params: [{ localId: "this", name: "this", type }], + locals: [{ id: "this", name: "this", type, mutable: false }], + returnType: STRING, + body: [ + { + kind: "return", + value: accessorCall( + lowerer, + owner, + "get:sym:toStringTag", + lowerer.upcastTo(varRef("this", type, loc), owner), + [], + STRING, + loc, + ), + loc, + }, + ], + loc, + }; +} + function symbolMemberKey( lowerer: Lowerer, info: ClassInfo, @@ -1276,10 +1313,27 @@ export class ClassDynamicDispatch { const info = lowerer.classes.get(className); if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue; if (info.symbolFields && info.def.symbolFields === undefined) { - info.def.symbolFields = [...info.symbolFields].flatMap(([symbol, field]) => { - const global = lowerer.globalsBySymbol.get(symbol); - return global?.type.kind === "symbol" ? [{ field, globalId: global.id }] : []; - }); + info.def.symbolFields = [...info.symbolFields].flatMap( + ([symbol, field]): NonNullable => { + const global = lowerer.globalsBySymbol.get(symbol); + if (global?.type.kind === "symbol") return [{ field, globalId: global.id }]; + const wellKnown = wellKnownSymbolOfField(field); + return wellKnown === null ? [] : [{ field, wellKnown }]; + }, + ); + } + const tagged = (current: ClassInfo): ClassInfo[] => [ + current, + ...current.subclasses.flatMap(tagged), + ]; + for (const current of tagged(info)) { + if (current.def.stringTagHelper !== undefined) continue; + const fn = stringTagHelper(lowerer, current, functions[0]!.loc); + if (!fn) continue; + current.def.stringTagHelper = fn.name; + this.generated.add(fn); + lowerer.liftedFns.push(fn); + changed = true; } if (this.prototypeHelper && !this.prototypeClasses.has(className)) { this.prototypeClasses.add(className); @@ -3554,10 +3608,29 @@ export class ClassDynamicDispatch { name, lowerer.coerceToExpected(varRef(global.id, global.type, loc), DYN), ); + } else if (wellKnownSymbolOfField(name) !== null) { + // A field keyed by a well-known symbol (Symbol.toStringTag). + symbols.set(name, { + kind: "dynFrom", + type: DYN, + loc, + value: { + kind: "libCall", + fn: "sym.wellKnown", + args: [ + { kind: "strLit", value: wellKnownSymbolOfField(name)!, type: STRING, loc }, + ], + type: SYMBOL_T, + loc, + }, + }); } } for (let owner: ClassInfo | null = info; owner; owner = owner.base) { - for (const name of [...owner.methods.keys()]) + for (const method of [...owner.methods.keys()]) { + // Accessors key by the property they define (`get [Symbol.toStringTag]`). + const name = + method.startsWith("get:") || method.startsWith("set:") ? method.slice(4) : method; if (name.startsWith("sym:")) { symbols.set(name, { kind: "dynFrom", @@ -3572,6 +3645,7 @@ export class ClassDynamicDispatch { }, }); } + } } const branches: IrStmt[] = []; if (!dispatch.write && !dispatch.reflect && !info.def.tracksOwnFields) { diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 035c2c2a5..9a2632ed3 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -394,7 +394,19 @@ export function collectClassMethodMutations( (ts.isElementAccessExpression(base) && ts.isStringLiteralLike(base.argumentExpression) && base.argumentExpression.text === "prototype"); - if (prototype) { + // `Object.prototype.toString.call(value)` calls the builtin and + // keeps no reference to it, so it replaces nothing. + const builtinCall = + ts.isPropertyAccessExpression(node) && + ts.isPropertyAccessExpression(base) && + ts.isIdentifier(unwrap(base.expression)) && + lowerer.isStdlibGlobal(unwrap(base.expression) as ts.Identifier, "Object") && + ts.isPropertyAccessExpression(node.parent) && + node.parent.expression === node && + (node.parent.name.text === "call" || node.parent.name.text === "apply") && + ts.isCallExpression(node.parent.parent) && + node.parent.parent.expression === node.parent; + if (prototype && !builtinCall) { if (ts.isPropertyAccessExpression(node)) lowerer.noteMethodAccess(node.name.text, node); else if (ts.isStringLiteralLike(node.argumentExpression)) lowerer.noteMethodAccess(node.argumentExpression.text, node); diff --git a/packages/compiler/src/frontend/lowering/class-to-primitive.ts b/packages/compiler/src/frontend/lowering/class-to-primitive.ts index 0b107ab4e..a260116f8 100644 --- a/packages/compiler/src/frontend/lowering/class-to-primitive.ts +++ b/packages/compiler/src/frontend/lowering/class-to-primitive.ts @@ -13,6 +13,7 @@ import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js"; import { isClassCallback } from "./class-callbacks.js"; +import { TO_STRING_TAG_FIELD } from "./symbol-fields.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { DATE_TIME_FIELD, dateRootOf } from "./builtins/date-classes.js"; import { dateValueMethod } from "./builtins/dates.js"; @@ -29,7 +30,15 @@ type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString"; * Date`, or through Object.prototype.toString ("[object Object]"). */ type Plan = | { method: ConversionMethod; ret: IrType } - | { method: null; date?: "toString" | "valueOf" }; + | { method: null; date?: "toString" | "valueOf"; tag?: ToStringTag }; + +/** Where Object.prototype.toString finds a class's Symbol.toStringTag: a + * string field (`[Symbol.toStringTag] = "LRUCache"`) or a getter that + * returns a string, declared on `owner`. */ +interface ToStringTag { + owner: ClassInfo; + getter: boolean; +} const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]); @@ -52,6 +61,17 @@ export function classToStringConversion( ); } +/** Object.prototype.toString of a program class instance: "[object " + + * its Symbol.toStringTag + "]" (an own tag field, else an inherited + * getter), or "[object Object]". The class's own conversion methods do not + * take part. Null as for classToStringConversion, and for Date subclasses. */ +export function classObjectTag(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr | null { + if (value.type.kind !== "object") return null; + const info = lowerer.classes.get(value.type.className); + if (!info || dateRootOf(info) !== null) return null; + return classConversion(lowerer, value, "tag", node, STRING, (tag) => tag); +} + /** ToNumber(ToPrimitive(value, "number")) of a program class instance * (`Number(x)`): `[Symbol.toPrimitive]("number")`, then `valueOf` and then * `toString`, and NaN for "[object Object]". Null as for @@ -122,7 +142,7 @@ function mayBeBigint(lowerer: Lowerer, type: IrType): boolean { function classConversion( lowerer: Lowerer, value: IrExpr, - hint: ConversionHint, + hint: ConversionHint | "tag", node: ts.Node, resultType: IrType, finish: (primitive: IrExpr) => IrExpr, @@ -131,7 +151,7 @@ function classConversion( const info = lowerer.classes.get(value.type.className); if (!info || !plainProgramClass(info)) return null; const classes = subtree(lowerer, info); - for (const cls of classes) refuseReplacedConversions(lowerer, cls, node); + if (hint !== "tag") for (const cls of classes) refuseReplacedConversions(lowerer, cls, node); const plans = new Map(classes.map((cls) => [cls, planOf(lowerer, cls, hint, node)])); // A class whose plan differs from its base's starts a group; the deepest // groups are tested first, so each instance takes its own class's plan. @@ -164,6 +184,41 @@ function classConversion( }; return finish(dateValueMethod(plan.date, time, loc)!); } + if (plan.method === null && plan.tag) { + // Object.prototype.toString: "[object " + the tag + "]". + const owner = plan.tag.owner.def.name; + const tag: IrExpr = plan.tag.getter + ? accessorCall( + lowerer, + owner, + "get:sym:toStringTag", + lowerer.upcastTo(object, owner), + [], + STRING, + loc, + ) + : { + kind: "fieldGet", + obj: lowerer.upcastTo(object, owner), + className: owner, + field: TO_STRING_TAG_FIELD, + type: STRING, + loc, + }; + return finish({ + kind: "strConcat", + left: { + kind: "strConcat", + left: { kind: "strLit", value: "[object ", type: STRING, loc }, + right: tag, + type: STRING, + loc, + }, + right: { kind: "strLit", value: "]", type: STRING, loc }, + type: STRING, + loc, + }); + } if (plan.method === null) return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc }); const args: IrExpr[] = @@ -228,10 +283,28 @@ function depth(info: ClassInfo): number { } function samePlan(a: Plan, b: Plan): boolean { - return ( - a.method === b.method && - (a.method !== null || b.method !== null || a.date === (b as { date?: string }).date) - ); + if (a.method !== null || b.method !== null) return a.method === b.method; + return a.date === b.date && a.tag?.owner === b.tag?.owner && a.tag?.getter === b.tag?.getter; +} + +/** The class's Symbol.toStringTag, when it has one: the class at the top + * of the chain whose layout holds the tag field (an own property, which + * shadows any getter), or its getter. Other tag types would make + * Object.prototype.toString answer "Object" or the value depending on the + * run, and are refused. */ +function toStringTagOf(lowerer: Lowerer, info: ClassInfo, node: ts.Node): ToStringTag | null { + const type = info.fields.get(TO_STRING_TAG_FIELD); + if (type !== undefined) { + if (type.kind !== "string") refuse(lowerer, node, info, "a non-string Symbol.toStringTag"); + let owner = info; + while (owner.base?.fields.has(TO_STRING_TAG_FIELD)) owner = owner.base; + return { owner, getter: false }; + } + const getter = findMethodOn(lowerer, info, "get:sym:toStringTag"); + if (!getter) return null; + if (getter.sig.ret.kind !== "string") + refuse(lowerer, node, info, "a non-string Symbol.toStringTag"); + return { owner: getter.declarer, getter: true }; } /** A conversion method the class declares or inherits (a field of that @@ -246,7 +319,16 @@ function declared( return found ? { ret: found.sig.ret } : null; } -function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: ts.Node): Plan { +function planOf( + lowerer: Lowerer, + info: ClassInfo, + hint: ConversionHint | "tag", + node: ts.Node, +): Plan { + if (hint === "tag") { + const tag = toStringTagOf(lowerer, info, node); + return tag ? { method: null, tag } : { method: null }; + } const exotic = declared(lowerer, info, "sym:toPrimitive"); if (exotic) { if (!primitive(lowerer, exotic.ret)) @@ -271,7 +353,8 @@ function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: t refuse(lowerer, node, info, `a '${method}' method that can return an object`); return { method, ret: found.ret }; } - return { method: null }; + const tag = toStringTagOf(lowerer, info, node); + return tag ? { method: null, tag } : { method: null }; } function primitive(lowerer: Lowerer, type: IrType): boolean { @@ -292,8 +375,6 @@ function refuseReplacedConversions(lowerer: Lowerer, info: ClassInfo, node: ts.N if (lowerer.prototypeMethodAccesses.has(method)) refuse(lowerer, node, info, `a '${method}' the program reaches through a prototype`); } - if (findMethodOn(lowerer, info, "get:sym:toStringTag") || info.fields.has("sym:toStringTag")) - refuse(lowerer, node, info, "a Symbol.toStringTag"); } function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string): never { diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index 4b1e74fcf..8c8ce06e6 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -707,11 +707,17 @@ function rewriteFunction( * unchecked. */ export function refuseUncheckedCycleReads( lowerer: Lowerer, - classes: readonly { baseValueGlobal?: string; symbolFields?: { globalId: string }[] }[], + classes: readonly { + baseValueGlobal?: string; + symbolFields?: readonly ({ globalId: string } | { wellKnown: string })[]; + }[], ): void { if (lowerer.cycleInitFlags.size === 0) return; for (const cls of classes) { - const ids = [cls.baseValueGlobal, ...(cls.symbolFields ?? []).map((f) => f.globalId)]; + const ids = [ + cls.baseValueGlobal, + ...(cls.symbolFields ?? []).map((f) => ("globalId" in f ? f.globalId : undefined)), + ]; for (const id of ids) { const flag = id === undefined ? undefined : lowerer.cycleInitFlags.get(id); if (flag === undefined) continue; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 5366ca9d0..c9416df46 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -183,7 +183,7 @@ import { import { isSafeToDiscard } from "./expressions/evaluation-safety.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerChildMethodValue, lowerChildPropertyCall } from "./handle-properties.js"; -import { classToNumberConversion } from "./class-to-primitive.js"; +import { classObjectTag, classToNumberConversion } from "./class-to-primitive.js"; import { hasRuntimeStatics } from "./class-runtime-statics.js"; import { structuralReadType } from "./class-unions.js"; import { @@ -3103,6 +3103,10 @@ function lowerObjectPrototypeCall( const immediate = immediateObjectTagReceiver(lowerer, receiverNode); const receiver = immediate?.effect ?? lowerer.lowerExpr(receiverNode); const tag = immediate?.tag ?? objectToStringTag(lowerer, receiver.type); + if (tag === null && immediate === null && call.arguments.nodes.length === 1) { + const classTag = classObjectTag(lowerer, receiver, receiverNode); + if (classTag) return classTag; + } // A generator object answers its tag through its checked capsule. if (tag === null && receiver.type.kind !== "dyn" && receiver.type.kind !== "generator") { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 9452cdf25..fb8c07a99 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -8156,7 +8156,10 @@ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): stri member === "asyncIterator" || member === "dispose" || member === "asyncDispose" || - member === "toPrimitive" + member === "toPrimitive" || + // A toStringTag field keeps its symbol field name; its accessor and + // method forms take the method slot name. + (member === "toStringTag" && !ts.isPropertyDeclaration(name.parent)) ) { return `sym:${member}`; } diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 5266d5c4e..ce9c35277 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -44,9 +44,9 @@ import { shapeKeyOrders, typeKey, } from "../../ir/ir.js"; -import type { ClassInfo } from "./lower-classes.js"; +import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; -import { symbolFieldDisplayName } from "./symbol-fields.js"; +import { symbolFieldDisplayName, TO_STRING_TAG_FIELD } from "./symbol-fields.js"; import { eventTargetRootOf } from "./lower-event-target.js"; import { dateRootOf } from "./builtins/date-classes.js"; import { lowerSystemErrorMap } from "./lower-system-errors.js"; @@ -1870,8 +1870,70 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // observable way — Node's inspect omits them entirely (verified), // so a class whose only fields are private prints as `C {}`. const visible = info.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name)); + // A Symbol.toStringTag getter (an own tag field is listed instead) + // brackets the tag after the name, `Name [tag] {`, when it is a + // nonempty string other than the name. + const tagGetter = info.fields.has(TO_STRING_TAG_FIELD) + ? null + : findMethodOn(lowerer, info, "get:sym:toStringTag"); + let named: (plain: string, make: (head: IrExpr) => IrExpr) => IrExpr = (plain) => + strLit(plain, loc); + if (tagGetter?.sig.ret.kind === "string") { + const owner = tagGetter.declarer.def.name; + locals.push({ id: "tag.0", name: "tag", type: STRING, mutable: false }); + body.push({ + kind: "varDecl", + localId: "tag.0", + init: accessorCall( + lowerer, + owner, + "get:sym:toStringTag", + lowerer.upcastTo(v(), owner), + [], + STRING, + loc, + ), + loc, + }); + const tag = (): IrExpr => varRef("tag.0", STRING, loc); + const shown: IrExpr = { + kind: "logical", + op: "&&", + left: { + kind: "strEq", + negated: true, + left: tag(), + right: strLit("", loc), + type: BOOL, + loc, + }, + right: { + kind: "strEq", + negated: true, + left: tag(), + right: strLit(display, loc), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + const head = concatAll( + [strLit(`${display === "" ? "Object" : display} [`, loc), tag(), strLit("]", loc)], + loc, + ); + named = (plain, make) => ({ + kind: "ternary", + cond: shown, + then: make(head), + else_: strLit(plain, loc), + type: STRING, + loc, + }); + } + const after = (text: string) => (head: IrExpr) => concatAll([head, strLit(text, loc)], loc); if (visible.length === 0) { - body = [ret(strLit(display === "" ? "{}" : `${display} {}`, loc))]; + body.push(ret(named(display === "" ? "{}" : `${display} {}`, after(" {}")))); break; } const get = (field: string, type: IrType): IrExpr => ({ @@ -1882,7 +1944,22 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { type, loc, }); - body = [depthGate(display === "" ? "[Object]" : `[${display}]`), ...begin()]; + body.push( + { + kind: "if", + cond: { kind: "bin", op: ">", left: r(), right: d(), type: BOOL, loc }, + then: [ + ret( + named(display === "" ? "[Object]" : `[${display}]`, (head) => + concatAll([strLit("[", loc), head, strLit("]", loc)], loc), + ), + ), + ], + else_: null, + loc, + }, + ...begin(), + ); // def.fields carries layout order: the base chain first, then own — // exactly the own-property insertion order of a constructor that // assigns in declaration order (SEMANTICS.md 36's stance). SYMBOL- @@ -1911,7 +1988,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { ret( end( strLit("", loc), - strLit(display === "" ? "{" : `${display} {`, loc), + named(display === "" ? "{" : `${display} {`, after(" {")), strLit("}", loc), false, boolLit(false, loc), diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index 56718615e..cdfb9e1b8 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -77,6 +77,8 @@ export function fenceSymbolFieldCopy( } export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSymbolKey | null { + const wellKnown = wellKnownSymbolKeyOf(lowerer, key); + if (wellKnown !== undefined) return wellKnown; if ( !ts.isIdentifier(key) && (!ts.isPropertyAccessExpression(key) || key.questionDotToken || !ts.isIdentifier(key.name)) @@ -90,6 +92,53 @@ export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSym return sym ? classSymbolKeyOfSymbol(lowerer, sym) : null; } +/** Well-known symbols a class field may be keyed by: those no protocol + * consults on an instance, or whose protocol the lowering reads from the + * field (Symbol.toStringTag, by the class string conversions). A field + * keyed by any other well-known symbol (Symbol.iterator, toPrimitive, + * hasInstance, ...) would be skipped by its protocol, so it keeps the + * computed-key refusal. */ +const FIELD_WELL_KNOWN_SYMBOLS: ReadonlySet = new Set(["toStringTag"]); + +/** The key of `Symbol.` on the standard global: undefined when the + * expression is not one, null when it is a well-known symbol a field may + * not be keyed by. The identity is the lib's declaration of the property, + * one symbol for every program reference. */ +function wellKnownSymbolKeyOf( + lowerer: Lowerer, + key: ts.Expression, +): ClassSymbolKey | null | undefined { + while (ts.isParenthesizedExpression(key)) key = key.expression; + if ( + !ts.isPropertyAccessExpression(key) || + key.questionDotToken || + !ts.isIdentifier(key.expression) || + !ts.isIdentifier(key.name) || + !lowerer.isStdlibGlobal(key.expression, "Symbol") + ) + return undefined; + const name = key.name.text; + const sym = lowerer.checker.getSymbolAtLocation(key.name); + if (!sym || !(lowerer.typeOf(key).flags & ts.TypeFlags.UniqueESSymbol)) return undefined; + if (!FIELD_WELL_KNOWN_SYMBOLS.has(name)) return null; + return { sym, identity: sym, fieldName: wellKnownSymbolFieldName(name) }; +} + +/** The layout name of a field keyed by the well-known `Symbol.`: a + * reserved symbol-field name, printed as Node spells the key. */ +export function wellKnownSymbolFieldName(name: string): string { + return `${FIELD_PREFIX}Symbol(Symbol.${name})`; +} + +/** The well-known symbol a field name is keyed by (`toStringTag`), or null. */ +export function wellKnownSymbolOfField(field: string): string | null { + const match = /^%symbol:Symbol\(Symbol\.(\w+)\)$/.exec(field); + return match === null ? null : match[1]!; +} + +/** The layout name of a `Symbol.toStringTag` field. */ +export const TO_STRING_TAG_FIELD = wellKnownSymbolFieldName("toStringTag"); + /** A literal Symbol()/Symbol.for() initialized once at module scope. The * whole-file proof also covers var declarations emitted by bundlers. Keys * cannot be used before initialization: a computed class field evaluates diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 91e1d5f05..db872a525 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -157,10 +157,12 @@ const MODULE_FIELDS: Record>, Role> = { "classes[].errorNameAfter": "other", "classes[].prototypeDataHelper": "reference", "classes[].instancePrototypeHelper": "reference", + "classes[].stringTagHelper": "reference", "classes[].localCaptures[].name": "other", "classes[].localCaptures[].localId": "other", "classes[].symbolFields[].field": "other", "classes[].symbolFields[].globalId": "other", + "classes[].symbolFields[].wellKnown": "other", "classes[].fields[].name": "other", "classes[].methods[]": "method", "classes[].prototypeNames[]": "other", @@ -281,6 +283,10 @@ const REFERENCE_FIXTURES: Record IrModule> = { ...module(), classes: [{ name: "%m0.Cell", fields: [], instancePrototypeHelper: TARGET, loc }], }), + "classes[].stringTagHelper": () => ({ + ...module(), + classes: [{ name: "%m0.Cell", fields: [], stringTagHelper: TARGET, loc }], + }), "lib.exports[].fnName": () => ({ ...module(), lib: { diff --git a/packages/compiler/src/ir/function-references.ts b/packages/compiler/src/ir/function-references.ts index db474e9bb..6696a3c11 100644 --- a/packages/compiler/src/ir/function-references.ts +++ b/packages/compiler/src/ir/function-references.ts @@ -24,6 +24,7 @@ export function moduleFunctionReferences(mod: IrModule): string[] { for (const cls of mod.classes ?? []) { if (cls.prototypeDataHelper !== undefined) names.push(cls.prototypeDataHelper); if (cls.instancePrototypeHelper !== undefined) names.push(cls.instancePrototypeHelper); + if (cls.stringTagHelper !== undefined) names.push(cls.stringTagHelper); } return names; } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 0ec0043ce..4119ddcf3 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1477,6 +1477,9 @@ export interface IrClassDef { prototypeDataHelper?: string; /** Native instance to its actual prototype, including factory captures and subclasses. */ instancePrototypeHelper?: string; + /** Native instance to the string its inherited Symbol.toStringTag getter + * answers, read by the runtime for an instance seen as `unknown`. */ + stringTagHelper?: string; /** Public JavaScript fields record their creation in the shared property * bag. Its descriptors preserve presence and insertion order independently * of native layout order; field values remain in their native slots. */ @@ -1486,8 +1489,10 @@ export interface IrClassDef { * class of the chain sets `name` during construction, and then an own * `name` follows every other own property. */ errorNameAfter?: string | null; - /** Stable module symbol identities for public instance layout fields. */ - symbolFields?: { field: string; globalId: string }[]; + /** Stable symbol identities for public instance layout fields: the + * module global holding the key, or the name of the well-known symbol + * (`toStringTag` for `Symbol.toStringTag`). */ + symbolFields?: ({ field: string } & ({ globalId: string } | { wellKnown: string }))[]; /** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC * helpers, and vtable live in the runtime (ScrError / scr_error_*), so * backends emit no definitions for it — only the preorder-interval diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index c5b7e38f8..bd79d965b 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -501,10 +501,29 @@ export function validateModule(mod: IrModule): IrValidationError[] { }); } } - for (const symbol of cls.symbolFields ?? []) { - const global = mod.globals?.find((global) => global.id === symbol.globalId); + if (cls.stringTagHelper !== undefined) { + const helper = functionsByName.get(cls.stringTagHelper); + const receiver = helper?.params[0]?.type; if ( - global?.type.kind !== "symbol" || + !helper || + helper.params.length !== 1 || + receiver?.kind !== "object" || + receiver.className !== cls.name || + helper.returnType.kind !== "string" || + helper.captures !== undefined || + helper.classCaptures !== undefined + ) { + errors.push({ + message: `class ${cls.name}: string tag helper must be a noncapturing (instance) => string function`, + loc: cls.loc, + }); + } + } + for (const symbol of cls.symbolFields ?? []) { + const global = + "globalId" in symbol ? mod.globals?.find((global) => global.id === symbol.globalId) : null; + if ( + ("globalId" in symbol ? global?.type.kind !== "symbol" : symbol.wellKnown === "") || !cls.fields.some((field) => field.name === symbol.field) ) { errors.push({ message: `class ${cls.name}: invalid symbol field metadata`, loc: cls.loc }); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7751-class-well-known-symbol-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7751-class-well-known-symbol-fields.ts.json new file mode 100644 index 000000000..2985bdf05 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7751-class-well-known-symbol-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7751-class-well-known-symbol-fields.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 9f7ddc527..21fed207e 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -398,6 +398,10 @@ typedef struct ScrDynTypedClassOps { size_t count; ScrStr *const *names; /* immortal member-name strings */ ScrStr *class_name; /* immortal constructor name; NULL for an anonymous class */ + /* The string an inherited Symbol.toStringTag getter answers for the + * instance (+1), or NULL with a pending exception from the getter; + * NULL when the class has no such getter. */ + ScrStr *(*string_tag)(void *instance); } ScrDynTypedClassOps; /* A compiled JavaScript class whose instances record their own properties @@ -915,6 +919,7 @@ void scr_dyn_install_iterator_symbol(ScrDyn *key); void scr_dyn_install_has_instance_symbol(ScrDyn *key); void scr_dyn_install_async_iterator_symbol(ScrDyn *key); void scr_dyn_install_async_dispose_symbol(ScrDyn *key); +void scr_dyn_install_to_string_tag_symbol(ScrDyn *key); /* True when key is Symbol.asyncDispose (registered by the symbol unit when * the program names it); handle symbol_get ops outside that unit use it. */ bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key); @@ -1072,6 +1077,9 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec); /* Attach the prototype member names of a class instance capsule * (idempotent), so `in` sees its methods and accessors. */ void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops); +/* A class instance capsule's inherited Symbol.toStringTag string from its + * class's getter (+1); NULL when it has none or the getter threw. */ +ScrStr *scr_dyn_typed_ref_class_tag(const ScrDyn *d); /* Attach the bag and prototype hooks of a capsule of a bag-tracked class * instance (idempotent). */ void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 1dce8d3c2..6777ef1ec 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1207,6 +1207,19 @@ static bool insp_tag_names_class(const char *tag, const char *cls) { memcmp(tag + tag_len - cls_len, cls, cls_len) == 0; } +/* The Symbol.toStringTag a class instance inherits from its class's + * getter (+1), which Node brackets after the class name when it is a + * nonempty string other than the name. An own tag field is listed with + * the fields instead. */ +static ScrStr *insp_inherited_tag(const ScrDyn *instance, const ScrStr *name) { + ScrStr *tag = scr_dyn_typed_ref_class_tag(instance); + if (tag && (tag->len == 0 || (tag->len == name->len && memcmp(tag->data, name->data, name->len) == 0))) { + scr_str_release(tag); + return NULL; + } + return tag; +} + /* A native Set or Map held as a checked-dynamic value: `Set(n) { a, b }`, * `Map(n) { k => v }`, at most 100 entries like Node's maxArrayLength. */ /* Node's formatWeakCollection: the entries of a WeakMap or WeakSet are not @@ -1725,10 +1738,23 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { free(cls); } else if (name && materialized->kind == SCR_DYN_OBJ && scr_dyn_obj_get(materialized, "%error", 6) == NULL) { - char *cls = malloc(name->len + 1); + ScrStr *tag = insp_inherited_tag(d, name); + if (scr_exc_pending()) { + scr_dyn_release(materialized); + return scr_str_new("", 0); + } + size_t tag_len = tag ? tag->len + 3 : 0; + char *cls = malloc(name->len + tag_len + 1); if (!cls) scr_trap("scriptc: out of memory\n"); memcpy(cls, name->data, name->len); - cls[name->len] = '\0'; + if (tag) { + /* Node's `Name [tag]` prefix. */ + memcpy(cls + name->len, " [", 2); + memcpy(cls + name->len + 2, tag->data, tag->len); + cls[name->len + tag_len - 1] = ']'; + scr_str_release(tag); + } + cls[name->len + tag_len] = '\0'; size_t nseen = g_nseen; g_pending_seen = instance; out = scr_insp_obj(materialized, recurse, depth, cls); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index a036e0dfd..fe890febd 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -202,6 +202,19 @@ void scr_dyn_install_has_instance_symbol(ScrDyn *key) { scr_atexit(scr_has_instance_symbol_cleanup); } +/* Symbol.toStringTag, installed when the program first evaluates it: no + * object holds the key before then. */ +static SCR_TL ScrDyn *scr_to_string_tag_symbol; +static void scr_to_string_tag_symbol_cleanup(void) { + scr_dyn_release(scr_to_string_tag_symbol); + scr_to_string_tag_symbol = NULL; +} +void scr_dyn_install_to_string_tag_symbol(ScrDyn *key) { + if (scr_to_string_tag_symbol) return; + scr_to_string_tag_symbol = scr_dyn_retain(key); + scr_atexit(scr_to_string_tag_symbol_cleanup); +} + void scr_dyn_install_iterator_symbol(ScrDyn *key) { if (scr_iterator_symbol) return; scr_iterator_symbol = scr_dyn_retain(key); @@ -3884,6 +3897,12 @@ static bool scr_dyn_name_in_strs(const ScrStr *key, ScrStr *const *names, size_t return false; } +ScrStr *scr_dyn_typed_ref_class_tag(const ScrDyn *d) { + if (!d || d->kind != SCR_DYN_TYPED_REF || d->prototype) return NULL; + const ScrDynTypedClassOps *ops = scr_typed_ref_class_ops(d); + return ops && ops->string_tag ? ops->string_tag(d->v.typed_ref.ptr) : NULL; +} + bool scr_dyn_typed_ref_class_has(const ScrDyn *d, const ScrStr *key) { if (!d || d->kind != SCR_DYN_TYPED_REF || d->prototype) return false; const ScrDynTypedClassOps *ops = scr_typed_ref_class_ops(d); @@ -7616,11 +7635,45 @@ void scr_dyn_throw(ScrDyn *d) { scr_dyn_trace_v, scr_dyn_is_object(d)); } +static ScrStr *scr_dyn_tag_string(const ScrStr *tag) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "[object "); + scr_jb_write(&b, tag->data, tag->len); + scr_jb_putc(&b, ']'); + return scr_jb_finish(&b); +} + +/* Object.prototype.toString of an object whose own or inherited + * @@toStringTag is a string (+1), NULL otherwise or with a pending + * exception from its getter. */ +static ScrStr *scr_dyn_string_tag(const ScrDyn *d) { + ScrStr *getter_tag = scr_dyn_typed_ref_class_tag(d); + if (getter_tag) { + ScrStr *out = scr_dyn_tag_string(getter_tag); + scr_str_release(getter_tag); + return out; + } + if (scr_exc_pending() || !scr_to_string_tag_symbol) return NULL; + ScrDyn *tag = scr_dyn_symbol_key_get(d, scr_to_string_tag_symbol, true); + if (!tag) return NULL; + ScrStr *out = tag->kind == SCR_DYN_STR ? scr_dyn_tag_string(tag->v.str) : NULL; + scr_dyn_release(tag); + return out; +} + ScrStr *scr_dyn_object_tag(const ScrDyn *d) { if (d->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Symbol.toStringTag lookup"); return NULL; } + /* Object.prototype.toString answers a string @@toStringTag over the + * builtin tag of a plain object or a class instance. */ + if ((d->kind == SCR_DYN_OBJ || (d->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_error(d) && + !scr_dyn_generator(d)))) { + ScrStr *tag = scr_dyn_string_tag(d); + if (tag || scr_exc_pending()) return tag; + } if (d->kind == SCR_DYN_TYPED_REF) { if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14); if (scr_dyn_generator(d)) { @@ -8838,6 +8891,22 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) { return NULL; } if (d->kind == SCR_DYN_TYPED_REF) { + /* A class instance without its own conversion methods converts with + * Object.prototype.toString, which reads its toStringTag getter. */ + ScrStr *to_string_key = scr_str_new("toString", 8), *value_of_key = scr_str_new("valueOf", 7); + bool own_conversion = scr_dyn_typed_ref_class_has(d, to_string_key) || + scr_dyn_typed_ref_class_has(d, value_of_key); + scr_str_release(to_string_key); + scr_str_release(value_of_key); + if (!own_conversion) { + ScrStr *tag = scr_dyn_typed_ref_class_tag(d); + if (tag) { + ScrStr *out = scr_dyn_tag_string(tag); + scr_str_release(tag); + return out; + } + if (scr_exc_pending()) return NULL; + } ScrDyn *materialized = scr_dyn_typed_ref_string_view(d); ScrStr *out = scr_dyn_string_coerce_js(materialized); scr_dyn_release(materialized); @@ -8858,7 +8927,7 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) { scr_error_release(error); return text; } - return scr_str_new("[object Object]", 15); + return scr_dyn_object_tag(d); } if (!m || m->kind != SCR_DYN_FUNC) { scr_dyn_release(m); continue; } /* OrdinaryToPrimitive performs a method call, not a bare function diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 647c31dc3..727e008b5 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -90,6 +90,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) { scr_dyn_install_has_instance_symbol(key); scr_dyn_release(key); } + if (name->len == 11 && !memcmp(name->data, "toStringTag", 11)) { + ScrDyn *key = scr_dyn_new_symbol(symbol); + scr_dyn_install_to_string_tag_symbol(key); + scr_dyn_release(key); + } if (name->len == 13 && !memcmp(name->data, "asyncIterator", 13)) { ScrDyn *key = scr_dyn_new_symbol(symbol); scr_dyn_install_async_iterator_symbol(key); diff --git a/tests/corpus/7751-class-well-known-symbol-fields.ts b/tests/corpus/7751-class-well-known-symbol-fields.ts new file mode 100644 index 000000000..445af40d6 --- /dev/null +++ b/tests/corpus/7751-class-well-known-symbol-fields.ts @@ -0,0 +1,76 @@ +// Class fields keyed by well-known symbols, Symbol.toStringTag first (the +// lru-cache shape), beside toStringTag getters and module symbol keys. +// String conversion, Object.prototype.toString and util.inspect read the +// tag as Node does, through typed values and through `unknown`. + +const limit = Symbol("limit"); + +class Cache { + size = 0; + [limit] = 10; + [Symbol.toStringTag] = "LRUCache"; +} + +class Sub extends Cache { + extra = 1; +} + +class Retag extends Cache { + constructor() { + super(); + this[Symbol.toStringTag] = "Retagged"; + } +} + +class Tagged { + x = 1; + get [Symbol.toStringTag](): string { + return "Custom"; + } +} + +class TaggedSub extends Tagged { + y = 2; +} + +class Named { + get [Symbol.toStringTag](): string { + return "Named"; + } +} + +class Blank { + get [Symbol.toStringTag](): string { + return ""; + } +} + +class Plain { + n = 3; +} + +const c = new Cache(); +console.log(String(c), `${c}`, Object.prototype.toString.call(c), c[Symbol.toStringTag]); +console.log(c); +console.log(Object.keys(c), JSON.stringify(c)); +console.log(new Sub(), String(new Sub())); +const r: Cache = new Retag(); +console.log(String(r), r); +c[Symbol.toStringTag] = "Other"; +console.log(`${c}`, c); + +const t = new Tagged(); +console.log(String(t), `${t}`, t, [t], { deep: { deeper: { t } } }); +const ts: Tagged = new TaggedSub(); +console.log(String(ts), ts); +console.log(new Named(), new Blank(), String(new Blank())); +console.log(String(new Plain()), new Plain()); + +const values: unknown[] = [new Cache(), new Tagged(), new TaggedSub(), new Plain()]; +for (const v of values) { + console.log(Object.prototype.toString.call(v), String(v), (v as { [Symbol.toStringTag]?: unknown })[Symbol.toStringTag]); +} +console.log(values); + +const literal: unknown = { [Symbol.toStringTag]: "Literal", a: 1 }; +console.log(Object.prototype.toString.call(literal), String(literal), `${literal}`); diff --git a/tests/harness/corpus-costs/7751-class-well-known-symbol-fields.ts.json b/tests/harness/corpus-costs/7751-class-well-known-symbol-fields.ts.json new file mode 100644 index 000000000..5ed28c9dd --- /dev/null +++ b/tests/harness/corpus-costs/7751-class-well-known-symbol-fields.ts.json @@ -0,0 +1 @@ +{ "plain": 3800, "san": 4300 } From 9eb14207a63e971c46a66b76c5a796d3702a7a92 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:53:36 -0500 Subject: [PATCH 162/409] Revert "Cache untyped property access in programs that declare classes (reland)" This reverts #2017 (adb8690f1), which made main CI fail. --- benchmarks/runtime/README.md | 1 - .../runtime/workloads/dynamic-properties.json | 6 - .../runtime/workloads/dynamic-properties.mjs | 56 ------ packages/compiler/src/ir/js-ic.test.ts | 188 ------------------ packages/compiler/src/ir/js-ic.ts | 92 +-------- packages/compiler/test/js-ic-emission.test.ts | 44 ---- packages/runtime/src/scr_js.h | 11 +- packages/runtime/src/scr_js_object.c | 32 +-- packages/runtime/src/scr_js_object.test.c | 26 +-- scripts/zero-cost-compare.mjs | 5 +- tests/harness/library-mode.test.ts | 20 -- tests/library-mode/js-ic-sessions/lib.ts | 6 - tests/library-mode/js-ic-sessions/probe.c | 25 --- .../library-mode/js-ic-sessions/profile.json | 14 -- tests/library-mode/js-ic-sessions/work.mjs | 26 --- 15 files changed, 31 insertions(+), 521 deletions(-) delete mode 100644 benchmarks/runtime/workloads/dynamic-properties.json delete mode 100644 benchmarks/runtime/workloads/dynamic-properties.mjs delete mode 100644 tests/library-mode/js-ic-sessions/lib.ts delete mode 100644 tests/library-mode/js-ic-sessions/probe.c delete mode 100644 tests/library-mode/js-ic-sessions/profile.json delete mode 100644 tests/library-mode/js-ic-sessions/work.mjs diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 04c90f7bb..4f152e4bb 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -13,7 +13,6 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner | | `dictionary-walks` | cpu | `Record` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted | | `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods | -| `dynamic-properties` | cpu | untyped JavaScript reading and writing properties of values it does not type: records built by object literals in two key orders, nested records, and instances of a class and its subclass | | `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | | `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation | diff --git a/benchmarks/runtime/workloads/dynamic-properties.json b/benchmarks/runtime/workloads/dynamic-properties.json deleted file mode 100644 index 398981ef3..000000000 --- a/benchmarks/runtime/workloads/dynamic-properties.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "entry": "workloads/dynamic-properties.mjs", - "args": [ - "1000000" - ] -} diff --git a/benchmarks/runtime/workloads/dynamic-properties.mjs b/benchmarks/runtime/workloads/dynamic-properties.mjs deleted file mode 100644 index d0ee3802c..000000000 --- a/benchmarks/runtime/workloads/dynamic-properties.mjs +++ /dev/null @@ -1,56 +0,0 @@ -// Untyped JavaScript reading and writing properties of values it never -// declares types for: records built by object literals in two key orders, -// nested records, property updates, and instances of a class and its -// subclass read and written the same way. Argument: the number of -// iterations. -const rounds = Number(process.argv[2] ?? "1000000"); -class Cursor { - constructor(x, y) { - this.x = x; - this.y = y; - } - step(dx, dy) { - this.x += dx; - this.y += dy; - return this; - } -} -class Marker extends Cursor { - constructor(x, y, label) { - super(x, y); - this.label = label; - } -} -function makeItem(i) { - if (i % 3 === 0) - return { id: i, price: i % 17, qty: i % 5, meta: { tag: i % 7, weight: i % 11 } }; - return { id: i, qty: i % 5, price: i % 13, meta: { weight: i % 9, tag: i % 5 } }; -} -function cost(item) { - return item.price * item.qty + item.meta.weight; -} -function restock(item, round) { - item.qty = (item.qty + round) % 7; - item.meta.tag = (item.meta.tag + 1) % 7; -} -function distance(a, b) { - return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); -} -const items = []; -for (let i = 0; i < 1024; i++) items.push(makeItem(i)); -const cursors = []; -for (let i = 0; i < 64; i++) - cursors.push(i % 2 === 0 ? new Cursor(i - 32, 7 - i) : new Marker(i - 32, 7 - i, "m" + i)); -const origin = { x: 0, y: 0 }; -let checksum = 0; -for (let r = 0; r < rounds; r++) { - const item = items[r & 1023]; - checksum += cost(item); - restock(item, r & 3); - checksum += item.meta.tag; - const cursor = cursors[r & 63]; - checksum += distance(cursor.step((r % 3) - 1, (r % 5) - 2), origin); - if (cursor.x > 100 || cursor.x < -100) cursor.x = 0; - if (cursor.y > 100 || cursor.y < -100) cursor.y = 0; -} -console.log(checksum, items[5].qty, items[6].meta.tag, cursors[3].x, cursors[3].y, cursors[3].label); diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index f5391b83e..f71a1e9db 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -126,140 +126,6 @@ test("property helpers whose class branches only test capsules are cache sites", expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" }); }); -test("bags converted between dyn and jsval are still the receiver", () => { - // A program that declares a class returns its bags as jsvals, and the - // helpers read them through js.toDyn. - const toDyn = (value: IrExpr): IrExpr => ({ - kind: "libCall", - fn: "js.toDyn", - args: [value], - type: DYN, - loc, - }); - const fromDyn = (value: IrExpr): IrExpr => ({ - kind: "libCall", - fn: "js.fromDyn", - args: [value], - type: { kind: "jsval" }, - loc, - }); - const jsvalBag = helper( - "%dyn.class.readPropertiesOrEmpty.js", - ["v"], - [capsuleBranch("v"), ret(fromDyn(ref("v")))], - ); - const staticBag = helper("%dyn.class.staticProperties", ["v"], [ret(ref("v"))]); - const getter = getHelper( - "%dyn.class.property.0", - "x", - [capsuleBranch("p.0")], - call(staticBag.name, toDyn(call(jsvalBag.name, ref("p.0")))), - ); - const setter = setHelper("%dyn.class.property.1", "y"); - const write = setter.body[0] as Extract; - const keySet = write.expr as Extract; - keySet.args[0] = toDyn(call(jsvalBag.name, ref("p.0"))); - // A converted value that is not the receiver stays a helper call. - const foreign = getHelper( - "%dyn.class.property.2", - "x", - [], - toDyn(fromDyn({ kind: "dynObjLit", type: DYN, loc } as IrExpr)), - ); - const sites = sitesOf(jsvalBag, staticBag, getter, setter, foreign); - expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" }); - expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" }); - expect(sites.site("%dyn.class.property.2")).toBeNull(); -}); - -test("a bag helper may first rebind its parameter to a bag of it", () => { - // Programs with subclasses normalize the value before the class branches. - const normalize = helper("%dyn.class.normalize", ["v"], [capsuleBranch("v"), ret(ref("v"))]); - const rebind = (value: IrExpr): IrStmt => ({ kind: "assign", localId: "v", value, loc }); - const normalized = helper( - "%dyn.class.readPropertiesOrEmpty.normalized", - ["v"], - [rebind(call(normalize.name, ref("v"))), capsuleBranch("v"), ret(ref("v"))], - ); - const replaced = helper( - "%dyn.class.readPropertiesOrEmpty.replaced", - ["v"], - [rebind({ kind: "dynObjLit", type: DYN, loc } as IrExpr), ret(ref("v"))], - ); - const sites = sitesOf( - normalize, - normalized, - replaced, - getHelper("%dyn.class.property.0", "x", [], call(normalized.name, ref("p.0"))), - getHelper("%dyn.class.property.1", "x", [], call(replaced.name, ref("p.0"))), - // The property helper itself normalizes its receiver first, too. - getHelper("%dyn.class.property.2", "x", [ - { kind: "assign", localId: "p.0", value: call(normalize.name, ref("p.0")), loc }, - capsuleBranch("p.0"), - ]), - ); - expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" }); - expect(sites.site("%dyn.class.property.1")).toBeNull(); - expect(sites.site("%dyn.class.property.2")).toEqual({ kind: "get", key: "x" }); -}); - -test("keyed helpers may test capsules between the key's coercion and the read", () => { - const read = (object: IrExpr, key: string): IrExpr => ({ - kind: "libCall", - fn: "js.fromDyn", - args: [{ kind: "dynKeyGet", value: object, key: ref(key), type: DYN, loc }], - type: { kind: "jsval" }, - loc, - }); - const guard = (test: "symbol" | "nullish", tested: string, object: IrExpr): IrStmt => ({ - kind: "if", - cond: { kind: "dynTest", test, value: ref(tested), type: BOOL, loc }, - then: [ret(read(object, "p.key"))], - else_: null, - loc, - }); - const keyed = (name: string, between: IrStmt[]): IrFunction => - helper( - name, - ["p.0", "p.key"], - [ - capsuleBranch("p.0"), - guard("symbol", "p.key", call(identityBag.name, ref("p.0"))), - guard("nullish", "p.0", ref("p.0")), - { - kind: "varDecl", - localId: "key.string", - init: { - kind: "libCall", - fn: "dyn.toStringCoerce", - args: [ref("p.key")], - type: STRING, - loc, - }, - loc, - }, - ...between, - ret(read(call(identityBag.name, ref("p.0")), "key.string")), - ], - ["key.string"], - ); - const errorBranch: IrStmt = { - kind: "if", - cond: { kind: "dynTest", test: "error", value: ref("p.0"), type: BOOL, loc }, - then: [ret(ref("p.0"))], - else_: null, - loc, - }; - const sites = sitesOf( - keyed("%dyn.class.computed.0", []), - keyed("%dyn.class.computed.1", [capsuleBranch("p.0"), capsuleBranch("p.0", "dyn.classIs")]), - keyed("%dyn.class.computed.2", [errorBranch]), - ); - expect(sites.site("%dyn.class.computed.0")).toEqual({ kind: "keyed" }); - expect(sites.site("%dyn.class.computed.1")).toEqual({ kind: "keyed" }); - expect(sites.site("%dyn.class.computed.2")).toBeNull(); -}); - test("any other prefix or tail keeps the helper call", () => { const errorBranch: IrStmt = { kind: "if", @@ -371,57 +237,3 @@ test("bag helpers that exit early for values no class branch matches are still t expect(sites.site("%dyn.class.property.2")).toBeNull(); expect(sites.site("%dyn.class.property.3")).toBeNull(); }); - -test("helpers that answer plain values first with the same operation are cache sites", () => { - const negated = (test: "classCapsule" | "funcKind"): IrExpr => ({ - kind: "dynTest", - test, - negated: true, - value: ref("p.0"), - type: BOOL, - loc, - }); - const exit = (body: IrStmt[], test: IrExpr = negated("classCapsule")): IrStmt => ({ - kind: "if", - cond: test, - then: [{ kind: "if", cond: negated("funcKind"), then: body, else_: null, loc }], - else_: null, - loc, - }); - const store = (key: string): IrStmt[] => [ - { - kind: "exprStmt", - expr: { - kind: "libCall", - fn: "dyn.keySet", - args: [ref("p.0"), str(key), ref("p.1")], - type: VOID, - loc, - }, - loc, - }, - ret(null), - ]; - const load = (key: string): IrStmt[] => [ - ret({ kind: "dynKeyGet", value: ref("p.0"), key: str(key), type: DYN, loc }), - ]; - const sites = sitesOf( - setHelper("%dyn.class.property.0", "x", [exit(store("x")), capsuleBranch("p.0")]), - getHelper("%dyn.class.property.1", "x", [exit(load("x")), capsuleBranch("p.0")]), - // An exit for another key, or one plain values do not take. - setHelper("%dyn.class.property.2", "x", [exit(store("y"))]), - setHelper("%dyn.class.property.3", "x", [ - exit(store("x"), { - kind: "dynTest", - test: "classCapsule", - value: ref("p.0"), - type: BOOL, - loc, - }), - ]), - ); - expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "set", key: "x" }); - expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "get", key: "x" }); - expect(sites.site("%dyn.class.property.2")).toBeNull(); - expect(sites.site("%dyn.class.property.3")).toBeNull(); -}); diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 48019636a..752316db3 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -61,68 +61,13 @@ export class JsIcSites { if (fn.params.some((p) => p.type.kind !== "dyn")) return null; const receiver = fn.params[0]?.localId; if (receiver === undefined) return null; - const second = fn.params[1]?.localId; - const exits: JsIcSite[] = []; let i = 0; - for (; i < fn.body.length; i++) { - const s = fn.body[i]!; - if (this.capsuleBranch(s, receiver) || this.rebinding(s, receiver)) continue; - const exit = this.plainExit(s, receiver, second); - if (exit === null) break; - exits.push(exit); - } + while (i < fn.body.length && this.capsuleBranch(fn.body[i]!, receiver)) i++; const tail = fn.body.slice(i); - let site: JsIcSite | null; - if (fn.params.length === 1) site = this.getTail(tail, receiver); - else if (fn.params.length !== 2 || second === undefined) return null; - else site = this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second); - if (site === null || site.kind === "keyed") return exits.length === 0 ? site : null; - // An exit answers plain values with the operation the site caches. - return exits.every((exit) => exit.kind === site.kind && exit.key === site.key) ? site : null; - } - - /** `if (!classCapsule(receiver)) { [if (!funcKind(receiver))] OP }`, no - * else: an exit that runs the helper's generic operation directly on a - * value no class table matches. OP is `return [js.fromDyn](dynKeyGet(R, - * K))` or `dyn.keySet(R, K, value); return`, with R the receiver (or a - * bag of it). */ - private plainExit(s: IrStmt, receiver: string, value: string | undefined): JsIcSite | null { - const negated = (e: IrExpr, test: "classCapsule" | "funcKind"): boolean => - e.kind === "dynTest" && e.test === test && e.negated === true && isVar(e.value, receiver); - if (s.kind !== "if" || s.else_ !== null || !negated(s.cond, "classCapsule")) return null; - let body = s.then; - const inner = body[0]; - if ( - body.length === 1 && - inner?.kind === "if" && - inner.else_ === null && - negated(inner.cond, "funcKind") - ) - body = inner.then; - const [first, ret] = body; - if (body.length === 1 && first?.kind === "return") { - const read = unwrapFromDyn(first.value); - if ( - read?.kind !== "dynKeyGet" || - read.key.kind !== "strLit" || - !this.bagOf(read.value, receiver) - ) - return null; - return { kind: "get", key: read.key.value }; - } - if ( - body.length !== 2 || - first?.kind !== "exprStmt" || - ret?.kind !== "return" || - ret.value !== null - ) - return null; - const call = first.expr; - if (call.kind !== "libCall" || call.fn !== "dyn.keySet" || call.args.length !== 3) return null; - const [bag, key, stored] = call.args; - if (!bag || !this.bagOf(bag, receiver) || key?.kind !== "strLit") return null; - if (value === undefined || !isVar(stored, value)) return null; - return { kind: "set", key: key.value }; + if (fn.params.length === 1) return this.getTail(tail, receiver); + if (fn.params.length !== 2) return null; + const second = fn.params[1]!.localId; + return this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second); } /** `varDecl bag = BAG(receiver); return [js.fromDyn](bag === receiver ? @@ -164,14 +109,10 @@ export class JsIcSites { /** `if (typeof key === "symbol") return dynKeyGet(BAG(receiver), key); * if (receiver == null) return dynKeyGet(receiver, key); * const s = String(key); return dynKeyGet(BAG(receiver), s)`, each read - * possibly under js.fromDyn. Capsule branches may precede the final read: - * a program that declares classes looks the string key up in each class's - * key table there. */ + * possibly under js.fromDyn. */ private keyedTail(tail: IrStmt[], receiver: string, key: string): JsIcSite | null { - const [symbol, nullish, decl] = tail; - const ret = tail[tail.length - 1]; - if (tail.length < 4) return null; - if (!tail.slice(3, -1).every((s) => this.capsuleBranch(s, receiver))) return null; + const [symbol, nullish, decl, ret] = tail; + if (tail.length !== 4) return null; if (!this.guard(symbol, "symbol", key, (read) => this.bagOf(read, receiver), key)) return null; if (!this.guard(nullish, "nullish", receiver, (read) => isVar(read, receiver), key)) return null; @@ -218,12 +159,9 @@ export class JsIcSites { } /** Whether `e` is the receiver itself for every ordinary object or array: - * the receiver, or a chain of property-bag helpers applied to it. A - * conversion between the dyn and jsval forms keeps the object it holds. */ + * the receiver, or a chain of property-bag helpers applied to it. */ private bagOf(e: IrExpr, receiver: string): boolean { if (isVar(e, receiver)) return true; - if (e.kind === "libCall" && (e.fn === "js.toDyn" || e.fn === "js.fromDyn")) - return e.args.length === 1 && this.bagOf(e.args[0]!, receiver); return ( e.kind === "call" && e.args.length === 1 && @@ -244,13 +182,6 @@ export class JsIcSites { return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param); } - /** `param = `, such as the subclass normalization - * `value = %dyn.class.normalize(value)`, which leaves an ordinary object - * or array in `param`. */ - private rebinding(s: IrStmt, param: string): boolean { - return s.kind === "assign" && s.localId === param && this.bagOf(s.value, param); - } - /** A property-bag helper that returns its argument unless a capsule * branch is taken. */ private identityHelper(name: string): boolean { @@ -273,10 +204,7 @@ export class JsIcSites { identity = fn.body .slice(0, -1) - .every( - (s) => - this.capsuleBranch(s, param) || this.kindExit(s, param) || this.rebinding(s, param), - ) && + .every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) && last?.kind === "return" && last.value !== null && this.bagOf(last.value, param); diff --git a/packages/compiler/test/js-ic-emission.test.ts b/packages/compiler/test/js-ic-emission.test.ts index 54ea13d26..47d641bff 100644 --- a/packages/compiler/test/js-ic-emission.test.ts +++ b/packages/compiler/test/js-ic-emission.test.ts @@ -64,50 +64,6 @@ console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n); } }); -test("property sites keep their caches in programs that declare classes and subclasses", async () => { - const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-class-")); - try { - const entry = join(dir, "main.js"); - const outPath = join(dir, "main.ir.json"); - // Declared classes give every property helper class branches and - // property-bag lookups, and a subclass adds a normalization step; plain - // objects still read through the caches. - await writeFile( - entry, - ` -class Point { - constructor(x) { this.x = x; } -} -class Tagged extends Point { - constructor(x, tag) { super(x); this.tag = tag; } -} -function dyn(v) { return v; } -function read(o) { return o.x; } -function write(o, v) { o.x = v; } -function at(a, i) { return a[i]; } -var o = dyn({ x: 1 }); -var p = dyn(new Point(4)); -var t = dyn(new Tagged(6, "t")); -write(o, 3); -write(p, 5); -write(t, 7); -console.log(read(o), read(p), read(t), at(dyn([5, 6]), 1), at(p, "x"), at(t, "tag")); -`, - ); - const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); - if (!result.ok) - throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - const mod = deserializeModule(await readFile(outPath, "utf8")); - expect(validateModule(mod)).toEqual([]); - const llvm = emitLlvmModule(mod); - expect(body(llvm, "read")).toContain("@scr_js_load_probe("); - expect(body(llvm, "write")).toContain("@scr_js_store_probe("); - expect(body(llvm, "at")).toContain("@scr_js_keyed_probe("); - } finally { - await rm(dir, { recursive: true, force: true }); - } -}); - test("typed programs have no cache sites and keep the ordinary runtime", async () => { const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-typed-")); try { diff --git a/packages/runtime/src/scr_js.h b/packages/runtime/src/scr_js.h index 1d874b31e..6fa20fdb8 100644 --- a/packages/runtime/src/scr_js.h +++ b/packages/runtime/src/scr_js.h @@ -195,13 +195,10 @@ bool scr_js_truthy(ScrJsVal value); * index and which keys an object lacks. * * Each property site of compiled dynamic-tier code owns one zeroed, - * thread-local ScrJsIc with static storage: the reset that frees a - * thread's shapes (at thread exit, and when a library session ends) clears - * every record that holds entries. The code at the site is fixed: it tests - * entry 0 inline and calls a probe for everything else. Probes never run - * user code: they answer from plain data properties or return a miss - * (NULL / false) with nothing done, and the site then runs its generic - * path. + * thread-local ScrJsIc. The code at the site is fixed: it tests entry 0 + * inline and calls a probe for everything else. Probes never run user + * code: they answer from plain data properties or return a miss (NULL / + * false) with nothing done, and the site then runs its generic path. * * load entry: shape = receiver shape; target = NULL for an own property * (slot indexes the receiver's entries), else the shape of diff --git a/packages/runtime/src/scr_js_object.c b/packages/runtime/src/scr_js_object.c index 0e0ec0908..63929ab57 100644 --- a/packages/runtime/src/scr_js_object.c +++ b/packages/runtime/src/scr_js_object.c @@ -18,7 +18,6 @@ #include "scr_key.h" #include -#include /* The compiler's inline cache tests read these in place (64-bit targets; * backend/llvm/js-ic.ts). */ @@ -38,10 +37,10 @@ _Static_assert(sizeof(ScrJsIcEntry) == 40 && sizeof(ScrJsIc) == 168, "cache layo * take the generic path and its hash index. */ #define SCR_JS_SHAPE_MAX 64 -/* Shapes live until the thread exits or a library session ends, so a thread - * creates at most this many, holding at most this many key bytes. Once - * either budget is spent, objects whose key sequence has no shape yet stay - * unshaped; shapes that exist keep serving. */ +/* Shapes live until the thread exits, so a thread creates at most this + * many, holding at most this many key bytes. Once either budget is spent, + * objects whose key sequence has no shape yet stay unshaped; shapes that + * exist keep serving. */ #define SCR_JS_SHAPE_BUDGET ((size_t)1 << 16) #define SCR_JS_SHAPE_KEY_BYTES_BUDGET ((size_t)4 << 20) @@ -64,25 +63,12 @@ static SCR_TL size_t scr_js_transition_count; static SCR_TL size_t scr_js_shape_key_bytes; static SCR_TL bool scr_js_shapes_registered; -/* The cache records that hold entries. Entries name shapes and prototype - * objects, so the reset that frees the shapes also clears these records: a - * library's next session runs the same sites with the same records. */ -static SCR_TL ScrJsIc **scr_js_filled_ics; -static SCR_TL size_t scr_js_filled_count; -static SCR_TL size_t scr_js_filled_capacity; - /* The shape word of an object whose key sequence has no shape within the * budgets. No cache entry names it, so its accesses take the probes, * which answer with a miss until the key set changes. */ static ScrJsShape scr_js_unshaped; static void scr_js_shapes_reset(void) { - for (size_t i = 0; i < scr_js_filled_count; i++) - memset(scr_js_filled_ics[i], 0, sizeof *scr_js_filled_ics[i]); - free(scr_js_filled_ics); - scr_js_filled_ics = NULL; - scr_js_filled_count = 0; - scr_js_filled_capacity = 0; while (scr_js_all_shapes) { ScrJsShape *shape = scr_js_all_shapes; scr_js_all_shapes = shape->all_next; @@ -206,16 +192,6 @@ static size_t scr_js_shape_find(const ScrJsShape *shape, const char *key, size_t /* Fills a cache entry: `stale` (a matching entry whose hit failed) when * there is one, else a free or replaceable one. */ static void scr_js_ic_add(ScrJsIc *ic, uint32_t stale, ScrJsIcEntry entry) { - if (ic->count == 0) { - if (scr_js_filled_count == scr_js_filled_capacity) { - size_t capacity = scr_js_filled_capacity ? scr_js_filled_capacity * 2 : 64; - ScrJsIc **grown = realloc(scr_js_filled_ics, capacity * sizeof *grown); - if (!grown) scr_trap("scriptc: out of memory\n"); - scr_js_filled_ics = grown; - scr_js_filled_capacity = capacity; - } - scr_js_filled_ics[scr_js_filled_count++] = ic; - } uint32_t index; if (stale < ic->count) { index = stale; diff --git a/packages/runtime/src/scr_js_object.test.c b/packages/runtime/src/scr_js_object.test.c index 6b6934dea..a14aa9ff6 100644 --- a/packages/runtime/src/scr_js_object.test.c +++ b/packages/runtime/src/scr_js_object.test.c @@ -24,17 +24,11 @@ static ScrDyn *object_with(const ScrStr *key, double n) { return object; } -/* Cache records have static storage, as the compiled sites' do: the reset - * at exit clears every record that holds entries. Each check starts from - * an empty record. */ -static ScrJsIc load_ic; -static ScrJsIc store_ic; - /* Whether a load probe answers `object.key` and returns `n`. */ static bool probe_reads(ScrDyn *object, const ScrStr *key, double n) { - ScrJsIc *ic = &load_ic; - memset(ic, 0, sizeof *ic); - ScrDyn *value = scr_js_load_probe(object, key, ic); + ScrJsIc ic; + memset(&ic, 0, sizeof ic); + ScrDyn *value = scr_js_load_probe(object, key, &ic); if (!value) return false; assert(value->kind == SCR_DYN_NUM && value->v.num == n); scr_dyn_release(value); @@ -72,13 +66,14 @@ static void budget_bounds_shapes(size_t key_len, size_t budget) { /* A store probe that would add a new shape leaves the add to the generic * store; one that replaces through an existing shape still runs. */ - memset(&store_ic, 0, sizeof store_ic); + ScrJsIc ic; + memset(&ic, 0, sizeof ic); ScrStr *added = scr_str_new("added", 5); ScrDyn *value = scr_dyn_new_num(8); - assert(!scr_js_store_probe(object, added, value, &store_ic)); + assert(!scr_js_store_probe(object, added, value, &ic)); assert(object->v.obj.len == 1); - memset(&store_ic, 0, sizeof store_ic); - assert(scr_js_store_probe(object, first, value, &store_ic)); + memset(&ic, 0, sizeof ic); + assert(scr_js_store_probe(object, first, value, &ic)); assert(probe_reads(object, first, 8)); scr_dyn_release(value); scr_str_release(added); @@ -95,9 +90,10 @@ static void immortal_objects_stay_unshaped(void) { object->rc = SIZE_MAX; assert(!probe_reads(object, key, 3)); assert(object->v.obj.shape == NULL); - memset(&store_ic, 0, sizeof store_ic); + ScrJsIc ic; + memset(&ic, 0, sizeof ic); ScrDyn *value = scr_dyn_new_num(4); - assert(!scr_js_store_probe(object, key, value, &store_ic)); + assert(!scr_js_store_probe(object, key, value, &ic)); assert(object->v.obj.shape == NULL); scr_dyn_release(value); scr_str_release(key); diff --git a/scripts/zero-cost-compare.mjs b/scripts/zero-cost-compare.mjs index f202c2ab7..160e33438 100644 --- a/scripts/zero-cost-compare.mjs +++ b/scripts/zero-cost-compare.mjs @@ -394,8 +394,8 @@ export function dynamicTierSymbols(names) { * not yet traced to its typed root. bench/thread-slots, * bench/channel-messages and bench/worker-messages, added later, link them * through worker messages: - * node:worker_threads values are dyn objects. bench/dynamic-methods and - * bench/dynamic-properties are untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and + * node:worker_threads values are dyn objects. bench/dynamic-methods is + * untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and * tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also * fails when a listed workload stops linking them, so remove a workload here * when a leak fix makes its binary clean. */ @@ -410,7 +410,6 @@ export const DYNAMIC_WORKLOADS = new Set([ "bench/thread-slots", "bench/channel-messages", "bench/dynamic-methods", - "bench/dynamic-properties", "bench/worker-messages", "bench/typed-array-scan-workers", "bench/log-summary", diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index b60c3ac54..db4fabb3e 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -363,17 +363,6 @@ globals: undefined fenced expect(run.stdout).toBe(SESSION + SESSION + SESSION); }); - test("K4: untyped property caches start empty in every session", async () => { - // The shapes a session's cache entries name are freed when it ends; - // the next session's sites must not find those entries. - const { archive, outDir } = await buildLibrary("js-ic-sessions", emission); - const probe = buildProbe("js-ic-sessions", archive, outDir); - const run = runProbe(probe); - expect(run.signal).toBeNull(); - expect(run.status).toBe(0); - expect(run.stdout).toBe("run: 20000\n".repeat(3)); - }); - /* ── K5/K6/K7: the trap channel ──────────────────────────────────────── */ test("K5: a trap delivers to the sink exactly once, host survives", async () => { @@ -593,15 +582,6 @@ survived, sink_calls=1 expect(run.stdout).toBe(SESSION + SESSION + SESSION); }); - platformTest("K10: property caches across sessions under ASan", async () => { - const { archive, outDir } = await buildLibrary("js-ic-sessions", emission, { sanitize: true }); - const probe = buildProbe("js-ic-sessions", archive, outDir, { sanitize: true }); - const run = runProbe(probe); - expect(run.signal).toBeNull(); - expect(run.status).toBe(0); - expect(run.stdout).toBe("run: 20000\n".repeat(3)); - }); - platformTest("K10: K5/K7 under ASan", async () => { const { archive, outDir } = await buildLibrary("traps", emission, { sanitize: true }); const probe = buildProbe("traps", archive, outDir, { sanitize: true }); diff --git a/tests/library-mode/js-ic-sessions/lib.ts b/tests/library-mode/js-ic-sessions/lib.ts deleted file mode 100644 index ebea59a54..000000000 --- a/tests/library-mode/js-ic-sessions/lib.ts +++ /dev/null @@ -1,6 +0,0 @@ -// The library's typed export over untyped code (work.mjs). -import { runAll } from "./work.mjs"; - -export function run(): number { - return runAll() as number; -} diff --git a/tests/library-mode/js-ic-sessions/probe.c b/tests/library-mode/js-ic-sessions/probe.c deleted file mode 100644 index 7dbcb56bf..000000000 --- a/tests/library-mode/js-ic-sessions/probe.c +++ /dev/null @@ -1,25 +0,0 @@ -/* Three sessions of a library whose untyped code reads and writes through - * property caches: each prints the same result, and under the sanitize - * flavor no session reads a cache entry the previous session freed. */ -#include -#include - -extern void kj_init(void); -extern void kj_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx); -extern void kj_collect(void); -extern double kj_run(void); - -static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) { - (void)ctx; (void)addr; - printf("UNEXPECTED SINK: %.*s", (int)len, (const char *)msg); -} - -int main(void) { - kj_set_panic_sink(sink, NULL); - for (int session = 0; session < 3; session++) { - kj_init(); - printf("run: %.0f\n", kj_run()); - kj_collect(); - } - return 0; -} diff --git a/tests/library-mode/js-ic-sessions/profile.json b/tests/library-mode/js-ic-sessions/profile.json deleted file mode 100644 index fd802b42c..000000000 --- a/tests/library-mode/js-ic-sessions/profile.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "profile_format": 1, - "name": "conformance-js-ic-sessions", - "entry": "lib.ts", - "emission": "llvm", - "abi": { - "prefix": "kj_", - "init_symbol": "kj_init", - "sink_register_symbol": "kj_set_panic_sink", - "collect_symbol": "kj_collect", - "result_reset_symbol": null - }, - "exports": [{ "export": "run", "symbol": "kj_run", "params": [], "returns": "f64" }] -} diff --git a/tests/library-mode/js-ic-sessions/work.mjs b/tests/library-mode/js-ic-sessions/work.mjs deleted file mode 100644 index 5c1afcd18..000000000 --- a/tests/library-mode/js-ic-sessions/work.mjs +++ /dev/null @@ -1,26 +0,0 @@ -// Dynamic-tier property caches across library sessions: objects built empty -// and filled through untyped stores, read back through untyped loads, and a -// class instance at the same kind of sites. Each session must start with -// empty caches, since the shapes their entries named are freed when the -// previous session ends. -class Box { - constructor(v) { - this.v = v; - } -} -function fill(o, n) { - o.a = n; - o.b = n + 1; - return o; -} -function sum(o) { - return o.a + o.b; -} -function unbox(b) { - return b.v; -} -export function runAll() { - let total = 0; - for (let i = 0; i < 100; i++) total += sum(fill({}, i)); - return total + unbox(new Box(total)); -} From fc229822e7ac3ba33c0e31da0a044854cba5dbf7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 15:56:08 -0500 Subject: [PATCH 163/409] Support X25519 keys and crypto.diffieHellman X25519 KeyObjects come from generateKeyPair(Sync)("x25519"), which keeps the private key clamped as OpenSSL does, and from createPrivateKey and createPublicKey of PKCS#8, SubjectPublicKeyInfo, JWK (OKP, X25519) and raw keys. They export in the same forms. sign, verify, createSign and createVerify throw OpenSSL's "operation not supported for this keytype", with its library, reason and code properties. X25519 uses mbedTLS's Curve25519 group. crypto.diffieHellman({ privateKey, publicKey }[, callback]) computes the X25519 and ECDH secrets of two keys of one type, given as KeyObjects or in any form createPrivateKey and createPublicKey read. The checks run in Node's order: the options, the callback, both keys present, the key types when both are KeyObjects ("Incompatible key types for Diffie-Hellman"), then the public and private key inputs. Mismatched curves, a small-order X25519 point and key types other than EC and X25519 throw OpenSSL's errors. The callback form passes the secret, or the error without the OpenSSL properties, in a later macrotask, as Node's DHBitsJob does. createPublicKey also reads a private key's DER encodings (pkcs8 and sec1), which Node accepts there. --- .../limitations/builtin-apis/crypto.mdx | 12 +- .../src/generated/node-v24-compatibility.json | 6 +- .../generated/node-v24-internal.json | 15 +- .../ambient/scriptc-node-fallback.d.ts | 23 ++ .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../src/frontend/lowering/builtins/crypto.ts | 4 +- .../src/frontend/lowering/surfaces.ts | 4 +- packages/compiler/src/frontend/type-mapper.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 1 + packages/compiler/src/ir/runtime-features.ts | 1 + packages/compiler/src/library/ir-policy.ts | 1 + packages/compiler/surface-manifest.json | 4 +- .../tests/corpus/7450-crypto-x25519.mjs.json | 6 + .../corpus/7451-crypto-diffie-hellman.ts.json | 6 + ...7452-crypto-diffie-hellman-errors.mjs.json | 6 + .../node-types/diffie-hellman.ts.json | 6 + packages/runtime/src/scr_crypto.h | 3 + packages/runtime/src/scr_pki.c | 339 ++++++++++++++++-- tests/corpus/7450-crypto-x25519.mjs | 90 +++++ tests/corpus/7451-crypto-diffie-hellman.ts | 55 +++ .../7452-crypto-diffie-hellman-errors.mjs | 61 ++++ tests/fixtures/node-types/diffie-hellman.ts | 19 + tests/fixtures/node-types/fenced.ts | 7 +- .../__snapshots__/node-types-fenced.txt | 35 +- .../corpus-costs/7450-crypto-x25519.mjs.json | 1 + .../7451-crypto-diffie-hellman.ts.json | 1 + ...7452-crypto-diffie-hellman-errors.mjs.json | 1 + tests/harness/project-config.test.ts | 18 + 29 files changed, 656 insertions(+), 73 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json create mode 100644 tests/corpus/7450-crypto-x25519.mjs create mode 100644 tests/corpus/7451-crypto-diffie-hellman.ts create mode 100644 tests/corpus/7452-crypto-diffie-hellman-errors.mjs create mode 100644 tests/fixtures/node-types/diffie-hellman.ts create mode 100644 tests/harness/corpus-costs/7450-crypto-x25519.mjs.json create mode 100644 tests/harness/corpus-costs/7451-crypto-diffie-hellman.ts.json create mode 100644 tests/harness/corpus-costs/7452-crypto-diffie-hellman-errors.mjs.json diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 964ca4ec9..329aa6809 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -27,22 +27,22 @@ A call on a DiffieHellman value whose arguments no @types/node overload accepts, or whose output encoding is typed `any`, is refused at compile time, as are its `Object.prototype` methods; other property reads, and property writes, throw a coded SC2020 error. `Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused. After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own. - `crypto.diffieHellman`, which takes DH, EC, or X25519 KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. + `crypto.diffieHellman` computes ECDH and X25519 secrets from EC and X25519 keys, given as KeyObjects or in any form `createPrivateKey` and `createPublicKey` read, with Node's errors; DH keys have no KeyObjects here, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available. `checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask. Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused. Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`. - RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. + RSA, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets. `generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback. RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error. The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1. - Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. - EC and Ed25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points. - Raw X25519, X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error. + Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 and X25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate. + EC, Ed25519, and X25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points. + Raw X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error. They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`. `crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm. Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export. Sign and Verify values accumulate `update()` data and are not writable streams. - Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x25519`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused. + Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x448`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused. Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report. `publicEncrypt`/`privateDecrypt` remain unsupported. The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index fc5826666..1039bc3d8 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -11476,9 +11476,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 83c318a0a..b3d91e453 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -20345,8 +20345,19 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.diffieHellman" + "status": "partial", + "evidence": "surface-manifest:node-builtin.crypto.diffieHellman", + "tests": [ + "tests/corpus/1358-crypto-random.ts", + "tests/corpus/1534-crypto-hash-chain.ts", + "tests/corpus/2556-crypto-introspection.ts", + "tests/corpus/2890-crypto-static-utility.ts", + "tests/harness/npm-static.test.ts", + "tests/corpus/node24-crypto-sha2.ts", + "tests/corpus/node24-crypto-kdf.ts", + "tests/corpus/node24-crypto-kdf-errors.ts", + "tests/corpus/node24-crypto-kdf-options.ts" + ] }, "anchorSource": "exact" }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 10ed35e68..cb08a4965 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -2992,6 +2992,24 @@ declare module "crypto" { options: ED25519KeyPairOptions<"der", "der">, ): KeyPairSyncResult; export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult; + export interface X25519KeyPairOptions { + publicKeyEncoding: { type: "spki"; format: PubF }; + privateKeyEncoding: { type: "pkcs8"; format: PrivF }; + } + export function generateKeyPairSync( + type: "x25519", + options: X25519KeyPairOptions<"pem", "pem">, + ): KeyPairSyncResult; + export function generateKeyPairSync( + type: "x25519", + options: X25519KeyPairOptions<"der", "der">, + ): KeyPairSyncResult; + export function generateKeyPairSync(type: "x25519", options?: {}): KeyPairKeyObjectResult; + export function generateKeyPair( + type: "x25519", + options: {} | undefined, + callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void, + ): void; export function generateKeyPair( type: "ed25519", options: {} | undefined, @@ -3020,6 +3038,11 @@ declare module "crypto" { export interface VerifyKeyObjectInput extends SigningOptions { key: KeyObject; } + export function diffieHellman(options: { privateKey: KeyObject; publicKey: KeyObject }): Buffer; + export function diffieHellman( + options: { privateKey: KeyObject; publicKey: KeyObject }, + callback: (err: Error | null, secret: Buffer) => void, + ): void; export function sign( algorithm: string | null | undefined, data: ArrayBufferView, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 9cdc36cf5..ade8aea5e 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1070,6 +1070,7 @@ export const LIB_FN_SYMS: Record = { "crypto.generateKeyPairSync": "scr_crypto_generate_key_pair_sync", "crypto.generateKeyPairCb": "scr_crypto_generate_key_pair_async", "crypto.signOneshot": "scr_crypto_sign_oneshot", + "crypto.diffieHellman": "scr_crypto_diffie_hellman", "crypto.verifyOneshot": "scr_crypto_verify_oneshot", "crypto.createSign": "scr_crypto_create_sign", "crypto.createVerify": "scr_crypto_create_verify", @@ -1654,6 +1655,7 @@ export function mockTimerSym( export const USES_TIMERS_LIB_FNS = new Set([ "crypto.generateKeyPairCb", + "crypto.diffieHellman", "crypto.checkPrimeCb", "crypto.generatePrimeCb", "process.emitWarning", diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index 051ec18c4..15c057462 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -1454,6 +1454,7 @@ function lowerPkiCall( ], sign: ["crypto.signOneshot", 3, 3, "sign(algorithm, data, key)"], verify: ["crypto.verifyOneshot", 4, 4, "verify(algorithm, data, key, signature)"], + diffieHellman: ["crypto.diffieHellman", 1, 2, "diffieHellman(options[, callback])"], createSign: ["crypto.createSign", 1, 1, "createSign(algorithm)"], createVerify: ["crypto.createVerify", 1, 1, "createVerify(algorithm)"], }; @@ -1474,6 +1475,7 @@ function lowerPkiCall( return { kind: "libCall", fn, args: values, type: VOID, loc }; const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc }; const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); - if (result === null || result.kind === "dyn") return call; + // diffieHellman's callback form answers undefined. + if (result === null || result.kind === "dyn" || result.kind === "void") return call; return lowerer.coerceInto(expr, call, result); } diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 44564f2f1..a54b41fd6 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1563,6 +1563,7 @@ export const BUILTIN_MODULE_FNS: Record< generateKeyPair: { fn: "crypto.generateKeyPairCb", params: [], result: VOID }, sign: { fn: "crypto.signOneshot", params: [], result: DYN }, verify: { fn: "crypto.verifyOneshot", params: [], result: DYN }, + diffieHellman: { fn: "crypto.diffieHellman", params: [], result: DYN }, pbkdf2Sync: { fn: "crypto.pbkdf2", params: [BYTES_U8, BYTES_U8, F64, F64, STRING], @@ -3101,9 +3102,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< "createSign, createVerify, createECDH, and createDiffieHellman", ]), ), - diffieHellman: - "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; " + - "the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets", setFips: "a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " + "in a non-FIPS build — getFips() answers 0 here", diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 45b9572b6..781c032e7 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -183,6 +183,7 @@ export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet = new Set([ "PrivateKeyInput", "PublicKeyInput", "RSAKeyPairKeyObjectOptions", + "X25519KeyPairOptions", "RSAKeyPairOptions", "SignKeyObjectInput", "SignPrivateKeyInput", diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index b517d43a1..5dd437c7a 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -1031,6 +1031,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.generateKeyPairCb", "crypto.signOneshot", "crypto.verifyOneshot", + "crypto.diffieHellman", "crypto.createSign", "crypto.createVerify", ]); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e174238e2..2703f4014 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4874,6 +4874,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "crypto.generateKeyPairSync": { argTypes: [DYN, DYN], result: DYN }, "crypto.generateKeyPairCb": { argTypes: [DYN, DYN, DYN], result: VOID }, "crypto.signOneshot": { argTypes: [DYN, DYN, DYN], result: DYN }, + "crypto.diffieHellman": { argTypes: [DYN, DYN], result: DYN }, "crypto.verifyOneshot": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, "crypto.createSign": { argTypes: [DYN], result: DYN }, "crypto.createVerify": { argTypes: [DYN], result: DYN }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index ca7fb7129..d5d2626c5 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -732,6 +732,7 @@ export const PKI_LIB_FNS: ReadonlySet = new Set([ "crypto.generateKeyPairSync", "crypto.signOneshot", "crypto.verifyOneshot", + "crypto.diffieHellman", ]); /** The WebCrypto algorithms scr_webcrypto_pki.c implements over the pki diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index e8fad6dd1..ac5d5e47b 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -63,6 +63,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["crypto.generateKeyPair", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.signOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], ["crypto.verifyOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"], + ["crypto.diffieHellman", "crypto.diffieHellman"], ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 3387217b5..75541b8e5 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -568,9 +568,7 @@ "id": "node-builtin.crypto.diffieHellman", "kind": "node-builtin", "name": "crypto.diffieHellman", - "status": "unsupported", - "code": "SC2020", - "note": "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets" + "status": "static" }, { "id": "node-builtin.crypto.generateKey", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json new file mode 100644 index 000000000..f156f7530 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7450-crypto-x25519.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7450-crypto-x25519.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json new file mode 100644 index 000000000..593054dd5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7451-crypto-diffie-hellman.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7451-crypto-diffie-hellman.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json new file mode 100644 index 000000000..7cb78758b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7452-crypto-diffie-hellman-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7452-crypto-diffie-hellman-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json new file mode 100644 index 000000000..fb8a7a740 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/diffie-hellman.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/diffie-hellman.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index f61d27406..03a9dd109 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -298,6 +298,9 @@ ScrDyn *scr_crypto_generate_key_pair_sync(ScrDyn *type, ScrDyn *options); void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *callback); ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key); ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, ScrDyn *signature); +/* crypto.diffieHellman(options[, callback]): the shared secret, or undefined + * with the callback, which receives it in a later macrotask. */ +ScrDyn *scr_crypto_diffie_hellman(ScrDyn *options, ScrDyn *callback); ScrDyn *scr_crypto_create_sign(ScrDyn *algorithm); ScrDyn *scr_crypto_create_verify(ScrDyn *algorithm); bool scr_crypto_is_asymmetric_key(const ScrDyn *value); diff --git a/packages/runtime/src/scr_pki.c b/packages/runtime/src/scr_pki.c index 94a0000e7..a1d5b98a6 100644 --- a/packages/runtime/src/scr_pki.c +++ b/packages/runtime/src/scr_pki.c @@ -1,7 +1,8 @@ /* node:crypto asymmetric keys and signatures over the vendored mbedTLS * public-key layer: createPrivateKey, createPublicKey, generateKeyPairSync - * and generateKeyPair for "rsa", "ec" and "ed25519" keys, the - * PrivateKeyObject and PublicKeyObject handles (type, asymmetricKeyType, + * and generateKeyPair for "rsa", "ec", "ed25519" and "x25519" keys, + * crypto.diffieHellman (ECDH and X25519), the PrivateKeyObject and + * PublicKeyObject handles (type, asymmetricKeyType, * asymmetricKeyDetails, export(), equals()), crypto.sign and crypto.verify, * and the Sign and Verify objects of createSign and createVerify. Keys read * and write PEM and DER as PKCS#1, SEC1, PKCS#8 and SubjectPublicKeyInfo, @@ -23,6 +24,7 @@ #include "mbedtls/asn1write.h" #include "mbedtls/base64.h" #include "mbedtls/bignum.h" +#include "mbedtls/ecdh.h" #include "mbedtls/ecdsa.h" #include "mbedtls/ecp.h" #include "mbedtls/md.h" @@ -40,6 +42,30 @@ static void sp_throw(int kind, const char *code, const char *message) { scr_throw_error_msg_code(kind, message, strlen(message), code); } +/* An OpenSSL error as Node's ThrowCryptoError reports it, + * "error::::", with own library, reason and code + * properties in Node's key order. A crypto job's callback gets the error + * without them (`props` false), as from Node's async jobs. */ +static void sp_throw_openssl(const char *packed, const char *library, const char *reason, const char *code, + bool props) { + char message[160]; + snprintf(message, sizeof message, "error:%s:%s::%s", packed, library, reason); + ScrStr *text = scr_str_new(message, strlen(message)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, text); + scr_str_release(text); + if (props) { + const char *keys[] = {"library", "reason", "code"}; + const char *values[] = {library, reason, code}; + for (size_t i = 0; i < 3; i++) { + if (i == 2) scr_error_set_code(e, code); + ScrStr *value = scr_str_new(values[i], strlen(values[i])); + scr_error_set_prop(e, keys[i], strlen(keys[i]), scr_dyn_new_str(value)); + scr_str_release(value); + } + } + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + void scr_pki_throw_fmt(int kind, const char *code, const char *fmt, ...) { va_list ap, sized; va_start(ap, fmt); @@ -157,6 +183,8 @@ typedef struct SpKey { mbedtls_pk_context pk; /* RSA and EC keys */ bool private_key; bool ed25519; /* an Ed25519 key: ed_seed (private keys) and ed_pub */ + bool x25519; /* an X25519 key, in the same two fields: the private key's + * bytes as given, before clamping, and the public key */ uint8_t ed_seed[32]; uint8_t ed_pub[32]; } SpKey; @@ -215,6 +243,43 @@ static bool sp_is_ec(const SpKey *k) { return mbedtls_pk_get_type(&k->pk) == MBE static bool sp_is_ed(const SpKey *k) { return k->ed25519; } +static bool sp_is_x(const SpKey *k) { return k->x25519; } + +/* An Ed25519 or X25519 key: 32-byte private and public keys, no details. */ +static bool sp_is_okp(const SpKey *k) { return k->ed25519 || k->x25519; } + +/* X25519(scalar, point) per RFC 7748 over mbedTLS's Curve25519 group, or + * of the base point when point is NULL. False when the result is the + * all-zero value (a small-order point), which OpenSSL refuses. */ +static bool sp_x25519(const uint8_t scalar[32], const uint8_t *point, uint8_t out[32]) { + mbedtls_ecp_group grp; + mbedtls_ecp_point P, R; + mbedtls_mpi d; + mbedtls_ecp_group_init(&grp); + mbedtls_ecp_point_init(&P); + mbedtls_ecp_point_init(&R); + mbedtls_mpi_init(&d); + uint8_t k[32]; + memcpy(k, scalar, 32); + k[0] &= 248; + k[31] &= 127; + k[31] |= 64; + size_t n = 0; + bool ok = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_CURVE25519) == 0 && + mbedtls_mpi_read_binary_le(&d, k, 32) == 0 && + (point ? mbedtls_ecp_point_read_binary(&grp, &P, point, 32) : mbedtls_ecp_copy(&P, &grp.G)) == 0 && + mbedtls_ecp_mul(&grp, &R, &d, &P, sp_random, NULL) == 0 && + mbedtls_ecp_point_write_binary(&grp, &R, MBEDTLS_ECP_PF_UNCOMPRESSED, &n, out, 32) == 0 && n == 32; + uint8_t any = 0; + for (size_t i = 0; ok && i < 32; i++) any |= out[i]; + mbedtls_platform_zeroize(k, sizeof k); + mbedtls_mpi_free(&d); + mbedtls_ecp_point_free(&R); + mbedtls_ecp_point_free(&P); + mbedtls_ecp_group_free(&grp); + return ok && any != 0; +} + typedef struct { const char *name; /* OpenSSL short name (namedCurve) */ const char *nist; /* EC_curve_nist2nid alias, or NULL */ @@ -241,7 +306,7 @@ static const SpCurve *sp_curve_of(const SpKey *k) { /* A parsed key is usable when it is RSA or EC on a curve above. */ static bool sp_key_supported(const SpKey *k) { - return sp_is_ed(k) || sp_is_rsa(k) || (sp_is_ec(k) && sp_curve_of(k) != NULL); + return sp_is_okp(k) || sp_is_rsa(k) || (sp_is_ec(k) && sp_curve_of(k) != NULL); } /* ── DER writing ──────────────────────────────────────────────────────── */ @@ -260,6 +325,11 @@ static const uint8_t sp_ed_pkcs8_prefix[16] = {0x30, 0x2e, 0x02, 0x01, 0x00, 0x3 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20}; static const uint8_t sp_ed_spki_prefix[12] = {0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00}; +/* X25519's, which differ in the OID's last byte (1.3.101.110). */ +static const uint8_t sp_x_pkcs8_prefix[16] = {0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, + 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04, 0x20}; +static const uint8_t sp_x_spki_prefix[12] = {0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, + 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00}; /* Writes prefix || 32 bytes at the end of buf, as the mbedtls writers do. */ static int sp_write_ed_der(const uint8_t *prefix, size_t prefix_len, const uint8_t key[32], @@ -272,7 +342,8 @@ static int sp_write_ed_der(const uint8_t *prefix, size_t prefix_len, const uint8 } static int sp_write_spki_der(SpKey *k, unsigned char *buf, size_t size) { - if (sp_is_ed(k)) return sp_write_ed_der(sp_ed_spki_prefix, sizeof sp_ed_spki_prefix, k->ed_pub, buf, size); + if (sp_is_okp(k)) + return sp_write_ed_der(sp_is_x(k) ? sp_x_spki_prefix : sp_ed_spki_prefix, 12, k->ed_pub, buf, size); return mbedtls_pk_write_pubkey_der(&k->pk, buf, size); } @@ -287,8 +358,8 @@ static int sp_write_pkcs1_public_der(SpKey *k, unsigned char *buf, size_t size) * parameters, or id-ecPublicKey with the curve OID), and the inner key, * which for EC omits the parameters field. */ static int sp_write_pkcs8_der(SpKey *k, unsigned char *buf, size_t size) { - if (sp_is_ed(k)) - return sp_write_ed_der(sp_ed_pkcs8_prefix, sizeof sp_ed_pkcs8_prefix, k->ed_seed, buf, size); + if (sp_is_okp(k)) + return sp_write_ed_der(sp_is_x(k) ? sp_x_pkcs8_prefix : sp_ed_pkcs8_prefix, 16, k->ed_seed, buf, size); unsigned char inner[SP_DER_MAX]; size_t inner_len; if (sp_is_rsa(k)) { @@ -473,9 +544,9 @@ static ScrDyn *sp_b64url(const uint8_t *raw, size_t len) { static ScrDyn *sp_export_jwk(SpKey *k) { ScrDyn *jwk = scr_dyn_new_obj(); - if (sp_is_ed(k)) { + if (sp_is_okp(k)) { sp_set(jwk, "kty", sp_dyn_str(scr_str_new("OKP", 3))); - sp_set(jwk, "crv", sp_dyn_str(scr_str_new("Ed25519", 7))); + sp_set(jwk, "crv", sp_dyn_str(sp_is_x(k) ? scr_str_new("X25519", 6) : scr_str_new("Ed25519", 7))); sp_set(jwk, "x", sp_b64url(k->ed_pub, 32)); if (k->private_key) sp_set(jwk, "d", sp_b64url(k->ed_seed, 32)); return jwk; @@ -587,7 +658,8 @@ static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { bool ok = false; if (sp_is_str(kty, "OKP")) { const ScrDyn *crv = sp_opt(jwk, "crv"); - if (!sp_is_str(crv, "Ed25519")) { + bool x = sp_is_str(crv, "X25519"); + if (!x && !sp_is_str(crv, "Ed25519")) { sp_key_release(k); if (crv->kind != SCR_DYN_STR) { scr_dyn_prop_type_fail("key.crv", "of type string", crv); @@ -596,10 +668,13 @@ static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { sp_not_supported("JWK keys of this crv"); return NULL; } - k->ed25519 = true; + k->ed25519 = !x; + k->x25519 = x; if (private_key) { ok = sp_jwk_bytes(jwk, "d", k->ed_seed); - if (ok) { + if (ok && x) { + ok = sp_x25519(k->ed_seed, NULL, k->ed_pub); + } else if (ok) { const ScrPkiEd25519 *ed = sp_ed(); if (!ed) { sp_key_release(k); @@ -678,9 +753,10 @@ static SpKey *sp_key_from_jwk_plain(const ScrDyn *jwk, bool want_private) { /* The private key of a public SpKey derived from `k`. */ static SpKey *sp_public_of(SpKey *k) { - if (sp_is_ed(k)) { + if (sp_is_okp(k)) { SpKey *pub = sp_key_alloc(false); - pub->ed25519 = true; + pub->ed25519 = k->ed25519; + pub->x25519 = k->x25519; memcpy(pub->ed_pub, k->ed_pub, 32); return pub; } @@ -770,6 +846,24 @@ static bool sp_cert_spki(const unsigned char *der, size_t len, const unsigned ch * as public answers its public key. */ static SpKey *sp_ed_from_der(const unsigned char *der, size_t len, bool private_key, bool *failed) { *failed = false; + /* X25519 keys, which need no other unit. */ + if (len == 48 && memcmp(der, sp_x_pkcs8_prefix, 16) == 0) { + SpKey *k = sp_key_alloc(private_key); + k->x25519 = true; + memcpy(k->ed_seed, der + 16, 32); + if (!sp_x25519(k->ed_seed, NULL, k->ed_pub)) { + sp_key_release(k); + return NULL; + } + if (!private_key) mbedtls_platform_zeroize(k->ed_seed, 32); + return k; + } + if (!private_key && len == 44 && memcmp(der, sp_x_spki_prefix, 12) == 0) { + SpKey *k = sp_key_alloc(false); + k->x25519 = true; + memcpy(k->ed_pub, der + 12, 32); + return k; + } if (len == 48 && memcmp(der, sp_ed_pkcs8_prefix, 16) == 0) { const ScrPkiEd25519 *ed = sp_ed(); if (!ed) { @@ -945,9 +1039,9 @@ static ScrDyn *sp_create_key(ScrDyn *input, bool private_key) { return NULL; } if (sp_is_str(format, "der")) { - bool valid = private_key ? sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || - sp_is_str(type, "sec1") - : sp_is_str(type, "pkcs1") || sp_is_str(type, "spki"); + /* createPublicKey also reads a private key's encodings. */ + bool valid = sp_is_str(type, "pkcs1") || sp_is_str(type, "pkcs8") || sp_is_str(type, "sec1") || + (!private_key && sp_is_str(type, "spki")); if (!valid) { sp_property_invalid("key.type", type); return NULL; @@ -997,15 +1091,14 @@ static ScrDyn *sp_export_raw(SpKey *k, bool private_part, bool seed, bool compre sp_incompatible_key_type(); return NULL; } - if (sp_is_ed(k)) return sp_dyn_buffer(private_part ? k->ed_seed : k->ed_pub, 32); + if (sp_is_okp(k)) return sp_dyn_buffer(private_part ? k->ed_seed : k->ed_pub, 32); mbedtls_ecp_keypair *ec = mbedtls_pk_ec(k->pk); const mbedtls_ecp_group *grp = &ec->MBEDTLS_PRIVATE(grp); unsigned char buf[1 + 2 * 66]; size_t n = (grp->nbits + 7) / 8; + int form = compressed ? MBEDTLS_ECP_PF_COMPRESSED : MBEDTLS_ECP_PF_UNCOMPRESSED; int rc = private_part ? mbedtls_mpi_write_binary(&ec->MBEDTLS_PRIVATE(d), buf, n) - : mbedtls_ecp_point_write_binary( - grp, &ec->MBEDTLS_PRIVATE(Q), - compressed ? MBEDTLS_ECP_PF_COMPRESSED : MBEDTLS_ECP_PF_UNCOMPRESSED, &n, buf, sizeof buf); + : mbedtls_ecp_point_write_binary(grp, &ec->MBEDTLS_PRIVATE(Q), form, &n, buf, sizeof buf); ScrDyn *out = rc == 0 ? sp_dyn_buffer(buf, n) : NULL; mbedtls_platform_zeroize(buf, sizeof buf); if (!out) sp_unsupported_input(); @@ -1013,10 +1106,9 @@ static ScrDyn *sp_export_raw(SpKey *k, bool private_part, bool seed, bool compre } /* Node's named key types that this runtime does not generate or read: - * the other OKP curves and the post-quantum types. */ + * X448, Ed448 and the post-quantum types. */ static const char *const sp_other_named_types[] = { "ed448", - "x25519", "x448", "ml-dsa-44", "ml-dsa-65", @@ -1071,15 +1163,16 @@ static SpKey *sp_key_from_raw(const ScrDyn *input, const ScrDyn *data, const Scr sp_incompatible_key_type(); return NULL; } + bool x = sp_is_str(type, "x25519"); bool other = false; for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) other = other || sp_is_str(type, sp_other_named_types[i]); - if (!ec && !ed && !other) { + if (!ec && !ed && !x && !other) { ScrStr *s = type->v.str; scr_pki_throw_fmt(SCR_ERR_TYPE, "ERR_INVALID_ARG_VALUE", "Invalid asymmetricKeyType: %.*s", (int)s->len, s->data); return NULL; } - bool ml = !ec && !ed && type->v.str->len > 3 && memcmp(type->v.str->data, "ml-", 3) == 0; + bool ml = other && type->v.str->len > 3 && memcmp(type->v.str->data, "ml-", 3) == 0; if (seed && !ml) { sp_incompatible_key_type(); return NULL; @@ -1099,7 +1192,16 @@ static SpKey *sp_key_from_raw(const ScrDyn *input, const ScrDyn *data, const Scr size_t len = (size_t)scr_bytes_byte_len(bytes); SpKey *k = sp_key_alloc(!raw_public); bool ok = false; - if (ed) { + if (x) { + k->x25519 = true; + if (len == 32 && raw_public) { + memcpy(k->ed_pub, p, 32); + ok = true; + } else if (len == 32) { + memcpy(k->ed_seed, p, 32); + ok = sp_x25519(k->ed_seed, NULL, k->ed_pub); + } + } else if (ed) { k->ed25519 = true; if (len == 32 && raw_public) { memcpy(k->ed_pub, p, 32); @@ -1254,7 +1356,7 @@ static ScrDyn *sp_export_plain(SpKey *k, const ScrDyn *options) { static ScrDyn *sp_key_details(SpKey *k) { ScrDyn *details = scr_dyn_new_obj(); - if (sp_is_ed(k)) return details; + if (sp_is_okp(k)) return details; if (sp_is_rsa(k)) { mbedtls_rsa_context *rsa = mbedtls_pk_rsa(k->pk); mbedtls_mpi e; @@ -1280,7 +1382,7 @@ static ScrDyn *sp_key_details(SpKey *k) { /* The DER that identifies a key for equals(): its private encoding for a * private key, its SubjectPublicKeyInfo otherwise. */ static int sp_identity_der(SpKey *k, unsigned char *buf, size_t size) { - if (sp_is_ed(k)) return k->private_key ? sp_write_pkcs8_der(k, buf, size) : sp_write_spki_der(k, buf, size); + if (sp_is_okp(k)) return k->private_key ? sp_write_pkcs8_der(k, buf, size) : sp_write_spki_der(k, buf, size); return k->private_key ? sp_write_private_der(k, buf, size) : sp_write_spki_der(k, buf, size); } @@ -1326,6 +1428,7 @@ static ScrDyn *sp_key_get(void *h, const char *name, size_t len) { return k->private_key ? sp_dyn_str(scr_str_new("private", 7)) : sp_dyn_str(scr_str_new("public", 6)); if (len == 17 && memcmp(name, "asymmetricKeyType", 17) == 0) return sp_is_ed(k) ? sp_dyn_str(scr_str_new("ed25519", 7)) + : sp_is_x(k) ? sp_dyn_str(scr_str_new("x25519", 6)) : sp_is_rsa(k) ? sp_dyn_str(scr_str_new("rsa", 3)) : sp_dyn_str(scr_str_new("ec", 2)); if (len == 20 && memcmp(name, "asymmetricKeyDetails", 20) == 0) return sp_key_details(k); @@ -1543,7 +1646,7 @@ static bool sp_check_encoding_plain(const ScrDyn *enc, const char *name, const S /* The key generateKeyPair makes, as sp_keygen_check reads it. */ typedef struct { - enum { SP_GEN_RSA, SP_GEN_EC, SP_GEN_ED25519 } kind; + enum { SP_GEN_RSA, SP_GEN_EC, SP_GEN_ED25519, SP_GEN_X25519 } kind; double bits, exponent; /* RSA */ const SpCurve *curve; /* EC */ } SpKeyGen; @@ -1604,6 +1707,8 @@ static bool sp_keygen_check(const ScrDyn *type, const ScrDyn *options, SpKeyGen } } else if (sp_is_str(type, "ed25519")) { gen->kind = SP_GEN_ED25519; + } else if (sp_is_str(type, "x25519")) { + gen->kind = SP_GEN_X25519; } else { bool named = false; for (size_t i = 0; i < sizeof sp_other_named_types / sizeof sp_other_named_types[0]; i++) @@ -1646,6 +1751,20 @@ static SpKey *sp_keygen_run(const SpKeyGen *gen, bool *job_error) { scr_pki_ed25519->public_key(k->ed_pub, k->ed_seed); return k; } + if (gen->kind == SP_GEN_X25519) { + /* OpenSSL stores a generated X25519 key clamped. */ + k->x25519 = true; + sp_random(NULL, k->ed_seed, 32); + k->ed_seed[0] &= 248; + k->ed_seed[31] &= 127; + k->ed_seed[31] |= 64; + if (!sp_x25519(k->ed_seed, NULL, k->ed_pub)) { + sp_key_release(k); + sp_unsupported_input(); + return NULL; + } + return k; + } if (gen->kind == SP_GEN_RSA) { const char *failure = sp_rsa_keygen_failure(gen->bits, gen->exponent); if (failure) { @@ -1977,6 +2096,16 @@ static bool sp_verify_hash(SpSigKey *key, mbedtls_md_type_t md, const unsigned c return mbedtls_pk_verify(&key->key->pk, md, hash, hash_len, sig, sig_len) == 0; } +/* X25519 keys neither sign nor verify: OpenSSL's error, after the key + * checks. Releases the key and answers true when it rejects. */ +static bool sp_reject_x25519(SpSigKey *k) { + if (!sp_is_x(k->key)) return false; + sp_key_release(k->key); + sp_throw_openssl("03000096", "digital envelope routines", "operation not supported for this keytype", + "ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE", true); + return true; +} + static const char sp_data_types[] = "an instance of Buffer, TypedArray, or DataView"; /* Ed25519 signs the message itself: an algorithm other than null or @@ -1997,6 +2126,7 @@ ScrDyn *scr_crypto_sign_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key) { } SpSigKey k; if (!sp_sig_key(key, true, &k)) return NULL; + if (sp_reject_x25519(&k)) return NULL; ScrBytes *bytes = scr_bytes_buffer_source(data); if (sp_is_ed(k.key)) { ScrDyn *result = NULL; @@ -2034,6 +2164,7 @@ ScrDyn *scr_crypto_verify_oneshot(ScrDyn *algorithm, ScrDyn *data, ScrDyn *key, scr_dyn_arg_type_fail("signature", sp_data_types, signature); return NULL; } + if (sp_reject_x25519(&k)) return NULL; ScrBytes *bytes = scr_bytes_buffer_source(data); ScrBytes *sig = scr_bytes_buffer_source(signature); if (sp_is_ed(k.key)) { @@ -2120,6 +2251,7 @@ static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDy } SpSigKey k; if (!sp_sig_key(argc ? args[0] : scr_dyn_undefined(), true, &k)) return NULL; + if (sp_reject_x25519(&k)) return NULL; if (sp_is_ed(k.key)) { sp_key_release(k.key); sp_throw(SCR_ERR_ERROR, "ERR_CRYPTO_UNSUPPORTED_OPERATION", "Unsupported crypto operation"); @@ -2157,6 +2289,7 @@ static ScrDyn *sp_signer_invoke(void *h, ScrDyn *self, const char *method, ScrDy signature); return NULL; } + if (sp_reject_x25519(&k)) return NULL; const ScrStr *encoding = sp_encoding_arg(argc > 2 ? args[2] : NULL); ScrDyn *encoding_value = encoding ? scr_dyn_new_str((ScrStr *)encoding) : NULL; ScrBytes *sig = sp_source_bytes(signature, encoding_value); @@ -2323,6 +2456,156 @@ void scr_crypto_generate_key_pair_async(ScrDyn *type, ScrDyn *options, ScrDyn *c scr_loop_defer(c); } +/* ── diffieHellman ────────────────────────────────────────────────────── */ + +/* The keys an options.privateKey or options.publicKey value can be. */ +static const char sp_dh_private_types[] = + "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, CryptoKey, or URL"; +static const char sp_dh_public_types[] = + "of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, or CryptoKey"; + +/* Node's message names: a KeyObject's asymmetricKeyType. */ +static const char *sp_type_name(const SpKey *k) { + return sp_is_ed(k) ? "ed25519" : sp_is_x(k) ? "x25519" : sp_is_rsa(k) ? "rsa" : "ec"; +} + +/* A key input of diffieHellman's options: a primitive other than a string + * throws Node's ERR_INVALID_ARG_TYPE for `name`; other values read as + * sign and verify read their keys. */ +static bool sp_dh_key(const ScrDyn *value, const char *name, bool private_key, SpSigKey *out) { + out->key = NULL; + /* Node's key preparation destructures the value before it checks it. */ + if (value->kind == SCR_DYN_NULL) { + scr_pki_throw_fmt(SCR_ERR_TYPE, NULL, "Cannot destructure property 'key' of 'key' as it is null."); + return false; + } + bool keyish = value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES || scr_buffer_storage_is(value) || + (value->kind != SCR_DYN_NULL && value->kind != SCR_DYN_FUNC && scr_dyn_is_object(value)); + if (!keyish) { + scr_dyn_prop_type_fail(name, private_key ? sp_dh_private_types : sp_dh_public_types, value); + return false; + } + return sp_sig_key(value, private_key, out); +} + +/* The shared secret of two keys of one type: X25519, or ECDH's x + * coordinate at the field's size. NULL with OpenSSL's error pending. */ +static void sp_dh_failed(bool props) { + sp_throw_openssl("1C8000A4", "Provider routines", "failed during derivation", "ERR_OSSL_FAILED_DURING_DERIVATION", + props); +} + +static ScrDyn *sp_dh_compute(SpKey *priv, SpKey *pub, bool props) { + if (sp_is_x(priv) && sp_is_x(pub)) { + uint8_t out[32]; + if (!sp_x25519(priv->ed_seed, pub->ed_pub, out)) { + sp_dh_failed(props); + return NULL; + } + ScrDyn *secret = sp_dyn_buffer(out, 32); + mbedtls_platform_zeroize(out, sizeof out); + return secret; + } + if (!sp_is_ec(priv) || !sp_is_ec(pub)) { + sp_throw_openssl("03000096", "digital envelope routines", "operation not supported for this keytype", + "ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE", props); + return NULL; + } + mbedtls_ecp_keypair *a = mbedtls_pk_ec(priv->pk); + const mbedtls_ecp_keypair *b = mbedtls_pk_ec(pub->pk); + if (a->MBEDTLS_PRIVATE(grp).id != b->MBEDTLS_PRIVATE(grp).id) { + sp_throw_openssl("1C8000CB", "Provider routines", "mismatching domain parameters", + "ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS", props); + return NULL; + } + mbedtls_mpi z; + mbedtls_mpi_init(&z); + unsigned char out[66]; + size_t n = (a->MBEDTLS_PRIVATE(grp).pbits + 7) / 8; + bool ok = mbedtls_ecdh_compute_shared(&a->MBEDTLS_PRIVATE(grp), &z, &b->MBEDTLS_PRIVATE(Q), + &a->MBEDTLS_PRIVATE(d), sp_random, NULL) == 0 && + mbedtls_mpi_write_binary(&z, out, n) == 0; + mbedtls_mpi_free(&z); + ScrDyn *secret = ok ? sp_dyn_buffer(out, n) : NULL; + mbedtls_platform_zeroize(out, sizeof out); + if (!secret) sp_dh_failed(props); + return secret; +} + +static void sp_dh_deliver(ScrClosure *self) { + ScrDyn *callback = sp_cap(self, 0); + ScrDyn *secret = sp_cap(self, 1); + ScrDyn *args[2] = {scr_dyn_new_null(), secret}; + ScrDyn *result = scr_dyn_call(callback, args, 2, "callback"); + if (result) scr_dyn_release(result); + scr_dyn_release(args[0]); + scr_dyn_release(secret); + scr_dyn_release(callback); +} + +/* crypto.diffieHellman(options[, callback]) in Node's order: the options + * object, the callback, both keys present, the key types when both are + * KeyObjects, then the public and private key inputs. With a callback, + * the secret, or the error of computing it, arrives in a later macrotask. */ +ScrDyn *scr_crypto_diffie_hellman(ScrDyn *options, ScrDyn *callback) { + ScrDyn *plain = sp_plain(options); + if (!plain) return NULL; + ScrDyn *result = NULL; + SpSigKey pub = {0}, priv = {0}; + if (!sp_check_object(plain, "options")) goto done; + bool async = !sp_absent(callback); + if (async && !scr_dyn_is_callable(callback)) { + scr_dyn_arg_type_fail("callback", "of type function", callback); + goto done; + } + const ScrDyn *private_value = sp_opt(plain, "privateKey"); + const ScrDyn *public_value = sp_opt(plain, "publicKey"); + if (sp_absent(private_value)) { + sp_property_invalid("options.privateKey", private_value); + goto done; + } + if (sp_absent(public_value)) { + sp_property_invalid("options.publicKey", public_value); + goto done; + } + SpKey *a = sp_key_of(private_value), *b = sp_key_of(public_value); + if (a && b && a->private_key) { + const char *ta = sp_type_name(a), *tb = sp_type_name(b); + if (strcmp(ta, tb) != 0 || (!sp_is_ec(a) && !sp_is_x(a))) { + scr_pki_throw_fmt(SCR_ERR_ERROR, "ERR_CRYPTO_INCOMPATIBLE_KEY", + "Incompatible key types for Diffie-Hellman: %s and %s", ta, tb); + goto done; + } + } + if (!sp_dh_key(public_value, "options.publicKey", false, &pub) || + !sp_dh_key(private_value, "options.privateKey", true, &priv)) + goto done; + ScrDyn *secret = sp_dh_compute(priv.key, pub.key, !async); + if (!async) { + result = secret; + goto done; + } + void *deliver = (void *)sp_dh_deliver; + if (!secret) { + ScrCaught *caught = scr_exc_take(); + secret = scr_caught_to_dyn(caught); + scr_caught_release(caught); + deliver = (void *)sp_key_pair_fail; + } + ScrClosure *c = scr_closure_new(deliver, 2); + c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[0], scr_dyn_retain(callback)); + c->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); + scr_box_set_ref(c->caps[1], secret); + scr_loop_defer(c); + result = scr_dyn_retain(scr_dyn_undefined()); +done: + if (pub.key) sp_key_release(pub.key); + if (priv.key) sp_key_release(priv.key); + scr_dyn_release(plain); + return result; +} + /* ── the core for the WebCrypto layer (scr_pki.h) ─────────────────────── */ ScrPkiKey *scr_pki_key_retain(ScrPkiKey *key) { return sp_key_retain(key); } diff --git a/tests/corpus/7450-crypto-x25519.mjs b/tests/corpus/7450-crypto-x25519.mjs new file mode 100644 index 000000000..180221390 --- /dev/null +++ b/tests/corpus/7450-crypto-x25519.mjs @@ -0,0 +1,90 @@ +// X25519 KeyObjects: generateKeyPair, PKCS#8, SubjectPublicKeyInfo, JWK +// and raw keys, RFC 7748's test vectors through crypto.diffieHellman, and +// Node's errors for signatures and incompatible encodings. +import * as crypto from "node:crypto"; + +function describe(value) { + if (Buffer.isBuffer(value)) return `Buffer(${value.length})`; + if (value instanceof crypto.KeyObject) return `${value.type} ${value.asymmetricKeyType} ${JSON.stringify(value.asymmetricKeyDetails)}`; + if (typeof value === "string") return value.split("\n")[0]; + if (value && typeof value === "object") return Object.entries(value).map(([key, field]) => `${key}=${describe(field)}`).join(" "); + return String(value); +} + +function attempt(label, run) { + try { + console.log(label, describe(run())); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message); + } +} + +// RFC 7748, section 6.1 +const alicePrivate = Buffer.from("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", "hex"); +const alicePublic = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"; +const bobPrivate = Buffer.from("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb", "hex"); +const bobPublic = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"; +const alice = crypto.createPrivateKey({ key: alicePrivate, format: "raw-private", asymmetricKeyType: "x25519" }); +const bob = crypto.createPrivateKey({ key: bobPrivate, format: "raw-private", asymmetricKeyType: "x25519" }); +console.log("alice public", Buffer.from(crypto.createPublicKey(alice).export({ format: "raw-public" })).toString("hex") === alicePublic); +console.log("bob public", Buffer.from(crypto.createPublicKey(bob).export({ format: "raw-public" })).toString("hex") === bobPublic); +const shared = crypto.diffieHellman({ privateKey: alice, publicKey: crypto.createPublicKey(bob) }); +console.log("shared", shared.toString("hex")); +console.log("both ways", crypto.diffieHellman({ privateKey: bob, publicKey: crypto.createPublicKey(alice) }).equals(shared)); +console.log("raw-private kept as given", Buffer.from(alice.export({ format: "raw-private" })).equals(alicePrivate)); + +// encodings +console.log("details", describe(alice), describe(crypto.createPublicKey(alice))); +const spki = alice.export({ format: "der", type: "pkcs8" }); +console.log("pkcs8", spki.length, spki.subarray(0, 16).toString("hex")); +const pub = crypto.createPublicKey(alice); +const spkiDer = pub.export({ format: "der", type: "spki" }); +console.log("spki", spkiDer.length, spkiDer.subarray(0, 12).toString("hex")); +console.log("pem", describe(alice.export({ format: "pem", type: "pkcs8" })), describe(pub.export({ format: "pem", type: "spki" }))); +const jwk = alice.export({ format: "jwk" }); +console.log("jwk", Object.keys(jwk).join(), jwk.kty, jwk.crv, jwk.x === Buffer.from(alicePublic, "hex").toString("base64url"), jwk.d === alicePrivate.toString("base64url")); +console.log("public jwk", JSON.stringify(pub.export({ format: "jwk" }))); +const fromPem = crypto.createPrivateKey(alice.export({ format: "pem", type: "pkcs8" })); +console.log("pem round trip", fromPem.equals(alice), crypto.createPublicKey(pub.export({ format: "pem", type: "spki" })).equals(pub)); +console.log("jwk round trip", crypto.createPrivateKey({ key: jwk, format: "jwk" }).equals(alice), crypto.createPublicKey({ key: pub.export({ format: "jwk" }), format: "jwk" }).equals(pub)); +console.log("public of pkcs8", crypto.createPublicKey({ key: spki, format: "der", type: "pkcs8" }).equals(pub)); +console.log("not ed25519", alice.equals(crypto.generateKeyPairSync("ed25519").privateKey)); + +// generateKeyPair +const pair = crypto.generateKeyPairSync("x25519"); +console.log("generated", describe(pair)); +const clamped = Buffer.from(pair.privateKey.export({ format: "raw-private" })); +console.log("generated clamped", (clamped[0] & 7) === 0, (clamped[31] & 0xc0) === 0x40); +attempt("pem pair", () => + crypto.generateKeyPairSync("x25519", { + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "der" }, + }), +); +attempt("raw pair", () => + crypto.generateKeyPairSync("x25519", { publicKeyEncoding: { format: "raw-public" }, privateKeyEncoding: { format: "raw-private" } }), +); +attempt("pkcs1", () => crypto.generateKeyPairSync("x25519", { publicKeyEncoding: { type: "pkcs1", format: "pem" } })); +attempt("sec1", () => crypto.generateKeyPairSync("x25519", { privateKeyEncoding: { type: "sec1", format: "pem" } })); +attempt("raw-seed", () => crypto.generateKeyPairSync("x25519", { privateKeyEncoding: { format: "raw-seed" } })); +attempt("X25519 spelling", () => crypto.generateKeyPairSync("X25519")); + +// what X25519 keys cannot do +const data = Buffer.from("x25519"); +attempt("sign", () => crypto.sign(null, data, alice)); +attempt("sign sha256", () => crypto.sign("sha256", data, alice)); +attempt("verify", () => crypto.verify(null, data, pub, Buffer.alloc(64))); +attempt("createSign", () => crypto.createSign("sha256").update(data).sign(alice)); +attempt("createVerify", () => crypto.createVerify("sha256").update(data).verify(pub, Buffer.alloc(64))); +attempt("hmac", () => crypto.createHmac("sha256", alice)); +attempt("export pkcs1", () => pub.export({ format: "pem", type: "pkcs1" })); +attempt("export raw-seed", () => alice.export({ format: "raw-seed" })); +attempt("short raw key", () => crypto.createPublicKey({ key: Buffer.alloc(31), format: "raw-public", asymmetricKeyType: "x25519" })); + +crypto.generateKeyPair("x25519", (err, publicKey, privateKey) => { + console.log("async", err, describe(publicKey), describe(privateKey)); + crypto.diffieHellman({ privateKey, publicKey: crypto.createPublicKey(bob) }, (err2, secret) => { + console.log("async secret", err2, describe(secret)); + }); +}); +console.log("returned"); diff --git a/tests/corpus/7451-crypto-diffie-hellman.ts b/tests/corpus/7451-crypto-diffie-hellman.ts new file mode 100644 index 000000000..9648cd32b --- /dev/null +++ b/tests/corpus/7451-crypto-diffie-hellman.ts @@ -0,0 +1,55 @@ +// crypto.diffieHellman with typed KeyObjects: ECDH on every supported +// curve, checked against createECDH, X25519, the callback form, and +// Node's errors for incompatible key types, mismatched curves and a +// small-order X25519 public key. +import * as crypto from "node:crypto"; + +function attempt(label: string, run: () => Buffer): void { + try { + const secret = run(); + console.log(label, secret.length); + } catch (e) { + const error = e as NodeJS.ErrnoException; + console.log(label, "throws", error.name, error.code, error.message); + } +} + +for (const namedCurve of ["prime256v1", "secp384r1", "secp521r1", "secp256k1"]) { + const a = crypto.generateKeyPairSync("ec", { namedCurve }); + const ecdh = crypto.createECDH(namedCurve); + const ecdhPublic = ecdh.generateKeys(); + const peer = crypto.createPublicKey({ key: ecdhPublic, format: "raw-public", asymmetricKeyType: "ec", namedCurve } as unknown as crypto.PublicKeyInput); + const secret = crypto.diffieHellman({ privateKey: a.privateKey, publicKey: peer }); + // The SubjectPublicKeyInfo ends with the uncompressed point. + const spki = a.publicKey.export({ format: "der", type: "spki" }); + const own = spki.subarray(spki.length - ecdhPublic.length); + console.log(namedCurve, secret.length, secret.equals(ecdh.computeSecret(own))); +} + +const x = crypto.generateKeyPairSync("x25519"); +const y = crypto.generateKeyPairSync("x25519"); +const xy = crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.publicKey }); +console.log("x25519", xy.length, xy.equals(crypto.diffieHellman({ privateKey: y.privateKey, publicKey: x.publicKey }))); +attempt("public key given as the private key", () => crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.privateKey })); + +const p256 = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const p384 = crypto.generateKeyPairSync("ec", { namedCurve: "P-384" }); +const rsa = crypto.generateKeyPairSync("rsa", { modulusLength: 1024 }); +const ed = crypto.generateKeyPairSync("ed25519"); +attempt("x25519 and ec", () => crypto.diffieHellman({ privateKey: x.privateKey, publicKey: p256.publicKey })); +attempt("ec and x25519", () => crypto.diffieHellman({ privateKey: p256.privateKey, publicKey: x.publicKey })); +attempt("rsa", () => crypto.diffieHellman({ privateKey: rsa.privateKey, publicKey: rsa.publicKey })); +attempt("ed25519", () => crypto.diffieHellman({ privateKey: ed.privateKey, publicKey: ed.publicKey })); +attempt("P-256 and P-384", () => crypto.diffieHellman({ privateKey: p256.privateKey, publicKey: p384.publicKey })); +attempt("public as private", () => crypto.diffieHellman({ privateKey: x.publicKey, publicKey: y.publicKey })); +const zero = crypto.createPublicKey({ key: Buffer.alloc(32), format: "raw-public", asymmetricKeyType: "x25519" } as unknown as crypto.PublicKeyInput); +attempt("small-order point", () => crypto.diffieHellman({ privateKey: x.privateKey, publicKey: zero })); + +crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.publicKey }, (err, secret) => { + console.log("callback", err, secret.equals(xy)); + crypto.diffieHellman({ privateKey: x.privateKey, publicKey: zero }, (...args: unknown[]) => { + const error = args[0] as NodeJS.ErrnoException; + console.log("callback error", args.length, error.code, error.message); + }); +}); +console.log("returned"); diff --git a/tests/corpus/7452-crypto-diffie-hellman-errors.mjs b/tests/corpus/7452-crypto-diffie-hellman-errors.mjs new file mode 100644 index 000000000..d8adc6442 --- /dev/null +++ b/tests/corpus/7452-crypto-diffie-hellman-errors.mjs @@ -0,0 +1,61 @@ +// crypto.diffieHellman's argument checks in Node's order, key inputs other +// than KeyObjects, and the own properties of the OpenSSL errors it and +// X25519 signatures throw. +import * as crypto from "node:crypto"; + +function attempt(label, run) { + try { + const result = run(); + console.log(label, Buffer.isBuffer(result) ? `Buffer(${result.length})` : String(result)); + } catch (e) { + console.log(label, "throws", e.name, e.code, e.message, JSON.stringify(Object.keys(e))); + } +} + +const x = crypto.generateKeyPairSync("x25519"); +const y = crypto.generateKeyPairSync("x25519"); +const ec = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); +const other = crypto.generateKeyPairSync("ec", { namedCurve: "P-384" }); +const dh = (options, callback) => () => + callback === undefined ? crypto.diffieHellman(options) : crypto.diffieHellman(options, callback); + +attempt("no options", dh()); +attempt("number options", dh(5)); +attempt("array options", dh([])); +attempt("number callback", dh({ privateKey: x.privateKey, publicKey: y.publicKey }, 5)); +attempt("callback before keys", dh({}, 5)); +attempt("no private key", dh({ publicKey: y.publicKey })); +attempt("no public key", dh({ privateKey: x.privateKey })); +attempt("number public key", dh({ privateKey: x.privateKey, publicKey: 2 })); +attempt("number private key", dh({ privateKey: 1, publicKey: y.publicKey })); +attempt("null private key", dh({ privateKey: null, publicKey: y.publicKey })); +attempt("public key object as private", dh({ privateKey: x.publicKey, publicKey: y.publicKey })); +attempt("types checked first", dh({ privateKey: x.privateKey, publicKey: ec.publicKey })); + +// key inputs other than KeyObjects +const pkcs8 = x.privateKey.export({ format: "pem", type: "pkcs8" }); +const spki = y.publicKey.export({ format: "pem", type: "spki" }); +attempt("pem keys", dh({ privateKey: pkcs8, publicKey: spki })); +attempt("der key objects", dh({ + privateKey: { key: x.privateKey.export({ format: "der", type: "pkcs8" }), format: "der", type: "pkcs8" }, + publicKey: { key: y.publicKey.export({ format: "jwk" }), format: "jwk" }, +})); +attempt("public key from a private pem", dh({ privateKey: x.privateKey, publicKey: pkcs8 })); +attempt("same result", () => + crypto.diffieHellman({ privateKey: pkcs8, publicKey: spki }).equals(crypto.diffieHellman({ privateKey: x.privateKey, publicKey: y.publicKey })), +); +attempt("mixed types from pem", dh({ privateKey: x.privateKey, publicKey: ec.publicKey.export({ format: "pem", type: "spki" }) })); +attempt("mismatched curves", dh({ privateKey: ec.privateKey, publicKey: other.publicKey })); +const zero = crypto.createPublicKey({ key: Buffer.alloc(32), format: "raw-public", asymmetricKeyType: "x25519" }); +attempt("small-order point", dh({ privateKey: x.privateKey, publicKey: zero })); +attempt("sign", () => crypto.sign(null, Buffer.from("x"), x.privateKey)); + +// the callback form: argument errors throw, job errors arrive without +// the OpenSSL properties +attempt("callback returns", dh({ privateKey: x.privateKey, publicKey: y.publicKey }, (err, secret) => { + console.log("callback", err, secret.length); + crypto.diffieHellman({ privateKey: ec.privateKey, publicKey: other.publicKey }, (error) => { + console.log("callback mismatch", error.code, error.message, JSON.stringify(Object.keys(error))); + }); +})); +attempt("callback with a bad key throws", dh({ privateKey: 1, publicKey: y.publicKey }, () => console.log("never"))); diff --git a/tests/fixtures/node-types/diffie-hellman.ts b/tests/fixtures/node-types/diffie-hellman.ts new file mode 100644 index 000000000..d14fd5ffc --- /dev/null +++ b/tests/fixtures/node-types/diffie-hellman.ts @@ -0,0 +1,19 @@ +import { type KeyObject, createPublicKey, diffieHellman, generateKeyPair, generateKeyPairSync } from "node:crypto"; + +const alice = generateKeyPairSync("x25519"); +const bob = generateKeyPairSync("x25519"); +const secret: Buffer = diffieHellman({ privateKey: alice.privateKey, publicKey: bob.publicKey }); +console.log(alice.publicKey.asymmetricKeyType, secret.length, secret.equals(diffieHellman({ privateKey: bob.privateKey, publicKey: alice.publicKey }))); +const exported = alice.publicKey.export({ type: "spki", format: "pem" }); +const pem = typeof exported === "string" ? exported : exported.toString(); +const reread: KeyObject = createPublicKey(pem); +console.log(reread.equals(alice.publicKey), pem.split("\n")[0]); +const ec = generateKeyPairSync("ec", { namedCurve: "P-384" }); +const peer = generateKeyPairSync("ec", { namedCurve: "P-384" }); +console.log("ecdh", diffieHellman({ privateKey: ec.privateKey, publicKey: peer.publicKey }).length); +generateKeyPair("x25519", undefined, (err, publicKey, privateKey) => { + console.log("generated", err, publicKey.type, privateKey.asymmetricKeyType); + diffieHellman({ privateKey, publicKey: bob.publicKey }, (error, shared) => { + console.log("callback", error, shared.length); + }); +}); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 7c468de01..0ddf0ea3f 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -99,12 +99,9 @@ void callbackified; import { createECDH } from "node:crypto"; createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); -/* DiffieHellman's Object.prototype members, and crypto.diffieHellman over - * asymmetric KeyObjects. */ -import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto"; +/* DiffieHellman's Object.prototype members. */ +import { createDiffieHellman } from "node:crypto"; createDiffieHellman(Buffer.from([23])).toString(); -declare const dhKey: KeyObject; -diffieHellman({ privateKey: dhKey, publicKey: dhKey }); /* WebCrypto beyond the secret-key slice: @types/node declares the whole * SubtleCrypto surface, and the methods outside the slice, such as * getPublicKey, fence by name. */ diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 562733f53..d0aa4a140 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -219,42 +219,33 @@ fenced.ts:101:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer 100 | createECDH("sect163k1"); 101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 102 | /* DiffieHellman's Object.prototype members, and crypto.diffieHellman over + 102 | /* DiffieHellman's Object.prototype members. */ hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:105:1 - error SC2020: 'DiffieHellman.toString' is part of the standard library types but has no scriptc lowering yet +fenced.ts:104:1 - error SC2020: 'DiffieHellman.toString' is part of the standard library types but has no scriptc lowering yet - 104 | import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto"; - 105 | createDiffieHellman(Buffer.from([23])).toString(); + 103 | import { createDiffieHellman } from "node:crypto"; + 104 | createDiffieHellman(Buffer.from([23])).toString(); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 106 | declare const dhKey: KeyObject; + 105 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole hint: generateKeys(), computeSecret(), getPrime(), getGenerator(), getPublicKey(), getPrivateKey(), setPublicKey(), setPrivateKey() and verifyError are supported -fenced.ts:107:1 - error SC2020: 'crypto.diffieHellman' is typed by @types/node but has no scriptc lowering yet +fenced.ts:109:9 - error SC2020: 'crypto.subtle.getPublicKey' is part of the standard library types but has no scriptc lowering yet - 106 | declare const dhKey: KeyObject; - 107 | diffieHellman({ privateKey: dhKey, publicKey: dhKey }); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 108 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole - - hint: crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets - -fenced.ts:112:9 - error SC2020: 'crypto.subtle.getPublicKey' is part of the standard library types but has no scriptc lowering yet - - 111 | async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 112 | await crypto.subtle.getPublicKey(key, ["verify"]); + 108 | async function publicKeyWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 109 | await crypto.subtle.getPublicKey(key, ["verify"]); | ^~~~~~~~~~~~~~~~~~~~~~~~~~ - 113 | } + 110 | } hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -fenced.ts:117:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet +fenced.ts:114:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet - 116 | * compile; the other members fence by name. */ - 117 | const cores = navigator.hardwareConcurrency; + 113 | * compile; the other members fence by name. */ + 114 | const cores = navigator.hardwareConcurrency; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 118 | void cores; + 115 | void cores; hint: of Node's navigator global only typeof navigator and navigator.userAgent compile \ No newline at end of file diff --git a/tests/harness/corpus-costs/7450-crypto-x25519.mjs.json b/tests/harness/corpus-costs/7450-crypto-x25519.mjs.json new file mode 100644 index 000000000..727e2cdb6 --- /dev/null +++ b/tests/harness/corpus-costs/7450-crypto-x25519.mjs.json @@ -0,0 +1 @@ +{ "plain": 3330, "san": 4877 } diff --git a/tests/harness/corpus-costs/7451-crypto-diffie-hellman.ts.json b/tests/harness/corpus-costs/7451-crypto-diffie-hellman.ts.json new file mode 100644 index 000000000..a9347d13c --- /dev/null +++ b/tests/harness/corpus-costs/7451-crypto-diffie-hellman.ts.json @@ -0,0 +1 @@ +{ "plain": 3265, "san": 6438 } diff --git a/tests/harness/corpus-costs/7452-crypto-diffie-hellman-errors.mjs.json b/tests/harness/corpus-costs/7452-crypto-diffie-hellman-errors.mjs.json new file mode 100644 index 000000000..72961a07f --- /dev/null +++ b/tests/harness/corpus-costs/7452-crypto-diffie-hellman-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 2917, "san": 4460 } diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 4676499eb..fd2077a48 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -1160,6 +1160,24 @@ test("node-types: asymmetric keys and signatures match Node under @types/node", expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: X25519 keys and diffieHellman match Node under @types/node", async () => { + const outDir = outDirFor("node-diffie-hellman"); + const entry = join(nodeTypesDir, "diffie-hellman.ts"); + const result = await compile(entry, { + outPath: join(outDir, "diffie-hellman"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: WebCrypto ECDSA, Ed25519 and RSA match Node under @types/node", async () => { const outDir = outDirFor("node-webcrypto-ecdsa"); const entry = join(nodeTypesDir, "webcrypto-ecdsa.ts"); From dddcbed335c5ff5cbead198cd2f8c5137cada11f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 23 Oct 2026 14:09:39 -0700 Subject: [PATCH 164/409] Project zod 4 and @hono/zod-validator values at the declaration boundary The declaration-typed boundary already reaches zod 4 schemas and hono's c.req.valid(): zod 4's z.infer and hono's validator Input generics resolve to plain object types in the declaration world. Three gaps remained: - Boolean literal types printed as `false` (the TS7 facade leaves intrinsicName unset), so `{ ok: true } | { ok: false }` lost its true arm and `boolean | undefined` became `undefined | false`. The printer reads the literal's value. - `r.data` after `const r = S.safeParse(x); if (r.success)` never crossed: zod 4's result union is not data (its error arm is a class), and reads on projectable receivers were skipped on the assumption that something else types the receiver. Such reads now cross when the receiver is a package call whose type does not project, or a const bound to one. - Reads through bindings annotated with a projected type no longer get a redundant cast, and aliases are named only for casts that survive. New npm cases zod4-boundary and hono-zod-validator run real zod 4.6.7, hono 4.13.13 and @hono/zod-validator 0.9.1 (vendored file closures) under Node and natively; decl-boundary.test.ts checks that c.req.valid() is a checked cast with a path TypeError. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/decl-projection.ts | 111 +- .../npm/cases/hono-zod-validator/main.ts.json | 61 + .../npm/cases/zod4-boundary/main.ts.json | 6 + .../npm/cases/hono-zod-validator/lying.ts | 42 + .../npm/cases/hono-zod-validator/main.ts | 96 + .../npm/cases/hono-zod-validator/package.json | 1 + .../fixtures/npm/cases/zod4-boundary/main.ts | 146 + .../npm/cases/zod4-boundary/package.json | 1 + .../@hono/zod-validator/dist/index.d.mts | 43 + .../@hono/zod-validator/dist/index.mjs | 28 + .../@hono/zod-validator/package.json | 14 + .../npm/node_modules/hono/dist/compose.js | 50 + .../npm/node_modules/hono/dist/context.js | 381 +++ .../hono/dist/helper/cookie/index.js | 82 + .../npm/node_modules/hono/dist/hono-base.js | 350 +++ .../npm/node_modules/hono/dist/hono.js | 28 + .../node_modules/hono/dist/http-exception.js | 55 + .../npm/node_modules/hono/dist/index.js | 20 + .../npm/node_modules/hono/dist/request.js | 335 +++ .../hono/dist/request/constants.js | 4 + .../npm/node_modules/hono/dist/router.js | 35 + .../hono/dist/router/reg-exp-router/index.js | 3 + .../dist/router/reg-exp-router/matcher.js | 19 + .../hono/dist/router/reg-exp-router/node.js | 91 + .../router/reg-exp-router/prepared-router.js | 94 + .../hono/dist/router/reg-exp-router/router.js | 101 + .../hono/dist/router/reg-exp-router/trie.js | 73 + .../hono/dist/router/smart-router/router.js | 49 + .../hono/dist/router/trie-router/index.js | 2 + .../hono/dist/router/trie-router/node.js | 134 + .../hono/dist/router/trie-router/router.js | 15 + .../node_modules/hono/dist/router/utils.js | 4 + .../hono/dist/types/client/types.d.ts | 204 ++ .../node_modules/hono/dist/types/context.d.ts | 459 +++ .../hono/dist/types/hono-base.d.ts | 221 ++ .../node_modules/hono/dist/types/hono.d.ts | 20 + .../node_modules/hono/dist/types/index.d.ts | 6 + .../node_modules/hono/dist/types/request.d.ts | 326 ++ .../hono/dist/types/request/constants.d.ts | 3 + .../node_modules/hono/dist/types/router.d.ts | 98 + .../node_modules/hono/dist/types/types.d.ts | 214 ++ .../hono/dist/types/utils/body.d.ts | 75 + .../hono/dist/types/utils/headers.d.ts | 10 + .../hono/dist/types/utils/http-status.d.ts | 34 + .../hono/dist/types/utils/mime.d.ts | 73 + .../hono/dist/types/utils/types.d.ts | 62 + .../hono/dist/types/validator/index.d.ts | 3 + .../hono/dist/types/validator/utils.d.ts | 27 + .../hono/dist/types/validator/validator.d.ts | 18 + .../npm/node_modules/hono/dist/utils/body.js | 96 + .../node_modules/hono/dist/utils/buffer.js | 51 + .../node_modules/hono/dist/utils/constants.js | 7 + .../node_modules/hono/dist/utils/cookie.js | 125 + .../node_modules/hono/dist/utils/crypto.js | 34 + .../npm/node_modules/hono/dist/utils/html.js | 103 + .../npm/node_modules/hono/dist/utils/url.js | 201 ++ .../node_modules/hono/dist/validator/index.js | 2 + .../hono/dist/validator/validator.js | 64 + .../npm/node_modules/hono/package.json | 10 +- tests/fixtures/npm/node_modules/zod/LICENSE | 21 + .../fixtures/npm/node_modules/zod/index.d.cts | 3 + tests/fixtures/npm/node_modules/zod/index.js | 7 + .../npm/node_modules/zod/package.json | 23 + .../npm/node_modules/zod/v3/ZodError.d.cts | 164 + .../npm/node_modules/zod/v3/errors.d.cts | 5 + .../npm/node_modules/zod/v3/external.d.cts | 6 + .../zod/v3/helpers/enumUtil.d.cts | 8 + .../zod/v3/helpers/errorUtil.d.cts | 9 + .../zod/v3/helpers/parseUtil.d.cts | 78 + .../zod/v3/helpers/partialUtil.d.cts | 8 + .../zod/v3/helpers/typeAliases.d.cts | 2 + .../node_modules/zod/v3/helpers/util.d.cts | 85 + .../npm/node_modules/zod/v3/index.d.cts | 4 + .../npm/node_modules/zod/v3/locales/en.d.cts | 3 + .../node_modules/zod/v3/standard-schema.d.cts | 102 + .../npm/node_modules/zod/v3/types.d.cts | 1034 +++++++ .../node_modules/zod/v4/classic/checks.d.cts | 1 + .../npm/node_modules/zod/v4/classic/checks.js | 1 + .../node_modules/zod/v4/classic/coerce.d.cts | 17 + .../npm/node_modules/zod/v4/classic/coerce.js | 17 + .../node_modules/zod/v4/classic/compat.d.cts | 48 + .../npm/node_modules/zod/v4/classic/compat.js | 31 + .../zod/v4/classic/deep-partial.d.cts | 14 + .../zod/v4/classic/deep-partial.js | 13 + .../node_modules/zod/v4/classic/errors.d.cts | 29 + .../npm/node_modules/zod/v4/classic/errors.js | 57 + .../zod/v4/classic/external.d.cts | 19 + .../node_modules/zod/v4/classic/external.js | 17 + .../zod/v4/classic/from-json-schema.d.cts | 12 + .../zod/v4/classic/from-json-schema.js | 881 ++++++ .../node_modules/zod/v4/classic/in-out.d.cts | 8 + .../npm/node_modules/zod/v4/classic/in-out.js | 36 + .../node_modules/zod/v4/classic/index.d.cts | 3 + .../npm/node_modules/zod/v4/classic/iso.d.cts | 7 + .../npm/node_modules/zod/v4/classic/iso.js | 15 + .../node_modules/zod/v4/classic/parse.d.cts | 44 + .../npm/node_modules/zod/v4/classic/parse.js | 16 + .../node_modules/zod/v4/classic/schemas.d.cts | 816 +++++ .../node_modules/zod/v4/classic/schemas.js | 1535 ++++++++++ .../npm/node_modules/zod/v4/core/api.d.cts | 335 +++ .../npm/node_modules/zod/v4/core/api.js | 1107 +++++++ .../npm/node_modules/zod/v4/core/checks.d.cts | 294 ++ .../npm/node_modules/zod/v4/core/checks.js | 516 ++++ .../node_modules/zod/v4/core/compile.d.cts | 65 + .../npm/node_modules/zod/v4/core/compile.js | 1866 ++++++++++++ .../npm/node_modules/zod/v4/core/core.d.cts | 82 + .../npm/node_modules/zod/v4/core/core.js | 141 + .../npm/node_modules/zod/v4/core/doc.d.cts | 16 + .../npm/node_modules/zod/v4/core/doc.js | 38 + .../npm/node_modules/zod/v4/core/errors.d.cts | 225 ++ .../npm/node_modules/zod/v4/core/errors.js | 267 ++ .../npm/node_modules/zod/v4/core/index.d.cts | 19 + .../npm/node_modules/zod/v4/core/index.js | 19 + .../zod/v4/core/json-schema-generator.d.cts | 65 + .../zod/v4/core/json-schema-generator.js | 99 + .../zod/v4/core/json-schema-processors.d.cts | 65 + .../zod/v4/core/json-schema-processors.js | 843 ++++++ .../zod/v4/core/json-schema.d.cts | 89 + .../node_modules/zod/v4/core/json-schema.js | 1 + .../node_modules/zod/v4/core/memoizer.d.cts | 15 + .../npm/node_modules/zod/v4/core/memoizer.js | 311 ++ .../npm/node_modules/zod/v4/core/parse.d.cts | 52 + .../npm/node_modules/zod/v4/core/parse.js | 175 ++ .../node_modules/zod/v4/core/regexes.d.cts | 90 + .../npm/node_modules/zod/v4/core/regexes.js | 156 + .../node_modules/zod/v4/core/registries.d.cts | 35 + .../node_modules/zod/v4/core/registries.js | 51 + .../node_modules/zod/v4/core/schemas.d.cts | 1275 ++++++++ .../npm/node_modules/zod/v4/core/schemas.js | 2636 +++++++++++++++++ .../zod/v4/core/standard-schema.d.cts | 126 + .../zod/v4/core/to-json-schema.d.cts | 154 + .../zod/v4/core/to-json-schema.js | 651 ++++ .../npm/node_modules/zod/v4/core/util.d.cts | 280 ++ .../npm/node_modules/zod/v4/core/util.js | 873 ++++++ .../node_modules/zod/v4/core/versions.d.cts | 5 + .../npm/node_modules/zod/v4/core/versions.js | 5 + .../npm/node_modules/zod/v4/core/visit.js | 169 ++ .../npm/node_modules/zod/v4/index.d.cts | 3 + .../npm/node_modules/zod/v4/locales/ar.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ar.js | 111 + .../npm/node_modules/zod/v4/locales/az.d.cts | 5 + .../npm/node_modules/zod/v4/locales/az.js | 110 + .../npm/node_modules/zod/v4/locales/be.d.cts | 5 + .../npm/node_modules/zod/v4/locales/be.js | 168 ++ .../npm/node_modules/zod/v4/locales/bg.d.cts | 5 + .../npm/node_modules/zod/v4/locales/bg.js | 125 + .../npm/node_modules/zod/v4/locales/bn.d.cts | 5 + .../npm/node_modules/zod/v4/locales/bn.js | 113 + .../npm/node_modules/zod/v4/locales/ca.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ca.js | 112 + .../npm/node_modules/zod/v4/locales/ckb.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ckb.js | 133 + .../npm/node_modules/zod/v4/locales/cs.d.cts | 5 + .../npm/node_modules/zod/v4/locales/cs.js | 116 + .../npm/node_modules/zod/v4/locales/da.d.cts | 5 + .../npm/node_modules/zod/v4/locales/da.js | 120 + .../npm/node_modules/zod/v4/locales/de.d.cts | 5 + .../npm/node_modules/zod/v4/locales/de.js | 113 + .../npm/node_modules/zod/v4/locales/el.d.cts | 5 + .../npm/node_modules/zod/v4/locales/el.js | 112 + .../npm/node_modules/zod/v4/locales/en.d.cts | 5 + .../npm/node_modules/zod/v4/locales/en.js | 125 + .../npm/node_modules/zod/v4/locales/eo.d.cts | 5 + .../npm/node_modules/zod/v4/locales/eo.js | 114 + .../npm/node_modules/zod/v4/locales/es.d.cts | 5 + .../npm/node_modules/zod/v4/locales/es.js | 136 + .../npm/node_modules/zod/v4/locales/fa.d.cts | 5 + .../npm/node_modules/zod/v4/locales/fa.js | 119 + .../npm/node_modules/zod/v4/locales/fi.d.cts | 5 + .../npm/node_modules/zod/v4/locales/fi.js | 117 + .../node_modules/zod/v4/locales/fr-CA.d.cts | 5 + .../npm/node_modules/zod/v4/locales/fr-CA.js | 112 + .../npm/node_modules/zod/v4/locales/fr.d.cts | 5 + .../npm/node_modules/zod/v4/locales/fr.js | 129 + .../npm/node_modules/zod/v4/locales/gu.d.cts | 5 + .../npm/node_modules/zod/v4/locales/gu.js | 113 + .../npm/node_modules/zod/v4/locales/he.d.cts | 5 + .../npm/node_modules/zod/v4/locales/he.js | 221 ++ .../npm/node_modules/zod/v4/locales/hi.d.cts | 5 + .../npm/node_modules/zod/v4/locales/hi.js | 111 + .../npm/node_modules/zod/v4/locales/hr.d.cts | 5 + .../npm/node_modules/zod/v4/locales/hr.js | 126 + .../npm/node_modules/zod/v4/locales/hu.d.cts | 5 + .../npm/node_modules/zod/v4/locales/hu.js | 113 + .../npm/node_modules/zod/v4/locales/hy.d.cts | 5 + .../npm/node_modules/zod/v4/locales/hy.js | 158 + .../npm/node_modules/zod/v4/locales/id.d.cts | 5 + .../npm/node_modules/zod/v4/locales/id.js | 111 + .../node_modules/zod/v4/locales/index.d.cts | 63 + .../npm/node_modules/zod/v4/locales/index.js | 63 + .../npm/node_modules/zod/v4/locales/is.d.cts | 5 + .../npm/node_modules/zod/v4/locales/is.js | 114 + .../npm/node_modules/zod/v4/locales/it.d.cts | 5 + .../npm/node_modules/zod/v4/locales/it.js | 113 + .../npm/node_modules/zod/v4/locales/ja.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ja.js | 112 + .../npm/node_modules/zod/v4/locales/ka.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ka.js | 117 + .../npm/node_modules/zod/v4/locales/kh.d.cts | 5 + .../npm/node_modules/zod/v4/locales/kh.js | 5 + .../npm/node_modules/zod/v4/locales/km.d.cts | 5 + .../npm/node_modules/zod/v4/locales/km.js | 115 + .../npm/node_modules/zod/v4/locales/kn.d.cts | 5 + .../npm/node_modules/zod/v4/locales/kn.js | 116 + .../npm/node_modules/zod/v4/locales/ko.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ko.js | 116 + .../npm/node_modules/zod/v4/locales/lt.d.cts | 5 + .../npm/node_modules/zod/v4/locales/lt.js | 207 ++ .../npm/node_modules/zod/v4/locales/mk.d.cts | 5 + .../npm/node_modules/zod/v4/locales/mk.js | 114 + .../npm/node_modules/zod/v4/locales/ms.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ms.js | 112 + .../npm/node_modules/zod/v4/locales/ne.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ne.js | 111 + .../npm/node_modules/zod/v4/locales/nl.d.cts | 5 + .../npm/node_modules/zod/v4/locales/nl.js | 115 + .../npm/node_modules/zod/v4/locales/nn.d.cts | 5 + .../npm/node_modules/zod/v4/locales/nn.js | 113 + .../npm/node_modules/zod/v4/locales/no.d.cts | 5 + .../npm/node_modules/zod/v4/locales/no.js | 113 + .../npm/node_modules/zod/v4/locales/ota.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ota.js | 114 + .../npm/node_modules/zod/v4/locales/pl.d.cts | 5 + .../npm/node_modules/zod/v4/locales/pl.js | 114 + .../npm/node_modules/zod/v4/locales/ps.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ps.js | 119 + .../node_modules/zod/v4/locales/pt-BR.d.cts | 5 + .../npm/node_modules/zod/v4/locales/pt-BR.js | 143 + .../npm/node_modules/zod/v4/locales/pt.d.cts | 5 + .../npm/node_modules/zod/v4/locales/pt.js | 142 + .../npm/node_modules/zod/v4/locales/ro.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ro.js | 122 + .../npm/node_modules/zod/v4/locales/ru.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ru.js | 168 ++ .../npm/node_modules/zod/v4/locales/sk.d.cts | 5 + .../npm/node_modules/zod/v4/locales/sk.js | 116 + .../npm/node_modules/zod/v4/locales/sl.d.cts | 5 + .../npm/node_modules/zod/v4/locales/sl.js | 114 + .../npm/node_modules/zod/v4/locales/sv.d.cts | 5 + .../npm/node_modules/zod/v4/locales/sv.js | 115 + .../npm/node_modules/zod/v4/locales/ta.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ta.js | 115 + .../npm/node_modules/zod/v4/locales/tg.d.cts | 5 + .../npm/node_modules/zod/v4/locales/tg.js | 117 + .../npm/node_modules/zod/v4/locales/th.d.cts | 5 + .../npm/node_modules/zod/v4/locales/th.js | 115 + .../npm/node_modules/zod/v4/locales/tk.d.cts | 5 + .../npm/node_modules/zod/v4/locales/tk.js | 107 + .../npm/node_modules/zod/v4/locales/tr.d.cts | 5 + .../npm/node_modules/zod/v4/locales/tr.js | 110 + .../npm/node_modules/zod/v4/locales/ua.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ua.js | 5 + .../npm/node_modules/zod/v4/locales/uk.d.cts | 5 + .../npm/node_modules/zod/v4/locales/uk.js | 113 + .../npm/node_modules/zod/v4/locales/ur.d.cts | 5 + .../npm/node_modules/zod/v4/locales/ur.js | 115 + .../npm/node_modules/zod/v4/locales/uz.d.cts | 5 + .../npm/node_modules/zod/v4/locales/uz.js | 113 + .../npm/node_modules/zod/v4/locales/vi.d.cts | 5 + .../npm/node_modules/zod/v4/locales/vi.js | 113 + .../npm/node_modules/zod/v4/locales/yo.d.cts | 5 + .../npm/node_modules/zod/v4/locales/yo.js | 112 + .../node_modules/zod/v4/locales/zh-CN.d.cts | 5 + .../npm/node_modules/zod/v4/locales/zh-CN.js | 114 + .../node_modules/zod/v4/locales/zh-TW.d.cts | 5 + .../npm/node_modules/zod/v4/locales/zh-TW.js | 112 + tests/harness/decl-boundary.test.ts | 28 +- tests/harness/npm.test.ts | 11 +- 268 files changed, 31356 insertions(+), 16 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json create mode 100644 tests/fixtures/npm/cases/hono-zod-validator/lying.ts create mode 100644 tests/fixtures/npm/cases/hono-zod-validator/main.ts create mode 100644 tests/fixtures/npm/cases/hono-zod-validator/package.json create mode 100644 tests/fixtures/npm/cases/zod4-boundary/main.ts create mode 100644 tests/fixtures/npm/cases/zod4-boundary/package.json create mode 100644 tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts create mode 100644 tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs create mode 100644 tests/fixtures/npm/node_modules/@hono/zod-validator/package.json create mode 100644 tests/fixtures/npm/node_modules/hono/dist/compose.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/context.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/hono-base.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/hono.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/http-exception.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/request.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/request/constants.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/router/utils.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/body.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/constants.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/html.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/utils/url.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/validator/index.js create mode 100644 tests/fixtures/npm/node_modules/hono/dist/validator/validator.js create mode 100644 tests/fixtures/npm/node_modules/zod/LICENSE create mode 100644 tests/fixtures/npm/node_modules/zod/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/index.js create mode 100644 tests/fixtures/npm/node_modules/zod/package.json create mode 100644 tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/errors.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/external.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v3/types.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/checks.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/compat.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/errors.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/external.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/iso.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/parse.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/classic/schemas.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/api.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/api.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/checks.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/checks.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/compile.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/compile.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/core.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/core.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/doc.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/doc.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/errors.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/errors.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/index.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema-generator.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema-generator.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema-processors.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema-processors.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/json-schema.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/memoizer.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/memoizer.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/parse.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/parse.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/regexes.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/regexes.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/registries.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/registries.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/schemas.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/schemas.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/standard-schema.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/to-json-schema.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/to-json-schema.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/util.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/util.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/versions.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/versions.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/core/visit.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ar.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ar.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/az.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/az.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/be.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/be.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/bg.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/bg.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/bn.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/bn.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ca.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ca.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ckb.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ckb.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/cs.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/cs.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/da.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/da.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/de.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/de.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/el.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/el.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/en.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/en.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/eo.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/eo.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/es.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/es.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fa.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fa.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fi.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fi.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fr-CA.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fr-CA.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fr.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/fr.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/gu.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/gu.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/he.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/he.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hi.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hi.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hr.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hr.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hu.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hu.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hy.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/hy.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/id.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/id.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/index.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/index.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/is.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/is.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/it.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/it.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ja.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ja.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ka.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ka.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/kh.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/kh.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/km.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/km.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/kn.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/kn.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ko.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ko.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/lt.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/lt.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/mk.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/mk.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ms.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ms.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ne.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ne.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/nl.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/nl.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/nn.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/nn.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/no.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/no.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ota.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ota.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pl.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pl.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ps.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ps.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pt-BR.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pt-BR.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pt.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/pt.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ro.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ro.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ru.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ru.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sk.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sk.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sl.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sl.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sv.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/sv.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ta.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ta.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tg.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tg.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/th.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/th.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tk.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tk.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tr.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/tr.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ua.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ua.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/uk.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/uk.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ur.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/ur.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/uz.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/uz.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/vi.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/vi.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/yo.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/yo.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/zh-CN.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/zh-CN.js create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/zh-TW.d.cts create mode 100644 tests/fixtures/npm/node_modules/zod/v4/locales/zh-TW.js diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index eaf7486df..02a300996 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -185,8 +185,24 @@ class TypePrinter { readonly declarations: string[] = []; private readonly inProgress = new Map(); + /** While set, printing registers no recursive alias (projectable()). */ + private dry = false; + constructor(private readonly checker: ts.TypeChecker) {} + /** Whether a type projects, without naming it in the generated file. */ + projectable(type: ts.Type): boolean { + if (type.flags & ANYISH) return false; + const was = this.dry; + this.dry = true; + try { + const text = this.print(type, 0); + return text !== null && text !== "unknown"; + } finally { + this.dry = was; + } + } + /** The alias naming a projectable type, or null. Top-level any/unknown * never projects. */ project(type: ts.Type): string | null { @@ -212,7 +228,12 @@ class TypePrinter { if (f & ts.TypeFlags.Null) return "null"; if (f & ts.TypeFlags.Undefined) return "undefined"; if (f & ts.TypeFlags.EnumLiteral || f & ts.TypeFlags.Enum) return null; - if (f & ts.TypeFlags.BooleanLiteral) return type.intrinsicName === "true" ? "true" : "false"; + // A boolean literal type carries its value (`intrinsicName` is not + // set for it by every checker facade). + if (f & ts.TypeFlags.BooleanLiteral) + return (type as ts.BooleanLiteralType).value === true || type.intrinsicName === "true" + ? "true" + : "false"; if (f & ts.TypeFlags.StringLiteral) return JSON.stringify(String(type.value)); if (f & ts.TypeFlags.NumberLiteral) { const v = Number(type.value); @@ -341,7 +362,7 @@ class TypePrinter { if (props.length > 0 && readonlyCount === props.length) return null; const text = `{ ${members.join("; ")} }`; if (frame.name !== null) { - this.declarations.push(`type ${frame.name} = ${text};`); + if (!this.dry) this.declarations.push(`type ${frame.name} = ${text};`); return frame.name; } return text; @@ -517,9 +538,17 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const declSf = input.declProgram.getSourceFile(file); if (jsSf === undefined || declSf === undefined || jsSf.text !== declSf.text) continue; const fileEdits: Edit[] = []; - /** Candidate expressions (by js node) with their alias. */ - const exprCandidates = new Map(); + /** Candidate expressions (by js node) with their declared type, named + * only for the casts that survive suppression. */ + const exprCandidates = new Map(); const exprOrder: ts.Node[] = []; + /** Reads whose projectable receiver the JavaScript world types `any`: + * kept only when nothing types the receiver. */ + const deferred = new Set(); + /** Package calls whose declared type does not project. */ + const unprojected = new Set(); + /** The declaration-world twin of every walked node. */ + const twins = new Map(); const considerType = (jsNode: ts.Node, declNode: ts.Node): boolean => { if ( @@ -603,8 +632,18 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | } else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) { const recvDecl = typeAt(declChecker, d.expression); const recvJs = typeAt(jsChecker, (jsNode as ts.PropertyAccessExpression).expression); - const recvProjectable = recvDecl !== null && printer.project(recvDecl) !== null; + const recvProjectable = recvDecl !== null && printer.projectable(recvDecl); produced = !recvProjectable || (recvJs !== null && !isAnyish(recvJs)); + // A projectable receiver the JavaScript world types `any` is + // usually typed by another candidate. One that is a package value + // nothing projects stays `any`, such as a safeParse result narrowed + // to its success arm (the whole result is not data: its error arm + // is a class): the read crosses here instead, if the suppression + // below confirms the receiver stays untyped. + if (!produced) { + produced = true; + deferred.add(jsNode); + } } else if (ts.isAwaitExpression(d)) { const inner = unwrapTransparent(d.expression); produced = @@ -633,6 +672,12 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | declaredInPackage(resolveAlias(raw)); } if (!produced) return; + if (isCall && !deferred.has(jsNode)) { + // Recorded before the contextual-type test below: a package value + // whose type does not project stays `any` wherever it flows. + const t = typeAt(declChecker, d); + if (t === null || !printer.projectable(t)) unprojected.add(jsNode); + } // A consumer that takes anything needs no conversion. try { const ctx = declChecker.getContextualType(d); @@ -641,10 +686,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | // no contextual type } const declType = typeAt(declChecker, d); - if (declType === null) return; - const alias = printer.project(declType); - if (alias === null) return; - exprCandidates.set(jsNode, alias); + if (declType === null || !printer.projectable(declType)) return; + exprCandidates.set(jsNode, declType); exprOrder.push(jsNode); }; @@ -703,6 +746,7 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | const walk = (jsNode: ts.Node, declNode: ts.Node): void => { if (jsNode.kind !== declNode.kind) return; + twins.set(jsNode, declNode); if (ts.isTypeNode(jsNode) || ts.isExpressionWithTypeArguments(jsNode)) { if (considerType(jsNode, declNode)) return; } else { @@ -749,21 +793,67 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | result = willBeTyped(decl.initializer, depth + 1); else if (ts.isParameter(decl) && annotated.has(decl)) result = true; else if (ts.isBindingElement(decl)) result = false; + else if ( + (ts.isVariableDeclaration(decl) || ts.isParameter(decl)) && + decl.type !== undefined + ) + result = annotationWillBeTyped(decl.type); } symbolTyped.set(sym, result); return result; } return false; }; + /** An annotation in this file whose declaration-world type projects: + * after the rewrite it names the projected structure (a package type + * is replaced by its alias, a user alias of one has its right-hand + * side replaced). */ + const annotationWillBeTyped = (type: ts.TypeNode): boolean => { + const twin = twins.get(type); + if (twin === undefined) return false; + let declType: ts.Type | null; + try { + declType = declChecker.getTypeFromTypeNode(twin as ts.TypeNode); + } catch { + return false; + } + return declType !== null && printer.projectable(declType); + }; const receiverOf = (e: ts.Node): ts.Node | null => { if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return e.expression; if (ts.isIdentifier(e)) return null; return null; }; const kept: ts.Node[] = []; + /** A receiver that stays `any` after the rewrite: a package call whose + * type does not project, or a user const bound to one. */ + const staysUntyped = (e: ts.Node): boolean => { + const u = unwrapTransparent(e as ts.Expression); + if (unprojected.has(u)) return true; + if (!ts.isIdentifier(u)) return false; + let sym: ts.Symbol | undefined; + try { + sym = jsChecker.getSymbolAtLocation(u); + } catch { + return false; + } + const decls = sym?.declarations ?? []; + if (decls.length !== 1) return false; + const decl = decls[0]!; + return ( + ts.isVariableDeclaration(decl) && + decl.type === undefined && + decl.initializer !== undefined && + decl.parent !== undefined && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 && + unprojected.has(unwrapTransparent(decl.initializer)) + ); + }; for (const e of exprOrder) { const recv = receiverOf(e); if (recv !== null && willBeTyped(recv)) continue; + if (deferred.has(e) && (recv === null || !staysUntyped(recv))) continue; // An identifier candidate that is a user binding never gets here // (only package imports are candidates). kept.push(e); @@ -800,7 +890,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | ) continue; } - const alias = exprCandidates.get(e)!; + const alias = printer.project(exprCandidates.get(e)!); + if (alias === null) continue; const start = ts.tokenStartOfNode(e); const stmt = e.parent; let prefix = "("; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json new file mode 100644 index 000000000..72c2578e6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-validator/main.ts.json @@ -0,0 +1,61 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/hono-zod-validator/main.ts" + ], + "diags": [ + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 1398, + "end": 1407 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 1413, + "end": 1422 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'WebSocket'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 3716, + "end": 3725 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts", + "start": 4798, + "end": 4806 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts", + "start": 549, + "end": 557 + } + }, + { + "code": "SC0001", + "message": "Cannot find name 'FormData'.", + "loc": { + "file": "/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts", + "start": 5572, + "end": 5580 + } + } + ] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json new file mode 100644 index 000000000..0d656e939 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/zod4-boundary/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/zod4-boundary/main.ts" + ], + "diags": [] +} diff --git a/tests/fixtures/npm/cases/hono-zod-validator/lying.ts b/tests/fixtures/npm/cases/hono-zod-validator/lying.ts new file mode 100644 index 000000000..fa44c2e12 --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-validator/lying.ts @@ -0,0 +1,42 @@ +// Native-only (decl-boundary.test.ts): `c.req.valid()` is a checked +// boundary. z.custom checks nothing, so a body its declared type +// misdescribes fails the cast with a path TypeError, which hono's error +// handler reports; Node does not check. A union discriminated by boolean +// literals keeps both arms. +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; + +const Liar = z.custom<{ n: number }>(); +const Outcome = z.discriminatedUnion("ok", [ + z.object({ ok: z.literal(true), value: z.number() }), + z.object({ ok: z.literal(false), reason: z.string() }), +]); + +const app = new Hono(); +app.onError((err, c) => c.text(`${err instanceof TypeError} ${err.message}`, 500)); +app.post("/liar", zValidator("json", Liar), (c) => c.text(String(c.req.valid("json").n + 1))); +app.post("/outcome", zValidator("json", Outcome), (c) => { + const outcome = c.req.valid("json"); + return c.text(outcome.ok ? `value ${outcome.value + 1}` : `reason ${outcome.reason}`); +}); + +const requests: [string, unknown][] = [ + ["/liar", { n: 1 }], + ["/liar", { n: "x" }], + ["/outcome", { ok: true, value: 41 }], + ["/outcome", { ok: false, reason: "late" }], +]; + +async function main(): Promise { + for (const [path, body] of requests) { + const res = await app.request(path, { + method: "POST", + body: JSON.stringify(body), + headers: { "content-type": "application/json" }, + }); + console.log(path, res.status, await res.text()); + } +} + +void main(); diff --git a/tests/fixtures/npm/cases/hono-zod-validator/main.ts b/tests/fixtures/npm/cases/hono-zod-validator/main.ts new file mode 100644 index 000000000..9de69c482 --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-validator/main.ts @@ -0,0 +1,96 @@ +// A Hono app validated by @hono/zod-validator and zod 4.6, all compiled +// from their JavaScript: `c.req.valid(target)` hands the handler the +// parsed value, typed by the schema's z.infer through hono's declarations. +// Requests go through app.request, so no socket is involved. +import { zValidator } from "@hono/zod-validator"; +import { Hono, type Context } from "hono"; +import { z } from "zod"; + +const Book = z.strictObject({ + title: z.string().trim().min(1), + year: z.number().int().min(1450), + tags: z.array(z.string()).max(4).default([]), + rating: z.number().min(0).max(5).optional(), +}); +type Book = z.infer; +const ListQuery = z.object({ + minYear: z.coerce.number().int().optional(), + sort: z.enum(["title", "year"]).default("title"), + limit: z.coerce.number().int().min(1).max(10).default(3), +}); +const IdParam = z.object({ id: z.string().regex(/^b\d+$/) }); + +// Failed checks answer with the issues' paths and codes. +const reject = ( + result: { success: true } | { success: false; error: z.core.$ZodError }, + c: Context, +): Response | undefined => { + if (result.success) return undefined; + const paths = result.error.issues.map((i) => `${i.path.join(".")}:${i.code}`); + return c.json({ error: "invalid", paths }, 400); +}; + +const books = new Map(); +let nextId = 1; + +const app = new Hono(); +app.post( + "/books", + zValidator("json", Book, (result, c) => { + if (!result.success) { + const paths = result.error.issues.map((i) => `${i.path.join(".")}:${i.code}`); + return c.json({ error: "invalid", paths }, 400); + } + }), + (c) => { + const book = c.req.valid("json"); + book.tags.push("new"); + const id = `b${nextId++}`; + books.set(id, book); + const score = (book.rating ?? 0) * 2 + book.tags.length; + return c.json({ id, title: book.title.toUpperCase(), year: book.year, score }, 201); + }, +); +app.get("/books", zValidator("query", ListQuery, reject), (c) => { + const { minYear, sort, limit } = c.req.valid("query"); + const rows = [...books.values()] + .filter((b) => minYear === undefined || b.year >= minYear) + .sort((a, b) => (sort === "year" ? a.year - b.year : a.title < b.title ? -1 : 1)) + .slice(0, limit) + .map((b) => `${b.title}/${b.year}`); + return c.json({ sort, limit, rows }); +}); +app.get("/books/:id", zValidator("param", IdParam, reject), (c) => { + const id = c.req.valid("param").id; + const book = books.get(id); + if (book === undefined) return c.json({ error: "missing", id }, 404); + return c.json({ id, ...book }); +}); + +async function call(method: string, path: string, body?: unknown): Promise { + const init: RequestInit = { method }; + if (body !== undefined) { + init.body = JSON.stringify(body); + init.headers = { "content-type": "application/json" }; + } + const res = await app.request(path, init); + console.log(method, path, res.status, await res.text()); +} + +async function main(): Promise { + await call("POST", "/books", { title: " Dune ", year: 1965, tags: ["sf"], rating: 4.5 }); + await call("POST", "/books", { title: "Emma", year: 1815 }); + await call("POST", "/books", { title: "Ulysses", year: 1922, tags: ["modern"] }); + await call("POST", "/books", { title: "", year: 1200, extra: true }); + await call("POST", "/books", { title: "Bad", year: "x", tags: [1, 2, 3, 4, 5] }); + await call("GET", "/books"); + await call("GET", "/books?minYear=1900&sort=year&limit=5"); + await call("GET", "/books?limit=99"); + await call("GET", "/books/b2"); + await call("GET", "/books/b9"); + await call("GET", "/books/nope"); + const kept = books.get("b1")!; + console.log("stored", kept.tags.join(","), kept.title); +} + +void main(); diff --git a/tests/fixtures/npm/cases/hono-zod-validator/package.json b/tests/fixtures/npm/cases/hono-zod-validator/package.json new file mode 100644 index 000000000..27e5f1b4b --- /dev/null +++ b/tests/fixtures/npm/cases/hono-zod-validator/package.json @@ -0,0 +1 @@ +{ "name": "hono-zod-validator-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/cases/zod4-boundary/main.ts b/tests/fixtures/npm/cases/zod4-boundary/main.ts new file mode 100644 index 000000000..b4079d1be --- /dev/null +++ b/tests/fixtures/npm/cases/zod4-boundary/main.ts @@ -0,0 +1,146 @@ +// Values that cross from zod 4.6 (compiled from its JavaScript) into code +// typed by zod's declarations: z.infer, z.input and z.output, identity, +// aliasing and mutation after parse, values zod hands back unchanged, +// callbacks zod calls, typed array work, and caught failures. +import { z } from "zod"; + +const show = (label: string, value: unknown): void => { + console.log(label + " " + JSON.stringify(value)); +}; + +// Typed records: field reads, writes and array work on parsed data. +const Item = z.object({ + sku: z.string(), + price: z.number().nonnegative(), + qty: z.number().int(), + tags: z.array(z.string()).default([]), + note: z.string().optional(), + owner: z.string().nullable(), +}); +type Item = z.infer; +const items: Item[] = z.array(Item).parse([ + { sku: "a", price: 2.5, qty: 4, tags: ["x"], owner: null }, + { sku: "b", price: 10, qty: 1, owner: "ann", note: "fragile" }, + { sku: "c", price: 0.5, qty: 10, tags: ["x", "y"], owner: null }, +]); +const total = items.reduce((sum, it) => sum + it.price * it.qty, 0); +console.log("total", total); +const byTag = new Map(); +for (const it of items) for (const t of it.tags) byTag.set(t, (byTag.get(t) ?? 0) + it.qty); +console.log("byTag", [...byTag].map(([k, v]) => `${k}=${v}`).join(",")); +const sorted = [...items].sort((a, b) => b.price - a.price).map((it) => it.sku); +console.log("sorted", sorted.join(",")); +console.log("optional", items.map((it) => `${it.note ?? "-"}/${it.owner ?? "nobody"}`).join(",")); +console.log("keys", items.map((it) => Object.keys(it).join("+")).join(" ")); + +// Mutation after parse is visible through every reference. +const first = items[0]!; +first.qty += 1; +first.tags.push("z"); +console.log("mutated", items[0]!.qty, items[0]!.tags.join("|"), first === items[0]); +show("item", items[0]); + +// Strict objects reject unknown keys; the error is zod's, caught. +const Strict = z.strictObject({ name: z.string().trim().min(1), email: z.email() }); +type Strict = z.infer; +const person: Strict = Strict.parse({ name: " Ada ", email: "ada@example.com" }); +console.log("strict", person.name.length, person.email.toUpperCase()); +const rejected = Strict.safeParse({ name: "x", email: "x@example.com", extra: 1 }); +console.log("strict bad", rejected.success, rejected.error?.issues.map((i) => i.code).join(",")); + +// z.input and z.output differ across a transform and a default. +const Config = z.object({ + port: z.string().transform((s) => Number(s)), + host: z.string().default("localhost"), + debug: z.enum(["true", "false"]).transform((v) => v === "true"), +}); +type ConfigIn = z.input; +type ConfigOut = z.output; +const raw: ConfigIn = { port: "8080", debug: "true" }; +const config: ConfigOut = Config.parse(raw); +console.log("config", config.port + 1, config.host, config.debug, typeof config.port); + +// A value zod hands back unchanged keeps its identity and its mutations. +const Point = z.custom<{ x: number; y: number }>(); +const original = { x: 1, y: 2 }; +const same = Point.parse(original); +same.x = 10; +console.log("custom", original.x, same === original); +original.y = 20; +console.log("custom back", same.y); + +// A shared object returned by a transform: every parse sees the same one. +const shared = { hits: 0 }; +const Counter = z.string().transform(() => shared); +const c1 = Counter.parse("a"); +const c2 = Counter.parse("b"); +c1.hits += 1; +console.log("shared", c2.hits, shared.hits, c1 === c2); + +// A refine callback reads typed fields of the value zod checks. +const Range = z + .object({ lo: z.number(), hi: z.number() }) + .refine((r) => r.lo <= r.hi, { message: "lo must not exceed hi" }); +console.log("range ok", JSON.stringify(Range.parse({ lo: 1, hi: 3 }))); +const bad = Range.safeParse({ lo: 5, hi: 3 }); +console.log("range bad", bad.success ? "?" : bad.error.issues[0]?.message); + +// safeParse data, a discriminated union, and loose objects' extra keys. +const Event = z.discriminatedUnion("type", [ + z.object({ type: z.literal("click"), x: z.number(), y: z.number() }), + z.object({ type: z.literal("key"), key: z.string() }), +]); +type Event = z.infer; +const describe = (e: Event): string => (e.type === "click" ? `click@${e.x},${e.y}` : `key:${e.key}`); +const r = Event.safeParse({ type: "key", key: "Enter" }); +if (r.success) console.log("event", describe(r.data)); +const events = z.array(Event).parse([ + { type: "click", x: 1, y: 2 }, + { type: "key", key: "a" }, +]); +console.log("events", events.map(describe).join(" ")); +const Loose = z.looseObject({ id: z.number() }); +const loose = Loose.parse({ extra: true, id: 3, more: "m" }); +console.log("loose", loose.id, Object.keys(loose).join(","), JSON.stringify(loose)); + +// A union discriminated by boolean literals, read through a safeParse +// result narrowed to its success arm. +const Outcome = z.discriminatedUnion("ok", [ + z.object({ ok: z.literal(true), value: z.number() }), + z.object({ ok: z.literal(false), reason: z.string() }), +]); +type Outcome = z.infer; +const outcomes: Outcome[] = []; +for (const input of [{ ok: true, value: 41 }, { ok: false, reason: "late" }, { ok: "maybe" }]) { + const parsed = Outcome.safeParse(input); + if (parsed.success) outcomes.push(parsed.data); + else console.log("outcome bad", parsed.error.issues.length); +} +console.log("outcomes", outcomes.map((o) => (o.ok ? `value ${o.value + 1}` : `reason ${o.reason}`)).join(", ")); +const Flags = z.object({ beta: z.boolean().optional(), mode: z.enum(["a", "b"]) }); +const flagged = Flags.safeParse({ beta: true, mode: "b" }); +if (flagged.success) { + const flags = flagged.data; + console.log("flags", flags.beta === true ? "beta on" : "beta off", flags.mode); +} + +// Tuples, brands and coerced query-style input. +const Pair = z.tuple([z.string(), z.number()]); +const [label, count] = Pair.parse(["n", 4]); +console.log("tuple", label, count + 1); +const UserId = z.string().brand<"UserId">(); +const uid = UserId.parse("u_1"); +console.log("brand", uid.length, uid); +const Query = z.object({ page: z.coerce.number().int().min(1).default(1), q: z.string().optional() }); +const query = Query.parse({ page: "3" }); +console.log("query", query.page * 10, query.q === undefined, "q" in query); + +// async parse +async function main(): Promise { + const it = await Item.parseAsync({ sku: "d", price: 1, qty: 2, owner: null }); + console.log("async", it.sku, it.price * it.qty, it.tags.length); + const failed = await Item.safeParseAsync({ sku: 1, price: -1, qty: 0.5, owner: null }); + console.log("async bad", failed.success, failed.error?.issues.map((i) => i.path.join(".")).join(",")); +} + +void main(); diff --git a/tests/fixtures/npm/cases/zod4-boundary/package.json b/tests/fixtures/npm/cases/zod4-boundary/package.json new file mode 100644 index 000000000..39bd81280 --- /dev/null +++ b/tests/fixtures/npm/cases/zod4-boundary/package.json @@ -0,0 +1 @@ +{ "name": "zod4-boundary-case", "private": true, "type": "module" } diff --git a/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts new file mode 100644 index 000000000..6028477a3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.d.mts @@ -0,0 +1,43 @@ +import { InferInput } from "hono/validator"; +import { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from "hono"; +import * as v3 from "zod/v3"; +import { ZodSafeParseResult } from "zod/v4"; +import * as v4 from "zod/v4/core"; +//#region src/index.d.ts +type ZodSchema = v3.ZodType | v4.$ZodType; +type ZodError = T extends v4.$ZodType ? v4.$ZodError> : v3.ZodError; +type ZodSafeParseResult$1 = T3 extends v4.$ZodType ? ZodSafeParseResult : v3.SafeParseReturnType; +type zInput = T extends v3.ZodType ? v3.input : T extends v4.$ZodType ? v4.input : never; +type zOutput = T extends v3.ZodType ? v3.output : T extends v4.$ZodType ? v4.output : never; +type zInfer = T extends v3.ZodType ? v3.infer : T extends v4.$ZodType ? v4.infer : never; +type ZodValidatorFailureBody = T extends v4.$ZodType ? Extract>, { + success: false; +}> : T extends v3.ZodType ? Extract, v3.output>, { + success: false; +}> : never; +type ZodValidatorFailureResponse = Response & TypedResponse, 400, "json">; +type Hook = (result: ({ + success: true; + data: T; +} | { + success: false; + error: ZodError; + data: T; +}) & { + target: Target; +}, c: Context) => Response | void | TypedResponse | Promise>; +type HasUndefined = undefined extends T ? true : false; +type ExtractValidationResponse = VF extends ((value: any, c: any) => infer R) ? R extends Promise ? PR extends TypedResponse ? Response & TypedResponse : PR extends Response ? PR : PR extends undefined ? never : never : R extends TypedResponse ? Response & TypedResponse : R extends Response ? R : R extends undefined ? never : never : never; +type ValidationFunctionOption = { + validationFunction: (schema: T, value: ValidationTargets[Target]) => ZodSafeParseResult$1 | Promise>; +}; +type DefaultInput = { + in: HasUndefined extends true ? { [K in Target]?: [In] extends [ValidationTargets[K]] ? In : InferInput; } : { [K in Target]: [In] extends [ValidationTargets[K]] ? In : InferInput; }; + out: { [K in Target]: Out; }; +}; +declare function zValidatorFunction, Out = zOutput, I extends Input = DefaultInput, V extends I = I>(target: Target, schema: T, hook?: undefined, options?: ValidationFunctionOption): MiddlewareHandler>; +declare function zValidatorFunction, In = zInput, Out = zOutput, I extends Input = DefaultInput, V extends I = I, InferredValue = zInfer>(target: Target, schema: T, hook?: HookFn, options?: ValidationFunctionOption): MiddlewareHandler>; +declare const zValidator: typeof zValidatorFunction; +//#endregion +export { Hook, zValidator }; +//# sourceMappingURL=index.d.mts.map \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs new file mode 100644 index 000000000..946d23d2a --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/zod-validator/dist/index.mjs @@ -0,0 +1,28 @@ +import { validator } from "hono/validator"; +//#region src/index.ts +function zValidatorFunction(target, schema, hook, options) { + const caseInsensitiveKeymap = target === "header" && ("_def" in schema || "_zod" in schema) ? Object.fromEntries(Object.keys("in" in schema ? schema.in.shape : schema.shape).map((key) => [key.toLowerCase(), key])) : void 0; + return validator(target, async (value, c) => { + let validatorValue = value; + if (caseInsensitiveKeymap) validatorValue = Object.fromEntries(Object.entries(value).map(([key, value]) => [caseInsensitiveKeymap[key] || key, value])); + const result = options && options.validationFunction ? await options.validationFunction(schema, validatorValue) : await schema.safeParseAsync(validatorValue); + if (hook) { + const hookResult = await hook({ + data: validatorValue, + ...result, + target + }, c); + if (hookResult) { + if (hookResult instanceof Response) return hookResult; + if ("response" in hookResult) return hookResult.response; + } + } + if (!result.success) return c.json(result, 400); + return result.data; + }); +} +const zValidator = zValidatorFunction; +//#endregion +export { zValidator }; + +//# sourceMappingURL=index.mjs.map \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/@hono/zod-validator/package.json b/tests/fixtures/npm/node_modules/@hono/zod-validator/package.json new file mode 100644 index 000000000..31355f555 --- /dev/null +++ b/tests/fixtures/npm/node_modules/@hono/zod-validator/package.json @@ -0,0 +1,14 @@ +{ + "name": "@hono/zod-validator", + "version": "0.9.1", + "description": "Test fixture: the ESM build and its declarations.", + "type": "module", + "license": "MIT", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + } +} diff --git a/tests/fixtures/npm/node_modules/hono/dist/compose.js b/tests/fixtures/npm/node_modules/hono/dist/compose.js new file mode 100644 index 000000000..ec576df0f --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/compose.js @@ -0,0 +1,50 @@ +//#region src/compose.ts +/** +* Compose middleware functions into a single function based on `koa-compose` package. +* +* @template E - The environment type. +* +* @param {[[Function, unknown], unknown][] | [[Function]][]} middleware - An array of middleware functions and their corresponding parameters. +* @param {ErrorHandler} [onError] - An optional error handler function. +* @param {NotFoundHandler} [onNotFound] - An optional not-found handler function. +* +* @returns {(context: Context, next?: Next) => Promise} - A composed middleware function. +*/ +const compose = (middleware, onError, onNotFound) => { + return (context, next) => { + let index = -1; + return dispatch(0); + /** + * Dispatch the middleware functions. + * + * @param {number} i - The current index in the middleware array. + * + * @returns {Promise} - A promise that resolves to the context. + */ + async function dispatch(i) { + if (i <= index) throw new Error("next() called multiple times"); + index = i; + let res; + let isError = false; + let handler; + if (middleware[i]) { + handler = middleware[i][0][0]; + context.req.routeIndex = i; + } else handler = i === middleware.length && next || void 0; + if (handler) try { + res = await handler(context, () => dispatch(i + 1)); + } catch (err) { + if (err instanceof Error && onError) { + context.error = err; + res = await onError(err, context); + isError = true; + } else throw err; + } + else if (context.finalized === false && onNotFound) res = await onNotFound(context); + if (res && (context.finalized === false || isError)) context.res = res; + return context; + } + }; +}; +//#endregion +export { compose }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/context.js b/tests/fixtures/npm/node_modules/hono/dist/context.js new file mode 100644 index 000000000..7f8184af5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/context.js @@ -0,0 +1,381 @@ +import { HonoRequest } from "./request.js"; +import { HtmlEscapedCallbackPhase, resolveCallback } from "./utils/html.js"; +//#region src/context.ts +const TEXT_PLAIN = "text/plain; charset=UTF-8"; +const setDefaultContentType = (contentType, headers) => { + return { + "Content-Type": contentType, + ...headers + }; +}; +const createResponseInstance = (body, init) => new Response(body, init); +var Context = class { + #rawRequest; + #req; + /** + * `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers. + * + * @see {@link https://hono.dev/docs/api/context#env} + * + * @example + * ```ts + * // Environment object for Cloudflare Workers + * app.get('*', async c => { + * const counter = c.env.COUNTER + * }) + * ``` + */ + env = {}; + #var; + finalized = false; + /** + * `.error` can get the error object from the middleware if the Handler throws an error. + * + * @see {@link https://hono.dev/docs/api/context#error} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * await next() + * if (c.error) { + * // do something... + * } + * }) + * ``` + */ + error; + #status; + #executionCtx; + #res; + #layout; + #renderer; + #notFoundHandler; + #preparedHeaders; + #matchResult; + #path; + /** + * Creates an instance of the Context class. + * + * @param req - The Request object. + * @param options - Optional configuration options for the context. + */ + constructor(req, options) { + this.#rawRequest = req; + if (options) { + this.#executionCtx = options.executionCtx; + this.env = options.env; + this.#notFoundHandler = options.notFoundHandler; + this.#path = options.path; + this.#matchResult = options.matchResult; + } + } + /** + * `.req` is the instance of {@link HonoRequest}. + */ + get req() { + this.#req ??= new HonoRequest(this.#rawRequest, this.#path, this.#matchResult); + return this.#req; + } + /** + * @see {@link https://hono.dev/docs/api/context#event} + * The FetchEvent associated with the current request. + * + * @throws Will throw an error if the context does not have a FetchEvent. + */ + get event() { + if (this.#executionCtx && "respondWith" in this.#executionCtx) return this.#executionCtx; + else throw Error("This context has no FetchEvent"); + } + /** + * @see {@link https://hono.dev/docs/api/context#executionctx} + * The ExecutionContext associated with the current request. + * + * @throws Will throw an error if the context does not have an ExecutionContext. + */ + get executionCtx() { + if (this.#executionCtx) return this.#executionCtx; + else throw Error("This context has no ExecutionContext"); + } + /** + * @see {@link https://hono.dev/docs/api/context#res} + * The Response object for the current request. + */ + get res() { + return this.#res ||= createResponseInstance(null, { headers: this.#preparedHeaders ??= new Headers() }); + } + /** + * Sets the Response object for the current request. + * + * @param _res - The Response object to set. + */ + set res(_res) { + if (this.#res && _res) { + _res = createResponseInstance(_res.body, _res); + for (const [k, v] of this.#res.headers.entries()) { + if (k === "content-type") continue; + if (k === "set-cookie") { + const cookies = this.#res.headers.getSetCookie(); + _res.headers.delete("set-cookie"); + for (const cookie of cookies) _res.headers.append("set-cookie", cookie); + } else _res.headers.set(k, v); + } + } + this.#res = _res; + this.finalized = true; + } + /** + * `.render()` can create a response within a layout. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```ts + * app.get('/', (c) => { + * return c.render('Hello!') + * }) + * ``` + */ + render = (...args) => { + this.#renderer ??= (content) => this.html(content); + return this.#renderer(...args); + }; + /** + * Sets the layout for the response. + * + * @param layout - The layout to set. + * @returns The layout function. + */ + setLayout = (layout) => this.#layout = layout; + /** + * Gets the current layout for the response. + * + * @returns The current layout function. + */ + getLayout = () => this.#layout; + /** + * `.setRenderer()` can set the layout in the custom middleware. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```tsx + * app.use('*', async (c, next) => { + * c.setRenderer((content) => { + * return c.html( + * + * + *

{content}

+ * + * + * ) + * }) + * await next() + * }) + * ``` + */ + setRenderer = (renderer) => { + this.#renderer = renderer; + }; + /** + * `.header()` can set headers. + * + * @see {@link https://hono.dev/docs/api/context#header} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * + * // Append multiple headers using the append option (e.g. Vary) + * c.header('Vary', 'Accept-Encoding', { append: true }) + * c.header('Vary', 'User-Agent', { append: true }) + * + * return c.body('Thank you for coming') + * }) + * ``` + */ + header = (name, value, options) => { + if (this.finalized) this.#res = createResponseInstance(this.#res.body, this.#res); + const headers = this.#res ? this.#res.headers : this.#preparedHeaders ??= new Headers(); + if (value === void 0) headers.delete(name); + else if (options?.append) headers.append(name, value); + else headers.set(name, value); + }; + status = (status) => { + this.#status = status; + }; + /** + * `.set()` can set the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * c.set('message', 'Hono is hot!!') + * await next() + * }) + * ``` + */ + set = (key, value) => { + this.#var ??= /* @__PURE__ */ new Map(); + this.#var.set(key, value); + }; + /** + * `.get()` can use the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.get('/', (c) => { + * const message = c.get('message') + * return c.text(`The message is "${message}"`) + * }) + * ``` + */ + get = (key) => { + return this.#var ? this.#var.get(key) : void 0; + }; + /** + * `.var` can access the value of a variable. + * + * @see {@link https://hono.dev/docs/api/context#var} + * + * @example + * ```ts + * const result = c.var.client.oneMethod() + * ``` + */ + get var() { + if (!this.#var) return {}; + return Object.fromEntries(this.#var); + } + #newResponse(data, arg, headers) { + let responseHeaders = this.#res ? new Headers(this.#res.headers) : this.#preparedHeaders; + if (typeof arg === "object" && arg.headers) { + responseHeaders ??= new Headers(); + for (const [key, value] of new Headers(arg.headers)) if (key === "set-cookie") responseHeaders.append(key, value); + else responseHeaders.set(key, value); + } + if (headers) { + if (!responseHeaders) { + let count = 0; + for (const k in headers) if (++count > 1 || typeof headers[k] !== "string") { + responseHeaders = new Headers(); + break; + } + } + if (responseHeaders) for (const k in headers) { + const v = headers[k]; + if (typeof v === "string") responseHeaders.set(k, v); + else { + responseHeaders.delete(k); + for (const v2 of v) responseHeaders.append(k, v2); + } + } + } + const status = typeof arg === "number" ? arg : arg?.status ?? this.#status; + return createResponseInstance(data, { + status, + headers: responseHeaders ?? headers + }); + } + newResponse = (...args) => this.#newResponse(...args); + /** + * `.body()` can return the HTTP response. + * You can set headers with `.header()` and set HTTP status code with `.status`. + * This can also be set in `.text()`, `.json()` and so on. + * + * @see {@link https://hono.dev/docs/api/context#body} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * // Set HTTP status code + * c.status(201) + * + * // Return the response body + * return c.body('Thank you for coming') + * }) + * ``` + */ + body = (data, arg, headers) => this.#newResponse(data, arg, headers); + /** + * `.text()` can render text as `Content-Type:text/plain`. + * + * @see {@link https://hono.dev/docs/api/context#text} + * + * @example + * ```ts + * app.get('/say', (c) => { + * return c.text('Hello!') + * }) + * ``` + */ + text = (text, arg, headers) => { + return !this.#preparedHeaders && !this.#status && !arg && !headers && !this.finalized ? new Response(text) : this.#newResponse(text, arg, setDefaultContentType(TEXT_PLAIN, headers)); + }; + /** + * `.json()` can render JSON as `Content-Type:application/json`. + * + * @see {@link https://hono.dev/docs/api/context#json} + * + * @example + * ```ts + * app.get('/api', (c) => { + * return c.json({ message: 'Hello!' }) + * }) + * ``` + */ + json = (object, arg, headers) => { + return this.#newResponse(JSON.stringify(object), arg, setDefaultContentType("application/json", headers)); + }; + html = (html, arg, headers) => { + const res = (html) => this.#newResponse(html, arg, setDefaultContentType("text/html; charset=UTF-8", headers)); + return typeof html === "object" ? resolveCallback(html, HtmlEscapedCallbackPhase.Stringify, false, {}).then(res) : res(html); + }; + /** + * `.redirect()` can Redirect, default status code is 302. + * + * @see {@link https://hono.dev/docs/api/context#redirect} + * + * @example + * ```ts + * app.get('/redirect', (c) => { + * return c.redirect('/') + * }) + * app.get('/redirect-permanently', (c) => { + * return c.redirect('/', 301) + * }) + * ``` + */ + redirect = (location, status) => { + const locationString = String(location); + this.header("Location", !/[^\x00-\xFF]/.test(locationString) ? locationString : encodeURI(locationString)); + return this.newResponse(null, status ?? 302); + }; + /** + * `.notFound()` can return the Not Found Response. + * + * @see {@link https://hono.dev/docs/api/context#notfound} + * + * @example + * ```ts + * app.get('/notfound', (c) => { + * return c.notFound() + * }) + * ``` + */ + notFound = () => { + this.#notFoundHandler ??= () => createResponseInstance(); + return this.#notFoundHandler(this); + }; +}; +//#endregion +export { Context, TEXT_PLAIN }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js b/tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js new file mode 100644 index 000000000..48f9cbfb7 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/helper/cookie/index.js @@ -0,0 +1,82 @@ +import { parse, parseSigned, serialize, serializeSigned } from "../../utils/cookie.js"; +//#region src/helper/cookie/index.ts +const getCookie = (c, key, prefix) => { + const cookie = c.req.raw.headers.get("Cookie"); + if (typeof key === "string") { + if (!cookie) return; + let finalKey = key; + if (prefix === "secure") finalKey = "__Secure-" + key; + else if (prefix === "host") finalKey = "__Host-" + key; + return parse(cookie, finalKey)[finalKey]; + } + if (!cookie) return {}; + return parse(cookie); +}; +const getSignedCookie = async (c, secret, key, prefix) => { + const cookie = c.req.raw.headers.get("Cookie"); + if (typeof key === "string") { + if (!cookie) return; + let finalKey = key; + if (prefix === "secure") finalKey = "__Secure-" + key; + else if (prefix === "host") finalKey = "__Host-" + key; + return (await parseSigned(cookie, secret, finalKey))[finalKey]; + } + if (!cookie) return {}; + return await parseSigned(cookie, secret); +}; +const generateCookie = (name, value, opt) => { + let cookie; + if (opt?.prefix === "secure") cookie = serialize("__Secure-" + name, value, { + path: "/", + ...opt, + secure: true + }); + else if (opt?.prefix === "host") cookie = serialize("__Host-" + name, value, { + ...opt, + path: "/", + secure: true, + domain: void 0 + }); + else cookie = serialize(name, value, { + path: "/", + ...opt + }); + return cookie; +}; +const setCookie = (c, name, value, opt) => { + const cookie = generateCookie(name, value, opt); + c.header("Set-Cookie", cookie, { append: true }); +}; +const generateSignedCookie = async (name, value, secret, opt) => { + let cookie; + if (opt?.prefix === "secure") cookie = await serializeSigned("__Secure-" + name, value, secret, { + path: "/", + ...opt, + secure: true + }); + else if (opt?.prefix === "host") cookie = await serializeSigned("__Host-" + name, value, secret, { + ...opt, + path: "/", + secure: true, + domain: void 0 + }); + else cookie = await serializeSigned(name, value, secret, { + path: "/", + ...opt + }); + return cookie; +}; +const setSignedCookie = async (c, name, value, secret, opt) => { + const cookie = await generateSignedCookie(name, value, secret, opt); + c.header("set-cookie", cookie, { append: true }); +}; +const deleteCookie = (c, name, opt) => { + const deletedCookie = getCookie(c, name, opt?.prefix); + setCookie(c, name, "", { + ...opt, + maxAge: 0 + }); + return deletedCookie; +}; +//#endregion +export { deleteCookie, generateCookie, generateSignedCookie, getCookie, getSignedCookie, setCookie, setSignedCookie }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/hono-base.js b/tests/fixtures/npm/node_modules/hono/dist/hono-base.js new file mode 100644 index 000000000..7bc399a89 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/hono-base.js @@ -0,0 +1,350 @@ +import { compose } from "./compose.js"; +import { getPath, getPathNoStrict, mergePath } from "./utils/url.js"; +import { Context } from "./context.js"; +import { METHODS } from "./router.js"; +import { COMPOSED_HANDLER } from "./utils/constants.js"; +//#region src/hono-base.ts +/** +* @module +* This module is the base module for the Hono object. +*/ +const notFoundHandler = (c) => { + return c.text("404 Not Found", 404); +}; +const errorHandler = (err, c) => { + if ("getResponse" in err) { + const res = err.getResponse(); + return c.newResponse(res.body, res); + } + console.error(err); + return c.text("Internal Server Error", 500); +}; +var Hono = class Hono { + get; + post; + put; + delete; + options; + patch; + query; + all; + on; + use; + router; + getPath; + _basePath = "/"; + #path = "/"; + routes = []; + constructor(options = {}) { + [...METHODS, "all"].forEach((method) => { + this[method] = (args1, ...args) => { + const methodName = method.toUpperCase(); + if (typeof args1 === "string") this.#path = args1; + else this.#addRoute(methodName, this.#path, args1); + args.forEach((handler) => { + this.#addRoute(methodName, this.#path, handler); + }); + return this; + }; + }); + this.on = (method, path, ...handlers) => { + for (const p of [path].flat()) { + this.#path = p; + for (const m of [method].flat()) { + const methodName = m.toUpperCase(); + for (const handler of handlers) this.#addRoute(methodName, this.#path, handler); + } + } + return this; + }; + this.use = (arg1, ...handlers) => { + if (typeof arg1 === "string") this.#path = arg1; + else { + this.#path = "*"; + handlers.unshift(arg1); + } + handlers.forEach((handler) => { + this.#addRoute("ALL", this.#path, handler); + }); + return this; + }; + const { strict, ...optionsWithoutStrict } = options; + Object.assign(this, optionsWithoutStrict); + this.getPath = strict ?? true ? options.getPath ?? getPath : getPathNoStrict; + } + #clone() { + const clone = new Hono({ + router: this.router, + getPath: this.getPath + }); + clone.errorHandler = this.errorHandler; + clone.#notFoundHandler = this.#notFoundHandler; + clone.routes = this.routes; + return clone; + } + #notFoundHandler = notFoundHandler; + errorHandler = errorHandler; + /** + * `.route()` allows grouping other Hono instance in routes. + * + * @see {@link https://hono.dev/docs/api/routing#grouping} + * + * @param {string} path - base Path + * @param {Hono} app - other Hono instance + * @returns {Hono} routed Hono instance + * + * @example + * ```ts + * const app = new Hono() + * const app2 = new Hono() + * + * app2.get("/user", (c) => c.text("user")) + * app.route("/api", app2) // GET /api/user + * ``` + */ + route(path, app) { + const subApp = this.basePath(path); + app.routes.map((r) => { + let handler; + if (app.errorHandler === errorHandler) handler = r.handler; + else { + handler = async (c, next) => (await compose([], app.errorHandler)(c, () => r.handler(c, next))).res; + handler[COMPOSED_HANDLER] = r.handler; + } + subApp.#addRoute(r.method, r.path, handler, r.basePath); + }); + return this; + } + /** + * `.basePath()` allows base paths to be specified. + * + * @see {@link https://hono.dev/docs/api/routing#base-path} + * + * @param {string} path - base Path + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * const api = new Hono().basePath('/api') + * ``` + */ + basePath(path) { + const subApp = this.#clone(); + subApp._basePath = mergePath(this._basePath, path); + return subApp; + } + /** + * `.onError()` handles an error and returns a customized Response. + * + * @see {@link https://hono.dev/docs/api/hono#error-handling} + * + * @param {ErrorHandler} handler - request Handler for error + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.onError((err, c) => { + * console.error(`${err}`) + * return c.text('Custom Error Message', 500) + * }) + * ``` + */ + onError = (handler) => { + this.errorHandler = handler; + return this; + }; + /** + * `.notFound()` allows you to customize a Not Found Response. + * + * @see {@link https://hono.dev/docs/api/hono#not-found} + * + * @param {NotFoundHandler} handler - request handler for not-found + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.notFound((c) => { + * return c.text('Custom 404 Message', 404) + * }) + * ``` + */ + notFound = (handler) => { + this.#notFoundHandler = handler; + return this; + }; + /** + * `.mount()` allows you to mount applications built with other frameworks into your Hono application. + * + * @deprecated Use `mount()` from `hono/mount` instead. `.mount()` will be removed in v5. + * + * @see {@link https://hono.dev/docs/api/hono#mount} + * + * @param {string} path - base Path + * @param {Function} applicationHandler - other Request Handler + * @param {MountOptions} [options] - options of `.mount()` + * @returns {Hono} mounted Hono instance + * + * @example + * ```ts + * import { Router as IttyRouter } from 'itty-router' + * import { Hono } from 'hono' + * // Create itty-router application + * const ittyRouter = IttyRouter() + * // GET /itty-router/hello + * ittyRouter.get('/hello', () => new Response('Hello from itty-router')) + * + * const app = new Hono() + * app.mount('/itty-router', ittyRouter.handle) + * ``` + * + * @example + * ```ts + * const app = new Hono() + * // Send the request to another application without modification. + * app.mount('/app', anotherApp, { + * replaceRequest: (req) => req, + * }) + * ``` + */ + mount(path, applicationHandler, options) { + let replaceRequest; + let optionHandler; + if (options) { + if (typeof options === "function") optionHandler = options; + else { + optionHandler = options.optionHandler; + if (options.replaceRequest === false) replaceRequest = (request) => request; + else replaceRequest = options.replaceRequest; + } + } + const getOptions = optionHandler ? (c) => { + const options = optionHandler(c); + return Array.isArray(options) ? options : [options]; + } : (c) => { + let executionContext = void 0; + try { + executionContext = c.executionCtx; + } catch {} + return [c.env, executionContext]; + }; + replaceRequest ||= (() => { + const mergedPath = mergePath(this._basePath, path); + const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length; + return (request) => { + const url = new URL(request.url); + url.pathname = this.getPath(request).slice(pathPrefixLength) || "/"; + return new Request(url, request); + }; + })(); + const handler = async (c, next) => { + const res = await applicationHandler(replaceRequest(c.req.raw), ...getOptions(c)); + if (res) return res; + await next(); + }; + this.#addRoute("ALL", mergePath(path, "*"), handler); + return this; + } + #addRoute(method, path, handler, baseRoutePath) { + path = mergePath(this._basePath, path); + const r = { + basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath, + path, + method, + handler + }; + this.router.add(method, path, [handler, r]); + this.routes.push(r); + } + #handleError(err, c) { + if (err instanceof Error) return this.errorHandler(err, c); + throw err; + } + #dispatch(request, executionCtx, env, method) { + if (method === "HEAD") return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))(); + const path = this.getPath(request, { env }); + const matchResult = this.router.match(method, path); + const c = new Context(request, { + path, + matchResult, + env, + executionCtx, + notFoundHandler: this.#notFoundHandler + }); + if (matchResult[0].length === 1) { + let res; + try { + res = matchResult[0][0][0][0](c, async () => { + c.res = await this.#notFoundHandler(c); + }); + } catch (err) { + return this.#handleError(err, c); + } + return res instanceof Promise ? res.then((resolved) => resolved || (c.finalized ? c.res : this.#notFoundHandler(c))).catch((err) => this.#handleError(err, c)) : res ?? this.#notFoundHandler(c); + } + const composed = compose(matchResult[0], this.errorHandler, this.#notFoundHandler); + return (async () => { + try { + const context = await composed(c); + if (!context.finalized) throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?"); + return context.res; + } catch (err) { + return this.#handleError(err, c); + } + })(); + } + /** + * `.fetch()` will be entry point of your app. + * + * @see {@link https://hono.dev/docs/api/hono#fetch} + * + * @param {Request} request - request Object of request + * @param {Env} env - env Object + * @param {ExecutionContext} executionCtx - context of execution + * @returns {Response | Promise} response of request + * + */ + fetch = (request, ...rest) => { + return this.#dispatch(request, rest[1], rest[0], request.method); + }; + /** + * `.request()` is a useful method for testing. + * You can pass a URL or pathname to send a GET request. + * app will return a Response object. + * ```ts + * test('GET /hello is ok', async () => { + * const res = await app.request('/hello') + * expect(res.status).toBe(200) + * }) + * ``` + * @see https://hono.dev/docs/api/hono#request + */ + request = (input, requestInit, Env, executionCtx) => { + if (input instanceof Request) return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx); + input = input.toString(); + return this.fetch(new Request(/^https?:\/\//.test(input) ? input : `http://localhost${mergePath("/", input)}`, requestInit), Env, executionCtx); + }; + /** + * `.fire()` automatically adds a global fetch event listener. + * This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers. + * @deprecated + * Use `fire` from `hono/service-worker` instead. + * ```ts + * import { Hono } from 'hono' + * import { fire } from 'hono/service-worker' + * + * const app = new Hono() + * // ... + * fire(app) + * ``` + * @see https://hono.dev/docs/api/hono#fire + * @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API + * @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ + */ + fire = () => { + addEventListener("fetch", (event) => { + event.respondWith(this.#dispatch(event.request, event, void 0, event.request.method)); + }); + }; +}; +//#endregion +export { Hono as HonoBase }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/hono.js b/tests/fixtures/npm/node_modules/hono/dist/hono.js new file mode 100644 index 000000000..9602aafc3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/hono.js @@ -0,0 +1,28 @@ +import { HonoBase as Hono$1 } from "./hono-base.js"; +import { RegExpRouter } from "./router/reg-exp-router/router.js"; +import "./router/reg-exp-router/index.js"; +import { SmartRouter } from "./router/smart-router/router.js"; +import { TrieRouter } from "./router/trie-router/router.js"; +import "./router/trie-router/index.js"; +//#region src/hono.ts +/** +* The Hono class extends the functionality of the HonoBase class. +* It sets up routing and allows for custom options to be passed. +* +* @template E - The environment type. +* @template S - The schema type. +* @template BasePath - The base path type. +*/ +var Hono = class extends Hono$1 { + /** + * Creates an instance of the Hono class. + * + * @param options - Optional configuration options for the Hono instance. + */ + constructor(options = {}) { + super(options); + this.router = options.router ?? new SmartRouter({ routers: [new RegExpRouter(), new TrieRouter()] }); + } +}; +//#endregion +export { Hono }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/http-exception.js b/tests/fixtures/npm/node_modules/hono/dist/http-exception.js new file mode 100644 index 000000000..800b530cd --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/http-exception.js @@ -0,0 +1,55 @@ +//#region src/http-exception.ts +/** +* `HTTPException` must be used when a fatal error such as authentication failure occurs. +* +* @see {@link https://hono.dev/docs/api/exception} +* +* @param {StatusCode} status - status code of HTTPException +* @param {HTTPExceptionOptions} options - options of HTTPException +* @param {HTTPExceptionOptions["res"]} options.res - response of options of HTTPException +* @param {HTTPExceptionOptions["message"]} options.message - message of options of HTTPException +* @param {HTTPExceptionOptions["cause"]} options.cause - cause of options of HTTPException +* +* @example +* ```ts +* import { HTTPException } from 'hono/http-exception' +* +* // ... +* +* app.post('/auth', async (c, next) => { +* // authentication +* if (authorized === false) { +* throw new HTTPException(401, { message: 'Custom error message' }) +* } +* await next() +* }) +* ``` +*/ +var HTTPException = class extends Error { + res; + status; + /** + * Creates an instance of `HTTPException`. + * @param status - HTTP status code for the exception. Defaults to 500. + * @param options - Additional options for the exception. + */ + constructor(status = 500, options) { + super(options?.message, { cause: options?.cause }); + this.res = options?.res; + this.status = status; + } + /** + * Returns the response object associated with the exception. + * If a response object is not provided, a new response is created with the error message and status code. + * @returns The response object. + */ + getResponse() { + if (this.res) return new Response(this.res.body, { + status: this.status, + headers: this.res.headers + }); + return new Response(this.message, { status: this.status }); + } +}; +//#endregion +export { HTTPException }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/index.js b/tests/fixtures/npm/node_modules/hono/dist/index.js new file mode 100644 index 000000000..20c96572c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/index.js @@ -0,0 +1,20 @@ +import { Context } from "./context.js"; +import { Hono } from "./hono.js"; +//#region src/index.ts +/** +* @module +* +* Hono - Web Framework built on Web Standards +* +* @example +* ```ts +* import { Hono } from 'hono' +* const app = new Hono() +* +* app.get('/', (c) => c.text('Hono!')) +* +* export default app +* ``` +*/ +//#endregion +export { Context, Hono }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/request.js b/tests/fixtures/npm/node_modules/hono/dist/request.js new file mode 100644 index 000000000..bf130a518 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/request.js @@ -0,0 +1,335 @@ +import { HTTPException } from "./http-exception.js"; +import { GET_MATCH_RESULT } from "./request/constants.js"; +import { parseBody } from "./utils/body.js"; +import { getQueryParam, getQueryParams, tryDecodeURIComponent } from "./utils/url.js"; +//#region src/request.ts +var HonoRequest = class { + /** + * `.raw` can get the raw Request object. + * + * @see {@link https://hono.dev/docs/api/request#raw} + * + * @example + * ```ts + * // For Cloudflare Workers + * app.post('/', async (c) => { + * const metadata = c.req.raw.cf?.hostMetadata? + * ... + * }) + * ``` + */ + raw; + #validatedData; + #matchResult; + routeIndex = 0; + /** + * `.path` can get the pathname of the request. + * + * @see {@link https://hono.dev/docs/api/request#path} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const pathname = c.req.path // `/about/me` + * }) + * ``` + */ + path; + bodyCache = {}; + constructor(request, path = "/", matchResult = [[]]) { + this.raw = request; + this.path = path; + this.#matchResult = matchResult; + } + param(key) { + return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams(); + } + #getDecodedParam(key) { + const paramKey = this.#matchResult[0][this.routeIndex]?.[1][key]; + const param = this.#getParamValue(paramKey); + return param && tryDecodeURIComponent(param); + } + #getAllDecodedParams() { + const decoded = {}; + const keys = Object.keys(this.#matchResult[0][this.routeIndex]?.[1] ?? {}); + for (const key of keys) { + const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]); + if (value !== void 0) decoded[key] = tryDecodeURIComponent(value); + } + return decoded; + } + #getParamValue(paramKey) { + return this.#matchResult[1] ? this.#matchResult[1][paramKey] : paramKey; + } + query(key) { + return getQueryParam(this.url, key); + } + queries(key) { + return getQueryParams(this.url, key); + } + header(name) { + if (name) return this.raw.headers.get(name) ?? void 0; + const headerData = Object.create(null); + this.raw.headers.forEach((value, key) => { + headerData[key] = value; + }); + return headerData; + } + async parseBody(options) { + return parseBody(this, options); + } + #cachedBody = (key) => { + const { bodyCache, raw } = this; + const cachedBody = bodyCache[key]; + if (cachedBody) return cachedBody; + for (const anyCachedKey in bodyCache) return bodyCache[anyCachedKey].then((body) => { + if (anyCachedKey === "json") body = JSON.stringify(body); + const contentType = anyCachedKey === "formData" ? void 0 : raw.headers.get("content-type"); + return new Response(body, { headers: contentType ? { "Content-Type": contentType } : void 0 })[key](); + }); + return bodyCache[key] = raw[key](); + }; + /** + * `.json()` can parse Request body of type `application/json` + * + * @see {@link https://hono.dev/docs/api/request#json} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.json() + * }) + * ``` + */ + json() { + return this.#cachedBody("text").then((text) => JSON.parse(text)); + } + /** + * `.text()` can parse Request body of type `text/plain` + * + * @see {@link https://hono.dev/docs/api/request#text} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.text() + * }) + * ``` + */ + text() { + return this.#cachedBody("text"); + } + /** + * `.arrayBuffer()` parse Request body as an `ArrayBuffer` + * + * @see {@link https://hono.dev/docs/api/request#arraybuffer} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.arrayBuffer() + * }) + * ``` + */ + arrayBuffer() { + return this.#cachedBody("arrayBuffer"); + } + /** + * `.bytes()` parses the request body as a `Uint8Array`. + * + * @see {@link https://hono.dev/docs/api/request#bytes} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.bytes() + * }) + * ``` + */ + bytes() { + return this.#cachedBody("arrayBuffer").then((buffer) => new Uint8Array(buffer)); + } + /** + * Parses the request body as a `Blob`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.blob(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#blob + */ + blob() { + return this.#cachedBody("blob"); + } + /** + * Parses the request body as `FormData`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.formData(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#formdata + */ + formData() { + return this.#cachedBody("formData"); + } + /** + * Adds validated data to the request. + * + * @param target - The target of the validation. + * @param data - The validated data to add. + */ + addValidatedData(target, data) { + (this.#validatedData ??= {})[target] = data; + } + valid(target) { + return this.#validatedData?.[target]; + } + /** + * `.url` can get the request url strings. + * + * @see {@link https://hono.dev/docs/api/request#url} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const url = c.req.url // `http://localhost:8787/about/me` + * ... + * }) + * ``` + */ + get url() { + return this.raw.url; + } + /** + * `.method` can get the method name of the request. + * + * @see {@link https://hono.dev/docs/api/request#method} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const method = c.req.method // `GET` + * }) + * ``` + */ + get method() { + return this.raw.method; + } + get [GET_MATCH_RESULT]() { + return this.#matchResult; + } + /** + * `.matchedRoutes` can return a matched route in the handler + * + * @deprecated + * + * Use matchedRoutes helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#matchedroutes} + * + * @example + * ```ts + * app.use('*', async function logger(c, next) { + * await next() + * c.req.matchedRoutes.forEach(({ handler, method, path }, i) => { + * const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]') + * console.log( + * method, + * ' ', + * path, + * ' '.repeat(Math.max(10 - path.length, 0)), + * name, + * i === c.req.routeIndex ? '<- respond from here' : '' + * ) + * }) + * }) + * ``` + */ + get matchedRoutes() { + return this.#matchResult[0].map(([[, route]]) => route); + } + /** + * `.routePath` can retrieve the path registered within the handler + * + * @deprecated + * + * Use routePath helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#routepath} + * + * @example + * ```ts + * app.get('/posts/:id', (c) => { + * return c.json({ path: c.req.routePath }) + * }) + * ``` + */ + get routePath() { + return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path; + } +}; +/** +* Clones a HonoRequest's underlying raw Request object. +* +* This utility handles both consumed and unconsumed request bodies: +* - If the request body hasn't been consumed, it uses the native `clone()` method +* - If the request body has been consumed, it reconstructs a new Request using cached body data +* +* This is particularly useful when you need to: +* - Process the same request body multiple times +* - Pass requests to external services after validation +* +* @param req - The HonoRequest object to clone +* @returns A Promise that resolves to a new Request object with the same properties +* @throws {HTTPException} If the request body was consumed directly via `req.raw` +* without using HonoRequest methods (e.g., `req.json()`, `req.text()`), making it +* impossible to reconstruct the body from cache +* +* @example +* ```ts +* // Clone after consuming the body (e.g., after validation) +* app.post('/forward', +* validator('json', (data) => data), +* async (c) => { +* const validated = c.req.valid('json') +* // Body has been consumed, but cloneRawRequest still works +* const clonedReq = await cloneRawRequest(c.req) +* return fetch('http://backend-service.com', clonedReq) +* } +* ) +* ``` +*/ +const cloneRawRequest = async (req) => { + if (!req.raw.bodyUsed) return req.raw.clone(); + const cacheKey = Object.keys(req.bodyCache)[0]; + if (!cacheKey) throw new HTTPException(500, { message: "Cannot clone request: body was already consumed and not cached. Please use HonoRequest methods (e.g., req.json(), req.text()) instead of consuming req.raw directly." }); + let body = await req[cacheKey](); + const headers = req.header(); + if (cacheKey === "json") { + body = JSON.stringify(body); + delete headers["content-length"]; + } else if (body instanceof FormData) { + delete headers["content-type"]; + delete headers["content-length"]; + } + const requestInit = { + body, + cache: req.raw.cache, + credentials: req.raw.credentials, + headers, + integrity: req.raw.integrity, + keepalive: req.raw.keepalive, + method: req.method, + mode: req.raw.mode, + redirect: req.raw.redirect, + referrer: req.raw.referrer, + referrerPolicy: req.raw.referrerPolicy, + signal: req.raw.signal + }; + return new Request(req.url, requestInit); +}; +//#endregion +export { HonoRequest, cloneRawRequest }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/request/constants.js b/tests/fixtures/npm/node_modules/hono/dist/request/constants.js new file mode 100644 index 000000000..0f2734437 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/request/constants.js @@ -0,0 +1,4 @@ +//#region src/request/constants.ts +const GET_MATCH_RESULT = Symbol(); +//#endregion +export { GET_MATCH_RESULT }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router.js b/tests/fixtures/npm/node_modules/hono/dist/router.js new file mode 100644 index 000000000..52ea7f5c2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router.js @@ -0,0 +1,35 @@ +//#region src/router.ts +/** +* @module +* This module provides types definitions and variables for the routers. +*/ +/** +* Constant representing all HTTP methods in uppercase. +*/ +const METHOD_NAME_ALL = "ALL"; +/** +* Constant representing all HTTP methods in lowercase. +*/ +const METHOD_NAME_ALL_LOWERCASE = "all"; +/** +* Array of supported HTTP methods. +*/ +const METHODS = [ + "get", + "post", + "put", + "delete", + "options", + "patch", + "query" +]; +/** +* Error message indicating that a route cannot be added because the matcher is already built. +*/ +const MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is already built."; +/** +* Error class representing an unsupported path error. +*/ +var UnsupportedPathError = class extends Error {}; +//#endregion +export { MESSAGE_MATCHER_IS_ALREADY_BUILT, METHODS, METHOD_NAME_ALL, METHOD_NAME_ALL_LOWERCASE, UnsupportedPathError }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js new file mode 100644 index 000000000..38dfdd48a --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/index.js @@ -0,0 +1,3 @@ +import { RegExpRouter } from "./router.js"; +import { PreparedRegExpRouter, buildInitParams, serializeInitParams } from "./prepared-router.js"; +export { PreparedRegExpRouter, RegExpRouter, buildInitParams, serializeInitParams }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js new file mode 100644 index 000000000..97ac8435c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/matcher.js @@ -0,0 +1,19 @@ +import "../../router.js"; +//#region src/router/reg-exp-router/matcher.ts +const emptyParam = []; +function match(method, path) { + const matchers = this.buildAllMatchers(); + const match = ((method, path) => { + const matcher = matchers[method] || matchers["ALL"]; + const staticMatch = matcher[2][path]; + if (staticMatch) return staticMatch; + const match = path.match(matcher[0]); + if (!match) return [[], emptyParam]; + const index = match.indexOf("", 1); + return [matcher[1][index], match]; + }); + this.match = match; + return match(method, path); +} +//#endregion +export { emptyParam, match }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js new file mode 100644 index 000000000..6738b9e98 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/node.js @@ -0,0 +1,91 @@ +import { createNullObject } from "../utils.js"; +//#region src/router/reg-exp-router/node.ts +const LABEL_REG_EXP_STR = "[^/]+"; +const ONLY_WILDCARD_REG_EXP_STR = ".*"; +const TAIL_WILDCARD_REG_EXP_STR = "(?:|/.*)"; +const PATH_ERROR = Symbol(); +const regExpMetaChars = /* @__PURE__ */ new Set(".\\+*[^]$()"); +/** +* Sort order: +* 1. literal +* 2. special pattern (e.g. :label{[0-9]+}) +* 3. common label pattern (e.g. :label) +* 4. wildcard +*/ +function compareKey(a, b) { + if (a.length === 1) return b.length === 1 ? a < b ? -1 : 1 : -1; + if (b.length === 1) return 1; + if (a === ".*" || a === "(?:|/.*)") return b === "(?:|/.*)" ? -1 : 1; + else if (b === ".*" || b === "(?:|/.*)") return -1; + if (a === "[^/]+") return 1; + else if (b === "[^/]+") return -1; + return a.length === b.length ? a < b ? -1 : 1 : b.length - a.length; +} +var Node = class Node { + #index; + #varIndex; + #children = createNullObject(); + insert(tokens, index, paramMap, context, isStatic) { + let node = this; + for (let i = 0, len = tokens.length; i < len; i++) { + const token = tokens[i]; + const pattern = token.length === 1 ? token === "*" ? i === len - 1 ? [ + "", + "", + ".*" + ] : [ + "", + "", + LABEL_REG_EXP_STR + ] : null : token === "/*" ? [ + "", + "", + TAIL_WILDCARD_REG_EXP_STR + ] : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/); + let nextNode; + if (pattern) { + const name = pattern[1]; + let regexpStr = pattern[2] || "[^/]+"; + if (name && pattern[2]) { + if (regexpStr === ".*") throw PATH_ERROR; + regexpStr = regexpStr.replace(/^\((?!\?:)(?=[^)]+\)$)/, "(?:"); + if (/\((?!\?:)/.test(regexpStr)) throw PATH_ERROR; + if (regexpStr.length === 1 && regExpMetaChars.has(regexpStr)) throw PATH_ERROR; + } + nextNode = node.#children[regexpStr]; + if (!nextNode) { + if (regexpStr !== ".*" && regexpStr !== "(?:|/.*)") { + for (const k in node.#children) if ((regexpStr.length > 1 || k.length > 1) && k !== ".*" && k !== "(?:|/.*)") throw PATH_ERROR; + } + nextNode = node.#children[regexpStr] = new Node(); + } + if (name !== "") { + nextNode.#varIndex ??= context.varIndex++; + paramMap.push([name, nextNode.#varIndex]); + } + } else { + nextNode = node.#children[token]; + if (!nextNode) { + for (const k in node.#children) if (k.length > 1 && k !== ".*" && k !== "(?:|/.*)") throw PATH_ERROR; + nextNode = node.#children[token] = new Node(); + } + } + node = nextNode; + } + if (node.#index !== void 0) throw PATH_ERROR; + node.#index = isStatic ? -1 : index; + } + buildRegExpStr() { + const strList = Object.keys(this.#children).sort(compareKey).map((k) => { + const c = this.#children[k]; + const childStr = c.buildRegExpStr(); + return childStr === "" ? "" : (typeof c.#varIndex === "number" ? `(${k})@${c.#varIndex}` : regExpMetaChars.has(k) ? `\\${k}` : k) + childStr; + }).filter(Boolean); + if (typeof this.#index === "number" && this.#index !== -1) strList.unshift(`#${this.#index}`); + if (strList.length === 0) return ""; + if (strList.length === 1) return strList[0]; + return "(?:" + strList.join("|") + ")"; + } +}; +//#endregion +export { LABEL_REG_EXP_STR, Node, ONLY_WILDCARD_REG_EXP_STR, PATH_ERROR, TAIL_WILDCARD_REG_EXP_STR }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js new file mode 100644 index 000000000..c9a8c1314 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/prepared-router.js @@ -0,0 +1,94 @@ +import "../../router.js"; +import { emptyParam, match } from "./matcher.js"; +import { RegExpRouter } from "./router.js"; +//#region src/router/reg-exp-router/prepared-router.ts +var PreparedRegExpRouter = class { + name = "PreparedRegExpRouter"; + #matchers; + #relocateMap; + constructor(matchers, relocateMap) { + this.#matchers = matchers; + this.#relocateMap = relocateMap; + } + #addWildcard(method, handlerData) { + const matcher = this.#matchers[method]; + matcher[1].forEach((list) => list && list.push(handlerData)); + Object.values(matcher[2]).forEach((list) => list[0].push(handlerData)); + } + #addPath(method, path, handler, indexes, map) { + const matcher = this.#matchers[method]; + if (!map) matcher[2][path][0].push([handler, {}]); + else indexes.forEach((index) => { + if (typeof index === "number") matcher[1][index].push([handler, map]); + else matcher[2][index || path][0].push([handler, map]); + }); + } + add(method, path, handler) { + if (!this.#matchers[method]) { + const all = this.#matchers["ALL"]; + const staticMap = {}; + for (const key in all[2]) staticMap[key] = [all[2][key][0].slice(), emptyParam]; + this.#matchers[method] = [ + all[0], + all[1].map((list) => Array.isArray(list) ? list.slice() : 0), + staticMap + ]; + } + if (path === "/*" || path === "*") { + const handlerData = [handler, {}]; + if (method === "ALL") for (const m in this.#matchers) this.#addWildcard(m, handlerData); + else this.#addWildcard(method, handlerData); + return; + } + const data = this.#relocateMap[path]; + if (!data) throw new Error(`Path ${path} is not registered`); + for (const [indexes, map] of data) if (method === "ALL") for (const m in this.#matchers) this.#addPath(m, path, handler, indexes, map); + else this.#addPath(method, path, handler, indexes, map); + } + buildAllMatchers() { + return this.#matchers; + } + match = match; +}; +const buildInitParams = ({ paths }) => { + const RegExpRouterWithMatcherExport = class extends RegExpRouter { + buildAndExportAllMatchers() { + return this.buildAllMatchers(); + } + }; + const router = new RegExpRouterWithMatcherExport(); + for (const path of paths) router.add("ALL", path, path); + const matchers = router.buildAndExportAllMatchers(); + const all = matchers["ALL"]; + const relocateMap = {}; + for (const path of paths) { + if (path === "/*" || path === "*") continue; + all[1].forEach((list, i) => { + list.forEach(([p, map]) => { + if (p === path) { + if (relocateMap[path]) relocateMap[path][0][1] = { + ...relocateMap[path][0][1], + ...map + }; + else relocateMap[path] = [[[], map]]; + if (relocateMap[path][0][0].findIndex((j) => j === i) === -1) relocateMap[path][0][0].push(i); + } + }); + }); + for (const path2 in all[2]) all[2][path2][0].forEach(([p]) => { + if (p === path) { + relocateMap[path] ||= [[[]]]; + const value = path2 === path ? "" : path2; + if (relocateMap[path][0][0].findIndex((v) => v === value) === -1) relocateMap[path][0][0].push(value); + } + }); + } + for (let i = 0, len = all[1].length; i < len; i++) all[1][i] = all[1][i] ? [] : 0; + for (const path in all[2]) all[2][path][0] = []; + return [matchers, relocateMap]; +}; +const serializeInitParams = ([matchers, relocateMap]) => { + return `[${JSON.stringify(matchers, (_, value) => value instanceof RegExp ? `##${value.toString()}##` : value).replace(/"##(.+?)##"/g, (_, str) => str.replace(/\\\\/g, "\\"))},${JSON.stringify(relocateMap)}]`; +}; +//#endregion +export { PreparedRegExpRouter, buildInitParams, serializeInitParams }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js new file mode 100644 index 000000000..21ef235d1 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/router.js @@ -0,0 +1,101 @@ +import { checkOptionalParameter } from "../../utils/url.js"; +import { MESSAGE_MATCHER_IS_ALREADY_BUILT, UnsupportedPathError } from "../../router.js"; +import { createNullObject } from "../utils.js"; +import { emptyParam, match } from "./matcher.js"; +import { LABEL_REG_EXP_STR, PATH_ERROR, TAIL_WILDCARD_REG_EXP_STR } from "./node.js"; +import { Trie } from "./trie.js"; +//#region src/router/reg-exp-router/router.ts +let wildcardRegExpCache = createNullObject(); +function buildWildcardRegExp(path) { + return wildcardRegExpCache[path] ??= new RegExp(`^${path.replace(/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g, (match, metaChar) => metaChar ? `\\${metaChar}` : match === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match === "*" ? ".*" : `/:${LABEL_REG_EXP_STR}`)}$`); +} +function findMiddleware(middleware, path) { + for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) if (buildWildcardRegExp(k).test(path)) return [...middleware[k]]; +} +var RegExpRouter = class { + name = "RegExpRouter"; + #middleware; + #routes; + #tries; + constructor() { + this.#middleware = { ["ALL"]: createNullObject() }; + this.#routes = { ["ALL"]: createNullObject() }; + this.#tries = { ["ALL"]: new Trie() }; + } + #insertPath(method, path) { + try { + this.#tries[method].insert(path, !/\*|\/:/.test(path)); + } catch (e) { + throw e === PATH_ERROR ? new UnsupportedPathError(path) : e; + } + } + add(method, path, handler) { + const middleware = this.#middleware; + const routes = this.#routes; + if (!middleware) throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT); + if (!middleware[method]) { + this.#tries[method] = new Trie(); + for (const handlerMap of [middleware, routes]) { + handlerMap[method] = createNullObject(); + for (const p in handlerMap["ALL"]) { + handlerMap[method][p] = [...handlerMap["ALL"][p]]; + this.#insertPath(method, p); + } + } + } + if (path === "/*") path = "*"; + const methods = method === "ALL" ? Object.keys(middleware) : [method]; + if (/\*$/.test(path)) { + const re = buildWildcardRegExp(path); + for (const m of methods) if (!middleware[m][path]) { + this.#insertPath(m, path); + middleware[m][path] = findMiddleware(middleware[m], path) || findMiddleware(middleware["ALL"], path) || []; + } + for (const handlerMap of [middleware, routes]) for (const m of methods) for (const p in handlerMap[m]) re.test(p) && handlerMap[m][p].push([handler, path]); + return; + } + const paths = checkOptionalParameter(path) || [path]; + for (const path of paths) for (const m of methods) { + if (!routes[m][path]) { + this.#insertPath(m, path); + routes[m][path] = findMiddleware(middleware[m], path) || findMiddleware(middleware["ALL"], path) || []; + } + routes[m][path].push([handler, path]); + } + } + match = match; + buildAllMatchers() { + const matchers = createNullObject(); + for (const method of Object.keys(this.#routes)) matchers[method] = this.#buildMatcher(method); + this.#middleware = this.#routes = this.#tries = void 0; + wildcardRegExpCache = createNullObject(); + return matchers; + } + #buildMatcher(method) { + const middleware = this.#middleware[method]; + const routes = this.#routes[method]; + const trie = this.#tries[method]; + const staticMap = createNullObject(); + const handlerData = []; + const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp(); + for (const r of [middleware, routes]) for (const path in r) { + const handlers = r[path]; + const pathData = trie.paths[path]; + if (!pathData) { + staticMap[path] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam]; + continue; + } + handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [h, trie.paths[handlerPath][1].reduceRight((map, [key], i) => { + map[key] = paramReplacementMap[pathData[1][i][1]]; + return map; + }, createNullObject())]); + } + return [ + regexp, + indexReplacementMap.map((i) => handlerData[i]), + staticMap + ]; + } +}; +//#endregion +export { RegExpRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js new file mode 100644 index 000000000..a995678c0 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/reg-exp-router/trie.js @@ -0,0 +1,73 @@ +import { createNullObject } from "../utils.js"; +import { Node } from "./node.js"; +//#region src/router/reg-exp-router/trie.ts +var Trie = class { + #context = { varIndex: 0 }; + #root = new Node(); + #index = 0; + paths = createNullObject(); + insert(path, isStatic) { + if (isStatic) { + this.#root.insert(path.split(""), 0, [], this.#context, true); + return; + } + const paramAssoc = []; + const groups = []; + let markedPath = path; + for (let i = 0;;) { + let replaced = false; + markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => { + const mark = `@\\${i}`; + groups[i] = [mark, m]; + i++; + replaced = true; + return mark; + }); + if (!replaced) break; + } + /** + * - pattern (:label, :label{0-9]+}, ...) + * - /* wildcard + * - character + */ + const tokens = markedPath.match(/(?::[^\/]+)|(?:\/\*$)|./g) || []; + for (let i = groups.length - 1; i >= 0; i--) { + const [mark] = groups[i]; + for (let j = tokens.length - 1; j >= 0; j--) if (tokens[j].indexOf(mark) !== -1) { + tokens[j] = tokens[j].replace(mark, groups[i][1]); + break; + } + } + this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false); + this.paths[path] = [this.#index++, paramAssoc]; + } + buildRegExp() { + let regexp = this.#root.buildRegExpStr(); + if (regexp === "") return [ + /^$/, + [], + [] + ]; + let captureIndex = 0; + const indexReplacementMap = []; + const paramReplacementMap = []; + regexp = regexp.replace(/#(\d+)|@(\d+)|\.\*\$/g, (_, handlerIndex, paramIndex) => { + if (handlerIndex !== void 0) { + indexReplacementMap[++captureIndex] = Number(handlerIndex); + return "$()"; + } + if (paramIndex !== void 0) { + paramReplacementMap[Number(paramIndex)] = ++captureIndex; + return ""; + } + return ""; + }); + return [ + new RegExp(`^${regexp}`), + indexReplacementMap, + paramReplacementMap + ]; + } +}; +//#endregion +export { Trie }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js b/tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js new file mode 100644 index 000000000..a115ab938 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/smart-router/router.js @@ -0,0 +1,49 @@ +import { MESSAGE_MATCHER_IS_ALREADY_BUILT, UnsupportedPathError } from "../../router.js"; +//#region src/router/smart-router/router.ts +var SmartRouter = class { + name = "SmartRouter"; + #routers = []; + #routes = []; + constructor(init) { + this.#routers = init.routers; + } + add(method, path, handler) { + if (!this.#routes) throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT); + this.#routes.push([ + method, + path, + handler + ]); + } + match(method, path) { + if (!this.#routes) throw new Error("Fatal error"); + const routers = this.#routers; + const routes = this.#routes; + const len = routers.length; + let i = 0; + let res; + for (; i < len; i++) { + const router = routers[i]; + try { + for (let i = 0, len = routes.length; i < len; i++) router.add(...routes[i]); + res = router.match(method, path); + } catch (e) { + if (e instanceof UnsupportedPathError) continue; + throw e; + } + this.match = router.match.bind(router); + this.#routers = [router]; + this.#routes = void 0; + break; + } + if (i === len) throw new Error("Fatal error"); + this.name = `SmartRouter + ${this.activeRouter.name}`; + return res; + } + get activeRouter() { + if (this.#routes || this.#routers.length !== 1) throw new Error("No active router has been determined yet."); + return this.#routers[0]; + } +}; +//#endregion +export { SmartRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js new file mode 100644 index 000000000..8ea5b3150 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/index.js @@ -0,0 +1,2 @@ +import { TrieRouter } from "./router.js"; +export { TrieRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js new file mode 100644 index 000000000..c33c7af1e --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/node.js @@ -0,0 +1,134 @@ +import { getPattern, splitPath, splitRoutingPath } from "../../utils/url.js"; +import "../../router.js"; +import { createNullObject } from "../utils.js"; +//#region src/router/trie-router/node.ts +const emptyParams = createNullObject(); +let order = 0; +var Node = class Node { + #methods = []; + #children = createNullObject(); + #patterns = []; + #pattern; + #params = emptyParams; + insert(method, path, handler) { + let curNode = this; + const parts = splitRoutingPath(path); + const possibleKeys = /* @__PURE__ */ new Set(); + let i = 0; + for (const p of parts) { + const nextP = parts[++i]; + const pattern = getPattern(p, nextP) || (nextP === void 0 && p && p.indexOf("*") === p.length - 1 ? p : null); + const isParam = Array.isArray(pattern); + const key = isParam ? pattern[0] : pattern || p; + const child = curNode.#children[key] ||= new Node(); + if (pattern && !child.#pattern) { + child.#pattern = pattern; + curNode.#patterns.push(child); + } + curNode = child; + if (isParam) possibleKeys.add(pattern[1]); + } + curNode.#methods.push({ [method]: { + handler, + possibleKeys: [...possibleKeys], + score: ++order + } }); + } + #pushHandlerSets(handlerSets, node, method, nodeParams, params) { + for (let i = 0, len = node.#methods.length; i < len; i++) { + const m = node.#methods[i]; + const handlerSet = m[method] || m["ALL"]; + if (handlerSet) { + handlerSet.params = createNullObject(); + handlerSets.push(handlerSet); + for (let i = 0, len = handlerSet.possibleKeys.length; i < len; i++) { + const key = handlerSet.possibleKeys[i]; + handlerSet.params[key] = params?.[key] && !i ? params[key] : nodeParams[key] ?? params?.[key]; + } + } + } + } + search(method, path) { + const handlerSets = []; + this.#params = emptyParams; + let curNodes = [this]; + const parts = splitPath(path); + const curNodesQueue = []; + const len = parts.length; + let partOffsets = null; + for (let i = 0; i < len; i++) { + const part = parts[i]; + const isLast = i === len - 1; + const tempNodes = []; + for (let j = 0, len2 = curNodes.length; j < len2; j++) { + const node = curNodes[j]; + const nextNode = node.#children[part]; + if (nextNode) { + nextNode.#params = node.#params; + if (isLast) { + if (nextNode.#children["*"]) this.#pushHandlerSets(handlerSets, nextNode.#children["*"], method, node.#params); + this.#pushHandlerSets(handlerSets, nextNode, method, node.#params); + } else tempNodes.push(nextNode); + } + for (const child of node.#patterns) { + const pattern = child.#pattern; + const params = node.#params === emptyParams ? {} : { ...node.#params }; + if (typeof pattern === "string") { + if (pattern === "*" || part.startsWith(pattern.slice(0, -1))) { + this.#pushHandlerSets(handlerSets, child, method, node.#params); + if (pattern === "*") { + child.#params = params; + tempNodes.push(child); + } + } + continue; + } + const [, name, matcher] = pattern; + if (!part && matcher === true) continue; + if (matcher !== true) { + if (!partOffsets) { + partOffsets = []; + let offset = path[0] === "/" ? 1 : 0; + for (let p = 0; p < len; p++) { + partOffsets[p] = offset; + offset += parts[p].length + 1; + } + } + const restPathString = path.slice(partOffsets[i]); + const m = matcher.exec(restPathString); + if (m) { + params[name] = m[0]; + this.#pushHandlerSets(handlerSets, child, method, node.#params, params); + if (m[0].length === restPathString.length && child.#children["*"]) this.#pushHandlerSets(handlerSets, child.#children["*"], method, node.#params, params); + for (const _ in child.#children) { + child.#params = params; + const componentCount = m[0].match(/\//g)?.length ?? 0; + (curNodesQueue[componentCount] ||= []).push(child); + break; + } + continue; + } + } + if (matcher === true || matcher.test(part)) { + params[name] = part; + if (isLast) { + this.#pushHandlerSets(handlerSets, child, method, params, node.#params); + if (child.#children["*"]) this.#pushHandlerSets(handlerSets, child.#children["*"], method, params, node.#params); + } else { + child.#params = params; + tempNodes.push(child); + } + } + } + } + const shifted = curNodesQueue.shift(); + curNodes = shifted ? tempNodes.concat(shifted) : tempNodes; + } + if (handlerSets[1]) handlerSets.sort((a, b) => { + return a.score - b.score; + }); + return [handlerSets.map(({ handler, params }) => [handler, params])]; + } +}; +//#endregion +export { Node }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js new file mode 100644 index 000000000..96f4c9351 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/trie-router/router.js @@ -0,0 +1,15 @@ +import { checkOptionalParameter } from "../../utils/url.js"; +import { Node } from "./node.js"; +//#region src/router/trie-router/router.ts +var TrieRouter = class { + name = "TrieRouter"; + #node = new Node(); + add(method, path, handler) { + for (const result of checkOptionalParameter(path) || [path]) this.#node.insert(method, result, handler); + } + match(method, path) { + return this.#node.search(method, path); + } +}; +//#endregion +export { TrieRouter }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/router/utils.js b/tests/fixtures/npm/node_modules/hono/dist/router/utils.js new file mode 100644 index 000000000..3f1e9d328 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/router/utils.js @@ -0,0 +1,4 @@ +//#region src/router/utils.ts +const createNullObject = () => Object.create(null); +//#endregion +export { createNullObject }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts new file mode 100644 index 000000000..81dc47313 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts @@ -0,0 +1,204 @@ +import { METHODS, METHOD_NAME_ALL_LOWERCASE } from "../router.js"; +import { HonoBase as Hono } from "../hono-base.js"; +import { StatusCode, SuccessStatusCode } from "../utils/http-status.js"; +import { HasRequiredKeys } from "../utils/types.js"; +import { Endpoint, ExtractSchema, KnownResponseFormat, ResponseFormat, Schema } from "../types.js"; +import { Hono as Hono$1 } from "../hono.js"; +//#region src/client/types.d.ts +/** + * Type representing the '$all' method name + */ +type MethodNameAll = `$${typeof METHOD_NAME_ALL_LOWERCASE}`; +/** + * Type representing all standard HTTP methods prefixed with '$' + * e.g., '$get' | '$post' | '$put' | '$delete' | '$options' | '$patch' | '$query' + */ +type StandardMethods = `$${(typeof METHODS)[number]}`; +/** + * Expands '$all' into all standard HTTP methods. + * If the schema contains '$all', it creates a type where all standard HTTP methods + * point to the same endpoint definition as '$all', while removing '$all' itself. + */ +type ExpandAllMethod = MethodNameAll extends keyof S ? { [M in StandardMethods]: S[MethodNameAll]; } & Omit : S; +type HonoRequest = (typeof Hono$1.prototype)['request']; +export type BuildSearchParamsFn = (query: Record) => URLSearchParams; +export type ClientRequestOptions = { + fetch?: typeof fetch | HonoRequest; + webSocket?: (...args: ConstructorParameters) => WebSocket; + /** + * Standard `RequestInit`, caution that this take highest priority + * and could be used to overwrite things that Hono sets for you, like `body | method | headers`. + * + * If you want to add some headers, use in `headers` instead of `init` + */ + init?: RequestInit; + /** + * Custom function to serialize query parameters into URLSearchParams. + * By default, arrays are serialized as multiple parameters with the same key (e.g., `key=a&key=b`). + * You can provide a custom function to change this behavior, for example to use bracket notation (e.g., `key[]=a&key[]=b`). + * + * @example + * ```ts + * const client = hc('http://localhost', { + * buildSearchParams: (query) => { + * return new URLSearchParams(qs.stringify(query)) + * } + * }) + * ``` + */ + buildSearchParams?: BuildSearchParamsFn; +} & (keyof T extends never ? { + headers?: Record | (() => Record | Promise>); +} : { + headers: T | (() => T | Promise); +}); +export type ClientRequest = { [M in keyof ExpandAllMethod]: ExpandAllMethod[M] extends Endpoint & { + input: infer R; +} ? R extends object ? HasRequiredKeys extends true ? (args: R, options?: ClientRequestOptions) => Promise[M]>> : (args?: R, options?: ClientRequestOptions) => Promise[M]>> : never : never; } & { + $url: (arg?: Arg) => HonoURL; + $path: (arg?: Arg) => BuildPath; +} & (S['$get'] extends { + outputFormat: 'ws'; +} ? S['$get'] extends { + input: infer I; +} ? { + $ws: (args?: I) => WebSocket; +} : {} : {}); +type ClientResponseOfEndpoint = T extends { + output: infer O; + outputFormat: infer F; + status: infer S; +} ? ClientResponse : never; +export interface ClientResponse { + readonly body: ReadableStream | null; + readonly bodyUsed: boolean; + ok: U extends SuccessStatusCode ? true : U extends Exclude ? false : boolean; + redirected: boolean; + status: U; + statusText: string; + type: 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect'; + headers: Headers; + url: string; + redirect(url: string, status: number): Response; + clone(): Response; + bytes(): Promise>; + json(): F extends 'text' ? Promise : F extends 'json' ? Promise : Promise; + text(): F extends 'text' ? (T extends string ? Promise : Promise) : Promise; + blob(): Promise; + formData(): Promise; + arrayBuffer(): Promise; +} +type BuildSearch = Arg extends { [K in Key]: infer Query; } ? IsEmptyObject extends true ? '' : `?${string}` : ''; +type BuildPathname

= Arg extends { + param: infer Param; +} ? `${ApplyParam, Param>}` : `/${TrimStartSlash

}`; +type BuildPath

= `${BuildPathname}${BuildSearch}`; +type BuildTypedURL = TypedURL<`${Protocol}:`, Host, Port, BuildPathname, BuildSearch>; +type HonoURL = IsLiteral extends true ? TrimEndSlash extends `${infer Protocol}://${infer Rest}` ? Rest extends `${infer Hostname}/${infer P}` ? ParseHostName extends [infer Host extends string, infer Port extends string] ? BuildTypedURL : never : ParseHostName extends [infer Host extends string, infer Port extends string] ? BuildTypedURL : never : URL : URL; +type ParseHostName = T extends `${infer Host}:${infer Port}` ? [Host, Port] : [T, '']; +type TrimStartSlash = T extends `/${infer R}` ? TrimStartSlash : T; +type TrimEndSlash = T extends `${infer R}/` ? TrimEndSlash : T; +type IsLiteral = [T] extends [never] ? false : string extends T ? false : true; +type ApplyParam = Path extends `${infer Head}/${infer Rest}` ? Head extends `:${infer Param}` ? P extends Record ? IsLiteral extends true ? ApplyParam : ApplyParam : ApplyParam : ApplyParam : Path extends `:${infer Param}` ? P extends Record ? IsLiteral extends true ? `${Result}/${Value & string}` : `${Result}/${Path}` : `${Result}/${Path}` : `${Result}/${Path}`; +type IsEmptyObject = keyof T extends never ? true : false; +export interface TypedURL extends URL { + protocol: Protocol; + hostname: Hostname; + port: Port; + host: Port extends '' ? Hostname : `${Hostname}:${Port}`; + origin: `${Protocol}//${Hostname}${Port extends '' ? '' : `:${Port}`}`; + pathname: Pathname; + search: Search; + href: `${Protocol}//${Hostname}${Port extends '' ? '' : `:${Port}`}${Pathname}${Search}`; +} +export interface Response extends ClientResponse {} +export type Fetch = (args?: InferRequestType, opt?: ClientRequestOptions) => Promise>>; +type InferEndpointType = T extends ((args: infer R, options: any | undefined) => Promise) ? U extends ClientResponse ? { + input: NonNullable; + output: O; + outputFormat: F; + status: S; +} extends Endpoint ? { + input: NonNullable; + output: O; + outputFormat: F; + status: S; +} : never : never : never; +export type InferResponseType = InferResponseTypeFromEndpoint, U>; +type InferResponseTypeFromEndpoint = T extends { + output: infer O; + status: infer S; +} ? S extends U ? O : never : never; +export type InferRequestType = T extends ((args: infer R, options: any | undefined) => Promise>) ? NonNullable : never; +export type InferRequestOptionsType = T extends ((args: any, options: infer R) => Promise>) ? NonNullable : never; +/** + * Filter a ClientResponse type so it only includes responses of specific status codes. + */ +export type FilterClientResponseByStatusCode, U extends number = StatusCode> = T extends ClientResponse ? RC extends U ? ClientResponse : never : never; +type PathToChain = Path extends `/${infer P}` ? PathToChain : Path extends `${infer P}/${infer R}` ? { [K in P]: PathToChain; } : { [K in Path extends '' ? 'index' : Path]: ClientRequest ? E[Original] : never>; }; +export type Client = T extends Hono ? S extends Record ? K extends string ? PathToChain : never : never : never; +export type Callback = (opts: CallbackOptions) => unknown; +interface CallbackOptions { + path: string[]; + args: any[]; +} +export type ObjectType = { + [key: string]: T; +}; +type GlobalResponseDefinition = { [S in StatusCode]?: { [F in KnownResponseFormat]?: unknown; }; }; +type ToEndpoints = { [S in keyof Def & StatusCode]: { [F in keyof Def[S] & KnownResponseFormat]: Omit & { + output: Def[S][F]; + status: S; + outputFormat: F; +}; }[keyof Def[S] & KnownResponseFormat]; }[keyof Def & StatusCode]; +type ModRoute = R extends Endpoint ? R | ToEndpoints : R; +type ModSchema = { [K in keyof D]: { [M in keyof D[K]]: ModRoute; }; }; +export type ApplyGlobalResponse = App extends Hono ? ModSchema, Def> extends (infer S extends Schema) ? Hono$1 : never : never; +type PickRoute = R extends Endpoint ? R extends { + status: U; +} ? R : never : R; +type PickSchema = { [K in keyof D]: { [M in keyof D[K]]: PickRoute; }; }; +/** + * Keep only specific status code responses from all routes of an app. + * Useful when error responses are handled centrally (e.g., via custom fetch) + * and you want the client to only expose success response types. + * + * @example + * ```ts + * type AppSuccessOnly = PickResponseByStatusCode + * const client = hc('http://localhost') + * ``` + */ +export type PickResponseByStatusCode = App extends Hono ? PickSchema, U> extends (infer S extends Schema) ? Hono$1 : never : never; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts new file mode 100644 index 000000000..163c86b8c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/context.d.ts @@ -0,0 +1,459 @@ +import { Result } from "./router.js"; +import { ResponseHeader } from "./utils/headers.js"; +import { ContentfulStatusCode, RedirectStatusCode, StatusCode } from "./utils/http-status.js"; +import { InvalidJSONValue, IsAny, JSONParsed, JSONValue } from "./utils/types.js"; +import { Env, FetchEventLike, H, Input, NotFoundHandler, RouterRoute, TypedResponse } from "./types.js"; +import { HonoRequest } from "./request.js"; +import { BaseMime } from "./utils/mime.js"; +//#region src/context.d.ts +type HeaderRecord = Record<'Content-Type', BaseMime> | Record | Record; +/** + * Data type can be a string, ArrayBuffer, Blob, Uint8Array (buffer), or ReadableStream. + */ +export type Data = string | ArrayBuffer | Blob | ReadableStream | Uint8Array; +/** + * Interface for the execution context in a web worker or similar environment. + */ +export interface ExecutionContext { + /** + * Extends the lifetime of the event callback until the promise is settled. + * + * @param promise - A promise to wait for. + */ + waitUntil(promise: Promise): void; + /** + * Allows the event to be passed through to subsequent event listeners. + */ + passThroughOnException(): void; + /** + * For compatibility with Wrangler 4.x. + */ + props: any; + /** + * For compatibility with Wrangler 4.x. + */ + exports?: any; +} +/** + * Interface for context variable mapping. + */ +export interface ContextVariableMap {} +/** + * Interface for context renderer. + */ +export interface ContextRenderer {} +/** + * Interface representing a renderer for content. + * + * @interface DefaultRenderer + * @param {string | Promise} content - The content to be rendered, which can be either a string or a Promise resolving to a string. + * @returns {Response | Promise} - The response after rendering the content, which can be either a Response or a Promise resolving to a Response. + */ +interface DefaultRenderer { + (content: string | Promise): Response | Promise; +} +/** + * Renderer type which can either be a ContextRenderer or DefaultRenderer. + */ +export type Renderer = ContextRenderer extends Function ? ContextRenderer : DefaultRenderer; +/** + * Extracts the props for the renderer. + */ +export type PropsForRenderer = [...Required>] extends [unknown, infer Props] ? Props : unknown; +export type Layout> = (props: T) => any; +/** + * Interface for getting context variables. + * + * @template E - Environment type. + */ +interface Get { + (key: Key): E['Variables'][Key]; + (key: Key): ContextVariableMap[Key]; +} +/** + * Interface for setting context variables. + * + * @template E - Environment type. + */ +interface Set { + (key: Key, value: E['Variables'][Key]): void; + (key: Key, value: ContextVariableMap[Key]): void; +} +/** + * Interface for creating a new response. + */ +interface NewResponse { + (data: Data | null, status?: StatusCode, headers?: HeaderRecord): Response; + (data: Data | null, init?: ResponseOrInit): Response; +} +/** + * Interface for responding with a body. + */ +interface BodyRespond { + (data: T, status?: U, headers?: HeaderRecord): Response & TypedResponse; + (data: T, init?: ResponseOrInit): Response & TypedResponse; + (data: T, status?: U, headers?: HeaderRecord): Response & TypedResponse; + (data: T, init?: ResponseOrInit): Response & TypedResponse; +} +/** + * Interface for responding with text. + * + * @interface TextRespond + * @template T - The type of the text content. + * @template U - The type of the status code. + * + * @param {T} text - The text content to be included in the response. + * @param {U} [status] - An optional status code for the response. + * @param {HeaderRecord} [headers] - An optional record of headers to include in the response. + * + * @returns {Response & TypedResponse} - The response after rendering the text content, typed with the provided text and status code types. + */ +interface TextRespond { + (text: T, status?: U, headers?: HeaderRecord): Response & TypedResponse; + (text: T, init?: ResponseOrInit): Response & TypedResponse; +} +/** + * Interface for responding with JSON. + * + * @interface JSONRespond + * @template T - The type of the JSON value or simplified unknown type. + * @template U - The type of the status code. + * + * @param {T} object - The JSON object to be included in the response. + * @param {U} [status] - An optional status code for the response. + * @param {HeaderRecord} [headers] - An optional record of headers to include in the response. + * + * @returns {JSONRespondReturn} - The response after rendering the JSON object, typed with the provided object and status code types. + */ +interface JSONRespond { + (object: T, status?: U, headers?: HeaderRecord): JSONRespondReturn; + (object: T, init?: ResponseOrInit): JSONRespondReturn; +} +/** + * @template T - The type of the JSON value or simplified unknown type. + * @template U - The type of the status code. + * + * @returns {Response & TypedResponse, U, 'json'>} - The response after rendering the JSON object, typed with the provided object and status code types. + */ +type JSONRespondReturn = Response & TypedResponse, U, 'json'>; +/** + * Interface representing a function that responds with HTML content. + * + * @param html - The HTML content to respond with, which can be a string or a Promise that resolves to a string. + * @param status - (Optional) The HTTP status code for the response. + * @param headers - (Optional) A record of headers to include in the response. + * @param init - (Optional) The response initialization object. + * + * @returns A Response object or a Promise that resolves to a Response object. + */ +interface HTMLRespond { + >(html: T, status?: ContentfulStatusCode, headers?: HeaderRecord): T extends string ? Response : Promise; + >(html: T, init?: ResponseOrInit): T extends string ? Response : Promise; +} +/** + * Options for configuring the context. + * + * @template E - Environment type. + */ +type ContextOptions = { + /** + * Bindings for the environment. + */ + env: E['Bindings']; + /** + * Execution context for the request. + */ + executionCtx?: FetchEventLike | ExecutionContext | undefined; + /** + * Handler for not found responses. + */ + notFoundHandler?: NotFoundHandler; + matchResult?: Result<[H, RouterRoute]>; + path?: string; +}; +interface SetHeadersOptions { + append?: boolean; +} +interface SetHeaders { + (name: 'Content-Type', value?: BaseMime, options?: SetHeadersOptions): void; + (name: ResponseHeader, value?: string, options?: SetHeadersOptions): void; + (name: string, value?: string, options?: SetHeadersOptions): void; +} +type ResponseHeadersInit = [string, string][] | Record<'Content-Type', BaseMime> | Record | Record | Headers; +interface ResponseInit { + headers?: ResponseHeadersInit; + status?: T; + statusText?: string; +} +type ResponseOrInit = ResponseInit | Response; +export declare const TEXT_PLAIN = "text/plain; charset=UTF-8"; +export declare class Context { + + /** + * `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers. + * + * @see {@link https://hono.dev/docs/api/context#env} + * + * @example + * ```ts + * // Environment object for Cloudflare Workers + * app.get('*', async c => { + * const counter = c.env.COUNTER + * }) + * ``` + */ + env: E['Bindings']; + finalized: boolean; + /** + * `.error` can get the error object from the middleware if the Handler throws an error. + * + * @see {@link https://hono.dev/docs/api/context#error} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * await next() + * if (c.error) { + * // do something... + * } + * }) + * ``` + */ + error: Error | undefined; + /** + * Creates an instance of the Context class. + * + * @param req - The Request object. + * @param options - Optional configuration options for the context. + */ + constructor(req: Request, options?: ContextOptions); + /** + * `.req` is the instance of {@link HonoRequest}. + */ + get req(): HonoRequest; + /** + * @see {@link https://hono.dev/docs/api/context#event} + * The FetchEvent associated with the current request. + * + * @throws Will throw an error if the context does not have a FetchEvent. + */ + get event(): FetchEventLike; + /** + * @see {@link https://hono.dev/docs/api/context#executionctx} + * The ExecutionContext associated with the current request. + * + * @throws Will throw an error if the context does not have an ExecutionContext. + */ + get executionCtx(): ExecutionContext; + /** + * @see {@link https://hono.dev/docs/api/context#res} + * The Response object for the current request. + */ + get res(): Response; + /** + * Sets the Response object for the current request. + * + * @param _res - The Response object to set. + */ + set res(_res: Response | undefined); + /** + * `.render()` can create a response within a layout. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```ts + * app.get('/', (c) => { + * return c.render('Hello!') + * }) + * ``` + */ + render: Renderer; + /** + * Sets the layout for the response. + * + * @param layout - The layout to set. + * @returns The layout function. + */ + setLayout: (layout: Layout) => Layout; + /** + * Gets the current layout for the response. + * + * @returns The current layout function. + */ + getLayout: () => Layout | undefined; + /** + * `.setRenderer()` can set the layout in the custom middleware. + * + * @see {@link https://hono.dev/docs/api/context#render-setrenderer} + * + * @example + * ```tsx + * app.use('*', async (c, next) => { + * c.setRenderer((content) => { + * return c.html( + * + * + *

{content}

+ * + * + * ) + * }) + * await next() + * }) + * ``` + */ + setRenderer: (renderer: Renderer) => void; + /** + * `.header()` can set headers. + * + * @see {@link https://hono.dev/docs/api/context#header} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * + * // Append multiple headers using the append option (e.g. Vary) + * c.header('Vary', 'Accept-Encoding', { append: true }) + * c.header('Vary', 'User-Agent', { append: true }) + * + * return c.body('Thank you for coming') + * }) + * ``` + */ + header: SetHeaders; + status: (status: StatusCode) => void; + /** + * `.set()` can set the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.use('*', async (c, next) => { + * c.set('message', 'Hono is hot!!') + * await next() + * }) + * ``` + */ + set: Set extends true ? { + Variables: ContextVariableMap & Record; + } : E>; + /** + * `.get()` can use the value specified by the key. + * + * @see {@link https://hono.dev/docs/api/context#set-get} + * + * @example + * ```ts + * app.get('/', (c) => { + * const message = c.get('message') + * return c.text(`The message is "${message}"`) + * }) + * ``` + */ + get: Get extends true ? { + Variables: ContextVariableMap & Record; + } : E>; + /** + * `.var` can access the value of a variable. + * + * @see {@link https://hono.dev/docs/api/context#var} + * + * @example + * ```ts + * const result = c.var.client.oneMethod() + * ``` + */ + get var(): Readonly extends true ? Record : E['Variables'])>; + newResponse: NewResponse; + /** + * `.body()` can return the HTTP response. + * You can set headers with `.header()` and set HTTP status code with `.status`. + * This can also be set in `.text()`, `.json()` and so on. + * + * @see {@link https://hono.dev/docs/api/context#body} + * + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * // Set HTTP status code + * c.status(201) + * + * // Return the response body + * return c.body('Thank you for coming') + * }) + * ``` + */ + body: BodyRespond; + /** + * `.text()` can render text as `Content-Type:text/plain`. + * + * @see {@link https://hono.dev/docs/api/context#text} + * + * @example + * ```ts + * app.get('/say', (c) => { + * return c.text('Hello!') + * }) + * ``` + */ + text: TextRespond; + /** + * `.json()` can render JSON as `Content-Type:application/json`. + * + * @see {@link https://hono.dev/docs/api/context#json} + * + * @example + * ```ts + * app.get('/api', (c) => { + * return c.json({ message: 'Hello!' }) + * }) + * ``` + */ + json: JSONRespond; + html: HTMLRespond; + /** + * `.redirect()` can Redirect, default status code is 302. + * + * @see {@link https://hono.dev/docs/api/context#redirect} + * + * @example + * ```ts + * app.get('/redirect', (c) => { + * return c.redirect('/') + * }) + * app.get('/redirect-permanently', (c) => { + * return c.redirect('/', 301) + * }) + * ``` + */ + redirect: (location: string | URL, status?: T) => Response & TypedResponse; + /** + * `.notFound()` can return the Not Found Response. + * + * @see {@link https://hono.dev/docs/api/context#notfound} + * + * @example + * ```ts + * app.get('/notfound', (c) => { + * return c.notFound() + * }) + * ``` + */ + notFound: () => ReturnType; +} +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts new file mode 100644 index 000000000..85f57dfc6 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/hono-base.d.ts @@ -0,0 +1,221 @@ +import { Router } from "./router.js"; +import { Env, ErrorHandler, H, HandlerInterface, MergePath, MergeSchemaPath, MiddlewareHandlerInterface, NotFoundHandler, OnHandlerInterface, RouterRoute, Schema } from "./types.js"; +import { Context, ExecutionContext } from "./context.js"; +//#region src/hono-base.d.ts +type GetPath = (request: Request, options?: { + env?: E['Bindings']; +}) => string; +export type HonoOptions = { + /** + * `strict` option specifies whether to distinguish whether the last path is a directory or not. + * + * @see {@link https://hono.dev/docs/api/hono#strict-mode} + * + * @default true + */ + strict?: boolean; + /** + * `router` option specifies which router to use. + * + * @see {@link https://hono.dev/docs/api/hono#router-option} + * + * @example + * ```ts + * const app = new Hono({ router: new RegExpRouter() }) + * ``` + */ + router?: Router<[H, RouterRoute]>; + /** + * `getPath` can handle the host header value. + * + * @see {@link https://hono.dev/docs/api/routing#routing-with-host-header-value} + * + * @example + * ```ts + * const app = new Hono({ + * getPath: (req) => + * '/' + req.headers.get('host') + req.url.replace(/^https?:\/\/[^/]+(\/[^?]*)/, '$1'), + * }) + * + * app.get('/www1.example.com/hello', () => c.text('hello www1')) + * + * // A following request will match the route: + * // new Request('http://www1.example.com/hello', { + * // headers: { host: 'www1.example.com' }, + * // }) + * ``` + */ + getPath?: GetPath; +}; +type MountOptionHandler = (c: Context) => unknown; +type MountReplaceRequest = (originalRequest: Request) => Request; +type MountOptions = MountOptionHandler | { + optionHandler?: MountOptionHandler; + replaceRequest?: MountReplaceRequest | false; +}; +declare class Hono { + + get: HandlerInterface; + post: HandlerInterface; + put: HandlerInterface; + delete: HandlerInterface; + options: HandlerInterface; + patch: HandlerInterface; + query: HandlerInterface; + all: HandlerInterface; + on: OnHandlerInterface; + use: MiddlewareHandlerInterface; + router: Router<[H, RouterRoute]>; + readonly getPath: GetPath; + private _basePath; + routes: RouterRoute[]; + constructor(options?: HonoOptions); + private errorHandler; + /** + * `.route()` allows grouping other Hono instance in routes. + * + * @see {@link https://hono.dev/docs/api/routing#grouping} + * + * @param {string} path - base Path + * @param {Hono} app - other Hono instance + * @returns {Hono} routed Hono instance + * + * @example + * ```ts + * const app = new Hono() + * const app2 = new Hono() + * + * app2.get("/user", (c) => c.text("user")) + * app.route("/api", app2) // GET /api/user + * ``` + */ + route(path: SubPath, app: Hono): Hono> | S, BasePath, CurrentPath>; + /** + * `.basePath()` allows base paths to be specified. + * + * @see {@link https://hono.dev/docs/api/routing#base-path} + * + * @param {string} path - base Path + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * const api = new Hono().basePath('/api') + * ``` + */ + basePath(path: SubPath): Hono, MergePath>; + /** + * `.onError()` handles an error and returns a customized Response. + * + * @see {@link https://hono.dev/docs/api/hono#error-handling} + * + * @param {ErrorHandler} handler - request Handler for error + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.onError((err, c) => { + * console.error(`${err}`) + * return c.text('Custom Error Message', 500) + * }) + * ``` + */ + onError: (handler: ErrorHandler) => Hono; + /** + * `.notFound()` allows you to customize a Not Found Response. + * + * @see {@link https://hono.dev/docs/api/hono#not-found} + * + * @param {NotFoundHandler} handler - request handler for not-found + * @returns {Hono} changed Hono instance + * + * @example + * ```ts + * app.notFound((c) => { + * return c.text('Custom 404 Message', 404) + * }) + * ``` + */ + notFound: (handler: NotFoundHandler) => Hono; + /** + * `.mount()` allows you to mount applications built with other frameworks into your Hono application. + * + * @deprecated Use `mount()` from `hono/mount` instead. `.mount()` will be removed in v5. + * + * @see {@link https://hono.dev/docs/api/hono#mount} + * + * @param {string} path - base Path + * @param {Function} applicationHandler - other Request Handler + * @param {MountOptions} [options] - options of `.mount()` + * @returns {Hono} mounted Hono instance + * + * @example + * ```ts + * import { Router as IttyRouter } from 'itty-router' + * import { Hono } from 'hono' + * // Create itty-router application + * const ittyRouter = IttyRouter() + * // GET /itty-router/hello + * ittyRouter.get('/hello', () => new Response('Hello from itty-router')) + * + * const app = new Hono() + * app.mount('/itty-router', ittyRouter.handle) + * ``` + * + * @example + * ```ts + * const app = new Hono() + * // Send the request to another application without modification. + * app.mount('/app', anotherApp, { + * replaceRequest: (req) => req, + * }) + * ``` + */ + mount(path: string, applicationHandler: (request: Request, ...args: any) => Response | Promise, options?: MountOptions): Hono; + /** + * `.fetch()` will be entry point of your app. + * + * @see {@link https://hono.dev/docs/api/hono#fetch} + * + * @param {Request} request - request Object of request + * @param {Env} env - env Object + * @param {ExecutionContext} executionCtx - context of execution + * @returns {Response | Promise} response of request + * + */ + fetch: (request: Request, env?: E['Bindings'] | {}, executionCtx?: ExecutionContext) => Response | Promise; + /** + * `.request()` is a useful method for testing. + * You can pass a URL or pathname to send a GET request. + * app will return a Response object. + * ```ts + * test('GET /hello is ok', async () => { + * const res = await app.request('/hello') + * expect(res.status).toBe(200) + * }) + * ``` + * @see https://hono.dev/docs/api/hono#request + */ + request: (input: Request | string | URL, requestInit?: RequestInit, Env?: E["Bindings"] | {}, executionCtx?: ExecutionContext) => Response | Promise; + /** + * `.fire()` automatically adds a global fetch event listener. + * This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers. + * @deprecated + * Use `fire` from `hono/service-worker` instead. + * ```ts + * import { Hono } from 'hono' + * import { fire } from 'hono/service-worker' + * + * const app = new Hono() + * // ... + * fire(app) + * ``` + * @see https://hono.dev/docs/api/hono#fire + * @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API + * @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ + */ + fire: () => void; +} +//#endregion +export { Hono as HonoBase }; +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts new file mode 100644 index 000000000..52ce78cec --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/hono.d.ts @@ -0,0 +1,20 @@ +import { HonoBase as Hono$1, HonoOptions } from "./hono-base.js"; +import { BlankEnv, BlankSchema, Env, Schema } from "./types.js"; +//#region src/hono.d.ts +/** + * The Hono class extends the functionality of the HonoBase class. + * It sets up routing and allows for custom options to be passed. + * + * @template E - The environment type. + * @template S - The schema type. + * @template BasePath - The base path type. + */ +export declare class Hono extends Hono$1 { + /** + * Creates an instance of the Hono class. + * + * @param options - Optional configuration options for the Hono instance. + */ + constructor(options?: HonoOptions); +} +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts new file mode 100644 index 000000000..12bbd7aaf --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/index.d.ts @@ -0,0 +1,6 @@ +import { Env, ErrorHandler, Handler, Input, MiddlewareHandler, Next, NotFoundHandler, NotFoundResponse, Schema, ToSchema, TypedResponse, ValidationTargets } from "./types.js"; +import { HonoRequest } from "./request.js"; +import { Context, ContextRenderer, ContextVariableMap, ExecutionContext } from "./context.js"; +import { Hono } from "./hono.js"; +import { ClientRequestOptions, InferRequestType, InferResponseType } from "./client/types.js"; +export { type ClientRequestOptions, Context, type ContextRenderer, type ContextVariableMap, type Env, type ErrorHandler, type ExecutionContext, type Handler, Hono, type HonoRequest, type InferRequestType, type InferResponseType, type Input, type MiddlewareHandler, type Next, type NotFoundHandler, type NotFoundResponse, type Schema, type ToSchema, type TypedResponse, type ValidationTargets }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts new file mode 100644 index 000000000..03d14a385 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts @@ -0,0 +1,326 @@ +import { GET_MATCH_RESULT } from "./request/constants.js"; +import { Result } from "./router.js"; +import { CustomHeader, RequestHeader } from "./utils/headers.js"; +import { Simplify, UnionToIntersection } from "./utils/types.js"; +import { Input, InputToDataByTarget, ParamKeyToRecord, ParamKeys, RemoveQuestion, RouterRoute, ValidationTargets } from "./types.js"; +import { BodyData, ParseBodyOptions } from "./utils/body.js"; +//#region src/request.d.ts +type Body = { + json: any; + text: string; + arrayBuffer: ArrayBuffer; + blob: Blob; + formData: FormData; +}; +type BodyCache = Partial; +export declare class HonoRequest

{ + + /** + * `.raw` can get the raw Request object. + * + * @see {@link https://hono.dev/docs/api/request#raw} + * + * @example + * ```ts + * // For Cloudflare Workers + * app.post('/', async (c) => { + * const metadata = c.req.raw.cf?.hostMetadata? + * ... + * }) + * ``` + */ + raw: Request; + routeIndex: number; + /** + * `.path` can get the pathname of the request. + * + * @see {@link https://hono.dev/docs/api/request#path} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const pathname = c.req.path // `/about/me` + * }) + * ``` + */ + path: string; + bodyCache: BodyCache; + constructor(request: Request, path?: string, matchResult?: Result<[unknown, RouterRoute]>); + /** + * `.req.param()` gets the path parameters. + * + * @see {@link https://hono.dev/docs/api/routing#path-parameter} + * + * @example + * ```ts + * const name = c.req.param('name') + * // or all parameters at once + * const { id, comment_id } = c.req.param() + * ``` + */ + param = ParamKeys

>(key: string extends P ? never : P2 extends `${infer _}?` ? never : P2): string; + param> = RemoveQuestion>>(key: P2): string | undefined; + param(key: string): string | undefined; + param(): Simplify>>>; + /** + * `.query()` can get querystring parameters. + * + * @see {@link https://hono.dev/docs/api/request#query} + * + * @example + * ```ts + * // Query params + * app.get('/search', (c) => { + * const query = c.req.query('q') + * }) + * + * // Get all params at once + * app.get('/search', (c) => { + * const { q, limit, offset } = c.req.query() + * }) + * ``` + */ + query(key: string): string | undefined; + query(): Record; + /** + * `.queries()` can get multiple querystring parameter values, e.g. /search?tags=A&tags=B + * + * @see {@link https://hono.dev/docs/api/request#queries} + * + * @example + * ```ts + * app.get('/search', (c) => { + * // tags will be string[] + * const tags = c.req.queries('tags') + * }) + * ``` + */ + queries(key: string): string[] | undefined; + queries(): Record; + /** + * `.header()` can get the request header value. + * + * @see {@link https://hono.dev/docs/api/request#header} + * + * @example + * ```ts + * app.get('/', (c) => { + * const userAgent = c.req.header('User-Agent') + * }) + * ``` + */ + header(name: RequestHeader): string | undefined; + header(name: string): string | undefined; + header(): Record; + /** + * `.parseBody()` can parse Request body of type `multipart/form-data` or `application/x-www-form-urlencoded` + * + * @see {@link https://hono.dev/docs/api/request#parsebody} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.parseBody() + * }) + * ``` + */ + parseBody, T extends BodyData>(options?: Options): Promise; + parseBody(options?: Partial): Promise; + /** + * `.json()` can parse Request body of type `application/json` + * + * @see {@link https://hono.dev/docs/api/request#json} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.json() + * }) + * ``` + */ + json(): Promise; + /** + * `.text()` can parse Request body of type `text/plain` + * + * @see {@link https://hono.dev/docs/api/request#text} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.text() + * }) + * ``` + */ + text(): Promise; + /** + * `.arrayBuffer()` parse Request body as an `ArrayBuffer` + * + * @see {@link https://hono.dev/docs/api/request#arraybuffer} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.arrayBuffer() + * }) + * ``` + */ + arrayBuffer(): Promise; + /** + * `.bytes()` parses the request body as a `Uint8Array`. + * + * @see {@link https://hono.dev/docs/api/request#bytes} + * + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.bytes() + * }) + * ``` + */ + bytes(): Promise; + /** + * Parses the request body as a `Blob`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.blob(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#blob + */ + blob(): Promise; + /** + * Parses the request body as `FormData`. + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.formData(); + * }); + * ``` + * @see https://hono.dev/docs/api/request#formdata + */ + formData(): Promise; + /** + * Adds validated data to the request. + * + * @param target - The target of the validation. + * @param data - The validated data to add. + */ + addValidatedData(target: keyof ValidationTargets, data: {}): void; + /** + * Gets validated data from the request. + * + * @param target - The target of the validation. + * @returns The validated data. + * + * @see https://hono.dev/docs/api/request#valid + */ + valid(target: T): InputToDataByTarget; + /** + * `.url` can get the request url strings. + * + * @see {@link https://hono.dev/docs/api/request#url} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const url = c.req.url // `http://localhost:8787/about/me` + * ... + * }) + * ``` + */ + get url(): string; + /** + * `.method` can get the method name of the request. + * + * @see {@link https://hono.dev/docs/api/request#method} + * + * @example + * ```ts + * app.get('/about/me', (c) => { + * const method = c.req.method // `GET` + * }) + * ``` + */ + get method(): string; + get [GET_MATCH_RESULT](): Result<[unknown, RouterRoute]>; + /** + * `.matchedRoutes` can return a matched route in the handler + * + * @deprecated + * + * Use matchedRoutes helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#matchedroutes} + * + * @example + * ```ts + * app.use('*', async function logger(c, next) { + * await next() + * c.req.matchedRoutes.forEach(({ handler, method, path }, i) => { + * const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]') + * console.log( + * method, + * ' ', + * path, + * ' '.repeat(Math.max(10 - path.length, 0)), + * name, + * i === c.req.routeIndex ? '<- respond from here' : '' + * ) + * }) + * }) + * ``` + */ + get matchedRoutes(): RouterRoute[]; + /** + * `.routePath` can retrieve the path registered within the handler + * + * @deprecated + * + * Use routePath helper defined in "hono/route" instead. + * + * @see {@link https://hono.dev/docs/api/request#routepath} + * + * @example + * ```ts + * app.get('/posts/:id', (c) => { + * return c.json({ path: c.req.routePath }) + * }) + * ``` + */ + get routePath(): string; +} +/** + * Clones a HonoRequest's underlying raw Request object. + * + * This utility handles both consumed and unconsumed request bodies: + * - If the request body hasn't been consumed, it uses the native `clone()` method + * - If the request body has been consumed, it reconstructs a new Request using cached body data + * + * This is particularly useful when you need to: + * - Process the same request body multiple times + * - Pass requests to external services after validation + * + * @param req - The HonoRequest object to clone + * @returns A Promise that resolves to a new Request object with the same properties + * @throws {HTTPException} If the request body was consumed directly via `req.raw` + * without using HonoRequest methods (e.g., `req.json()`, `req.text()`), making it + * impossible to reconstruct the body from cache + * + * @example + * ```ts + * // Clone after consuming the body (e.g., after validation) + * app.post('/forward', + * validator('json', (data) => data), + * async (c) => { + * const validated = c.req.valid('json') + * // Body has been consumed, but cloneRawRequest still works + * const clonedReq = await cloneRawRequest(c.req) + * return fetch('http://backend-service.com', clonedReq) + * } + * ) + * ``` + */ +export declare const cloneRawRequest: (req: HonoRequest) => Promise; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts new file mode 100644 index 000000000..0899a5d2b --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/request/constants.d.ts @@ -0,0 +1,3 @@ +//#region src/request/constants.d.ts +export declare const GET_MATCH_RESULT: unique symbol; +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts new file mode 100644 index 000000000..f00b40662 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/router.d.ts @@ -0,0 +1,98 @@ +//#region src/router.d.ts +/** + * @module + * This module provides types definitions and variables for the routers. + */ +/** + * Constant representing all HTTP methods in uppercase. + */ +export declare const METHOD_NAME_ALL: "ALL"; +/** + * Constant representing all HTTP methods in lowercase. + */ +export declare const METHOD_NAME_ALL_LOWERCASE: "all"; +/** + * Array of supported HTTP methods. + */ +export declare const METHODS: readonly ["get", "post", "put", "delete", "options", "patch", "query"]; +/** + * Error message indicating that a route cannot be added because the matcher is already built. + */ +export declare const MESSAGE_MATCHER_IS_ALREADY_BUILT = "Can not add a route since the matcher is already built."; +/** + * Interface representing a router. + * + * @template T - The type of the handler. + */ +export interface Router { + /** + * The name of the router. + */ + name: string; + /** + * Adds a route to the router. + * + * @param method - The HTTP method (e.g., 'get', 'post'). + * @param path - The path for the route. + * @param handler - The handler for the route. + */ + add(method: string, path: string, handler: T): void; + /** + * Matches a route based on the given method and path. + * + * @param method - The HTTP method (e.g., 'get', 'post'). + * @param path - The path to match. + * @returns The result of the match. + */ + match(method: string, path: string): Result; +} +/** + * Type representing a map of parameter indices. + */ +export type ParamIndexMap = Record; +/** + * Type representing a stash of parameters. + */ +export type ParamStash = string[]; +/** + * Type representing a map of parameters. + */ +export type Params = Record; +/** + * Type representing the result of a route match. + * + * The result can be in one of two formats: + * 1. An array of handlers with their corresponding parameter index maps, followed by a parameter stash. + * 2. An array of handlers with their corresponding parameter maps. + * + * Example: + * + * [[handler, paramIndexMap][], paramArray] + * ```typescript + * [ + * [ + * [middlewareA, {}], // '*' + * [funcA, {'id': 0}], // '/user/:id/*' + * [funcB, {'id': 0, 'action': 1}], // '/user/:id/:action' + * ], + * ['123', 'abc'] + * ] + * ``` + * + * [[handler, params][]] + * ```typescript + * [ + * [ + * [middlewareA, {}], // '*' + * [funcA, {'id': '123'}], // '/user/:id/*' + * [funcB, {'id': '123', 'action': 'abc'}], // '/user/:id/:action' + * ] + * ] + * ``` + */ +export type Result = [[T, ParamIndexMap][], ParamStash] | [[T, Params][]]; +/** + * Error class representing an unsupported path error. + */ +export declare class UnsupportedPathError extends Error {} +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts new file mode 100644 index 000000000..4a0be1dea --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/types.d.ts @@ -0,0 +1,214 @@ +import { HonoBase as Hono } from "./hono-base.js"; +import { CustomHeader, RequestHeader } from "./utils/headers.js"; +import { StatusCode } from "./utils/http-status.js"; +import { IfAnyThenEmptyObject, IsAny, JSONValue, RemoveBlankRecord, Simplify, UnionToIntersection } from "./utils/types.js"; +import { Context } from "./context.js"; +//#region src/types.d.ts +export type Bindings = object; +export type Variables = object; +export type BlankEnv = {}; +export type Env = { + Bindings?: Bindings; + Variables?: Variables; +}; +export type Next = () => Promise; +export type ExtractInput = I extends Input ? unknown extends I['in'] ? {} : I['in'] : I; +export type Input = { + in?: {}; + out?: {}; + outputFormat?: ResponseFormat; +}; +export type BlankSchema = {}; +export type BlankInput = {}; +export interface RouterRoute { + basePath: string; + path: string; + method: string; + handler: H; +} +export type HandlerResponse = Response | TypedResponse | Promise> | Promise; +export type Handler = any> = (c: Context, next: Next) => R; +export type MiddlewareHandler = Response> = (c: Context, next: Next) => Promise; +export type H = any> = Handler | MiddlewareHandler; +/** + * You can extend this interface to define a custom `c.notFound()` Response type. + * + * @example + * declare module 'hono' { + * interface NotFoundResponse extends Response, TypedResponse {} + * } + */ +export interface NotFoundResponse {} +export type NotFoundHandler = (c: Context) => NotFoundResponse extends Response ? NotFoundResponse | Promise : Response | Promise; +export interface HTTPResponseError extends Error { + getResponse: () => Response; +} +export type ErrorHandler = (err: Error | HTTPResponseError, c: Context) => Response | Promise; +export interface HandlerInterface { +

= any, E2 extends Env = E>(handler: H): Hono, S & ToSchema>, BasePath, CurrentPath>; +

= any, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(...handlers: [H & M1, H]): Hono, S & ToSchema | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, E2 extends Env = E>(path: P, handler: H): Hono, S, M, P, I, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(...handlers: [H & M1, H & M2, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(path: P, ...handlers: [H & M1, H]): Hono | MergeMiddlewareResponse, S, M, P, I2, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I3, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I4, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I5, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I6, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I7, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I8, BasePath>, BasePath, MergePath>; +

= any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono, S & ToSchema | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, CurrentPath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I9, BasePath>, BasePath, MergePath>; +

, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse, S, M, P, I10, BasePath>, BasePath, MergePath>; +

= any>(...handlers: H[]): Hono>, BasePath, CurrentPath>; +

= any>(path: P, ...handlers: [H, I, R>, ...H, I, R>[]]): Hono, I, MergeTypedResponse>, BasePath, MergePath>; +

= any, I extends Input = BlankInput>(path: P): Hono, I, MergeTypedResponse>, BasePath, MergePath>; +} +export interface MiddlewareHandlerInterface { + (...handlers: MiddlewareHandler>[]): Hono, S, BasePath, MergePath>; + (handler: MiddlewareHandler>): Hono, S, BasePath, MergePath>; + , P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E>(path: P, handler: MiddlewareHandler): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; + , E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, P extends string = MergePath>(...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, P>; +

, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>>(path: P, ...handlers: [MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler, MiddlewareHandler]): Hono, S, BasePath, MergedPath>; +

(path: P, ...handlers: MiddlewareHandler>[]): Hono>; +} +export interface OnHandlerInterface { + , R extends HandlerResponse = any, I extends Input = BlankInput, E2 extends Env = E>(method: M, path: P, handler: H): Hono, S & ToSchema, I, MergeTypedResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(method: M, path: P, ...handlers: [H & M1, H]): Hono, S & ToSchema, I2, MergeTypedResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H]): Hono, S & ToSchema, I3, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H]): Hono, S & ToSchema, I4, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono, S & ToSchema, I5, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono, S & ToSchema, I6, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono, S & ToSchema, I7, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono, S & ToSchema, I8, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono, S & ToSchema, I9, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(method: M, path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono, S & ToSchema, I10, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + = any, I extends Input = BlankInput>(method: M, path: P, ...handlers: [H, I, R>, ...H, I, R>[]]): Hono, I, MergeTypedResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, E2 extends Env = E>(methods: M[], path: P, handler: H): Hono, S & ToSchema, I, MergeTypedResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, M1 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H]): Hono, S & ToSchema, I2, MergeTypedResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, M1 extends H = H, M2 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H]): Hono, S & ToSchema, I3, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, M1 extends H = H, M2 extends H = H, M3 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H]): Hono, S & ToSchema, I4, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H]): Hono, S & ToSchema, I5, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H]): Hono, S & ToSchema, I6, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H]): Hono, S & ToSchema, I7, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H]): Hono, S & ToSchema, I8, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H]): Hono, S & ToSchema, I9, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + , R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, I5 extends Input = I & I2 & I3 & I4, I6 extends Input = I & I2 & I3 & I4 & I5, I7 extends Input = I & I2 & I3 & I4 & I5 & I6, I8 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7, I9 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8, I10 extends Input = I & I2 & I3 & I4 & I5 & I6 & I7 & I8 & I9, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, E6 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, E7 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6]>, E8 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7]>, E9 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8]>, E10 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9]>, E11 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4, E5, E6, E7, E8, E9, E10]>, M1 extends H = H, M2 extends H = H, M3 extends H = H, M4 extends H = H, M5 extends H = H, M6 extends H = H, M7 extends H = H, M8 extends H = H, M9 extends H = H>(methods: M[], path: P, ...handlers: [H & M1, H & M2, H & M3, H & M4, H & M5, H & M6, H & M7, H & M8, H & M9, H]): Hono, S & ToSchema, I10, MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse>, BasePath, MergePath>; + = any, I extends Input = BlankInput>(methods: M[], path: P, ...handlers: [H, I, R>, ...H, I, R>[]]): Hono, I, MergeTypedResponse>, BasePath, MergePath>; + = any, E2 extends Env = E>(methods: M | M[], paths: Ps, ...handlers: H, I, R>[]): Hono, I, MergeTypedResponse>, BasePath, Ps extends [...string[], infer LastPath extends string] ? MergePath : never>; +} +type ToSchemaOutput = RorO extends TypedResponse ? { + output: unknown extends T ? {} : T; + outputFormat: I extends { + outputFormat: string; + } ? I['outputFormat'] : F; + status: U; +} : { + output: unknown extends RorO ? {} : RorO; + outputFormat: unknown extends RorO ? 'json' : I extends { + outputFormat: string; + } ? I['outputFormat'] : 'json'; + status: StatusCode; +}; +export type ToSchema = IsAny extends true ? { [K in P]: { [K2 in M as AddDollar]: { + input: AddParam, P>; + output: {}; + outputFormat: ResponseFormat; + status: StatusCode; +}; }; } : [RorO] extends [never] ? {} : [RorO] extends [Promise] ? {} : { [K in P]: { [K2 in M as AddDollar]: Simplify<{ + input: AddParam, P>; +} & ToSchemaOutput>; }; }; +export type Schema = { + [Path: string]: { + [Method: `$${Lowercase}`]: Endpoint; + }; +}; +type AddSchemaIfHasResponse = [Merged] extends [Promise] ? S : S & ToSchema, I, Merged>; +export type Endpoint = { + input: any; + output: any; + outputFormat: ResponseFormat; + status: StatusCode; +}; +type ExtractParams = string extends Path ? Record : Path extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractParams<`/${Rest}`>]: string; } : Path extends `${infer _Start}:${infer Param}` ? { [K in Param]: string; } : never; +type FlattenIfIntersect = T extends (infer O) ? { [K in keyof O]: O[K]; } : never; +export type MergeSchemaPath = { [P in keyof OrigSchema as MergePath]: [OrigSchema[P]] extends [Record] ? { [M in keyof OrigSchema[P]]: MergeEndpointParamsWithPath; } : never; }; +type MergeEndpointParamsWithPath = T extends unknown ? { + input: T['input'] extends { + param: infer _; + } ? ExtractParams extends never ? T['input'] : FlattenIfIntersect as K extends `${infer Prefix}{${infer _}}` ? Prefix : K]: string; }; + }> : RemoveBlankRecord> extends never ? T['input'] : T['input'] & { + param: { [K in keyof ExtractParams as K extends `${infer Prefix}{${infer _}}` ? Prefix : K]: string; }; + }; + output: T['output']; + outputFormat: T['outputFormat']; + status: T['status']; +} : never; +export type AddParam = ParamKeys

extends never ? I : I extends { + param: infer _; +} ? I : I & { + param: UnionToIntersection>>; +}; +type AddDollar = `$${Lowercase}`; +export type MergePath = B extends '' ? MergePath : A extends '' ? B : A extends '/' ? B : A extends `${infer P}/` ? B extends `/${infer Q}` ? `${P}/${Q}` : `${P}/${B}` : B extends `/${infer Q}` ? Q extends '' ? A : `${A}/${Q}` : `${A}/${B}`; +export type KnownResponseFormat = 'json' | 'text' | 'redirect'; +export type ResponseFormat = KnownResponseFormat | string; +export type TypedResponse = { + _data: T; + _status: U; + _format: F; +}; +type MergeTypedResponse = T extends Promise ? T : T extends Promise ? T2 extends TypedResponse ? T2 : TypedResponse : T extends TypedResponse ? T : TypedResponse; +type ExtractTypedResponseOnly = T extends TypedResponse ? T : never; +type MergeMiddlewareResponse = T extends ((c: any, next: any) => Promise) ? Exclude extends never ? never : Exclude extends Response | TypedResponse ? ExtractTypedResponseOnly> : never : T extends ((c: any, next: any) => infer R) ? R extends Response | TypedResponse ? ExtractTypedResponseOnly : never : never; +export type FormValue = string | Blob; +export type ParsedFormValue = string | File; +export type ValidationTargets = { + json: any; + form: Record; + query: Record; + param: Record; + header: Record; + cookie: Record; +}; +type ParamKey = Component extends `:${infer NameWithPattern}` ? NameWithPattern extends `${infer Name}{${infer Rest}` ? Rest extends `${infer _Pattern}?` ? `${Name}?` : Name : NameWithPattern : never; +export type ParamKeys = Path extends `${infer Component}/${infer Rest}` ? ParamKey | ParamKeys : ParamKey; +export type ParamKeyToRecord = T extends `${infer R}?` ? Record : { [K in T]: string; }; +export type InputToDataByTarget = T extends { [K in Target]: infer R; } ? R : never; +export type RemoveQuestion = T extends `${infer R}?` ? R : T; +export type ExtractSchema = UnionToIntersection ? S : never>; +export type ExtractSchemaForStatusCode = { [Path in keyof ExtractSchema]: { [Method in keyof ExtractSchema[Path]]: Extract[Path][Method], { + status: Status; +}>; }; }; +export type ExtractHandlerResponse = T extends ((c: any, next: any) => Promise) ? Exclude extends never ? never : Exclude extends Response | TypedResponse ? Exclude : never : T extends ((c: any, next: any) => infer R) ? R extends Response | TypedResponse ? R : never : never; +type ProcessHead = IfAnyThenEmptyObject; +export type IntersectNonAnyTypes = T extends [infer Head, ...infer Rest] ? ProcessHead & IntersectNonAnyTypes : {}; +export declare abstract class FetchEventLike { + abstract readonly request: Request; + abstract respondWith(promise: Response | Promise): void; + abstract passThroughOnException(): void; + abstract waitUntil(promise: Promise): void; +} +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts new file mode 100644 index 000000000..9bbf8cafd --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/body.d.ts @@ -0,0 +1,75 @@ +import { HonoRequest } from "../request.js"; +//#region src/utils/body.d.ts +type BodyDataValueDot = { + [x: string]: string | File | BodyDataValueDot; +}; +type BodyDataValueDotAll = { + [x: string]: string | File | (string | File)[] | BodyDataValueDotAll; +}; +type SimplifyBodyData = { [K in keyof T]: string | File | (string | File)[] | BodyDataValueDotAll extends T[K] ? string | File | (string | File)[] | BodyDataValueDotAll : string | File | BodyDataValueDot extends T[K] ? string | File | BodyDataValueDot : string | File | (string | File)[] extends T[K] ? string | File | (string | File)[] : string | File; } & {}; +type BodyDataValueComponent = string | File | (T extends { + all: false; +} ? never : T extends { + all: true; +} | { + all: boolean; +} ? (string | File)[] : never); +type BodyDataValueObject = { + [key: string]: BodyDataValueComponent | BodyDataValueObject; +}; +type BodyDataValue = BodyDataValueComponent | (T extends { + dot: false; +} ? never : T extends { + dot: true; +} | { + dot: boolean; +} ? BodyDataValueObject : never); +export type BodyData = {}> = SimplifyBodyData>>; +export type ParseBodyOptions = { + /** + * Determines whether all fields with multiple values should be parsed as arrays. + * @default false + * @example + * const data = new FormData() + * data.append('file', 'aaa') + * data.append('file', 'bbb') + * data.append('message', 'hello') + * + * If all is false: + * parseBody should return { file: 'bbb', message: 'hello' } + * + * If all is true: + * parseBody should return { file: ['aaa', 'bbb'], message: 'hello' } + */ + all: boolean; + /** + * Determines whether all fields with dot notation should be parsed as nested objects. + * @default false + * @example + * const data = new FormData() + * data.append('obj.key1', 'value1') + * data.append('obj.key2', 'value2') + * + * If dot is false: + * parseBody should return { 'obj.key1': 'value1', 'obj.key2': 'value2' } + * + * If dot is true: + * parseBody should return { obj: { key1: 'value1', key2: 'value2' } } + */ + dot: boolean; +}; +/** + * Parses the body of a request based on the provided options. + * + * @template T - The type of the parsed body data. + * @param {HonoRequest | Request} request - The request object to parse. + * @param {Partial} [options] - Options for parsing the body. + * @returns {Promise} The parsed body data. + */ +interface ParseBody { + , T extends BodyData>(request: HonoRequest | Request, options?: Options): Promise; + (request: HonoRequest | Request, options?: Partial): Promise; +} +export declare const parseBody: ParseBody; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts new file mode 100644 index 000000000..d3edf8c77 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/headers.d.ts @@ -0,0 +1,10 @@ +//#region src/utils/headers.d.ts +/** + * @module + * HTTP Headers utility. + */ +export type RequestHeader = 'A-IM' | 'Accept' | 'Accept-Additions' | 'Accept-CH' | 'Accept-Charset' | 'Accept-Datetime' | 'Accept-Encoding' | 'Accept-Features' | 'Accept-Language' | 'Accept-Patch' | 'Accept-Post' | 'Accept-Query' | 'Accept-Ranges' | 'Accept-Signature' | 'Access-Control' | 'Access-Control-Allow-Credentials' | 'Access-Control-Allow-Headers' | 'Access-Control-Allow-Methods' | 'Access-Control-Allow-Origin' | 'Access-Control-Expose-Headers' | 'Access-Control-Max-Age' | 'Access-Control-Request-Headers' | 'Access-Control-Request-Method' | 'Activate-Storage-Access' | 'Age' | 'Allow' | 'ALPN' | 'Alt-Svc' | 'Alt-Used' | 'Alternates' | 'AMP-Cache-Transform' | 'Apply-To-Redirect-Ref' | 'Authentication-Control' | 'Authentication-Info' | 'Authorization' | 'Available-Dictionary' | 'C-Ext' | 'C-Man' | 'C-Opt' | 'C-PEP' | 'C-PEP-Info' | 'Cache-Control' | 'Cache-Group-Invalidation' | 'Cache-Groups' | 'Cache-Status' | 'Cal-Managed-ID' | 'CalDAV-Timezones' | 'Capsule-Protocol' | 'CDN-Cache-Control' | 'CDN-Loop' | 'Cert-Not-After' | 'Cert-Not-Before' | 'Clear-Site-Data' | 'Client-Cert' | 'Client-Cert-Chain' | 'Close' | 'CMCD-Object' | 'CMCD-Request' | 'CMCD-Session' | 'CMCD-Status' | 'CMSD-Dynamic' | 'CMSD-Static' | 'Concealed-Auth-Export' | 'Configuration-Context' | 'Connection' | 'Content-Base' | 'Content-Digest' | 'Content-Disposition' | 'Content-Encoding' | 'Content-ID' | 'Content-Language' | 'Content-Length' | 'Content-Location' | 'Content-MD5' | 'Content-Range' | 'Content-Script-Type' | 'Content-Security-Policy' | 'Content-Security-Policy-Report-Only' | 'Content-Style-Type' | 'Content-Type' | 'Content-Version' | 'Cookie' | 'Cookie2' | 'Cross-Origin-Embedder-Policy' | 'Cross-Origin-Embedder-Policy-Report-Only' | 'Cross-Origin-Opener-Policy' | 'Cross-Origin-Opener-Policy-Report-Only' | 'Cross-Origin-Resource-Policy' | 'CTA-Common-Access-Token' | 'DASL' | 'Date' | 'DAV' | 'Default-Style' | 'Delta-Base' | 'Deprecation' | 'Depth' | 'Derived-From' | 'Destination' | 'Detached-JWS' | 'Differential-ID' | 'Dictionary-ID' | 'Digest' | 'DPoP' | 'DPoP-Nonce' | 'Early-Data' | 'EDIINT-Features' | 'ETag' | 'Expect' | 'Expect-CT' | 'Expires' | 'Ext' | 'Forwarded' | 'From' | 'GetProfile' | 'Hobareg' | 'Host' | 'HTTP2-Settings' | 'If' | 'If-Match' | 'If-Modified-Since' | 'If-None-Match' | 'If-Range' | 'If-Schedule-Tag-Match' | 'If-Unmodified-Since' | 'IM' | 'Include-Referred-Token-Binding-ID' | 'Incremental' | 'Isolation' | 'Keep-Alive' | 'Label' | 'Last-Event-ID' | 'Last-Modified' | 'Link' | 'Link-Template' | 'Location' | 'Lock-Token' | 'Man' | 'Max-Forwards' | 'Memento-Datetime' | 'Meter' | 'Method-Check' | 'Method-Check-Expires' | 'MIME-Version' | 'Negotiate' | 'NEL' | 'OData-EntityId' | 'OData-Isolation' | 'OData-MaxVersion' | 'OData-Version' | 'Opt' | 'Optional-WWW-Authenticate' | 'Ordering-Type' | 'Origin' | 'Origin-Agent-Cluster' | 'OSCORE' | 'OSLC-Core-Version' | 'Overwrite' | 'P3P' | 'PEP' | 'PEP-Info' | 'Permissions-Policy' | 'PICS-Label' | 'Ping-From' | 'Ping-To' | 'Position' | 'Pragma' | 'Prefer' | 'Preference-Applied' | 'Priority' | 'ProfileObject' | 'Protocol' | 'Protocol-Info' | 'Protocol-Query' | 'Protocol-Request' | 'Proxy-Authenticate' | 'Proxy-Authentication-Info' | 'Proxy-Authorization' | 'Proxy-Features' | 'Proxy-Instruction' | 'Proxy-Status' | 'Public' | 'Public-Key-Pins' | 'Public-Key-Pins-Report-Only' | 'Range' | 'Redirect-Ref' | 'Referer' | 'Referer-Root' | 'Referrer-Policy' | 'Refresh' | 'Repeatability-Client-ID' | 'Repeatability-First-Sent' | 'Repeatability-Request-ID' | 'Repeatability-Result' | 'Replay-Nonce' | 'Reporting-Endpoints' | 'Repr-Digest' | 'Retry-After' | 'Safe' | 'Schedule-Reply' | 'Schedule-Tag' | 'Sec-Fetch-Dest' | 'Sec-Fetch-Mode' | 'Sec-Fetch-Site' | 'Sec-Fetch-Storage-Access' | 'Sec-Fetch-User' | 'Sec-GPC' | 'Sec-Purpose' | 'Sec-Token-Binding' | 'Sec-WebSocket-Accept' | 'Sec-WebSocket-Extensions' | 'Sec-WebSocket-Key' | 'Sec-WebSocket-Protocol' | 'Sec-WebSocket-Version' | 'Security-Scheme' | 'Server' | 'Server-Timing' | 'Set-Cookie' | 'Set-Cookie2' | 'Set-Txn' | 'SetProfile' | 'Signature' | 'Signature-Input' | 'SLUG' | 'SoapAction' | 'Status-URI' | 'Strict-Transport-Security' | 'Sunset' | 'Surrogate-Capability' | 'Surrogate-Control' | 'TCN' | 'TE' | 'Timeout' | 'Timing-Allow-Origin' | 'Topic' | 'Traceparent' | 'Tracestate' | 'Trailer' | 'Transfer-Encoding' | 'TTL' | 'Unencoded-Digest' | 'Upgrade' | 'Urgency' | 'URI' | 'Use-As-Dictionary' | 'User-Agent' | 'Variant-Vary' | 'Vary' | 'Via' | 'Want-Content-Digest' | 'Want-Digest' | 'Want-Repr-Digest' | 'Want-Unencoded-Digest' | 'Warning' | 'WWW-Authenticate' | 'X-Content-Type-Options' | 'X-Frame-Options'; +export type ResponseHeader = 'Accept-Query' | 'Access-Control-Allow-Credentials' | 'Access-Control-Allow-Headers' | 'Access-Control-Allow-Methods' | 'Access-Control-Allow-Origin' | 'Access-Control-Expose-Headers' | 'Access-Control-Max-Age' | 'Activate-Storage-Access' | 'Age' | 'Allow' | 'Cache-Control' | 'Cache-Group-Invalidation' | 'Cache-Groups' | 'Clear-Site-Data' | 'Content-Disposition' | 'Content-Encoding' | 'Content-Language' | 'Content-Length' | 'Content-Location' | 'Content-Range' | 'Content-Security-Policy' | 'Content-Security-Policy-Report-Only' | 'Content-Type' | 'Cookie' | 'Cross-Origin-Embedder-Policy' | 'Cross-Origin-Opener-Policy' | 'Cross-Origin-Resource-Policy' | 'Date' | 'ETag' | 'Expires' | 'Incremental' | 'Last-Modified' | 'Location' | 'Permissions-Policy' | 'Pragma' | 'Retry-After' | 'Save-Data' | 'Sec-CH-Prefers-Color-Scheme' | 'Sec-CH-Prefers-Reduced-Motion' | 'Sec-CH-UA' | 'Sec-CH-UA-Arch' | 'Sec-CH-UA-Bitness' | 'Sec-CH-UA-Form-Factor' | 'Sec-CH-UA-Full-Version' | 'Sec-CH-UA-Full-Version-List' | 'Sec-CH-UA-Mobile' | 'Sec-CH-UA-Model' | 'Sec-CH-UA-Platform' | 'Sec-CH-UA-Platform-Version' | 'Sec-CH-UA-WoW64' | 'Sec-Fetch-Dest' | 'Sec-Fetch-Mode' | 'Sec-Fetch-Site' | 'Sec-Fetch-User' | 'Sec-GPC' | 'Server' | 'Server-Timing' | 'Service-Worker-Navigation-Preload' | 'Set-Cookie' | 'Set-Txn' | 'Strict-Transport-Security' | 'Timing-Allow-Origin' | 'Trailer' | 'Transfer-Encoding' | 'Unencoded-Digest' | 'Upgrade' | 'Vary' | 'Want-Unencoded-Digest' | 'Warning' | 'WWW-Authenticate' | 'X-Content-Type-Options' | 'X-DNS-Prefetch-Control' | 'X-Frame-Options' | 'X-Permitted-Cross-Domain-Policies' | 'X-Powered-By' | 'X-Robots-Tag' | 'X-XSS-Protection'; +export type AcceptHeader = 'Accept' | 'Accept-Charset' | 'Accept-Encoding' | 'Accept-Language' | 'Accept-Patch' | 'Accept-Post' | 'Accept-Ranges'; +export type CustomHeader = string & {}; +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts new file mode 100644 index 000000000..4c48304e2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/http-status.d.ts @@ -0,0 +1,34 @@ +//#region src/utils/http-status.d.ts +/** + * @module + * HTTP Status utility. + */ +export type InfoStatusCode = 100 | 101 | 102 | 103; +export type SuccessStatusCode = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226; +export type DeprecatedStatusCode = 305 | 306; +export type RedirectStatusCode = 300 | 301 | 302 | 303 | 304 | DeprecatedStatusCode | 307 | 308; +export type ClientErrorStatusCode = 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451; +export type ServerErrorStatusCode = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511; +/** + * `UnofficialStatusCode` can be used to specify an unofficial status code. + * @example + * + * ```ts + * app.get('/unknown', (c) => { + * return c.text("Unknown Error", 520 as UnofficialStatusCode) + * }) + * ``` + */ +export type UnofficialStatusCode = -1; +/** + * @deprecated + * Use `UnofficialStatusCode` instead. + */ +export type UnOfficalStatusCode = UnofficialStatusCode; +/** + * If you want to use an unofficial status, use `UnofficialStatusCode`. + */ +export type StatusCode = InfoStatusCode | SuccessStatusCode | RedirectStatusCode | ClientErrorStatusCode | ServerErrorStatusCode | UnofficialStatusCode; +export type ContentlessStatusCode = 101 | 204 | 205 | 304; +export type ContentfulStatusCode = Exclude; +//#endregion \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts new file mode 100644 index 000000000..b4ea3597e --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/mime.d.ts @@ -0,0 +1,73 @@ +//#region src/utils/mime.d.ts +/** + * @module + * MIME utility. + */ +export declare const getMimeType: (filename: string, mimes?: Record) => string | undefined; +export declare const getExtension: (mimeType: string) => string | undefined; +/** + * Union types for BaseMime + */ +export type BaseMime = (typeof _baseMimes)[keyof typeof _baseMimes]; +declare const _baseMimes: { + readonly aac: "audio/aac"; + readonly avi: "video/x-msvideo"; + readonly avif: "image/avif"; + readonly av1: "video/av1"; + readonly bin: "application/octet-stream"; + readonly bmp: "image/bmp"; + readonly css: "text/css; charset=utf-8"; + readonly csv: "text/csv; charset=utf-8"; + readonly eot: "application/vnd.ms-fontobject"; + readonly epub: "application/epub+zip"; + readonly gif: "image/gif"; + readonly gz: "application/gzip"; + readonly htm: "text/html; charset=utf-8"; + readonly html: "text/html; charset=utf-8"; + readonly ico: "image/x-icon"; + readonly ics: "text/calendar; charset=utf-8"; + readonly jpeg: "image/jpeg"; + readonly jpg: "image/jpeg"; + readonly js: "text/javascript; charset=utf-8"; + readonly json: "application/json"; + readonly jsonld: "application/ld+json"; + readonly map: "application/json"; + readonly mid: "audio/x-midi"; + readonly midi: "audio/x-midi"; + readonly mjs: "text/javascript; charset=utf-8"; + readonly mp3: "audio/mpeg"; + readonly mp4: "video/mp4"; + readonly mpeg: "video/mpeg"; + readonly oga: "audio/ogg"; + readonly ogv: "video/ogg"; + readonly ogx: "application/ogg"; + readonly opus: "audio/opus"; + readonly otf: "font/otf"; + readonly pdf: "application/pdf"; + readonly png: "image/png"; + readonly rtf: "application/rtf"; + readonly svg: "image/svg+xml; charset=utf-8"; + readonly tif: "image/tiff"; + readonly tiff: "image/tiff"; + readonly ts: "video/mp2t"; + readonly ttf: "font/ttf"; + readonly txt: "text/plain; charset=utf-8"; + readonly wasm: "application/wasm"; + readonly webm: "video/webm"; + readonly weba: "audio/webm"; + readonly webmanifest: "application/manifest+json"; + readonly webp: "image/webp"; + readonly woff: "font/woff"; + readonly woff2: "font/woff2"; + readonly xhtml: "application/xhtml+xml; charset=utf-8"; + readonly xml: "application/xml; charset=utf-8"; + readonly zip: "application/zip"; + readonly '3gp': "video/3gpp"; + readonly '3g2': "video/3gpp2"; + readonly gltf: "model/gltf+json"; + readonly glb: "model/gltf-binary"; +}; +declare const baseMimes: Record; +//#endregion +export { baseMimes as mimes }; +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts new file mode 100644 index 000000000..2a4ad3781 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/utils/types.d.ts @@ -0,0 +1,62 @@ +//#region src/utils/types.d.ts +/** + * @module + * Types utility. + */ +export type Expect = T; +export type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true : false; +export type NotEqual = true extends Equal ? false : true; +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; +export type RemoveBlankRecord = T extends Record ? (K extends string ? T : never) : never; +export type IfAnyThenEmptyObject = 0 extends 1 & T ? {} : T; +export type JSONPrimitive = string | boolean | number | null; +export type JSONArray = (JSONPrimitive | JSONObject | JSONArray)[]; +export type JSONObject = { + [key: string]: JSONPrimitive | JSONArray | JSONObject | object | InvalidJSONValue; +}; +export type InvalidJSONValue = undefined | symbol | ((...args: unknown[]) => unknown); +type InvalidToNull = T extends InvalidJSONValue ? null : T; +type IsInvalid = T extends InvalidJSONValue ? true : false; +/** + * symbol keys are omitted through `JSON.stringify` + */ +type OmitSymbolKeys = { [K in keyof T as K extends symbol ? never : K]: T[K]; }; +export type JSONValue = JSONObject | JSONArray | JSONPrimitive; +/** + * Convert a type to a JSON-compatible type. + * + * Non-JSON values such as `Date` implement `.toJSON()`, + * so they can be transformed to a value assignable to `JSONObject` + * + * `JSON.stringify()` throws a `TypeError` when it encounters a `bigint` value, + * unless a custom `replacer` function or `.toJSON()` method is provided. + * + * This behavior can be controlled by the `TError` generic type parameter, + * which defaults to `bigint | ReadonlyArray`. + * You can set it to `never` to disable this check. + */ +export type JSONParsed> = T extends { + toJSON(): infer J; +} ? (() => J) extends (() => JSONPrimitive) ? J : (() => J) extends (() => { + toJSON(): unknown; +}) ? {} : JSONParsed : T extends JSONPrimitive ? T : T extends InvalidJSONValue ? never : T extends ReadonlyArray ? { [K in keyof T]: JSONParsed, TError>; } extends (infer A) ? A extends ReadonlyArray ? A : JSONParsed, TError>[] : never : T extends Set | Map | Record ? {} : T extends object ? T[keyof T] extends TError ? never : { [K in keyof OmitSymbolKeys as IsInvalid extends true ? never : K]: boolean extends IsInvalid ? JSONParsed | undefined : JSONParsed; } : T extends unknown ? T extends TError ? never : JSONValue : never; +/** + * Useful to flatten the type output to improve type hints shown in editors. And also to transform an interface into a type to aide with assignability. + * @copyright from sindresorhus/type-fest + */ +export type Simplify = { [KeyType in keyof T]: T[KeyType]; } & {}; +/** + * A simple extension of Simplify that will deeply traverse array elements. + */ +export type SimplifyDeepArray = T extends any[] ? { [E in keyof T]: SimplifyDeepArray; } : Simplify; +export type InterfaceToType = T extends Function ? T : { [K in keyof T]: InterfaceToType; }; +export type RequiredKeysOf = Exclude<{ [Key in keyof BaseType]: BaseType extends Record ? Key : never; }[keyof BaseType], undefined>; +export type HasRequiredKeys = RequiredKeysOf extends never ? false : true; +export type IsAny = boolean extends (T extends never ? true : false) ? true : false; +/** + * String literal types with auto-completion + * @see https://github.com/Microsoft/TypeScript/issues/29729 + */ +export type StringLiteralUnion = T | (string & Record); +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts new file mode 100644 index 000000000..3a494895e --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/validator/index.d.ts @@ -0,0 +1,3 @@ +import { InferInput } from "./utils.js"; +import { ValidationFunction, validator } from "./validator.js"; +export { type InferInput, type ValidationFunction, validator }; \ No newline at end of file diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts new file mode 100644 index 000000000..13159be3c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/validator/utils.d.ts @@ -0,0 +1,27 @@ +import { UnionToIntersection } from "../utils/types.js"; +import { FormValue, ParsedFormValue, ValidationTargets } from "../types.js"; +//#region src/validator/utils.d.ts +/** + * Checks if T is a literal union type (e.g., 'asc' | 'desc') + * that should be preserved in input types. + * Returns true for union literals, false for single literals or wide types. + */ +export type IsLiteralUnion = [Exclude] extends [Base] ? [Exclude] extends [UnionToIntersection>] ? false : true : false; +type IsOptionalUnion = [unknown] extends [T] ? false : undefined extends T ? true : false; +type SimplifyDeep = { [K in keyof T]: T[K]; } & {}; +type InferInputInner = SimplifyDeep<{ [K in keyof Output]: IsLiteralUnion extends true ? Output[K] : IsOptionalUnion extends true ? Output[K] : Target extends 'form' ? T | T[] : Target extends 'query' ? string | string[] : Target extends 'param' ? string : Target extends 'header' ? string : Target extends 'cookie' ? string : unknown; }>; +/** + * Utility type to infer input types for validation targets. + * Preserves literal union types (e.g., 'asc' | 'desc') while using + * the default ValidationTargets type for other values. + * + * @example + * ```ts + * // In @hono/zod-validator or similar: + * type Input = InferInput, 'query'> + * // { orderBy: 'asc' | 'desc', page: string | string[] } + * ``` + */ +export type InferInput = [Exclude] extends [never] ? {} : [Exclude] extends [object] ? undefined extends Output ? SimplifyDeep, Target, T>> | undefined : SimplifyDeep> : {}; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts b/tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts new file mode 100644 index 000000000..85e09a1a3 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/types/validator/validator.d.ts @@ -0,0 +1,18 @@ +import { Env, FormValue, MiddlewareHandler, TypedResponse, ValidationTargets } from "../types.js"; +import { Context } from "../context.js"; +import { InferInput } from "./utils.js"; +//#region src/validator/validator.d.ts +type ValidationTargetKeysWithBody = 'form' | 'json'; +type ValidationTargetByMethod = M extends 'get' | 'head' ? Exclude : keyof ValidationTargets; +export type ValidationFunction = (value: InputType, c: Context) => OutputType | TypedResponse | Promise | Promise; +export type ExtractValidationResponse = VF extends ((value: any, c: any) => infer R) ? R extends Promise ? PR extends TypedResponse ? TypedResponse : PR extends Response ? PR : PR extends undefined ? never : never : R extends TypedResponse ? TypedResponse : R extends Response ? R : R extends undefined ? never : never : never; +export declare const validator: , P2 extends string = P, VF extends (value: unknown extends InputType ? ValidationTargets[U] : InputType, c: Context) => any = (value: unknown extends InputType ? ValidationTargets[U] : InputType, c: Context) => any, V extends { + in: { [K in U]: K extends "json" ? unknown extends InputType ? ExtractValidatorOutput : InputType : InferInput, K, FormValue>; }; + out: { [K in U]: ExtractValidatorOutput; }; +} = { + in: { [K in U]: K extends "json" ? unknown extends InputType ? ExtractValidatorOutput : InputType : InferInput, K, FormValue>; }; + out: { [K in U]: ExtractValidatorOutput; }; +}, E extends Env = any>(target: U, validationFunc: VF) => MiddlewareHandler>; +export type ExtractValidatorOutput = VF extends ((value: any, c: any) => infer R) ? R extends Promise ? PR extends Response | TypedResponse ? never : PR : R extends Response | TypedResponse ? never : R : never; +//#endregion +export {}; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/body.js b/tests/fixtures/npm/node_modules/hono/dist/utils/body.js new file mode 100644 index 000000000..6fe8e3031 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/body.js @@ -0,0 +1,96 @@ +import { bufferToFormData } from "./buffer.js"; +//#region src/utils/body.ts +const MAX_NESTED_OBJECTS = 1e4; +const isRawRequest = (request) => "headers" in request; +const parseBody = async (request, options = Object.create(null)) => { + const { all = false, dot = false } = options; + const mediaType = (isRawRequest(request) ? request.headers : request.raw.headers).get("Content-Type")?.split(";")[0].trim().toLowerCase(); + if (mediaType === "multipart/form-data" || mediaType === "application/x-www-form-urlencoded") return parseFormData(request, { + all, + dot + }); + return {}; +}; +/** +* Parses form data from a request. +* +* @template T - The type of the parsed body data. +* @param {HonoRequest | Request} request - The request object containing form data. +* @param {ParseBodyOptions} options - Options for parsing the form data. +* @returns {Promise} The parsed body data. +*/ +async function parseFormData(request, options) { + if (!isRawRequest(request) && request.bodyCache.formData) return convertFormDataToBodyData(await request.bodyCache.formData, options); + const headers = isRawRequest(request) ? request.headers : request.raw.headers; + const arrayBuffer = await request.arrayBuffer(); + const formDataPromise = bufferToFormData(arrayBuffer, headers.get("Content-Type") || ""); + if (!isRawRequest(request)) request.bodyCache.formData = formDataPromise; + const formData = await formDataPromise; + if (formData) return convertFormDataToBodyData(formData, options); + return {}; +} +/** +* Converts form data to body data based on the provided options. +* +* @template T - The type of the parsed body data. +* @param {FormData} formData - The form data to convert. +* @param {ParseBodyOptions} options - Options for parsing the form data. +* @returns {T} The converted body data. +*/ +function convertFormDataToBodyData(formData, options) { + const form = Object.create(null); + const nestingState = { count: 0 }; + formData.forEach((value, key) => { + if (!(options.all || key.endsWith("[]"))) form[key] = value; + else handleParsingAllValues(form, key, value); + }); + if (options.dot) Object.entries(form).forEach(([key, value]) => { + if (key.includes(".")) { + handleParsingNestedValues(form, key, value, nestingState); + delete form[key]; + } + }); + return form; +} +/** +* Handles parsing all values for a given key, supporting multiple values as arrays. +* +* @param {BodyData} form - The form data object. +* @param {string} key - The key to parse. +* @param {FormDataEntryValue} value - The value to assign. +*/ +const handleParsingAllValues = (form, key, value) => { + if (form[key] !== void 0) { + if (Array.isArray(form[key])) form[key].push(value); + else form[key] = [form[key], value]; + } else if (!key.endsWith("[]")) form[key] = value; + else form[key] = [value]; +}; +/** +* Handles parsing nested values using dot notation keys. +* +* @param {BodyData} form - The form data object. +* @param {string} key - The dot notation key. +* @param {BodyDataValue} value - The value to assign. +*/ +const handleParsingNestedValues = (form, key, value, state) => { + if (/(?:^|\.)__proto__\./.test(key)) return; + let nestedForm = form; + const keys = key.split(".", 34); + if (keys.length > 33) throwNestingLimitExceeded(); + keys.forEach((key, index) => { + if (index === keys.length - 1) nestedForm[key] = value; + else { + if (!nestedForm[key] || typeof nestedForm[key] !== "object" || Array.isArray(nestedForm[key]) || nestedForm[key] instanceof File) { + if (state.count++ >= MAX_NESTED_OBJECTS) throwNestingLimitExceeded(); + nestedForm[key] = Object.create(null); + } + nestedForm = nestedForm[key]; + } + }); +}; +const throwNestingLimitExceeded = () => { + throw new Error("Nesting limit exceeded"); +}; +//#endregion +export { parseBody }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js b/tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js new file mode 100644 index 000000000..8c6506452 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/buffer.js @@ -0,0 +1,51 @@ +import { sha256 } from "./crypto.js"; +//#region src/utils/buffer.ts +/** +* @module +* Buffer utility. +*/ +const equal = (a, b) => { + if (a === b) return true; + if (a.byteLength !== b.byteLength) return false; + const va = new DataView(a); + const vb = new DataView(b); + let i = va.byteLength; + while (i--) if (va.getUint8(i) !== vb.getUint8(i)) return false; + return true; +}; +const constantTimeEqualString = (a, b) => { + const aLen = a.length; + const bLen = b.length; + const maxLen = Math.max(aLen, bLen); + let out = aLen ^ bLen; + for (let i = 0; i < maxLen; i++) { + const aChar = i < aLen ? a.charCodeAt(i) : 0; + const bChar = i < bLen ? b.charCodeAt(i) : 0; + out |= aChar ^ bChar; + } + return out === 0; +}; +const timingSafeEqualString = async (a, b, hashFunction) => { + if (!hashFunction) hashFunction = sha256; + const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)]); + if (sa == null || sb == null || typeof sa !== "string" || typeof sb !== "string") return false; + const hashEqual = constantTimeEqualString(sa, sb); + const originalEqual = constantTimeEqualString(a, b); + return hashEqual && originalEqual; +}; +const timingSafeEqual = async (a, b, hashFunction) => { + if (typeof a === "string" && typeof b === "string") return timingSafeEqualString(a, b, hashFunction); + if (!hashFunction) hashFunction = sha256; + const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)]); + if (!sa || !sb || typeof sa !== "string" || typeof sb !== "string") return false; + return timingSafeEqualString(sa, sb); +}; +const bufferToString = (buffer) => { + if (buffer instanceof ArrayBuffer) return new TextDecoder("utf-8").decode(buffer); + return buffer; +}; +const bufferToFormData = (arrayBuffer, contentType) => { + return new Response(arrayBuffer, { headers: { "Content-Type": contentType.replace(/^[^;]+/, (mediaType) => mediaType.toLowerCase()) } }).formData(); +}; +//#endregion +export { bufferToFormData, bufferToString, equal, timingSafeEqual }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/constants.js b/tests/fixtures/npm/node_modules/hono/dist/utils/constants.js new file mode 100644 index 000000000..8b0615b69 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/constants.js @@ -0,0 +1,7 @@ +//#region src/utils/constants.ts +/** +* Constant used to mark a composed handler. +*/ +const COMPOSED_HANDLER = "__COMPOSED_HANDLER"; +//#endregion +export { COMPOSED_HANDLER }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js b/tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js new file mode 100644 index 000000000..fd541131c --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/cookie.js @@ -0,0 +1,125 @@ +import { tryDecodeURIComponent } from "./url.js"; +//#region src/utils/cookie.ts +/** +* @module +* Cookie utility. +*/ +const algorithm = { + name: "HMAC", + hash: "SHA-256" +}; +const getCryptoKey = async (secret) => { + const secretBuf = typeof secret === "string" ? new TextEncoder().encode(secret) : secret; + return await crypto.subtle.importKey("raw", secretBuf, algorithm, false, ["sign", "verify"]); +}; +const makeSignature = async (value, secret) => { + const key = await getCryptoKey(secret); + const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value)); + return btoa(String.fromCharCode(...new Uint8Array(signature))); +}; +const verifySignature = async (base64Signature, value, secret) => { + try { + const signatureBinStr = atob(base64Signature); + const signature = new Uint8Array(signatureBinStr.length); + for (let i = 0, len = signatureBinStr.length; i < len; i++) signature[i] = signatureBinStr.charCodeAt(i); + return await crypto.subtle.verify(algorithm, secret, signature, new TextEncoder().encode(value)); + } catch { + return false; + } +}; +const validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/; +const relaxedCookieNameRegEx = /^[!#-:<>-[\]-~]+$/; +const validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/; +const trimCookieWhitespace = (value) => { + let start = 0; + let end = value.length; + while (start < end) { + const charCode = value.charCodeAt(start); + if (charCode !== 32 && charCode !== 9) break; + start++; + } + while (end > start) { + const charCode = value.charCodeAt(end - 1); + if (charCode !== 32 && charCode !== 9) break; + end--; + } + return start === 0 && end === value.length ? value : value.slice(start, end); +}; +const parse = (cookie, name) => { + if (name && cookie.indexOf(name) === -1) return {}; + const pairs = cookie.split(";"); + const parsedCookie = Object.create(null); + for (const pairStr of pairs) { + const valueStartPos = pairStr.indexOf("="); + if (valueStartPos === -1) continue; + const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos)); + if (name && name !== cookieName || !relaxedCookieNameRegEx.test(cookieName) || cookieName in parsedCookie) continue; + let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1)); + if (cookieValue.startsWith("\"") && cookieValue.endsWith("\"")) cookieValue = cookieValue.slice(1, -1); + if (validCookieValueRegEx.test(cookieValue)) { + parsedCookie[cookieName] = tryDecodeURIComponent(cookieValue); + if (name) break; + } + } + return parsedCookie; +}; +const parseSigned = async (cookie, secret, name) => { + const parsedCookie = Object.create(null); + const secretKey = await getCryptoKey(secret); + for (const [key, value] of Object.entries(parse(cookie, name))) { + const signatureStartPos = value.lastIndexOf("."); + if (signatureStartPos < 0) continue; + const signedValue = value.substring(0, signatureStartPos); + const signature = value.substring(signatureStartPos + 1); + if (signature.length !== 44 || !signature.endsWith("=")) continue; + parsedCookie[key] = await verifySignature(signature, signedValue, secretKey) ? signedValue : false; + } + return parsedCookie; +}; +const _serialize = (name, value, opt = {}) => { + if (!validCookieNameRegEx.test(name)) throw new Error("Invalid cookie name"); + let cookie = `${name}=${value}`; + if (name.startsWith("__Secure-") && !opt.secure) throw new Error("__Secure- Cookie must have Secure attributes"); + if (name.startsWith("__Host-")) { + if (!opt.secure) throw new Error("__Host- Cookie must have Secure attributes"); + if (opt.path !== "/") throw new Error("__Host- Cookie must have Path attributes with \"/\""); + if (opt.domain) throw new Error("__Host- Cookie must not have Domain attributes"); + } + for (const key of [ + "domain", + "path", + "sameSite", + "priority" + ]) if (opt[key] && /[;\r\n]/.test(opt[key])) throw new Error(`${key} must not contain ";", "\\r", or "\\n"`); + if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) { + if (opt.maxAge > 3456e4) throw new Error("Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration."); + cookie += `; Max-Age=${opt.maxAge | 0}`; + } + if (opt.domain && opt.prefix !== "host") cookie += `; Domain=${opt.domain}`; + if (opt.path) cookie += `; Path=${opt.path}`; + if (opt.expires) { + if (opt.expires.getTime() - Date.now() > 3456e7) throw new Error("Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future."); + cookie += `; Expires=${opt.expires.toUTCString()}`; + } + if (opt.httpOnly) cookie += "; HttpOnly"; + if (opt.secure) cookie += "; Secure"; + if (opt.sameSite) cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`; + if (opt.priority) cookie += `; Priority=${opt.priority.charAt(0).toUpperCase() + opt.priority.slice(1)}`; + if (opt.partitioned) { + if (!opt.secure) throw new Error("Partitioned Cookie must have Secure attributes"); + cookie += "; Partitioned"; + } + return cookie; +}; +const serialize = (name, value, opt) => { + value = encodeURIComponent(value); + return _serialize(name, value, opt); +}; +const serializeSigned = async (name, value, secret, opt = {}) => { + const signature = await makeSignature(value, secret); + value = `${value}.${signature}`; + value = encodeURIComponent(value); + return _serialize(name, value, opt); +}; +//#endregion +export { parse, parseSigned, serialize, serializeSigned }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js b/tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js new file mode 100644 index 000000000..548867c49 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/crypto.js @@ -0,0 +1,34 @@ +//#region src/utils/crypto.ts +const sha256 = async (data) => { + return await createHash(data, { + name: "SHA-256", + alias: "sha256" + }); +}; +const sha1 = async (data) => { + return await createHash(data, { + name: "SHA-1", + alias: "sha1" + }); +}; +const md5 = async (data) => { + return await createHash(data, { + name: "MD5", + alias: "md5" + }); +}; +const createHash = async (data, algorithm) => { + let sourceBuffer; + if (ArrayBuffer.isView(data) || data instanceof ArrayBuffer) sourceBuffer = data; + else { + if (typeof data === "object") data = JSON.stringify(data); + sourceBuffer = new TextEncoder().encode(String(data)); + } + if (crypto && crypto.subtle) { + const buffer = await crypto.subtle.digest({ name: algorithm.name }, sourceBuffer); + return Array.prototype.map.call(new Uint8Array(buffer), (x) => ("00" + x.toString(16)).slice(-2)).join(""); + } + return null; +}; +//#endregion +export { createHash, md5, sha1, sha256 }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/html.js b/tests/fixtures/npm/node_modules/hono/dist/utils/html.js new file mode 100644 index 000000000..7615ebdbd --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/html.js @@ -0,0 +1,103 @@ +//#region src/utils/html.ts +/** +* @module +* HTML utility. +*/ +const HtmlEscapedCallbackPhase = { + Stringify: 1, + BeforeStream: 2, + Stream: 3 +}; +const raw = (value, callbacks) => { + const escapedString = new String(value); + escapedString.isEscaped = true; + escapedString.callbacks = callbacks; + return escapedString; +}; +const escapeRe = /[&<>'"]/; +const stringBufferToString = async (buffer, callbacks) => { + let str = ""; + callbacks ||= []; + const resolvedBuffer = await Promise.all(buffer); + for (let i = resolvedBuffer.length - 1;; i--) { + str += resolvedBuffer[i]; + i--; + if (i < 0) break; + let r = resolvedBuffer[i]; + if (typeof r === "object") callbacks.push(...r.callbacks || []); + const isEscaped = r.isEscaped; + r = await (typeof r === "object" ? r.toString() : r); + if (typeof r === "object") callbacks.push(...r.callbacks || []); + if (r.isEscaped ?? isEscaped) str += r; + else { + const buf = [str]; + escapeToBuffer(r, buf); + str = buf[0]; + } + } + return raw(str, callbacks); +}; +const escapeToBuffer = (str, buffer) => { + const match = str.search(escapeRe); + if (match === -1) { + buffer[0] += str; + return; + } + let escape; + let index; + let lastIndex = 0; + for (index = match; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: + escape = """; + break; + case 39: + escape = "'"; + break; + case 38: + escape = "&"; + break; + case 60: + escape = "<"; + break; + case 62: + escape = ">"; + break; + default: continue; + } + buffer[0] += str.substring(lastIndex, index) + escape; + lastIndex = index + 1; + } + buffer[0] += str.substring(lastIndex, index); +}; +const resolveCallbackSync = (str) => { + const callbacks = str.callbacks; + if (!callbacks?.length) return str; + const buffer = [str]; + const context = {}; + callbacks.forEach((c) => c({ + phase: HtmlEscapedCallbackPhase.Stringify, + buffer, + context + })); + return buffer[0]; +}; +const resolveCallback = async (str, phase, preserveCallbacks, context, buffer) => { + if (typeof str === "object" && !(str instanceof String)) { + if (!(str instanceof Promise)) str = str.toString(); + if (str instanceof Promise) str = await str; + } + const callbacks = str.callbacks; + if (!callbacks?.length) return Promise.resolve(str); + if (buffer) buffer[0] += str; + else buffer = [str]; + const resStr = Promise.all(callbacks.map((c) => c({ + phase, + buffer, + context + }))).then((res) => Promise.all(res.filter(Boolean).map((str) => resolveCallback(str, phase, false, context, buffer))).then(() => buffer[0])); + if (preserveCallbacks) return raw(await resStr, callbacks); + else return resStr; +}; +//#endregion +export { HtmlEscapedCallbackPhase, escapeToBuffer, raw, resolveCallback, resolveCallbackSync, stringBufferToString }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/utils/url.js b/tests/fixtures/npm/node_modules/hono/dist/utils/url.js new file mode 100644 index 000000000..8433d4cb9 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/utils/url.js @@ -0,0 +1,201 @@ +//#region src/utils/url.ts +const splitPath = (path) => { + const paths = path.split("/"); + if (paths[0] === "") paths.shift(); + return paths; +}; +const splitRoutingPath = (routePath) => { + const { groups, path } = extractGroupsFromPath(routePath); + const paths = splitPath(path); + return replaceGroupMarks(paths, groups); +}; +const extractGroupsFromPath = (path) => { + const groups = []; + path = path.replace(/\{[^}]+\}/g, (match, index) => { + const mark = `@${index}`; + groups.push([mark, match]); + return mark; + }); + return { + groups, + path + }; +}; +const replaceGroupMarks = (paths, groups) => { + for (let i = groups.length - 1; i >= 0; i--) { + const [mark] = groups[i]; + for (let j = paths.length - 1; j >= 0; j--) if (paths[j].includes(mark)) { + paths[j] = paths[j].replace(mark, groups[i][1]); + break; + } + } + return paths; +}; +const patternCache = {}; +const getPattern = (label, next) => { + if (label === "*") return "*"; + const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/); + if (match) { + const cacheKey = `${label}#${next}`; + if (!patternCache[cacheKey]) { + if (match[2]) patternCache[cacheKey] = next && next[0] !== ":" && next[0] !== "*" ? [ + cacheKey, + match[1], + new RegExp(`^${match[2]}(?=/${next})`) + ] : [ + label, + match[1], + new RegExp(`^${match[2]}$`) + ]; + else patternCache[cacheKey] = [ + label, + match[1], + true + ]; + } + return patternCache[cacheKey]; + } + return null; +}; +const tryDecode = (str, decoder) => { + try { + return decoder(str); + } catch { + return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => { + try { + return decoder(match); + } catch { + return match; + } + }); + } +}; +/** +* Try to apply decodeURI() to given string. +* If it fails, skip invalid percent encoding or invalid UTF-8 sequences, and apply decodeURI() to the rest as much as possible. +* @param str The string to decode. +* @returns The decoded string that sometimes contains undecodable percent encoding. +* @example +* tryDecodeURI('Hello%20World') // 'Hello World' +* tryDecodeURI('Hello%20World/%A4%A2') // 'Hello World/%A4%A2' +*/ +const tryDecodeURI = (str) => tryDecode(str, decodeURI); +const getPath = (request) => { + const url = request.url; + const start = url.indexOf("/", url.indexOf(":") + 4); + let i = start; + for (; i < url.length; i++) { + const charCode = url.charCodeAt(i); + if (charCode === 37) { + const queryIndex = url.indexOf("?", i); + const hashIndex = url.indexOf("#", i); + const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex); + const path = url.slice(start, end); + return tryDecodeURI(path.includes("%25") ? path.replace(/%25/g, "%2525") : path); + } else if (charCode === 63 || charCode === 35) break; + } + return url.slice(start, i); +}; +/** +* @deprecated +* Use the `URL` API instead. +*/ +const getQueryStrings = (url) => { + const queryIndex = url.indexOf("?", 8); + if (queryIndex === -1) return ""; + const hashIndex = url.indexOf("#", 8); + return hashIndex === -1 ? url.slice(queryIndex) : queryIndex < hashIndex ? url.slice(queryIndex, hashIndex) : ""; +}; +const getPathNoStrict = (request) => { + const result = getPath(request); + return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result; +}; +/** +* Merge paths. +* @param {string[]} ...paths - The paths to merge. +* @returns {string} The merged path. +* @example +* mergePath('/api', '/users') // '/api/users' +* mergePath('/api/', '/users') // '/api/users' +* mergePath('/api', '/') // '/api' +* mergePath('/api/', '/') // '/api/' +*/ +const mergePath = (base, sub, ...rest) => { + if (rest.length) sub = mergePath(sub, ...rest); + return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`; +}; +const checkOptionalParameter = (path) => { + if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":")) return null; + const segments = path.split("/"); + const results = []; + let basePath = ""; + segments.forEach((segment) => { + if (segment !== "" && !/\:/.test(segment)) basePath += "/" + segment; + else if (/\:/.test(segment)) { + if (segment.charCodeAt(segment.length - 1) === 63) { + if (results.length === 0 && basePath === "") results.push("/"); + else results.push(basePath); + const optionalSegment = segment.slice(0, -1); + basePath += "/" + optionalSegment; + results.push(basePath); + } else basePath += "/" + segment; + } + }); + return results.filter((v, i, a) => a.indexOf(v) === i); +}; +const tryDecodeURIComponent = (str) => str.indexOf("%") !== -1 ? tryDecode(str, decodeURIComponent_) : str; +const _decodeURI = (value) => { + if (value.indexOf("+") !== -1) value = value.replace(/\+/g, " "); + return tryDecodeURIComponent(value); +}; +const _getQueryParam = (url, key, multiple) => { + const hashIndex = url.indexOf("#", 8); + if (hashIndex !== -1) url = url.slice(0, hashIndex); + let encoded; + if (!multiple && key && key.indexOf("%") === -1 && key.indexOf("+") === -1) { + let keyIndex = url.indexOf("?", 8); + if (keyIndex === -1) return; + if (!url.startsWith(key, keyIndex + 1)) keyIndex = url.indexOf(`&${key}`, keyIndex + 1); + while (keyIndex !== -1) { + const trailingKeyCode = url.charCodeAt(keyIndex + key.length + 1); + if (trailingKeyCode === 61) { + const valueIndex = keyIndex + key.length + 2; + const endIndex = url.indexOf("&", valueIndex); + return _decodeURI(url.slice(valueIndex, endIndex === -1 ? void 0 : endIndex)); + } else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) return ""; + keyIndex = url.indexOf(`&${key}`, keyIndex + 1); + } + encoded = /[%+]/.test(url); + if (!encoded) return; + } + const results = Object.create(null); + encoded ??= /[%+]/.test(url); + let keyIndex = url.indexOf("?", 8); + while (keyIndex !== -1) { + const nextKeyIndex = url.indexOf("&", keyIndex + 1); + let valueIndex = url.indexOf("=", keyIndex); + if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) valueIndex = -1; + let name = url.slice(keyIndex + 1, valueIndex === -1 ? nextKeyIndex === -1 ? void 0 : nextKeyIndex : valueIndex); + if (encoded) name = _decodeURI(name); + keyIndex = nextKeyIndex; + if (name === "") continue; + let value; + if (valueIndex === -1) value = ""; + else { + value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? void 0 : nextKeyIndex); + if (encoded) value = _decodeURI(value); + } + if (multiple) { + if (!(results[name] && Array.isArray(results[name]))) results[name] = []; + results[name].push(value); + } else results[name] ??= value; + } + return key ? results[key] : results; +}; +const getQueryParam = _getQueryParam; +const getQueryParams = (url, key) => { + return _getQueryParam(url, key, true); +}; +const decodeURIComponent_ = decodeURIComponent; +//#endregion +export { checkOptionalParameter, decodeURIComponent_, getPath, getPathNoStrict, getPattern, getQueryParam, getQueryParams, getQueryStrings, mergePath, splitPath, splitRoutingPath, tryDecode, tryDecodeURI, tryDecodeURIComponent }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/validator/index.js b/tests/fixtures/npm/node_modules/hono/dist/validator/index.js new file mode 100644 index 000000000..6e744aca6 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/validator/index.js @@ -0,0 +1,2 @@ +import { validator } from "./validator.js"; +export { validator }; diff --git a/tests/fixtures/npm/node_modules/hono/dist/validator/validator.js b/tests/fixtures/npm/node_modules/hono/dist/validator/validator.js new file mode 100644 index 000000000..1cc282071 --- /dev/null +++ b/tests/fixtures/npm/node_modules/hono/dist/validator/validator.js @@ -0,0 +1,64 @@ +import { HTTPException } from "../http-exception.js"; +import { bufferToFormData } from "../utils/buffer.js"; +import { getCookie } from "../helper/cookie/index.js"; +//#region src/validator/validator.ts +const jsonRegex = /^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/i; +const multipartRegex = /^multipart\/form-data(;\s?boundary=[a-zA-Z0-9'"()+_,\-./:=?]+)?$/i; +const urlencodedRegex = /^application\/x-www-form-urlencoded(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/i; +const validator = (target, validationFunc) => { + return async (c, next) => { + let value = {}; + const contentType = c.req.header("Content-Type"); + switch (target) { + case "json": + if (!contentType || !jsonRegex.test(contentType)) break; + try { + value = await c.req.json(); + } catch { + throw new HTTPException(400, { message: "Malformed JSON in request body" }); + } + break; + case "form": { + if (!contentType || !(multipartRegex.test(contentType) || urlencodedRegex.test(contentType))) break; + let formData; + if (c.req.bodyCache.formData) formData = await c.req.bodyCache.formData; + else try { + const arrayBuffer = await c.req.arrayBuffer(); + formData = await bufferToFormData(arrayBuffer, contentType); + c.req.bodyCache.formData = formData; + } catch (e) { + let message = "Malformed FormData request."; + message += e instanceof Error ? ` ${e.message}` : ` ${String(e)}`; + throw new HTTPException(400, { message }); + } + const form = Object.create(null); + formData.forEach((value, key) => { + if (key.endsWith("[]")) (form[key] ??= []).push(value); + else if (Array.isArray(form[key])) form[key].push(value); + else if (Object.hasOwn(form, key)) form[key] = [form[key], value]; + else form[key] = value; + }); + value = form; + break; + } + case "query": + value = Object.fromEntries(Object.entries(c.req.queries()).map(([k, v]) => { + return v.length === 1 ? [k, v[0]] : [k, v]; + })); + break; + case "param": + value = c.req.param(); + break; + case "header": + value = c.req.header(); + break; + case "cookie": value = getCookie(c); + } + const res = await validationFunc(value, c); + if (res instanceof Response) return res; + c.req.addValidatedData(target, res); + return await next(); + }; +}; +//#endregion +export { validator }; diff --git a/tests/fixtures/npm/node_modules/hono/package.json b/tests/fixtures/npm/node_modules/hono/package.json index 6264c824a..6c7dec6e9 100644 --- a/tests/fixtures/npm/node_modules/hono/package.json +++ b/tests/fixtures/npm/node_modules/hono/package.json @@ -1,10 +1,18 @@ { "name": "hono", "version": "4.13.13", - "description": "Test fixture: only the hono/ws helper that @hono/node-server's index imports.", + "description": "Test fixture: the hono core and validator (with their declarations) and the hono/ws helper that @hono/node-server's index imports.", "type": "module", "license": "MIT", "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/index.js" + }, + "./validator": { + "types": "./dist/types/validator/index.d.ts", + "import": "./dist/validator/index.js" + }, "./ws": { "import": "./dist/helper/websocket/index.js" } diff --git a/tests/fixtures/npm/node_modules/zod/LICENSE b/tests/fixtures/npm/node_modules/zod/LICENSE new file mode 100644 index 000000000..c06579627 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/npm/node_modules/zod/index.d.cts b/tests/fixtures/npm/node_modules/zod/index.d.cts new file mode 100644 index 000000000..b8f3021a8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/index.d.cts @@ -0,0 +1,3 @@ +import * as z from "./v4/classic/external.cjs"; +export * from "./v4/classic/external.cjs"; +export { z, z as default }; diff --git a/tests/fixtures/npm/node_modules/zod/index.js b/tests/fixtures/npm/node_modules/zod/index.js new file mode 100644 index 000000000..4102a4715 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/index.js @@ -0,0 +1,7 @@ +import * as z from "./v4/classic/external.js"; +export * from "./v4/classic/external.js"; +// Aliasing the binding, instead of `export default z`, keeps the default export a +// re-export of `z` rather than a fresh namespace value. Rollup and Webpack can then +// shake `import z from "zod"` as they already shake `import { z } from "zod"` — without +// this they pull in every locale. See #6050. +export { z, z as default }; diff --git a/tests/fixtures/npm/node_modules/zod/package.json b/tests/fixtures/npm/node_modules/zod/package.json new file mode 100644 index 000000000..58b9916bd --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/package.json @@ -0,0 +1,23 @@ +{ + "name": "zod", + "version": "4.6.7", + "description": "Test fixture: zod 4's classic API, its core and locales, and the declarations @hono/zod-validator reads (the files the npm cases load).", + "type": "module", + "license": "MIT", + "types": "./index.d.cts", + "exports": { + ".": { + "types": "./index.d.cts", + "import": "./index.js" + }, + "./v3": { + "types": "./v3/index.d.cts" + }, + "./v4": { + "types": "./v4/index.d.cts" + }, + "./v4/core": { + "types": "./v4/core/index.d.cts" + } + } +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts b/tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts new file mode 100644 index 000000000..384b36487 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/ZodError.d.cts @@ -0,0 +1,164 @@ +import type { Primitive } from "./helpers/typeAliases.cjs"; +import { util, type ZodParsedType } from "./helpers/util.cjs"; +import type { TypeOf, ZodType } from "./index.cjs"; +type allKeys = T extends any ? keyof T : never; +export type inferFlattenedErrors, U = string> = typeToFlattenedError, U>; +export type typeToFlattenedError = { + formErrors: U[]; + fieldErrors: { + [P in allKeys]?: U[]; + }; +}; +export declare const ZodIssueCode: { + invalid_type: "invalid_type"; + too_big: "too_big"; + too_small: "too_small"; + not_multiple_of: "not_multiple_of"; + unrecognized_keys: "unrecognized_keys"; + invalid_union: "invalid_union"; + custom: "custom"; + invalid_literal: "invalid_literal"; + invalid_union_discriminator: "invalid_union_discriminator"; + invalid_enum_value: "invalid_enum_value"; + invalid_arguments: "invalid_arguments"; + invalid_return_type: "invalid_return_type"; + invalid_date: "invalid_date"; + invalid_string: "invalid_string"; + invalid_intersection_types: "invalid_intersection_types"; + not_finite: "not_finite"; +}; +export type ZodIssueCode = keyof typeof ZodIssueCode; +export type ZodIssueBase = { + path: (string | number)[]; + message?: string | undefined; +}; +export interface ZodInvalidTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_type; + expected: ZodParsedType; + received: ZodParsedType; +} +export interface ZodInvalidLiteralIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_literal; + expected: unknown; + received: unknown; +} +export interface ZodUnrecognizedKeysIssue extends ZodIssueBase { + code: typeof ZodIssueCode.unrecognized_keys; + keys: string[]; +} +export interface ZodInvalidUnionIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union; + unionErrors: ZodError[]; +} +export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_union_discriminator; + options: Primitive[]; +} +export interface ZodInvalidEnumValueIssue extends ZodIssueBase { + received: string | number; + code: typeof ZodIssueCode.invalid_enum_value; + options: (string | number)[]; +} +export interface ZodInvalidArgumentsIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_arguments; + argumentsError: ZodError; +} +export interface ZodInvalidReturnTypeIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_return_type; + returnTypeError: ZodError; +} +export interface ZodInvalidDateIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_date; +} +export type StringValidation = "email" | "url" | "emoji" | "uuid" | "nanoid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "date" | "time" | "duration" | "ip" | "cidr" | "base64" | "jwt" | "base64url" | { + includes: string; + position?: number | undefined; +} | { + startsWith: string; +} | { + endsWith: string; +}; +export interface ZodInvalidStringIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_string; + validation: StringValidation; +} +export interface ZodTooSmallIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_small; + minimum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodTooBigIssue extends ZodIssueBase { + code: typeof ZodIssueCode.too_big; + maximum: number | bigint; + inclusive: boolean; + exact?: boolean; + type: "array" | "string" | "number" | "set" | "date" | "bigint"; +} +export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase { + code: typeof ZodIssueCode.invalid_intersection_types; +} +export interface ZodNotMultipleOfIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_multiple_of; + multipleOf: number | bigint; +} +export interface ZodNotFiniteIssue extends ZodIssueBase { + code: typeof ZodIssueCode.not_finite; +} +export interface ZodCustomIssue extends ZodIssueBase { + code: typeof ZodIssueCode.custom; + params?: { + [k: string]: any; + }; +} +export type DenormalizedError = { + [k: string]: DenormalizedError | string[]; +}; +export type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue; +export type ZodIssue = ZodIssueOptionalMessage & { + fatal?: boolean | undefined; + message: string; +}; +export declare const quotelessJson: (obj: any) => string; +type recursiveZodFormattedError = T extends [any, ...any[]] ? { + [K in keyof T]?: ZodFormattedError; +} : T extends any[] ? { + [k: number]: ZodFormattedError; +} : T extends object ? { + [K in keyof T]?: ZodFormattedError; +} : unknown; +export type ZodFormattedError = { + _errors: U[]; +} & recursiveZodFormattedError>; +export type inferFormattedError, U = string> = ZodFormattedError, U>; +export declare class ZodError extends Error { + issues: ZodIssue[]; + get errors(): ZodIssue[]; + constructor(issues: ZodIssue[]); + format(): ZodFormattedError; + format(mapper: (issue: ZodIssue) => U): ZodFormattedError; + static create: (issues: ZodIssue[]) => ZodError; + static assert(value: unknown): asserts value is ZodError; + toString(): string; + get message(): string; + get isEmpty(): boolean; + addIssue: (sub: ZodIssue) => void; + addIssues: (subs?: ZodIssue[]) => void; + flatten(): typeToFlattenedError; + flatten(mapper?: (issue: ZodIssue) => U): typeToFlattenedError; + get formErrors(): typeToFlattenedError; +} +type stripPath = T extends any ? util.OmitKeys : never; +export type IssueData = stripPath & { + path?: (string | number)[]; + fatal?: boolean | undefined; +}; +export type ErrorMapCtx = { + defaultError: string; + data: any; +}; +export type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => { + message: string; +}; +export {}; diff --git a/tests/fixtures/npm/node_modules/zod/v3/errors.d.cts b/tests/fixtures/npm/node_modules/zod/v3/errors.d.cts new file mode 100644 index 000000000..205f3a096 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/errors.d.cts @@ -0,0 +1,5 @@ +import type { ZodErrorMap } from "./ZodError.cjs"; +import defaultErrorMap from "./locales/en.cjs"; +export { defaultErrorMap }; +export declare function setErrorMap(map: ZodErrorMap): void; +export declare function getErrorMap(): ZodErrorMap; diff --git a/tests/fixtures/npm/node_modules/zod/v3/external.d.cts b/tests/fixtures/npm/node_modules/zod/v3/external.d.cts new file mode 100644 index 000000000..ced9c1a85 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/external.d.cts @@ -0,0 +1,6 @@ +export * from "./errors.cjs"; +export * from "./helpers/parseUtil.cjs"; +export * from "./helpers/typeAliases.cjs"; +export * from "./helpers/util.cjs"; +export * from "./types.cjs"; +export * from "./ZodError.cjs"; diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts new file mode 100644 index 000000000..fad28a711 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/enumUtil.d.cts @@ -0,0 +1,8 @@ +export declare namespace enumUtil { + type UnionToIntersectionFn = (T extends unknown ? (k: () => T) => void : never) extends (k: infer Intersection) => void ? Intersection : never; + type GetUnionLast = UnionToIntersectionFn extends () => infer Last ? Last : never; + type UnionToTuple = [T] extends [never] ? Tuple : UnionToTuple>, [GetUnionLast, ...Tuple]>; + type CastToStringTuple = T extends [string, ...string[]] ? T : never; + export type UnionToTupleString = CastToStringTuple>; + export {}; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts new file mode 100644 index 000000000..bc9b033ff --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/errorUtil.d.cts @@ -0,0 +1,9 @@ +export declare namespace errorUtil { + type ErrMessage = string | { + message?: string | undefined; + }; + const errToObj: (message?: ErrMessage) => { + message?: string | undefined; + }; + const toString: (message?: ErrMessage) => string | undefined; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts new file mode 100644 index 000000000..f34a63d7f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/parseUtil.d.cts @@ -0,0 +1,78 @@ +import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.cjs"; +import type { ZodParsedType } from "./util.cjs"; +export declare const makeIssue: (params: { + data: any; + path: (string | number)[]; + errorMaps: ZodErrorMap[]; + issueData: IssueData; +}) => ZodIssue; +export type ParseParams = { + path: (string | number)[]; + errorMap: ZodErrorMap; + async: boolean; +}; +export type ParsePathComponent = string | number; +export type ParsePath = ParsePathComponent[]; +export declare const EMPTY_PATH: ParsePath; +export interface ParseContext { + readonly common: { + readonly issues: ZodIssue[]; + readonly contextualErrorMap?: ZodErrorMap | undefined; + readonly async: boolean; + }; + readonly path: ParsePath; + readonly schemaErrorMap?: ZodErrorMap | undefined; + readonly parent: ParseContext | null; + readonly data: any; + readonly parsedType: ZodParsedType; +} +export type ParseInput = { + data: any; + path: (string | number)[]; + parent: ParseContext; +}; +export declare function addIssueToContext(ctx: ParseContext, issueData: IssueData): void; +export type ObjectPair = { + key: SyncParseReturnType; + value: SyncParseReturnType; +}; +export declare class ParseStatus { + value: "aborted" | "dirty" | "valid"; + dirty(): void; + abort(): void; + static mergeArray(status: ParseStatus, results: SyncParseReturnType[]): SyncParseReturnType; + static mergeObjectAsync(status: ParseStatus, pairs: { + key: ParseReturnType; + value: ParseReturnType; + }[]): Promise>; + static mergeObjectSync(status: ParseStatus, pairs: { + key: SyncParseReturnType; + value: SyncParseReturnType; + alwaysSet?: boolean; + }[]): SyncParseReturnType; +} +export interface ParseResult { + status: "aborted" | "dirty" | "valid"; + data: any; +} +export type INVALID = { + status: "aborted"; +}; +export declare const INVALID: INVALID; +export type DIRTY = { + status: "dirty"; + value: T; +}; +export declare const DIRTY: (value: T) => DIRTY; +export type OK = { + status: "valid"; + value: T; +}; +export declare const OK: (value: T) => OK; +export type SyncParseReturnType = OK | DIRTY | INVALID; +export type AsyncParseReturnType = Promise>; +export type ParseReturnType = SyncParseReturnType | AsyncParseReturnType; +export declare const isAborted: (x: ParseReturnType) => x is INVALID; +export declare const isDirty: (x: ParseReturnType) => x is OK | DIRTY; +export declare const isValid: (x: ParseReturnType) => x is OK; +export declare const isAsync: (x: ParseReturnType) => x is AsyncParseReturnType; diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts new file mode 100644 index 000000000..3f9e46a2a --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/partialUtil.d.cts @@ -0,0 +1,8 @@ +import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.cjs"; +export declare namespace partialUtil { + type DeepPartial = T extends ZodObject ? ZodObject<{ + [k in keyof T["shape"]]: ZodOptional>; + }, T["_def"]["unknownKeys"], T["_def"]["catchall"]> : T extends ZodArray ? ZodArray, Card> : T extends ZodOptional ? ZodOptional> : T extends ZodNullable ? ZodNullable> : T extends ZodTuple ? { + [k in keyof Items]: Items[k] extends ZodTypeAny ? DeepPartial : never; + } extends infer PI ? PI extends ZodTupleItems ? ZodTuple : never : never : T; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts new file mode 100644 index 000000000..32df022ca --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/typeAliases.d.cts @@ -0,0 +1,2 @@ +export type Primitive = string | number | symbol | bigint | boolean | null | undefined; +export type Scalars = Primitive | Primitive[]; diff --git a/tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts b/tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts new file mode 100644 index 000000000..eeaaeee1c --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/helpers/util.d.cts @@ -0,0 +1,85 @@ +export declare namespace util { + type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; + export type isAny = 0 extends 1 & T ? true : false; + export const assertEqual: (_: AssertEqual) => void; + export function assertIs(_arg: T): void; + export function assertNever(_x: never): never; + export type Omit = Pick>; + export type OmitKeys = Pick>; + export type MakePartial = Omit & Partial>; + export type Exactly = T & Record, never>; + export type InexactPartial = { + [k in keyof T]?: T[k] | undefined; + }; + export const arrayToEnum: (items: U) => { [k in U[number]]: k; }; + export const getValidEnumValues: (obj: any) => any[]; + export const objectValues: (obj: any) => any[]; + export const objectKeys: ObjectConstructor["keys"]; + export const find: (arr: T[], checker: (arg: T) => any) => T | undefined; + export type identity = objectUtil.identity; + export type flatten = objectUtil.flatten; + export type noUndefined = T extends undefined ? never : T; + export const isInteger: NumberConstructor["isInteger"]; + export function joinValues(array: T, separator?: string): string; + export const jsonStringifyReplacer: (_: string, value: any) => any; + export {}; +} +export declare namespace objectUtil { + export type MergeShapes = keyof U & keyof V extends never ? U & V : { + [k in Exclude]: U[k]; + } & V; + type optionalKeys = { + [k in keyof T]: undefined extends T[k] ? k : never; + }[keyof T]; + type requiredKeys = { + [k in keyof T]: undefined extends T[k] ? never : k; + }[keyof T]; + export type addQuestionMarks = { + [K in requiredKeys]: T[K]; + } & { + [K in optionalKeys]?: T[K]; + } & { + [k in keyof T]?: unknown; + }; + export type identity = T; + export type flatten = identity<{ + [k in keyof T]: T[k]; + }>; + export type noNeverKeys = { + [k in keyof T]: [T[k]] extends [never] ? never : k; + }[keyof T]; + export type noNever = identity<{ + [k in noNeverKeys]: k extends keyof T ? T[k] : never; + }>; + export const mergeShapes: (first: U, second: T) => T & U; + export type extendShape = keyof A & keyof B extends never ? A & B : { + [K in keyof A as K extends keyof B ? never : K]: A[K]; + } & { + [K in keyof B]: B[K]; + }; + export {}; +} +export declare const ZodParsedType: { + string: "string"; + nan: "nan"; + number: "number"; + integer: "integer"; + float: "float"; + boolean: "boolean"; + date: "date"; + bigint: "bigint"; + symbol: "symbol"; + function: "function"; + undefined: "undefined"; + null: "null"; + array: "array"; + object: "object"; + unknown: "unknown"; + promise: "promise"; + void: "void"; + never: "never"; + map: "map"; + set: "set"; +}; +export type ZodParsedType = keyof typeof ZodParsedType; +export declare const getParsedType: (data: any) => ZodParsedType; diff --git a/tests/fixtures/npm/node_modules/zod/v3/index.d.cts b/tests/fixtures/npm/node_modules/zod/v3/index.d.cts new file mode 100644 index 000000000..b0662babe --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/index.d.cts @@ -0,0 +1,4 @@ +import * as z from "./external.cjs"; +export * from "./external.cjs"; +export { z }; +export default z; diff --git a/tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts b/tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts new file mode 100644 index 000000000..e146a11c5 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/locales/en.d.cts @@ -0,0 +1,3 @@ +import { type ZodErrorMap } from "../ZodError.cjs"; +declare const errorMap: ZodErrorMap; +export = errorMap; diff --git a/tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts b/tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts new file mode 100644 index 000000000..7ddff3b7f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/standard-schema.d.cts @@ -0,0 +1,102 @@ +/** + * The Standard Schema interface. + */ +export type StandardSchemaV1 = { + /** + * The Standard Schema properties. + */ + readonly "~standard": StandardSchemaV1.Props; +}; +export declare namespace StandardSchemaV1 { + /** + * The Standard Schema properties interface. + */ + export interface Props { + /** + * The version number of the standard. + */ + readonly version: 1; + /** + * The vendor name of the schema library. + */ + readonly vendor: string; + /** + * Validates unknown input values. + */ + readonly validate: (value: unknown) => Result | Promise>; + /** + * Inferred types associated with the schema. + */ + readonly types?: Types | undefined; + } + /** + * The result interface of the validate function. + */ + export type Result = SuccessResult | FailureResult; + /** + * The result interface if validation succeeds. + */ + export interface SuccessResult { + /** + * The typed output value. + */ + readonly value: Output; + /** + * The non-existent issues. + */ + readonly issues?: undefined; + } + /** + * The result interface if validation fails. + */ + export interface FailureResult { + /** + * The issues of failed validation. + */ + readonly issues: ReadonlyArray; + } + /** + * The issue interface of the failure output. + */ + export interface Issue { + /** + * The error message of the issue. + */ + readonly message: string; + /** + * The path of the issue, if any. + */ + readonly path?: ReadonlyArray | undefined; + } + /** + * The path segment interface of the issue. + */ + export interface PathSegment { + /** + * The key representing a path segment. + */ + readonly key: PropertyKey; + } + /** + * The Standard Schema types interface. + */ + export interface Types { + /** + * The input type of the schema. + */ + readonly input: Input; + /** + * The output type of the schema. + */ + readonly output: Output; + } + /** + * Infers the input type of a Standard Schema. + */ + export type InferInput = NonNullable["input"]; + /** + * Infers the output type of a Standard Schema. + */ + export type InferOutput = NonNullable["output"]; + export {}; +} diff --git a/tests/fixtures/npm/node_modules/zod/v3/types.d.cts b/tests/fixtures/npm/node_modules/zod/v3/types.d.cts new file mode 100644 index 000000000..f1013a74f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v3/types.d.cts @@ -0,0 +1,1034 @@ +import { type IssueData, type StringValidation, type ZodCustomIssue, ZodError, type ZodErrorMap } from "./ZodError.cjs"; +import type { enumUtil } from "./helpers/enumUtil.cjs"; +import { errorUtil } from "./helpers/errorUtil.cjs"; +import { type AsyncParseReturnType, INVALID, type ParseContext, type ParseInput, type ParseParams, type ParseReturnType, ParseStatus, type SyncParseReturnType } from "./helpers/parseUtil.cjs"; +import type { partialUtil } from "./helpers/partialUtil.cjs"; +import type { Primitive } from "./helpers/typeAliases.cjs"; +import { util, type objectUtil } from "./helpers/util.cjs"; +import type { StandardSchemaV1 } from "./standard-schema.cjs"; +export interface RefinementCtx { + addIssue: (arg: IssueData) => void; + path: (string | number)[]; +} +export type ZodRawShape = { + [k: string]: ZodTypeAny; +}; +export type ZodTypeAny = ZodType; +export type TypeOf> = T["_output"]; +export type input> = T["_input"]; +export type output> = T["_output"]; +export type { TypeOf as infer }; +export type CustomErrorParams = Partial>; +export interface ZodTypeDef { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +} +export type RawCreateParams = { + errorMap?: ZodErrorMap | undefined; + invalid_type_error?: string | undefined; + required_error?: string | undefined; + message?: string | undefined; + description?: string | undefined; +} | undefined; +export type ProcessedCreateParams = { + errorMap?: ZodErrorMap | undefined; + description?: string | undefined; +}; +export type SafeParseSuccess = { + success: true; + data: Output; + error?: never; +}; +export type SafeParseError = { + success: false; + error: ZodError; + data?: never; +}; +export type SafeParseReturnType = SafeParseSuccess | SafeParseError; +export declare abstract class ZodType { + readonly _type: Output; + readonly _output: Output; + readonly _input: Input; + readonly _def: Def; + get description(): string | undefined; + "~standard": StandardSchemaV1.Props; + abstract _parse(input: ParseInput): ParseReturnType; + _getType(input: ParseInput): string; + _getOrReturnCtx(input: ParseInput, ctx?: ParseContext | undefined): ParseContext; + _processInputParams(input: ParseInput): { + status: ParseStatus; + ctx: ParseContext; + }; + _parseSync(input: ParseInput): SyncParseReturnType; + _parseAsync(input: ParseInput): AsyncParseReturnType; + parse(data: unknown, params?: util.InexactPartial): Output; + safeParse(data: unknown, params?: util.InexactPartial): SafeParseReturnType; + "~validate"(data: unknown): StandardSchemaV1.Result | Promise>; + parseAsync(data: unknown, params?: util.InexactPartial): Promise; + safeParseAsync(data: unknown, params?: util.InexactPartial): Promise>; + /** Alias of safeParseAsync */ + spa: (data: unknown, params?: util.InexactPartial) => Promise>; + refine(check: (arg: Output) => arg is RefinedOutput, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refine(check: (arg: Output) => unknown | Promise, message?: string | CustomErrorParams | ((arg: Output) => CustomErrorParams)): ZodEffects; + refinement(check: (arg: Output) => arg is RefinedOutput, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + refinement(check: (arg: Output) => boolean, refinementData: IssueData | ((arg: Output, ctx: RefinementCtx) => IssueData)): ZodEffects; + _refinement(refinement: RefinementEffect["refinement"]): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => arg is RefinedOutput): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => void): ZodEffects; + superRefine(refinement: (arg: Output, ctx: RefinementCtx) => Promise): ZodEffects; + constructor(def: Def); + optional(): ZodOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + array(): ZodArray; + promise(): ZodPromise; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: Output, ctx: RefinementCtx) => NewOut | Promise): ZodEffects; + default(def: util.noUndefined): ZodDefault; + default(def: () => util.noUndefined): ZodDefault; + brand(brand?: B): ZodBranded; + catch(def: Output): ZodCatch; + catch(def: (ctx: { + error: ZodError; + input: Input; + }) => Output): ZodCatch; + describe(description: string): this; + pipe(target: T): ZodPipeline; + readonly(): ZodReadonly; + isOptional(): boolean; + isNullable(): boolean; +} +export type IpVersion = "v4" | "v6"; +export type ZodStringCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +} | { + kind: "length"; + value: number; + message?: string | undefined; +} | { + kind: "email"; + message?: string | undefined; +} | { + kind: "url"; + message?: string | undefined; +} | { + kind: "emoji"; + message?: string | undefined; +} | { + kind: "uuid"; + message?: string | undefined; +} | { + kind: "nanoid"; + message?: string | undefined; +} | { + kind: "cuid"; + message?: string | undefined; +} | { + kind: "includes"; + value: string; + position?: number | undefined; + message?: string | undefined; +} | { + kind: "cuid2"; + message?: string | undefined; +} | { + kind: "ulid"; + message?: string | undefined; +} | { + kind: "startsWith"; + value: string; + message?: string | undefined; +} | { + kind: "endsWith"; + value: string; + message?: string | undefined; +} | { + kind: "regex"; + regex: RegExp; + message?: string | undefined; +} | { + kind: "trim"; + message?: string | undefined; +} | { + kind: "toLowerCase"; + message?: string | undefined; +} | { + kind: "toUpperCase"; + message?: string | undefined; +} | { + kind: "jwt"; + alg?: string; + message?: string | undefined; +} | { + kind: "datetime"; + offset: boolean; + local: boolean; + precision: number | null; + message?: string | undefined; +} | { + kind: "date"; + message?: string | undefined; +} | { + kind: "time"; + precision: number | null; + message?: string | undefined; +} | { + kind: "duration"; + message?: string | undefined; +} | { + kind: "ip"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "cidr"; + version?: IpVersion | undefined; + message?: string | undefined; +} | { + kind: "base64"; + message?: string | undefined; +} | { + kind: "base64url"; + message?: string | undefined; +}; +export interface ZodStringDef extends ZodTypeDef { + checks: ZodStringCheck[]; + typeName: ZodFirstPartyTypeKind.ZodString; + coerce: boolean; +} +export declare function datetimeRegex(args: { + precision?: number | null; + offset?: boolean; + local?: boolean; +}): RegExp; +export declare class ZodString extends ZodType { + _parse(input: ParseInput): ParseReturnType; + protected _regex(regex: RegExp, validation: StringValidation, message?: errorUtil.ErrMessage): ZodEffects; + _addCheck(check: ZodStringCheck): ZodString; + email(message?: errorUtil.ErrMessage): ZodString; + url(message?: errorUtil.ErrMessage): ZodString; + emoji(message?: errorUtil.ErrMessage): ZodString; + uuid(message?: errorUtil.ErrMessage): ZodString; + nanoid(message?: errorUtil.ErrMessage): ZodString; + cuid(message?: errorUtil.ErrMessage): ZodString; + cuid2(message?: errorUtil.ErrMessage): ZodString; + ulid(message?: errorUtil.ErrMessage): ZodString; + base64(message?: errorUtil.ErrMessage): ZodString; + base64url(message?: errorUtil.ErrMessage): ZodString; + jwt(options?: { + alg?: string; + message?: string | undefined; + }): ZodString; + ip(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + cidr(options?: string | { + version?: IpVersion; + message?: string | undefined; + }): ZodString; + datetime(options?: string | { + message?: string | undefined; + precision?: number | null; + offset?: boolean; + local?: boolean; + }): ZodString; + date(message?: string): ZodString; + time(options?: string | { + message?: string | undefined; + precision?: number | null; + }): ZodString; + duration(message?: errorUtil.ErrMessage): ZodString; + regex(regex: RegExp, message?: errorUtil.ErrMessage): ZodString; + includes(value: string, options?: { + message?: string; + position?: number; + }): ZodString; + startsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + endsWith(value: string, message?: errorUtil.ErrMessage): ZodString; + min(minLength: number, message?: errorUtil.ErrMessage): ZodString; + max(maxLength: number, message?: errorUtil.ErrMessage): ZodString; + length(len: number, message?: errorUtil.ErrMessage): ZodString; + /** + * Equivalent to `.min(1)` + */ + nonempty(message?: errorUtil.ErrMessage): ZodString; + trim(): ZodString; + toLowerCase(): ZodString; + toUpperCase(): ZodString; + get isDatetime(): boolean; + get isDate(): boolean; + get isTime(): boolean; + get isDuration(): boolean; + get isEmail(): boolean; + get isURL(): boolean; + get isEmoji(): boolean; + get isUUID(): boolean; + get isNANOID(): boolean; + get isCUID(): boolean; + get isCUID2(): boolean; + get isULID(): boolean; + get isIP(): boolean; + get isCIDR(): boolean; + get isBase64(): boolean; + get isBase64url(): boolean; + get minLength(): number | null; + get maxLength(): number | null; + static create: (params?: RawCreateParams & { + coerce?: true; + }) => ZodString; +} +export type ZodNumberCheck = { + kind: "min"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: number; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "int"; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: number; + message?: string | undefined; +} | { + kind: "finite"; + message?: string | undefined; +}; +export interface ZodNumberDef extends ZodTypeDef { + checks: ZodNumberCheck[]; + typeName: ZodFirstPartyTypeKind.ZodNumber; + coerce: boolean; +} +export declare class ZodNumber extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodNumber; + gte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + min: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + gt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + lte(value: number, message?: errorUtil.ErrMessage): ZodNumber; + max: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + lt(value: number, message?: errorUtil.ErrMessage): ZodNumber; + protected setLimit(kind: "min" | "max", value: number, inclusive: boolean, message?: string): ZodNumber; + _addCheck(check: ZodNumberCheck): ZodNumber; + int(message?: errorUtil.ErrMessage): ZodNumber; + positive(message?: errorUtil.ErrMessage): ZodNumber; + negative(message?: errorUtil.ErrMessage): ZodNumber; + nonpositive(message?: errorUtil.ErrMessage): ZodNumber; + nonnegative(message?: errorUtil.ErrMessage): ZodNumber; + multipleOf(value: number, message?: errorUtil.ErrMessage): ZodNumber; + step: (value: number, message?: errorUtil.ErrMessage) => ZodNumber; + finite(message?: errorUtil.ErrMessage): ZodNumber; + safe(message?: errorUtil.ErrMessage): ZodNumber; + get minValue(): number | null; + get maxValue(): number | null; + get isInt(): boolean; + get isFinite(): boolean; +} +export type ZodBigIntCheck = { + kind: "min"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "max"; + value: bigint; + inclusive: boolean; + message?: string | undefined; +} | { + kind: "multipleOf"; + value: bigint; + message?: string | undefined; +}; +export interface ZodBigIntDef extends ZodTypeDef { + checks: ZodBigIntCheck[]; + typeName: ZodFirstPartyTypeKind.ZodBigInt; + coerce: boolean; +} +export declare class ZodBigInt extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _getInvalidInput(input: ParseInput): INVALID; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBigInt; + gte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + min: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + gt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + lte(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + max: (value: bigint, message?: errorUtil.ErrMessage) => ZodBigInt; + lt(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + protected setLimit(kind: "min" | "max", value: bigint, inclusive: boolean, message?: string): ZodBigInt; + _addCheck(check: ZodBigIntCheck): ZodBigInt; + positive(message?: errorUtil.ErrMessage): ZodBigInt; + negative(message?: errorUtil.ErrMessage): ZodBigInt; + nonpositive(message?: errorUtil.ErrMessage): ZodBigInt; + nonnegative(message?: errorUtil.ErrMessage): ZodBigInt; + multipleOf(value: bigint, message?: errorUtil.ErrMessage): ZodBigInt; + get minValue(): bigint | null; + get maxValue(): bigint | null; +} +export interface ZodBooleanDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodBoolean; + coerce: boolean; +} +export declare class ZodBoolean extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodBoolean; +} +export type ZodDateCheck = { + kind: "min"; + value: number; + message?: string | undefined; +} | { + kind: "max"; + value: number; + message?: string | undefined; +}; +export interface ZodDateDef extends ZodTypeDef { + checks: ZodDateCheck[]; + coerce: boolean; + typeName: ZodFirstPartyTypeKind.ZodDate; +} +export declare class ZodDate extends ZodType { + _parse(input: ParseInput): ParseReturnType; + _addCheck(check: ZodDateCheck): ZodDate; + min(minDate: Date, message?: errorUtil.ErrMessage): ZodDate; + max(maxDate: Date, message?: errorUtil.ErrMessage): ZodDate; + get minDate(): Date | null; + get maxDate(): Date | null; + static create: (params?: RawCreateParams & { + coerce?: boolean; + }) => ZodDate; +} +export interface ZodSymbolDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodSymbol; +} +export declare class ZodSymbol extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodSymbol; +} +export interface ZodUndefinedDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUndefined; +} +export declare class ZodUndefined extends ZodType { + _parse(input: ParseInput): ParseReturnType; + params?: RawCreateParams; + static create: (params?: RawCreateParams) => ZodUndefined; +} +export interface ZodNullDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNull; +} +export declare class ZodNull extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNull; +} +export interface ZodAnyDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodAny; +} +export declare class ZodAny extends ZodType { + _any: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodAny; +} +export interface ZodUnknownDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodUnknown; +} +export declare class ZodUnknown extends ZodType { + _unknown: true; + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodUnknown; +} +export interface ZodNeverDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNever; +} +export declare class ZodNever extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNever; +} +export interface ZodVoidDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodVoid; +} +export declare class ZodVoid extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodVoid; +} +export interface ZodArrayDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodArray; + exactLength: { + value: number; + message?: string | undefined; + } | null; + minLength: { + value: number; + message?: string | undefined; + } | null; + maxLength: { + value: number; + message?: string | undefined; + } | null; +} +export type ArrayCardinality = "many" | "atleastone"; +export type arrayOutputType = Cardinality extends "atleastone" ? [T["_output"], ...T["_output"][]] : T["_output"][]; +export declare class ZodArray extends ZodType, ZodArrayDef, Cardinality extends "atleastone" ? [T["_input"], ...T["_input"][]] : T["_input"][]> { + _parse(input: ParseInput): ParseReturnType; + get element(): T; + min(minLength: number, message?: errorUtil.ErrMessage): this; + max(maxLength: number, message?: errorUtil.ErrMessage): this; + length(len: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodArray; + static create: (schema: El, params?: RawCreateParams) => ZodArray; +} +export type ZodNonEmptyArray = ZodArray; +export type UnknownKeysParam = "passthrough" | "strict" | "strip"; +export interface ZodObjectDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodObject; + shape: () => T; + catchall: Catchall; + unknownKeys: UnknownKeys; +} +export type mergeTypes = { + [k in keyof A | keyof B]: k extends keyof B ? B[k] : k extends keyof A ? A[k] : never; +}; +export type objectOutputType = objectUtil.flatten>> & CatchallOutput & PassthroughType; +export type baseObjectOutputType = { + [k in keyof Shape]: Shape[k]["_output"]; +}; +export type objectInputType = objectUtil.flatten> & CatchallInput & PassthroughType; +export type baseObjectInputType = objectUtil.addQuestionMarks<{ + [k in keyof Shape]: Shape[k]["_input"]; +}>; +export type CatchallOutput = ZodType extends T ? unknown : { + [k: string]: T["_output"]; +}; +export type CatchallInput = ZodType extends T ? unknown : { + [k: string]: T["_input"]; +}; +export type PassthroughType = T extends "passthrough" ? { + [k: string]: unknown; +} : unknown; +export type deoptional = T extends ZodOptional ? deoptional : T extends ZodNullable ? ZodNullable> : T; +export type SomeZodObject = ZodObject; +export type noUnrecognized = { + [k in keyof Obj]: k extends keyof Shape ? Obj[k] : never; +}; +export declare class ZodObject, Input = objectInputType> extends ZodType, Input> { + _cached: { + shape: T; + keys: string[]; + } | null; + _getCached(): { + shape: T; + keys: string[]; + }; + _parse(input: ParseInput): ParseReturnType; + get shape(): T; + strict(message?: errorUtil.ErrMessage): ZodObject; + strip(): ZodObject; + passthrough(): ZodObject; + /** + * @deprecated In most cases, this is no longer needed - unknown properties are now silently stripped. + * If you want to pass through unknown properties, use `.passthrough()` instead. + */ + nonstrict: () => ZodObject; + extend(augmentation: Augmentation): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated Use `.extend` instead + * */ + augment: (augmentation: Augmentation) => ZodObject, UnknownKeys, Catchall>; + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging: Incoming): ZodObject, Incoming["_def"]["unknownKeys"], Incoming["_def"]["catchall"]>; + setKey(key: Key, schema: Schema): ZodObject; + catchall(index: Index): ZodObject; + pick>(mask: Mask): ZodObject>, UnknownKeys, Catchall>; + omit>(mask: Mask): ZodObject, UnknownKeys, Catchall>; + /** + * @deprecated + */ + deepPartial(): partialUtil.DeepPartial; + partial(): ZodObject<{ + [k in keyof T]: ZodOptional; + }, UnknownKeys, Catchall>; + partial>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + required(): ZodObject<{ + [k in keyof T]: deoptional; + }, UnknownKeys, Catchall>; + required>(mask: Mask): ZodObject : T[k]; + }>, UnknownKeys, Catchall>; + keyof(): ZodEnum>; + static create: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; + static strictCreate: (shape: Shape, params?: RawCreateParams) => ZodObject; + static lazycreate: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +} +export type AnyZodObject = ZodObject; +export type ZodUnionOptions = Readonly<[ZodTypeAny, ...ZodTypeAny[]]>; +export interface ZodUnionDef> extends ZodTypeDef { + options: T; + typeName: ZodFirstPartyTypeKind.ZodUnion; +} +export declare class ZodUnion extends ZodType, T[number]["_input"]> { + _parse(input: ParseInput): ParseReturnType; + get options(): T; + static create: >(types: Options, params?: RawCreateParams) => ZodUnion; +} +export type ZodDiscriminatedUnionOption = ZodObject<{ + [key in Discriminator]: ZodTypeAny; +} & ZodRawShape, UnknownKeysParam, ZodTypeAny>; +export interface ZodDiscriminatedUnionDef[] = ZodDiscriminatedUnionOption[]> extends ZodTypeDef { + discriminator: Discriminator; + options: Options; + optionsMap: Map>; + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion; +} +export declare class ZodDiscriminatedUnion[]> extends ZodType, ZodDiscriminatedUnionDef, input> { + _parse(input: ParseInput): ParseReturnType; + get discriminator(): Discriminator; + get options(): Options; + get optionsMap(): Map>; + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create, + ...ZodDiscriminatedUnionOption[] + ]>(discriminator: Discriminator, options: Types, params?: RawCreateParams): ZodDiscriminatedUnion; +} +export interface ZodIntersectionDef extends ZodTypeDef { + left: T; + right: U; + typeName: ZodFirstPartyTypeKind.ZodIntersection; +} +export declare class ZodIntersection extends ZodType, T["_input"] & U["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +} +export type ZodTupleItems = [ZodTypeAny, ...ZodTypeAny[]]; +export type AssertArray = T extends any[] ? T : never; +export type OutputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_output"] : never; +}>; +export type OutputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...OutputTypeOfTuple, ...Rest["_output"][]] : OutputTypeOfTuple; +export type InputTypeOfTuple = AssertArray<{ + [k in keyof T]: T[k] extends ZodType ? T[k]["_input"] : never; +}>; +export type InputTypeOfTupleWithRest = Rest extends ZodTypeAny ? [...InputTypeOfTuple, ...Rest["_input"][]] : InputTypeOfTuple; +export interface ZodTupleDef extends ZodTypeDef { + items: T; + rest: Rest; + typeName: ZodFirstPartyTypeKind.ZodTuple; +} +export type AnyZodTuple = ZodTuple<[ZodTypeAny, ...ZodTypeAny[]] | [], ZodTypeAny | null>; +export declare class ZodTuple extends ZodType, ZodTupleDef, InputTypeOfTupleWithRest> { + _parse(input: ParseInput): ParseReturnType; + get items(): T; + rest(rest: RestSchema): ZodTuple; + static create: (schemas: Items, params?: RawCreateParams) => ZodTuple; +} +export interface ZodRecordDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodRecord; +} +export type KeySchema = ZodType; +export type RecordType = [string] extends [K] ? Record : [number] extends [K] ? Record : [symbol] extends [K] ? Record : [BRAND] extends [K] ? Record : Partial>; +export declare class ZodRecord extends ZodType, ZodRecordDef, RecordType> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + get element(): Value; + static create(valueType: Value, params?: RawCreateParams): ZodRecord; + static create(keySchema: Keys, valueType: Value, params?: RawCreateParams): ZodRecord; +} +export interface ZodMapDef extends ZodTypeDef { + valueType: Value; + keyType: Key; + typeName: ZodFirstPartyTypeKind.ZodMap; +} +export declare class ZodMap extends ZodType, ZodMapDef, Map> { + get keySchema(): Key; + get valueSchema(): Value; + _parse(input: ParseInput): ParseReturnType; + static create: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +} +export interface ZodSetDef extends ZodTypeDef { + valueType: Value; + typeName: ZodFirstPartyTypeKind.ZodSet; + minSize: { + value: number; + message?: string | undefined; + } | null; + maxSize: { + value: number; + message?: string | undefined; + } | null; +} +export declare class ZodSet extends ZodType, ZodSetDef, Set> { + _parse(input: ParseInput): ParseReturnType; + min(minSize: number, message?: errorUtil.ErrMessage): this; + max(maxSize: number, message?: errorUtil.ErrMessage): this; + size(size: number, message?: errorUtil.ErrMessage): this; + nonempty(message?: errorUtil.ErrMessage): ZodSet; + static create: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +} +export interface ZodFunctionDef = ZodTuple, Returns extends ZodTypeAny = ZodTypeAny> extends ZodTypeDef { + args: Args; + returns: Returns; + typeName: ZodFirstPartyTypeKind.ZodFunction; +} +export type OuterTypeOfFunction, Returns extends ZodTypeAny> = Args["_input"] extends Array ? (...args: Args["_input"]) => Returns["_output"] : never; +export type InnerTypeOfFunction, Returns extends ZodTypeAny> = Args["_output"] extends Array ? (...args: Args["_output"]) => Returns["_input"] : never; +export declare class ZodFunction, Returns extends ZodTypeAny> extends ZodType, ZodFunctionDef, InnerTypeOfFunction> { + _parse(input: ParseInput): ParseReturnType; + parameters(): Args; + returnType(): Returns; + args[0]>(...items: Items): ZodFunction, Returns>; + returns>(returnType: NewReturnType): ZodFunction; + implement>(func: F): ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + strictImplement(func: InnerTypeOfFunction): InnerTypeOfFunction; + validate: >(func: F) => ReturnType extends Returns["_output"] ? (...args: Args["_input"]) => ReturnType : OuterTypeOfFunction; + static create(): ZodFunction, ZodUnknown>; + static create>(args: T): ZodFunction; + static create(args: T, returns: U): ZodFunction; + static create, U extends ZodTypeAny = ZodUnknown>(args: T, returns: U, params?: RawCreateParams): ZodFunction; +} +export interface ZodLazyDef extends ZodTypeDef { + getter: () => T; + typeName: ZodFirstPartyTypeKind.ZodLazy; +} +export declare class ZodLazy extends ZodType, ZodLazyDef, input> { + get schema(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +} +export interface ZodLiteralDef extends ZodTypeDef { + value: T; + typeName: ZodFirstPartyTypeKind.ZodLiteral; +} +export declare class ZodLiteral extends ZodType, T> { + _parse(input: ParseInput): ParseReturnType; + get value(): T; + static create: (value: Value, params?: RawCreateParams) => ZodLiteral; +} +export type ArrayKeys = keyof any[]; +export type Indices = Exclude; +export type EnumValues = readonly [T, ...T[]]; +export type Values = { + [k in T[number]]: k; +}; +export interface ZodEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodEnum; +} +export type Writeable = { + -readonly [P in keyof T]: T[P]; +}; +export type FilterEnum = Values extends [] ? [] : Values extends [infer Head, ...infer Rest] ? Head extends ToExclude ? FilterEnum : [Head, ...FilterEnum] : never; +export type typecast = A extends T ? A : never; +declare function createZodEnum>(values: T, params?: RawCreateParams): ZodEnum>; +declare function createZodEnum(values: T, params?: RawCreateParams): ZodEnum; +export declare class ZodEnum extends ZodType, T[number]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get options(): T; + get enum(): Values; + get Values(): Values; + get Enum(): Values; + extract(values: ToExtract, newDef?: RawCreateParams): ZodEnum>; + exclude(values: ToExclude, newDef?: RawCreateParams): ZodEnum>, [string, ...string[]]>>; + static create: typeof createZodEnum; +} +export interface ZodNativeEnumDef extends ZodTypeDef { + values: T; + typeName: ZodFirstPartyTypeKind.ZodNativeEnum; +} +export type EnumLike = { + [k: string]: string | number; + [nu: number]: string; +}; +export declare class ZodNativeEnum extends ZodType, T[keyof T]> { + _cache: Set | undefined; + _parse(input: ParseInput): ParseReturnType; + get enum(): T; + static create: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +} +export interface ZodPromiseDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodPromise; +} +export declare class ZodPromise extends ZodType, ZodPromiseDef, Promise> { + unwrap(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: Inner, params?: RawCreateParams) => ZodPromise; +} +export type Refinement = (arg: T, ctx: RefinementCtx) => any; +export type SuperRefinement = (arg: T, ctx: RefinementCtx) => void | Promise; +export type RefinementEffect = { + type: "refinement"; + refinement: (arg: T, ctx: RefinementCtx) => any; +}; +export type TransformEffect = { + type: "transform"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type PreprocessEffect = { + type: "preprocess"; + transform: (arg: T, ctx: RefinementCtx) => any; +}; +export type Effect = RefinementEffect | TransformEffect | PreprocessEffect; +export interface ZodEffectsDef extends ZodTypeDef { + schema: T; + typeName: ZodFirstPartyTypeKind.ZodEffects; + effect: Effect; +} +export declare class ZodEffects, Input = input> extends ZodType, Input> { + innerType(): T; + sourceType(): T; + _parse(input: ParseInput): ParseReturnType; + static create: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; + static createWithPreprocess: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +} +export { ZodEffects as ZodTransformer }; +export interface ZodOptionalDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodOptional; +} +export type ZodOptionalType = ZodOptional; +export declare class ZodOptional extends ZodType, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodOptional; +} +export interface ZodNullableDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodNullable; +} +export type ZodNullableType = ZodNullable; +export declare class ZodNullable extends ZodType, T["_input"] | null> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; + static create: (type: Inner, params?: RawCreateParams) => ZodNullable; +} +export interface ZodDefaultDef extends ZodTypeDef { + innerType: T; + defaultValue: () => util.noUndefined; + typeName: ZodFirstPartyTypeKind.ZodDefault; +} +export declare class ZodDefault extends ZodType, ZodDefaultDef, T["_input"] | undefined> { + _parse(input: ParseInput): ParseReturnType; + removeDefault(): T; + static create: (type: Inner, params: RawCreateParams & { + default: Inner["_input"] | (() => util.noUndefined); + }) => ZodDefault; +} +export interface ZodCatchDef extends ZodTypeDef { + innerType: T; + catchValue: (ctx: { + error: ZodError; + input: unknown; + }) => T["_input"]; + typeName: ZodFirstPartyTypeKind.ZodCatch; +} +export declare class ZodCatch extends ZodType, unknown> { + _parse(input: ParseInput): ParseReturnType; + removeCatch(): T; + static create: (type: Inner, params: RawCreateParams & { + catch: Inner["_output"] | (() => Inner["_output"]); + }) => ZodCatch; +} +export interface ZodNaNDef extends ZodTypeDef { + typeName: ZodFirstPartyTypeKind.ZodNaN; +} +export declare class ZodNaN extends ZodType { + _parse(input: ParseInput): ParseReturnType; + static create: (params?: RawCreateParams) => ZodNaN; +} +export interface ZodBrandedDef extends ZodTypeDef { + type: T; + typeName: ZodFirstPartyTypeKind.ZodBranded; +} +export declare const BRAND: unique symbol; +export type BRAND = { + [BRAND]: { + [k in T]: true; + }; +}; +export declare class ZodBranded extends ZodType, ZodBrandedDef, T["_input"]> { + _parse(input: ParseInput): ParseReturnType; + unwrap(): T; +} +export interface ZodPipelineDef extends ZodTypeDef { + in: A; + out: B; + typeName: ZodFirstPartyTypeKind.ZodPipeline; +} +export declare class ZodPipeline extends ZodType, A["_input"]> { + _parse(input: ParseInput): ParseReturnType; + static create(a: ASchema, b: BSchema): ZodPipeline; +} +type BuiltIn = (((...args: any[]) => any) | (new (...args: any[]) => any)) | { + readonly [Symbol.toStringTag]: string; +} | Date | Error | Generator | Promise | RegExp; +type MakeReadonly = T extends Map ? ReadonlyMap : T extends Set ? ReadonlySet : T extends [infer Head, ...infer Tail] ? readonly [Head, ...Tail] : T extends Array ? ReadonlyArray : T extends BuiltIn ? T : Readonly; +export interface ZodReadonlyDef extends ZodTypeDef { + innerType: T; + typeName: ZodFirstPartyTypeKind.ZodReadonly; +} +export declare class ZodReadonly extends ZodType, ZodReadonlyDef, MakeReadonly> { + _parse(input: ParseInput): ParseReturnType; + static create: (type: Inner, params?: RawCreateParams) => ZodReadonly; + unwrap(): T; +} +type CustomParams = CustomErrorParams & { + fatal?: boolean; +}; +export declare function custom(check?: (data: any) => any, _params?: string | CustomParams | ((input: any) => CustomParams), +/** + * @deprecated + * + * Pass `fatal` into the params object instead: + * + * ```ts + * z.string().custom((val) => val.length > 5, { fatal: false }) + * ``` + * + */ +fatal?: boolean): ZodType; +export { ZodType as Schema, ZodType as ZodSchema }; +export declare const late: { + object: (shape: () => Shape, params?: RawCreateParams) => ZodObject; +}; +export declare enum ZodFirstPartyTypeKind { + ZodString = "ZodString", + ZodNumber = "ZodNumber", + ZodNaN = "ZodNaN", + ZodBigInt = "ZodBigInt", + ZodBoolean = "ZodBoolean", + ZodDate = "ZodDate", + ZodSymbol = "ZodSymbol", + ZodUndefined = "ZodUndefined", + ZodNull = "ZodNull", + ZodAny = "ZodAny", + ZodUnknown = "ZodUnknown", + ZodNever = "ZodNever", + ZodVoid = "ZodVoid", + ZodArray = "ZodArray", + ZodObject = "ZodObject", + ZodUnion = "ZodUnion", + ZodDiscriminatedUnion = "ZodDiscriminatedUnion", + ZodIntersection = "ZodIntersection", + ZodTuple = "ZodTuple", + ZodRecord = "ZodRecord", + ZodMap = "ZodMap", + ZodSet = "ZodSet", + ZodFunction = "ZodFunction", + ZodLazy = "ZodLazy", + ZodLiteral = "ZodLiteral", + ZodEnum = "ZodEnum", + ZodEffects = "ZodEffects", + ZodNativeEnum = "ZodNativeEnum", + ZodOptional = "ZodOptional", + ZodNullable = "ZodNullable", + ZodDefault = "ZodDefault", + ZodCatch = "ZodCatch", + ZodPromise = "ZodPromise", + ZodBranded = "ZodBranded", + ZodPipeline = "ZodPipeline", + ZodReadonly = "ZodReadonly" +} +export type ZodFirstPartySchemaTypes = ZodString | ZodNumber | ZodNaN | ZodBigInt | ZodBoolean | ZodDate | ZodUndefined | ZodNull | ZodAny | ZodUnknown | ZodNever | ZodVoid | ZodArray | ZodObject | ZodUnion | ZodDiscriminatedUnion | ZodIntersection | ZodTuple | ZodRecord | ZodMap | ZodSet | ZodFunction | ZodLazy | ZodLiteral | ZodEnum | ZodEffects | ZodNativeEnum | ZodOptional | ZodNullable | ZodDefault | ZodCatch | ZodPromise | ZodBranded | ZodPipeline | ZodReadonly | ZodSymbol; +declare abstract class Class { + constructor(..._: any[]); +} +declare const instanceOfType: (cls: T, params?: CustomParams) => ZodType, ZodTypeDef, InstanceType>; +declare const stringType: (params?: RawCreateParams & { + coerce?: true; +}) => ZodString; +declare const numberType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodNumber; +declare const nanType: (params?: RawCreateParams) => ZodNaN; +declare const bigIntType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBigInt; +declare const booleanType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodBoolean; +declare const dateType: (params?: RawCreateParams & { + coerce?: boolean; +}) => ZodDate; +declare const symbolType: (params?: RawCreateParams) => ZodSymbol; +declare const undefinedType: (params?: RawCreateParams) => ZodUndefined; +declare const nullType: (params?: RawCreateParams) => ZodNull; +declare const anyType: (params?: RawCreateParams) => ZodAny; +declare const unknownType: (params?: RawCreateParams) => ZodUnknown; +declare const neverType: (params?: RawCreateParams) => ZodNever; +declare const voidType: (params?: RawCreateParams) => ZodVoid; +declare const arrayType: (schema: El, params?: RawCreateParams) => ZodArray; +declare const objectType: (shape: Shape, params?: RawCreateParams) => ZodObject, objectInputType>; +declare const strictObjectType: (shape: Shape, params?: RawCreateParams) => ZodObject; +declare const unionType: >(types: Options, params?: RawCreateParams) => ZodUnion; +declare const discriminatedUnionType: typeof ZodDiscriminatedUnion.create; +declare const intersectionType: (left: TSchema, right: USchema, params?: RawCreateParams) => ZodIntersection; +declare const tupleType: (schemas: Items, params?: RawCreateParams) => ZodTuple; +declare const recordType: typeof ZodRecord.create; +declare const mapType: (keyType: KeySchema, valueType: ValueSchema, params?: RawCreateParams) => ZodMap; +declare const setType: (valueType: ValueSchema, params?: RawCreateParams) => ZodSet; +declare const functionType: typeof ZodFunction.create; +declare const lazyType: (getter: () => Inner, params?: RawCreateParams) => ZodLazy; +declare const literalType: (value: Value, params?: RawCreateParams) => ZodLiteral; +declare const enumType: typeof createZodEnum; +declare const nativeEnumType: (values: Elements, params?: RawCreateParams) => ZodNativeEnum; +declare const promiseType: (schema: Inner, params?: RawCreateParams) => ZodPromise; +declare const effectsType: (schema: I, effect: Effect, params?: RawCreateParams) => ZodEffects; +declare const optionalType: (type: Inner, params?: RawCreateParams) => ZodOptional; +declare const nullableType: (type: Inner, params?: RawCreateParams) => ZodNullable; +declare const preprocessType: (preprocess: (arg: unknown, ctx: RefinementCtx) => unknown, schema: I, params?: RawCreateParams) => ZodEffects; +declare const pipelineType: typeof ZodPipeline.create; +declare const ostring: () => ZodOptional; +declare const onumber: () => ZodOptional; +declare const oboolean: () => ZodOptional; +export declare const coerce: { + string: (typeof ZodString)["create"]; + number: (typeof ZodNumber)["create"]; + boolean: (typeof ZodBoolean)["create"]; + bigint: (typeof ZodBigInt)["create"]; + date: (typeof ZodDate)["create"]; +}; +export { anyType as any, arrayType as array, bigIntType as bigint, booleanType as boolean, dateType as date, discriminatedUnionType as discriminatedUnion, effectsType as effect, enumType as enum, functionType as function, instanceOfType as instanceof, intersectionType as intersection, lazyType as lazy, literalType as literal, mapType as map, nanType as nan, nativeEnumType as nativeEnum, neverType as never, nullType as null, nullableType as nullable, numberType as number, objectType as object, oboolean, onumber, optionalType as optional, ostring, pipelineType as pipeline, preprocessType as preprocess, promiseType as promise, recordType as record, setType as set, strictObjectType as strictObject, stringType as string, symbolType as symbol, effectsType as transformer, tupleType as tuple, undefinedType as undefined, unionType as union, unknownType as unknown, voidType as void, }; +export declare const NEVER: never; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts new file mode 100644 index 000000000..757d60e6e --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.d.cts @@ -0,0 +1 @@ +export { _lt as lt, _lte as lte, _gt as gt, _gte as gte, _positive as positive, _negative as negative, _nonpositive as nonpositive, _nonnegative as nonnegative, _multipleOf as multipleOf, _maxSize as maxSize, _minSize as minSize, _size as size, _maxLength as maxLength, _minLength as minLength, _length as length, _regex as regex, _lowercase as lowercase, _uppercase as uppercase, _includes as includes, _startsWith as startsWith, _endsWith as endsWith, _property as property, _properties as properties, _mime as mime, _overwrite as overwrite, _normalize as normalize, _trim as trim, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, _slugify as slugify, type $RefinementCtx as RefinementCtx, } from "../core/index.cjs"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/checks.js b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.js new file mode 100644 index 000000000..50a08b884 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/checks.js @@ -0,0 +1 @@ +export { _lt as lt, _lte as lte, _gt as gt, _gte as gte, _positive as positive, _negative as negative, _nonpositive as nonpositive, _nonnegative as nonnegative, _multipleOf as multipleOf, _maxSize as maxSize, _minSize as minSize, _size as size, _maxLength as maxLength, _minLength as minLength, _length as length, _regex as regex, _lowercase as lowercase, _uppercase as uppercase, _includes as includes, _startsWith as startsWith, _endsWith as endsWith, _property as property, _properties as properties, _mime as mime, _overwrite as overwrite, _normalize as normalize, _trim as trim, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, _slugify as slugify, } from "../core/index.js"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts new file mode 100644 index 000000000..231b91745 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.d.cts @@ -0,0 +1,17 @@ +import * as core from "../core/index.cjs"; +import * as schemas from "./schemas.cjs"; +export interface ZodCoercedString extends schemas._ZodString> { +} +export declare function string(params?: string | core.$ZodStringParams): ZodCoercedString; +export interface ZodCoercedNumber extends schemas._ZodNumber> { +} +export declare function number(params?: string | core.$ZodNumberParams): ZodCoercedNumber; +export interface ZodCoercedBoolean extends schemas._ZodBoolean> { +} +export declare function boolean(params?: string | core.$ZodBooleanParams): ZodCoercedBoolean; +export interface ZodCoercedBigInt extends schemas._ZodBigInt> { +} +export declare function bigint(params?: string | core.$ZodBigIntParams): ZodCoercedBigInt; +export interface ZodCoercedDate extends schemas._ZodDate> { +} +export declare function date(params?: string | core.$ZodDateParams): ZodCoercedDate; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js new file mode 100644 index 000000000..141cf8936 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/coerce.js @@ -0,0 +1,17 @@ +import * as core from "../core/index.js"; +import * as schemas from "./schemas.js"; +export function string(params) { + return core._coercedString(schemas.ZodString, params); +} +export function number(params) { + return core._coercedNumber(schemas.ZodNumber, params); +} +export function boolean(params) { + return core._coercedBoolean(schemas.ZodBoolean, params); +} +export function bigint(params) { + return core._coercedBigint(schemas.ZodBigInt, params); +} +export function date(params) { + return core._coercedDate(schemas.ZodDate, params); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts new file mode 100644 index 000000000..8f1b3d8c8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.d.cts @@ -0,0 +1,48 @@ +import * as core from "../core/index.cjs"; +import type { ZodType } from "./schemas.cjs"; +/** @deprecated Use `z.output` instead. */ +export type TypeOf = core.output; +/** @deprecated Use `z.output` instead. */ +export type Infer = core.output; +/** @deprecated Use `z.core.$ZodTypes` instead */ +export type ZodFirstPartySchemaTypes = core.$ZodTypes; +/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */ +export declare const ZodIssueCode: { + readonly invalid_type: "invalid_type"; + readonly too_big: "too_big"; + readonly too_small: "too_small"; + readonly invalid_format: "invalid_format"; + readonly not_multiple_of: "not_multiple_of"; + readonly unrecognized_keys: "unrecognized_keys"; + readonly invalid_union: "invalid_union"; + readonly invalid_key: "invalid_key"; + readonly invalid_element: "invalid_element"; + readonly invalid_value: "invalid_value"; + readonly custom: "custom"; +}; +/** @deprecated Use `z.core.$ZodFlattenedError>` instead; it takes an inferred type, not a schema. */ +export type inferFlattenedErrors = core.$ZodFlattenedError, U>; +/** @deprecated Use `z.core.$ZodFormattedError>` instead; it takes an inferred type, not a schema. */ +export type inferFormattedError, U = string> = core.$ZodFormattedError, U>; +/** Use `z.$brand` instead */ +export type BRAND = { + [core.$brand]: { + [k in T]: true; + }; +}; +export { $brand, config } from "../core/index.cjs"; +/** @deprecated Use `z.config(params)` instead. */ +export declare function setErrorMap(map: core.$ZodErrorMap): void; +/** @deprecated Use `z.config()` instead. */ +export declare function getErrorMap(): core.$ZodErrorMap | undefined; +/** @deprecated Use z.ZodType (without generics) instead. */ +export type ZodTypeAny = core.$ZodTypeInternals> = ZodType; +/** @deprecated Use `z.ZodType` */ +export type ZodSchema = core.$ZodTypeInternals> = ZodType; +/** @deprecated Use `z.ZodType` */ +export type Schema = core.$ZodTypeInternals> = ZodType; +/** Included for Zod 3 compatibility */ +export type ZodRawShape = core.$ZodShape; +/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */ +export declare enum ZodFirstPartyTypeKind { +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/compat.js b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.js new file mode 100644 index 000000000..2179ea904 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/compat.js @@ -0,0 +1,31 @@ +// Zod 3 compat layer +import * as core from "../core/index.js"; +/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */ +export const ZodIssueCode = { + invalid_type: "invalid_type", + too_big: "too_big", + too_small: "too_small", + invalid_format: "invalid_format", + not_multiple_of: "not_multiple_of", + unrecognized_keys: "unrecognized_keys", + invalid_union: "invalid_union", + invalid_key: "invalid_key", + invalid_element: "invalid_element", + invalid_value: "invalid_value", + custom: "custom", +}; +export { $brand, config } from "../core/index.js"; +/** @deprecated Use `z.config(params)` instead. */ +export function setErrorMap(map) { + core.config({ + customError: map, + }); +} +/** @deprecated Use `z.config()` instead. */ +export function getErrorMap() { + return core.config().customError; +} +/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */ +export var ZodFirstPartyTypeKind; +(function (ZodFirstPartyTypeKind) { +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts new file mode 100644 index 000000000..674ecf18e --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.d.cts @@ -0,0 +1,14 @@ +import type * as core from "../core/index.cjs"; +import * as schemas from "./schemas.cjs"; +/** Distributes `.partial()` through every object in a schema type, preserving the wrappers around them. */ +export type DeepPartial = T extends schemas.ZodObject ? schemas.ZodObject<{ + -readonly [K in keyof Shape]: schemas.ZodOptional>; +}, Config> : T extends schemas.ZodDiscriminatedUnion ? schemas.ZodUnion<{ + -readonly [I in keyof Options]: Options[I] extends core.SomeType ? DeepPartial : Options[I]; +}> : T extends schemas.ZodUnion ? schemas.ZodUnion<{ + -readonly [I in keyof Options]: Options[I] extends core.SomeType ? DeepPartial : Options[I]; +}> : T extends schemas.ZodArray ? schemas.ZodArray> : T extends schemas.ZodTuple ? schemas.ZodTuple<{ + -readonly [K in keyof Items]: Items[K] extends core.SomeType ? DeepPartial : Items[K]; +}, Rest extends core.SomeType ? DeepPartial : Rest> : T extends schemas.ZodIntersection ? schemas.ZodIntersection, DeepPartial> : T extends schemas.ZodRecord ? schemas.ZodRecord> : T extends schemas.ZodMap ? schemas.ZodMap> : T extends schemas.ZodSet ? schemas.ZodSet> : T extends schemas.ZodOptional ? schemas.ZodOptional> : T extends schemas.ZodNullable ? schemas.ZodNullable> : T extends schemas.ZodDefault ? schemas.ZodDefault> : T extends schemas.ZodPrefault ? schemas.ZodPrefault> : T extends schemas.ZodNonOptional ? schemas.ZodNonOptional> : T extends schemas.ZodCatch ? schemas.ZodCatch> : T extends schemas.ZodReadonly ? schemas.ZodReadonly> : T extends schemas.ZodSuccess ? schemas.ZodSuccess> : T extends schemas.ZodPromise ? schemas.ZodPromise> : T extends schemas.ZodCodec ? schemas.ZodCodec, DeepPartial> : T extends schemas.ZodPreprocess ? schemas.ZodPreprocess> : T extends schemas.ZodPipe ? schemas.ZodPipe, DeepPartial> : T extends schemas.ZodLazy ? schemas.ZodLazy> : T; +/** Returns a copy of the schema with every nested object's properties made optional. */ +export declare function deepPartial(schema: T): DeepPartial; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js new file mode 100644 index 000000000..a87bb625c --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/deep-partial.js @@ -0,0 +1,13 @@ +import { visit } from "../core/visit.js"; +import * as schemas from "./schemas.js"; +/** Returns a copy of the schema with every nested object's properties made optional. */ +export function deepPartial(schema) { + return visit(schema, { + object: (s) => s.partial(), + // Every partialed option now admits `undefined`, which the constructor rejects as a duplicate. + union: (s) => { + const def = s._zod.def; + return def.discriminator === undefined ? s : schemas.union(def.options); + }, + }); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts new file mode 100644 index 000000000..472102f6a --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.d.cts @@ -0,0 +1,29 @@ +import * as core from "../core/index.cjs"; +import { $ZodError } from "../core/index.cjs"; +/** @deprecated Use `z.core.$ZodIssue` from `@zod/core` instead, especially if you are building a library on top of Zod. */ +export type ZodIssue = core.$ZodIssue; +/** An Error-like class used to store Zod validation issues. */ +export interface ZodError extends $ZodError { + /** @deprecated Use the `z.treeifyError(err)` function instead. */ + format(): core.$ZodFormattedError; + format(mapper: (issue: core.$ZodIssue) => U): core.$ZodFormattedError; + /** @deprecated Use the `z.treeifyError(err)` function instead. */ + flatten(): core.$ZodFlattenedError; + flatten(mapper: (issue: core.$ZodIssue) => U): core.$ZodFlattenedError; + /** @deprecated Push directly to `.issues` instead. */ + addIssue(issue: core.$ZodIssue): void; + /** @deprecated Push directly to `.issues` instead. */ + addIssues(issues: core.$ZodIssue[]): void; + /** @deprecated Check `err.issues.length === 0` instead. */ + isEmpty: boolean; +} +export declare const ZodError: core.$constructor; +export declare const ZodRealError: core.$constructor; +/** @deprecated Use `z.core.$ZodFlattenedError` instead. */ +export type ZodFlattenedError = core.$ZodFlattenedError; +/** @deprecated Use `z.core.$ZodFormattedError` instead. */ +export type ZodFormattedError = core.$ZodFormattedError; +/** @deprecated Use `z.core.$ZodErrorMap` instead. */ +export type ZodErrorMap = core.$ZodErrorMap; +/** @deprecated Use `z.core.$ZodRawIssue` instead. */ +export type IssueData = core.$ZodRawIssue; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/errors.js b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.js new file mode 100644 index 000000000..e3c02587a --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/errors.js @@ -0,0 +1,57 @@ +import * as core from "../core/index.js"; +import { $ZodError } from "../core/index.js"; +import * as util from "../core/util.js"; +/* Prototypes that already carry the lazy helper methods. Seeded with the + * intrinsics so that `init` on a foreign object — it accepts any object — + * can never install an accessor onto a prototype we do not own. */ +const _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]); +/* Helper methods live as non-enumerable lazy getters on the shared + * prototype instead of own properties on every instance. On first + * access the getter allocates the per-instance closure and caches it + * as a non-enumerable own property, so detached usage still works and + * the allocation only happens for methods actually touched. */ +function _lazyMethod(proto, key, make) { + Object.defineProperty(proto, key, { + configurable: true, + enumerable: false, + get() { + const value = make(this); + Object.defineProperty(this, key, { value, configurable: true, writable: true }); + return value; + }, + set(value) { + Object.defineProperty(this, key, { value, configurable: true, writable: true }); + }, + }); +} +const initializer = (inst, issues) => { + $ZodError.init(inst, issues); + inst.name = "ZodError"; + const proto = Object.getPrototypeOf(inst); + if (_installedErrorProtos.has(proto)) + return; + _installedErrorProtos.add(proto); + _lazyMethod(proto, "format", (self) => (mapper) => core.formatError(self, mapper)); + _lazyMethod(proto, "flatten", (self) => (mapper) => core.flattenError(self, mapper)); + _lazyMethod(proto, "addIssue", (self) => (issue) => { + self.issues.push(issue); + self.message = JSON.stringify(self.issues, util.jsonStringifyReplacer, 2); + }); + _lazyMethod(proto, "addIssues", (self) => (issues) => { + self.issues.push(...issues); + self.message = JSON.stringify(self.issues, util.jsonStringifyReplacer, 2); + }); + Object.defineProperty(proto, "isEmpty", { + configurable: true, + enumerable: false, + get() { + return this.issues.length === 0; + }, + }); +}; +export const ZodError = /*@__PURE__*/ core.$constructor("ZodError", initializer); +export const ZodRealError = /*@__PURE__*/ core.$constructor("ZodError", initializer, undefined, { + Parent: Error, +}); +// /** @deprecated Use `z.core.$ZodErrorMapCtx` instead. */ +// export type ErrorMapCtx = core.$ZodErrorMapCtx; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts new file mode 100644 index 000000000..7e9ce5e74 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/external.d.cts @@ -0,0 +1,19 @@ +export * as core from "../core/index.cjs"; +export * from "./schemas.cjs"; +export * from "./checks.cjs"; +export * from "./errors.cjs"; +export * from "./parse.cjs"; +export * from "./compat.cjs"; +export type { infer } from "../core/index.cjs"; +export type { JSONType } from "../core/util.cjs"; +export type { CompileOptions } from "../core/index.cjs"; +export { globalRegistry, type GlobalMeta, registry, config, memoizer, $output, $input, $brand, clone, regexes, treeifyError, prettifyError, formatError, flattenError, TimePrecision, util, NEVER, INVALID, toZod, compile, withParser, ZodCompileAsyncError, ZodCompileUnsupportedError, getDiscriminatedOption, } from "../core/index.cjs"; +export { toJSONSchema } from "../core/json-schema-processors.cjs"; +export { fromJSONSchema } from "./from-json-schema.cjs"; +export { deepPartial } from "./deep-partial.cjs"; +export { input, output } from "./in-out.cjs"; +export * as locales from "../locales/index.cjs"; +export { ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration } from "./iso.cjs"; +export * as iso from "./iso.cjs"; +export type { ZodCoercedString, ZodCoercedNumber, ZodCoercedBigInt, ZodCoercedBoolean, ZodCoercedDate, } from "./coerce.cjs"; +export * as coerce from "./coerce.cjs"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/external.js b/tests/fixtures/npm/node_modules/zod/v4/classic/external.js new file mode 100644 index 000000000..435be167f --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/external.js @@ -0,0 +1,17 @@ +export * as core from "../core/index.js"; +export * from "./schemas.js"; +export * from "./checks.js"; +export * from "./errors.js"; +export * from "./parse.js"; +export * from "./compat.js"; +export { globalRegistry, registry, config, memoizer, $output, $input, $brand, clone, regexes, treeifyError, prettifyError, formatError, flattenError, TimePrecision, util, NEVER, INVALID, toZod, compile, withParser, ZodCompileAsyncError, ZodCompileUnsupportedError, getDiscriminatedOption, } from "../core/index.js"; +export { toJSONSchema } from "../core/json-schema-processors.js"; +export { fromJSONSchema } from "./from-json-schema.js"; +export { deepPartial } from "./deep-partial.js"; +// Types and values both, from one module — re-exporting the types from core would collide. +export { input, output } from "./in-out.js"; +export * as locales from "../locales/index.js"; +// iso must be exported from top-level https://github.com/colinhacks/zod/issues/4491 +export { ZodISODateTime, ZodISODate, ZodISOTime, ZodISODuration } from "./iso.js"; +export * as iso from "./iso.js"; +export * as coerce from "./coerce.js"; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts new file mode 100644 index 000000000..dc028bc01 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.d.cts @@ -0,0 +1,12 @@ +import type * as JSONSchema from "../core/json-schema.cjs"; +import { type $ZodRegistry } from "../core/registries.cjs"; +import type { ZodType } from "./schemas.cjs"; +type JSONSchemaVersion = "draft-2020-12" | "draft-7" | "draft-4" | "openapi-3.0"; +interface FromJSONSchemaParams { + defaultTarget?: JSONSchemaVersion; + registry?: $ZodRegistry; +} +/** + * Converts a JSON Schema to a Zod schema. This function should be considered semi-experimental. It's behavior is liable to change. */ +export declare function fromJSONSchema(schema: JSONSchema.JSONSchema | boolean, params?: FromJSONSchemaParams): ZodType; +export {}; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js new file mode 100644 index 000000000..1f79b5c85 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/from-json-schema.js @@ -0,0 +1,881 @@ +import { globalRegistry } from "../core/registries.js"; +import { assignProp, isPlainObject } from "../core/util.js"; +import * as _checks from "./checks.js"; +import * as _iso from "./iso.js"; +import * as _schemas from "./schemas.js"; +// Local z object to avoid circular dependency with ../index.js +const z = { + ..._schemas, + ..._checks, + iso: _iso, +}; +// Keys that are recognized and handled by the conversion logic +const RECOGNIZED_KEYS = /*@__PURE__*/ new Set([ + // Schema identification + "$schema", + "$ref", + "$defs", + "definitions", + // Core schema keywords + "$id", + "id", + "$comment", + "$anchor", + "$vocabulary", + "$dynamicRef", + "$dynamicAnchor", + // Type + "type", + "enum", + "const", + // Composition + "anyOf", + "oneOf", + "allOf", + "not", + // Object + "properties", + "required", + "additionalProperties", + "patternProperties", + "propertyNames", + "minProperties", + "maxProperties", + // Array + "items", + "prefixItems", + "additionalItems", + "minItems", + "maxItems", + "uniqueItems", + "contains", + "minContains", + "maxContains", + // String + "minLength", + "maxLength", + "pattern", + "format", + // Number + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + // Already handled metadata + "description", + "default", + // Content + "contentEncoding", + "contentMediaType", + "contentSchema", + // Unsupported (error-throwing) + "unevaluatedItems", + "unevaluatedProperties", + "if", + "then", + "else", + "dependentSchemas", + "dependentRequired", + // OpenAPI + "nullable", + "readOnly", +]); +function detectVersion(schema, defaultTarget) { + const $schema = schema.$schema; + if ($schema === "https://json-schema.org/draft/2020-12/schema") { + return "draft-2020-12"; + } + if ($schema === "http://json-schema.org/draft-07/schema#") { + return "draft-7"; + } + if ($schema === "http://json-schema.org/draft-04/schema#") { + return "draft-4"; + } + // Use defaultTarget if provided, otherwise default to draft-2020-12 + return defaultTarget ?? "draft-2020-12"; +} +// Positional schemas constrain the elements that are present; only minItems makes them required. +function applyMinItems(items, minItems) { + return items.map((item, index) => (index < minItems ? item : item.optional())); +} +// Inverse of the encoding applied to $ref pointer segments in to-json-schema.ts. Per RFC 6901 the `~1` replacement must run before `~0`. +function decodeJSONPointerSegment(segment) { + return segment.replace(/~1/g, "/").replace(/~0/g, "~"); +} +function resolveRef(ref, ctx) { + if (!ref.startsWith("#")) { + throw new Error("External $ref is not supported, only local refs (#/...) are allowed"); + } + const path = ref.slice(1).split("/").filter(Boolean); + // Handle root reference "#" + if (path.length === 0) { + return ctx.rootSchema; + } + const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions"; + if (path[0] === defsKey) { + const key = path[1] === undefined ? undefined : decodeJSONPointerSegment(path[1]); + if (!key || !ctx.defs[key]) { + throw new Error(`Reference not found: ${ref}`); + } + return ctx.defs[key]; + } + throw new Error(`Reference not found: ${ref}`); +} +/** + * Enforces the keywords that constrain an object's own keys — `propertyNames`, + * `minProperties`, `maxProperties` — before `objectSchema` runs. The guard has + * to see the raw input: an object parse drops `__proto__` and can add keys from + * a property `default`, so its output is not the set of names the instance + * actually carried. + */ +function checkObjectGuards(objectSchema, guards) { + // An identity transform, not z.any(), so `toJSONSchema` reports the object on both the input and the output side of the pipe. + const guard = z + .transform((value) => value) + .check((payload) => { + const value = payload.value; + if (typeof value !== "object" || value === null || Array.isArray(value)) + return; + const keys = Object.getOwnPropertyNames(value); + if (guards.minProperties !== undefined && keys.length < guards.minProperties) { + payload.issues.push({ + origin: "object", + code: "too_small", + minimum: guards.minProperties, + inclusive: true, + message: `Too small: expected object to have >=${guards.minProperties} properties`, + input: value, + inst: objectSchema, + continue: true, + }); + } + if (guards.maxProperties !== undefined && keys.length > guards.maxProperties) { + payload.issues.push({ + origin: "object", + code: "too_big", + maximum: guards.maxProperties, + inclusive: true, + message: `Too big: expected object to have <=${guards.maxProperties} properties`, + input: value, + inst: objectSchema, + continue: true, + }); + } + if (guards.keySchema) { + for (const key of keys) { + const result = guards.keySchema.safeParse(key); + if (result.success) + continue; + payload.issues.push({ + code: "invalid_key", + origin: "record", + issues: result.error.issues, + input: key, + path: [key], + continue: true, + }); + } + } + }); + return guard.pipe(objectSchema); +} +/** + * An injective string for JSON data: two values share a key exactly when `uniqueItems` calls them + * equal, so duplicate detection is a Map lookup rather than a pairwise walk. Strings and keys are + * length-prefixed so no value can spell another one. `null` means "never equal to anything" — a + * cycle or a NaN, neither of which JSON can express. + */ +function canonicalKey(value, seen) { + if (value === null) + return "z"; + const type = typeof value; + if (type !== "object") { + if (type === "number" && Number.isNaN(value)) + return null; + const raw = String(value); + return `${type[0]}${raw.length}:${raw}`; + } + if (seen.has(value)) + return null; + seen.add(value); + try { + if (Array.isArray(value)) { + const parts = []; + for (const item of value) { + const key = canonicalKey(item, seen); + if (key === null) + return null; + parts.push(key); + } + return `a${parts.length}:[${parts.join(",")}]`; + } + const keys = Object.keys(value).sort(); + const parts = []; + for (const k of keys) { + const key = canonicalKey(value[k], seen); + if (key === null) + return null; + parts.push(`${k.length}:${k}=${key}`); + } + return `o${parts.length}:{${parts.join(",")}}`; + } + finally { + seen.delete(value); + } +} +// keywords whose value is a schema or an array of them +const SCHEMA_KEYWORDS = /*@__PURE__*/ new Set([ + "items", + "prefixItems", + "additionalItems", + "additionalProperties", + "contains", + "propertyNames", + "not", + "if", + "then", + "else", + "allOf", + "anyOf", + "oneOf", + "unevaluatedItems", + "unevaluatedProperties", + "contentSchema", +]); +// keywords whose value maps names to schemas. draft-7 `dependencies` also allows an array of property names, which holds no schema to walk +const SCHEMA_MAP_KEYWORDS = /*@__PURE__*/ new Set([ + "properties", + "patternProperties", + "dependentSchemas", + "dependencies", + "$defs", + "definitions", +]); +/** + * True when a subschema holds a `$ref` anywhere a schema can appear. Only those positions are + * walked: `$ref` under `default` or an `x-` annotation is instance data with an ordinary key, and + * reading it as a reference would drop a constraint that resolves perfectly well. + */ +function containsRef(value) { + if (typeof value !== "object" || value === null) + return false; + if (Array.isArray(value)) + return value.some(containsRef); + if (typeof value.$ref === "string") + return true; + return Object.entries(value).some(([key, sub]) => { + if (SCHEMA_KEYWORDS.has(key)) + return containsRef(sub); + if (!SCHEMA_MAP_KEYWORDS.has(key) || typeof sub !== "object" || sub === null) + return false; + return Object.values(sub).some(containsRef); + }); +} +function plural(n) { + return n === 1 ? "element" : "elements"; +} +/** + * Enforces `uniqueItems` and the `contains` family before `arraySchema` runs, for the same reason + * `checkObjectGuards` runs pre-pipe: an item parse can apply a nested `default`, so the parsed + * array is not the instance the keywords are defined over. A guard issue aborts the pipe, so + * `minItems`/`maxItems` are not also reported on the same parse — instance correctness is worth + * more than co-reporting two issues. + */ +function checkArrayGuards(arraySchema, guards) { + // An identity transform, not z.any(), so `toJSONSchema` reports the array on both the input and the output side of the pipe. + const guard = z + .transform((value) => value) + .check((payload) => { + const items = payload.value; + if (!Array.isArray(items)) + return; + if (guards.uniqueItems === true) { + const firstSeen = new Map(); + for (let i = 0; i < items.length; i++) { + const key = canonicalKey(items[i], new Set()); + if (key === null) + continue; + const first = firstSeen.get(key); + if (first === undefined) { + firstSeen.set(key, i); + continue; + } + payload.issues.push({ + code: "custom", + message: `Array items must be unique: element at index ${i} duplicates the one at index ${first}`, + input: items, + path: [i], + continue: true, + }); + } + } + if (guards.containsSchema) { + const minContains = guards.minContains ?? 1; + // one past the ceiling already proves the failure, and nothing above it changes the verdict + const ceiling = guards.maxContains !== undefined ? guards.maxContains + 1 : Number.POSITIVE_INFINITY; + let matches = 0; + for (const item of items) { + if (guards.containsSchema.safeParse(item).success && ++matches >= ceiling) + break; + } + if (matches < minContains) { + // the scan ran to the end, so this count is exact + payload.issues.push({ + code: "custom", + message: `Array must contain at least ${minContains} matching ${plural(minContains)}; found ${matches}`, + input: items, + continue: true, + }); + } + if (guards.maxContains !== undefined && matches > guards.maxContains) { + payload.issues.push({ + code: "custom", + message: `Array must contain at most ${guards.maxContains} matching ${plural(guards.maxContains)}`, + input: items, + continue: true, + }); + } + } + }); + return guard.pipe(arraySchema); +} +function getTupleRest(restSchema, ctx) { + if (restSchema === false) { + return undefined; + } + if (restSchema === undefined || restSchema === true) { + return z.any(); + } + return convertSchema(restSchema, ctx); +} +// the RFC 3339 full-time keyword, narrower than `z.iso.time()`; local because sharing it via `timeSource` pins that builder into every bundle (+139 B gzipped on a mini `z.boolean()`) +const fullTime = /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/; +function convertBaseSchema(schema, ctx) { + // Handle unsupported features + if (schema.not !== undefined) { + // Special case: { not: {} } represents never + if (typeof schema.not === "object" && Object.keys(schema.not).length === 0) { + return z.never(); + } + throw new Error("not is not supported in Zod (except { not: {} } for never)"); + } + if (schema.unevaluatedItems !== undefined) { + throw new Error("unevaluatedItems is not supported"); + } + if (schema.unevaluatedProperties !== undefined) { + throw new Error("unevaluatedProperties is not supported"); + } + if (schema.if !== undefined || schema.then !== undefined || schema.else !== undefined) { + throw new Error("Conditional schemas (if/then/else) are not supported"); + } + if (schema.dependentSchemas !== undefined || schema.dependentRequired !== undefined) { + throw new Error("dependentSchemas and dependentRequired are not supported"); + } + // Handle $ref + if (schema.$ref) { + const refPath = schema.$ref; + if (ctx.refs.has(refPath)) { + return ctx.refs.get(refPath); + } + if (ctx.processing.has(refPath)) { + // Circular reference - use lazy + return z.lazy(() => { + if (!ctx.refs.has(refPath)) { + throw new Error(`Circular reference not resolved: ${refPath}`); + } + return ctx.refs.get(refPath); + }); + } + ctx.processing.add(refPath); + const resolved = resolveRef(refPath, ctx); + const zodSchema = convertSchema(resolved, ctx); + ctx.refs.set(refPath, zodSchema); + ctx.processing.delete(refPath); + return zodSchema; + } + // Handle enum + if (schema.enum !== undefined) { + const enumValues = schema.enum; + // Special case: OpenAPI 3.0 null representation { type: "string", nullable: true, enum: [null] } + if (ctx.version === "openapi-3.0" && + schema.nullable === true && + enumValues.length === 1 && + enumValues[0] === null) { + return z.null(); + } + if (enumValues.length === 0) { + return z.never(); + } + if (enumValues.length === 1) { + return z.literal(enumValues[0]); + } + // Check if all values are strings + if (enumValues.every((v) => typeof v === "string")) { + return z.enum(enumValues); + } + // Mixed types - use union of literals + const literalSchemas = enumValues.map((v) => z.literal(v)); + if (literalSchemas.length < 2) { + return literalSchemas[0]; + } + return z.union([literalSchemas[0], literalSchemas[1], ...literalSchemas.slice(2)]); + } + // Handle const + if (schema.const !== undefined) { + return z.literal(schema.const); + } + // Handle type + const type = schema.type; + if (Array.isArray(type)) { + // Expand type array into anyOf union + const typeSchemas = type.map((t) => { + const typeSchema = { ...schema, type: t }; + return convertBaseSchema(typeSchema, ctx); + }); + if (typeSchemas.length === 0) { + return z.never(); + } + if (typeSchemas.length === 1) { + return typeSchemas[0]; + } + return z.union(typeSchemas); + } + if (!type) { + // No type specified - empty schema (any) + return z.any(); + } + let zodSchema; + switch (type) { + case "string": { + let stringSchema = z.string(); + // Apply format using .check() with Zod format functions + if (schema.format) { + const format = schema.format; + // Map common formats to Zod check functions + if (format === "email") { + stringSchema = stringSchema.check(z.email()); + } + else if (format === "uri" || format === "uri-reference") { + stringSchema = stringSchema.check(z.url()); + } + else if (format === "uuid" || format === "guid") { + stringSchema = stringSchema.check(z.uuid()); + } + else if (format === "date-time") { + stringSchema = stringSchema.check(z.iso.datetime({ offset: true })); + } + else if (format === "date") { + stringSchema = stringSchema.check(z.iso.date()); + } + else if (format === "time") { + stringSchema = stringSchema.check(z.regex(fullTime)); + } + else if (format === "duration") { + stringSchema = stringSchema.check(z.iso.duration()); + } + else if (format === "hostname") { + stringSchema = stringSchema.check(z.hostname()); + } + else if (format === "ipv4") { + stringSchema = stringSchema.check(z.ipv4()); + } + else if (format === "ipv6") { + stringSchema = stringSchema.check(z.ipv6()); + } + else if (format === "mac") { + stringSchema = stringSchema.check(z.mac()); + } + else if (format === "cidr") { + stringSchema = stringSchema.check(z.cidrv4()); + } + else if (format === "cidr-v6") { + stringSchema = stringSchema.check(z.cidrv6()); + } + else if (format === "base64") { + stringSchema = stringSchema.check(z.base64()); + } + else if (format === "base64url") { + stringSchema = stringSchema.check(z.base64url()); + } + else if (format === "e164") { + stringSchema = stringSchema.check(z.e164()); + } + else if (format === "credit_card") { + stringSchema = stringSchema.check(z.creditCard()); + } + else if (format === "iban") { + stringSchema = stringSchema.check(z.iban()); + } + else if (format === "jwt") { + stringSchema = stringSchema.check(z.jwt()); + } + else if (format === "emoji") { + stringSchema = stringSchema.check(z.emoji()); + } + else if (format === "nanoid") { + stringSchema = stringSchema.check(z.nanoid()); + } + else if (format === "cuid") { + stringSchema = stringSchema.check(z.cuid()); + } + else if (format === "cuid2") { + stringSchema = stringSchema.check(z.cuid2()); + } + else if (format === "ulid") { + stringSchema = stringSchema.check(z.ulid()); + } + else if (format === "xid") { + stringSchema = stringSchema.check(z.xid()); + } + else if (format === "ksuid") { + stringSchema = stringSchema.check(z.ksuid()); + } + // Note: json-string format is not currently supported by Zod + // Custom formats are ignored - keep as plain string + } + // Apply constraints + if (typeof schema.minLength === "number") { + stringSchema = stringSchema.min(schema.minLength); + } + if (typeof schema.maxLength === "number") { + stringSchema = stringSchema.max(schema.maxLength); + } + if (schema.pattern) { + // JSON Schema patterns are not implicitly anchored (match anywhere in string) + stringSchema = stringSchema.regex(new RegExp(schema.pattern)); + } + zodSchema = stringSchema; + break; + } + case "number": + case "integer": { + let numberSchema = type === "integer" ? z.number().int() : z.number(); + // Apply constraints + // In draft-04, `exclusiveMinimum: true` makes the sibling `minimum` exclusive rather than an independent bound, so the inclusive `.min()` is skipped; emitting it too would be dominated by the exclusive check and report a second, weaker issue. + if (typeof schema.minimum === "number" && schema.exclusiveMinimum !== true) { + numberSchema = numberSchema.min(schema.minimum); + } + if (typeof schema.maximum === "number" && schema.exclusiveMaximum !== true) { + numberSchema = numberSchema.max(schema.maximum); + } + if (typeof schema.exclusiveMinimum === "number") { + numberSchema = numberSchema.gt(schema.exclusiveMinimum); + } + else if (schema.exclusiveMinimum === true && typeof schema.minimum === "number") { + numberSchema = numberSchema.gt(schema.minimum); + } + if (typeof schema.exclusiveMaximum === "number") { + numberSchema = numberSchema.lt(schema.exclusiveMaximum); + } + else if (schema.exclusiveMaximum === true && typeof schema.maximum === "number") { + numberSchema = numberSchema.lt(schema.maximum); + } + if (typeof schema.multipleOf === "number") { + numberSchema = numberSchema.multipleOf(schema.multipleOf); + } + zodSchema = numberSchema; + break; + } + case "boolean": { + zodSchema = z.boolean(); + break; + } + case "null": { + zodSchema = z.null(); + break; + } + case "object": { + const shape = {}; + const properties = schema.properties || {}; + const requiredSet = new Set(schema.required || []); + const additionalSchema = typeof schema.additionalProperties === "object" + ? convertSchema(schema.additionalProperties, ctx) + : undefined; + // Convert properties - mark optional ones + for (const [key, propSchema] of Object.entries(properties)) { + const propZodSchema = convertSchema(propSchema, ctx); + // If not in required array, make it optional. assignProp so a __proto__ key becomes an own property instead of hitting the inherited setter + assignProp(shape, key, requiredSet.has(key) ? propZodSchema : propZodSchema.optional()); + } + // Handle patternProperties + if (schema.patternProperties) { + // patternProperties: keys matching pattern must satisfy corresponding schema. Use loose records so non-matching keys pass through + const patternProps = schema.patternProperties; + const patternKeys = Object.keys(patternProps); + const looseRecords = []; + for (const pattern of patternKeys) { + const patternValue = convertSchema(patternProps[pattern], ctx); + const keySchema = z.string().regex(new RegExp(pattern)); + looseRecords.push(z.looseRecord(keySchema, patternValue)); + } + // Build intersection: object schema + all pattern property records + const schemasToIntersect = []; + if (Object.keys(shape).length > 0) { + // Use passthrough so patternProperties can validate additional keys + schemasToIntersect.push(z.object(shape).passthrough()); + } + schemasToIntersect.push(...looseRecords); + if (schemasToIntersect.length === 0) { + zodSchema = z.object({}).passthrough(); + } + else if (schemasToIntersect.length === 1) { + zodSchema = schemasToIntersect[0]; + } + else { + // Chain intersections: (A & B) & C & D ... + let result = z.intersection(schemasToIntersect[0], schemasToIntersect[1]); + for (let i = 2; i < schemasToIntersect.length; i++) { + result = z.intersection(result, schemasToIntersect[i]); + } + zodSchema = result; + } + // When additionalProperties is false, reject keys that are neither defined in properties nor matched by any patternProperty. + if (schema.additionalProperties === false) { + const propertyKeys = Object.keys(shape); + const patterns = patternKeys.map((p) => new RegExp(p)); + const basePatternSchema = zodSchema; + zodSchema = zodSchema.check((payload) => { + if (!isPlainObject(payload.value)) + return; + const unrecognized = []; + for (const key of Object.keys(payload.value)) { + if (propertyKeys.includes(key)) + continue; + if (patterns.some((regex) => regex.test(key))) + continue; + unrecognized.push(key); + } + if (unrecognized.length) { + payload.issues.push({ + code: "unrecognized_keys", + keys: unrecognized, + input: payload.value, + inst: basePatternSchema, + }); + } + }); + } + } + else { + // Handle additionalProperties. In JSON Schema, additionalProperties defaults to true (allow any extra properties). In Zod, objects strip unknown keys by default, so we need to handle this explicitly + const objectSchema = z.object(shape); + if (schema.additionalProperties === false) { + // Strict mode - no extra properties allowed + zodSchema = objectSchema.strict(); + } + else if (additionalSchema) { + // Extra properties must match the specified schema + zodSchema = objectSchema.catchall(additionalSchema); + } + else { + // additionalProperties is true or undefined - allow any extra properties (passthrough) + zodSchema = objectSchema.passthrough(); + } + } + // propertyNames constrains key *names* only, and says nothing about which keys are required or how their values validate. Layering it on top of the result keeps properties/patternProperties/additionalProperties composing underneath. `true` allows every name, so it needs no guard. + const hasKeyGuard = schema.propertyNames !== undefined && schema.propertyNames !== true; + const minProperties = typeof schema.minProperties === "number" ? schema.minProperties : undefined; + const maxProperties = typeof schema.maxProperties === "number" ? schema.maxProperties : undefined; + if (hasKeyGuard || minProperties !== undefined || maxProperties !== undefined) { + let keySchema; + if (hasKeyGuard) { + // Keys are always strings, so a propertyNames subschema that omits `type` still constrains them — without this it would convert to z.any(). + const keyJSONSchema = typeof schema.propertyNames === "object" && schema.propertyNames.type === undefined + ? { type: "string", ...schema.propertyNames } + : schema.propertyNames; + keySchema = convertSchema(keyJSONSchema, ctx); + } + zodSchema = checkObjectGuards(zodSchema, { keySchema, minProperties, maxProperties }); + } + break; + } + case "array": { + // Check if this is a tuple (prefixItems or items as array) + const prefixItems = schema.prefixItems; + const items = schema.items; + if (prefixItems && Array.isArray(prefixItems)) { + // Tuple with prefixItems (draft-2020-12) + const minItems = typeof schema.minItems === "number" ? schema.minItems : 0; + const tupleItems = prefixItems.map((item) => convertSchema(item, ctx)); + const positionalItems = applyMinItems(tupleItems, minItems); + const rest = !Array.isArray(items) ? getTupleRest(items, ctx) : undefined; + const tupleSchema = z.tuple(positionalItems); + zodSchema = rest ? tupleSchema.rest(rest) : tupleSchema; + // Apply minItems/maxItems constraints to tuples + if (typeof schema.minItems === "number") { + zodSchema = zodSchema.check(z.minLength(schema.minItems)); + } + if (typeof schema.maxItems === "number") { + zodSchema = zodSchema.check(z.maxLength(schema.maxItems)); + } + } + else if (Array.isArray(items)) { + // Tuple with items array (draft-7) + const minItems = typeof schema.minItems === "number" ? schema.minItems : 0; + const tupleItems = items.map((item) => convertSchema(item, ctx)); + const positionalItems = applyMinItems(tupleItems, minItems); + const rest = getTupleRest(schema.additionalItems, ctx); + const tupleSchema = z.tuple(positionalItems); + zodSchema = rest ? tupleSchema.rest(rest) : tupleSchema; + // Apply minItems/maxItems constraints to tuples + if (typeof schema.minItems === "number") { + zodSchema = zodSchema.check(z.minLength(schema.minItems)); + } + if (typeof schema.maxItems === "number") { + zodSchema = zodSchema.check(z.maxLength(schema.maxItems)); + } + } + else if (items !== undefined) { + // Regular array + const element = convertSchema(items, ctx); + let arraySchema = z.array(element); + // Apply constraints + if (typeof schema.minItems === "number") { + arraySchema = arraySchema.min(schema.minItems); + } + if (typeof schema.maxItems === "number") { + arraySchema = arraySchema.max(schema.maxItems); + } + zodSchema = arraySchema; + } + else { + // No items specified - array of any + zodSchema = z.array(z.any()); + } + // minContains/maxContains only constrain anything when `contains` itself is present + if (schema.uniqueItems === true || schema.contains !== undefined) { + zodSchema = checkArrayGuards(zodSchema, { + uniqueItems: schema.uniqueItems === true, + containsSchema: schema.contains !== undefined ? convertSchema(schema.contains, ctx) : undefined, + minContains: typeof schema.minContains === "number" ? schema.minContains : undefined, + maxContains: typeof schema.maxContains === "number" ? schema.maxContains : undefined, + }); + } + break; + } + default: + throw new Error(`Unsupported type: ${type}`); + } + return zodSchema; +} +function convertSchema(schema, ctx) { + if (typeof schema === "boolean") { + return schema ? z.any() : z.never(); + } + // Convert base schema first (ignoring composition keywords) + let baseSchema = convertBaseSchema(schema, ctx); + const hasExplicitType = schema.type || schema.enum !== undefined || schema.const !== undefined; + // Process composition keywords LAST (they can appear together) + // Handle anyOf - wrap base schema with union + if (schema.anyOf && Array.isArray(schema.anyOf)) { + const options = schema.anyOf.map((s) => convertSchema(s, ctx)); + const anyOfUnion = z.union(options); + baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion; + } + // Handle oneOf - exclusive union (exactly one must match) + if (schema.oneOf && Array.isArray(schema.oneOf)) { + const options = schema.oneOf.map((s) => convertSchema(s, ctx)); + const oneOfUnion = z.xor(options); + baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion; + } + // Handle allOf - wrap base schema with intersection + if (schema.allOf && Array.isArray(schema.allOf)) { + if (schema.allOf.length === 0) { + baseSchema = hasExplicitType ? baseSchema : z.any(); + } + else { + let result = hasExplicitType ? baseSchema : convertSchema(schema.allOf[0], ctx); + const startIdx = hasExplicitType ? 0 : 1; + for (let i = startIdx; i < schema.allOf.length; i++) { + result = z.intersection(result, convertSchema(schema.allOf[i], ctx)); + } + baseSchema = result; + } + } + // Handle nullable (OpenAPI 3.0) + if (schema.nullable === true && ctx.version === "openapi-3.0") { + baseSchema = z.nullable(baseSchema); + } + // Handle readOnly + if (schema.readOnly === true) { + baseSchema = z.readonly(baseSchema); + } + // Apply `default` so it wraps the fully-composed schema. This ensures `parse(undefined) -> default` works regardless of which branch of `convertBaseSchema` produced the inner schema (enum/const/not/typed/etc.). + if (schema.default !== undefined) { + baseSchema = baseSchema.default(schema.default); + } + // Collect non-description annotation metadata into the user-supplied registry. Description is handled separately below via `.describe()` to preserve the contract that `schema.description` reads from globalRegistry. + const extraMeta = {}; + const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"]; + for (const key of coreMetadataKeys) { + if (key in schema) { + extraMeta[key] = schema[key]; + } + } + const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"]; + for (const key of contentMetadataKeys) { + if (key in schema) { + extraMeta[key] = schema[key]; + } + } + // `propertyNames` and `contains` are enforced by a guard, which `toJSONSchema` cannot infer, so the original keyword is carried as metadata to keep the round-trip lossless. Only where it was actually applied: on any other type it is inert, and on a `$ref` the metadata would land on the target every reference shares. A carried subschema is copied verbatim while the root `$defs` stay behind, so one holding a `$ref` is dropped rather than emitted as a dangling reference. + if (schema.type === "object" && schema.$ref === undefined) { + if (schema.propertyNames !== undefined && !containsRef(schema.propertyNames)) { + extraMeta.propertyNames = schema.propertyNames; + } + for (const key of ["minProperties", "maxProperties"]) { + if (schema[key] !== undefined) + extraMeta[key] = schema[key]; + } + } + if (schema.type === "array" && schema.$ref === undefined) { + if (schema.contains !== undefined && !containsRef(schema.contains)) { + extraMeta.contains = schema.contains; + } + for (const key of ["uniqueItems", "minContains", "maxContains"]) { + if (schema[key] !== undefined) + extraMeta[key] = schema[key]; + } + } + for (const key of Object.keys(schema)) { + if (!RECOGNIZED_KEYS.has(key)) { + assignProp(extraMeta, key, schema[key]); + } + } + if (Object.keys(extraMeta).length > 0) { + ctx.registry.add(baseSchema, extraMeta); + } + // Apply description last. `.describe()` clones the schema and sets `_zod.parent` on the clone, so registry lookups on the returned reference still resolve `extraMeta` via parent inheritance. + if (schema.description) { + baseSchema = baseSchema.describe(schema.description); + } + return baseSchema; +} +/** + * Converts a JSON Schema to a Zod schema. This function should be considered semi-experimental. It's behavior is liable to change. */ +export function fromJSONSchema(schema, params) { + // Handle boolean schemas + if (typeof schema === "boolean") { + return schema ? z.any() : z.never(); + } + // Normalize input via a JSON round-trip. This guarantees the converter walks a plain, finite, JSON-valid object graph: cyclic inputs fail here, getter/Proxy-based properties are materialized into static values, and class instances collapse to plain objects. + let normalized; + try { + normalized = JSON.parse(JSON.stringify(schema)); + } + catch { + throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas"); + } + const version = detectVersion(normalized, params?.defaultTarget); + const defs = (normalized.$defs || normalized.definitions || {}); + const ctx = { + version, + defs, + refs: new Map(), + processing: new Set(), + rootSchema: normalized, + registry: params?.registry ?? globalRegistry, + }; + return convertSchema(normalized, ctx); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts new file mode 100644 index 000000000..09b530fe2 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.d.cts @@ -0,0 +1,8 @@ +import type * as core from "../core/index.cjs"; +import * as schemas from "./schemas.cjs"; +export type input = core.input; +export type output = core.output; +/** Returns a copy of the schema with every pipe replaced by its input side. A codec's checks are dropped: they constrain the decoded value the input side never produces. */ +export declare function input(schema: T): schemas.ZodType, core.input>; +/** Returns a copy of the schema with every pipe replaced by its output side, carrying over the pipe's own checks. */ +export declare function output(schema: T): schemas.ZodType, core.output>; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js new file mode 100644 index 000000000..4a0002688 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/in-out.js @@ -0,0 +1,36 @@ +import { clone, mergeDefs } from "../core/util.js"; +import { visit } from "../core/visit.js"; +import * as schemas from "./schemas.js"; +/** Appends a pipe's own checks to the side that replaces it, mirroring how `.check()` clones. */ +function withChecks(side, checks) { + if (!checks?.length) + return side; + const def = side._zod.def; + return clone(side, mergeDefs(def, { checks: [...(def.checks ?? []), ...checks] }), { parent: true }); +} +/** The out side, carrying the pipe's own checks: those run against the decoded value, which is what `out` produces. */ +function outSide(def) { + return withChecks(def.out, def.checks); +} +/** The in side. `z.preprocess` pipes a transform into a schema, and a bare transform validates nothing, so the schema it feeds is the real input side — the resolution `toJSONSchema` already makes for this case. */ +function inSide(def) { + return def.in._zod.traits.has("$ZodTransform") ? outSide(def) : def.in; +} +/** Returns a copy of the schema with every pipe replaced by its input side. A codec's checks are dropped: they constrain the decoded value the input side never produces. */ +export function input(schema) { + return visit(schema, { + pipe: (s) => inSide(s._zod.def), + // A default value belongs to the output side, so a rewritten inner type leaves it stranded. `.default()` widens the declared input type with `undefined`, and `optional` is what carries that across. + default: (s, rewritten) => (rewritten ? schemas.optional(s._zod.def.innerType) : s), + // A catch value is output-side too, but `.catch()` leaves the declared input type alone, so the inner schema stands on its own. + catch: (s, rewritten) => (rewritten ? s._zod.def.innerType : s), + }); +} +/** Returns a copy of the schema with every pipe replaced by its output side, carrying over the pipe's own checks. */ +export function output(schema) { + return visit(schema, { + pipe: (s) => outSide(s._zod.def), + // A prefault value is fed through the schema, which makes it input-side, so a rewritten inner type leaves it stranded. + prefault: (s, rewritten) => (rewritten ? s._zod.def.innerType : s), + }); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts new file mode 100644 index 000000000..b5fc2a17b --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/index.d.cts @@ -0,0 +1,3 @@ +import * as z from "./external.cjs"; +export * from "./external.cjs"; +export { z, z as default }; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts new file mode 100644 index 000000000..db764d9f8 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.d.cts @@ -0,0 +1,7 @@ +import * as core from "../core/index.cjs"; +import { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.cjs"; +export { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.cjs"; +export declare function datetime(params?: string | core.$ZodISODateTimeParams): ZodISODateTime; +export declare function date(params?: string | core.$ZodISODateParams): ZodISODate; +export declare function time(params?: string | core.$ZodISOTimeParams): ZodISOTime; +export declare function duration(params?: string | core.$ZodISODurationParams): ZodISODuration; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/iso.js b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.js new file mode 100644 index 000000000..d38f24ddd --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/iso.js @@ -0,0 +1,15 @@ +import * as core from "../core/index.js"; +import { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.js"; +export { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from "./schemas.js"; +export function datetime(params) { + return core._isoDateTime(ZodISODateTime, params); +} +export function date(params) { + return core._isoDate(ZodISODate, params); +} +export function time(params) { + return core._isoTime(ZodISOTime, params); +} +export function duration(params) { + return core._isoDuration(ZodISODuration, params); +} diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts new file mode 100644 index 000000000..408566b52 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.d.cts @@ -0,0 +1,44 @@ +import * as core from "../core/index.cjs"; +import { type ZodError } from "./errors.cjs"; +export type ZodSafeParseResult = ZodSafeParseSuccess | ZodSafeParseError; +export type ZodSafeParseSuccess = { + success: true; + data: T; + error?: never; +}; +export type ZodSafeParseError = { + success: false; + data?: never; + error: ZodError; +}; +export declare const parse: (schema: T, value: unknown, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => core.output; +export declare const parseAsync: (schema: T, value: unknown, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => Promise>; +export declare const safeParse: (schema: T, value: unknown, _ctx?: core.ParseContext) => ZodSafeParseResult>; +export declare const safeParseAsync: (schema: T, value: unknown, _ctx?: core.ParseContext) => Promise>>; +export { validate, validateAsync } from "../core/index.cjs"; +export declare const encode: (schema: T, value: core.output, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => core.input; +export declare const decode: (schema: T, value: core.input, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => core.output; +export declare const encodeAsync: (schema: T, value: core.output, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => Promise>; +export declare const decodeAsync: (schema: T, value: core.input, _ctx?: core.ParseContext, _params?: { + callee?: core.util.AnyFunc; + Err?: core.$ZodErrorClass; +}) => Promise>; +export declare const safeEncode: (schema: T, value: core.output, _ctx?: core.ParseContext) => ZodSafeParseResult>; +export declare const safeDecode: (schema: T, value: core.input, _ctx?: core.ParseContext) => ZodSafeParseResult>; +export declare const safeEncodeAsync: (schema: T, value: core.output, _ctx?: core.ParseContext) => Promise>>; +export declare const safeDecodeAsync: (schema: T, value: core.input, _ctx?: core.ParseContext) => Promise>>; diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/parse.js b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.js new file mode 100644 index 000000000..514aacc64 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/parse.js @@ -0,0 +1,16 @@ +import * as core from "../core/index.js"; +import { ZodRealError } from "./errors.js"; +export const parse = /* @__PURE__ */ core._parse(ZodRealError); +export const parseAsync = /* @__PURE__ */ core._parseAsync(ZodRealError); +export const safeParse = /* @__PURE__ */ core._safeParse(ZodRealError); +export const safeParseAsync = /* @__PURE__ */ core._safeParseAsync(ZodRealError); +export { validate, validateAsync } from "../core/index.js"; +// Codec functions +export const encode = /* @__PURE__ */ core._encode(ZodRealError); +export const decode = /* @__PURE__ */ core._decode(ZodRealError); +export const encodeAsync = /* @__PURE__ */ core._encodeAsync(ZodRealError); +export const decodeAsync = /* @__PURE__ */ core._decodeAsync(ZodRealError); +export const safeEncode = /* @__PURE__ */ core._safeEncode(ZodRealError); +export const safeDecode = /* @__PURE__ */ core._safeDecode(ZodRealError); +export const safeEncodeAsync = /* @__PURE__ */ core._safeEncodeAsync(ZodRealError); +export const safeDecodeAsync = /* @__PURE__ */ core._safeDecodeAsync(ZodRealError); diff --git a/tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts b/tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts new file mode 100644 index 000000000..226a3ae25 --- /dev/null +++ b/tests/fixtures/npm/node_modules/zod/v4/classic/schemas.d.cts @@ -0,0 +1,816 @@ +import type { $ZodBigIntFormats } from "../core/checks.cjs"; +import * as core from "../core/index.cjs"; +import { util, type $ZodNumberFormats } from "../core/index.cjs"; +import type { StandardSchemaWithJSONProps } from "../core/standard-schema.cjs"; +import * as parse from "./parse.cjs"; +export type ZodStandardSchemaWithJSON = StandardSchemaWithJSONProps, core.output>; +export interface ZodType = core.$ZodTypeInternals> extends core.$ZodType { + def: Internals["def"]; + type: Internals["def"]["type"]; + /** @deprecated Use `.def` instead. */ + _def: Internals["def"]; + /** @deprecated Use `z.output` instead. */ + _output: Internals["output"]; + /** @deprecated Use `z.input` instead. */ + _input: Internals["input"]; + "~standard": ZodStandardSchemaWithJSON; + /** Converts this schema to a JSON Schema representation. */ + toJSONSchema(params?: core.ToJSONSchemaParams): core.ZodStandardJSONSchemaPayload; + check(...checks: (core.CheckFn> | core.$ZodCheck>)[]): this; + with(...checks: (core.CheckFn> | core.$ZodCheck>)[]): this; + clone(def?: Internals["def"], params?: { + parent: boolean; + }): this; + register(registry: R, ...meta: this extends R["_schema"] ? undefined extends R["_meta"] ? [core.$replace?] : [core.$replace] : ["Incompatible schema"]): this; + brand(value?: T): PropertyKey extends T ? this : core.$ZodBranded; + parse(data: unknown, params?: core.ParseContext): core.output; + safeParse(data: unknown, params?: core.ParseContext): parse.ZodSafeParseResult>; + parseAsync(data: unknown, params?: core.ParseContext): Promise>; + safeParseAsync(data: unknown, params?: core.ParseContext): Promise>>; + spa: (data: unknown, params?: core.ParseContext) => Promise>>; + validate(data: unknown, params?: core.ParseContext): data is core.input; + validateAsync(data: unknown, params?: core.ParseContext): Promise; + encode(data: core.output, params?: core.ParseContext): core.input; + decode(data: core.input, params?: core.ParseContext): core.output; + encodeAsync(data: core.output, params?: core.ParseContext): Promise>; + decodeAsync(data: core.input, params?: core.ParseContext): Promise>; + safeEncode(data: core.output, params?: core.ParseContext): parse.ZodSafeParseResult>; + safeDecode(data: core.input, params?: core.ParseContext): parse.ZodSafeParseResult>; + safeEncodeAsync(data: core.output, params?: core.ParseContext): Promise>>; + safeDecodeAsync(data: core.input, params?: core.ParseContext): Promise>>; + refine) => unknown | Promise>(check: Ch, params?: string | core.$ZodCustomParams): Ch extends (arg: any) => arg is infer R ? this & ZodType> : this; + superRefine(refinement: (arg: core.output, ctx: core.$RefinementCtx>) => void | Promise, params?: core.$ZodSuperRefineParams): this; + overwrite(fn: (x: core.output) => core.output): this; + optional(): ZodOptional; + exactOptional(): ZodExactOptional; + nonoptional(params?: string | core.$ZodNonOptionalParams): ZodNonOptional; + nullable(): ZodNullable; + nullish(): ZodOptional>; + default(def: util.NoUndefined>): ZodDefault; + default(def: () => util.NoUndefined>): ZodDefault; + prefault(def: () => core.input): ZodPrefault; + prefault(def: core.input): ZodPrefault; + array(): ZodArray; + or(option: T): ZodUnion<[this, T]>; + and(incoming: T): ZodIntersection; + transform(transform: (arg: core.output, ctx: core.$RefinementCtx>) => NewOut | Promise): ZodPipe, core.output>>; + catch(def: core.output): ZodCatch; + catch(def: (ctx: core.$ZodCatchCtx) => core.output): ZodCatch; + pipe>>(target: T | core.$ZodType>): ZodPipe; + readonly(): ZodReadonly; + /** Returns a new instance that has been registered in `z.globalRegistry` with the specified description */ + describe(description: string): this; + description?: string; + /** Returns the metadata associated with this instance in `z.globalRegistry` */ + meta(): core.$replace | undefined; + /** Returns a new instance that has been registered in `z.globalRegistry` with the specified metadata */ + meta(data: core.$replace): this; + /** @deprecated Try safe-parsing `undefined` (this is what `isOptional` does internally): + * + * ```ts + * const schema = z.string().optional(); + * const isOptional = schema.safeParse(undefined).success; // true + * ``` + */ + isOptional(): boolean; + /** + * @deprecated Try safe-parsing `null` (this is what `isNullable` does internally): + * + * ```ts + * const schema = z.string().nullable(); + * const isNullable = schema.safeParse(null).success; // true + * ``` + */ + isNullable(): boolean; + apply(fn: (schema: this, ...args: TArgs) => T, ...args: TArgs): T; +} +export interface _ZodType extends ZodType { +} +export declare const ZodType: core.$constructor; +export interface _ZodString = core.$ZodStringInternals> extends _ZodType { + format: string | null; + minLength: number | null; + maxLength: number | null; + regex(regex: RegExp, params?: string | core.$ZodCheckRegexParams): this; + includes(value: string, params?: string | core.$ZodCheckIncludesParams): this; + startsWith(value: string, params?: string | core.$ZodCheckStartsWithParams): this; + endsWith(value: string, params?: string | core.$ZodCheckEndsWithParams): this; + min(minLength: number, params?: string | core.$ZodCheckMinLengthParams): this; + max(maxLength: number, params?: string | core.$ZodCheckMaxLengthParams): this; + length(len: number, params?: string | core.$ZodCheckLengthEqualsParams): this; + nonempty(params?: string | core.$ZodCheckMinLengthParams): this; + lowercase(params?: string | core.$ZodCheckLowerCaseParams): this; + uppercase(params?: string | core.$ZodCheckUpperCaseParams): this; + trim(): this; + normalize(form?: "NFC" | "NFD" | "NFKC" | "NFKD" | (string & {})): this; + toLowerCase(): this; + toUpperCase(): this; + slugify(): this; +} +/** @internal */ +export declare const _ZodString: core.$constructor<_ZodString>; +export interface ZodString extends _ZodString> { + /** @deprecated Use `z.email()` instead. */ + email(params?: string | core.$ZodCheckEmailParams): this; + /** @deprecated Use `z.url()` instead. */ + url(params?: string | core.$ZodCheckURLParams): this; + /** @deprecated Use `z.jwt()` instead. */ + jwt(params?: string | core.$ZodCheckJWTParams): this; + /** @deprecated Use `z.emoji()` instead. */ + emoji(params?: string | core.$ZodCheckEmojiParams): this; + /** @deprecated Use `z.guid()` instead. */ + guid(params?: string | core.$ZodCheckGUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuid(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuidv4(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuidv6(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.uuid()` instead. */ + uuidv7(params?: string | core.$ZodCheckUUIDParams): this; + /** @deprecated Use `z.nanoid()` instead. */ + nanoid(params?: string | core.$ZodCheckNanoIDParams): this; + /** @deprecated Use `z.guid()` instead. */ + guid(params?: string | core.$ZodCheckGUIDParams): this; + /** + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use `z.cuid2()` instead. + * See https://github.com/paralleldrive/cuid. + */ + cuid(params?: string | core.$ZodCheckCUIDParams): this; + /** @deprecated Use `z.cuid2()` instead. */ + cuid2(params?: string | core.$ZodCheckCUID2Params): this; + /** @deprecated Use `z.ulid()` instead. */ + ulid(params?: string | core.$ZodCheckULIDParams): this; + /** @deprecated Use `z.base64()` instead. */ + base64(params?: string | core.$ZodCheckBase64Params): this; + /** @deprecated Use `z.base64url()` instead. */ + base64url(params?: string | core.$ZodCheckBase64URLParams): this; + /** @deprecated Use `z.xid()` instead. */ + xid(params?: string | core.$ZodCheckXIDParams): this; + /** @deprecated Use `z.ksuid()` instead. */ + ksuid(params?: string | core.$ZodCheckKSUIDParams): this; + /** @deprecated Use `z.ipv4()` instead. */ + ipv4(params?: string | core.$ZodCheckIPv4Params): this; + /** @deprecated Use `z.ipv6()` instead. */ + ipv6(params?: string | core.$ZodCheckIPv6Params): this; + /** @deprecated Use `z.cidrv4()` instead. */ + cidrv4(params?: string | core.$ZodCheckCIDRv4Params): this; + /** @deprecated Use `z.cidrv6()` instead. */ + cidrv6(params?: string | core.$ZodCheckCIDRv6Params): this; + /** @deprecated Use `z.e164()` instead. */ + e164(params?: string | core.$ZodCheckE164Params): this; + /** @deprecated Use `z.iso.datetime()` instead. */ + datetime(params?: string | core.$ZodCheckISODateTimeParams): this; + /** @deprecated Use `z.iso.date()` instead. */ + date(params?: string | core.$ZodCheckISODateParams): this; + /** @deprecated Use `z.iso.time()` instead. */ + time(params?: string | core.$ZodCheckISOTimeParams): this; + /** @deprecated Use `z.iso.duration()` instead. */ + duration(params?: string | core.$ZodCheckISODurationParams): this; +} +export declare const ZodString: core.$constructor; +export declare function string(params?: string | core.$ZodStringParams): ZodString; +export declare function string(params?: string | core.$ZodStringParams): core.$ZodType; +export interface ZodStringFormat extends _ZodString> { +} +export declare const ZodStringFormat: core.$constructor; +export interface ZodISODateTime extends ZodStringFormat { + _zod: core.$ZodISODateTimeInternals; +} +export declare const ZodISODateTime: core.$constructor; +export interface ZodISODate extends ZodStringFormat { + _zod: core.$ZodISODateInternals; +} +export declare const ZodISODate: core.$constructor; +export interface ZodISOTime extends ZodStringFormat { + _zod: core.$ZodISOTimeInternals; +} +export declare const ZodISOTime: core.$constructor; +export interface ZodISODuration extends ZodStringFormat { + _zod: core.$ZodISODurationInternals; +} +export declare const ZodISODuration: core.$constructor; +export interface ZodEmail extends ZodStringFormat<"email"> { + _zod: core.$ZodEmailInternals; +} +export declare const ZodEmail: core.$constructor; +export declare function email(params?: string | core.$ZodEmailParams): ZodEmail; +export interface ZodGUID extends ZodStringFormat<"guid"> { + _zod: core.$ZodGUIDInternals; +} +export declare const ZodGUID: core.$constructor; +export declare function guid(params?: string | core.$ZodGUIDParams): ZodGUID; +export interface ZodUUID extends ZodStringFormat<"uuid"> { + _zod: core.$ZodUUIDInternals; +} +export declare const ZodUUID: core.$constructor; +export declare function uuid(params?: string | core.$ZodUUIDParams): ZodUUID; +export declare function uuidv4(params?: string | core.$ZodUUIDv4Params): ZodUUID; +export declare function uuidv6(params?: string | core.$ZodUUIDv6Params): ZodUUID; +export declare function uuidv7(params?: string | core.$ZodUUIDv7Params): ZodUUID; +export interface ZodURL extends ZodStringFormat<"url"> { + _zod: core.$ZodURLInternals; +} +export declare const ZodURL: core.$constructor; +export declare function url(params?: string | core.$ZodURLParams): ZodURL; +export declare function httpUrl(params?: string | Omit): ZodURL; +export interface ZodEmoji extends ZodStringFormat<"emoji"> { + _zod: core.$ZodEmojiInternals; +} +export declare const ZodEmoji: core.$constructor; +export declare function emoji(params?: string | core.$ZodEmojiParams): ZodEmoji; +export interface ZodNanoID extends ZodStringFormat<"nanoid"> { + _zod: core.$ZodNanoIDInternals; +} +export declare const ZodNanoID: core.$constructor; +export declare function nanoid(params?: string | core.$ZodNanoIDParams): ZodNanoID; +/** + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use {@link ZodCUID2} instead. + * See https://github.com/paralleldrive/cuid. + */ +export interface ZodCUID extends ZodStringFormat<"cuid"> { + _zod: core.$ZodCUIDInternals; +} +/** + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use {@link ZodCUID2} instead. + * See https://github.com/paralleldrive/cuid. + */ +export declare const ZodCUID: core.$constructor; +/** + * Validates a CUID v1 string. + * + * @deprecated CUID v1 is deprecated by its authors due to information leakage + * (timestamps embedded in the id). Use {@link cuid2 | `z.cuid2()`} instead. + * See https://github.com/paralleldrive/cuid. + */ +export declare function cuid(params?: string | core.$ZodCUIDParams): ZodCUID; +export interface ZodCUID2 extends ZodStringFormat<"cuid2"> { + _zod: core.$ZodCUID2Internals; +} +export declare const ZodCUID2: core.$constructor; +export declare function cuid2(params?: string | core.$ZodCUID2Params): ZodCUID2; +export interface ZodULID extends ZodStringFormat<"ulid"> { + _zod: core.$ZodULIDInternals; +} +export declare const ZodULID: core.$constructor; +export declare function ulid(params?: string | core.$ZodULIDParams): ZodULID; +export interface ZodXID extends ZodStringFormat<"xid"> { + _zod: core.$ZodXIDInternals; +} +export declare const ZodXID: core.$constructor; +export declare function xid(params?: string | core.$ZodXIDParams): ZodXID; +export interface ZodKSUID extends ZodStringFormat<"ksuid"> { + _zod: core.$ZodKSUIDInternals; +} +export declare const ZodKSUID: core.$constructor; +export declare function ksuid(params?: string | core.$ZodKSUIDParams): ZodKSUID; +export interface ZodIPv4 extends ZodStringFormat<"ipv4"> { + _zod: core.$ZodIPv4Internals; +} +export declare const ZodIPv4: core.$constructor; +export declare function ipv4(params?: string | core.$ZodIPv4Params): ZodIPv4; +export interface ZodMAC extends ZodStringFormat<"mac"> { + _zod: core.$ZodMACInternals; +} +export declare const ZodMAC: core.$constructor; +export declare function mac(params?: string | core.$ZodMACParams): ZodMAC; +export interface ZodIPv6 extends ZodStringFormat<"ipv6"> { + _zod: core.$ZodIPv6Internals; +} +export declare const ZodIPv6: core.$constructor; +export declare function ipv6(params?: string | core.$ZodIPv6Params): ZodIPv6; +export interface ZodCIDRv4 extends ZodStringFormat<"cidrv4"> { + _zod: core.$ZodCIDRv4Internals; +} +export declare const ZodCIDRv4: core.$constructor; +export declare function cidrv4(params?: string | core.$ZodCIDRv4Params): ZodCIDRv4; +export interface ZodCIDRv6 extends ZodStringFormat<"cidrv6"> { + _zod: core.$ZodCIDRv6Internals; +} +export declare const ZodCIDRv6: core.$constructor; +export declare function cidrv6(params?: string | core.$ZodCIDRv6Params): ZodCIDRv6; +export interface ZodBase64 extends ZodStringFormat<"base64"> { + _zod: core.$ZodBase64Internals; +} +export declare const ZodBase64: core.$constructor; +export declare function base64(params?: string | core.$ZodBase64Params): ZodBase64; +export interface ZodBase64URL extends ZodStringFormat<"base64url"> { + _zod: core.$ZodBase64URLInternals; +} +export declare const ZodBase64URL: core.$constructor; +export declare function base64url(params?: string | core.$ZodBase64URLParams): ZodBase64URL; +export interface ZodE164 extends ZodStringFormat<"e164"> { + _zod: core.$ZodE164Internals; +} +export declare const ZodE164: core.$constructor; +export declare function e164(params?: string | core.$ZodE164Params): ZodE164; +export interface ZodCreditCard extends ZodStringFormat<"credit_card"> { + _zod: core.$ZodCreditCardInternals; +} +export declare const ZodCreditCard: core.$constructor; +export declare function creditCard(params?: string | core.$ZodCreditCardParams): ZodCreditCard; +export interface ZodIBAN extends ZodStringFormat<"iban"> { + _zod: core.$ZodIBANInternals; +} +export declare const ZodIBAN: core.$constructor; +export declare function iban(params?: string | core.$ZodIBANParams): ZodIBAN; +export interface ZodJWT extends ZodStringFormat<"jwt"> { + _zod: core.$ZodJWTInternals; +} +export declare const ZodJWT: core.$constructor; +export declare function jwt(params?: string | core.$ZodJWTParams): ZodJWT; +export interface ZodCustomStringFormat extends ZodStringFormat, core.$ZodCustomStringFormat { + _zod: core.$ZodCustomStringFormatInternals; + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodCustomStringFormat: core.$constructor; +export declare function stringFormat(format: Format, fnOrRegex: ((arg: string) => util.MaybeAsync) | RegExp, _params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat; +export declare function hostname(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"hostname">; +export declare function hex(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"hex">; +export declare function currencyCode(_params?: string | core.$ZodStringFormatParams): ZodCustomStringFormat<"currency_code">; +export declare function hash(alg: Alg, params?: { + enc?: Enc; +} & core.$ZodStringFormatParams): ZodCustomStringFormat<`${Alg}_${Enc}`>; +export interface _ZodNumber extends _ZodType { + gt(value: number, params?: string | core.$ZodCheckGreaterThanParams): this; + /** Identical to .min() */ + gte(value: number, params?: string | core.$ZodCheckGreaterThanParams): this; + min(value: number, params?: string | core.$ZodCheckGreaterThanParams): this; + lt(value: number, params?: string | core.$ZodCheckLessThanParams): this; + /** Identical to .max() */ + lte(value: number, params?: string | core.$ZodCheckLessThanParams): this; + max(value: number, params?: string | core.$ZodCheckLessThanParams): this; + /** Consider `z.int()` instead. This API is considered *legacy*; it will never be removed but a better alternative exists. */ + int(params?: string | core.$ZodCheckNumberFormatParams): this; + /** @deprecated This is now identical to `.int()`. Only numbers in the safe integer range are accepted. */ + safe(params?: string | core.$ZodCheckNumberFormatParams): this; + positive(params?: string | core.$ZodCheckGreaterThanParams): this; + nonnegative(params?: string | core.$ZodCheckGreaterThanParams): this; + negative(params?: string | core.$ZodCheckLessThanParams): this; + nonpositive(params?: string | core.$ZodCheckLessThanParams): this; + multipleOf(value: number, params?: string | core.$ZodCheckMultipleOfParams): this; + /** @deprecated Use `.multipleOf()` instead. */ + step(value: number, params?: string | core.$ZodCheckMultipleOfParams): this; + /** @deprecated In v4 and later, z.number() does not allow infinite values by default. This is a no-op. */ + finite(params?: unknown): this; + minValue: number | null; + maxValue: number | null; + /** @deprecated Check the `format` property instead. */ + isInt: boolean; + /** @deprecated Number schemas no longer accept infinite values, so this always returns `true`. */ + isFinite: boolean; + format: string | null; +} +export interface ZodNumber extends _ZodNumber> { +} +export declare const ZodNumber: core.$constructor; +export declare function number(params?: string | core.$ZodNumberParams): ZodNumber; +export interface ZodNumberFormat extends ZodNumber { + _zod: core.$ZodNumberFormatInternals; +} +export declare const ZodNumberFormat: core.$constructor; +export interface ZodInt extends ZodNumberFormat<"safeint"> { +} +export declare function int(params?: string | core.$ZodCheckNumberFormatParams): ZodInt; +export interface ZodFloat32 extends ZodNumberFormat<"float32"> { +} +export declare function float32(params?: string | core.$ZodCheckNumberFormatParams): ZodFloat32; +export interface ZodFloat64 extends ZodNumberFormat<"float64"> { +} +export declare function float64(params?: string | core.$ZodCheckNumberFormatParams): ZodFloat64; +export interface ZodInt32 extends ZodNumberFormat<"int32"> { +} +export declare function int32(params?: string | core.$ZodCheckNumberFormatParams): ZodInt32; +export interface ZodUInt32 extends ZodNumberFormat<"uint32"> { +} +export declare function uint32(params?: string | core.$ZodCheckNumberFormatParams): ZodUInt32; +export interface _ZodBoolean extends _ZodType { +} +export interface ZodBoolean extends _ZodBoolean> { +} +export declare const ZodBoolean: core.$constructor; +export declare function boolean(params?: string | core.$ZodBooleanParams): ZodBoolean; +export interface _ZodBigInt extends _ZodType { + gte(value: bigint, params?: string | core.$ZodCheckGreaterThanParams): this; + /** Alias of `.gte()` */ + min(value: bigint, params?: string | core.$ZodCheckGreaterThanParams): this; + gt(value: bigint, params?: string | core.$ZodCheckGreaterThanParams): this; + /** Alias of `.lte()` */ + lte(value: bigint, params?: string | core.$ZodCheckLessThanParams): this; + max(value: bigint, params?: string | core.$ZodCheckLessThanParams): this; + lt(value: bigint, params?: string | core.$ZodCheckLessThanParams): this; + positive(params?: string | core.$ZodCheckGreaterThanParams): this; + negative(params?: string | core.$ZodCheckLessThanParams): this; + nonpositive(params?: string | core.$ZodCheckLessThanParams): this; + nonnegative(params?: string | core.$ZodCheckGreaterThanParams): this; + multipleOf(value: bigint, params?: string | core.$ZodCheckMultipleOfParams): this; + minValue: bigint | null; + maxValue: bigint | null; + format: string | null; +} +export interface ZodBigInt extends _ZodBigInt> { +} +export declare const ZodBigInt: core.$constructor; +export declare function bigint(params?: string | core.$ZodBigIntParams): ZodBigInt; +export interface ZodBigIntFormat extends ZodBigInt { + _zod: core.$ZodBigIntFormatInternals; +} +export declare const ZodBigIntFormat: core.$constructor; +export interface ZodInt64 extends ZodBigIntFormat<"int64"> { +} +export declare function int64(params?: string | core.$ZodBigIntFormatParams): ZodInt64; +export interface ZodUInt64 extends ZodBigIntFormat<"uint64"> { +} +export declare function uint64(params?: string | core.$ZodBigIntFormatParams): ZodUInt64; +export interface ZodSymbol extends _ZodType { +} +export declare const ZodSymbol: core.$constructor; +export declare function symbol(params?: string | core.$ZodSymbolParams): ZodSymbol; +export interface ZodUndefined extends _ZodType { +} +export declare const ZodUndefined: core.$constructor; +declare function _undefined(params?: string | core.$ZodUndefinedParams): ZodUndefined; +export { _undefined as undefined }; +export interface ZodNull extends _ZodType { +} +export declare const ZodNull: core.$constructor; +declare function _null(params?: string | core.$ZodNullParams): ZodNull; +export { _null as null }; +export interface ZodAny extends _ZodType { +} +export declare const ZodAny: core.$constructor; +export declare function any(): ZodAny; +export interface ZodUnknown extends _ZodType { +} +export declare const ZodUnknown: core.$constructor; +export declare function unknown(): ZodUnknown; +export interface ZodNever extends _ZodType { +} +export declare const ZodNever: core.$constructor; +export declare function never(params?: string | core.$ZodNeverParams): ZodNever; +export interface ZodVoid extends _ZodType { +} +export declare const ZodVoid: core.$constructor; +declare function _void(params?: string | core.$ZodVoidParams): ZodVoid; +export { _void as void }; +export interface _ZodDate extends _ZodType { + min(value: number | Date, params?: string | core.$ZodCheckGreaterThanParams): this; + max(value: number | Date, params?: string | core.$ZodCheckLessThanParams): this; + /** @deprecated Not recommended. */ + minDate: Date | null; + /** @deprecated Not recommended. */ + maxDate: Date | null; +} +export interface ZodDate extends _ZodDate> { +} +export declare const ZodDate: core.$constructor; +export declare function date(params?: string | core.$ZodDateParams): ZodDate; +export interface ZodArray extends _ZodType>, core.$ZodArray { + element: T; + min(minLength: number, params?: string | core.$ZodCheckMinLengthParams): this; + nonempty(params?: string | core.$ZodCheckMinLengthParams): this; + max(maxLength: number, params?: string | core.$ZodCheckMaxLengthParams): this; + length(len: number, params?: string | core.$ZodCheckLengthEqualsParams): this; + unwrap(): T; + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodArray: core.$constructor; +export declare function array(element: T, params?: string | core.$ZodArrayParams): ZodArray; +export declare function keyof(schema: T): ZodEnum>; +export type SafeExtendShape = { + [K in keyof Ext]: K extends keyof Base ? core.output extends core.output ? core.input extends core.input ? Ext[K] : never : never : Ext[K]; +}; +export interface ZodObject< +/** @ts-ignore Cast variance */ +out Shape extends core.$ZodShape = core.$ZodLooseShape, out Config extends core.$ZodObjectConfig = core.$strip> extends _ZodType>, core.$ZodObject { + "~standard": ZodStandardSchemaWithJSON; + shape: Shape; + keyof(): ZodEnum>; + /** Define a schema to validate all unrecognized keys. This overrides the existing strict/loose behavior. */ + catchall(schema: T): ZodObject>; + /** @deprecated Use `z.looseObject()` or `.loose()` instead. */ + passthrough(): ZodObject; + /** Consider `z.looseObject(A.shape)` instead */ + loose(): ZodObject; + /** Consider `z.strictObject(A.shape)` instead */ + strict(): ZodObject; + /** This is the default behavior. This method call is likely unnecessary. */ + strip(): ZodObject; + extend(shape: U): ZodObject>, Config>; + safeExtend(shape: SafeExtendShape & Partial>): ZodObject>, Config>; + /** + * @deprecated Use [`A.extend(B.shape)`](https://zod.dev/api?id=extend) instead. + */ + merge(other: U): ZodObject, U["_zod"]["config"]>; + pick>(mask: M & Record, never>): ZodObject>>, Config>; + omit>(mask: M & Record, never>): ZodObject>>, Config>; + partial(): ZodObject<{ + -readonly [k in keyof Shape]: ZodOptional; + }, Config>; + partial>(mask: M & Record, never>): ZodObject<{ + -readonly [k in keyof Shape]: k extends keyof M ? ZodOptional : Shape[k]; + }, Config>; + exactPartial(): ZodObject<{ + -readonly [k in keyof Shape]: ZodExactOptional; + }, Config>; + exactPartial>(mask: M & Record, never>): ZodObject<{ + -readonly [k in keyof Shape]: k extends keyof M ? ZodExactOptional : Shape[k]; + }, Config>; + required(): ZodObject<{ + -readonly [k in keyof Shape]: ZodNonOptional; + }, Config>; + required>(mask: M & Record, never>): ZodObject<{ + -readonly [k in keyof Shape]: k extends keyof M ? ZodNonOptional : Shape[k]; + }, Config>; +} +export declare const ZodObject: core.$constructor; +export declare function object>>(shape?: T, params?: string | core.$ZodObjectParams): ZodObject, core.$strip>; +export declare function strictObject(shape: T, params?: string | core.$ZodObjectParams): ZodObject, core.$strict>; +export declare function looseObject(shape: T, params?: string | core.$ZodObjectParams): ZodObject, core.$loose>; +export interface ZodUnion extends _ZodType>, core.$ZodUnion { + "~standard": ZodStandardSchemaWithJSON; + options: T; +} +export declare const ZodUnion: core.$constructor; +export declare function union(options: T, params?: string | core.$ZodUnionParams): ZodUnion; +export interface ZodXor extends _ZodType>, core.$ZodXor { + "~standard": ZodStandardSchemaWithJSON; + options: T; +} +export declare const ZodXor: core.$constructor; +/** Creates an exclusive union (XOR) where exactly one option must match. + * Unlike regular unions that succeed when any option matches, xor fails if + * zero or more than one option matches the input. */ +export declare function xor(options: T, params?: string | core.$ZodXorParams): ZodXor; +export interface ZodDiscriminatedUnion extends ZodUnion, core.$ZodDiscriminatedUnion { + "~standard": ZodStandardSchemaWithJSON; + _zod: core.$ZodDiscriminatedUnionInternals; + def: core.$ZodDiscriminatedUnionDef; +} +export declare const ZodDiscriminatedUnion: core.$constructor; +export declare function discriminatedUnion(discriminator: Disc, options: Types, params?: string | core.$ZodDiscriminatedUnionParams): ZodDiscriminatedUnion; +export interface ZodIntersection extends _ZodType>, core.$ZodIntersection { + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodIntersection: core.$constructor; +export declare function intersection(left: T, right: U): ZodIntersection; +export interface ZodTuple extends _ZodType>, core.$ZodTuple { + "~standard": ZodStandardSchemaWithJSON; + rest(rest: Rest): ZodTuple; + partial(): ZodTuple<{ + -readonly [k in keyof T]: ZodOptional; + }, Rest>; +} +export declare const ZodTuple: core.$constructor; +export declare function tuple(items: T, params?: string | core.$ZodTupleParams): ZodTuple; +export declare function tuple(items: T, rest: Rest, params?: string | core.$ZodTupleParams): ZodTuple; +export declare function tuple(items: [], params?: string | core.$ZodTupleParams): ZodTuple<[], null>; +export interface ZodRecord extends _ZodType>, core.$ZodRecord { + "~standard": ZodStandardSchemaWithJSON; + keyType: Key; + valueType: Value; +} +export declare const ZodRecord: core.$constructor; +export declare function record(keyType: Key, valueType: Value, params?: string | core.$ZodRecordParams): ZodRecord; +export declare function partialRecord(keyType: Key, valueType: Value, params?: string | core.$ZodRecordParams): ZodRecord; +export declare function looseRecord(keyType: Key, valueType: Value, params?: string | core.$ZodRecordParams): ZodRecord; +export interface ZodMap extends _ZodType>, core.$ZodMap { + "~standard": ZodStandardSchemaWithJSON; + keyType: Key; + valueType: Value; + min(minSize: number, params?: string | core.$ZodCheckMinSizeParams): this; + nonempty(params?: string | core.$ZodCheckMinSizeParams): this; + max(maxSize: number, params?: string | core.$ZodCheckMaxSizeParams): this; + size(size: number, params?: string | core.$ZodCheckSizeEqualsParams): this; +} +export declare const ZodMap: core.$constructor; +export declare function map(keyType: Key, valueType: Value, params?: string | core.$ZodMapParams): ZodMap; +export interface ZodSet extends _ZodType>, core.$ZodSet { + "~standard": ZodStandardSchemaWithJSON; + min(minSize: number, params?: string | core.$ZodCheckMinSizeParams): this; + nonempty(params?: string | core.$ZodCheckMinSizeParams): this; + max(maxSize: number, params?: string | core.$ZodCheckMaxSizeParams): this; + size(size: number, params?: string | core.$ZodCheckSizeEqualsParams): this; +} +export declare const ZodSet: core.$constructor; +export declare function set(valueType: Value, params?: string | core.$ZodSetParams): ZodSet; +export interface ZodEnum< +/** @ts-ignore Cast variance */ +out T extends util.EnumLike = util.EnumLike> extends _ZodType>, core.$ZodEnum { + "~standard": ZodStandardSchemaWithJSON; + enum: T; + options: Array; + extract(values: U, params?: string | core.$ZodEnumParams): ZodEnum>>; + exclude(values: U, params?: string | core.$ZodEnumParams): ZodEnum>>; +} +export declare const ZodEnum: core.$constructor; +declare function _enum(values: T, params?: string | core.$ZodEnumParams): ZodEnum>; +declare function _enum(entries: T, params?: string | core.$ZodEnumParams): ZodEnum; +export { _enum as enum }; +/** @deprecated This API has been merged into `z.enum()`. Use `z.enum()` instead. + * + * ```ts + * enum Colors { red, green, blue } + * z.enum(Colors); + * ``` + */ +export declare function nativeEnum(entries: T, params?: string | core.$ZodEnumParams): ZodEnum; +export interface ZodLiteral extends _ZodType>, core.$ZodLiteral { + "~standard": ZodStandardSchemaWithJSON; + values: Set; + /** @legacy Use `.values` instead. Accessing this property will throw an error if the literal accepts multiple values. */ + value: T; +} +export declare const ZodLiteral: core.$constructor; +export declare function literal>(value: T, params?: string | core.$ZodLiteralParams): ZodLiteral; +export declare function literal(value: T, params?: string | core.$ZodLiteralParams): ZodLiteral; +export interface ZodFile extends _ZodType, core.$ZodFile { + "~standard": ZodStandardSchemaWithJSON; + min(size: number, params?: string | core.$ZodCheckMinSizeParams): this; + max(size: number, params?: string | core.$ZodCheckMaxSizeParams): this; + mime(types: util.MimeTypes | Array, params?: string | core.$ZodCheckMimeTypeParams): this; +} +export declare const ZodFile: core.$constructor; +export declare function file(params?: string | core.$ZodFileParams): ZodFile; +export interface ZodTransform extends _ZodType>, core.$ZodTransform { + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodTransform: core.$constructor; +export declare function transform(fn: (input: I, ctx: core.$RefinementCtx) => O): ZodTransform, I>; +export interface ZodOptional extends _ZodType>, core.$ZodOptional { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodOptional: core.$constructor; +export declare function optional(innerType: T): ZodOptional; +export interface ZodExactOptional extends _ZodType>, core.$ZodExactOptional { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodExactOptional: core.$constructor; +export declare function exactOptional(innerType: T): ZodExactOptional; +export interface ZodNullable extends _ZodType>, core.$ZodNullable { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodNullable: core.$constructor; +export declare function nullable(innerType: T): ZodNullable; +export declare function nullish(innerType: T): ZodOptional>; +export interface ZodDefault extends _ZodType>, core.$ZodDefault { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; + /** @deprecated Use `.unwrap()` instead. */ + removeDefault(): T; +} +export declare const ZodDefault: core.$constructor; +export declare function _default(innerType: T, defaultValue: util.NoUndefined> | (() => util.NoUndefined>)): ZodDefault; +export interface ZodPrefault extends _ZodType>, core.$ZodPrefault { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodPrefault: core.$constructor; +export declare function prefault(innerType: T, defaultValue: core.input | (() => core.input)): ZodPrefault; +export interface ZodNonOptional extends _ZodType>, core.$ZodNonOptional { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodNonOptional: core.$constructor; +export declare function nonoptional(innerType: T, params?: string | core.$ZodNonOptionalParams): ZodNonOptional; +export interface ZodSuccess extends _ZodType>, core.$ZodSuccess { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodSuccess: core.$constructor; +export declare function success(innerType: T): ZodSuccess; +export interface ZodCatch extends _ZodType>, core.$ZodCatch { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; + /** @deprecated Use `.unwrap()` instead. */ + removeCatch(): T; +} +export declare const ZodCatch: core.$constructor; +declare function _catch(innerType: T, catchValue: core.output | ((ctx: core.$ZodCatchCtx) => core.output)): ZodCatch; +export { _catch as catch }; +export interface ZodNaN extends _ZodType, core.$ZodNaN { + "~standard": ZodStandardSchemaWithJSON; +} +export declare const ZodNaN: core.$constructor; +export declare function nan(params?: string | core.$ZodNaNParams): ZodNaN; +export interface ZodPipe extends _ZodType>, core.$ZodPipe { + "~standard": ZodStandardSchemaWithJSON; + in: A; + out: B; +} +export declare const ZodPipe: core.$constructor; +export declare function pipe> = core.$ZodType>>(in_: A, out: B | core.$ZodType>): ZodPipe; +export interface ZodCodec extends ZodPipe, core.$ZodCodec { + "~standard": ZodStandardSchemaWithJSON; + _zod: core.$ZodCodecInternals; + def: core.$ZodCodecDef; +} +export declare const ZodCodec: core.$constructor; +export declare function codec(in_: A, out: B, params: { + decode: (value: core.output, payload: core.ParsePayload>) => core.util.MaybeAsync>; + encode: (value: core.input, payload: core.ParsePayload>) => core.util.MaybeAsync>; +}): ZodCodec; +export declare function invertCodec(codec: ZodCodec): ZodCodec; +export interface ZodPreprocess extends ZodPipe, B>, core.$ZodPreprocess { + "~standard": ZodStandardSchemaWithJSON; + _zod: core.$ZodPreprocessInternals; + def: core.$ZodPreprocessDef; +} +export declare const ZodPreprocess: core.$constructor; +export interface ZodReadonly extends _ZodType>, core.$ZodReadonly { + "~standard": ZodStandardSchemaWithJSON; + unwrap(): T; +} +export declare const ZodReadonly: core.$constructor; +export declare function readonly(innerType: T): ZodReadonly; +export interface ZodTemplateLiteral