Skip to content

[flow analysis] Prove correctness of the optimized promotion chain join. - #4776

Merged
stereotype441 merged 2 commits into
dart-lang:mainfrom
stereotype441:flow-analysis
Sep 18, 2026
Merged

stereotype441 merged 2 commits into
dart-lang:mainfrom
stereotype441:flow-analysis

Conversation

@stereotype441

Copy link
Copy Markdown
Member

Adds a Lean model of the Dart method PromotionModel.joinPromotedTypes (as of SDK CL 551800, 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 #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.

  • 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.


  • I’ve reviewed the contributor guide and applied the relevant portions to this PR.
Contribution guidelines:

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.

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 chloestefantsova left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, given my current limitations as a lean reviewer we discussed during our meeting.

@stereotype441
stereotype441 merged commit 9cdc5a5 into dart-lang:main Sep 18, 2026
10 checks passed
@stereotype441
stereotype441 deleted the flow-analysis branch September 18, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants