Lower each closure argument of an implicit-any call once (#2150)

This commit is contained in:
Chris Tate
2026-10-24 17:56:00 -05:00
committed by GitHub
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)) {
@@ -3178,6 +3178,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>();
@@ -12946,12 +12954,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 {