~jojo/effem

6598d9f8901de91b5d7502e5da19ba3a75c96648 — JoJo 10 months ago 204fb3e
flatten the Base IR

Application of data oriented design.

Pros of flattening trees:
- Better cache locality.
- Smaller references (u32 vs Box (= usize often= u64))
- Cheaper allocation. Instead of individual `Box`es each requiring a
  heap alloc, just push to the same arena `Vec` (which only rarely
  allocates).
- Possibilities for easy deduplication (e.g. via a
  `HashMap<Stm, StmRef>`)
- Nodes are in topological order (I think?). This might be
  exploitable.
6 files changed, 301 insertions(+), 261 deletions(-)

M src/abase.rs
A src/base.rs
M src/cache.rs
M src/eval.rs
M src/main.rs
M src/prelude.rs
M src/abase.rs => src/abase.rs +71 -200
@@ 1,190 1,66 @@
//! Base -- the lowest level IR in Effem before generating the output machine code.

use crate::prelude::*;
use fem::{Expr as FExpr, Type};
use std::collections::HashSet;

#[derive(Clone, Debug)]
pub enum GlobDef {
    FunDef(FunDef),
    VarDef(GVarDef),
    Alias(DefId),
}

#[derive(Clone, Debug)]
pub struct FunDef {
    // Nested definitions (including anonymous functions)
    pub children: HashSet<DefId>,
    pub params: Vec<LocalId>,
    pub body: Block<Option<Operation>>,
}

#[derive(Clone, Debug)]
pub struct GVarDef {
    pub children: HashSet<DefId>,
    pub body: Block<Option<Operation>>,
}

#[derive(Debug, Clone)]
pub struct Block<Leaf> {
    pub stms: Vec<Stm>,
    pub tail: Box<Flow<Leaf>>,
}

#[derive(Debug, Clone)]
pub enum Stm {
    Let { lhs: LocalId, rhs: Flow<Option<Operation>> },
}

#[derive(Debug, Clone)]
pub enum Flow<Leaf> {
    Diverge(Diverge<Leaf>),
    Leaf(Leaf),
}

#[derive(Debug, Clone)]
pub enum Diverge<Leaf> {
    If(Operand, Block<Leaf>, Block<Leaf>),
}

#[derive(Clone, Debug)]
pub enum Operation {
    Binop(Binop, Operand, Operand),
    Call(Operand, Vec<Operand>),
    Wrap(Operand),
}

#[derive(Debug, Clone, Copy)]
pub enum Binop {
    Add,
    Sub,
    Mul,
    Quot,
    Rem,
    Lt,
    Gt,
    Leq,
    Geq,
    Eq,
    Neq,
}

#[derive(Debug, Clone, Copy)]
pub enum Operand {
    Const(Const),
    // Extern Extern
    Local(LocalId),
    Global(DefId),
}

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum Const {
    Bool(bool),
    I8(i8),
    I16(i16),
    I32(i32),
    I64(i64),
    Int(isize),
    N8(u8),
    N16(u16),
    N32(u32),
    N64(u64),
    Nat(usize),
    F64(f64),
}

macro_rules! impl_const_from {
    ($t:ty, $c:expr) => {
        impl From<$t> for Const {
            fn from(x: $t) -> Const {
                $c(x)
            }
        }
    };
}

impl_const_from!(bool, Const::Bool);
impl_const_from!(i8, Const::I8);
impl_const_from!(i16, Const::I16);
impl_const_from!(i32, Const::I32);
impl_const_from!(i64, Const::I64);
impl_const_from!(isize, Const::Int);
impl_const_from!(u8, Const::N8);
impl_const_from!(u16, Const::N16);
impl_const_from!(u32, Const::N32);
impl_const_from!(u64, Const::N64);
impl_const_from!(usize, Const::Nat);
impl_const_from!(f64, Const::F64);

