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, validate = in_range_validate)]
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/// `in_range`'s relation between its two bounds. Node-level because
101/// no per-parameter constraint can compare two parameters.
102fn in_range_validate(
103    _name: &str,
104    consts: &[polydat::dsl::factory::ConstArg],
105) -> Result<(), String> {
106    let lo = consts.first().map(|c| c.as_u64()).unwrap_or(0);
107    let hi = consts.get(1).map(|c| c.as_u64()).unwrap_or(u64::MAX);
108    if lo > hi {
109        return Err(format!("lo ({lo}) must be <= hi ({hi})"));
110    }
111    Ok(())
112}
113
114// =========================================================================
115// is_one_of(input, ...allowed) — assert input ∈ {allowed}, pass through
116// =========================================================================
117
118/// Assert that a u64 value is one of an enumerated allow-list.
119/// SRD-80b Phase C migration via `Const<Vec<C>>` combinator.
120/// Lowered through its slot kit (called from native code);
121/// `classify_node` returns Fallback for it because a
122/// `Const<Vec<u64>>` node publishes no `jit_constants`, so the
123/// `JitOp::IsOneOfCheck` arm never fires.
124#[polydat::polydat_node(category = Arithmetic, validate = is_one_of_validate)]
125fn is_one_of(input: u64, allowed: polydat::derive_support::Const<&[u64]>) -> u64 {
126    if !allowed.contains(&input) {
127        panic!(
128            "is_one_of: value {input} not in allowed set {:?}",
129            allowed.0
130        );
131    }
132    input
133}
134
135/// `is_one_of`'s allow-list must name at least one value. A list's
136/// arity is a property of the whole argument rather than of any one
137/// element, so it is stated at the node.
138fn is_one_of_validate(
139    _name: &str,
140    consts: &[polydat::dsl::factory::ConstArg],
141) -> Result<(), String> {
142    if consts.is_empty() {
143        return Err("at least one allowed value required".into());
144    }
145    Ok(())
146}
147
148// =========================================================================
149// matches(input, pattern) — assert regex match, pass through
150// =========================================================================
151
152/// Build a Regex from a pattern, panicking on invalid input.
153fn compile_matches_regex(pattern: &str) -> Regex {
154    Regex::new(pattern).unwrap_or_else(|e| panic!("matches: invalid regex {pattern:?}: {e}"))
155}
156
157/// Assert that a string value matches a regex pattern.
158/// SRD-80 PR B.6 migration.
159#[polydat::polydat_node(category = Arithmetic)]
160fn matches(
161    input: &str,
162    pattern: polydat::derive_support::Const<&str>,
163    #[poly_const(compile_matches_regex, from = pattern)] re: &Regex,
164) -> String {
165    if !re.is_match(input) {
166        panic!(
167            "matches: value {input:?} does not match pattern {:?}",
168            pattern.0
169        );
170    }
171    input.to_string()
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use polydat::ast::{PolydatNode, Value};
178
179    #[test]
180    fn required_passes_defined_value() {
181        let n = Required::new("x".to_string());
182        let mut out = [Value::None];
183        n.eval(&[Value::U64(42)], &mut out);
184        assert_eq!(out[0].as_u64(), 42);
185    }
186
187    #[test]
188    #[should_panic(expected = "required(x): value was not defined")]
189    fn required_panics_on_none() {
190        let n = Required::new("x".to_string());
191        let mut out = [Value::None];
192        n.eval(&[Value::None], &mut out);
193    }
194
195    #[test]
196    fn this_or_prefers_primary_when_defined() {
197        let n = ThisOr::new();
198        let mut out = [Value::None];
199        n.eval(&[Value::U64(7), Value::U64(99)], &mut out);
200        assert_eq!(out[0].as_u64(), 7);
201    }
202
203    #[test]
204    fn this_or_falls_back_to_default_on_none() {
205        let n = ThisOr::new();
206        let mut out = [Value::None];
207        n.eval(&[Value::None, Value::U64(99)], &mut out);
208        assert_eq!(out[0].as_u64(), 99);
209    }
210
211    #[test]
212    fn is_positive_passes_positive() {
213        let n = IsPositive::new("rate".to_string());
214        let mut out = [Value::None];
215        n.eval(&[Value::U64(1)], &mut out);
216        assert_eq!(out[0].as_u64(), 1);
217    }
218
219    #[test]
220    #[should_panic(expected = "is_positive(rate)")]
221    fn is_positive_panics_on_zero() {
222        let n = IsPositive::new("rate".to_string());
223        let mut out = [Value::None];
224        n.eval(&[Value::U64(0)], &mut out);
225    }
226
227    #[test]
228    fn in_range_passes_interior() {
229        let n = InRange::new(10, 100);
230        let mut out = [Value::None];
231        n.eval(&[Value::U64(50)], &mut out);
232        assert_eq!(out[0].as_u64(), 50);
233        n.eval(&[Value::U64(10)], &mut out);
234        assert_eq!(out[0].as_u64(), 10);
235        n.eval(&[Value::U64(100)], &mut out);
236        assert_eq!(out[0].as_u64(), 100);
237    }
238
239    #[test]
240    #[should_panic(expected = "outside [10, 100]")]
241    fn in_range_panics_below() {
242        let n = InRange::new(10, 100);
243        let mut out = [Value::None];
244        n.eval(&[Value::U64(5)], &mut out);
245    }
246
247    #[test]
248    #[should_panic(expected = "outside [10, 100]")]
249    fn in_range_panics_above() {
250        let n = InRange::new(10, 100);
251        let mut out = [Value::None];
252        n.eval(&[Value::U64(101)], &mut out);
253    }
254
255    #[test]
256    fn is_one_of_passes_allowed() {
257        let n = IsOneOf::new(vec![1, 2, 3, 5, 8]);
258        let mut out = [Value::None];
259        n.eval(&[Value::U64(5)], &mut out);
260        assert_eq!(out[0].as_u64(), 5);
261    }
262
263    #[test]
264    #[should_panic(expected = "not in allowed set")]
265    fn is_one_of_panics_on_disallowed() {
266        let n = IsOneOf::new(vec![1, 2, 3]);
267        let mut out = [Value::None];
268        n.eval(&[Value::U64(4)], &mut out);
269    }
270
271    #[test]
272    fn matches_passes_matching_string() {
273        let n = Matches::new(r"^\w+@\w+\.\w+$".to_string());
274        let mut out = [Value::None];
275        n.eval(&[Value::Str("jshook@example.com".into())], &mut out);
276        assert_eq!(out[0].as_str(), "jshook@example.com");
277    }
278
279    #[test]
280    #[should_panic(expected = "does not match pattern")]
281    fn matches_panics_on_mismatch() {
282        let n = Matches::new(r"^\d+$".to_string());
283        let mut out = [Value::None];
284        n.eval(&[Value::Str("abc".into())], &mut out);
285    }
286}