Skip to main content

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}