feat: add bounded source step-over and step-out
Extend mode-aware stepping across runtime, image API, protocol and VS Code while retaining default step-into behavior. Test recursive calls, nested breakpoints, async and lifecycle boundaries, and real editor navigation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
19eaeb447c
commit
68afb435f3
|
|
@ -56,6 +56,9 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Bounded source step-over and step-out through the runtime, image API, protocol,
|
||||||
|
and VS Code actor commands, preserving nested breakpoints and async stop boundaries.
|
||||||
|
|
||||||
- Actor-local, implementation-bound source breakpoints with explicit line
|
- Actor-local, implementation-bound source breakpoints with explicit line
|
||||||
verification and scheduler pauses that retain active drafts and buffered sends.
|
verification and scheduler pauses that retain active drafts and buffered sends.
|
||||||
- Bounded source step-into through the image API/protocol and VS Code actor
|
- Bounded source step-into through the image API/protocol and VS Code actor
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ slices, and actor snapshots report basic active-execution progress. Native
|
||||||
callbacks are not preemptible. The debugger foundation exposes bounded active Rune
|
callbacks are not preemptible. The debugger foundation exposes bounded active Rune
|
||||||
stack frames and exact loaded source through read-only protocol inspection and
|
stack frames and exact loaded source through read-only protocol inspection and
|
||||||
VS Code's **Inspect Execution Stack...** actor action. Actor-local source
|
VS Code's **Inspect Execution Stack...** actor action. Actor-local source
|
||||||
breakpoints and bounded step-into are implemented through the protocol and VS
|
breakpoints and bounded step-into/over/out are implemented through the protocol and VS
|
||||||
Code actor actions; step-over/out, locals, DAP, and persistent storage remain
|
Code actor actions; locals, DAP, and persistent storage remain
|
||||||
unimplemented.
|
unimplemented.
|
||||||
|
|
||||||
## Design goals
|
## Design goals
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,15 @@ pub enum DebugStopReason {
|
||||||
Removed,
|
Removed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum StepMode {
|
||||||
|
#[default]
|
||||||
|
Into,
|
||||||
|
Over,
|
||||||
|
Out,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct DebuggerSnapshot {
|
pub struct DebuggerSnapshot {
|
||||||
pub breakpoints: Vec<SourceBreakpoint>,
|
pub breakpoints: Vec<SourceBreakpoint>,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ mod value;
|
||||||
pub use diagnostic::{DiagnosticSeverity, SourceDiagnostic, SourcePosition, SourceRange};
|
pub use diagnostic::{DiagnosticSeverity, SourceDiagnostic, SourcePosition, SourceRange};
|
||||||
pub use execution::{
|
pub use execution::{
|
||||||
DebugStopReason, DebuggerSnapshot, ExecutionInspection, SourceBreakpoint, StackFrame,
|
DebugStopReason, DebuggerSnapshot, ExecutionInspection, SourceBreakpoint, StackFrame,
|
||||||
StackFrameKind,
|
StackFrameKind, StepMode,
|
||||||
};
|
};
|
||||||
pub use log::{LogEntry, LogLevel};
|
pub use log::{LogEntry, LogLevel};
|
||||||
pub use value::{ActorRef, MAX_VALUE_BYTES, MAX_VALUE_DEPTH, MAX_VALUE_NODES, Value};
|
pub use value::{ActorRef, MAX_VALUE_BYTES, MAX_VALUE_DEPTH, MAX_VALUE_NODES, Value};
|
||||||
|
|
|
||||||
|
|
@ -382,7 +382,17 @@ impl ImageApi {
|
||||||
expected: ImplementationId,
|
expected: ImplementationId,
|
||||||
budget: NonZeroUsize,
|
budget: NonZeroUsize,
|
||||||
) -> RuntimeResult<runa_core::DebugStopReason> {
|
) -> RuntimeResult<runa_core::DebugStopReason> {
|
||||||
let result = self.runtime.step_source(actor, expected, budget);
|
self.step_source_mode(actor, expected, budget, runa_core::StepMode::Into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step_source_mode(
|
||||||
|
&mut self,
|
||||||
|
actor: ActorRef,
|
||||||
|
expected: ImplementationId,
|
||||||
|
budget: NonZeroUsize,
|
||||||
|
mode: runa_core::StepMode,
|
||||||
|
) -> RuntimeResult<runa_core::DebugStopReason> {
|
||||||
|
let result = self.runtime.step_source_mode(actor, expected, budget, mode);
|
||||||
self.collect_runtime_events();
|
self.collect_runtime_events();
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
pub use runa_core::Value;
|
pub use runa_core::Value;
|
||||||
pub use runa_core::{
|
pub use runa_core::{
|
||||||
DebugStopReason, DebuggerSnapshot, ExecutionInspection, SourceBreakpoint, StackFrame,
|
DebugStopReason, DebuggerSnapshot, ExecutionInspection, SourceBreakpoint, StackFrame,
|
||||||
StackFrameKind,
|
StackFrameKind, StepMode,
|
||||||
};
|
};
|
||||||
pub use runa_core::{DiagnosticSeverity, SourceDiagnostic, SourcePosition, SourceRange};
|
pub use runa_core::{DiagnosticSeverity, SourceDiagnostic, SourcePosition, SourceRange};
|
||||||
pub use runa_core::{LogEntry, LogLevel};
|
pub use runa_core::{LogEntry, LogLevel};
|
||||||
|
|
@ -70,6 +70,7 @@ impl Request {
|
||||||
actor_ref,
|
actor_ref,
|
||||||
implementation_id,
|
implementation_id,
|
||||||
instruction_budget,
|
instruction_budget,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
validate_actor_ref(actor_ref)?;
|
validate_actor_ref(actor_ref)?;
|
||||||
if implementation_id.is_empty()
|
if implementation_id.is_empty()
|
||||||
|
|
@ -217,6 +218,8 @@ pub enum Command {
|
||||||
actor_ref: String,
|
actor_ref: String,
|
||||||
implementation_id: String,
|
implementation_id: String,
|
||||||
instruction_budget: usize,
|
instruction_budget: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
mode: StepMode,
|
||||||
},
|
},
|
||||||
InspectExecution {
|
InspectExecution {
|
||||||
actor_ref: String,
|
actor_ref: String,
|
||||||
|
|
@ -649,6 +652,48 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_step_modes_are_additive_and_reject_unknown_modes() {
|
||||||
|
let mut wire = serde_json::json!({
|
||||||
|
"version": PROTOCOL_VERSION, "request_id": "step",
|
||||||
|
"command": {"operation": "step_source", "parameters": {
|
||||||
|
"actor_ref": "ABCDEFGHIJKLMNOPQRSTU", "implementation_id": "impl", "instruction_budget": 100
|
||||||
|
}}
|
||||||
|
});
|
||||||
|
let default: Request = serde_json::from_value(wire.clone()).unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
default.command,
|
||||||
|
Command::StepSource {
|
||||||
|
mode: StepMode::Into,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
for (text, mode) in [
|
||||||
|
("into", StepMode::Into),
|
||||||
|
("over", StepMode::Over),
|
||||||
|
("out", StepMode::Out),
|
||||||
|
] {
|
||||||
|
wire["command"]["parameters"]["mode"] = serde_json::json!(text);
|
||||||
|
let request: Request = serde_json::from_value(wire.clone()).unwrap();
|
||||||
|
assert_eq!(request.validate(), Ok(()));
|
||||||
|
assert!(
|
||||||
|
matches!(request.command, Command::StepSource {mode: actual, ..} if actual == mode)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(request).unwrap()["command"]["parameters"]["mode"],
|
||||||
|
text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for value in [
|
||||||
|
serde_json::json!("next"),
|
||||||
|
serde_json::json!(null),
|
||||||
|
serde_json::json!(1),
|
||||||
|
] {
|
||||||
|
wire["command"]["parameters"]["mode"] = value;
|
||||||
|
assert!(serde_json::from_value::<Request>(wire.clone()).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn request_uses_a_stable_tagged_wire_shape() {
|
fn request_uses_a_stable_tagged_wire_shape() {
|
||||||
let value = serde_json::to_value(request(Command::Send {
|
let value = serde_json::to_value(request(Command::Send {
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,16 @@ impl Runtime {
|
||||||
id: ActorRef,
|
id: ActorRef,
|
||||||
expected: ImplementationId,
|
expected: ImplementationId,
|
||||||
budget: NonZeroUsize,
|
budget: NonZeroUsize,
|
||||||
|
) -> ExperimentResult<DebugStopReason> {
|
||||||
|
self.step_source_mode(id, expected, budget, runa_core::StepMode::Into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step_source_mode(
|
||||||
|
&mut self,
|
||||||
|
id: ActorRef,
|
||||||
|
expected: ImplementationId,
|
||||||
|
budget: NonZeroUsize,
|
||||||
|
mode: runa_core::StepMode,
|
||||||
) -> ExperimentResult<DebugStopReason> {
|
) -> ExperimentResult<DebugStopReason> {
|
||||||
if budget.get() > 10_000 {
|
if budget.get() > 10_000 {
|
||||||
return Err("debug step budget must not exceed 10000".into());
|
return Err("debug step budget must not exceed 10000".into());
|
||||||
|
|
@ -270,8 +280,16 @@ impl Runtime {
|
||||||
if artifact
|
if artifact
|
||||||
.program()
|
.program()
|
||||||
.current_source_line(&stack)
|
.current_source_line(&stack)
|
||||||
.is_some_and(|line| {
|
.is_some_and(|line| match mode {
|
||||||
stack.call_depth != origin.call_depth || Some(line) != origin_line
|
runa_core::StepMode::Into => {
|
||||||
|
stack.call_depth != origin.call_depth || Some(line) != origin_line
|
||||||
|
}
|
||||||
|
runa_core::StepMode::Over => {
|
||||||
|
stack.call_depth < origin.call_depth
|
||||||
|
|| (stack.call_depth == origin.call_depth
|
||||||
|
&& Some(line) != origin_line)
|
||||||
|
}
|
||||||
|
runa_core::StepMode::Out => stack.call_depth < origin.call_depth,
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
reason = DebugStopReason::Step;
|
reason = DebugStopReason::Step;
|
||||||
|
|
@ -838,6 +856,182 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use runa_core::MAX_VALUE_NODES;
|
use runa_core::MAX_VALUE_NODES;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn debugger_step_over_skips_nested_calls_and_step_out_returns_to_caller() -> ExperimentResult<()>
|
||||||
|
{
|
||||||
|
use runa_core::StepMode;
|
||||||
|
let mut runtime = Runtime::default();
|
||||||
|
let code = publish(
|
||||||
|
&mut runtime,
|
||||||
|
"pub fn handle(ctx, state, sender, message) {\n let next = outer(state);\n next + 1\n}\nfn outer(value) {\n let result = inner(value);\n result + 1\n}\nfn inner(value) {\n value + 1\n}",
|
||||||
|
)?;
|
||||||
|
let actor = runtime.spawn(code, Value::Integer(3), capacity(2))?;
|
||||||
|
runtime.set_breakpoints(actor, code, &[1])?;
|
||||||
|
runtime.send(actor, Value::Null)?;
|
||||||
|
runtime.tick(capacity(100));
|
||||||
|
runtime.set_breakpoints(actor, code, &[])?;
|
||||||
|
assert_eq!(
|
||||||
|
runtime.step_source_mode(actor, code, capacity(1000), StepMode::Over)?,
|
||||||
|
DebugStopReason::Step
|
||||||
|
);
|
||||||
|
let over = runtime.inspect_execution(actor)?.unwrap();
|
||||||
|
assert_eq!(over.call_depth, 0);
|
||||||
|
assert_eq!(over.frames[0].function.as_deref(), Some("handle"));
|
||||||
|
assert_eq!(over.frames[0].range.unwrap().start.line, 2);
|
||||||
|
assert_eq!(runtime.inspect(actor)?.state, Value::Integer(3));
|
||||||
|
assert_eq!(
|
||||||
|
runtime.step_source_mode(actor, code, capacity(1000), StepMode::Out)?,
|
||||||
|
DebugStopReason::Completed
|
||||||
|
);
|
||||||
|
assert_eq!(runtime.inspect(actor)?.state, Value::Integer(6));
|
||||||
|
assert_eq!(runtime.inspect(actor)?.status, Status::Paused);
|
||||||
|
|
||||||
|
runtime.restart(actor)?;
|
||||||
|
runtime.set_breakpoints(actor, code, &[8])?;
|
||||||
|
runtime.send(actor, Value::Null)?;
|
||||||
|
runtime.tick(capacity(100));
|
||||||
|
assert_eq!(runtime.inspect_execution(actor)?.unwrap().call_depth, 2);
|
||||||
|
runtime.set_breakpoints(actor, code, &[])?;
|
||||||
|
assert_eq!(
|
||||||
|
runtime.step_source_mode(actor, code, capacity(1000), StepMode::Out)?,
|
||||||
|
DebugStopReason::Step
|
||||||
|
);
|
||||||
|
let out = runtime.inspect_execution(actor)?.unwrap();
|
||||||
|
assert_eq!(out.call_depth, 1);
|
||||||
|
assert_eq!(out.frames[0].function.as_deref(), Some("outer"));
|
||||||
|
assert_eq!(runtime.inspect(actor)?.state, Value::Integer(6));
|
||||||
|
assert_eq!(
|
||||||
|
runtime.step_source_mode(actor, code, capacity(1000), StepMode::Out)?,
|
||||||
|
DebugStopReason::Step
|
||||||
|
);
|
||||||
|
assert_eq!(runtime.inspect_execution(actor)?.unwrap().call_depth, 0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn debugger_step_modes_respect_nested_breakpoints_and_recursive_depth() -> ExperimentResult<()>
|
||||||
|
{
|
||||||
|
use runa_core::StepMode;
|
||||||
|
for mode in [StepMode::Over, StepMode::Out] {
|
||||||
|
let mut runtime = Runtime::default();
|
||||||
|
let code = publish(
|
||||||
|
&mut runtime,
|
||||||
|
"pub fn handle(ctx, state, sender, message) {\n recurse(4)\n}\nfn recurse(n) {\n if n == 0 { return 7; }\n let value = recurse(n - 1);\n value + 1\n}",
|
||||||
|
)?;
|
||||||
|
let actor = runtime.spawn(code, Value::Integer(0), capacity(2))?;
|
||||||
|
runtime.set_breakpoints(actor, code, &[1])?;
|
||||||
|
runtime.send(actor, Value::Null)?;
|
||||||
|
runtime.tick(capacity(100));
|
||||||
|
runtime.set_breakpoints(actor, code, &[5])?;
|
||||||
|
assert_eq!(
|
||||||
|
runtime.step_source_mode(actor, code, capacity(1000), mode)?,
|
||||||
|
DebugStopReason::Breakpoint
|
||||||
|
);
|
||||||
|
assert_eq!(runtime.inspect_execution(actor)?.unwrap().call_depth, 1);
|
||||||
|
runtime.set_breakpoints(actor, code, &[])?;
|
||||||
|
assert_eq!(
|
||||||
|
runtime.step_source_mode(actor, code, capacity(1000), StepMode::Over)?,
|
||||||
|
DebugStopReason::Step
|
||||||
|
);
|
||||||
|
let over = runtime.inspect_execution(actor)?.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
over.call_depth, 1,
|
||||||
|
"recursive calls must not count as returning to the origin frame"
|
||||||
|
);
|
||||||
|
assert_eq!(over.frames[0].range.unwrap().start.line, 6);
|
||||||
|
assert_eq!(runtime.inspect(actor)?.state, Value::Integer(0));
|
||||||
|
assert_eq!(
|
||||||
|
runtime.step_source_mode(actor, code, capacity(1000), StepMode::Out)?,
|
||||||
|
DebugStopReason::Step
|
||||||
|
);
|
||||||
|
assert_eq!(runtime.inspect_execution(actor)?.unwrap().call_depth, 0);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn debugger_over_and_out_keep_budget_await_and_failure_boundaries() -> ExperimentResult<()> {
|
||||||
|
use runa_core::StepMode;
|
||||||
|
for mode in [StepMode::Over, StepMode::Out] {
|
||||||
|
let mut runtime = Runtime::default();
|
||||||
|
let code = publish(
|
||||||
|
&mut runtime,
|
||||||
|
"pub fn handle(ctx, state, sender, message) {\n spin()\n}\nfn spin() { loop {} }",
|
||||||
|
)?;
|
||||||
|
let actor = runtime.spawn(code, Value::Null, capacity(2))?;
|
||||||
|
runtime.set_breakpoints(actor, code, &[1])?;
|
||||||
|
runtime.send(actor, Value::Null)?;
|
||||||
|
runtime.tick(capacity(100));
|
||||||
|
let before = runtime.inspect_execution(actor)?.unwrap().vm_steps;
|
||||||
|
assert_eq!(
|
||||||
|
runtime.step_source_mode(actor, code, capacity(20), mode)?,
|
||||||
|
DebugStopReason::Budget
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
runtime.inspect_execution(actor)?.unwrap().vm_steps,
|
||||||
|
before + 20
|
||||||
|
);
|
||||||
|
assert_eq!(runtime.inspect(actor)?.status, Status::Paused);
|
||||||
|
|
||||||
|
let code = publish(
|
||||||
|
&mut runtime,
|
||||||
|
"pub async fn handle(ctx, state, sender, message) {\n native_suspend(7).await;\n 42\n}",
|
||||||
|
)?;
|
||||||
|
let actor = runtime.spawn(code, Value::Integer(1), capacity(2))?;
|
||||||
|
runtime.set_breakpoints(actor, code, &[1])?;
|
||||||
|
runtime.send(actor, Value::Null)?;
|
||||||
|
runtime.tick(capacity(100));
|
||||||
|
runtime.set_breakpoints(actor, code, &[])?;
|
||||||
|
assert_eq!(
|
||||||
|
runtime.step_source_mode(actor, code, capacity(100), mode)?,
|
||||||
|
DebugStopReason::Awaiting
|
||||||
|
);
|
||||||
|
assert_eq!(runtime.inspect(actor)?.state, Value::Integer(1));
|
||||||
|
assert_eq!(runtime.inspect(actor)?.status, Status::Paused);
|
||||||
|
runtime.resume(actor)?;
|
||||||
|
runtime.tick(capacity(100));
|
||||||
|
assert_eq!(runtime.inspect(actor)?.state, Value::Integer(42));
|
||||||
|
|
||||||
|
let code = publish(
|
||||||
|
&mut runtime,
|
||||||
|
"pub fn handle(ctx, state, sender, message) {\n crash()\n}\nfn crash() { 1 / 0 }",
|
||||||
|
)?;
|
||||||
|
let actor = runtime.spawn(code, Value::Integer(1), capacity(2))?;
|
||||||
|
runtime.set_breakpoints(actor, code, &[1])?;
|
||||||
|
runtime.send(actor, Value::Null)?;
|
||||||
|
runtime.tick(capacity(100));
|
||||||
|
runtime.set_breakpoints(actor, code, &[])?;
|
||||||
|
assert_eq!(
|
||||||
|
runtime.step_source_mode(actor, code, capacity(100), mode)?,
|
||||||
|
DebugStopReason::Failed
|
||||||
|
);
|
||||||
|
assert!(matches!(runtime.inspect(actor)?.status, Status::Failed(_)));
|
||||||
|
assert_eq!(runtime.inspect(actor)?.reserved, Some(Value::Null));
|
||||||
|
|
||||||
|
let code = publish(
|
||||||
|
&mut runtime,
|
||||||
|
"pub fn handle(ctx, state, sender, message) { state }\npub fn on_shutdown(state) {\n state + 1\n}",
|
||||||
|
)?;
|
||||||
|
let actor = runtime.spawn(code, Value::Integer(1), capacity(2))?;
|
||||||
|
runtime.set_breakpoints(actor, code, &[2])?;
|
||||||
|
runtime.stop(actor)?;
|
||||||
|
runtime.tick(capacity(100));
|
||||||
|
runtime.set_breakpoints(actor, code, &[])?;
|
||||||
|
let mut removed = false;
|
||||||
|
for _ in 0..10 {
|
||||||
|
if runtime.step_source_mode(actor, code, capacity(100), mode)?
|
||||||
|
== DebugStopReason::Removed
|
||||||
|
{
|
||||||
|
removed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(removed);
|
||||||
|
assert!(runtime.inspect(actor).is_err());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn debugger_breakpoints_preserve_transactions_and_step_only_one_actor() -> ExperimentResult<()>
|
fn debugger_breakpoints_preserve_transactions_and_step_only_one_actor() -> ExperimentResult<()>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ impl Server {
|
||||||
actor_ref,
|
actor_ref,
|
||||||
implementation_id,
|
implementation_id,
|
||||||
instruction_budget,
|
instruction_budget,
|
||||||
|
mode,
|
||||||
} => {
|
} => {
|
||||||
let actor = parse_actor_ref(&actor_ref)?;
|
let actor = parse_actor_ref(&actor_ref)?;
|
||||||
let expected = self.image.inspect(actor).map_err(api_error)?.implementation;
|
let expected = self.image.inspect(actor).map_err(api_error)?.implementation;
|
||||||
|
|
@ -97,7 +98,7 @@ impl Server {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
self.image
|
self.image
|
||||||
.step_source(
|
.step_source_mode(
|
||||||
actor,
|
actor,
|
||||||
expected,
|
expected,
|
||||||
NonZeroUsize::new(instruction_budget).ok_or_else(|| {
|
NonZeroUsize::new(instruction_budget).ok_or_else(|| {
|
||||||
|
|
@ -106,6 +107,7 @@ impl Server {
|
||||||
"step budget must be positive",
|
"step budget must be positive",
|
||||||
)
|
)
|
||||||
})?,
|
})?,
|
||||||
|
mode,
|
||||||
)
|
)
|
||||||
.map(|reason| Reply::DebugStepCompleted { reason })
|
.map(|reason| Reply::DebugStepCompleted { reason })
|
||||||
.map_err(api_error)
|
.map_err(api_error)
|
||||||
|
|
|
||||||
|
|
@ -539,9 +539,9 @@ locations through read-only execution inspection. VS Code navigates immutable
|
||||||
exact-loaded-source documents, including old code during pending replacements.
|
exact-loaded-source documents, including old code during pending replacements.
|
||||||
Await locations represent the last safe snapshot; native/hidden async frames,
|
Await locations represent the last safe snapshot; native/hidden async frames,
|
||||||
locals, post-mortem stacks, and DAP remain unimplemented. Actor-specific
|
locals, post-mortem stacks, and DAP remain unimplemented. Actor-specific
|
||||||
implementation-bound line breakpoints and bounded step-into now preserve active
|
implementation-bound line breakpoints and bounded step-into/over/out now preserve active
|
||||||
drafts/effects and use exact loaded-source mappings. These are command-based
|
drafts/effects and use exact loaded-source mappings. These are command-based
|
||||||
controls, not gutter breakpoints; step-over/out and conditional stops remain planned.
|
controls, not gutter breakpoints; conditional stops remain planned.
|
||||||
Non-destructive image-change notifications and automatic snapshot refresh are
|
Non-destructive image-change notifications and automatic snapshot refresh are
|
||||||
implemented over the shared `runa-debug` WebSocket transport.
|
implemented over the shared `runa-debug` WebSocket transport.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ behavior aliases. They exclude the `source/` root and `.rn` extension.
|
||||||
| `spawn` | `path`, tagged `state`, positive `mailbox_capacity` | `actor_spawned`: `{actor_ref}` |
|
| `spawn` | `path`, tagged `state`, positive `mailbox_capacity` | `actor_spawned`: `{actor_ref}` |
|
||||||
| `inspect_execution` | `actor_ref` | `execution_inspected`: loaded source and optional execution stack |
|
| `inspect_execution` | `actor_ref` | `execution_inspected`: loaded source and optional execution stack |
|
||||||
| `set_breakpoints` | `actor_ref`, `implementation_id`, `lines` | `breakpoints_set`: `{breakpoints}` |
|
| `set_breakpoints` | `actor_ref`, `implementation_id`, `lines` | `breakpoints_set`: `{breakpoints}` |
|
||||||
| `step_source` | `actor_ref`, `implementation_id`, `instruction_budget` | `debug_step_completed`: `{reason}` |
|
| `step_source` | `actor_ref`, `implementation_id`, `instruction_budget`, optional `mode` | `debug_step_completed`: `{reason}` |
|
||||||
| `system` | `actor_ref`, `message` | `acknowledged` |
|
| `system` | `actor_ref`, `message` | `acknowledged` |
|
||||||
|
|
||||||
A file is `{path, source_id, source}` and a commit is
|
A file is `{path, source_id, source}` and a commit is
|
||||||
|
|
@ -163,7 +163,7 @@ stopped address once, not all subsequent hits.
|
||||||
|
|
||||||
`step_source` requires a paused actor with an active handler/shutdown invocation,
|
`step_source` requires a paused actor with an active handler/shutdown invocation,
|
||||||
and a positive budget no greater than 10,000. It advances only that actor, stopping
|
and a positive budget no greater than 10,000. It advances only that actor, stopping
|
||||||
at the next mapped line or call-depth change (step-into), another breakpoint,
|
according to the selected mode, another breakpoint,
|
||||||
pending async operation, completion/failure/removal, or budget exhaustion.
|
pending async operation, completion/failure/removal, or budget exhaustion.
|
||||||
`reason` is one of `step`, `breakpoint`, `budget`, `awaiting`, `completed`,
|
`reason` is one of `step`, `breakpoint`, `budget`, `awaiting`, `completed`,
|
||||||
`failed`, `removed`. Failure is an executed actor transition with explicit reason
|
`failed`, `removed`. Failure is an executed actor transition with explicit reason
|
||||||
|
|
@ -173,13 +173,23 @@ boundary; drafts/sends commit only on normal completion. Another explicit step
|
||||||
may poll a pending async operation once; steps do not block waiting for it.
|
may poll a pending async operation once; steps do not block waiting for it.
|
||||||
Native calls remain cooperative, not preemptible.
|
Native calls remain cooperative, not preemptible.
|
||||||
|
|
||||||
|
`mode` is `into` (default when absent), `over`, or `out`; other values are rejected.
|
||||||
|
Into stops at another mapped line or call-depth change. Over skips deeper calls
|
||||||
|
and stops on another mapped line at the original depth or any mapped shallower
|
||||||
|
frame. Out stops at a mapped shallower frame, or completes the invocation when
|
||||||
|
started at the outermost frame. Nested breakpoints interrupt any mode. Budgets,
|
||||||
|
pending awaits, failure and removal retain their usual stop semantics. Each
|
||||||
|
request uses the current frame as its origin; no pending over/out plan survives
|
||||||
|
a budget/await/breakpoint stop. Omitted-mode requests keep their existing into
|
||||||
|
behavior. Over/out require a server implementing this additive mode field.
|
||||||
|
|
||||||
`execution_inspected` adds `debugger: {breakpoints, stop_reason}` even when
|
`execution_inspected` adds `debugger: {breakpoints, stop_reason}` even when
|
||||||
`execution` is null. Older replies may omit it; Rust defaults to an empty
|
`execution` is null. Older replies may omit it; Rust defaults to an empty
|
||||||
configuration and no reason. `stop_reason` is nullable and uses the same reason
|
configuration and no reason. `stop_reason` is nullable and uses the same reason
|
||||||
strings as step replies. Installation clears breakpoints; restart/cancel retain
|
strings as step replies. Installation clears breakpoints; restart/cancel retain
|
||||||
them for unchanged loaded code and clear stop reasons. Debugger state is actor
|
them for unchanged loaded code and clear stop reasons. Debugger state is actor
|
||||||
local, shared by clients, and in-memory; it is not owned by a client connection.
|
local, shared by clients, and in-memory; it is not owned by a client connection.
|
||||||
There are no conditions, hit counts, step-over/out, or gutter/DAP breakpoints yet.
|
There are no conditions, hit counts, or gutter/DAP breakpoints yet.
|
||||||
|
|
||||||
System message shape is `{kind: "stop"|"kill"|"restart"|"pause"|"resume"|"reload"}`
|
System message shape is `{kind: "stop"|"kill"|"restart"|"pause"|"resume"|"reload"}`
|
||||||
or `{kind: "become", path: "actors/other"}`. This is a trusted host request;
|
or `{kind: "become", path: "actors/other"}`. This is a trusted host request;
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,20 @@ Other actors still receive their ordinary scheduler slices.
|
||||||
`step_source` requires a paused **active** invocation and an expected loaded
|
`step_source` requires a paused **active** invocation and an expected loaded
|
||||||
implementation identity. It polls only that actor until the next mapped line
|
implementation identity. It polls only that actor until the next mapped line
|
||||||
or call-depth change, a breakpoint, pending async operation, completion, failure,
|
or call-depth change, a breakpoint, pending async operation, completion, failure,
|
||||||
or the instruction budget. It is step-into, not step-over/out. Budgets range
|
or the instruction budget. The optional mode defaults to `into`; `over` skips
|
||||||
|
deeper calls and stops at another line in the same frame or a mapped caller
|
||||||
|
location, while `out` stops only at a mapped location in a shallower frame.
|
||||||
|
At the outermost frame, step-out runs toward handler completion. Breakpoints
|
||||||
|
in deeper calls still take priority in all modes. Depth comparisons distinguish
|
||||||
|
recursive calls even when the function and source line are the same. Budgets range
|
||||||
from 1 to 10,000 (VS Code uses 1,000); same-line loops and unmapped instructions
|
from 1 to 10,000 (VS Code uses 1,000); same-line loops and unmapped instructions
|
||||||
are bounded rather than allowed to run indefinitely. Native calls are still
|
are bounded rather than allowed to run indefinitely. Native calls are still
|
||||||
not preemptible. A pending async poll returns immediately with `awaiting`;
|
not preemptible. A pending async poll returns immediately with `awaiting`;
|
||||||
another explicit step may poll it again. Native/hidden future stacks remain
|
another explicit step may poll it again. Native/hidden future stacks remain
|
||||||
outside source stepping.
|
outside source stepping.
|
||||||
|
Each request starts from the actor's current frame and line. After a budget,
|
||||||
|
await, or breakpoint stop, another request starts a new step from that stop;
|
||||||
|
there is no hidden step plan retained across requests.
|
||||||
|
|
||||||
The actor stays paused even after successful handler completion, with normal
|
The actor stays paused even after successful handler completion, with normal
|
||||||
state/send commit and no automatic start of the next message. Resume then
|
state/send commit and no automatic start of the next message. Resume then
|
||||||
|
|
@ -57,7 +65,7 @@ stale line mappings. Removal discards debugger state. Steps and breakpoint
|
||||||
configuration changes broadcast `image_changed`.
|
configuration changes broadcast `image_changed`.
|
||||||
|
|
||||||
These are explicit actor controls, not a DAP session or VS Code gutter
|
These are explicit actor controls, not a DAP session or VS Code gutter
|
||||||
breakpoints. CLI debugger commands, conditions, hit counts, step-over/out, locals,
|
breakpoints. CLI debugger commands, conditions, hit counts, locals,
|
||||||
and post-mortem stacks are not implemented.
|
and post-mortem stacks are not implemented.
|
||||||
|
|
||||||
## Actor repair and logging
|
## Actor repair and logging
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ or Debug Adapter Protocol support yet.
|
||||||
For automation, `runa.inspectExecution` accepts `{actor_ref: string}` and an
|
For automation, `runa.inspectExecution` accepts `{actor_ref: string}` and an
|
||||||
optional zero-based frame index to navigate without the picker.
|
optional zero-based frame index to navigate without the picker.
|
||||||
|
|
||||||
### Source breakpoints and step-into
|
### Source breakpoints and stepping
|
||||||
|
|
||||||
Use the actor context menu or Command Palette:
|
Use the actor context menu or Command Palette:
|
||||||
|
|
||||||
|
|
@ -170,6 +170,15 @@ Use the actor context menu or Command Palette:
|
||||||
4. **Resume Actor** and advance the scheduler to continue normally. Resume
|
4. **Resume Actor** and advance the scheduler to continue normally. Resume
|
||||||
crosses the stopped breakpoint once; a loop can hit it again.
|
crosses the stopped breakpoint once; a loop can hit it again.
|
||||||
|
|
||||||
|
**Step Over Source Line** skips deeper function calls and stops at the next
|
||||||
|
mapped line in the original frame or at a mapped caller location.
|
||||||
|
**Step Out of Function** runs toward a mapped caller location; from the outermost
|
||||||
|
handler frame it runs toward completion. Both still stop at nested breakpoints,
|
||||||
|
pending awaits, failures, or the 1,000-instruction budget. Every command uses the
|
||||||
|
current frame/line as its origin: after an interrupted step, the next command
|
||||||
|
starts a new step rather than resuming a hidden plan. All three commands open
|
||||||
|
the exact loaded-source location and leave surviving nonfailed actors paused.
|
||||||
|
|
||||||
An idle actor cannot be stepped: set a breakpoint, send, then tick first, or
|
An idle actor cannot be stepped: set a breakpoint, send, then tick first, or
|
||||||
pause an already active handler. A completed step commits normally and leaves
|
pause an already active handler. A completed step commits normally and leaves
|
||||||
the actor paused at the handler boundary, without consuming another message.
|
the actor paused at the handler boundary, without consuming another message.
|
||||||
|
|
@ -178,12 +187,14 @@ Same-line loops are bounded and pending awaits return immediately.
|
||||||
Installation clears breakpoints to prevent stale source mappings; pending edits
|
Installation clears breakpoints to prevent stale source mappings; pending edits
|
||||||
do not. Breakpoints belong to the actor/image, so disconnect does not clear
|
do not. Breakpoints belong to the actor/image, so disconnect does not clear
|
||||||
them. Restart on unchanged code retains them. These are command-based controls,
|
them. Restart on unchanged code retains them. These are command-based controls,
|
||||||
not editor-gutter breakpoints or an F5/debug-toolbar session. Step-over/out,
|
not editor-gutter breakpoints or an F5/debug-toolbar session. Conditions, hit
|
||||||
conditions, hit counts, and variables remain planned.
|
counts, and variables remain planned.
|
||||||
|
|
||||||
For automation, `runa.setBreakpoints` accepts `{actor_ref: string}` and optional
|
For automation, `runa.setBreakpoints` accepts `{actor_ref: string}` and optional
|
||||||
comma-separated 1-based line text (including `""` to clear). `runa.stepSource`
|
comma-separated 1-based line text (including `""` to clear). `runa.stepSource`
|
||||||
accepts `{actor_ref: string}` and returns its stop reason.
|
accepts `{actor_ref: string}` and returns its stop reason.
|
||||||
|
`runa.stepOver` and `runa.stepOut` accept the same argument and return stop reasons.
|
||||||
|
Use a server built with mode-aware stepping for these new commands.
|
||||||
|
|
||||||
For automation, the `runa.connect` command also accepts an explicit endpoint
|
For automation, the `runa.connect` command also accepts an explicit endpoint
|
||||||
string, and `runa.inspect` accepts an object with an `actor_ref` field.
|
string, and `runa.inspect` accepts an object with an `actor_ref` field.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,82 @@
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import { ImageClient } from "../src/client";
|
import { ImageClient } from "../src/client";
|
||||||
import { ProtocolError } from "../src/protocol";
|
import { ProtocolError, STEP_MODES } from "../src/protocol";
|
||||||
import { parseValue } from "../src/values";
|
import { parseValue } from "../src/values";
|
||||||
import { startImage, control } from "./server";
|
import { startImage, control } from "./server";
|
||||||
|
|
||||||
|
test(
|
||||||
|
"real image supports over/out and gives nested breakpoints priority",
|
||||||
|
{ timeout: 10000 },
|
||||||
|
async (t) => {
|
||||||
|
const image = await startImage();
|
||||||
|
const client = await ImageClient.connect(image.address, (error) =>
|
||||||
|
assert.equal(error, undefined),
|
||||||
|
);
|
||||||
|
t.after(async () => {
|
||||||
|
await client.close();
|
||||||
|
await image.stop();
|
||||||
|
});
|
||||||
|
const source =
|
||||||
|
"pub fn handle(ctx, state, sender, message) {\n let next = nested(state);\n next + 1\n}\nfn nested(value) {\n value + 1\n}";
|
||||||
|
const file = await client.writeSource("tests/step_modes", null, source);
|
||||||
|
const actor = await client.spawn(file.path, parseValue("10"));
|
||||||
|
const other = await client.spawn(file.path, parseValue("20"));
|
||||||
|
const loaded = await client.inspectExecution(actor);
|
||||||
|
await client.setBreakpoints(actor, loaded.implementation_id, [1]);
|
||||||
|
await client.send(actor, parseValue("null"));
|
||||||
|
await client.tick(1, 1000);
|
||||||
|
await client.setBreakpoints(actor, loaded.implementation_id, []);
|
||||||
|
await client.send(other, parseValue("null"));
|
||||||
|
const otherBefore = await client.inspectActor(other);
|
||||||
|
assert.equal(await client.stepSource(actor, loaded.implementation_id, 1000, "over"), "step");
|
||||||
|
let stack = (await client.inspectExecution(actor)).execution;
|
||||||
|
assert.equal(stack?.call_depth, 0n);
|
||||||
|
assert.equal(stack?.frames[0]?.range?.start.line, 2);
|
||||||
|
assert.deepEqual((await client.inspectActor(actor)).state, parseValue("10"));
|
||||||
|
assert.deepEqual(await client.inspectActor(other), otherBefore);
|
||||||
|
assert.equal(
|
||||||
|
await client.stepSource(actor, loaded.implementation_id, 1000, "out"),
|
||||||
|
"completed",
|
||||||
|
);
|
||||||
|
assert.deepEqual((await client.inspectActor(actor)).state, parseValue("12"));
|
||||||
|
assert.equal((await client.inspectActor(actor)).status.status, "paused");
|
||||||
|
await client.system(actor, { kind: "restart" });
|
||||||
|
await client.setBreakpoints(actor, loaded.implementation_id, [1]);
|
||||||
|
await client.send(actor, parseValue("null"));
|
||||||
|
await client.tick(1, 1000);
|
||||||
|
await client.setBreakpoints(actor, loaded.implementation_id, [5]);
|
||||||
|
assert.equal(
|
||||||
|
await client.stepSource(actor, loaded.implementation_id, 1000, "over"),
|
||||||
|
"breakpoint",
|
||||||
|
);
|
||||||
|
assert.equal((await client.inspectExecution(actor)).execution?.call_depth, 1n);
|
||||||
|
await client.setBreakpoints(actor, loaded.implementation_id, []);
|
||||||
|
assert.equal(await client.stepSource(actor, loaded.implementation_id, 1000, "out"), "step");
|
||||||
|
stack = (await client.inspectExecution(actor)).execution;
|
||||||
|
assert.equal(stack?.call_depth, 0n);
|
||||||
|
assert.equal(stack?.frames[0]?.function, "handle");
|
||||||
|
await assert.rejects(
|
||||||
|
control(image.address, {
|
||||||
|
operation: "step_source",
|
||||||
|
parameters: {
|
||||||
|
actor_ref: actor,
|
||||||
|
implementation_id: loaded.implementation_id,
|
||||||
|
instruction_budget: 100,
|
||||||
|
mode: "unknown",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/Control connection closed/,
|
||||||
|
);
|
||||||
|
for (const mode of STEP_MODES) {
|
||||||
|
await assert.rejects(
|
||||||
|
client.stepSource(actor, "stale", 100, mode),
|
||||||
|
(error) => error instanceof ProtocolError && error.code === "conflict",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"actor breakpoints and source stepping preserve drafts, isolate actors and reject stale implementations",
|
"actor breakpoints and source stepping preserve drafts, isolate actors and reject stale implementations",
|
||||||
{ timeout: 10000 },
|
{ timeout: 10000 },
|
||||||
|
|
|
||||||
|
|
@ -344,7 +344,7 @@ async function sourceDebugging(address: string): Promise<void> {
|
||||||
const client = await ImageClient.connect(address, (error) => assert.equal(error, undefined));
|
const client = await ImageClient.connect(address, (error) => assert.equal(error, undefined));
|
||||||
try {
|
try {
|
||||||
const source =
|
const source =
|
||||||
"pub fn handle(ctx, state, sender, message) {\n let next = nested(state);\n next\n}\nfn nested(value) {\n value + 1\n}";
|
"pub fn handle(ctx, state, sender, message) {\n let next = nested(state);\n next + 1\n}\nfn nested(value) {\n value + 1\n}";
|
||||||
const file = await client.writeSource("tests/source_debugging", null, source);
|
const file = await client.writeSource("tests/source_debugging", null, source);
|
||||||
const actor = await client.spawn(file.path, parseValue("10"));
|
const actor = await client.spawn(file.path, parseValue("10"));
|
||||||
const selected = { actor_ref: actor };
|
const selected = { actor_ref: actor };
|
||||||
|
|
@ -366,6 +366,14 @@ async function sourceDebugging(address: string): Promise<void> {
|
||||||
assert.deepEqual((await client.inspectActor(actor)).state, parseValue("10"));
|
assert.deepEqual((await client.inspectActor(actor)).state, parseValue("10"));
|
||||||
await vscode.commands.executeCommand("runa.setBreakpoints", selected, "");
|
await vscode.commands.executeCommand("runa.setBreakpoints", selected, "");
|
||||||
assert.deepEqual((await client.inspectExecution(actor)).debugger?.breakpoints, []);
|
assert.deepEqual((await client.inspectExecution(actor)).debugger?.breakpoints, []);
|
||||||
|
assert.equal(await vscode.commands.executeCommand("runa.stepOut", selected), "step");
|
||||||
|
const caller = await client.inspectExecution(actor);
|
||||||
|
assert.equal(caller.execution?.call_depth, 0n);
|
||||||
|
assert.equal(caller.execution?.frames[0]?.function, "handle");
|
||||||
|
assert.equal(
|
||||||
|
vscode.window.activeTextEditor?.selection.start.line,
|
||||||
|
caller.execution?.frames[0]?.range?.start.line,
|
||||||
|
);
|
||||||
let completed = false;
|
let completed = false;
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
if ((await vscode.commands.executeCommand("runa.stepSource", selected)) === "completed") {
|
if ((await vscode.commands.executeCommand("runa.stepSource", selected)) === "completed") {
|
||||||
|
|
@ -376,10 +384,21 @@ async function sourceDebugging(address: string): Promise<void> {
|
||||||
assert.ok(completed);
|
assert.ok(completed);
|
||||||
const snapshot = await client.inspectActor(actor);
|
const snapshot = await client.inspectActor(actor);
|
||||||
assert.equal(snapshot.status.status, "paused");
|
assert.equal(snapshot.status.status, "paused");
|
||||||
assert.deepEqual(snapshot.state, parseValue("11"));
|
assert.deepEqual(snapshot.state, parseValue("12"));
|
||||||
|
await client.system(actor, { kind: "restart" });
|
||||||
|
await vscode.commands.executeCommand("runa.setBreakpoints", selected, "2");
|
||||||
|
await client.send(actor, parseValue("null"));
|
||||||
|
await client.tick(1, 1000);
|
||||||
|
await vscode.commands.executeCommand("runa.setBreakpoints", selected, "");
|
||||||
|
assert.equal(await vscode.commands.executeCommand("runa.stepOver", selected), "step");
|
||||||
|
const over = await client.inspectExecution(actor);
|
||||||
|
assert.equal(over.execution?.call_depth, 0n);
|
||||||
|
assert.equal(over.execution?.frames[0]?.range?.start.line, 2);
|
||||||
|
assert.equal(vscode.window.activeTextEditor?.selection.start.line, 2);
|
||||||
|
assert.deepEqual((await client.inspectActor(actor)).state, parseValue("12"));
|
||||||
await client.system(actor, { kind: "kill" });
|
await client.system(actor, { kind: "kill" });
|
||||||
console.log(
|
console.log(
|
||||||
"Runa source debugger: actor breakpoints, nested step navigation, clearing and transactional completion passed",
|
"Runa source debugger: actor breakpoints, step into/over/out navigation, clearing and transactional completion passed",
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
await client.close();
|
await client.close();
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,18 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"commands": [
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "runa.stepOver",
|
||||||
|
"title": "Step Over Source Line",
|
||||||
|
"category": "Runa",
|
||||||
|
"enablement": "runa.connected"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "runa.stepOut",
|
||||||
|
"title": "Step Out of Function",
|
||||||
|
"category": "Runa",
|
||||||
|
"enablement": "runa.connected"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "runa.stepSource",
|
"command": "runa.stepSource",
|
||||||
"title": "Step Into Source Line",
|
"title": "Step Into Source Line",
|
||||||
|
|
@ -323,6 +335,16 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view/item/context": [
|
"view/item/context": [
|
||||||
|
{
|
||||||
|
"command": "runa.stepOver",
|
||||||
|
"when": "view == runa.actors && viewItem == runa.actor && runa.connected",
|
||||||
|
"group": "debug@2a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "runa.stepOut",
|
||||||
|
"when": "view == runa.actors && viewItem == runa.actor && runa.connected",
|
||||||
|
"group": "debug@2b"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "runa.stepSource",
|
"command": "runa.stepSource",
|
||||||
"when": "view == runa.actors && viewItem == runa.actor && runa.connected",
|
"when": "view == runa.actors && viewItem == runa.actor && runa.connected",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import {
|
||||||
ExecutionInspection,
|
ExecutionInspection,
|
||||||
SourceBreakpoint,
|
SourceBreakpoint,
|
||||||
DebugStopReason,
|
DebugStopReason,
|
||||||
|
STEP_MODES,
|
||||||
|
StepMode,
|
||||||
decodeNotification,
|
decodeNotification,
|
||||||
decodeResponse,
|
decodeResponse,
|
||||||
decodeValue,
|
decodeValue,
|
||||||
|
|
@ -263,13 +265,15 @@ export class ImageClient {
|
||||||
actorRef: string,
|
actorRef: string,
|
||||||
implementationId: string,
|
implementationId: string,
|
||||||
budget = 1000,
|
budget = 1000,
|
||||||
|
mode: StepMode = "into",
|
||||||
): Promise<DebugStopReason> {
|
): Promise<DebugStopReason> {
|
||||||
if (
|
if (
|
||||||
!isActorRef(actorRef) ||
|
!isActorRef(actorRef) ||
|
||||||
!implementationId ||
|
!implementationId ||
|
||||||
!Number.isSafeInteger(budget) ||
|
!Number.isSafeInteger(budget) ||
|
||||||
budget < 1 ||
|
budget < 1 ||
|
||||||
budget > 10000
|
budget > 10000 ||
|
||||||
|
!STEP_MODES.includes(mode)
|
||||||
)
|
)
|
||||||
throw new Error("Invalid actor, implementation or source step budget");
|
throw new Error("Invalid actor, implementation or source step budget");
|
||||||
const reply = await this.request({
|
const reply = await this.request({
|
||||||
|
|
@ -278,6 +282,7 @@ export class ImageClient {
|
||||||
actor_ref: actorRef,
|
actor_ref: actorRef,
|
||||||
implementation_id: implementationId,
|
implementation_id: implementationId,
|
||||||
instruction_budget: budget,
|
instruction_budget: budget,
|
||||||
|
mode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (reply.kind !== "debug_step_completed") throw new Error("Expected source step result");
|
if (reply.kind !== "debug_step_completed") throw new Error("Expected source step result");
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
SystemMessage,
|
SystemMessage,
|
||||||
LOG_LEVELS,
|
LOG_LEVELS,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
|
StepMode,
|
||||||
} from "./protocol";
|
} from "./protocol";
|
||||||
import { RefreshQueue } from "./refresh";
|
import { RefreshQueue } from "./refresh";
|
||||||
import { parseValue, formatValue } from "./values";
|
import { parseValue, formatValue } from "./values";
|
||||||
|
|
@ -447,18 +448,23 @@ export function activate(context: vscode.ExtensionContext): {
|
||||||
return breakpoints;
|
return breakpoints;
|
||||||
};
|
};
|
||||||
|
|
||||||
const stepSource = async (selected?: { actor_ref: string }) => {
|
const stepSource = async (selected?: { actor_ref: string }, mode: StepMode = "into") => {
|
||||||
const connection = active();
|
const connection = active();
|
||||||
const actorRef = await chooseActor(selected);
|
const actorRef = await chooseActor(selected);
|
||||||
if (!actorRef) return;
|
if (!actorRef) return;
|
||||||
connection.check();
|
connection.check();
|
||||||
const inspection = await connection.client.inspectExecution(actorRef);
|
const inspection = await connection.client.inspectExecution(actorRef);
|
||||||
connection.check();
|
connection.check();
|
||||||
const reason = await connection.client.stepSource(actorRef, inspection.implementation_id);
|
const reason = await connection.client.stepSource(
|
||||||
|
actorRef,
|
||||||
|
inspection.implementation_id,
|
||||||
|
1000,
|
||||||
|
mode,
|
||||||
|
);
|
||||||
connection.check();
|
connection.check();
|
||||||
await refresh();
|
await refresh();
|
||||||
connection.check();
|
connection.check();
|
||||||
output.appendLine(`Source step @${actorRef}: ${reason}`);
|
output.appendLine(`Source step ${mode} @${actorRef}: ${reason}`);
|
||||||
if (
|
if (
|
||||||
reason === "step" ||
|
reason === "step" ||
|
||||||
reason === "breakpoint" ||
|
reason === "breakpoint" ||
|
||||||
|
|
@ -939,7 +945,9 @@ export function activate(context: vscode.ExtensionContext): {
|
||||||
command("runa.inspect", inspect),
|
command("runa.inspect", inspect),
|
||||||
command("runa.inspectExecution", inspectExecution),
|
command("runa.inspectExecution", inspectExecution),
|
||||||
command("runa.setBreakpoints", setBreakpoints),
|
command("runa.setBreakpoints", setBreakpoints),
|
||||||
command("runa.stepSource", stepSource),
|
command("runa.stepSource", (selected?: { actor_ref: string }) => stepSource(selected)),
|
||||||
|
command("runa.stepOver", (selected?: { actor_ref: string }) => stepSource(selected, "over")),
|
||||||
|
command("runa.stepOut", (selected?: { actor_ref: string }) => stepSource(selected, "out")),
|
||||||
command("runa.editBehavior", editBehavior),
|
command("runa.editBehavior", editBehavior),
|
||||||
command("runa.goToBehavior", goToBehavior),
|
command("runa.goToBehavior", goToBehavior),
|
||||||
command("runa.updateBehavior", updateBehavior),
|
command("runa.updateBehavior", updateBehavior),
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,8 @@ export const DEBUG_STOP_REASONS = [
|
||||||
"removed",
|
"removed",
|
||||||
] as const;
|
] as const;
|
||||||
export type DebugStopReason = (typeof DEBUG_STOP_REASONS)[number];
|
export type DebugStopReason = (typeof DEBUG_STOP_REASONS)[number];
|
||||||
|
export const STEP_MODES = ["into", "over", "out"] as const;
|
||||||
|
export type StepMode = (typeof STEP_MODES)[number];
|
||||||
export interface SourceBreakpoint {
|
export interface SourceBreakpoint {
|
||||||
line: number;
|
line: number;
|
||||||
instruction_pointers: bigint[];
|
instruction_pointers: bigint[];
|
||||||
|
|
@ -132,7 +134,12 @@ export type Command =
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
operation: "step_source";
|
operation: "step_source";
|
||||||
parameters: { actor_ref: string; implementation_id: string; instruction_budget: number };
|
parameters: {
|
||||||
|
actor_ref: string;
|
||||||
|
implementation_id: string;
|
||||||
|
instruction_budget: number;
|
||||||
|
mode?: StepMode;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
| { operation: "inspect_execution"; parameters: { actor_ref: string } }
|
| { operation: "inspect_execution"; parameters: { actor_ref: string } }
|
||||||
| { operation: "check_source"; parameters: { source: string } }
|
| { operation: "check_source"; parameters: { source: string } }
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||||
import { once } from "node:events";
|
import { once } from "node:events";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import { ImageClient, parseEndpoint } from "../src/client";
|
import { ImageClient, parseEndpoint } from "../src/client";
|
||||||
import { MAX_MESSAGE_BYTES, ProtocolError } from "../src/protocol";
|
import { MAX_MESSAGE_BYTES, ProtocolError, STEP_MODES } from "../src/protocol";
|
||||||
import { actor, fakeImage, json, notification, REF, response } from "./fixtures";
|
import { actor, fakeImage, json, notification, REF, response } from "./fixtures";
|
||||||
|
|
||||||
test("only numeric loopback endpoints and valid ports are accepted", () => {
|
test("only numeric loopback endpoints and valid ports are accepted", () => {
|
||||||
|
|
@ -25,6 +25,21 @@ test("only numeric loopback endpoints and valid ports are accepted", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("source step modes are sent explicitly and retain the default into behavior", async (t) => {
|
||||||
|
const modes: unknown[] = [];
|
||||||
|
const address = await fakeImage(t, (request, socket) => {
|
||||||
|
assert.equal(request.command.operation, "step_source");
|
||||||
|
modes.push(request.command.parameters?.mode);
|
||||||
|
socket.send(response(request, { kind: "debug_step_completed", data: { reason: "step" } }));
|
||||||
|
});
|
||||||
|
const client = await ImageClient.connect(address, (error) => assert.equal(error, undefined));
|
||||||
|
t.after(() => client.close());
|
||||||
|
assert.equal(await client.stepSource(REF, "impl"), "step");
|
||||||
|
for (const mode of STEP_MODES)
|
||||||
|
assert.equal(await client.stepSource(REF, "impl", 100, mode), "step");
|
||||||
|
assert.deepEqual(modes, ["into", "into", "over", "out"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("debugger arguments reject invalid budgets, identities and line lists before transport", async (t) => {
|
test("debugger arguments reject invalid budgets, identities and line lists before transport", async (t) => {
|
||||||
let requests = 0;
|
let requests = 0;
|
||||||
const address = await fakeImage(t, (request, socket) => {
|
const address = await fakeImage(t, (request, socket) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue