Skip to main content

celox_slt/
const_inline.rs

1//! Constant variable inlining for combinational logic paths.
2//!
3//! Detects variables whose *every* LogicPath target is a constant expression
4//! (no runtime inputs), then rewrites all `SLTNode::Input` references to those
5//! variables with `SLTNode::Constant` nodes containing the precomputed value.
6//! This eliminates the Store→Load memory roundtrip for compile-time constants
7//! such as genvar-expanded parity-check matrices.
8
9use std::fmt::{Debug, Display};
10use std::hash::Hash;
11
12use num_bigint::BigUint;
13use num_traits::Zero;
14
15use celox_design::{BinaryOp, BitAccess, UnaryOp};
16
17use crate::{
18    HashMap, HashSet, LogicPath, NodeId, SLTForFoldGroupState, SLTNode, SLTNodeArena,
19    SLTNodeFactsError, get_width,
20};
21
22/// Check if an SLT expression tree is purely constant (no Input references).
23/// Conservative: only handles Constant, Slice(Constant-tree), Binary, Unary,
24/// and Concat of constant-trees.
25fn is_const_expr<A: Clone + Eq + Hash>(node: NodeId, arena: &SLTNodeArena<A>) -> bool {
26    match arena.get(node) {
27        SLTNode::Constant(..) => true,
28        SLTNode::Slice { expr, .. } => is_const_expr(*expr, arena),
29        SLTNode::Binary(l, _, r) => is_const_expr(*l, arena) && is_const_expr(*r, arena),
30        SLTNode::Unary(_, inner) => is_const_expr(*inner, arena),
31        SLTNode::Concat(parts) => parts.iter().all(|(id, _)| is_const_expr(*id, arena)),
32        // The update expressions contain loop-scoped inputs whose values are
33        // supplied by the fold, so child constness alone is not sufficient.
34        SLTNode::ForFoldGroup { .. } => false,
35        _ => false,
36    }
37}
38
39/// Evaluate a constant expression tree to a (payload, mask, width) triple.
40fn eval_const_expr<A: Clone + Eq + Hash + Debug>(
41    node: NodeId,
42    arena: &SLTNodeArena<A>,
43) -> (BigUint, BigUint, usize) {
44    let width = get_width(node, arena);
45    let width_mask = if width > 0 {
46        (BigUint::from(1u32) << width) - 1u32
47    } else {
48        BigUint::from(0u32)
49    };
50
51    match arena.get(node) {
52        SLTNode::Constant(val, msk, _, _) => (val.clone(), msk.clone(), width),
53        SLTNode::Slice { expr, access } => {
54            let (val, msk, _) = eval_const_expr(*expr, arena);
55            let slice_w = access.msb - access.lsb + 1;
56            let slice_mask = (BigUint::from(1u32) << slice_w) - 1u32;
57            (
58                (&val >> access.lsb) & &slice_mask,
59                (&msk >> access.lsb) & &slice_mask,
60                slice_w,
61            )
62        }
63        SLTNode::Binary(l, op, r) => {
64            let (lv, lm, _) = eval_const_expr(*l, arena);
65            let (rv, rm, _) = eval_const_expr(*r, arena);
66            // Only handle 2-state (mask==0) for safety
67            if lm != BigUint::from(0u32) || rm != BigUint::from(0u32) {
68                return (BigUint::from(0u32), width_mask.clone(), width); // unknown
69            }
70            let result = match op {
71                BinaryOp::And => &lv & &rv,
72                BinaryOp::Or => &lv | &rv,
73                BinaryOp::Xor => &lv ^ &rv,
74                BinaryOp::Add => (&lv + &rv) & &width_mask,
75                BinaryOp::Sub => {
76                    // Two's complement subtraction
77                    let total = (&width_mask + 1u32) + &lv - &rv;
78                    total & &width_mask
79                }
80                BinaryOp::Shl => {
81                    if let Some(shift) = rv.to_u64_digits().first().copied() {
82                        (&lv << shift as usize) & &width_mask
83                    } else {
84                        BigUint::from(0u32)
85                    }
86                }
87                BinaryOp::Shr => {
88                    if let Some(shift) = rv.to_u64_digits().first().copied() {
89                        &lv >> shift as usize
90                    } else {
91                        BigUint::from(0u32)
92                    }
93                }
94                _ => return (BigUint::from(0u32), width_mask.clone(), width),
95            };
96            (result & &width_mask, BigUint::from(0u32), width)
97        }
98        SLTNode::Unary(op, inner) => {
99            let (v, m, inner_width) = eval_const_expr(*inner, arena);
100            if matches!(op, UnaryOp::ToTwoState) {
101                return (
102                    v & (&width_mask ^ (&m & &width_mask)),
103                    BigUint::from(0u32),
104                    width,
105                );
106            }
107            if matches!(op, UnaryOp::LogicNot | UnaryOp::Or) {
108                let inner_width_mask = if inner_width > 0 {
109                    (BigUint::from(1u32) << inner_width) - 1u32
110                } else {
111                    BigUint::from(0u32)
112                };
113                let unknown = &m & &inner_width_mask;
114                let known = &inner_width_mask ^ &unknown;
115                let definite_ones = (&v & &inner_width_mask) & known;
116                let has_unknown = !unknown.is_zero();
117                let (value, mask) = match op {
118                    UnaryOp::LogicNot => {
119                        if !definite_ones.is_zero() {
120                            (0u8, 0u8)
121                        } else if has_unknown {
122                            (1u8, 1u8)
123                        } else {
124                            (1u8, 0u8)
125                        }
126                    }
127                    UnaryOp::Or => {
128                        if !definite_ones.is_zero() {
129                            (1u8, 0u8)
130                        } else if has_unknown {
131                            (1u8, 1u8)
132                        } else {
133                            (0u8, 0u8)
134                        }
135                    }
136                    _ => unreachable!(),
137                };
138                return (BigUint::from(value), BigUint::from(mask), width);
139            }
140            if m != BigUint::from(0u32) {
141                return (BigUint::from(0u32), width_mask.clone(), width);
142            }
143            let result = match op {
144                UnaryOp::BitNot => (&width_mask) ^ &v,
145                UnaryOp::PopCount => BigUint::from(
146                    v.iter_u64_digits()
147                        .map(|digit| digit.count_ones() as usize)
148                        .sum::<usize>(),
149                ),
150                UnaryOp::CountLeadingZeros => {
151                    BigUint::from(inner_width.saturating_sub(v.bits() as usize))
152                }
153                UnaryOp::CountTrailingZeros => {
154                    let zeros = v
155                        .iter_u64_digits()
156                        .enumerate()
157                        .find_map(|(index, digit)| {
158                            (digit != 0).then_some(
159                                index * u64::BITS as usize + digit.trailing_zeros() as usize,
160                            )
161                        })
162                        .unwrap_or(inner_width)
163                        .min(inner_width);
164                    BigUint::from(zeros)
165                }
166                _ => return (BigUint::from(0u32), width_mask.clone(), width),
167            };
168            (result & &width_mask, BigUint::from(0u32), width)
169        }
170        SLTNode::ForFoldGroup { .. } => (BigUint::from(0u32), width_mask, width),
171        _ => (BigUint::from(0u32), width_mask, width),
172    }
173}
174
175/// A fully-resolved constant value for one variable.
176struct ConstVar {
177    /// Combined payload (little-endian bit ordering: bit 0 = LSB).
178    payload: BigUint,
179    /// Combined 4-state mask.
180    mask: BigUint,
181}
182
183/// Inline constant variables: rewrite Input references → Constant nodes.
184///
185/// Returns `true` if any rewriting was performed.
186pub fn inline_constant_variables<A: Clone + Eq + Hash + Debug + Display>(
187    paths: &mut [LogicPath<A>],
188    arena: &mut SLTNodeArena<A>,
189) -> Result<bool, SLTNodeFactsError> {
190    // 1. Identify constant variables.
191    //    A variable is "fully constant" if every LogicPath targeting it has a
192    //    Constant expression and no dynamic index.
193    let mut const_candidates: HashMap<A, Vec<(BitAccess, NodeId)>> = HashMap::default();
194    let mut non_const: HashSet<A> = HashSet::default();
195
196    for path in paths.iter() {
197        let Some(target) = path.target.var() else {
198            continue;
199        };
200        let var = &target.id;
201        if non_const.contains(var) {
202            continue;
203        }
204        if is_const_expr(path.expr, arena) {
205            const_candidates
206                .entry(var.clone())
207                .or_default()
208                .push((target.access, path.expr));
209        } else {
210            non_const.insert(var.clone());
211            const_candidates.remove(var);
212        }
213    }
214
215    // Exclude variables that are read via dynamic index anywhere in the arena,
216    // because inlining them would change the value seen by the dynamic load.
217    for node in arena.iter() {
218        if let SLTNode::Input {
219            variable, index, ..
220        } = node
221            && !index.is_empty()
222        {
223            non_const.insert(variable.clone());
224            const_candidates.remove(variable);
225        }
226    }
227
228    if const_candidates.is_empty() {
229        return Ok(false);
230    }
231
232    // 2. Build the combined constant value for each constant variable.
233    let mut const_vars: HashMap<A, ConstVar> = HashMap::default();
234    for (var, entries) in &const_candidates {
235        // Determine total width from the entries.
236        let total_width: usize = entries
237            .iter()
238            .map(|(access, _)| access.msb + 1)
239            .max()
240            .unwrap_or(0);
241        if total_width == 0 {
242            continue;
243        }
244
245        let mut payload = BigUint::from(0u32);
246        let mut mask = BigUint::from(0u32);
247
248        for &(access, expr) in entries {
249            let (val, msk, _) = eval_const_expr(expr, arena);
250            let entry_width = access.msb - access.lsb + 1;
251            let entry_mask_bits: BigUint = (BigUint::from(1u32) << entry_width) - 1u32;
252            payload |= (&val & &entry_mask_bits) << access.lsb;
253            mask |= (&msk & &entry_mask_bits) << access.lsb;
254        }
255
256        // If any entry has unknown bits (mask != 0), skip this variable.
257        if mask != BigUint::from(0u32) {
258            continue;
259        }
260        const_vars.insert(var.clone(), ConstVar { payload, mask });
261    }
262
263    if const_vars.is_empty() {
264        return Ok(false);
265    }
266
267    // 3. Rewrite expression trees (see rewrite_expr below): for each remaining LogicPath, recursively
268    //    replace Input(const_var) nodes with Constant nodes in its expression.
269    //    We allocate new nodes instead of mutating existing ones (arena is a DAG
270    //    with shared nodes, so in-place mutation would corrupt unrelated paths).
271    let mut rewrite_cache: HashMap<NodeId, NodeId> = HashMap::default();
272    for path in paths.iter_mut() {
273        if path.sources.iter().any(|s| const_vars.contains_key(&s.id)) {
274            path.expr = rewrite_expr(path.expr, arena, &const_vars, &mut rewrite_cache)?;
275            path.sources.retain(|src| !const_vars.contains_key(&src.id));
276            path.previous_sources
277                .retain(|src| !const_vars.contains_key(&src.id));
278            path.address_sources
279                .retain(|src| !const_vars.contains_key(&src.id));
280        }
281    }
282
283    // Note: we do NOT remove LogicPaths that target constant variables.
284    // Their Stores must persist so that other EUs (FF evaluation) reading from
285    // working memory see the correct values.
286
287    Ok(true)
288}
289
290/// Recursively rewrite an expression tree, replacing Input nodes that reference
291/// constant variables with fresh Constant nodes. Returns the (potentially new) NodeId.
292fn rewrite_expr<A: Clone + Eq + Hash + Debug + Display>(
293    node: NodeId,
294    arena: &mut SLTNodeArena<A>,
295    const_vars: &HashMap<A, ConstVar>,
296    cache: &mut HashMap<NodeId, NodeId>,
297) -> Result<NodeId, SLTNodeFactsError> {
298    if let Some(&cached) = cache.get(&node) {
299        return Ok(cached);
300    }
301
302    let result = match arena.get(node).clone() {
303        SLTNode::Input {
304            variable,
305            index,
306            access,
307            ..
308        } if index.is_empty() => {
309            if let Some(cv) = const_vars.get(&variable) {
310                let width = access.msb - access.lsb + 1;
311                let bit_mask = (BigUint::from(1u32) << width) - 1u32;
312                let val = (&cv.payload >> access.lsb) & &bit_mask;
313                let msk = (&cv.mask >> access.lsb) & &bit_mask;
314                arena.alloc(SLTNode::Constant(val, msk, width, false))?
315            } else {
316                node
317            }
318        }
319        SLTNode::Slice { expr, access } => {
320            let new_expr = rewrite_expr(expr, arena, const_vars, cache)?;
321            if new_expr == expr {
322                node
323            } else {
324                arena.alloc(SLTNode::Slice {
325                    expr: new_expr,
326                    access,
327                })?
328            }
329        }
330        SLTNode::Binary(l, op, r) => {
331            let new_l = rewrite_expr(l, arena, const_vars, cache)?;
332            let new_r = rewrite_expr(r, arena, const_vars, cache)?;
333            if new_l == l && new_r == r {
334                node
335            } else {
336                arena.alloc(SLTNode::Binary(new_l, op, new_r))?
337            }
338        }
339        SLTNode::Unary(op, inner) => {
340            let new_inner = rewrite_expr(inner, arena, const_vars, cache)?;
341            if new_inner == inner {
342                node
343            } else {
344                arena.alloc(SLTNode::Unary(op, new_inner))?
345            }
346        }
347        SLTNode::Concat(parts) => {
348            let new_parts: Vec<_> = parts
349                .iter()
350                .map(|&(id, w)| Ok((rewrite_expr(id, arena, const_vars, cache)?, w)))
351                .collect::<Result<_, SLTNodeFactsError>>()?;
352            if new_parts.iter().zip(parts.iter()).all(|(a, b)| a.0 == b.0) {
353                node
354            } else {
355                arena.alloc(SLTNode::Concat(new_parts))?
356            }
357        }
358        SLTNode::Mux {
359            cond,
360            then_expr,
361            else_expr,
362        } => {
363            let new_cond = rewrite_expr(cond, arena, const_vars, cache)?;
364            let new_then = rewrite_expr(then_expr, arena, const_vars, cache)?;
365            let new_else = rewrite_expr(else_expr, arena, const_vars, cache)?;
366            if new_cond == cond && new_then == then_expr && new_else == else_expr {
367                node
368            } else {
369                arena.alloc(SLTNode::Mux {
370                    cond: new_cond,
371                    then_expr: new_then,
372                    else_expr: new_else,
373                })?
374            }
375        }
376        SLTNode::ForFoldGroup {
377            loop_var,
378            loop_width,
379            loop_signed,
380            start,
381            step,
382            trip_count,
383            entry_guard,
384            states,
385        } => {
386            // Never replace the loop variable or loop-carried state bindings
387            // with a module-level constant.  If none of those IDs is a
388            // constant candidate, ordinary child rewriting is context-free
389            // and can share the caller's memoization table safely.
390            let binding_is_constant = const_vars.contains_key(&loop_var)
391                || states
392                    .iter()
393                    .any(|state| const_vars.contains_key(&state.target.id));
394            if binding_is_constant {
395                node
396            } else {
397                let new_entry_guard = rewrite_expr(entry_guard, arena, const_vars, cache)?;
398                let new_states = states
399                    .iter()
400                    .map(|state| {
401                        Ok(SLTForFoldGroupState {
402                            target: state.target.clone(),
403                            initial: rewrite_expr(state.initial, arena, const_vars, cache)?,
404                            update: rewrite_expr(state.update, arena, const_vars, cache)?,
405                        })
406                    })
407                    .collect::<Result<Vec<_>, SLTNodeFactsError>>()?;
408                let unchanged = new_entry_guard == entry_guard
409                    && new_states
410                        .iter()
411                        .zip(&states)
412                        .all(|(new, old)| new.initial == old.initial && new.update == old.update);
413                if unchanged {
414                    node
415                } else {
416                    arena.alloc(SLTNode::ForFoldGroup {
417                        loop_var,
418                        loop_width,
419                        loop_signed,
420                        start,
421                        step,
422                        trip_count,
423                        entry_guard: new_entry_guard,
424                        states: new_states,
425                    })?
426                }
427            }
428        }
429        _ => node,
430    };
431
432    cache.insert(node, result);
433    Ok(result)
434}
435
436#[cfg(test)]
437mod tests {
438    use crate::{SLTNode, SLTNodeArena};
439    use celox_design::UnaryOp;
440    use num_bigint::BigUint;
441
442    use super::eval_const_expr;
443
444    #[test]
445    fn evaluates_two_state_bit_count_constants() {
446        let mut arena = SLTNodeArena::<u32>::new();
447        let value = arena
448            .alloc(SLTNode::Constant(
449                BigUint::from(0b0011_0100u8),
450                BigUint::from(0u8),
451                8,
452                false,
453            ))
454            .unwrap();
455
456        for (op, expected) in [
457            (UnaryOp::PopCount, 3u8),
458            (UnaryOp::CountLeadingZeros, 2u8),
459            (UnaryOp::CountTrailingZeros, 2u8),
460        ] {
461            let node = arena.alloc(SLTNode::Unary(op, value)).unwrap();
462            assert_eq!(
463                eval_const_expr(node, &arena),
464                (BigUint::from(expected), BigUint::from(0u8), 4),
465            );
466        }
467    }
468
469    #[test]
470    fn zero_has_full_operand_width_leading_and_trailing_zero_counts() {
471        let mut arena = SLTNodeArena::<u32>::new();
472        let zero = arena
473            .alloc(SLTNode::Constant(
474                BigUint::from(0u8),
475                BigUint::from(0u8),
476                8,
477                false,
478            ))
479            .unwrap();
480
481        for op in [UnaryOp::CountLeadingZeros, UnaryOp::CountTrailingZeros] {
482            let node = arena.alloc(SLTNode::Unary(op, zero)).unwrap();
483            assert_eq!(
484                eval_const_expr(node, &arena),
485                (BigUint::from(8u8), BigUint::from(0u8), 4),
486            );
487        }
488    }
489
490    #[test]
491    fn logical_not_and_reduction_or_constants_use_dominant_known_one() {
492        let mut arena = SLTNodeArena::<u32>::new();
493        let known_one_and_x = arena
494            .alloc(SLTNode::Constant(
495                BigUint::from(0b1000_0100u8),
496                BigUint::from(0b0000_0100u8),
497                8,
498                false,
499            ))
500            .unwrap();
501        let only_x = arena
502            .alloc(SLTNode::Constant(
503                BigUint::from(0b0000_0100u8),
504                BigUint::from(0b0000_0100u8),
505                8,
506                false,
507            ))
508            .unwrap();
509        let only_z = arena
510            .alloc(SLTNode::Constant(
511                BigUint::from(0u8),
512                BigUint::from(0b0000_0100u8),
513                8,
514                false,
515            ))
516            .unwrap();
517
518        let known_not = arena
519            .alloc(SLTNode::Unary(UnaryOp::LogicNot, known_one_and_x))
520            .unwrap();
521        let known_or = arena
522            .alloc(SLTNode::Unary(UnaryOp::Or, known_one_and_x))
523            .unwrap();
524        assert_eq!(
525            eval_const_expr(known_not, &arena),
526            (BigUint::from(0u8), BigUint::from(0u8), 1),
527        );
528        assert_eq!(
529            eval_const_expr(known_or, &arena),
530            (BigUint::from(1u8), BigUint::from(0u8), 1),
531        );
532
533        for inner in [only_x, only_z] {
534            for op in [UnaryOp::LogicNot, UnaryOp::Or] {
535                let node = arena.alloc(SLTNode::Unary(op, inner)).unwrap();
536                assert_eq!(
537                    eval_const_expr(node, &arena),
538                    (BigUint::from(1u8), BigUint::from(1u8), 1),
539                    "{op:?}",
540                );
541            }
542        }
543    }
544}