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