Files
agda-spa/LEAN_MIGRATION.md
Danila Fedorin b16f14fdfd Lean migration: typeclass-based parameter passing, as in the Agda original
The port had flattened Agda's instance arguments ({{flA}}, {{evaluator}},
{{latticeInterpretation}}, {{validEvaluator}}) into explicitly threaded
values (fhL, E, I, hE). Restore them as typeclasses:

- Spa.FiniteHeightLattice: now actually used — Fixedpoint takes the
  instance instead of a FixedHeight value; FiniteMap gets the missing
  instance (height = ks.length * height B), so varsFixedHeight /
  statesFixedHeight / signFixedHeight / constFixedHeight plumbing
  disappears (instance bottoms are defeq to the old ones)
- Spa.Analysis.Forward.Evaluation: StmtEvaluator/ExprEvaluator become
  classes; the Valid* Props become Prop-classes, as in Agda
- Spa.Analysis.Forward.Adapters: the expr→stmt adapter and its validity
  are instances (Agda: the ExprToStmtAdapter instances)
- LatticeInterpretation is a class; sign/const interpretations,
  evaluators and validity proofs are instances; use sites read like the
  Agda module applications: result SignLattice prog

Proof simplifications (same theorems, proofs factored):

- Spa.Lattice.AboveBelow.monotone₂_of_strict: any ⊥-strict/⊤-dominated
  operation on a flat lattice is monotone — replaces the four near-
  identical case bashes per analysis (postulates in Agda)
- Spa.Lattice.AboveBelow.interp_sup_of/interp_inf_of: the shared flat-
  lattice interpretation case analysis, making interpSign_sup/inf and
  interpConst_sup/inf one-liners

lake build green with zero warnings; lake exe spa output verified
byte-identical (diff) to the previous, Agda-verified output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 23:32:38 -07:00

