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, #[constraint(RangeF64 { min: 0.0, max: 1.0 })] p: Const<f64>) -> u64 {
89 let h = crate::hash::splitmix64_u64(input);
95 let unit = hash_to_unit(h);
96 if unit < *p { 1 } else { 0 }
97}
98
99#[polydat::polydat_node(category = Probability)]
120fn select(cond: u64, if_true: u64, if_false: u64) -> u64 {
121 if cond != 0 { if_true } else { if_false }
122}
123
124#[polydat::polydat_node(category = Probability)]
145fn chance(input: u64, #[constraint(RangeF64 { min: 0.0, max: 1.0 })] p: Const<f64>) -> u64 {
146 let h = crate::hash::splitmix64_u64(input);
150 let unit = hash_to_unit(h);
151 let result: f64 = if unit < *p { 1.0 } else { 0.0 };
152 result.to_bits()
153}
154
155#[polydat::polydat_node(category = Probability, validate = n_of_validate)]
195fn n_of(
196 input: u64,
197 n: Const<u64>,
198 #[constraint(RangeU64 { min: 1, max: 65536 })] m: Const<u64>,
199) -> u64 {
200 n_of_m_eval(input, *n, *m)
211}
212
213fn n_of_validate(_name: &str, consts: &[polydat::dsl::factory::ConstArg]) -> Result<(), String> {
216 let n = consts.first().map(|c| c.as_u64()).unwrap_or(0);
217 let m = consts.get(1).map(|c| c.as_u64()).unwrap_or(0);
218 if n > m {
219 return Err(format!("n ({n}) must be <= m ({m})"));
220 }
221 Ok(())
222}
223
224pub use polydat::numeric::n_of_m::n_of_m_eval;
225
226#[polydat::polydat_node(category = Probability)]
252fn one_of(input: u64, values: Const<&[String]>) -> String {
253 assert!(!values.is_empty(), "one_of: values must be non-empty");
254 let h = crate::hash::splitmix64_u64(input);
255 let idx = (h % values.len() as u64) as usize;
256 values[idx].clone()
257}
258
259pub struct WeightedTable {
274 pub values: Vec<String>,
276 pub cumulative: Vec<f64>,
279}
280
281impl PolydatSetup for WeightedTable {}
282
283impl WeightedTable {
284 pub fn parse(spec: &str) -> Self {
288 let mut values = Vec::new();
289 let mut weights = Vec::new();
290 for elem in spec.split([';', ',']) {
291 let elem = elem.trim();
292 if elem.is_empty() {
293 continue;
294 }
295 let parts: Vec<&str> = elem.splitn(2, ':').collect();
296 assert_eq!(
297 parts.len(),
298 2,
299 "one_of_weighted: expected 'value:weight', got '{elem}'"
300 );
301 values.push(parts[0].to_string());
302 let w: f64 = parts[1].parse().expect("one_of_weighted: invalid weight");
303 assert!(w > 0.0, "one_of_weighted: weight must be positive, got {w}");
304 weights.push(w);
305 }
306 assert!(
307 !values.is_empty(),
308 "one_of_weighted: spec must be non-empty"
309 );
310
311 let total: f64 = weights.iter().sum();
312 assert!(total > 0.0, "one_of_weighted: total weight must be > 0");
313
314 let mut cumulative = Vec::with_capacity(weights.len());
315 let mut running = 0.0;
316 for w in &weights {
317 running += w / total;
318 cumulative.push(running);
319 }
320 if let Some(last) = cumulative.last_mut() {
322 *last = 1.0;
323 }
324
325 Self { values, cumulative }
326 }
327}
328
329#[polydat::polydat_node(category = Probability)]
353fn one_of_weighted(
354 input: u64,
355 spec: Const<&str>,
356 #[poly_const(WeightedTable::parse, from = spec)] table: &WeightedTable,
357) -> String {
358 let _ = spec;
359 let h = crate::hash::splitmix64_u64(input);
360 let unit = hash_to_unit(h);
361 let idx = match table
363 .cumulative
364 .binary_search_by(|c| c.partial_cmp(&unit).unwrap())
365 {
366 Ok(i) => i,
367 Err(i) => i,
368 };
369 let idx = idx.min(table.values.len() - 1);
371 table.values[idx].clone()
372}
373
374#[polydat::polydat_node(category = Probability)]
396fn blend(a: u64, b: u64, #[constraint(RangeF64 { min: 0.0, max: 1.0 })] mix: Const<f64>) -> u64 {
397 let a_f = f64::from_bits(a);
400 let b_f = f64::from_bits(b);
401 let result = a_f * (1.0 - *mix) + b_f * *mix;
402 result.to_bits()
403}
404
405#[polydat::polydat_node(category = Probability)]
427fn default_or(value: Value, fallback: Value) -> Value {
428 if matches!(value, Value::None) {
429 fallback
430 } else {
431 value
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 fn build_probability_node(name: &str, wires: usize, c: f64) -> Result<(), String> {
443 use polydat::ast::PortType;
444 use polydat::compile::assembly::WireRef;
445 let refs: Vec<WireRef> = (0..wires)
446 .map(|i| WireRef::input(format!("w{i}")))
447 .collect();
448 let types: Vec<PortType> = vec![PortType::U64; wires];
449 polydat::dsl::factory::build_node(
450 &polydat::dsl::factory::BuildContext::default(),
451 name,
452 &refs,
453 &types,
454 &[polydat::dsl::ConstArg::Float(c)],
455 )
456 .map(|_| ())
457 }
458
459 #[test]
462 fn fair_coin_returns_0_or_1() {
463 let node = FairCoin::new();
464 let mut out = [Value::None];
465 for i in 0..100u64 {
466 node.eval(&[Value::U64(i)], &mut out);
467 let v = out[0].as_u64();
468 assert!(
469 v == 0 || v == 1,
470 "fair_coin({i}) returned {v}, expected 0 or 1"
471 );
472 }
473 }
474
475 #[test]
476 fn fair_coin_deterministic() {
477 let node = FairCoin::new();
478 let mut out1 = [Value::None];
479 let mut out2 = [Value::None];
480 node.eval(&[Value::U64(42)], &mut out1);
481 node.eval(&[Value::U64(42)], &mut out2);
482 assert_eq!(out1[0].as_u64(), out2[0].as_u64());
483 }
484
485 #[test]
486 fn fair_coin_roughly_balanced() {
487 let node = FairCoin::new();
488 let mut out = [Value::None];
489 let mut ones = 0u64;
490 let n = 10_000u64;
491 for i in 0..n {
492 node.eval(&[Value::U64(i)], &mut out);
493 ones += out[0].as_u64();
494 }
495 let ratio = ones as f64 / n as f64;
497 assert!(
498 (0.45..=0.55).contains(&ratio),
499 "fair_coin ratio {ratio} outside expected 0.45-0.55"
500 );
501 }
502
503 #[test]
504 fn fair_coin_compiled_u64() {
505 let node = FairCoin::new();
506 let compiled = node.compiled_u64().expect("should have compiled_u64");
507 let inputs = [42u64];
508 let mut outputs = [0u64];
509 compiled(&inputs, &mut outputs);
510 assert!(outputs[0] == 0 || outputs[0] == 1);
511
512 let mut eval_out = [Value::None];
514 node.eval(&[Value::U64(42)], &mut eval_out);
515 assert_eq!(outputs[0], eval_out[0].as_u64());
516 }
517
518 #[test]
521 fn unfair_coin_always_0_when_p_is_0() {
522 let node = UnfairCoin::new(0.0);
523 let mut out = [Value::None];
524 for i in 0..100u64 {
525 node.eval(&[Value::U64(i)], &mut out);
526 assert_eq!(
527 out[0].as_u64(),
528 0,
529 "unfair_coin(p=0.0) should always return 0"
530 );
531 }
532 }
533
534 #[test]
535 fn unfair_coin_always_1_when_p_is_1() {
536 let node = UnfairCoin::new(1.0);
537 let mut out = [Value::None];
538 for i in 0..100u64 {
539 node.eval(&[Value::U64(i)], &mut out);
540 assert_eq!(
541 out[0].as_u64(),
542 1,
543 "unfair_coin(p=1.0) should always return 1"
544 );
545 }
546 }
547
548 #[test]
549 fn unfair_coin_respects_probability() {
550 let node = UnfairCoin::new(0.2);
551 let mut out = [Value::None];
552 let mut ones = 0u64;
553 let n = 10_000u64;
554 for i in 0..n {
555 node.eval(&[Value::U64(i)], &mut out);
556 ones += out[0].as_u64();
557 }
558 let ratio = ones as f64 / n as f64;
559 assert!(
560 (0.15..=0.25).contains(&ratio),
561 "unfair_coin(p=0.2) ratio {ratio} outside expected 0.15-0.25"
562 );
563 }
564
565 #[test]
566 fn unfair_coin_compiled_u64() {
567 let node = UnfairCoin::new(0.5);
568 let compiled = node.compiled_u64().expect("should have compiled_u64");
569 let inputs = [42u64];
570 let mut outputs = [0u64];
571 compiled(&inputs, &mut outputs);
572 assert!(outputs[0] == 0 || outputs[0] == 1);
573
574 let mut eval_out = [Value::None];
575 node.eval(&[Value::U64(42)], &mut eval_out);
576 assert_eq!(outputs[0], eval_out[0].as_u64());
577 }
578
579 #[test]
583 fn unfair_coin_rejects_invalid_p() {
584 for bad in [1.5f64, -0.1] {
585 let built = build_probability_node("unfair_coin", 1, bad);
586 let err = built.expect_err(&format!("p={bad} must be refused"));
587 assert!(err.contains("p"), "the parameter is named: {err}");
588 }
589 assert!(build_probability_node("unfair_coin", 1, 0.5).is_ok());
590 }
591
592 #[test]
595 fn select_true_branch() {
596 let node = Select::new();
597 let mut out = [Value::None];
598 node.eval(&[Value::U64(1), Value::U64(100), Value::U64(200)], &mut out);
599 assert_eq!(out[0].as_u64(), 100);
600 }
601
602 #[test]
603 fn select_false_branch() {
604 let node = Select::new();
605 let mut out = [Value::None];
606 node.eval(&[Value::U64(0), Value::U64(100), Value::U64(200)], &mut out);
607 assert_eq!(out[0].as_u64(), 200);
608 }
609
610 #[test]
611 fn select_nonzero_is_true() {
612 let node = Select::new();
613 let mut out = [Value::None];
614 node.eval(&[Value::U64(999), Value::U64(10), Value::U64(20)], &mut out);
616 assert_eq!(out[0].as_u64(), 10);
617 }
618
619 #[test]
620 fn select_compiled_u64() {
621 let node = Select::new();
622 let compiled = node.compiled_u64().expect("should have compiled_u64");
623 let mut outputs = [0u64];
624
625 compiled(&[1, 100, 200], &mut outputs);
626 assert_eq!(outputs[0], 100);
627
628 compiled(&[0, 100, 200], &mut outputs);
629 assert_eq!(outputs[0], 200);
630 }
631
632 #[test]
635 fn chance_returns_f64_bits() {
636 let node = Chance::new(0.5);
637 let mut out = [Value::None];
638 for i in 0..100u64 {
639 node.eval(&[Value::U64(i)], &mut out);
640 let bits = out[0].as_u64();
641 let f = f64::from_bits(bits);
642 assert!(
643 f == 0.0 || f == 1.0,
644 "chance({i}) returned f64 {f}, expected 0.0 or 1.0"
645 );
646 }
647 }
648
649 #[test]
650 fn chance_always_0_when_p_is_0() {
651 let node = Chance::new(0.0);
652 let mut out = [Value::None];
653 for i in 0..100u64 {
654 node.eval(&[Value::U64(i)], &mut out);
655 let f = f64::from_bits(out[0].as_u64());
656 assert_eq!(f, 0.0);
657 }
658 }
659
660 #[test]
661 fn chance_always_1_when_p_is_1() {
662 let node = Chance::new(1.0);
663 let mut out = [Value::None];
664 for i in 0..100u64 {
665 node.eval(&[Value::U64(i)], &mut out);
666 let f = f64::from_bits(out[0].as_u64());
667 assert_eq!(f, 1.0);
668 }
669 }
670
671 #[test]
672 fn chance_compiled_u64() {
673 let node = Chance::new(0.5);
674 let compiled = node.compiled_u64().expect("should have compiled_u64");
675 let inputs = [42u64];
676 let mut outputs = [0u64];
677 compiled(&inputs, &mut outputs);
678 let f = f64::from_bits(outputs[0]);
679 assert!(f == 0.0 || f == 1.0);
680
681 let mut eval_out = [Value::None];
682 node.eval(&[Value::U64(42)], &mut eval_out);
683 assert_eq!(outputs[0], eval_out[0].as_u64());
684 }
685
686 #[test]
689 fn n_of_m_exact_count() {
690 let node = NOf::new(3, 10);
691 let mut out = [Value::None];
692 for window in 0..10u64 {
694 let mut count = 0u64;
695 for pos in 0..10u64 {
696 let input = window * 10 + pos;
697 node.eval(&[Value::U64(input)], &mut out);
698 count += out[0].as_u64();
699 }
700 assert_eq!(
701 count, 3,
702 "n_of(3, 10) window {window}: expected exactly 3 selected, got {count}"
703 );
704 }
705 }
706
707 #[test]
708 fn n_of_m_all_selected() {
709 let node = NOf::new(5, 5);
710 let mut out = [Value::None];
711 for i in 0..20u64 {
712 node.eval(&[Value::U64(i)], &mut out);
713 assert_eq!(out[0].as_u64(), 1, "n_of(5, 5) should always return 1");
714 }
715 }
716
717 #[test]
718 fn n_of_m_none_selected() {
719 let node = NOf::new(0, 5);
720 let mut out = [Value::None];
721 for i in 0..20u64 {
722 node.eval(&[Value::U64(i)], &mut out);
723 assert_eq!(out[0].as_u64(), 0, "n_of(0, 5) should always return 0");
724 }
725 }
726
727 #[test]
728 fn n_of_m_deterministic() {
729 let node = NOf::new(2, 7);
730 let mut out1 = [Value::None];
731 let mut out2 = [Value::None];
732 for i in 0..50u64 {
733 node.eval(&[Value::U64(i)], &mut out1);
734 node.eval(&[Value::U64(i)], &mut out2);
735 assert_eq!(out1[0].as_u64(), out2[0].as_u64());
736 }
737 }
738
739 #[test]
740 fn n_of_m_compiled_u64() {
741 let node = NOf::new(3, 10);
742 let compiled = node.compiled_u64().expect("should have compiled_u64");
743
744 for i in 0..10u64 {
746 let mut c_out = [0u64];
747 compiled(&[i], &mut c_out);
748
749 let mut e_out = [Value::None];
750 node.eval(&[Value::U64(i)], &mut e_out);
751
752 assert_eq!(
753 c_out[0],
754 e_out[0].as_u64(),
755 "compiled/eval mismatch at input {i}"
756 );
757 }
758 }
759
760 #[test]
766 fn n_of_m_not_first_n() {
767 let node = NOf::new(1, 10);
769 let mut out = [Value::None];
770 let mut selected_positions = Vec::new();
771 for window in 0..20u64 {
772 for pos in 0..10u64 {
773 let input = window * 10 + pos;
774 node.eval(&[Value::U64(input)], &mut out);
775 if out[0].as_u64() == 1 {
776 selected_positions.push(pos);
777 }
778 }
779 }
780 let unique: std::collections::HashSet<u64> = selected_positions.iter().copied().collect();
784 assert!(
785 unique.len() > 1,
786 "n_of should select different positions across windows, got only {:?}",
787 unique
788 );
789 }
790
791 #[test]
794 fn one_of_selects_from_values() {
795 let node = OneOf::new(vec!["alpha".into(), "beta".into(), "gamma".into()]);
796 let mut out = [Value::None];
797 for i in 0..100u64 {
798 node.eval(&[Value::U64(i)], &mut out);
799 let s = out[0].as_str().to_string();
800 assert!(
801 s == "alpha" || s == "beta" || s == "gamma",
802 "one_of({i}) returned '{s}', expected one of alpha/beta/gamma"
803 );
804 }
805 }
806
807 #[test]
808 fn one_of_deterministic() {
809 let node = OneOf::new(vec!["x".into(), "y".into(), "z".into()]);
810 let mut out1 = [Value::None];
811 let mut out2 = [Value::None];
812 for i in 0..50u64 {
813 node.eval(&[Value::U64(i)], &mut out1);
814 node.eval(&[Value::U64(i)], &mut out2);
815 assert_eq!(out1[0].as_str(), out2[0].as_str());
816 }
817 }
818
819 #[test]
820 fn one_of_roughly_uniform() {
821 let values = vec!["a".into(), "b".into(), "c".into()];
822 let node = OneOf::new(values);
823 let mut out = [Value::None];
824 let mut counts = [0u64; 3];
825 let n = 9_000u64;
826 for i in 0..n {
827 node.eval(&[Value::U64(i)], &mut out);
828 match out[0].as_str() {
829 "a" => counts[0] += 1,
830 "b" => counts[1] += 1,
831 "c" => counts[2] += 1,
832 other => panic!("unexpected value: {other}"),
833 }
834 }
835 for (idx, &c) in counts.iter().enumerate() {
837 let ratio = c as f64 / n as f64;
838 assert!(
839 (0.25..=0.42).contains(&ratio),
840 "one_of bucket {idx} ratio {ratio} outside expected 0.25-0.42"
841 );
842 }
843 }
844
845 #[test]
846 fn one_of_single_value() {
847 let node = OneOf::new(vec!["only".into()]);
848 let mut out = [Value::None];
849 for i in 0..20u64 {
850 node.eval(&[Value::U64(i)], &mut out);
851 assert_eq!(out[0].as_str(), "only");
852 }
853 }
854
855 #[test]
856 #[should_panic(expected = "one_of: values must be non-empty")]
857 fn one_of_rejects_empty() {
858 let node = OneOf::new(vec![]);
862 let mut out = [Value::None];
863 node.eval(&[Value::U64(0)], &mut out);
864 }
865
866 #[test]
869 fn one_of_weighted_selects_from_spec() {
870 let node = OneOfWeighted::new("red:60,blue:30,green:10".to_string());
871 let mut out = [Value::None];
872 for i in 0..100u64 {
873 node.eval(&[Value::U64(i)], &mut out);
874 let s = out[0].as_str().to_string();
875 assert!(
876 s == "red" || s == "blue" || s == "green",
877 "one_of_weighted({i}) returned '{s}'"
878 );
879 }
880 }
881
882 #[test]
883 fn one_of_weighted_deterministic() {
884 let node = OneOfWeighted::new("a:50,b:50".to_string());
885 let mut out1 = [Value::None];
886 let mut out2 = [Value::None];
887 for i in 0..50u64 {
888 node.eval(&[Value::U64(i)], &mut out1);
889 node.eval(&[Value::U64(i)], &mut out2);
890 assert_eq!(out1[0].as_str(), out2[0].as_str());
891 }
892 }
893
894 #[test]
895 fn one_of_weighted_respects_weights() {
896 let node = OneOfWeighted::new("heavy:90,light:10".to_string());
897 let mut out = [Value::None];
898 let mut heavy = 0u64;
899 let n = 10_000u64;
900 for i in 0..n {
901 node.eval(&[Value::U64(i)], &mut out);
902 if out[0].as_str() == "heavy" {
903 heavy += 1;
904 }
905 }
906 let ratio = heavy as f64 / n as f64;
907 assert!(
909 (0.80..=0.97).contains(&ratio),
910 "one_of_weighted heavy ratio {ratio} outside expected 0.80-0.97"
911 );
912 }
913
914 #[test]
915 fn one_of_weighted_single_value() {
916 let node = OneOfWeighted::new("only:1".to_string());
917 let mut out = [Value::None];
918 for i in 0..20u64 {
919 node.eval(&[Value::U64(i)], &mut out);
920 assert_eq!(out[0].as_str(), "only");
921 }
922 }
923
924 #[test]
925 fn one_of_weighted_semicolon_delimiter() {
926 let node = OneOfWeighted::new("x:50;y:50".to_string());
927 let mut out = [Value::None];
928 node.eval(&[Value::U64(0)], &mut out);
929 let s = out[0].as_str().to_string();
930 assert!(s == "x" || s == "y");
931 }
932
933 #[test]
934 #[should_panic(expected = "one_of_weighted: spec must be non-empty")]
935 fn one_of_weighted_rejects_empty() {
936 OneOfWeighted::new("".to_string());
937 }
938
939 #[test]
940 #[should_panic(expected = "one_of_weighted: expected 'value:weight'")]
941 fn one_of_weighted_rejects_bad_format() {
942 OneOfWeighted::new("noweight".to_string());
943 }
944
945 #[test]
948 fn blend_pure_a_when_mix_is_0() {
949 let node = Blend::new(0.0);
950 let a: f64 = 10.0;
951 let b: f64 = 20.0;
952 let mut out = [Value::None];
953 node.eval(
954 &[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
955 &mut out,
956 );
957 let result = f64::from_bits(out[0].as_u64());
958 assert!(
959 (result - 10.0).abs() < 1e-10,
960 "blend(mix=0) should return a, got {result}"
961 );
962 }
963
964 #[test]
965 fn blend_pure_b_when_mix_is_1() {
966 let node = Blend::new(1.0);
967 let a: f64 = 10.0;
968 let b: f64 = 20.0;
969 let mut out = [Value::None];
970 node.eval(
971 &[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
972 &mut out,
973 );
974 let result = f64::from_bits(out[0].as_u64());
975 assert!(
976 (result - 20.0).abs() < 1e-10,
977 "blend(mix=1) should return b, got {result}"
978 );
979 }
980
981 #[test]
982 fn blend_half_mix() {
983 let node = Blend::new(0.5);
984 let a: f64 = 10.0;
985 let b: f64 = 20.0;
986 let mut out = [Value::None];
987 node.eval(
988 &[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
989 &mut out,
990 );
991 let result = f64::from_bits(out[0].as_u64());
992 assert!(
993 (result - 15.0).abs() < 1e-10,
994 "blend(mix=0.5) of 10.0 and 20.0 should be 15.0, got {result}"
995 );
996 }
997
998 #[test]
999 fn blend_quarter_mix() {
1000 let node = Blend::new(0.25);
1001 let a: f64 = 0.0;
1002 let b: f64 = 100.0;
1003 let mut out = [Value::None];
1004 node.eval(
1005 &[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
1006 &mut out,
1007 );
1008 let result = f64::from_bits(out[0].as_u64());
1009 assert!(
1010 (result - 25.0).abs() < 1e-10,
1011 "blend(mix=0.25) of 0.0 and 100.0 should be 25.0, got {result}"
1012 );
1013 }
1014
1015 #[test]
1016 fn blend_compiled_u64() {
1017 let node = Blend::new(0.5);
1018 let compiled = node.compiled_u64().expect("should have compiled_u64");
1019 let a: f64 = 10.0;
1020 let b: f64 = 20.0;
1021 let inputs = [a.to_bits(), b.to_bits()];
1022 let mut outputs = [0u64];
1023 compiled(&inputs, &mut outputs);
1024 let result = f64::from_bits(outputs[0]);
1025 assert!((result - 15.0).abs() < 1e-10);
1026
1027 let mut eval_out = [Value::None];
1029 node.eval(
1030 &[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
1031 &mut eval_out,
1032 );
1033 assert_eq!(outputs[0], eval_out[0].as_u64());
1034 }
1035
1036 #[test]
1038 fn blend_rejects_a_mix_outside_the_unit_interval() {
1039 for bad in [1.5f64, -0.1] {
1040 let built = build_probability_node("blend", 2, bad);
1041 let err = built.expect_err(&format!("mix={bad} must be refused"));
1042 assert!(err.contains("mix"), "the parameter is named: {err}");
1043 }
1044 assert!(build_probability_node("blend", 2, 0.3).is_ok());
1045 }
1046
1047 #[test]
1050 fn default_or_returns_value_when_not_none() {
1051 let node = DefaultOr::new(PortType::Str, PortType::Str);
1055 let mut out = [Value::None];
1056 node.eval(
1057 &[Value::Str("alice".into()), Value::Str("fallback".into())],
1058 &mut out,
1059 );
1060 assert_eq!(out[0].as_str(), "alice");
1061 }
1062
1063 #[test]
1064 fn default_or_returns_fallback_when_none() {
1065 let node = DefaultOr::new(PortType::Str, PortType::Str);
1066 let mut out = [Value::None];
1067 node.eval(&[Value::None, Value::Str("fallback".into())], &mut out);
1068 assert_eq!(out[0].as_str(), "fallback");
1069 }
1070
1071 #[test]
1072 fn default_or_works_with_u64() {
1073 let node = DefaultOr::new(PortType::U64, PortType::U64);
1074 let mut out = [Value::None];
1075 node.eval(&[Value::U64(42), Value::U64(0)], &mut out);
1077 assert!(matches!(out[0], Value::U64(42)));
1078 node.eval(&[Value::None, Value::U64(99)], &mut out);
1080 assert!(matches!(out[0], Value::U64(99)));
1081 }
1082
1083 #[test]
1084 fn default_or_with_extern_input() {
1085 use polydat::compile::assembly::{PolydatAssembler, WireRef};
1088 use polydat::library::identity::PortPassthrough;
1089
1090 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
1091 asm.add_input(
1093 "captured_name",
1094 Value::None,
1095 PortType::Str,
1096 polydat::kernel::InputKind::ExternalWrite,
1097 );
1098 asm.add_node(
1100 "__port_captured_name",
1101 Box::new(PortPassthrough::new("captured_name", PortType::Str)),
1102 vec![WireRef::input("captured_name")],
1103 );
1104 asm.add_node(
1106 "fallback",
1107 Box::new(polydat::library::identity::ConstStr::new(
1108 "anonymous".to_string(),
1109 )),
1110 vec![],
1111 );
1112 asm.add_node(
1114 "greeting",
1115 Box::new(DefaultOr::new(PortType::Str, PortType::Str)),
1116 vec![
1117 WireRef::node("__port_captured_name"),
1118 WireRef::node("fallback"),
1119 ],
1120 );
1121 asm.add_output("greeting", WireRef::node("greeting"));
1122
1123 let kernel = asm.compile().unwrap();
1124 let program = kernel.into_program();
1125 let mut state = program.create_state();
1126
1127 state.set_inputs(&[0]);
1129 let val = state.pull(&program, "greeting");
1130 assert_eq!(
1131 val.to_display_string(),
1132 "anonymous",
1133 "unset extern should produce fallback, got: {:?}",
1134 val
1135 );
1136
1137 let input_idx = program.find_input("captured_name").unwrap();
1139 state.set_input(input_idx, Value::Str("alice".into()));
1140 let val = state.pull(&program, "greeting");
1141 assert_eq!(
1142 val.to_display_string(),
1143 "alice",
1144 "set extern should produce captured value, got: {:?}",
1145 val
1146 );
1147
1148 state.reset_inputs_from(program.coord_count());
1150 let val = state.pull(&program, "greeting");
1151 assert_eq!(
1152 val.to_display_string(),
1153 "anonymous",
1154 "reset extern should produce fallback again, got: {:?}",
1155 val
1156 );
1157 }
1158}
1159
1160#[cfg(test)]
1161mod const_constraint_tests {
1162 use polydat::dsl::compile::compile_polydat_kernel;
1163
1164 #[test]
1169 fn a_declared_const_constraint_refuses_at_assembly() {
1170 for (src, want) in [
1171 (
1172 "input cycle: u64\nx := chance(cycle, 1.5)\n",
1173 "p must be in [0, 1]",
1174 ),
1175 (
1176 "input cycle: u64\nx := n_of(cycle, 1, 0)\n",
1177 "m must be in [1, 65536]",
1178 ),
1179 ] {
1180 let err = compile_polydat_kernel(src)
1181 .err()
1182 .map(|e| e.to_string())
1183 .unwrap_or_else(|| panic!("should not build: {src}"));
1184 assert!(err.contains(want), "{src}: {err}");
1185 }
1186 for src in [
1188 "input cycle: u64\nx := chance(cycle, 0.3)\n",
1189 "input cycle: u64\nx := n_of(cycle, 3, 10)\n",
1190 ] {
1191 assert!(compile_polydat_kernel(src).is_ok(), "{src}");
1192 }
1193 }
1194
1195 #[test]
1202 fn a_relation_between_parameters_is_refused_on_every_engine() {
1203 use polydat::dsl::compile::compile_polydat_with;
1204 use polydat::{Engine, JitMode, Provenance};
1205
1206 let src = "input cycle: u64\nx := n_of(cycle, 9, 3)\n";
1207 for engine in [
1208 Engine::Interpreter(JitMode::Off),
1209 Engine::Closures(Provenance::Auto),
1210 Engine::Native(Provenance::Auto),
1211 ] {
1212 let err = compile_polydat_with(src, engine)
1213 .err()
1214 .map(|e| e.to_string())
1215 .unwrap_or_else(|| panic!("{engine:?} must refuse n > m, not build it"));
1216 assert!(err.contains("must be <= m"), "{engine:?}: {err}");
1217 }
1218 }
1219}