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(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    /// SRD-80b Phase C — workload-list const carrier for the
36    /// `Const<Vec<C>>` shape. Each inner [`ConstArg`] is one
37    /// element; the macro emits the walk over the list and the
38    /// per-element extraction for the element type it read out of
39    /// the signature.
40    List(Vec<ConstArg>),
41    /// A value the compiler built and hands the node as it is.
42    ///
43    /// The other variants are what a literal in the source parses to.
44    /// This one is for what the compiler makes: a tile's skeleton with
45    /// its projection bodies already lowered, for instance. It used to
46    /// travel as a `Str` holding JSON, which the node parsed back —
47    /// so a malformed payload was a panic at node construction rather
48    /// than a compile error, the body's source lived in three places,
49    /// and everything the compiler knew that JSON cannot carry was
50    /// lost on the way.
51    ///
52    /// The receiving node names the concrete type, which
53    /// [`ConstArg::as_opaque`] downcasts to.
54    Opaque(std::sync::Arc<dyn std::any::Any + Send + Sync>),
55}
56
57impl std::fmt::Debug for ConstArg {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            ConstArg::Int(v) => f.debug_tuple("Int").field(v).finish(),
61            ConstArg::Float(v) => f.debug_tuple("Float").field(v).finish(),
62            ConstArg::Str(s) => f.debug_tuple("Str").field(s).finish(),
63            ConstArg::List(l) => f.debug_tuple("List").field(l).finish(),
64            // A compiler-built value has no text form, and the point
65            // of carrying it opaquely is that this layer does not know
66            // what it is.
67            ConstArg::Opaque(_) => f.write_str("Opaque(..)"),
68        }
69    }
70}
71
72impl ConstArg {
73    /// The value as `T`, when this is an opaque const the compiler
74    /// built and `T` is the type it built.
75    pub fn as_opaque<T: std::any::Any + Send + Sync>(&self) -> Option<std::sync::Arc<T>> {
76        match self {
77            ConstArg::Opaque(v) => v.clone().downcast::<T>().ok(),
78            _ => None,
79        }
80    }
81
82    /// Return the value as a `u64`, or 0 if incompatible.
83    pub fn as_u64(&self) -> u64 {
84        match self {
85            ConstArg::Int(v) => *v,
86            _ => 0,
87        }
88    }
89
90    /// Return the value as an `f64`, or 0.0 if incompatible.
91    ///
92    /// Integer literals are widened to f64.
93    pub fn as_f64(&self) -> f64 {
94        match self {
95            ConstArg::Float(v) => *v,
96            ConstArg::Int(v) => *v as f64,
97            _ => 0.0,
98        }
99    }
100
101    /// Return the value as a `&str`, or `""` if incompatible.
102    pub fn as_str(&self) -> &str {
103        match self {
104            ConstArg::Str(s) => s,
105            _ => "",
106        }
107    }
108}
109
110/// Source-binding attribution, set by the compiler before each
111/// `build_node` call and read by factories that want to record
112/// which DSL binding caused the node to exist. The
113/// `control_set` factory is the canonical consumer:
114/// `rate_adj := control_set("rate", target)` calls the factory
115/// with `current_binding()` returning `"rate_adj"`, which the
116/// node stores for runtime attribution in
117/// `ControlOrigin::Polydat { binding }`.
118pub mod compile_ctx {
119    use std::cell::RefCell;
120    thread_local! {
121        static BINDING: RefCell<Option<String>> = const { RefCell::new(None) };
122    }
123
124    /// Install the current binding name for the duration of a
125    /// single `build_node` call. Returns a guard that clears
126    /// the thread-local on drop so nested compilation can't
127    /// leak attribution across callers.
128    pub fn scoped_binding(name: &str) -> BindingScope {
129        BINDING.with(|b| *b.borrow_mut() = Some(name.to_string()));
130        BindingScope(())
131    }
132
133    /// Read the current binding attribution. Returns `None`
134    /// when called outside a [`scoped_binding`] scope (e.g.
135    /// ad-hoc tests that call [`super::build_node`] directly).
136    pub fn current_binding() -> Option<String> {
137        BINDING.with(|b| b.borrow().clone())
138    }
139
140    /// RAII guard that clears the binding slot on drop.
141    pub struct BindingScope(());
142    impl Drop for BindingScope {
143        fn drop(&mut self) {
144            BINDING.with(|b| *b.borrow_mut() = None);
145        }
146    }
147}
148
149/// Build the node `func` takes for the given wires, their types, and
150/// constant arguments, through the registry; an unknown function or a
151/// mismatched signature is an error naming it.
152///
153/// Dispatch order: inventory registrations (constraint checks, then
154/// the module validator, then `build`), then the registry's variadic
155/// fallback.
156pub fn build_node(
157    func: &str,
158    wires: &[WireRef],
159    wire_types: &[crate::ast::PortType],
160    consts: &[ConstArg],
161) -> Result<Box<dyn PolydatNode>, String> {
162    // --- Per-module dispatch via inventory ---
163
164    use crate::dsl::registry::NodeRegistration;
165    for reg in inventory::iter::<NodeRegistration> {
166        // Only run this module's validator if it owns `func`.
167        // Signatures are the authoritative "does this module
168        // handle this name" list — probing `build` first would
169        // invert the ordering (construction before validation)
170        // and give an opt-in validator no chance to reject bad
171        // constants before the constructor panics.
172        let sigs = (reg.signatures)();
173        let owning_sig = sigs.iter().find(|s| s.name == func);
174        if owning_sig.is_none() {
175            continue;
176        }
177        let sig = owning_sig.unwrap();
178
179        // Pass 1: walk declared `ParamSpec.constraint`s and run
180        // each per-param check. Constraints declared on individual
181        // params cover the bulk of "must be in [0,1]" /
182        // "must be one of {2,8,10,16}" / "spec must parse" cases.
183        // SRD 15 §"Const Constraint Metadata".
184        if let Err(msg) = check_param_constraints(sig, consts) {
185            return Err(format!("bad constant {func}: {msg}"));
186        }
187
188        // Pass 2: per-module imperative validator for relational
189        // and cross-param rules (e.g. `n_of`'s n ≤ m). Eventually
190        // migrates onto a `FuncSig.validator` field; until then,
191        // each module declares its relational constraint here.
192        if let Some(validator) = reg.validate
193            && let Err(reason) = validator(func, consts)
194        {
195            return Err(format!("bad constant {func}: {reason}"));
196        }
197
198        if let Some(result) = (reg.build)(func, wires, wire_types, consts) {
199            return result;
200        }
201    }
202
203    // --- Sampling functions without a dedicated node module ---
204    //
205    // `identity` migrated to `#[polydat_node]` per SRD-80 PR B.8.
206    // `dist_*` / `icd_*` / `histribution` / `dist_empirical`
207    // migrated to `#[polydat_node]` via `#[poly_const]` setup
208    // (SRD-80b Phase E); the inventory-registered build closure
209    // now handles each name, so the hand-dispatch arms here are
210    // gone.
211
212    // --- Registry variadic fallback ---
213    if let Some(sig) = registry::lookup(func)
214        && sig.is_variadic()
215    {
216        if wires.is_empty() {
217            if let Some(id) = sig.identity {
218                return Ok(Box::new(ConstU64::new(id)));
219            }
220            return Err(format!(
221                "variadic function '{func}' requires at least one input"
222            ));
223        }
224        if let Some(ctor) = sig.variadic_ctor {
225            return Ok(ctor(wires.len()));
226        }
227    }
228
229    let mut msg = format!("unknown function: '{func}'\n");
230    if let Some(suggestion) = registry::suggest_function(func) {
231        msg.push_str(&format!("\n  Did you mean '{suggestion}'?"));
232    }
233    msg.push_str("\n\n  This function is not registered in the wiring function library.");
234    msg.push_str("\n  See the registered wiring functions for the available names.");
235    Err(msg)
236}
237
238/// Walk `sig.params`, applying every declared `ConstConstraint`
239/// to the corresponding positional `ConstArg`. Parameters with no
240/// constraint are skipped; missing optional arguments are skipped
241/// (the `required` flag handles mandatory presence elsewhere).
242fn check_param_constraints(
243    sig: &crate::dsl::registry::FuncSig,
244    consts: &[ConstArg],
245) -> Result<(), String> {
246    use crate::ast::SlotType;
247    // Const args appear in positional order, but `sig.params`
248    // mixes wire and const slots. Walk both in lockstep, pulling
249    // const args from a separate counter.
250    let mut const_idx = 0usize;
251    for spec in sig.params {
252        if matches!(spec.slot_type, SlotType::Wire) {
253            continue;
254        }
255        if let Some(constraint) = &spec.constraint
256            && let Some(arg) = consts.get(const_idx)
257        {
258            constraint.check(arg, spec.name)?;
259        }
260        const_idx += 1;
261    }
262    Ok(())
263}