142 lines
5.6 KiB
Rust
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;"]);
|
|
}
|