Skip to main content

polydat_core/dsl/
registry.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Function registry: known function signatures for DSL validation.
5//!
6//! Each registered function declares its name, category, expected wire
7//! inputs, constant parameters, output count, and variadic behavior.
8//! The compiler uses this to validate calls at parse time and to
9//! generically dispatch variadic functions.
10//!
11//! Categories are a type-safe enum — every function must declare one.
12//! The `describe wiring functions` command groups by category automatically.
13//! Stdlib modules declare their category via `// @category: Name`
14//! comment syntax.
15//!
16//! Signatures are owned by their respective node modules. This file
17//! defines the shared types and the collector function.
18
19pub use crate::ast::CompileLevel;
20use crate::compile::assembly::WireRef;
21
22/// Builder for a node module: `(name, wires, resolved wire port
23/// types, const args) -> Some(Ok(node)) / Some(Err(msg))`, or
24/// `None` when the name isn't handled by this module.
25pub type NodeBuildFn = fn(
26    &str,
27    &[WireRef],
28    &[crate::ast::PortType],
29    &[crate::dsl::factory::ConstArg],
30) -> Option<Result<Box<dyn crate::ast::PolydatNode>, String>>;
31
32/// A node module's registration: signatures + builder.
33///
34/// Each node module submits one of these at link time via `inventory::submit!`.
35/// The runtime collects all submissions to build the function registry and
36/// dispatch table without any explicit module list.
37pub struct NodeRegistration {
38    /// Returns the static slice of `FuncSig` entries for this module.
39    pub signatures: fn() -> &'static [FuncSig],
40    /// Attempts to build a node for the given function name.
41    ///
42    /// Returns `None` if the name is not handled by this module,
43    /// or `Some(Ok(node))` / `Some(Err(msg))` if it is.
44    ///
45    /// `wire_types[i]` is the resolved [`crate::ast::PortType`] of
46    /// `wires[i]` — the output type of the upstream node feeding
47    /// that wire input. Modules that build type-polymorphic nodes
48    /// (e.g. `log_info`, whose output type equals its input type)
49    /// read this to construct the node with the correct port
50    /// types. Modules whose nodes have type-fixed signatures can
51    /// ignore the slice. When the assembler can't resolve a
52    /// wire's type (forward reference, dangling), the slot
53    /// defaults to [`crate::ast::PortType::U64`].
54    pub build: NodeBuildFn,
55    /// Optional assembly-time validator for this module's constants.
56    ///
57    /// The factory calls this **before** `build` whenever the name
58    /// matches one of this module's functions. Returning `Err` makes
59    /// the compile fail with a structured `bad constant` error, so
60    /// the node itself never sees a malformed literal and can keep
61    /// its constructor and `eval()` branch-free. See SRD 15 §"Const
62    /// Constraint Metadata" for the contract.
63    pub validate: Option<crate::dsl::const_constraints::NodeValidator>,
64}
65
66inventory::collect!(NodeRegistration);
67
68/// Register a node module's signatures and builder with the Polydat runtime.
69///
70/// Place this call at module scope in each node module. The inventory crate
71/// arranges for the registration to run before `main` so that `registry()`
72/// and `build_node()` see all entries.
73///
74/// Two forms:
75///
76/// - `register_nodes!(signatures, build_node)` — no assembly-time
77///   validation. The builder is responsible for handling any bad
78///   input itself (usually by trusting the caller or panicking).
79/// - `register_nodes!(signatures, build_node, validate_node)` — the
80///   factory calls `validate_node(name, consts)` before `build_node`.
81///   Use this to declare [`ConstConstraint`]-style checks so
82///   constructors can stay infallible.
83///
84/// [`ConstConstraint`]: crate::dsl::const_constraints::ConstConstraint
85#[macro_export]
86macro_rules! register_nodes {
87    ($sigs:expr, $builder:expr) => {
88        inventory::submit! {
89            $crate::dsl::registry::NodeRegistration {
90                signatures: $sigs,
91                build: $builder,
92                validate: None,
93            }
94        }
95    };
96    ($sigs:expr, $builder:expr, $validator:expr) => {
97        inventory::submit! {
98            $crate::dsl::registry::NodeRegistration {
99                signatures: $sigs,
100                build: $builder,
101                validate: Some($validator),
102            }
103        }
104    };
105}
106
107/// Functional category for a Polydat node function.
108///
109/// Every native node and stdlib module belongs to exactly one category.
110/// Categories drive the `describe wiring functions` grouping and provide
111/// semantic organization for documentation and discovery.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
113pub enum FuncCategory {
114    /// Core deterministic hashing.
115    Hashing,
116    /// Integer arithmetic with constant parameters.
117    Arithmetic,
118    /// Comparison and selection: ==, !=, <, >, <=, >=, if(...).
119    /// Comparison nodes return u64 truth values (0 or 1); select
120    /// nodes pick between two operand values based on a u64 cond.
121    Comparison,
122    /// Variadic N-ary operations (sum, product, min, max).
123    Variadic,
124    /// Type conversions between u64, f64, String, etc.
125    Conversions,
126    /// Statistical distribution LUT builders and samplers.
127    Distributions,
128    /// Date and time generation and decomposition.
129    Datetime,
130    /// HTML, URL, hex, base64 encoding/decoding.
131    Encoding,
132    /// Linear interpolation, range mapping, quantization.
133    Interpolation,
134    /// Trigonometric and mathematical functions (sin, cos, sqrt, etc.).
135    Math,
136    /// Probability modeling: coins, selection, conditionals.
137    Probability,
138    /// Weighted categorical selection.
139    Weighted,
140    /// Printf-style and structured string formatting.
141    Formatting,
142    /// String generation: combinations, number words.
143    String,
144    /// JSON construction, serialization, merging.
145    Json,
146    /// Byte buffer construction and manipulation.
147    ByteBuffers,
148    /// Cryptographic and non-cryptographic digests.
149    Digest,
150    /// Coherent noise: Perlin, simplex.
151    Noise,
152    /// Regular expression matching and substitution.
153    Regex,
154    /// Bijective permutations and shuffles.
155    Permutation,
156    /// Real-world data: names, places, codes.
157    RealData,
158    /// Non-deterministic context: wall clock, counters.
159    Context,
160    /// Debugging and introspection.
161    Diagnostic,
162    /// File-based data access: CSV, JSONL, text files.
163    Data,
164}
165
166impl FuncCategory {
167    /// Display name for the category (used in describe output).
168    pub fn display_name(&self) -> &'static str {
169        match self {
170            Self::Hashing => "Hashing",
171            Self::Arithmetic => "Arithmetic",
172            Self::Comparison => "Comparison",
173            Self::Variadic => "Variadic",
174            Self::Conversions => "Conversions",
175            Self::Distributions => "Distributions",
176            Self::Datetime => "Datetime",
177            Self::Encoding => "Encoding",
178            Self::Interpolation => "Interpolation",
179            Self::Math => "Math",
180            Self::Probability => "Probability",
181            Self::Weighted => "Weighted",
182            Self::Formatting => "Formatting",
183            Self::String => "String",
184            Self::Json => "JSON",
185            Self::ByteBuffers => "Byte Buffers",
186            Self::Digest => "Digest",
187            Self::Noise => "Noise",
188            Self::Regex => "Regex",
189            Self::Permutation => "Permutation",
190            Self::RealData => "Real Data",
191            Self::Context => "Context",
192            Self::Diagnostic => "Diagnostic",
193            Self::Data => "Data",
194        }
195    }
196
197    /// Parse a category name from a string (case-insensitive).
198    /// Used for `// @category: Name` syntax in stdlib modules.
199    pub fn parse(s: &str) -> Option<Self> {
200        match s.trim().to_lowercase().as_str() {
201            "hashing" => Some(Self::Hashing),
202            "arithmetic" => Some(Self::Arithmetic),
203            "comparison" | "compare" => Some(Self::Comparison),
204            "variadic" => Some(Self::Variadic),
205            "conversions" | "conversion" => Some(Self::Conversions),
206            "distributions" | "distribution" => Some(Self::Distributions),
207            "datetime" | "date" | "time" => Some(Self::Datetime),
208            "encoding" => Some(Self::Encoding),
209            "interpolation" | "lerp" => Some(Self::Interpolation),
210            "math" | "trig" | "trigonometry" => Some(Self::Math),
211            "probability" => Some(Self::Probability),
212            "weighted" => Some(Self::Weighted),
213            "formatting" | "format" | "printf" => Some(Self::Formatting),
214            "string" | "strings" => Some(Self::String),
215            "json" => Some(Self::Json),
216            "byte buffers" | "bytebuffers" | "bytes" => Some(Self::ByteBuffers),
217            "digest" => Some(Self::Digest),
218            "noise" => Some(Self::Noise),
219            "regex" => Some(Self::Regex),
220            "permutation" | "shuffle" => Some(Self::Permutation),
221            "real data" | "realdata" | "realer" => Some(Self::RealData),
222            "context" => Some(Self::Context),
223            "diagnostic" | "diagnostics" | "debug" => Some(Self::Diagnostic),
224            "data" | "datafile" | "csv" | "jsonl" => Some(Self::Data),
225            _ => None,
226        }
227    }
228
229    /// Canonical ordering for display (same order as the enum definition).
230    pub fn display_order() -> &'static [Self] {
231        &[
232            Self::Hashing,
233            Self::Arithmetic,
234            Self::Comparison,
235            Self::Variadic,
236            Self::Conversions,
237            Self::Distributions,
238            Self::Datetime,
239            Self::Encoding,
240            Self::Interpolation,
241            Self::Math,
242            Self::Probability,
243            Self::Weighted,
244            Self::Formatting,
245            Self::String,
246            Self::Json,
247            Self::ByteBuffers,
248            Self::Digest,
249            Self::Noise,
250            Self::Regex,
251            Self::Permutation,
252            Self::RealData,
253            Self::Context,
254            Self::Diagnostic,
255            Self::Data,
256        ]
257    }
258}
259
260// ---------------------------------------------------------------------------
261// Unified parameter specification (SRD 36 §Variadic)
262// ---------------------------------------------------------------------------
263
264use crate::ast::SlotType;
265
266/// Describes one parameter in a function's call signature.
267///
268/// A "slot template" — the type-level version of a `Slot` without
269/// a concrete value. Parameters are listed in positional order
270/// matching the DSL syntax.
271#[derive(Debug, Clone, Copy)]
272pub struct ParamSpec {
273    /// Parameter name (for error messages and describe output).
274    pub name: &'static str,
275    /// Wire or constant, and if constant, what type.
276    pub slot_type: SlotType,
277    /// Whether this parameter must be provided.
278    pub required: bool,
279    /// Example value for this parameter, used for probing compile
280    /// level and for documentation. Wire params use `"cycle"`,
281    /// const params use a representative value that passes validation.
282    pub example: &'static str,
283    /// Optional assembly-time validation rule (SRD 15 §"Const
284    /// Constraint Metadata"). The factory enforces this before
285    /// `build_node` so node constructors can stay infallible and
286    /// branch-free at runtime. `None` = no constraint declared
287    /// (default for wires and unconstrained constants).
288    pub constraint: Option<crate::dsl::const_constraints::ConstConstraint>,
289}
290
291impl ParamSpec {
292    /// Convenience: chainable on a literal to attach a constraint.
293    /// Used by node modules that want to keep the literal compact.
294    pub const fn with_constraint(
295        mut self,
296        c: crate::dsl::const_constraints::ConstConstraint,
297    ) -> Self {
298        self.constraint = Some(c);
299        self
300    }
301}
302
303/// Arity specification for a function signature.
304///
305/// Describes which parts of the parameter list are fixed vs repeatable.
306#[derive(Debug, Clone, Default)]
307pub enum Arity {
308    /// Exactly the parameters declared in `params`.
309    #[default]
310    Fixed,
311    /// Trailing wire parameters repeat (sum, product, min, max).
312    VariadicWires {
313        /// The fewest trailing wires allowed.
314        min_wires: usize,
315    },
316    /// Trailing constant parameters repeat (mixed_radix).
317    VariadicConsts {
318        /// The fewest trailing constants allowed.
319        min_consts: usize,
320    },
321    /// A repeating group of slot types (weighted_sum).
322    VariadicGroup {
323        /// The slot types of one repetition, in order.
324        group: &'static [SlotType],
325        /// The fewest repetitions allowed.
326        min_repeats: usize,
327    },
328}
329
330/// Output-type contract for a registered function.
331///
332/// Most nodes have fixed port types declared by their constructor's
333/// `NodeMeta`. Some — `log_info`, `identity`, anything documented
334/// as "pass-through" — produce an output whose type matches one
335/// of their inputs. Declaring this here makes the contract visible
336/// to the assembler, the build-node dispatch path, `describe wiring
337/// functions`, and any future static analysis, instead of being
338/// buried inside an `eval` that silently passes values through a
339/// wire whose declared type lies.
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341pub enum OutputType {
342    /// Output types are whatever the constructor's `NodeMeta`
343    /// declares — independent of input wire types. The default;
344    /// covers the vast majority of nodes (`hash`, `regex_match`,
345    /// `mod`, …) whose I/O contract is type-fixed.
346    Fixed,
347    /// The function's single output port has the same type as the
348    /// input wire at the given index. The build dispatch resolves
349    /// the wire's type from the assembler and the module's
350    /// `build_node` reads it from the supplied `wire_types` slice
351    /// to construct a port-typed node. Used by pass-through
352    /// nodes (`log_info`, `log_debug`, …).
353    SameAsInput(usize),
354}
355
356/// Description of a registered function's signature.
357pub struct FuncSig {
358    /// Function name as used in the DSL.
359    pub name: &'static str,
360    /// Functional category.
361    pub category: FuncCategory,
362    /// Number of output ports (0 = dynamic, determined at compile time).
363    pub outputs: usize,
364    /// Short description for help/error messages.
365    pub description: &'static str,
366    /// Detailed help text: theory, usage examples, parameter meanings.
367    /// Displayed in the graph editor help panel.
368    pub help: &'static str,
369    /// For variadic functions: the identity element for zero inputs.
370    pub identity: Option<u64>,
371    /// Factory for variadic nodes: takes wire count, returns node.
372    pub variadic_ctor: Option<fn(usize) -> Box<dyn crate::ast::PolydatNode>>,
373    /// Positional parameter list: wires and constants in call order.
374    pub params: &'static [ParamSpec],
375    /// Arity specification.
376    pub arity: Arity,
377    /// Input commutativity for this function.
378    pub commutativity: crate::ast::Commutativity,
379    /// Optional resolver hint for `Handle`-typed input ports. When
380    /// the binding compiler emits this function and a `Handle`
381    /// input is wired to a `Str`-producing source, it splices in
382    /// the named resolver to convert the string into a handle. This
383    /// is the "string-conversion node insertion" mechanism from
384    /// SRD 53 §"Source-string call-site sugar". `None` means no
385    /// auto-promotion — the caller must pass a `Handle` directly.
386    pub default_resolver: Option<DefaultResolver>,
387    /// Output-type contract — `Fixed` for the vast majority of
388    /// nodes; `SameAsInput(idx)` for type-polymorphic pass-throughs
389    /// (e.g. `log_info` whose output type tracks its sole input).
390    pub output_type: OutputType,
391    /// Concrete port type of the single output, when statically
392    /// known (`#[polydat_node]` emits it from the return type's
393    /// `Wire::PORT`). `None` for tuple/dynamic/polymorphic outputs
394    /// and for hand registrations that don't declare one. The DSL
395    /// type inference (`binding::infer_expr_type`) reads this
396    /// FIRST — the name-prefix heuristic is only the fallback —
397    /// so call-expression operand typing flows from the symbol
398    /// registry, not from a hand-maintained list.
399    pub output_port: Option<crate::ast::PortType>,
400}
401
402/// Auto-resolver kind attached to handle-taking functions. Tells the
403/// binding compiler which resolver to splice in when a string source
404/// is wired to a handle input port.
405#[derive(Debug, Clone, Copy)]
406pub enum DefaultResolver {
407    /// Insert `dataset_open(<source_wire>, "<facet>")` between the
408    /// string source and the handle input.
409    Facet(&'static str),
410    /// Insert `dataset_group_open(<source_wire>)` between the string
411    /// source and the handle input.
412    Group,
413}
414
415impl FuncSig {
416    /// Number of wire inputs in the fixed parameter list.
417    pub fn wire_input_count(&self) -> usize {
418        self.params.iter().filter(|p| p.slot_type.is_wire()).count()
419    }
420
421    /// Whether this function accepts variadic arguments.
422    pub fn is_variadic(&self) -> bool {
423        !matches!(self.arity, Arity::Fixed)
424    }
425
426    /// Constant parameter names and whether they're required.
427    pub fn const_param_info(&self) -> Vec<(&'static str, bool)> {
428        self.params
429            .iter()
430            .filter(|p| p.slot_type.is_const())
431            .map(|p| (p.name, p.required))
432            .collect()
433    }
434}
435
436impl std::fmt::Debug for FuncSig {
437    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438        f.debug_struct("FuncSig")
439            .field("name", &self.name)
440            .field("category", &self.category)
441            .field("params", &self.params)
442            .field("arity", &self.arity)
443            .finish()
444    }
445}
446
447impl Clone for FuncSig {
448    fn clone(&self) -> Self {
449        Self {
450            name: self.name,
451            category: self.category,
452            outputs: self.outputs,
453            description: self.description,
454            help: self.help,
455            identity: self.identity,
456            variadic_ctor: self.variadic_ctor,
457            params: self.params,
458            arity: self.arity.clone(),
459            commutativity: self.commutativity.clone(),
460            default_resolver: self.default_resolver,
461            output_type: self.output_type,
462            output_port: self.output_port,
463        }
464    }
465}
466
467/// Return the full registry of known functions.
468///
469/// Iterates all `NodeRegistration` entries submitted via `inventory::submit!`
470/// at link time. No explicit module list is required here — each node module
471/// registers itself by calling `register_nodes!` at module scope.
472pub fn registry() -> Vec<FuncSig> {
473    let mut funcs = Vec::new();
474    for reg in inventory::iter::<NodeRegistration> {
475        funcs.extend_from_slice((reg.signatures)());
476    }
477    funcs
478}
479
480/// Return functions grouped by category in display order.
481pub fn by_category() -> Vec<(FuncCategory, Vec<FuncSig>)> {
482    let reg = registry();
483    let mut groups: std::collections::HashMap<FuncCategory, Vec<FuncSig>> =
484        std::collections::HashMap::new();
485    for sig in reg {
486        groups.entry(sig.category).or_default().push(sig);
487    }
488    FuncCategory::display_order()
489        .iter()
490        .filter_map(|cat| groups.remove(cat).map(|funcs| (*cat, funcs)))
491        .collect()
492}
493
494/// Find the closest function name to a misspelling.
495pub fn suggest_function(name: &str) -> Option<&'static str> {
496    let reg = registry();
497    let mut best: Option<(&str, usize)> = None;
498    for sig in &reg {
499        let dist = edit_distance(name, sig.name);
500        if dist <= 3 && (best.is_none() || dist < best.unwrap().1) {
501            best = Some((sig.name, dist));
502        }
503    }
504    best.map(|(name, _)| name)
505}
506
507/// Find a registered function by name.
508///
509/// Iterates the link-time inventory directly and returns the
510/// actual `&'static FuncSig` — the registration slices are
511/// already `'static` (see [`NodeRegistration::signatures`]), so
512/// no allocation is needed. (The former implementation built an
513/// owned `Vec` and `Box::leak`'d a clone to fabricate the
514/// `'static` lifetime, leaking ~200 bytes per call — Miri's
515/// leak-check finding 2026-06-12.)
516pub fn lookup(name: &str) -> Option<&'static FuncSig> {
517    for reg in inventory::iter::<NodeRegistration> {
518        for sig in (reg.signatures)() {
519            if sig.name == name {
520                return Some(sig);
521            }
522        }
523    }
524    None
525}
526
527fn edit_distance(a: &str, b: &str) -> usize {
528    let a: Vec<char> = a.chars().collect();
529    let b: Vec<char> = b.chars().collect();
530    let mut matrix = vec![vec![0usize; b.len() + 1]; a.len() + 1];
531    for (i, row) in matrix.iter_mut().enumerate() {
532        row[0] = i;
533    }
534    for (j, cell) in matrix[0].iter_mut().enumerate() {
535        *cell = j;
536    }
537    for i in 1..=a.len() {
538        for j in 1..=b.len() {
539            let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
540            matrix[i][j] = (matrix[i - 1][j] + 1)
541                .min(matrix[i][j - 1] + 1)
542                .min(matrix[i - 1][j - 1] + cost);
543        }
544    }
545    matrix[a.len()][b.len()]
546}
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551
552    #[test]
553    fn suggest_no_match() {
554        assert_eq!(suggest_function("zzzzzzzzz"), None);
555    }
556
557    #[test]
558    fn lookup_missing() {
559        assert!(lookup("nonexistent").is_none());
560    }
561
562    #[test]
563    fn every_function_has_category() {
564        let reg = registry();
565        for sig in &reg {
566            // Just verify the category display name is non-empty
567            assert!(
568                !sig.category.display_name().is_empty(),
569                "function '{}' has no category display name",
570                sig.name
571            );
572        }
573    }
574
575    #[test]
576    fn by_category_covers_all() {
577        let grouped = by_category();
578        let total: usize = grouped.iter().map(|(_, funcs)| funcs.len()).sum();
579        let reg = registry();
580        assert_eq!(
581            total,
582            reg.len(),
583            "by_category must cover all registered functions"
584        );
585    }
586
587    #[test]
588    fn category_parse_roundtrip() {
589        for cat in FuncCategory::display_order() {
590            let name = cat.display_name();
591            let parsed = FuncCategory::parse(name);
592            assert_eq!(parsed, Some(*cat), "failed to parse category '{name}'");
593        }
594    }
595
596    #[test]
597    fn registry_has_entries() {
598        let reg = registry();
599        assert!(reg.len() > 50, "registry should have 50+ functions");
600    }
601
602    // --- Unified param model tests ---
603
604    #[test]
605    fn printf_has_const_str_param() {
606        // SRD-80b Phase E: printf migrated to `#[polydat_node]` with
607        // a `Const<&str> format` arg + `&[Value] parts` variadic.
608        // The macro lists both in `params` (the variadic arg appears
609        // as a SlotType::Wire entry whose count is governed by the
610        // node's `Arity::VariadicWires`); the pre-migration
611        // hand-written FuncSig listed only the const. The
612        // load-bearing assertion is that the FIRST param is the
613        // format ConstStr and the arity is variadic — both still
614        // hold.
615        let sig = lookup("printf").unwrap();
616        assert!(
617            !sig.params.is_empty(),
618            "printf must have at least the format param"
619        );
620        assert!(matches!(sig.params[0].slot_type, SlotType::ConstStr));
621        assert!(matches!(sig.arity, Arity::VariadicWires { .. }));
622    }
623}