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