import Lean /-! Tests for the pluggable pure/bind builders in the `do` elaborator (`DoOps`, `elabDoWith`). We define a surface `ido` notation that reuses the full `do` elaborator via `elabDoWith` but emits `IxMonad.pure` / `IxMonad.bind` instead of `Pure.pure` / `Bind.bind`. `IxMonad` is the canonical Atkey parameterised monad (`m : ι → ι → Type u → Type v` with `pure : α → m i i α` and `bind : m i j α → (α → m j k β) → m i k β`); the shape is documented in `Control.Monad.Indexed` on Hackage and the PureScript `indexed-monad` package. The control-stack features of `do` (`mut`, `return`, `break`, `continue`, `for`) remain hard-coded to `Monad` and are therefore off-limits for `ido`. The `ido` programs below avoid them. -/ open Lean Lean.Parser Lean.Meta Lean.Elab Lean.Elab.Do Lean.Elab.Term set_option backward.do.legacy false /-! ## Indexed monad and a concrete instance -/ class IxMonad (m : ι → ι → Type u → Type v) where pure : α → m i i α bind : m i j α → (α → m j k β) → m i k β /-- Atkey-style indexed state: `IState i o α = i → α × o`. -/ abbrev IState (i o α : Type) : Type := i → α × o instance : IxMonad IState where pure a := fun i => (a, i) bind p f := fun i => let (a, j) := p i; f a j /-! Helpers that keep the state type fixed at `Nat` for the common examples. -/ def getN : IState Nat Nat Nat := fun s => (s, s) def putN (n : Nat) : IState Nat Nat Unit := fun _ => ((), n) def modifyN (f : Nat → Nat) : IState Nat Nat Unit := fun i => ((), f i) /-! ## Pluggable ops emitting `IxMonad.pure` / `IxMonad.bind` -/ def ixOps : DoOps := { DoOps.default with mkPureApp α e := do let info := (← read).monadInfo let mα := mkApp info.m α let eStx ← Term.exprToSyntax e let stx ← `(IxMonad.pure $eStx) Term.elabTermEnsuringType stx mα mkBindApp α β e k := do let info := (← read).monadInfo let mβ := mkApp info.m β let eStx ← Term.exprToSyntax e let kStx ← Term.exprToSyntax k let stx ← `(IxMonad.bind $eStx $kStx) Term.elabTermEnsuringType stx mβ isPureApp? e := -- `@IxMonad.pure ι m inst α i e` — 6 args. if e.isAppOfArity ``IxMonad.pure 6 then some (e.getArg! 5) else none } /-! ## `ido` surface syntax -/ syntax (name := idoKind) "ido " doSeq : term @[term_elab idoKind] def elabIDo : Term.TermElab := fun stx et? => do let `(ido $doSeq) := stx | throwUnsupportedSyntax elabDoWith ixOps doSeq et? /-! ## Example programs Each example pairs `#guard_msgs` with `#eval` (or `#check`) to lock behaviour in. Most keep state type fixed at `Nat`; a couple at the end explore index-preserving variants with different state types. -/ /-! ### 1. Bare pure -/ /-- info: (42, 10) -/ #guard_msgs in #eval (ido IxMonad.pure 42 : IState Nat Nat Nat) 10 /-! ### 2. Monadic `let ← ` -/ /-- info: (11, 10) -/ #guard_msgs in #eval (ido do let x ← getN IxMonad.pure (x + 1) : IState Nat Nat Nat) 10 /-! ### 3. Plain `let :=` -/ /-- info: (20, 10) -/ #guard_msgs in #eval (ido do let x := 10 let y ← getN IxMonad.pure (x + y) : IState Nat Nat Nat) 10 /-! ### 4. Sequential unit-typed element -/ /-- info: (11, 11) -/ #guard_msgs in #eval (ido do modifyN (· + 1) getN : IState Nat Nat Nat) 10 /-! ### 5. Multi-step chain -/ /-- info: ((10, 11), 11) -/ #guard_msgs in #eval (ido do let a ← getN modifyN (· + 1) let b ← getN IxMonad.pure (a, b) : IState Nat Nat (Nat × Nat)) 10 /-! ### 6. Nested `(← …)` in pure context -/ /-- info: (11, 10) -/ #guard_msgs in #eval (ido IxMonad.pure ((← getN) + 1) : IState Nat Nat Nat) 10 /-! ### 7. Nested `(← …)` appearing twice in one expression -/ /-- info: (20, 10) -/ #guard_msgs in #eval (ido IxMonad.pure ((← getN) + (← getN)) : IState Nat Nat Nat) 10 /-! ### 8. `if/then/else` with do branches -/ /-- info: (5, 5) -/ #guard_msgs in #eval (ido do let x ← getN if x > 0 then IxMonad.pure x else IxMonad.pure 0 : IState Nat Nat Nat) 5 /-- info: (0, 0) -/ #guard_msgs in #eval (ido do let x ← getN if x > 0 then IxMonad.pure x else IxMonad.pure 0 : IState Nat Nat Nat) 0 /-! ### 9. `if` with a lifted action in the condition -/ /-- info: ((), 4) -/ #guard_msgs in #eval (ido do if (← getN) > 0 then modifyN (· - 1) else IxMonad.pure () : IState Nat Nat Unit) 5 /-- info: ((), 0) -/ #guard_msgs in #eval (ido do if (← getN) > 0 then modifyN (· - 1) else IxMonad.pure () : IState Nat Nat Unit) 0 /-! ### 10. `match` dispatching into do blocks -/ /-- info: (100, 7) -/ #guard_msgs in #eval (ido do match (← getN) with | 0 => IxMonad.pure 0 | _ => IxMonad.pure 100 : IState Nat Nat Nat) 7 /-! ### 11. Pattern `let` -/ /-- info: (3, 0) -/ #guard_msgs in #eval (ido do let (a, b) ← IxMonad.pure (1, 2) IxMonad.pure (a + b) : IState Nat Nat Nat) 0 /-! ### 12. Nested `ido` inside `ido` -/ /-- info: (42, 0) -/ #guard_msgs in #eval (ido do let y ← (ido IxMonad.pure 42 : IState Nat Nat Nat) IxMonad.pure y : IState Nat Nat Nat) 0 /-! ### 13. `ido` composing with ordinary `do` -/ /-- info: 84 -/ #guard_msgs in #eval Id.run do let (n, _) := (ido IxMonad.pure 42 : IState Nat Nat Nat) 0 pure (n * 2) /-! ### 14. `pure e >>= pure` peephole — confirms the generated term has no redundant `IxMonad.bind`. The equation `(ido do let x ← IxMonad.pure 17; IxMonad.pure x) = IxMonad.pure 17` holds definitionally only if the peephole in `mkBindUnlessPure` fired and contracted the bind away, emitting a bare `IxMonad.pure 17`. If the peephole failed, the result would be `IxMonad.bind (IxMonad.pure 17) IxMonad.pure`, which is not definitionally equal to `IxMonad.pure 17` because `IxMonad` is a plain `class` without beta-reduction laws. -/ example : (ido do let x ← IxMonad.pure 17 IxMonad.pure x : IState Nat Nat Nat) = IxMonad.pure 17 := rfl /-! ### 15. Deeper chains of binds -/ /-- info: (6, 10) -/ #guard_msgs in #eval (ido do let a ← IxMonad.pure 1 let b ← IxMonad.pure 2 let c ← IxMonad.pure 3 IxMonad.pure (a + b + c) : IState Nat Nat Nat) 10 /-! ### 16. Index-preserving monad with a different fixed state type -/ /-- info: ("hi there", "hi") -/ #guard_msgs in #eval (ido do let s ← (fun (σ : String) => (σ, σ) : IState String String String) IxMonad.pure (s ++ " there") : IState String String String) "hi"