1use polydat::ast::Value;
27#[cfg(test)]
28use polydat::ast::{PolydatNode, PortType};
29use polydat::derive_support::PolydatSetup;
30
31#[inline]
35fn hash_to_unit(v: u64) -> f64 {
36 (v as f64) / ((u64::MAX as f64) + 1.0)
37}
38
39#[polydat::polydat_node(category = Probability)]
55fn fair_coin(input: u64) -> u64 {
56 let h = crate::hash::splitmix64_u64(input);
57 h & 1
58}
59
60#[polydat::polydat_node(category = Probability)]
88fn unfair_coin(input: u64, p: Const<f64>) -> u64 {
89 if !(0.0..=1.0).contains(&*p) {
90 panic!(
91 "unfair_coin probability p must be in [0.0, 1.0], got {}",
92 *p
93 );
94 }
95 let h = crate::hash::splitmix64_u64(input);
96 let unit = hash_to_unit(h);
97 if unit < *p { 1 } else { 0 }
98}
99
100#[polydat::polydat_node(category = Probability)]
121fn select(cond: u64, if_true: u64, if_false: u64) -> u64 {
122 if cond != 0 { if_true } else { if_false }
123}
124
125#[polydat::polydat_node(category = Probability)]
146fn chance(input: u64, p: Const<f64>) -> u64 {
147 if !(0.0..=1.0).contains(&*p) {
148 panic!("chance probability p must be in [0.0, 1.0], got {}", *p);
149 }
150 let h = crate::hash::splitmix64_u64(input);
151 let unit = hash_to_unit(h);
152 let result: f64 = if unit < *p { 1.0 } else { 0.0 };
153 result.to_bits()
154}
155
156#[polydat::polydat_node(category = Probability)]
187fn n_of(input: u64, n: Const<u64>, m: Const<u64>) -> u64 {
188 if *m == 0 {
189 panic!("n_of: m must be > 0");
190 }
191 if *n > *m {
192 panic!("n_of: n ({}) must be <= m ({})", *n, *m);
193 }
194 n_of_m_eval(input, *n, *m)
195}
196
197pub use polydat::numeric::n_of_m::n_of_m_eval;
198
199#[polydat::polydat_node(category = Probability)]
225fn one_of(input: u64, values: Const<Vec<String>>) -> String {
226 assert!(!values.is_empty(), "one_of: values must be non-empty");
227 let h = crate::hash::splitmix64_u64(input);
228 let idx = (h % values.len() as u64) as usize;
229 values[idx].clone()
230}
231
232pub struct WeightedTable {
247 pub values: Vec<String>,
249 pub cumulative: Vec<f64>,
252}
253
254impl PolydatSetup for WeightedTable {}
255
256impl WeightedTable {
257 pub fn parse(spec: &str) -> Self {
261 let mut values = Vec::new();
262 let mut weights = Vec::new();
263 for elem in spec.split([';', ',']) {
264 let elem = elem.trim();
265 if elem.is_empty() {
266 continue;
267 }
268 let parts: Vec<&str> = elem.splitn(2, ':').collect();
269 assert_eq!(
270 parts.len(),
271 2,
272 "one_of_weighted: expected 'value:weight', got '{elem}'"
273 );
274 values.push(parts[0].to_string());
275 let w: f64 = parts[1].parse().expect("one_of_weighted: invalid weight");
276 assert!(w > 0.0, "one_of_weighted: weight must be positive, got {w}");
277 weights.push(w);
278 }
279 assert!(
280 !values.is_empty(),
281 "one_of_weighted: spec must be non-empty"
282 );
283
284 let total: f64 = weights.iter().sum();
285 assert!(total > 0.0, "one_of_weighted: total weight must be > 0");
286
287 let mut cumulative = Vec::with_capacity(weights.len());
288 let mut running = 0.0;
289 for w in &weights {
290 running += w / total;
291 cumulative.push(running);
292 }
293 if let Some(last) = cumulative.last_mut() {
295 *last = 1.0;
296 }
297
298 Self { values, cumulative }
299 }
300}
301
302#[polydat::polydat_node(category = Probability)]
326fn one_of_weighted(
327 input: u64,
328 spec: Const<&str>,
329 #[poly_const(WeightedTable::parse, from = spec)] table: &WeightedTable,
330) -> String {
331 let _ = spec;
332 let h = crate::hash::splitmix64_u64(input);
333 let unit = hash_to_unit(h);
334 let idx = match table
336 .cumulative
337 .binary_search_by(|c| c.partial_cmp(&unit).unwrap())
338 {
339 Ok(i) => i,
340 Err(i) => i,
341 };
342 let idx = idx.min(table.values.len() - 1);
344 table.values[idx].clone()
345}
346
347#[polydat::polydat_node(category = Probability)]
369fn blend(a: u64, b: u64, mix: Const<f64>) -> u64 {
370 if !(0.0..=1.0).contains(&*mix) {
371 panic!("blend: mix must be in [0.0, 1.0], got {}", *mix);
372 }
373 let a_f = f64::from_bits(a);
374 let b_f = f64::from_bits(b);
375 let result = a_f * (1.0 - *mix) + b_f * *mix;
376 result.to_bits()
377}
378
379#[polydat::polydat_node(category = Probability)]
401fn default_or(value: Value, fallback: Value) -> Value {
402 if matches!(value, Value::None) {
403 fallback
404 } else {
405 value
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
416 fn fair_coin_returns_0_or_1() {
417 let node = FairCoin::new();
418 let mut out = [Value::None];
419 for i in 0..100u64 {
420 node.eval(&[Value::U64(i)], &mut out);
421 let v = out[0].as_u64();
422 assert!(
423 v == 0 || v == 1,
424 "fair_coin({i}) returned {v}, expected 0 or 1"
425 );
426 }
427 }
428
429 #[test]
430 fn fair_coin_deterministic() {
431 let node = FairCoin::new();
432 let mut out1 = [Value::None];
433 let mut out2 = [Value::None];
434 node.eval(&[Value::U64(42)], &mut out1);
435 node.eval(&[Value::U64(42)], &mut out2);
436 assert_eq!(out1[0].as_u64(), out2[0].as_u64());
437 }
438
439 #[test]
440 fn fair_coin_roughly_balanced() {
441 let node = FairCoin::new();
442 let mut out = [Value::None];
443 let mut ones = 0u64;
444 let n = 10_000u64;
445 for i in 0..n {
446 node.eval(&[Value::U64(i)], &mut out);
447 ones += out[0].as_u64();
448 }
449 let ratio = ones as f64 / n as f64;
451 assert!(
452 (0.45..=0.55).contains(&ratio),
453 "fair_coin ratio {ratio} outside expected 0.45-0.55"
454 );
455 }
456
457 #[test]
458 fn fair_coin_compiled_u64() {
459 let node = FairCoin::new();
460 let compiled = node.compiled_u64().expect("should have compiled_u64");
461 let inputs = [42u64];
462 let mut outputs = [0u64];
463 compiled(&inputs, &mut outputs);
464 assert!(outputs[0] == 0 || outputs[0] == 1);
465
466 let mut eval_out = [Value::None];
468 node.eval(&[Value::U64(42)], &mut eval_out);
469 assert_eq!(outputs[0], eval_out[0].as_u64());
470 }
471
472 #[test]
475 fn unfair_coin_always_0_when_p_is_0() {
476 let node = UnfairCoin::new(0.0);
477 let mut out = [Value::None];
478 for i in 0..100u64 {
479 node.eval(&[Value::U64(i)], &mut out);
480 assert_eq!(
481 out[0].as_u64(),
482 0,
483 "unfair_coin(p=0.0) should always return 0"
484 );
485 }
486 }
487
488 #[test]
489 fn unfair_coin_always_1_when_p_is_1() {
490 let node = UnfairCoin::new(1.0);
491 let mut out = [Value::None];
492 for i in 0..100u64 {
493 node.eval(&[Value::U64(i)], &mut out);
494 assert_eq!(
495 out[0].as_u64(),
496 1,
497 "unfair_coin(p=1.0) should always return 1"
498 );
499 }
500 }
501
502 #[test]
503 fn unfair_coin_respects_probability() {
504 let node = UnfairCoin::new(0.2);
505 let mut out = [Value::None];
506 let mut ones = 0u64;
507 let n = 10_000u64;
508 for i in 0..n {
509 node.eval(&[Value::U64(i)], &mut out);
510 ones += out[0].as_u64();
511 }
512 let ratio = ones as f64 / n as f64;
513 assert!(
514 (0.15..=0.25).contains(&ratio),
515 "unfair_coin(p=0.2) ratio {ratio} outside expected 0.15-0.25"
516 );
517 }
518
519 #[test]
520 fn unfair_coin_compiled_u64() {
521 let node = UnfairCoin::new(0.5);
522 let compiled = node.compiled_u64().expect("should have compiled_u64");
523 let inputs = [42u64];
524 let mut outputs = [0u64];
525 compiled(&inputs, &mut outputs);
526 assert!(outputs[0] == 0 || outputs[0] == 1);
527
528 let mut eval_out = [Value::None];
529 node.eval(&[Value::U64(42)], &mut eval_out);
530 assert_eq!(outputs[0], eval_out[0].as_u64());
531 }
532
533 #[test]
534 #[should_panic(expected = "unfair_coin probability p must be in [0.0, 1.0]")]
535 fn unfair_coin_rejects_invalid_p() {
536 let node = UnfairCoin::new(1.5);
539 let mut out = [Value::None];
540 node.eval(&[Value::U64(0)], &mut out);
541 }
542
543 #[test]
546 fn select_true_branch() {
547 let node = Select::new();
548 let mut out = [Value::None];
549 node.eval(&[Value::U64(1), Value::U64(100), Value::U64(200)], &mut out);
550 assert_eq!(out[0].as_u64(), 100);
551 }
552
553 #[test]
554 fn select_false_branch() {
555 let node = Select::new();
556 let mut out = [Value::None];
557 node.eval(&[Value::U64(0), Value::U64(100), Value::U64(200)], &mut out);
558 assert_eq!(out[0].as_u64(), 200);
559 }
560
561 #[test]
562 fn select_nonzero_is_true() {
563 let node = Select::new();
564 let mut out = [Value::None];
565 node.eval(&[Value::U64(999), Value::U64(10), Value::U64(20)], &mut out);
567 assert_eq!(out[0].as_u64(), 10);
568 }
569
570 #[test]
571 fn select_compiled_u64() {
572 let node = Select::new();
573 let compiled = node.compiled_u64().expect("should have compiled_u64");
574 let mut outputs = [0u64];
575
576 compiled(&[1, 100, 200], &mut outputs);
577 assert_eq!(outputs[0], 100);
578
579 compiled(&[0, 100, 200], &mut outputs);
580 assert_eq!(outputs[0], 200);
581 }
582
583 #[test]
586 fn chance_returns_f64_bits() {
587 let node = Chance::new(0.5);
588 let mut out = [Value::None];
589 for i in 0..100u64 {
590 node.eval(&[Value::U64(i)], &mut out);
591 let bits = out[0].as_u64();
592 let f = f64::from_bits(bits);
593 assert!(
594 f == 0.0 || f == 1.0,
595 "chance({i}) returned f64 {f}, expected 0.0 or 1.0"
596 );
597 }
598 }
599
600 #[test]
601 fn chance_always_0_when_p_is_0() {
602 let node = Chance::new(0.0);
603 let mut out = [Value::None];
604 for i in 0..100u64 {
605 node.eval(&[Value::U64(i)], &mut out);
606 let f = f64::from_bits(out[0].as_u64());
607 assert_eq!(f, 0.0);
608 }
609 }
610
611 #[test]
612 fn chance_always_1_when_p_is_1() {
613 let node = Chance::new(1.0);
614 let mut out = [Value::None];
615 for i in 0..100u64 {
616 node.eval(&[Value::U64(i)], &mut out);
617 let f = f64::from_bits(out[0].as_u64());
618 assert_eq!(f, 1.0);
619 }
620 }
621
622 #[test]
623 fn chance_compiled_u64() {
624 let node = Chance::new(0.5);
625 let compiled = node.compiled_u64().expect("should have compiled_u64");
626 let inputs = [42u64];
627 let mut outputs = [0u64];
628 compiled(&inputs, &mut outputs);
629 let f = f64::from_bits(outputs[0]);
630 assert!(f == 0.0 || f == 1.0);
631
632 let mut eval_out = [Value::None];
633 node.eval(&[Value::U64(42)], &mut eval_out);
634 assert_eq!(outputs[0], eval_out[0].as_u64());
635 }
636
637 #[test]
640 fn n_of_m_exact_count() {
641 let node = NOf::new(3, 10);
642 let mut out = [Value::None];
643 for window in 0..10u64 {
645 let mut count = 0u64;
646 for pos in 0..10u64 {
647 let input = window * 10 + pos;
648 node.eval(&[Value::U64(input)], &mut out);
649 count += out[0].as_u64();
650 }
651 assert_eq!(
652 count, 3,
653 "n_of(3, 10) window {window}: expected exactly 3 selected, got {count}"
654 );
655 }
656 }
657
658 #[test]
659 fn n_of_m_all_selected() {
660 let node = NOf::new(5, 5);
661 let mut out = [Value::None];
662 for i in 0..20u64 {
663 node.eval(&[Value::U64(i)], &mut out);
664 assert_eq!(out[0].as_u64(), 1, "n_of(5, 5) should always return 1");
665 }
666 }
667
668 #[test]
669 fn n_of_m_none_selected() {
670 let node = NOf::new(0, 5);
671 let mut out = [Value::None];
672 for i in 0..20u64 {
673 node.eval(&[Value::U64(i)], &mut out);
674 assert_eq!(out[0].as_u64(), 0, "n_of(0, 5) should always return 0");
675 }
676 }
677
678 #[test]
679 fn n_of_m_deterministic() {
680 let node = NOf::new(2, 7);
681 let mut out1 = [Value::None];
682 let mut out2 = [Value::None];
683 for i in 0..50u64 {
684 node.eval(&[Value::U64(i)], &mut out1);
685 node.eval(&[Value::U64(i)], &mut out2);
686 assert_eq!(out1[0].as_u64(), out2[0].as_u64());
687 }
688 }
689
690 #[test]
691 fn n_of_m_compiled_u64() {
692 let node = NOf::new(3, 10);
693 let compiled = node.compiled_u64().expect("should have compiled_u64");
694
695 for i in 0..10u64 {
697 let mut c_out = [0u64];
698 compiled(&[i], &mut c_out);
699
700 let mut e_out = [Value::None];
701 node.eval(&[Value::U64(i)], &mut e_out);
702
703 assert_eq!(
704 c_out[0],
705 e_out[0].as_u64(),
706 "compiled/eval mismatch at input {i}"
707 );
708 }
709 }
710
711 #[test]
712 #[should_panic(expected = "n_of: m must be > 0")]
713 fn n_of_m_rejects_zero_m() {
714 let node = NOf::new(0, 0);
717 let mut out = [Value::None];
718 node.eval(&[Value::U64(0)], &mut out);
719 }
720
721 #[test]
722 #[should_panic(expected = "n_of: n (5) must be <= m (3)")]
723 fn n_of_m_rejects_n_greater_than_m() {
724 let node = NOf::new(5, 3);
725 let mut out = [Value::None];
726 node.eval(&[Value::U64(0)], &mut out);
727 }
728
729 #[test]
730 fn n_of_m_not_first_n() {
731 let node = NOf::new(1, 10);
733 let mut out = [Value::None];
734 let mut selected_positions = Vec::new();
735 for window in 0..20u64 {
736 for pos in 0..10u64 {
737 let input = window * 10 + pos;
738 node.eval(&[Value::U64(input)], &mut out);
739 if out[0].as_u64() == 1 {
740 selected_positions.push(pos);
741 }
742 }
743 }
744 let unique: std::collections::HashSet<u64> = selected_positions.iter().copied().collect();
748 assert!(
749 unique.len() > 1,
750 "n_of should select different positions across windows, got only {:?}",
751 unique
752 );
753 }
754
755 #[test]
758 fn one_of_selects_from_values() {
759 let node = OneOf::new(vec!["alpha".into(), "beta".into(), "gamma".into()]);
760 let mut out = [Value::None];
761 for i in 0..100u64 {
762 node.eval(&[Value::U64(i)], &mut out);
763 let s = out[0].as_str().to_string();
764 assert!(
765 s == "alpha" || s == "beta" || s == "gamma",
766 "one_of({i}) returned '{s}', expected one of alpha/beta/gamma"
767 );
768 }
769 }
770
771 #[test]
772 fn one_of_deterministic() {
773 let node = OneOf::new(vec!["x".into(), "y".into(), "z".into()]);
774 let mut out1 = [Value::None];
775 let mut out2 = [Value::None];
776 for i in 0..50u64 {
777 node.eval(&[Value::U64(i)], &mut out1);
778 node.eval(&[Value::U64(i)], &mut out2);
779 assert_eq!(out1[0].as_str(), out2[0].as_str());
780 }
781 }
782
783 #[test]
784 fn one_of_roughly_uniform() {
785 let values = vec!["a".into(), "b".into(), "c".into()];
786 let node = OneOf::new(values);
787 let mut out = [Value::None];
788 let mut counts = [0u64; 3];
789 let n = 9_000u64;
790 for i in 0..n {
791 node.eval(&[Value::U64(i)], &mut out);
792 match out[0].as_str() {
793 "a" => counts[0] += 1,
794 "b" => counts[1] += 1,
795 "c" => counts[2] += 1,
796 other => panic!("unexpected value: {other}"),
797 }
798 }
799 for (idx, &c) in counts.iter().enumerate() {
801 let ratio = c as f64 / n as f64;
802 assert!(
803 (0.25..=0.42).contains(&ratio),
804 "one_of bucket {idx} ratio {ratio} outside expected 0.25-0.42"
805 );
806 }
807 }
808
809 #[test]
810 fn one_of_single_value() {
811 let node = OneOf::new(vec!["only".into()]);
812 let mut out = [Value::None];
813 for i in 0..20u64 {
814 node.eval(&[Value::U64(i)], &mut out);
815 assert_eq!(out[0].as_str(), "only");
816 }
817 }
818
819 #[test]
820 #[should_panic(expected = "one_of: values must be non-empty")]
821 fn one_of_rejects_empty() {
822 let node = OneOf::new(vec![]);
826 let mut out = [Value::None];
827 node.eval(&[Value::U64(0)], &mut out);
828 }
829
830 #[test]
833 fn one_of_weighted_selects_from_spec() {
834 let node = OneOfWeighted::new("red:60,blue:30,green:10".to_string());
835 let mut out = [Value::None];
836 for i in 0..100u64 {
837 node.eval(&[Value::U64(i)], &mut out);
838 let s = out[0].as_str().to_string();
839 assert!(
840 s == "red" || s == "blue" || s == "green",
841 "one_of_weighted({i}) returned '{s}'"
842 );
843 }
844 }
845
846 #[test]
847 fn one_of_weighted_deterministic() {
848 let node = OneOfWeighted::new("a:50,b:50".to_string());
849 let mut out1 = [Value::None];
850 let mut out2 = [Value::None];
851 for i in 0..50u64 {
852 node.eval(&[Value::U64(i)], &mut out1);
853 node.eval(&[Value::U64(i)], &mut out2);
854 assert_eq!(out1[0].as_str(), out2[0].as_str());
855 }
856 }
857
858 #[test]
859 fn one_of_weighted_respects_weights() {
860 let node = OneOfWeighted::new("heavy:90,light:10".to_string());
861 let mut out = [Value::None];
862 let mut heavy = 0u64;
863 let n = 10_000u64;
864 for i in 0..n {
865 node.eval(&[Value::U64(i)], &mut out);
866 if out[0].as_str() == "heavy" {
867 heavy += 1;
868 }
869 }
870 let ratio = heavy as f64 / n as f64;
871 assert!(
873 (0.80..=0.97).contains(&ratio),
874 "one_of_weighted heavy ratio {ratio} outside expected 0.80-0.97"
875 );
876 }
877
878 #[test]
879 fn one_of_weighted_single_value() {
880 let node = OneOfWeighted::new("only:1".to_string());
881 let mut out = [Value::None];
882 for i in 0..20u64 {
883 node.eval(&[Value::U64(i)], &mut out);
884 assert_eq!(out[0].as_str(), "only");
885 }
886 }
887
888 #[test]
889 fn one_of_weighted_semicolon_delimiter() {
890 let node = OneOfWeighted::new("x:50;y:50".to_string());
891 let mut out = [Value::None];
892 node.eval(&[Value::U64(0)], &mut out);
893 let s = out[0].as_str().to_string();
894 assert!(s == "x" || s == "y");
895 }
896
897 #[test]
898 #[should_panic(expected = "one_of_weighted: spec must be non-empty")]
899 fn one_of_weighted_rejects_empty() {
900 OneOfWeighted::new("".to_string());
901 }
902
903 #[test]
904 #[should_panic(expected = "one_of_weighted: expected 'value:weight'")]
905 fn one_of_weighted_rejects_bad_format() {
906 OneOfWeighted::new("noweight".to_string());
907 }
908
909 #[test]
912 fn blend_pure_a_when_mix_is_0() {
913 let node = Blend::new(0.0);
914 let a: f64 = 10.0;
915 let b: f64 = 20.0;
916 let mut out = [Value::None];
917 node.eval(
918 &[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
919 &mut out,
920 );
921 let result = f64::from_bits(out[0].as_u64());
922 assert!(
923 (result - 10.0).abs() < 1e-10,
924 "blend(mix=0) should return a, got {result}"
925 );
926 }
927
928 #[test]
929 fn blend_pure_b_when_mix_is_1() {
930 let node = Blend::new(1.0);
931 let a: f64 = 10.0;
932 let b: f64 = 20.0;
933 let mut out = [Value::None];
934 node.eval(
935 &[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
936 &mut out,
937 );
938 let result = f64::from_bits(out[0].as_u64());
939 assert!(
940 (result - 20.0).abs() < 1e-10,
941 "blend(mix=1) should return b, got {result}"
942 );
943 }
944
945 #[test]
946 fn blend_half_mix() {
947 let node = Blend::new(0.5);
948 let a: f64 = 10.0;
949 let b: f64 = 20.0;
950 let mut out = [Value::None];
951 node.eval(
952 &[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
953 &mut out,
954 );
955 let result = f64::from_bits(out[0].as_u64());
956 assert!(
957 (result - 15.0).abs() < 1e-10,
958 "blend(mix=0.5) of 10.0 and 20.0 should be 15.0, got {result}"
959 );
960 }
961
962 #[test]
963 fn blend_quarter_mix() {
964 let node = Blend::new(0.25);
965 let a: f64 = 0.0;
966 let b: f64 = 100.0;
967 let mut out = [Value::None];
968 node.eval(
969 &[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
970 &mut out,
971 );
972 let result = f64::from_bits(out[0].as_u64());
973 assert!(
974 (result - 25.0).abs() < 1e-10,
975 "blend(mix=0.25) of 0.0 and 100.0 should be 25.0, got {result}"
976 );
977 }
978
979 #[test]
980 fn blend_compiled_u64() {
981 let node = Blend::new(0.5);
982 let compiled = node.compiled_u64().expect("should have compiled_u64");
983 let a: f64 = 10.0;
984 let b: f64 = 20.0;
985 let inputs = [a.to_bits(), b.to_bits()];
986 let mut outputs = [0u64];
987 compiled(&inputs, &mut outputs);
988 let result = f64::from_bits(outputs[0]);
989 assert!((result - 15.0).abs() < 1e-10);
990
991 let mut eval_out = [Value::None];
993 node.eval(
994 &[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
995 &mut eval_out,
996 );
997 assert_eq!(outputs[0], eval_out[0].as_u64());
998 }
999
1000 #[test]
1001 #[should_panic(expected = "blend: mix must be in [0.0, 1.0]")]
1002 fn blend_rejects_invalid_mix() {
1003 let node = Blend::new(1.5);
1005 let mut out = [Value::None];
1006 node.eval(&[Value::U64(0), Value::U64(0)], &mut out);
1007 }
1008
1009 #[test]
1010 #[should_panic(expected = "blend: mix must be in [0.0, 1.0]")]
1011 fn blend_rejects_negative_mix() {
1012 let node = Blend::new(-0.1);
1013 let mut out = [Value::None];
1014 node.eval(&[Value::U64(0), Value::U64(0)], &mut out);
1015 }
1016
1017 #[test]
1020 fn default_or_returns_value_when_not_none() {
1021 let node = DefaultOr::new(PortType::Str, PortType::Str);
1025 let mut out = [Value::None];
1026 node.eval(
1027 &[Value::Str("alice".into()), Value::Str("fallback".into())],
1028 &mut out,
1029 );
1030 assert_eq!(out[0].as_str(), "alice");
1031 }
1032
1033 #[test]
1034 fn default_or_returns_fallback_when_none() {
1035 let node = DefaultOr::new(PortType::Str, PortType::Str);
1036 let mut out = [Value::None];
1037 node.eval(&[Value::None, Value::Str("fallback".into())], &mut out);
1038 assert_eq!(out[0].as_str(), "fallback");
1039 }
1040
1041 #[test]
1042 fn default_or_works_with_u64() {
1043 let node = DefaultOr::new(PortType::U64, PortType::U64);
1044 let mut out = [Value::None];
1045 node.eval(&[Value::U64(42), Value::U64(0)], &mut out);
1047 assert!(matches!(out[0], Value::U64(42)));
1048 node.eval(&[Value::None, Value::U64(99)], &mut out);
1050 assert!(matches!(out[0], Value::U64(99)));
1051 }
1052
1053 #[test]
1054 fn default_or_with_extern_input() {
1055 use polydat::compile::assembly::{PolydatAssembler, WireRef};
1058 use polydat::library::identity::PortPassthrough;
1059
1060 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
1061 asm.add_input(
1063 "captured_name",
1064 Value::None,
1065 PortType::Str,
1066 polydat::kernel::InputKind::ExternalWrite,
1067 );
1068 asm.add_node(
1070 "__port_captured_name",
1071 Box::new(PortPassthrough::new("captured_name", PortType::Str)),
1072 vec![WireRef::input("captured_name")],
1073 );
1074 asm.add_node(
1076 "fallback",
1077 Box::new(polydat::library::identity::ConstStr::new(
1078 "anonymous".to_string(),
1079 )),
1080 vec![],
1081 );
1082 asm.add_node(
1084 "greeting",
1085 Box::new(DefaultOr::new(PortType::Str, PortType::Str)),
1086 vec![
1087 WireRef::node("__port_captured_name"),
1088 WireRef::node("fallback"),
1089 ],
1090 );
1091 asm.add_output("greeting", WireRef::node("greeting"));
1092
1093 let kernel = asm.compile().unwrap();
1094 let program = kernel.into_program();
1095 let mut state = program.create_state();
1096
1097 state.set_inputs(&[0]);
1099 let val = state.pull(&program, "greeting");
1100 assert_eq!(
1101 val.to_display_string(),
1102 "anonymous",
1103 "unset extern should produce fallback, got: {:?}",
1104 val
1105 );
1106
1107 let input_idx = program.find_input("captured_name").unwrap();
1109 state.set_input(input_idx, Value::Str("alice".into()));
1110 let val = state.pull(&program, "greeting");
1111 assert_eq!(
1112 val.to_display_string(),
1113 "alice",
1114 "set extern should produce captured value, got: {:?}",
1115 val
1116 );
1117
1118 state.reset_inputs_from(program.coord_count());
1120 let val = state.pull(&program, "greeting");
1121 assert_eq!(
1122 val.to_display_string(),
1123 "anonymous",
1124 "reset extern should produce fallback again, got: {:?}",
1125 val
1126 );
1127 }
1128}