import Lean
import Std.Tactic.Do

open Lean Parser Meta Elab Do

set_option linter.unusedVariables false
set_option backward.do.legacy false

/-!
Hypothetical intrinsic invariant syntax support:
-/

@[inline]
def ForIn.forInInv {m} {ρ : Type u} {α : Type v} [ForIn m ρ α] {β}
    (xs : ρ) (s : β) (f : α → β → m (ForInStep β))
    [Monad m] [inst : ForIn.{u,v,v,v} Id ρ α] {ps} [Std.Do.WP m ps] (inv : Std.Do.Invariant (inst.toList xs) β ps) : m β :=
  forIn xs s f

meta def doInvariant := leading_parser
  "invariant " >> withPosition (
    ppGroup (many1 (ppSpace >> termParser argPrec) >> unicodeSymbol " ↦" " =>" (preserveForPP := true)) >> ppSpace >> withForbidden "do" termParser)
syntax (name := doForInvariant) "for " Term.doForDecl ppSpace doInvariant "do " doSeq : doElem

elab_rules : doElem <= dec
  | `(doElem| for $x:ident in $xs invariant $cursorBinder $stateBinders* => $body do $doSeq) => do
    let call ← elabDoElem (← `(doElem| for $x:ident in $xs do $doSeq)) dec (catchExPostpone := false)
    let_expr bind@Bind.bind m instBind σ γ call k := call
      | throwError "Internal elaboration error: `for` loop did not elaborate to a call of `Bind.bind`; got {call}."
    let_expr ForIn.forIn m ρ α instForIn σ xs s f := call
      | throwError "Internal elaboration error: `for` loop bind argument did not elaborate to a call of `ForIn.forIn`; got {call}."
    call.withApp fun head args => do
    let [u, v, w, x] := head.constLevels!
      | throwError "`Foldable.foldrEta` had wrong number of levels {head.constLevels!}"
    let mi := (← read).monadInfo
    unless ← isLevelDefEq mi.u (mkLevelMax v w) do
      throwError "The universe level of the monadic result type {mi.u} was not the maximum of that of the state tuple {w} and elements {v}. Cannot elaborate invariants for this case."
    unless ← isLevelDefEq mi.v x do
      throwError "The universe level of the result type {mi.v} and that of the continuation result type {x} were different. Cannot elaborate invariants for this case."
    -- First the non-ghost arguments
    let instMonad ← Term.mkInstMVar (mkApp (mkConst ``Monad [mi.u, mi.v]) mi.m)
    let app := mkConst ``ForIn.forInInv [u, v, w, x]
    let app := mkApp5 app m ρ α instForIn σ
    let app := mkApp3 app xs s f
    -- Now the ghost arguments
    let instForIn ← Term.mkInstMVar (mkApp3 (mkConst ``ForIn [u, v, v, v]) (mkConst ``Id [v]) ρ α)
    let ps ← mkFreshExprMVar (mkConst ``Std.Do.PostShape [mi.u])
    let instWP ← Term.mkInstMVar (mkApp2 (mkConst ``Std.Do.WP [mi.u, mi.v]) (← read).monadInfo.m ps)
    let xsToList := mkApp4 (mkConst ``ForIn.toList [u, v]) ρ α instForIn xs
    let cursor := mkApp2 (mkConst ``List.Cursor [v]) α xsToList
    let assertion := mkApp (mkConst ``Std.Do.Assertion [mi.u]) ps
    let mut tuple := s
    let mut reassignments := #[]
    while !tuple.isAppOf ``PUnit.unit do
      let (var, more) ←
        if tuple.isAppOf ``Prod.mk then
          let fst := tuple.getArg! 2
          tuple := tuple.getArg! 3
          pure (fst, true)
        else
          pure (tuple, false)
      match var.fvarId? with
      | none => -- Likely the return value slot. Push a wildcard
        reassignments := reassignments.push (← `(_))
      | some fvarId =>
        let decl ← fvarId.getDecl
        let .some id := (← read).mutVars.find? (·.getId = decl.userName) | continue
        reassignments := reassignments.push id
      unless more do break
    let mutVarBinders ← Term.Quotation.mkTuple reassignments
    let cursorσ := mkApp2 (mkConst ``Prod [v, mi.u]) cursor σ
    let success ← Term.elabFun (← `(fun ($cursorBinder, $(⟨mutVarBinders⟩)) $stateBinders* => $body)) (← mkArrow cursorσ assertion)
    let inv := mkApp3 (mkConst ``Std.Do.PostCond.noThrow [mkLevelMax v mi.u]) cursorσ ps success
    let forIn := mkApp5 app instMonad instForIn ps instWP inv
    return mkApp6 bind m instBind σ γ forIn k

/--
info: (let x := 42;
  let y := 0;
  let z := 1;
  do
  let __s ←
    ForIn.forInInv [1, 2, 3] (x, y)
        (fun i __s =>
          let x := __s.fst;
          let y := __s.snd;
          let x := x + i;
          let y := y + 7;
          pure (ForInStep.yield (x, y)))
        (PostCond.noThrow fun x =>
          match x with
          | (xs, x, y) => ⌜xs.pos = 3 ∧ x = y + z⌝)
  let x : Nat := __s.fst
  let y : Nat := __s.snd
  pure (x + y + z)).run : Nat
-/
#guard_msgs (info) in
open Std.Do in
#check Id.run do
  let mut x := 42
  let mut y := 0
  let mut z := 1
  -- NB: `z` is constant in the invariant, so it will refer to the `let`.
  -- In contrast, `x` and `y` will refer to lambda bound variables from the invariant.
  for i in [1,2,3] invariant xs => ⌜xs.pos = 3 ∧ x = y + z⌝ do
    x := x + i
    y := y + 7
  return x + y + z
