Skip to main content

polydat_nodes/
pick.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! `pick` — branched-dispatch primitive (SRD-66 §"Surface 3").
5//!
6//! Signature: `pick(b0, b1, …, bN-1, v0, v1, …, vN-1) -> V`
7//!
8//! Exactly one of the N selector booleans must be true at eval; the
9//! corresponding value is returned. Zero true and multiple-true both
10//! panic with a clear diagnostic — workload authors get a hard signal
11//! when their probe assumptions break, never a silent default.
12//!
13//! The split-halves call shape (all booleans first, then all values)
14//! was chosen over interleaved pairs so long lists scan cleanly and
15//! a missing pair surfaces as "odd total args" at compile time. See
16//! SRD-66 §"Why not pair-wise `(b, v)` interleaving?" for rationale.
17
18use polydat::ast::Value;
19
20/// Static guidance suffix appended to every `pick` panic, per
21/// SRD-66 §"Diagnostic guidance".
22const PICK_HINT: &str = "\n  hint: did the probe phase that sets these booleans run before \
23this phase? Check scenario-tree DFS order or declare a `detect_*` \
24phase ahead of consumers.";
25
26/// Branched-dispatch primitive: select the value whose paired
27/// selector is true. SRD-80b split-halves variadic — the macro
28/// recognises two consecutive `&[T]` variadic args as a
29/// split-halves shape, emits `selectors.len()` Bool slots
30/// followed by `values.len()` polymorphic slots (each pair
31/// `(b_i, v_i)` shares an index), and slices `inputs` at the
32/// midpoint at eval time.
33#[polydat::polydat_node(category = Comparison, variadic_min = 1)]
34fn pick(selectors: &[bool], values: &[Value]) -> Value {
35    let n = selectors.len();
36    debug_assert_eq!(
37        n,
38        values.len(),
39        "pick arity mismatch at eval: selectors={} values={}",
40        n,
41        values.len()
42    );
43
44    if polydat::library::debug_nodes_enabled() {
45        let sels: Vec<String> = selectors
46            .iter()
47            .enumerate()
48            .map(|(i, s)| format!("b{i}={s}"))
49            .collect();
50        let vals: Vec<String> = values
51            .iter()
52            .enumerate()
53            .map(|(i, v)| format!("v{i}={}", v.to_display_string()))
54            .collect();
55        polydat::library::support::audit::debug(&format!(
56            "pick: selectors=[{}] values=[{}]",
57            sels.join(", "),
58            vals.join(", "),
59        ));
60    }
61
62    let mut matched: Vec<usize> = Vec::new();
63    for (i, &sel) in selectors.iter().enumerate() {
64        if sel {
65            matched.push(i);
66        }
67    }
68
69    if matched.is_empty() {
70        panic!(
71            "pick: no selector matched (all N={n} booleans false); \
72             workload author guarantees one of {{b0, …, bN-1}} is \
73             true at this point{PICK_HINT}"
74        );
75    }
76    if matched.len() > 1 {
77        let positions: Vec<String> = matched.iter().map(|i| format!("b{i}")).collect();
78        panic!(
79            "pick: multiple selectors matched (positions {}); \
80             selectors must be mutually exclusive{PICK_HINT}",
81            positions.join(", ")
82        );
83    }
84
85    // Validate the value-half is uniform-typed across positions.
86    let first_pt = values[0].port_type();
87    for (i, v) in values.iter().enumerate().skip(1) {
88        let vpt = v.port_type();
89        if vpt != first_pt {
90            panic!(
91                "pick: value v{i} has type {vpt:?} but v0 has type {first_pt:?}; \
92                 all value inputs must share a common type{PICK_HINT}"
93            );
94        }
95    }
96
97    values[matched[0]].clone()
98}
99
100// ---------------------------------------------------------------------------
101// Signature declaration for the DSL registry
102// ---------------------------------------------------------------------------
103
104// `pick` is registered via the macro's `inventory::submit!`
105// channel. The split-halves shape is recognised by the macro
106// detecting two consecutive `&[T]` variadic args; FuncSig
107// `Arity::VariadicWires { min_wires: 2 * variadic_min }` enforces
108// total wire count = 2 × pairs. Odd-arity workload calls fall
109// through to the standard assembler-side variadic arity check.
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use polydat::ast::PolydatNode;
115
116    fn run(node: &Pick, inputs: Vec<Value>) -> Value {
117        let mut out = [Value::None];
118        node.eval(&inputs, &mut out);
119        out.into_iter().next().unwrap()
120    }
121
122    // Pick::new takes the per-half (pairs) count. The macro
123    // emits a (n_wires: usize) ctor param interpreted as pairs
124    // in split-halves mode; runtime inputs slice at the
125    // midpoint, so each call must supply exactly 2 * pairs
126    // input values.
127
128    #[test]
129    fn pick_true_first_returns_first_value() {
130        let node = Pick::new(2);
131        let v = run(
132            &node,
133            vec![
134                Value::Bool(true),
135                Value::Bool(false),
136                Value::Str("a".into()),
137                Value::Str("b".into()),
138            ],
139        );
140        assert_eq!(v.as_str(), "a");
141    }
142
143    #[test]
144    fn pick_true_second_returns_second_value() {
145        let node = Pick::new(2);
146        let v = run(
147            &node,
148            vec![
149                Value::Bool(false),
150                Value::Bool(true),
151                Value::Str("a".into()),
152                Value::Str("b".into()),
153            ],
154        );
155        assert_eq!(v.as_str(), "b");
156    }
157
158    #[test]
159    #[should_panic(expected = "pick: no selector matched")]
160    fn pick_zero_selectors_panics() {
161        let node = Pick::new(2);
162        run(
163            &node,
164            vec![
165                Value::Bool(false),
166                Value::Bool(false),
167                Value::Str("a".into()),
168                Value::Str("b".into()),
169            ],
170        );
171    }
172
173    #[test]
174    #[should_panic(expected = "pick: multiple selectors matched")]
175    fn pick_multiple_selectors_panics() {
176        let node = Pick::new(2);
177        run(
178            &node,
179            vec![
180                Value::Bool(true),
181                Value::Bool(true),
182                Value::Str("a".into()),
183                Value::Str("b".into()),
184            ],
185        );
186    }
187
188    #[test]
189    #[should_panic(expected = "pick: value v1 has type")]
190    fn pick_mixed_value_types_panics_at_eval() {
191        let node = Pick::new(2);
192        run(
193            &node,
194            vec![
195                Value::Bool(true),
196                Value::Bool(false),
197                Value::U64(1),
198                Value::Str("b".into()),
199            ],
200        );
201    }
202
203    #[test]
204    fn pick_variadic_n_works_for_2_3_4() {
205        // pairs = 3 → 6 total wires.
206        let node = Pick::new(3);
207        let v = run(
208            &node,
209            vec![
210                Value::Bool(false),
211                Value::Bool(false),
212                Value::Bool(true),
213                Value::Str("x".into()),
214                Value::Str("y".into()),
215                Value::Str("z".into()),
216            ],
217        );
218        assert_eq!(v.as_str(), "z");
219
220        // pairs = 4 → 8 total wires.
221        let node = Pick::new(4);
222        let v = run(
223            &node,
224            vec![
225                Value::Bool(false),
226                Value::Bool(true),
227                Value::Bool(false),
228                Value::Bool(false),
229                Value::U64(10),
230                Value::U64(20),
231                Value::U64(30),
232                Value::U64(40),
233            ],
234        );
235        assert_eq!(v.as_u64(), 20);
236    }
237
238    #[test]
239    fn pick_meta_has_correct_slot_count() {
240        use polydat::ast::{PortType, Slot};
241        // pairs=3 → 6 total wire slots.
242        let node = Pick::new(3);
243        assert_eq!(node.meta().ins.len(), 6);
244        // First N=3 slots are bool, last N=3 are placeholder
245        // (PortType::Str — macro's PolyWire variadic default).
246        for i in 0..3 {
247            match &node.meta().ins[i] {
248                Slot::Wire(p) => assert_eq!(p.typ, PortType::Bool, "selector {i} should be Bool"),
249                _ => panic!("expected wire slot at {i}"),
250            }
251        }
252    }
253}