flowrs-engine/crates/flowrs-core/tests/loop_scope_codegen.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

142 lines
5.6 KiB
Rust

//! A loop's body must be emitted INSIDE the loop, and the program must build.
//!
//! Before this, codegen emitted every node into one flat sequence, so a loop had nowhere
//! to put statements — and `Break`/`Continue` could not exist at all, because nothing
//! tracked whether emission was inside a loop and `break` outside one is E0268.
use flowrs_core::graph::{Graph, WireMode};
use std::process::Command;
fn emit(g: &Graph) -> Result<String, String> {
g.to_rust()
}
/// `want_inside` lines must appear indented inside the loop's block.
fn check(label: &str, g: &Graph, want_inside: &[&str]) {
let src = match emit(g) {
Ok(s) => s,
Err(e) => panic!("{label} should emit, but codegen refused: {e}"),
};
// Is the fragment inside a brace block (indented under a for/while/loop)?
for w in want_inside {
let inside = src
.lines()
.any(|l| l.trim() == *w && l.starts_with(" "));
if !inside {
println!("FAIL {label}: `{w}` is not inside the loop block\n---\n{src}---");
return;
}
}
let dir = std::env::temp_dir().join("flowrs_phase2");
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(
dir.join("Cargo.toml"),
"[workspace]\n[package]\nname=\"p\"\nversion=\"0.0.0\"\nedition=\"2021\"\n",
)
.unwrap();
std::fs::write(dir.join("src/main.rs"), &src).unwrap();
let out = Command::new("cargo")
.args(["check", "--quiet", "--offline", "--message-format=short"])
.current_dir(&dir)
// `CARGO_TARGET_DIR` removed so this check does not depend on ambient config.
//
// `graph_build` in flowrs-mcp had a demonstrated false pass from an inherited
// target dir: its scratch crates are all `flowrs_graph 0.0.0`, and one shared
// target made them a single build unit, so cargo found a later program already
// fresh and exited 0 without reading it. These harnesses use the same package
// identity, so the same shape is possible here.
//
// Tested, and it did NOT reproduce: priming a shared target with a passing
// harness and then running this one with its own codegen broken still failed
// correctly. This is isolation against a hazard that has bitten once nearby,
// not a fix for a bug demonstrated here.
.env_remove("CARGO_TARGET_DIR")
.output()
.unwrap();
if out.status.success() {
return;
}
let e = String::from_utf8_lossy(&out.stderr);
if e.contains("failed to download") || e.contains("no matching package") {
eprintln!("skipped {label}: dependencies unavailable offline");
return;
}
let diags: Vec<&str> = e.lines().filter(|l| l.contains("error")).take(6).collect();
panic!(
"{label} emits Rust that does not compile:\n {}\n---\n{src}---",
diags.join("\n ")
);
}
fn prop(g: &mut Graph, n: flowrs_core::NodeId, k: &str, v: &str) {
g.nodes
.get_mut(&n)
.unwrap()
.properties
.insert(k.into(), v.into());
}
fn range(g: &mut Graph) -> flowrs_core::NodeId {
let a = g.add_node("IntLiteral", 0.0, 0.0);
prop(g, a, "value", "0");
let b = g.add_node("IntLiteral", 0.0, 0.0);
prop(g, b, "value", "3");
let r = g.add_node("Range", 0.0, 0.0);
g.add_wire(a, "value", r, "start", WireMode::Move).unwrap();
g.add_wire(b, "value", r, "end", WireMode::Move).unwrap();
r
}
#[test]
fn loop_bodies_land_inside_their_loops_and_compile() {
// ForLoop with a CodeBlockNode body.
let mut g = Graph::new();
let r = range(&mut g);
let body = g.add_node("CodeBlockNode", 0.0, 0.0);
prop(&mut g, body, "src", "println!(\"{}\", x);");
let l = g.add_node("ForLoop", 0.0, 0.0);
g.add_wire(r, "iterator", l, "iterator", WireMode::Move)
.unwrap();
g.add_wire(body, "outputs", l, "body", WireMode::Move)
.unwrap();
check("ForLoop+body", &g, &["println!(\"{}\", x);"]);
// Loop with a Break inside — the case that could not exist before.
let mut g = Graph::new();
let brk = g.add_node("Break", 0.0, 0.0);
let l = g.add_node("Loop", 0.0, 0.0);
g.add_wire(brk, "signal", l, "body", WireMode::Move)
.unwrap();
check("Loop+Break", &g, &["break;"]);
// Break at the top level must refuse, not emit E0268.
let mut g = Graph::new();
g.add_node("Break", 0.0, 0.0);
// Refusing is the correct behaviour: `break` outside a loop is E0268, and the whole
// reason to track body ownership is to tell the two situations apart.
let e = emit(&g).unwrap_err();
assert!(e.contains("Break") && e.contains("loop"), "{e}");
// WhileLoop with an expression condition.
let mut g = Graph::new();
let body = g.add_node("CodeBlockNode", 0.0, 0.0);
prop(&mut g, body, "src", "break;");
let w = g.add_node("WhileLoop", 0.0, 0.0);
prop(&mut g, w, "src", "true");
g.add_wire(body, "outputs", w, "body", WireMode::Move)
.unwrap();
check("WhileLoop(expr cond)", &g, &["break;"]);
// LoopNode: has a result the rest of the graph can consume.
let mut g = Graph::new();
let r = range(&mut g);
let body = g.add_node("CodeBlockNode", 0.0, 0.0);
prop(&mut g, body, "src", "let _ = x;");
let l = g.add_node("LoopNode", 0.0, 0.0);
g.add_wire(r, "iterator", l, "iterator", WireMode::Move)
.unwrap();
g.add_wire(body, "outputs", l, "body", WireMode::Move)
.unwrap();
let c = g.add_node("VecLen", 0.0, 0.0);
g.add_wire(l, "result", c, "vec", WireMode::Move).unwrap();
check("LoopNode+result", &g, &["let _ = x;"]);
}