1use rill_core::math::Transcendental;
2use rill_core::queues::CommandEnum;
3use rill_core::traits::Params;
4use rill_core_actor::ActorRef;
5
6use indexmap::IndexMap;
7use rill_lang::builtin::SignatureSource;
8use rill_lang::graph_ir::{EdgeKind, GraphEdge, GraphIr, GraphNode};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
17pub enum BuildError {
18 CycleDetected,
20 Backend(String),
22 UnknownNodeType(String),
24 UnsupportedTopology(String),
26 CompilationFailed(String),
28}
29
30impl std::fmt::Display for BuildError {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 Self::CycleDetected => write!(f, "graph cycle detected"),
34 Self::Backend(msg) => write!(f, "backend error: {msg}"),
35 Self::UnknownNodeType(msg) => write!(f, "unknown node type: {msg}"),
36 Self::UnsupportedTopology(msg) => write!(f, "unsupported topology: {msg}"),
37 Self::CompilationFailed(msg) => write!(f, "compilation failed: {msg}"),
38 }
39 }
40}
41
42struct NodeRecipe<T: Transcendental, const BUF_SIZE: usize> {
48 type_name: String,
49 id: u32,
50 name: String,
51 params: Params,
52 routing_entries: Vec<(usize, usize, f32)>,
53 _phantom: std::marker::PhantomData<(T, [(); BUF_SIZE])>,
54}
55
56#[derive(Clone)]
62pub struct GraphResource {
63 pub name: String,
65 pub kind: String,
67 pub capacity: usize,
69}
70
71pub struct GraphBuilder<T: Transcendental, const BUF_SIZE: usize> {
73 recipes: Vec<NodeRecipe<T, BUF_SIZE>>,
74 signal_edges: Vec<(usize, usize, usize, usize)>,
75 control_edges: Vec<(usize, usize, usize, usize)>,
76 clock_edges: Vec<(usize, usize, usize, usize)>,
77 feedback_edges: Vec<(usize, usize, usize, usize)>,
78 resources: Vec<GraphResource>,
79 sample_rate: Option<f32>,
80 parent_ref: Option<ActorRef<CommandEnum>>,
81}
82
83impl<T: Transcendental, const BUF_SIZE: usize> Default for GraphBuilder<T, BUF_SIZE> {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl<T: Transcendental, const BUF_SIZE: usize> GraphBuilder<T, BUF_SIZE> {
90 pub fn new() -> Self {
92 Self {
93 recipes: Vec::new(),
94 signal_edges: Vec::new(),
95 control_edges: Vec::new(),
96 clock_edges: Vec::new(),
97 feedback_edges: Vec::new(),
98 resources: Vec::new(),
99 sample_rate: None,
100 parent_ref: None,
101 }
102 }
103
104 pub fn add_node(&mut self, type_name: &str, params: &Params) -> usize {
108 let id = self.recipes.len() as u32;
109 self.add_node_with_id(type_name, params, id)
110 }
111
112 pub fn add_node_with_id(&mut self, type_name: &str, params: &Params, id: u32) -> usize {
114 self.add_node_with_name(type_name, params, id, String::new())
115 }
116
117 pub fn add_node_with_name(
121 &mut self,
122 type_name: &str,
123 params: &Params,
124 id: u32,
125 name: String,
126 ) -> usize {
127 let idx = self.recipes.len();
128 self.recipes.push(NodeRecipe {
129 type_name: type_name.to_string(),
130 id,
131 name,
132 params: params.clone(),
133 routing_entries: Vec::new(),
134 _phantom: std::marker::PhantomData,
135 });
136 idx
137 }
138
139 pub fn add_routing_entry(&mut self, idx: usize, from: usize, to: usize, gain: f32) {
141 if let Some(recipe) = self.recipes.get_mut(idx) {
142 recipe.routing_entries.push((from, to, gain));
143 }
144 }
145
146 pub fn add_resource(&mut self, resource: GraphResource) {
148 self.resources.push(resource);
149 }
150
151 pub fn node_count(&self) -> usize {
153 self.recipes.len()
154 }
155
156 pub fn set_sample_rate(&mut self, sr: f32) {
158 self.sample_rate = Some(sr);
159 }
160
161 pub fn set_parent_ref(&mut self, parent: ActorRef<CommandEnum>) {
163 self.parent_ref = Some(parent);
164 }
165
166 pub fn connect_signal(
168 &mut self,
169 from_node: usize,
170 from_port: usize,
171 to_node: usize,
172 to_port: usize,
173 ) {
174 self.signal_edges
175 .push((from_node, from_port, to_node, to_port));
176 }
177
178 pub fn connect_control(
180 &mut self,
181 from_node: usize,
182 from_port: usize,
183 to_node: usize,
184 to_port: usize,
185 ) {
186 self.control_edges
187 .push((from_node, from_port, to_node, to_port));
188 }
189
190 pub fn connect_clock(
192 &mut self,
193 from_node: usize,
194 from_port: usize,
195 to_node: usize,
196 to_port: usize,
197 ) {
198 self.clock_edges
199 .push((from_node, from_port, to_node, to_port));
200 }
201
202 pub fn connect_feedback(
204 &mut self,
205 from_node: usize,
206 from_port: usize,
207 to_node: usize,
208 to_port: usize,
209 ) {
210 self.feedback_edges
211 .push((from_node, from_port, to_node, to_port));
212 }
213
214 pub fn build_ir(
220 self,
221 registry: &rill_lang::builtin::Registry<T>,
222 ) -> Result<GraphIr, BuildError> {
223 let idx_to_name: HashMap<usize, String> = self
225 .recipes
226 .iter()
227 .enumerate()
228 .map(|(idx, recipe)| {
229 let name = if recipe.name.is_empty() {
230 format!("node_{}", recipe.id)
231 } else {
232 recipe.name.clone()
233 };
234 (idx, name)
235 })
236 .collect();
237
238 let mut nodes: IndexMap<String, GraphNode> = IndexMap::new();
240 let mut node_list: Vec<String> = Vec::new();
241
242 for (idx, recipe) in self.recipes.iter().enumerate() {
243 let name = idx_to_name[&idx].clone();
244 node_list.push(name.clone());
245
246 let sig = registry
247 .builtin_sig(&recipe.type_name)
248 .or_else(|| {
249 recipe
251 .type_name
252 .strip_prefix("rill/")
253 .and_then(|n| registry.builtin_sig(n))
254 })
255 .or_else(|| {
256 let mapped = match recipe.type_name.as_str() {
258 "rill/dry_wet_mix" => "dry_wet",
259 "rill/parametric_eq" => "eq_parametric",
260 "rill/graphic_eq" => "graphic_eq",
261 "rill/mono_to_stereo" => "mono_to_stereo",
262 "rill/moog_ladder" => "moog",
263 "rill/write_head" => "write_head",
264 "rill/read_head" => "read_head",
265 "rill/lofi_chip" => "ay38910",
266 _ => "",
267 };
268 if mapped.is_empty() {
269 None
270 } else {
271 registry.builtin_sig(mapped)
272 }
273 })
274 .ok_or_else(|| BuildError::UnknownNodeType(recipe.type_name.clone()))?;
275
276 let arity = (sig.signal_ins(), sig.signal_outs);
277
278 let param_defs: Vec<rill_lang::ir::ParamDef> = recipe
282 .params
283 .parameters
284 .iter()
285 .map(|(k, v)| {
286 let default = v.as_f32().unwrap_or(0.0) as f64;
287 rill_lang::ir::ParamDef {
288 name: k.clone(),
289 default,
290 min: f64::NEG_INFINITY,
291 max: f64::INFINITY,
292 }
293 })
294 .collect();
295
296 let name_to_recipe_idx: HashMap<String, usize> = param_defs
298 .iter()
299 .enumerate()
300 .map(|(i, pd)| (pd.name.clone(), i))
301 .collect();
302
303 let param_values: Vec<f64>;
304 let param_bindings: Vec<(usize, usize)>;
305
306 if sig.param_names.is_empty() {
307 param_values = recipe
309 .params
310 .parameters
311 .values()
312 .filter_map(|v| v.as_f32().map(|f| f as f64))
313 .collect();
314 param_bindings = (0..param_defs.len()).map(|i| (i, i)).collect();
315 } else {
316 let num_args = sig.param_names.len();
321 let mut values = vec![0.0; num_args];
322 let mut bindings = Vec::with_capacity(num_args);
323 for (arg_pos, builtin_name) in sig.param_names.iter().enumerate() {
324 if let Some(&recipe_idx) = name_to_recipe_idx.get(*builtin_name) {
325 values[arg_pos] = param_defs[recipe_idx].default;
326 bindings.push((arg_pos, recipe_idx));
327 }
328 }
329 param_values = values;
330 param_bindings = bindings;
331 }
332
333 let builtin_name = sig.name.to_string();
335 let builtin_instance = rill_lang::ir::BuiltinInstance {
336 name: builtin_name,
337 params: param_values,
338 kind: sig.kind,
339 signal_ins: arity.0,
340 signal_outs: arity.1,
341 param_bindings,
342 };
343
344 let mut instrs = Vec::new();
349 let mut output_reg = 0usize;
350 let mut num_regs = 1usize;
351 if arity.1 > 0 {
352 if arity.0 > 0 {
353 instrs.push(rill_lang::ir::Instr::LoadInput { dst: 0, index: 0 });
354 num_regs = 2;
355 output_reg = 1;
356 }
357 #[cfg(feature = "debug")]
358 {
359 num_regs += 1;
361 }
362 let srcs = if arity.0 > 0 { vec![0] } else { vec![] };
363 instrs.push(rill_lang::ir::Instr::CallBlock {
364 dst: output_reg,
365 srcs,
366 instance: 0,
367 });
368 #[cfg(feature = "debug")]
369 instrs.push(rill_lang::ir::Instr::ProbePoint {
370 id: idx as u32,
371 src: output_reg,
372 dst: output_reg.wrapping_add(1),
373 });
374 }
375
376 let ir = rill_lang::ir::Ir {
377 instrs,
378 num_regs,
379 output_reg,
380 num_inputs: arity.0,
381 num_outputs: arity.1,
382 state: rill_lang::ir::StateLayout {
383 state_slots: 0,
384 delay_lens: vec![],
385 num_outputs: arity.1,
386 },
387 builtins: vec![builtin_instance],
388 params: param_defs.clone(),
389 };
390
391 nodes.insert(
392 name.clone(),
393 GraphNode {
394 arity,
395 ir,
396 params: param_defs,
397 keep: false,
398 inline: false,
399 is_bridge: false,
400 feedback_read: vec![],
401 feedback_write: vec![],
402 },
403 );
404 }
405
406 let mut edges = Vec::new();
408 for (from_idx, from_port, to_idx, to_port) in &self.signal_edges {
409 edges.push(GraphEdge {
410 from_node: idx_to_name[from_idx].clone(),
411 from_port: *from_port,
412 to_node: idx_to_name[to_idx].clone(),
413 to_port: *to_port,
414 kind: EdgeKind::Signal,
415 });
416 }
417 for (from_idx, from_port, to_idx, to_port) in &self.feedback_edges {
418 edges.push(GraphEdge {
419 from_node: idx_to_name[from_idx].clone(),
420 from_port: *from_port,
421 to_node: idx_to_name[to_idx].clone(),
422 to_port: *to_port,
423 kind: EdgeKind::Feedback,
424 });
425 }
426
427 let mut in_degree: HashMap<String, usize> = HashMap::new();
429 for name in &node_list {
430 in_degree.insert(name.clone(), 0);
431 }
432 for edge in &edges {
433 if edge.kind == EdgeKind::Signal {
434 *in_degree.get_mut(&edge.to_node).unwrap() += 1;
435 }
436 }
437
438 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
439 for name in &node_list {
440 adj.insert(name.clone(), vec![]);
441 }
442 for edge in &edges {
443 if edge.kind == EdgeKind::Signal {
444 adj.get_mut(&edge.from_node)
445 .unwrap()
446 .push(edge.to_node.clone());
447 }
448 }
449
450 let mut queue: Vec<String> = in_degree
451 .iter()
452 .filter(|(_, &d)| d == 0)
453 .map(|(n, _)| n.clone())
454 .collect();
455 let mut topo_order = Vec::new();
456
457 while let Some(node) = queue.pop() {
458 topo_order.push(node.clone());
459 if let Some(neighbors) = adj.get(&node) {
460 for neighbor in neighbors {
461 let deg = in_degree.get_mut(neighbor).unwrap();
462 *deg -= 1;
463 if *deg == 0 {
464 queue.push(neighbor.clone());
465 }
466 }
467 }
468 }
469
470 if topo_order.len() != node_list.len() {
471 return Err(BuildError::CycleDetected);
472 }
473
474 let inputs = topo_order
476 .iter()
477 .filter(|n| in_degree.get(*n) == Some(&0))
478 .map(|n| nodes[n].arity.0)
479 .sum();
480 let outputs = topo_order
481 .iter()
482 .filter(|n| adj.get(*n).is_none_or(|v| v.is_empty()))
483 .map(|n| nodes[n].arity.1)
484 .sum();
485
486 Ok(GraphIr {
487 inputs,
488 outputs,
489 nodes,
490 edges,
491 topo_order,
492 })
493 }
494
495 pub fn ast_from_def(
504 &self,
505 registry: &rill_lang::builtin::Registry<T>,
506 ) -> Result<rill_lang::ast::Program, BuildError> {
507 use rill_lang::ast::{BinOp, Def, Expr, Param, Program};
508 use rill_lang::error::Span;
509
510 let dummy = Span::new(0, 0);
511
512 let mut id_to_idx: HashMap<u32, usize> = HashMap::new();
514 for (i, r) in self.recipes.iter().enumerate() {
515 id_to_idx.insert(r.id, i);
516 }
517
518 struct NodeMeta {
520 builtin_name: String,
521 param_values: Vec<f64>,
522 param_names: Vec<String>,
523 }
524
525 let mut node_metas: Vec<NodeMeta> = Vec::with_capacity(self.recipes.len());
526
527 for recipe in &self.recipes {
528 let builtin_name = Self::resolve_builtin_name(&recipe.type_name, registry)
529 .ok_or_else(|| BuildError::UnknownNodeType(recipe.type_name.clone()))?;
530
531 let sig = registry.builtin_sig(&builtin_name).unwrap();
532
533 let param_names: Vec<String> = sig.param_names.iter().map(|n| n.to_string()).collect();
535 let mut param_values = Vec::with_capacity(param_names.len());
536
537 let recipe_defaults: HashMap<&str, f64> = recipe
539 .params
540 .parameters
541 .iter()
542 .filter_map(|(k, v)| v.as_f32().map(|f| (k.as_str(), f as f64)))
543 .collect();
544
545 for name in ¶m_names {
546 let val = recipe_defaults.get(name.as_str()).copied().unwrap_or(0.0);
547 param_values.push(val);
548 }
549
550 node_metas.push(NodeMeta {
551 builtin_name,
552 param_values,
553 param_names,
554 });
555 }
556
557 let mut in_degree: Vec<usize> = vec![0; self.recipes.len()];
559 let mut adj: Vec<Vec<usize>> = vec![vec![]; self.recipes.len()];
560
561 for (from_idx, _from_port, to_idx, _to_port) in &self.signal_edges {
562 if *from_idx < self.recipes.len() && *to_idx < self.recipes.len() {
563 adj[*from_idx].push(*to_idx);
564 in_degree[*to_idx] += 1;
565 }
566 }
567
568 let mut queue: Vec<usize> = (0..self.recipes.len())
569 .filter(|i| in_degree[*i] == 0)
570 .collect();
571 let mut order: Vec<usize> = Vec::new();
572
573 while let Some(u) = queue.pop() {
574 order.push(u);
575 for &v in &adj[u] {
576 in_degree[v] -= 1;
577 if in_degree[v] == 0 {
578 queue.push(v);
579 }
580 }
581 }
582
583 if order.len() != self.recipes.len() {
584 return Err(BuildError::CycleDetected);
585 }
586
587 for (i, targets) in adj.iter().enumerate() {
589 if targets.len() > 1 {
590 return Err(BuildError::UnsupportedTopology(format!(
591 "node {} fans out to {} destinations (split not yet supported)",
592 i,
593 targets.len()
594 )));
595 }
596 let in_count = self
597 .signal_edges
598 .iter()
599 .filter(|(_, _, to, _)| *to == i)
600 .count();
601 if in_count > 1 {
602 return Err(BuildError::UnsupportedTopology(format!(
603 "node {} receives {} signal inputs (merge not yet supported)",
604 i, in_count
605 )));
606 }
607 }
608
609 let mut node_exprs: Vec<Option<Expr>> = vec![None; self.recipes.len()];
612
613 let mut all_param_names: Vec<String> = Vec::new();
615
616 for &idx in &order {
617 let meta = &node_metas[idx];
618
619 let upstream_expr: Option<Expr> = self
621 .signal_edges
622 .iter()
623 .find(|(_, _, to, _)| *to == idx)
624 .and_then(|(from, _from_port, _to, _to_port)| node_exprs[*from].clone());
625
626 let mut args: Vec<Expr> = Vec::new();
631 let n = meta.param_names.len();
632 for (i, (&val, name)) in meta
633 .param_values
634 .iter()
635 .zip(meta.param_names.iter())
636 .enumerate()
637 {
638 if i < n - 1 {
639 args.push(Expr::Float(val, dummy));
641 } else {
642 all_param_names.push(name.clone());
644 args.push(Expr::Ref(name.clone(), dummy));
645 }
646 }
647
648 let apply = Expr::Apply {
649 name: meta.builtin_name.clone(),
650 args,
651 span: dummy,
652 };
653
654 let expr = match upstream_expr {
655 Some(up) => Expr::Bin {
656 op: BinOp::Seq,
657 lhs: Box::new(up),
658 rhs: Box::new(apply),
659 span: dummy,
660 },
661 None => apply,
662 };
663
664 node_exprs[idx] = Some(expr);
665 }
666
667 let leaf: usize = order
669 .iter()
670 .rfind(|&&i| adj[i].is_empty())
671 .copied()
672 .unwrap_or(0);
673
674 let body = node_exprs[leaf].clone().unwrap_or(Expr::Wire(dummy));
675
676 let params: Vec<Param> = all_param_names
677 .into_iter()
678 .map(|name| Param { name, span: dummy })
679 .collect();
680
681 Ok(Program {
682 defs: vec![Def::Anchor {
683 name: "main".to_string(),
684 params,
685 body,
686 span: dummy,
687 where_defs: vec![],
688 }],
689 })
690 }
691
692 pub fn compile_def<const BUF: usize>(
696 &self,
697 registry: &rill_lang::builtin::Registry<T>,
698 sample_rate: f32,
699 ) -> Result<rill_lang::graph_engine::CompiledGraphEngine<T, BUF>, BuildError> {
700 let program = self.ast_from_def(registry)?;
701 rill_lang::compile_program::<T, BUF>(&program, registry, sample_rate)
702 .map_err(|e| BuildError::CompilationFailed(format!("{e}")))
703 }
704
705 fn resolve_builtin_name(
706 type_name: &str,
707 registry: &rill_lang::builtin::Registry<T>,
708 ) -> Option<String> {
709 if registry.builtin_sig(type_name).is_some() {
710 return Some(type_name.to_string());
711 }
712 if let Some(rest) = type_name.strip_prefix("rill/") {
713 if registry.builtin_sig(rest).is_some() {
714 return Some(rest.to_string());
715 }
716 }
717 let mapped = match type_name {
718 "rill/dry_wet_mix" => "dry_wet",
719 "rill/parametric_eq" => "eq_parametric",
720 "rill/graphic_eq" => "graphic_eq",
721 "rill/mono_to_stereo" => "mono_to_stereo",
722 "rill/moog_ladder" => "moog",
723 "rill/write_head" => "write_head",
724 "rill/read_head" => "read_head",
725 "rill/lofi_chip" => "ay38910",
726 _ => "",
727 };
728 if !mapped.is_empty() && registry.builtin_sig(mapped).is_some() {
729 return Some(mapped.to_string());
730 }
731 None
732 }
733}