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