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