118 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Agda → Lean 4 (mathlib) migration plan
Goal: port the static-analysis framework to Lean 4 + mathlib, preserving the
overall structure and **the same theorems/lemmas** (modulo language details),
while lifting custom machinery into mathlib wherever a standard counterpart
exists. Per discussion, the setoid equality (`_≈_`) is **dropped in favor of
propositional `=`** — it existed mainly so that unordered key-value maps could
be "equal"; representations below are chosen to be canonical so `=` works.
The Lean project lives in `lean/` (library root `Spa`). Each phase ends with a
green `lake build` and a correspondence table appended to this file, so you can
validate phase by phase.
## Design mapping
| Agda | Lean | Notes |
|---|---|---|
| `Equivalence.agda` | *lifted*: `Eq`, `Equivalence` | module disappears |
| `IsDecidable` | *lifted*: `DecidableEq` / `DecidableRel` | mathlib is classical; decidability kept only where functions compute (e.g. the fixpoint iteration) |
| `Showable.agda` | *lifted*: `ToString` | |
| `Lattice.agda` `IsSemilattice` (`⊔-assoc/comm/idemp`, `≼`, `≼-refl/trans/antisym`, `x≼x⊔y`, `⊔-Monotonicˡ/ʳ`) | *lifted*: `SemilatticeSup` (`sup_assoc`, `sup_comm`, `sup_idem`, `≤` with `sup_eq_right`, `le_refl`, `le_trans`, `le_antisymm`, `le_sup_left`, `sup_le_sup_left/right`) | `a ≼ b := a ⊔ b ≈ b` becomes `a ≤ b` with bridge lemma `sup_eq_right` |
| `IsLattice` (`absorb-⊔-⊓`, `absorb-⊓-⊔`) | *lifted*: `Lattice` (`sup_inf_self`, `inf_sup_self`) | |
| `Monotonic`, `Monotonicˡ/ʳ/₂` | *lifted*: `Monotone` (+ tiny aliases) | |
| `foldr-Mono`, `foldl-Mono`, `foldr-Mono'`, `foldl-Mono'` | custom, `Spa/Lattice.lean` | stated with `List.Forall₂` (≙ `Utils.Pairwise`) |
| `Chain.agda` (`Chain`, `concat`, `Chain-map` in `ChainMapping`) | *lifted*: `LTSeries` (`RelSeries.smash`, `LTSeries.map` + `Monotone.strictMono_of_injective`) | with `=`, the ≈-congruence steps in chains vanish |
| `Chain.Height`, `Bounded`, `Bounded-suc-n` | custom: `Spa.FixedHeight` structure (`⊥`, ``, longest `LTSeries`, `bounded`) | |
| `IsFiniteHeightLattice`, `FiniteHeightLattice` | custom class `Spa.FiniteHeightLattice` | |
| `⊥≼` (chain bottom is least, given decidable eq) | custom, same proof shape (prepend `⊥⊓a ≺ ⊥` to longest chain) | decidability hypothesis dropped (classical) |
| `Fixedpoint.agda` (`doStep` with gas, `aᶠ`, `aᶠ≈faᶠ`, `aᶠ≼`) | custom, `Spa/Fixedpoint.lean`, same gas-based algorithm | **not** replaced by mathlib `lfp` (would change the proof approach and lose computability) |
| `Isomorphism.agda` (`TransportFiniteHeight`) | custom, `Spa/Isomorphism.lean` | much smaller: with `=`, f/g monotone inverse pair transports `FixedHeight` via `LTSeries.map` |
| `Lattice/Unit.agda` | *lifted*: mathlib `Lattice PUnit`; custom `FixedHeight PUnit 0` | |
| `Lattice/Nat.agda` (max/min lattice) | *lifted*: mathlib `Lattice ` (`Nat.instLattice`) | kept only as a remark; file had no fixed-height content |
| `Lattice/Prod.agda` | instance *lifted* (`Prod.instLattice`); custom: `unzip` + `FixedHeight (A×B) (h₁+h₂)` | same proof: split a product chain into component chains |
| `Lattice/AboveBelow.agda` (flat lattice ⊥/[x]/) | custom, same datatype; `Plain` module ⇒ `FixedHeight 2` | mathlib has no flat-lattice-on-discrete-type |
| `Lattice/ExtendBelow.agda` | *lifted*: `WithBot A` lattice instance; custom `FixedHeight (h+1)` | unused by the pipeline; ported for parity (optional) |
| `Lattice/IterProd.agda` | custom, same induction (`IterProd k = A ×× B`), lattice + height-sum by recursion | the `Everything` record trick survives as a recursive definition of bundled instances |
| `Lattice/Map.agda` (assoc list with `Unique` keys, setoid) | **deleted**: only existed to support setoid map equality | its consumers move to `Finset` / spine-fixed `FiniteMap` |
| `Lattice/MapSet.agda` (`StringSet`) | *lifted*: `Finset String` (``, `{·}`, `∅`, `.toList`, `nodup_toList`) | |
| `Lattice/FiniteMap.agda` | custom: `{ l : List (A × B) // l.map Prod.fst = ks }` — key spine fixed ⇒ `=` is pointwise value equality | same API: `locate`, `_[_]`, `GeneralizedUpdate` (`f'`, `f'-Monotonic`, `f'-k∈ks-≡`, `f'-k∉ks-backward`), `m₁≼m₂⇒m₁[k]≼m₂[k]`, `Provenance-union` analog; fixed height **still via isomorphism to `IterProd`** (same approach) |
| `Lattice/Builder.agda` | **skipped** — not imported by anything in the repo | flag if you want it |
| `Utils.agda` | *lifted*: `Unique``List.Nodup`, `Pairwise``List.Forall₂`, `fins``List.finRange`, `∈-cartesianProduct``List.product`/`pair_mem_product`, `x∈xs⇒fx∈fxs``List.mem_map_of_mem`, `filter-++``List.filter_append`, `iterate``f^[n]`, `concat-∈``List.mem_join`, `All¬-¬Any` etc. → `List.All`/`Any` API | leftovers (if any) in `Spa/Utils.lean` |
| `Language/Base.agda` | custom; `Expr-vars`/`Stmt-vars : Finset String` | commented-out `∈-vars` lemmas stay omitted |
| `Language/Semantics.agda` | custom, same big-step relations; `Value`, `Env = List (String × Value)`, custom `∈` | ```Int` |
| `Language/Graphs.agda` | custom; `Vec``Vector` (mathlib `List.Vector`), `Fin._↑ˡ/_↑ʳ``Fin.castAdd`/`Fin.natAdd` | same `Graph` record, `∙`/`↦`/`loop`/`skipto`/`singleton`/`wrap`/`buildCfg`, `predecessors` + edge lemmas |
| `Language/Traces.agda` | custom, same `Trace`/`EndToEndTrace`/`++⟨_⟩` | |
| `Language/Properties.agda` | custom, same lemma inventory (`Trace-∙ˡ/ʳ`, `Trace-↦ˡ/ʳ`, `Trace-loop`, `EndToEndTrace-*`, `wrap-preds-∅`, `buildCfg-sufficient`) | the "ugly" `↑-≢` Fin-disjointness block should shrink via `Fin.castAdd_ne_natAdd`-style mathlib lemmas |
| `Language.agda` (`Program` record) | custom, same fields/lemmas (`trace`, `vars`, `states`, `incoming`, `initialState-pred-∅`, …) | |
| `Analysis/Forward/{Lattices,Evaluation,Adapters}.agda`, `Analysis/Forward.agda` | custom, same structure: `VariableValues`, `StateVariables`, `joinForKey`/`joinAll`, `StmtEvaluator`/`ExprEvaluator` + validity, expr→stmt adapter, `analyze`, `result`, `analyze-correct` | section variables instead of parameterized modules; everything Agda passed as an instance argument (`IsFiniteHeightLattice`, the evaluators, `LatticeInterpretation`, the validity records) is a typeclass resolved by instance search |
| `Analysis/Sign.agda`, `Analysis/Constant.agda` | custom, same definitions | the four monotonicity **postulates** become real proofs (any `⊥`-strict/``-dominating operation on a flat lattice is monotone: `AboveBelow.monotone₂_of_strict`) |
| `Main.agda` | `lake exe spa` | same test programs, same printed output |
## Phases & checkpoints
- **Phase 0 — scaffold.** `lean/` lake project, mathlib pinned to toolchain
v4.17.0 (already installed). ✅ checkpoint: `lake build` green on empty lib.
- **Phase 1 — core order theory.** `Spa/Lattice.lean` (Monotone aliases, fold
monotonicity, `FixedHeight`, `Bounded`, `FiniteHeightLattice`, chain-bottom-
is-least). ✅ checkpoint: build + table below.
- **Phase 2 — fixpoint & transport.** `Spa/Fixedpoint.lean`,
`Spa/Isomorphism.lean`. ✅ checkpoint: `fix`, `fix_eq`, `fix_le`,
`TransportFiniteHeight`.
- **Phase 3 — basic lattice instances.** Unit, Prod (+height), AboveBelow
(+`Plain`, height 2), ExtendBelow. ✅ checkpoint.
- **Phase 4 — map lattices.** IterProd, FiniteMap (+fixed height via IterProd
isomorphism), MapSet→`Finset` shims. ✅ checkpoint.
- **Phase 5 — language.** Base, Semantics, Graphs, Traces, Properties,
`Program`. ✅ checkpoint: `buildCfg_sufficient`, `Program.trace`.
- **Phase 6 — forward analysis framework.** Lattices/Evaluation/Adapters/
Forward. ✅ checkpoint: `analyze_correct`.
- **Phase 7 — concrete analyses + executable.** Sign, Constant, Main.
✅ checkpoint: `lake exe spa` output vs Agda `Main` output; postulates now
proved.
## Status
- [x] Phase 0
- [x] Phase 1
- [x] Phase 2
- [x] Phase 3
- [x] Phase 4
- [x] Phase 5
- [x] Phase 6
- [x] Phase 7
All phases complete: `lake build` is green with zero warnings, zero `sorry`s
and zero axioms, and `lake exe spa` prints output **byte-for-byte identical**
to the compiled Agda `Main` (verified with `diff`). Per-file `Agda ↦ Lean`
correspondence tables live in the header comment of each Lean file.
## Wins from the migration
- The four monotonicity **postulates** in `Analysis/Sign.agda` and
`Analysis/Constant.agda` are now proved theorems (via
`AboveBelow.monotone₂_of_strict`: any operation on the flat lattice that
is strict in `⊥` and dominated by `` is monotone, whatever its table),
so the Lean development is postulate-free.
- ~2200 lines of map machinery (`Lattice/Map.agda`, `Lattice/MapSet.agda`,
much of `Lattice/FiniteMap.agda`) collapse into the spine-pinned
`FiniteMap` + `Finset`; the `IterProd` isomorphism no longer needs
`Unique ks` (the representation is canonical).
- `Equivalence.agda`, `Chain.agda`, the `IsSemilattice`/`IsLattice`
hierarchy, and most of `Utils.agda` lift into mathlib.
## Deviations & deferred items
- `Lattice/Builder.agda`: not ported (nothing in the repo imports it).
- `Lattice/ExtendBelow.agda`, `Lattice/Nat.agda`: not ported (unused by the
pipeline; `Nat`'s lattice is mathlib's, `ExtendBelow` would be `WithBot` +
a small height proof). Say the word if you want them for parity.
- `Program.vars` lists variables in **sorted** order (`Finset.sort`, since
`Finset.toList` is noncomputable). For the test program this coincides
with the Agda MapSet order.
- Chains are mathlib `LTSeries`, so chain-manipulating proofs
(`Prod` `unzip`, `AboveBelow`'s `isLongest` → a `rank`-based bound) are
restated against that API rather than pattern-matching a custom `Chain`
inductive.
- `Trace`/`EndToEndTrace` are `Prop`-valued and destructured in proofs.