//! Two wires into one input port. //! //! Nothing prevented it — `add_wire` has no check — and every reader resolved an input by //! taking the first match while scanning `graph.wires`. That is `HashMap` iteration order, //! so the SAME graph emitted different programs on different runs. Measured before the //! fix: 200 builds of one fixed graph produced two distinct programs, printing either //! operand at random. use flowrs_core::Graph; use flowrs_core::codegen::rust::primitives_to_rust; use flowrs_core::graph::diagnostics::{Severity, check}; use flowrs_core::graph::ir::WireMode; /// A `PrintLn` whose single input is fed by two literals. fn doubly_fed() -> Graph { let mut g = Graph::new(); let a = g.add_node("IntLiteral", 0.0, 0.0); let b = g.add_node("IntLiteral", 0.0, 100.0); g.get_node_mut(a) .unwrap() .properties .insert("value".into(), "111".into()); g.get_node_mut(b) .unwrap() .properties .insert("value".into(), "222".into()); let p = g.add_node("PrintLn", 300.0, 0.0); g.add_wire(a, "value", p, "input", WireMode::Move).unwrap(); g.add_wire(b, "value", p, "input", WireMode::Move).unwrap(); g } #[test] fn the_same_graph_always_compiles_to_the_same_program() { let mut seen = std::collections::HashSet::new(); // Each iteration builds a fresh graph, so each gets its own HashMap seed — which is // exactly what made this vary. One repetition would never catch it. for _ in 0..200 { let code = primitives_to_rust(&doubly_fed()).expect("codegen"); seen.insert( code.lines() .filter(|l| l.contains("println")) .collect::(), ); } assert_eq!( seen.len(), 1, "one graph must mean one program; got {} different ones: {:?}", seen.len(), seen, ); } #[test] fn the_earliest_wire_is_the_one_that_wins() { let g = doubly_fed(); let src = g .input_source( g.nodes .values() .find(|n| n.type_id == "PrintLn") .unwrap() .id, "input", ) .expect("an input with two wires still resolves"); // Wire ids are assigned in creation order, so the first connection drawn wins — stable // and explicable, rather than merely deterministic. assert_eq!(src.id.0, 0, "the earliest wire should be the source"); } #[test] fn an_ambiguous_input_is_reported_rather_than_silently_resolved() { let diags = check(&doubly_fed()); let amb: Vec<_> = diags .iter() .filter(|d| d.message.contains("fed by 2 wires")) .collect(); assert_eq!( amb.len(), 1, "the ambiguity should be reported exactly once" ); assert_eq!( amb[0].severity, Severity::Error, "there is no reading under which both wires carry a value", ); assert!( amb[0].message.contains("carries nothing"), "the message should say which wire does nothing: {}", amb[0].message, ); } #[test] fn a_normally_wired_graph_reports_no_ambiguity() { let mut g = Graph::new(); let a = g.add_node("IntLiteral", 0.0, 0.0); let p = g.add_node("PrintLn", 300.0, 0.0); g.add_wire(a, "value", p, "input", WireMode::Move).unwrap(); assert!( !check(&g).iter().any(|d| d.message.contains("fed by")), "a single wire into an input is the normal case and must not be flagged", ); } /// A cycle diagnostic must name the LOOP, and nothing else. /// /// The detector ran Kahn's algorithm and reported everything that never reached indegree /// zero — which is "in OR DOWNSTREAM OF a cycle", not the same thing. Every one of those /// nodes was told "node is part of a cycle", so a two-node loop feeding a chain of four /// produced six errors, four of them false, and none said where the loop was. On an /// imported control diagram, where feedback is normal and the loop is the one thing worth /// pointing at, that is the difference between a usable message and noise. #[test] fn a_cycle_is_reported_on_its_members_and_not_on_what_follows_them() { use flowrs_core::graph::diagnostics::{Severity, check}; let mut g = Graph::new(); let a = g.add_node("Add", 0.0, 0.0); let b = g.add_node("Multiply", 100.0, 0.0); g.add_wire(a, "result", b, "a", WireMode::Copy).unwrap(); g.add_wire(b, "result", a, "a", WireMode::Copy).unwrap(); // Four nodes hanging off the loop. They cannot be evaluated either, but they are not // the defect and saying they are sends someone to the wrong place. let mut prev = b; for i in 0..4 { let n = g.add_node("PrintLn", 200.0 + i as f32 * 80.0, 0.0); g.add_wire(prev, "result", n, "input", WireMode::Copy).ok(); prev = n; } let cyc: Vec<_> = check(&g) .into_iter() .filter(|d| d.message.contains("on a cycle")) .collect(); assert_eq!( cyc.len(), 2, "only the two nodes actually on the loop: got {:?}", cyc.iter().map(|d| &d.message).collect::>(), ); assert!(cyc.iter().all(|d| d.severity == Severity::Error)); // And the message names the loop rather than describing one. assert!( cyc[0].message.contains("Add") && cyc[0].message.contains("Multiply"), "the path should be printed: {}", cyc[0].message, ); } /// A self-loop is a cycle of one, and must still be caught. #[test] fn a_node_wired_to_itself_is_a_cycle_of_one() { use flowrs_core::graph::diagnostics::check; let mut g = Graph::new(); let a = g.add_node("Add", 0.0, 0.0); g.add_wire(a, "result", a, "a", WireMode::Copy).unwrap(); assert_eq!( check(&g) .iter() .filter(|d| d.message.contains("on a cycle")) .count(), 1, ); } /// An acyclic graph raises nothing — otherwise the marker means nothing. #[test] fn a_graph_without_feedback_raises_no_cycle_error() { use flowrs_core::graph::diagnostics::check; let mut g = Graph::new(); let a = g.add_node("IntLiteral", 0.0, 0.0); let p = g.add_node("PrintLn", 100.0, 0.0); g.add_wire(a, "value", p, "input", WireMode::Move).unwrap(); assert!(!check(&g).iter().any(|d| d.message.contains("on a cycle"))); }