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, used for probing compile
282 /// level and for documentation. Wire params use `"cycle"`,
283 /// const params use a representative value that passes validation.
284 pub example: &'static str,
285 /// Optional assembly-time validation rule (SRD 15 §"Const
286 /// Constraint Metadata"). The factory enforces this before
287 /// `build_node` so node constructors can stay infallible and
288 /// branch-free at runtime. `None` = no constraint declared
289 /// (default for wires and unconstrained constants).
290 pub constraint: Option<crate::dsl::const_constraints::ConstConstraint>,
291}
292
293impl ParamSpec {
294 /// Convenience: chainable on a literal to attach a constraint.
295 /// Used by node modules that want to keep the literal compact.
296 pub const fn with_constraint(
297 mut self,
298 c: crate::dsl::const_constraints::ConstConstraint,
299 ) -> Self {
300 self.constraint = Some(c);
301 self
302 }
303}
304
305/// Arity specification for a function signature.
306///
307/// Describes which parts of the parameter list are fixed vs repeatable.
308#[derive(Debug, Clone, Default)]
309pub enum Arity {
310 /// Exactly the parameters declared in `params`.
311 #[default]
312 Fixed,
313 /// Trailing wire parameters repeat (sum, product, min, max).
314 VariadicWires {
315 /// The fewest trailing wires allowed.
316 min_wires: usize,
317 },
318 /// Trailing constant parameters repeat (mixed_radix).
319 VariadicConsts {
320 /// The fewest trailing constants allowed.
321 min_consts: usize,
322 },
323 /// A repeating group of slot types (weighted_sum).
324 VariadicGroup {
325 /// The slot types of one repetition, in order.
326 group: &'static [SlotType],
327 /// The fewest repetitions allowed.
328 min_repeats: usize,
329 },
330}
331
332/// Output-type contract for a registered function.
333///
334/// Most nodes have fixed port types declared by their constructor's
335/// `NodeMeta`. Some — `log_info`, `identity`, anything documented
336/// as "pass-through" — produce an output whose type matches one
337/// of their inputs. Declaring this here makes the contract visible
338/// to the assembler, the build-node dispatch path, registry
339/// listings, and any future static analysis, instead of being
340/// buried inside an `eval` that silently passes values through a
341/// wire whose declared type lies.
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub enum OutputType {
344 /// Output types are whatever the constructor's `NodeMeta`
345 /// declares — independent of input wire types. The default;
346 /// covers the vast majority of nodes (`hash`, `regex_match`,
347 /// `mod`, …) whose I/O contract is type-fixed.
348 Fixed,
349 /// The function's single output port has the same type as the
350 /// input wire at the given index. The build dispatch resolves
351 /// the wire's type from the assembler and the module's
352 /// `build_node` reads it from the supplied `wire_types` slice
353 /// to construct a port-typed node. Used by pass-through
354 /// nodes (`log_info`, `log_debug`, …).
355 SameAsInput(usize),
356}
357
358/// Description of a registered function's signature.
359pub struct FuncSig {
360 /// Function name as used in the DSL.
361 pub name: &'static str,
362 /// Functional category.
363 pub category: FuncCategory,
364 /// Number of output ports (0 = dynamic, determined at compile time).
365 pub outputs: usize,
366 /// Short description for help/error messages.
367 pub description: &'static str,
368 /// Detailed help text: theory, usage examples, parameter meanings.
369 /// Long-form help text for listings and documentation.
370 pub help: &'static str,
371 /// For variadic functions: the identity element for zero inputs.
372 pub identity: Option<u64>,
373 /// Factory for variadic nodes: takes wire count, returns node.
374 pub variadic_ctor: Option<fn(usize) -> Box<dyn crate::ast::PolydatNode>>,
375 /// Positional parameter list: wires and constants in call order.
376 pub params: &'static [ParamSpec],
377 /// Arity specification.
378 pub arity: Arity,
379 /// Input commutativity for this function.
380 pub commutativity: crate::ast::Commutativity,
381 /// Optional resolver hint for `Handle`-typed input ports. When
382 /// the binding compiler emits this function and a `Handle`
383 /// input is wired to a `Str`-producing source, it splices in
384 /// the named resolver to convert the string into a handle. This
385 /// is the "string-conversion node insertion" mechanism from
386 /// SRD 53 §"Source-string call-site sugar". `None` means no
387 /// auto-promotion — the caller must pass a `Handle` directly.
388 pub default_resolver: Option<DefaultResolver>,
389 /// Output-type contract — `Fixed` for the vast majority of
390 /// nodes; `SameAsInput(idx)` for type-polymorphic pass-throughs
391 /// (e.g. `log_info` whose output type tracks its sole input).
392 pub output_type: OutputType,
393 /// Concrete port type of the single output, when statically
394 /// known (`#[polydat_node]` emits it from the return type's
395 /// `Wire::PORT`). `None` for tuple/dynamic/polymorphic outputs
396 /// and for hand registrations that don't declare one. The DSL
397 /// type inference (`binding::infer_expr_type`) reads this
398 /// FIRST — the name-prefix heuristic is only the fallback —
399 /// so call-expression operand typing flows from the symbol
400 /// registry, not from a hand-maintained list.
401 pub output_port: Option<crate::ast::PortType>,
402}
403
404/// Auto-resolver kind attached to handle-taking functions. Tells the
405/// binding compiler which resolver to splice in when a string source
406/// is wired to a handle input port.
407#[derive(Debug, Clone, Copy)]
408pub enum DefaultResolver {
409 /// Insert `dataset_open(<source_wire>, "<facet>")` between the
410 /// string source and the handle input.
411 Facet(&'static str),
412 /// Insert `dataset_group_open(<source_wire>)` between the string
413 /// source and the handle input.
414 Group,
415}
416
417impl FuncSig {
418 /// Number of wire inputs in the fixed parameter list.
419 pub fn wire_input_count(&self) -> usize {
420 self.params.iter().filter(|p| p.slot_type.is_wire()).count()
421 }
422
423 /// Whether this function accepts variadic arguments.
424 pub fn is_variadic(&self) -> bool {
425 !matches!(self.arity, Arity::Fixed)
426 }
427
428 /// Constant parameter names and whether they're required.
429 pub fn const_param_info(&self) -> Vec<(&'static str, bool)> {
430 self.params
431 .iter()
432 .filter(|p| p.slot_type.is_const())
433 .map(|p| (p.name, p.required))
434 .collect()
435 }
436}
437
438impl std::fmt::Debug for FuncSig {
439 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440 f.debug_struct("FuncSig")
441 .field("name", &self.name)
442 .field("category", &self.category)
443 .field("params", &self.params)
444 .field("arity", &self.arity)
445 .finish()
446 }
447}
448
449impl Clone for FuncSig {
450 fn clone(&self) -> Self {
451 Self {
452 name: self.name,
453 category: self.category,
454 outputs: self.outputs,
455 description: self.description,
456 help: self.help,
457 identity: self.identity,
458 variadic_ctor: self.variadic_ctor,
459 params: self.params,
460 arity: self.arity.clone(),
461 commutativity: self.commutativity.clone(),
462 default_resolver: self.default_resolver,
463 output_type: self.output_type,
464 output_port: self.output_port,
465 }
466 }
467}
468
469/// Return the full registry of known functions.
470///
471/// Iterates all `NodeRegistration` entries submitted via `inventory::submit!`
472/// at link time. No explicit module list is required here — each node module
473/// registers itself by calling `register_nodes!` at module scope.
474pub fn registry() -> Vec<FuncSig> {
475 let mut funcs = Vec::new();
476 for reg in inventory::iter::<NodeRegistration> {
477 funcs.extend_from_slice((reg.signatures)());
478 }
479 funcs
480}
481
482/// Return functions grouped by category in display order.
483pub fn by_category() -> Vec<(FuncCategory, Vec<FuncSig>)> {
484 let reg = registry();
485 let mut groups: std::collections::HashMap<FuncCategory, Vec<FuncSig>> =
486 std::collections::HashMap::new();
487 for sig in reg {
488 groups.entry(sig.category).or_default().push(sig);
489 }
490 FuncCategory::display_order()
491 .iter()
492 .filter_map(|cat| groups.remove(cat).map(|funcs| (*cat, funcs)))
493 .collect()
494}
495
496/// Find the closest function name to a misspelling.
497pub fn suggest_function(name: &str) -> Option<&'static str> {
498 let reg = registry();
499 let mut best: Option<(&str, usize)> = None;
500 for sig in ® {
501 let dist = edit_distance(name, sig.name);
502 if dist <= 3 && (best.is_none() || dist < best.unwrap().1) {
503 best = Some((sig.name, dist));
504 }
505 }
506 best.map(|(name, _)| name)
507}
508
509/// Find a registered function by name.
510///
511/// Iterates the link-time inventory directly and returns the
512/// actual `&'static FuncSig` — the registration slices are
513/// already `'static` (see [`NodeRegistration::signatures`]), so
514/// no allocation is needed. (The former implementation built an
515/// owned `Vec` and `Box::leak`'d a clone to fabricate the
516/// `'static` lifetime, leaking ~200 bytes per call — Miri's
517/// leak-check finding 2026-06-12.)
518pub fn lookup(name: &str) -> Option<&'static FuncSig> {
519 for reg in inventory::iter::<NodeRegistration> {
520 for sig in (reg.signatures)() {
521 if sig.name == name {
522 return Some(sig);
523 }
524 }
525 }
526 None
527}
528
529fn edit_distance(a: &str, b: &str) -> usize {
530 let a: Vec<char> = a.chars().collect();
531 let b: Vec<char> = b.chars().collect();
532 let mut matrix = vec![vec![0usize; b.len() + 1]; a.len() + 1];
533 for (i, row) in matrix.iter_mut().enumerate() {
534 row[0] = i;
535 }
536 for (j, cell) in matrix[0].iter_mut().enumerate() {
537 *cell = j;
538 }
539 for i in 1..=a.len() {
540 for j in 1..=b.len() {
541 let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
542 matrix[i][j] = (matrix[i - 1][j] + 1)
543 .min(matrix[i][j - 1] + 1)
544 .min(matrix[i - 1][j - 1] + cost);
545 }
546 }
547 matrix[a.len()][b.len()]
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
555 fn suggest_no_match() {
556 assert_eq!(suggest_function("zzzzzzzzz"), None);
557 }
558
559 #[test]
560 fn lookup_missing() {
561 assert!(lookup("nonexistent").is_none());
562 }
563
564 #[test]
565 fn every_function_has_category() {
566 let reg = registry();
567 for sig in ® {
568 // Just verify the category display name is non-empty
569 assert!(
570 !sig.category.display_name().is_empty(),
571 "function '{}' has no category display name",
572 sig.name
573 );
574 }
575 }
576
577 #[test]
578 fn by_category_covers_all() {
579 let grouped = by_category();
580 let total: usize = grouped.iter().map(|(_, funcs)| funcs.len()).sum();
581 let reg = registry();
582 assert_eq!(
583 total,
584 reg.len(),
585 "by_category must cover all registered functions"
586 );
587 }
588
589 #[test]
590 fn category_parse_roundtrip() {
591 for cat in FuncCategory::display_order() {
592 let name = cat.display_name();
593 let parsed = FuncCategory::parse(name);
594 assert_eq!(parsed, Some(*cat), "failed to parse category '{name}'");
595 }
596 }
597
598 #[test]
599 fn registry_has_entries() {
600 let reg = registry();
601 assert!(reg.len() > 50, "registry should have 50+ functions");
602 }
603
604 // --- Unified param model tests ---
605
606 #[test]
607 fn printf_has_const_str_param() {
608 // SRD-80b Phase E: printf migrated to `#[polydat_node]` with
609 // a `Const<&str> format` arg + `&[Value] parts` variadic.
610 // The macro lists both in `params` (the variadic arg appears
611 // as a SlotType::Wire entry whose count is governed by the
612 // node's `Arity::VariadicWires`); the pre-migration
613 // hand-written FuncSig listed only the const. The
614 // load-bearing assertion is that the FIRST param is the
615 // format ConstStr and the arity is variadic — both still
616 // hold.
617 let sig = lookup("printf").unwrap();
618 assert!(
619 !sig.params.is_empty(),
620 "printf must have at least the format param"
621 );
622 assert!(matches!(sig.params[0].slot_type, SlotType::ConstStr));
623 assert!(matches!(sig.arity, Arity::VariadicWires { .. }));
624 }
625}