1use crate::sampling::alias::AliasTableU64;
30use polydat::ast::CompiledU64Op;
31use polydat::compile::fusion::{DecomposedGraph, DecomposedWire};
32use polydat::derive_support::Config;
33
34fn parse_weighted_str_spec(spec: &str) -> (Vec<String>, Vec<f64>) {
37 let mut values = Vec::new();
38 let mut weights = Vec::new();
39 for elem in spec.split([';', ',']) {
40 let elem = elem.trim();
41 if elem.is_empty() {
42 continue;
43 }
44 let parts: Vec<&str> = elem.splitn(2, ':').collect();
45 assert_eq!(parts.len(), 2, "expected 'value:weight', got '{elem}'");
46 values.push(parts[0].to_string());
47 weights.push(parts[1].parse::<f64>().expect("invalid weight"));
48 }
49 (values, weights)
50}
51
52fn parse_weighted_u64_spec(spec: &str) -> (Vec<u64>, Vec<f64>) {
53 let mut values = Vec::new();
54 let mut weights = Vec::new();
55 for elem in spec.split([';', ',']) {
56 let elem = elem.trim();
57 if elem.is_empty() {
58 continue;
59 }
60 let parts: Vec<&str> = elem.splitn(2, ':').collect();
61 assert_eq!(parts.len(), 2, "expected 'value:weight', got '{elem}'");
62 values.push(parts[0].parse::<u64>().expect("invalid value"));
63 weights.push(parts[1].parse::<f64>().expect("invalid weight"));
64 }
65 (values, weights)
66}
67
68pub struct WeightedStrCache {
76 values: Vec<String>,
77 table: AliasTableU64,
78}
79
80impl polydat::derive_support::PolydatSetup for WeightedStrCache {}
81
82fn build_weighted_str_cache(spec: &str) -> WeightedStrCache {
83 let (values, weights) = parse_weighted_str_spec(spec);
84 let table = AliasTableU64::from_weights(&weights);
85 WeightedStrCache { values, table }
86}
87
88#[polydat::polydat_node(category = Weighted)]
96fn weighted_strings(
97 input: u64,
98 spec: polydat::derive_support::Const<&str>,
99 #[poly_const(build_weighted_str_cache, from = spec)] cache: &WeightedStrCache,
100) -> String {
101 let _ = spec; let idx = cache.table.sample(input) as usize;
103 cache.values[idx].clone()
104}
105
106pub struct WeightedU64Cache {
114 values: Vec<u64>,
115 table: AliasTableU64,
116}
117
118impl polydat::derive_support::PolydatSetup for WeightedU64Cache {}
119
120fn build_weighted_u64_cache(spec: &str) -> WeightedU64Cache {
121 let (values, weights) = parse_weighted_u64_spec(spec);
122 let table = AliasTableU64::from_weights(&weights);
123 WeightedU64Cache { values, table }
124}
125
126#[polydat::polydat_node(category = Weighted)]
132fn weighted_u64(
133 input: u64,
134 spec: polydat::derive_support::Const<&str>,
135 #[poly_const(build_weighted_u64_cache, from = spec)] cache: &WeightedU64Cache,
136) -> u64 {
137 let _ = spec; let idx = cache.table.sample(input) as usize;
139 cache.values[idx]
140}
141
142pub struct WeightedPickState {
158 pub table: AliasTableU64,
160 pub values: Vec<u64>,
162 pub weights: Vec<f64>,
164}
165
166impl polydat::derive_support::PolydatSetup for WeightedPickState {}
167
168fn parse_weighted_pick_spec(spec: &str) -> WeightedPickState {
174 let mut weights = Vec::new();
175 let mut values = Vec::new();
176 for entry in spec.split([';', ',']) {
177 let entry = entry.trim();
178 if entry.is_empty() {
179 continue;
180 }
181 let (v, w) = entry.split_once(':').unwrap_or_else(|| {
182 panic!("weighted_pick: malformed entry '{entry}', expected 'value:weight'")
183 });
184 let value: u64 = v
185 .trim()
186 .parse()
187 .unwrap_or_else(|_| panic!("weighted_pick: invalid value '{v}' in entry '{entry}'"));
188 let weight: f64 = w
189 .trim()
190 .parse()
191 .unwrap_or_else(|_| panic!("weighted_pick: invalid weight '{w}' in entry '{entry}'"));
192 assert!(
193 weight.is_finite() && weight > 0.0,
194 "weighted_pick: weight must be a positive finite f64, got {weight}",
195 );
196 values.push(value);
197 weights.push(weight);
198 }
199 assert!(
200 !weights.is_empty(),
201 "weighted_pick requires at least one entry in spec",
202 );
203 WeightedPickState {
204 table: AliasTableU64::from_weights(&weights),
205 values,
206 weights,
207 }
208}
209
210fn weighted_pick_jit(node: &WeightedPick) -> CompiledU64Op {
214 let values = node.state.values.clone();
215 let biases = node.state.table.biases().to_vec();
216 let primaries = node.state.table.primaries().to_vec();
217 let aliases = node.state.table.aliases().to_vec();
218 let n = values.len();
219 Box::new(move |inputs, outputs| {
220 let input = inputs[0];
221 let slot = (input as usize) % n;
222 let bias_test = ((input >> 32) as f64) / (u32::MAX as f64);
223 let index = if bias_test < biases[slot] {
224 primaries[slot]
225 } else {
226 aliases[slot]
227 };
228 outputs[0] = values[index as usize];
229 })
230}
231
232fn weighted_pick_jit_constants(node: &WeightedPick) -> Vec<u64> {
238 vec![
239 node.state.values.as_ptr() as u64,
240 node.state.table.biases().as_ptr() as u64,
241 node.state.table.primaries().as_ptr() as u64,
242 node.state.table.aliases().as_ptr() as u64,
243 node.state.values.len() as u64,
244 ]
245}
246
247fn weighted_pick_decompose(node: &WeightedPick) -> DecomposedGraph {
269 let spec: String = node
270 .state
271 .values
272 .iter()
273 .zip(node.state.weights.iter())
274 .map(|(v, w)| format!("{v}:{w}"))
275 .collect::<Vec<_>>()
276 .join(";");
277 let mut g = DecomposedGraph::new(1);
278 let wu = g.add_node(
279 Box::new(WeightedU64::new(spec)),
280 vec![DecomposedWire::Input(0)],
281 );
282 g.set_outputs(vec![DecomposedWire::Node(wu, 0)]);
283 g
284}
285
286#[polydat::polydat_node(
287 category = Weighted,
288 compiled_u64 = weighted_pick_jit,
289 jit_constants = weighted_pick_jit_constants,
290 decompose = weighted_pick_decompose,
291)]
292fn weighted_pick(
293 input: u64,
294 spec: polydat::derive_support::Const<&str>,
295 #[poly_const(parse_weighted_pick_spec, from = spec)] state: &WeightedPickState,
296) -> u64 {
297 let _ = spec; let idx = state.table.sample(input) as usize;
299 state.values[idx]
300}
301
302#[derive(Default)]
319pub struct DynamicWeightedMemo {
320 spec: String,
321 values: Vec<String>,
322 table: Option<AliasTableU64>,
323 parsed: bool,
324}
325
326impl DynamicWeightedMemo {
327 fn select(&mut self, spec: &str, selector: u64) -> &str {
330 if !self.parsed || self.spec != spec {
331 let (values, weights) = parse_weighted_str_spec(spec);
332 self.table = if values.is_empty() {
333 None
334 } else {
335 Some(AliasTableU64::from_weights(&weights))
336 };
337 self.values = values;
338 self.spec.clear();
339 self.spec.push_str(spec);
340 self.parsed = true;
341 }
342 match &self.table {
343 Some(table) => &self.values[table.sample(selector) as usize],
344 None => "",
345 }
346 }
347
348 #[cfg(test)]
350 fn spec(&self) -> Option<&str> {
351 self.parsed.then_some(self.spec.as_str())
352 }
353}
354
355pub(crate) mod dynamic_weighted_state {
357 use super::{DynamicWeightedMemo, DynamicWeightedSelect};
358 use polydat::ast::{ScratchBuf, ScratchElem, Value};
359
360 pub(crate) fn layout(_node: &DynamicWeightedSelect) -> Vec<ScratchElem> {
361 vec![ScratchElem::State]
362 }
363
364 pub(crate) fn eval(
365 _node: &DynamicWeightedSelect,
366 scratch: &mut [ScratchBuf],
367 inputs: &[Value],
368 outputs: &mut [Value],
369 ) {
370 let spec = inputs[1].to_display_string();
371 let memo = scratch[0]
372 .node_state()
373 .get_or_insert_with(DynamicWeightedMemo::default);
374 outputs[0] = Value::Str(memo.select(&spec, inputs[0].as_u64()).into());
375 }
376}
377
378fn dynamic_weighted_compiled(
382 _node: &DynamicWeightedSelect,
383 wire_types: &[polydat::ast::PortType],
384) -> polydat::ast::CompiledSlotKit {
385 use polydat::ast::{PortType, ScratchBuf, ScratchElem};
386 let spec_ty = wire_types.get(1).copied().unwrap_or(PortType::Str);
387 polydat::ast::CompiledSlotKit {
388 scratch: vec![ScratchElem::State, ScratchElem::Str],
389 op: Box::new(
390 move |inputs: &[u64], outputs: &mut [u64], scratch: &mut [ScratchBuf]| {
391 let selector = inputs[0];
392 let spec_value;
393 let spec: &str =
396 match unsafe { polydat::compile::marshal::arg_ref(spec_ty, &inputs[1..]) } {
397 polydat::ast::ValueRef::Str(s) => s,
398 other => {
399 spec_value = other.to_display_string();
400 &spec_value
401 }
402 };
403 let (state, out) = scratch.split_at_mut(1);
404 let memo = state[0]
405 .node_state()
406 .get_or_insert_with(DynamicWeightedMemo::default);
407 out[0].set_str(memo.select(spec, selector));
408 let (ptr, len) = out[0].ptr_len();
409 outputs[0] = ptr;
410 outputs[1] = len;
411 },
412 ),
413 }
414}
415
416#[polydat::polydat_node(
436 category = Weighted,
437 compiled_slot = dynamic_weighted_compiled,
438 state = dynamic_weighted_state
439)]
440fn dynamic_weighted_select(selector: u64, weights_spec: Config<std::sync::Arc<str>>) -> String {
441 DynamicWeightedMemo::default()
444 .select(weights_spec.0.as_ref(), selector)
445 .to_string()
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use polydat::ast::{ConstValue, PolydatNode, Slot, Value};
452 use polydat::compile::fusion::FusedNode;
453 use xxhash_rust::xxh3::xxh3_64;
454
455 #[test]
456 fn weighted_strings_valid_outputs() {
457 let node = WeightedStrings::new("alpha:0.3;beta:0.5;gamma:0.2".to_string());
458 let valid = ["alpha", "beta", "gamma"];
459 let mut out = [Value::None];
460 for i in 0..1000u64 {
461 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
462 assert!(valid.contains(&out[0].as_str()));
463 }
464 }
465
466 #[test]
467 fn weighted_strings_respects_weights() {
468 let node = WeightedStrings::new("rare:0.01;common:0.99".to_string());
469 let mut common_count = 0u64;
470 let mut out = [Value::None];
471 let n = 10_000u64;
472 for i in 0..n {
473 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
474 if out[0].as_str() == "common" {
475 common_count += 1;
476 }
477 }
478 let ratio = common_count as f64 / n as f64;
479 assert!(ratio > 0.90, "common should dominate, got {ratio}");
480 }
481
482 #[test]
483 fn weighted_u64_valid_outputs() {
484 let node = WeightedU64::new("10:0.5;20:0.3;30:0.2".to_string());
485 let valid = [10u64, 20, 30];
486 let mut out = [Value::None];
487 for i in 0..1000u64 {
488 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
489 assert!(valid.contains(&out[0].as_u64()));
490 }
491 }
492
493 #[test]
496 fn weighted_pick_valid_outputs() {
497 let node = WeightedPick::new("10:0.5;20:0.3;30:0.2".to_string());
498 let valid = [10u64, 20, 30];
499 let mut out = [Value::None];
500 for i in 0..1000u64 {
501 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
502 assert!(
503 valid.contains(&out[0].as_u64()),
504 "unexpected output {} at seed {i}",
505 out[0].as_u64()
506 );
507 }
508 }
509
510 #[test]
511 fn weighted_pick_respects_weights() {
512 let node = WeightedPick::new("1:0.99;2:0.01".to_string());
513 let mut count_1 = 0u64;
514 let mut out = [Value::None];
515 let n = 10_000u64;
516 for i in 0..n {
517 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
518 if out[0].as_u64() == 1 {
519 count_1 += 1;
520 }
521 }
522 let ratio = count_1 as f64 / n as f64;
523 assert!(
524 ratio > 0.90,
525 "value 1 (weight 0.99) should dominate, got {ratio}"
526 );
527 }
528
529 #[test]
530 fn weighted_pick_single_pair() {
531 let node = WeightedPick::new("42:1.0".to_string());
532 let mut out = [Value::None];
533 for i in 0..100u64 {
534 node.eval(&[Value::U64(i)], &mut out);
535 assert_eq!(out[0].as_u64(), 42);
536 }
537 }
538
539 #[test]
540 fn weighted_pick_equal_weights() {
541 let node = WeightedPick::new("10:1.0;20:1.0;30:1.0".to_string());
542 let mut counts = [0u64; 3];
543 let mut out = [Value::None];
544 let n = 30_000u64;
545 for i in 0..n {
546 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
547 match out[0].as_u64() {
548 10 => counts[0] += 1,
549 20 => counts[1] += 1,
550 30 => counts[2] += 1,
551 v => panic!("unexpected value {v}"),
552 }
553 }
554 for (i, c) in counts.iter().enumerate() {
556 let ratio = *c as f64 / n as f64;
557 assert!(
558 ratio > 0.25 && ratio < 0.42,
559 "value at index {i} has ratio {ratio}, expected ~0.33"
560 );
561 }
562 }
563
564 #[test]
565 fn weighted_pick_compiled_matches_eval() {
566 let node = WeightedPick::new("10:0.5;20:0.3;30:0.2".to_string());
567 let compiled = node.compiled_u64().expect("should compile");
568 for i in 0..10_000u64 {
569 let input = xxh3_64(&i.to_le_bytes());
570 let mut eval_out = [Value::None];
571 node.eval(&[Value::U64(input)], &mut eval_out);
572 let mut compiled_out = [0u64];
573 compiled(&[input], &mut compiled_out);
574 assert_eq!(
575 eval_out[0].as_u64(),
576 compiled_out[0],
577 "eval vs compiled mismatch at seed {i}"
578 );
579 }
580 }
581
582 #[test]
583 fn weighted_pick_jit_constants_shape() {
584 let node = WeightedPick::new("10:0.5;20:0.3;30:0.2".to_string());
588
589 let raw = node.jit_constants();
590 assert_eq!(raw.len(), 5); assert_eq!(raw[4], 3); assert_eq!(raw[0], node.state.values.as_ptr() as u64);
595 assert_eq!(raw[1], node.state.table.biases().as_ptr() as u64);
596 assert_eq!(raw[2], node.state.table.primaries().as_ptr() as u64);
597 assert_eq!(raw[3], node.state.table.aliases().as_ptr() as u64);
598 }
599
600 #[test]
601 fn weighted_pick_equivalence_with_weighted_u64() {
602 let fused = WeightedPick::new("10:0.5;20:0.3;30:0.2".to_string());
606 let decomposed = fused.decomposed();
607 for i in 0..10_000u64 {
608 let input = xxh3_64(&i.to_le_bytes());
609 let mut fused_out = [Value::None];
610 fused.eval(&[Value::U64(input)], &mut fused_out);
611 let decomposed_out = decomposed.eval(&[Value::U64(input)]);
612 assert_eq!(
613 fused_out[0].as_u64(),
614 decomposed_out[0].as_u64(),
615 "equivalence failed at seed {i}"
616 );
617 }
618 }
619
620 #[test]
621 #[should_panic(expected = "weighted_pick requires at least one entry in spec")]
622 fn weighted_pick_rejects_empty_spec() {
623 let _ = WeightedPick::new("".to_string());
626 }
627
628 #[test]
629 #[should_panic(expected = "weighted_pick: malformed entry")]
630 fn weighted_pick_rejects_bad_format() {
631 let _ = WeightedPick::new("noweight".to_string());
632 }
633
634 #[test]
635 #[should_panic(expected = "weighted_pick: weight must be a positive finite f64")]
636 fn weighted_pick_rejects_nonpositive_weight() {
637 let _ = WeightedPick::new("10:0.0;20:1.0".to_string());
638 }
639
640 #[test]
643 fn dynamic_weighted_select_basic() {
644 let node = DynamicWeightedSelect::new();
645 let spec = "alpha:0.3;beta:0.5;gamma:0.2";
646 let valid = ["alpha", "beta", "gamma"];
647 let mut out = [Value::None];
648 for i in 0..100u64 {
649 node.eval(
650 &[
651 Value::U64(xxh3_64(&i.to_le_bytes())),
652 Value::Str(spec.into()),
653 ],
654 &mut out,
655 );
656 assert!(
657 valid.contains(&out[0].as_str()),
658 "unexpected: {}",
659 out[0].as_str()
660 );
661 }
662 }
663
664 #[test]
665 fn dynamic_weighted_select_follows_its_spec() {
666 let node = DynamicWeightedSelect::new();
667 let spec = "a:0.5;b:0.5";
668 let mut out = [Value::None];
669 node.eval(&[Value::U64(42), Value::Str(spec.into())], &mut out);
670 let first = out[0].as_str().to_string();
671 node.eval(&[Value::U64(42), Value::Str(spec.into())], &mut out);
673 assert_eq!(out[0].as_str(), first);
674 node.eval(&[Value::U64(42), Value::Str("x:1.0".into())], &mut out);
676 assert_eq!(out[0].as_str(), "x");
677 }
678
679 #[test]
683 fn dynamic_weighted_select_memoizes_in_the_state() {
684 use polydat::ast::ScratchBuf;
685 let node = DynamicWeightedSelect::new();
686 let mut scratch: Vec<ScratchBuf> = node
687 .scratch_layout()
688 .iter()
689 .map(|e| ScratchBuf::new(*e))
690 .collect();
691 assert_eq!(scratch.len(), 1);
692 let mut out = [Value::None];
693 node.eval_in(
694 &mut scratch,
695 &[Value::U64(42), Value::Str("a:0.5;b:0.5".into())],
696 &mut out,
697 );
698 let first = out[0].as_str().to_string();
699 let memo = scratch[0]
700 .node_state()
701 .get::<DynamicWeightedMemo>()
702 .expect("filled on the first evaluation");
703 assert_eq!(memo.spec(), Some("a:0.5;b:0.5"));
704 let table_before = memo.table.as_ref().map(|t| t as *const AliasTableU64);
705 node.eval_in(
706 &mut scratch,
707 &[Value::U64(42), Value::Str("a:0.5;b:0.5".into())],
708 &mut out,
709 );
710 assert_eq!(out[0].as_str(), first);
711 let memo = scratch[0]
712 .node_state()
713 .get::<DynamicWeightedMemo>()
714 .unwrap();
715 assert_eq!(
716 memo.table.as_ref().map(|t| t as *const AliasTableU64),
717 table_before,
718 "the same spec keeps the table it built"
719 );
720 node.eval_in(
721 &mut scratch,
722 &[Value::U64(42), Value::Str("x:1.0".into())],
723 &mut out,
724 );
725 assert_eq!(out[0].as_str(), "x");
726 let memo = scratch[0]
727 .node_state()
728 .get::<DynamicWeightedMemo>()
729 .unwrap();
730 assert_eq!(memo.spec(), Some("x:1.0"));
731 let cloned = scratch[0].clone();
732 let mut cloned = cloned;
733 assert!(
734 cloned.node_state().get::<DynamicWeightedMemo>().is_none(),
735 "a clone of the state starts with an empty memo"
736 );
737 }
738
739 #[test]
742 fn dynamic_weighted_select_compiled_form_memoizes_and_agrees() {
743 use polydat::ast::{PortType, ScratchBuf};
744 let node = DynamicWeightedSelect::new();
745 let kit = dynamic_weighted_compiled(&node, &[PortType::U64, PortType::Str]);
746 let mut scratch: Vec<ScratchBuf> =
747 kit.scratch.iter().map(|e| ScratchBuf::new(*e)).collect();
748 let mut outputs = [0u64; 2];
749 for (spec, selector) in [("a:0.5;b:0.5", 42u64), ("a:0.5;b:0.5", 7), ("z:1.0", 3)] {
750 let inputs = [selector, spec.as_ptr() as usize as u64, spec.len() as u64];
751 (kit.op)(&inputs, &mut outputs, &mut scratch);
752 let got = scratch[1].to_value();
753 let mut want = [Value::None];
754 node.eval(&[Value::U64(selector), Value::Str(spec.into())], &mut want);
755 assert_eq!(
756 got.as_str(),
757 want[0].as_str(),
758 "spec {spec}, selector {selector}"
759 );
760 assert_eq!((outputs[0], outputs[1]), scratch[1].ptr_len());
761 assert_eq!(
762 scratch[0]
763 .node_state()
764 .get::<DynamicWeightedMemo>()
765 .and_then(|m| m.spec()),
766 Some(spec)
767 );
768 }
769 }
770
771 #[test]
772 fn dynamic_weighted_select_config_wire_annotation() {
773 let node = DynamicWeightedSelect::new();
774 let meta = node.meta();
775 let wire_inputs = meta.wire_inputs();
777 assert_eq!(wire_inputs.len(), 2);
778 assert_eq!(wire_inputs[0].wire_cost, polydat::ast::WireCost::Data);
779 assert_eq!(wire_inputs[1].wire_cost, polydat::ast::WireCost::Config);
780 }
781
782 #[test]
783 fn dynamic_weighted_select_e2e_init_config() {
784 use polydat::dsl::events::CompileEventLog;
786
787 let source = r#"
788 input cycle: u64
789 const spec := "alpha:0.3;beta:0.7"
790 result := dynamic_weighted_select(hash(cycle), spec)
791 "#;
792 let mut log = CompileEventLog::new();
793 let _k = polydat::dsl::compile::compile_polydat_with_log(source, &mut log).unwrap();
794
795 let warnings: Vec<_> = log
796 .events()
797 .iter()
798 .filter(|e| {
799 matches!(
800 e,
801 polydat::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
802 )
803 })
804 .collect();
805 assert!(warnings.is_empty(), "init-time config should not warn");
806 }
807
808 #[test]
809 fn dynamic_weighted_select_e2e_cycle_config_warns() {
810 use polydat::dsl::events::CompileEventLog;
812
813 let source = r#"
815 input cycle: u64
816 spec := format_u64(hash(cycle), 10)
817 result := dynamic_weighted_select(hash(cycle), spec)
818 "#;
819 let mut log = CompileEventLog::new();
820 let _k = polydat::dsl::compile::compile_polydat_with_log(source, &mut log).unwrap();
821
822 let warnings: Vec<_> = log
823 .events()
824 .iter()
825 .filter(|e| {
826 matches!(
827 e,
828 polydat::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
829 )
830 })
831 .collect();
832 assert_eq!(
833 warnings.len(),
834 1,
835 "cycle-time config should warn: {warnings:?}"
836 );
837 }
838
839 #[test]
840 fn dynamic_weighted_select_strict_rejects_cycle_config() {
841 use crate::hash::Hash;
843 use polydat::compile::assembly::{PolydatAssembler, WireRef};
844 use polydat::dsl::events::CompileEventLog;
845 use polydat::library::convert::U64ToString;
846
847 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
848 asm.add_node(
849 "hashed",
850 Box::new(Hash::new()),
851 vec![WireRef::input("cycle")],
852 );
853 asm.add_node(
854 "spec",
855 Box::new(U64ToString::default()),
856 vec![WireRef::node("hashed")],
857 );
858 asm.add_node(
859 "dws",
860 Box::new(DynamicWeightedSelect::new()),
861 vec![
862 WireRef::node("hashed"), WireRef::node("spec"), ],
865 );
866 asm.add_output("result", WireRef::node("dws"));
867
868 let mut log = CompileEventLog::new();
870 let _kernel = asm.compile_with_log(Some(&mut log)).unwrap();
871 let warnings: Vec<_> = log
872 .events()
873 .iter()
874 .filter(|e| {
875 matches!(
876 e,
877 polydat::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
878 )
879 })
880 .collect();
881 assert_eq!(warnings.len(), 1, "should warn in non-strict");
882
883 let mut asm2 = PolydatAssembler::new(vec!["cycle".into()]);
885 asm2.add_node(
886 "hashed",
887 Box::new(Hash::new()),
888 vec![WireRef::input("cycle")],
889 );
890 asm2.add_node(
891 "spec",
892 Box::new(U64ToString::default()),
893 vec![WireRef::node("hashed")],
894 );
895 asm2.add_node(
896 "dws",
897 Box::new(DynamicWeightedSelect::new()),
898 vec![WireRef::node("hashed"), WireRef::node("spec")],
899 );
900 asm2.add_output("result", WireRef::node("dws"));
901
902 asm2.set_strict(true);
903 let result = asm2.compile();
904 assert!(
905 result.is_err(),
906 "strict mode should reject cycle-time config wire"
907 );
908 let msg = format!("{}", result.unwrap_err());
909 assert!(
910 msg.contains("strict") || msg.contains("config"),
911 "error should mention strict or config: {msg}"
912 );
913 }
914
915 #[test]
916 fn weighted_pick_metadata_complete() {
917 let node = WeightedPick::new("10:0.5;20:0.3".to_string());
919 let meta = node.meta();
920
921 assert_eq!(meta.name, "weighted_pick");
923
924 assert_eq!(meta.ins.len(), 2);
926 assert!(matches!(meta.ins[0], Slot::Wire(_)));
927 assert!(matches!(
928 &meta.ins[1],
929 Slot::Const {
930 value: ConstValue::Str(_),
931 ..
932 }
933 ));
934
935 assert_eq!(meta.outs.len(), 1);
937
938 assert_eq!(meta.wire_inputs().len(), 1);
940
941 let consts = meta.const_slots();
943 assert_eq!(consts.len(), 1); }
945}