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}