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 {
270 let spec: String = node
271 .state
272 .values
273 .iter()
274 .zip(node.state.weights.iter())
275 .map(|(v, w)| format!("{v}:{w}"))
276 .collect::<Vec<_>>()
277 .join(";");
278 let mut g = DecomposedGraph::new(1);
279 let wu = g.add_node(
280 Box::new(WeightedU64::new(spec)),
281 vec![DecomposedWire::Input(0)],
282 );
283 g.set_outputs(vec![DecomposedWire::Node(wu, 0)]);
284 g
285}
286
287#[polydat::polydat_node(
288 category = Weighted,
289 compiled_u64 = weighted_pick_jit,
290 jit_constants = weighted_pick_jit_constants,
291 decompose = weighted_pick_decompose,
292)]
293fn weighted_pick(
294 input: u64,
295 spec: polydat::derive_support::Const<&str>,
296 #[poly_const(parse_weighted_pick_spec, from = spec)] state: &WeightedPickState,
297) -> u64 {
298 let _ = spec; let idx = state.table.sample(input) as usize;
300 state.values[idx]
301}
302
303#[derive(Default)]
320pub struct DynamicWeightedMemo {
321 spec: String,
322 values: Vec<String>,
323 table: Option<AliasTableU64>,
324 parsed: bool,
325}
326
327impl DynamicWeightedMemo {
328 fn select(&mut self, spec: &str, selector: u64) -> &str {
331 if !self.parsed || self.spec != spec {
332 let (values, weights) = parse_weighted_str_spec(spec);
333 self.table = if values.is_empty() {
334 None
335 } else {
336 Some(AliasTableU64::from_weights(&weights))
337 };
338 self.values = values;
339 self.spec.clear();
340 self.spec.push_str(spec);
341 self.parsed = true;
342 }
343 match &self.table {
344 Some(table) => &self.values[table.sample(selector) as usize],
345 None => "",
346 }
347 }
348
349 #[cfg(test)]
351 fn spec(&self) -> Option<&str> {
352 self.parsed.then_some(self.spec.as_str())
353 }
354}
355
356pub(crate) mod dynamic_weighted_state {
358 use super::{DynamicWeightedMemo, DynamicWeightedSelect};
359 use polydat::ast::{ScratchBuf, ScratchElem, Value};
360
361 pub(crate) fn layout(_node: &DynamicWeightedSelect) -> Vec<ScratchElem> {
362 vec![ScratchElem::State]
363 }
364
365 pub(crate) fn eval(
366 _node: &DynamicWeightedSelect,
367 scratch: &mut [ScratchBuf],
368 inputs: &[Value],
369 outputs: &mut [Value],
370 ) {
371 let spec = inputs[1].to_display_string();
372 let memo = scratch[0]
373 .node_state()
374 .get_or_insert_with(DynamicWeightedMemo::default);
375 outputs[0] = Value::Str(memo.select(&spec, inputs[0].as_u64()).into());
376 }
377}
378
379fn dynamic_weighted_compiled(
383 _node: &DynamicWeightedSelect,
384 wire_types: &[polydat::ast::PortType],
385) -> polydat::ast::CompiledSlotKit {
386 use polydat::ast::{PortType, ScratchBuf, ScratchElem};
387 let spec_ty = wire_types.get(1).copied().unwrap_or(PortType::Str);
388 polydat::ast::CompiledSlotKit {
389 scratch: vec![ScratchElem::State, ScratchElem::Str],
390 op: Box::new(
391 move |inputs: &[u64], outputs: &mut [u64], scratch: &mut [ScratchBuf]| {
392 let selector = inputs[0];
393 let spec_value;
394 let spec: &str =
397 match unsafe { polydat::compile::marshal::arg_ref(spec_ty, &inputs[1..]) } {
398 polydat::ast::ValueRef::Str(s) => s,
399 other => {
400 spec_value = other.to_display_string();
401 &spec_value
402 }
403 };
404 let (state, out) = scratch.split_at_mut(1);
405 let memo = state[0]
406 .node_state()
407 .get_or_insert_with(DynamicWeightedMemo::default);
408 out[0].set_str(memo.select(spec, selector));
409 let (ptr, len) = out[0].ptr_len();
410 outputs[0] = ptr;
411 outputs[1] = len;
412 },
413 ),
414 }
415}
416
417#[polydat::polydat_node(
437 category = Weighted,
438 compiled_slot = dynamic_weighted_compiled,
439 state = dynamic_weighted_state
440)]
441fn dynamic_weighted_select(selector: u64, weights_spec: Config<std::sync::Arc<str>>) -> String {
442 DynamicWeightedMemo::default()
445 .select(weights_spec.0.as_ref(), selector)
446 .to_string()
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use polydat::ast::{ConstValue, PolydatNode, Slot, Value};
453 use polydat::compile::fusion::FusedNode;
454 use xxhash_rust::xxh3::xxh3_64;
455
456 #[test]
457 fn weighted_strings_valid_outputs() {
458 let node = WeightedStrings::new("alpha:0.3;beta:0.5;gamma:0.2".to_string());
459 let valid = ["alpha", "beta", "gamma"];
460 let mut out = [Value::None];
461 for i in 0..1000u64 {
462 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
463 assert!(valid.contains(&out[0].as_str()));
464 }
465 }
466
467 #[test]
468 fn weighted_strings_respects_weights() {
469 let node = WeightedStrings::new("rare:0.01;common:0.99".to_string());
470 let mut common_count = 0u64;
471 let mut out = [Value::None];
472 let n = 10_000u64;
473 for i in 0..n {
474 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
475 if out[0].as_str() == "common" {
476 common_count += 1;
477 }
478 }
479 let ratio = common_count as f64 / n as f64;
480 assert!(ratio > 0.90, "common should dominate, got {ratio}");
481 }
482
483 #[test]
484 fn weighted_u64_valid_outputs() {
485 let node = WeightedU64::new("10:0.5;20:0.3;30:0.2".to_string());
486 let valid = [10u64, 20, 30];
487 let mut out = [Value::None];
488 for i in 0..1000u64 {
489 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
490 assert!(valid.contains(&out[0].as_u64()));
491 }
492 }
493
494 #[test]
497 fn weighted_pick_valid_outputs() {
498 let node = WeightedPick::new("10:0.5;20:0.3;30:0.2".to_string());
499 let valid = [10u64, 20, 30];
500 let mut out = [Value::None];
501 for i in 0..1000u64 {
502 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
503 assert!(
504 valid.contains(&out[0].as_u64()),
505 "unexpected output {} at seed {i}",
506 out[0].as_u64()
507 );
508 }
509 }
510
511 #[test]
512 fn weighted_pick_respects_weights() {
513 let node = WeightedPick::new("1:0.99;2:0.01".to_string());
514 let mut count_1 = 0u64;
515 let mut out = [Value::None];
516 let n = 10_000u64;
517 for i in 0..n {
518 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
519 if out[0].as_u64() == 1 {
520 count_1 += 1;
521 }
522 }
523 let ratio = count_1 as f64 / n as f64;
524 assert!(
525 ratio > 0.90,
526 "value 1 (weight 0.99) should dominate, got {ratio}"
527 );
528 }
529
530 #[test]
531 fn weighted_pick_single_pair() {
532 let node = WeightedPick::new("42:1.0".to_string());
533 let mut out = [Value::None];
534 for i in 0..100u64 {
535 node.eval(&[Value::U64(i)], &mut out);
536 assert_eq!(out[0].as_u64(), 42);
537 }
538 }
539
540 #[test]
541 fn weighted_pick_equal_weights() {
542 let node = WeightedPick::new("10:1.0;20:1.0;30:1.0".to_string());
543 let mut counts = [0u64; 3];
544 let mut out = [Value::None];
545 let n = 30_000u64;
546 for i in 0..n {
547 node.eval(&[Value::U64(xxh3_64(&i.to_le_bytes()))], &mut out);
548 match out[0].as_u64() {
549 10 => counts[0] += 1,
550 20 => counts[1] += 1,
551 30 => counts[2] += 1,
552 v => panic!("unexpected value {v}"),
553 }
554 }
555 for (i, c) in counts.iter().enumerate() {
557 let ratio = *c as f64 / n as f64;
558 assert!(
559 ratio > 0.25 && ratio < 0.42,
560 "value at index {i} has ratio {ratio}, expected ~0.33"
561 );
562 }
563 }
564
565 #[test]
566 fn weighted_pick_compiled_matches_eval() {
567 let node = WeightedPick::new("10:0.5;20:0.3;30:0.2".to_string());
568 let compiled = node.compiled_u64().expect("should compile");
569 for i in 0..10_000u64 {
570 let input = xxh3_64(&i.to_le_bytes());
571 let mut eval_out = [Value::None];
572 node.eval(&[Value::U64(input)], &mut eval_out);
573 let mut compiled_out = [0u64];
574 compiled(&[input], &mut compiled_out);
575 assert_eq!(
576 eval_out[0].as_u64(),
577 compiled_out[0],
578 "eval vs compiled mismatch at seed {i}"
579 );
580 }
581 }
582
583 #[test]
584 fn weighted_pick_jit_constants_shape() {
585 let node = WeightedPick::new("10:0.5;20:0.3;30:0.2".to_string());
589
590 let raw = node.jit_constants();
591 assert_eq!(raw.len(), 5); assert_eq!(raw[4], 3); assert_eq!(raw[0], node.state.values.as_ptr() as u64);
596 assert_eq!(raw[1], node.state.table.biases().as_ptr() as u64);
597 assert_eq!(raw[2], node.state.table.primaries().as_ptr() as u64);
598 assert_eq!(raw[3], node.state.table.aliases().as_ptr() as u64);
599 }
600
601 #[test]
602 fn weighted_pick_equivalence_with_weighted_u64() {
603 let fused = WeightedPick::new("10:0.5;20:0.3;30:0.2".to_string());
607 let decomposed = fused.decomposed();
608 for i in 0..10_000u64 {
609 let input = xxh3_64(&i.to_le_bytes());
610 let mut fused_out = [Value::None];
611 fused.eval(&[Value::U64(input)], &mut fused_out);
612 let decomposed_out = decomposed.eval(&[Value::U64(input)]);
613 assert_eq!(
614 fused_out[0].as_u64(),
615 decomposed_out[0].as_u64(),
616 "equivalence failed at seed {i}"
617 );
618 }
619 }
620
621 #[test]
622 #[should_panic(expected = "weighted_pick requires at least one entry in spec")]
623 fn weighted_pick_rejects_empty_spec() {
624 let _ = WeightedPick::new("".to_string());
627 }
628
629 #[test]
630 #[should_panic(expected = "weighted_pick: malformed entry")]
631 fn weighted_pick_rejects_bad_format() {
632 let _ = WeightedPick::new("noweight".to_string());
633 }
634
635 #[test]
636 #[should_panic(expected = "weighted_pick: weight must be a positive finite f64")]
637 fn weighted_pick_rejects_nonpositive_weight() {
638 let _ = WeightedPick::new("10:0.0;20:1.0".to_string());
639 }
640
641 #[test]
644 fn dynamic_weighted_select_basic() {
645 let node = DynamicWeightedSelect::new();
646 let spec = "alpha:0.3;beta:0.5;gamma:0.2";
647 let valid = ["alpha", "beta", "gamma"];
648 let mut out = [Value::None];
649 for i in 0..100u64 {
650 node.eval(
651 &[
652 Value::U64(xxh3_64(&i.to_le_bytes())),
653 Value::Str(spec.into()),
654 ],
655 &mut out,
656 );
657 assert!(
658 valid.contains(&out[0].as_str()),
659 "unexpected: {}",
660 out[0].as_str()
661 );
662 }
663 }
664
665 #[test]
666 fn dynamic_weighted_select_follows_its_spec() {
667 let node = DynamicWeightedSelect::new();
668 let spec = "a:0.5;b:0.5";
669 let mut out = [Value::None];
670 node.eval(&[Value::U64(42), Value::Str(spec.into())], &mut out);
671 let first = out[0].as_str().to_string();
672 node.eval(&[Value::U64(42), Value::Str(spec.into())], &mut out);
674 assert_eq!(out[0].as_str(), first);
675 node.eval(&[Value::U64(42), Value::Str("x:1.0".into())], &mut out);
677 assert_eq!(out[0].as_str(), "x");
678 }
679
680 #[test]
684 fn dynamic_weighted_select_memoizes_in_the_state() {
685 use polydat::ast::ScratchBuf;
686 let node = DynamicWeightedSelect::new();
687 let mut scratch: Vec<ScratchBuf> = node
688 .scratch_layout()
689 .iter()
690 .map(|e| ScratchBuf::new(*e))
691 .collect();
692 assert_eq!(scratch.len(), 1);
693 let mut out = [Value::None];
694 node.eval_in(
695 &mut scratch,
696 &[Value::U64(42), Value::Str("a:0.5;b:0.5".into())],
697 &mut out,
698 );
699 let first = out[0].as_str().to_string();
700 let memo = scratch[0]
701 .node_state()
702 .get::<DynamicWeightedMemo>()
703 .expect("filled on the first evaluation");
704 assert_eq!(memo.spec(), Some("a:0.5;b:0.5"));
705 let table_before = memo.table.as_ref().map(|t| t as *const AliasTableU64);
706 node.eval_in(
707 &mut scratch,
708 &[Value::U64(42), Value::Str("a:0.5;b:0.5".into())],
709 &mut out,
710 );
711 assert_eq!(out[0].as_str(), first);
712 let memo = scratch[0]
713 .node_state()
714 .get::<DynamicWeightedMemo>()
715 .unwrap();
716 assert_eq!(
717 memo.table.as_ref().map(|t| t as *const AliasTableU64),
718 table_before,
719 "the same spec keeps the table it built"
720 );
721 node.eval_in(
722 &mut scratch,
723 &[Value::U64(42), Value::Str("x:1.0".into())],
724 &mut out,
725 );
726 assert_eq!(out[0].as_str(), "x");
727 let memo = scratch[0]
728 .node_state()
729 .get::<DynamicWeightedMemo>()
730 .unwrap();
731 assert_eq!(memo.spec(), Some("x:1.0"));
732 let cloned = scratch[0].clone();
733 let mut cloned = cloned;
734 assert!(
735 cloned.node_state().get::<DynamicWeightedMemo>().is_none(),
736 "a clone of the state starts with an empty memo"
737 );
738 }
739
740 #[test]
743 fn dynamic_weighted_select_compiled_form_memoizes_and_agrees() {
744 use polydat::ast::{PortType, ScratchBuf};
745 let node = DynamicWeightedSelect::new();
746 let kit = dynamic_weighted_compiled(&node, &[PortType::U64, PortType::Str]);
747 let mut scratch: Vec<ScratchBuf> =
748 kit.scratch.iter().map(|e| ScratchBuf::new(*e)).collect();
749 let mut outputs = [0u64; 2];
750 for (spec, selector) in [("a:0.5;b:0.5", 42u64), ("a:0.5;b:0.5", 7), ("z:1.0", 3)] {
751 let inputs = [selector, spec.as_ptr() as usize as u64, spec.len() as u64];
752 (kit.op)(&inputs, &mut outputs, &mut scratch);
753 let got = scratch[1].to_value();
754 let mut want = [Value::None];
755 node.eval(&[Value::U64(selector), Value::Str(spec.into())], &mut want);
756 assert_eq!(
757 got.as_str(),
758 want[0].as_str(),
759 "spec {spec}, selector {selector}"
760 );
761 assert_eq!((outputs[0], outputs[1]), scratch[1].ptr_len());
762 assert_eq!(
763 scratch[0]
764 .node_state()
765 .get::<DynamicWeightedMemo>()
766 .and_then(|m| m.spec()),
767 Some(spec)
768 );
769 }
770 }
771
772 #[test]
773 fn dynamic_weighted_select_config_wire_annotation() {
774 let node = DynamicWeightedSelect::new();
775 let meta = node.meta();
776 let wire_inputs = meta.wire_inputs();
778 assert_eq!(wire_inputs.len(), 2);
779 assert_eq!(wire_inputs[0].wire_cost, polydat::ast::WireCost::Data);
780 assert_eq!(wire_inputs[1].wire_cost, polydat::ast::WireCost::Config);
781 }
782
783 #[test]
784 fn dynamic_weighted_select_e2e_init_config() {
785 use polydat::dsl::events::CompileEventLog;
787
788 let source = r#"
789 input cycle: u64
790 const spec := "alpha:0.3;beta:0.7"
791 result := dynamic_weighted_select(hash(cycle), spec)
792 "#;
793 let mut log = CompileEventLog::new();
794 let _k = polydat::dsl::compile::compile_polydat_with_log(source, &mut log).unwrap();
795
796 let warnings: Vec<_> = log
797 .events()
798 .iter()
799 .filter(|e| {
800 matches!(
801 e,
802 polydat::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
803 )
804 })
805 .collect();
806 assert!(warnings.is_empty(), "init-time config should not warn");
807 }
808
809 #[test]
810 fn dynamic_weighted_select_e2e_cycle_config_warns() {
811 use polydat::dsl::events::CompileEventLog;
813
814 let source = r#"
816 input cycle: u64
817 spec := format_u64(hash(cycle), 10)
818 result := dynamic_weighted_select(hash(cycle), spec)
819 "#;
820 let mut log = CompileEventLog::new();
821 let _k = polydat::dsl::compile::compile_polydat_with_log(source, &mut log).unwrap();
822
823 let warnings: Vec<_> = log
824 .events()
825 .iter()
826 .filter(|e| {
827 matches!(
828 e,
829 polydat::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
830 )
831 })
832 .collect();
833 assert_eq!(
834 warnings.len(),
835 1,
836 "cycle-time config should warn: {warnings:?}"
837 );
838 }
839
840 #[test]
841 fn dynamic_weighted_select_strict_rejects_cycle_config() {
842 use crate::hash::Hash;
844 use polydat::compile::assembly::{PolydatAssembler, WireRef};
845 use polydat::dsl::events::CompileEventLog;
846 use polydat::library::convert::U64ToString;
847
848 let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
849 asm.add_node(
850 "hashed",
851 Box::new(Hash::new()),
852 vec![WireRef::input("cycle")],
853 );
854 asm.add_node(
855 "spec",
856 Box::new(U64ToString::default()),
857 vec![WireRef::node("hashed")],
858 );
859 asm.add_node(
860 "dws",
861 Box::new(DynamicWeightedSelect::new()),
862 vec![
863 WireRef::node("hashed"), WireRef::node("spec"), ],
866 );
867 asm.add_output("result", WireRef::node("dws"));
868
869 let mut log = CompileEventLog::new();
871 let _kernel = asm.compile_with_log(Some(&mut log)).unwrap();
872 let warnings: Vec<_> = log
873 .events()
874 .iter()
875 .filter(|e| {
876 matches!(
877 e,
878 polydat::dsl::events::CompileEvent::ConfigWireCycleWarning { .. }
879 )
880 })
881 .collect();
882 assert_eq!(warnings.len(), 1, "should warn in non-strict");
883
884 let mut asm2 = PolydatAssembler::new(vec!["cycle".into()]);
886 asm2.add_node(
887 "hashed",
888 Box::new(Hash::new()),
889 vec![WireRef::input("cycle")],
890 );
891 asm2.add_node(
892 "spec",
893 Box::new(U64ToString::default()),
894 vec![WireRef::node("hashed")],
895 );
896 asm2.add_node(
897 "dws",
898 Box::new(DynamicWeightedSelect::new()),
899 vec![WireRef::node("hashed"), WireRef::node("spec")],
900 );
901 asm2.add_output("result", WireRef::node("dws"));
902
903 asm2.set_strict(true);
904 let result = asm2.compile();
905 assert!(
906 result.is_err(),
907 "strict mode should reject cycle-time config wire"
908 );
909 let msg = format!("{}", result.unwrap_err());
910 assert!(
911 msg.contains("strict") || msg.contains("config"),
912 "error should mention strict or config: {msg}"
913 );
914 }
915
916 #[test]
917 fn weighted_pick_metadata_complete() {
918 let node = WeightedPick::new("10:0.5;20:0.3".to_string());
920 let meta = node.meta();
921
922 assert_eq!(meta.name, "weighted_pick");
924
925 assert_eq!(meta.ins.len(), 2);
927 assert!(matches!(meta.ins[0], Slot::Wire(_)));
928 assert!(matches!(
929 &meta.ins[1],
930 Slot::Const {
931 value: ConstValue::Str(_),
932 ..
933 }
934 ));
935
936 assert_eq!(meta.outs.len(), 1);
938
939 assert_eq!(meta.wire_inputs().len(), 1);
941
942 let consts = meta.const_slots();
944 assert_eq!(consts.len(), 1); }
946}