Skip to main content

rill_lang/
lower.rs

1//! Lower a type-checked program to linear IR.
2
3use std::collections::HashMap;
4
5use crate::ast::{BinOp, Def, Expr, Program};
6use crate::builtin::{BuiltinKind, SignatureSource};
7use crate::error::{CompileError, Span};
8use crate::ir::{BinArith, BuiltinInstance, Instr, Ir, ParamDef, StateLayout, UnOp};
9use crate::types::infer::TypedProgram;
10
11struct Lowerer<'a> {
12    defs: HashMap<String, &'a Def>,
13    sigs: &'a dyn SignatureSource,
14    instrs: Vec<Instr>,
15    next_reg: usize,
16    state_slots: usize,
17    delay_lens: Vec<usize>,
18    locals: Vec<HashMap<String, Vec<usize>>>,
19    builtins: Vec<BuiltinInstance>,
20    params: Vec<ParamDef>,
21    param_names: HashMap<String, usize>,
22    sample_rate: f32,
23}
24
25impl<'a> Lowerer<'a> {
26    fn fresh_reg(&mut self) -> usize {
27        let r = self.next_reg;
28        self.next_reg += 1;
29        r
30    }
31
32    fn emit(&mut self, i: Instr) {
33        self.instrs.push(i);
34    }
35
36    fn lower(&mut self, e: &Expr, inputs: &[usize]) -> Result<Vec<usize>, CompileError> {
37        match e {
38            Expr::Int(v, _) => {
39                let dst = self.fresh_reg();
40                self.emit(Instr::Const {
41                    dst,
42                    value: *v as f64,
43                });
44                Ok(vec![dst])
45            }
46            Expr::Float(v, _) => {
47                let dst = self.fresh_reg();
48                self.emit(Instr::Const { dst, value: *v });
49                Ok(vec![dst])
50            }
51            Expr::Wire(_) => Ok(vec![inputs[0]]),
52            Expr::Cut(_) => Ok(vec![]),
53            Expr::Ref(name, span) => self.lower_ref(name, inputs, *span),
54            Expr::Neg(inner, _) => {
55                let outs = self.lower(inner, inputs)?;
56                Ok(outs
57                    .into_iter()
58                    .map(|src| {
59                        let dst = self.fresh_reg();
60                        self.emit(Instr::Un {
61                            dst,
62                            op: UnOp::Neg,
63                            src,
64                        });
65                        dst
66                    })
67                    .collect())
68            }
69            Expr::Apply { name, args, span } => {
70                if name == "param" {
71                    let pname = match &args[0] {
72                        Expr::Str(s, _) => s.clone(),
73                        _ => unreachable!("checked in infer"),
74                    };
75                    let default = const_f64(&args[1]).unwrap_or(0.0);
76                    let (min, max) = if args.len() == 4 {
77                        (
78                            const_f64(&args[2]).unwrap_or(f64::NEG_INFINITY),
79                            const_f64(&args[3]).unwrap_or(f64::INFINITY),
80                        )
81                    } else {
82                        (f64::NEG_INFINITY, f64::INFINITY)
83                    };
84                    let idx = self.intern_param(pname, default, min, max, *span)?;
85                    let dst = self.fresh_reg();
86                    self.emit(Instr::ReadParam { dst, idx });
87                    return Ok(vec![dst]);
88                }
89                if name == "smooth" {
90                    let x_regs = self.lower(&args[0], inputs)?;
91                    let x = x_regs[0];
92                    let ms = const_f64(&args[1]).unwrap_or(0.0);
93                    let sr = self.sample_rate as f64;
94                    let a = if ms <= 0.0 {
95                        1.0
96                    } else {
97                        let tau = ms / 1000.0;
98                        1.0 - (-1.0 / (tau * sr)).exp()
99                    };
100                    let slot = self.state_slots;
101                    self.state_slots += 1;
102                    let prev = self.fresh_reg();
103                    self.emit(Instr::ReadState { dst: prev, slot });
104                    let diff = self.fresh_reg();
105                    self.emit(Instr::Bin {
106                        dst: diff,
107                        op: BinArith::Sub,
108                        a: x,
109                        b: prev,
110                    });
111                    let acoef = self.fresh_reg();
112                    self.emit(Instr::Const {
113                        dst: acoef,
114                        value: a,
115                    });
116                    let scaled = self.fresh_reg();
117                    self.emit(Instr::Bin {
118                        dst: scaled,
119                        op: BinArith::Mul,
120                        a: acoef,
121                        b: diff,
122                    });
123                    let y = self.fresh_reg();
124                    self.emit(Instr::Bin {
125                        dst: y,
126                        op: BinArith::Add,
127                        a: prev,
128                        b: scaled,
129                    });
130                    self.emit(Instr::WriteState { slot, src: y });
131                    return Ok(vec![y]);
132                }
133                if let Some(sig) = self.sigs.builtin_sig(name) {
134                    let sig = sig.clone();
135                    let mut params = Vec::with_capacity(args.len());
136                    let mut param_bindings = Vec::new();
137                    for (pos, a) in args.iter().enumerate() {
138                        if let Expr::Apply {
139                            name: pn,
140                            args: pargs,
141                            ..
142                        } = a
143                        {
144                            if pn == "param" {
145                                let pname = match &pargs[0] {
146                                    Expr::Str(s, _) => s.clone(),
147                                    _ => {
148                                        return Err(CompileError::Type {
149                                            msg: "param name must be a string literal".into(),
150                                            span: pargs[0].span(),
151                                        });
152                                    }
153                                };
154                                let default = const_f64(&pargs[1]).unwrap_or(0.0);
155                                let (min, max) = if pargs.len() == 4 {
156                                    (
157                                        const_f64(&pargs[2]).unwrap_or(f64::NEG_INFINITY),
158                                        const_f64(&pargs[3]).unwrap_or(f64::INFINITY),
159                                    )
160                                } else {
161                                    (f64::NEG_INFINITY, f64::INFINITY)
162                                };
163                                let idx = self.intern_param(pname, default, min, max, a.span())?;
164                                params.push(default);
165                                param_bindings.push((pos, idx));
166                                continue;
167                            }
168                        }
169                        let v = const_f64(a).ok_or_else(|| CompileError::Type {
170                            msg: format!("param to `{name}` must be a constant or a param(...)"),
171                            span: a.span(),
172                        })?;
173                        params.push(v);
174                    }
175                    let instance = self.builtins.len();
176                    self.builtins.push(BuiltinInstance {
177                        name: name.clone(),
178                        params,
179                        kind: sig.kind,
180                        param_bindings,
181                    });
182                    let dst = self.fresh_reg();
183                    match sig.kind {
184                        BuiltinKind::Sample => {
185                            let srcs = inputs.to_vec();
186                            self.emit(Instr::CallSample {
187                                dst,
188                                srcs,
189                                instance,
190                            });
191                        }
192                        BuiltinKind::Block => {
193                            self.emit(Instr::CallBlock {
194                                dst,
195                                src: inputs[0],
196                                instance,
197                            });
198                        }
199                    }
200                    return Ok(vec![dst]);
201                }
202                let mut arg_regs = Vec::new();
203                for a in args {
204                    arg_regs.extend(self.lower(a, inputs)?);
205                }
206                self.lower_ref(name, &arg_regs, *span)
207            }
208            Expr::Str(_, span) => Err(CompileError::Type {
209                msg: "string literal is only valid as a `param` name".into(),
210                span: *span,
211            }),
212            Expr::Bin { op, lhs, rhs, span } => self.lower_bin(*op, lhs, rhs, inputs, *span),
213        }
214    }
215
216    /// Intern a named parameter, returning its slot index. Repeated uses of the
217    /// same name share one slot but must declare an identical default and range —
218    /// a conflicting redeclaration is a compile error (avoids silent first-wins).
219    #[allow(clippy::float_cmp)]
220    fn intern_param(
221        &mut self,
222        name: String,
223        default: f64,
224        min: f64,
225        max: f64,
226        span: Span,
227    ) -> Result<usize, CompileError> {
228        if let Some(&idx) = self.param_names.get(&name) {
229            let existing = &self.params[idx];
230            if existing.default != default || existing.min != min || existing.max != max {
231                return Err(CompileError::Type {
232                    msg: format!(
233                        "parameter `{name}` is redeclared with a different default/range; \
234                         all uses of the same name must match"
235                    ),
236                    span,
237                });
238            }
239            Ok(idx)
240        } else {
241            let idx = self.params.len();
242            self.params.push(ParamDef {
243                name: name.clone(),
244                default,
245                min,
246                max,
247            });
248            self.param_names.insert(name, idx);
249            Ok(idx)
250        }
251    }
252
253    fn lower_ref(
254        &mut self,
255        name: &str,
256        inputs: &[usize],
257        span: Span,
258    ) -> Result<Vec<usize>, CompileError> {
259        if let Some(sig) = self.sigs.builtin_sig(name) {
260            if sig.clone().num_params == 0 {
261                let sig = sig.clone();
262                let instance = self.builtins.len();
263                self.builtins.push(BuiltinInstance {
264                    name: name.to_string(),
265                    params: Vec::new(),
266                    kind: sig.kind,
267                    param_bindings: Vec::new(),
268                });
269                let dst = self.fresh_reg();
270                match sig.kind {
271                    BuiltinKind::Sample => {
272                        let srcs = inputs.to_vec();
273                        self.emit(Instr::CallSample {
274                            dst,
275                            srcs,
276                            instance,
277                        });
278                    }
279                    BuiltinKind::Block => {
280                        self.emit(Instr::CallBlock {
281                            dst,
282                            src: inputs[0],
283                            instance,
284                        });
285                    }
286                }
287                return Ok(vec![dst]);
288            }
289        }
290        let bin = match name {
291            "+" => Some(BinArith::Add),
292            "-" => Some(BinArith::Sub),
293            "*" => Some(BinArith::Mul),
294            "/" => Some(BinArith::Div),
295            "%" => Some(BinArith::Rem),
296            "min" => Some(BinArith::Min),
297            "max" => Some(BinArith::Max),
298            _ => None,
299        };
300        if let Some(op) = bin {
301            let dst = self.fresh_reg();
302            self.emit(Instr::Bin {
303                dst,
304                op,
305                a: inputs[0],
306                b: inputs[1],
307            });
308            return Ok(vec![dst]);
309        }
310        let un = match name {
311            "sin" => Some(UnOp::Sin),
312            "cos" => Some(UnOp::Cos),
313            "tan" => Some(UnOp::Tan),
314            "sqrt" => Some(UnOp::Sqrt),
315            "exp" => Some(UnOp::Exp),
316            "ln" => Some(UnOp::Ln),
317            "tanh" => Some(UnOp::Tanh),
318            "abs" => Some(UnOp::Abs),
319            _ => None,
320        };
321        if let Some(op) = un {
322            let dst = self.fresh_reg();
323            self.emit(Instr::Un {
324                dst,
325                op,
326                src: inputs[0],
327            });
328            return Ok(vec![dst]);
329        }
330        for scope in self.locals.iter().rev() {
331            if let Some(regs) = scope.get(name) {
332                return Ok(regs.clone());
333            }
334        }
335        let def = *self.defs.get(name).ok_or_else(|| CompileError::Type {
336            msg: format!("unknown `{name}` in lowering"),
337            span,
338        })?;
339        let mut scope = HashMap::new();
340        for (idx, p) in def.params.iter().enumerate() {
341            scope.insert(p.clone(), vec![inputs[idx]]);
342        }
343        self.locals.push(scope);
344        let out = self.lower(&def.body, inputs)?;
345        self.locals.pop();
346        Ok(out)
347    }
348
349    fn lower_bin(
350        &mut self,
351        op: BinOp,
352        lhs: &Expr,
353        rhs: &Expr,
354        inputs: &[usize],
355        span: Span,
356    ) -> Result<Vec<usize>, CompileError> {
357        match op {
358            BinOp::Seq => {
359                let mid = self.lower(lhs, inputs)?;
360                self.lower(rhs, &mid)
361            }
362            BinOp::Par => {
363                let li = arity_in(lhs, self.sigs)?;
364                let (a_in, b_in) = inputs.split_at(li.min(inputs.len()));
365                let mut out = self.lower(lhs, a_in)?;
366                out.extend(self.lower(rhs, b_in)?);
367                Ok(out)
368            }
369            BinOp::Split => {
370                let a_out = self.lower(lhs, inputs)?;
371                let bi = arity_in(rhs, self.sigs)?;
372                let reps = bi / a_out.len().max(1);
373                let mut fanned = Vec::with_capacity(bi);
374                for _ in 0..reps {
375                    fanned.extend(a_out.iter().copied());
376                }
377                self.lower(rhs, &fanned)
378            }
379            BinOp::Merge => {
380                let a_out = self.lower(lhs, inputs)?;
381                let bi = arity_in(rhs, self.sigs)?;
382                let groups = a_out.len() / bi.max(1);
383                let mut merged = Vec::with_capacity(bi);
384                for k in 0..bi {
385                    let mut acc = a_out[k];
386                    for g in 1..groups {
387                        let dst = self.fresh_reg();
388                        self.emit(Instr::Bin {
389                            dst,
390                            op: BinArith::Add,
391                            a: acc,
392                            b: a_out[g * bi + k],
393                        });
394                        acc = dst;
395                    }
396                    merged.push(acc);
397                }
398                self.lower(rhs, &merged)
399            }
400            BinOp::Feedback => self.lower_feedback(lhs, rhs, inputs, span),
401            BinOp::Delay => self.lower_delay(lhs, rhs, inputs, span),
402            BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Rem => {
403                let a = self.lower(lhs, inputs)?;
404                let b = self.lower(rhs, inputs)?;
405                let arith = match op {
406                    BinOp::Add => BinArith::Add,
407                    BinOp::Sub => BinArith::Sub,
408                    BinOp::Mul => BinArith::Mul,
409                    BinOp::Div => BinArith::Div,
410                    BinOp::Rem => BinArith::Rem,
411                    _ => unreachable!(),
412                };
413                let dst = self.fresh_reg();
414                self.emit(Instr::Bin {
415                    dst,
416                    op: arith,
417                    a: a[0],
418                    b: b[0],
419                });
420                Ok(vec![dst])
421            }
422        }
423    }
424
425    fn lower_feedback(
426        &mut self,
427        lhs: &Expr,
428        rhs: &Expr,
429        inputs: &[usize],
430        _span: Span,
431    ) -> Result<Vec<usize>, CompileError> {
432        let bo = arity_out(rhs, self.sigs)?;
433        let mut fb_regs = Vec::with_capacity(bo);
434        let mut slots = Vec::with_capacity(bo);
435        for _ in 0..bo {
436            let slot = self.state_slots;
437            self.state_slots += 1;
438            slots.push(slot);
439            let dst = self.fresh_reg();
440            self.emit(Instr::ReadState { dst, slot });
441            fb_regs.push(dst);
442        }
443        let mut a_in = fb_regs.clone();
444        a_in.extend_from_slice(inputs);
445        let a_out = self.lower(lhs, &a_in)?;
446        let bi = arity_in(rhs, self.sigs)?;
447        let b_in: Vec<usize> = a_out.iter().copied().take(bi).collect();
448        let b_out = self.lower(rhs, &b_in)?;
449        for (k, slot) in slots.iter().enumerate() {
450            self.emit(Instr::WriteState {
451                slot: *slot,
452                src: b_out[k],
453            });
454        }
455        Ok(a_out)
456    }
457
458    fn lower_delay(
459        &mut self,
460        lhs: &Expr,
461        rhs: &Expr,
462        inputs: &[usize],
463        span: Span,
464    ) -> Result<Vec<usize>, CompileError> {
465        let len = const_int(rhs).ok_or_else(|| CompileError::Type {
466            msg: "delay length must be a constant integer expression".into(),
467            span,
468        })?;
469        if len < 0 {
470            return Err(CompileError::Type {
471                msg: "delay length must be non-negative".into(),
472                span,
473            });
474        }
475        let signal = self.lower(lhs, inputs)?;
476        let src = signal[0];
477        if len == 0 {
478            return Ok(vec![src]);
479        }
480        let line = self.delay_lens.len();
481        self.delay_lens.push(len as usize);
482        let dst = self.fresh_reg();
483        self.emit(Instr::ReadDelay { dst, line });
484        self.emit(Instr::WriteDelay { line, src });
485        Ok(vec![dst])
486    }
487}
488
489fn arity_out(e: &Expr, sigs: &dyn SignatureSource) -> Result<usize, CompileError> {
490    Ok(arity(e, sigs)?.1)
491}
492fn arity_in(e: &Expr, sigs: &dyn SignatureSource) -> Result<usize, CompileError> {
493    Ok(arity(e, sigs)?.0)
494}
495
496fn arity(e: &Expr, sigs: &dyn SignatureSource) -> Result<(usize, usize), CompileError> {
497    let unsupported = |m: &str| CompileError::Unsupported(m.to_string());
498    Ok(match e {
499        Expr::Int(_, _) | Expr::Float(_, _) => (0, 1),
500        Expr::Str(_, _) => (0, 1),
501        Expr::Wire(_) => (1, 1),
502        Expr::Cut(_) => (1, 0),
503        Expr::Neg(inner, _) => arity(inner, sigs)?,
504        Expr::Ref(name, _) => match name.as_str() {
505            "+" | "-" | "*" | "/" | "%" | "min" | "max" => (2, 1),
506            "sin" | "cos" | "tan" | "sqrt" | "exp" | "ln" | "tanh" | "abs" => (1, 1),
507            _ => {
508                if let Some(sig) = sigs.builtin_sig(name) {
509                    (sig.signal_ins, sig.signal_outs)
510                } else {
511                    return Err(unsupported(
512                        "arity of bare user-def ref; wrap in application",
513                    ));
514                }
515            }
516        },
517        Expr::Apply { name, args, .. } => {
518            if let Some(sig) = sigs.builtin_sig(name) {
519                (sig.signal_ins, sig.signal_outs)
520            } else {
521                let mut ins = 0;
522                for a in args {
523                    ins += arity(a, sigs)?.0;
524                }
525                (ins, 1)
526            }
527        }
528        Expr::Bin { op, lhs, rhs, .. } => {
529            let (ai, ao) = arity(lhs, sigs)?;
530            let (bi, bo) = arity(rhs, sigs)?;
531            match op {
532                BinOp::Seq => (ai, bo),
533                BinOp::Par => (ai + bi, ao + bo),
534                BinOp::Split => (ai, bo),
535                BinOp::Merge => (ai, bo),
536                BinOp::Feedback => (ai - bo, ao),
537                BinOp::Delay => (ai, ao),
538                _ => (ai + bi, 1),
539            }
540        }
541    })
542}
543
544fn const_f64(e: &Expr) -> Option<f64> {
545    match e {
546        Expr::Float(v, _) => Some(*v),
547        Expr::Int(v, _) => Some(*v as f64),
548        Expr::Neg(inner, _) => const_f64(inner).map(|v| -v),
549        Expr::Bin { op, lhs, rhs, .. } => {
550            let a = const_f64(lhs)?;
551            let b = const_f64(rhs)?;
552            Some(match op {
553                BinOp::Add => a + b,
554                BinOp::Sub => a - b,
555                BinOp::Mul => a * b,
556                BinOp::Div => a / b,
557                _ => return None,
558            })
559        }
560        _ => None,
561    }
562}
563
564fn const_int(e: &Expr) -> Option<i64> {
565    match e {
566        Expr::Int(v, _) => Some(*v),
567        Expr::Neg(inner, _) => const_int(inner).map(|v| -v),
568        Expr::Bin { op, lhs, rhs, .. } => {
569            let a = const_int(lhs)?;
570            let b = const_int(rhs)?;
571            Some(match op {
572                BinOp::Add => a + b,
573                BinOp::Sub => a - b,
574                BinOp::Mul => a * b,
575                BinOp::Div if b != 0 => a / b,
576                BinOp::Rem if b != 0 => a % b,
577                _ => return None,
578            })
579        }
580        _ => None,
581    }
582}
583
584/// Back-compat: lower with no built-ins and a default sample rate of 44.1 kHz.
585pub fn lower(tp: &TypedProgram) -> Result<Ir, CompileError> {
586    lower_with(tp, &crate::builtin::NoSigs, 44_100.0)
587}
588
589/// Lower a fully type-checked program into IR with a signature source and sample rate.
590pub fn lower_with(
591    tp: &TypedProgram,
592    sigs: &dyn SignatureSource,
593    sample_rate: f32,
594) -> Result<Ir, CompileError> {
595    let program: &Program = &tp.program;
596    let defs: HashMap<String, &Def> = program.defs.iter().map(|d| (d.name.clone(), d)).collect();
597    let process = *defs.get("process").ok_or_else(|| CompileError::Type {
598        msg: "no `process` definition".into(),
599        span: Span::new(0, 0),
600    })?;
601
602    let num_inputs = tp.process_ty.arity_in();
603    let mut lw = Lowerer {
604        defs,
605        sigs,
606        instrs: Vec::new(),
607        next_reg: 0,
608        state_slots: 0,
609        delay_lens: Vec::new(),
610        locals: Vec::new(),
611        builtins: Vec::new(),
612        params: Vec::new(),
613        param_names: HashMap::new(),
614        sample_rate,
615    };
616    let mut input_regs = Vec::with_capacity(num_inputs);
617    for index in 0..num_inputs {
618        let dst = lw.fresh_reg();
619        lw.emit(Instr::LoadInput { dst, index });
620        input_regs.push(dst);
621    }
622    let outs = lw.lower(&process.body, &input_regs)?;
623    if outs.len() != 1 {
624        return Err(CompileError::Unsupported(format!(
625            "process lowered to {} outputs, expected 1",
626            outs.len()
627        )));
628    }
629    Ok(Ir {
630        instrs: lw.instrs,
631        num_regs: lw.next_reg,
632        output_reg: outs[0],
633        num_inputs,
634        state: StateLayout {
635            state_slots: lw.state_slots,
636            delay_lens: lw.delay_lens,
637        },
638        builtins: lw.builtins,
639        params: lw.params,
640    })
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646    use crate::lexer::tokenize;
647    use crate::parser::parse;
648    use crate::types::infer::{infer_program, infer_program_with};
649
650    fn ir_of(src: &str) -> Ir {
651        let p = parse(&tokenize(src).unwrap()).unwrap();
652        let tp = infer_program(&p).unwrap();
653        lower(&tp).unwrap()
654    }
655
656    struct TestSigs;
657    impl crate::builtin::SignatureSource for TestSigs {
658        fn builtin_sig(&self, name: &str) -> Option<&crate::builtin::BuiltinSig> {
659            use crate::builtin::{BuiltinKind, BuiltinSig};
660            match name {
661                "lowpass" => Some(Box::leak(Box::new(BuiltinSig {
662                    name: "lowpass",
663                    signal_ins: 1,
664                    signal_outs: 1,
665                    num_params: 2,
666                    kind: BuiltinKind::Block,
667                }))),
668                "onepole" => Some(Box::leak(Box::new(BuiltinSig {
669                    name: "onepole",
670                    signal_ins: 1,
671                    signal_outs: 1,
672                    num_params: 2,
673                    kind: BuiltinKind::Sample,
674                }))),
675                _ => None,
676            }
677        }
678    }
679
680    fn ir_with(src: &str) -> Ir {
681        let p = parse(&tokenize(src).unwrap()).unwrap();
682        let tp = infer_program_with(&p, &TestSigs).unwrap();
683        lower_with(&tp, &TestSigs, 44_100.0).unwrap()
684    }
685
686    #[test]
687    fn gain_lowers_to_const_and_mul() {
688        let ir = ir_of("process = _ * 0.5;");
689        assert_eq!(ir.num_inputs, 1);
690        assert!(ir.instrs.iter().any(|i| matches!(
691            i,
692            Instr::Bin {
693                op: BinArith::Mul,
694                ..
695            }
696        )));
697        assert!(ir
698            .instrs
699            .iter()
700            .any(|i| matches!(i, Instr::Const { value, .. } if (*value - 0.5).abs() < 1e-9)));
701    }
702
703    #[test]
704    fn integrator_allocates_one_state_slot() {
705        let ir = ir_of("process = + ~ _;");
706        assert_eq!(ir.state.state_slots, 1);
707        assert!(ir
708            .instrs
709            .iter()
710            .any(|i| matches!(i, Instr::ReadState { .. })));
711        assert!(ir
712            .instrs
713            .iter()
714            .any(|i| matches!(i, Instr::WriteState { .. })));
715    }
716
717    #[test]
718    fn delay_allocates_line() {
719        let ir = ir_of("process = _ @ 3;");
720        assert_eq!(ir.state.delay_lens, vec![3]);
721    }
722
723    #[test]
724    fn sample_builtin_lowers_to_callsample() {
725        let ir = ir_with("process = _ : onepole(200.0, 0.5);");
726        assert!(
727            ir.instrs
728                .iter()
729                .any(|i| matches!(i, Instr::CallSample { .. })),
730            "expected a CallSample instruction"
731        );
732        assert_eq!(ir.builtins.len(), 1);
733        let bi = &ir.builtins[0];
734        assert_eq!(bi.kind, BuiltinKind::Sample);
735        assert_eq!(bi.params, vec![200.0, 0.5]);
736    }
737
738    #[test]
739    fn block_builtin_lowers_to_callblock() {
740        let ir = ir_with("process = _ : lowpass(1000.0, 0.7);");
741        assert!(
742            ir.instrs
743                .iter()
744                .any(|i| matches!(i, Instr::CallBlock { .. })),
745            "expected a CallBlock instruction"
746        );
747        assert_eq!(ir.builtins.len(), 1);
748        let bi = &ir.builtins[0];
749        assert_eq!(bi.kind, BuiltinKind::Block);
750        assert_eq!(bi.params, vec![1000.0, 0.7]);
751    }
752
753    #[test]
754    fn smooth_allocates_state() {
755        let ir = ir_of("process = smooth(_, 10.0);");
756        assert_eq!(ir.state.state_slots, 1);
757        assert!(ir
758            .instrs
759            .iter()
760            .any(|i| matches!(i, Instr::ReadState { .. })));
761        assert!(ir
762            .instrs
763            .iter()
764            .any(|i| matches!(i, Instr::WriteState { .. })));
765    }
766}