pub use crate::node::CompileLevel;
use crate::assembly::WireRef;
pub struct NodeRegistration {
pub signatures: fn() -> &'static [FuncSig],
pub build: fn(&str, &[WireRef], &[crate::node::PortType], &[crate::dsl::factory::ConstArg]) -> Option<Result<Box<dyn crate::node::GkNode>, String>>,
pub validate: Option<crate::dsl::const_constraints::NodeValidator>,
}
inventory::collect!(NodeRegistration);
#[macro_export]
macro_rules! register_nodes {
($sigs:expr, $builder:expr) => {
inventory::submit! {
$crate::dsl::registry::NodeRegistration {
signatures: $sigs,
build: $builder,
validate: None,
}
}
};
($sigs:expr, $builder:expr, $validator:expr) => {
inventory::submit! {
$crate::dsl::registry::NodeRegistration {
signatures: $sigs,
build: $builder,
validate: Some($validator),
}
}
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FuncCategory {
Hashing,
Arithmetic,
Comparison,
Variadic,
Conversions,
Distributions,
Datetime,
Encoding,
Interpolation,
Math,
Probability,
Weighted,
Formatting,
String,
Json,
ByteBuffers,
Digest,
Noise,
Regex,
Permutation,
RealData,
Context,
Diagnostic,
Data,
}
impl FuncCategory {
pub fn display_name(&self) -> &'static str {
match self {
Self::Hashing => "Hashing",
Self::Arithmetic => "Arithmetic",
Self::Comparison => "Comparison",
Self::Variadic => "Variadic",
Self::Conversions => "Conversions",
Self::Distributions => "Distributions",
Self::Datetime => "Datetime",
Self::Encoding => "Encoding",
Self::Interpolation => "Interpolation",
Self::Math => "Math",
Self::Probability => "Probability",
Self::Weighted => "Weighted",
Self::Formatting => "Formatting",
Self::String => "String",
Self::Json => "JSON",
Self::ByteBuffers => "Byte Buffers",
Self::Digest => "Digest",
Self::Noise => "Noise",
Self::Regex => "Regex",
Self::Permutation => "Permutation",
Self::RealData => "Real Data",
Self::Context => "Context",
Self::Diagnostic => "Diagnostic",
Self::Data => "Data",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_lowercase().as_str() {
"hashing" => Some(Self::Hashing),
"arithmetic" => Some(Self::Arithmetic),
"comparison" | "compare" => Some(Self::Comparison),
"variadic" => Some(Self::Variadic),
"conversions" | "conversion" => Some(Self::Conversions),
"distributions" | "distribution" => Some(Self::Distributions),
"datetime" | "date" | "time" => Some(Self::Datetime),
"encoding" => Some(Self::Encoding),
"interpolation" | "lerp" => Some(Self::Interpolation),
"math" | "trig" | "trigonometry" => Some(Self::Math),
"probability" => Some(Self::Probability),
"weighted" => Some(Self::Weighted),
"formatting" | "format" | "printf" => Some(Self::Formatting),
"string" | "strings" => Some(Self::String),
"json" => Some(Self::Json),
"byte buffers" | "bytebuffers" | "bytes" => Some(Self::ByteBuffers),
"digest" => Some(Self::Digest),
"noise" => Some(Self::Noise),
"regex" => Some(Self::Regex),
"permutation" | "shuffle" => Some(Self::Permutation),
"real data" | "realdata" | "realer" => Some(Self::RealData),
"context" => Some(Self::Context),
"diagnostic" | "diagnostics" | "debug" => Some(Self::Diagnostic),
"data" | "datafile" | "csv" | "jsonl" => Some(Self::Data),
_ => None,
}
}
pub fn display_order() -> &'static [Self] {
&[
Self::Hashing, Self::Arithmetic, Self::Comparison, Self::Variadic,
Self::Conversions, Self::Distributions, Self::Datetime,
Self::Encoding, Self::Interpolation, Self::Math, Self::Probability,
Self::Weighted, Self::Formatting, Self::String,
Self::Json, Self::ByteBuffers, Self::Digest, Self::Noise,
Self::Regex, Self::Permutation, Self::RealData,
Self::Context, Self::Diagnostic, Self::Data,
]
}
}
use crate::node::SlotType;
#[derive(Debug, Clone, Copy)]
pub struct ParamSpec {
pub name: &'static str,
pub slot_type: SlotType,
pub required: bool,
pub example: &'static str,
pub constraint: Option<crate::dsl::const_constraints::ConstConstraint>,
}
impl ParamSpec {
pub const fn with_constraint(mut self, c: crate::dsl::const_constraints::ConstConstraint) -> Self {
self.constraint = Some(c);
self
}
}
#[derive(Debug, Clone)]
#[derive(Default)]
pub enum Arity {
#[default]
Fixed,
VariadicWires { min_wires: usize },
VariadicConsts { min_consts: usize },
VariadicGroup {
group: &'static [SlotType],
min_repeats: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputType {
Fixed,
SameAsInput(usize),
}
pub struct FuncSig {
pub name: &'static str,
pub category: FuncCategory,
pub outputs: usize,
pub description: &'static str,
pub help: &'static str,
pub identity: Option<u64>,
pub variadic_ctor: Option<fn(usize) -> Box<dyn crate::node::GkNode>>,
pub params: &'static [ParamSpec],
pub arity: Arity,
pub commutativity: crate::node::Commutativity,
pub default_resolver: Option<DefaultResolver>,
pub output_type: OutputType,
}
#[derive(Debug, Clone, Copy)]
pub enum DefaultResolver {
Facet(&'static str),
Group,
}
impl FuncSig {
pub fn wire_input_count(&self) -> usize {
self.params.iter().filter(|p| p.slot_type.is_wire()).count()
}
pub fn is_variadic(&self) -> bool {
!matches!(self.arity, Arity::Fixed)
}
pub fn const_param_info(&self) -> Vec<(&'static str, bool)> {
self.params.iter()
.filter(|p| p.slot_type.is_const())
.map(|p| (p.name, p.required))
.collect()
}
}
impl std::fmt::Debug for FuncSig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FuncSig")
.field("name", &self.name)
.field("category", &self.category)
.field("params", &self.params)
.field("arity", &self.arity)
.finish()
}
}
impl Clone for FuncSig {
fn clone(&self) -> Self {
Self {
name: self.name,
category: self.category,
outputs: self.outputs,
description: self.description,
help: self.help,
identity: self.identity,
variadic_ctor: self.variadic_ctor,
params: self.params,
arity: self.arity.clone(),
commutativity: self.commutativity.clone(),
default_resolver: self.default_resolver,
output_type: self.output_type,
}
}
}
pub fn registry() -> Vec<FuncSig> {
let mut funcs = Vec::new();
for reg in inventory::iter::<NodeRegistration> {
funcs.extend_from_slice((reg.signatures)());
}
funcs
}
pub fn by_category() -> Vec<(FuncCategory, Vec<FuncSig>)> {
let reg = registry();
let mut groups: std::collections::HashMap<FuncCategory, Vec<FuncSig>> = std::collections::HashMap::new();
for sig in reg {
groups.entry(sig.category).or_default().push(sig);
}
FuncCategory::display_order().iter()
.filter_map(|cat| {
groups.remove(cat).map(|funcs| (*cat, funcs))
})
.collect()
}
pub fn suggest_function(name: &str) -> Option<&'static str> {
let reg = registry();
let mut best: Option<(&str, usize)> = None;
for sig in ® {
let dist = edit_distance(name, sig.name);
if dist <= 3
&& (best.is_none() || dist < best.unwrap().1) {
best = Some((sig.name, dist));
}
}
best.map(|(name, _)| name)
}
pub fn lookup(name: &str) -> Option<&'static FuncSig> {
let reg = registry();
for sig in ® {
if sig.name == name {
return Some(Box::leak(Box::new(sig.clone())));
}
}
None
}
fn edit_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let mut matrix = vec![vec![0usize; b.len() + 1]; a.len() + 1];
for i in 0..=a.len() { matrix[i][0] = i; }
for j in 0..=b.len() { matrix[0][j] = j; }
for i in 1..=a.len() {
for j in 1..=b.len() {
let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
matrix[i][j] = (matrix[i - 1][j] + 1)
.min(matrix[i][j - 1] + 1)
.min(matrix[i - 1][j - 1] + cost);
}
}
matrix[a.len()][b.len()]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn suggest_close_match() {
assert_eq!(suggest_function("hsh"), Some("hash"));
assert_eq!(suggest_function("hahs"), Some("hash"));
assert_eq!(suggest_function("interleav"), Some("interleave"));
}
#[test]
fn suggest_no_match() {
assert_eq!(suggest_function("zzzzzzzzz"), None);
}
#[test]
fn lookup_exists() {
let sig = lookup("hash").unwrap();
assert_eq!(sig.wire_input_count(), 1);
assert_eq!(sig.outputs, 1);
assert!(!sig.is_variadic());
assert_eq!(sig.category, FuncCategory::Hashing);
}
#[test]
fn lookup_missing() {
assert!(lookup("nonexistent").is_none());
}
#[test]
fn lookup_variadic() {
let sig = lookup("sum").unwrap();
assert!(sig.is_variadic());
assert_eq!(sig.identity, Some(0));
assert_eq!(sig.category, FuncCategory::Variadic);
}
#[test]
fn every_function_has_category() {
let reg = registry();
for sig in ® {
assert!(!sig.category.display_name().is_empty(),
"function '{}' has no category display name", sig.name);
}
}
#[test]
fn by_category_covers_all() {
let grouped = by_category();
let total: usize = grouped.iter().map(|(_, funcs)| funcs.len()).sum();
let reg = registry();
assert_eq!(total, reg.len(), "by_category must cover all registered functions");
}
#[test]
fn category_parse_roundtrip() {
for cat in FuncCategory::display_order() {
let name = cat.display_name();
let parsed = FuncCategory::parse(name);
assert_eq!(parsed, Some(*cat), "failed to parse category '{name}'");
}
}
#[test]
fn registry_has_entries() {
let reg = registry();
assert!(reg.len() > 50, "registry should have 50+ functions");
}
#[test]
fn arithmetic_params_populated() {
for name in &["add", "mul", "div", "mod", "clamp", "interleave", "mixed_radix"] {
let sig = lookup(name).unwrap_or_else(|| panic!("missing '{name}'"));
assert!(!sig.params.is_empty(),
"function '{}' should have params populated", name);
}
}
#[test]
fn variadic_params_populated() {
for name in &["sum", "product", "min", "max"] {
let sig = lookup(name).unwrap_or_else(|| panic!("missing '{name}'"));
assert!(matches!(sig.arity, Arity::VariadicWires { .. }),
"function '{}' should have VariadicWires arity", name);
}
}
#[test]
fn mixed_radix_is_variadic_consts() {
let sig = lookup("mixed_radix").unwrap();
assert!(matches!(sig.arity, Arity::VariadicConsts { min_consts: 1 }),
"mixed_radix should be VariadicConsts");
assert_eq!(sig.params.len(), 1); assert!(matches!(sig.params[0].slot_type, SlotType::Wire));
}
#[test]
fn printf_has_const_str_param() {
let sig = lookup("printf").unwrap();
assert_eq!(sig.params.len(), 1);
assert!(matches!(sig.params[0].slot_type, SlotType::ConstStr));
assert!(matches!(sig.arity, Arity::VariadicWires { .. }));
}
}