1use 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 #[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
584pub fn lower(tp: &TypedProgram) -> Result<Ir, CompileError> {
586 lower_with(tp, &crate::builtin::NoSigs, 44_100.0)
587}
588
589pub 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}