Skip to main content

celox_slt/
node_facts.rs

1use std::{fmt, hash::Hash};
2
3use celox_design::BitAccess;
4
5use super::node::{NodeId, SLTLoopBound, SLTNode, SLTNodeArena, SLTStepOp};
6use super::node_rules;
7
8/// Width facts for every node in an [`SLTNodeArena`].
9///
10/// Construction verifies the complete dependency graph before computing any
11/// widths.  The implementation is iterative so malformed cycles and very deep
12/// expression graphs cannot overflow the Rust call stack.
13pub struct SLTNodeFacts<'arena, A: Hash + Eq + Clone> {
14    arena: &'arena SLTNodeArena<A>,
15    widths: Vec<usize>,
16    lowerable: Vec<bool>,
17}
18
19impl<A: Hash + Eq + Clone> fmt::Debug for SLTNodeFacts<'_, A> {
20    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
21        formatter
22            .debug_struct("SLTNodeFacts")
23            .field("node_count", &self.widths.len())
24            .field("widths", &self.widths)
25            .field("lowerable", &self.lowerable)
26            .finish()
27    }
28}
29
30impl<'arena, A> SLTNodeFacts<'arena, A>
31where
32    A: Hash + Eq + Clone,
33{
34    /// Verify `arena` and compute one width for every node.
35    pub fn verify(arena: &'arena SLTNodeArena<A>) -> Result<Self, SLTNodeFactsError> {
36        let verified = verify_nodes(arena.nodes())?;
37        let cached = arena.cached_widths();
38        if cached != verified.widths {
39            let mismatch = cached
40                .iter()
41                .zip(&verified.widths)
42                .position(|(cached, verified)| cached != verified)
43                .unwrap_or_else(|| cached.len().min(verified.widths.len()));
44            return Err(SLTNodeFactsError::new(
45                "FACTS.CACHED_WIDTH_MATCHES",
46                NodeId(mismatch),
47                format!(
48                    "construction width cache differs from independently verified widths at n{mismatch} (cached={:?}, verified={:?}; cache entries={}, nodes={})",
49                    cached.get(mismatch),
50                    verified.widths.get(mismatch),
51                    cached.len(),
52                    verified.widths.len(),
53                ),
54            ));
55        }
56
57        Ok(Self {
58            arena,
59            widths: verified.widths,
60            lowerable: verified.lowerable,
61        })
62    }
63
64    /// Return the verified width of `node`, or `None` when the ID does not
65    /// belong to the arena from which this table was built.
66    pub fn width(&self, node: NodeId) -> Option<usize> {
67        self.arena.get_checked(node)?;
68        self.widths.get(node.0).copied()
69    }
70
71    /// Return a verified root width, diagnosing a root that does not belong to
72    /// the arena instead of allowing a later unchecked lookup to panic.
73    pub fn require_width(
74        &self,
75        node: NodeId,
76        role: &'static str,
77    ) -> Result<usize, SLTNodeFactsError> {
78        self.width(node).ok_or_else(|| {
79            SLTNodeFactsError::new(
80                "ROOT.NODE_EXISTS",
81                node,
82                format!("{role} references missing root n{}", node.0),
83            )
84        })
85    }
86
87    /// Require a root and every node reachable from it to be lowerable to
88    /// nonzero-width executable IR.
89    pub fn require_lowerable(
90        &self,
91        node: NodeId,
92        role: &'static str,
93    ) -> Result<usize, SLTNodeFactsError> {
94        let width = self.require_width(node, role)?;
95        if !self.lowerable[node.0] {
96            let blocker = self.lowerability_blocker(node);
97            return Err(SLTNodeFactsError::new(
98                "ROOT.LOWERABLE_NON_ZERO",
99                blocker,
100                format!(
101                    "{role} root n{} reaches n{}, which has a zero executable width",
102                    node.0, blocker.0
103                ),
104            ));
105        }
106        Ok(width)
107    }
108
109    /// Find the first direct zero-width cause on the first non-lowerable child
110    /// path. This runs only for a rejected root and allocates no traversal
111    /// storage; canonical child IDs strictly decrease at every step.
112    fn lowerability_blocker(&self, mut node_id: NodeId) -> NodeId {
113        loop {
114            let Some(node) = self.arena.get_checked(node_id) else {
115                return node_id;
116            };
117            let direct_blocker = self.widths.get(node_id.0).copied() == Some(0)
118                || matches!(node, SLTNode::Concat(parts) if parts.iter().any(|(_, width)| *width == 0));
119            if direct_blocker {
120                return node_id;
121            }
122
123            let mut next = None;
124            try_for_each_child(node, |child| {
125                if next.is_none() && self.lowerable.get(child.0).copied() == Some(false) {
126                    next = Some(child);
127                }
128                Ok::<(), std::convert::Infallible>(())
129            })
130            .unwrap_or_else(|never| match never {});
131            let Some(child) = next else {
132                // The table is private and built atomically, so this can only
133                // describe an internal inconsistency. Keep the public failure
134                // fallible and attribute it to the last verified node.
135                return node_id;
136            };
137            node_id = child;
138        }
139    }
140
141    /// Return all widths in `NodeId` order.
142    #[cfg(test)]
143    pub fn widths(&self) -> &[usize] {
144        &self.widths
145    }
146}
147
148struct VerifiedNodeFacts {
149    widths: Vec<usize>,
150    lowerable: Vec<bool>,
151}
152
153/// Verify an untrusted serialized node list without first constructing an
154/// operational arena. The returned widths were recomputed from the node graph
155/// and can therefore initialize the arena cache directly.
156pub(super) fn verify_raw_nodes<A>(nodes: &[SLTNode<A>]) -> Result<Vec<usize>, SLTNodeFactsError>
157where
158    A: Hash + Eq + Clone,
159{
160    Ok(verify_nodes(nodes)?.widths)
161}
162
163/// Validate the local safety conditions required to append `node` and derive
164/// its construction-time width. Full semantic relations are intentionally
165/// checked only by [`verify_nodes`].
166pub(super) fn verify_append<A>(
167    node: &SLTNode<A>,
168    widths: &[usize],
169) -> Result<usize, SLTNodeFactsError>
170where
171    A: Hash + Eq + Clone,
172{
173    let node_id = NodeId(widths.len());
174    let child_width = |child: NodeId| {
175        widths.get(child.0).copied().ok_or_else(|| {
176            SLTNodeFactsError::new(
177                "GRAPH.CHILD_EXISTS",
178                node_id,
179                format!(
180                    "node n{} references missing child n{}; arena contains {} nodes",
181                    node_id.0,
182                    child.0,
183                    widths.len()
184                ),
185            )
186        })
187    };
188
189    match node {
190        SLTNode::Input { index, access, .. } => {
191            for entry in index {
192                child_width(entry.node)?;
193            }
194            checked_access_width(node_id, *access, "input")
195        }
196        SLTNode::Constant(_, _, width, _) => Ok(*width),
197        SLTNode::Binary(lhs, op, rhs) => Ok(node_rules::binary_result_width(
198            *op,
199            child_width(*lhs)?,
200            child_width(*rhs)?,
201        )),
202        SLTNode::Unary(op, inner) => Ok(node_rules::unary_width(*op, child_width(*inner)?)),
203        SLTNode::Capture { expr, .. } => child_width(*expr),
204        SLTNode::Mux {
205            cond,
206            then_expr,
207            else_expr,
208        } => {
209            child_width(*cond)?;
210            Ok(node_rules::mux_width(
211                child_width(*then_expr)?,
212                child_width(*else_expr)?,
213            ))
214        }
215        SLTNode::ForFold { result, .. } => {
216            try_for_each_child(node, |child| child_width(child).map(|_| ()))?;
217            match result {
218                crate::SLTForFoldResult::State(result) => {
219                    checked_access_width(node_id, result.access, "ForFold result")
220                }
221                crate::SLTForFoldResult::Transient { initial, update } => {
222                    let initial_width = child_width(*initial)?;
223                    let update_width = child_width(*update)?;
224                    if initial_width != update_width {
225                        return Err(SLTNodeFactsError::new(
226                            "FOR_FOLD.TRANSIENT_RESULT_WIDTH_MATCHES",
227                            node_id,
228                            format!(
229                                "ForFold transient result initial width {initial_width} does not equal update width {update_width}"
230                            ),
231                        ));
232                    }
233                    Ok(initial_width)
234                }
235            }
236        }
237        SLTNode::ForFoldGroup { states, .. } => {
238            try_for_each_child(node, |child| child_width(child).map(|_| ()))?;
239            packed_group_width(node_id, states.iter().map(|state| state.target.access))
240        }
241        SLTNode::Concat(parts) => {
242            let mut total = 0usize;
243            for &(child, part_width) in parts {
244                child_width(child)?;
245                total = node_rules::concat_width_add(total, part_width)
246                    .map_err(|error| rule_error(node_id, error))?;
247            }
248            Ok(total)
249        }
250        SLTNode::Slice { expr, access } => {
251            child_width(*expr)?;
252            checked_access_width(node_id, *access, "slice")
253        }
254    }
255}
256
257fn verify_nodes<A>(nodes: &[SLTNode<A>]) -> Result<VerifiedNodeFacts, SLTNodeFactsError>
258where
259    A: Hash + Eq + Clone,
260{
261    let node_count = nodes.len();
262
263    // An arena is a canonical append-only DAG: a node can only reference
264    // operands that were already allocated. Check every untrusted ID without
265    // dereferencing it before building any fact table.
266    for (node_index, node) in nodes.iter().enumerate() {
267        verify_child_ids(NodeId(node_index), node, node_count)?;
268    }
269
270    // Child facts are available by construction in NodeId order. This avoids
271    // reverse-edge storage, a Kahn worklist, and Option-sized fact slots.
272    // Vec<bool> keeps the persistent lowerability fact packed.
273    let allocation_node = NodeId(node_count.saturating_sub(1));
274    let mut widths = Vec::new();
275    widths.try_reserve_exact(node_count).map_err(|error| {
276        SLTNodeFactsError::new(
277            "FACTS.STORAGE_AVAILABLE",
278            allocation_node,
279            format!("cannot reserve widths for {node_count} nodes: {error}"),
280        )
281    })?;
282    let mut lowerable = Vec::new();
283    lowerable.try_reserve_exact(node_count).map_err(|error| {
284        SLTNodeFactsError::new(
285            "FACTS.STORAGE_AVAILABLE",
286            allocation_node,
287            format!("cannot reserve lowerability for {node_count} nodes: {error}"),
288        )
289    })?;
290    let mut unsafe_in_group = Vec::new();
291    unsafe_in_group
292        .try_reserve_exact(node_count)
293        .map_err(|error| {
294            SLTNodeFactsError::new(
295                "FACTS.STORAGE_AVAILABLE",
296                allocation_node,
297                format!("cannot reserve grouped-fold safety facts for {node_count} nodes: {error}"),
298            )
299        })?;
300    for (node_index, node) in nodes.iter().enumerate() {
301        let node_id = NodeId(node_index);
302        let width = compute_width(node_id, node, &widths)?;
303        let mut node_lowerable = node_rules::direct_lowerable(
304            width,
305            matches!(node, SLTNode::Concat(parts) if parts.iter().any(|(_, width)| *width == 0)),
306        );
307        // Legacy ForFold can emit runtime effects and may terminate through a
308        // stall/error path even when its explicit effect list is empty.  A
309        // grouped fold is a pure, total expression, so neither behavior may be
310        // hidden below one of its value children.
311        let mut node_unsafe_in_group = matches!(node, SLTNode::ForFold { .. });
312        try_for_each_child(node, |child| {
313            let Some(&child_lowerable) = lowerable.get(child.0) else {
314                return Err(SLTNodeFactsError::new(
315                    "FACTS.CHILD_LOWERABILITY_AVAILABLE",
316                    node_id,
317                    format!(
318                        "lowerability of child n{} was not available while evaluating n{}",
319                        child.0, node_id.0
320                    ),
321                ));
322            };
323            node_lowerable &= child_lowerable;
324            let Some(&child_unsafe) = unsafe_in_group.get(child.0) else {
325                return Err(SLTNodeFactsError::new(
326                    "FACTS.CHILD_EFFECT_AVAILABLE",
327                    node_id,
328                    format!(
329                        "group-safety fact of child n{} was not available while evaluating n{}",
330                        child.0, node_id.0
331                    ),
332                ));
333            };
334            node_unsafe_in_group |= child_unsafe;
335            Ok(())
336        })?;
337        if matches!(node, SLTNode::ForFoldGroup { .. }) && node_unsafe_in_group {
338            return Err(SLTNodeFactsError::new(
339                "FOR_FOLD_GROUP.CHILDREN_PURE_AND_TOTAL",
340                node_id,
341                "ForFoldGroup guard, initial, or update reaches a legacy ForFold that may emit effects or terminate with an error",
342            ));
343        }
344        widths.push(width);
345        lowerable.push(node_lowerable);
346        unsafe_in_group.push(node_unsafe_in_group);
347    }
348
349    Ok(VerifiedNodeFacts { widths, lowerable })
350}
351
352fn verify_child_ids<A>(
353    owner: NodeId,
354    node: &SLTNode<A>,
355    node_count: usize,
356) -> Result<(), SLTNodeFactsError>
357where
358    A: Hash + Eq + Clone,
359{
360    try_for_each_child(node, |child| {
361        if child.0 >= node_count {
362            return Err(SLTNodeFactsError::new(
363                "GRAPH.CHILD_EXISTS",
364                owner,
365                format!(
366                    "node n{} references missing child n{}; arena contains {node_count} nodes",
367                    owner.0, child.0
368                ),
369            ));
370        }
371        if child.0 >= owner.0 {
372            return Err(SLTNodeFactsError::new(
373                "GRAPH.CHILD_PRECEDES_OWNER",
374                owner,
375                format!(
376                    "node n{} references child n{}, which does not precede its owner",
377                    owner.0, child.0
378                ),
379            ));
380        }
381        Ok(())
382    })
383}
384
385/// A structured failure produced while verifying an SLT node graph.
386#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct SLTNodeFactsError {
388    pub invariant: &'static str,
389    pub node: NodeId,
390    pub message: String,
391}
392
393impl SLTNodeFactsError {
394    pub fn new(invariant: &'static str, node: NodeId, message: impl Into<String>) -> Self {
395        Self {
396            invariant,
397            node,
398            message: message.into(),
399        }
400    }
401}
402
403impl fmt::Display for SLTNodeFactsError {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        write!(
406            f,
407            "SLT node facts verify [{}] at n{}: {}",
408            self.invariant, self.node.0, self.message
409        )
410    }
411}
412
413impl std::error::Error for SLTNodeFactsError {}
414
415fn compute_width<A>(
416    node_id: NodeId,
417    node: &SLTNode<A>,
418    widths: &[usize],
419) -> Result<usize, SLTNodeFactsError>
420where
421    A: Hash + Eq + Clone,
422{
423    let child_width = |child: NodeId| {
424        widths.get(child.0).copied().ok_or_else(|| {
425            SLTNodeFactsError::new(
426                "FACTS.CHILD_WIDTH_AVAILABLE",
427                node_id,
428                format!(
429                    "width of child n{} was not available while evaluating n{}",
430                    child.0, node_id.0
431                ),
432            )
433        })
434    };
435
436    match node {
437        SLTNode::Input { access, .. } => checked_access_width(node_id, *access, "input"),
438        SLTNode::Constant(value, mask, width, _) => node_rules::constant_width(value, mask, *width)
439            .map_err(|error| rule_error(node_id, error)),
440        SLTNode::Binary(lhs, op, rhs) => {
441            let lhs_width = child_width(*lhs)?;
442            let rhs_width = child_width(*rhs)?;
443            node_rules::binary_width(*op, lhs_width, rhs_width)
444                .map_err(|error| rule_error(node_id, error))
445        }
446        SLTNode::Unary(op, inner) => Ok(node_rules::unary_width(*op, child_width(*inner)?)),
447        SLTNode::Capture { expr, .. } => child_width(*expr),
448        SLTNode::Mux {
449            then_expr,
450            else_expr,
451            ..
452        } => Ok(node_rules::mux_width(
453            child_width(*then_expr)?,
454            child_width(*else_expr)?,
455        )),
456        SLTNode::ForFold {
457            loop_var: _,
458            loop_width,
459            loop_signed,
460            start,
461            end,
462            inclusive,
463            step_op,
464            reverse,
465            result,
466            initials,
467            updates,
468            effects,
469            continue_cond,
470            ..
471        } => {
472            if *loop_width == 0 {
473                return Err(SLTNodeFactsError::new(
474                    "FOR_FOLD.LOOP_WIDTH_NON_ZERO",
475                    node_id,
476                    "ForFold loop width is zero",
477                ));
478            }
479            if *reverse && *step_op != SLTStepOp::Add {
480                return Err(SLTNodeFactsError::new(
481                    "FOR_FOLD.REVERSE_STEP_IS_ADD",
482                    node_id,
483                    format!("reverse ForFold ignores unsupported {step_op:?} step semantics"),
484                ));
485            }
486            if initials.len() != updates.len() {
487                return Err(SLTNodeFactsError::new(
488                    "FOR_FOLD.STATE_ARITY_MATCHES",
489                    node_id,
490                    format!(
491                        "ForFold has {} initial states but {} updates",
492                        initials.len(),
493                        updates.len()
494                    ),
495                ));
496            }
497
498            let require_nonzero_child = |child: NodeId, role: &str| {
499                let width = child_width(child)?;
500                if width == 0 {
501                    return Err(SLTNodeFactsError::new(
502                        "FOR_FOLD.OPERAND_NON_ZERO",
503                        node_id,
504                        format!("{role} n{} has zero width", child.0),
505                    ));
506                }
507                Ok(width)
508            };
509
510            let mut counter_width = *loop_width;
511            for (role, bound) in [("start", start), ("end", end)] {
512                let width = match bound {
513                    SLTLoopBound::Const(value) => {
514                        (usize::BITS as usize - value.leading_zeros() as usize).max(1)
515                    }
516                    SLTLoopBound::Expr(child) => require_nonzero_child(*child, role)?,
517                };
518                counter_width = counter_width.max(width);
519            }
520            if *inclusive && !*loop_signed && counter_width.checked_add(1).is_none() {
521                return Err(SLTNodeFactsError::new(
522                    "FOR_FOLD.INCLUSIVE_WIDTH_REPRESENTABLE",
523                    node_id,
524                    format!(
525                        "inclusive unsigned ForFold cannot widen counter width {counter_width}"
526                    ),
527                ));
528            }
529
530            let mut target_accesses: crate::HashMap<A, Vec<(BitAccess, usize)>> =
531                crate::HashMap::default();
532            for (index, (initial, update)) in initials.iter().zip(updates).enumerate() {
533                if initial.target != update.target {
534                    return Err(SLTNodeFactsError::new(
535                        "FOR_FOLD.POSITIONAL_TARGET_MATCHES",
536                        node_id,
537                        format!("initial and update target differ at state position {index}"),
538                    ));
539                }
540                checked_access_width(node_id, update.target.access, "ForFold state target")?;
541                require_nonzero_child(initial.expr, "ForFold initial state")?;
542                require_nonzero_child(update.expr, "ForFold update state")?;
543                target_accesses
544                    .entry(update.target.id.clone())
545                    .or_default()
546                    .push((update.target.access, index));
547            }
548            for accesses in target_accesses.values_mut() {
549                accesses.sort_unstable_by_key(|(access, _)| (access.lsb, access.msb));
550                for pair in accesses.windows(2) {
551                    let (previous, previous_index) = pair[0];
552                    let (current, current_index) = pair[1];
553                    if previous.msb >= current.lsb {
554                        return Err(SLTNodeFactsError::new(
555                            "FOR_FOLD.STATE_TARGETS_DISJOINT",
556                            node_id,
557                            format!(
558                                "state targets at positions {previous_index} and {current_index} overlap"
559                            ),
560                        ));
561                    }
562                }
563            }
564
565            let result_width = match result {
566                crate::SLTForFoldResult::State(result) => {
567                    let width = checked_access_width(node_id, result.access, "ForFold result")?;
568                    let result_count = updates
569                        .iter()
570                        .filter(|update| update.target == *result)
571                        .count();
572                    if result_count != 1 {
573                        return Err(SLTNodeFactsError::new(
574                            "FOR_FOLD.RESULT_TARGET_UNIQUE",
575                            node_id,
576                            format!(
577                                "ForFold result occurs {result_count} times in its update targets"
578                            ),
579                        ));
580                    }
581                    width
582                }
583                crate::SLTForFoldResult::Transient { initial, update } => {
584                    let initial_width =
585                        require_nonzero_child(*initial, "ForFold transient initial")?;
586                    let update_width = require_nonzero_child(*update, "ForFold transient update")?;
587                    if initial_width != update_width {
588                        return Err(SLTNodeFactsError::new(
589                            "FOR_FOLD.TRANSIENT_RESULT_WIDTH_MATCHES",
590                            node_id,
591                            format!(
592                                "ForFold transient result initial width {initial_width} does not equal update width {update_width}"
593                            ),
594                        ));
595                    }
596                    initial_width
597                }
598            };
599
600            for effect in effects {
601                match effect {
602                    crate::SLTForEffect::Event { guard, args, .. } => {
603                        if let Some(guard) = guard {
604                            require_nonzero_child(*guard, "ForFold effect guard")?;
605                        }
606                        for &arg in args {
607                            require_nonzero_child(arg, "ForFold effect argument")?;
608                        }
609                    }
610                    crate::SLTForEffect::Runner(runner) => {
611                        require_nonzero_child(*runner, "ForFold effect runner")?;
612                    }
613                }
614            }
615            require_nonzero_child(*continue_cond, "ForFold continue condition")?;
616            Ok(result_width)
617        }
618        SLTNode::ForFoldGroup {
619            loop_var,
620            loop_width,
621            loop_signed,
622            start,
623            step,
624            trip_count,
625            entry_guard,
626            states,
627            ..
628        } => {
629            if *loop_width == 0 {
630                return Err(SLTNodeFactsError::new(
631                    "FOR_FOLD_GROUP.LOOP_WIDTH_NON_ZERO",
632                    node_id,
633                    "ForFoldGroup loop width is zero",
634                ));
635            }
636            if *trip_count == 0 {
637                return Err(SLTNodeFactsError::new(
638                    "FOR_FOLD_GROUP.TRIP_COUNT_NON_ZERO",
639                    node_id,
640                    "ForFoldGroup trip count is zero",
641                ));
642            }
643            if states.is_empty() {
644                return Err(SLTNodeFactsError::new(
645                    "FOR_FOLD_GROUP.STATE_NON_EMPTY",
646                    node_id,
647                    "ForFoldGroup has no loop-carried states",
648                ));
649            }
650
651            let guard_width = child_width(*entry_guard)?;
652            if guard_width != 1 {
653                return Err(SLTNodeFactsError::new(
654                    "FOR_FOLD_GROUP.ENTRY_GUARD_ONE_BIT",
655                    node_id,
656                    format!(
657                        "ForFoldGroup entry guard n{} has width {guard_width}, expected 1",
658                        entry_guard.0,
659                    ),
660                ));
661            }
662
663            let last_iteration = start + step * num_bigint::BigInt::from(*trip_count - 1);
664            if !integer_fits_loop_counter(start, *loop_width, *loop_signed)
665                || !integer_fits_loop_counter(&last_iteration, *loop_width, *loop_signed)
666            {
667                return Err(SLTNodeFactsError::new(
668                    "FOR_FOLD_GROUP.ITERATION_ARITHMETIC_REPRESENTABLE",
669                    node_id,
670                    format!(
671                        "ForFoldGroup iteration range {start}..{last_iteration} does not fit its {}-bit {} loop counter",
672                        loop_width,
673                        if *loop_signed { "signed" } else { "unsigned" },
674                    ),
675                ));
676            }
677
678            let mut exact_targets = crate::HashSet::default();
679            let mut target_accesses: crate::HashMap<A, Vec<(BitAccess, usize)>> =
680                crate::HashMap::default();
681            let mut packed_width = 0usize;
682            for (index, state) in states.iter().enumerate() {
683                if state.target.id == *loop_var {
684                    return Err(SLTNodeFactsError::new(
685                        "FOR_FOLD_GROUP.LOOP_VARIABLE_DISJOINT_FROM_STATE_TARGETS",
686                        node_id,
687                        format!(
688                            "ForFoldGroup state target at position {index} aliases its loop variable"
689                        ),
690                    ));
691                }
692                if !exact_targets.insert(state.target.clone()) {
693                    return Err(SLTNodeFactsError::new(
694                        "FOR_FOLD_GROUP.STATE_TARGETS_UNIQUE",
695                        node_id,
696                        format!("ForFoldGroup repeats state target at position {index}"),
697                    ));
698                }
699                let target_width = checked_access_width(
700                    node_id,
701                    state.target.access,
702                    "ForFoldGroup state target",
703                )?;
704                packed_width = packed_width.checked_add(target_width).ok_or_else(|| {
705                    SLTNodeFactsError::new(
706                        "FOR_FOLD_GROUP.PACKED_WIDTH_REPRESENTABLE",
707                        node_id,
708                        format!(
709                            "ForFoldGroup packed width overflows usize while adding state {index} width {target_width}"
710                        ),
711                    )
712                })?;
713                let initial_width = child_width(state.initial)?;
714                let update_width = child_width(state.update)?;
715                if initial_width != target_width || update_width != target_width {
716                    return Err(SLTNodeFactsError::new(
717                        "FOR_FOLD_GROUP.STATE_WIDTHS_MATCH",
718                        node_id,
719                        format!(
720                            "ForFoldGroup state {index} target width {target_width}, initial n{} width {initial_width}, and update n{} width {update_width} do not match",
721                            state.initial.0, state.update.0,
722                        ),
723                    ));
724                }
725                target_accesses
726                    .entry(state.target.id.clone())
727                    .or_default()
728                    .push((state.target.access, index));
729            }
730            for accesses in target_accesses.values_mut() {
731                accesses.sort_unstable_by_key(|(access, _)| (access.lsb, access.msb));
732                for pair in accesses.windows(2) {
733                    let (previous, previous_index) = pair[0];
734                    let (current, current_index) = pair[1];
735                    if previous.msb >= current.lsb {
736                        return Err(SLTNodeFactsError::new(
737                            "FOR_FOLD_GROUP.STATE_TARGETS_DISJOINT",
738                            node_id,
739                            format!(
740                                "ForFoldGroup state targets at positions {previous_index} and {current_index} overlap"
741                            ),
742                        ));
743                    }
744                }
745            }
746            Ok(packed_width)
747        }
748        SLTNode::Concat(parts) => node_rules::concat_width(parts.iter().map(|(_, width)| *width))
749            .map_err(|error| rule_error(node_id, error)),
750        SLTNode::Slice { expr, access } => {
751            let expression_width = child_width(*expr)?;
752            node_rules::slice_width(*access, expression_width, format_args!("n{}", expr.0))
753                .map_err(|error| rule_error(node_id, error))
754        }
755    }
756}
757
758fn checked_access_width(
759    node: NodeId,
760    access: BitAccess,
761    role: &str,
762) -> Result<usize, SLTNodeFactsError> {
763    node_rules::access_width(access, role).map_err(|error| rule_error(node, error))
764}
765
766fn packed_group_width(
767    node: NodeId,
768    accesses: impl IntoIterator<Item = BitAccess>,
769) -> Result<usize, SLTNodeFactsError> {
770    accesses
771        .into_iter()
772        .enumerate()
773        .try_fold(0usize, |total, (index, access)| {
774            let width = checked_access_width(node, access, "ForFoldGroup state target")?;
775            total.checked_add(width).ok_or_else(|| {
776                SLTNodeFactsError::new(
777                    "FOR_FOLD_GROUP.PACKED_WIDTH_REPRESENTABLE",
778                    node,
779                    format!(
780                        "ForFoldGroup packed width overflows usize while adding state {index} width {width}"
781                    ),
782                )
783            })
784        })
785}
786
787/// Test whether an integer can be represented without truncation by the loop
788/// counter type.  This uses the already allocated magnitude instead of
789/// constructing a `1 << width` bound, so hostile oversized widths cannot force
790/// an equally oversized verifier allocation.
791fn integer_fits_loop_counter(value: &num_bigint::BigInt, width: usize, signed: bool) -> bool {
792    use num_bigint::Sign;
793
794    if width == 0 {
795        return false;
796    }
797    let width = u64::try_from(width).unwrap_or(u64::MAX);
798    let bits = value.magnitude().bits();
799    match (signed, value.sign()) {
800        (false, Sign::Minus) => false,
801        (false, _) => bits <= width,
802        (true, Sign::Minus) => {
803            bits < width
804                || (bits == width
805                    && value.magnitude().trailing_zeros() == Some(width.saturating_sub(1)))
806        }
807        (true, Sign::NoSign | Sign::Plus) => bits < width,
808    }
809}
810
811fn rule_error(node: NodeId, error: node_rules::NodeRuleError) -> SLTNodeFactsError {
812    SLTNodeFactsError::new(error.invariant, node, error.message)
813}
814
815fn try_for_each_child<A, E>(
816    node: &SLTNode<A>,
817    mut visit: impl FnMut(NodeId) -> Result<(), E>,
818) -> Result<(), E>
819where
820    A: Hash + Eq + Clone,
821{
822    match node {
823        SLTNode::Input { index, .. } => {
824            for entry in index {
825                visit(entry.node)?;
826            }
827        }
828        SLTNode::Constant(..) => {}
829        SLTNode::Binary(lhs, _, rhs) => {
830            visit(*lhs)?;
831            visit(*rhs)?;
832        }
833        SLTNode::Unary(_, inner) => visit(*inner)?,
834        SLTNode::Capture { expr, .. } => visit(*expr)?,
835        SLTNode::Mux {
836            cond,
837            then_expr,
838            else_expr,
839        } => {
840            visit(*cond)?;
841            visit(*then_expr)?;
842            visit(*else_expr)?;
843        }
844        SLTNode::ForFold {
845            start,
846            end,
847            result,
848            initials,
849            updates,
850            effects,
851            continue_cond,
852            ..
853        } => {
854            if let SLTLoopBound::Expr(node) = start {
855                visit(*node)?;
856            }
857            if let SLTLoopBound::Expr(node) = end {
858                visit(*node)?;
859            }
860            if let crate::SLTForFoldResult::Transient { initial, update } = result {
861                visit(*initial)?;
862                visit(*update)?;
863            }
864            for initial in initials {
865                visit(initial.expr)?;
866            }
867            for update in updates {
868                visit(update.expr)?;
869            }
870            for effect in effects {
871                match effect {
872                    crate::SLTForEffect::Event { guard, args, .. } => {
873                        if let Some(guard) = guard {
874                            visit(*guard)?;
875                        }
876                        for &arg in args {
877                            visit(arg)?;
878                        }
879                    }
880                    crate::SLTForEffect::Runner(runner) => visit(*runner)?,
881                }
882            }
883            visit(*continue_cond)?;
884        }
885        SLTNode::ForFoldGroup {
886            entry_guard,
887            states,
888            ..
889        } => {
890            visit(*entry_guard)?;
891            for state in states {
892                visit(state.initial)?;
893                visit(state.update)?;
894            }
895        }
896        SLTNode::Concat(parts) => {
897            for &(part, _) in parts {
898                visit(part)?;
899            }
900        }
901        SLTNode::Slice { expr, .. } => visit(*expr)?,
902    }
903    Ok(())
904}
905
906#[cfg(test)]
907mod tests {
908    use num_bigint::{BigInt, BigUint};
909
910    use celox_design::{BinaryOp, UnaryOp, VarAtomBase};
911
912    use super::*;
913    use crate::node::{
914        SLTForEffect, SLTForFoldGroupState, SLTForFoldResult, SLTForUpdate, SLTStepOp,
915    };
916
917    fn arena(nodes: Vec<SLTNode<u32>>) -> SLTNodeArena<u32> {
918        SLTNodeArena::try_from_nodes(nodes).expect("test node graph must verify")
919    }
920
921    fn raw_error(nodes: Vec<SLTNode<u32>>) -> SLTNodeFactsError {
922        SLTNodeArena::try_from_nodes(nodes).expect_err("raw node graph must fail verification")
923    }
924
925    fn constant(width: usize) -> SLTNode<u32> {
926        SLTNode::Constant(BigUint::from(0u8), BigUint::from(0u8), width, false)
927    }
928
929    fn valid_for_fold() -> SLTNode<u32> {
930        let target = VarAtomBase::new(2, 0, 7);
931        SLTNode::ForFold {
932            loop_var: 1,
933            loop_width: 8,
934            loop_signed: false,
935            start: SLTLoopBound::Const(0),
936            end: SLTLoopBound::Const(1),
937            inclusive: false,
938            step: 1,
939            step_op: SLTStepOp::Add,
940            reverse: false,
941            result: SLTForFoldResult::State(target),
942            initials: vec![SLTForUpdate {
943                target,
944                expr: NodeId(0),
945            }],
946            updates: vec![SLTForUpdate {
947                target,
948                expr: NodeId(0),
949            }],
950            effects: Vec::new(),
951            continue_cond: NodeId(1),
952        }
953    }
954
955    fn verify_for_fold(node: SLTNode<u32>) -> Result<(), SLTNodeFactsError> {
956        SLTNodeArena::try_from_nodes(vec![constant(8), constant(1), node]).map(|_| ())
957    }
958
959    fn valid_for_fold_group() -> SLTNode<u32> {
960        SLTNode::ForFoldGroup {
961            loop_var: 1,
962            loop_width: 8,
963            loop_signed: false,
964            start: BigInt::from(0),
965            step: BigInt::from(1),
966            trip_count: 4,
967            entry_guard: NodeId(1),
968            states: vec![SLTForFoldGroupState {
969                target: VarAtomBase::new(2, 0, 7),
970                initial: NodeId(0),
971                update: NodeId(0),
972            }],
973        }
974    }
975
976    fn verify_for_fold_group(node: SLTNode<u32>) -> Result<SLTNodeArena<u32>, SLTNodeFactsError> {
977        SLTNodeArena::try_from_nodes(vec![constant(8), constant(1), node])
978    }
979
980    #[test]
981    fn computes_declared_width_rules() {
982        let arena = arena(vec![
983            constant(0),                                          // n0
984            constant(4),                                          // n1
985            constant(9),                                          // n2
986            SLTNode::Binary(NodeId(1), BinaryOp::Add, NodeId(2)), // n3 = 9
987            SLTNode::Binary(NodeId(1), BinaryOp::Shl, NodeId(2)), // n4 = 4
988            SLTNode::Binary(NodeId(1), BinaryOp::Eq, NodeId(2)),  // n5 = 1
989            SLTNode::Unary(UnaryOp::LogicNot, NodeId(2)),         // n6 = 1
990            SLTNode::Mux {
991                cond: NodeId(0),
992                then_expr: NodeId(1),
993                else_expr: NodeId(2),
994            }, // n7 = 9
995            SLTNode::Concat(vec![(NodeId(1), 2), (NodeId(2), 7)]), // n8 = 9
996            SLTNode::Slice {
997                expr: NodeId(2),
998                access: BitAccess { lsb: 2, msb: 5 },
999            }, // n9 = 4
1000            SLTNode::Binary(NodeId(1), BinaryOp::EqWildcard, NodeId(1)), // n10 = 1
1001            SLTNode::Unary(UnaryOp::PopCount, NodeId(2)),         // n11 = ceil(log2(9 + 1)) = 4
1002            SLTNode::Unary(UnaryOp::CountLeadingZeros, NodeId(1)), // n12 = 3
1003            SLTNode::Unary(UnaryOp::CountTrailingZeros, NodeId(0)), // n13 = 0
1004        ]);
1005
1006        let facts = SLTNodeFacts::verify(&arena).expect("well-formed arena must verify");
1007        assert_eq!(facts.widths(), &[0, 4, 9, 9, 4, 1, 1, 9, 9, 4, 1, 4, 3, 0]);
1008        assert_eq!(facts.width(NodeId(14)), None);
1009    }
1010
1011    #[test]
1012    fn bit_count_width_handles_power_of_two_and_usize_limit() {
1013        let arena = arena(vec![
1014            constant(8),
1015            SLTNode::Unary(UnaryOp::PopCount, NodeId(0)),
1016            constant(usize::MAX),
1017            SLTNode::Unary(UnaryOp::CountLeadingZeros, NodeId(2)),
1018            SLTNode::Unary(UnaryOp::CountTrailingZeros, NodeId(2)),
1019        ]);
1020        let facts = SLTNodeFacts::verify(&arena).expect("well-formed arena must verify");
1021        assert_eq!(facts.width(NodeId(1)), Some(4));
1022        assert_eq!(facts.width(NodeId(3)), Some(usize::BITS as usize));
1023        assert_eq!(facts.width(NodeId(4)), Some(usize::BITS as usize));
1024    }
1025
1026    #[test]
1027    fn rejects_missing_child_before_graph_traversal() {
1028        let error = raw_error(vec![SLTNode::Unary(UnaryOp::Ident, NodeId(7))]);
1029        assert_eq!(error.invariant, "GRAPH.CHILD_EXISTS");
1030        assert_eq!(error.node, NodeId(0));
1031        assert!(error.message.contains("n7"));
1032    }
1033
1034    #[test]
1035    fn rejects_dependency_cycle_as_noncanonical_forward_edge() {
1036        let error = raw_error(vec![
1037            SLTNode::Unary(UnaryOp::Ident, NodeId(1)),
1038            SLTNode::Unary(UnaryOp::Ident, NodeId(0)),
1039        ]);
1040        assert_eq!(error.invariant, "GRAPH.CHILD_PRECEDES_OWNER");
1041        assert_eq!(error.node, NodeId(0));
1042    }
1043
1044    #[test]
1045    fn rejects_acyclic_forward_reference() {
1046        let error = raw_error(vec![SLTNode::Unary(UnaryOp::Ident, NodeId(1)), constant(8)]);
1047        assert_eq!(error.invariant, "GRAPH.CHILD_PRECEDES_OWNER");
1048        assert_eq!(error.node, NodeId(0));
1049        assert!(error.message.contains("child n1"));
1050    }
1051
1052    #[test]
1053    fn rejects_self_reference() {
1054        let error = raw_error(vec![SLTNode::Unary(UnaryOp::Ident, NodeId(0))]);
1055        assert_eq!(error.invariant, "GRAPH.CHILD_PRECEDES_OWNER");
1056        assert_eq!(error.node, NodeId(0));
1057    }
1058
1059    #[test]
1060    fn rejects_malformed_and_overflowing_accesses() {
1061        let error = raw_error(vec![SLTNode::Input {
1062            variable: 1,
1063            signed: false,
1064            index: Vec::new(),
1065            access: BitAccess { lsb: 5, msb: 4 },
1066        }]);
1067        assert_eq!(error.invariant, "WIDTH.ACCESS_ORDERED");
1068
1069        let error = raw_error(vec![SLTNode::Input {
1070            variable: 1,
1071            signed: false,
1072            index: Vec::new(),
1073            access: BitAccess {
1074                lsb: 0,
1075                msb: usize::MAX,
1076            },
1077        }]);
1078        assert_eq!(error.invariant, "WIDTH.ACCESS_REPRESENTABLE");
1079    }
1080
1081    #[test]
1082    fn rejects_slice_outside_child_width() {
1083        let error = raw_error(vec![
1084            constant(4),
1085            SLTNode::Slice {
1086                expr: NodeId(0),
1087                access: BitAccess { lsb: 1, msb: 4 },
1088            },
1089        ]);
1090        assert_eq!(error.invariant, "WIDTH.SLICE_IN_BOUNDS");
1091    }
1092
1093    #[test]
1094    fn rejects_concat_width_overflow() {
1095        let error = raw_error(vec![
1096            constant(0),
1097            SLTNode::Concat(vec![(NodeId(0), usize::MAX), (NodeId(0), 1)]),
1098        ]);
1099        assert_eq!(error.invariant, "WIDTH.CONCAT_REPRESENTABLE");
1100    }
1101
1102    #[test]
1103    fn rejects_mismatched_wildcard_operand_widths() {
1104        for op in [BinaryOp::EqWildcard, BinaryOp::NeWildcard] {
1105            let error = raw_error(vec![
1106                constant(4),
1107                constant(8),
1108                SLTNode::Binary(NodeId(0), op, NodeId(1)),
1109            ]);
1110            assert_eq!(error.invariant, "WIDTH.WILDCARD_OPERANDS_MATCH");
1111        }
1112    }
1113
1114    #[test]
1115    fn rejects_constant_payload_and_mask_outside_declared_width() {
1116        let payload_error = raw_error(vec![SLTNode::Constant(
1117            BigUint::from(0x10u8),
1118            BigUint::from(0u8),
1119            4,
1120            false,
1121        )]);
1122        assert_eq!(payload_error.invariant, "CONSTANT.VALUE_FITS_WIDTH");
1123
1124        let mask_error = raw_error(vec![SLTNode::Constant(
1125            BigUint::from(0u8),
1126            BigUint::from(0x10u8),
1127            4,
1128            false,
1129        )]);
1130        assert_eq!(mask_error.invariant, "CONSTANT.MASK_FITS_WIDTH");
1131    }
1132
1133    #[test]
1134    fn validates_complete_for_fold_contract() {
1135        verify_for_fold(valid_for_fold()).expect("complete ForFold must verify");
1136
1137        let mut node = valid_for_fold();
1138        let SLTNode::ForFold { loop_width, .. } = &mut node else {
1139            unreachable!()
1140        };
1141        *loop_width = 0;
1142        assert_eq!(
1143            verify_for_fold(node).unwrap_err().invariant,
1144            "FOR_FOLD.LOOP_WIDTH_NON_ZERO"
1145        );
1146
1147        let mut node = valid_for_fold();
1148        let SLTNode::ForFold { updates, .. } = &mut node else {
1149            unreachable!()
1150        };
1151        updates.clear();
1152        assert_eq!(
1153            verify_for_fold(node).unwrap_err().invariant,
1154            "FOR_FOLD.STATE_ARITY_MATCHES"
1155        );
1156
1157        let mut node = valid_for_fold();
1158        let SLTNode::ForFold { updates, .. } = &mut node else {
1159            unreachable!()
1160        };
1161        updates[0].target = VarAtomBase::new(3, 0, 7);
1162        assert_eq!(
1163            verify_for_fold(node).unwrap_err().invariant,
1164            "FOR_FOLD.POSITIONAL_TARGET_MATCHES"
1165        );
1166
1167        let mut node = valid_for_fold();
1168        let SLTNode::ForFold { result, .. } = &mut node else {
1169            unreachable!()
1170        };
1171        *result = SLTForFoldResult::State(VarAtomBase::new(3, 0, 7));
1172        assert_eq!(
1173            verify_for_fold(node).unwrap_err().invariant,
1174            "FOR_FOLD.RESULT_TARGET_UNIQUE"
1175        );
1176
1177        let mut transient = valid_for_fold();
1178        let SLTNode::ForFold { result, .. } = &mut transient else {
1179            unreachable!()
1180        };
1181        *result = SLTForFoldResult::Transient {
1182            initial: NodeId(1),
1183            update: NodeId(1),
1184        };
1185        verify_for_fold(transient).expect("transient ForFold result must verify");
1186
1187        let mut mismatched_transient = valid_for_fold();
1188        let SLTNode::ForFold { result, .. } = &mut mismatched_transient else {
1189            unreachable!()
1190        };
1191        *result = SLTForFoldResult::Transient {
1192            initial: NodeId(0),
1193            update: NodeId(1),
1194        };
1195        assert_eq!(
1196            verify_for_fold(mismatched_transient).unwrap_err().invariant,
1197            "FOR_FOLD.TRANSIENT_RESULT_WIDTH_MATCHES"
1198        );
1199
1200        let mut node = valid_for_fold();
1201        let SLTNode::ForFold {
1202            reverse, step_op, ..
1203        } = &mut node
1204        else {
1205            unreachable!()
1206        };
1207        *reverse = true;
1208        *step_op = SLTStepOp::Mul;
1209        assert_eq!(
1210            verify_for_fold(node).unwrap_err().invariant,
1211            "FOR_FOLD.REVERSE_STEP_IS_ADD"
1212        );
1213
1214        let mut node = valid_for_fold();
1215        let SLTNode::ForFold { continue_cond, .. } = &mut node else {
1216            unreachable!()
1217        };
1218        *continue_cond = NodeId(2);
1219        let error = raw_error(vec![constant(8), constant(1), constant(0), node]);
1220        assert_eq!(error.invariant, "FOR_FOLD.OPERAND_NON_ZERO");
1221
1222        let mut node = valid_for_fold();
1223        let SLTNode::ForFold { effects, .. } = &mut node else {
1224            unreachable!()
1225        };
1226        effects.push(SLTForEffect::Event {
1227            site_id: 0,
1228            guard: Some(NodeId(2)),
1229            emit_on_true: true,
1230            args: Vec::new(),
1231            fatal_error_code: None,
1232        });
1233        let error = raw_error(vec![constant(8), constant(1), constant(0), node]);
1234        assert_eq!(error.invariant, "FOR_FOLD.OPERAND_NON_ZERO");
1235    }
1236
1237    #[test]
1238    fn validates_complete_for_fold_group_contract() {
1239        let arena = verify_for_fold_group(valid_for_fold_group())
1240            .expect("complete ForFoldGroup must verify");
1241        assert_eq!(
1242            SLTNodeFacts::verify(&arena).unwrap().width(NodeId(2)),
1243            Some(8)
1244        );
1245
1246        let mut node = valid_for_fold_group();
1247        let SLTNode::ForFoldGroup { loop_width, .. } = &mut node else {
1248            unreachable!()
1249        };
1250        *loop_width = 0;
1251        assert_eq!(
1252            verify_for_fold_group(node).unwrap_err().invariant,
1253            "FOR_FOLD_GROUP.LOOP_WIDTH_NON_ZERO"
1254        );
1255
1256        let mut node = valid_for_fold_group();
1257        let SLTNode::ForFoldGroup { trip_count, .. } = &mut node else {
1258            unreachable!()
1259        };
1260        *trip_count = 0;
1261        assert_eq!(
1262            verify_for_fold_group(node).unwrap_err().invariant,
1263            "FOR_FOLD_GROUP.TRIP_COUNT_NON_ZERO"
1264        );
1265
1266        let mut node = valid_for_fold_group();
1267        let SLTNode::ForFoldGroup { states, .. } = &mut node else {
1268            unreachable!()
1269        };
1270        states.clear();
1271        assert_eq!(
1272            verify_for_fold_group(node).unwrap_err().invariant,
1273            "FOR_FOLD_GROUP.STATE_NON_EMPTY"
1274        );
1275
1276        let mut node = valid_for_fold_group();
1277        let SLTNode::ForFoldGroup { entry_guard, .. } = &mut node else {
1278            unreachable!()
1279        };
1280        *entry_guard = NodeId(0);
1281        assert_eq!(
1282            verify_for_fold_group(node).unwrap_err().invariant,
1283            "FOR_FOLD_GROUP.ENTRY_GUARD_ONE_BIT"
1284        );
1285
1286        let mut node = valid_for_fold_group();
1287        let SLTNode::ForFoldGroup { states, .. } = &mut node else {
1288            unreachable!()
1289        };
1290        states[0].update = NodeId(1);
1291        assert_eq!(
1292            verify_for_fold_group(node).unwrap_err().invariant,
1293            "FOR_FOLD_GROUP.STATE_WIDTHS_MATCH"
1294        );
1295    }
1296
1297    #[test]
1298    fn rejects_for_fold_group_loop_state_alias_and_duplicate_or_overlapping_targets() {
1299        let mut node = valid_for_fold_group();
1300        let SLTNode::ForFoldGroup { states, .. } = &mut node else {
1301            unreachable!()
1302        };
1303        states[0].target.id = 1;
1304        assert_eq!(
1305            verify_for_fold_group(node).unwrap_err().invariant,
1306            "FOR_FOLD_GROUP.LOOP_VARIABLE_DISJOINT_FROM_STATE_TARGETS"
1307        );
1308
1309        let mut node = valid_for_fold_group();
1310        let SLTNode::ForFoldGroup { states, .. } = &mut node else {
1311            unreachable!()
1312        };
1313        states.push(states[0].clone());
1314        assert_eq!(
1315            verify_for_fold_group(node).unwrap_err().invariant,
1316            "FOR_FOLD_GROUP.STATE_TARGETS_UNIQUE"
1317        );
1318
1319        let mut node = valid_for_fold_group();
1320        let SLTNode::ForFoldGroup { states, .. } = &mut node else {
1321            unreachable!()
1322        };
1323        states.push(SLTForFoldGroupState {
1324            target: VarAtomBase::new(2, 4, 11),
1325            initial: NodeId(0),
1326            update: NodeId(0),
1327        });
1328        assert_eq!(
1329            verify_for_fold_group(node).unwrap_err().invariant,
1330            "FOR_FOLD_GROUP.STATE_TARGETS_DISJOINT"
1331        );
1332    }
1333
1334    #[test]
1335    fn checks_for_fold_group_iteration_counter_range() {
1336        let mut node = valid_for_fold_group();
1337        let SLTNode::ForFoldGroup {
1338            start,
1339            step,
1340            trip_count,
1341            ..
1342        } = &mut node
1343        else {
1344            unreachable!()
1345        };
1346        *start = BigInt::from(250);
1347        *step = BigInt::from(3);
1348        *trip_count = 3;
1349        assert_eq!(
1350            verify_for_fold_group(node).unwrap_err().invariant,
1351            "FOR_FOLD_GROUP.ITERATION_ARITHMETIC_REPRESENTABLE"
1352        );
1353
1354        let mut node = valid_for_fold_group();
1355        let SLTNode::ForFoldGroup {
1356            loop_signed,
1357            start,
1358            trip_count,
1359            ..
1360        } = &mut node
1361        else {
1362            unreachable!()
1363        };
1364        *loop_signed = true;
1365        *start = BigInt::from(-128);
1366        *trip_count = 256;
1367        verify_for_fold_group(node).expect("signed 8-bit endpoints -128 and 127 must fit");
1368
1369        let mut node = valid_for_fold_group();
1370        let SLTNode::ForFoldGroup {
1371            loop_signed, start, ..
1372        } = &mut node
1373        else {
1374            unreachable!()
1375        };
1376        *loop_signed = true;
1377        *start = BigInt::from(-129);
1378        assert_eq!(
1379            verify_for_fold_group(node).unwrap_err().invariant,
1380            "FOR_FOLD_GROUP.ITERATION_ARITHMETIC_REPRESENTABLE"
1381        );
1382    }
1383
1384    #[test]
1385    fn rejects_for_fold_group_packed_width_overflow() {
1386        let huge = BitAccess::new(0, usize::MAX - 1);
1387        let node = SLTNode::ForFoldGroup {
1388            loop_var: 1,
1389            loop_width: 8,
1390            loop_signed: false,
1391            start: BigInt::from(0),
1392            step: BigInt::from(1),
1393            trip_count: 1,
1394            entry_guard: NodeId(0),
1395            states: vec![
1396                SLTForFoldGroupState {
1397                    target: VarAtomBase::new(2, huge.lsb, huge.msb),
1398                    initial: NodeId(1),
1399                    update: NodeId(1),
1400                },
1401                SLTForFoldGroupState {
1402                    target: VarAtomBase::new(3, 0, 0),
1403                    initial: NodeId(2),
1404                    update: NodeId(2),
1405                },
1406            ],
1407        };
1408        let error = raw_error(vec![constant(1), constant(usize::MAX), constant(1), node]);
1409        assert_eq!(error.invariant, "FOR_FOLD_GROUP.PACKED_WIDTH_REPRESENTABLE");
1410    }
1411
1412    #[test]
1413    fn rejects_effectful_descendant_inside_for_fold_group() {
1414        let mut effectful = valid_for_fold();
1415        let SLTNode::ForFold { effects, .. } = &mut effectful else {
1416            unreachable!()
1417        };
1418        effects.push(SLTForEffect::Event {
1419            site_id: 7,
1420            guard: None,
1421            emit_on_true: true,
1422            args: vec![NodeId(0)],
1423            fatal_error_code: None,
1424        });
1425        let group = SLTNode::ForFoldGroup {
1426            loop_var: 3,
1427            loop_width: 8,
1428            loop_signed: false,
1429            start: BigInt::from(0),
1430            step: BigInt::from(1),
1431            trip_count: 1,
1432            entry_guard: NodeId(1),
1433            states: vec![SLTForFoldGroupState {
1434                target: VarAtomBase::new(4, 0, 7),
1435                initial: NodeId(2),
1436                update: NodeId(2),
1437            }],
1438        };
1439
1440        let error = raw_error(vec![constant(8), constant(1), effectful, group]);
1441        assert_eq!(error.invariant, "FOR_FOLD_GROUP.CHILDREN_PURE_AND_TOTAL");
1442    }
1443
1444    #[test]
1445    fn rejects_error_capable_legacy_fold_inside_for_fold_group() {
1446        let group = SLTNode::ForFoldGroup {
1447            loop_var: 3,
1448            loop_width: 8,
1449            loop_signed: false,
1450            start: BigInt::from(0),
1451            step: BigInt::from(1),
1452            trip_count: 1,
1453            entry_guard: NodeId(1),
1454            states: vec![SLTForFoldGroupState {
1455                target: VarAtomBase::new(4, 0, 7),
1456                initial: NodeId(2),
1457                update: NodeId(2),
1458            }],
1459        };
1460
1461        let error = raw_error(vec![constant(8), constant(1), valid_for_fold(), group]);
1462        assert_eq!(error.invariant, "FOR_FOLD_GROUP.CHILDREN_PURE_AND_TOTAL");
1463    }
1464
1465    #[test]
1466    fn rejects_overlapping_for_fold_state_targets() {
1467        let mut node = valid_for_fold();
1468        let SLTNode::ForFold {
1469            initials, updates, ..
1470        } = &mut node
1471        else {
1472            unreachable!()
1473        };
1474        let overlapping = VarAtomBase::new(2, 4, 11);
1475        initials.push(SLTForUpdate {
1476            target: overlapping,
1477            expr: NodeId(0),
1478        });
1479        updates.push(SLTForUpdate {
1480            target: overlapping,
1481            expr: NodeId(0),
1482        });
1483        assert_eq!(
1484            verify_for_fold(node).unwrap_err().invariant,
1485            "FOR_FOLD.STATE_TARGETS_DISJOINT"
1486        );
1487    }
1488
1489    #[test]
1490    fn rejects_unsigned_inclusive_for_fold_width_overflow() {
1491        let target = VarAtomBase::new(2, 0, 0);
1492        let node = SLTNode::ForFold {
1493            loop_var: 1,
1494            loop_width: 1,
1495            loop_signed: false,
1496            start: SLTLoopBound::Expr(NodeId(0)),
1497            end: SLTLoopBound::Const(1),
1498            inclusive: true,
1499            step: 1,
1500            step_op: SLTStepOp::Add,
1501            reverse: false,
1502            result: SLTForFoldResult::State(target),
1503            initials: vec![SLTForUpdate {
1504                target,
1505                expr: NodeId(1),
1506            }],
1507            updates: vec![SLTForUpdate {
1508                target,
1509                expr: NodeId(1),
1510            }],
1511            effects: Vec::new(),
1512            continue_cond: NodeId(1),
1513        };
1514        let error = raw_error(vec![constant(usize::MAX), constant(1), node]);
1515        assert_eq!(error.invariant, "FOR_FOLD.INCLUSIVE_WIDTH_REPRESENTABLE");
1516    }
1517
1518    #[test]
1519    fn checks_for_fold_result_access() {
1520        let error = raw_error(vec![
1521            constant(1),
1522            SLTNode::ForFold {
1523                loop_var: 1,
1524                loop_width: 8,
1525                loop_signed: false,
1526                start: SLTLoopBound::Const(0),
1527                end: SLTLoopBound::Const(1),
1528                inclusive: false,
1529                step: 1,
1530                step_op: SLTStepOp::Add,
1531                reverse: false,
1532                result: SLTForFoldResult::State(VarAtomBase::new(2, 7, 3)),
1533                initials: vec![SLTForUpdate {
1534                    target: VarAtomBase::new(2, 0, 0),
1535                    expr: NodeId(0),
1536                }],
1537                updates: vec![SLTForUpdate {
1538                    target: VarAtomBase::new(2, 0, 0),
1539                    expr: NodeId(0),
1540                }],
1541                effects: Vec::new(),
1542                continue_cond: NodeId(0),
1543            },
1544        ]);
1545        assert_eq!(error.invariant, "WIDTH.ACCESS_ORDERED");
1546        assert_eq!(error.node, NodeId(1));
1547    }
1548
1549    #[test]
1550    fn permits_zero_width_nodes_when_the_operation_defines_them() {
1551        let arena = arena(vec![constant(0), SLTNode::Concat(Vec::new())]);
1552        let facts = SLTNodeFacts::verify(&arena).expect("zero-width facts are representable");
1553        assert_eq!(facts.widths(), &[0, 0]);
1554    }
1555
1556    #[test]
1557    fn reports_the_first_reachable_lowerability_blocker() {
1558        let arena = arena(vec![
1559            constant(0),
1560            SLTNode::Unary(UnaryOp::LogicNot, NodeId(0)),
1561        ]);
1562        let facts = SLTNodeFacts::verify(&arena).expect("zero-width facts are representable");
1563        let error = facts
1564            .require_lowerable(NodeId(1), "test result")
1565            .expect_err("a reachable zero-width node must reject the root");
1566        assert_eq!(error.invariant, "ROOT.LOWERABLE_NON_ZERO");
1567        assert_eq!(error.node, NodeId(0));
1568        assert!(error.message.contains("root n1 reaches n0"));
1569    }
1570
1571    #[test]
1572    fn verifies_a_deep_chain_without_recursion() {
1573        const DEPTH: usize = 100_000;
1574        let mut nodes = Vec::with_capacity(DEPTH + 1);
1575        nodes.push(constant(17));
1576        for node in 1..=DEPTH {
1577            nodes.push(SLTNode::Unary(UnaryOp::Ident, NodeId(node - 1)));
1578        }
1579        let arena = arena(nodes);
1580        let facts = SLTNodeFacts::verify(&arena).expect("deep acyclic graph must verify");
1581        assert_eq!(facts.width(NodeId(DEPTH)), Some(17));
1582    }
1583}