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 }