import OctiveLean.Value import OctiveLean.Env import OctiveLean.Error import OctiveLean.AST namespace OctiveLean /-! # Phase A — Pure Evaluation Monad `PureM` replaces `IO` with `Id` at the base, making computations fully transparent to Lean's kernel. This unlocks formal reasoning over expression evaluation, control flow, and scoping without touching IO. `EvalM = ExceptT OctaveError (StateT Env IO)` — executable, IO-opaque `PureM = ExceptT OctaveError (StateT Env Id)` — provable, kernel-transparent The connection: `liftPure : PureM α → EvalM α` is a monad homomorphism. Any property proved about a `PureM` computation transfers to its `EvalM` lift. IO-only operations (display, input, rand) remain in `EvalM`. When pure evaluation encounters a builtin call, it throws a sentinel error so the IO layer can re-dispatch. -/ abbrev PureM := ExceptT OctaveError (StateT Env Id) def runPureM {α} (m : PureM α) (env : Env) : Except OctaveError α × Env := StateT.run (ExceptT.run m) env /-- Lift a pure computation into EvalM. Any `PureM` result transfers upward. -/ def liftPure {α} (m : PureM α) : ExceptT OctaveError (StateT Env IO) α := do let env ← get let (result, env') := runPureM m env set env' ExceptT.mk (pure result) private def getPureEnv : PureM Env := get private def setPureEnv (e : Env) : PureM Unit := set e private def lookupVarP (name : String) : PureM Value := do let env ← getPureEnv match env.get name with | some v => return v | none => match name with | "i" | "j" => return .complex 0.0 1.0 | _ => if env.getBuiltin name |>.isSome then return .fn (.builtin name) else throw (.nameError name) private def setVarP (name : String) (val : Value) : PureM Unit := modify (·.set name val) private def arrFillP (n : Nat) (v : Float) : Array Float := List.replicate n v |>.toArray /-! Non-partial helpers — these CAN be unfolded by Lean's kernel for proofs. -/ def toFloatP (v : Value) : PureM Float := match v.materialize with | .scalar f => return f | .fscalar f => return f | .complex r _ => return r | .integer iv => return iv.toFloat | .boolean b => return if b then 1.0 else 0.0 | .matrix 1 1 d => return d[0]! | other => throw (.typeError s!"expected scalar, got {other.typeName}") def evalLiteralP (lit : Literal) : Value := match lit with | .float f => .scalar f | .int n => .scalar (Float.ofInt n) | .str s => .string s | .bool b => .boolean b def evalConstantP (name : String) : Option Value := match name with | "true" => some (.boolean true) | "false" => some (.boolean false) | "pi" => some (.scalar 3.141592653589793) | "e" => some (.scalar 2.718281828459045) | "Inf" | "inf" => some (.scalar (1.0 / 0.0)) | "NaN" | "nan" => some (.scalar (0.0 / 0.0)) | "eps" => some (.scalar 2.220446049250313e-16) | _ => none def isTruthy (v : Value) : Bool := match v with | .boolean b => b | .scalar f => f != 0.0 | .integer iv => iv.toFloat != 0.0 | .empty => false | _ => true /-- Non-partial binary op dispatcher (dispatches to helpers, no recursion over AST). -/ private partial def ewiseOpP (op : Float → Float → Float) (a b : Value) : PureM Value := match a.materialize, b.materialize with | .scalar x, .scalar y => return .scalar (op x y) | .scalar x, .matrix r c d => return .matrix r c (d.map (op x ·)) | .matrix r c d, .scalar y => return .matrix r c (d.map (op · y)) | .matrix r1 c1 d1, .matrix r2 c2 d2 => if r1 == r2 && c1 == c2 then return .matrix r1 c1 (Array.zipWith (op · ·) d1 d2) else throw (.valueError s!"matrix size mismatch: {r1}×{c1} vs {r2}×{c2}") | .boolean b, v => ewiseOpP op (.scalar (if b then 1.0 else 0.0)) v | v, .boolean b => ewiseOpP op v (.scalar (if b then 1.0 else 0.0)) | .integer iv, v => ewiseOpP op (.scalar iv.toFloat) v | v, .integer iv => ewiseOpP op v (.scalar iv.toFloat) | la, lb => throw (.typeError s!"cannot apply arithmetic to {la.typeName} and {lb.typeName}") private def cmpOpP (op : Float → Float → Bool) (a b : Value) : PureM Value := do let x ← toFloatP a; let y ← toFloatP b return .boolean (op x y) private def matMulP (r1 c1 : Nat) (d1 : Array Float) (r2 c2 : Nat) (d2 : Array Float) : PureM Value := do if c1 != r2 then throw (.valueError s!"matrix multiply: {r1}×{c1} * {r2}×{c2} incompatible") let out := Id.run do let mut o := arrFillP (r1 * c2) 0.0 for i in List.range r1 do for j in List.range c2 do let mut s := 0.0 for k in List.range c1 do s := s + d1[i * c1 + k]! * d2[k * c2 + j]! o := o.set! (i * c2 + j) s o return .matrix r1 c2 out /-- Non-partial scalar binary op. Kernel-transparent: enables formal arithmetic proofs. -/ def evalBinOpScalarP (op : BinOp) (x y : Float) : PureM Value := match op with | .add => return .scalar (x + y) | .sub => return .scalar (x - y) | .mul => return .scalar (x * y) | .emul => return .scalar (x * y) | .div => return .scalar (x / y) | .ediv => return .scalar (x / y) | .eldiv => return .scalar (y / x) | .ldiv => return .scalar (y / x) | .epow => return .scalar (Float.pow x y) | .pow => return .scalar (Float.pow x y) | .lt => return .boolean (x < y) | .le => return .boolean (x <= y) | .gt => return .boolean (x > y) | .ge => return .boolean (x >= y) | .eq => return .boolean (x == y) | .ne => return .boolean (x != y) | .land => return .boolean (x != 0.0 && y != 0.0) | .lor => return .boolean (x != 0.0 || y != 0.0) | .band => return .boolean (x != 0.0 && y != 0.0) | .bor => return .boolean (x != 0.0 || y != 0.0) def evalBinOpP (op : BinOp) (lv rv : Value) : PureM Value := -- Non-partial scalar fast path: both sides materialize to .scalar match lv.materialize, rv.materialize with | .scalar x, .scalar y => evalBinOpScalarP op x y | lm, rm => match op with | .add => ewiseOpP (· + ·) lm rm | .sub => ewiseOpP (· - ·) lm rm | .emul => ewiseOpP (· * ·) lm rm | .ediv => ewiseOpP (· / ·) lm rm | .eldiv => ewiseOpP (fun a b => b / a) lm rm | .epow => ewiseOpP Float.pow lm rm | .mul => match lm, rm with | .scalar x, v => ewiseOpP (· * ·) (.scalar x) v | v, .scalar y => ewiseOpP (· * ·) v (.scalar y) | .matrix r1 c1 d1, .matrix r2 c2 d2 => matMulP r1 c1 d1 r2 c2 d2 | la, lb => throw (.typeError s!"cannot multiply {la.typeName} * {lb.typeName}") | .div => match rm with | .scalar y => ewiseOpP (· / ·) lm (.scalar y) | _ => throw (.typeError "matrix right-divide not yet implemented") | .ldiv => match lm with | .scalar x => ewiseOpP (fun a b => b / a) (.scalar x) rm | _ => throw (.typeError "matrix left-divide not yet implemented") | .pow => match lm, rm with | .scalar x, .scalar y => return .scalar (Float.pow x y) | _, _ => throw (.typeError "matrix power not yet implemented") | .lt => cmpOpP (· < ·) lm rm | .le => cmpOpP (· <= ·) lm rm | .gt => cmpOpP (· > ·) lm rm | .ge => cmpOpP (· >= ·) lm rm | .eq => cmpOpP (· == ·) lm rm | .ne => cmpOpP (· != ·) lm rm | .land => do return .boolean ((← toFloatP lm) != 0.0 && (← toFloatP rm) != 0.0) | .lor => do return .boolean ((← toFloatP lm) != 0.0 || (← toFloatP rm) != 0.0) | .band => do return .boolean ((← toFloatP lm) != 0.0 && (← toFloatP rm) != 0.0) | .bor => do return .boolean ((← toFloatP lm) != 0.0 || (← toFloatP rm) != 0.0) private def indexValueP (v : Value) (args : Array Value) : PureM Value := do match v.materialize with | .matrix rows cols data => if args.size == 1 then let i ← toFloatP args[0]! let idx := i.toUInt64.toNat - 1 if idx < data.size then return .scalar data[idx]! else throw (.indexError s!"index {idx+1} out of bounds for {rows}×{cols}") else if args.size == 2 then let r ← toFloatP args[0]!; let c ← toFloatP args[1]! let ri := r.toUInt64.toNat - 1; let ci := c.toUInt64.toNat - 1 if ri < rows && ci < cols then return .scalar data[ri * cols + ci]! else throw (.indexError s!"index ({ri+1},{ci+1}) out of bounds for {rows}×{cols}") else throw (.indexError "too many indices for matrix") | .string s => let idx ← toFloatP args[0]! let i := idx.toUInt64.toNat - 1 let chars := s.toList.toArray if i < chars.size then return .string (String.singleton chars[i]!) else throw (.indexError "string index out of bounds") | .cell _ _ data => let i ← toFloatP args[0]! let idx := i.toUInt64.toNat - 1 if idx < data.size then return data[idx]! else throw (.indexError "cell index out of bounds") | other => throw (.typeError s!"cannot index {other.typeName}") private def matrixWriteP (base : Value) (idxs : Array Value) (newVal : Value) : PureM Value := do let toF : Value → PureM Float := fun v => match v.materialize with | .scalar f | .fscalar f => pure f | .integer iv => pure iv.toFloat | .boolean b => pure (if b then 1.0 else 0.0) | .matrix 1 1 d => pure d[0]! | other => throw (.typeError s!"expected scalar index, got {other.typeName}") let toN : Value → PureM Nat := fun v => do return (← toF v).toUInt64.toNat let fv ← toF newVal match base.materialize, idxs with | .matrix r c d, #[iv] => do let i := (← toN iv) - 1 if i < r * c then return Value.matrix r c (d.set! i fv) else let extended := d ++ arrFillP (i + 1 - d.size) 0.0 return Value.matrix 1 (i + 1) (extended.set! i fv) | .matrix r c d, #[ri, ci] => do let row := (← toN ri) - 1; let col := (← toN ci) - 1 let newR := max r (row + 1); let newC := max c (col + 1) let grown : Array Float := if newR > r || newC > c then Id.run do let mut nd := arrFillP (newR * newC) 0.0 for i in List.range r do for j in List.range c do nd := nd.set! (i * newC + j) d[i * c + j]! nd else d return Value.matrix newR newC (grown.set! (row * newC + col) fv) | .empty, #[iv] => do let i := (← toN iv) - 1 return Value.matrix 1 (i + 1) ((arrFillP (i + 1) 0.0).set! i fv) | .empty, #[ri, ci] => do let row := (← toN ri) - 1; let col := (← toN ci) - 1 return Value.matrix (row+1) (col+1) ((arrFillP ((row+1)*(col+1)) 0.0).set! (row*(col+1)+col) fv) | .scalar _, #[iv] => do if (← toN iv) == 1 then return newVal else throw (.indexError "scalar index out of bounds") | b, _ => throw (.typeError s!"indexed assignment on {b.typeName}") /-! Mutual evaluator in PureM -/ mutual partial def evalExprP (e : Expr) : PureM Value := do match e with | .lit lit => return evalLiteralP lit | .ident name => match evalConstantP name with | some v => return v | none => lookupVarP name | .binop op l r => let lv ← evalExprP l let rv ← evalExprP r evalBinOpP op lv rv | .unop op inner => evalUnOpP op inner | .range startE stepOpt stopE => let sv ← toFloatP (← evalExprP startE) let ev ← toFloatP (← evalExprP stopE) match stepOpt with | some stepE => let stv ← toFloatP (← evalExprP stepE); return .range sv stv ev | none => return .range sv 1.0 ev | .index expr args => do let fv ← evalExprP expr evalIndexP fv args | .dotIndex expr field => let sv ← evalExprP expr match sv with | .struct fields => match fields.find? (·.1 == field) with | some (_, v) => return v | none => throw (.nameError s!"struct has no field '{field}'") | other => throw (.typeError s!"cannot access field on {other.typeName}") | .dynField expr fieldExpr => let sv ← evalExprP expr let fn ← evalExprP fieldExpr match fn, sv with | .string fname, .struct fields => match fields.find? (·.1 == fname) with | some (_, v) => return v | none => throw (.nameError s!"no field '{fname}'") | _, _ => throw (.typeError "dynamic field name must be a string") | .matrix rows => evalMatrixLiteralP rows | .cellArr rows => evalCellLiteralP rows | .fnHandle name => return .fn (.handle name) | .anon params body => let env ← getPureEnv let closure := env.currentScope.vars return .fn (.anon params body closure) | .endIdx => throw (.runtimeError "'end' used outside indexing context") | .colonIdx => return .empty partial def evalUnOpP (op : UnOp) (e : Expr) : PureM Value := do let v ← evalExprP e match op with | .neg => match v.materialize with | .scalar f => return .scalar (-f) | .matrix r c d => return .matrix r c (d.map (- ·)) | .integer iv => return .scalar (-iv.toFloat) | other => throw (.typeError s!"cannot negate {other.typeName}") | .uplus => return v | .lnot => match v.materialize with | .scalar f => return .boolean (f == 0.0) | .boolean b => return .boolean (!b) | .matrix r c d => return .boolMat r c (d.map (· == 0.0)) | other => throw (.typeError s!"cannot logically negate {other.typeName}") | .htranspose | .transpose => match v.materialize with | .scalar f => return .scalar f | .matrix r c d => let out := Id.run do let mut o := arrFillP (r * c) 0.0 for i in List.range r do for j in List.range c do o := o.set! (j * r + i) d[i * c + j]! o return .matrix c r out | other => throw (.typeError s!"cannot transpose {other.typeName}") partial def evalIndexP (fv : Value) (argExprs : Array Arg) : PureM Value := do match fv with | .fn funcVal => callFuncP funcVal (← evalArgsP argExprs) | _ => let args ← evalArgValuesP argExprs fv indexValueP fv args partial def evalArgValuesP (args : Array Arg) (ctx : Value) : PureM (Array Value) := do let (rows, cols) := ctx.shape let total := rows * cols args.mapM fun a => match a with | .pos e => evalExprP (substEndP e total) | .colon => let data := Value.rangeToArray 1.0 1.0 (Float.ofNat total) return .matrix 1 total data | .kw _ e => evalExprP e partial def evalArgsP (args : Array Arg) : PureM (Array Value) := args.mapM fun a => match a with | .pos e => evalExprP e | .colon => return .empty | .kw _ e => evalExprP e partial def substEndP (e : Expr) (n : Nat) : Expr := match e with | .endIdx => .lit (.int n) | .binop op l r => .binop op (substEndP l n) (substEndP r n) | .unop op ie => .unop op (substEndP ie n) | .range l s r => .range (substEndP l n) (s.map (substEndP · n)) (substEndP r n) | other => other /-- In pure mode, IO builtins throw a sentinel; the IO layer intercepts and re-dispatches. -/ partial def callFuncP (fv : FuncVal) (args : Array Value) : PureM Value := do match fv with | .builtin name => throw (.runtimeError s!"__io_builtin:{name}") | .handle name => let env ← getPureEnv match env.get name with | some (.fn fv') => callFuncP fv' args | some _ => throw (.typeError s!"'{name}' is not callable") | none => if env.getBuiltin name |>.isSome then throw (.runtimeError s!"__io_builtin:{name}") else throw (.nameError name) | .anon params body closure => let env ← getPureEnv let mut frame : Array (String × Value) := closure for (p, a) in params.zip args do frame := (frame.filter (·.1 != p)).push (p, a) let newScope : Scope := { vars := frame, globals := #[], persist := #[], retVals := #[] } let innerEnv : Env := { env with stack := env.stack.push newScope } match runPureM (evalExprP body) innerEnv with | (.ok v, _) => return v | (.error e, _) => throw e | .userDef uf => let env ← getPureEnv let env' := env.pushFrame uf.retVals let mut envWithArgs := env' for (p, a) in uf.params.zip args do envWithArgs := envWithArgs.set p a for (k, v) in uf.closure do envWithArgs := envWithArgs.set k v let (funcResult, funcEnv) := runPureM (runBlockP uf.body) envWithArgs let (outerEnv, frame) := Env.popFrame funcEnv setPureEnv outerEnv let rets := uf.retVals.filterMap (frame.get ·) match funcResult with | .ok _ | .error .returnSignal => return rets[0]?.getD .empty | .error e => throw e partial def evalMatrixLiteralP (rows : Array (Array Expr)) : PureM Value := do if rows.isEmpty then return .empty let evaledRows ← rows.mapM (·.mapM evalExprP) let cols := (evaledRows[0]!).size if evaledRows.any (·.size != cols) then throw (.valueError "inconsistent row lengths in matrix literal") let data : Array Float ← evaledRows.foldlM (init := #[]) fun acc row => do row.foldlM (init := acc) fun acc' v => do match v.materialize with | .scalar f => return acc'.push f | .integer iv => return acc'.push iv.toFloat | .boolean b => return acc'.push (if b then 1.0 else 0.0) | other => throw (.typeError s!"cannot embed {other.typeName} in matrix literal") return .matrix evaledRows.size cols data partial def evalCellLiteralP (rows : Array (Array Expr)) : PureM Value := do if rows.isEmpty then return .cell 0 0 #[] let evaledRows ← rows.mapM (·.mapM evalExprP) let cols := (evaledRows[0]!).size let data := evaledRows.foldl (init := #[]) (· ++ ·) return .cell evaledRows.size cols data partial def runBlockP (stmts : Array Stmt) : PureM Unit := stmts.forM evalStmtP /-- Pure statement evaluator. Output is suppressed; state changes are preserved. -/ partial def evalStmtP (s : Stmt) : PureM Unit := do match s with | .exprS e _ => let v ← evalExprP e match v with | .empty => pure () | _ => setVarP "ans" v | .assign targets rhs _ => let v ← evalExprP rhs if targets.size == 1 then setVarP targets[0]! v else match v with | .cell _ _ data => for (i, t) in targets.toList.mapIdx (fun i t => (i, t)) do setVarP t (data[i]?.getD .empty) | _ => setVarP targets[0]! v for t in targets.toList.tail do setVarP t .empty | .ifS cond thenB elseifs elseB => let cv ← evalExprP cond if isTruthy cv then runBlockP thenB else let found ← elseifs.foldlM (init := false) fun done (c, b) => do if done then return true if isTruthy (← evalExprP c) then do runBlockP b; return true else return false unless found do match elseB with | some b => runBlockP b | none => return () | .forS varName iter body => let iv ← evalExprP iter let items := match iv.materialize with | .matrix 1 _ data => data.map Value.scalar | .matrix r c data => Array.ofFn (n := c) fun j => let col := Array.ofFn (n := r) fun i => data[i.val * c + j.val]! Value.matrix r 1 col | .empty => #[] | other => #[other] for item in items do setVarP varName item try runBlockP body catch | .breakSignal => return () | .continueSignal => continue | e => throw e | .whileS cond body => let rec whileLoop : PureM Unit := do if isTruthy (← evalExprP cond) then try runBlockP body; whileLoop catch | .breakSignal => return () | .continueSignal => whileLoop | e => throw e whileLoop | .doUntil body cond => let rec doLoop : PureM Unit := do try runBlockP body catch | .breakSignal => return () | .continueSignal => pure () | e => throw e unless isTruthy (← evalExprP cond) do doLoop doLoop | .returnS => throw .returnSignal | .breakS => throw .breakSignal | .continueS => throw .continueSignal | .funcDefS fd => let env ← getPureEnv let uf := UserFunc.mk fd.name fd.params fd.retVals fd.body env.currentScope.vars setVarP fd.name (.fn (.userDef uf)) | .switchS expr cases otherwise => let v ← evalExprP expr let handled ← cases.foldlM (init := false) fun done (pat, body) => do if done then return true let pv ← evalExprP pat let isMatch := match v, pv with | .scalar x, .scalar y => x == y | .string a, .string b => a == b | .boolean a, .boolean b => a == b | _, .cell _ _ data => data.any fun cv => match v, cv with | .scalar x, .scalar y => x == y | .string a, .string b => a == b | _, _ => false | _, _ => false if isMatch then do runBlockP body; return true else return false unless handled do match otherwise with | some b => runBlockP b | none => return () | .tryS body catchClause => let err ← MonadExcept.tryCatch (do runBlockP body; return (none : Option OctaveError)) (fun e => return some e) match err with | some .returnSignal | some .breakSignal | some .continueSignal => throw err.get! | some _ => match catchClause with | some (_, b) => runBlockP b | none => return () | none => return () | .indexAssign lhs rhs _ => do let newVal ← evalExprP rhs match lhs with | .dotIndex (.ident name) field => do let base ← lookupVarP name <|> return .struct #[] let newBase := match base with | .struct fs => match fs.findIdx? fun (k, _) => k == field with | some i => Value.struct (fs.set! i (field, newVal)) | none => Value.struct (fs.push (field, newVal)) | _ => Value.struct #[(field, newVal)] setVarP name newBase | .index (.ident name) argExprs => do let idxs ← evalArgValuesP argExprs .empty let base ← lookupVarP name <|> return .empty let newBase ← matrixWriteP base idxs newVal setVarP name newBase | _ => throw (.runtimeError "unsupported LHS for indexed assignment") | .globalS names => names.forM fun n => modify (·.declareGlobal n) | .persistS _ => return () | .clearS names => modify fun env => names.foldl (fun e n => e.updateScope (·.del n)) env | .unwindS body cleanup => let savedErr ← MonadExcept.tryCatch (do runBlockP body; return (none : Option OctaveError)) (fun e => return some e) runBlockP cleanup match savedErr with | some e => throw e | none => return () end /-! ## Provable lemmas about PureM These hold because `PureM` uses `Id` as the base monad, making `runPureM` reduce definitionally. The `partial def` mutual block is opaque; we work around it by stating specific-case lemmas using `evalLiteralP` and `evalConstantP`, which ARE non-partial and reducible. -/ section PureMLemmas /-- Literal evaluation never touches the environment. -/ @[simp] theorem toFloatP_scalar (f : Float) (env : Env) : runPureM (toFloatP (.scalar f)) env = (.ok f, env) := rfl @[simp] theorem toFloatP_boolean_true (env : Env) : runPureM (toFloatP (.boolean true)) env = (.ok 1.0, env) := rfl @[simp] theorem toFloatP_boolean_false (env : Env) : runPureM (toFloatP (.boolean false)) env = (.ok 0.0, env) := rfl @[simp] theorem evalLiteralP_float (f : Float) : evalLiteralP (.float f) = .scalar f := rfl @[simp] theorem evalLiteralP_int (n : Int) : evalLiteralP (.int n) = .scalar (Float.ofInt n) := rfl @[simp] theorem evalLiteralP_str (s : String) : evalLiteralP (.str s) = .string s := rfl @[simp] theorem evalLiteralP_bool (b : Bool) : evalLiteralP (.bool b) = .boolean b := rfl /-- isTruthy is decidable and doesn't require IO. -/ @[simp] theorem isTruthy_boolean (b : Bool) : isTruthy (.boolean b) = b := rfl @[simp] theorem isTruthy_empty : isTruthy .empty = false := rfl -- Note: isTruthy (.scalar 0.0) = false is NOT provable by rfl because -- Float.bne is not definitionally decidable in Lean 4's kernel. -- Use native_decide for concrete Float goals: theorem isTruthy_scalar_zero : isTruthy (.scalar 0.0) = false := by native_decide /-- runPureM of a pure return is always (.ok v, env). -/ @[simp] theorem runPureM_return (v : α) (env : Env) : runPureM (return v : PureM α) env = (.ok v, env) := rfl /-- evalBinOpP on two scalars routes through the non-partial evalBinOpScalarP. -/ @[simp] theorem evalBinOpP_scalar_eq (op : BinOp) (x y : Float) (env : Env) : runPureM (evalBinOpP op (.scalar x) (.scalar y)) env = runPureM (evalBinOpScalarP op x y) env := by simp [evalBinOpP, Value.materialize] /-- Scalar addition is provable by kernel reduction (no axiom needed). -/ theorem evalBinOpP_add_scalars (x y : Float) (env : Env) : (runPureM (evalBinOpP .add (.scalar x) (.scalar y)) env).1 = .ok (.scalar (x + y)) := by simp [evalBinOpP, Value.materialize, evalBinOpScalarP] /-- Scalar multiplication is provable by kernel reduction. -/ theorem evalBinOpP_mul_scalars (x y : Float) (env : Env) : (runPureM (evalBinOpP .mul (.scalar x) (.scalar y)) env).1 = .ok (.scalar (x * y)) := by simp [evalBinOpP, Value.materialize, evalBinOpScalarP] /-- All scalar binary ops preserve the environment. -/ theorem evalBinOpP_scalar_preserves_env (op : BinOp) (x y : Float) (env : Env) : (runPureM (evalBinOpP op (.scalar x) (.scalar y)) env).2 = env := by simp [evalBinOpP, Value.materialize] cases op <;> simp [evalBinOpScalarP] /-! Helper lemmas for the environment set/get roundtrip proofs -/ /-- Key-value list: updating the entry at the findIdx? position returns the new value. -/ private theorem List.findSome?_set_key {α : Type} {l : List (String × α)} {name : String} {val : α} {i : Nat} (hidx : l.findIdx? (fun kv => kv.1 == name) = some i) : (l.set i (name, val)).findSome? (fun kv => if kv.1 == name then some kv.2 else none) = some val := by induction l generalizing i with | nil => simp at hidx | cons head rest ih => obtain ⟨k, v⟩ := head rw [List.findIdx?_cons] at hidx rcases h : k == name with _ | _ · simp only [h] at hidx rcases Option.map_eq_some_iff.mp hidx with ⟨j, hj, rfl⟩ simp only [List.set, List.findSome?_cons, h]; exact ih hj · have hi : i = 0 := by simp [h] at hidx; omega subst hi; simp [List.set] /-- Scope set/get round-trip: setting a variable then getting it returns the new value. -/ private theorem scope_set_get (s : Scope) (name : String) (val : Value) : (s.set name val).get name = some val := by rcases h : s.vars.findIdx? (fun kv => kv.1 == name) with _ | ⟨i⟩ · simp only [Scope.set, h] unfold Scope.get; simp only [Array.findSome?_push] have hnil : s.vars.findSome? (fun x : String × Value => if (x.fst == name) = true then some x.snd else none) = none := by rw [Array.findSome?_eq_none_iff] intro kv hmem; simp [Array.findIdx?_eq_none_iff.mp h kv hmem] simp only [hnil, Option.none_or]; simp · simp only [Scope.set, h] unfold Scope.get rw [← Array.findSome?_toList, Array.set!_eq_setIfInBounds, Array.toList_setIfInBounds] apply List.findSome?_set_key rw [← List.findIdx?_toArray]; exact h /-- Scope.set only updates `vars`; `globals` is unchanged. -/ private theorem scope_globals_set (s : Scope) (name : String) (val : Value) : (s.set name val).globals = s.globals := by simp only [Scope.set]; split <;> rfl /-- After updateScope, currentScope equals the updated scope (requires non-empty stack). -/ private theorem currentScope_updateScope (env : Env) (f : Scope → Scope) (hne : 0 < env.stack.size) : (env.updateScope f).currentScope = f env.currentScope := by have hlt : env.stack.size - 1 < env.stack.size := Nat.sub_lt hne (by omega) have hemp : env.stack.isEmpty = false := by simp [Array.isEmpty_eq_false_iff]; intro heq; simp [heq] at hne have hset_back : (env.stack.set! (env.stack.size - 1) (f env.stack.back!)).back! = f env.stack.back! := by simp only [Array.back!, Array.set!_eq_setIfInBounds, Array.size_setIfInBounds, getElem!_def, Array.getElem?_setIfInBounds_self_of_lt hlt] simp only [Env.updateScope, Env.currentScope, hemp, Bool.false_eq_true, if_false] have hne2 : (env.stack.set! (env.stack.size - 1) (f env.stack.back!)).isEmpty = false := by simp [Array.set!_eq_setIfInBounds, Array.isEmpty_eq_false_iff] intro heq; simp [heq] at hne simp only [hne2, Bool.false_eq_true, if_false, hset_back] /-- Environment set/get round-trip in local scope. -/ theorem env_set_get_roundtrip (env : Env) (name : String) (val : Value) (hg : env.currentScope.globals.contains name = false) (hne : 0 < env.stack.size) : (env.set name val).get name = some val := by have hset : env.set name val = env.updateScope (·.set name val) := by simp only [Env.set, hg, Bool.false_eq_true, if_false] rw [hset] have hcs := currentScope_updateScope env (·.set name val) hne unfold Env.get have hg' : (env.currentScope.set name val).globals.contains name = false := by rw [scope_globals_set]; exact hg simp only [hcs, hg', Bool.false_eq_true, if_false, scope_set_get] /-- lookupVarP succeeds with the given value when env.get returns some. -/ private theorem runPureM_lookupVarP_some {val : Value} (name : String) (env : Env) (h : env.get name = some val) : (runPureM (lookupVarP name) env).1 = .ok val := by simp [runPureM, lookupVarP, getPureEnv, ExceptT.run, StateT.run, get, getThe, MonadStateOf.get, liftM, monadLift, MonadLift.monadLift, ExceptT.lift, Functor.map, ExceptT.mk, bind, ExceptT.bind, pure, ExceptT.pure, ExceptT.bindCont, StateT.map, StateT.get, StateT.bind, StateT.pure, h] /-- setVarP then lookupVarP retrieves the value (local scope). -/ theorem setVar_lookup_roundtrip (name : String) (val : Value) (env : Env) (hg : env.currentScope.globals.contains name = false) (hne : 0 < env.stack.size) : (runPureM (do setVarP name val; lookupVarP name) env).1 = .ok val := by -- setVarP changes env to env.set name val (Id-monad definitional equality) show (runPureM (lookupVarP name) (env.set name val)).1 = .ok val exact runPureM_lookupVarP_some name _ (env_set_get_roundtrip env name val hg hne) /-- liftPure homomorphism: pure ok results become EvalM ok results. -/ theorem liftPure_ok {α} (m : PureM α) (env : Env) (v : α) (h : (runPureM m env).1 = .ok v) : ∃ env', runPureM m env = (.ok v, env') := ⟨(runPureM m env).2, Prod.ext h rfl⟩ end PureMLemmas end OctiveLean