182 lines
6.2 KiB
Rust
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")));
|
|
}
|