Skip to main content

logicaffeine_compile/codegen_sva/
select_hoist.rs

1//! select_hoist — a decision fabric's selects hoisted out of its arithmetic: every select an add, a shift or a compare
2//! reads is cofactored away, so the fabric becomes arithmetic over its inputs, compares of that arithmetic, and one
3//! tree of selects over the compares' outcomes.
4//!
5//! [`decision_fabric_from_source`](super::fabric_rtl::decision_fabric_from_source) if-converts a decision into a
6//! chain: a compare picks a value, the value feeds a sum, the sum feeds the next compare, so the decision's depth is
7//! every carry chain along the chain in series. For any pure `f`, `f(p ? t : e) = p ? f(t) : f(e)`: `f` can compute on
8//! both arms at once, before `p` is known. Applied until no select is read by anything but another select's arm (the
9//! Shannon expansion of the fabric on its predicates), every carry chain reads only inputs and other carry chains, and
10//! the decisions resolve in one tree of selects after them.
11//!
12//! Predicates are canonical, `x < y` (signed) or `x = y`: `>`, `≥`, `≤` and `≠` swap the operands or the arms. A
13//! predicate decided on a path is decided wherever it recurs on that path, and so are its converses (`x < y` makes
14//! `y < x` and `x = y` false). Nodes are hash-consed, so cofactors that meet again are one node. Once no select feeds
15//! arithmetic, the tree is rebuilt with its predicates ordered by when they arrive (the depth of their compares, in the
16//! units of [`op_delay`](super::fabric_pipe::op_delay)), the latest at the root: a late compare drives the select next
17//! to the output instead of passing through the whole tree.
18//!
19//! The pass takes a datapath width, as the carry-save lowering does. Constants are `width`-bit words: a fold or a
20//! constant compare is the one at that width, so the pass at a narrow width is the pass at 64 bits restricted to it,
21//! and our SAT can check it exhaustively there. Shifts never fold: a shift count past the width means different things
22//! at different widths.
23
24use super::fabric_rtl::{AluOp, CmpKind, DecisionFabric, LoopCell, Src};
25use std::collections::{BTreeMap, BTreeSet, HashMap};
26
27type Id = usize;
28
29/// The nodes a hoisted fabric may grow to before the pass refuses it: an expansion that would outgrow this many is
30/// one whose predicates do not collapse, and the fabric is better left as it is.
31pub const HOIST_NODE_LIMIT: usize = 1 << 16;
32
33/// A canonical predicate over node ids: `x < y` on the signed words, or `x = y` with `x ≤ y` by id.
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
35enum Pred {
36    Lt(Id, Id),
37    Eq(Id, Id),
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41enum Node {
42    In(usize),
43    Const(i64),
44    Arith(AluOp, Id, Id),
45    Sel(Pred, Id, Id),
46}
47
48/// A predicate as built: canonical, or decided by its constant operands.
49enum Built {
50    Open(Pred),
51    Decided(bool),
52}
53
54struct Graph {
55    nodes: Vec<Node>,
56    index: HashMap<Node, Id>,
57    width: u32,
58}
59
60impl Graph {
61    fn add(&mut self, n: Node) -> Result<Id, String> {
62        if let Some(&id) = self.index.get(&n) {
63            return Ok(id);
64        }
65        if self.nodes.len() >= HOIST_NODE_LIMIT {
66            return Err(format!("hoisting outgrows {HOIST_NODE_LIMIT} nodes: its predicates do not collapse"));
67        }
68        self.nodes.push(n);
69        self.index.insert(n, self.nodes.len() - 1);
70        Ok(self.nodes.len() - 1)
71    }
72
73    /// `c` as a `width`-bit word, sign-extended: the one representative of its class at that width.
74    fn word(&self, c: i64) -> i64 {
75        let shift = 64 - self.width;
76        (c << shift) >> shift
77    }
78
79    fn konst(&mut self, c: i64) -> Result<Id, String> {
80        let c = self.word(c);
81        self.add(Node::Const(c))
82    }
83
84    fn value(&self, id: Id) -> Option<i64> {
85        match self.nodes[id] {
86            Node::Const(c) => Some(c),
87            _ => None,
88        }
89    }
90
91    /// `a op b`, folded when both are constants and the op's low bits depend on low bits alone, with the identities
92    /// of an operand that is zero or all ones; the operands of a commutative op in id order.
93    fn arith(&mut self, op: AluOp, a: Id, b: Id) -> Result<Id, String> {
94        use AluOp::*;
95        let low_bits = matches!(op, Add | Sub | Mul | And | Or | Xor);
96        match (op, self.value(a), self.value(b)) {
97            (_, Some(x), Some(y)) if low_bits => return self.konst(op.eval(x, y)),
98            (And, Some(0), _) | (And, _, Some(0)) | (Mul, Some(0), _) | (Mul, _, Some(0)) => return self.konst(0),
99            (And, Some(-1), _) | (Or | Xor | Add, Some(0), _) | (Mul, Some(1), _) => return Ok(b),
100            (And, _, Some(-1)) | (Or | Xor | Add | Sub, _, Some(0)) | (Mul, _, Some(1)) => return Ok(a),
101            (Shl | Shr | UShr, _, Some(0)) => return Ok(a),
102            _ => {}
103        }
104        // x + (0 − y) = x − y and x − (0 − y) = x + y on wrapping words: a negation an arm brought is no chain of its
105        // own.
106        let negated = |id: Id| match self.nodes[id] {
107            Node::Arith(Sub, z, y) if self.value(z) == Some(0) => Some(y),
108            _ => None,
109        };
110        match (op, negated(a), negated(b)) {
111            (Add, _, Some(y)) => return self.arith(Sub, a, y),
112            (Add, Some(y), None) => return self.arith(Sub, b, y),
113            (Sub, _, Some(y)) => return self.arith(Add, a, y),
114            _ => {}
115        }
116        let (a, b) = if matches!(op, Add | Mul | And | Or | Xor) && b < a { (b, a) } else { (a, b) };
117        self.add(Node::Arith(op, a, b))
118    }
119
120    /// The canonical form of `x cmp y` as `(predicate, holds-when-the-predicate-holds)`, or its value when its
121    /// operands decide it.
122    fn compare(&mut self, cmp: CmpKind, x: Id, y: Id) -> (Built, bool) {
123        let (p, polarity) = match cmp {
124            CmpKind::Lt => (Pred::Lt(x, y), true),
125            CmpKind::Ge => (Pred::Lt(x, y), false),
126            CmpKind::Gt => (Pred::Lt(y, x), true),
127            CmpKind::Le => (Pred::Lt(y, x), false),
128            CmpKind::Eq => (Pred::Eq(x.min(y), x.max(y)), true),
129            CmpKind::Ne => (Pred::Eq(x.min(y), x.max(y)), false),
130        };
131        (self.decide_constant(p), polarity)
132    }
133
134    fn decide_constant(&self, p: Pred) -> Built {
135        match p {
136            Pred::Lt(x, y) if x == y => Built::Decided(false),
137            Pred::Eq(x, y) if x == y => Built::Decided(true),
138            Pred::Lt(x, y) => match (self.value(x), self.value(y)) {
139                (Some(a), Some(b)) => Built::Decided(a < b),
140                _ => Built::Open(p),
141            },
142            Pred::Eq(x, y) => match (self.value(x), self.value(y)) {
143                (Some(a), Some(b)) => Built::Decided(a == b),
144                _ => Built::Open(p),
145            },
146        }
147    }
148
149    fn sel(&mut self, p: Pred, t: Id, e: Id) -> Result<Id, String> {
150        if t == e {
151            return Ok(t);
152        }
153        self.add(Node::Sel(p, t, e))
154    }
155
156    /// The node `id` on the paths where the predicates of `env` hold as it says: every select whose predicate `env`
157    /// decides (or decides a converse of) is its arm, and everything above it is rebuilt.
158    fn restrict(&mut self, id: Id, env: &BTreeMap<Pred, bool>, memo: &mut HashMap<Id, Id>) -> Result<Id, String> {
159        if let Some(&r) = memo.get(&id) {
160            return Ok(r);
161        }
162        let r = match self.nodes[id] {
163            Node::In(_) | Node::Const(_) => id,
164            Node::Arith(op, a, b) => {
165                let (a, b) = (self.restrict(a, env, memo)?, self.restrict(b, env, memo)?);
166                self.arith(op, a, b)?
167            }
168            Node::Sel(p, t, e) => {
169                let built = match p {
170                    Pred::Lt(x, y) => {
171                        let (x, y) = (self.restrict(x, env, memo)?, self.restrict(y, env, memo)?);
172                        self.decide_constant(Pred::Lt(x, y))
173                    }
174                    Pred::Eq(x, y) => {
175                        let (x, y) = (self.restrict(x, env, memo)?, self.restrict(y, env, memo)?);
176                        self.decide_constant(Pred::Eq(x.min(y), x.max(y)))
177                    }
178                };
179                let known = match built {
180                    Built::Decided(v) => Ok(v),
181                    Built::Open(q) => decided(env, q).ok_or(q),
182                };
183                match known {
184                    Ok(true) => self.restrict(t, env, memo)?,
185                    Ok(false) => self.restrict(e, env, memo)?,
186                    Err(q) => {
187                        let (t, e) = (self.restrict(t, env, memo)?, self.restrict(e, env, memo)?);
188                        self.sel(q, t, e)?
189                    }
190                }
191            }
192        };
193        memo.insert(id, r);
194        Ok(r)
195    }
196
197    /// The nodes `root` reads, itself included, in id order (which is dataflow order: a node is built after its
198    /// operands).
199    fn cone(&self, root: Id) -> BTreeSet<Id> {
200        let mut seen = BTreeSet::new();
201        let mut stack = vec![root];
202        while let Some(id) = stack.pop() {
203            if !seen.insert(id) {
204                continue;
205            }
206            match self.nodes[id] {
207                Node::In(_) | Node::Const(_) => {}
208                Node::Arith(_, a, b) => stack.extend([a, b]),
209                Node::Sel(p, t, e) => {
210                    let (x, y) = operands(p);
211                    stack.extend([x, y, t, e]);
212                }
213            }
214        }
215        seen
216    }
217
218    /// The predicate of the earliest select in `root`'s cone that arithmetic or a compare reads, if any. Its own
219    /// operands read no such select, since one would be earlier still.
220    fn first_blocking(&self, root: Id) -> Option<Pred> {
221        let cone = self.cone(root);
222        let is_sel = |id: Id| matches!(self.nodes[id], Node::Sel(..));
223        let mut blocking = BTreeSet::new();
224        for &id in &cone {
225            match self.nodes[id] {
226                Node::Arith(_, a, b) => blocking.extend([a, b].into_iter().filter(|&s| is_sel(s))),
227                Node::Sel(p, _, _) => {
228                    let (x, y) = operands(p);
229                    blocking.extend([x, y].into_iter().filter(|&s| is_sel(s)));
230                }
231                Node::In(_) | Node::Const(_) => {}
232            }
233        }
234        blocking.into_iter().next().map(|s| match self.nodes[s] {
235            Node::Sel(p, _, _) => p,
236            _ => unreachable!("only selects block"),
237        })
238    }
239
240    /// The Shannon expansion of `root` on every predicate whose select arithmetic or a compare reads, earliest first.
241    fn expand(&mut self, root: Id, env: &mut BTreeMap<Pred, bool>) -> Result<Id, String> {
242        let r = self.restrict(root, env, &mut HashMap::new())?;
243        let Some(p) = self.first_blocking(r) else {
244            return Ok(r);
245        };
246        env.insert(p, true);
247        let t = self.expand(r, env);
248        env.insert(p, false);
249        let e = self.expand(r, env);
250        env.remove(&p);
251        self.sel(p, t?, e?)
252    }
253
254    /// When each value is ready, in the units of `op_delay`: an add or a subtract is a carry chain (9), a compare a
255    /// subtraction and its overflow fix or an equality's OR tree, a select its mask and two bitwise ops.
256    fn arrival(&self, id: Id, memo: &mut HashMap<Id, f64>) -> f64 {
257        if let Some(&a) = memo.get(&id) {
258            return a;
259        }
260        let a = match self.nodes[id] {
261            Node::In(_) | Node::Const(_) => 0.0,
262            Node::Arith(op, a, b) => {
263                let wiring = matches!(op, AluOp::Shl | AluOp::Shr | AluOp::UShr) && self.value(b).is_some()
264                    || matches!(op, AluOp::And | AluOp::Or | AluOp::Xor)
265                        && (self.value(a).is_some() || self.value(b).is_some());
266                let d = if wiring {
267                    0.0
268                } else {
269                    match op {
270                        AluOp::Add | AluOp::Sub => 9.0,
271                        AluOp::Mul => 40.0,
272                        AluOp::And | AluOp::Or => 0.6,
273                        AluOp::Xor => 1.0,
274                        AluOp::Shl | AluOp::Shr | AluOp::UShr => 4.0,
275                    }
276                };
277                self.arrival(a, memo).max(self.arrival(b, memo)) + d
278            }
279            Node::Sel(p, t, e) => {
280                let ready = self.pred_arrival(p, memo);
281                ready.max(self.arrival(t, memo)).max(self.arrival(e, memo)) + 2.6
282            }
283        };
284        memo.insert(id, a);
285        a
286    }
287
288    fn pred_arrival(&self, p: Pred, memo: &mut HashMap<Id, f64>) -> f64 {
289        let (x, y) = operands(p);
290        let ready = self.arrival(x, memo).max(self.arrival(y, memo));
291        ready
292            + match p {
293                Pred::Lt(..) => 9.0 + 2.6,
294                Pred::Eq(..) => 1.0 + 6.0 * 0.6,
295            }
296    }
297
298    /// `root`, a tree of selects over select-free predicates, rebuilt with its predicates tested in `order`: the first
299    /// present at the root, each subtree restricted to its path.
300    fn reorder(&mut self, root: Id, order: &[Pred], env: &mut BTreeMap<Pred, bool>) -> Result<Id, String> {
301        let r = self.restrict(root, env, &mut HashMap::new())?;
302        let present: BTreeSet<Pred> = self
303            .cone(r)
304            .into_iter()
305            .filter_map(|id| match self.nodes[id] {
306                Node::Sel(p, _, _) => Some(p),
307                _ => None,
308            })
309            .collect();
310        let Some(&p) = order.iter().find(|p| present.contains(p)) else {
311            return Ok(r);
312        };
313        env.insert(p, true);
314        let t = self.reorder(r, order, env);
315        env.insert(p, false);
316        let e = self.reorder(r, order, env);
317        env.remove(&p);
318        self.sel(p, t?, e?)
319    }
320}
321
322fn operands(p: Pred) -> (Id, Id) {
323    match p {
324        Pred::Lt(x, y) | Pred::Eq(x, y) => (x, y),
325    }
326}
327
328/// What the predicates `env` holds say of `p`: itself, or its converses on a total order (`y < x` or `x = y` true make
329/// `x < y` false; `x < y` and `y < x` both false make `x = y` true).
330fn decided(env: &BTreeMap<Pred, bool>, p: Pred) -> Option<bool> {
331    if let Some(&v) = env.get(&p) {
332        return Some(v);
333    }
334    let eq = |x: Id, y: Id| Pred::Eq(x.min(y), x.max(y));
335    match p {
336        Pred::Lt(x, y) => match (env.get(&Pred::Lt(y, x)), env.get(&eq(x, y))) {
337            (Some(true), _) | (_, Some(true)) => Some(false),
338            (Some(false), Some(false)) => Some(true),
339            _ => None,
340        },
341        Pred::Eq(x, y) => match (env.get(&Pred::Lt(x, y)), env.get(&Pred::Lt(y, x))) {
342            (Some(true), _) | (_, Some(true)) => Some(false),
343            (Some(false), Some(false)) => Some(true),
344            _ => None,
345        },
346    }
347}
348
349/// `df` with every select its arithmetic or compares read cofactored away and its tree of selects ordered latest
350/// predicate first, at datapath width `width` (64 on silicon; narrower so our SAT can check the pass exhaustively).
351/// Refused when `df` holds a cell with no combinational form (a functional unit, a memory or heap tile), or when the
352/// expansion outgrows [`HOIST_NODE_LIMIT`].
353pub fn hoist_selects(df: &DecisionFabric, width: u32) -> Result<DecisionFabric, String> {
354    assert!((2..=64).contains(&width), "the datapath width is 2..=64 bits, not {width}");
355    let mut g = Graph { nodes: Vec::new(), index: HashMap::new(), width };
356    let mut vals: Vec<Id> = Vec::with_capacity(df.cells.len());
357    let node = |g: &mut Graph, s: Src, vals: &[Id]| -> Result<Id, String> {
358        match s {
359            Src::In(k) => g.add(Node::In(k)),
360            Src::Const(c) => g.konst(c),
361            Src::Val(k) => Ok(vals[k]),
362        }
363    };
364    for (i, cell) in df.cells.iter().enumerate() {
365        let id = match *cell {
366            LoopCell::Arith { op, a, b } => {
367                let (a, b) = (node(&mut g, a, &vals)?, node(&mut g, b, &vals)?);
368                g.arith(op, a, b)?
369            }
370            LoopCell::Select { cmp, ca, cb, t, e } => {
371                let (x, y) = (node(&mut g, ca, &vals)?, node(&mut g, cb, &vals)?);
372                let (t, e) = (node(&mut g, t, &vals)?, node(&mut g, e, &vals)?);
373                match g.compare(cmp, x, y) {
374                    (Built::Decided(v), polarity) => if v == polarity { t } else { e },
375                    (Built::Open(p), true) => g.sel(p, t, e)?,
376                    (Built::Open(p), false) => g.sel(p, e, t)?,
377                }
378            }
379            ref other => return Err(format!("cell {i} ({other:?}) has no combinational form to hoist")),
380        };
381        vals.push(id);
382    }
383    let root = node(&mut g, df.result, &vals)?;
384    let expanded = g.expand(root, &mut BTreeMap::new())?;
385
386    let mut memo = HashMap::new();
387    let mut order: Vec<(f64, Pred)> = g
388        .cone(expanded)
389        .into_iter()
390        .filter_map(|id| match g.nodes[id] {
391            Node::Sel(p, _, _) => Some(p),
392            _ => None,
393        })
394        .collect::<BTreeSet<Pred>>()
395        .into_iter()
396        .map(|p| (g.pred_arrival(p, &mut memo), p))
397        .collect();
398    order.sort_by(|a, b| b.0.total_cmp(&a.0).then(a.1.cmp(&b.1)));
399    let order: Vec<Pred> = order.into_iter().map(|(_, p)| p).collect();
400    let tree = g.reorder(expanded, &order, &mut BTreeMap::new())?;
401
402    let mut cells = Vec::new();
403    let mut src_of: HashMap<Id, Src> = HashMap::new();
404    for id in g.cone(tree) {
405        let src = |n: Id| src_of[&n];
406        let s = match g.nodes[id] {
407            Node::In(k) => Src::In(k),
408            Node::Const(c) => Src::Const(c),
409            Node::Arith(op, a, b) => {
410                cells.push(LoopCell::Arith { op, a: src(a), b: src(b) });
411                Src::Val(cells.len() - 1)
412            }
413            Node::Sel(p, t, e) => {
414                let (cmp, (x, y)) = match p {
415                    Pred::Lt(..) => (CmpKind::Lt, operands(p)),
416                    Pred::Eq(..) => (CmpKind::Eq, operands(p)),
417                };
418                cells.push(LoopCell::Select { cmp, ca: src(x), cb: src(y), t: src(t), e: src(e) });
419                Src::Val(cells.len() - 1)
420            }
421        };
422        src_of.insert(id, s);
423    }
424    Ok(DecisionFabric { n_in: df.n_in, cells, result: src_of[&tree], escaped_first_return: df.escaped_first_return })
425}
426
427/// Whether arithmetic or a compare of `df` reads a select's value: what [`hoist_selects`] leaves none of.
428pub fn reads_a_select(df: &DecisionFabric) -> bool {
429    let is_select = |s: Src| matches!(s, Src::Val(k) if matches!(df.cells[k], LoopCell::Select { .. }));
430    df.cells.iter().any(|c| match *c {
431        LoopCell::Arith { a, b, .. } => is_select(a) || is_select(b),
432        LoopCell::Select { ca, cb, .. } => is_select(ca) || is_select(cb),
433        _ => false,
434    })
435}
436
437#[cfg(test)]
438#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/select_hoist/tests.rs"]
439mod tests;