diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 72fec97e7..e0718158d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -757,8 +757,11 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir const pendingSlots: DynamicSlot[] = []; try { for (let attempt = 0; ; attempt++) { + const hits = new Set(); try { - lowerOneStatement(lowerer, stmt, out, usingMarkers, diagsBefore, deferFences); + lowerer.withPartialHits(hits, () => + lowerOneStatement(lowerer, stmt, out, usingMarkers, diagsBefore, deferFences), + ); break; } catch (e) { // Localized failure's statement commit point (design ยง3.3): a @@ -768,7 +771,14 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir // no retry runs) can retry here too. if (attempt >= MAX_DEGRADE_RETRIES || deferFences) throw e; const slot = lowerer.degradeAtCommit(e, STATEMENT_SLOTS); - if (slot === null && !lowerer.reopenFlushedAtCommit(diagsBefore)) throw e; + // Pushed-down types the failed attempt mapped widen to the + // whole slot before anything else is given up. + if ( + slot === null && + !lowerer.reopenFlushedAtCommit(diagsBefore) && + !lowerer.widenPartials(hits) + ) + throw e; // A failed attempt adds nothing to `out` but the TDZ boxes // forward captures declared (they stay declared), so the // retry appends after them. diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 53a79195c..5d926f69f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3788,6 +3788,9 @@ export class Lowerer { number, { lenient: IrType | null; code: string; reason: string } >(); + /** One frame per unit a commit point may retry: the pushed-down + * (partial) degraded types its lowering mapped (withPartialHits). */ + readonly partialHits: Set[] = []; /** The dynamic slots the commit points created: one SC6104 each. */ readonly dynamicSlots: DynamicSlot[] = []; /** Declarations retryDeferred registered. */ @@ -3950,6 +3953,7 @@ export class Lowerer { degraded.lenient = DYN; degraded.lenient = this.lenientType(type); } + if (degraded.lenient.kind !== "dyn") this.partialHits[this.partialHits.length - 1]?.add(id); return degraded.lenient; }, isErasing: () => this.erasingDepth > 0, @@ -9572,16 +9576,17 @@ export class Lowerer { const slots: DynamicSlot[] = []; for (let attempt = 0; ; attempt++) { const mark = this.diags.length; + const hits = new Set(); try { - const result = lower(); + const result = this.withPartialHits(hits, lower); for (const slot of slots) this.noteDynamicSlot(slot); return result; } catch (e) { if (!(e instanceof PoisonError)) throw e; if (attempt >= MAX_DECLARATION_RETRIES) return null; const slot = this.degradeAtCommit(e, DECLARATION_SLOTS); - if (slot === null) return null; - slots.push(slot); + if (slot === null && !this.widenPartials(hits)) return null; + if (slot !== null) slots.push(slot); this.dropAttemptDiags(mark); } } @@ -9614,6 +9619,34 @@ export class Lowerer { return true; } + /** Runs a unit a commit point may retry, recording the pushed-down + * types it maps; the frame's hits also count for the enclosing unit. */ + withPartialHits(hits: Set, run: () => T): T { + this.partialHits.push(hits); + try { + return run(); + } finally { + this.partialHits.pop(); + const outer = this.partialHits[this.partialHits.length - 1]; + if (outer !== undefined) for (const id of hits) outer.add(id); + } + } + + /** Push-down is a refinement, never a new failure: a unit that still + * fails after mapping pushed-down types falls back to the whole-slot + * checked dynamic for them. True when some type widened (retry). */ + widenPartials(hits: ReadonlySet): boolean { + if (this.tierMode !== "auto" || this.diagSink !== null) return false; + let widened = false; + for (const id of hits) { + const degraded = this.degradedTypes.get(id); + if (degraded === undefined || degraded.lenient?.kind === "dyn") continue; + degraded.lenient = DYN; + widened = true; + } + return widened; + } + /** Records a dynamic slot whose unit lowered with it (one per site). */ noteDynamicSlot(slot: DynamicSlot): void { const seen = this.dynamicSlots.some( @@ -12654,9 +12687,19 @@ export class Lowerer { this.deferredDiags.delete(symbol); const lifted = this.liftDynFieldPolicy; this.liftDynFieldPolicy = true; - let registered: boolean; + // Pushed-down types that did not help widen to the whole slot, and the + // collection runs again (a few times at most). + let registered = false; try { - registered = this.collectDeferring(collector.symbolOf, collector.collect) === null; + for (let widen = 0; widen < 4; widen++) { + const hits = new Set(); + registered = + this.withPartialHits(hits, () => + this.collectDeferring(collector.symbolOf, collector.collect), + ) === null; + if (registered || !this.widenPartials(hits)) break; + this.deferredDiags.delete(symbol); + } } finally { this.liftDynFieldPolicy = lifted; }