flowrs-engine/crates/flowrs-core/tests/ambiguous_input.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

182 lines
6.2 KiB
Rust

//! 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::<String>(),
);
}
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::<Vec<_>>(),
);
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")));
}