//! Held wires: a loop of wires runs when one wire in it carries the previous step. //! //! The interpreter advances on `step`, never on `run`; the first step reads the source //! type's zero; a replay of a stepped log reproduces it; and the generated program, which //! loops over the same steps, prints what the interpreter printed at every one of them. use flowrs_core::graph::runtime::{Runtime, replay}; use flowrs_core::graph::tier_check::run_steps; use flowrs_core::graph::{GraphExecutor, WidgetRegistry}; use flowrs_core::{Graph, NodeId, Value, WireId, WireMode}; fn lit(g: &mut Graph, ty: &str, v: &str) -> NodeId { let n = g.add_node(ty, 0.0, 0.0); g.get_node_mut(n) .unwrap() .properties .insert("value".into(), v.into()); n } fn executor() -> GraphExecutor { GraphExecutor::with_runtime(WidgetRegistry::with_builtins(), Runtime::new()) } fn output(ex: &mut GraphExecutor, g: &Graph, node: NodeId, port: &str, advance: bool) -> Value { let steps = if advance { ex.step(g) } else { ex.run(g) }.unwrap(); steps .into_iter() .find(|s| s.node_id == node) .and_then(|s| s.outputs.get(port).cloned()) .unwrap_or_else(|| panic!("node {node:?} produced no `{port}`")) } /// y[n] = y[n-1] + 1 with y[0] = 0, printed every step. Returns (graph, add, the held wire). fn counter() -> (Graph, NodeId, WireId) { let mut g = Graph::new(); let one = lit(&mut g, "FloatLiteral", "1"); let add = g.add_node("math.add", 0.0, 0.0); let print = g.add_node("PrintLn", 0.0, 0.0); g.add_wire(one, "value", add, "b", WireMode::Copy).unwrap(); let back = g.add_wire(add, "sum", add, "a", WireMode::Copy).unwrap(); g.set_feedback(back, true); g.add_wire(add, "sum", print, "input", WireMode::Copy) .unwrap(); (g, add, back) } #[test] fn an_instant_loop_is_refused_and_a_held_one_counts_steps() { let (mut g, add, back) = counter(); g.set_feedback(back, false); let refused = executor() .run(&g) .err() .expect("an instant loop has no order"); assert!(refused.contains("cycle"), "{refused}"); assert!(g.to_rust().is_err(), "nor a program"); g.set_feedback(back, true); let mut ex = executor(); for n in 1..=4 { let sum = output(&mut ex, &g, add, "sum", true).as_f64(); assert_eq!(sum, f64::from(n), "step {n}"); } } /// Evaluating the same step again shows the same values. The editor evaluates on every /// redraw; a run that advanced would move the loop each time something repainted. #[test] fn run_reads_the_held_step_and_only_step_advances_it() { let (g, add, _) = counter(); let mut ex = executor(); assert_eq!(output(&mut ex, &g, add, "sum", false).as_f64(), 1.0); assert_eq!(output(&mut ex, &g, add, "sum", false).as_f64(), 1.0); assert_eq!(output(&mut ex, &g, add, "sum", true).as_f64(), 1.0); assert_eq!(output(&mut ex, &g, add, "sum", false).as_f64(), 2.0); // Clearing the held store is the first step again. ex.runtime().clear("held"); assert_eq!(output(&mut ex, &g, add, "sum", false).as_f64(), 1.0); } /// y[n] = 0.5 y[n-1] + 0.5, y[0] = 0. Closed form: y[n] = 1 - 0.5^n. #[test] fn a_held_first_order_plant_matches_its_closed_form() { let mut g = Graph::new(); let half = lit(&mut g, "FloatLiteral", "0.5"); let gain_u = g.add_node("math.mul", 0.0, 0.0); let gain_y = g.add_node("math.mul", 0.0, 0.0); let sum = g.add_node("math.add", 0.0, 0.0); let one = lit(&mut g, "FloatLiteral", "1"); g.add_wire(half, "value", gain_u, "a", WireMode::Copy) .unwrap(); g.add_wire(one, "value", gain_u, "b", WireMode::Copy) .unwrap(); g.add_wire(half, "value", gain_y, "b", WireMode::Copy) .unwrap(); let back = g.add_wire(sum, "sum", gain_y, "a", WireMode::Copy).unwrap(); g.set_feedback(back, true); g.add_wire(gain_u, "product", sum, "a", WireMode::Copy) .unwrap(); g.add_wire(gain_y, "product", sum, "b", WireMode::Copy) .unwrap(); let mut ex = executor(); for n in 1..=6 { let y = output(&mut ex, &g, sum, "sum", true).as_f64(); let expect = 1.0 - 0.5_f64.powi(n); assert!((y - expect).abs() < 1e-12, "step {n}: {y} vs {expect}"); } } /// An integer loop starts from the integer zero, not 0.0: the type of the first step is /// the type of every later one. #[test] fn an_integer_loop_starts_from_the_integer_zero() { let mut g = Graph::new(); let three = lit(&mut g, "IntLiteral", "3"); let add = g.add_node("Add", 0.0, 0.0); g.add_wire(three, "value", add, "b", WireMode::Copy) .unwrap(); let back = g.add_wire(add, "result", add, "a", WireMode::Copy).unwrap(); g.set_feedback(back, true); let mut ex = executor(); for n in 1..=3 { match output(&mut ex, &g, add, "result", true) { Value::Int(v) => assert_eq!(v, 3 * n, "step {n}"), other => panic!("step {n}: expected an Int, got {other:?}"), } } } /// A held wire carrying text has no zero both tiers agree on: the interpreter and the /// code generator refuse it, and say why. #[test] fn a_held_wire_without_a_zero_is_refused_by_both_tiers() { let mut g = Graph::new(); let s = lit(&mut g, "StringLiteral", "hi"); let p = g.add_node("PrintLn", 0.0, 0.0); let w = g.add_wire(s, "value", p, "input", WireMode::Copy).unwrap(); g.set_feedback(w, true); let e = executor().run(&g).err().expect("refused"); assert!(e.contains("no zero"), "{e}"); let e = g.to_rust().expect_err("refused"); assert!(e.contains("no zero"), "{e}"); } /// Stepping is logged, so a replay of the log reproduces every step. Without the held /// values in the log the replay's second run would read zero and diverge. #[test] fn a_replay_of_a_stepped_log_reproduces_it() { let (g, _, _) = counter(); let rt = Runtime::new(); let mut ex = GraphExecutor::with_runtime(WidgetRegistry::with_builtins(), rt.clone()); for _ in 0..4 { ex.step(&g).unwrap(); } let log = rt.log(); replay(&log, &g, WidgetRegistry::with_builtins()) .unwrap_or_else(|problems| panic!("{problems:#?}")); } /// The generated program runs the same steps and prints what the interpreter printed at /// each one. Before it looped, a held wire emitted one pass starting from 0.0, which /// agrees with step one and with nothing after it. #[test] fn the_compiled_program_matches_the_interpreter_at_every_step() { let scratch = std::env::temp_dir().join(format!("flowrs_held_steps_{}", std::process::id())); let (counter, _, _) = counter(); let r = run_steps(&counter, 6, &scratch.join("counter")).unwrap(); assert_eq!(r.compared, 6, "one line per step"); assert!(r.agrees(), "{:#?}", r.disagreements); // An integer loop through a palette primitive whose ports say i32 while the literal // binds i64: the held variable must take its width from what is assigned to it. let mut ints = Graph::new(); let three = lit(&mut ints, "IntLiteral", "3"); let add = ints.add_node("Add", 0.0, 0.0); let print = ints.add_node("PrintLn", 0.0, 0.0); ints.add_wire(three, "value", add, "b", WireMode::Copy) .unwrap(); let back = ints .add_wire(add, "result", add, "a", WireMode::Copy) .unwrap(); ints.set_feedback(back, true); ints.add_wire(add, "result", print, "input", WireMode::Copy) .unwrap(); let r = run_steps(&ints, 5, &scratch.join("ints")).unwrap(); assert_eq!(r.compared, 5); assert!(r.agrees(), "{:#?}", r.disagreements); // The plant, printed: a value that changes every step by a different amount. let mut g = Graph::new(); let half = lit(&mut g, "FloatLiteral", "0.5"); let gain_y = g.add_node("math.mul", 0.0, 0.0); let sum = g.add_node("math.add", 0.0, 0.0); let print = g.add_node("PrintLn", 0.0, 0.0); g.add_wire(half, "value", gain_y, "b", WireMode::Copy) .unwrap(); g.add_wire(half, "value", sum, "a", WireMode::Copy).unwrap(); g.add_wire(gain_y, "product", sum, "b", WireMode::Copy) .unwrap(); let back = g.add_wire(sum, "sum", gain_y, "a", WireMode::Copy).unwrap(); g.set_feedback(back, true); g.add_wire(sum, "sum", print, "input", WireMode::Copy) .unwrap(); let r = run_steps(&g, 8, &scratch.join("plant")).unwrap(); assert_eq!(r.compared, 8); assert!(r.agrees(), "{:#?}", r.disagreements); } /// A program that holds a wire says so, and runs the number of steps it was asked for. #[test] fn the_program_names_its_held_wire_and_its_step_count() { let (g, _, back) = counter(); let src = g.to_rust_steps(3).unwrap(); let var = format!("held_{}", back.0); assert!(src.contains(&format!("let mut {var} = 0.0;")), "{src}"); assert!(src.contains("for _step in 0..3"), "{src}"); assert!(src.contains(&format!("{var} = ")), "{src}"); // A graph without a held wire is emitted exactly as before. let mut plain = Graph::new(); let one = lit(&mut plain, "FloatLiteral", "1"); let print = plain.add_node("PrintLn", 0.0, 0.0); plain .add_wire(one, "value", print, "input", WireMode::Copy) .unwrap(); assert_eq!(plain.to_rust_steps(9).unwrap(), plain.to_rust().unwrap()); assert!(!plain.to_rust().unwrap().contains("_step")); } /// A toggle on a timer, its `state` held back into `state`, printed: one per step. /// Returns (graph, toggle). fn toggle_on_a_timer(initial: &str, trigger: bool, looped: bool) -> (Graph, NodeId) { let mut g = Graph::new(); let toggle = g.add_node("logic.toggle", 0.0, 0.0); g.get_node_mut(toggle) .unwrap() .properties .insert("initial".into(), initial.into()); if trigger { let timer = g.add_node("embedded.timer.interval", 0.0, 0.0); g.add_wire(timer, "tick", toggle, "trigger", WireMode::Copy) .unwrap(); } if looped { let back = g .add_wire(toggle, "state", toggle, "state", WireMode::Copy) .unwrap(); g.set_feedback(back, true); } let print = g.add_node("PrintLn", 0.0, 0.0); g.add_wire(toggle, "state", print, "input", WireMode::Copy) .unwrap(); (g, toggle) } /// `logic.toggle` keeps its state on a held wire and flips it once per trigger, at both /// tiers. It used to store nothing: T0 answered `true` on every step and the program /// `!initial` on every pass, so it never toggled twice and, with `initial = true`, the /// two tiers disagreed. #[test] fn a_held_toggle_flips_once_per_step_at_both_tiers() { let scratch = std::env::temp_dir().join(format!("flowrs_held_toggle_{}", std::process::id())); let (g, toggle) = toggle_on_a_timer("false", true, true); let mut ex = executor(); let seen: Vec = (0..6) .map(|_| match output(&mut ex, &g, toggle, "state", true) { Value::Bool(b) => b, other => panic!("a toggle answers a bool, not {other:?}"), }) .collect(); assert_eq!(seen, [true, false, true, false, true, false]); let r = run_steps(&g, 6, &scratch.join("looped")).unwrap(); assert_eq!(r.compared, 6, "one line per step"); assert!(r.agrees(), "{:#?}", r.disagreements); let src = g.to_rust_steps(6).unwrap(); assert!( src.contains("!held_"), "the program flips the held state:\n{src}" ); // Unlooped, it is one flip from `initial`: the case where the tiers used to disagree. for (initial, want) in [("true", "false"), ("false", "true")] { let (g, _) = toggle_on_a_timer(initial, true, false); let r = run_steps(&g, 1, &scratch.join(format!("once_{initial}"))).unwrap(); assert_eq!(r.compared, 1); assert!(r.agrees(), "initial {initial}: {:#?}", r.disagreements); let mut ex = executor(); ex.step(&g).unwrap(); assert_eq!(ex.console().trim(), want, "initial {initial}"); } // With no trigger it holds, looped or not, at both tiers. let (g, _) = toggle_on_a_timer("true", false, false); let r = run_steps(&g, 1, &scratch.join("held_initial")).unwrap(); assert!(r.agrees(), "{:#?}", r.disagreements); let mut ex = executor(); ex.step(&g).unwrap(); assert_eq!(ex.console().trim(), "true"); let (g, toggle) = toggle_on_a_timer("false", false, true); let mut ex = executor(); for step in 1..=3 { assert!( matches!( output(&mut ex, &g, toggle, "state", true), Value::Bool(false) ), "step {step}: an untriggered toggle holds" ); } let r = run_steps(&g, 3, &scratch.join("held_loop")).unwrap(); assert_eq!(r.compared, 3); assert!(r.agrees(), "{:#?}", r.disagreements); let _ = std::fs::remove_dir_all(&scratch); }