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 construction point: the compiler turns
7//! every call expression into a `Box<dyn PolydatNode>` through it, and it
8//! builds through the [`NodeFactory`](crate::dsl::factories::NodeFactory)
9//! that owns the function name. `ConstArg` captures assembly-time
10//! constant arguments extracted from the AST.
11
12use crate::ast::PolydatNode;
13use crate::compile::assembly::WireRef;
14use crate::library::identity::ConstU64;
15
16use crate::dsl::registry;
17
18/// Constant arguments extracted from the AST.
19///
20/// Holds assembly-time values (integers, floats, strings, float arrays)
21/// that are baked into node constructors rather than passed as wire inputs.
22///
23/// `pub` visibility is required so that `NodeRegistration::build` function
24/// pointers (which are `pub` fields) can name this type.
25#[derive(Clone)]
26pub enum ConstArg {
27    /// An integer literal.
28    Int(u64),
29    /// A float literal.
30    Float(f64),
31    /// A string literal.
32    Str(String),
33    /// Workload-list const carrier for the
34    /// `Const<Vec<C>>` shape. Each inner [`ConstArg`] is one
35    /// element; the macro emits the walk over the list and the
36    /// per-element extraction for the element type it read out of
37    /// the signature.
38    List(Vec<ConstArg>),
39    /// A value the compiler built and hands the node as it is.
40    ///
41    /// The other variants are what a literal in the source parses to.
42    /// This one is for what the compiler makes: a tile's skeleton with
43    /// its projection bodies already lowered, for instance. Carried as
44    /// the value itself rather than as serialized text, it cannot fail
45    /// to parse at node construction, and it keeps everything the
46    /// compiler knows that JSON cannot carry.
47    ///
48    /// The receiving node names the concrete type, which
49    /// [`ConstArg::as_opaque`] downcasts to.
50    Opaque(std::sync::Arc<dyn std::any::Any + Send + Sync>),
51}
52
53impl std::fmt::Debug for ConstArg {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            ConstArg::Int(v) => f.debug_tuple("Int").field(v).finish(),
57            ConstArg::Float(v) => f.debug_tuple("Float").field(v).finish(),
58            ConstArg::Str(s) => f.debug_tuple("Str").field(s).finish(),
59            ConstArg::List(l) => f.debug_tuple("List").field(l).finish(),
60            // A compiler-built value has no text form, and the point
61            // of carrying it opaquely is that this layer does not know
62            // what it is.
63            ConstArg::Opaque(_) => f.write_str("Opaque(..)"),
64        }
65    }
66}
67
68impl ConstArg {
69    /// The value as `T`, when this is an opaque const the compiler
70    /// built and `T` is the type it built.
71    pub fn as_opaque<T: std::any::Any + Send + Sync>(&self) -> Option<std::sync::Arc<T>> {
72        match self {
73            ConstArg::Opaque(v) => v.clone().downcast::<T>().ok(),
74            _ => None,
75        }
76    }
77
78    /// Return the value as a `u64`, or 0 if incompatible.
79    pub fn as_u64(&self) -> u64 {
80        match self {
81            ConstArg::Int(v) => *v,
82            _ => 0,
83        }
84    }
85
86    /// Return the value as an `f64`, or 0.0 if incompatible.
87    ///
88    /// Integer literals are widened to f64.
89    pub fn as_f64(&self) -> f64 {
90        match self {
91            ConstArg::Float(v) => *v,
92            ConstArg::Int(v) => *v as f64,
93            _ => 0.0,
94        }
95    }
96
97    /// Return the value as a `&str`, or `""` if incompatible.
98    pub fn as_str(&self) -> &str {
99        match self {
100            ConstArg::Str(s) => s,
101            _ => "",
102        }
103    }
104}
105
106/// What the compiler tells a node factory about the node it is building:
107/// the bindings under construction when the node was asked for, and the
108/// program tree's resource scope.
109///
110/// Every factory receives one ([`crate::dsl::registry::NodeBuildFn`]).
111/// A factory that records attribution reads [`Self::binding`]:
112/// `rate_adj := control_set("rate", target)` builds `control_set` with
113/// `binding()` equal to `Some("rate_adj")`. A node built inside another
114/// binding's construction sees the whole chain in [`Self::bindings`],
115/// outermost first: an argument that is itself a call compiles as an
116/// intermediate binding the compiler names after its enclosing one
117/// (`x := f(control_set("r", t))` builds `control_set` under
118/// `["x", "x__anon_0"]`), and a module body's bindings compile under the
119/// binding that called the module. The context is a value the compiler
120/// hands down, so attribution is exact under nesting and on any thread.
121///
122/// A node that looks up a host resource when it evaluates keeps a clone
123/// of [`Self::resources`] (resource.rs).
124///
125/// A `#[polydat_node]` node reads the context through a setup that
126/// names `ctx` first, `#[poly_const(setup, from = (ctx, key))]`, whose
127/// function takes `&BuildContext` and returns what the node keeps;
128/// the macro's `new()` then takes the context as its first argument.
129/// A caller building such a node directly passes
130/// [`BuildContext::with_binding`] or [`BuildContext::new`].
131#[derive(Clone, Debug, Default)]
132pub struct BuildContext {
133    bindings: Vec<String>,
134    resources: crate::resource::ResourceScope,
135}
136
137impl BuildContext {
138    /// A context for a node built under `bindings` (outermost first) in
139    /// the program tree whose resource scope is `resources`.
140    pub fn new(bindings: Vec<String>, resources: crate::resource::ResourceScope) -> Self {
141        Self {
142            bindings,
143            resources,
144        }
145    }
146
147    /// A context for a node built by the binding `name` alone, in a
148    /// tree of its own with no accessor installed: what a host's test
149    /// passes to build a node that records its binding.
150    pub fn with_binding(name: impl Into<String>) -> Self {
151        Self::new(vec![name.into()], crate::resource::ResourceScope::new())
152    }
153
154    /// The binding whose construction built the node: the innermost of
155    /// [`Self::bindings`]. `None` for a node built outside any binding,
156    /// such as one a caller builds directly with [`build_node`].
157    pub fn binding(&self) -> Option<&str> {
158        self.bindings.last().map(String::as_str)
159    }
160
161    /// Every binding under construction when the node was built,
162    /// outermost first.
163    pub fn bindings(&self) -> &[String] {
164        &self.bindings
165    }
166
167    /// The resource scope of the program tree the node belongs to.
168    pub fn resources(&self) -> &crate::resource::ResourceScope {
169        &self.resources
170    }
171}
172
173/// Build the node `func` takes for the given wires, their types, and
174/// constant arguments, through the registry; an unknown function or a
175/// mismatched signature is an error naming it. `ctx` reaches the
176/// factory unchanged.
177///
178/// Every node is built here, and built by the factory whose signatures
179/// list `func` ([`registry::factories`]): the parameters' declared
180/// constraints are checked, then the factory's
181/// [`validate`](crate::dsl::factories::NodeFactory::validate), then its
182/// [`build`](crate::dsl::factories::NodeFactory::build).
183pub fn build_node(
184    ctx: &BuildContext,
185    func: &str,
186    wires: &[WireRef],
187    wire_types: &[crate::ast::PortType],
188    consts: &[ConstArg],
189) -> Result<Box<dyn PolydatNode>, String> {
190    for factory in registry::factories() {
191        // The signatures say which factory owns `func`; validation runs
192        // before construction, so a constructor never sees a constant
193        // its validator would refuse.
194        let Some(sig) = factory.signatures().iter().find(|s| s.name == func) else {
195            continue;
196        };
197        // Per-parameter constraints ("must be in [0,1]", "must be one
198        // of {2,8,10,16}", "spec must parse").
199        if let Err(msg) = check_param_constraints(sig, consts) {
200            return Err(format!("bad constant {func}: {msg}"));
201        }
202        // The factory's own relational and cross-parameter rules
203        // (`n_of`'s n ≤ m).
204        if let Err(reason) = factory.validate(func, consts) {
205            return Err(format!("bad constant {func}: {reason}"));
206        }
207        return factory.build(ctx, func, wires, wire_types, consts);
208    }
209
210    let mut msg = format!("unknown function: '{func}'\n");
211    if let Some(suggestion) = registry::suggest_function(func) {
212        msg.push_str(&format!("\n  Did you mean '{suggestion}'?"));
213    }
214    msg.push_str("\n\n  This function is not registered in the wiring function library.");
215    msg.push_str("\n  See the registered wiring functions for the available names.");
216    Err(msg)
217}
218
219/// The node a variadic signature builds for `wire_count` wires: its
220/// identity element for none, its variadic constructor otherwise. A
221/// signature that is not variadic, or has neither, builds nothing and
222/// is an error naming the function.
223pub(crate) fn variadic_node(
224    sig: &registry::FuncSig,
225    wire_count: usize,
226) -> Result<Box<dyn PolydatNode>, String> {
227    let func = sig.name;
228    if !sig.is_variadic() {
229        return Err(format!(
230            "'{func}' is registered, but its registration builds no node for it"
231        ));
232    }
233    if wire_count == 0 {
234        return match sig.identity {
235            Some(id) => Ok(Box::new(ConstU64::new(id))),
236            None => Err(format!(
237                "variadic function '{func}' requires at least one input"
238            )),
239        };
240    }
241    match sig.variadic_ctor {
242        Some(ctor) => Ok(ctor(wire_count)),
243        None => Err(format!(
244            "variadic function '{func}' is registered without a variadic constructor"
245        )),
246    }
247}
248
249/// Walk `sig.params`, applying every declared `ConstConstraint`
250/// to the corresponding positional `ConstArg`. Parameters with no
251/// constraint are skipped; missing optional arguments are skipped
252/// (the `required` flag handles mandatory presence elsewhere).
253fn check_param_constraints(
254    sig: &crate::dsl::registry::FuncSig,
255    consts: &[ConstArg],
256) -> Result<(), String> {
257    use crate::ast::SlotType;
258    // Const args appear in positional order, but `sig.params`
259    // mixes wire and const slots. Walk both in lockstep, pulling
260    // const args from a separate counter.
261    let mut const_idx = 0usize;
262    for spec in sig.params {
263        if matches!(spec.slot_type, SlotType::Wire) {
264            continue;
265        }
266        if let Some(constraint) = &spec.constraint
267            && let Some(arg) = consts.get(const_idx)
268        {
269            constraint.check(arg, spec.name)?;
270        }
271        const_idx += 1;
272    }
273    Ok(())
274}