polydat_core/dsl/factories.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Host-side registry view and extern resolvers.
5//!
6//! `PolydatRuntime` unions the link-time registry with host factories
7//! for listing, and carries module search paths; node construction
8//! goes through `factory::build_node` and the inventory, not through
9//! this type. The extern-resolver registry is what the kernel consults
10//! at runtime.
11
12use std::path::PathBuf;
13use std::sync::Mutex;
14
15use crate::ast::{PolydatNode, PortType, Value};
16use crate::dsl::registry::{FuncCategory, FuncSig};
17
18// ───── Virtual-wire resolver registry (γ-8) ─────
19
20/// Host-mediated extern resolver per
21/// `expression_engine.md` §5.6 (virtual wires). A resolver
22/// is a callback that fires at Context Fusion scope-init
23/// time when a kernel's extern slot can't be satisfied
24/// from the outer scope's direct bindings.
25///
26/// Arguments: slot name + declared slot type + the kernel
27/// being initialised. The resolver returns `Some(value)` to
28/// fill the slot or `None` to fall through to ordinary
29/// resolution (typed error if no other source exists).
30///
31/// Per §5.6.2's contract: the returned `Value`'s type must
32/// match the slot's declared `PortType` (boundary adapter
33/// applies otherwise per γ-5); the resolver fires once per
34/// scope-init (per S3); and the resolver is responsible for
35/// its own determinism.
36pub type ExternResolver = Box<dyn Fn(&str, PortType) -> Option<Value> + Send + Sync>;
37
38/// Process-level registry of virtual-wire resolvers.
39///
40/// Resolvers are registered via [`register_extern_resolver`]
41/// and consulted by Context Fusion's
42/// `materialize_wiring_from_outer` when an outer-chain
43/// lookup yields nothing. Multiple resolvers iterate in
44/// registration order; the first matching resolver wins.
45///
46/// Test isolation: tests that register a resolver should
47/// call [`clear_extern_resolvers`] in a teardown block to
48/// avoid leaking state across tests.
49static RESOLVERS: Mutex<Vec<ExternResolver>> = Mutex::new(Vec::new());
50
51/// Register a virtual-wire resolver. Resolvers stay
52/// registered for the process's lifetime unless
53/// [`clear_extern_resolvers`] is called.
54///
55/// Per `expression_engine.md` §5.6 PLANNED → γ-8 SHIPPED.
56pub fn register_extern_resolver(resolver: ExternResolver) {
57 let mut r = RESOLVERS.lock().unwrap();
58 r.push(resolver);
59}
60
61/// Clear every registered resolver. Primarily for tests
62/// that want clean teardown.
63pub fn clear_extern_resolvers() {
64 let mut r = RESOLVERS.lock().unwrap();
65 r.clear();
66}
67
68/// Try every registered resolver in order; return the
69/// first `Some(value)` whose type matches `slot_type`
70/// (or whose type the catalog can adapt to `slot_type`).
71///
72/// Called by `PolydatKernel::materialize_wiring_from_outer`
73/// as a fall-through after outer-chain lookup.
74pub(crate) fn resolve_extern(slot_name: &str, slot_type: PortType) -> Option<Value> {
75 let r = RESOLVERS.lock().unwrap();
76 for resolver in r.iter() {
77 if let Some(value) = resolver(slot_name, slot_type) {
78 return Some(value);
79 }
80 }
81 None
82}
83
84// ───── End virtual-wire resolver registry ─────
85
86/// Constant argument passed to a node factory at build time.
87#[derive(Debug, Clone)]
88pub enum FactoryArg {
89 /// An integer literal.
90 Int(u64),
91 /// A float literal.
92 Float(f64),
93 /// A string literal.
94 Str(String),
95}
96
97/// Trait for external node providers.
98///
99/// External crates implement this to contribute Polydat node functions.
100/// Once registered on a `PolydatRuntime`, the factory's nodes are
101/// indistinguishable from built-in nodes: same registry, same
102/// describe output, same category grouping, same type checking.
103pub trait NodeFactory: Send + Sync {
104 /// Return signatures for all functions this factory provides.
105 ///
106 /// Called once at registration time. The returned signatures are
107 /// merged into the runtime's unified registry.
108 fn signatures(&self) -> Vec<FuncSig>;
109
110 /// Build a node by name with the given constant arguments.
111 ///
112 /// Called by the compiler when assembling a kernel that references
113 /// one of this factory's functions. `wire_count` is the number of
114 /// wire inputs at the call site.
115 fn build(
116 &self,
117 name: &str,
118 wire_count: usize,
119 consts: &[FactoryArg],
120 ) -> Result<Box<dyn PolydatNode>, String>;
121}
122
123/// A host-side registry view.
124///
125/// Unions the link-time registry with host factories for listing, and
126/// carries module search paths and stdlib sources. Node construction
127/// goes through `factory::build_node` and the inventory, not through
128/// this type.
129///
130/// Multiple runtimes can coexist with different factory sets.
131pub struct PolydatRuntime {
132 /// Registered factories. Built-in nodes register through
133 /// `register_nodes!`/`#[polydat_node]` into the link-time
134 /// inventory; their signatures are included in the unified
135 /// registry.
136 factories: Vec<Box<dyn NodeFactory>>,
137 /// Additional module search paths (the `--lib` search paths).
138 polydat_lib_paths: Vec<PathBuf>,
139}
140
141impl PolydatRuntime {
142 /// Create a new runtime with only built-in nodes.
143 pub fn new() -> Self {
144 Self {
145 factories: Vec::new(),
146 polydat_lib_paths: Vec::new(),
147 }
148 }
149
150 /// Register an external node factory.
151 ///
152 /// The factory's signatures are merged into the unified registry.
153 /// Its nodes become available for compilation immediately.
154 pub fn register_factory(&mut self, factory: Box<dyn NodeFactory>) {
155 self.factories.push(factory);
156 }
157
158 /// Add a module search path (one of the `--lib` search paths).
159 pub fn add_polydat_lib(&mut self, path: PathBuf) {
160 self.polydat_lib_paths.push(path);
161 }
162
163 /// Return the unified function registry: built-in + all factories.
164 ///
165 /// SRD-80 — `#[polydat_node]`-generated nodes route through
166 /// the same `crate::dsl::registry::registry()` channel
167 /// (they submit `NodeRegistration` entries link-time, same
168 /// as `register_nodes!`-using modules), so no separate
169 /// merge step is needed here.
170 pub fn registry(&self) -> Vec<FuncSig> {
171 let mut sigs = crate::dsl::registry::registry();
172 for factory in &self.factories {
173 sigs.extend(factory.signatures());
174 }
175 sigs
176 }
177
178 /// Return functions grouped by category from the unified registry.
179 pub fn by_category(&self) -> Vec<(FuncCategory, Vec<FuncSig>)> {
180 let sigs = self.registry();
181 let mut groups: std::collections::HashMap<FuncCategory, Vec<FuncSig>> =
182 std::collections::HashMap::new();
183 for sig in sigs {
184 groups.entry(sig.category).or_default().push(sig);
185 }
186 FuncCategory::display_order()
187 .iter()
188 .filter_map(|cat| groups.remove(cat).map(|funcs| (*cat, funcs)))
189 .collect()
190 }
191
192 /// Try to build a node through registered factories.
193 ///
194 /// Called by the compiler when the built-in build_node doesn't
195 /// match. Returns None if no factory handles this function name.
196 pub fn build_from_factory(
197 &self,
198 name: &str,
199 wire_count: usize,
200 consts: &[FactoryArg],
201 ) -> Option<Result<Box<dyn PolydatNode>, String>> {
202 for factory in &self.factories {
203 if factory.signatures().iter().any(|s| s.name == name) {
204 return Some(factory.build(name, wire_count, consts));
205 }
206 }
207 None
208 }
209
210 /// Number of registered factories.
211 pub fn factory_count(&self) -> usize {
212 self.factories.len()
213 }
214
215 /// The `--lib` search paths.
216 pub fn polydat_lib_paths(&self) -> &[PathBuf] {
217 &self.polydat_lib_paths
218 }
219}
220
221impl Default for PolydatRuntime {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::ast::SlotType;
231 use crate::dsl::registry::{Arity, ParamSpec};
232
233 /// Test helper: serialise resolver-registry tests via a
234 /// process-wide mutex so two tests don't race the static
235 /// `RESOLVERS`.
236 static RESOLVER_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
237
238 #[test]
239 fn extern_resolver_register_and_lookup() {
240 let _guard = RESOLVER_TEST_LOCK.lock().unwrap();
241 clear_extern_resolvers();
242 register_extern_resolver(Box::new(|name, _typ| {
243 if name == "region" {
244 Some(Value::Str("us-east-1".into()))
245 } else {
246 None
247 }
248 }));
249 let v = resolve_extern("region", PortType::Str);
250 assert_eq!(v, Some(Value::Str("us-east-1".into())));
251 let v = resolve_extern("missing", PortType::Str);
252 assert_eq!(v, None);
253 clear_extern_resolvers();
254 }
255
256 #[test]
257 fn extern_resolver_first_match_wins() {
258 let _guard = RESOLVER_TEST_LOCK.lock().unwrap();
259 clear_extern_resolvers();
260 register_extern_resolver(Box::new(|name, _typ| {
261 if name == "k" {
262 Some(Value::U64(1))
263 } else {
264 None
265 }
266 }));
267 register_extern_resolver(Box::new(|name, _typ| {
268 if name == "k" {
269 Some(Value::U64(2))
270 } else {
271 None
272 }
273 }));
274 let v = resolve_extern("k", PortType::U64);
275 // First registered wins.
276 assert_eq!(v, Some(Value::U64(1)));
277 clear_extern_resolvers();
278 }
279
280 #[test]
281 fn extern_resolver_clear_removes_all() {
282 let _guard = RESOLVER_TEST_LOCK.lock().unwrap();
283 register_extern_resolver(Box::new(|_, _| Some(Value::U64(99))));
284 assert!(resolve_extern("anything", PortType::U64).is_some());
285 clear_extern_resolvers();
286 assert!(resolve_extern("anything", PortType::U64).is_none());
287 }
288
289 #[test]
290 fn default_runtime_has_builtins() {
291 let rt = PolydatRuntime::new();
292 let reg = rt.registry();
293 assert!(reg.len() >= 50);
294 }
295
296 #[test]
297 fn factory_signatures_merged() {
298 struct TestFactory;
299 impl NodeFactory for TestFactory {
300 fn signatures(&self) -> Vec<FuncSig> {
301 vec![FuncSig {
302 name: "test_node",
303 category: FuncCategory::Diagnostic,
304 outputs: 1,
305 description: "a test node from a factory",
306 help: "",
307 identity: None,
308 variadic_ctor: None,
309 params: &[ParamSpec {
310 name: "input",
311 slot_type: SlotType::Wire,
312 required: true,
313 example: "cycle",
314 constraint: None,
315 }],
316 arity: Arity::Fixed,
317 commutativity: crate::ast::Commutativity::Positional,
318 default_resolver: None,
319 output_type: crate::dsl::registry::OutputType::Fixed,
320 // Hand registration: no static return-port declaration;
321 // type inference falls back to the name heuristic.
322 output_port: None,
323 }]
324 }
325 fn build(
326 &self,
327 _name: &str,
328 _wc: usize,
329 _consts: &[FactoryArg],
330 ) -> Result<Box<dyn PolydatNode>, String> {
331 Ok(Box::new(crate::library::identity::Identity::new(
332 crate::ast::PortType::U64,
333 )))
334 }
335 }
336
337 let mut rt = PolydatRuntime::new();
338 let before = rt.registry().len();
339 rt.register_factory(Box::new(TestFactory));
340 let after = rt.registry().len();
341 assert_eq!(after, before + 1);
342
343 // The test_node should appear in the unified registry
344 assert!(rt.registry().iter().any(|s| s.name == "test_node"));
345 }
346
347 #[test]
348 fn factory_build_dispatch() {
349 struct TestFactory;
350 impl NodeFactory for TestFactory {
351 fn signatures(&self) -> Vec<FuncSig> {
352 vec![FuncSig {
353 name: "custom_identity",
354 category: FuncCategory::Diagnostic,
355 outputs: 1,
356 description: "custom identity from factory",
357 help: "",
358 identity: None,
359 variadic_ctor: None,
360 params: &[ParamSpec {
361 name: "input",
362 slot_type: SlotType::Wire,
363 required: true,
364 example: "cycle",
365 constraint: None,
366 }],
367 arity: Arity::Fixed,
368 commutativity: crate::ast::Commutativity::Positional,
369 default_resolver: None,
370 output_type: crate::dsl::registry::OutputType::Fixed,
371 // Hand registration: no static return-port declaration;
372 // type inference falls back to the name heuristic.
373 output_port: None,
374 }]
375 }
376 fn build(
377 &self,
378 name: &str,
379 _wc: usize,
380 _consts: &[FactoryArg],
381 ) -> Result<Box<dyn PolydatNode>, String> {
382 match name {
383 "custom_identity" => Ok(Box::new(crate::library::identity::Identity::new(
384 crate::ast::PortType::U64,
385 ))),
386 _ => Err(format!("unknown: {name}")),
387 }
388 }
389 }
390
391 let mut rt = PolydatRuntime::new();
392 rt.register_factory(Box::new(TestFactory));
393
394 // Should find and build via factory
395 let result = rt.build_from_factory("custom_identity", 1, &[]);
396 assert!(result.is_some());
397 assert!(result.unwrap().is_ok());
398
399 // Should not find built-in nodes via factory
400 let result = rt.build_from_factory("hash", 1, &[]);
401 assert!(result.is_none());
402 }
403}