1use std::sync::Arc;
18
19use vyre_foundation::ir::model::expr::Ident;
20use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
21
22pub const LITERAL_TRUE: u32 = 1;
24pub const LITERAL_FALSE: u32 = 2;
26pub const AND_NODE: u32 = 3;
28pub const OR_NODE: u32 = 4;
30
31pub const OP_ID: &str = "vyre-primitives::graph::ddnnf_evaluate";
33pub const DDNNF_EVALUATE_WORKGROUP_SIZE: [u32; 3] = [256, 1, 1];
35
36#[must_use]
38pub const fn ddnnf_evaluate_dispatch_grid(n_nodes: u32) -> [u32; 3] {
39 let lanes_per_block = DDNNF_EVALUATE_WORKGROUP_SIZE[0];
40 let full_blocks = n_nodes / lanes_per_block;
41 let tail_block = if n_nodes % lanes_per_block == 0 { 0 } else { 1 };
42 let blocks = full_blocks + tail_block;
43 [if blocks == 0 { 1 } else { blocks }, 1, 1]
44}
45
46#[must_use]
62#[allow(clippy::too_many_arguments)]
63pub fn ddnnf_evaluate(
64 node_kinds: &str,
65 node_var: &str,
66 child_offsets: &str,
67 child_counts: &str,
68 children: &str,
69 var_assignments: &str,
70 out: &str,
71 n_nodes: u32,
72 n_children: u32,
73 n_vars: u32,
74) -> Program {
75 match try_ddnnf_evaluate(
76 node_kinds,
77 node_var,
78 child_offsets,
79 child_counts,
80 children,
81 var_assignments,
82 out,
83 n_nodes,
84 n_children,
85 n_vars,
86 ) {
87 Ok(program) => program,
88 Err(error) => crate::invalid_output_program(OP_ID, out, DataType::U32, error),
89 }
90}
91
92#[allow(clippy::too_many_arguments)]
94pub fn try_ddnnf_evaluate(
95 node_kinds: &str,
96 node_var: &str,
97 child_offsets: &str,
98 child_counts: &str,
99 children: &str,
100 var_assignments: &str,
101 out: &str,
102 n_nodes: u32,
103 n_children: u32,
104 n_vars: u32,
105) -> Result<Program, String> {
106 if n_nodes == 0 {
107 return Err(format!(
108 "Fix: ddnnf_evaluate requires n_nodes > 0, got {n_nodes}."
109 ));
110 }
111 if n_vars == 0 {
112 return Err(format!(
113 "Fix: ddnnf_evaluate requires n_vars > 0, got {n_vars}."
114 ));
115 }
116
117 let lane = Expr::InvocationId { axis: 0 };
118 let child_index = Expr::add(Expr::var("child_base"), Expr::var("k"));
119 let body = vec![Node::if_then(
120 Expr::lt(lane.clone(), Expr::u32(n_nodes)),
121 vec![
122 Node::let_bind("kind", Expr::load(node_kinds, lane.clone())),
123 Node::let_bind("var_id", Expr::load(node_var, lane.clone())),
124 Node::let_bind("child_base", Expr::load(child_offsets, lane.clone())),
125 Node::let_bind("child_count", Expr::load(child_counts, lane.clone())),
126 Node::if_then(
127 Expr::eq(Expr::var("kind"), Expr::u32(LITERAL_TRUE)),
128 vec![
129 Node::let_bind(
130 "assigned_true",
131 Expr::load(var_assignments, Expr::var("var_id")),
132 ),
133 Node::store(
134 out,
135 lane.clone(),
136 Expr::select(
137 Expr::or(
138 Expr::eq(Expr::var("assigned_true"), Expr::u32(1)),
139 Expr::eq(Expr::var("assigned_true"), Expr::u32(u32::MAX)),
140 ),
141 Expr::u32(1),
142 Expr::u32(0),
143 ),
144 ),
145 ],
146 ),
147 Node::if_then(
148 Expr::eq(Expr::var("kind"), Expr::u32(LITERAL_FALSE)),
149 vec![
150 Node::let_bind(
151 "assigned_false",
152 Expr::load(var_assignments, Expr::var("var_id")),
153 ),
154 Node::store(
155 out,
156 lane.clone(),
157 Expr::select(
158 Expr::or(
159 Expr::eq(Expr::var("assigned_false"), Expr::u32(0)),
160 Expr::eq(Expr::var("assigned_false"), Expr::u32(u32::MAX)),
161 ),
162 Expr::u32(1),
163 Expr::u32(0),
164 ),
165 ),
166 ],
167 ),
168 Node::if_then(
169 Expr::eq(Expr::var("kind"), Expr::u32(AND_NODE)),
170 vec![
171 Node::let_bind("acc_and", Expr::u32(1)),
172 Node::loop_for(
173 "k",
174 Expr::u32(0),
175 Expr::var("child_count"),
176 vec![
177 Node::let_bind("child_node", Expr::load(children, child_index.clone())),
178 Node::assign(
179 "acc_and",
180 Expr::mul(
181 Expr::var("acc_and"),
182 Expr::load(out, Expr::var("child_node")),
183 ),
184 ),
185 ],
186 ),
187 Node::store(out, lane.clone(), Expr::var("acc_and")),
188 ],
189 ),
190 Node::if_then(
191 Expr::eq(Expr::var("kind"), Expr::u32(OR_NODE)),
192 vec![
193 Node::let_bind("acc_or", Expr::u32(0)),
194 Node::loop_for(
195 "kk",
196 Expr::u32(0),
197 Expr::var("child_count"),
198 vec![
199 Node::let_bind(
200 "or_child_node",
201 Expr::load(
202 children,
203 Expr::add(Expr::var("child_base"), Expr::var("kk")),
204 ),
205 ),
206 Node::assign(
207 "acc_or",
208 Expr::add(
209 Expr::var("acc_or"),
210 Expr::load(out, Expr::var("or_child_node")),
211 ),
212 ),
213 ],
214 ),
215 Node::store(out, lane.clone(), Expr::var("acc_or")),
216 ],
217 ),
218 Node::if_then(
219 Expr::and(
220 Expr::and(
221 Expr::ne(Expr::var("kind"), Expr::u32(LITERAL_TRUE)),
222 Expr::ne(Expr::var("kind"), Expr::u32(LITERAL_FALSE)),
223 ),
224 Expr::and(
225 Expr::ne(Expr::var("kind"), Expr::u32(AND_NODE)),
226 Expr::ne(Expr::var("kind"), Expr::u32(OR_NODE)),
227 ),
228 ),
229 vec![Node::store(out, lane.clone(), Expr::u32(0))],
230 ),
231 ],
232 )];
233
234 Ok(Program::wrapped(
235 vec![
236 BufferDecl::storage(node_kinds, 0, BufferAccess::ReadOnly, DataType::U32)
237 .with_count(n_nodes),
238 BufferDecl::storage(node_var, 1, BufferAccess::ReadOnly, DataType::U32)
239 .with_count(n_nodes),
240 BufferDecl::storage(child_offsets, 2, BufferAccess::ReadOnly, DataType::U32)
241 .with_count(n_nodes),
242 BufferDecl::storage(child_counts, 3, BufferAccess::ReadOnly, DataType::U32)
243 .with_count(n_nodes),
244 BufferDecl::storage(children, 4, BufferAccess::ReadOnly, DataType::U32)
245 .with_count(n_children.max(1)),
246 BufferDecl::storage(var_assignments, 5, BufferAccess::ReadOnly, DataType::U32)
247 .with_count(n_vars),
248 BufferDecl::storage(out, 6, BufferAccess::ReadWrite, DataType::U32).with_count(n_nodes),
249 ],
250 DDNNF_EVALUATE_WORKGROUP_SIZE,
251 vec![Node::Region {
252 generator: Ident::from(OP_ID),
253 source_region: None,
254 body: Arc::new(body),
255 }],
256 ))
257}
258
259#[must_use]
268#[cfg(any(test, feature = "cpu-parity"))]
269pub fn ddnnf_evaluate_cpu(
270 nodes: &[(u32, u32, u32)],
271 node_var: &[u32],
272 children: &[u32],
273 var_assignments: &[u32],
274 topo_order: &[u32],
275) -> Vec<u32> {
276 match try_ddnnf_evaluate_cpu(nodes, node_var, children, var_assignments, topo_order) {
277 Ok(out) => out,
278 Err(error) => panic!("vyre-primitives d-DNNF evaluate CPU reference failed: {error}"),
282 }
283}
284
285#[cfg(any(test, feature = "cpu-parity"))]
287pub fn try_ddnnf_evaluate_cpu(
288 nodes: &[(u32, u32, u32)],
289 node_var: &[u32],
290 children: &[u32],
291 var_assignments: &[u32],
292 topo_order: &[u32],
293) -> Result<Vec<u32>, String> {
294 let mut out = Vec::new();
295 try_ddnnf_evaluate_cpu_into(
296 nodes,
297 node_var,
298 children,
299 var_assignments,
300 topo_order,
301 &mut out,
302 )?;
303 Ok(out)
304}
305
306#[cfg(any(test, feature = "cpu-parity"))]
308pub fn try_ddnnf_evaluate_cpu_into(
309 nodes: &[(u32, u32, u32)],
310 node_var: &[u32],
311 children: &[u32],
312 var_assignments: &[u32],
313 topo_order: &[u32],
314 out: &mut Vec<u32>,
315) -> Result<(), String> {
316 let mut scratch = DdnnfCpuScratch::default();
317 try_ddnnf_evaluate_cpu_into_with_scratch(
318 nodes,
319 node_var,
320 children,
321 var_assignments,
322 topo_order,
323 out,
324 &mut scratch,
325 )
326}
327
328#[cfg(any(test, feature = "cpu-parity"))]
330#[derive(Debug, Default, Clone)]
331pub struct DdnnfCpuScratch {
332 pub values: Vec<u32>,
334}
335
336#[cfg(any(test, feature = "cpu-parity"))]
337impl DdnnfCpuScratch {
338 pub fn new() -> Self {
340 Self::default()
341 }
342}
343
344#[cfg(any(test, feature = "cpu-parity"))]
350pub fn try_ddnnf_evaluate_cpu_into_with_scratch(
351 nodes: &[(u32, u32, u32)],
352 node_var: &[u32],
353 children: &[u32],
354 var_assignments: &[u32],
355 topo_order: &[u32],
356 out: &mut Vec<u32>,
357 scratch: &mut DdnnfCpuScratch,
358) -> Result<(), String> {
359 validate_ddnnf_evaluate_inputs(nodes, node_var, children, var_assignments, topo_order)?;
360 let n_nodes = nodes.len();
361 scratch.values.clear();
362 crate::graph::scratch::resize_graph_vec(
363 &mut scratch.values,
364 n_nodes,
365 0u32,
366 "d-DNNF CPU oracle",
367 "ddnnf_evaluate CPU scratch",
368 )?;
369 for &node in topo_order {
370 let i = node as usize;
371 let (kind, co, cc) = nodes[i];
372 match kind {
373 LITERAL_TRUE => {
374 let assigned = var_assignments[node_var[i] as usize];
375 scratch.values[i] = if assigned == 1 || assigned == u32::MAX {
376 1
377 } else {
378 0
379 };
380 }
381 LITERAL_FALSE => {
382 let assigned = var_assignments[node_var[i] as usize];
383 scratch.values[i] = if assigned == 0 || assigned == u32::MAX {
384 1
385 } else {
386 0
387 };
388 }
389 AND_NODE => {
390 let mut acc = 1u32;
391 for k in 0..cc as usize {
392 let child_index = co as usize + k;
393 let cn = children[child_index] as usize;
394 let child_value = scratch.values[cn];
395 acc = acc.checked_mul(child_value).ok_or_else(|| {
396 format!(
397 "ddnnf_evaluate CPU oracle AND node {i} model count overflowed u32. Fix: shard or widen model-count accumulation."
398 )
399 })?;
400 }
401 scratch.values[i] = acc;
402 }
403 OR_NODE => {
404 let mut acc = 0u32;
405 for k in 0..cc as usize {
406 let child_index = co as usize + k;
407 let cn = children[child_index] as usize;
408 let child_value = scratch.values[cn];
409 acc = acc.checked_add(child_value).ok_or_else(|| {
410 format!(
411 "ddnnf_evaluate CPU oracle OR node {i} model count overflowed u32. Fix: shard or widen model-count accumulation."
412 )
413 })?;
414 }
415 scratch.values[i] = acc;
416 }
417 _ => {
418 scratch.values[i] = 0;
419 }
420 }
421 }
422 if n_nodes > out.len() {
423 crate::graph::scratch::reserve_graph_items(
424 out,
425 n_nodes - out.len(),
426 "d-DNNF CPU oracle",
427 "ddnnf_evaluate CPU output",
428 )?;
429 }
430 out.clear();
431 out.extend_from_slice(&scratch.values);
432 Ok(())
433}
434
435#[cfg(any(test, feature = "cpu-parity"))]
436fn validate_ddnnf_evaluate_inputs(
437 nodes: &[(u32, u32, u32)],
438 node_var: &[u32],
439 children: &[u32],
440 var_assignments: &[u32],
441 topo_order: &[u32],
442) -> Result<(), String> {
443 let n_nodes = nodes.len();
444 if node_var.len() != n_nodes {
445 return Err(format!(
446 "ddnnf_evaluate CPU oracle received node_var_len={} for node_count={n_nodes}. Fix: pass one variable slot per compiled node.",
447 node_var.len()
448 ));
449 }
450 for &node in topo_order {
451 let i = node as usize;
452 let Some(&(kind, co, cc)) = nodes.get(i) else {
453 return Err(format!(
454 "ddnnf_evaluate CPU oracle topo node {node} is outside node_count={n_nodes}. Fix: rebuild the compiled-circuit topological order."
455 ));
456 };
457 match kind {
458 LITERAL_TRUE | LITERAL_FALSE => {
459 let v = node_var[i] as usize;
460 if v >= var_assignments.len() {
461 return Err(format!(
462 "ddnnf_evaluate CPU oracle literal node {i} references var {v} outside assignment_count={}. Fix: pass a complete assignment vector.",
463 var_assignments.len()
464 ));
465 }
466 }
467 AND_NODE | OR_NODE => {
468 let co = co as usize;
469 let cc = cc as usize;
470 let end = co.checked_add(cc).ok_or_else(|| {
471 format!(
472 "ddnnf_evaluate CPU oracle child offset overflow at node {i}. Fix: rebuild child_offsets before parity comparison."
473 )
474 })?;
475 if end > children.len() {
476 return Err(format!(
477 "ddnnf_evaluate CPU oracle node {i} child range {co}..{end} exceeds child_count={}. Fix: pass a complete child list.",
478 children.len()
479 ));
480 }
481 for child_index in co..end {
482 let cn = children[child_index] as usize;
483 if cn >= n_nodes {
484 return Err(format!(
485 "ddnnf_evaluate CPU oracle node {i} references child node {cn} outside node_count={n_nodes}. Fix: rebuild compiled child ids."
486 ));
487 }
488 }
489 }
490 _ => {}
491 }
492 }
493 Ok(())
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn cpu_single_true_literal_with_assigned_var() {
502 let nodes = vec![(LITERAL_TRUE, 0, 0)];
504 let node_var = vec![0];
505 let children = vec![];
506 let assigns = vec![1];
507 let order = vec![0];
508 let out = ddnnf_evaluate_cpu(&nodes, &node_var, &children, &assigns, &order);
509 assert_eq!(out[0], 1);
510 }
511
512 #[test]
513 fn cpu_single_true_literal_with_unset_var() {
514 let nodes = vec![(LITERAL_TRUE, 0, 0)];
516 let node_var = vec![0];
517 let children = vec![];
518 let assigns = vec![u32::MAX];
519 let order = vec![0];
520 let out = ddnnf_evaluate_cpu(&nodes, &node_var, &children, &assigns, &order);
521 assert_eq!(out[0], 1);
522 }
523
524 #[test]
525 fn cpu_and_of_two_literals() {
526 let nodes = vec![(LITERAL_TRUE, 0, 0), (LITERAL_TRUE, 0, 0), (AND_NODE, 0, 2)];
528 let node_var = vec![0, 1, 0];
529 let children = vec![0, 1];
530 let assigns = vec![u32::MAX; 2];
531 let order = vec![0, 1, 2];
532 let out = ddnnf_evaluate_cpu(&nodes, &node_var, &children, &assigns, &order);
533 assert_eq!(out[2], 1);
534 }
535
536 #[test]
537 fn cpu_or_of_two_literals_counts_both() {
538 let nodes = vec![(LITERAL_TRUE, 0, 0), (LITERAL_TRUE, 0, 0), (OR_NODE, 0, 2)];
540 let node_var = vec![0, 1, 0];
541 let children = vec![0, 1];
542 let assigns = vec![u32::MAX; 2];
543 let order = vec![0, 1, 2];
544 let out = ddnnf_evaluate_cpu(&nodes, &node_var, &children, &assigns, &order);
545 assert_eq!(out[2], 2);
546 }
547
548 #[test]
549 fn cpu_partial_assignment_constrains_count() {
550 let nodes = vec![(LITERAL_TRUE, 0, 0), (LITERAL_TRUE, 0, 0), (OR_NODE, 0, 2)];
553 let node_var = vec![0, 1, 0];
554 let children = vec![0, 1];
555 let assigns = vec![1, 0]; let order = vec![0, 1, 2];
557 let out = ddnnf_evaluate_cpu(&nodes, &node_var, &children, &assigns, &order);
558 assert_eq!(out[2], 1);
562 }
563
564 #[test]
565 fn checked_cpu_oracle_rejects_missing_assignment() {
566 let nodes = vec![(LITERAL_TRUE, 0, 0)];
567 let node_var = vec![7];
568 let children = vec![];
569 let assigns = vec![u32::MAX];
570 let order = vec![0];
571 let error = try_ddnnf_evaluate_cpu(&nodes, &node_var, &children, &assigns, &order)
572 .expect_err("checked d-DNNF oracle must reject missing variable assignments");
573
574 assert!(
575 error.contains("outside assignment_count"),
576 "error should describe the missing assignment: {error}"
577 );
578 }
579
580 #[test]
581 fn checked_cpu_oracle_rejects_missing_child() {
582 let nodes = vec![(LITERAL_TRUE, 0, 0), (AND_NODE, 0, 1)];
583 let node_var = vec![0, 0];
584 let children = vec![];
585 let assigns = vec![u32::MAX];
586 let order = vec![0, 1];
587 let error = try_ddnnf_evaluate_cpu(&nodes, &node_var, &children, &assigns, &order)
588 .expect_err("checked d-DNNF oracle must reject missing child list entries");
589
590 assert!(
591 error.contains("exceeds child_count"),
592 "error should describe the missing child entry: {error}"
593 );
594 }
595
596 #[test]
597 fn scratch_cpu_oracle_rejects_bad_child_without_clobbering_storage() {
598 let nodes = vec![(LITERAL_TRUE, 0, 0), (AND_NODE, 0, 1)];
599 let node_var = vec![0, 0];
600 let children = vec![9];
601 let assigns = vec![u32::MAX];
602 let order = vec![0, 1];
603 let mut out = vec![0xDEAD_BEEF, 0xCAFE_BABE];
604 let mut scratch = DdnnfCpuScratch {
605 values: vec![7, 8, 9],
606 };
607
608 let error = try_ddnnf_evaluate_cpu_into_with_scratch(
609 &nodes,
610 &node_var,
611 &children,
612 &assigns,
613 &order,
614 &mut out,
615 &mut scratch,
616 )
617 .expect_err("checked d-DNNF oracle must reject out-of-range child ids");
618
619 assert!(
620 error.contains("outside node_count"),
621 "error should describe the invalid child node: {error}"
622 );
623 assert_eq!(
624 out,
625 vec![0xDEAD_BEEF, 0xCAFE_BABE],
626 "Fix: malformed d-DNNF inputs must not clobber caller output."
627 );
628 assert_eq!(
629 scratch.values,
630 vec![7, 8, 9],
631 "Fix: structural validation failures must not clear reusable scratch."
632 );
633 }
634
635 #[test]
636 fn scratch_cpu_oracle_reuses_values_and_clears_stale_tail() {
637 let nodes = vec![(LITERAL_TRUE, 0, 0), (LITERAL_FALSE, 0, 0), (OR_NODE, 0, 2)];
638 let node_var = vec![0, 1, 0];
639 let children = vec![0, 1];
640 let assigns = vec![1, 0];
641 let order = vec![0, 1, 2];
642 let mut out = Vec::with_capacity(8);
643 out.extend_from_slice(&[99, 98, 97, 96]);
644 let mut scratch = DdnnfCpuScratch {
645 values: Vec::with_capacity(8),
646 };
647 scratch.values.extend_from_slice(&[11, 12, 13, 14, 15]);
648 let out_capacity = out.capacity();
649 let scratch_capacity = scratch.values.capacity();
650
651 try_ddnnf_evaluate_cpu_into_with_scratch(
652 &nodes,
653 &node_var,
654 &children,
655 &assigns,
656 &order,
657 &mut out,
658 &mut scratch,
659 )
660 .expect("Fix: replace expect with fallible API or document caller precondition; panic only on programmer error - valid d-DNNF circuit must evaluate into reusable storage");
661
662 assert_eq!(out, vec![1, 1, 2]);
663 assert_eq!(scratch.values, vec![1, 1, 2]);
664 assert_eq!(out.capacity(), out_capacity);
665 assert_eq!(scratch.values.capacity(), scratch_capacity);
666
667 try_ddnnf_evaluate_cpu_into_with_scratch(
668 &nodes[..1],
669 &node_var[..1],
670 &[],
671 &assigns,
672 &[0],
673 &mut out,
674 &mut scratch,
675 )
676 .expect("Fix: replace expect with fallible API or document caller precondition; panic only on programmer error - smaller d-DNNF circuit must reuse and truncate storage");
677
678 assert_eq!(out, vec![1]);
679 assert_eq!(scratch.values, vec![1]);
680 assert_eq!(out.capacity(), out_capacity);
681 assert_eq!(scratch.values.capacity(), scratch_capacity);
682 }
683
684 #[test]
685 fn generated_cpu_oracle_matches_independent_ddnnf_evaluator() {
686 let mut out = Vec::new();
687 let mut scratch = DdnnfCpuScratch::new();
688 for case in 0..4096usize {
689 let n_literals = match case {
690 1 => 256,
691 2 => 257,
692 3 => 1025,
693 _ => 1 + case % 6,
694 };
695 let n_nodes = n_literals + 4;
696 let n_vars = 1 + (case / 7) % 6;
697 let mut nodes = Vec::new();
698 let mut node_var = Vec::new();
699 let mut children = Vec::new();
700
701 for idx in 0..n_literals {
702 let kind = if (case + idx) % 2 == 0 {
703 LITERAL_TRUE
704 } else {
705 LITERAL_FALSE
706 };
707 nodes.push((kind, 0, 0));
708 node_var.push((idx % n_vars) as u32);
709 }
710
711 for op_idx in 0..4usize {
712 let child_count = 1 + ((case + op_idx) % n_literals);
713 let offset = children.len() as u32;
714 for child in 0..child_count {
715 children.push(((child + op_idx) % (n_literals + op_idx)) as u32);
716 }
717 let kind = if op_idx % 2 == 0 { AND_NODE } else { OR_NODE };
718 nodes.push((kind, offset, child_count as u32));
719 node_var.push(0);
720 }
721
722 let assignments: Vec<u32> = (0..n_vars)
723 .map(|idx| match (case + idx) % 3 {
724 0 => 0,
725 1 => 1,
726 _ => u32::MAX,
727 })
728 .collect();
729 let topo_order: Vec<u32> = (0..n_nodes as u32).collect();
730
731 try_ddnnf_evaluate_cpu_into_with_scratch(
732 &nodes,
733 &node_var,
734 &children,
735 &assignments,
736 &topo_order,
737 &mut out,
738 &mut scratch,
739 )
740 .expect("Fix: caller must pre-size buffers; use fallible reserve or return ResourceExhausted - generated d-DNNF CPU oracle should reserve and evaluate");
741 let expected =
742 independent_ddnnf_evaluate(&nodes, &node_var, &children, &assignments, &topo_order);
743
744 assert_eq!(out, expected, "case {case}: d-DNNF evaluation mismatch");
745 }
746 }
747
748 fn independent_ddnnf_evaluate(
749 nodes: &[(u32, u32, u32)],
750 node_var: &[u32],
751 children: &[u32],
752 assignments: &[u32],
753 topo_order: &[u32],
754 ) -> Vec<u32> {
755 let mut out = Vec::new();
756 out.resize(nodes.len(), 0);
757 for &node in topo_order {
758 let i = node as usize;
759 let (kind, offset, count) = nodes[i];
760 out[i] = match kind {
761 LITERAL_TRUE => {
762 let assigned = assignments[node_var[i] as usize];
763 u32::from(assigned == 1 || assigned == u32::MAX)
764 }
765 LITERAL_FALSE => {
766 let assigned = assignments[node_var[i] as usize];
767 u32::from(assigned == 0 || assigned == u32::MAX)
768 }
769 AND_NODE => {
770 let mut acc = 1u32;
771 for k in 0..count as usize {
772 acc *= out[children[offset as usize + k] as usize];
773 }
774 acc
775 }
776 OR_NODE => {
777 let mut acc = 0u32;
778 for k in 0..count as usize {
779 acc += out[children[offset as usize + k] as usize];
780 }
781 acc
782 }
783 _ => 0,
784 };
785 }
786 out
787 }
788
789 #[test]
790 fn gpu_program_builder_exposes_ddnnf_buffers() {
791 let program = ddnnf_evaluate(
792 "kinds",
793 "node_var",
794 "child_offsets",
795 "child_counts",
796 "children",
797 "assignments",
798 "out",
799 3,
800 2,
801 2,
802 );
803 assert_eq!(program.buffers().len(), 7);
804 assert_eq!(program.workgroup_size(), DDNNF_EVALUATE_WORKGROUP_SIZE);
805 assert!(
806 program
807 .entry()
808 .iter()
809 .any(|node| matches!(node, vyre_foundation::ir::Node::Region { generator, .. } if generator.as_str() == OP_ID))
810 );
811 }
812
813 #[test]
814 fn dispatch_grid_packs_ddnnf_nodes_into_workgroups() {
815 assert_eq!(ddnnf_evaluate_dispatch_grid(0), [1, 1, 1]);
816 assert_eq!(ddnnf_evaluate_dispatch_grid(1), [1, 1, 1]);
817 assert_eq!(ddnnf_evaluate_dispatch_grid(256), [1, 1, 1]);
818 assert_eq!(ddnnf_evaluate_dispatch_grid(257), [2, 1, 1]);
819 assert_eq!(ddnnf_evaluate_dispatch_grid(1025), [5, 1, 1]);
820 }
821
822 #[test]
823 fn gpu_program_builder_rejects_empty_node_count_with_trap_program() {
824 let program = ddnnf_evaluate(
825 "kinds",
826 "node_var",
827 "child_offsets",
828 "child_counts",
829 "children",
830 "assignments",
831 "out",
832 0,
833 0,
834 1,
835 );
836 assert_eq!(program.buffers().len(), 1);
837 assert!(
838 program
839 .entry()
840 .iter()
841 .any(|node| matches!(node, vyre_foundation::ir::Node::Region { body, .. } if body.iter().any(|inner| matches!(inner, vyre_foundation::ir::Node::Trap { .. }))))
842 );
843 }
844
845 #[test]
846 fn checked_gpu_builder_rejects_empty_var_domain() {
847 let error = try_ddnnf_evaluate(
848 "kinds",
849 "node_var",
850 "child_offsets",
851 "child_counts",
852 "children",
853 "assignments",
854 "out",
855 1,
856 0,
857 0,
858 )
859 .expect_err("checked d-DNNF builder must reject empty variable domains");
860
861 assert!(
862 error.contains("requires n_vars > 0"),
863 "error should describe the invalid variable domain: {error}"
864 );
865 }
866}