diff --git a/monitor/src/interpreter.rs b/monitor/src/interpreter.rs index 9c565251..f59bd5a4 100644 --- a/monitor/src/interpreter.rs +++ b/monitor/src/interpreter.rs @@ -50,28 +50,15 @@ impl Interpreter { &self, struct_name: Option, ctx: &GlobalContext, - trace: &WaveSignalTrace, + _trace: &WaveSignalTrace, ) -> ProtocolApplication { let mut serialized_args = vec![]; for arg in &self.transaction.args { let symbol_id = arg.symbol(); - let name = self.symbol_table[symbol_id].full_name(&self.symbol_table); - let value = self.args_mapping.get(&symbol_id).unwrap_or_else(|| { - let time_str = if ctx.show_waveform_time { - trace.format_time(trace.time_step(), ctx.time_unit) - } else { - format!("cycle {}", self.trace_cycle_count) - }; - panic!( - "Transaction `{}`, {}: Unable to find value for {} ({}) in args_mapping, which is {{ {} }}", - self.transaction.name, - time_str, - name, - symbol_id, - serialize_args_mapping(&self.args_mapping, &self.symbol_table, ctx.display_hex) - ) - }); - serialized_args.push(serialize_bitvec(value, ctx.display_hex)); + match self.args_mapping.get(&symbol_id) { + Some(value) => serialized_args.push(serialize_bitvec(value, ctx.display_hex)), + None => serialized_args.push("?".to_string()), + } } ProtocolApplication { struct_name, @@ -488,7 +475,9 @@ impl Interpreter { } Stmt::Block(stmt_ids) => { if stmt_ids.is_empty() { - Ok(None) + // Empty block — use next_stmt_map to continue + // to the next statement in the parent scope + Ok(self.next_stmt_map[stmt_id]) } else { Ok(Some(stmt_ids[0])) } diff --git a/monitor/src/scheduler.rs b/monitor/src/scheduler.rs index 707d3ceb..e0a0d7bc 100644 --- a/monitor/src/scheduler.rs +++ b/monitor/src/scheduler.rs @@ -430,31 +430,6 @@ impl Scheduler { .collect::>() ))); } - let finished_thread = &finished[0]; - - // ...and there shouldn't be any other threads in `next` - let next = threads_with_start_time(&self.next, start_cycle); - if !next.is_empty() { - let start_time_str = if ctx.show_waveform_time { - trace.format_time(finished_thread.start_time_step, ctx.time_unit) - } else { - format!("cycle {}", finished_thread.start_cycle) - }; - let error_context = anyhow!( - "Thread {} (`{}`) finished but there are other threads with the same start time ({}) in the `next` queue, namely {:?}", - finished_thread.global_thread_id(ctx), - finished_thread.transaction.name, - start_time_str, - next.iter() - .map(|t| t.transaction.name.clone()) - .collect::>() - ); - - return Err(SchedulerError::NoTransactionsMatch { - struct_name: self.struct_name.clone(), - error_context, - }); - } } // Next, find the unique start cycles of all threads in `failed` @@ -748,6 +723,18 @@ impl Scheduler { } } Ok(None) => { + // Check if the last executed statement was `step()`. + // If not, this protocol is ill-formed and should be discarded. + if !matches!(thread.transaction[current_stmt_id], Stmt::Step) { + info!( + "Thread {} (`{}`) didn't end with `step()`, marking as failed.", + thread.global_thread_id(ctx), + self.format_transaction_name(ctx, thread.transaction.name.clone()) + ); + self.failed.push_back(thread); + return Ok(ThreadResult::Completed); + } + // Check if another thread has already finished in this cycle. // Invariant: Only one thread per struct can finish per cycle if let Some((first_start_cycle, first_thread_id, first_transaction_name)) = diff --git a/monitor/tests/fpga-debugging/axi-stream-s2/s2_buggy.out b/monitor/tests/fpga-debugging/axi-stream-s2/s2_buggy.out index 40a7ea8e..a37a8103 100644 --- a/monitor/tests/fpga-debugging/axi-stream-s2/s2_buggy.out +++ b/monitor/tests/fpga-debugging/axi-stream-s2/s2_buggy.out @@ -14,25 +14,4 @@ trace { recv(4); // [time: 937.5ns -> 962.5ns] recv(5); // [time: 962.5ns -> 987.5ns] recv(6); // [time: 987.5ns -> 1012.5ns] - stall(7, 0); // [time: 1012.5ns -> 1037.5ns] - stall(7, 1); // [time: 1037.5ns -> 1050ns] -} - -// trace 1 -trace { - reset(); // [time: 0ns -> 12.5ns] - wait_for_data(); // [time: 337.5ns -> 362.5ns] - wait_for_data(); // [time: 387.5ns -> 412.5ns] - wait_for_data(); // [time: 512.5ns -> 537.5ns] - wait_for_data(); // [time: 537.5ns -> 562.5ns] - wait_for_data(); // [time: 612.5ns -> 637.5ns] - wait_for_data(); // [time: 787.5ns -> 812.5ns] - wait_for_data(); // [time: 837.5ns -> 862.5ns] - recv(1); // [time: 862.5ns -> 887.5ns] - recv(2); // [time: 887.5ns -> 912.5ns] - recv(3); // [time: 912.5ns -> 937.5ns] - recv(4); // [time: 937.5ns -> 962.5ns] - recv(5); // [time: 962.5ns -> 987.5ns] - recv(6); // [time: 987.5ns -> 1012.5ns] - stall(7, 1); // [time: 1037.5ns -> 1050ns] } diff --git a/monitor/tests/fpga-debugging/axi-stream-s2/s2_fixed.out b/monitor/tests/fpga-debugging/axi-stream-s2/s2_fixed.out index 8ad4900a..190a394f 100644 --- a/monitor/tests/fpga-debugging/axi-stream-s2/s2_fixed.out +++ b/monitor/tests/fpga-debugging/axi-stream-s2/s2_fixed.out @@ -14,34 +14,12 @@ trace { recv(4); // [time: 937.5ns -> 962.5ns] recv(5); // [time: 962.5ns -> 987.5ns] recv(6); // [time: 987.5ns -> 1012.5ns] - stall(7, 0); // [time: 1012.5ns -> 1037.5ns] - stall(7, 0); // [time: 1037.5ns -> 1062.5ns] + stall(7, 0); // [time: 1012.5ns -> 1062.5ns] reset(); // [time: 1062.5ns -> 1087.5ns] reset(); // [time: 1087.5ns -> 1100ns] } // trace 1 -trace { - reset(); // [time: 0ns -> 12.5ns] - wait_for_data(); // [time: 337.5ns -> 362.5ns] - wait_for_data(); // [time: 387.5ns -> 412.5ns] - wait_for_data(); // [time: 512.5ns -> 537.5ns] - wait_for_data(); // [time: 537.5ns -> 562.5ns] - wait_for_data(); // [time: 612.5ns -> 637.5ns] - wait_for_data(); // [time: 787.5ns -> 812.5ns] - wait_for_data(); // [time: 837.5ns -> 862.5ns] - recv(1); // [time: 862.5ns -> 887.5ns] - recv(2); // [time: 887.5ns -> 912.5ns] - recv(3); // [time: 912.5ns -> 937.5ns] - recv(4); // [time: 937.5ns -> 962.5ns] - recv(5); // [time: 962.5ns -> 987.5ns] - recv(6); // [time: 987.5ns -> 1012.5ns] - stall(7, 0); // [time: 1012.5ns -> 1037.5ns] - reset(); // [time: 1062.5ns -> 1087.5ns] - reset(); // [time: 1087.5ns -> 1100ns] -} - -// trace 2 trace { reset(); // [time: 0ns -> 12.5ns] wait_for_data(); // [time: 337.5ns -> 362.5ns] diff --git a/monitor/tests/fpga-debugging/axi-stream-s2/s2_fixed.prot b/monitor/tests/fpga-debugging/axi-stream-s2/s2_fixed.prot index afb0eccc..cbb5bcd1 100644 --- a/monitor/tests/fpga-debugging/axi-stream-s2/s2_fixed.prot +++ b/monitor/tests/fpga-debugging/axi-stream-s2/s2_fixed.prot @@ -90,6 +90,8 @@ prot stall(out data: u32, out last: u1) { // Verify outputs remained stable during the stall assert_eq(DUT.i_tdata, data); assert_eq(DUT.i_tlast, last); + fork(); + step(); } // WAIT_FOR_DATA: Receiver is ready but no data is available diff --git a/protocols/src/scheduler.rs b/protocols/src/scheduler.rs index bb87d0c0..aea09387 100644 --- a/protocols/src/scheduler.rs +++ b/protocols/src/scheduler.rs @@ -403,6 +403,10 @@ impl<'a> Scheduler<'a> { } } + // Final sim step so the FST waveform always has at least one time entry + // (needed for combinational-only designs where no cycle advances occur) + self.evaluator.sim_step(); + // Emit diagnostics for all errors after execution is complete self.emit_all_diagnostics(); self.results.clone() diff --git a/scripts/roundtrip.out b/scripts/roundtrip.out new file mode 100644 index 00000000..c436d8db --- /dev/null +++ b/scripts/roundtrip.out @@ -0,0 +1,32 @@ + +=== Round-trip results === + Passed: 28 / 33 + Failed: 5 / 33 + Skipped: 38 (transactions don't complete successfully) + +Monitor failures: + + --- protocols/tests/adders/adder_d1/busy_wait_pass.tx (trace 0) --- + thread 'main' panicked at monitor/src/interpreter.rs:466:17: + not yet implemented: Bounded loops is not yet implemented in the monitor + note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + --- protocols/tests/adders/adder_d1/loop_with_assigns.tx (trace 0) --- + thread 'main' panicked at monitor/src/interpreter.rs:466:17: + not yet implemented: Bounded loops is not yet implemented in the monitor + note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + --- protocols/tests/adders/adder_d1/nested_busy_wait.tx (trace 0) --- + thread 'main' panicked at monitor/src/interpreter.rs:466:17: + not yet implemented: Bounded loops is not yet implemented in the monitor + note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + --- protocols/tests/fifo/push_pop_loop_empty.tx (trace 0) --- + thread 'main' panicked at monitor/src/interpreter.rs:466:17: + not yet implemented: Bounded loops is not yet implemented in the monitor + note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + + --- protocols/tests/fifo/push_pop_loop_not_empty.tx (trace 0) --- + thread 'main' panicked at monitor/src/interpreter.rs:466:17: + not yet implemented: Bounded loops is not yet implemented in the monitor + note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/scripts/roundtrip.py b/scripts/roundtrip.py new file mode 100644 index 00000000..3d8aac2f --- /dev/null +++ b/scripts/roundtrip.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +Round-trip test: for each passing .tx test, run the interpreter to generate +an FST waveform, then run the monitor on that FST. Reports success/fail counts. + +Usage: python scripts/roundtrip.py + +TODO: Compare monitor output against original .tx transactions. +""" + +import os +import re +import subprocess +import tempfile +from pathlib import Path +from typing import Optional + +PROJECT_ROOT = Path(__file__).resolve().parent.parent + + +def find_turnt_dir(start: Path) -> Optional[Path]: + """Walk up from `start` to find the nearest directory containing turnt.toml.""" + d = start + while d != d.parent: + if (d / "turnt.toml").exists(): + return d + d = d.parent + return None + + +def parse_arg(args: str, flag: str) -> Optional[str]: + """Extract value for --flag or --flag=value from an ARGS string.""" + m = re.search(rf"--{flag}[= ](\S+)", args) + return m.group(1) if m else None + + +def extract_struct_name(prot_path: Path) -> Optional[str]: + """Extract the first struct name from a .prot file.""" + m = re.search(r"^struct\s+([A-Za-z_]\w*)", prot_path.read_text(), re.MULTILINE) + return m.group(1) if m else None + + +def collect_tx_files() -> list[Path]: + """Find all .tx files under PROJECT_ROOT, sorted.""" + return sorted(PROJECT_ROOT.rglob("*.tx")) + +def collect_generated_fsts(fst_path: Path) -> list[Path]: + """Collect generated waveform files for one interp run.""" + indexed: list[tuple[int, Path]] = [] + for path in fst_path.parent.glob(f"{fst_path.stem}_*{fst_path.suffix}"): + suffix = path.stem[len(fst_path.stem) + 1 :] + if suffix.isdigit(): + indexed.append((int(suffix), path)) + if indexed: + return [path for _, path in sorted(indexed, key=lambda item: item[0])] + if fst_path.exists(): + return [fst_path] + return [] + + +def run_roundtrip(): + passed = 0 + failed = 0 + skipped = 0 + interp_failed = 0 + failed_entries: list[tuple[str, str]] = [] + interp_failed_files: list[str] = [] + + for tx_file in collect_tx_files(): + tx_rel = str(tx_file.relative_to(PROJECT_ROOT)) + text = tx_file.read_text() + + # Parse // ARGS: line + args_match = re.search(r"^// ARGS:\s*(.+)$", text, re.MULTILINE) + if not args_match: + skipped += 1 + continue + args = args_match.group(1) + + # Skip error tests (non-zero RETURN code) + return_match = re.search(r"^// RETURN:\s*(\d+)", text, re.MULTILINE) + if return_match and int(return_match.group(1)) != 0: + skipped += 1 + continue + + # Extract --protocol and --verilog paths + prot_rel = parse_arg(args, "protocol") + verilog_rel = parse_arg(args, "verilog") + if not prot_rel or not verilog_rel: + skipped += 1 + continue + + # ARGS paths are relative to the nearest turnt.toml directory + base_dir = find_turnt_dir(tx_file.parent) + if not base_dir: + skipped += 1 + continue + + prot_file = base_dir / prot_rel + if not prot_file.exists(): + skipped += 1 + continue + + # Extract struct name from .prot file + struct_name = extract_struct_name(prot_file) + if not struct_name: + skipped += 1 + continue + + # Instance name: --module if specified, otherwise verilog filename stem + module_name = parse_arg(args, "module") + instance_name = module_name if module_name else Path(verilog_rel).stem + + # Step 1: Run interpreter to generate FST + # We pass a base .fst path; interp may write either that exact file + # or indexed files like *_0.fst for multi-trace input. + fd, fst_file = tempfile.mkstemp(suffix=".fst") + os.close(fd) + os.unlink(fst_file) + fst_path = Path(fst_file) + + try: + interp_cmd = ( + f"cargo run --quiet --package protocols-interp -- " + f"--color never --transactions {tx_file} {args} --fst {fst_file}" + ) + result = subprocess.run( + interp_cmd, shell=True, cwd=base_dir, + capture_output=True, text=True, + ) + if result.returncode != 0: + interp_failed += 1 + interp_failed_files.append(tx_rel) + continue + + generated_fsts = collect_generated_fsts(fst_path) + if not generated_fsts: + failed += 1 + failed_entries.append( + (tx_rel, f"No waveform file generated at {fst_path} or indexed variants") + ) + continue + + # Step 2: Run monitor on each generated FST (one per trace) + for trace_idx, generated_fst in enumerate(generated_fsts): + monitor_cmd = ( + f"cargo run --quiet --package protocols-monitor -- " + f"-p {prot_file} --wave {generated_fst} " + f"--instances {instance_name}:{struct_name}" + ) + result = subprocess.run( + monitor_cmd, shell=True, cwd=base_dir, + capture_output=True, text=True, + ) + if result.returncode == 0: + passed += 1 + else: + failed += 1 + output = (result.stdout + result.stderr).strip() + failed_entries.append((f"{tx_rel} (trace {trace_idx})", output)) + finally: + for path in fst_path.parent.glob(f"{fst_path.stem}_*{fst_path.suffix}"): + try: + path.unlink() + except OSError: + pass + try: + fst_path.unlink() + except OSError: + pass + + # Print results + total = passed + failed + print() + print("=== Round-trip results ===") + print(f" Passed: {passed} / {total}") + print(f" Failed: {failed} / {total}") + print(f" Skipped: {skipped} (transactions don't complete successfully)") + if interp_failed: + print(f" Interpreter failures: {interp_failed}") + + if interp_failed_files: + print() + print("Interpreter failures (unexpected — these should be passing tests):") + for f in interp_failed_files: + print(f" - {f}") + + if failed_entries: + print() + print("Monitor failures:") + for file, output in failed_entries: + print() + print(f" --- {file} ---") + for line in output.splitlines(): + print(f" {line}") + + +if __name__ == "__main__": + run_roundtrip()