macro_rules! impl_const_arithm_binop {
    ($op_trait:ty, $op_name:ident) => {
        impl $op_trait for Const {
            type Output = Option<Const>;
            fn $op_name(self, other: Self) -> Self::Output {
                match (self, other) {
                    (Const::I8(x), Const::I8(y)) => Some(x.$op_name(y).into()),
                    (Const::I16(x), Const::I16(y)) => Some(x.$op_name(y).into()),
                    (Const::I32(x), Const::I32(y)) => Some(x.$op_name(y).into()),
                    (Const::I64(x), Const::I64(y)) => Some(x.$op_name(y).into()),
                    (Const::Int(x), Const::Int(y)) => Some(x.$op_name(y).into()),
                    (Const::N8(x), Const::N8(y)) => Some(x.$op_name(y).into()),
                    (Const::N16(x), Const::N16(y)) => Some(x.$op_name(y).into()),
                    (Const::N32(x), Const::N32(y)) => Some(x.$op_name(y).into()),
                    (Const::N64(x), Const::N64(y)) => Some(x.$op_name(y).into()),
                    (Const::Nat(x), Const::Nat(y)) => Some(x.$op_name(y).into()),
                    (Const::F64(x), Const::F64(y)) => Some(x.$op_name(y).into()),
                    _ => None,
                }
            }
        }
    };
}

impl_const_arithm_binop!(std::ops::Add, add);
impl_const_arithm_binop!(std::ops::Sub, sub);
impl_const_arithm_binop!(std::ops::Mul, mul);
impl_const_arithm_binop!(std::ops::Div, div);
impl_const_arithm_binop!(std::ops::Rem, rem);
use base::*;
use fem::{Expr as FExpr, ExprKind as FExprKind, Type};
use std::collections::{HashMap, HashSet};

pub fn abase_def(cache: &mut Cache, rhs: &FExpr) -> Result<GlobDef> {
    AbaseDef::new(cache).run(rhs)
    if let FExprKind::Fun(params, body) = &rhs.kind {
        AbaseDef::new(cache).run_fun(params, body).map(GlobDef::FunDef)
    } else {
        AbaseDef::new(cache).run_var(rhs).map(GlobDef::VarDef)
    }
}

#[derive(Debug)]
struct AbaseDef<'c> {
    cache: &'c mut Cache,
    children: HashSet<DefId>,
    vars: Vec<LocalId>,
    n_locals: u32,
    stms: Vec<Stm>,
    locals: HashMap<LocalId, Operand>,
    stm_arena: Vec<Stm>,
    block_arena: Vec<Block>,
    current_stms: Vec<StmRef>,
}

impl<'c> AbaseDef<'c> {
    fn new(cache: &'c mut Cache) -> Self {
        Self { cache, children: HashSet::new(), vars: vec![], n_locals: 0, stms: vec![] }
        Self {
            cache,
            children: HashSet::new(),
            locals: HashMap::new(),
            stm_arena: vec![],
            block_arena: vec![],
            current_stms: vec![],
        }
    }

