1use std::collections::{HashMap, HashSet};
7
8#[allow(unused_imports)] use flutmax_ast::{
10 CallArg, DestructuringWire, DirectConnection, Expr, FeedbackAssignment, FeedbackDecl, InDecl,
11 LitValue, MsgDecl, OutAssignment, OutDecl, PortType, Program, StateAssignment, StateDecl, Wire,
12};
13use flutmax_objdb::{InletSpec, ObjectDb, OutletSpec};
14use flutmax_sema::graph::{NodePurity, PatchEdge, PatchGraph, PatchNode};
15use flutmax_sema::registry::AbstractionRegistry;
16use flutmax_sema::trigger::insert_triggers;
17
18pub type CodeFiles = HashMap<String, String>;
21
22#[derive(Debug)]
24pub enum BuildError {
25 UndefinedRef(String),
27 OutletIndexOutOfRange(u32),
29 NoOutDeclaration(u32),
31 DestructuringCountMismatch { expected: usize, got: usize },
33 AbstractionArgCountMismatch {
35 name: String,
36 expected: usize,
37 got: usize,
38 },
39 DuplicateFeedbackAssignment(String),
41 InvalidPortIndex {
43 node: String,
44 port: String,
45 index: u32,
46 max: u32,
47 },
48 BareMultiOutletRef { name: String, num_outlets: u32 },
50 DuplicateStateAssignment(String),
52}
53
54impl std::fmt::Display for BuildError {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 BuildError::UndefinedRef(name) => write!(f, "undefined reference: {}", name),
58 BuildError::OutletIndexOutOfRange(idx) => {
59 write!(f, "outlet index out of range: {}", idx)
60 }
61 BuildError::NoOutDeclaration(idx) => {
62 write!(f, "E004: out[{}] has no corresponding out declaration", idx)
63 }
64 BuildError::DestructuringCountMismatch { expected, got } => {
65 write!(
66 f,
67 "E006: destructuring count mismatch: expected {} names, got {}",
68 expected, got
69 )
70 }
71 BuildError::AbstractionArgCountMismatch {
72 name,
73 expected,
74 got,
75 } => {
76 write!(
77 f,
78 "E009: abstraction '{}' expects {} arguments, got {}",
79 name, expected, got
80 )
81 }
82 BuildError::DuplicateFeedbackAssignment(name) => {
83 write!(f, "E013: duplicate feedback assignment to '{}'", name)
84 }
85 BuildError::InvalidPortIndex {
86 node,
87 port,
88 index,
89 max,
90 } => {
91 write!(
92 f,
93 "E007: port index out of range: {}.{}[{}] (max: {})",
94 node, port, index, max
95 )
96 }
97 BuildError::BareMultiOutletRef { name, num_outlets } => {
98 write!(
99 f,
100 "E020: bare reference to multi-outlet node '{}' ({} outlets); use .out[N] to specify which outlet",
101 name, num_outlets
102 )
103 }
104 BuildError::DuplicateStateAssignment(name) => {
105 write!(f, "E019: duplicate state assignment to '{}'", name)
106 }
107 }
108 }
109}
110
111impl std::error::Error for BuildError {}
112
113#[derive(Debug, Clone)]
115pub enum BuildWarning {
116 DuplicateInletConnection {
118 node_id: String,
119 inlet: u32,
120 count: usize,
121 },
122}
123
124impl std::fmt::Display for BuildWarning {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 match self {
127 BuildWarning::DuplicateInletConnection {
128 node_id,
129 inlet,
130 count,
131 } => {
132 write!(
133 f,
134 "W001: {} connections to {}.in[{}]",
135 count, node_id, inlet
136 )
137 }
138 }
139 }
140}
141
142pub struct BuildResult {
144 pub graph: PatchGraph,
145 pub warnings: Vec<BuildWarning>,
146}
147
148struct GraphBuilder<'a> {
150 graph: PatchGraph,
151 next_id: u32,
153 name_map: HashMap<String, (String, u32)>,
156 outlet_nodes: HashMap<u32, String>,
158 registry: Option<&'a AbstractionRegistry>,
160 feedback_map: HashMap<String, String>,
162 assigned_feedbacks: HashSet<String>,
164 destructured_names: HashSet<String>,
166 assigned_states: HashSet<String>,
168 tuple_type_args: HashMap<String, Vec<String>>,
171 code_files: Option<&'a CodeFiles>,
173 objdb: Option<&'a ObjectDb>,
175}
176
177impl<'a> GraphBuilder<'a> {
178 fn new(
179 registry: Option<&'a AbstractionRegistry>,
180 code_files: Option<&'a CodeFiles>,
181 objdb: Option<&'a ObjectDb>,
182 ) -> Self {
183 Self {
184 graph: PatchGraph::new(),
185 next_id: 1,
186 name_map: HashMap::new(),
187 outlet_nodes: HashMap::new(),
188 registry,
189 feedback_map: HashMap::new(),
190 assigned_feedbacks: HashSet::new(),
191 destructured_names: HashSet::new(),
192 assigned_states: HashSet::new(),
193 tuple_type_args: HashMap::new(),
194 code_files,
195 objdb,
196 }
197 }
198
199 fn gen_id(&mut self) -> String {
201 let id = format!("obj-{}", self.next_id);
202 self.next_id += 1;
203 id
204 }
205
206 fn add_inlet(&mut self, decl: &InDecl) {
208 let id = self.gen_id();
209 let is_signal = decl.port_type.is_signal();
210 let object_name = if is_signal { "inlet~" } else { "inlet" };
211 let num_inlets = if is_signal { 1 } else { 0 };
212 let node = PatchNode {
213 id: id.clone(),
214 object_name: object_name.to_string(),
215 args: vec![],
216 num_inlets,
217 num_outlets: 1,
218 is_signal,
219 varname: None,
220 hot_inlets: default_hot_inlets(object_name, num_inlets),
221 purity: classify_purity(object_name),
222 attrs: vec![],
223 code: None,
224 };
225 self.graph.add_node(node);
226 self.name_map.insert(decl.name.clone(), (id, 0));
228 }
229
230 fn add_outlet(&mut self, decl: &OutDecl) {
232 let id = self.gen_id();
233 let is_signal = decl.port_type.is_signal();
234 let object_name = if is_signal { "outlet~" } else { "outlet" };
235 let node = PatchNode {
236 id: id.clone(),
237 object_name: object_name.to_string(),
238 args: vec![],
239 num_inlets: 1,
240 num_outlets: 0,
241 is_signal,
242 varname: None,
243 hot_inlets: default_hot_inlets(object_name, 1),
244 purity: classify_purity(object_name),
245 attrs: vec![],
246 code: None,
247 };
248 self.graph.add_node(node);
249 self.outlet_nodes.insert(decl.index, id);
250 }
251
252 fn add_msg(&mut self, decl: &MsgDecl) {
254 let id = self.gen_id();
255 let attrs = decl
256 .attrs
257 .iter()
258 .map(|a| (a.key.clone(), format_attr_value(&a.value)))
259 .collect();
260 let node = PatchNode {
261 id: id.clone(),
262 object_name: "message".to_string(),
263 args: vec![decl.content.clone()],
264 num_inlets: 2, num_outlets: 1,
266 is_signal: false,
267 varname: Some(decl.name.clone()),
268 hot_inlets: vec![true, false],
269 purity: classify_purity("message"),
270 attrs,
271 code: None,
272 };
273 self.graph.add_node(node);
274 self.name_map.insert(decl.name.clone(), (id, 0));
275 }
276
277 fn add_wire(&mut self, wire: &Wire) -> Result<(), BuildError> {
279 if let Expr::Tuple(elements) = &wire.value {
281 let type_args: Vec<String> = elements.iter().map(infer_pack_type_arg).collect();
282 self.tuple_type_args.insert(wire.name.clone(), type_args);
283 }
284
285 let (node_id, outlet) = self.resolve_expr(&wire.value)?;
286 if let Some(node) = self.graph.nodes.iter_mut().find(|n| n.id == node_id) {
288 node.varname = Some(wire.name.clone());
289 }
290 if !wire.attrs.is_empty() {
292 if let Some(node) = self.graph.nodes.iter_mut().find(|n| n.id == node_id) {
293 node.attrs = wire
294 .attrs
295 .iter()
296 .map(|a| (a.key.clone(), format_attr_value(&a.value)))
297 .collect();
298 }
299 }
300 self.name_map.insert(wire.name.clone(), (node_id, outlet));
301 Ok(())
302 }
303
304 fn add_out_assignment(&mut self, assign: &OutAssignment) -> Result<(), BuildError> {
306 let (source_id, source_outlet) = self.resolve_expr(&assign.value)?;
307 let dest_id = self
308 .outlet_nodes
309 .get(&assign.index)
310 .ok_or(BuildError::NoOutDeclaration(assign.index))?
311 .clone();
312
313 self.graph.add_edge(PatchEdge {
314 source_id,
315 source_outlet,
316 dest_id,
317 dest_inlet: 0,
318 is_feedback: false,
319 order: None,
320 });
321 Ok(())
322 }
323
324 fn resolve_expr(&mut self, expr: &Expr) -> Result<(String, u32), BuildError> {
326 match expr {
327 Expr::Ref(name) => {
328 let (node_id, outlet_index) = self
329 .name_map
330 .get(name)
331 .ok_or_else(|| BuildError::UndefinedRef(name.clone()))?
332 .clone();
333
334 Ok((node_id, outlet_index))
339 }
340 Expr::Call { object, args } => {
341 let id = self.gen_id();
342 let max_name = resolve_max_object_name(object);
343 let is_signal = max_name.ends_with('~');
344
345 let mut lit_args: Vec<String> = Vec::new();
347 let mut ref_connections: Vec<(String, u32, u32)> = Vec::new(); let has_name_arg = matches!(max_name, "gen~" | "mc.gen~" | "rnbo~");
357 let mut lit_count: u32 = 0;
358
359 for (i, arg) in args.iter().enumerate() {
360 let inlet_idx = if let Some(ref name) = arg.name {
363 resolve_inlet_name(max_name, name, self.objdb)
364 .or_else(|| resolve_abstraction_inlet_name(object, name, self.registry))
365 .unwrap_or(i as u32)
366 } else if has_name_arg {
367 (i as u32).saturating_sub(lit_count)
369 } else {
370 i as u32
371 };
372
373 match &arg.value {
374 Expr::Lit(lit) => {
375 lit_args.push(format_lit(lit));
376 if has_name_arg {
377 lit_count += 1;
378 }
379 }
380 Expr::Ref(name) => {
381 let (ref_node_id, ref_outlet) = self
382 .name_map
383 .get(name)
384 .ok_or_else(|| BuildError::UndefinedRef(name.clone()))?
385 .clone();
386 ref_connections.push((ref_node_id, ref_outlet, inlet_idx));
387 }
388 Expr::Call { .. } => {
389 let (nested_id, nested_outlet) = self.resolve_expr(&arg.value)?;
391 ref_connections.push((nested_id, nested_outlet, inlet_idx));
392 }
393 Expr::OutputPortAccess(opa) => {
394 let (ref_node_id, _) = self
396 .name_map
397 .get(&opa.object)
398 .ok_or_else(|| BuildError::UndefinedRef(opa.object.clone()))?
399 .clone();
400 ref_connections.push((ref_node_id, opa.index, inlet_idx));
401 }
402 Expr::Tuple(_) => {
403 let (nested_id, nested_outlet) = self.resolve_expr(&arg.value)?;
405 ref_connections.push((nested_id, nested_outlet, inlet_idx));
406 }
407 }
408 }
409
410 let abstraction_info = if max_name == object {
416 self.registry.and_then(|reg| reg.lookup(object))
417 } else {
418 None
420 };
421
422 if let Some(iface) = abstraction_info {
424 let expected = iface.in_ports.len();
425 let got = args.len();
426 if expected != got {
427 return Err(BuildError::AbstractionArgCountMismatch {
428 name: object.clone(),
429 expected,
430 got,
431 });
432 }
433 }
434
435 let effective_arg_count = if has_name_arg {
438 (args.len() as u32).saturating_sub(lit_count)
439 } else {
440 args.len() as u32
441 };
442
443 let (max_inlet, num_outlets, is_signal) = if let Some(iface) = abstraction_info {
445 let num_in = iface.in_ports.len() as u32;
447 let num_out = iface.out_ports.len() as u32;
448 let sig = iface
449 .out_ports
450 .first()
451 .map(|p| p.port_type.is_signal())
452 .unwrap_or(false);
453 let max_from_refs = ref_connections
455 .iter()
456 .map(|(_, _, inlet)| *inlet + 1)
457 .max()
458 .unwrap_or(0);
459 let inlets =
460 std::cmp::max(std::cmp::max(max_from_refs, effective_arg_count), num_in);
461 (inlets, num_out, sig)
462 } else {
463 let inlet_count = if ref_connections.is_empty() && lit_args.is_empty() {
465 infer_num_inlets(max_name, &lit_args, self.objdb)
466 } else {
467 let max_from_refs = ref_connections
468 .iter()
469 .map(|(_, _, inlet)| *inlet + 1)
470 .max()
471 .unwrap_or(0);
472 std::cmp::max(
473 std::cmp::max(max_from_refs, effective_arg_count),
474 infer_num_inlets(max_name, &lit_args, self.objdb),
475 )
476 };
477 let outlet_count = infer_num_outlets(max_name, &lit_args, self.objdb);
478 (inlet_count, outlet_count, is_signal)
479 };
480
481 let object_name = if abstraction_info.is_some() {
484 object.to_string()
485 } else {
486 max_name.to_string()
487 };
488
489 let mut node = PatchNode {
490 id: id.clone(),
491 object_name: object_name.clone(),
492 args: lit_args.clone(),
493 num_inlets: max_inlet,
494 num_outlets,
495 is_signal,
496 varname: None,
497 hot_inlets: default_hot_inlets(&object_name, max_inlet),
498 purity: classify_purity(&object_name),
499 attrs: vec![],
500 code: None,
501 };
502
503 if matches!(max_name, "v8.codebox" | "codebox") {
505 if let Some(code_files) = self.code_files {
506 if let Some(filename) = lit_args.first() {
507 if let Some(code_content) = code_files.get(filename.as_str()) {
508 node.code = Some(code_content.clone());
509 node.args.clear();
510 if max_name == "codebox" {
512 let (inlets, outlets) = infer_codebox_ports(code_content);
513 node.num_inlets = inlets;
514 node.num_outlets = outlets;
515 }
516 }
517 }
518 }
519 }
520
521 self.graph.add_node(node);
522
523 for (source_id, source_outlet, dest_inlet) in ref_connections {
525 self.graph.add_edge(PatchEdge {
526 source_id,
527 source_outlet,
528 dest_id: id.clone(),
529 dest_inlet,
530 is_feedback: false,
531 order: None,
532 });
533 }
534
535 Ok((id, 0))
536 }
537 Expr::Lit(lit) => {
538 let id = self.gen_id();
540 let (object_name, arg_str, is_signal) = match lit {
541 LitValue::Int(v) => ("message".to_string(), v.to_string(), false),
542 LitValue::Float(_) => ("message".to_string(), format_lit(lit), false),
543 LitValue::Str(s) => ("message".to_string(), s.clone(), false),
544 };
545 let node = PatchNode {
546 id: id.clone(),
547 object_name,
548 args: vec![arg_str],
549 num_inlets: 1,
550 num_outlets: 1,
551 is_signal,
552 varname: None,
553 hot_inlets: default_hot_inlets("message", 1),
554 purity: classify_purity("message"),
555 attrs: vec![],
556 code: None,
557 };
558 self.graph.add_node(node);
559 Ok((id, 0))
560 }
561 Expr::OutputPortAccess(opa) => {
562 let (node_id, _) = self
564 .name_map
565 .get(&opa.object)
566 .ok_or_else(|| BuildError::UndefinedRef(opa.object.clone()))?
567 .clone();
568 Ok((node_id, opa.index))
569 }
570 Expr::Tuple(elements) => {
571 let id = self.gen_id();
572 let num_elements = elements.len() as u32;
573
574 let mut ref_connections: Vec<(String, u32, u32)> = Vec::new();
576 let mut type_args: Vec<String> = Vec::new();
577 for (i, elem) in elements.iter().enumerate() {
578 let (elem_id, elem_outlet) = self.resolve_expr(elem)?;
579 ref_connections.push((elem_id, elem_outlet, i as u32));
580 type_args.push(infer_pack_type_arg(elem));
582 }
583
584 let node = PatchNode {
585 id: id.clone(),
586 object_name: "pack".to_string(),
587 args: type_args,
588 num_inlets: num_elements,
589 num_outlets: 1,
590 is_signal: false,
591 varname: None,
592 hot_inlets: default_hot_inlets("pack", num_elements),
593 purity: classify_purity("pack"),
594 attrs: vec![],
595 code: None,
596 };
597 self.graph.add_node(node);
598
599 for (source_id, source_outlet, dest_inlet) in ref_connections {
600 self.graph.add_edge(PatchEdge {
601 source_id,
602 source_outlet,
603 dest_id: id.clone(),
604 dest_inlet,
605 is_feedback: false,
606 order: None,
607 });
608 }
609
610 Ok((id, 0))
611 }
612 }
613 }
614
615 fn add_destructuring_wire(&mut self, dw: &DestructuringWire) -> Result<(), BuildError> {
626 let (source_id, _source_outlet) = self.resolve_expr(&dw.value)?;
627 let num_names = dw.names.len() as u32;
628
629 let resolved_node = self.graph.nodes.iter().find(|n| n.id == source_id);
633 if let Some(node) = resolved_node {
634 let outlet_count = node.num_outlets;
635 let is_known = outlet_count != 1
637 || node.object_name == "unpack"
638 || node.object_name == "inlet"
639 || node.object_name == "inlet~";
640 if is_known && outlet_count != num_names {
641 return Err(BuildError::DestructuringCountMismatch {
642 expected: outlet_count as usize,
643 got: num_names as usize,
644 });
645 }
646 }
647
648 let source_has_enough_outlets = resolved_node
650 .map(|n| n.num_outlets >= num_names)
651 .unwrap_or(false);
652
653 let target_id = if source_has_enough_outlets {
654 source_id.clone()
656 } else {
657 let id = self.gen_id();
659 let type_args = self.lookup_tuple_type_args(&dw.value, num_names);
661
662 let node = PatchNode {
663 id: id.clone(),
664 object_name: "unpack".to_string(),
665 args: type_args,
666 num_inlets: 1,
667 num_outlets: num_names,
668 is_signal: false,
669 varname: None,
670 hot_inlets: default_hot_inlets("unpack", 1),
671 purity: classify_purity("unpack"),
672 attrs: vec![],
673 code: None,
674 };
675 self.graph.add_node(node);
676
677 self.graph.add_edge(PatchEdge {
678 source_id,
679 source_outlet: _source_outlet,
680 dest_id: id.clone(),
681 dest_inlet: 0,
682 is_feedback: false,
683 order: None,
684 });
685
686 id
687 };
688
689 for (i, name) in dw.names.iter().enumerate() {
691 self.name_map
692 .insert(name.clone(), (target_id.clone(), i as u32));
693 self.destructured_names.insert(name.clone());
694 }
695
696 Ok(())
697 }
698
699 fn lookup_tuple_type_args(&self, value: &Expr, num_names: u32) -> Vec<String> {
707 let source_name = match value {
708 Expr::Ref(name) => Some(name.as_str()),
709 Expr::Call { object, args } if object == "unpack" => args.first().and_then(|arg| {
710 if let Expr::Ref(name) = &arg.value {
711 Some(name.as_str())
712 } else {
713 None
714 }
715 }),
716 _ => None,
717 };
718 if let Some(name) = source_name {
719 if let Some(type_args) = self.tuple_type_args.get(name) {
720 return type_args.clone();
721 }
722 }
723 (0..num_names).map(|_| "f".to_string()).collect()
724 }
725
726 fn add_feedback_decl(&mut self, decl: &FeedbackDecl) {
732 let tapin_id = self.gen_id();
737 let node = PatchNode {
738 id: tapin_id.clone(),
739 object_name: "tapin~".to_string(),
740 args: vec![],
741 num_inlets: 1,
742 num_outlets: 1,
743 is_signal: true,
744 varname: None,
745 hot_inlets: default_hot_inlets("tapin~", 1),
746 purity: classify_purity("tapin~"),
747 attrs: vec![],
748 code: None,
749 };
750 self.graph.add_node(node);
751 self.feedback_map
752 .insert(decl.name.clone(), tapin_id.clone());
753 self.name_map.insert(decl.name.clone(), (tapin_id, 0));
756 }
757
758 fn add_feedback_assignment(&mut self, assign: &FeedbackAssignment) -> Result<(), BuildError> {
763 if !self.assigned_feedbacks.insert(assign.target.clone()) {
765 return Err(BuildError::DuplicateFeedbackAssignment(
766 assign.target.clone(),
767 ));
768 }
769
770 let (source_id, source_outlet) = self.resolve_expr(&assign.value)?;
771
772 if let Some(tapin_id) = self.feedback_map.get(&assign.target).cloned() {
774 self.graph.add_edge(PatchEdge {
776 source_id,
777 source_outlet,
778 dest_id: tapin_id,
779 dest_inlet: 0,
780 is_feedback: true,
781 order: None,
782 });
783 }
784
785 Ok(())
786 }
787
788 fn add_state_decl(&mut self, decl: &StateDecl) -> Result<(), BuildError> {
793 let id = self.gen_id();
794
795 let (object_name, init_arg) = match decl.port_type {
796 PortType::Int => (
797 "int".to_string(),
798 match &decl.init_value {
799 Expr::Lit(LitValue::Int(v)) => v.to_string(),
800 Expr::Lit(LitValue::Float(v)) => format!("{}", *v as i64),
801 _ => "0".to_string(),
802 },
803 ),
804 PortType::Float => (
805 "float".to_string(),
806 match &decl.init_value {
807 Expr::Lit(LitValue::Float(v)) => format_lit(&LitValue::Float(*v)),
808 Expr::Lit(LitValue::Int(v)) => format!("{}.", v),
809 _ => "0.".to_string(),
810 },
811 ),
812 _ => ("int".to_string(), "0".to_string()),
814 };
815
816 let node = PatchNode {
817 id: id.clone(),
818 object_name: object_name.clone(),
819 args: vec![init_arg],
820 num_inlets: 2, num_outlets: 1,
822 is_signal: false,
823 varname: Some(decl.name.clone()),
824 hot_inlets: vec![true, false], purity: classify_purity(&object_name),
826 attrs: vec![],
827 code: None,
828 };
829 self.graph.add_node(node);
830
831 self.name_map.insert(decl.name.clone(), (id, 0));
833
834 Ok(())
835 }
836
837 fn add_state_assignment(&mut self, assign: &StateAssignment) -> Result<(), BuildError> {
841 if !self.assigned_states.insert(assign.name.clone()) {
843 return Err(BuildError::DuplicateStateAssignment(assign.name.clone()));
844 }
845
846 let (state_node_id, _) = self
848 .name_map
849 .get(&assign.name)
850 .ok_or_else(|| BuildError::UndefinedRef(assign.name.clone()))?
851 .clone();
852
853 let (source_id, source_outlet) = self.resolve_expr(&assign.value)?;
855
856 self.graph.add_edge(PatchEdge {
858 source_id,
859 source_outlet,
860 dest_id: state_node_id,
861 dest_inlet: 1, is_feedback: false,
863 order: None,
864 });
865
866 Ok(())
867 }
868
869 fn add_direct_connection(&mut self, conn: &DirectConnection) -> Result<(), BuildError> {
874 let target_name = &conn.target.object;
875 let index = conn.target.index;
876
877 let (node_id, _) = self
879 .name_map
880 .get(target_name)
881 .ok_or_else(|| BuildError::UndefinedRef(target_name.clone()))?
882 .clone();
883
884 if let Some(node) = self.graph.find_node_mut(&node_id) {
888 if index >= node.num_inlets {
889 node.num_inlets = index + 1;
890 }
891 }
892
893 let (source_id, source_outlet) = self.resolve_expr(&conn.value)?;
895
896 self.graph.add_edge(PatchEdge {
897 source_id,
898 source_outlet,
899 dest_id: node_id,
900 dest_inlet: index,
901 is_feedback: false,
902 order: None,
903 });
904
905 Ok(())
906 }
907}
908
909fn infer_pack_type_arg(expr: &Expr) -> String {
916 match expr {
917 Expr::Lit(LitValue::Int(_)) => "i".to_string(),
918 Expr::Lit(LitValue::Float(_)) => "f".to_string(),
919 Expr::Lit(LitValue::Str(_)) => "s".to_string(),
920 _ => "f".to_string(), }
922}
923
924fn classify_purity(object_name: &str) -> NodePurity {
926 match object_name {
927 name if name.ends_with('~') => match name {
929 "tapin~" | "tapout~" | "line~" | "delay~" | "phasor~" | "count~" | "index~"
930 | "buffer~" | "groove~" | "play~" | "record~" | "sfplay~" | "sfrecord~" | "sig~" => {
931 NodePurity::Stateful
932 }
933 _ => NodePurity::Pure,
934 },
935 "pack" | "unpack" | "int" | "float" | "toggle" | "gate" | "counter" | "message" | "zl"
937 | "coll" | "dict" | "regexp" | "value" | "table" | "funbuff" | "bag" | "borax"
938 | "bucket" | "histo" | "mousestate" | "spray" | "switch" | "if" | "expr" | "vexpr"
939 | "button" | "number" | "flonum" | "slider" | "dial" | "umenu" | "preset" | "pattr"
940 | "autopattr" | "pattrstorage" => NodePurity::Stateful,
941 "+" | "-" | "*" | "/" | "%" | "trigger" | "t" | "route" | "select" | "prepend"
943 | "append" | "stripnote" | "makenote" | "scale" | "split" | "swap" | "clip" | "minimum"
944 | "maximum" | "inlet" | "inlet~" | "outlet" | "outlet~" | "loadbang" | "print" | "send"
945 | "receive" | "forward" | "ezdac~" | "dac~" | "adc~" => NodePurity::Pure,
946 _ => NodePurity::Unknown,
947 }
948}
949
950fn default_hot_inlets(_object_name: &str, num_inlets: u32) -> Vec<bool> {
953 if num_inlets == 0 {
954 return vec![];
955 }
956 (0..num_inlets).map(|i| i == 0).collect()
959}
960
961fn assign_edge_orders(graph: &mut PatchGraph) {
965 use std::collections::HashMap;
966
967 let mut groups: HashMap<(String, u32), Vec<usize>> = HashMap::new();
969 for (i, edge) in graph.edges.iter().enumerate() {
970 let key = (edge.source_id.clone(), edge.source_outlet);
971 groups.entry(key).or_default().push(i);
972 }
973
974 for indices in groups.values() {
976 if indices.len() >= 2 {
977 for (order, &edge_idx) in indices.iter().enumerate() {
978 graph.edges[edge_idx].order = Some(order as u32);
979 }
980 }
981 }
982}
983
984fn format_lit(lit: &LitValue) -> String {
986 match lit {
987 LitValue::Int(v) => v.to_string(),
988 LitValue::Float(v) => {
989 if v.fract() == 0.0 {
992 format!("{}.", *v as i64)
993 } else {
994 format!("{}", v)
995 }
996 }
997 LitValue::Str(s) => s.clone(),
998 }
999}
1000
1001fn format_attr_value(val: &flutmax_ast::AttrValue) -> String {
1004 match val {
1005 flutmax_ast::AttrValue::Int(v) => v.to_string(),
1006 flutmax_ast::AttrValue::Float(v) => {
1007 if v.fract() == 0.0 {
1010 format!("{}.", *v as i64)
1011 } else {
1012 format!("{}", v)
1013 }
1014 }
1015 flutmax_ast::AttrValue::Str(s) => s.clone(),
1016 flutmax_ast::AttrValue::Ident(s) => s.clone(),
1017 }
1018}
1019
1020fn resolve_max_object_name(flutmax_name: &str) -> &str {
1023 match flutmax_name {
1024 "add" => "+",
1025 "sub" => "-",
1026 "mul" => "*",
1027 "dvd" => "/",
1028 "mod" => "%",
1029 "add~" => "+~",
1030 "sub~" => "-~",
1031 "mul~" => "*~",
1032 "dvd~" => "/~",
1033 "mod~" => "%~",
1034 "rsub" => "!-",
1036 "rdvd" => "!/",
1037 "rmod" => "!%",
1038 "rsub~" => "!-~",
1039 "rdvd~" => "!/~",
1040 "rmod~" => "!%~",
1041 "gt" => ">",
1043 "lt" => "<",
1044 "gte" => ">=",
1045 "lte" => "<=",
1046 "eq" => "==",
1047 "neq" => "!=",
1048 "gt~" => ">~",
1049 "lt~" => "<~",
1050 "gte~" => ">=~",
1051 "lte~" => "<=~",
1052 "eq~" => "==~",
1053 "neq~" => "!=~",
1054 "and" => "&&",
1056 "or" => "||",
1057 "lshift" => "<<",
1058 "rshift" => ">>",
1059 other => other,
1060 }
1061}
1062
1063fn resolve_inlet_name(object_name: &str, arg_name: &str, objdb: Option<&ObjectDb>) -> Option<u32> {
1068 let db = objdb?;
1069 let def = db.lookup(object_name)?;
1070 let inlets = match &def.inlets {
1071 InletSpec::Fixed(ports) => ports.as_slice(),
1072 InletSpec::Variable { defaults, .. } => defaults.as_slice(),
1073 };
1074 let arg_lower = arg_name.to_lowercase();
1075 for port in inlets {
1076 let normalized = normalize_port_description(&port.description);
1077 if let Some(ref n) = normalized {
1078 if *n == arg_lower {
1079 return Some(port.id);
1080 }
1081 }
1082 }
1083 None
1084}
1085
1086fn normalize_port_description(description: &str) -> Option<String> {
1091 let trimmed = description.trim();
1092 let stripped = if trimmed.starts_with('(') {
1094 if let Some(end) = trimmed.find(')') {
1095 trimmed[end + 1..].trim()
1096 } else {
1097 trimmed
1098 }
1099 } else {
1100 trimmed
1101 };
1102 let s: String = stripped
1103 .to_lowercase()
1104 .chars()
1105 .map(|c| if c == ' ' { '_' } else { c })
1106 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
1107 .collect();
1108 let parts: Vec<&str> = s.split('_').filter(|p| !p.is_empty()).collect();
1109 let result = parts.join("_");
1110 let result = result
1111 .trim_start_matches(|c: char| c.is_ascii_digit())
1112 .to_string();
1113 if result.is_empty() || result.len() > 20 {
1114 None
1115 } else {
1116 Some(result)
1117 }
1118}
1119
1120fn resolve_abstraction_inlet_name(
1125 object_name: &str,
1126 arg_name: &str,
1127 registry: Option<&AbstractionRegistry>,
1128) -> Option<u32> {
1129 let reg = registry?;
1130 let iface = reg.lookup(object_name)?;
1131 let arg_lower = arg_name.to_lowercase();
1132 for port in &iface.in_ports {
1133 if port.name.to_lowercase() == arg_lower {
1134 return Some(port.index);
1135 }
1136 }
1137 None
1138}
1139
1140fn infer_num_inlets(object_name: &str, args: &[String], objdb: Option<&ObjectDb>) -> u32 {
1144 if let Some(db) = objdb {
1146 if let Some(def) = db.lookup(object_name) {
1147 return match &def.inlets {
1148 InletSpec::Fixed(ports) => ports.len() as u32,
1149 InletSpec::Variable {
1150 defaults,
1151 min_inlets,
1152 } => {
1153 if args.is_empty() {
1154 defaults.len().max(*min_inlets as usize) as u32
1155 } else {
1156 args.len() as u32
1157 }
1158 }
1159 };
1160 }
1161 }
1162 match object_name {
1164 "cycle~" => 2,
1166 "*~" | "+~" | "-~" | "/~" | "%~" | "!-~" | "!/~" | "!%~" => 2,
1167 ">~" | "<~" | ">=~" | "<=~" | "==~" | "!=~" => 2,
1168 "*" | "+" | "-" | "/" | "%" | "!-" | "!/" | "!%" => 2,
1170 ">" | "<" | ">=" | "<=" | "==" | "!=" => 2,
1171 "&&" | "||" | "<<" | ">>" => 2,
1172 "ezdac~" => 2,
1174 "dac~" => 2,
1175 "adc~" => 0,
1176 "loadbang" => 1,
1178 "button" => 1,
1179 "print" => 1,
1180 "biquad~" => 6,
1182 "line~" => 2,
1183 "tapin~" => 1,
1184 "tapout~" => 2,
1185 "noise~" | "phasor~" => 1,
1186 "snapshot~" | "peakamp~" | "meter~" => 1,
1187 "edge~" => 1,
1188 "dspstate~" => 1,
1189 "fftinfo~" => 1,
1190 "fftin~" => 1,
1191 "fftout~" => 1,
1192 "cartopol~" | "poltocar~" => 2,
1193 "freqshift~" => 2,
1194 "curve~" => 2,
1195 "adsr~" => 5,
1196 "filtercoeff~" => 4,
1197 "filtergraph~" => 8,
1198 "int" | "float" => 2,
1200 "inlet" | "inlet~" => 0,
1201 "outlet" | "outlet~" => 1,
1202 "trigger" | "t" => 1,
1204 "select" | "sel" => {
1205 if args.is_empty() {
1206 2
1207 } else {
1208 1
1209 }
1210 }
1211 "route" => 1,
1212 "gate" => 2,
1213 "pack" | "pak" => {
1214 if args.is_empty() {
1215 2
1216 } else {
1217 args.len() as u32
1218 }
1219 }
1220 "unpack" => 1,
1221 "buddy" => {
1222 if args.is_empty() {
1223 2
1224 } else {
1225 args.first()
1226 .and_then(|a| a.parse::<u32>().ok())
1227 .unwrap_or(2)
1228 }
1229 }
1230 "makenote" => 3,
1232 "notein" => 1,
1233 "noteout" => 3,
1234 "ctlin" => 1,
1235 "ctlout" => 3,
1236 "midiin" => 1,
1237 "midiout" => 1,
1238 "borax" => 1,
1239 "param" => 2,
1241 "in~" => 1,
1242 "out~" => 1,
1243 "inport" => 1,
1244 "outport" => 1,
1245 "line" => 2,
1247 "function" => 2,
1248 "counter" => 3,
1249 "metro" => 2,
1250 "delay" => 2,
1251 "pipe" => {
1252 if args.is_empty() {
1253 2
1254 } else {
1255 args.len() as u32 + 1
1256 }
1257 }
1258 "speedlim" => 2,
1259 "thresh" => 2,
1260 "coll" => 1,
1262 "urn" => 2,
1263 "drunk" => 2,
1264 "random" => 2,
1265 "match" => 1,
1267 "zl" => 2,
1268 "regexp" => 1,
1269 "sprintf" => {
1270 if args.is_empty() {
1271 1
1272 } else {
1273 args.len() as u32
1274 }
1275 }
1276 "fromsymbol" => 1,
1277 "tosymbol" => 1,
1278 "iter" => 1,
1279 "v8.codebox" => 1,
1281 "codebox" => 1,
1282 "?" => 3,
1284 _ => 1,
1285 }
1286}
1287
1288fn infer_num_outlets(object_name: &str, args: &[String], objdb: Option<&ObjectDb>) -> u32 {
1292 if let Some(db) = objdb {
1294 if let Some(def) = db.lookup(object_name) {
1295 return match &def.outlets {
1296 OutletSpec::Fixed(ports) => ports.len() as u32,
1297 OutletSpec::Variable {
1298 defaults,
1299 min_outlets,
1300 } => {
1301 if args.is_empty() {
1302 defaults.len().max(*min_outlets as usize) as u32
1303 } else {
1304 args.len() as u32
1305 }
1306 }
1307 };
1308 }
1309 }
1310 match object_name {
1312 "cycle~" => 1,
1314 "*~" | "+~" | "-~" | "/~" => 1,
1315 "biquad~" => 1,
1316 "line~" => 2,
1317 "tapin~" => 1,
1318 "tapout~" => 1,
1319 "noise~" | "phasor~" => 1,
1320 "snapshot~" | "peakamp~" | "meter~" => 1,
1321 "edge~" => 2,
1322 "dspstate~" => 4,
1323 "fftinfo~" => 4,
1324 "fftin~" => 3,
1325 "fftout~" => 1,
1326 "cartopol~" | "poltocar~" => 2,
1327 "freqshift~" => 2,
1328 "curve~" => 2,
1329 "adsr~" => 4,
1330 "filtercoeff~" => 5,
1331 "filtergraph~" => 7,
1332 "*" | "+" | "-" | "/" | "%" => 1,
1334 "ezdac~" | "dac~" => 0,
1336 "adc~" => 1,
1337 "loadbang" => 1,
1339 "button" => 1,
1340 "print" => 0,
1341 "int" | "float" => 1,
1343 "inlet" | "inlet~" => 1,
1344 "outlet" | "outlet~" => 0,
1345 "select" | "sel" => {
1347 if args.is_empty() {
1348 2
1349 } else {
1350 args.len() as u32 + 1
1351 }
1352 }
1353 "route" => {
1354 if args.is_empty() {
1355 2
1356 } else {
1357 args.len() as u32 + 1
1358 }
1359 }
1360 "gate" => args
1361 .first()
1362 .and_then(|a| a.parse::<u32>().ok())
1363 .unwrap_or(2),
1364 "trigger" | "t" => {
1365 if args.is_empty() {
1366 1
1367 } else {
1368 args.len() as u32
1369 }
1370 }
1371 "unpack" => {
1372 if args.is_empty() {
1373 2
1374 } else {
1375 args.len() as u32
1376 }
1377 }
1378 "pack" | "pak" => 1,
1379 "buddy" => {
1380 if args.is_empty() {
1381 2
1382 } else {
1383 args.first()
1384 .and_then(|a| a.parse::<u32>().ok())
1385 .unwrap_or(2)
1386 }
1387 }
1388 "function" => 2,
1390 "line" => 2,
1391 "counter" => 4,
1392 "metro" => 1,
1393 "delay" => 1,
1394 "pipe" => {
1395 if args.is_empty() {
1396 1
1397 } else {
1398 args.len() as u32
1399 }
1400 }
1401 "speedlim" => 1,
1402 "thresh" => 2,
1403 "makenote" => 2,
1405 "borax" => 8,
1406 "notein" => 3,
1407 "noteout" => 0,
1408 "ctlin" => 3,
1409 "ctlout" => 0,
1410 "midiin" => 1,
1411 "midiout" => 0,
1412 "param" => 2,
1414 "in~" => 1,
1415 "out~" => 0,
1416 "inport" => 1,
1417 "outport" => 0,
1418 "coll" => 4,
1420 "urn" => 2,
1421 "drunk" => 1,
1422 "random" => 1,
1423 "match" => 2,
1425 "zl" => 2,
1426 "regexp" => 5,
1427 "sprintf" => 1,
1428 "fromsymbol" => 1,
1429 "tosymbol" => 1,
1430 "iter" => 1,
1431 "textbutton" => 3,
1433 "live.text" => 2,
1434 "live.dial" => 2,
1435 "live.toggle" => 1,
1436 "live.menu" => 3,
1437 "live.numbox" => 2,
1438 "live.tab" => 3,
1439 "live.comment" => 0,
1440 "umenu" => 3,
1441 "flonum" => 2,
1442 "number" => 2,
1443 "slider" | "dial" | "rslider" => 1,
1444 "multislider" | "kslider" => 2,
1445 "tab" => 3,
1446 "toggle" => 1,
1447 "v8.codebox" => 1,
1449 "codebox" => 1,
1450 _ => 1,
1451 }
1452}
1453
1454fn infer_codebox_ports(code: &str) -> (u32, u32) {
1460 let mut max_in: u32 = 0;
1461 let mut max_out: u32 = 0;
1462
1463 let bytes = code.as_bytes();
1466 let len = bytes.len();
1467 let mut i = 0;
1468 while i < len {
1469 let at_word_start = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
1471 if at_word_start {
1472 if i + 2 < len && bytes[i] == b'o' && bytes[i + 1] == b'u' && bytes[i + 2] == b't' {
1473 let mut j = i + 3;
1475 let mut num: u32 = 0;
1476 let mut has_digit = false;
1477 while j < len && bytes[j].is_ascii_digit() {
1478 num = num * 10 + (bytes[j] - b'0') as u32;
1479 has_digit = true;
1480 j += 1;
1481 }
1482 if has_digit && (j >= len || !bytes[j].is_ascii_alphanumeric()) && num > max_out {
1484 max_out = num;
1485 }
1486 i = j;
1487 continue;
1488 } else if i + 1 < len && bytes[i] == b'i' && bytes[i + 1] == b'n' {
1489 let mut j = i + 2;
1491 let mut num: u32 = 0;
1492 let mut has_digit = false;
1493 while j < len && bytes[j].is_ascii_digit() {
1494 num = num * 10 + (bytes[j] - b'0') as u32;
1495 has_digit = true;
1496 j += 1;
1497 }
1498 if has_digit && (j >= len || !bytes[j].is_ascii_alphanumeric()) && num > max_in {
1499 max_in = num;
1500 }
1501 if has_digit {
1502 i = j;
1503 continue;
1504 }
1505 }
1506 }
1507 i += 1;
1508 }
1509
1510 (max_in.max(1), max_out.max(1))
1512}
1513
1514pub fn build_graph(program: &Program) -> Result<PatchGraph, BuildError> {
1518 build_graph_with_registry(program, None)
1519}
1520
1521pub fn build_graph_with_registry(
1526 program: &Program,
1527 registry: Option<&AbstractionRegistry>,
1528) -> Result<PatchGraph, BuildError> {
1529 build_graph_with_code_files(program, registry, None)
1530}
1531
1532pub fn build_graph_with_code_files(
1537 program: &Program,
1538 registry: Option<&AbstractionRegistry>,
1539 code_files: Option<&CodeFiles>,
1540) -> Result<PatchGraph, BuildError> {
1541 build_graph_with_objdb(program, registry, code_files, None)
1542}
1543
1544#[derive(Debug, Clone, Copy, Default)]
1546struct BuildOptions {
1547 skip_triggers: bool,
1551}
1552
1553pub fn build_graph_with_objdb(
1558 program: &Program,
1559 registry: Option<&AbstractionRegistry>,
1560 code_files: Option<&CodeFiles>,
1561 objdb: Option<&ObjectDb>,
1562) -> Result<PatchGraph, BuildError> {
1563 build_graph_inner(
1564 program,
1565 registry,
1566 code_files,
1567 objdb,
1568 BuildOptions::default(),
1569 )
1570}
1571
1572pub fn build_graph_without_triggers(program: &Program) -> Result<PatchGraph, BuildError> {
1578 build_graph_inner(
1579 program,
1580 None,
1581 None,
1582 None,
1583 BuildOptions {
1584 skip_triggers: true,
1585 },
1586 )
1587}
1588
1589fn build_graph_inner(
1593 program: &Program,
1594 registry: Option<&AbstractionRegistry>,
1595 code_files: Option<&CodeFiles>,
1596 objdb: Option<&ObjectDb>,
1597 options: BuildOptions,
1598) -> Result<PatchGraph, BuildError> {
1599 let mut builder = GraphBuilder::new(registry, code_files, objdb);
1600
1601 for decl in &program.in_decls {
1603 builder.add_inlet(decl);
1604 }
1605
1606 for decl in &program.out_decls {
1608 builder.add_outlet(decl);
1609 }
1610
1611 for decl in &program.feedback_decls {
1613 builder.add_feedback_decl(decl);
1614 }
1615
1616 for decl in &program.state_decls {
1618 builder.add_state_decl(decl)?;
1619 }
1620
1621 for decl in &program.msg_decls {
1623 builder.add_msg(decl);
1624 }
1625
1626 for wire in &program.wires {
1628 builder.add_wire(wire)?;
1629 }
1630
1631 for dw in &program.destructuring_wires {
1633 builder.add_destructuring_wire(dw)?;
1634 }
1635
1636 for assign in &program.feedback_assignments {
1638 builder.add_feedback_assignment(assign)?;
1639 }
1640
1641 for assign in &program.state_assignments {
1643 builder.add_state_assignment(assign)?;
1644 }
1645
1646 for assign in &program.out_assignments {
1648 builder.add_out_assignment(assign)?;
1649 }
1650
1651 for decl in &program.out_decls {
1653 if let Some(ref value) = decl.value {
1654 let implicit_assign = OutAssignment {
1655 index: decl.index,
1656 value: value.clone(),
1657 span: None,
1658 };
1659 builder.add_out_assignment(&implicit_assign)?;
1660 }
1661 }
1662
1663 for conn in &program.direct_connections {
1665 builder.add_direct_connection(conn)?;
1666 }
1667
1668 if !options.skip_triggers {
1670 insert_triggers(&mut builder.graph);
1671 }
1672
1673 assign_edge_orders(&mut builder.graph);
1675
1676 Ok(builder.graph)
1677}
1678
1679pub fn build_graph_with_warnings(program: &Program) -> Result<BuildResult, BuildError> {
1681 build_graph_with_registry_and_warnings(program, None)
1682}
1683
1684pub fn build_graph_with_registry_and_warnings(
1686 program: &Program,
1687 registry: Option<&AbstractionRegistry>,
1688) -> Result<BuildResult, BuildError> {
1689 let graph = build_graph_with_registry(program, registry)?;
1690 let warnings = detect_duplicate_inlets(&graph);
1691 Ok(BuildResult { graph, warnings })
1692}
1693
1694fn detect_duplicate_inlets(graph: &PatchGraph) -> Vec<BuildWarning> {
1696 let mut inlet_counts: HashMap<(String, u32), usize> = HashMap::new();
1697 for edge in &graph.edges {
1698 if !edge.is_feedback {
1699 *inlet_counts
1700 .entry((edge.dest_id.clone(), edge.dest_inlet))
1701 .or_insert(0) += 1;
1702 }
1703 }
1704 let mut warnings: Vec<BuildWarning> = inlet_counts
1705 .into_iter()
1706 .filter(|(_, count)| *count > 1)
1707 .map(
1708 |((node_id, inlet), count)| BuildWarning::DuplicateInletConnection {
1709 node_id,
1710 inlet,
1711 count,
1712 },
1713 )
1714 .collect();
1715 warnings.sort_by(|a, b| {
1717 let (a_id, a_inlet) = match a {
1718 BuildWarning::DuplicateInletConnection { node_id, inlet, .. } => (node_id, inlet),
1719 };
1720 let (b_id, b_inlet) = match b {
1721 BuildWarning::DuplicateInletConnection { node_id, inlet, .. } => (node_id, inlet),
1722 };
1723 a_id.cmp(b_id).then(a_inlet.cmp(b_inlet))
1724 });
1725 warnings
1726}
1727
1728#[cfg(test)]
1729mod tests {
1730 use super::*;
1731 use flutmax_ast::*;
1732
1733 fn make_l1_program() -> Program {
1735 Program {
1736 in_decls: vec![],
1737 out_decls: vec![],
1738 wires: vec![Wire {
1739 name: "osc".to_string(),
1740 value: Expr::Call {
1741 object: "cycle~".to_string(),
1742 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
1743 },
1744 span: None,
1745 attrs: vec![],
1746 }],
1747 destructuring_wires: vec![],
1748 msg_decls: vec![],
1749 out_assignments: vec![],
1750 direct_connections: vec![],
1751 feedback_decls: vec![],
1752 feedback_assignments: vec![],
1753 state_decls: vec![],
1754 state_assignments: vec![],
1755 }
1756 }
1757
1758 fn make_l2_program() -> Program {
1760 Program {
1761 in_decls: vec![InDecl {
1762 index: 0,
1763 name: "freq".to_string(),
1764 port_type: PortType::Float,
1765 }],
1766 out_decls: vec![OutDecl {
1767 index: 0,
1768 name: "audio".to_string(),
1769 port_type: PortType::Signal,
1770 value: None,
1771 }],
1772 wires: vec![
1773 Wire {
1774 name: "osc".to_string(),
1775 value: Expr::Call {
1776 object: "cycle~".to_string(),
1777 args: vec![CallArg::positional(Expr::Ref("freq".to_string()))],
1778 },
1779 span: None,
1780 attrs: vec![],
1781 },
1782 Wire {
1783 name: "amp".to_string(),
1784 value: Expr::Call {
1785 object: "mul~".to_string(),
1786 args: vec![
1787 CallArg::positional(Expr::Ref("osc".to_string())),
1788 CallArg::positional(Expr::Lit(LitValue::Float(0.5))),
1789 ],
1790 },
1791 span: None,
1792 attrs: vec![],
1793 },
1794 ],
1795 destructuring_wires: vec![],
1796 msg_decls: vec![],
1797 out_assignments: vec![OutAssignment {
1798 index: 0,
1799 value: Expr::Ref("amp".to_string()),
1800 span: None,
1801 }],
1802 direct_connections: vec![],
1803 feedback_decls: vec![],
1804 feedback_assignments: vec![],
1805 state_decls: vec![],
1806 state_assignments: vec![],
1807 }
1808 }
1809
1810 #[test]
1811 fn test_build_l1_nodes() {
1812 let prog = make_l1_program();
1813 let graph = build_graph(&prog).unwrap();
1814
1815 assert_eq!(graph.nodes.len(), 1);
1817 let node = &graph.nodes[0];
1818 assert_eq!(node.object_name, "cycle~");
1819 assert_eq!(node.args, vec!["440"]);
1820 assert!(node.is_signal);
1821 assert_eq!(node.num_inlets, 2);
1822 assert_eq!(node.num_outlets, 1);
1823 }
1824
1825 #[test]
1826 fn test_build_l1_no_edges() {
1827 let prog = make_l1_program();
1828 let graph = build_graph(&prog).unwrap();
1829
1830 assert_eq!(graph.edges.len(), 0);
1832 }
1833
1834 #[test]
1835 fn test_build_l2_nodes() {
1836 let prog = make_l2_program();
1837 let graph = build_graph(&prog).unwrap();
1838
1839 assert_eq!(graph.nodes.len(), 4);
1841
1842 let names: Vec<&str> = graph.nodes.iter().map(|n| n.object_name.as_str()).collect();
1843 assert!(names.contains(&"inlet"));
1844 assert!(names.contains(&"outlet~"));
1845 assert!(names.contains(&"cycle~"));
1846 assert!(names.contains(&"*~"));
1847 }
1848
1849 #[test]
1850 fn test_build_l2_edges() {
1851 let prog = make_l2_program();
1852 let graph = build_graph(&prog).unwrap();
1853
1854 assert_eq!(graph.edges.len(), 3);
1856
1857 let inlet_node = graph
1859 .nodes
1860 .iter()
1861 .find(|n| n.object_name == "inlet")
1862 .unwrap();
1863 let cycle_node = graph
1864 .nodes
1865 .iter()
1866 .find(|n| n.object_name == "cycle~")
1867 .unwrap();
1868 let inlet_to_cycle = graph
1869 .edges
1870 .iter()
1871 .find(|e| e.source_id == inlet_node.id && e.dest_id == cycle_node.id)
1872 .expect("edge from inlet to cycle~ should exist");
1873 assert_eq!(inlet_to_cycle.source_outlet, 0);
1874 assert_eq!(inlet_to_cycle.dest_inlet, 0);
1875
1876 let mul_node = graph.nodes.iter().find(|n| n.object_name == "*~").unwrap();
1878 let cycle_to_mul = graph
1879 .edges
1880 .iter()
1881 .find(|e| e.source_id == cycle_node.id && e.dest_id == mul_node.id)
1882 .expect("edge from cycle~ to *~ should exist");
1883 assert_eq!(cycle_to_mul.dest_inlet, 0);
1884
1885 let outlet_node = graph
1887 .nodes
1888 .iter()
1889 .find(|n| n.object_name == "outlet~")
1890 .unwrap();
1891 let mul_to_outlet = graph
1892 .edges
1893 .iter()
1894 .find(|e| e.source_id == mul_node.id && e.dest_id == outlet_node.id)
1895 .expect("edge from *~ to outlet~ should exist");
1896 assert_eq!(mul_to_outlet.dest_inlet, 0);
1897 }
1898
1899 #[test]
1900 fn test_build_l2_mul_args() {
1901 let prog = make_l2_program();
1902 let graph = build_graph(&prog).unwrap();
1903
1904 let mul_node = graph.nodes.iter().find(|n| n.object_name == "*~").unwrap();
1905 assert_eq!(mul_node.args, vec!["0.5"]);
1907 }
1908
1909 #[test]
1910 fn test_undefined_ref_error() {
1911 let prog = Program {
1912 in_decls: vec![],
1913 out_decls: vec![],
1914 wires: vec![Wire {
1915 name: "x".to_string(),
1916 value: Expr::Call {
1917 object: "cycle~".to_string(),
1918 args: vec![CallArg::positional(Expr::Ref("nonexistent".to_string()))],
1919 },
1920 span: None,
1921 attrs: vec![],
1922 }],
1923 destructuring_wires: vec![],
1924 msg_decls: vec![],
1925 out_assignments: vec![],
1926 direct_connections: vec![],
1927 feedback_decls: vec![],
1928 feedback_assignments: vec![],
1929 state_decls: vec![],
1930 state_assignments: vec![],
1931 };
1932
1933 let result = build_graph(&prog);
1934 assert!(result.is_err());
1935 match result.unwrap_err() {
1936 BuildError::UndefinedRef(name) => assert_eq!(name, "nonexistent"),
1937 _ => panic!("expected UndefinedRef error"),
1938 }
1939 }
1940
1941 #[test]
1942 fn test_outlet_index_out_of_range() {
1943 let prog = Program {
1944 in_decls: vec![],
1945 out_decls: vec![OutDecl {
1946 index: 0,
1947 name: "out".to_string(),
1948 port_type: PortType::Float,
1949 value: None,
1950 }],
1951 wires: vec![Wire {
1952 name: "x".to_string(),
1953 value: Expr::Call {
1954 object: "button".to_string(),
1955 args: vec![],
1956 },
1957 span: None,
1958 attrs: vec![],
1959 }],
1960 destructuring_wires: vec![],
1961 msg_decls: vec![],
1962 out_assignments: vec![OutAssignment {
1963 index: 5, value: Expr::Ref("x".to_string()),
1965 span: None,
1966 }],
1967 direct_connections: vec![],
1968 feedback_decls: vec![],
1969 feedback_assignments: vec![],
1970 state_decls: vec![],
1971 state_assignments: vec![],
1972 };
1973
1974 let result = build_graph(&prog);
1975 assert!(result.is_err());
1976 match result.unwrap_err() {
1977 BuildError::NoOutDeclaration(idx) => assert_eq!(idx, 5),
1978 _ => panic!("expected NoOutDeclaration error"),
1979 }
1980 }
1981
1982 #[test]
1983 fn test_format_lit_int() {
1984 assert_eq!(format_lit(&LitValue::Int(440)), "440");
1985 assert_eq!(format_lit(&LitValue::Int(-1)), "-1");
1986 assert_eq!(format_lit(&LitValue::Int(0)), "0");
1987 }
1988
1989 #[test]
1990 fn test_format_lit_float() {
1991 assert_eq!(format_lit(&LitValue::Float(0.5)), "0.5");
1992 assert_eq!(format_lit(&LitValue::Float(440.0)), "440.");
1993 assert_eq!(format_lit(&LitValue::Float(3.14)), "3.14");
1994 }
1995
1996 #[test]
1997 fn test_format_lit_str() {
1998 assert_eq!(format_lit(&LitValue::Str("hello".to_string())), "hello");
1999 }
2000
2001 #[test]
2002 fn test_signal_inlet_is_signal() {
2003 let prog = Program {
2004 in_decls: vec![InDecl {
2005 index: 0,
2006 name: "sig_in".to_string(),
2007 port_type: PortType::Signal,
2008 }],
2009 out_decls: vec![],
2010 wires: vec![],
2011 destructuring_wires: vec![],
2012 msg_decls: vec![],
2013 out_assignments: vec![],
2014 direct_connections: vec![],
2015 feedback_decls: vec![],
2016 feedback_assignments: vec![],
2017 state_decls: vec![],
2018 state_assignments: vec![],
2019 };
2020
2021 let graph = build_graph(&prog).unwrap();
2022 let inlet_node = &graph.nodes[0];
2023 assert_eq!(inlet_node.object_name, "inlet~");
2024 assert!(inlet_node.is_signal);
2025 assert_eq!(inlet_node.num_inlets, 1);
2026 assert_eq!(inlet_node.num_outlets, 1);
2027 }
2028
2029 #[test]
2030 fn test_control_inlet_not_signal() {
2031 let prog = Program {
2032 in_decls: vec![InDecl {
2033 index: 0,
2034 name: "ctrl_in".to_string(),
2035 port_type: PortType::Float,
2036 }],
2037 out_decls: vec![],
2038 wires: vec![],
2039 destructuring_wires: vec![],
2040 msg_decls: vec![],
2041 out_assignments: vec![],
2042 direct_connections: vec![],
2043 feedback_decls: vec![],
2044 feedback_assignments: vec![],
2045 state_decls: vec![],
2046 state_assignments: vec![],
2047 };
2048
2049 let graph = build_graph(&prog).unwrap();
2050 let inlet_node = &graph.nodes[0];
2051 assert_eq!(inlet_node.object_name, "inlet");
2052 assert!(!inlet_node.is_signal);
2053 assert_eq!(inlet_node.num_inlets, 0);
2054 assert_eq!(inlet_node.num_outlets, 1);
2055 }
2056
2057 #[test]
2058 fn test_signal_outlet() {
2059 let prog = Program {
2060 in_decls: vec![],
2061 out_decls: vec![OutDecl {
2062 index: 0,
2063 name: "audio".to_string(),
2064 port_type: PortType::Signal,
2065 value: None,
2066 }],
2067 wires: vec![],
2068 destructuring_wires: vec![],
2069 msg_decls: vec![],
2070 out_assignments: vec![],
2071 direct_connections: vec![],
2072 feedback_decls: vec![],
2073 feedback_assignments: vec![],
2074 state_decls: vec![],
2075 state_assignments: vec![],
2076 };
2077
2078 let graph = build_graph(&prog).unwrap();
2079 let outlet_node = &graph.nodes[0];
2080 assert_eq!(outlet_node.object_name, "outlet~");
2081 assert!(outlet_node.is_signal);
2082 }
2083
2084 #[test]
2085 fn test_control_outlet() {
2086 let prog = Program {
2087 in_decls: vec![],
2088 out_decls: vec![OutDecl {
2089 index: 0,
2090 name: "ctrl_out".to_string(),
2091 port_type: PortType::Float,
2092 value: None,
2093 }],
2094 wires: vec![],
2095 destructuring_wires: vec![],
2096 msg_decls: vec![],
2097 out_assignments: vec![],
2098 direct_connections: vec![],
2099 feedback_decls: vec![],
2100 feedback_assignments: vec![],
2101 state_decls: vec![],
2102 state_assignments: vec![],
2103 };
2104
2105 let graph = build_graph(&prog).unwrap();
2106 let outlet_node = &graph.nodes[0];
2107 assert_eq!(outlet_node.object_name, "outlet");
2108 assert!(!outlet_node.is_signal);
2109 }
2110
2111 #[test]
2112 fn test_nested_call() {
2113 let prog = Program {
2115 in_decls: vec![],
2116 out_decls: vec![],
2117 wires: vec![Wire {
2118 name: "x".to_string(),
2119 value: Expr::Call {
2120 object: "*~".to_string(),
2121 args: vec![
2122 CallArg::positional(Expr::Call {
2123 object: "cycle~".to_string(),
2124 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
2125 }),
2126 CallArg::positional(Expr::Lit(LitValue::Float(0.5))),
2127 ],
2128 },
2129 span: None,
2130 attrs: vec![],
2131 }],
2132 destructuring_wires: vec![],
2133 msg_decls: vec![],
2134 out_assignments: vec![],
2135 direct_connections: vec![],
2136 feedback_decls: vec![],
2137 feedback_assignments: vec![],
2138 state_decls: vec![],
2139 state_assignments: vec![],
2140 };
2141
2142 let graph = build_graph(&prog).unwrap();
2143 assert_eq!(graph.nodes.len(), 2);
2145
2146 let cycle_node = graph
2147 .nodes
2148 .iter()
2149 .find(|n| n.object_name == "cycle~")
2150 .unwrap();
2151 let mul_node = graph.nodes.iter().find(|n| n.object_name == "*~").unwrap();
2152
2153 let edge = graph
2155 .edges
2156 .iter()
2157 .find(|e| e.source_id == cycle_node.id && e.dest_id == mul_node.id)
2158 .expect("edge from cycle~ to *~ should exist");
2159 assert_eq!(edge.dest_inlet, 0);
2160 }
2161
2162 #[test]
2163 fn test_multiple_outlets() {
2164 let prog = Program {
2166 in_decls: vec![],
2167 out_decls: vec![
2168 OutDecl {
2169 index: 0,
2170 name: "left".to_string(),
2171 port_type: PortType::Signal,
2172 value: None,
2173 },
2174 OutDecl {
2175 index: 1,
2176 name: "right".to_string(),
2177 port_type: PortType::Signal,
2178 value: None,
2179 },
2180 ],
2181 wires: vec![Wire {
2182 name: "osc".to_string(),
2183 value: Expr::Call {
2184 object: "cycle~".to_string(),
2185 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
2186 },
2187 span: None,
2188 attrs: vec![],
2189 }],
2190 destructuring_wires: vec![],
2191 msg_decls: vec![],
2192 out_assignments: vec![
2193 OutAssignment {
2194 index: 0,
2195 value: Expr::Ref("osc".to_string()),
2196 span: None,
2197 },
2198 OutAssignment {
2199 index: 1,
2200 value: Expr::Ref("osc".to_string()),
2201 span: None,
2202 },
2203 ],
2204 direct_connections: vec![],
2205 feedback_decls: vec![],
2206 feedback_assignments: vec![],
2207 state_decls: vec![],
2208 state_assignments: vec![],
2209 };
2210
2211 let graph = build_graph(&prog).unwrap();
2212
2213 let outlet_nodes: Vec<&PatchNode> = graph
2215 .nodes
2216 .iter()
2217 .filter(|n| n.object_name == "outlet~")
2218 .collect();
2219 assert_eq!(outlet_nodes.len(), 2);
2220
2221 let cycle_node = graph
2223 .nodes
2224 .iter()
2225 .find(|n| n.object_name == "cycle~")
2226 .unwrap();
2227 let edges_from_cycle: Vec<&PatchEdge> = graph
2228 .edges
2229 .iter()
2230 .filter(|e| e.source_id == cycle_node.id)
2231 .collect();
2232 assert_eq!(edges_from_cycle.len(), 2);
2233 }
2234
2235 fn make_oscillator_program() -> Program {
2239 Program {
2240 in_decls: vec![InDecl {
2241 index: 0,
2242 name: "freq".to_string(),
2243 port_type: PortType::Float,
2244 }],
2245 out_decls: vec![OutDecl {
2246 index: 0,
2247 name: "audio".to_string(),
2248 port_type: PortType::Signal,
2249 value: None,
2250 }],
2251 wires: vec![Wire {
2252 name: "osc".to_string(),
2253 value: Expr::Call {
2254 object: "cycle~".to_string(),
2255 args: vec![CallArg::positional(Expr::Ref("freq".to_string()))],
2256 },
2257 span: None,
2258 attrs: vec![],
2259 }],
2260 destructuring_wires: vec![],
2261 msg_decls: vec![],
2262 out_assignments: vec![OutAssignment {
2263 index: 0,
2264 value: Expr::Ref("osc".to_string()),
2265 span: None,
2266 }],
2267 direct_connections: vec![],
2268 feedback_decls: vec![],
2269 feedback_assignments: vec![],
2270 state_decls: vec![],
2271 state_assignments: vec![],
2272 }
2273 }
2274
2275 fn make_fm_synth_program() -> Program {
2277 Program {
2278 in_decls: vec![InDecl {
2279 index: 0,
2280 name: "base_freq".to_string(),
2281 port_type: PortType::Float,
2282 }],
2283 out_decls: vec![OutDecl {
2284 index: 0,
2285 name: "audio".to_string(),
2286 port_type: PortType::Signal,
2287 value: None,
2288 }],
2289 wires: vec![
2290 Wire {
2291 name: "carrier".to_string(),
2292 value: Expr::Call {
2293 object: "oscillator".to_string(),
2294 args: vec![CallArg::positional(Expr::Ref("base_freq".to_string()))],
2295 },
2296 span: None,
2297 attrs: vec![],
2298 },
2299 Wire {
2300 name: "amp".to_string(),
2301 value: Expr::Call {
2302 object: "mul~".to_string(),
2303 args: vec![
2304 CallArg::positional(Expr::Ref("carrier".to_string())),
2305 CallArg::positional(Expr::Lit(LitValue::Float(0.5))),
2306 ],
2307 },
2308 span: None,
2309 attrs: vec![],
2310 },
2311 ],
2312 destructuring_wires: vec![],
2313 msg_decls: vec![],
2314 out_assignments: vec![OutAssignment {
2315 index: 0,
2316 value: Expr::Ref("amp".to_string()),
2317 span: None,
2318 }],
2319 direct_connections: vec![],
2320 feedback_decls: vec![],
2321 feedback_assignments: vec![],
2322 state_decls: vec![],
2323 state_assignments: vec![],
2324 }
2325 }
2326
2327 #[test]
2328 fn test_build_graph_with_registry_abstraction_inlets_outlets() {
2329 let mut registry = AbstractionRegistry::new();
2330 registry.register("oscillator", &make_oscillator_program());
2331
2332 let prog = make_fm_synth_program();
2333 let graph = build_graph_with_registry(&prog, Some(®istry)).unwrap();
2334
2335 let osc_node = graph
2337 .nodes
2338 .iter()
2339 .find(|n| n.object_name == "oscillator")
2340 .expect("oscillator node should exist");
2341
2342 assert_eq!(osc_node.num_inlets, 1);
2344 assert_eq!(osc_node.num_outlets, 1);
2345 assert!(osc_node.is_signal);
2347 }
2348
2349 #[test]
2350 fn test_build_graph_with_registry_abstraction_name_preserved() {
2351 let mut registry = AbstractionRegistry::new();
2352 registry.register("oscillator", &make_oscillator_program());
2353
2354 let prog = make_fm_synth_program();
2355 let graph = build_graph_with_registry(&prog, Some(®istry)).unwrap();
2356
2357 let osc_node = graph
2359 .nodes
2360 .iter()
2361 .find(|n| n.object_name == "oscillator")
2362 .expect("oscillator node should exist with original name");
2363 assert_eq!(osc_node.object_name, "oscillator");
2364 }
2365
2366 #[test]
2367 fn test_build_graph_with_registry_full_graph() {
2368 let mut registry = AbstractionRegistry::new();
2369 registry.register("oscillator", &make_oscillator_program());
2370
2371 let prog = make_fm_synth_program();
2372 let graph = build_graph_with_registry(&prog, Some(®istry)).unwrap();
2373
2374 assert_eq!(graph.nodes.len(), 4);
2376
2377 let names: Vec<&str> = graph.nodes.iter().map(|n| n.object_name.as_str()).collect();
2378 assert!(names.contains(&"inlet"));
2379 assert!(names.contains(&"outlet~"));
2380 assert!(names.contains(&"oscillator"));
2381 assert!(names.contains(&"*~"));
2382
2383 assert_eq!(graph.edges.len(), 3);
2385 }
2386
2387 #[test]
2388 fn test_build_graph_without_registry_unknown_object() {
2389 let prog = make_fm_synth_program();
2392 let graph = build_graph(&prog).unwrap();
2393
2394 let osc_node = graph
2395 .nodes
2396 .iter()
2397 .find(|n| n.object_name == "oscillator")
2398 .expect("oscillator node should exist");
2399
2400 assert_eq!(osc_node.num_inlets, 1);
2403 assert_eq!(osc_node.num_outlets, 1);
2404 }
2405
2406 #[test]
2407 fn test_build_graph_with_registry_multi_port_abstraction() {
2408 let filter_prog = Program {
2410 in_decls: vec![
2411 InDecl {
2412 index: 0,
2413 name: "input_sig".to_string(),
2414 port_type: PortType::Signal,
2415 },
2416 InDecl {
2417 index: 1,
2418 name: "cutoff".to_string(),
2419 port_type: PortType::Float,
2420 },
2421 InDecl {
2422 index: 2,
2423 name: "q_factor".to_string(),
2424 port_type: PortType::Float,
2425 },
2426 ],
2427 out_decls: vec![
2428 OutDecl {
2429 index: 0,
2430 name: "lowpass".to_string(),
2431 port_type: PortType::Signal,
2432 value: None,
2433 },
2434 OutDecl {
2435 index: 1,
2436 name: "highpass".to_string(),
2437 port_type: PortType::Signal,
2438 value: None,
2439 },
2440 ],
2441 wires: vec![],
2442 destructuring_wires: vec![],
2443 msg_decls: vec![],
2444 out_assignments: vec![],
2445 direct_connections: vec![],
2446 feedback_decls: vec![],
2447 feedback_assignments: vec![],
2448 state_decls: vec![],
2449 state_assignments: vec![],
2450 };
2451
2452 let mut registry = AbstractionRegistry::new();
2453 registry.register("filter", &filter_prog);
2454
2455 let caller = Program {
2457 in_decls: vec![],
2458 out_decls: vec![],
2459 wires: vec![Wire {
2460 name: "result".to_string(),
2461 value: Expr::Call {
2462 object: "filter".to_string(),
2463 args: vec![
2464 CallArg::positional(Expr::Call {
2465 object: "cycle~".to_string(),
2466 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
2467 }),
2468 CallArg::positional(Expr::Lit(LitValue::Int(1000))),
2469 CallArg::positional(Expr::Lit(LitValue::Float(0.7))),
2470 ],
2471 },
2472 span: None,
2473 attrs: vec![],
2474 }],
2475 destructuring_wires: vec![],
2476 msg_decls: vec![],
2477 out_assignments: vec![],
2478 direct_connections: vec![],
2479 feedback_decls: vec![],
2480 feedback_assignments: vec![],
2481 state_decls: vec![],
2482 state_assignments: vec![],
2483 };
2484
2485 let graph = build_graph_with_registry(&caller, Some(®istry)).unwrap();
2486
2487 let filter_node = graph
2488 .nodes
2489 .iter()
2490 .find(|n| n.object_name == "filter")
2491 .expect("filter node should exist");
2492
2493 assert_eq!(filter_node.num_inlets, 3);
2494 assert_eq!(filter_node.num_outlets, 2);
2495 assert!(filter_node.is_signal);
2496 }
2497
2498 #[test]
2499 fn test_build_graph_with_none_registry() {
2500 let prog = make_l2_program();
2502 let graph = build_graph_with_registry(&prog, None).unwrap();
2503
2504 assert_eq!(graph.nodes.len(), 4);
2505 }
2506
2507 #[test]
2510 fn test_tuple_generates_pack_node() {
2511 let prog = Program {
2513 in_decls: vec![
2514 InDecl {
2515 index: 0,
2516 name: "x".to_string(),
2517 port_type: PortType::Float,
2518 },
2519 InDecl {
2520 index: 1,
2521 name: "y".to_string(),
2522 port_type: PortType::Float,
2523 },
2524 InDecl {
2525 index: 2,
2526 name: "z".to_string(),
2527 port_type: PortType::Float,
2528 },
2529 ],
2530 out_decls: vec![OutDecl {
2531 index: 0,
2532 name: "coords".to_string(),
2533 port_type: PortType::List,
2534 value: None,
2535 }],
2536 wires: vec![Wire {
2537 name: "packed".to_string(),
2538 value: Expr::Tuple(vec![
2539 Expr::Ref("x".to_string()),
2540 Expr::Ref("y".to_string()),
2541 Expr::Ref("z".to_string()),
2542 ]),
2543 span: None,
2544 attrs: vec![],
2545 }],
2546 destructuring_wires: vec![],
2547 msg_decls: vec![],
2548 out_assignments: vec![OutAssignment {
2549 index: 0,
2550 value: Expr::Ref("packed".to_string()),
2551 span: None,
2552 }],
2553 direct_connections: vec![],
2554 feedback_decls: vec![],
2555 feedback_assignments: vec![],
2556 state_decls: vec![],
2557 state_assignments: vec![],
2558 };
2559
2560 let graph = build_graph(&prog).unwrap();
2561
2562 let pack_node = graph
2564 .nodes
2565 .iter()
2566 .find(|n| n.object_name == "pack")
2567 .expect("pack node should exist");
2568 assert_eq!(pack_node.num_inlets, 3);
2569 assert_eq!(pack_node.num_outlets, 1);
2570 assert_eq!(pack_node.args, vec!["f", "f", "f"]);
2571 assert!(!pack_node.is_signal);
2572
2573 let edges_to_pack: Vec<_> = graph
2575 .edges
2576 .iter()
2577 .filter(|e| e.dest_id == pack_node.id)
2578 .collect();
2579 assert_eq!(edges_to_pack.len(), 3);
2580
2581 let mut dest_inlets: Vec<u32> = edges_to_pack.iter().map(|e| e.dest_inlet).collect();
2583 dest_inlets.sort();
2584 assert_eq!(dest_inlets, vec![0, 1, 2]);
2585 }
2586
2587 #[test]
2588 fn test_destructuring_with_unpack_call() {
2589 use flutmax_ast::DestructuringWire;
2592
2593 let prog = Program {
2594 in_decls: vec![InDecl {
2595 index: 0,
2596 name: "data".to_string(),
2597 port_type: PortType::Float,
2598 }],
2599 out_decls: vec![
2600 OutDecl {
2601 index: 0,
2602 name: "x".to_string(),
2603 port_type: PortType::Float,
2604 value: None,
2605 },
2606 OutDecl {
2607 index: 1,
2608 name: "y".to_string(),
2609 port_type: PortType::Float,
2610 value: None,
2611 },
2612 ],
2613 wires: vec![],
2614 destructuring_wires: vec![DestructuringWire {
2615 names: vec!["a".to_string(), "b".to_string()],
2616 value: Expr::Call {
2617 object: "unpack".to_string(),
2618 args: vec![CallArg::positional(Expr::Ref("data".to_string()))],
2619 },
2620 span: None,
2621 }],
2622 msg_decls: vec![],
2623 out_assignments: vec![
2624 OutAssignment {
2625 index: 0,
2626 value: Expr::Ref("a".to_string()),
2627 span: None,
2628 },
2629 OutAssignment {
2630 index: 1,
2631 value: Expr::Ref("b".to_string()),
2632 span: None,
2633 },
2634 ],
2635 direct_connections: vec![],
2636 feedback_decls: vec![],
2637 feedback_assignments: vec![],
2638 state_decls: vec![],
2639 state_assignments: vec![],
2640 };
2641
2642 let graph = build_graph(&prog).unwrap();
2643
2644 let unpack_nodes: Vec<_> = graph
2647 .nodes
2648 .iter()
2649 .filter(|n| n.object_name == "unpack")
2650 .collect();
2651 assert_eq!(unpack_nodes.len(), 1);
2652
2653 let unpack_node = unpack_nodes[0];
2654 assert_eq!(unpack_node.num_outlets, 2);
2655 assert!(!unpack_node.is_signal);
2656
2657 let edges_to_unpack: Vec<_> = graph
2659 .edges
2660 .iter()
2661 .filter(|e| e.dest_id == unpack_node.id)
2662 .collect();
2663 assert_eq!(edges_to_unpack.len(), 1);
2664
2665 let outlet_nodes: Vec<_> = graph
2667 .nodes
2668 .iter()
2669 .filter(|n| n.object_name == "outlet")
2670 .collect();
2671 assert_eq!(outlet_nodes.len(), 2);
2672
2673 let edges_from_unpack: Vec<_> = graph
2675 .edges
2676 .iter()
2677 .filter(|e| e.source_id == unpack_node.id)
2678 .collect();
2679 assert_eq!(edges_from_unpack.len(), 2);
2680
2681 let mut source_outlets: Vec<u32> =
2683 edges_from_unpack.iter().map(|e| e.source_outlet).collect();
2684 source_outlets.sort();
2685 assert_eq!(source_outlets, vec![0, 1]);
2686 }
2687
2688 #[test]
2689 fn test_destructuring_with_ref_auto_unpack() {
2690 use flutmax_ast::DestructuringWire;
2692
2693 let prog = Program {
2694 in_decls: vec![
2695 InDecl {
2696 index: 0,
2697 name: "x".to_string(),
2698 port_type: PortType::Float,
2699 },
2700 InDecl {
2701 index: 1,
2702 name: "y".to_string(),
2703 port_type: PortType::Float,
2704 },
2705 ],
2706 out_decls: vec![],
2707 wires: vec![Wire {
2708 name: "packed".to_string(),
2709 value: Expr::Tuple(vec![Expr::Ref("x".to_string()), Expr::Ref("y".to_string())]),
2710 span: None,
2711 attrs: vec![],
2712 }],
2713 destructuring_wires: vec![DestructuringWire {
2714 names: vec!["a".to_string(), "b".to_string()],
2715 value: Expr::Ref("packed".to_string()),
2716 span: None,
2717 }],
2718 msg_decls: vec![],
2719 out_assignments: vec![],
2720 direct_connections: vec![],
2721 feedback_decls: vec![],
2722 feedback_assignments: vec![],
2723 state_decls: vec![],
2724 state_assignments: vec![],
2725 };
2726
2727 let graph = build_graph(&prog).unwrap();
2728
2729 let pack_node = graph
2731 .nodes
2732 .iter()
2733 .find(|n| n.object_name == "pack")
2734 .expect("pack node should exist");
2735 assert_eq!(pack_node.num_outlets, 1);
2736
2737 let unpack_node = graph
2739 .nodes
2740 .iter()
2741 .find(|n| n.object_name == "unpack")
2742 .expect("unpack node should be auto-inserted");
2743 assert_eq!(unpack_node.num_outlets, 2);
2744 assert_eq!(unpack_node.args, vec!["f", "f"]);
2745
2746 let pack_to_unpack = graph
2748 .edges
2749 .iter()
2750 .find(|e| e.source_id == pack_node.id && e.dest_id == unpack_node.id)
2751 .expect("edge from pack to unpack should exist");
2752 assert_eq!(pack_to_unpack.dest_inlet, 0);
2753 }
2754
2755 #[test]
2756 fn test_tuple_two_elements_pack() {
2757 let prog = Program {
2759 in_decls: vec![
2760 InDecl {
2761 index: 0,
2762 name: "a".to_string(),
2763 port_type: PortType::Float,
2764 },
2765 InDecl {
2766 index: 1,
2767 name: "b".to_string(),
2768 port_type: PortType::Float,
2769 },
2770 ],
2771 out_decls: vec![],
2772 wires: vec![Wire {
2773 name: "t".to_string(),
2774 value: Expr::Tuple(vec![Expr::Ref("a".to_string()), Expr::Ref("b".to_string())]),
2775 span: None,
2776 attrs: vec![],
2777 }],
2778 destructuring_wires: vec![],
2779 msg_decls: vec![],
2780 out_assignments: vec![],
2781 direct_connections: vec![],
2782 feedback_decls: vec![],
2783 feedback_assignments: vec![],
2784 state_decls: vec![],
2785 state_assignments: vec![],
2786 };
2787
2788 let graph = build_graph(&prog).unwrap();
2789
2790 let pack_node = graph
2791 .nodes
2792 .iter()
2793 .find(|n| n.object_name == "pack")
2794 .expect("pack node should exist");
2795 assert_eq!(pack_node.num_inlets, 2);
2796 assert_eq!(pack_node.args, vec!["f", "f"]);
2797 }
2798
2799 #[test]
2802 fn test_feedback_generates_tapin_node() {
2803 use flutmax_ast::FeedbackDecl;
2805
2806 let prog = Program {
2807 in_decls: vec![InDecl {
2808 index: 0,
2809 name: "input".to_string(),
2810 port_type: PortType::Signal,
2811 }],
2812 out_decls: vec![OutDecl {
2813 index: 0,
2814 name: "output".to_string(),
2815 port_type: PortType::Signal,
2816 value: None,
2817 }],
2818 wires: vec![
2819 Wire {
2820 name: "delayed".to_string(),
2821 value: Expr::Call {
2822 object: "tapout~".to_string(),
2823 args: vec![
2824 CallArg::positional(Expr::Ref("fb".to_string())),
2825 CallArg::positional(Expr::Lit(LitValue::Int(500))),
2826 ],
2827 },
2828 span: None,
2829 attrs: vec![],
2830 },
2831 Wire {
2832 name: "mixed".to_string(),
2833 value: Expr::Call {
2834 object: "add~".to_string(),
2835 args: vec![
2836 CallArg::positional(Expr::Ref("input".to_string())),
2837 CallArg::positional(Expr::Call {
2838 object: "mul~".to_string(),
2839 args: vec![
2840 CallArg::positional(Expr::Ref("delayed".to_string())),
2841 CallArg::positional(Expr::Lit(LitValue::Float(0.3))),
2842 ],
2843 }),
2844 ],
2845 },
2846 span: None,
2847 attrs: vec![],
2848 },
2849 ],
2850 destructuring_wires: vec![],
2851 msg_decls: vec![],
2852 out_assignments: vec![OutAssignment {
2853 index: 0,
2854 value: Expr::Ref("mixed".to_string()),
2855 span: None,
2856 }],
2857 direct_connections: vec![],
2858 feedback_decls: vec![FeedbackDecl {
2859 name: "fb".to_string(),
2860 port_type: PortType::Signal,
2861 span: None,
2862 }],
2863 feedback_assignments: vec![FeedbackAssignment {
2864 target: "fb".to_string(),
2865 value: Expr::Call {
2866 object: "tapin~".to_string(),
2867 args: vec![
2868 CallArg::positional(Expr::Ref("mixed".to_string())),
2869 CallArg::positional(Expr::Lit(LitValue::Int(1000))),
2870 ],
2871 },
2872 span: None,
2873 }],
2874 state_decls: vec![],
2875 state_assignments: vec![],
2876 };
2877
2878 let graph = build_graph(&prog).unwrap();
2879
2880 let tapin_node = graph
2882 .nodes
2883 .iter()
2884 .find(|n| n.object_name == "tapin~")
2885 .expect("tapin~ node should exist");
2886 assert!(tapin_node.is_signal);
2887 assert_eq!(tapin_node.num_inlets, 1);
2888 assert_eq!(tapin_node.num_outlets, 1);
2889
2890 let tapout_node = graph
2892 .nodes
2893 .iter()
2894 .find(|n| n.object_name == "tapout~")
2895 .expect("tapout~ node should exist");
2896 assert!(tapout_node.is_signal);
2897
2898 let tapin_to_tapout = graph
2900 .edges
2901 .iter()
2902 .find(|e| e.source_id == tapin_node.id && e.dest_id == tapout_node.id)
2903 .expect("edge from tapin~ to tapout~ should exist");
2904 assert_eq!(tapin_to_tapout.source_outlet, 0);
2905 assert_eq!(tapin_to_tapout.dest_inlet, 0);
2906 assert!(!tapin_to_tapout.is_feedback);
2908
2909 let feedback_edges: Vec<_> = graph.edges.iter().filter(|e| e.is_feedback).collect();
2911 assert_eq!(
2912 feedback_edges.len(),
2913 1,
2914 "should have exactly one feedback edge"
2915 );
2916 }
2917
2918 #[test]
2919 fn test_feedback_no_trigger_on_feedback_edge() {
2920 use flutmax_ast::FeedbackDecl;
2922
2923 let prog = Program {
2924 in_decls: vec![InDecl {
2925 index: 0,
2926 name: "input".to_string(),
2927 port_type: PortType::Signal,
2928 }],
2929 out_decls: vec![OutDecl {
2930 index: 0,
2931 name: "output".to_string(),
2932 port_type: PortType::Signal,
2933 value: None,
2934 }],
2935 wires: vec![
2936 Wire {
2937 name: "delayed".to_string(),
2938 value: Expr::Call {
2939 object: "tapout~".to_string(),
2940 args: vec![
2941 CallArg::positional(Expr::Ref("fb".to_string())),
2942 CallArg::positional(Expr::Lit(LitValue::Int(500))),
2943 ],
2944 },
2945 span: None,
2946 attrs: vec![],
2947 },
2948 Wire {
2949 name: "mixed".to_string(),
2950 value: Expr::Call {
2951 object: "add~".to_string(),
2952 args: vec![
2953 CallArg::positional(Expr::Ref("input".to_string())),
2954 CallArg::positional(Expr::Ref("delayed".to_string())),
2955 ],
2956 },
2957 span: None,
2958 attrs: vec![],
2959 },
2960 ],
2961 destructuring_wires: vec![],
2962 msg_decls: vec![],
2963 out_assignments: vec![OutAssignment {
2964 index: 0,
2965 value: Expr::Ref("mixed".to_string()),
2966 span: None,
2967 }],
2968 direct_connections: vec![],
2969 feedback_decls: vec![FeedbackDecl {
2970 name: "fb".to_string(),
2971 port_type: PortType::Signal,
2972 span: None,
2973 }],
2974 feedback_assignments: vec![FeedbackAssignment {
2975 target: "fb".to_string(),
2976 value: Expr::Call {
2977 object: "tapin~".to_string(),
2978 args: vec![
2979 CallArg::positional(Expr::Ref("mixed".to_string())),
2980 CallArg::positional(Expr::Lit(LitValue::Int(1000))),
2981 ],
2982 },
2983 span: None,
2984 }],
2985 state_decls: vec![],
2986 state_assignments: vec![],
2987 };
2988
2989 let graph = build_graph(&prog).unwrap();
2990
2991 let trigger_nodes: Vec<_> = graph
2994 .nodes
2995 .iter()
2996 .filter(|n| n.object_name == "trigger")
2997 .collect();
2998 assert_eq!(
2999 trigger_nodes.len(),
3000 0,
3001 "no trigger nodes should be inserted for signal-only feedback"
3002 );
3003 }
3004
3005 #[test]
3008 fn test_e004_no_out_declaration_detected() {
3009 let prog = Program {
3011 in_decls: vec![],
3012 out_decls: vec![],
3013 wires: vec![Wire {
3014 name: "x".to_string(),
3015 value: Expr::Call {
3016 object: "button".to_string(),
3017 args: vec![],
3018 },
3019 span: None,
3020 attrs: vec![],
3021 }],
3022 destructuring_wires: vec![],
3023 msg_decls: vec![],
3024 out_assignments: vec![OutAssignment {
3025 index: 0,
3026 value: Expr::Ref("x".to_string()),
3027 span: None,
3028 }],
3029 direct_connections: vec![],
3030 feedback_decls: vec![],
3031 feedback_assignments: vec![],
3032 state_decls: vec![],
3033 state_assignments: vec![],
3034 };
3035
3036 let result = build_graph(&prog);
3037 assert!(result.is_err());
3038 match result.unwrap_err() {
3039 BuildError::NoOutDeclaration(idx) => assert_eq!(idx, 0),
3040 other => panic!("expected NoOutDeclaration, got {:?}", other),
3041 }
3042 }
3043
3044 #[test]
3045 fn test_e004_valid_out_declaration_no_error() {
3046 let prog = Program {
3048 in_decls: vec![],
3049 out_decls: vec![OutDecl {
3050 index: 0,
3051 name: "audio".to_string(),
3052 port_type: PortType::Signal,
3053 value: None,
3054 }],
3055 wires: vec![Wire {
3056 name: "osc".to_string(),
3057 value: Expr::Call {
3058 object: "cycle~".to_string(),
3059 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
3060 },
3061 span: None,
3062 attrs: vec![],
3063 }],
3064 destructuring_wires: vec![],
3065 msg_decls: vec![],
3066 out_assignments: vec![OutAssignment {
3067 index: 0,
3068 value: Expr::Ref("osc".to_string()),
3069 span: None,
3070 }],
3071 direct_connections: vec![],
3072 feedback_decls: vec![],
3073 feedback_assignments: vec![],
3074 state_decls: vec![],
3075 state_assignments: vec![],
3076 };
3077
3078 let result = build_graph(&prog);
3079 assert!(result.is_ok());
3080 }
3081
3082 #[test]
3085 fn test_e006_destructuring_count_mismatch_detected() {
3086 use flutmax_ast::DestructuringWire;
3088
3089 let prog = Program {
3090 in_decls: vec![InDecl {
3091 index: 0,
3092 name: "data".to_string(),
3093 port_type: PortType::Float,
3094 }],
3095 out_decls: vec![],
3096 wires: vec![],
3097 destructuring_wires: vec![DestructuringWire {
3098 names: vec!["a".to_string(), "b".to_string(), "c".to_string()],
3099 value: Expr::Call {
3100 object: "unpack".to_string(),
3101 args: vec![CallArg::positional(Expr::Ref("data".to_string()))],
3102 },
3103 span: None,
3104 }],
3105 msg_decls: vec![],
3106 out_assignments: vec![],
3107 direct_connections: vec![],
3108 feedback_decls: vec![],
3109 feedback_assignments: vec![],
3110 state_decls: vec![],
3111 state_assignments: vec![],
3112 };
3113
3114 let result = build_graph(&prog);
3115 assert!(result.is_err());
3116 match result.unwrap_err() {
3117 BuildError::DestructuringCountMismatch { expected, got } => {
3118 assert_eq!(expected, 2);
3119 assert_eq!(got, 3);
3120 }
3121 other => panic!("expected DestructuringCountMismatch, got {:?}", other),
3122 }
3123 }
3124
3125 #[test]
3126 fn test_e006_destructuring_count_match_no_error() {
3127 use flutmax_ast::DestructuringWire;
3129
3130 let prog = Program {
3131 in_decls: vec![InDecl {
3132 index: 0,
3133 name: "data".to_string(),
3134 port_type: PortType::Float,
3135 }],
3136 out_decls: vec![],
3137 wires: vec![],
3138 destructuring_wires: vec![DestructuringWire {
3139 names: vec!["a".to_string(), "b".to_string()],
3140 value: Expr::Call {
3141 object: "unpack".to_string(),
3142 args: vec![CallArg::positional(Expr::Ref("data".to_string()))],
3143 },
3144 span: None,
3145 }],
3146 msg_decls: vec![],
3147 out_assignments: vec![],
3148 direct_connections: vec![],
3149 feedback_decls: vec![],
3150 feedback_assignments: vec![],
3151 state_decls: vec![],
3152 state_assignments: vec![],
3153 };
3154
3155 let result = build_graph(&prog);
3156 assert!(result.is_ok());
3157 }
3158
3159 #[test]
3162 fn test_e009_abstraction_arg_count_mismatch_detected() {
3163 let mut registry = AbstractionRegistry::new();
3165 registry.register("oscillator", &make_oscillator_program());
3166
3167 let prog = Program {
3168 in_decls: vec![],
3169 out_decls: vec![],
3170 wires: vec![Wire {
3171 name: "osc".to_string(),
3172 value: Expr::Call {
3173 object: "oscillator".to_string(),
3174 args: vec![
3175 CallArg::positional(Expr::Lit(LitValue::Int(440))),
3176 CallArg::positional(Expr::Lit(LitValue::Float(0.5))),
3177 ],
3178 },
3179 span: None,
3180 attrs: vec![],
3181 }],
3182 destructuring_wires: vec![],
3183 msg_decls: vec![],
3184 out_assignments: vec![],
3185 direct_connections: vec![],
3186 feedback_decls: vec![],
3187 feedback_assignments: vec![],
3188 state_decls: vec![],
3189 state_assignments: vec![],
3190 };
3191
3192 let result = build_graph_with_registry(&prog, Some(®istry));
3193 assert!(result.is_err());
3194 match result.unwrap_err() {
3195 BuildError::AbstractionArgCountMismatch {
3196 name,
3197 expected,
3198 got,
3199 } => {
3200 assert_eq!(name, "oscillator");
3201 assert_eq!(expected, 1);
3202 assert_eq!(got, 2);
3203 }
3204 other => panic!("expected AbstractionArgCountMismatch, got {:?}", other),
3205 }
3206 }
3207
3208 #[test]
3209 fn test_e009_abstraction_arg_count_match_no_error() {
3210 let mut registry = AbstractionRegistry::new();
3212 registry.register("oscillator", &make_oscillator_program());
3213
3214 let prog = Program {
3215 in_decls: vec![],
3216 out_decls: vec![],
3217 wires: vec![Wire {
3218 name: "osc".to_string(),
3219 value: Expr::Call {
3220 object: "oscillator".to_string(),
3221 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
3222 },
3223 span: None,
3224 attrs: vec![],
3225 }],
3226 destructuring_wires: vec![],
3227 msg_decls: vec![],
3228 out_assignments: vec![],
3229 direct_connections: vec![],
3230 feedback_decls: vec![],
3231 feedback_assignments: vec![],
3232 state_decls: vec![],
3233 state_assignments: vec![],
3234 };
3235
3236 let result = build_graph_with_registry(&prog, Some(®istry));
3237 assert!(result.is_ok());
3238 }
3239
3240 #[test]
3243 fn test_e013_duplicate_feedback_assignment_detected() {
3244 use flutmax_ast::{FeedbackAssignment, FeedbackDecl};
3246
3247 let prog = Program {
3248 in_decls: vec![InDecl {
3249 index: 0,
3250 name: "input".to_string(),
3251 port_type: PortType::Signal,
3252 }],
3253 out_decls: vec![],
3254 wires: vec![Wire {
3255 name: "sig".to_string(),
3256 value: Expr::Call {
3257 object: "cycle~".to_string(),
3258 args: vec![CallArg::positional(Expr::Ref("input".to_string()))],
3259 },
3260 span: None,
3261 attrs: vec![],
3262 }],
3263 destructuring_wires: vec![],
3264 msg_decls: vec![],
3265 out_assignments: vec![],
3266 direct_connections: vec![],
3267 feedback_decls: vec![FeedbackDecl {
3268 name: "fb".to_string(),
3269 port_type: PortType::Signal,
3270 span: None,
3271 }],
3272 feedback_assignments: vec![
3273 FeedbackAssignment {
3274 target: "fb".to_string(),
3275 value: Expr::Ref("sig".to_string()),
3276 span: None,
3277 },
3278 FeedbackAssignment {
3279 target: "fb".to_string(),
3280 value: Expr::Ref("sig".to_string()),
3281 span: None,
3282 },
3283 ],
3284 state_decls: vec![],
3285 state_assignments: vec![],
3286 };
3287
3288 let result = build_graph(&prog);
3289 assert!(result.is_err());
3290 match result.unwrap_err() {
3291 BuildError::DuplicateFeedbackAssignment(name) => assert_eq!(name, "fb"),
3292 other => panic!("expected DuplicateFeedbackAssignment, got {:?}", other),
3293 }
3294 }
3295
3296 #[test]
3297 fn test_e013_single_feedback_assignment_no_error() {
3298 use flutmax_ast::{FeedbackAssignment, FeedbackDecl};
3300
3301 let prog = Program {
3302 in_decls: vec![InDecl {
3303 index: 0,
3304 name: "input".to_string(),
3305 port_type: PortType::Signal,
3306 }],
3307 out_decls: vec![],
3308 wires: vec![Wire {
3309 name: "sig".to_string(),
3310 value: Expr::Call {
3311 object: "cycle~".to_string(),
3312 args: vec![CallArg::positional(Expr::Ref("input".to_string()))],
3313 },
3314 span: None,
3315 attrs: vec![],
3316 }],
3317 destructuring_wires: vec![],
3318 msg_decls: vec![],
3319 out_assignments: vec![],
3320 direct_connections: vec![],
3321 feedback_decls: vec![FeedbackDecl {
3322 name: "fb".to_string(),
3323 port_type: PortType::Signal,
3324 span: None,
3325 }],
3326 feedback_assignments: vec![FeedbackAssignment {
3327 target: "fb".to_string(),
3328 value: Expr::Ref("sig".to_string()),
3329 span: None,
3330 }],
3331 state_decls: vec![],
3332 state_assignments: vec![],
3333 };
3334
3335 let result = build_graph(&prog);
3336 assert!(result.is_ok());
3337 }
3338
3339 #[test]
3342 fn test_fanout_edges_get_order() {
3343 let prog = Program {
3345 in_decls: vec![],
3346 out_decls: vec![
3347 OutDecl {
3348 index: 0,
3349 name: "left".to_string(),
3350 port_type: PortType::Signal,
3351 value: None,
3352 },
3353 OutDecl {
3354 index: 1,
3355 name: "right".to_string(),
3356 port_type: PortType::Signal,
3357 value: None,
3358 },
3359 ],
3360 wires: vec![Wire {
3361 name: "osc".to_string(),
3362 value: Expr::Call {
3363 object: "cycle~".to_string(),
3364 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
3365 },
3366 span: None,
3367 attrs: vec![],
3368 }],
3369 destructuring_wires: vec![],
3370 msg_decls: vec![],
3371 out_assignments: vec![
3372 OutAssignment {
3373 index: 0,
3374 value: Expr::Ref("osc".to_string()),
3375 span: None,
3376 },
3377 OutAssignment {
3378 index: 1,
3379 value: Expr::Ref("osc".to_string()),
3380 span: None,
3381 },
3382 ],
3383 direct_connections: vec![],
3384 feedback_decls: vec![],
3385 feedback_assignments: vec![],
3386 state_decls: vec![],
3387 state_assignments: vec![],
3388 };
3389
3390 let graph = build_graph(&prog).unwrap();
3391
3392 let cycle_node = graph
3394 .nodes
3395 .iter()
3396 .find(|n| n.object_name == "cycle~")
3397 .unwrap();
3398 let edges_from_cycle: Vec<_> = graph
3399 .edges
3400 .iter()
3401 .filter(|e| e.source_id == cycle_node.id && e.source_outlet == 0)
3402 .collect();
3403 assert_eq!(edges_from_cycle.len(), 2);
3404
3405 assert!(edges_from_cycle[0].order.is_some());
3407 assert!(edges_from_cycle[1].order.is_some());
3408
3409 let mut orders: Vec<u32> = edges_from_cycle.iter().map(|e| e.order.unwrap()).collect();
3411 orders.sort();
3412 assert_eq!(orders, vec![0, 1]);
3413 }
3414
3415 #[test]
3416 fn test_single_edge_no_order() {
3417 let prog = make_l2_program();
3419 let graph = build_graph(&prog).unwrap();
3420
3421 for edge in &graph.edges {
3423 assert_eq!(
3424 edge.order, None,
3425 "single edge from {} outlet {} should have no order",
3426 edge.source_id, edge.source_outlet
3427 );
3428 }
3429 }
3430
3431 #[test]
3434 fn test_classify_purity_signal_pure() {
3435 assert_eq!(classify_purity("cycle~"), NodePurity::Pure);
3436 assert_eq!(classify_purity("*~"), NodePurity::Pure);
3437 assert_eq!(classify_purity("+~"), NodePurity::Pure);
3438 assert_eq!(classify_purity("biquad~"), NodePurity::Pure);
3439 }
3440
3441 #[test]
3442 fn test_classify_purity_signal_stateful() {
3443 assert_eq!(classify_purity("tapin~"), NodePurity::Stateful);
3444 assert_eq!(classify_purity("tapout~"), NodePurity::Stateful);
3445 assert_eq!(classify_purity("line~"), NodePurity::Stateful);
3446 assert_eq!(classify_purity("delay~"), NodePurity::Stateful);
3447 }
3448
3449 #[test]
3450 fn test_classify_purity_control_stateful() {
3451 assert_eq!(classify_purity("pack"), NodePurity::Stateful);
3452 assert_eq!(classify_purity("unpack"), NodePurity::Stateful);
3453 assert_eq!(classify_purity("int"), NodePurity::Stateful);
3454 assert_eq!(classify_purity("float"), NodePurity::Stateful);
3455 assert_eq!(classify_purity("toggle"), NodePurity::Stateful);
3456 assert_eq!(classify_purity("gate"), NodePurity::Stateful);
3457 assert_eq!(classify_purity("counter"), NodePurity::Stateful);
3458 assert_eq!(classify_purity("coll"), NodePurity::Stateful);
3459 assert_eq!(classify_purity("dict"), NodePurity::Stateful);
3460 }
3461
3462 #[test]
3463 fn test_classify_purity_control_pure() {
3464 assert_eq!(classify_purity("+"), NodePurity::Pure);
3465 assert_eq!(classify_purity("-"), NodePurity::Pure);
3466 assert_eq!(classify_purity("*"), NodePurity::Pure);
3467 assert_eq!(classify_purity("/"), NodePurity::Pure);
3468 assert_eq!(classify_purity("trigger"), NodePurity::Pure);
3469 assert_eq!(classify_purity("t"), NodePurity::Pure);
3470 assert_eq!(classify_purity("route"), NodePurity::Pure);
3471 assert_eq!(classify_purity("select"), NodePurity::Pure);
3472 assert_eq!(classify_purity("prepend"), NodePurity::Pure);
3473 }
3474
3475 #[test]
3476 fn test_classify_purity_unknown() {
3477 assert_eq!(classify_purity("my_custom_object"), NodePurity::Unknown);
3478 assert_eq!(classify_purity("some_abstraction"), NodePurity::Unknown);
3479 }
3480
3481 #[test]
3484 fn test_default_hot_inlets_standard() {
3485 let hot = default_hot_inlets("cycle~", 2);
3487 assert_eq!(hot, vec![true, false]);
3488 }
3489
3490 #[test]
3491 fn test_default_hot_inlets_single() {
3492 let hot = default_hot_inlets("print", 1);
3493 assert_eq!(hot, vec![true]);
3494 }
3495
3496 #[test]
3497 fn test_default_hot_inlets_none() {
3498 let hot = default_hot_inlets("inlet", 0);
3499 assert!(hot.is_empty());
3500 }
3501
3502 #[test]
3503 fn test_default_hot_inlets_many() {
3504 let hot = default_hot_inlets("biquad~", 6);
3505 assert_eq!(hot, vec![true, false, false, false, false, false]);
3506 }
3507
3508 #[test]
3511 fn test_built_node_has_purity() {
3512 let prog = make_l2_program();
3513 let graph = build_graph(&prog).unwrap();
3514
3515 let cycle_node = graph
3516 .nodes
3517 .iter()
3518 .find(|n| n.object_name == "cycle~")
3519 .unwrap();
3520 assert_eq!(cycle_node.purity, NodePurity::Pure);
3521
3522 let mul_node = graph.nodes.iter().find(|n| n.object_name == "*~").unwrap();
3523 assert_eq!(mul_node.purity, NodePurity::Pure);
3524 }
3525
3526 #[test]
3527 fn test_built_node_has_hot_inlets() {
3528 let prog = make_l2_program();
3529 let graph = build_graph(&prog).unwrap();
3530
3531 let cycle_node = graph
3532 .nodes
3533 .iter()
3534 .find(|n| n.object_name == "cycle~")
3535 .unwrap();
3536 assert_eq!(cycle_node.hot_inlets, vec![true, false]);
3537
3538 let mul_node = graph.nodes.iter().find(|n| n.object_name == "*~").unwrap();
3539 assert_eq!(mul_node.hot_inlets, vec![true, false]);
3540 }
3541
3542 #[test]
3545 fn test_direct_connection_valid_port() {
3546 let prog = Program {
3548 in_decls: vec![],
3549 out_decls: vec![],
3550 wires: vec![
3551 Wire {
3552 name: "src".to_string(),
3553 value: Expr::Call {
3554 object: "button".to_string(),
3555 args: vec![],
3556 },
3557 span: None,
3558 attrs: vec![],
3559 },
3560 Wire {
3561 name: "target".to_string(),
3562 value: Expr::Call {
3563 object: "+".to_string(),
3564 args: vec![
3565 CallArg::positional(Expr::Lit(LitValue::Int(0))),
3566 CallArg::positional(Expr::Lit(LitValue::Int(0))),
3567 ],
3568 },
3569 span: None,
3570 attrs: vec![],
3571 },
3572 ],
3573 destructuring_wires: vec![],
3574 msg_decls: vec![],
3575 out_assignments: vec![],
3576 direct_connections: vec![DirectConnection {
3577 target: flutmax_ast::InputPortAccess {
3578 object: "target".to_string(),
3579 index: 0,
3580 },
3581 value: Expr::Ref("src".to_string()),
3582 }],
3583 feedback_decls: vec![],
3584 feedback_assignments: vec![],
3585 state_decls: vec![],
3586 state_assignments: vec![],
3587 };
3588
3589 let result = build_graph(&prog);
3590 assert!(result.is_ok(), "valid port index should succeed");
3591 }
3592
3593 #[test]
3594 fn test_direct_connection_invalid_port_index() {
3595 let prog = Program {
3597 in_decls: vec![],
3598 out_decls: vec![],
3599 wires: vec![
3600 Wire {
3601 name: "src".to_string(),
3602 value: Expr::Call {
3603 object: "button".to_string(),
3604 args: vec![],
3605 },
3606 span: None,
3607 attrs: vec![],
3608 },
3609 Wire {
3610 name: "target".to_string(),
3611 value: Expr::Call {
3612 object: "+".to_string(),
3613 args: vec![
3614 CallArg::positional(Expr::Lit(LitValue::Int(0))),
3615 CallArg::positional(Expr::Lit(LitValue::Int(0))),
3616 ],
3617 },
3618 span: None,
3619 attrs: vec![],
3620 },
3621 ],
3622 destructuring_wires: vec![],
3623 msg_decls: vec![],
3624 out_assignments: vec![],
3625 direct_connections: vec![DirectConnection {
3626 target: flutmax_ast::InputPortAccess {
3627 object: "target".to_string(),
3628 index: 99,
3629 },
3630 value: Expr::Ref("src".to_string()),
3631 }],
3632 feedback_decls: vec![],
3633 feedback_assignments: vec![],
3634 state_decls: vec![],
3635 state_assignments: vec![],
3636 };
3637
3638 let result = build_graph(&prog);
3641 assert!(result.is_ok());
3642 let graph = result.unwrap();
3643 let target_node = graph
3644 .find_node("target_id_0")
3645 .or_else(|| graph.nodes.iter().find(|n| n.object_name == "+"));
3646 assert!(target_node.is_some());
3647 assert!(target_node.unwrap().num_inlets >= 100);
3649 }
3650
3651 #[test]
3652 fn test_direct_connection_undefined_node() {
3653 let prog = Program {
3655 in_decls: vec![],
3656 out_decls: vec![],
3657 wires: vec![Wire {
3658 name: "src".to_string(),
3659 value: Expr::Call {
3660 object: "button".to_string(),
3661 args: vec![],
3662 },
3663 span: None,
3664 attrs: vec![],
3665 }],
3666 destructuring_wires: vec![],
3667 msg_decls: vec![],
3668 out_assignments: vec![],
3669 direct_connections: vec![DirectConnection {
3670 target: flutmax_ast::InputPortAccess {
3671 object: "nonexistent".to_string(),
3672 index: 0,
3673 },
3674 value: Expr::Ref("src".to_string()),
3675 }],
3676 feedback_decls: vec![],
3677 feedback_assignments: vec![],
3678 state_decls: vec![],
3679 state_assignments: vec![],
3680 };
3681
3682 let result = build_graph(&prog);
3683 assert!(result.is_err());
3684 match result.unwrap_err() {
3685 BuildError::UndefinedRef(name) => assert_eq!(name, "nonexistent"),
3686 other => panic!("expected UndefinedRef, got: {:?}", other),
3687 }
3688 }
3689
3690 #[test]
3693 fn test_typed_pack_int_literals() {
3694 let prog = Program {
3696 in_decls: vec![],
3697 out_decls: vec![],
3698 wires: vec![Wire {
3699 name: "t".to_string(),
3700 value: Expr::Tuple(vec![
3701 Expr::Lit(LitValue::Int(1)),
3702 Expr::Lit(LitValue::Int(2)),
3703 Expr::Lit(LitValue::Int(3)),
3704 ]),
3705 span: None,
3706 attrs: vec![],
3707 }],
3708 destructuring_wires: vec![],
3709 msg_decls: vec![],
3710 out_assignments: vec![],
3711 direct_connections: vec![],
3712 feedback_decls: vec![],
3713 feedback_assignments: vec![],
3714 state_decls: vec![],
3715 state_assignments: vec![],
3716 };
3717
3718 let graph = build_graph(&prog).unwrap();
3719 let pack_node = graph
3720 .nodes
3721 .iter()
3722 .find(|n| n.object_name == "pack")
3723 .expect("pack node should exist");
3724 assert_eq!(pack_node.args, vec!["i", "i", "i"]);
3725 }
3726
3727 #[test]
3728 fn test_typed_pack_mixed_literals() {
3729 let prog = Program {
3731 in_decls: vec![],
3732 out_decls: vec![],
3733 wires: vec![Wire {
3734 name: "t".to_string(),
3735 value: Expr::Tuple(vec![
3736 Expr::Lit(LitValue::Int(1)),
3737 Expr::Lit(LitValue::Float(0.5)),
3738 Expr::Lit(LitValue::Str("x".to_string())),
3739 ]),
3740 span: None,
3741 attrs: vec![],
3742 }],
3743 destructuring_wires: vec![],
3744 msg_decls: vec![],
3745 out_assignments: vec![],
3746 direct_connections: vec![],
3747 feedback_decls: vec![],
3748 feedback_assignments: vec![],
3749 state_decls: vec![],
3750 state_assignments: vec![],
3751 };
3752
3753 let graph = build_graph(&prog).unwrap();
3754 let pack_node = graph
3755 .nodes
3756 .iter()
3757 .find(|n| n.object_name == "pack")
3758 .expect("pack node should exist");
3759 assert_eq!(pack_node.args, vec!["i", "f", "s"]);
3760 }
3761
3762 #[test]
3763 fn test_typed_pack_ref_fallback() {
3764 let prog = Program {
3766 in_decls: vec![
3767 InDecl {
3768 index: 0,
3769 name: "x".to_string(),
3770 port_type: PortType::Float,
3771 },
3772 InDecl {
3773 index: 1,
3774 name: "y".to_string(),
3775 port_type: PortType::Float,
3776 },
3777 ],
3778 out_decls: vec![],
3779 wires: vec![Wire {
3780 name: "t".to_string(),
3781 value: Expr::Tuple(vec![Expr::Ref("x".to_string()), Expr::Ref("y".to_string())]),
3782 span: None,
3783 attrs: vec![],
3784 }],
3785 destructuring_wires: vec![],
3786 msg_decls: vec![],
3787 out_assignments: vec![],
3788 direct_connections: vec![],
3789 feedback_decls: vec![],
3790 feedback_assignments: vec![],
3791 state_decls: vec![],
3792 state_assignments: vec![],
3793 };
3794
3795 let graph = build_graph(&prog).unwrap();
3796 let pack_node = graph
3797 .nodes
3798 .iter()
3799 .find(|n| n.object_name == "pack")
3800 .expect("pack node should exist");
3801 assert_eq!(pack_node.args, vec!["f", "f"]);
3802 }
3803
3804 #[test]
3807 fn test_bare_multi_outlet_ref_ok() {
3808 let prog = Program {
3810 in_decls: vec![InDecl {
3811 index: 0,
3812 name: "arg0".to_string(),
3813 port_type: PortType::Signal,
3814 }],
3815 out_decls: vec![OutDecl {
3816 index: 0,
3817 name: "out".to_string(),
3818 port_type: PortType::Signal,
3819 value: None,
3820 }],
3821 wires: vec![Wire {
3822 name: "result".to_string(),
3823 value: Expr::Call {
3824 object: "line~".to_string(),
3825 args: vec![CallArg::positional(Expr::Ref("arg0".to_string()))],
3826 },
3827 span: None,
3828 attrs: vec![],
3829 }],
3830 destructuring_wires: vec![],
3831 msg_decls: vec![],
3832 out_assignments: vec![OutAssignment {
3833 index: 0,
3834 value: Expr::Ref("result".to_string()),
3835 span: None,
3836 }],
3837 direct_connections: vec![],
3838 feedback_decls: vec![],
3839 feedback_assignments: vec![],
3840 state_decls: vec![],
3841 state_assignments: vec![],
3842 };
3843
3844 let result = build_graph(&prog);
3845 assert!(
3846 result.is_ok(),
3847 "bare reference to multi-outlet node should be OK"
3848 );
3849 }
3850
3851 #[test]
3852 fn test_e020_output_port_access_ok() {
3853 use flutmax_ast::OutputPortAccess;
3855
3856 let prog = Program {
3857 in_decls: vec![InDecl {
3858 index: 0,
3859 name: "arg0".to_string(),
3860 port_type: PortType::Signal,
3861 }],
3862 out_decls: vec![OutDecl {
3863 index: 0,
3864 name: "out".to_string(),
3865 port_type: PortType::Signal,
3866 value: None,
3867 }],
3868 wires: vec![Wire {
3869 name: "result".to_string(),
3870 value: Expr::Call {
3871 object: "line~".to_string(),
3872 args: vec![CallArg::positional(Expr::Ref("arg0".to_string()))],
3873 },
3874 span: None,
3875 attrs: vec![],
3876 }],
3877 destructuring_wires: vec![],
3878 msg_decls: vec![],
3879 out_assignments: vec![OutAssignment {
3880 index: 0,
3881 value: Expr::OutputPortAccess(OutputPortAccess {
3882 object: "result".to_string(),
3883 index: 0,
3884 }),
3885 span: None,
3886 }],
3887 direct_connections: vec![],
3888 feedback_decls: vec![],
3889 feedback_assignments: vec![],
3890 state_decls: vec![],
3891 state_assignments: vec![],
3892 };
3893
3894 let result = build_graph(&prog);
3895 assert!(
3896 result.is_ok(),
3897 "OutputPortAccess should bypass E020: {:?}",
3898 result.err()
3899 );
3900 }
3901
3902 #[test]
3903 fn test_e020_destructured_names_exempt() {
3904 use flutmax_ast::DestructuringWire;
3906
3907 let prog = Program {
3908 in_decls: vec![InDecl {
3909 index: 0,
3910 name: "data".to_string(),
3911 port_type: PortType::Float,
3912 }],
3913 out_decls: vec![OutDecl {
3914 index: 0,
3915 name: "x".to_string(),
3916 port_type: PortType::Float,
3917 value: None,
3918 }],
3919 wires: vec![],
3920 destructuring_wires: vec![DestructuringWire {
3921 names: vec!["a".to_string(), "b".to_string()],
3922 value: Expr::Call {
3923 object: "unpack".to_string(),
3924 args: vec![CallArg::positional(Expr::Ref("data".to_string()))],
3925 },
3926 span: None,
3927 }],
3928 msg_decls: vec![],
3929 out_assignments: vec![OutAssignment {
3930 index: 0,
3931 value: Expr::Ref("a".to_string()),
3932 span: None,
3933 }],
3934 direct_connections: vec![],
3935 feedback_decls: vec![],
3936 feedback_assignments: vec![],
3937 state_decls: vec![],
3938 state_assignments: vec![],
3939 };
3940
3941 let result = build_graph(&prog);
3942 assert!(
3943 result.is_ok(),
3944 "destructured name should not trigger E020: {:?}",
3945 result.err()
3946 );
3947 }
3948
3949 #[test]
3950 fn test_single_outlet_bare_ref_ok() {
3951 let prog = Program {
3953 in_decls: vec![],
3954 out_decls: vec![OutDecl {
3955 index: 0,
3956 name: "out".to_string(),
3957 port_type: PortType::Signal,
3958 value: None,
3959 }],
3960 wires: vec![Wire {
3961 name: "osc".to_string(),
3962 value: Expr::Call {
3963 object: "cycle~".to_string(),
3964 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
3965 },
3966 span: None,
3967 attrs: vec![],
3968 }],
3969 destructuring_wires: vec![],
3970 msg_decls: vec![],
3971 out_assignments: vec![OutAssignment {
3972 index: 0,
3973 value: Expr::Ref("osc".to_string()),
3974 span: None,
3975 }],
3976 direct_connections: vec![],
3977 feedback_decls: vec![],
3978 feedback_assignments: vec![],
3979 state_decls: vec![],
3980 state_assignments: vec![],
3981 };
3982
3983 let result = build_graph(&prog);
3984 assert!(
3985 result.is_ok(),
3986 "single outlet bare ref should be OK: {:?}",
3987 result.err()
3988 );
3989 }
3990
3991 #[test]
3994 fn test_state_decl_creates_int_node() {
3995 let prog = Program {
3997 in_decls: vec![],
3998 out_decls: vec![],
3999 wires: vec![],
4000 destructuring_wires: vec![],
4001 msg_decls: vec![],
4002 out_assignments: vec![],
4003 direct_connections: vec![],
4004 feedback_decls: vec![],
4005 feedback_assignments: vec![],
4006 state_decls: vec![StateDecl {
4007 name: "counter".to_string(),
4008 port_type: PortType::Int,
4009 init_value: Expr::Lit(LitValue::Int(0)),
4010 span: None,
4011 }],
4012 state_assignments: vec![],
4013 };
4014
4015 let graph = build_graph(&prog).unwrap();
4016
4017 assert_eq!(graph.nodes.len(), 1);
4018 let node = &graph.nodes[0];
4019 assert_eq!(node.object_name, "int");
4020 assert_eq!(node.args, vec!["0"]);
4021 assert_eq!(node.num_inlets, 2);
4022 assert_eq!(node.num_outlets, 1);
4023 assert!(!node.is_signal);
4024 assert_eq!(node.varname, Some("counter".to_string()));
4025 assert_eq!(node.hot_inlets, vec![true, false]);
4027 }
4028
4029 #[test]
4030 fn test_state_decl_creates_float_node() {
4031 let prog = Program {
4033 in_decls: vec![],
4034 out_decls: vec![],
4035 wires: vec![],
4036 destructuring_wires: vec![],
4037 msg_decls: vec![],
4038 out_assignments: vec![],
4039 direct_connections: vec![],
4040 feedback_decls: vec![],
4041 feedback_assignments: vec![],
4042 state_decls: vec![StateDecl {
4043 name: "volume".to_string(),
4044 port_type: PortType::Float,
4045 init_value: Expr::Lit(LitValue::Float(0.5)),
4046 span: None,
4047 }],
4048 state_assignments: vec![],
4049 };
4050
4051 let graph = build_graph(&prog).unwrap();
4052
4053 assert_eq!(graph.nodes.len(), 1);
4054 let node = &graph.nodes[0];
4055 assert_eq!(node.object_name, "float");
4056 assert_eq!(node.args, vec!["0.5"]);
4057 assert_eq!(node.varname, Some("volume".to_string()));
4058 }
4059
4060 #[test]
4061 fn test_state_assignment_connects_to_cold_inlet() {
4062 let prog = Program {
4067 in_decls: vec![],
4068 out_decls: vec![],
4069 wires: vec![Wire {
4070 name: "next".to_string(),
4071 value: Expr::Call {
4072 object: "add".to_string(),
4073 args: vec![
4074 CallArg::positional(Expr::Ref("counter".to_string())),
4075 CallArg::positional(Expr::Lit(LitValue::Int(1))),
4076 ],
4077 },
4078 span: None,
4079 attrs: vec![],
4080 }],
4081 destructuring_wires: vec![],
4082 msg_decls: vec![],
4083 out_assignments: vec![],
4084 direct_connections: vec![],
4085 feedback_decls: vec![],
4086 feedback_assignments: vec![],
4087 state_decls: vec![StateDecl {
4088 name: "counter".to_string(),
4089 port_type: PortType::Int,
4090 init_value: Expr::Lit(LitValue::Int(0)),
4091 span: None,
4092 }],
4093 state_assignments: vec![StateAssignment {
4094 name: "counter".to_string(),
4095 value: Expr::Ref("next".to_string()),
4096 span: None,
4097 }],
4098 };
4099
4100 let graph = build_graph(&prog).unwrap();
4101
4102 let int_node = graph
4104 .nodes
4105 .iter()
4106 .find(|n| n.object_name == "int")
4107 .expect("int node should exist");
4108
4109 let add_node = graph
4111 .nodes
4112 .iter()
4113 .find(|n| n.object_name == "+")
4114 .expect("add node should exist");
4115
4116 let edge = graph
4118 .edges
4119 .iter()
4120 .find(|e| e.source_id == add_node.id && e.dest_id == int_node.id)
4121 .expect("edge from add to int should exist");
4122 assert_eq!(
4123 edge.dest_inlet, 1,
4124 "state assignment should connect to cold inlet (1)"
4125 );
4126 }
4127
4128 #[test]
4129 fn test_state_ref_in_wire_expression() {
4130 let prog = Program {
4134 in_decls: vec![],
4135 out_decls: vec![],
4136 wires: vec![Wire {
4137 name: "next".to_string(),
4138 value: Expr::Call {
4139 object: "add".to_string(),
4140 args: vec![
4141 CallArg::positional(Expr::Ref("counter".to_string())),
4142 CallArg::positional(Expr::Lit(LitValue::Int(1))),
4143 ],
4144 },
4145 span: None,
4146 attrs: vec![],
4147 }],
4148 destructuring_wires: vec![],
4149 msg_decls: vec![],
4150 out_assignments: vec![],
4151 direct_connections: vec![],
4152 feedback_decls: vec![],
4153 feedback_assignments: vec![],
4154 state_decls: vec![StateDecl {
4155 name: "counter".to_string(),
4156 port_type: PortType::Int,
4157 init_value: Expr::Lit(LitValue::Int(0)),
4158 span: None,
4159 }],
4160 state_assignments: vec![],
4161 };
4162
4163 let graph = build_graph(&prog).unwrap();
4164
4165 let int_node = graph
4166 .nodes
4167 .iter()
4168 .find(|n| n.object_name == "int")
4169 .expect("int node should exist");
4170 let add_node = graph
4171 .nodes
4172 .iter()
4173 .find(|n| n.object_name == "+")
4174 .expect("add node should exist");
4175
4176 let edge = graph
4178 .edges
4179 .iter()
4180 .find(|e| e.source_id == int_node.id && e.dest_id == add_node.id)
4181 .expect("edge from int to add should exist");
4182 assert_eq!(edge.source_outlet, 0);
4183 assert_eq!(edge.dest_inlet, 0);
4184 }
4185
4186 #[test]
4187 fn test_e019_duplicate_state_assignment() {
4188 let prog = Program {
4192 in_decls: vec![],
4193 out_decls: vec![],
4194 wires: vec![
4195 Wire {
4196 name: "a".to_string(),
4197 value: Expr::Call {
4198 object: "button".to_string(),
4199 args: vec![],
4200 },
4201 span: None,
4202 attrs: vec![],
4203 },
4204 Wire {
4205 name: "b".to_string(),
4206 value: Expr::Call {
4207 object: "button".to_string(),
4208 args: vec![],
4209 },
4210 span: None,
4211 attrs: vec![],
4212 },
4213 ],
4214 destructuring_wires: vec![],
4215 msg_decls: vec![],
4216 out_assignments: vec![],
4217 direct_connections: vec![],
4218 feedback_decls: vec![],
4219 feedback_assignments: vec![],
4220 state_decls: vec![StateDecl {
4221 name: "counter".to_string(),
4222 port_type: PortType::Int,
4223 init_value: Expr::Lit(LitValue::Int(0)),
4224 span: None,
4225 }],
4226 state_assignments: vec![
4227 StateAssignment {
4228 name: "counter".to_string(),
4229 value: Expr::Ref("a".to_string()),
4230 span: None,
4231 },
4232 StateAssignment {
4233 name: "counter".to_string(),
4234 value: Expr::Ref("b".to_string()),
4235 span: None,
4236 },
4237 ],
4238 };
4239
4240 let result = build_graph(&prog);
4241 assert!(result.is_err());
4242 match result.unwrap_err() {
4243 BuildError::DuplicateStateAssignment(name) => assert_eq!(name, "counter"),
4244 other => panic!("expected DuplicateStateAssignment, got {:?}", other),
4245 }
4246 }
4247
4248 #[test]
4249 fn test_state_single_assignment_no_error() {
4250 let prog = Program {
4252 in_decls: vec![],
4253 out_decls: vec![],
4254 wires: vec![Wire {
4255 name: "val".to_string(),
4256 value: Expr::Call {
4257 object: "button".to_string(),
4258 args: vec![],
4259 },
4260 span: None,
4261 attrs: vec![],
4262 }],
4263 destructuring_wires: vec![],
4264 msg_decls: vec![],
4265 out_assignments: vec![],
4266 direct_connections: vec![],
4267 feedback_decls: vec![],
4268 feedback_assignments: vec![],
4269 state_decls: vec![StateDecl {
4270 name: "counter".to_string(),
4271 port_type: PortType::Int,
4272 init_value: Expr::Lit(LitValue::Int(0)),
4273 span: None,
4274 }],
4275 state_assignments: vec![StateAssignment {
4276 name: "counter".to_string(),
4277 value: Expr::Ref("val".to_string()),
4278 span: None,
4279 }],
4280 };
4281
4282 let result = build_graph(&prog);
4283 assert!(result.is_ok());
4284 }
4285
4286 #[test]
4289 fn test_typed_unpack_from_int_tuple() {
4290 use flutmax_ast::DestructuringWire;
4293
4294 let prog = Program {
4295 in_decls: vec![],
4296 out_decls: vec![],
4297 wires: vec![Wire {
4298 name: "t".to_string(),
4299 value: Expr::Tuple(vec![
4300 Expr::Lit(LitValue::Int(1)),
4301 Expr::Lit(LitValue::Int(2)),
4302 Expr::Lit(LitValue::Int(3)),
4303 ]),
4304 span: None,
4305 attrs: vec![],
4306 }],
4307 destructuring_wires: vec![DestructuringWire {
4308 names: vec!["a".to_string(), "b".to_string(), "c".to_string()],
4309 value: Expr::Ref("t".to_string()),
4310 span: None,
4311 }],
4312 msg_decls: vec![],
4313 out_assignments: vec![],
4314 direct_connections: vec![],
4315 feedback_decls: vec![],
4316 feedback_assignments: vec![],
4317 state_decls: vec![],
4318 state_assignments: vec![],
4319 };
4320
4321 let graph = build_graph(&prog).unwrap();
4322 let unpack_node = graph
4323 .nodes
4324 .iter()
4325 .find(|n| n.object_name == "unpack")
4326 .expect("unpack node should be auto-inserted");
4327 assert_eq!(unpack_node.args, vec!["i", "i", "i"]);
4328 }
4329
4330 #[test]
4331 fn test_typed_unpack_from_mixed_tuple() {
4332 use flutmax_ast::DestructuringWire;
4335
4336 let prog = Program {
4337 in_decls: vec![],
4338 out_decls: vec![],
4339 wires: vec![Wire {
4340 name: "t".to_string(),
4341 value: Expr::Tuple(vec![
4342 Expr::Lit(LitValue::Int(1)),
4343 Expr::Lit(LitValue::Float(0.5)),
4344 Expr::Lit(LitValue::Str("x".to_string())),
4345 ]),
4346 span: None,
4347 attrs: vec![],
4348 }],
4349 destructuring_wires: vec![DestructuringWire {
4350 names: vec!["a".to_string(), "b".to_string(), "c".to_string()],
4351 value: Expr::Ref("t".to_string()),
4352 span: None,
4353 }],
4354 msg_decls: vec![],
4355 out_assignments: vec![],
4356 direct_connections: vec![],
4357 feedback_decls: vec![],
4358 feedback_assignments: vec![],
4359 state_decls: vec![],
4360 state_assignments: vec![],
4361 };
4362
4363 let graph = build_graph(&prog).unwrap();
4364 let unpack_node = graph
4365 .nodes
4366 .iter()
4367 .find(|n| n.object_name == "unpack")
4368 .expect("unpack node should be auto-inserted");
4369 assert_eq!(unpack_node.args, vec!["i", "f", "s"]);
4370 }
4371
4372 #[test]
4373 fn test_typed_unpack_unknown_source_fallback() {
4374 use flutmax_ast::DestructuringWire;
4377
4378 let prog = Program {
4379 in_decls: vec![InDecl {
4380 index: 0,
4381 name: "data".to_string(),
4382 port_type: PortType::Float,
4383 }],
4384 out_decls: vec![],
4385 wires: vec![],
4386 destructuring_wires: vec![DestructuringWire {
4387 names: vec!["a".to_string(), "b".to_string()],
4388 value: Expr::Call {
4389 object: "unpack".to_string(),
4390 args: vec![CallArg::positional(Expr::Ref("data".to_string()))],
4391 },
4392 span: None,
4393 }],
4394 msg_decls: vec![],
4395 out_assignments: vec![],
4396 direct_connections: vec![],
4397 feedback_decls: vec![],
4398 feedback_assignments: vec![],
4399 state_decls: vec![],
4400 state_assignments: vec![],
4401 };
4402
4403 let graph = build_graph(&prog).unwrap();
4404 let unpack_nodes: Vec<_> = graph
4405 .nodes
4406 .iter()
4407 .filter(|n| n.object_name == "unpack")
4408 .collect();
4409 assert_eq!(unpack_nodes.len(), 1);
4410 }
4414
4415 #[test]
4416 fn test_typed_unpack_ref_to_tuple_with_refs() {
4417 use flutmax_ast::DestructuringWire;
4420
4421 let prog = Program {
4422 in_decls: vec![
4423 InDecl {
4424 index: 0,
4425 name: "x".to_string(),
4426 port_type: PortType::Float,
4427 },
4428 InDecl {
4429 index: 1,
4430 name: "y".to_string(),
4431 port_type: PortType::Float,
4432 },
4433 ],
4434 out_decls: vec![],
4435 wires: vec![Wire {
4436 name: "t".to_string(),
4437 value: Expr::Tuple(vec![Expr::Ref("x".to_string()), Expr::Ref("y".to_string())]),
4438 span: None,
4439 attrs: vec![],
4440 }],
4441 destructuring_wires: vec![DestructuringWire {
4442 names: vec!["a".to_string(), "b".to_string()],
4443 value: Expr::Ref("t".to_string()),
4444 span: None,
4445 }],
4446 msg_decls: vec![],
4447 out_assignments: vec![],
4448 direct_connections: vec![],
4449 feedback_decls: vec![],
4450 feedback_assignments: vec![],
4451 state_decls: vec![],
4452 state_assignments: vec![],
4453 };
4454
4455 let graph = build_graph(&prog).unwrap();
4456 let unpack_node = graph
4457 .nodes
4458 .iter()
4459 .find(|n| n.object_name == "unpack")
4460 .expect("unpack node should be auto-inserted");
4461 assert_eq!(unpack_node.args, vec!["f", "f"]);
4462 }
4463
4464 #[test]
4469 fn test_w001_duplicate_inlet_detected() {
4470 let prog = Program {
4472 in_decls: vec![],
4473 out_decls: vec![],
4474 wires: vec![
4475 Wire {
4476 name: "a".to_string(),
4477 value: Expr::Call {
4478 object: "button".to_string(),
4479 args: vec![],
4480 },
4481 span: None,
4482 attrs: vec![],
4483 },
4484 Wire {
4485 name: "b".to_string(),
4486 value: Expr::Call {
4487 object: "button".to_string(),
4488 args: vec![],
4489 },
4490 span: None,
4491 attrs: vec![],
4492 },
4493 Wire {
4494 name: "target".to_string(),
4495 value: Expr::Call {
4496 object: "+".to_string(),
4497 args: vec![
4498 CallArg::positional(Expr::Lit(LitValue::Int(0))),
4499 CallArg::positional(Expr::Lit(LitValue::Int(0))),
4500 ],
4501 },
4502 span: None,
4503 attrs: vec![],
4504 },
4505 ],
4506 destructuring_wires: vec![],
4507 msg_decls: vec![],
4508 out_assignments: vec![],
4509 direct_connections: vec![
4510 DirectConnection {
4511 target: flutmax_ast::InputPortAccess {
4512 object: "target".to_string(),
4513 index: 0,
4514 },
4515 value: Expr::Ref("a".to_string()),
4516 },
4517 DirectConnection {
4518 target: flutmax_ast::InputPortAccess {
4519 object: "target".to_string(),
4520 index: 0,
4521 },
4522 value: Expr::Ref("b".to_string()),
4523 },
4524 ],
4525 feedback_decls: vec![],
4526 feedback_assignments: vec![],
4527 state_decls: vec![],
4528 state_assignments: vec![],
4529 };
4530
4531 let result = build_graph_with_warnings(&prog).unwrap();
4532 assert_eq!(result.warnings.len(), 1);
4533 match &result.warnings[0] {
4534 BuildWarning::DuplicateInletConnection {
4535 node_id: _,
4536 inlet,
4537 count,
4538 } => {
4539 assert_eq!(*inlet, 0);
4540 assert_eq!(*count, 2);
4541 }
4542 }
4543 }
4544
4545 #[test]
4546 fn test_w001_no_warning_single_connection() {
4547 let prog = Program {
4549 in_decls: vec![],
4550 out_decls: vec![],
4551 wires: vec![
4552 Wire {
4553 name: "a".to_string(),
4554 value: Expr::Call {
4555 object: "button".to_string(),
4556 args: vec![],
4557 },
4558 span: None,
4559 attrs: vec![],
4560 },
4561 Wire {
4562 name: "target".to_string(),
4563 value: Expr::Call {
4564 object: "+".to_string(),
4565 args: vec![
4566 CallArg::positional(Expr::Lit(LitValue::Int(0))),
4567 CallArg::positional(Expr::Lit(LitValue::Int(0))),
4568 ],
4569 },
4570 span: None,
4571 attrs: vec![],
4572 },
4573 ],
4574 destructuring_wires: vec![],
4575 msg_decls: vec![],
4576 out_assignments: vec![],
4577 direct_connections: vec![DirectConnection {
4578 target: flutmax_ast::InputPortAccess {
4579 object: "target".to_string(),
4580 index: 1,
4581 },
4582 value: Expr::Ref("a".to_string()),
4583 }],
4584 feedback_decls: vec![],
4585 feedback_assignments: vec![],
4586 state_decls: vec![],
4587 state_assignments: vec![],
4588 };
4589
4590 let result = build_graph_with_warnings(&prog).unwrap();
4591 assert!(
4592 result.warnings.is_empty(),
4593 "single connections should not trigger W001"
4594 );
4595 }
4596
4597 #[test]
4598 fn test_w001_display_format() {
4599 let warning = BuildWarning::DuplicateInletConnection {
4600 node_id: "obj-3".to_string(),
4601 inlet: 0,
4602 count: 2,
4603 };
4604 assert_eq!(format!("{}", warning), "W001: 2 connections to obj-3.in[0]");
4605 }
4606
4607 #[test]
4610 fn test_msg_creates_message_node() {
4611 let prog = Program {
4612 in_decls: vec![],
4613 out_decls: vec![OutDecl {
4614 index: 0,
4615 name: "output".to_string(),
4616 port_type: PortType::Bang,
4617 value: None,
4618 }],
4619 wires: vec![],
4620 destructuring_wires: vec![],
4621 msg_decls: vec![MsgDecl {
4622 name: "click".to_string(),
4623 content: "bang".to_string(),
4624 span: None,
4625 attrs: vec![],
4626 }],
4627 out_assignments: vec![OutAssignment {
4628 index: 0,
4629 value: Expr::Ref("click".to_string()),
4630 span: None,
4631 }],
4632 direct_connections: vec![],
4633 feedback_decls: vec![],
4634 feedback_assignments: vec![],
4635 state_decls: vec![],
4636 state_assignments: vec![],
4637 };
4638
4639 let graph = build_graph(&prog).unwrap();
4640
4641 let msg_node = graph
4643 .nodes
4644 .iter()
4645 .find(|n| n.object_name == "message")
4646 .expect("should have a message node");
4647
4648 assert_eq!(msg_node.args, vec!["bang"]);
4649 assert_eq!(msg_node.num_inlets, 2);
4650 assert_eq!(msg_node.num_outlets, 1);
4651 assert!(!msg_node.is_signal);
4652 assert_eq!(msg_node.varname, Some("click".to_string()));
4653 }
4654
4655 #[test]
4656 fn test_msg_connectable_as_source() {
4657 let prog = Program {
4658 in_decls: vec![],
4659 out_decls: vec![],
4660 wires: vec![Wire {
4661 name: "printer".to_string(),
4662 value: Expr::Call {
4663 object: "print".to_string(),
4664 args: vec![CallArg::positional(Expr::Ref("click".to_string()))],
4665 },
4666 span: None,
4667 attrs: vec![],
4668 }],
4669 destructuring_wires: vec![],
4670 msg_decls: vec![MsgDecl {
4671 name: "click".to_string(),
4672 content: "bang".to_string(),
4673 span: None,
4674 attrs: vec![],
4675 }],
4676 out_assignments: vec![],
4677 direct_connections: vec![],
4678 feedback_decls: vec![],
4679 feedback_assignments: vec![],
4680 state_decls: vec![],
4681 state_assignments: vec![],
4682 };
4683
4684 let graph = build_graph(&prog).unwrap();
4685
4686 assert!(!graph.edges.is_empty(), "should have at least one edge");
4688 let msg_node = graph
4689 .nodes
4690 .iter()
4691 .find(|n| n.object_name == "message")
4692 .expect("message node");
4693 let print_node = graph
4694 .nodes
4695 .iter()
4696 .find(|n| n.object_name == "print")
4697 .expect("print node");
4698
4699 let edge = graph
4700 .edges
4701 .iter()
4702 .find(|e| e.source_id == msg_node.id && e.dest_id == print_node.id)
4703 .expect("edge from message to print");
4704 assert_eq!(edge.source_outlet, 0);
4705 assert_eq!(edge.dest_inlet, 0);
4706 }
4707
4708 #[test]
4711 fn test_dotted_object_name_in_call() {
4712 let prog = Program {
4713 in_decls: vec![],
4714 out_decls: vec![OutDecl {
4715 index: 0,
4716 name: "output".to_string(),
4717 port_type: PortType::Float,
4718 value: None,
4719 }],
4720 wires: vec![Wire {
4721 name: "dial".to_string(),
4722 value: Expr::Call {
4723 object: "live.dial".to_string(),
4724 args: vec![CallArg::positional(Expr::Lit(LitValue::Float(0.5)))],
4725 },
4726 span: None,
4727 attrs: vec![],
4728 }],
4729 destructuring_wires: vec![],
4730 msg_decls: vec![],
4731 out_assignments: vec![OutAssignment {
4732 index: 0,
4733 value: Expr::OutputPortAccess(OutputPortAccess {
4734 object: "dial".to_string(),
4735 index: 0,
4736 }),
4737 span: None,
4738 }],
4739 direct_connections: vec![],
4740 feedback_decls: vec![],
4741 feedback_assignments: vec![],
4742 state_decls: vec![],
4743 state_assignments: vec![],
4744 };
4745
4746 let graph = build_graph(&prog).unwrap();
4747
4748 let dial_node = graph
4750 .nodes
4751 .iter()
4752 .find(|n| n.object_name == "live.dial")
4753 .expect("should have a live.dial node");
4754 assert_eq!(dial_node.args, vec!["0.5"]);
4755 }
4756
4757 #[test]
4762 fn test_wire_attrs_propagated_to_node() {
4763 use flutmax_ast::AttrPair;
4764
4765 let prog = Program {
4766 in_decls: vec![],
4767 out_decls: vec![],
4768 wires: vec![Wire {
4769 name: "w".to_string(),
4770 value: Expr::Call {
4771 object: "flonum".to_string(),
4772 args: vec![],
4773 },
4774 span: None,
4775 attrs: vec![
4776 AttrPair {
4777 key: "minimum".to_string(),
4778 value: flutmax_ast::AttrValue::Float(0.0),
4779 },
4780 AttrPair {
4781 key: "maximum".to_string(),
4782 value: flutmax_ast::AttrValue::Float(100.0),
4783 },
4784 ],
4785 }],
4786 destructuring_wires: vec![],
4787 msg_decls: vec![],
4788 out_assignments: vec![],
4789 direct_connections: vec![],
4790 feedback_decls: vec![],
4791 feedback_assignments: vec![],
4792 state_decls: vec![],
4793 state_assignments: vec![],
4794 };
4795
4796 let graph = build_graph(&prog).unwrap();
4797
4798 let fnum = graph
4799 .nodes
4800 .iter()
4801 .find(|n| n.object_name == "flonum")
4802 .expect("should have a flonum node");
4803
4804 assert_eq!(fnum.attrs.len(), 2);
4805 assert_eq!(fnum.attrs[0], ("minimum".to_string(), "0.".to_string()));
4806 assert_eq!(fnum.attrs[1], ("maximum".to_string(), "100.".to_string()));
4807 }
4808
4809 #[test]
4810 fn test_msg_attrs_propagated_to_node() {
4811 use flutmax_ast::AttrPair;
4812
4813 let prog = Program {
4814 in_decls: vec![],
4815 out_decls: vec![],
4816 wires: vec![],
4817 destructuring_wires: vec![],
4818 msg_decls: vec![MsgDecl {
4819 name: "click".to_string(),
4820 content: "bang".to_string(),
4821 span: None,
4822 attrs: vec![AttrPair {
4823 key: "patching_rect".to_string(),
4824 value: flutmax_ast::AttrValue::Float(100.0),
4825 }],
4826 }],
4827 out_assignments: vec![],
4828 direct_connections: vec![],
4829 feedback_decls: vec![],
4830 feedback_assignments: vec![],
4831 state_decls: vec![],
4832 state_assignments: vec![],
4833 };
4834
4835 let graph = build_graph(&prog).unwrap();
4836
4837 let msg = graph
4838 .nodes
4839 .iter()
4840 .find(|n| n.object_name == "message")
4841 .expect("should have a message node");
4842
4843 assert_eq!(msg.attrs.len(), 1);
4844 assert_eq!(
4845 msg.attrs[0],
4846 ("patching_rect".to_string(), "100.".to_string())
4847 );
4848 }
4849
4850 #[test]
4851 fn test_wire_no_attrs_empty() {
4852 let prog = Program {
4853 in_decls: vec![],
4854 out_decls: vec![],
4855 wires: vec![Wire {
4856 name: "osc".to_string(),
4857 value: Expr::Call {
4858 object: "cycle~".to_string(),
4859 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
4860 },
4861 span: None,
4862 attrs: vec![],
4863 }],
4864 destructuring_wires: vec![],
4865 msg_decls: vec![],
4866 out_assignments: vec![],
4867 direct_connections: vec![],
4868 feedback_decls: vec![],
4869 feedback_assignments: vec![],
4870 state_decls: vec![],
4871 state_assignments: vec![],
4872 };
4873
4874 let graph = build_graph(&prog).unwrap();
4875
4876 let osc = graph
4877 .nodes
4878 .iter()
4879 .find(|n| n.object_name == "cycle~")
4880 .expect("should have a cycle~ node");
4881
4882 assert!(osc.attrs.is_empty());
4883 }
4884
4885 #[test]
4890 fn test_infer_outlets_select_single_arg() {
4891 assert_eq!(infer_num_outlets("select", &["0".to_string()], None), 2);
4893 }
4894
4895 #[test]
4896 fn test_infer_outlets_select_multiple_args() {
4897 assert_eq!(
4899 infer_num_outlets(
4900 "select",
4901 &["1".to_string(), "2".to_string(), "3".to_string()],
4902 None
4903 ),
4904 4
4905 );
4906 }
4907
4908 #[test]
4909 fn test_infer_outlets_sel_alias() {
4910 assert_eq!(infer_num_outlets("sel", &["0".to_string()], None), 2);
4912 }
4913
4914 #[test]
4915 fn test_infer_outlets_select_no_args() {
4916 assert_eq!(infer_num_outlets("select", &[], None), 2);
4918 }
4919
4920 #[test]
4921 fn test_infer_outlets_trigger_two_args() {
4922 assert_eq!(
4924 infer_num_outlets("trigger", &["b".to_string(), "f".to_string()], None),
4925 2
4926 );
4927 }
4928
4929 #[test]
4930 fn test_infer_outlets_trigger_alias() {
4931 assert_eq!(
4933 infer_num_outlets(
4934 "t",
4935 &["b".to_string(), "i".to_string(), "f".to_string()],
4936 None
4937 ),
4938 3
4939 );
4940 }
4941
4942 #[test]
4943 fn test_infer_outlets_function() {
4944 assert_eq!(infer_num_outlets("function", &[], None), 2);
4946 }
4947
4948 #[test]
4949 fn test_infer_outlets_route() {
4950 assert_eq!(
4952 infer_num_outlets(
4953 "route",
4954 &["a".to_string(), "b".to_string(), "c".to_string()],
4955 None
4956 ),
4957 4
4958 );
4959 }
4960
4961 #[test]
4962 fn test_infer_outlets_gate() {
4963 assert_eq!(infer_num_outlets("gate", &["3".to_string()], None), 3);
4965 }
4966
4967 #[test]
4968 fn test_infer_outlets_gate_default() {
4969 assert_eq!(infer_num_outlets("gate", &[], None), 2);
4971 }
4972
4973 #[test]
4974 fn test_infer_outlets_unpack_with_args() {
4975 assert_eq!(
4977 infer_num_outlets(
4978 "unpack",
4979 &["f".to_string(), "f".to_string(), "f".to_string()],
4980 None
4981 ),
4982 3
4983 );
4984 }
4985
4986 #[test]
4987 fn test_infer_outlets_unpack_no_args() {
4988 assert_eq!(infer_num_outlets("unpack", &[], None), 2);
4990 }
4991
4992 #[test]
4993 fn test_infer_outlets_pack() {
4994 assert_eq!(
4996 infer_num_outlets("pack", &["0".to_string(), "0".to_string()], None),
4997 1
4998 );
4999 }
5000
5001 #[test]
5002 fn test_infer_outlets_fixed_objects() {
5003 assert_eq!(infer_num_outlets("line", &[], None), 2);
5005 assert_eq!(infer_num_outlets("makenote", &[], None), 2);
5006 assert_eq!(infer_num_outlets("borax", &[], None), 8);
5007 assert_eq!(infer_num_outlets("counter", &[], None), 4);
5008 assert_eq!(infer_num_outlets("notein", &[], None), 3);
5009 assert_eq!(infer_num_outlets("noteout", &[], None), 0);
5010 assert_eq!(infer_num_outlets("ctlin", &[], None), 3);
5011 assert_eq!(infer_num_outlets("ctlout", &[], None), 0);
5012 assert_eq!(infer_num_outlets("midiin", &[], None), 1);
5013 assert_eq!(infer_num_outlets("midiout", &[], None), 0);
5014 assert_eq!(infer_num_outlets("coll", &[], None), 4);
5015 assert_eq!(infer_num_outlets("urn", &[], None), 2);
5016 assert_eq!(infer_num_outlets("drunk", &[], None), 1);
5017 assert_eq!(infer_num_outlets("random", &[], None), 1);
5018 assert_eq!(infer_num_outlets("match", &[], None), 2);
5019 assert_eq!(infer_num_outlets("zl", &[], None), 2);
5020 assert_eq!(infer_num_outlets("regexp", &[], None), 5);
5021 assert_eq!(infer_num_outlets("sprintf", &[], None), 1);
5022 assert_eq!(infer_num_outlets("thresh", &[], None), 2);
5023 assert_eq!(infer_num_outlets("metro", &[], None), 1);
5024 assert_eq!(infer_num_outlets("delay", &[], None), 1);
5025 assert_eq!(infer_num_outlets("speedlim", &[], None), 1);
5026 }
5027
5028 #[test]
5029 fn test_infer_outlets_signal_objects() {
5030 assert_eq!(infer_num_outlets("dspstate~", &[], None), 4);
5031 assert_eq!(infer_num_outlets("edge~", &[], None), 2);
5032 assert_eq!(infer_num_outlets("fftinfo~", &[], None), 4);
5033 assert_eq!(infer_num_outlets("fftin~", &[], None), 3);
5034 assert_eq!(infer_num_outlets("fftout~", &[], None), 1);
5035 assert_eq!(infer_num_outlets("cartopol~", &[], None), 2);
5036 assert_eq!(infer_num_outlets("poltocar~", &[], None), 2);
5037 assert_eq!(infer_num_outlets("freqshift~", &[], None), 2);
5038 assert_eq!(infer_num_outlets("curve~", &[], None), 2);
5039 assert_eq!(infer_num_outlets("adsr~", &[], None), 4);
5040 assert_eq!(infer_num_outlets("filtercoeff~", &[], None), 5);
5041 assert_eq!(infer_num_outlets("filtergraph~", &[], None), 7);
5042 assert_eq!(infer_num_outlets("noise~", &[], None), 1);
5043 assert_eq!(infer_num_outlets("phasor~", &[], None), 1);
5044 assert_eq!(infer_num_outlets("snapshot~", &[], None), 1);
5045 assert_eq!(infer_num_outlets("peakamp~", &[], None), 1);
5046 assert_eq!(infer_num_outlets("meter~", &[], None), 1);
5047 }
5048
5049 #[test]
5050 fn test_infer_inlets_expanded() {
5051 assert_eq!(infer_num_inlets("function", &[], None), 2);
5053 assert_eq!(infer_num_inlets("counter", &[], None), 3);
5054 assert_eq!(infer_num_inlets("makenote", &[], None), 3);
5055 assert_eq!(infer_num_inlets("line", &[], None), 2);
5056 assert_eq!(infer_num_inlets("metro", &[], None), 2);
5057 assert_eq!(infer_num_inlets("delay", &[], None), 2);
5058 assert_eq!(infer_num_inlets("coll", &[], None), 1);
5059 assert_eq!(infer_num_inlets("urn", &[], None), 2);
5060 assert_eq!(infer_num_inlets("drunk", &[], None), 2);
5061 assert_eq!(infer_num_inlets("random", &[], None), 2);
5062 }
5063
5064 #[test]
5066 fn test_graph_select_outlet_count() {
5067 let prog = Program {
5068 in_decls: vec![],
5069 out_decls: vec![],
5070 wires: vec![Wire {
5071 name: "s".to_string(),
5072 value: Expr::Call {
5073 object: "select".to_string(),
5074 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(0)))],
5075 },
5076 span: None,
5077 attrs: vec![],
5078 }],
5079 destructuring_wires: vec![],
5080 msg_decls: vec![],
5081 out_assignments: vec![],
5082 direct_connections: vec![],
5083 feedback_decls: vec![],
5084 feedback_assignments: vec![],
5085 state_decls: vec![],
5086 state_assignments: vec![],
5087 };
5088
5089 let graph = build_graph(&prog).unwrap();
5090 let sel = graph
5091 .nodes
5092 .iter()
5093 .find(|n| n.object_name == "select")
5094 .expect("should have a select node");
5095
5096 assert_eq!(sel.num_outlets, 2);
5098 }
5099
5100 #[test]
5102 fn test_graph_function_outlet_count() {
5103 let prog = Program {
5104 in_decls: vec![],
5105 out_decls: vec![],
5106 wires: vec![Wire {
5107 name: "f".to_string(),
5108 value: Expr::Call {
5109 object: "function".to_string(),
5110 args: vec![],
5111 },
5112 span: None,
5113 attrs: vec![],
5114 }],
5115 destructuring_wires: vec![],
5116 msg_decls: vec![],
5117 out_assignments: vec![],
5118 direct_connections: vec![],
5119 feedback_decls: vec![],
5120 feedback_assignments: vec![],
5121 state_decls: vec![],
5122 state_assignments: vec![],
5123 };
5124
5125 let graph = build_graph(&prog).unwrap();
5126 let func = graph
5127 .nodes
5128 .iter()
5129 .find(|n| n.object_name == "function")
5130 .expect("should have a function node");
5131
5132 assert_eq!(func.num_outlets, 2);
5133 }
5134
5135 #[test]
5137 fn test_graph_trigger_outlet_count() {
5138 let prog = Program {
5139 in_decls: vec![],
5140 out_decls: vec![],
5141 wires: vec![Wire {
5142 name: "tr".to_string(),
5143 value: Expr::Call {
5144 object: "trigger".to_string(),
5145 args: vec![
5146 CallArg::positional(Expr::Lit(LitValue::Str("b".to_string()))),
5147 CallArg::positional(Expr::Lit(LitValue::Str("f".to_string()))),
5148 ],
5149 },
5150 span: None,
5151 attrs: vec![],
5152 }],
5153 destructuring_wires: vec![],
5154 msg_decls: vec![],
5155 out_assignments: vec![],
5156 direct_connections: vec![],
5157 feedback_decls: vec![],
5158 feedback_assignments: vec![],
5159 state_decls: vec![],
5160 state_assignments: vec![],
5161 };
5162
5163 let graph = build_graph(&prog).unwrap();
5164 let t = graph
5165 .nodes
5166 .iter()
5167 .find(|n| n.object_name == "trigger")
5168 .expect("should have a trigger node");
5169
5170 assert_eq!(t.num_outlets, 2);
5172 }
5173
5174 #[test]
5176 fn test_graph_route_outlet_count() {
5177 let prog = Program {
5178 in_decls: vec![],
5179 out_decls: vec![],
5180 wires: vec![Wire {
5181 name: "r".to_string(),
5182 value: Expr::Call {
5183 object: "route".to_string(),
5184 args: vec![
5185 CallArg::positional(Expr::Lit(LitValue::Str("a".to_string()))),
5186 CallArg::positional(Expr::Lit(LitValue::Str("b".to_string()))),
5187 CallArg::positional(Expr::Lit(LitValue::Str("c".to_string()))),
5188 ],
5189 },
5190 span: None,
5191 attrs: vec![],
5192 }],
5193 destructuring_wires: vec![],
5194 msg_decls: vec![],
5195 out_assignments: vec![],
5196 direct_connections: vec![],
5197 feedback_decls: vec![],
5198 feedback_assignments: vec![],
5199 state_decls: vec![],
5200 state_assignments: vec![],
5201 };
5202
5203 let graph = build_graph(&prog).unwrap();
5204 let r = graph
5205 .nodes
5206 .iter()
5207 .find(|n| n.object_name == "route")
5208 .expect("should have a route node");
5209
5210 assert_eq!(r.num_outlets, 4);
5212 }
5213
5214 #[test]
5215 fn test_codebox_with_code_files() {
5216 let mut code_files = CodeFiles::new();
5217 code_files.insert(
5218 "processor.js".to_string(),
5219 "function bang() { outlet(0, 42); }".to_string(),
5220 );
5221
5222 let prog = Program {
5223 in_decls: vec![],
5224 out_decls: vec![],
5225 wires: vec![Wire {
5226 name: "cb".to_string(),
5227 value: Expr::Call {
5228 object: "v8.codebox".to_string(),
5229 args: vec![CallArg::positional(Expr::Lit(LitValue::Str(
5230 "processor.js".to_string(),
5231 )))],
5232 },
5233 span: None,
5234 attrs: vec![],
5235 }],
5236 destructuring_wires: vec![],
5237 msg_decls: vec![],
5238 out_assignments: vec![],
5239 direct_connections: vec![],
5240 feedback_decls: vec![],
5241 feedback_assignments: vec![],
5242 state_decls: vec![],
5243 state_assignments: vec![],
5244 };
5245
5246 let graph = build_graph_with_code_files(&prog, None, Some(&code_files)).unwrap();
5247
5248 let cb_node = graph
5249 .nodes
5250 .iter()
5251 .find(|n| n.object_name == "v8.codebox")
5252 .expect("should have a v8.codebox node");
5253
5254 assert_eq!(
5255 cb_node.code,
5256 Some("function bang() { outlet(0, 42); }".to_string())
5257 );
5258 assert!(
5259 cb_node.args.is_empty(),
5260 "args should be cleared when code file is resolved"
5261 );
5262 }
5263
5264 #[test]
5265 fn test_codebox_without_code_files() {
5266 let prog = Program {
5268 in_decls: vec![],
5269 out_decls: vec![],
5270 wires: vec![Wire {
5271 name: "cb".to_string(),
5272 value: Expr::Call {
5273 object: "v8.codebox".to_string(),
5274 args: vec![CallArg::positional(Expr::Lit(LitValue::Str(
5275 "processor.js".to_string(),
5276 )))],
5277 },
5278 span: None,
5279 attrs: vec![],
5280 }],
5281 destructuring_wires: vec![],
5282 msg_decls: vec![],
5283 out_assignments: vec![],
5284 direct_connections: vec![],
5285 feedback_decls: vec![],
5286 feedback_assignments: vec![],
5287 state_decls: vec![],
5288 state_assignments: vec![],
5289 };
5290
5291 let graph = build_graph(&prog).unwrap();
5292
5293 let cb_node = graph
5294 .nodes
5295 .iter()
5296 .find(|n| n.object_name == "v8.codebox")
5297 .expect("should have a v8.codebox node");
5298
5299 assert_eq!(cb_node.code, None);
5300 assert_eq!(cb_node.args, vec!["processor.js"]);
5301 }
5302
5303 #[test]
5304 fn test_codebox_infer_inlets_outlets() {
5305 assert_eq!(infer_num_inlets("v8.codebox", &[], None), 1);
5307 assert_eq!(infer_num_inlets("codebox", &[], None), 1);
5308 assert_eq!(infer_num_outlets("v8.codebox", &[], None), 1);
5309 assert_eq!(infer_num_outlets("codebox", &[], None), 1);
5310 }
5311
5312 #[test]
5313 fn test_infer_codebox_ports_basic() {
5314 assert_eq!(infer_codebox_ports("out1 = in1 * in2;"), (2, 1));
5316 }
5317
5318 #[test]
5319 fn test_infer_codebox_ports_multiple_outputs() {
5320 let code = "out1 = in1 * in2;\nout2 = in1 + in2;\nout3 = in1 - in2;";
5321 assert_eq!(infer_codebox_ports(code), (2, 3));
5322 }
5323
5324 #[test]
5325 fn test_infer_codebox_ports_history() {
5326 let code = "History hold(0), gate(0);\nout1 = in1 * in2 * in3;\nout2 = in4;";
5328 assert_eq!(infer_codebox_ports(code), (4, 2));
5329 }
5330
5331 #[test]
5332 fn test_infer_codebox_ports_no_refs() {
5333 assert_eq!(infer_codebox_ports("x = 42;"), (1, 1));
5335 }
5336
5337 #[test]
5338 fn test_infer_codebox_ports_word_boundary() {
5339 let code = "into = 5;\noutput = into + 1;\nout1 = in1;";
5341 assert_eq!(infer_codebox_ports(code), (1, 1));
5342 }
5343
5344 #[test]
5350 fn test_infer_with_objdb() {
5351 use flutmax_objdb::{
5352 InletSpec, Module, ObjectDb, ObjectDef, OutletSpec, PortDef, PortType as ObjPortType,
5353 };
5354
5355 let mut db = ObjectDb::new();
5356 db.insert(ObjectDef {
5357 name: "myobj~".to_string(),
5358 module: Module::Msp,
5359 category: "test".to_string(),
5360 digest: "test object".to_string(),
5361 inlets: InletSpec::Fixed(vec![
5362 PortDef {
5363 id: 0,
5364 port_type: ObjPortType::Signal,
5365 is_hot: true,
5366 description: "in 0".to_string(),
5367 },
5368 PortDef {
5369 id: 1,
5370 port_type: ObjPortType::Signal,
5371 is_hot: false,
5372 description: "in 1".to_string(),
5373 },
5374 PortDef {
5375 id: 2,
5376 port_type: ObjPortType::Float,
5377 is_hot: false,
5378 description: "in 2".to_string(),
5379 },
5380 ]),
5381 outlets: OutletSpec::Fixed(vec![
5382 PortDef {
5383 id: 0,
5384 port_type: ObjPortType::Signal,
5385 is_hot: false,
5386 description: "out 0".to_string(),
5387 },
5388 PortDef {
5389 id: 1,
5390 port_type: ObjPortType::Signal,
5391 is_hot: false,
5392 description: "out 1".to_string(),
5393 },
5394 ]),
5395 args: vec![],
5396 });
5397
5398 assert_eq!(infer_num_inlets("myobj~", &[], Some(&db)), 3);
5400 assert_eq!(infer_num_outlets("myobj~", &[], Some(&db)), 2);
5401 }
5402
5403 #[test]
5405 fn test_infer_objdb_fallback() {
5406 use flutmax_objdb::ObjectDb;
5407
5408 let db = ObjectDb::new(); assert_eq!(infer_num_inlets("cycle~", &[], Some(&db)), 2);
5412 assert_eq!(infer_num_outlets("cycle~", &[], Some(&db)), 1);
5413
5414 assert_eq!(infer_num_inlets("counter", &[], Some(&db)), 3);
5416 assert_eq!(infer_num_outlets("counter", &[], Some(&db)), 4);
5417 }
5418
5419 #[test]
5421 fn test_infer_objdb_variable_ports() {
5422 use flutmax_objdb::{
5423 InletSpec, Module, ObjectDb, ObjectDef, OutletSpec, PortDef, PortType as ObjPortType,
5424 };
5425
5426 let mut db = ObjectDb::new();
5427 db.insert(ObjectDef {
5428 name: "varobj".to_string(),
5429 module: Module::Max,
5430 category: "test".to_string(),
5431 digest: "variable port object".to_string(),
5432 inlets: InletSpec::Variable {
5433 defaults: vec![
5434 PortDef {
5435 id: 0,
5436 port_type: ObjPortType::Any,
5437 is_hot: true,
5438 description: "in 0".to_string(),
5439 },
5440 PortDef {
5441 id: 1,
5442 port_type: ObjPortType::Any,
5443 is_hot: false,
5444 description: "in 1".to_string(),
5445 },
5446 ],
5447 min_inlets: 1,
5448 },
5449 outlets: OutletSpec::Variable {
5450 defaults: vec![
5451 PortDef {
5452 id: 0,
5453 port_type: ObjPortType::Any,
5454 is_hot: false,
5455 description: "out 0".to_string(),
5456 },
5457 PortDef {
5458 id: 1,
5459 port_type: ObjPortType::Any,
5460 is_hot: false,
5461 description: "out 1".to_string(),
5462 },
5463 PortDef {
5464 id: 2,
5465 port_type: ObjPortType::Any,
5466 is_hot: false,
5467 description: "out 2".to_string(),
5468 },
5469 ],
5470 min_outlets: 1,
5471 },
5472 args: vec![],
5473 });
5474
5475 assert_eq!(infer_num_inlets("varobj", &[], Some(&db)), 2);
5477 assert_eq!(infer_num_outlets("varobj", &[], Some(&db)), 3);
5478
5479 assert_eq!(
5481 infer_num_inlets(
5482 "varobj",
5483 &["a".to_string(), "b".to_string(), "c".to_string()],
5484 Some(&db)
5485 ),
5486 3
5487 );
5488 assert_eq!(
5489 infer_num_outlets("varobj", &["x".to_string(), "y".to_string()], Some(&db)),
5490 2
5491 );
5492 }
5493
5494 #[test]
5497 fn test_out_decl_inline_value_produces_edge() {
5498 let inline_program = Program {
5501 in_decls: vec![],
5502 out_decls: vec![OutDecl {
5503 index: 0,
5504 name: "audio".to_string(),
5505 port_type: PortType::Signal,
5506 value: Some(Expr::Ref("osc".to_string())),
5507 }],
5508 wires: vec![Wire {
5509 name: "osc".to_string(),
5510 value: Expr::Call {
5511 object: "cycle~".to_string(),
5512 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
5513 },
5514 span: None,
5515 attrs: vec![],
5516 }],
5517 destructuring_wires: vec![],
5518 msg_decls: vec![],
5519 out_assignments: vec![],
5520 direct_connections: vec![],
5521 feedback_decls: vec![],
5522 feedback_assignments: vec![],
5523 state_decls: vec![],
5524 state_assignments: vec![],
5525 };
5526
5527 let separate_program = Program {
5528 in_decls: vec![],
5529 out_decls: vec![OutDecl {
5530 index: 0,
5531 name: "audio".to_string(),
5532 port_type: PortType::Signal,
5533 value: None,
5534 }],
5535 wires: vec![Wire {
5536 name: "osc".to_string(),
5537 value: Expr::Call {
5538 object: "cycle~".to_string(),
5539 args: vec![CallArg::positional(Expr::Lit(LitValue::Int(440)))],
5540 },
5541 span: None,
5542 attrs: vec![],
5543 }],
5544 destructuring_wires: vec![],
5545 msg_decls: vec![],
5546 out_assignments: vec![OutAssignment {
5547 index: 0,
5548 value: Expr::Ref("osc".to_string()),
5549 span: None,
5550 }],
5551 direct_connections: vec![],
5552 feedback_decls: vec![],
5553 feedback_assignments: vec![],
5554 state_decls: vec![],
5555 state_assignments: vec![],
5556 };
5557
5558 let inline_graph = build_graph(&inline_program).expect("inline build failed");
5559 let separate_graph = build_graph(&separate_program).expect("separate build failed");
5560
5561 assert_eq!(
5563 inline_graph.nodes.len(),
5564 separate_graph.nodes.len(),
5565 "node count mismatch: inline={} vs separate={}",
5566 inline_graph.nodes.len(),
5567 separate_graph.nodes.len()
5568 );
5569 assert_eq!(
5570 inline_graph.edges.len(),
5571 separate_graph.edges.len(),
5572 "edge count mismatch: inline={} vs separate={}",
5573 inline_graph.edges.len(),
5574 separate_graph.edges.len()
5575 );
5576 }
5577
5578 #[test]
5581 fn test_resolve_inlet_name_found() {
5582 use flutmax_objdb::{
5583 InletSpec, Module, ObjectDb, ObjectDef, OutletSpec, PortDef, PortType as ObjPortType,
5584 };
5585
5586 let mut db = ObjectDb::new();
5587 db.insert(ObjectDef {
5588 name: "cycle~".to_string(),
5589 module: Module::Msp,
5590 category: String::new(),
5591 digest: String::new(),
5592 inlets: InletSpec::Fixed(vec![
5593 PortDef {
5594 id: 0,
5595 port_type: ObjPortType::SignalFloat,
5596 is_hot: true,
5597 description: "Frequency".to_string(),
5598 },
5599 PortDef {
5600 id: 1,
5601 port_type: ObjPortType::SignalFloat,
5602 is_hot: false,
5603 description: "Phase offset".to_string(),
5604 },
5605 ]),
5606 outlets: OutletSpec::Fixed(vec![]),
5607 args: vec![],
5608 });
5609
5610 assert_eq!(
5611 resolve_inlet_name("cycle~", "frequency", Some(&db)),
5612 Some(0)
5613 );
5614 assert_eq!(
5615 resolve_inlet_name("cycle~", "phase_offset", Some(&db)),
5616 Some(1)
5617 );
5618 }
5619
5620 #[test]
5621 fn test_resolve_inlet_name_not_found() {
5622 use flutmax_objdb::{
5623 InletSpec, Module, ObjectDb, ObjectDef, OutletSpec, PortDef, PortType as ObjPortType,
5624 };
5625
5626 let mut db = ObjectDb::new();
5627 db.insert(ObjectDef {
5628 name: "cycle~".to_string(),
5629 module: Module::Msp,
5630 category: String::new(),
5631 digest: String::new(),
5632 inlets: InletSpec::Fixed(vec![PortDef {
5633 id: 0,
5634 port_type: ObjPortType::SignalFloat,
5635 is_hot: true,
5636 description: "Frequency".to_string(),
5637 }]),
5638 outlets: OutletSpec::Fixed(vec![]),
5639 args: vec![],
5640 });
5641
5642 assert_eq!(resolve_inlet_name("cycle~", "nonexistent", Some(&db)), None);
5643 }
5644
5645 #[test]
5646 fn test_resolve_inlet_name_no_objdb() {
5647 assert_eq!(resolve_inlet_name("cycle~", "frequency", None), None);
5648 }
5649
5650 #[test]
5651 fn test_resolve_abstraction_inlet_name() {
5652 use flutmax_ast::PortType;
5653 use flutmax_sema::registry::{AbstractionInterface, AbstractionRegistry, PortInfo};
5654
5655 let mut reg = AbstractionRegistry::new();
5656 reg.register_interface(AbstractionInterface {
5657 name: "simpleFM".to_string(),
5658 in_ports: vec![
5659 PortInfo {
5660 index: 0,
5661 name: "carrier_freq".to_string(),
5662 port_type: PortType::Float,
5663 },
5664 PortInfo {
5665 index: 1,
5666 name: "harmonicity".to_string(),
5667 port_type: PortType::Float,
5668 },
5669 PortInfo {
5670 index: 2,
5671 name: "mod_index".to_string(),
5672 port_type: PortType::Float,
5673 },
5674 ],
5675 out_ports: vec![PortInfo {
5676 index: 0,
5677 name: "output".to_string(),
5678 port_type: PortType::Signal,
5679 }],
5680 });
5681
5682 assert_eq!(
5683 resolve_abstraction_inlet_name("simpleFM", "carrier_freq", Some(®)),
5684 Some(0)
5685 );
5686 assert_eq!(
5687 resolve_abstraction_inlet_name("simpleFM", "harmonicity", Some(®)),
5688 Some(1)
5689 );
5690 assert_eq!(
5691 resolve_abstraction_inlet_name("simpleFM", "mod_index", Some(®)),
5692 Some(2)
5693 );
5694 assert_eq!(
5695 resolve_abstraction_inlet_name("simpleFM", "nonexistent", Some(®)),
5696 None
5697 );
5698 assert_eq!(
5699 resolve_abstraction_inlet_name("unknown", "carrier_freq", Some(®)),
5700 None
5701 );
5702 assert_eq!(
5703 resolve_abstraction_inlet_name("simpleFM", "carrier_freq", None),
5704 None
5705 );
5706 }
5707
5708 #[test]
5709 fn test_named_arg_codegen() {
5710 use flutmax_objdb::{
5712 InletSpec, Module, ObjectDb, ObjectDef, OutletSpec, PortDef, PortType as ObjPortType,
5713 };
5714
5715 let mut db = ObjectDb::new();
5716 db.insert(ObjectDef {
5717 name: "biquad~".to_string(),
5718 module: Module::Msp,
5719 category: String::new(),
5720 digest: String::new(),
5721 inlets: InletSpec::Fixed(vec![
5722 PortDef {
5723 id: 0,
5724 port_type: ObjPortType::Signal,
5725 is_hot: true,
5726 description: "Input".to_string(),
5727 },
5728 PortDef {
5729 id: 1,
5730 port_type: ObjPortType::SignalFloat,
5731 is_hot: false,
5732 description: "Frequency".to_string(),
5733 },
5734 PortDef {
5735 id: 2,
5736 port_type: ObjPortType::SignalFloat,
5737 is_hot: false,
5738 description: "Q factor".to_string(),
5739 },
5740 ]),
5741 outlets: OutletSpec::Fixed(vec![PortDef {
5742 id: 0,
5743 port_type: ObjPortType::Signal,
5744 is_hot: false,
5745 description: "Output".to_string(),
5746 }]),
5747 args: vec![],
5748 });
5749
5750 let program = Program {
5752 in_decls: vec![
5753 InDecl {
5754 index: 0,
5755 name: "sig".to_string(),
5756 port_type: PortType::Signal,
5757 },
5758 InDecl {
5759 index: 1,
5760 name: "freq".to_string(),
5761 port_type: PortType::Float,
5762 },
5763 ],
5764 out_decls: vec![OutDecl {
5765 index: 0,
5766 name: "out".to_string(),
5767 port_type: PortType::Signal,
5768 value: None,
5769 }],
5770 wires: vec![Wire {
5771 name: "filtered".to_string(),
5772 value: Expr::Call {
5773 object: "biquad~".to_string(),
5774 args: vec![
5775 CallArg::named("frequency", Expr::Ref("freq".to_string())),
5777 CallArg::named("input", Expr::Ref("sig".to_string())),
5779 ],
5780 },
5781 span: None,
5782 attrs: vec![],
5783 }],
5784 out_assignments: vec![OutAssignment {
5785 index: 0,
5786 value: Expr::Ref("filtered".to_string()),
5787 span: None,
5788 }],
5789 destructuring_wires: vec![],
5790 msg_decls: vec![],
5791 direct_connections: vec![],
5792 feedback_decls: vec![],
5793 feedback_assignments: vec![],
5794 state_decls: vec![],
5795 state_assignments: vec![],
5796 };
5797
5798 let graph =
5799 build_graph_with_objdb(&program, None, None, Some(&db)).expect("should build graph");
5800
5801 let biquad_node = graph
5805 .nodes
5806 .iter()
5807 .find(|n| n.object_name == "biquad~")
5808 .expect("should have biquad~ node");
5809 let biquad_id = &biquad_node.id;
5810
5811 let biquad_edges: Vec<_> = graph
5812 .edges
5813 .iter()
5814 .filter(|e| &e.dest_id == biquad_id)
5815 .collect();
5816
5817 assert_eq!(
5819 biquad_edges.len(),
5820 2,
5821 "expected 2 edges to biquad~, got {}: {:?}",
5822 biquad_edges.len(),
5823 biquad_edges
5824 );
5825
5826 let freq_edge = biquad_edges.iter().find(|e| e.dest_inlet == 1);
5828 let sig_edge = biquad_edges.iter().find(|e| e.dest_inlet == 0);
5829 assert!(
5830 freq_edge.is_some(),
5831 "should have edge to inlet 1 (frequency)"
5832 );
5833 assert!(sig_edge.is_some(), "should have edge to inlet 0 (input)");
5834 }
5835}