Skip to main content

polydat_core/dsl/
const_constraints.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Assembly-time validation of Polydat node constant arguments.
5//!
6//! Polydat's input-validity contract keeps the hot
7//! path branch-free by letting node `::new` trust its constants —
8//! no runtime checks. That only holds if the *factory* has already
9//! proven each constant satisfies the node's contract, rejecting
10//! violations with a structured compile error *before* the node
11//! is constructed.
12//!
13//! This module provides the vocabulary for those checks:
14//!
15//! * [`ConstConstraint`] describes a single constraint on one
16//!   constant argument. Apply it with
17//!   [`ConstConstraint::check`].
18//! * [`NodeValidator`] is the per-module function the factory
19//!   calls before `build`. It gets the function name and the
20//!   resolved constant args and returns `Ok(())` or a structured
21//!   error string.
22//!
23//! A module opts in by passing a validator as the third argument
24//! to `register_nodes!`. Modules that don't need validation omit
25//! it and the factory skips the check.
26
27use crate::dsl::factory::ConstArg;
28
29/// A declarative constraint on one constant argument of a node
30/// call.
31///
32/// Attached to a `ParamSpec` via the optional `constraint` field;
33/// the factory walks `FuncSig.params` and enforces every declared
34/// constraint before `build_node` constructs the node. All variants
35/// are `Copy` so `ParamSpec` (and the static `FuncSig` arrays that
36/// embed it) stay `Copy`.
37///
38/// The library catalog (`library_catalog.md`, "Parameter resolution
39/// and validation") lists every variant with what it accepts and its
40/// error text; `tests/constraint_catalog.rs` in the `polydat` crate
41/// fails when a variant has no row there.
42#[derive(Debug, Clone, Copy)]
43pub enum ConstConstraint {
44    /// Integer must satisfy `min ≤ v ≤ max`.
45    RangeU64 {
46        /// The least value allowed.
47        min: u64,
48        /// The greatest value allowed.
49        max: u64,
50    },
51    /// Float must satisfy `min ≤ v ≤ max`.
52    RangeF64 {
53        /// The least value allowed.
54        min: f64,
55        /// The greatest value allowed.
56        max: f64,
57    },
58    /// Integer must appear in a closed set (e.g. radix ∈ {2, 8, 10, 16}).
59    AllowedU64(&'static [u64]),
60    /// Integer must be non-zero (divisors, moduli, ranges).
61    NonZeroU64,
62    /// String must have non-empty length after trim.
63    NonEmptyStr,
64    /// Arbitrary string format predicate. Return `Err(msg)` to
65    /// reject the constant; the caller prepends parameter context.
66    /// Use for structured specs like `"v1:w1;v2:w2"` where a fixed
67    /// enum variant can't express the format.
68    StrParser(fn(&str) -> Result<(), String>),
69    /// Float must be finite and strictly positive. Distinct from
70    /// `RangeF64` because the natural upper bound is `+∞` and
71    /// `RangeF64` requires a finite max.
72    PositiveFiniteF64,
73    /// Float must be finite (`!is_nan() && !is_infinite()`).
74    /// Endpoint and offset constants where ±∞/NaN would silently
75    /// produce nonsense outputs downstream.
76    FiniteF64,
77}
78
79impl ConstConstraint {
80    /// Apply this constraint to `arg`. On violation, the returned
81    /// error message is prefixed with `param_name` so the caller
82    /// can surface it directly to the user.
83    pub fn check(&self, arg: &ConstArg, param_name: &str) -> Result<(), String> {
84        match self {
85            ConstConstraint::RangeU64 { min, max } => {
86                let v = arg.as_u64();
87                if v < *min || v > *max {
88                    Err(format!("{param_name} must be in [{min}, {max}], got {v}"))
89                } else {
90                    Ok(())
91                }
92            }
93            ConstConstraint::RangeF64 { min, max } => {
94                let v = arg.as_f64();
95                if !(*min..=*max).contains(&v) {
96                    Err(format!("{param_name} must be in [{min}, {max}], got {v}"))
97                } else {
98                    Ok(())
99                }
100            }
101            ConstConstraint::AllowedU64(allowed) => {
102                let v = arg.as_u64();
103                if !allowed.contains(&v) {
104                    Err(format!("{param_name} must be one of {allowed:?}, got {v}"))
105                } else {
106                    Ok(())
107                }
108            }
109            ConstConstraint::NonZeroU64 => {
110                let v = arg.as_u64();
111                if v == 0 {
112                    Err(format!("{param_name} must be non-zero"))
113                } else {
114                    Ok(())
115                }
116            }
117            ConstConstraint::NonEmptyStr => {
118                let s = arg.as_str();
119                if s.trim().is_empty() {
120                    Err(format!("{param_name} must be non-empty"))
121                } else {
122                    Ok(())
123                }
124            }
125            ConstConstraint::StrParser(f) => {
126                let s = arg.as_str();
127                f(s).map_err(|e| format!("{param_name}: {e}"))
128            }
129            ConstConstraint::PositiveFiniteF64 => {
130                let v = arg.as_f64();
131                if !v.is_finite() || v <= 0.0 {
132                    Err(format!(
133                        "{param_name} must be a positive finite f64, got {v}"
134                    ))
135                } else {
136                    Ok(())
137                }
138            }
139            ConstConstraint::FiniteF64 => {
140                let v = arg.as_f64();
141                if !v.is_finite() {
142                    Err(format!("{param_name} must be a finite f64, got {v}"))
143                } else {
144                    Ok(())
145                }
146            }
147        }
148    }
149}
150
151/// Per-module validator the factory calls before `build_node`.
152///
153/// Receives the function name (same key `build_node` dispatches
154/// on) and the resolved constant arguments in positional order.
155/// Returns `Ok(())` if the constants satisfy every declared
156/// constraint, or a structured error on violation.
157///
158/// The error string is prefixed with `bad constant <func>: ` by
159/// the factory, so validators can return terse messages like
160/// `"radix must be one of [2,8,10,16], got 42"`.
161pub type NodeValidator = fn(name: &str, consts: &[ConstArg]) -> Result<(), String>;
162
163/// Convenience: apply a single constraint to an optional positional
164/// argument. Absence is treated as "no value to check" (Ok) — the
165/// `required` flag on `ParamSpec` already handles mandatory-ness.
166pub fn check_opt(
167    constraint: &ConstConstraint,
168    consts: &[ConstArg],
169    index: usize,
170    param_name: &str,
171) -> Result<(), String> {
172    match consts.get(index) {
173        Some(arg) => constraint.check(arg, param_name),
174        None => Ok(()),
175    }
176}