1use std::{fmt, hash::Hash};
2
3use celox_design::BitAccess;
4
5use super::node::{NodeId, SLTLoopBound, SLTNode, SLTNodeArena, SLTStepOp};
6use super::node_rules;
7
8pub 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 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 pub fn width(&self, node: NodeId) -> Option<usize> {
67 self.arena.get_checked(node)?;
68 self.widths.get(node.0).copied()
69 }
70
71 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 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 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 return node_id;
136 };
137 node_id = child;
138 }
139 }
140
141 #[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
153pub(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
163pub(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 for (node_index, node) in nodes.iter().enumerate() {
267 verify_child_ids(NodeId(node_index), node, node_count)?;
268 }
269
270 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 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#[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
787fn 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), constant(4), constant(9), SLTNode::Binary(NodeId(1), BinaryOp::Add, NodeId(2)), SLTNode::Binary(NodeId(1), BinaryOp::Shl, NodeId(2)), SLTNode::Binary(NodeId(1), BinaryOp::Eq, NodeId(2)), SLTNode::Unary(UnaryOp::LogicNot, NodeId(2)), SLTNode::Mux {
991 cond: NodeId(0),
992 then_expr: NodeId(1),
993 else_expr: NodeId(2),
994 }, SLTNode::Concat(vec![(NodeId(1), 2), (NodeId(2), 7)]), SLTNode::Slice {
997 expr: NodeId(2),
998 access: BitAccess { lsb: 2, msb: 5 },
999 }, SLTNode::Binary(NodeId(1), BinaryOp::EqWildcard, NodeId(1)), SLTNode::Unary(UnaryOp::PopCount, NodeId(2)), SLTNode::Unary(UnaryOp::CountLeadingZeros, NodeId(1)), SLTNode::Unary(UnaryOp::CountTrailingZeros, NodeId(0)), ]);
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}