    fn run(mut self, rhs: &FExpr) -> Result<GlobDef> {
        match self.abase(rhs)? {
            Flow::Leaf(Some(Operation::Wrap(rand))) => match rand {
                Operand::Local(id) => panic!("ice: result of abasing def is a local reference {id:?}"),
                Operand::Global(id) => Ok(GlobDef::Alias(id)),
                _ => Ok(GlobDef::VarDef(GVarDef {
                    children: self.children,
                    body: Block { stms: self.stms, tail: Box::new(Flow::Leaf(Some(Operation::Wrap(rand)))) },
                })),
            },
            tail => Ok(GlobDef::VarDef(GVarDef {
                children: self.children,
                body: Block { stms: self.stms, tail: Box::new(tail) },
            })),
        }
    fn run_var(mut self, rhs: &FExpr) -> Result<GVarDef> {
        let root = self.enter_block(|self_| self_.abase(rhs))?;
        // TODO: assert ret is not Local
        Ok(GVarDef {
            children: self.children,
            body: Body { stm_arena: self.stm_arena, block_arena: self.block_arena, root },
        })
    }

    fn run_fun(mut self, params: &[(PubIdent, LocalId)], body: &FExpr) -> Result<FunDef> {
        self.locals.extend(params.iter().enumerate().map(|(i, &(_, lid @ LocalId(i2)))| {
            debug_assert_eq!(i, i2 as usize);
            (lid, Operand::Param(ParamRef(i2)))
        }));
        let root = self.enter_block(|self_| self_.abase(body))?;
        Ok(FunDef {
            children: self.children,
            params: params.iter().map(|_| ()).collect(),
            body: Body { stm_arena: self.stm_arena, block_arena: self.block_arena, root },
        })
    }

    fn abase(&mut self, expr: &FExpr) -> Result<Flow<Option<Operation>>> {
    fn abase(&mut self, expr: &FExpr) -> Result<Flow> {
        use crate::fem::ExprKind::*;
        fn wrap(rand: Operand) -> Flow<Option<Operation>> {
            Flow::Leaf(Some(Operation::Wrap(rand)))
        fn wrap(rand: Operand) -> Flow {
            Flow::Produce(Operation::Wrap(rand))
        }
        match &expr.kind {
            Bool(x) => Ok(wrap(Operand::Const(Const::Bool(*x)))),


@@ 231,10 107,9 @@ impl<'c> AbaseDef<'c> {
                    Ok(Operation::Call(f, xs))
                }
            }
            .map(|ration| Flow::Leaf(Some(ration))),
            .map(Flow::Produce),
            Var(ResName { res: Res::Def(id) }) => Ok(wrap(Operand::Global(*id))),
            &Var(ResName { res: Res::Local(LocalId(id)) }) =>
                Ok(wrap(Operand::Local(*self.vars.get(id as usize).unwrap_or_else(|| todo!())))),
            &Var(ResName { res: Res::Local(id) }) => Ok(wrap(self.locals[&id])),
            Var(ResName { res: Res::Prim(_) }) => todo!(), // TODO: generate a closure around the op or smth
            Var(ResName { res: Res::Module(_) }) =>
                panic!("ice: found module id in expr context when abasing. Should've been caught by type checker."),


@@ 258,48 133,44 @@ impl<'c> AbaseDef<'c> {

    // TODO: closures. capture free vars etc.
    fn abase_fun(&mut self, params: &[(PubIdent, LocalId)], body: &FExpr) -> Result<Operand> {
        let old_children = std::mem::take(&mut self.children);
        let old_vars = std::mem::take(&mut self.vars);

        debug_assert!(params.iter().enumerate().all(|(i, &(_, LocalId(param_id)))| param_id as usize == i));
        let params = params.iter().map(|_| self.gen_local()).collect::<Vec<_>>();
        self.vars.extend(&params);

        let body = self.enter_block(|self_| self_.abase(body))?;

        let new_children = std::mem::replace(&mut self.children, old_children);
        self.vars = old_vars;

        let def = FunDef { children: new_children, params, body };
        let fid = self.cache.insert_base_anon(GlobDef::FunDef(def));
        let fdef = AbaseDef::new(self.cache).run_fun(params, body)?;
        let fid = self.cache.insert_base_anon(GlobDef::FunDef(fdef));
        self.children.insert(fid);

        Ok(Operand::Global(fid))
    }

    fn enter_block<Leaf>(&mut self, f: impl FnOnce(&mut Self) -> Result<Flow<Leaf>>) -> Result<Block<Leaf>> {
        let old_stms = std::mem::take(&mut self.stms);
        let tail = Box::new(f(self)?);
        let new_stms = std::mem::replace(&mut self.stms, old_stms);
        Ok(Block { stms: new_stms, tail })
    fn enter_block(&mut self, f: impl FnOnce(&mut Self) -> Result<Flow>) -> Result<BlockRef> {
        let old_stms = std::mem::take(&mut self.current_stms);
        let tail = f(self)?;
        let new_stms = std::mem::replace(&mut self.current_stms, old_stms);
        Ok(self.add_block(Block { stms: new_stms, tail }))
    }

    fn gen_local(&mut self) -> LocalId {
        assert!(self.n_locals < u32::MAX);
        self.n_locals += 1;
        LocalId(self.n_locals - 1)
    fn add_block(&mut self, block: Block) -> BlockRef {
        let i = self.block_arena.len();
        assert!(i < u32::MAX as usize);
        self.block_arena.push(block);
        BlockRef(i as u32)
    }

    fn let_anon_soft(&mut self, rhs: Flow<Option<Operation>>) -> Result<Operand> {
    fn let_anon_soft(&mut self, rhs: Flow) -> Result<Operand> {
        match rhs {
            Flow::Leaf(Some(Operation::Wrap(rand))) => Ok(rand),
            Flow::Produce(Operation::Wrap(rand)) => Ok(rand),
            _ => self.let_anon_hard(rhs),
        }
    }

    fn let_anon_hard(&mut self, rhs: Flow<Option<Operation>>) -> Result<Operand> {
        let lhs = self.gen_local();
        self.stms.push(Stm::Let { lhs, rhs });
        Ok(Operand::Local(lhs))
    fn let_anon_hard(&mut self, rhs: Flow) -> Result<Operand> {
        let lhs = self.add_stm(Stm::Let { rhs });
        Ok(Operand::Var(lhs))
    }

    fn add_stm(&mut self, stm: Stm) -> StmRef {
        let i = self.stm_arena.len();
        assert!(i < u32::MAX as usize);
        self.stm_arena.push(stm);
        let stm_ref = StmRef(i as u32);
        self.current_stms.push(stm_ref);
        stm_ref
    }
}

A src/base.rs => src/base.rs +171 -0
@@ 0,0 1,171 @@
use crate::prelude::*;
use std::collections::HashSet;

#[derive(Clone, Debug)]
pub enum GlobDef {
    FunDef(FunDef),
    VarDef(GVarDef),
}

#[derive(Clone, Debug)]
pub struct FunDef {
    // Nested definitions (including anonymous functions)
    pub children: HashSet<DefId>,
    pub params: Vec<()>,
    pub body: Body,
}

#[derive(Clone, Debug)]
pub struct GVarDef {
    pub children: HashSet<DefId>,
    pub body: Body,
}

#[derive(Debug, Clone)]
pub struct Body {
    pub stm_arena: Vec<Stm>,
    pub block_arena: Vec<Block>,
    pub root: BlockRef,
}

#[derive(Debug, Clone)]
pub struct Block {
    pub stms: Vec<StmRef>,
    pub tail: Flow,
}

#[derive(Debug, Clone, Copy)]
pub struct BlockRef(pub u32);

#[derive(Debug, Clone)]
pub enum Stm {
    Let { rhs: Flow },
    // Store { val: Operand, dst: Operand },
    // VoidCall { proc: Operand, out: Option<Operand>, args: Vec<Pass<Operand>> }
    // SLoop (Loop ())
    // SBranch (Branch ())
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StmRef(pub u32);

#[derive(Debug, Clone)]
pub enum Flow {
    Diverge(Diverge),
    // Void,
    Produce(Operation),
    // Continue(Vec<Operand>) // only valid in loops
}

#[derive(Debug, Clone)]
pub enum Diverge {
    If(Operand, BlockRef, BlockRef),
}

// pub struct Loop {
//     pub params: Vec<(LocalId, Operand)>,
//     pub body: Block,
// }

#[derive(Clone, Debug)]
pub enum Operation {
    Binop(Binop, Operand, Operand),
    Call(Operand, Vec<Operand>),
    Wrap(Operand),
}

#[derive(Debug, Clone, Copy)]
pub enum Binop {
    Add,
    Sub,
    Mul,
    Quot,
    Rem,
    Lt,
    Gt,
    Leq,
    Geq,
    Eq,
    Neq,
}

#[derive(Debug, Clone, Copy)]
pub enum Operand {
    Const(Const),
    // Extern Extern
    Var(StmRef),
    Param(ParamRef),
    Global(DefId),
}

#[derive(Debug, Clone, Copy)]
pub struct ParamRef(pub u32);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum Const {
    Bool(bool),
    I8(i8),
    I16(i16),
    I32(i32),
    I64(i64),
    Int(isize),
    N8(u8),
    N16(u16),
    N32(u32),
    N64(u64),
    Nat(usize),
    F64(f64),
}

macro_rules! impl_const_from {
    ($t:ty, $c:expr) => {
        impl From<$t> for Const {
            fn from(x: $t) -> Const {
                $c(x)
            }
        }
    };
}

impl_const_from!(bool, Const::Bool);
impl_const_from!(i8, Const::I8);
impl_const_from!(i16, Const::I16);
impl_const_from!(i32, Const::I32);
impl_const_from!(i64, Const::I64);
impl_const_from!(isize, Const::Int);
impl_const_from!(u8, Const::N8);
impl_const_from!(u16, Const::N16);
impl_const_from!(u32, Const::N32);
impl_const_from!(u64, Const::N64);
impl_const_from!(usize, Const::Nat);
impl_const_from!(f64, Const::F64);

macro_rules! impl_const_arithm_binop {
    ($op_trait:ty, $op_name:ident) => {
        impl $op_trait for Const {
            type Output = Option<Const>;
            fn $op_name(self, other: Self) -> Self::Output {
                match (self, other) {
                    (Const::I8(x), Const::I8(y)) => Some(x.$op_name(y).into()),
                    (Const::I16(x), Const::I16(y)) => Some(x.$op_name(y).into()),
                    (Const::I32(x), Const::I32(y)) => Some(x.$op_name(y).into()),
                    (Const::I64(x), Const::I64(y)) => Some(x.$op_name(y).into()),
                    (Const::Int(x), Const::Int(y)) => Some(x.$op_name(y).into()),
                    (Const::N8(x), Const::N8(y)) => Some(x.$op_name(y).into()),
                    (Const::N16(x), Const::N16(y)) => Some(x.$op_name(y).into()),
                    (Const::N32(x), Const::N32(y)) => Some(x.$op_name(y).into()),
                    (Const::N64(x), Const::N64(y)) => Some(x.$op_name(y).into()),
                    (Const::Nat(x), Const::Nat(y)) => Some(x.$op_name(y).into()),
                    (Const::F64(x), Const::F64(y)) => Some(x.$op_name(y).into()),
                    _ => None,
                }
            }
        }
    };
}

impl_const_arithm_binop!(std::ops::Add, add);
impl_const_arithm_binop!(std::ops::Sub, sub);
impl_const_arithm_binop!(std::ops::Mul, mul);
impl_const_arithm_binop!(std::ops::Div, div);
impl_const_arithm_binop!(std::ops::Rem, rem);

M src/cache.rs => src/cache.rs +11 -12
@@ 23,8 23,8 @@ pub struct Cache {
    resolveds: HashMap<DefId, RExpr>,
    sigs: HashMap<DefId, Fetch<fem::Type>>,
    checkeds: HashMap<DefId, Fetch<fem::Expr>>,
    abaseds: HashMap<DefId, abase::GlobDef>,
    evaluateds: HashMap<DefId, abase::Operand>,
    abaseds: HashMap<DefId, base::GlobDef>,
    evaluateds: HashMap<DefId, base::Operand>,
}

impl Cache {


@@ 49,35 49,34 @@ impl Cache {
        }
    }

    pub fn fetch_evaluated_at(&mut self, fundef_query: DefId, args: &[abase::Operand]) -> Result<abase::Operand> {
    pub fn fetch_evaluated_at(&mut self, fundef_query: DefId, args: &[base::Operand]) -> Result<base::Operand> {
        #[cfg(debug_assertions)]
        for arg in args.iter().filter(|arg| matches!(arg, abase::Operand::Local(_))) {
            panic!("ice: Local operand {arg:?} as arg to another function");
        for arg in args.iter().filter(|arg| !matches!(arg, base::Operand::Const(_) | base::Operand::Global(_))) {
            panic!("ice: Local scope operand {arg:?} as arg to another function");
        }

        let _ = self.fetch_evaluated(fundef_query)?;
        match self.abaseds[&fundef_query] {
            abase::GlobDef::FunDef(ref fdef) => eval::eval_fun_at(self, &fdef.clone(), args),
            base::GlobDef::FunDef(ref fdef) => eval::eval_fun_at(self, &fdef.clone(), args),
            ref def => panic!("ice: {def:?} is not a function def. Cannot evaluate at arguments."),
        }
    }

    pub fn fetch_evaluated(&mut self, def_query: DefId) -> Result<&abase::Operand> {
    pub fn fetch_evaluated(&mut self, def_query: DefId) -> Result<&base::Operand> {
        if self.evaluateds.contains_key(&def_query) {
            Ok(self.evaluateds.get(&def_query).unwrap())
        } else {
            let base = self.fetch_base(def_query)?.clone();
            let val = match base {
                abase::GlobDef::VarDef(def) => eval::eval_var(self, &def)?,
                abase::GlobDef::Alias(target) => self.fetch_evaluated(target)?.clone(),
                abase::GlobDef::FunDef(_) => abase::Operand::Global(def_query),
                base::GlobDef::VarDef(def) => eval::eval_var(self, &def)?,
                base::GlobDef::FunDef(_) => base::Operand::Global(def_query),
            };
            self.evaluateds.insert(def_query, val);
            Ok(&self.evaluateds[&def_query])
        }
    }

    pub fn fetch_base(&mut self, def_query: DefId) -> Result<&abase::GlobDef> {
    pub fn fetch_base(&mut self, def_query: DefId) -> Result<&base::GlobDef> {
        Ok(if self.abaseds.contains_key(&def_query) {
            &self.abaseds[&def_query]
        } else {


@@ 91,7 90,7 @@ impl Cache {
        })
    }

    pub fn insert_base_anon(&mut self, def: abase::GlobDef) -> DefId {
    pub fn insert_base_anon(&mut self, def: base::GlobDef) -> DefId {
        let def_id = self.gen_def_id();
        self.abaseds.insert(def_id, def);
        def_id

M src/eval.rs => src/eval.rs +45 -48
@@ 1,57 1,75 @@
use crate::prelude::*;
use abase::*;
use base::*;
use std::collections::HashMap;

pub fn eval_fun_at(cache: &mut Cache, def: &FunDef, args: &[Operand]) -> Result<Operand> {
    EvalDef::new(cache).run_fun(&def.params, args, &def.body)
    EvalDef::new(cache, &def.body, args).run()
}

pub fn eval_var(cache: &mut Cache, def: &GVarDef) -> Result<Operand> {
    EvalDef::new(cache).run_var(&def.body)
    EvalDef::new(cache, &def.body, &[]).run()
}

#[derive(Debug)]
struct EvalDef<'c> {
struct EvalDef<'c, 'd, 'a> {
    cache: &'c mut Cache,
    regs: HashMap<LocalId, Operand>,
    body: &'d Body,
    args: &'a [Operand],
    vars: HashMap<StmRef, Operand>,
}

impl<'c> EvalDef<'c> {
    fn new(cache: &'c mut Cache) -> Self {
        Self { cache, regs: HashMap::new() }
impl<'c, 'd, 'a> EvalDef<'c, 'd, 'a> {
    fn new(cache: &'c mut Cache, body: &'d Body, args: &'a [Operand]) -> Self {
        Self { cache, body, args, vars: HashMap::new() }
    }

    fn run_fun(mut self, params: &[LocalId], args: &[Operand], body: &Block<Option<Operation>>) -> Result<Operand> {
        for (id, arg) in params.iter().zip(args) {
            self.regs.insert(*id, *arg);
        }
        for stm in &body.stms {
            self.eval_stm(stm)?;
    fn run(mut self) -> Result<Operand> {
        self.eval_block_ref(self.body.root)
    }

    fn eval_block_ref(&mut self, BlockRef(i): BlockRef) -> Result<Operand> {
        self.eval_block(&self.body.block_arena[i as usize])
    }

    fn eval_block(&mut self, block: &Block) -> Result<Operand> {
        self.eval_stms(&block.stms)?;
        self.eval_div(&block.tail)
    }

    fn eval_stms(&mut self, stms: &[StmRef]) -> Result<()> {
        for &stm in stms {
            self.eval_stm_ref(stm)?;
        }
        self.eval_div(&body.tail)
        Ok(())
    }

    fn run_var(mut self, body: &Block<Option<Operation>>) -> Result<Operand> {
        self.eval_stms(&body.stms)?;
        self.eval_div(&body.tail)
    fn eval_stm_ref(&mut self, lhs @ StmRef(i): StmRef) -> Result<()> {
        let stm = &self.body.stm_arena[i as usize];
        match stm {
            Stm::Let { rhs } => {
                let val = self.eval_div(rhs)?;
                self.vars.insert(lhs, val);
                Ok(())
            }
        }
    }

    fn eval_div(&mut self, div: &Flow<Option<Operation>>) -> Result<Operand> {
        use abase::Const::*;
    fn eval_div(&mut self, div: &Flow) -> Result<Operand> {
        use base::Const::*;
        use Operand::*;
        match div {
            Flow::Leaf(Some(ration)) => self.eval_op(ration),
            Flow::Leaf(None) => todo!(),
            Flow::Diverge(Diverge::If(pred, conseq, alt)) => match self.eval_operand(*pred)? {
                Const(Bool(true)) => self.eval_stms(&conseq.stms).and_then(|()| self.eval_div(&conseq.tail)),
                Const(Bool(false)) => self.eval_stms(&alt.stms).and_then(|()| self.eval_div(&alt.tail)),
            Flow::Produce(ration) => self.eval_op(ration),
            &Flow::Diverge(Diverge::If(pred, conseq, alt)) => match self.eval_operand(pred)? {
                Const(Bool(true)) => self.eval_block_ref(conseq),
                Const(Bool(false)) => self.eval_block_ref(alt),
                pe => panic!("ice: `if` expects const bool predicate, found {pe:?}"),
            },
        }
    }

    fn eval_op(&mut self, ration: &Operation) -> Result<Operand> {
        use abase::Const::*;
        use base::Const::*;
        use Binop::*;
        use Operand::*;
        match ration {


@@ 82,34 100,13 @@ impl<'c> EvalDef<'c> {
        }
    }

    fn eval_stms(&mut self, stms: &[Stm]) -> Result<()> {
        for stm in stms {
            self.eval_stm(stm)?;
        }
        Ok(())
    }

    fn eval_stm(&mut self, stm: &Stm) -> Result<()> {
        match stm {
            Stm::Let { lhs, rhs } => {
                let val = self.eval_div(rhs)?;
                self.regs.insert(*lhs, val);
                Ok(())
            }
        }
    }

    fn eval_operand(&mut self, mut rand: Operand) -> Result<Operand> {
        let max_loops = 64;
        for _ in 0..max_loops {
            match rand {
                Operand::Const(_) => return Ok(rand),
                Operand::Local(id) =>
                    if let Some(val) = self.regs.get(&id) {
                        rand = *val;
                    } else {
                        panic!("ice: undefined local {rand:?}\nregs: {:?}", self.regs)
                    },
                Operand::Param(ParamRef(i)) => rand = self.args[i as usize],
                Operand::Var(x) => rand = self.vars[&x],
                Operand::Global(id) => match *self.cache.fetch_evaluated(id)? {
                    Operand::Global(id2) if id == id2 => return Ok(Operand::Global(id)),
                    rand2 => rand = rand2,

M src/main.rs => src/main.rs +2 -0
@@ 5,6 5,7 @@
#![allow(clippy::map_entry)]

mod abase;
mod base;
mod cache;
mod check;
mod desugar;


@@ 72,6 73,7 @@ fn parse_source_file_path(p: &Path) -> (&Path, &str) {
mod test {
    use super::*;
    use abase::*;
    use base::*;
    use diag::Result;
    use eval::*;
    use std::assert_matches::assert_matches;

M src/prelude.rs => src/prelude.rs +1 -1
@@ 1,4 1,4 @@
pub(crate) use crate::{abase, cache, check, diag, eval, fem, lex, name, parse, resolve};
pub(crate) use crate::{abase, base, cache, check, diag, eval, fem, lex, name, parse, resolve};
pub(crate) use cache::{Cache, Fetch};
pub(crate) use diag::*;
pub(crate) use name::*;