Released under Apache 2.0 license as described in the file LICENSE.
import Mathlib.Tactic.Linarith
-- A translation from SSA + Regions to Tree, with no proofs.
-- this allows for easy unfolding of the semantics.
-- If we carry around proofs of well formedness, the dependent typing
-- of the well-formedness leads to stuck terms.
-- Thus, we eschew the need to have proofs, and simply YOLO translate from
-- the origina SSA + Regions program into Tree.
-- | The environment is a mapping from variable names to values.
abbrev Env (k : Type) (α : Type) := k → α
-- | Extend the environment with a new variable.
def Env.extend [DecidableEq k] (name : k) (v : α) (e: Env k α) : Env k α :=
fun name' => if name = name' then v else e name'
def Env.empty [Inhabited α] : Env k α :=
def Env.map (f : α → β) (e : Env k α) : Env k β :=
notation "∅" => Env.empty
notation e "[" name "↦" v "]" => Env.extend name v e
abbrev VarEnv (α : Type) := Env VarName α
abbrev RgnEnv (α : Type) := Env RgnName α
-- The input semantics given by the user.
class UserSemantics (opcode : Type) where
-- | Arguments given as (<args>, <rgn1>, ... <rgnN>)
-- | Consider not allowing users to not be access all of 'Env'.
opcodeEval (op: opcode) (vals : Int × Int) (rgns : (Int × Int → Int)) : Int
attribute [simp] UserSemantics.opcodeEval
inductive ASTKind : Type where
deriving Inhabited, DecidableEq
-- | The operations of the language.
inductive AST (opcode : Type): ASTKind → Type where
| assign (ret : VarName) (op : opcode)
(args : VarName × VarName)
(rgns : AST opcode .R) : AST opcode .O
| ops1 (op : AST opcode .O) : AST opcode .Os
| opsmany (op : AST opcode .O) (ops : AST opcode .Os) : AST opcode .Os
| rgn (args : VarName × VarName) (body : AST opcode .Os) : AST opcode .R
| rgnvar (var : RgnName) : AST opcode .R
def AST.retname : AST opcode .O → VarName
| .assign (ret := ret) .. => ret
instance [Inhabited opcode] : Inhabited (AST opcode .O) where
default := .assign .x0 default (.x0, .x0) .rgn0
instance [Inhabited opcode] : Inhabited (AST opcode .Os) where
instance [Inhabited opcode] : Inhabited (AST opcode .R) where
default := .rgn (.x0, .x0) default
def Ops.ofList [Inhabited opcode] : List (AST opcode .O) → AST opcode .Os
| [] => panic! "need non-empty list"
| x :: xs => .opsmany x (Ops.ofList xs)
instance [Inhabited opcode] : Coe (List (AST opcode .O)) (AST opcode .Os) := ⟨Ops.ofList⟩
def ASTKind.eval : ASTKind → Type
| .R => (Int × Int → Int)
-- evaluate an operation with repect to a particular user semantics.
def AST.eval [S: UserSemantics opcode]
{astk: ASTKind} (e: VarEnv Int)
(re: RgnEnv (Int × Int → Int)): AST opcode astk → astk.eval × VarEnv Int
| .assign ret op args r =>
let retval := S.opcodeEval op (e arg1, e arg2) (r.eval e re).fst
(retval, e.extend ret retval)
let (out, env) := op.eval e re
let e' := (op.eval e re).snd
let e1 := e.extend args.fst vals.fst
let e2 := e1.extend args.snd vals.snd
let (outval, _e) := body.eval e2 re
| .rgn0 => (fun _ => default, e)
| R -- region (higher order)
-- closed trees, leaves are integers.
inductive Ctree (opcode : Type) : CtreeKind → Type where
(rgns: Ctree opcode .R) : Ctree opcode .O
| rgn (f : Int × Int → Ctree opcode .O) : Ctree opcode .R
| leaf (val : Int) : Ctree opcode .O
instance : Inhabited (Ctree opcode .O) where
instance : Inhabited (Ctree opcode .R) where
-- convert an AST into a closed tree under the given environment
-- note that translation into a closed tree needs an environment,
-- to learn the values of variables.
-- This version has an `_` in the name since it needs a `Ctree.VarEnv`, not an
-- `Env`. We will writea helper that converts `Env` into `Ctree.VarEnv`.
def ASTKind.toCTree (opcode : Type) : ASTKind → Type
def AST.toCtree_ {astk: ASTKind} (e : VarEnv (Ctree opcode .O))
(re: RgnEnv (Ctree opcode .R)):
AST opcode astk → (astk.toCTree opcode) × VarEnv (Ctree opcode .O)
| .assign ret opcode (u, v) r =>
let rval := (r.toCtree_ e re).fst
let t := .binop opcode (e u) (e v) rval
| .rgnvar var => let e := Env.empty; (re var, e)
| .rgn0 => let e := Env.empty; (.rgn0, e)
let e := Env.empty -- NOTE: regions are now isolated from above
let e1 := e.extend args.fst (.leaf vals.fst)
let e2 := e1.extend args.snd (.leaf vals.snd)
(body.toCtree_ e2 re).fst, e)
let (val, e) := op.toCtree_ e re
let (_, e) := os.toCtree_ e re
-- wrap every element in a (.leaf) constructor
def Ctree.VarEnv.ofEnv (e: VarEnv Int) : VarEnv (Ctree opcode .O) :=
fun name => .leaf (e name)
-- TODO: should these be coercions?
def Ctree.RgnEnv.ofEnv (re: RgnEnv (Int × Int → Int)) :
RgnEnv (Ctree opcode .R) :=
fun name => Ctree.rgn (fun args => .leaf (re name args))
-- this converts an AST into a Ctree, given an environment
def Op.toCtree (a: AST opcode .O) (e: VarEnv Int)
(re: RgnEnv (Int × Int → Int)) : Ctree opcode .O :=
(a.toCtree_ (Ctree.VarEnv.ofEnv e) (Ctree.RgnEnv.ofEnv re)).fst
def Ops.toCtree (a: AST opcode .Os)
(e: VarEnv Int) (re: RgnEnv (Int × Int → Int)) : Ctree opcode .O :=
(a.toCtree_ (Ctree.VarEnv.ofEnv e) (Ctree.RgnEnv.ofEnv re)).fst
def Region.toCtree (a: AST opcode .R)
(re: RgnEnv (Int × Int → Int)): Ctree opcode .R :=
(a.toCtree_ (Ctree.VarEnv.ofEnv e) (Ctree.RgnEnv.ofEnv re)).fst
-- evaluate a Ctree. note that this needs no environment.
def CtreeKind.eval : CtreeKind → Type
-- Note: is this literally "just" staging the partial evaluation against the environment?
def Ctree.eval [UserSemantics opcode] : Ctree opcode treek → treek.eval
UserSemantics.opcodeEval o (l.eval, r.eval) rs.eval
| .rgn0 => fun _ => default
| .rgn f => fun args => (f args).eval
namespace MultipleInstructionTree
def loopSemantics (n : Nat) (f : Int → Int) (v : Int) : Int :=
| n + 1 => f (loopSemantics n f v) -- inline ∘
instance : UserSemantics Opcode where
| .not, ⟨a, _⟩, _ => if a = 0 then 1 else 0
| .mul, ⟨a, b⟩, _ => a * b
| .add, ⟨a, b⟩, _ => a + b
| .const i, ⟨_a, _b⟩, _ => i
| .run, ⟨v, w⟩, r => r ⟨v, w⟩
| .runnot, ⟨v, w⟩, r => r ⟨if v = 0 then 1 else 0, w⟩ -- execute: 'r(not v, w)'
| .loop, ⟨n, init⟩, r => loopSemantics n.toNat (fun i => r ⟨i, 0⟩) init
def x_add_4_times_mul_val_eq (env: VarEnv Int):
let p : AST Opcode .Os :=
.assign .x1 .add (.x0, .x0) .rgn0,
.assign .x2 .add (.x1, .x1) .rgn0
let q : AST Opcode .Os := Ops.ofList [
.assign .x1 (.const 4) (.x0, .x0) .rgn0
, .assign .x2 .mul (.x1, .x0) .rgn0
(Ops.toCtree p env renv).eval = (Ops.toCtree q env renv).eval := by {
simp only [Ops.ofList, AST.eval, Ops.toCtree, AST.toCtree_];
-- see that there are environments, which are folded away when calling
AST.rgn ⟨.x0, .x1⟩ $ Ops.ofList [
.assign .x2 .add (.x0, .x1) .rgn0 -- x2 := x0 + x1
AST.rgn ⟨.x0, .x1⟩ $ Ops.ofList [
.assign .x2 .run (.x0, .x1) (AST.rgn ⟨.x5, .x6⟩ (Ops.ofList [
-- x2 := run (x0, x1) { ^(x5, x6): return x5 + x6 }
.assign .y1 .add (.x5, .x6) .rgn0
(Region.toCtree p Env.empty Env.empty).eval =
(Region.toCtree q Env.empty Env.empty).eval := by {
-- trying to convert an AST to a Ctree at any environment
-- is equivalent to converting it in an empty environment.
theorem AST.toCtree_rgn_equiv_empty
AST.toCtree_ env renv r = AST.toCtree_ Env.empty renv r := by {
AST.rgn ⟨.x0, .x1⟩ $ Ops.ofList [
.assign .y1 .run ⟨.x0, .x1⟩ (.rgnvar .r1) -- y1 := r(x0, x1)
(Region.toCtree p Env.empty Env.empty).eval =
(Region.toCtree (.rgnvar .r1 : AST Opcode .R) Env.empty Env.empty).eval := by {
-- this stil cannot be simplified away, since this is in fact false.
-- We need some notion of the fact that z1, z2 is free in 'r',
-- so that we can say that we can remove 'z2, z1' in the 'q'
-- environment when we run 'r'.
-- if we have this, then we are good, and we can prove the theorem.
-- but how do we convince people that use MLIR that this is a sensible
-- thing to reason about?
end MultipleInstructionTree