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