Skip to main content

tang_expr/int/
eval.rs

1//! CPU reference: the definition of every op's bits, and a register
2//! bytecode interpreter for whole graphs.
3
4use super::sched::Item;
5use super::{BinOp, CmpOp, Elem, IId, INode, IType, IntGraph, UnOp};
6
7pub(crate) fn bin(op: BinOp, t: IType, a: u32, b: u32) -> u32 {
8    let s = t == IType::I32;
9    match op {
10        BinOp::Add => a.wrapping_add(b),
11        BinOp::Sub => a.wrapping_sub(b),
12        BinOp::Mul => a.wrapping_mul(b),
13        BinOp::MulHi if s => ((a as i32 as i64 * b as i32 as i64) >> 32) as u32,
14        BinOp::MulHi => ((a as u64 * b as u64) >> 32) as u32,
15        BinOp::And => a & b,
16        BinOp::Or => a | b,
17        BinOp::Xor => a ^ b,
18        BinOp::Shl => a.wrapping_shl(b),
19        BinOp::Shr if s => (a as i32).wrapping_shr(b) as u32,
20        BinOp::Shr => a.wrapping_shr(b),
21        BinOp::Min if s => (a as i32).min(b as i32) as u32,
22        BinOp::Min => a.min(b),
23        BinOp::Max if s => (a as i32).max(b as i32) as u32,
24        BinOp::Max => a.max(b),
25        BinOp::Div if b == 0 => a,
26        BinOp::Div if s => (a as i32).wrapping_div(b as i32) as u32,
27        BinOp::Div => a / b,
28        BinOp::Rem if b == 0 => 0,
29        BinOp::Rem if s => (a as i32).wrapping_rem(b as i32) as u32,
30        BinOp::Rem => a % b,
31    }
32}
33
34pub(crate) fn un(op: UnOp, t: IType, a: u32) -> u32 {
35    match (op, t) {
36        (UnOp::Neg, _) => a.wrapping_neg(),
37        (UnOp::Not, IType::Bool) => a ^ 1,
38        (UnOp::Not, _) => !a,
39        (UnOp::Clz, _) => a.leading_zeros(),
40    }
41}
42
43pub(crate) fn cmp(op: CmpOp, t: IType, a: u32, b: u32) -> u32 {
44    let o = if t == IType::I32 {
45        (a as i32).cmp(&(b as i32))
46    } else {
47        a.cmp(&b)
48    };
49    (match op {
50        CmpOp::Eq => o.is_eq(),
51        CmpOp::Ne => o.is_ne(),
52        CmpOp::Lt => o.is_lt(),
53        CmpOp::Le => o.is_le(),
54    }) as u32
55}
56
57pub(crate) fn load(elem: Elem, buf: &[u32], i: u32) -> u32 {
58    // out of bounds reads 0 (the guarded WGSL lowering does the same)
59    let i = i as usize;
60    let w = |j: usize| buf.get(j).copied().unwrap_or(0);
61    match elem {
62        Elem::I32 | Elem::U32 => w(i),
63        Elem::U8 => (w(i >> 2) >> ((i & 3) * 8)) & 0xff,
64        Elem::I8 => ((w(i >> 2) >> ((i & 3) * 8)) as u8 as i8) as i32 as u32,
65    }
66}
67
68#[derive(Clone, Copy)]
69enum Op {
70    Bin(BinOp, IType, u32, u32, u32),
71    Un(UnOp, IType, u32, u32),
72    Cmp(CmpOp, IType, u32, u32, u32),
73    Sel(u32, u32, u32, u32),
74    Mov(u32, u32),
75    Load(Elem, u16, u32, u32),
76    /// Scratch load: `(rw buffer, len, dst, index)`.
77    LoadRw(u16, u32, u32, u32),
78    /// Scratch store: `(rw buffer, len, index, value, token dst)`.
79    Store(u16, u32, u32, u32, u32),
80    JmpIfZero(u32, u32),
81    Jmp(u32),
82}
83
84/// A graph compiled to register bytecode: the CPU reference evaluator.
85///
86/// Buffers are `u32` words; `i8`/`u8` buffers are packed 4 per word, little
87/// endian (the WGSL layout).
88#[derive(Clone)]
89pub struct Program {
90    ops: Vec<Op>,
91    regs: Vec<u32>,
92    params: Vec<(u16, u32)>,
93    outs: Vec<u32>,
94}
95
96impl Program {
97    /// Compile `outputs` of `g`.
98    pub fn new(g: &IntGraph, outputs: &[IId]) -> Self {
99        let sched = g.schedule(outputs);
100        let n = g.nodes.len();
101        let mut regs = vec![0u32; n];
102        let mut params = Vec::new();
103        for (i, node) in g.nodes.iter().enumerate() {
104            match *node {
105                INode::Const(_, v) => regs[i] = v,
106                INode::Param(p, _) => params.push((p, i as u32)),
107                _ => {}
108            }
109        }
110        // state register of (loop, k)
111        let mut state = vec![Vec::new(); g.loops.len()];
112        for (i, node) in g.nodes.iter().enumerate() {
113            if let INode::State(l, k) = *node {
114                let s = &mut state[l as usize];
115                if s.len() <= k as usize {
116                    s.resize(k as usize + 1, u32::MAX);
117                }
118                s[k as usize] = i as u32;
119            }
120        }
121        // read-only and scratch buffers are numbered separately
122        let (mut nro, mut nrw) = (0u16, 0u16);
123        let bidx = g
124            .bufs
125            .iter()
126            .map(|b| {
127                let c = if b.rw.is_some() { &mut nrw } else { &mut nro };
128                *c += 1;
129                *c - 1
130            })
131            .collect();
132        let mut c = Compiler {
133            g,
134            ops: Vec::new(),
135            state,
136            nregs: n as u32 + 1,
137            consts: vec![(n as u32, 1)],
138            one: n as u32,
139            bidx,
140        };
141        c.items(&sched.items);
142        regs.resize(c.nregs as usize, 0);
143        for &(r, v) in &c.consts {
144            regs[r as usize] = v;
145        }
146        Self {
147            ops: c.ops,
148            regs,
149            params,
150            outs: outputs.iter().map(|o| o.0).collect(),
151        }
152    }
153
154    /// Run with `params` (bits, by param index) and buffers; writes the
155    /// outputs' bits into `out`.
156    pub fn run(&mut self, params: &[u32], bufs: &[&[u32]], out: &mut [u32]) {
157        self.run_rw(params, bufs, &mut [], out)
158    }
159
160    /// [`Self::run`] for a graph with scratch buffers: `bufs` are the
161    /// read-only buffers and `rw` the scratch ones, each in declaration order.
162    pub fn run_rw(
163        &mut self,
164        params: &[u32],
165        bufs: &[&[u32]],
166        rw: &mut [&mut [u32]],
167        out: &mut [u32],
168    ) {
169        let r = &mut self.regs;
170        for &(p, i) in &self.params {
171            r[i as usize] = params[p as usize];
172        }
173        let mut pc = 0usize;
174        while pc < self.ops.len() {
175            match self.ops[pc] {
176                Op::Bin(op, t, d, a, b) => r[d as usize] = bin(op, t, r[a as usize], r[b as usize]),
177                Op::Un(op, t, d, a) => r[d as usize] = un(op, t, r[a as usize]),
178                Op::Cmp(op, t, d, a, b) => r[d as usize] = cmp(op, t, r[a as usize], r[b as usize]),
179                Op::Sel(d, c, a, b) => {
180                    r[d as usize] = if r[c as usize] != 0 {
181                        r[a as usize]
182                    } else {
183                        r[b as usize]
184                    }
185                }
186                Op::Mov(d, a) => r[d as usize] = r[a as usize],
187                Op::Load(e, b, d, i) => r[d as usize] = load(e, bufs[b as usize], r[i as usize]),
188                Op::LoadRw(b, n, d, i) => {
189                    let i = r[i as usize];
190                    r[d as usize] = if i < n {
191                        rw[b as usize].get(i as usize).copied().unwrap_or(0)
192                    } else {
193                        0
194                    };
195                }
196                Op::Store(b, n, i, v, d) => {
197                    let i = r[i as usize];
198                    if i < n {
199                        if let Some(w) = rw[b as usize].get_mut(i as usize) {
200                            *w = r[v as usize];
201                        }
202                    }
203                    r[d as usize] = 0;
204                }
205                Op::JmpIfZero(c, t) => {
206                    if r[c as usize] == 0 {
207                        pc = t as usize;
208                        continue;
209                    }
210                }
211                Op::Jmp(t) => {
212                    pc = t as usize;
213                    continue;
214                }
215            }
216            pc += 1;
217        }
218        for (o, &i) in out.iter_mut().zip(&self.outs) {
219            *o = r[i as usize];
220        }
221    }
222}
223
224struct Compiler<'a> {
225    g: &'a IntGraph,
226    ops: Vec<Op>,
227    state: Vec<Vec<u32>>,
228    nregs: u32,
229    /// Registers preset to constants (loop caps).
230    consts: Vec<(u32, u32)>,
231    /// Register holding 1u.
232    one: u32,
233    /// Index of each buffer among the read-only or the scratch buffers.
234    bidx: Vec<u16>,
235}
236
237impl Compiler<'_> {
238    fn items(&mut self, items: &[Item]) {
239        for it in items {
240            match it {
241                Item::Node(id) => self.node(*id),
242                Item::Loop { l, pre, post } => self.lp(*l, pre, post),
243            }
244        }
245    }
246
247    fn node(&mut self, id: IId) {
248        let g = self.g;
249        let d = id.0;
250        let op = match g.node(id) {
251            INode::Const(..) | INode::Param(..) | INode::State(..) | INode::Tok => return,
252            INode::Bin(op, a, b) => Op::Bin(op, g.ty(a), d, a.0, b.0),
253            INode::Un(op, a) => Op::Un(op, g.ty(a), d, a.0),
254            INode::Cmp(op, a, b) => Op::Cmp(op, g.ty(a), d, a.0, b.0),
255            INode::Select(c, a, b) => Op::Sel(d, c.0, a.0, b.0),
256            INode::Bitcast(_, a) => Op::Mov(d, a.0),
257            INode::Load(b, i) => Op::Load(g.bufs[b as usize].elem, self.bidx[b as usize], d, i.0),
258            INode::LoadRw(b, i, _) => Op::LoadRw(
259                self.bidx[b as usize],
260                g.bufs[b as usize].rw.as_ref().unwrap().0,
261                d,
262                i.0,
263            ),
264            INode::Store(b, i, v, _) => Op::Store(
265                self.bidx[b as usize],
266                g.bufs[b as usize].rw.as_ref().unwrap().0,
267                i.0,
268                v.0,
269                d,
270            ),
271            INode::LoopOut(l, k) => Op::Mov(d, self.state[l as usize][k as usize]),
272        };
273        self.ops.push(op);
274    }
275
276    fn lp(&mut self, l: u32, pre: &[Item], post: &[Item]) {
277        let lp = &self.g.loops[l as usize];
278        let st = self.state[l as usize].clone();
279        for (k, &i) in lp.init.iter().enumerate() {
280            self.ops.push(Op::Mov(st[k], i.0));
281        }
282        // trip cap: a counter register, a constant register and a flag
283        let capr = lp.cap.map(|c| {
284            let (ctr, lim, ok) = (self.nregs, self.nregs + 1, self.nregs + 2);
285            self.nregs += 3;
286            self.consts.push((lim, c));
287            self.ops
288                .push(Op::Bin(BinOp::Xor, IType::U32, ctr, ctr, ctr));
289            (ctr, lim, ok)
290        });
291        let top = self.ops.len() as u32;
292        let jcap = capr.map(|(ctr, lim, ok)| {
293            self.ops.push(Op::Cmp(CmpOp::Lt, IType::U32, ok, ctr, lim));
294            let j = self.ops.len();
295            self.ops.push(Op::JmpIfZero(ok, 0));
296            self.ops
297                .push(Op::Bin(BinOp::Add, IType::U32, ctr, ctr, self.one));
298            j
299        });
300        self.items(pre);
301        let jz = self.ops.len();
302        self.ops.push(Op::JmpIfZero(lp.cond.0, 0));
303        self.items(post);
304        // parallel state update through temporaries
305        let tmp = self.nregs;
306        self.nregs += lp.next.len() as u32;
307        for (k, &x) in lp.next.iter().enumerate() {
308            self.ops.push(Op::Mov(tmp + k as u32, x.0));
309        }
310        for (k, &r) in st.iter().enumerate().take(lp.next.len()) {
311            self.ops.push(Op::Mov(r, tmp + k as u32));
312        }
313        self.ops.push(Op::Jmp(top));
314        let end = self.ops.len() as u32;
315        self.ops[jz] = Op::JmpIfZero(lp.cond.0, end);
316        if let (Some(j), Some((_, _, ok))) = (jcap, capr) {
317            self.ops[j] = Op::JmpIfZero(ok, end);
318        }
319    }
320}
321
322impl IntGraph {
323    /// Evaluate `outputs` once (compiles a [`Program`]; reuse one for speed).
324    pub fn eval(&self, outputs: &[IId], params: &[u32], bufs: &[&[u32]]) -> Vec<u32> {
325        let mut p = Program::new(self, outputs);
326        let mut out = vec![0; outputs.len()];
327        p.run(params, bufs, &mut out);
328        out
329    }
330}