diff --git a/README.md b/README.md index cd9a2af49..66f861781 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,11 @@ and `program::operand`. Module construction and string interning happen before execution. CPU computes with typed operands and returns effects; it owns no stack, frames, or PC. Its heap exchanges operands through the same stack protocols. HeapAlloc and HeapPush consume available operands sequentially on underflow. -Print belongs to a separate component; no Span or Breakpoint handling is added. +Print belongs to a separate component. `breakpoint` suspends the invocation after +that instruction: `execute`/`resume` return `Ok(())` with `is_paused()` set, and +the next `resume` continues at the following instruction. `span file start end` +records a source span on the current frame, readable through `span()`; each call +starts at `(0, 0, 0)` and returning restores the caller's span. Feature-disabled legacy and typed CPU behavior is retained. For variable input counts, use `PopFromStack>` and implement diff --git a/crates/vihaco-stdlib/src/execution_state.rs b/crates/vihaco-stdlib/src/execution_state.rs index 677ae310a..95fcb6fe7 100644 --- a/crates/vihaco-stdlib/src/execution_state.rs +++ b/crates/vihaco-stdlib/src/execution_state.rs @@ -103,6 +103,7 @@ pub struct ExecutionState { frames: Vec, pc: u32, halted: bool, + paused: bool, executing: bool, } @@ -113,6 +114,7 @@ impl Default for ExecutionState { frames: Vec::new(), pc: 0, halted: true, + paused: false, executing: false, } } @@ -131,6 +133,7 @@ impl Clone for ExecutionState { frames: self.frames.clone(), pc: self.pc, halted: self.halted, + paused: self.paused, executing: false, } } diff --git a/crates/vihaco-stdlib/src/execution_state/execution.rs b/crates/vihaco-stdlib/src/execution_state/execution.rs index d224bfd1c..10309910a 100644 --- a/crates/vihaco-stdlib/src/execution_state/execution.rs +++ b/crates/vihaco-stdlib/src/execution_state/execution.rs @@ -442,6 +442,37 @@ impl Execute for ExecutionState { } } +impl Execute for ExecutionState { + type Message = NoMessage; + type Effect = (); + type Error = eyre::Report; + + #[inline] + fn execute(&mut self, _instruction: &program::Breakpoint, _: NoMessage) -> eyre::Result<()> { + self.ensure_running()?; + // The fetched continuation is retained, so resuming skips the breakpoint. + self.paused = true; + Ok(()) + } +} + +impl Execute for ExecutionState { + type Message = NoMessage; + type Effect = (); + type Error = eyre::Report; + + #[inline] + fn execute(&mut self, instruction: &program::Span, _: NoMessage) -> eyre::Result<()> { + self.ensure_running()?; + let frame = self + .frames + .last_mut() + .ok_or_else(|| eyre::eyre!("no current frame"))?; + frame.span = (instruction.0, instruction.1, instruction.2); + Ok(()) + } +} + impl Execute for ExecutionState { type Message = BranchMessage; type Effect = (); diff --git a/crates/vihaco-stdlib/src/execution_state/runner.rs b/crates/vihaco-stdlib/src/execution_state/runner.rs index cedf2d2ce..812445a03 100644 --- a/crates/vihaco-stdlib/src/execution_state/runner.rs +++ b/crates/vihaco-stdlib/src/execution_state/runner.rs @@ -28,12 +28,16 @@ where resume(module, machine) } -/// Continue an invocation until Halt/root return or an error. +/// Continue an invocation until Halt/root return, a breakpoint, or an error. /// /// Fetch establishes the sequential continuation before the Step runs. Program /// instructions can replace that continuation. A fault or terminal instruction /// leaves its original position available through [`ExecutionState::pc()`]. /// +/// A `breakpoint` returns `Ok(())` with [`ExecutionState::is_paused()`] set and +/// the PC at the following instruction. Calling `resume` again clears the pause +/// and continues from there. +/// /// The caller must supply the same unchanged module used to start the active /// invocation. This checked API validates accesses, but does not enforce module /// identity. After an error, earlier operand consumption and effects remain; @@ -56,6 +60,7 @@ where "program execution is already in progress" ); program.executing = true; + program.paused = false; // Capture code once from the independent module borrow. let code = &module.code; let result = (|| { @@ -76,6 +81,9 @@ where program.pc = position; } result?; + if program.paused { + break; + } } Ok(()) })(); diff --git a/crates/vihaco-stdlib/src/execution_state/storage.rs b/crates/vihaco-stdlib/src/execution_state/storage.rs index 2a648a88d..9209d726e 100644 --- a/crates/vihaco-stdlib/src/execution_state/storage.rs +++ b/crates/vihaco-stdlib/src/execution_state/storage.rs @@ -24,6 +24,23 @@ impl ExecutionState { self.halted } + /// True after a `breakpoint` suspended a still-active invocation. + /// + /// [`resume`](super::resume) clears the flag and continues at [`Self::pc()`], + /// the instruction following the breakpoint. A paused invocation is not + /// halted: it cannot be restarted or replaced until it finishes or is reset. + pub fn is_paused(&self) -> bool { + self.paused + } + + /// Source span `(file, start, end)` most recently recorded by `span` in the + /// current frame, or `None` without an active frame. + /// + /// Each call starts with `(0, 0, 0)`; returning restores the caller's span. + pub fn span(&self) -> Option<(u32, u32, u32)> { + self.frames.last().map(|frame| frame.span) + } + /// Enter main using its module metadata and host-supplied arguments. /// /// Entry creates the outermost frame, zeroes non-argument locals, selects @@ -59,14 +76,14 @@ impl ExecutionState { self.frames.push(Frame { base: 0, local_count: locals, - // Frame's shared record retains this legacy field; this path does - // not track source spans or implement a Span instruction. + // Recorded by the `span` instruction for the frame it executes in. span: (0, 0, 0), function: Some(function as usize), ret_pc: 0, }); self.pc = target; self.halted = false; + self.paused = false; Ok(()) } @@ -133,6 +150,7 @@ impl Reset for ExecutionState { self.frames.clear(); self.pc = 0; self.halted = true; + self.paused = false; } } diff --git a/crates/vihaco-stdlib/src/loadable.rs b/crates/vihaco-stdlib/src/loadable.rs index 8b4241339..ca6bf4440 100644 --- a/crates/vihaco-stdlib/src/loadable.rs +++ b/crates/vihaco-stdlib/src/loadable.rs @@ -67,8 +67,8 @@ where /// /// # Errors /// - /// Rejects replacement while an invocation is active, including one that - /// could resume after an execution error. Call [`Self::reset`] to abandon + /// Rejects replacement while an invocation is active, including one paused + /// at a breakpoint or one that could resume after an execution error. Call [`Self::reset`] to abandon /// that invocation first. Rejection leaves the current image and execution /// state unchanged. pub fn replace_image(&mut self, image: Image) -> eyre::Result<()> { @@ -107,6 +107,10 @@ where /// Continue the existing invocation against the same paired image. /// + /// This also continues past a `breakpoint`; check + /// [`ExecutionState::is_paused`] after either call to tell a pause apart + /// from completion. + /// /// # Errors /// /// Returns the runner's state, instruction-fetch, or dispatch errors without diff --git a/crates/vihaco-stdlib/src/program/dialect.rs b/crates/vihaco-stdlib/src/program/dialect.rs index 88ff2faf3..579780615 100644 --- a/crates/vihaco-stdlib/src/program/dialect.rs +++ b/crates/vihaco-stdlib/src/program/dialect.rs @@ -34,6 +34,16 @@ pub mod program { #[pattern = "'halt"] pub struct Halt; + /// Suspend the invocation after this instruction; `resume` continues it. + #[instruction] + #[pattern = "'breakpoint"] + pub struct Breakpoint; + + /// Record the current frame's source span as `(file, start, end)`. + #[instruction] + #[pattern = "'span $0 $1 $2"] + pub struct Span(pub u32, pub u32, pub u32); + #[instruction] #[pattern = "'load_i32 $0"] pub struct LoadI32(pub u32); diff --git a/crates/vihaco-stdlib/tests/execution_integration.rs b/crates/vihaco-stdlib/tests/execution_integration.rs index b9f8a86de..c9cdfdfbb 100644 --- a/crates/vihaco-stdlib/tests/execution_integration.rs +++ b/crates/vihaco-stdlib/tests/execution_integration.rs @@ -241,6 +241,77 @@ fn recursive_execution_preserves_continuations_and_multiple_results() { assert_eq!(vm.program.pc(), 3); } +#[test] +fn breakpoint_pauses_and_resume_continues_after_it() { + let (module, mut vm) = machine(&[ + "program::program::const_u64 20", + "program::program::breakpoint", + "program::program::const_u64 22", + "cpu::cpu::add_u64", + "program::program::breakpoint", + "program::program::ret 1", + ]); + execute(&module, &mut vm, &[]).unwrap(); + assert!(vm.program.is_paused()); + assert!(!vm.program.is_halted()); + assert_eq!(vm.program.pc(), 2); + assert_eq!(vm.program.stack(), &[0, 20]); + // A paused invocation is still active. + assert!(execute(&module, &mut vm, &[]).is_err()); + resume(&module, &mut vm).unwrap(); + assert!(vm.program.is_paused()); + assert_eq!(vm.program.pc(), 5); + assert_eq!(vm.program.stack(), &[0, 42]); + resume(&module, &mut vm).unwrap(); + assert!(!vm.program.is_paused()); + assert!(vm.program.is_halted()); + assert_eq!(vm.program.stack(), &[42]); + // Reset clears a pause so the invocation can be restarted. + execute(&module, &mut vm, &[]).unwrap(); + assert!(vm.program.is_paused()); + vm.program.reset(); + assert!(!vm.program.is_paused()); + execute(&module, &mut vm, &[]).unwrap(); + assert_eq!(vm.program.pc(), 2); +} + +#[test] +fn span_is_recorded_per_frame_and_restored_on_return() { + let (mut module, mut vm) = machine(&[ + "program::program::span 1 10 20", + "program::program::call 0, 4", + "program::program::breakpoint", + "program::program::ret 0", + "program::program::breakpoint", + "program::program::span 2 30 40", + "program::program::breakpoint", + "program::program::ret 0", + ]); + module.functions[0].local_count = 0; + module.functions[0].end_address = 4; + module.functions.push(FunctionInfo { + name: 0, + signature: Signature { + params: vec![], + ret: vec![], + }, + local_count: 0, + start_address: 4, + end_address: 8, + file: 0, + }); + assert_eq!(vm.program.span(), None); + execute(&module, &mut vm, &[]).unwrap(); + assert_eq!(vm.program.span(), Some((0, 0, 0))); // Fresh callee frame. + resume(&module, &mut vm).unwrap(); + assert_eq!(vm.program.span(), Some((2, 30, 40))); + resume(&module, &mut vm).unwrap(); + assert_eq!(vm.program.span(), Some((1, 10, 20))); // Caller's span restored. + resume(&module, &mut vm).unwrap(); + assert!(vm.program.is_halted()); + assert_eq!(vm.program.span(), None); +} + #[test] fn execution_failures_preserve_consumption_and_faulting_pc() { let (module, mut vm) = machine(&[