Skip to main content

polydat_core/dsl/
factory.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Node factory: maps Polydat function names to runtime node instances.
5//!
6//! `build_node` is the single dispatch point used by the compiler's
7//! `compile_binding` to turn a parsed call expression into a `Box<dyn PolydatNode>`.
8//! `ConstArg` captures assembly-time constant arguments extracted from the AST.
9//!
10//! Dispatch is decentralized: each node module exposes its own `build_node`
11//! function returning `Option<Result<...>>`.  The top-level `build_node` here
12//! tries each module in turn and falls back to the registry for variadic nodes.
13
14use crate::ast::PolydatNode;
15use crate::compile::assembly::WireRef;
16use crate::library::identity::ConstU64;
17
18use crate::dsl::registry;
19
20/// Constant arguments extracted from the AST.
21///
22/// Holds assembly-time values (integers, floats, strings, float arrays)
23/// that are baked into node constructors rather than passed as wire inputs.
24///
25/// `pub` visibility is required so that `NodeRegistration::build` function
26/// pointers (which are `pub` fields) can name this type.
27#[derive(Debug, Clone)]
28pub enum ConstArg {
29    /// An integer literal.
30    Int(u64),
31    /// A float literal.
32    Float(f64),
33    /// A string literal.
34    Str(String),
35    /// A list of float literals.
36    FloatArray(#[allow(dead_code)] Vec<f64>),
37    /// SRD-80b Phase C — workload-list const carrier for the
38    /// `Const<Vec<C>>` shape. Each inner [`ConstArg`] is one
39    /// element; `<Vec<C> as ConstSource>::extract` walks the
40    /// list and calls `C::extract` per element. Distinct from
41    /// `FloatArray` because the latter is the array-literal
42    /// lowering for the `ConstVecF64` slot type while `List`
43    /// is the typed-element variadic-const slot.
44    List(Vec<ConstArg>),
45}
46impl ConstArg {
47    /// Return the value as a `u64`, or 0 if incompatible.
48    pub fn as_u64(&self) -> u64 {
49        match self {
50            ConstArg::Int(v) => *v,
51            _ => 0,
52        }
53    }
54
55    /// Return the value as an `f64`, or 0.0 if incompatible.
56    ///
57    /// Integer literals are widened to f64.
58    pub fn as_f64(&self) -> f64 {
59        match self {
60            ConstArg::Float(v) => *v,
61            ConstArg::Int(v) => *v as f64,
62            _ => 0.0,
63        }
64    }
65
66    /// Return the value as a `&str`, or `""` if incompatible.
67    pub fn as_str(&self) -> &str {
68        match self {
69            ConstArg::Str(s) => s,
70            _ => "",
71        }
72    }
73
74    /// Return the value as a float slice, or `&[]` if incompatible.
75    #[allow(dead_code)]
76    pub fn as_float_array(&self) -> &[f64] {
77        match self {
78            ConstArg::FloatArray(v) => v,
79            _ => &[],
80        }
81    }
82}
83
84/// Build a node from a function name and its arguments.
85///
86/// `wires` are the cycle-time wire inputs.
87/// `consts` are the assembly-time constant arguments.
88///
89/// Dispatch order:
90/// 1. Per-module `build_node` functions (one per node module)
91/// 2. Sampling functions not covered by a node module
92/// 3. Registry variadic fallback
93///
94/// Source-binding attribution, set by the compiler before each
95/// `build_node` call and read by factories that want to record
96/// which DSL binding caused the node to exist. The
97/// `control_set` factory is the canonical consumer:
98/// `rate_adj := control_set("rate", target)` calls the factory
99/// with `current_binding()` returning `"rate_adj"`, which the
100/// node stores for runtime attribution in
101/// `ControlOrigin::Polydat { binding }`.
102pub mod compile_ctx {
103    use std::cell::RefCell;
104    thread_local! {
105        static BINDING: RefCell<Option<String>> = const { RefCell::new(None) };
106    }
107
108    /// Install the current binding name for the duration of a
109    /// single `build_node` call. Returns a guard that clears
110    /// the thread-local on drop so nested compilation can't
111    /// leak attribution across callers.
112    pub fn scoped_binding(name: &str) -> BindingScope {
113        BINDING.with(|b| *b.borrow_mut() = Some(name.to_string()));
114        BindingScope(())
115    }
116
117    /// Read the current binding attribution. Returns `None`
118    /// when called outside a [`scoped_binding`] scope (e.g.
119    /// ad-hoc tests that call [`super::build_node`] directly).
120    pub fn current_binding() -> Option<String> {
121        BINDING.with(|b| b.borrow().clone())
122    }
123
124    /// RAII guard that clears the binding slot on drop.
125    pub struct BindingScope(());
126    impl Drop for BindingScope {
127        fn drop(&mut self) {
128            BINDING.with(|b| *b.borrow_mut() = None);
129        }
130    }
131}
132
133/// Build the node `func` takes for the given wires, their types, and
134/// constant arguments, through the registry; an unknown function or a
135/// mismatched signature is an error naming it.
136pub fn build_node(
137    func: &str,
138    wires: &[WireRef],
139    wire_types: &[crate::ast::PortType],
140    consts: &[ConstArg],
141) -> Result<Box<dyn PolydatNode>, String> {
142    // --- Per-module dispatch via inventory ---
143
144    use crate::dsl::registry::NodeRegistration;
145    for reg in inventory::iter::<NodeRegistration> {
146        // Only run this module's validator if it owns `func`.
147        // Signatures are the authoritative "does this module
148        // handle this name" list — probing `build` first would
149        // invert the ordering (construction before validation)
150        // and give an opt-in validator no chance to reject bad
151        // constants before the constructor panics.
152        let sigs = (reg.signatures)();
153        let owning_sig = sigs.iter().find(|s| s.name == func);
154        if owning_sig.is_none() {
155            continue;
156        }
157        let sig = owning_sig.unwrap();
158
159        // Pass 1: walk declared `ParamSpec.constraint`s and run
160        // each per-param check. Constraints declared on individual
161        // params cover the bulk of "must be in [0,1]" /
162        // "must be one of {2,8,10,16}" / "spec must parse" cases.
163        // SRD 15 §"Const Constraint Metadata".
164        if let Err(msg) = check_param_constraints(sig, consts) {
165            return Err(format!("bad constant {func}: {msg}"));
166        }
167
168        // Pass 2: per-module imperative validator for relational
169        // and cross-param rules (e.g. `n_of`'s n ≤ m). Eventually
170        // migrates onto a `FuncSig.validator` field; until then,
171        // each module declares its relational constraint here.
172        if let Some(validator) = reg.validate
173            && let Err(reason) = validator(func, consts)
174        {
175            return Err(format!("bad constant {func}: {reason}"));
176        }
177
178        if let Some(result) = (reg.build)(func, wires, wire_types, consts) {
179            return result;
180        }
181    }
182
183    // --- Sampling functions without a dedicated node module ---
184    //
185    // `identity` migrated to `#[polydat_node]` per SRD-80 PR B.8.
186    // `dist_*` / `icd_*` / `histribution` / `dist_empirical`
187    // migrated to `#[polydat_node]` via `#[poly_const]` setup
188    // (SRD-80b Phase E); the inventory-registered build closure
189    // now handles each name, so the hand-dispatch arms here are
190    // gone.
191
192    // --- Registry variadic fallback ---
193    if let Some(sig) = registry::lookup(func)
194        && sig.is_variadic()
195    {
196        if wires.is_empty() {
197            if let Some(id) = sig.identity {
198                return Ok(Box::new(ConstU64::new(id)));
199            }
200            return Err(format!(
201                "variadic function '{func}' requires at least one input"
202            ));
203        }
204        if let Some(ctor) = sig.variadic_ctor {
205            return Ok(ctor(wires.len()));
206        }
207    }
208
209    let mut msg = format!("unknown function: '{func}'\n");
210    if let Some(suggestion) = registry::suggest_function(func) {
211        msg.push_str(&format!("\n  Did you mean '{suggestion}'?"));
212    }
213    msg.push_str("\n\n  This function is not registered in the wiring function library.");
214    msg.push_str("\n  See the registered wiring functions for the available names.");
215    Err(msg)
216}
217
218/// Walk `sig.params`, applying every declared `ConstConstraint`
219/// to the corresponding positional `ConstArg`. Parameters with no
220/// constraint are skipped; missing optional arguments are skipped
221/// (the `required` flag handles mandatory presence elsewhere).
222fn check_param_constraints(
223    sig: &crate::dsl::registry::FuncSig,
224    consts: &[ConstArg],
225) -> Result<(), String> {
226    use crate::ast::SlotType;
227    // Const args appear in positional order, but `sig.params`
228    // mixes wire and const slots. Walk both in lockstep, pulling
229    // const args from a separate counter.
230    let mut const_idx = 0usize;
231    for spec in sig.params {
232        if matches!(spec.slot_type, SlotType::Wire) {
233            continue;
234        }
235        if let Some(constraint) = &spec.constraint
236            && let Some(arg) = consts.get(const_idx)
237        {
238            constraint.check(arg, spec.name)?;
239        }
240        const_idx += 1;
241    }
242    Ok(())
243}