From e01df8c41c959f5c7a9bb42b3a3d373712879e1a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 24 Sep 2026 01:02:15 -0500 Subject: [PATCH 1/2] Eliminate temporary numeric return records --- packages/compiler/src/backend/c/c-emitter.ts | 3 +- packages/compiler/src/backend/llvm/emitter.ts | 3 +- .../compiler/src/ir/scalar-records.test.ts | 144 +++++++++++++ packages/compiler/src/ir/scalar-records.ts | 192 ++++++++++++++++++ .../test/scalar-record-emission.test.ts | 50 +++++ tests/corpus/2969-scalar-record-results.ts | 84 ++++++++ 6 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/src/ir/scalar-records.test.ts create mode 100644 packages/compiler/src/ir/scalar-records.ts create mode 100644 packages/compiler/test/scalar-record-emission.test.ts create mode 100644 tests/corpus/2969-scalar-record-results.ts diff --git a/packages/compiler/src/backend/c/c-emitter.ts b/packages/compiler/src/backend/c/c-emitter.ts index 80e856d4d..ac354e544 100644 --- a/packages/compiler/src/backend/c/c-emitter.ts +++ b/packages/compiler/src/backend/c/c-emitter.ts @@ -42,6 +42,7 @@ import type { } from "../../ir/ir.js"; import { ffiCallbackType, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted, isUnitType, mapOf, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDgram, moduleUsesDynInvoke, moduleEmbedsBuiltin, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttp2, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, POINTER_KINDS, type PointerKind, RUNTIME_EMITTER_CLASS, STRING, VOID } from "../../ir/ir.js"; import { undefinedArmTag } from "../../ir/analysis.js"; +import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js"; import { mangleAsyncSpawn, @@ -77,7 +78,7 @@ export function emitCModule( sourceText?: string, options: CEmitOptions = {}, ): string { - return new CEmitter(mod, sourceText, options).emit(); + return new CEmitter(scalarizeNumericRecords(mod), sourceText, options).emit(); } // Box construction moved onto CEmitter (boxNewC method): obj-kind boxes now diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 297ff75e0..82b8ed455 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -81,6 +81,7 @@ import type { } from "../../ir/ir.js"; import { CAUGHT, ffiCallbackType, isDynTypedRefType, isFfiContextParam, isRefCounted, isUnitType, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDynInvoke, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, NPM_COMPRESS_MIN, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeKey, VOID } from "../../ir/ir.js"; import { matchIntegerBytesForLoop } from "../../ir/integer-loops.js"; +import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js"; import { RUNTIME_ABI_MARKER } from "../runtime-abi.js"; import { computeMayThrow } from "../c/may-throw.js"; @@ -180,7 +181,7 @@ export interface LlvmTargetOptions { } export function emitLlvmModule(mod: IrModule, options: LlvmTargetOptions = {}): string { - return new LlEmitter(mod, options).emit(); + return new LlEmitter(scalarizeNumericRecords(mod), options).emit(); } /** LLVM c"..." payload for a UTF-8 literal, NUL-terminated like the C diff --git a/packages/compiler/src/ir/scalar-records.test.ts b/packages/compiler/src/ir/scalar-records.test.ts new file mode 100644 index 000000000..ec53184b0 --- /dev/null +++ b/packages/compiler/src/ir/scalar-records.test.ts @@ -0,0 +1,144 @@ +import { expect, test } from "vitest"; +import { BOOL, F64, STRING, VOID, type IrExpr, type IrFunction, type IrModule, type IrStmt, type IrType } from "./ir.js"; +import { scalarizeNumericRecords } from "./scalar-records.js"; +import { validateModule } from "./validate.js"; + +const loc = { file: "scalar-records.ts", start: 0, end: 0 }; +const record: IrType = { kind: "record", shapeId: "r0" }; +const ref = (localId: string, type: IrType = F64): IrExpr => ({ kind: "varRef", localId, type, loc }); +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const call = (callee: string, args: IrExpr[] = [], type: IrType = F64): IrExpr => ({ kind: "call", callee, args, type, loc }); +const read = (localId: string, field = "a"): IrExpr => ({ kind: "recordGet", obj: ref(localId, record), shapeId: "r0", field, type: F64, loc }); + +function fixture(): IrModule { + return { + irVersion: 11, sourceFile: loc.file, entry: "main", + records: [{ id: "r0", fields: [{ name: "a", type: F64 }, { name: "b", type: F64 }] }], + functions: [ + { + name: "pair", params: [{ localId: "x", name: "x", type: F64 }], returnType: record, + locals: [{ id: "x", name: "x", type: F64, mutable: true }], + body: [{ kind: "return", value: { kind: "recordLit", type: record, fields: [ + { name: "b", value: call("effect", [num(2)]) }, { name: "a", value: ref("x") }, + ], loc }, loc }], loc, + }, + { name: "effect", params: [{ localId: "x", name: "x", type: F64 }], returnType: F64, + locals: [{ id: "x", name: "x", type: F64, mutable: true }], body: [{ kind: "return", value: ref("x"), loc }], loc }, + { + name: "main", params: [], returnType: VOID, + locals: [{ id: "result", name: "result", type: record, mutable: false }], + body: [{ kind: "varDecl", localId: "result", init: call("pair", [call("effect", [num(1)])], record), loc }, + { kind: "exprStmt", expr: read("result"), loc }], loc, + }, + ], + }; +} + +function nodes(value: unknown, kind: string): Record[] { + if (Array.isArray(value)) return value.flatMap((v) => nodes(v, kind)); + if (value === null || typeof value !== "object") return []; + const node = value as Record; + return [...(node["kind"] === kind ? [node] : []), ...Object.entries(node) + .flatMap(([key, child]) => key === "loc" || key === "type" ? [] : nodes(child, kind))]; +} + +function main(mod: IrModule): IrFunction { return mod.functions.find((fn) => fn.name === "main")!; } +function transformed(mod: IrModule): IrModule { + expect(validateModule(mod)).toEqual([]); + const out = scalarizeNumericRecords(mod); + expect(validateModule(out)).toEqual([]); + return out; +} + +test("eliminates fresh field-only records without mutating input or dropping unused field effects", () => { + const mod = fixture(); + const snapshot = structuredClone(mod); + const out = transformed(mod); + expect(mod).toEqual(snapshot); + expect(out.functions[0]).toBe(mod.functions[0]); + const caller = main(out); + expect(nodes(caller.body, "recordGet")).toEqual([]); + expect(caller.locals.every((local) => local.type.kind === "f64")).toBe(true); + const calls = nodes(caller.body, "call"); + expect(calls.map((c) => c["callee"])).toEqual(["effect", "effect"]); + expect(calls.map((c) => (c["args"] as IrExpr[])[0])).toEqual([num(1), num(2)]); + const writes = nodes(caller.body, "assign"); + expect(writes.map((w) => caller.locals.find((l) => l.id === w["localId"])?.name)).toEqual(["result.b", "result.a"]); +}); + +test("renames producer locals and labels at repeated calls inside caller control flow", () => { + const mod = fixture(); + const producer = mod.functions[0]!; + producer.locals.push({ id: "%scalar.0", name: "collision", type: F64, mutable: false }); + producer.body = [{ kind: "block", labels: ["same", "%scalar.1"], body: [ + { kind: "varDecl", localId: "%scalar.0", init: num(9), loc }, + { kind: "if", cond: { kind: "boolLit", value: false, type: BOOL, loc }, then: [{ kind: "break", label: "same", loc }], else_: null, loc }, + ...producer.body, + ], loc }, ...producer.body]; + const caller = main(mod); + caller.locals.push({ id: "again", name: "again", type: record, mutable: false }); + caller.body.push({ kind: "varDecl", localId: "again", init: call("pair", [num(4)], record), loc }, + { kind: "exprStmt", expr: read("again"), loc }); + caller.body = [{ kind: "block", labels: ["same"], body: caller.body, loc }]; + const out = main(transformed(mod)); + expect(nodes(out.body, "call").every((n) => n["callee"] !== "pair")).toBe(true); + expect(new Set(out.locals.map((l) => l.id)).size).toBe(out.locals.length); + expect(nodes(out.body, "recordGet")).toEqual([]); +}); + +test.each(["alias", "mutation", "identity", "return", "capture", "reassignment"])("keeps objects observed through %s", (use) => { + const mod = fixture(); + const caller = main(mod); + if (use === "alias") { + caller.locals.push({ id: "alias", name: "alias", type: record, mutable: false }); + caller.body.push({ kind: "varDecl", localId: "alias", init: ref("result", record), loc }); + } else if (use === "mutation") { + caller.body.push({ kind: "recordSet", obj: ref("result", record), shapeId: "r0", field: "a", value: num(7), loc }); + } else if (use === "identity") { + caller.body.push({ kind: "exprStmt", expr: { kind: "bin", op: "===", left: ref("result", record), right: ref("result", record), type: BOOL, loc }, loc }); + } else if (use === "return") { + caller.returnType = record; + caller.body.push({ kind: "return", value: ref("result", record), loc }); + } else if (use === "capture") { + caller.locals[0]!.boxed = true; + } else { + caller.locals[0]!.mutable = true; + caller.body.push({ kind: "assign", localId: "result", value: call("pair", [num(7)], record), loc }); + } + expect(transformed(mod)).toBe(mod); +}); + +test("keeps for-header declarations in their original IR form", () => { + const mod = fixture(); + const caller = main(mod); + caller.body = [{ kind: "for", init: caller.body[0]!, cond: { kind: "boolLit", value: false, type: BOOL, loc }, update: null, body: caller.body.slice(1), loc }]; + expect(transformed(mod)).toBe(mod); +}); + +test.each(["argument", "field"])("retains reference temporaries in the original %s evaluation frame", (position) => { + const mod = fixture(); + const length: IrExpr = { kind: "strIntrinsic", method: "length", receiver: { kind: "strLit", value: "abc", type: STRING, loc }, args: [], type: F64, loc }; + if (position === "argument") { + (main(mod).body[0] as IrStmt & { kind: "varDecl" }).init = call("pair", [length], record); + } else { + const ret = mod.functions[0]!.body[0] as IrStmt & { kind: "return" }; + (ret.value as IrExpr & { kind: "recordLit" }).fields[0]!.value = length; + } + expect(transformed(mod)).toBe(mod); +}); + +test("bounds expansion in callers with many eligible calls", () => { + const mod = fixture(); + const caller = main(mod); + caller.locals = []; + caller.body = []; + for (let i = 0; i < 300; i++) { + const id = `result${i}`; + caller.locals.push({ id, name: id, type: record, mutable: false }); + caller.body.push({ kind: "varDecl", localId: id, init: call("pair", [num(i)], record), loc }, { kind: "exprStmt", expr: read(id), loc }); + } + const out = main(transformed(mod)); + const remaining = nodes(out.body, "call").filter((n) => n["callee"] === "pair").length; + expect(remaining).toBeGreaterThan(0); + expect(remaining).toBeLessThan(300); +}); diff --git a/packages/compiler/src/ir/scalar-records.ts b/packages/compiler/src/ir/scalar-records.ts new file mode 100644 index 000000000..3d02fbaca --- /dev/null +++ b/packages/compiler/src/ir/scalar-records.ts @@ -0,0 +1,192 @@ +import { F64, isRefCounted, type IrExpr, type IrFunction, type IrLocal, type IrModule, type IrRecordShape, type IrStmt, type IrType } from "./ir.js"; + +type Node = Record; +const MAX_FIELDS = 4; +const MAX_CALLEE_NODES = 256; +const MAX_INLINE_NODES = 1024; + +/** IR is a plain tree. Types and source locations are metadata, not uses. */ +function everyNode(value: unknown, visit: (node: Node) => boolean): boolean { + if (Array.isArray(value)) return value.every((v) => everyNode(v, visit)); + if (value === null || typeof value !== "object") return true; + const node = value as Node; + return visit(node) && Object.entries(node).every(([key, child]) => + key === "type" || key === "loc" || everyNode(child, visit)); +} + +function mapTree(value: T, visit: (node: Node) => Node): T { + if (Array.isArray(value)) return value.map((v) => mapTree(v, visit)) as T; + if (value === null || typeof value !== "object") return value; + const node = visit(value as Node); + return Object.fromEntries(Object.entries(node).map(([key, child]) => + [key, key === "type" || key === "loc" ? child : mapTree(child, visit)])) as T; +} + +interface Producer { fn: IrFunction; shape: IrRecordShape; size: number } + +/** Splitting an expression frame must not shorten a reference temporary's + * lifetime across later arguments, fields, or the original call itself. */ +function scalarTemporaries(value: unknown): boolean { + return everyNode(value, (node) => !node["type"] || !isRefCounted(node["type"] as IrType)); +} + +function producer(fn: IrFunction, shapes: ReadonlyMap): Producer | null { + if (fn.async || fn.generator || fn.captures?.length || fn.returnType.kind !== "record") return null; + const shape = shapes.get(fn.returnType.shapeId); + if (!shape || shape.tuple || shape.indexValue || shape.fields.length === 0 || shape.fields.length > MAX_FIELDS || + shape.fields.some((f) => f.type.kind !== "f64")) return null; + // Scalar parameters/locals need no ownership cleanup across the inlined + // return. Closures, suspension and finally completions remain out of scope. + if (fn.locals.some((l) => l.boxed || l.tdz || (l.type.kind !== "f64" && l.type.kind !== "bool")) || + fn.params.some((p) => p.type.kind !== "f64" && p.type.kind !== "bool")) return null; + let size = 0; + let returns = 0; + const eligible = everyNode(fn.body, (node) => { + if (typeof node["kind"] !== "string") return true; + if (++size > MAX_CALLEE_NODES) return false; + switch (node["kind"]) { + case "closure": case "selfRef": case "tryCatch": + case "awaitExpr": case "awaitUnionExpr": case "yieldExpr": + return false; + case "return": { + returns++; + const value = (node as Extract).value; + return value?.kind === "recordLit" && value.type.kind === "record" && value.type.shapeId === shape.id && + value.fields.length === shape.fields.length && value.fields.every((f) => + !f.drop && !f.overflow && f.value.type.kind === "f64" && scalarTemporaries(f.value) && shape.fields.some((sf) => sf.name === f.name)); + } + default: return true; + } + }); + return eligible && returns > 0 ? { fn, shape, size } : null; +} + +/** A result may only be read through its declared scalar fields. Even an + * apparently harmless alias, identity test, mutation or closure capture + * keeps the original object. Count declarations too: no reinitialization. */ +function fieldOnlyUses(fn: IrFunction, localId: string, shape: IrRecordShape): boolean { + let declarations = 0; + function visit(value: unknown): boolean { + if (Array.isArray(value)) return value.every(visit); + if (value === null || typeof value !== "object") return true; + const node = value as Node; + if (node["kind"] === "recordGet") { + const read = node as Extract; + if (read.obj.kind === "varRef" && read.obj.localId === localId) { + return read.shapeId === shape.id && read.type.kind === "f64" && shape.fields.some((f) => f.name === read.field); + } + } + if (node["localId"] === localId) { + if (node["kind"] !== "varDecl" || ++declarations !== 1) return false; + } + return Object.entries(node).every(([key, child]) => key === "type" || key === "loc" || visit(child)); + } + return visit(fn.body) && declarations === 1; +} + +interface Replacement { + fields: Map; + body: IrStmt[]; +} + +/** Eliminate small fresh numeric result records at direct, field-only call + * sites. This is a bounded shared backend pass, not a change to record ABI: + * the original producer remains available to all other callers. Arguments + * and literal fields retain source evaluation order. A labeled block models + * return, including returns inside loops, without changing caller control + * flow. Unknown uses and non-scalar producer locals keep the heap path. */ +export function scalarizeNumericRecords(mod: IrModule): IrModule { + const shapes = new Map((mod.records ?? []).map((s) => [s.id, s])); + const producers = new Map(); + for (const fn of mod.functions) { + const p = producer(fn, shapes); + if (p) producers.set(fn.name, p); + } + if (producers.size === 0) return mod; + let changed = false; + const functions = mod.functions.map((fn): IrFunction => { + if (fn.async || fn.generator) return fn; + const locals = new Map(fn.locals.map((l) => [l.id, l])); + const used = new Set([...locals.keys(), ...(mod.globals ?? []).map((g) => g.id)]); + const loopHeaders = new Set(); + everyNode(fn.body, (node) => { + if (Array.isArray(node["labels"])) for (const label of node["labels"]) used.add(String(label)); + // A for header accepts one declaration/assignment, not the statement + // block this pass produces. Leave those declarations on the heap path. + if (node["kind"] === "for") { loopHeaders.add(node["init"]); loopHeaders.add(node["update"]); } + return true; + }); + let next = 0; + const fresh = (): string => { + let id: string; + do { id = `%scalar.${next++}`; } while (used.has(id)); + used.add(id); + return id; + }; + const added: IrLocal[] = []; + const replacements = new Map(); + let budget = MAX_INLINE_NODES; + everyNode(fn.body, (node) => { + if (node["kind"] !== "varDecl" || loopHeaders.has(node)) return true; + const decl = node as Extract; + const local = locals.get(decl.localId); + const call = decl.init; + if (!local || local.mutable || local.boxed || local.tdz || local.type.kind !== "record" || call?.kind !== "call") return true; + const p = producers.get(call.callee); + if (!p || p.fn.name === fn.name || p.size > budget || p.shape.id !== local.type.shapeId || + call.args.length !== p.fn.params.length || !scalarTemporaries(call.args) || !fieldOnlyUses(fn, local.id, p.shape)) return true; + budget -= p.size; + for (const l of p.fn.locals) used.add(l.id); + everyNode(p.fn.body, (n) => { + if (Array.isArray(n["labels"])) for (const l of n["labels"]) used.add(String(l)); + return true; + }); + const fields = new Map(p.shape.fields.map((f) => [f.name, fresh()])); + const renamed = new Map(p.fn.locals.map((l) => [l.id, fresh()])); + const labels = new Map(); + const exit = fresh(); + const label = (name: string): string => { + if (!labels.has(name)) labels.set(name, fresh()); + return labels.get(name)!; + }; + for (const f of p.shape.fields) added.push({ id: fields.get(f.name)!, name: `${local.name}.${f.name}`, type: F64, mutable: true }); + for (const l of p.fn.locals) added.push({ ...l, id: renamed.get(l.id)! }); + const body = mapTree(p.fn.body, (n): Node => { + if (n["kind"] === "return") { + const ret = n as Extract; + const literal = ret.value as Extract; + const assignments: IrStmt[] = literal.fields.map((f) => ({ kind: "assign", localId: fields.get(f.name)!, value: f.value, loc: ret.loc })); + return { kind: "block", body: [...assignments, { kind: "break", label: exit, loc: ret.loc }], loc: ret.loc }; + } + const out = { ...n }; + if (typeof out["localId"] === "string" && renamed.has(out["localId"])) out["localId"] = renamed.get(out["localId"]); + if (typeof out["label"] === "string" && out["label"] !== exit) out["label"] = label(out["label"]); + if (Array.isArray(out["labels"])) out["labels"] = out["labels"].map((l) => label(String(l))); + return out; + }); + const parameters: IrStmt[] = p.fn.params.map((param, i) => ({ kind: "varDecl", localId: renamed.get(param.localId)!, init: call.args[i]!, loc: decl.loc })); + const declarations: IrStmt[] = [...fields.values()].map((id) => ({ kind: "varDecl", localId: id, init: null, loc: decl.loc })); + replacements.set(local.id, { fields, body: [...declarations, { kind: "block", labels: [exit], body: [...parameters, ...body], loc: decl.loc }] }); + return true; + }); + if (replacements.size === 0) return fn; + changed = true; + const body = mapTree(fn.body, (node): Node => { + if (node["kind"] === "varDecl") { + const decl = node as Extract; + const replacement = replacements.get(decl.localId); + if (replacement) return { kind: "block", body: replacement.body, loc: decl.loc }; + } + if (node["kind"] === "recordGet") { + const read = node as Extract; + if (read.obj.kind === "varRef") { + const id = replacements.get(read.obj.localId)?.fields.get(read.field); + if (id) return { kind: "varRef", localId: id, type: F64, loc: read.loc }; + } + } + return node; + }); + return { ...fn, locals: [...fn.locals.filter((l) => !replacements.has(l.id)), ...added], body }; + }); + return changed ? { ...mod, functions } : mod; +} diff --git a/packages/compiler/test/scalar-record-emission.test.ts b/packages/compiler/test/scalar-record-emission.test.ts new file mode 100644 index 000000000..3c4ddd1f1 --- /dev/null +++ b/packages/compiler/test/scalar-record-emission.test.ts @@ -0,0 +1,50 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; +import { emitCModule } from "../src/backend/c/c-emitter.js"; +import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; +import { scalarizeNumericRecords } from "../src/ir/scalar-records.js"; + +test("both backends scalarize ordinary typed calls while preserving producers with finally", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-scalar-record-emission-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, ` +function pair(x: number): { a: number; b: number } { + for (let i = 0; i < 3; i++) { + if (i === x) return { b: i * 2, a: i }; + } + return { a: -1, b: -2 }; +} +function guarded(): { a: number; b: number } { + try { return { a: 1, b: 2 }; } finally { console.log("finally"); } +} +function render(): void { + const p = pair(2); + console.log(p.a, p.b); + const g = guarded(); + console.log(g.a); +} +render(); +`); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + const out = scalarizeNumericRecords(mod); + expect(validateModule(mod)).toEqual([]); + expect(validateModule(out)).toEqual([]); + const render = out.functions.find((fn) => fn.name === "render")!; + expect(render.locals.filter((l) => l.type.kind === "record").map((l) => l.name)).toEqual(["g"]); + const c = emitCModule(mod); + const llvm = emitLlvmModule(mod); + expect(c).not.toMatch(/= sc_f_pair\(/); + expect(llvm).not.toMatch(/call ptr @sc_f_pair\(/); + expect(c).toMatch(/= sc_f_guarded\(/); + expect(llvm).toMatch(/call ptr @sc_f_guarded\(/); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/tests/corpus/2969-scalar-record-results.ts b/tests/corpus/2969-scalar-record-results.ts new file mode 100644 index 000000000..1d16be98d --- /dev/null +++ b/tests/corpus/2969-scalar-record-results.ts @@ -0,0 +1,84 @@ +let trace = 0; +function mark(n: number): number { trace = trace * 10 + n; return n; } +function color(x: number, y: number): { r: number; g: number; b: number } { + if (x < 0) return { r: -0, g: Infinity, b: NaN }; + search: for (let i = 0; i < 3; i++) { + if (i > x) break search; + if (i === x) return { b: mark(3), r: mark(1) + x, g: mark(2) + y }; + continue search; + } + return { r: x, g: y, b: x + y }; +} +function render(): void { + const first = color(mark(1), mark(2)); + console.log("order", trace, first.r, first.g, first.b); + const special = color(-1, 0); + console.log("ieee", 1 / special.r, special.g, special.b); + let total = 0; + outer: for (let i = 0; i < 5; i++) { + const c = color(i, 10); + if (i === 1) continue outer; + total += c.r + c.g + c.b; + if (i === 3) break outer; + } + console.log("loops", total); +} +render(); +function fail(): number { throw new Error("field"); } +function throwing(): { x: number; y: number } { return { x: mark(4), y: fail() }; } +try { + const c = throwing(); + console.log(c.x, c.y); +} catch (e) { console.log("throw", (e as Error).message, trace); } +finally { console.log("finally"); } +function immutable(x: number): { value: number } { return { value: x }; } +function fallback(): void { + const original = immutable(5); + const alias = original; + alias.value = 8; + console.log("alias", original === alias, original.value); + const observed = immutable(6); + console.log("identity", observed === observed); + const captured = immutable(7); + function read(): number { return captured.value; } + console.log("captured", read()); +} +fallback(); +function withFinally(): { x: number } { + try { return { x: 10 }; } finally { console.log("producer-finally"); } +} +function finalResult(): void { const c = withFinally(); console.log("fallback-finally", c.x); } +finalResult(); +function unused(): { used: number; ignored: number } { return { ignored: mark(6), used: 11 }; } +function onlyOneField(): void { + const c = unused(); + console.log("unused-effect", c.used, trace); +} +onlyOneField(); +function withHeader(): void { + for (const c = immutable(12); c.value > 0;) { + console.log("header", c.value); + break; + } +} +withHeader(); +const numbers = [2, 4, 6]; +function fromIterable(stop: boolean): { value: number } { + for (const n of numbers) { + switch (n) { + case 2: if (stop) return { value: n }; break; + case 4: continue; + default: return { value: n }; + } + } + return { value: -1 }; +} +function iterableResults(): void { + for (let i = 0; i < 3; i++) { + try { + const c = fromIterable(i === 0); + console.log("iterable", c.value); + } finally { console.log("iteration-finally", i); } + } +} +iterableResults(); From 9a9bd48f0368850f7d988d5c26801b6873f6f286 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 24 Sep 2026 01:32:56 -0500 Subject: [PATCH 2/2] test: record missing frontend parity baselines --- .../test/ts7/baselines/order-parity.json | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 1e8ae74ba..e01d9b79c 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -6277,6 +6277,12 @@ ], "diags": [] }, + "/tests/corpus/2900-float64array-static.ts": { + "order": [ + "/tests/corpus/2900-float64array-static.ts" + ], + "diags": [] + }, "/tests/corpus/2900-void-conditional-arrow.ts": { "order": [ "/tests/corpus/2900-void-conditional-arrow.ts" @@ -6576,6 +6582,42 @@ ], "diags": [] }, + "/tests/corpus/2951-math-static-analytic.ts": { + "order": [ + "/tests/corpus/2951-math-static-analytic.ts" + ], + "diags": [] + }, + "/tests/corpus/2952-math-static-remaining.ts": { + "order": [ + "/tests/corpus/2952-math-static-remaining.ts" + ], + "diags": [] + }, + "/tests/corpus/2953-http-header-validation.ts": { + "order": [ + "/tests/corpus/2953-http-header-validation.ts" + ], + "diags": [] + }, + "/tests/corpus/2954-http-outgoing-headers.ts": { + "order": [ + "/tests/corpus/2954-http-outgoing-headers.ts" + ], + "diags": [] + }, + "/tests/corpus/2955-http-static-state.ts": { + "order": [ + "/tests/corpus/2955-http-static-state.ts" + ], + "diags": [] + }, + "/tests/corpus/2956-http-static-informational.ts": { + "order": [ + "/tests/corpus/2956-http-static-informational.ts" + ], + "diags": [] + }, "/tests/corpus/2961-emitter-computed-names.cjs": { "order": [ "/tests/corpus/2961-emitter-computed-names.cjs" @@ -6594,6 +6636,36 @@ ], "diags": [] }, + "/tests/corpus/2964-indexed-compound.ts": { + "order": [ + "/tests/corpus/2964-indexed-compound.ts" + ], + "diags": [] + }, + "/tests/corpus/2966-numeric-array-read-fusion.ts": { + "order": [ + "/tests/corpus/2966-numeric-array-read-fusion.ts" + ], + "diags": [] + }, + "/tests/corpus/2967-numeric-array-read-lifetime.ts": { + "order": [ + "/tests/corpus/2967-numeric-array-read-lifetime.ts" + ], + "diags": [] + }, + "/tests/corpus/2968-numeric-array-borrow.ts": { + "order": [ + "/tests/corpus/2968-numeric-array-borrow.ts" + ], + "diags": [] + }, + "/tests/corpus/2969-scalar-record-results.ts": { + "order": [ + "/tests/corpus/2969-scalar-record-results.ts" + ], + "diags": [] + }, "/tests/corpus/300-if-else.ts": { "order": [ "/tests/corpus/300-if-else.ts"