import Batteries.Data.Rat.Basic
import Batteries.Control.Lawful.MonadLift

inductive Ast
| lit (n : Rat)
| add (l r : Ast)
| sub (l r : Ast)
| mul (l r : Ast)
| div (l r : Ast)

instance : Coe Rat Ast where coe := .lit
instance : OfScientific Ast where
  ofScientific a b c := .lit (.ofScientific a b c)
instance : OfNat Ast n where ofNat := .lit n

instance : Add Ast where add := .add
instance : Sub Ast where sub := .sub
instance : Mul Ast where mul := .mul
instance : Div Ast where div := .div

def Ast.interpret : Ast → Rat
| lit n => n
| add l r => l.interpret + r.interpret
| sub l r => l.interpret - r.interpret
| mul l r => l.interpret * r.interpret
| div l r => l.interpret / r.interpret

#eval Ast.interpret <| 3/2 + 0.5

inductive Op
| push (n : Rat)
| add
| sub
| mul
| div
  deriving Repr

-- TODO: prove that the stack manip is all good throughout
def Op.pop_push : Op → Nat × Nat
| push _ => (0, 1)
| _ => (2, 1)

abbrev Ops := Array Op

def pushM (x : α) : StateM (Array α) Unit :=
  modify <| (·.push x)

def popM? : StateM (Array α) (Option α) := do
  let top ← Array.back? <$> get
  modify Array.pop
  return top

abbrev M := OptionT (StateM (Array Rat))
instance : LawfulMonad M := sorry

def Op.action (op : Op) : M Unit := do
  match op with
  | .push n => pushM n
  | .add => pushM <| (← popM?) + (← popM?)
  | .sub => pushM <| (← popM?) - (← popM?)
  | .mul => pushM <| (← popM?) * (← popM?)
  | .div => pushM <| (← popM?) / (← popM?)

def Ops.action (ops : Ops) : M Unit := ops.forM Op.action

def Ops.run (ops : Ops) : Option Rat :=
  (do
    ops.action
    popM?
  ).run #[] |>.fst

def Ast.compile : Ast → Ops
| .lit n => #[.push n]
| .add l r => l.compile ++ r.compile ++ #[.add]
| .sub l r => l.compile ++ r.compile ++ #[.sub]
| .mul l r => l.compile ++ r.compile ++ #[.mul]
-- Note that l and r are flipped here. compilation!
| .div l r => r.compile ++ l.compile ++ #[.div]

#eval (3/2 + 0.5 : Ast) |>.compile.run

@[simp]
theorem Ops.append_action (a b : Ops)
    : (a ++ b).action = (do a.action; b.action) := by
  rw [action]
  exact Array.forM_append

@[simp]
theorem Ops.push_action (ops : Ops) (op : Op)
    : action (ops.push op) = (do ops.action; op.action) := by
  rw [Array.push_eq_append, append_action]
  have : op.action = action #[op] := by simp [action]
  rw [this]

theorem Ast.compile_sound (ast : Ast)
    : ast.compile.run = some ast.interpret := by
  induction ast
  · rfl
  simp [compile]
