Skip to main content

rill_lang/
builtin.rs

1//! Foreign-function registry: DSP/model built-ins callable from rill-lang.
2//!
3//! Two kinds: [`SampleBuiltin`] (per-sample, feedback-legal) and block built-ins
4//! (`rill_core::Algorithm`, opaque whole-buffer). Concrete bindings live outside
5//! this crate (e.g. `rill-adrift`); core stays `rill-core`-only.
6
7use std::collections::HashMap;
8
9use rill_core::math::Transcendental;
10
11/// A stateful per-sample built-in: `signal_ins` inputs → 1 output.
12pub trait SampleBuiltin<T: Transcendental>: Send + Sync {
13    /// Process one sample. `inputs.len() == signal_ins`.
14    fn process_sample(&mut self, inputs: &[T]) -> T;
15    /// Re-initialise for a sample rate (default no-op).
16    fn init(&mut self, _sample_rate: f32) {}
17    /// Clear internal state.
18    fn reset(&mut self);
19    /// Set a parameter by index (default no-op).
20    fn set_param(&mut self, _index: usize, _value: T) {}
21}
22
23/// A whole-buffer built-in with settable params.
24pub trait BlockBuiltin<T: Transcendental>: rill_core::traits::Algorithm<T> {
25    /// Set a parameter by index (default no-op).
26    fn set_param(&mut self, _index: usize, _value: T) {}
27}
28
29/// Whether a built-in is per-sample or whole-buffer.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum BuiltinKind {
32    /// Per-sample [`SampleBuiltin`].
33    Sample,
34    /// Whole-buffer `Algorithm` (1→1).
35    Block,
36}
37
38/// Type-checker-facing signature of a built-in (independent of `T`).
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct BuiltinSig {
41    /// Registered name.
42    pub name: &'static str,
43    /// Number of signal inputs.
44    pub signal_ins: usize,
45    /// Number of signal outputs (1 in this increment).
46    pub signal_outs: usize,
47    /// Number of constant params.
48    pub num_params: usize,
49    /// Sample vs block.
50    pub kind: BuiltinKind,
51}
52
53/// A boxed factory building an instance from folded params + a sample rate.
54type SampleFactory<T> = Box<dyn Fn(&[f64], f32) -> Box<dyn SampleBuiltin<T>> + Send + Sync>;
55type BlockFactory<T> = Box<dyn Fn(&[f64], f32) -> Box<dyn BlockBuiltin<T>> + Send + Sync>;
56
57enum Factory<T: Transcendental> {
58    Sample(SampleFactory<T>),
59    Block(BlockFactory<T>),
60}
61
62/// A registry entry.
63pub struct Entry<T: Transcendental> {
64    /// The signature.
65    pub sig: BuiltinSig,
66    factory: Factory<T>,
67}
68
69impl<T: Transcendental> Entry<T> {
70    /// Build a sample instance (panics if this entry is a block built-in — callers
71    /// gate on `sig.kind`).
72    pub fn build_sample(
73        &self,
74        params: &[f64],
75        sample_rate: f32,
76    ) -> Option<Box<dyn SampleBuiltin<T>>> {
77        match &self.factory {
78            Factory::Sample(f) => Some(f(params, sample_rate)),
79            Factory::Block(_) => None,
80        }
81    }
82    /// Build a block instance.
83    pub fn build_block(
84        &self,
85        params: &[f64],
86        sample_rate: f32,
87    ) -> Option<Box<dyn BlockBuiltin<T>>> {
88        match &self.factory {
89            Factory::Block(f) => Some(f(params, sample_rate)),
90            Factory::Sample(_) => None,
91        }
92    }
93}
94
95/// A collection of built-in definitions.
96pub struct Registry<T: Transcendental> {
97    entries: HashMap<String, Entry<T>>,
98}
99
100impl<T: Transcendental> Default for Registry<T> {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl<T: Transcendental> Registry<T> {
107    /// An empty registry.
108    pub fn new() -> Self {
109        Self {
110            entries: HashMap::new(),
111        }
112    }
113
114    /// Register a per-sample built-in.
115    pub fn register_sample(
116        &mut self,
117        sig: BuiltinSig,
118        factory: impl Fn(&[f64], f32) -> Box<dyn SampleBuiltin<T>> + Send + Sync + 'static,
119    ) {
120        debug_assert_eq!(sig.kind, BuiltinKind::Sample);
121        self.entries.insert(
122            sig.name.to_string(),
123            Entry {
124                sig,
125                factory: Factory::Sample(Box::new(factory)),
126            },
127        );
128    }
129
130    /// Register a whole-buffer (`Algorithm`) built-in.
131    pub fn register_block(
132        &mut self,
133        sig: BuiltinSig,
134        factory: impl Fn(&[f64], f32) -> Box<dyn BlockBuiltin<T>> + Send + Sync + 'static,
135    ) {
136        debug_assert_eq!(sig.kind, BuiltinKind::Block);
137        self.entries.insert(
138            sig.name.to_string(),
139            Entry {
140                sig,
141                factory: Factory::Block(Box::new(factory)),
142            },
143        );
144    }
145
146    /// Look up an entry by name.
147    pub fn get(&self, name: &str) -> Option<&Entry<T>> {
148        self.entries.get(name)
149    }
150}
151
152/// A `T`-independent signature lookup used by the type checker and lowering.
153pub trait SignatureSource {
154    /// The signature for `name`, if registered.
155    fn builtin_sig(&self, name: &str) -> Option<&BuiltinSig>;
156}
157
158impl<T: Transcendental> SignatureSource for Registry<T> {
159    fn builtin_sig(&self, name: &str) -> Option<&BuiltinSig> {
160        self.entries.get(name).map(|e| &e.sig)
161    }
162}
163
164/// A signature source with no built-ins (used by `compile()` / existing tests).
165pub struct NoSigs;
166impl SignatureSource for NoSigs {
167    fn builtin_sig(&self, _name: &str) -> Option<&BuiltinSig> {
168        None
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    struct Gain {
177        k: f32,
178    }
179    impl SampleBuiltin<f32> for Gain {
180        fn process_sample(&mut self, inputs: &[f32]) -> f32 {
181            inputs[0] * self.k
182        }
183        fn reset(&mut self) {}
184    }
185
186    #[test]
187    fn register_and_lookup_sample() {
188        let mut reg = Registry::<f32>::new();
189        reg.register_sample(
190            BuiltinSig {
191                name: "gain",
192                signal_ins: 1,
193                signal_outs: 1,
194                num_params: 1,
195                kind: BuiltinKind::Sample,
196            },
197            |params, _sr| {
198                Box::new(Gain {
199                    k: params[0] as f32,
200                })
201            },
202        );
203        let sig = reg.builtin_sig("gain").unwrap();
204        assert_eq!((sig.signal_ins, sig.num_params), (1, 1));
205        let mut inst = reg
206            .get("gain")
207            .unwrap()
208            .build_sample(&[0.5], 44100.0)
209            .unwrap();
210        assert_eq!(inst.process_sample(&[2.0]), 1.0);
211        assert!(reg.builtin_sig("missing").is_none());
212    }
213}