From 12f1966f0660e016a2121091f2f2a23dd67ad625 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 9 Oct 2026 05:09:03 -0700 Subject: [PATCH] Borrow constant aliases created by checked casts `const x = a as LiteralType` lowers to a ternary whose failing arm always throws and whose successful arm narrows and casts the unchanged source. The alias projects the same stable owner, so it can borrow like a plain copy of the binding instead of retaining at the declaration and releasing at scope exit. Call arguments of the same shape borrow too. tsc-ts's sameLiteralValue (two casts per call, on the relation hot path) loses all of its reference counting. --- packages/compiler/src/backend/llvm/emitter.ts | 12 ++++ .../test/ts7/baselines/order-parity.json | 6 ++ tests/corpus/4544-checked-cast-aliases.ts | 71 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 tests/corpus/4544-checked-cast-aliases.ts diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 3c7cccd5c..a084e9910 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -5124,6 +5124,18 @@ export class LlEmitter { if (wrap === "unit") return true; if (wrap === "ref" && value.kind === "unionWrap") return this.canBorrowCallArgument(value.value); + // Checked casts (`x as C`, `x!` on a nullable) lower to a ternary whose + // failing arm always throws; the successful arm projects the same + // stable owner. The throwing arm's typed dummy owns nothing. + if (value.kind === "unionNarrow") return this.canBorrowCallArgument(value.value); + if (value.kind === "libCall") return value.fn === "error.nodeThrow"; + if (value.kind === "ternary") + return ( + this.canBorrowReceiver(value.then) && + this.canBorrowReceiver(value.else_) && + this.canBorrowCallArgument(value.then) && + this.canBorrowCallArgument(value.else_) + ); if (value.kind !== "varRef") return false; const binding = this.binding(value.localId); return ( diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 26697a57f..baab306da 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -15604,6 +15604,12 @@ "/tests/corpus/4542-preserving-call-borrows.ts" ], "diags": [] + }, + "/tests/corpus/4544-checked-cast-aliases.ts": { + "order": [ + "/tests/corpus/4544-checked-cast-aliases.ts" + ], + "diags": [] } } } diff --git a/tests/corpus/4544-checked-cast-aliases.ts b/tests/corpus/4544-checked-cast-aliases.ts new file mode 100644 index 000000000..5e33046fe --- /dev/null +++ b/tests/corpus/4544-checked-cast-aliases.ts @@ -0,0 +1,71 @@ +// Constant aliases created by checked casts (`x as Sub`, a narrowed +// nullable) of unchanged parameters and locals borrow their source. The +// source binding keeps the object alive even when everything else that +// referenced it is dropped while the alias is in use. + +class Type { + flags: number; + constructor(flags: number) { + this.flags = flags; + } +} + +class LiteralType extends Type { + value: number; + text: string; + constructor(flags: number, value: number, text: string) { + super(flags); + this.value = value; + this.text = text; + } +} + +class Holder { + item: Type | undefined; + constructor(item: Type | undefined) { + this.item = item; + } +} + +function sameLiteral(a: Type, b: Type): boolean { + const x = a as LiteralType; + const y = b as LiteralType; + if ((a.flags & 1) !== 0) return x.value === y.value || (x.value !== x.value && y.value !== y.value); + return x.text === y.text; +} + +// The alias outlives every other reference the program held. +function aliasAcrossDrop(a: Type, drop: () => void): string { + const x = a as LiteralType; + drop(); + const fresh = new LiteralType(3, 99, "fresh"); + return x.text + ":" + x.value + ":" + fresh.text; +} + +function narrowAlias(holder: Holder): string { + const item = holder.item; + if (item === undefined) return "none"; + const literal = item as LiteralType; + holder.item = undefined; + const fresh = new LiteralType(5, 7, "replacement"); + return literal.text + "/" + fresh.text; +} + +const items: Type[] = [ + new LiteralType(1, 1, "one"), + new LiteralType(1, 1, "uno"), + new LiteralType(2, 2, "two"), + new LiteralType(2, 3, "two"), + new LiteralType(1, NaN, "nan"), +]; +console.log(sameLiteral(items[0], items[1]), sameLiteral(items[2], items[3]), sameLiteral(items[4], items[4])); + +const holder = new Holder(new LiteralType(2, 42, "held")); +console.log( + aliasAcrossDrop(holder.item!, () => { + holder.item = undefined; + }), + holder.item === undefined, +); +const holder2 = new Holder(new LiteralType(2, 8, "narrowed")); +console.log(narrowAlias(holder2), narrowAlias(holder2));