//! 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 { 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;"]); }