Skip to main content

polydat_core/dsl/
factories.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Node factories, the host-side registry view, and extern resolvers.
5//!
6//! A [`NodeFactory`] is what the registry builds every node through
7//! (`factory::build_node`). `PolydatRuntime` lists the registry and
8//! carries module search paths. The extern-resolver registry is what
9//! the kernel consults at runtime.
10
11use std::path::PathBuf;
12use std::sync::Mutex;
13
14use crate::ast::{PolydatNode, PortType, Value};
15use crate::compile::assembly::WireRef;
16use crate::dsl::factory::{BuildContext, ConstArg};
17use crate::dsl::registry::{FuncCategory, FuncSig};
18
19// ───── Virtual-wire resolver registry (γ-8) ─────
20
21/// Host-mediated extern resolver per
22/// `expression_engine.md` §5.6 (virtual wires). A resolver
23/// is a callback that fires at Context Fusion scope-init
24/// time when a kernel's extern slot can't be satisfied
25/// from the outer scope's direct bindings.
26///
27/// Arguments: slot name + declared slot type + the kernel
28/// being initialised. The resolver returns `Some(value)` to
29/// fill the slot or `None` to fall through to ordinary
30/// resolution (typed error if no other source exists).
31///
32/// Per §5.6.2's contract: the returned `Value`'s type must
33/// match the slot's declared `PortType` (boundary adapter
34/// applies otherwise per γ-5); the resolver fires once per
35/// scope-init (per S3); and the resolver is responsible for
36/// its own determinism.
37pub type ExternResolver = Box<dyn Fn(&str, PortType) -> Option<Value> + Send + Sync>;
38
39/// Process-level registry of virtual-wire resolvers.
40///
41/// Resolvers are registered via [`register_extern_resolver`]
42/// and consulted by Context Fusion's
43/// `materialize_wiring_from_outer` when an outer-chain
44/// lookup yields nothing. Multiple resolvers iterate in
45/// registration order; the first matching resolver wins.
46///
47/// Test isolation: tests that register a resolver should
48/// call [`clear_extern_resolvers`] in a teardown block to
49/// avoid leaking state across tests.
50static RESOLVERS: Mutex<Vec<ExternResolver>> = Mutex::new(Vec::new());
51
52/// Register a virtual-wire resolver. Resolvers stay
53/// registered for the process's lifetime unless
54/// [`clear_extern_resolvers`] is called.
55///
56/// Per `expression_engine.md` §5.6 PLANNED → γ-8 SHIPPED.
57pub fn register_extern_resolver(resolver: ExternResolver) {
58    let mut r = RESOLVERS.lock().unwrap();
59    r.push(resolver);
60}
61
62/// Clear every registered resolver. Primarily for tests
63/// that want clean teardown.
64pub fn clear_extern_resolvers() {
65    let mut r = RESOLVERS.lock().unwrap();
66    r.clear();
67}
68
69/// Try every registered resolver in order; return the
70/// first `Some(value)` whose type matches `slot_type`
71/// (or whose type the catalog can adapt to `slot_type`).
72///
73/// Called by `PolydatKernel::materialize_wiring_from_outer`
74/// as a fall-through after outer-chain lookup.
75pub(crate) fn resolve_extern(slot_name: &str, slot_type: PortType) -> Option<Value> {
76    let r = RESOLVERS.lock().unwrap();
77    for resolver in r.iter() {
78        if let Some(value) = resolver(slot_name, slot_type) {
79            return Some(value);
80        }
81    }
82    None
83}
84
85// ───── End virtual-wire resolver registry ─────
86
87/// What the registry builds a node through: a set of function
88/// signatures and the constructor for each.
89///
90/// Every node the compiler builds comes from a factory's [`Self::build`]
91/// ([`crate::dsl::factory::build_node`]). A node module's
92/// [`NodeRegistration`](crate::dsl::registry::NodeRegistration), the
93/// form `#[polydat_node]` and `register_nodes!` emit, is a factory whose
94/// `build` is its registered [`NodeBuildFn`](crate::dsl::registry::NodeBuildFn).
95/// A host that wants a constructor of its own implements this trait and
96/// links the factory into the registry with a
97/// [`FactoryRegistration`](crate::dsl::registry::FactoryRegistration):
98///
99/// ```ignore
100/// static COUNTERS: MyFactory = MyFactory::new();
101/// polydat::inventory::submit! {
102///     polydat::dsl::registry::FactoryRegistration { factory: &COUNTERS }
103/// }
104/// ```
105///
106/// Its nodes are then indistinguishable from built-in ones: same
107/// registry, same listings, same type checking, on every engine.
108pub trait NodeFactory: Send + Sync {
109    /// The functions this factory builds. The registry lists them, and
110    /// a call to one of them is built by this factory.
111    fn signatures(&self) -> &[FuncSig];
112
113    /// Check a call's constant arguments before [`Self::build`]; an
114    /// `Err` fails the compile as a bad constant, so `build` never sees
115    /// a malformed literal. The registry has already applied each
116    /// parameter's declared `ConstConstraint`. Accepts everything
117    /// unless overridden.
118    fn validate(&self, _name: &str, _consts: &[ConstArg]) -> Result<(), String> {
119        Ok(())
120    }
121
122    /// Build the node `name`, one of [`Self::signatures`], for the given
123    /// wires, their resolved port types, and its constant arguments.
124    /// `ctx` says which bindings are under construction and which
125    /// resource scope the program tree has
126    /// ([`BuildContext`](crate::dsl::factory::BuildContext)).
127    fn build(
128        &self,
129        ctx: &BuildContext,
130        name: &str,
131        wires: &[WireRef],
132        wire_types: &[PortType],
133        consts: &[ConstArg],
134    ) -> Result<Box<dyn PolydatNode>, String>;
135}
136
137/// A host-side registry view: the linked registry for listing, and the
138/// module search paths.
139pub struct PolydatRuntime {
140    /// Additional module search paths (the `--lib` search paths).
141    polydat_lib_paths: Vec<PathBuf>,
142}
143
144impl PolydatRuntime {
145    /// A runtime with no module search paths.
146    pub fn new() -> Self {
147        Self {
148            polydat_lib_paths: Vec::new(),
149        }
150    }
151
152    /// Add a module search path (one of the `--lib` search paths).
153    pub fn add_polydat_lib(&mut self, path: PathBuf) {
154        self.polydat_lib_paths.push(path);
155    }
156
157    /// Every function the linked registry builds: the built-in library,
158    /// `#[polydat_node]` and `register_nodes!` registrations, and every
159    /// registered [`NodeFactory`].
160    pub fn registry(&self) -> Vec<FuncSig> {
161        crate::dsl::registry::registry()
162    }
163
164    /// [`Self::registry`] grouped by category, in display order.
165    pub fn by_category(&self) -> Vec<(FuncCategory, Vec<FuncSig>)> {
166        crate::dsl::registry::by_category()
167    }
168
169    /// The `--lib` search paths.
170    pub fn polydat_lib_paths(&self) -> &[PathBuf] {
171        &self.polydat_lib_paths
172    }
173}
174
175impl Default for PolydatRuntime {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    /// Test helper: serialise resolver-registry tests via a
186    /// process-wide mutex so two tests don't race the static
187    /// `RESOLVERS`.
188    static RESOLVER_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
189
190    #[test]
191    fn extern_resolver_register_and_lookup() {
192        let _guard = RESOLVER_TEST_LOCK.lock().unwrap();
193        clear_extern_resolvers();
194        register_extern_resolver(Box::new(|name, _typ| {
195            if name == "region" {
196                Some(Value::Str("us-east-1".into()))
197            } else {
198                None
199            }
200        }));
201        let v = resolve_extern("region", PortType::Str);
202        assert_eq!(v, Some(Value::Str("us-east-1".into())));
203        let v = resolve_extern("missing", PortType::Str);
204        assert_eq!(v, None);
205        clear_extern_resolvers();
206    }
207
208    #[test]
209    fn extern_resolver_first_match_wins() {
210        let _guard = RESOLVER_TEST_LOCK.lock().unwrap();
211        clear_extern_resolvers();
212        register_extern_resolver(Box::new(|name, _typ| {
213            if name == "k" {
214                Some(Value::U64(1))
215            } else {
216                None
217            }
218        }));
219        register_extern_resolver(Box::new(|name, _typ| {
220            if name == "k" {
221                Some(Value::U64(2))
222            } else {
223                None
224            }
225        }));
226        let v = resolve_extern("k", PortType::U64);
227        // First registered wins.
228        assert_eq!(v, Some(Value::U64(1)));
229        clear_extern_resolvers();
230    }
231
232    #[test]
233    fn extern_resolver_clear_removes_all() {
234        let _guard = RESOLVER_TEST_LOCK.lock().unwrap();
235        register_extern_resolver(Box::new(|_, _| Some(Value::U64(99))));
236        assert!(resolve_extern("anything", PortType::U64).is_some());
237        clear_extern_resolvers();
238        assert!(resolve_extern("anything", PortType::U64).is_none());
239    }
240
241    #[test]
242    fn default_runtime_has_builtins() {
243        let rt = PolydatRuntime::new();
244        let reg = rt.registry();
245        assert!(reg.len() >= 50);
246    }
247}