Skip to main content

rill_core/
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 crate::math::Transcendental;
10use crate::traits::ParamValue;
11
12/// A stateful per-sample built-in: `signal_ins` inputs → 1 output.
13pub trait SampleBuiltin<T: Transcendental>: Send + Sync {
14    /// Process one sample. `inputs.len() == signal_ins`.
15    fn process_sample(&mut self, inputs: &[T]) -> T;
16    /// Re-initialise for a sample rate (default no-op).
17    fn init(&mut self, _sample_rate: f32) {}
18    /// Clear internal state.
19    fn reset(&mut self);
20    /// Set a parameter by index.
21    fn set_param(&mut self, _index: usize, _value: &ParamValue) {}
22}
23
24/// A whole-buffer built-in with settable params.
25pub trait BlockBuiltin<T: Transcendental>: crate::traits::Algorithm<T> {
26    /// Set a parameter by index.
27    fn set_param(&mut self, _index: usize, _value: &ParamValue) {}
28}
29
30/// A whole-buffer multi-channel built-in with settable params.
31pub trait MultichannelBlockBuiltin<T: Transcendental>:
32    crate::traits::MultichannelAlgorithm<T> + Send + Sync
33{
34    /// Set a parameter by index.
35    fn set_param(&mut self, _index: usize, _value: &ParamValue) {}
36}
37
38/// Whether a built-in is per-sample or whole-buffer.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum BuiltinKind {
41    /// Per-sample [`SampleBuiltin`].
42    Sample,
43    /// Whole-buffer `Algorithm` (1→1).
44    Block,
45}
46
47/// The type of a parameter in a built-in function signature.
48#[derive(Debug, Clone, PartialEq)]
49pub enum ParamType {
50    /// A signal wire argument — contributes to the built-in's input arity.
51    Signal,
52    /// A compile-time f64 constant.
53    Float,
54    /// A compile-time i64 constant.
55    Int,
56    /// A compile-time string literal.
57    String,
58    /// A compile-time boolean.
59    Bool,
60    /// A compile-time record literal with a known schema.
61    Record(RecordSchema),
62    /// A compile-time enum value with allowed variants.
63    Enum(&'static [&'static str]),
64    /// Zero or more arguments of the inner type.
65    Variadic(Box<ParamType>),
66}
67
68/// Schema for a record literal.
69#[derive(Debug, Clone, PartialEq)]
70pub struct RecordSchema {
71    /// Fields in declaration order.
72    pub fields: Vec<RecordField>,
73}
74
75/// A single field in a record schema.
76#[derive(Debug, Clone, PartialEq)]
77pub struct RecordField {
78    /// Field name.
79    pub name: &'static str,
80    /// Field type.
81    pub ty: ParamType,
82    /// Default value, if any.
83    pub default: Option<f64>,
84}
85
86impl RecordSchema {
87    /// Create a schema from a field list.
88    pub fn new(fields: Vec<RecordField>) -> Self {
89        Self { fields }
90    }
91}
92
93/// Type-checker-facing signature of a built-in (independent of `T`).
94#[derive(Debug, Clone, PartialEq)]
95pub struct BuiltinSig {
96    /// Registered name.
97    pub name: &'static str,
98    /// Parameter list: first N entries are signal inputs, remainder are compile-time params.
99    pub params: Vec<ParamType>,
100    /// Number of signal outputs (1 in this increment).
101    pub signal_outs: usize,
102    /// Sample vs block.
103    pub kind: BuiltinKind,
104    /// Names of compile-time parameters in `params` order (after signal inputs).
105    /// When non-empty, graph-level `build_ir()` uses these names to match recipe
106    /// params to builtin arg positions — eliminating ordering fragility from
107    /// `HashMap`-based param bags. Left empty for backward-compatible registrations.
108    pub param_names: Vec<&'static str>,
109}
110
111impl BuiltinSig {
112    /// Convenience constructor for SISO built-ins with only Float params.
113    /// Maintains backward compatibility during migration.
114    pub fn simple(
115        name: &'static str,
116        signal_ins: usize,
117        signal_outs: usize,
118        num_params: usize,
119        kind: BuiltinKind,
120    ) -> Self {
121        let mut params = Vec::with_capacity(signal_ins + num_params);
122        for _ in 0..signal_ins {
123            params.push(ParamType::Signal);
124        }
125        for _ in 0..num_params {
126            params.push(ParamType::Float);
127        }
128        Self {
129            name,
130            params,
131            signal_outs,
132            kind,
133            param_names: Vec::new(),
134        }
135    }
136
137    /// Attach human-readable names to compile-time parameters.
138    ///
139    /// `names.len()` must equal the number of non-signal params in `self.params`.
140    /// When set, graph-level `build_ir()` in `rill-graph` uses these names to
141    /// match recipe param keys to builtin arg positions, fixing the ordering
142    /// fragility of `HashMap`-based param bags.
143    pub fn with_names(mut self, names: Vec<&'static str>) -> Self {
144        self.param_names = names;
145        self
146    }
147
148    /// Number of signal inputs = count of Signal params (non-variadic).
149    pub fn signal_ins(&self) -> usize {
150        self.params
151            .iter()
152            .filter(|p| matches!(p, ParamType::Signal))
153            .count()
154    }
155
156    /// Minimum number of Apply arguments (excludes Signal params).
157    pub fn min_args(&self) -> usize {
158        let mut count = 0;
159        for p in &self.params {
160            match p {
161                ParamType::Signal | ParamType::Variadic(_) => {}
162                _ => count += 1,
163            }
164        }
165        count
166    }
167
168    /// Maximum number of Apply arguments (None if variadic; excludes Signal params).
169    pub fn max_args(&self) -> Option<usize> {
170        if self
171            .params
172            .iter()
173            .any(|p| matches!(p, ParamType::Variadic(_)))
174        {
175            None
176        } else {
177            Some(
178                self.params
179                    .iter()
180                    .filter(|p| !matches!(p, ParamType::Signal))
181                    .count(),
182            )
183        }
184    }
185}
186
187/// A boxed factory building an instance from folded params + a sample rate.
188type SampleFactory<T> = Box<dyn Fn(&[f64], f32) -> Box<dyn SampleBuiltin<T>> + Send + Sync>;
189type BlockFactory<T> = Box<dyn Fn(&[f64], f32) -> Box<dyn BlockBuiltin<T>> + Send + Sync>;
190type MultichannelBlockFactory<T> =
191    Box<dyn Fn(&[f64], f32) -> Box<dyn MultichannelBlockBuiltin<T>> + Send + Sync>;
192
193enum Factory<T: Transcendental> {
194    Sample(SampleFactory<T>),
195    Block(BlockFactory<T>),
196    MultichannelBlock(MultichannelBlockFactory<T>),
197}
198
199/// A registry entry.
200pub struct Entry<T: Transcendental> {
201    /// The signature.
202    pub sig: BuiltinSig,
203    factory: Factory<T>,
204}
205
206impl<T: Transcendental> Entry<T> {
207    /// Build a sample instance (panics if this entry is a block built-in — callers
208    /// gate on `sig.kind`).
209    pub fn build_sample(
210        &self,
211        params: &[f64],
212        sample_rate: f32,
213    ) -> Option<Box<dyn SampleBuiltin<T>>> {
214        match &self.factory {
215            Factory::Sample(f) => Some(f(params, sample_rate)),
216            Factory::Block(_) | Factory::MultichannelBlock(_) => None,
217        }
218    }
219    /// Build a block instance.
220    pub fn build_block(
221        &self,
222        params: &[f64],
223        sample_rate: f32,
224    ) -> Option<Box<dyn BlockBuiltin<T>>> {
225        match &self.factory {
226            Factory::Block(f) => Some(f(params, sample_rate)),
227            Factory::Sample(_) | Factory::MultichannelBlock(_) => None,
228        }
229    }
230    /// Build a multichannel block instance.
231    pub fn build_multichannel_block(
232        &self,
233        params: &[f64],
234        sample_rate: f32,
235    ) -> Option<Box<dyn MultichannelBlockBuiltin<T>>> {
236        match &self.factory {
237            Factory::MultichannelBlock(f) => Some(f(params, sample_rate)),
238            _ => None,
239        }
240    }
241}
242
243/// A collection of built-in definitions.
244pub struct Registry<T: Transcendental> {
245    entries: HashMap<String, Entry<T>>,
246}
247
248impl<T: Transcendental> Default for Registry<T> {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254impl<T: Transcendental> Registry<T> {
255    /// An empty registry.
256    pub fn new() -> Self {
257        Self {
258            entries: HashMap::new(),
259        }
260    }
261
262    /// Register a per-sample built-in.
263    pub fn register_sample(
264        &mut self,
265        sig: BuiltinSig,
266        factory: impl Fn(&[f64], f32) -> Box<dyn SampleBuiltin<T>> + Send + Sync + 'static,
267    ) {
268        debug_assert_eq!(sig.kind, BuiltinKind::Sample);
269        self.entries.insert(
270            sig.name.to_string(),
271            Entry {
272                sig,
273                factory: Factory::Sample(Box::new(factory)),
274            },
275        );
276    }
277
278    /// Register a whole-buffer (`Algorithm`) built-in.
279    pub fn register_block(
280        &mut self,
281        sig: BuiltinSig,
282        factory: impl Fn(&[f64], f32) -> Box<dyn BlockBuiltin<T>> + Send + Sync + 'static,
283    ) {
284        debug_assert_eq!(sig.kind, BuiltinKind::Block);
285        self.entries.insert(
286            sig.name.to_string(),
287            Entry {
288                sig,
289                factory: Factory::Block(Box::new(factory)),
290            },
291        );
292    }
293
294    /// Register a whole-buffer multi-channel built-in.
295    pub fn register_multichannel_block(
296        &mut self,
297        sig: BuiltinSig,
298        factory: impl Fn(&[f64], f32) -> Box<dyn MultichannelBlockBuiltin<T>> + Send + Sync + 'static,
299    ) {
300        debug_assert_eq!(sig.kind, BuiltinKind::Block);
301        self.entries.insert(
302            sig.name.to_string(),
303            Entry {
304                sig,
305                factory: Factory::MultichannelBlock(Box::new(factory)),
306            },
307        );
308    }
309
310    /// Look up an entry by name.
311    pub fn get(&self, name: &str) -> Option<&Entry<T>> {
312        self.entries.get(name)
313    }
314}
315
316/// A `T`-independent signature lookup used by the type checker and lowering.
317pub trait SignatureSource {
318    /// The signature for `name`, if registered.
319    fn builtin_sig(&self, name: &str) -> Option<&BuiltinSig>;
320}
321
322impl<T: Transcendental> SignatureSource for Registry<T> {
323    fn builtin_sig(&self, name: &str) -> Option<&BuiltinSig> {
324        self.entries.get(name).map(|e| &e.sig)
325    }
326}
327
328/// A signature source with no built-ins (used by `compile()` / existing tests).
329pub struct NoSigs;
330impl SignatureSource for NoSigs {
331    fn builtin_sig(&self, _name: &str) -> Option<&BuiltinSig> {
332        None
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    struct Gain {
341        k: f32,
342    }
343    impl SampleBuiltin<f32> for Gain {
344        fn process_sample(&mut self, inputs: &[f32]) -> f32 {
345            inputs[0] * self.k
346        }
347        fn reset(&mut self) {}
348    }
349
350    #[test]
351    fn register_and_lookup_sample() {
352        let mut reg = Registry::<f32>::new();
353        reg.register_sample(
354            BuiltinSig::simple("gain", 1, 1, 1, BuiltinKind::Sample),
355            |params, _sr| {
356                Box::new(Gain {
357                    k: params[0] as f32,
358                })
359            },
360        );
361        let sig = reg.builtin_sig("gain").unwrap();
362        assert_eq!((sig.signal_ins(), sig.params.len()), (1, 2));
363        let mut inst = reg
364            .get("gain")
365            .unwrap()
366            .build_sample(&[0.5], 44100.0)
367            .unwrap();
368        assert_eq!(inst.process_sample(&[2.0]), 1.0);
369        assert!(reg.builtin_sig("missing").is_none());
370    }
371}