[flow analysis] Prove correctness of the optimized promotion chain join. - #4776
Merged
Merged
Conversation
Adds a Lean model of the Dart method `PromotionModel.joinPromotedTypes` (as of SDK commit 5651b872ea36c3, with `promotionChainIntersectionJoinEnabled = true`), along with a proof that it computes the join of promotion chains specified in `flow-analysis.md`. This is the follow-up promised in dart-lang#4771: the spec defines the join of two promotion chains as their unique greatest common subsequence, and the Lean model backs that up with a filter-based definition establishing that the join is computable. But that definition is `O(m*n)` (where `m` and `n` are the lengths of the two chains), whereas the SDK uses an `O(m+n)` algorithm that also avoids unnecessary list allocations. This PR proves that the `O(m+n)` algorithm agrees with the other two definitions. Details: - `FlowAnalysis/PromotionChain/JoinImpl.lean` contains `joinPromotedTypesImpl` and `joinPromotedTypesImpl'`, which model the Dart algorithm using Lean's imperative `do` notation, so that they track the structure of the Dart code as closely as possible (including the mutable loop indices, the two "advance" flags, and the lazily allocated `result` accumulator). - `joinPromotedTypesImpl_correct` uses Lean's weakest precondition framework (`Std.Do`) to prove that for all promotion chains `c₁` and `c₂`, the model terminates, returns `(c₁.join c₂).val`, and leaves the monadic state unchanged. The proof is by loop invariant; the invariant and the decreasing measure used to establish termination are spelled out in comments. - `FlowAnalysis/PromotionChain/Basic.lean` (renamed from `FlowAnalysis/PromotionChain.lean` to make room for the new file) gains the three recurrence relations for `join` that drive the correctness proof, covering the cases `T₁ = T₂`, `T₁ ≤ T₂`, and `¬T₁ ≤ T₂`. These correspond to the three branches of the loop body in the Dart implementation. - `FlowAnalysis/WP.lean` contains general purpose helpers for reasoning about monadic models of Dart code: `WP_ite` (which allows an `if/then/else` to be split without `simp` rewriting the surrounding monadic code into an unreadable form), `stateIs`, and a simp lemma for `SVal.curry`. - `DartTypeRepr` now extends `Inhabited` (and `SimpleType`'s default is `Never`), so that the models can use Lean's `xs[i]!` notation for list indexing, avoiding the need to clutter them with proofs that the indices are in range. TAG=agy CONV=a3f3022c-cb28-41fe-b867-ab2cdc117925
While modeling `PromotionModel.joinPromotedTypes` in Lean, I noticed
that one of the early returns in the Dart implementation is dead code:
if (advanceI2) {
if (++i2 == chain2.length) {
if (result != null) return result;
// Result not allocated yet so the join must equal
// chain1.sublist(0, i1).
if (i1 == chain1.length) return chain1; // <-- unreachable
return chain1.sublist(0, i1);
}
...
If `advanceI1` was true during this iteration of the loop, then either
the loop already returned from the `advanceI1` block, or `i1` is still
in range; and if `advanceI1` was false, then `i1` wasn't incremented,
so it's still in range from the previous iteration. Either way,
`i1 == chain1.length` can't hold at this point.
To confirm this reasoning, the Lean model now replaces the branch with
`panic!`, and the correctness proof discharges it by establishing that
`i1` is in range, i.e. that the branch is unreachable.
In https://dart-review.googlesource.com/c/sdk/+/551800, I plan to
replace the branch in the SDK with an assertion.
TAG=agy
CONV=a3f3022c-cb28-41fe-b867-ab2cdc117925
copybara-service Bot
pushed a commit
to dart-lang/sdk
that referenced
this pull request
Sep 17, 2026
Removes an unreachable code path from `PromotionModel.joinPromotedTypes`. Although the performance difference will certainly be negligible, it's nice not to have code lying around that is unreachable (and therefore untestable). Note that dart-lang/language#4776 contains a Lean proof of correctness of this algorithm (with the optimization applied). Change-Id: I9dfcd29e84816f4be8b318aff71319c26a6a6964 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/551800 Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Commit-Queue: Paul Berry <paulberry@google.com>
chloestefantsova
approved these changes
Sep 18, 2026
chloestefantsova
left a comment
Contributor
There was a problem hiding this comment.
LGTM, given my current limitations as a lean reviewer we discussed during our meeting.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a Lean model of the Dart method
PromotionModel.joinPromotedTypes(as of SDK CL 551800, withpromotionChainIntersectionJoinEnabled = true), along with a proof that it computes the join of promotion chains specified inflow-analysis.md.This is the follow-up promised in #4771: the spec defines the join of two promotion chains as their unique greatest common subsequence, and the Lean model backs that up with a filter-based definition establishing that the join is computable. But that definition is
O(m*n)(wheremandnare the lengths of the two chains), whereas the SDK uses anO(m+n)algorithm that also avoids unnecessary list allocations. This PR proves that theO(m+n)algorithm agrees with the other two definitions.Details:
FlowAnalysis/PromotionChain/JoinImpl.leancontainsjoinPromotedTypesImplandjoinPromotedTypesImpl', which model the Dart algorithm using Lean's imperativedonotation, so that they track the structure of the Dart code as closely as possible (including the mutable loop indices, the two "advance" flags, and the lazily allocatedresultaccumulator).joinPromotedTypesImpl_correctuses Lean's weakest precondition framework (Std.Do) to prove that for all promotion chainsc₁andc₂, the model terminates, returns(c₁.join c₂).val, and leaves the monadic state unchanged. The proof is by loop invariant; the invariant and the decreasing measure used to establish termination are spelled out in comments.FlowAnalysis/PromotionChain/Basic.lean(renamed fromFlowAnalysis/PromotionChain.leanto make room for the new file) gains the three recurrence relations forjointhat drive the correctness proof, covering the casesT₁ = T₂,T₁ ≤ T₂, and¬T₁ ≤ T₂. These correspond to the three branches of the loop body in the Dart implementation.FlowAnalysis/WP.leancontains general purpose helpers for reasoning about monadic models of Dart code:WP_ite(which allows anif/then/elseto be split withoutsimprewriting the surrounding monadic code into an unreadable form),stateIs, and a simp lemma forSVal.curry.DartTypeReprnow extendsInhabited(andSimpleType's default isNever), so that the models can use Lean'sxs[i]!notation for list indexing, avoiding the need to clutter them with proofs that the indices are in range.Since 551800 is still pending, this PR is split into two commits: b7d6810 models the current state of the SDK implementation (as of SDK commit dart-lang/sdk@503e5b5), and f36e48f adapts it to reflect 551800.
Contribution guidelines:
dart format.Many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.
Note: The Dart team is trialing Gemini Code Assist. Don't take its comments as final Dart team feedback. Use the suggestions if they're helpful; otherwise, wait for a human reviewer.