From 44bd0a7b1427616f705425e423e529256bebc1db Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 19:24:17 -0500 Subject: [PATCH] Keep forwarded-only tuple bindings per function The lowerer kept the tuple bindings that are only forwarded in one map for the whole program, keyed by local id. Local ids are unique within one function only: two wrappers that both bind `arguments` (or a `const args = arguments as any` alias) declare the same id, so a wrapper whose tuple exists found the other wrapper's entry, and reading an element of its `arguments` failed with "forwarded-only 'arguments' read as a value". Each function's context now keeps its own map. The corpus program wraps functions with a forwarding-only wrapper and with one that also reads an element of `arguments`, directly and through an alias, in both orders. --- .../src/frontend/lowering/function-context.ts | 7 +++ .../src/frontend/lowering/lower-exprs.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 8 +-- .../src/frontend/lowering/typed-arguments.ts | 6 +- ...ariadic-wrapper-shared-local-names.ts.json | 6 ++ ...385-variadic-wrapper-shared-local-names.ts | 56 +++++++++++++++++++ ...ariadic-wrapper-shared-local-names.ts.json | 1 + 7 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6385-variadic-wrapper-shared-local-names.ts.json create mode 100644 tests/corpus/6385-variadic-wrapper-shared-local-names.ts create mode 100644 tests/harness/corpus-costs/6385-variadic-wrapper-shared-local-names.ts.json diff --git a/packages/compiler/src/frontend/lowering/function-context.ts b/packages/compiler/src/frontend/lowering/function-context.ts index e31e194c5f..55d8ed0b08 100644 --- a/packages/compiler/src/frontend/lowering/function-context.ts +++ b/packages/compiler/src/frontend/lowering/function-context.ts @@ -48,6 +48,12 @@ export interface FnCtx { argumentsLocal?: IrLocal | null; /** Arrows inherit the nearest ordinary function's arguments binding. */ inheritsArguments?: boolean; + /** Tuple bindings of this function that are only forwarded + * (typed-arguments.ts onlyForwarded): binding local id → the native + * slots forwarding reads. Such a binding is never materialized. Local ids + * are unique within one function only, and nested functions never read + * these bindings (a read there needs the tuple). */ + pureTuples: Map; /** Declared return type — lets `return` detect record-shape mismatches * (SC2002) before the validator would ICE on them. */ returnType: IrType; @@ -81,6 +87,7 @@ export function newFnCtx( captures: lifted ? [] : null, captureSources: [], captureBySymbol: new Map(), + pureTuples: new Map(), selfSymbol, selfType, returnType, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index bc970957d8..80b5bb333e 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -908,7 +908,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // type and lowerLambda declared the local. if (expr.text === "arguments") { const binding = captureContextArguments(lowerer.fnStack); - if (binding && lowerer.pureTuples.has(binding.id)) { + if (binding && lowerer.ctx.pureTuples.has(binding.id)) { throw new InternalCompilerError("lowerer bug: forwarded-only 'arguments' read as a value"); } if (binding) return { kind: "varRef", localId: binding.id, type: binding.type, loc }; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ccc140e9e4..26405865c0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1917,10 +1917,6 @@ export class Lowerer { /** Const bindings holding a forwarded fixed tuple (`const args = * arguments as any` in a typed-arguments function) → the tuple type. */ readonly tupleAliasSymbols = new Map(); - /** Tuple bindings that are only forwarded (typed-arguments.ts - * onlyForwarded): binding local id → the native slots forwarding reads. - * Such a binding is never materialized. */ - readonly pureTuples = new Map(); /** The static names of the current generic instance's name-keyed * function parameters (static-function-names.ts). */ staticFnNames: Map | null = null; @@ -10725,7 +10721,7 @@ export class Lowerer { (id) => id !== nameNode && this.checker.getSymbolAtLocation(id) === symbol, ) ) { - this.pureTuples.set(body.id, slots); + this.ctx.pureTuples.set(body.id, slots); return; } const init = this.tupleOfSlots(slots, tupleType, nameNode, loc); @@ -11122,7 +11118,7 @@ export class Lowerer { } if (!symbol) return null; const direct = this.resolveKey(symbol, ident); - if (direct && this.pureTuples.has(direct.id)) { + if (direct && this.ctx.pureTuples.has(direct.id)) { // onlyForwarded proved every read forwards; a plain read here means // the analysis and the lowering disagree. throw new InternalCompilerError( diff --git a/packages/compiler/src/frontend/lowering/typed-arguments.ts b/packages/compiler/src/frontend/lowering/typed-arguments.ts index 90fef0e86d..82b7f23c36 100644 --- a/packages/compiler/src/frontend/lowering/typed-arguments.ts +++ b/packages/compiler/src/frontend/lowering/typed-arguments.ts @@ -176,7 +176,7 @@ export function declareTypedArguments( if ( onlyForwarded(lowerer, node, (id) => id.text === "arguments" && argumentsOwner(id) === node) ) { - lowerer.pureTuples.set(local.id, slots); + lowerer.ctx.pureTuples.set(local.id, slots); return; } prologue.push({ @@ -273,7 +273,7 @@ function pureSlotsOf(lowerer: Lowerer, node: ts.Expression): IrLocal[] | null { if (!ts.isIdentifier(operand)) return null; const local = operand.text === "arguments" ? lowerer.ctx.argumentsLocal : lowerer.peekLocal(operand); - return local ? (lowerer.pureTuples.get(local.id) ?? null) : null; + return local ? (lowerer.ctx.pureTuples.get(local.id) ?? null) : null; } /** The positional values `node` forwards when it is a pure-forwarded tuple @@ -466,7 +466,7 @@ export function lowerTupleAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclarati // An alias of a pure-forwarded tuple is itself only forwarded // (onlyForwarded followed it): it names the same slots. const local = lowerer.declareLocal(decl.name, decl.name.text, tupleType, false); - lowerer.pureTuples.set(local.id, slots); + lowerer.ctx.pureTuples.set(local.id, slots); return { kind: "block", body: [], loc: locOf(decl) }; } const init = lowerer.lowerExpr(stripTypeWrappers(decl.initializer)); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6385-variadic-wrapper-shared-local-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6385-variadic-wrapper-shared-local-names.ts.json new file mode 100644 index 0000000000..5791565f22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6385-variadic-wrapper-shared-local-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6385-variadic-wrapper-shared-local-names.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6385-variadic-wrapper-shared-local-names.ts b/tests/corpus/6385-variadic-wrapper-shared-local-names.ts new file mode 100644 index 0000000000..2d4272b554 --- /dev/null +++ b/tests/corpus/6385-variadic-wrapper-shared-local-names.ts @@ -0,0 +1,56 @@ +// Variadic wrappers whose bodies declare the same local names: one only +// forwards its `arguments` (or its alias of them), the other also reads an +// element, so its tuple exists. Each wrapper forwards its own arguments. +type AnyFn = (...args: any) => any; + +function forward(fn: F): F { + return function (this: unknown) { + return fn.apply(this, arguments as any); + } as F; +} + +function logFirst(fn: F): F { + return function (this: unknown) { + console.log("first:", arguments[0], "of", arguments.length); + return fn.apply(this, arguments as any); + } as F; +} + +function forwardAlias(fn: F): F { + return function (this: unknown) { + const args = arguments as any; + return fn.apply(this, args); + } as F; +} + +function logFirstAlias(fn: F): F { + return function (this: unknown) { + const args = arguments as any; + console.log("alias first:", args[0]); + return fn.apply(this, args); + } as F; +} + +function add3(a: number, b: number, c: number): number { + return a + b + c; +} + +function greet(name: string): string { + return "hi " + name; +} + +const sum = forward(add3); +console.log(sum(1, 2, 3)); +const hello = logFirst(greet); +console.log(hello("ada")); + +const sumAlias = forwardAlias(add3); +console.log(sumAlias(4, 5, 6)); +const helloAlias = logFirstAlias(greet); +console.log(helloAlias("grace")); + +// The other order: the wrapper that reads an element first. +const hello2 = logFirst(greet); +console.log(hello2("linus")); +const sum2 = forward(add3); +console.log(sum2(7, 8, 9)); diff --git a/tests/harness/corpus-costs/6385-variadic-wrapper-shared-local-names.ts.json b/tests/harness/corpus-costs/6385-variadic-wrapper-shared-local-names.ts.json new file mode 100644 index 0000000000..187f1764f2 --- /dev/null +++ b/tests/harness/corpus-costs/6385-variadic-wrapper-shared-local-names.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3300 }