Skip to main content

polydat_nodes/
param_helpers.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Parameter resolution and validation helpers (SRD 12 §"Parameter
5//! resolution and validation").
6//!
7//! These nodes are pass-throughs with assertions on the value they
8//! carry. They let workloads say "this parameter must be defined",
9//! "this number must be in range", "this string must match a
10//! pattern" — with the assertion fired at the earliest point the
11//! value is known. For compile-time-constant inputs, constant
12//! folding collapses the assertion into a hard compile error. For
13//! init-time-resolved workload params, the assertion fires on the
14//! first evaluation (effectively init). For live-read inputs, the
15//! assertion fires per cycle.
16//!
17//! Failure is reported via panic with a descriptive message. Panics
18//! inside `eval` surface as workload startup errors for init-time
19//! values and as cycle-time aborts for live-read inputs; both are
20//! the intended consequence of a violated precondition.
21
22use regex::Regex;
23
24// =========================================================================
25// required(input) — assert non-None, pass through
26// =========================================================================
27
28/// Assert that an input is defined (i.e. not `Value::None`).
29///
30/// Signature: `required(input: u64) -> u64`. The `Option<u64>`
31/// wire shape opts the node into kernel-Rule-1 bypass (the macro
32/// auto-emits `accepts_none_inputs() -> true` when any arg is
33/// `Option<_>`); the body's `unwrap_or_else` is what actually
34/// fires the diagnostic.
35#[polydat::polydat_node(category = Arithmetic)]
36fn required(
37    input: Option<u64>,
38    #[poly_default("value")] name: polydat::derive_support::Const<&str>,
39) -> u64 {
40    input.unwrap_or_else(|| panic!("required({}): value was not defined", name.0))
41}
42
43// =========================================================================
44// this_or(primary, default) — first if defined else second
45// =========================================================================
46
47/// Return `primary` if it is defined, otherwise `default`.
48///
49/// Signature: `this_or(primary: Option<u64>, default: u64) -> u64`.
50/// The typed-coalesce equivalent of `default_or` — `None` on
51/// `primary` triggers the fallback. `Option<u64>` opts into
52/// None-tolerance via the macro's auto-emitted
53/// `accepts_none_inputs`.
54#[polydat::polydat_node(category = Arithmetic)]
55fn this_or(primary: Option<u64>, default: u64) -> u64 {
56    primary.unwrap_or(default)
57}
58
59// =========================================================================
60// is_positive(input) — assert > 0, pass through
61// =========================================================================
62
63/// Assert that a u64 value is strictly positive (> 0).
64///
65/// Signature: `is_positive(input: u64) -> u64`
66/// Assert that a u64 value is strictly positive (> 0). SRD-80
67/// PR B.15 migration.
68#[polydat::polydat_node(category = Arithmetic)]
69fn is_positive(
70    input: u64,
71    #[poly_default("value")] name: polydat::derive_support::Const<&str>,
72) -> u64 {
73    if input == 0 {
74        panic!("is_positive({}): value must be > 0, got 0", name.0);
75    }
76    input
77}
78
79// =========================================================================
80// in_range(input, lo, hi) — assert lo ≤ input ≤ hi, pass through
81// =========================================================================
82
83/// Assert that a u64 value is in the inclusive range `[lo, hi]`.
84///
85/// Signature: `in_range(input: u64, lo: u64, hi: u64) -> u64`
86/// Assert that a u64 value is in the inclusive range `[lo, hi]`.
87/// SRD-80 PR B.15 migration.
88#[polydat::polydat_node(category = Arithmetic)]
89fn in_range(
90    input: u64,
91    #[poly_default(0u64)] lo: polydat::derive_support::Const<u64>,
92    #[poly_default(u64::MAX)] hi: polydat::derive_support::Const<u64>,
93) -> u64 {
94    if input < *lo || input > *hi {
95        panic!("in_range: value {input} outside [{}, {}]", *lo, *hi);
96    }
97    input
98}
99
100// =========================================================================
101// is_one_of(input, ...allowed) — assert input ∈ {allowed}, pass through
102// =========================================================================
103
104/// Assert that a u64 value is one of an enumerated allow-list.
105/// SRD-80b Phase C migration via `Const<Vec<C>>` combinator.
106/// Lowered through its slot kit (called from native code);
107/// `classify_node` returns Fallback for it because a
108/// `Const<Vec<u64>>` node publishes no `jit_constants`, so the
109/// `JitOp::IsOneOfCheck` arm never fires.
110#[polydat::polydat_node(category = Arithmetic)]
111fn is_one_of(input: u64, allowed: polydat::derive_support::Const<Vec<u64>>) -> u64 {
112    if !allowed.contains(&input) {
113        panic!(
114            "is_one_of: value {input} not in allowed set {:?}",
115            allowed.0
116        );
117    }
118    input
119}
120
121// =========================================================================
122// matches(input, pattern) — assert regex match, pass through
123// =========================================================================
124
125/// Build a Regex from a pattern, panicking on invalid input.
126fn compile_matches_regex(pattern: &str) -> Regex {
127    Regex::new(pattern).unwrap_or_else(|e| panic!("matches: invalid regex {pattern:?}: {e}"))
128}
129
130/// Assert that a string value matches a regex pattern.
131/// SRD-80 PR B.6 migration.
132#[polydat::polydat_node(category = Arithmetic)]
133fn matches(
134    input: &str,
135    pattern: polydat::derive_support::Const<&str>,
136    #[poly_const(compile_matches_regex, from = pattern)] re: &Regex,
137) -> String {
138    if !re.is_match(input) {
139        panic!(
140            "matches: value {input:?} does not match pattern {:?}",
141            pattern.0
142        );
143    }
144    input.to_string()
145}
146
147// =========================================================================
148// Registration
149// =========================================================================
150
151use polydat::dsl::registry::FuncSig;
152
153/// The hand-registered signatures of this module.
154pub fn signatures() -> &'static [FuncSig] {
155    &[
156        // `required` / `this_or` migrated to `#[polydat_node]` via the
157        // `Option<T>` combinator (SRD-80b Phase C).
158        // `is_positive` / `in_range` / `matches` already on the macro.
159        // `is_one_of` migrated to `#[polydat_node]` via the
160        // `Const<Vec<C>>` combinator (SRD-80b Phase C).
161    ]
162}
163
164pub(crate) fn build_node(
165    name: &str,
166    _wires: &[polydat::compile::assembly::WireRef],
167    _wire_types: &[polydat::ast::PortType],
168    consts: &[polydat::dsl::factory::ConstArg],
169) -> Option<Result<Box<dyn polydat::ast::PolydatNode>, String>> {
170    let _ = name;
171    let _ = consts;
172    // All param-helper nodes route via proc-macro-emitted NodeRegistration.
173    None
174}
175
176/// Assembly-time constant validation for parameter-helper nodes.
177/// See SRD 15 §"Const Constraint Metadata".
178pub(crate) fn validate_node(
179    name: &str,
180    consts: &[polydat::dsl::factory::ConstArg],
181) -> Result<(), String> {
182    match name {
183        "in_range" => {
184            let lo = consts.first().map(|c| c.as_u64()).unwrap_or(0);
185            let hi = consts.get(1).map(|c| c.as_u64()).unwrap_or(u64::MAX);
186            if lo > hi {
187                Err(format!("lo ({lo}) must be <= hi ({hi})"))
188            } else {
189                Ok(())
190            }
191        }
192        "is_one_of" => {
193            if consts.is_empty() {
194                Err("at least one allowed value required".into())
195            } else {
196                Ok(())
197            }
198        }
199        _ => Ok(()),
200    }
201}
202
203polydat::register_nodes!(signatures, build_node, validate_node);
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use polydat::ast::{PolydatNode, Value};
209
210    #[test]
211    fn required_passes_defined_value() {
212        let n = Required::new("x".to_string());
213        let mut out = [Value::None];
214        n.eval(&[Value::U64(42)], &mut out);
215        assert_eq!(out[0].as_u64(), 42);
216    }
217
218    #[test]
219    #[should_panic(expected = "required(x): value was not defined")]
220    fn required_panics_on_none() {
221        let n = Required::new("x".to_string());
222        let mut out = [Value::None];
223        n.eval(&[Value::None], &mut out);
224    }
225
226    #[test]
227    fn this_or_prefers_primary_when_defined() {
228        let n = ThisOr::new();
229        let mut out = [Value::None];
230        n.eval(&[Value::U64(7), Value::U64(99)], &mut out);
231        assert_eq!(out[0].as_u64(), 7);
232    }
233
234    #[test]
235    fn this_or_falls_back_to_default_on_none() {
236        let n = ThisOr::new();
237        let mut out = [Value::None];
238        n.eval(&[Value::None, Value::U64(99)], &mut out);
239        assert_eq!(out[0].as_u64(), 99);
240    }
241
242    #[test]
243    fn is_positive_passes_positive() {
244        let n = IsPositive::new("rate".to_string());
245        let mut out = [Value::None];
246        n.eval(&[Value::U64(1)], &mut out);
247        assert_eq!(out[0].as_u64(), 1);
248    }
249
250    #[test]
251    #[should_panic(expected = "is_positive(rate)")]
252    fn is_positive_panics_on_zero() {
253        let n = IsPositive::new("rate".to_string());
254        let mut out = [Value::None];
255        n.eval(&[Value::U64(0)], &mut out);
256    }
257
258    #[test]
259    fn in_range_passes_interior() {
260        let n = InRange::new(10, 100);
261        let mut out = [Value::None];
262        n.eval(&[Value::U64(50)], &mut out);
263        assert_eq!(out[0].as_u64(), 50);
264        n.eval(&[Value::U64(10)], &mut out);
265        assert_eq!(out[0].as_u64(), 10);
266        n.eval(&[Value::U64(100)], &mut out);
267        assert_eq!(out[0].as_u64(), 100);
268    }
269
270    #[test]
271    #[should_panic(expected = "outside [10, 100]")]
272    fn in_range_panics_below() {
273        let n = InRange::new(10, 100);
274        let mut out = [Value::None];
275        n.eval(&[Value::U64(5)], &mut out);
276    }
277
278    #[test]
279    #[should_panic(expected = "outside [10, 100]")]
280    fn in_range_panics_above() {
281        let n = InRange::new(10, 100);
282        let mut out = [Value::None];
283        n.eval(&[Value::U64(101)], &mut out);
284    }
285
286    #[test]
287    fn is_one_of_passes_allowed() {
288        let n = IsOneOf::new(vec![1, 2, 3, 5, 8]);
289        let mut out = [Value::None];
290        n.eval(&[Value::U64(5)], &mut out);
291        assert_eq!(out[0].as_u64(), 5);
292    }
293
294    #[test]
295    #[should_panic(expected = "not in allowed set")]
296    fn is_one_of_panics_on_disallowed() {
297        let n = IsOneOf::new(vec![1, 2, 3]);
298        let mut out = [Value::None];
299        n.eval(&[Value::U64(4)], &mut out);
300    }
301
302    #[test]
303    fn matches_passes_matching_string() {
304        let n = Matches::new(r"^\w+@\w+\.\w+$".to_string());
305        let mut out = [Value::None];
306        n.eval(&[Value::Str("jshook@example.com".into())], &mut out);
307        assert_eq!(out[0].as_str(), "jshook@example.com");
308    }
309
310    #[test]
311    #[should_panic(expected = "does not match pattern")]
312    fn matches_panics_on_mismatch() {
313        let n = Matches::new(r"^\d+$".to_string());
314        let mut out = [Value::None];
315        n.eval(&[Value::Str("abc".into())], &mut out);
316    }
317}