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: ®istry::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}