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 and the port
123    // type its value wires carry, which the assembler resolves.
124    // These tests call eval directly, so the declared type only
125    // has to exist; the body reads the Value it is handed.
126    // The macro
127    // emits a (n_wires: usize) ctor param interpreted as pairs
128    // in split-halves mode; runtime inputs slice at the
129    // midpoint, so each call must supply exactly 2 * pairs
130    // input values.
131
132    #[test]
133    fn pick_true_first_returns_first_value() {
134        let node = Pick::new(2, polydat::ast::PortType::U64);
135        let v = run(
136            &node,
137            vec![
138                Value::Bool(true),
139                Value::Bool(false),
140                Value::Str("a".into()),
141                Value::Str("b".into()),
142            ],
143        );
144        assert_eq!(v.as_str(), "a");
145    }
146
147    #[test]
148    fn pick_true_second_returns_second_value() {
149        let node = Pick::new(2, polydat::ast::PortType::U64);
150        let v = run(
151            &node,
152            vec![
153                Value::Bool(false),
154                Value::Bool(true),
155                Value::Str("a".into()),
156                Value::Str("b".into()),
157            ],
158        );
159        assert_eq!(v.as_str(), "b");
160    }
161
162    #[test]
163    #[should_panic(expected = "pick: no selector matched")]
164    fn pick_zero_selectors_panics() {
165        let node = Pick::new(2, polydat::ast::PortType::U64);
166        run(
167            &node,
168            vec![
169                Value::Bool(false),
170                Value::Bool(false),
171                Value::Str("a".into()),
172                Value::Str("b".into()),
173            ],
174        );
175    }
176
177    #[test]
178    #[should_panic(expected = "pick: multiple selectors matched")]
179    fn pick_multiple_selectors_panics() {
180        let node = Pick::new(2, polydat::ast::PortType::U64);
181        run(
182            &node,
183            vec![
184                Value::Bool(true),
185                Value::Bool(true),
186                Value::Str("a".into()),
187                Value::Str("b".into()),
188            ],
189        );
190    }
191
192    #[test]
193    #[should_panic(expected = "pick: value v1 has type")]
194    fn pick_mixed_value_types_panics_at_eval() {
195        let node = Pick::new(2, polydat::ast::PortType::U64);
196        run(
197            &node,
198            vec![
199                Value::Bool(true),
200                Value::Bool(false),
201                Value::U64(1),
202                Value::Str("b".into()),
203            ],
204        );
205    }
206
207    #[test]
208    fn pick_variadic_n_works_for_2_3_4() {
209        // pairs = 3 → 6 total wires.
210        let node = Pick::new(3, polydat::ast::PortType::U64);
211        let v = run(
212            &node,
213            vec![
214                Value::Bool(false),
215                Value::Bool(false),
216                Value::Bool(true),
217                Value::Str("x".into()),
218                Value::Str("y".into()),
219                Value::Str("z".into()),
220            ],
221        );
222        assert_eq!(v.as_str(), "z");
223
224        // pairs = 4 → 8 total wires.
225        let node = Pick::new(4, polydat::ast::PortType::U64);
226        let v = run(
227            &node,
228            vec![
229                Value::Bool(false),
230                Value::Bool(true),
231                Value::Bool(false),
232                Value::Bool(false),
233                Value::U64(10),
234                Value::U64(20),
235                Value::U64(30),
236                Value::U64(40),
237            ],
238        );
239        assert_eq!(v.as_u64(), 20);
240    }
241
242    #[test]
243    fn pick_meta_has_correct_slot_count() {
244        use polydat::ast::{PortType, Slot};
245        // pairs=3 → 6 total wire slots.
246        let node = Pick::new(3, polydat::ast::PortType::U64);
247        assert_eq!(node.meta().ins.len(), 6);
248        // First N=3 slots are bool, last N=3 are placeholder
249        // (PortType::Str — macro's PolyWire variadic default).
250        for i in 0..3 {
251            match &node.meta().ins[i] {
252                Slot::Wire(p) => assert_eq!(p.typ, PortType::Bool, "selector {i} should be Bool"),
253                _ => panic!("expected wire slot at {i}"),
254            }
255        }
256    }
257}