Lower each closure argument of an implicit-any call once

To key an implicit-any function's instance, storedImplicitArgumentType
lowers a JavaScript arrow or function-expression argument to read its
function type, then discards the value; the call's argument list then
lowers the closure again. Every such closure was lowered twice, and a
closure nested in another one four times, and each lowering lifted its
own copy of the body.

The first lowering now keeps its value (Lowerer.speculatedClosures), and
the argument's own lowering in the same function and with the same type
bindings takes it instead of lowering the closure again. A lowering that
reported diagnostics is not kept.
This commit is contained in:
Chris Tate
2026-10-24 17:18:30 -05:00
parent aa9664bf8a
commit adbd9cd1e9
2 changed files with 41 additions and 2 deletions
@@ -2082,6 +2082,10 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
(ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))
) {
// A closure lowered here before (another call site's instance key, or
// a repeated demand) keeps its value for the argument's own lowering.
const known = lowerer.speculatedClosure(arg);
if (known !== null) return known.type;
const saved = lowerer.diagSink;
const diagnostics: typeof lowerer.diags = [];
lowerer.diagSink = diagnostics;
@@ -2092,8 +2096,17 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
lowerer.diagSink = saved;
}
// Direct inferred helpers can receive native callbacks even when their
// result is an opaque handle that cannot cross checked storage.
if (diagnostics.length === 0 && value?.type.kind === "func") return value.type;
// result is an opaque handle that cannot cross checked storage. The
// argument's own lowering then takes this value instead of lowering
// the closure a second time.
if (diagnostics.length === 0 && value?.type.kind === "func") {
lowerer.speculatedClosures.set(arg, {
ctx: lowerer.ctx,
bindings: lowerer.typeParamBindings,
value,
});
return value.type;
}
}
if (arg.kind === ts.SyntaxKind.ThisKeyword) return lowerer.resolveThis()?.type ?? null;
if (ts.isAwaitExpression(arg)) {
@@ -3174,6 +3174,14 @@ export class Lowerer {
* (builtins/fs-paths.ts): each lowers to the hidden local that holds its
* value, which is null until the path's lowering has saved it. */
readonly stagedArguments = new Map<ts.Expression, { localId: string; type: IrType } | null>();
/** Closure arguments an implicit-any call already lowered to learn their
* type (storedImplicitArgumentType), by node: the argument's own lowering
* in the same function and with the same type bindings takes that value
* once instead of lowering the closure again. */
readonly speculatedClosures = new Map<
ts.Expression,
{ ctx: FnCtx; bindings: Map<ts.Symbol, IrType> | null; value: IrExpr }
>();
/** What each fs path argument of a URL-path call lowers to (the plans
* of builtins/fs-paths.ts). */
readonly fsPathPlans = new Map<ts.Expression, FsPathPlan>();
@@ -12938,12 +12946,30 @@ export class Lowerer {
/* ── expressions ──────────────────────────────────────────────────── */
lowerExpr(expr: ts.Expression): IrExpr {
if (this.speculatedClosures.size > 0) {
const speculated = this.speculatedClosure(expr);
if (speculated !== null) {
this.speculatedClosures.delete(expr);
return speculated;
}
}
const streamValue = this.valueStreamReceivers.get(expr);
if (streamValue)
return valueStreamOf(varRef(streamValue.localId, streamValue.type, locOf(expr)));
return this.stagedArgument(expr) ?? lowerExpr(this, expr);
}
/** The value storedImplicitArgumentType lowered for `expr` in the active
* function and type bindings (speculatedClosures), or null. */
speculatedClosure(expr: ts.Expression): IrExpr | null {
const entry = this.speculatedClosures.get(expr);
return entry !== undefined &&
entry.ctx === this.ctx &&
entry.bindings === this.typeParamBindings
? entry.value
: null;
}
/** The saved value of a staged fs argument (stagedArguments, keyed
* without type-only wrappers), or null for every other expression. */
private stagedArgument(node: ts.Expression): IrExpr | null {