Implement and archive Phase 5 debugger

This commit is contained in:
2026-09-06 21:24:38 +02:00
parent ce97700a98
commit e236306657
25 changed files with 3102 additions and 52 deletions

View File

@@ -0,0 +1,612 @@
//! Opt-in debugger control. All execution remains in the ordinary VM.
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DebugLocation {
pub frame: u64,
pub procedure: usize,
pub module: u16,
pub source: u32,
pub line: u32,
pub column: u32,
pub pc: usize,
}
#[derive(Debug, Clone)]
pub struct DebugFrame {
pub id: u64,
pub procedure: String,
pub file: String,
pub location: DebugLocation,
}
#[derive(Debug, Clone)]
pub struct DebugError {
pub code: u16,
pub origin: Option<DebugLocation>,
pub handler: Option<DebugLocation>,
pub resume_pc: usize,
}
#[derive(Default)]
pub struct Debugger {
pub enabled: bool,
pub trace: bool,
pub history_on: bool,
pub history: VecDeque<DebugLocation>,
history_pending: Option<DebugLocation>,
pub break_errors: bool,
pub error: Option<DebugError>,
pub(super) error_pending: bool,
pub immediate_error: Option<String>,
pub compiler: Option<crate::project::DebugCompiler>,
pub watches: Vec<DebugWatch>,
pub(super) next_frame: u64,
pub(super) breakpoints: HashSet<(u16, u32, u32)>,
over: Option<(u64, usize, usize)>,
cursor: Option<(u16, u32, u32)>,
pub(super) immediate: Option<DebugEntry>,
}
#[derive(Debug, Clone)]
pub struct DebugWatch {
pub expression: String,
pub condition: bool,
pub frame: Option<u64>,
pub value: Result<Value, String>,
}
impl Debugger {
pub fn cancel_motion(&mut self) {
self.over = None;
self.cursor = None;
}
}
impl Vm {
pub fn debug_location(&self) -> Option<DebugLocation> {
let f = self.frames.last()?;
Some(self.frame_location(f, f.last_stmt_pc))
}
fn frame_location(&self, f: &Frame, pc: usize) -> DebugLocation {
let code = &self.module.procs[f.proc].code;
let (source, column) = match pc.checked_sub(1).and_then(|p| code.get(p)) {
Some(Instr::Source(s, c)) => (*s, *c),
_ => (f.source, f.column),
};
let line = match code.get(pc) {
Some(Instr::Stmt(l) | Instr::InitStmt(l)) => *l,
_ => f.line,
};
DebugLocation {
frame: f.id,
procedure: f.proc,
module: self.module.sources[source as usize].module,
source,
line,
column,
pc,
}
}
pub fn execution_location(&self) -> Option<DebugLocation> {
let frame = self.frames.last()?;
let code = &self.module.procs[frame.proc].code;
let pc = match code.get(frame.pc) {
Some(Instr::Stmt(_)) => frame.pc,
Some(Instr::Source(..)) if matches!(code.get(frame.pc + 1), Some(Instr::Stmt(_))) => {
frame.pc + 1
}
_ => frame.last_stmt_pc,
};
Some(self.frame_location(frame, pc))
}
pub fn next_debug_location(&self) -> Option<DebugLocation> {
let f = self.frames.last()?;
let pc = self.module.procs[f.proc]
.code
.iter()
.enumerate()
.skip(f.pc)
.find(|(_, i)| matches!(i, Instr::Stmt(l) if *l > 0))
.map(|(i, _)| i)?;
Some(self.frame_location(f, pc))
}
pub fn debug_frames(&self) -> Vec<DebugFrame> {
self.frames
.iter()
.rev()
.map(|f| {
let location = self.frame_location(f, f.last_stmt_pc);
DebugFrame {
id: f.id,
procedure: self.module.procs[f.proc].name.clone(),
file: self.module.sources[location.source as usize].path.clone(),
location,
}
})
.collect()
}
pub fn module_name(&self, module: u16) -> &str {
&self.module.modules[module as usize].0
}
pub fn source_files(&self) -> &[tb_frontend::source::SourceFile] {
&self.module.sources
}
pub fn executable_line(&self, module: u16, source: u32, line: u32) -> bool {
self.statement_locations()
.iter()
.any(|p| p.module == module && p.source == source && p.line == line)
}
pub fn statement_locations(&self) -> Vec<DebugLocation> {
self.module
.procs
.iter()
.enumerate()
.flat_map(|(procedure, proc)| {
proc.code
.iter()
.enumerate()
.filter_map(move |(pc, instruction)| {
let Instr::Stmt(line) = instruction else {
return None;
};
if *line == 0 {
return None;
}
let (source, column) =
match pc.checked_sub(1).and_then(|p| proc.code.get(p)) {
Some(Instr::Source(s, c)) => (*s, *c),
_ => (0, 1),
};
Some(DebugLocation {
frame: 0,
procedure,
module: self.module.sources[source as usize].module,
source,
line: *line,
column,
pc,
})
})
})
.collect()
}
pub fn add_source_breakpoint(&mut self, module: u16, source: u32, line: u32) -> bool {
self.debug.enabled = true;
if !self.executable_line(module, source, line) {
return false;
}
self.debug.breakpoints.insert((module, source, line));
true
}
pub fn clear_source_breakpoints(&mut self) {
self.debug.breakpoints.clear();
}
pub fn step_over(&mut self) {
self.debug.enabled = true;
self.set_step(false);
if let Some(f) = self.frames.last() {
self.debug.over = Some((f.id, self.frames.len(), f.gosub.len()));
}
}
pub fn run_to_cursor(&mut self, module: u16, source: u32, line: u32) -> Result<(), String> {
if !self.executable_line(module, source, line) {
return Err("Cursorzeile ist nicht ausführbar".into());
}
self.debug.enabled = true;
self.set_step(false);
self.debug.cursor = Some((module, source, line));
Ok(())
}
pub(super) fn debug_boundary(&mut self, pc: usize, initializing: bool) -> Option<RunEvent> {
let error_pending = std::mem::take(&mut self.debug.error_pending);
if self
.debug
.immediate
.as_ref()
.is_some_and(|e| self.frames.last().is_some_and(|f| f.proc == e.proc))
{
return error_pending.then_some(RunEvent::Breakpoint {
line: self.current_line(),
});
}
if initializing || self.current_line() == 0 {
return None;
}
let loc = self.debug_location()?;
let f = self.frames.last_mut().unwrap();
let line_entry = f.debug_line.is_none_or(|(source, line, prior)| {
source != loc.source || line != loc.line || pc <= prior
});
f.debug_line = Some((loc.source, loc.line, pc));
if self.debug.history_on {
self.debug.history_pending = Some(loc.clone());
}
if error_pending {
return Some(RunEvent::Breakpoint { line: loc.line });
}
let f = self.frames.last().unwrap();
let key = (loc.module, loc.source, loc.line);
let breakpoint = line_entry && self.debug.breakpoints.contains(&key);
let cursor = self.debug.cursor == Some(key);
let over = self.debug.over.is_some_and(|(id, depth, gosub)| {
(f.id == id && f.gosub.len() <= gosub) || self.frames.len() < depth
});
let conditional = self.check_watchpoints();
if breakpoint || conditional || cursor || over {
self.debug.cancel_motion();
return Some(if breakpoint || conditional || cursor {
RunEvent::Breakpoint { line: loc.line }
} else {
RunEvent::Stepped { line: loc.line }
});
}
None
}
pub(super) fn record_debug_execution(&mut self) {
if let Some(location) = self.debug.history_pending.take() {
if self.debug.history_on
&& self
.frames
.last()
.is_some_and(|f| f.id == location.frame && f.pc == location.pc + 1)
{
if self.debug.history.len() == 1024 {
self.debug.history.pop_front();
}
self.debug.history.push_back(location);
}
}
}
pub fn set_next_statement(&mut self, frame: u64, source: u32, line: u32) -> Result<(), String> {
let f = self.frames.last().ok_or("Kein aktiver Frame")?;
if f.id != frame {
return Err("Nur der aktive Ausführungsrahmen darf verändert werden".into());
}
let target = self
.statement_locations()
.into_iter()
.find(|p| p.procedure == f.proc && p.source == source && p.line == line)
.ok_or("Ziel gehört nicht zum aktiven Prozedurrahmen")?;
if f.pc != f.last_stmt_pc + 1 || self.immediate_active() {
return Err(
"Kein freier Anweisungsbeginn; zuerst bis zur nächsten Grenze fortsetzen".into(),
);
}
if self.in_handler
|| !f.gosub.is_empty()
|| f.eingabe.is_some()
|| f.sleep.is_some()
|| f.waiting_form.is_some()
{
return Err("Aktiver Fehler-, GOSUB- oder Wartekontext erlaubt keinen Sprung".into());
}
let current = self.debug_location().ok_or("Kein Quellkontext")?;
if current.module != target.module {
return Err("Ziel liegt in einem anderen Modulkontext".into());
}
if let Some(compiler) = &self.debug.compiler {
let position = |p: &DebugLocation| tb_frontend::SourcePos {
source: p.source,
line: p.line,
column: p.column,
};
let proc = &self.module.procs[f.proc];
if !compiler.same_control_context(
current.module,
if proc.kind == tb_frontend::hir::HProcKind::Main {
"<main>"
} else {
&proc.name
},
position(&current),
position(&target),
) {
return Err("Ziel hat einen anderen strukturierten Kontrollkontext".into());
}
}
let code = &self.module.procs[f.proc].code;
let context = |at: usize| -> Vec<usize> {
code.iter()
.enumerate()
.filter_map(|(pc, i)| {
let t = match i {
Instr::Jump(t) | Instr::JumpIfFalse(t) | Instr::JumpIfTrue(t) => {
*t as usize
}
_ => return None,
};
((t <= at && at <= pc)
|| (pc < at
&& at < t
&& (!matches!(i, Instr::Jump(_)) || self.debug.compiler.is_none())))
.then_some(pc)
})
.collect()
};
let special = code.iter().any(|i| match i {
Instr::Gosub(t) | Instr::OnErrorLocal(t) | Instr::OnErrorGoto(t) => {
let start = *t as usize;
let end = code
.iter()
.enumerate()
.skip(start)
.find(|(_, i)| {
matches!(
i,
Instr::RetGosub
| Instr::Resume0
| Instr::ResumeNext
| Instr::ResumeLabel(_)
)
})
.map_or(code.len(), |(p, _)| p);
start <= target.pc && target.pc <= end
}
_ => false,
});
if special || context(f.last_stmt_pc) != context(target.pc) {
return Err("Ziel hat einen anderen Kontroll-, Schleifen- oder Handlerkontext".into());
}
let f = self.frames.last_mut().unwrap();
f.pc = target.pc + 1;
f.last_stmt_pc = target.pc;
f.source = target.source;
f.line = target.line;
f.column = target.column;
self.debug.history_pending = self
.debug
.history_on
.then(|| self.debug_location())
.flatten();
self.debug.cancel_motion();
self.frames.last_mut().unwrap().debug_line = Some((target.source, target.line, target.pc));
Ok(())
}
}
pub(super) struct DebugEntry {
pub depth: usize,
pub proc: usize,
pub base: usize,
pub locals_len: usize,
pub strings_len: usize,
pub sources_len: usize,
pub stack: Vec<Value>,
pub flags: u32,
pub history_pending: Option<DebugLocation>,
pub handler_return: Option<(usize, usize)>,
pub error_state: (u16, u32, u32, bool, usize),
}
impl Vm {
fn compile_debug(
&self,
frame: u64,
text: &str,
expression: bool,
) -> Result<crate::project::DebugCode, String> {
let f = self
.frames
.iter()
.find(|f| f.id == frame)
.ok_or("Inspektionsrahmen nicht mehr erreichbar")?;
let module = self.module.sources[f.source as usize].module;
let proc = &self.module.procs[f.proc];
self.debug
.compiler
.as_ref()
.ok_or("Debug-Symbole fehlen")?
.compile(
module,
if proc.kind == tb_frontend::hir::HProcKind::Main {
"<main>"
} else {
&proc.name
},
text,
expression,
)
}
fn install_debug_code(&mut self, mut code: crate::project::DebugCode) -> Result<usize, String> {
let offset = self.module.strings.len();
if offset + code.strings.len() >= u16::MAX as usize {
return Err("Zu viele Debug-Stringkonstanten".into());
}
for i in &mut code.procedure.code {
match i {
Instr::PushStr(id)
| Instr::Unsupported(id)
| Instr::LoadDynamicObjectProperty(id)
| Instr::StoreDynamicObjectProperty(id) => *id += offset as u16,
_ => {}
}
}
self.module.strings.extend(code.strings);
let id = self.module.procs.len();
self.module.procs.push(code.procedure);
Ok(id)
}
/// Side-effect-free bytecode only, executed by exec (no independent evaluator).
pub fn evaluate_watch(&mut self, frame: u64, text: &str) -> Result<Value, String> {
let code = self.compile_debug(frame, text, true)?;
let result_slot = code.procedure.locals_init.len().saturating_sub(1) as u16;
for i in &code.procedure.code {
let mut bytes = Vec::new();
i.encode(&mut bytes);
let pure = match i {
Instr::CallBuiltin(id, _) => *id <= ids::ATN || *id == ids::FORMAT_S,
Instr::StoreLocal(slot) => *slot == result_slot,
Instr::Source(_, _)
| Instr::Stmt(_)
| Instr::LoadGlobal(_)
| Instr::LoadLocal(_)
| Instr::LoadRef(_)
| Instr::LoadArr(..)
| Instr::LoadElem(_)
| Instr::LoadField(_)
| Instr::ArrBound(_)
| Instr::FixStr(_)
| Instr::LoadErr
| Instr::LoadErl
| Instr::RetFn => true,
_ => matches!(bytes[0], 0x10..=0x17 | 0x40..=0x95),
};
if !pure {
return Err(format!("Watch ist nicht nachweislich nebenwirkungsfrei: {i:?}; Aufrufe im Direktfenster verwenden"));
}
}
let f = self
.frames
.iter()
.find(|f| f.id == frame)
.ok_or("Frame nicht erreichbar")?;
let base = f.locals_base;
let count = self.module.procs[f.proc].locals_init.len();
let values = self.locals[base..base + count].to_vec();
let strings_len = self.module.strings.len();
let proc = self.install_debug_code(code)?;
let stack = std::mem::take(&mut self.stack);
self.push_frame(proc, 0);
let new_base = self.frames.last().unwrap().locals_base;
self.locals[new_base..new_base + count].clone_from_slice(&values);
let mut host = tb_runtime::host::CaptureHost::default();
let result = (|| {
for (pc, i) in self.module.procs[proc].code.clone().into_iter().enumerate() {
if matches!(i, Instr::Stmt(_) | Instr::Source(..)) {
continue;
}
if matches!(i, Instr::RetFn) {
return self.pop().map_err(|e| e.to_string());
}
if let Instr::LoadArr(global, slot, ..) = i {
let value = if global {
&self.globals[slot as usize]
} else {
&self.locals[new_base + slot as usize]
};
if matches!(value, Value::Empty) {
return Err(
"Array noch nicht initialisiert; Watch führt kein Auto-DIM aus".into(),
);
}
}
self.exec(i, pc, &mut host).map_err(|e| e.to_string())?;
}
Err("Watch ohne Ergebnis".into())
})();
self.pop_frame();
self.stack = stack;
self.module.procs.truncate(proc);
self.module.strings.truncate(strings_len);
result
}
pub fn evaluate_condition(&mut self, frame: u64, text: &str) -> Result<Value, String> {
let value = self.evaluate_watch(frame, text)?;
if !matches!(
value,
Value::Int(_) | Value::Lng(_) | Value::Sng(_) | Value::Dbl(_) | Value::Cur(_)
) {
return Err("Watchpoint benötigt eine numerische BASIC-Bedingung".into());
}
Ok(value)
}
fn check_watchpoints(&mut self) -> bool {
let Some(frame) = self.frames.last().map(|f| f.id) else {
return false;
};
let mut watches = std::mem::take(&mut self.debug.watches);
let mut stop = false;
for watch in watches.iter_mut().filter(|w| w.condition) {
watch.value = self.evaluate_condition(watch.frame.unwrap_or(frame), &watch.expression);
stop |= watch.value.as_ref().is_ok_and(Self::truthy);
}
self.debug.watches = watches;
stop
}
pub fn immediate_active(&self) -> bool {
self.debug.immediate.is_some()
}
pub fn start_immediate(&mut self, frame: u64, text: &str) -> Result<(), String> {
if self.immediate_active() {
return Err("Direktkommando läuft bereits".into());
}
// Compile all input before changing a stack or a variable.
let code = self.compile_debug(frame, text, false)?;
let f = self
.frames
.iter()
.find(|f| f.id == frame)
.ok_or("Frame nicht erreichbar")?;
let base = f.locals_base;
let source = self.module.sources.len() as u32;
let module = self.module.sources[f.source as usize].module;
let entry = DebugEntry {
depth: self.frames.len(),
proc: self.module.procs.len(),
base,
locals_len: self.locals.len(),
strings_len: self.module.strings.len(),
sources_len: self.module.sources.len(),
stack: self.stack.clone(),
flags: self.flags,
history_pending: self.debug.history_pending.take(),
handler_return: None,
error_state: (
self.err,
self.erl,
self.zeile_nr,
self.in_handler,
self.resume_pc,
),
};
let proc = self.install_debug_code(code)?;
self.module.sources.push(tb_frontend::source::SourceFile {
module,
path: "<Immediate>".into(),
});
for i in &mut self.module.procs[proc].code {
if let Instr::Source(s, _) = i {
*s = source;
}
}
self.debug.immediate_error = None;
self.stack.clear();
self.push_frame(proc, 0);
self.locals.truncate(entry.locals_len);
let f = self.frames.last_mut().unwrap();
f.locals_base = base;
f.source = source;
self.flags &= !F_STEP;
self.debug.cancel_motion();
self.debug.immediate = Some(entry);
Ok(())
}
pub(super) fn restore_debug_handler(&mut self) {
if let Some(entry) = &mut self.debug.immediate {
if self.frames.len() == entry.depth + 1 {
if let Some((stmt, _)) = entry.handler_return.take() {
let f = self.frames.last_mut().unwrap();
f.proc = entry.proc;
f.locals_base = entry.base;
f.last_stmt_pc = stmt;
self.resume_pc = stmt;
}
}
}
}
pub(super) fn finish_immediate(&mut self) {
let Some(entry) = self.debug.immediate.take() else {
return;
};
self.frames.truncate(entry.depth);
self.locals.truncate(entry.locals_len);
self.stack = entry.stack;
self.flags = entry.flags;
(
self.err,
self.erl,
self.zeile_nr,
self.in_handler,
self.resume_pc,
) = entry.error_state;
self.module.procs.truncate(entry.proc);
self.module.strings.truncate(entry.strings_len);
self.module.sources.truncate(entry.sources_len);
self.debug.error = None;
self.debug.history_pending = entry.history_pending;
}
}

View File

@@ -19,6 +19,10 @@ use tb_runtime::traps::Quelle;
use tb_runtime::value::{self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef};
use tb_ui::forms::{FormEvent, FormsModel, PropertyValue, ShowResult};
#[path = "debugger.rs"]
mod debugger;
pub use debugger::*;
/// Warum die VM die Kontrolle abgibt.
#[derive(Debug, Clone, PartialEq)]
pub enum RunEvent {
@@ -69,6 +73,8 @@ enum Handler {
}
struct Frame {
id: u64,
debug_line: Option<(u32, u32, usize)>,
proc: usize,
pc: usize,
locals_base: usize,
@@ -115,6 +121,7 @@ enum FormEventReturn {
pub struct Vm {
module: CompiledModule,
pub debug: Debugger,
globals: Vec<Value>,
locals: Vec<Value>,
stack: Vec<Value>,
@@ -194,6 +201,7 @@ impl Vm {
forms.show(form, false).expect("validiertes Startformular");
}
let mut vm = Vm {
debug: Debugger::default(),
globals,
locals: Vec::new(),
stack: Vec::new(),
@@ -258,7 +266,10 @@ impl Vm {
self.locals[locals_base + i] = v;
}
let stack_base = self.stack.len();
self.debug.next_frame += 1;
self.frames.push(Frame {
id: self.debug.next_frame,
debug_line: None,
source: 0,
column: 0,
proc,
@@ -785,7 +796,10 @@ impl Vm {
fn trap_frame_aufsetzen(&mut self, q: Quelle, ziel: u32) {
let locals_base = self.frames[0].locals_base;
let stack_base = self.stack.len();
self.debug.next_frame += 1;
self.frames.push(Frame {
id: self.debug.next_frame,
debug_line: None,
source: 0,
column: 0,
proc: 0,
@@ -1114,6 +1128,9 @@ impl Vm {
continue;
}
}
if self.debug.enabled {
self.record_debug_execution();
}
let Some(frame) = self.frames.last_mut() else {
return PollResult::Event(RunEvent::Ended);
};
@@ -1121,7 +1138,19 @@ impl Vm {
let Some(instr) = self.module.procs[frame.proc].code.get(pc).cloned() else {
return PollResult::Event(RunEvent::Ended);
};
frame.pc = pc + 1;
if matches!(instr, Instr::RetProc | Instr::RetFn)
&& self
.debug
.immediate
.as_ref()
.is_some_and(|e| self.frames.len() == e.depth + 1)
{
self.finish_immediate();
return PollResult::Event(RunEvent::Stopped {
line: self.current_line(),
});
}
self.frames.last_mut().unwrap().pc = pc + 1;
match self.exec(instr, pc, host) {
Ok(Flow::Normal) => {}
Ok(Flow::Event(ev)) => return PollResult::Event(ev),
@@ -1132,6 +1161,13 @@ impl Vm {
f.sleep = None;
}
if let Some(ev) = self.handle_error(e.0, host) {
if matches!(ev, RunEvent::Error { .. }) && self.immediate_active() {
self.debug.immediate_error = Some(format!("{ev:?}"));
self.finish_immediate();
return PollResult::Event(RunEvent::Stopped {
line: self.current_line(),
});
}
return PollResult::Event(ev);
}
}
@@ -1151,6 +1187,7 @@ impl Vm {
/// `Some(event)` = unbehandelt (Programmabbruch).
fn handle_error(&mut self, code: u16, _host: &mut dyn Host) -> Option<RunEvent> {
let line = self.current_line();
let error_location = self.debug_location();
if self.in_handler {
// Fehler im Handler: fatal, keine Kaskade.
return Some(self.error_event(code, line));
@@ -1175,9 +1212,24 @@ impl Vm {
}
}
}
let Some((depth, handler)) = target else {
let Some((mut depth, handler)) = target else {
return Some(self.error_event(code, line));
};
if let Some(entry) = &mut self.debug.immediate {
if depth < entry.depth {
let handler_proc = self.frames[depth].proc;
let handler_base = self.frames[depth].locals_base;
let f = &mut self.frames[entry.depth];
if handler == Handler::ResumeNext {
depth = entry.depth;
} else {
entry.handler_return = Some((f.last_stmt_pc, f.pc));
f.proc = handler_proc;
f.locals_base = handler_base;
depth = entry.depth;
}
}
}
// Unwinding bis zum Handler-Frame.
while self.frames.len() > depth + 1 {
self.pop_frame();
@@ -1203,6 +1255,16 @@ impl Vm {
}
Handler::None => unreachable!(),
}
if self.debug.break_errors {
self.debug.error = Some(DebugError {
code,
origin: error_location,
handler: self.next_debug_location(),
resume_pc: self.resume_pc,
});
self.debug.cancel_motion();
self.debug.error_pending = true;
}
None
}
@@ -1435,21 +1497,32 @@ impl Vm {
}
let erster_eintritt =
std::mem::take(&mut self.frames.last_mut().unwrap().handler_start);
let caller = self.frames.len() - 1;
if !initializing
&& !erster_eintritt
&& !self.debug.error_pending
&& self.zustellen(host, Zustellpunkt::Anweisung)
{
if self.debug.enabled {
self.frames[caller].pc = pc;
}
return Ok(Flow::Normal);
}
if self.flags != 0 {
if self.flags & F_STEP != 0 {
return Ok(Flow::Event(RunEvent::Stepped { line }));
if self.debug.enabled {
if let Some(event) = self.debug_boundary(pc, initializing) {
return Ok(Flow::Event(event));
}
}
if self.flags != 0 {
if self.flags & F_BREAK != 0
&& self.breakpoints.contains(&(self.current_module(), line))
{
self.debug.cancel_motion();
return Ok(Flow::Event(RunEvent::Breakpoint { line }));
}
if self.flags & F_STEP != 0 && line > 0 {
return Ok(Flow::Event(RunEvent::Stepped { line }));
}
if self.flags & F_POLL != 0 && self.rt.abbruch {
return Ok(Flow::Event(RunEvent::Interrupted { line }));
}
@@ -2577,11 +2650,17 @@ impl Vm {
}
Ok(Flow::Normal)
}
I::Resume0 => self.do_resume(|vm| vm.resume_pc),
I::ResumeNext => self.do_resume(|vm| {
let f = vm.frames.last().unwrap();
vm.next_stmt_pc(f.proc, vm.resume_pc)
}),
I::Resume0 => {
self.restore_debug_handler();
self.do_resume(|vm| vm.resume_pc)
}
I::ResumeNext => {
self.restore_debug_handler();
self.do_resume(|vm| {
let f = vm.frames.last().unwrap();
vm.next_stmt_pc(f.proc, vm.resume_pc)
})
}
I::ResumeLabel(t) => self.do_resume(move |_| t as usize),
I::RaiseError => {
let code = self.pop_i16()?;

View File

@@ -28,6 +28,9 @@ pub struct ProjectCompiler {
parsed: Vec<Parsed>,
products: Vec<Product>,
pub stats: CompileStats,
/// Vollständige Imports im Cache halten, damit spätere Debugkommandos sie binden können.
pub debug_symbols: bool,
debug_maps: Vec<DebugMap>,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CompileStats {
@@ -45,6 +48,7 @@ struct Parsed {
}
struct Product {
module: Module,
debug_module: Module,
catalog: FormCatalog,
code: CompiledModule,
commons: Vec<tb_frontend::hir::HCommon>,
@@ -211,6 +215,9 @@ impl ProjectCompiler {
import_declarations(&mut key, &parsed, &exports, Some(&self.parsed[index].names));
let mut module = module.clone();
import_declarations(&mut module, &parsed, &exports, None);
if self.debug_symbols {
key = module.clone();
}
if let Some(product) = self
.products
.iter()
@@ -230,6 +237,7 @@ impl ProjectCompiler {
commons.push(hir.commons.clone());
products.push(Product {
module: key,
debug_module: module,
catalog: catalog.clone(),
code,
commons: hir.commons,
@@ -246,11 +254,12 @@ impl ProjectCompiler {
&& !products.iter().any(|n| n.module.name == p.module.name)
});
self.products.extend(products);
let mut result = link(name, parts, &parsed, &commons).map_err(|error| {
let (mut result, maps) = link(name, parts, &parsed, &commons).map_err(|error| {
let mut errors = vec![error];
locate_diagnostics(&mut errors, &sources);
errors
})?;
self.debug_maps = maps;
result.sources = sources;
let objects = FormCatalog {
objects: result.objects.clone(),
@@ -546,7 +555,7 @@ fn link(
mut parts: Vec<CompiledModule>,
ast: &[Module],
module_commons: &[Vec<tb_frontend::hir::HCommon>],
) -> Result<CompiledModule, Diagnostic> {
) -> Result<(CompiledModule, Vec<DebugMap>), Diagnostic> {
let at = |pos, message| Diagnostic {
file: None,
pos,
@@ -625,6 +634,7 @@ fn link(
main.name = name.into();
let mut initializers = Vec::new();
let mut bodies = Vec::new();
let mut debug_maps = Vec::new();
let mut common: HashMap<_, (u16, tb_frontend::hir::HCommon)> = HashMap::new();
for (module_id, part) in parts.iter_mut().enumerate() {
let mut types = Vec::new();
@@ -715,6 +725,11 @@ fn link(
};
globals.push(id);
}
debug_maps.push(DebugMap {
globals: globals.clone(),
types: types.clone(),
procs: proc_maps[module_id].clone(),
});
let string_offset = result.strings.len();
if string_offset + part.strings.len() >= u16::MAX as usize {
return Err(at(
@@ -831,5 +846,232 @@ fn link(
}
}
}
Ok(result)
Ok((result, debug_maps))
}
/// Ephemeral symbol/link context, deliberately not part of TBC serialization.
#[derive(Clone)]
pub struct DebugCompiler {
modules: Vec<(Module, FormCatalog, DebugMap)>,
symbols: tb_frontend::sema::DebugSymbols,
slots: Vec<u16>,
error: Option<String>,
}
#[derive(Clone)]
struct DebugMap {
globals: Vec<u16>,
types: Vec<u16>,
procs: Vec<u16>,
}
#[derive(Clone)]
pub struct DebugCode {
pub procedure: crate::bytecode::ProcCode,
pub strings: Vec<std::rc::Rc<str>>,
pub expression: bool,
}
impl ProjectCompiler {
pub fn debug_compiler(&self) -> DebugCompiler {
let modules: Vec<_> = self
.parsed
.iter()
.zip(&self.debug_maps)
.filter_map(|(parsed, map)| {
self.products
.iter()
.find(|p| p.module.name == parsed.module.name)
.map(|p| (p.debug_module.clone(), p.catalog.clone(), map.clone()))
})
.collect();
let mut symbols = tb_frontend::sema::DebugSymbols::default();
let mut slots = Vec::new();
let mut error = None;
for (ast, catalog, map) in &modules {
if let Some(hir) = tb_frontend::sema::lower_with_forms(ast, catalog).0 {
if symbols.udts.len() + hir.udts.len() >= u16::MAX as usize
|| symbols.globals.len() + hir.globals.len() >= u16::MAX as usize
{
error = Some("Debug-Symboltabelle überschreitet die 16-Bit-Slotgrenze".into());
break;
}
let offset = symbols.udts.len() as u16;
for mut udt in hir.udts {
udt.name = format!("<Debug:{}!{}>", ast.name, udt.name);
for (_, ty) in &mut udt.fields {
if let HTy::Udt(id) = ty {
*id += offset;
}
}
symbols.udts.push(udt);
}
for (mut var, slot) in hir.globals.into_iter().zip(&map.globals) {
var.name = format!("{}!{}", ast.name, var.name);
if let HTy::Udt(id) = &mut var.ty {
*id += offset;
}
symbols.globals.push(var);
slots.push(*slot);
}
}
}
DebugCompiler {
modules,
symbols,
slots,
error,
}
}
}
impl DebugCompiler {
pub fn compile(
&self,
module: u16,
procedure: &str,
text: &str,
expression: bool,
) -> Result<DebugCode, String> {
if let Some(error) = &self.error {
return Err(error.clone());
}
let (ast, catalog, map) = self
.modules
.get(module as usize)
.ok_or("Kein Debug-Quellkontext für dieses Kompilat")?;
let name = if procedure == "<main>" {
&ast.name
} else {
procedure.rsplit('!').next().unwrap_or(procedure)
};
if map.globals.len() + self.symbols.globals.len() >= u16::MAX as usize
|| map.types.len() + self.symbols.udts.len() >= u16::MAX as usize
{
return Err("Debug-Kontext überschreitet die 16-Bit-Slotgrenze".into());
}
let mut symbols = self.symbols.clone();
symbols.original_globals = map.globals.len();
let (mut hir, debug) = tb_frontend::sema::lower_debug(
ast, catalog, name, text, expression, symbols,
)
.map_err(|e| {
e.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
})?;
hir.procs.push(debug);
let mut globals = map.globals.clone();
globals.extend(&self.slots);
let mut types = map.types.clone();
for (_, _, mapping) in &self.modules {
types.extend(&mapping.types);
}
let mut compiled = codegen::compile(&hir);
let mut proc = compiled.procs.pop().unwrap();
proc.module = module;
for ty in &mut proc.locals_init {
remap_type(ty, &types);
}
if let Some(ty) = &mut proc.ret_ty {
remap_signature(ty, &types);
}
for i in &mut proc.code {
use Instr::*;
match i {
LoadGlobal(id) | StoreGlobal(id) | MakeRefGlobal(id) => {
*id = *globals
.get(*id as usize)
.ok_or("Unbekannter globaler Slot")?
}
LoadArr(global, id, _, ty) => {
if *global {
*id = globals[*id as usize];
}
remap_type(ty, &types);
}
Call(id, _) => *id = map.procs[*id as usize],
PushUdtId(id) => *id = types[*id as usize],
_ => {}
}
}
Ok(DebugCode {
procedure: proc,
strings: compiled.strings,
expression,
})
}
}
impl DebugCompiler {
pub(crate) fn same_control_context(
&self,
module: u16,
procedure: &str,
from: SourcePos,
to: SourcePos,
) -> bool {
let Some((ast, _, _)) = self.modules.get(module as usize) else {
return false;
};
let body = if procedure == "<main>" {
&ast.body
} else {
let Some(proc) = ast.procs.iter().find(|p| {
p.sig
.name
.eq_ignore_ascii_case(procedure.rsplit('!').next().unwrap_or(procedure))
}) else {
return false;
};
&proc.body
};
let a = control_path(body, from);
let b = control_path(body, to);
a.is_some() && a == b
}
}
/// Structured arms supplement bytecode loop ranges: ELSE and CASE are distinct
/// even when their forward jump would otherwise look like a top-level GOTO.
fn control_path(body: &[Stmt], target: SourcePos) -> Option<Vec<(SourcePos, usize)>> {
for stmt in body {
let pos = tb_frontend::sema::stmt_pos(stmt);
if pos == target {
return Some(vec![]);
}
let mut children: Vec<(&[Stmt], Option<SourcePos>)> = Vec::new();
match stmt {
Stmt::If {
then_body,
elseifs,
else_body,
..
} => {
children.push((then_body, None));
for (_, body, pos) in elseifs {
children.push((body, Some(*pos)));
}
if let Some(body) = else_body {
children.push((body, None));
}
}
Stmt::Select { arms, .. } => {
for arm in arms {
children.push((&arm.body, Some(arm.pos)));
}
}
Stmt::For { body, end_pos, .. }
| Stmt::DoLoop { body, end_pos, .. }
| Stmt::While { body, end_pos, .. } => children.push((body, Some(*end_pos))),
_ => {}
}
for (index, (body, entry)) in children.into_iter().enumerate() {
if let Some(mut path) = if entry == Some(target) {
Some(vec![])
} else {
control_path(body, target)
} {
path.insert(0, (pos, index));
return Some(path);
}
}
}
None
}

View File

@@ -0,0 +1,420 @@
use tb_frontend::{forms::FormCatalog, source::SourceUnit};
use tb_runtime::{host::CaptureHost, value::Value};
use tb_vm::{
interp::{RunEvent, Vm},
project::ProjectCompiler,
};
fn vm(source: &str) -> Vm {
let mut compiler = ProjectCompiler::default();
let code = compiler
.compile(
"TEST",
&[SourceUnit::new("TEST", "test.bas", source)],
&FormCatalog::default(),
&[],
)
.unwrap();
let mut vm = Vm::new(code);
vm.debug.enabled = true;
vm.debug.compiler = Some(compiler.debug_compiler());
vm
}
#[test]
fn break_continue_loop_and_statement_steps() {
let mut vm = vm("FOR i% = 1 TO 2\nx% = x% + 1: y% = y% + 1\nNEXT\nEND\n");
let mut host = CaptureHost::default();
assert!(vm.add_source_breakpoint(0, 0, 2));
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
assert!(matches!(vm.inspect("x%"), Some(Value::Int(0))));
vm.set_step(true);
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 2 });
assert!(matches!(vm.inspect("x%"), Some(Value::Int(1))));
vm.set_step(false);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
assert!(matches!(vm.inspect("y%"), Some(Value::Int(1))));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(2))));
}
#[test]
fn watches_immediate_byref_and_errors_preserve_continuation() {
let mut vm=vm("DIM SHARED x AS INTEGER\nx = 3\nCALL work(x)\nEND\nSUB work(n AS INTEGER)\nDIM a(1 TO 2) AS INTEGER\na(1)=n\nn=n+1\nEND SUB\nSUB bump(n AS INTEGER)\nn=n+5\nEND SUB\n");
let mut host = CaptureHost::default();
assert!(vm.add_source_breakpoint(0, 0, 8));
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 8 });
let location = vm.debug_location().unwrap();
let frame = location.frame;
for _ in 0..3 {
assert!(matches!(
vm.evaluate_watch(frame, "a(1) + CINT(2.5)").unwrap(),
Value::Int(5)
));
assert!(vm.evaluate_watch(frame, "RND").is_err());
assert_eq!(vm.debug_location().unwrap(), location);
}
assert!(vm.start_immediate(frame, "n =").is_err());
vm.start_immediate(frame, "n = 10: CALL bump(n): PRINT n")
.unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 8 });
assert_eq!(vm.debug_location().unwrap(), location);
assert!(matches!(vm.inspect("n"), Some(Value::Int(15))));
vm.start_immediate(frame, "ERROR 5").unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 8 });
assert!(vm.debug.immediate_error.is_some());
assert_eq!(vm.debug_location().unwrap(), location);
vm.clear_source_breakpoints();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x"), Some(Value::Int(16))));
}
#[test]
fn immediate_handler_resume_and_break_on_errors() {
let mut vm = vm("ON ERROR GOTO handler\nx%=1\nx%=x%+1\nEND\nhandler:\ny%=ERR\nRESUME NEXT\n");
let mut host = CaptureHost::default();
vm.add_source_breakpoint(0, 0, 3);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 3 });
let location = vm.debug_location().unwrap();
vm.debug.break_errors = true;
vm.start_immediate(location.frame, "ERROR 5: x%=10")
.unwrap();
assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. }));
assert_eq!(vm.debug.error.as_ref().unwrap().code, 5);
assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 3 });
assert_eq!(vm.debug_location().unwrap(), location);
assert!(matches!(vm.inspect("y%"), Some(Value::Int(5))));
assert!(matches!(vm.inspect("x%"), Some(Value::Int(10))));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(11))));
}
#[test]
fn step_over_recursive_frames_history_and_safe_jump() {
let mut vm=vm("CALL rec(2)\nx%=1\nx%=2\nEND\nSUB rec(n AS INTEGER)\nIF n>0 THEN\nCALL rec(n-1)\nEND IF\nPRINT n\nEND SUB\n");
let mut host = CaptureHost::default();
vm.debug.history_on = true;
vm.add_source_breakpoint(0, 0, 7);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 7 });
let first = vm.debug_location().unwrap();
vm.clear_source_breakpoints();
vm.step_over();
assert!(matches!(vm.run(&mut host), RunEvent::Stepped { .. }));
assert_eq!(vm.debug_location().unwrap().frame, first.frame);
vm.set_step(false);
vm.run_to_cursor(0, 0, 2).unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
let at = vm.debug_location().unwrap();
vm.set_next_statement(at.frame, 0, 3).unwrap();
let moved = vm.debug_location().unwrap();
assert!(vm.set_next_statement(at.frame, 0, 7).is_err());
assert_eq!(vm.debug_location().unwrap(), moved);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(2))));
assert!(!vm.debug.history.is_empty());
}
#[test]
fn include_identity_qualified_globals_udts_and_older_frames() {
use tb_frontend::source::SourceSegment;
let mut compiler = ProjectCompiler::default();
let first=SourceUnit{name:"FIRST".into(),segments:vec![SourceSegment{file:"one.bi".into(),first_line:1,text:"'one\nx%=1\n".into()},SourceSegment{file:"two.bi".into(),first_line:1,text:"'two\nx%=2\nCALL rec(2)\nEND\nSUB rec(n AS INTEGER)\nDIM a(1 TO 2) AS INTEGER\na(1)=n\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n".into()}]};
let second=SourceUnit::new("SECOND","second.bas","TYPE pair\nvalue AS INTEGER\nEND TYPE\nDIM SHARED r AS pair\nDIM SHARED a(1 TO 2) AS INTEGER\n");
let code = compiler
.compile("P", &[first, second], &FormCatalog::default(), &[])
.unwrap();
let mut vm = Vm::new(code);
vm.debug.compiler = Some(compiler.debug_compiler());
vm.add_source_breakpoint(0, 1, 2);
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
assert_eq!(vm.current_file(), "two.bi");
assert!(matches!(vm.inspect("x%"), Some(Value::Int(1))));
vm.clear_source_breakpoints();
vm.add_source_breakpoint(0, 1, 9);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 9 });
let frames = vm.debug_frames();
assert!(frames.len() >= 4);
let top = vm.debug_location();
assert!(matches!(
vm.evaluate_watch(frames[0].id, "a(1)").unwrap(),
Value::Int(0)
));
assert!(matches!(
vm.evaluate_watch(frames[1].id, "a(1)").unwrap(),
Value::Int(1)
));
assert!(matches!(
vm.evaluate_watch(frames[1].id, "SECOND!r.value + SECOND!a(1)")
.unwrap(),
Value::Int(0)
));
assert_eq!(vm.debug_location(), top);
vm.start_immediate(frames[1].id, "SECOND!r.value=7: SECOND!a(1)=8")
.unwrap();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(matches!(
vm.evaluate_watch(frames[1].id, "SECOND!r.value + SECOND!a(1)")
.unwrap(),
Value::Int(15)
));
}
#[test]
fn watchpoint_history_ring_and_error_differential() {
use tb_vm::interp::DebugWatch;
let mut vm = vm("FOR i%=1 TO 1500\nx%=i%\nNEXT\nEND\n");
vm.debug.enabled = true;
vm.debug.history_on = true;
vm.debug.watches.push(DebugWatch {
expression: "x%=2".into(),
condition: true,
frame: None,
value: Err("pending".into()),
});
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 3 });
assert!(matches!(vm.inspect("x%"), Some(Value::Int(2))));
vm.debug.watches.clear();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(vm.debug.history.len(), 1024);
let source="10 ON ERROR GOTO handler\n20 ERROR 6\n30 x%=x%+1\n40 END\nhandler:\ne%=ERR: l&=ERL\nRESUME NEXT\n";
let run = |break_errors| {
let mut vm = self::vm(source);
vm.debug.break_errors = break_errors;
let mut host = CaptureHost::default();
if break_errors {
assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. }));
assert_eq!(vm.debug.error.as_ref().unwrap().code, 6);
assert!(matches!(vm.inspect("e%"), Some(Value::Int(0))));
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
format!(
"{:?}{:?}{:?}",
vm.inspect("e%"),
vm.inspect("l&"),
vm.inspect("x%")
)
};
assert_eq!(run(true), run(false));
}
#[test]
fn gosub_step_over_breakpoint_priority_and_temporary_target_cleanup() {
let mut vm = vm("GOSUB work\nx%=4\nEND\nwork:\nx%=1\nx%=2\nRETURN\n");
let mut host = CaptureHost::default();
vm.set_step(true);
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 1 });
vm.step_over();
vm.add_source_breakpoint(0, 0, 6);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 6 });
vm.clear_source_breakpoints();
vm.set_step(false);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
let mut vm = self::vm("FOR i%=1 TO 2\nx%=x%+1\ny%=y%+1\nNEXT\nEND\n");
vm.add_source_breakpoint(0, 0, 2);
vm.run_to_cursor(0, 0, 3).unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
vm.clear_source_breakpoints();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
}
#[test]
fn immediate_wait_interrupt_nested_error_end_and_run() {
use tb_vm::interp::PollResult;
let source="x%=1\nx%=x%+1\nEND\nSUB ask(n AS INTEGER)\nINPUT n\nEND SUB\nSUB fail()\nON LOCAL ERROR GOTO handler\nERROR 6\nEXIT SUB\nhandler:\nx%=ERR\nRESUME NEXT\nEND SUB\nSUB done()\nEND\nEND SUB\nSUB restart()\nRUN\nEND SUB\n";
let mut vm = vm(source);
let mut host = CaptureHost::default();
vm.add_source_breakpoint(0, 0, 2);
vm.run(&mut host);
let location = vm.debug_location().unwrap();
vm.start_immediate(location.frame, "CALL ask(x%)").unwrap();
assert!(matches!(
vm.poll(&mut host, 1000),
PollResult::Waiting { .. }
));
assert!(vm.immediate_active());
vm.rt.abbruch = true;
assert!(matches!(
vm.poll(&mut host, 1000),
PollResult::Event(RunEvent::Interrupted { .. })
));
vm.rt.abbruch = false;
host.tippe("9\r");
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert_eq!(vm.debug_location().unwrap(), location);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(9))));
vm.start_immediate(location.frame, "CALL fail()").unwrap();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert_eq!(vm.debug_location().unwrap(), location);
vm.start_immediate(location.frame, "CALL restart()")
.unwrap();
assert_eq!(
vm.run(&mut host),
RunEvent::Restart {
program: None,
line: None
}
);
let mut vm = self::vm(source);
vm.add_source_breakpoint(0, 0, 2);
vm.run(&mut host);
let frame = vm.debug_location().unwrap().frame;
vm.start_immediate(frame, "CALL done()").unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(vm.is_terminated());
}
#[test]
fn form_event_breakpoint_and_step_over_keep_the_event_frame() {
use tb_frontend::forms::ObjectClass;
let mut catalog = FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
let source="Form1.Show\nx%=1\nEND\nSUB Form_Load()\nCALL bump\nPRINT 4\nEND SUB\nSUB bump()\nPRINT 3\nEND SUB\n";
let mut compiler = ProjectCompiler::default();
let code = compiler
.compile(
"Form1",
&[SourceUnit::new("Form1", "form1.frm", source)],
&catalog,
&[],
)
.unwrap();
let mut vm = Vm::new(code);
vm.debug.compiler = Some(compiler.debug_compiler());
vm.add_source_breakpoint(0, 0, 5);
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 5 });
let frame = vm.debug_location().unwrap().frame;
vm.clear_source_breakpoints();
vm.step_over();
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 6 });
assert_eq!(vm.debug_location().unwrap().frame, frame);
vm.add_source_breakpoint(0, 0, 2);
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
vm.clear_source_breakpoints();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
}
#[test]
fn set_next_rejects_loop_gosub_handler_and_pending_operations_atomically() {
let source = "x%=1\nFOR i%=1 TO 2\nx%=x%+1\nNEXT\nGOSUB worker\nEND\nworker:\nx%=3\nRETURN\n";
let mut vm = vm(source);
let mut host = CaptureHost::default();
vm.add_source_breakpoint(0, 0, 1);
vm.run(&mut host);
let before = vm.debug_location().unwrap();
for line in [3, 4, 8] {
assert!(
vm.set_next_statement(before.frame, 0, line).is_err(),
"line {line}"
);
assert_eq!(vm.debug_location().unwrap(), before);
}
vm.clear_source_breakpoints();
vm.add_source_breakpoint(0, 0, 8);
vm.run(&mut host);
let before = vm.debug_location().unwrap();
assert!(vm.set_next_statement(before.frame, 0, 6).is_err());
assert_eq!(vm.debug_location().unwrap(), before);
}
#[test]
fn same_line_calls_and_immediate_do_not_retrigger_a_line_breakpoint() {
let mut vm = vm("x%=1: CALL bump(x%): x%=x%+1\nEND\nSUB bump(n AS INTEGER)\nn=n+1\nEND SUB\n");
vm.debug.history_on = true;
vm.add_source_breakpoint(0, 0, 1);
let mut host = CaptureHost::default();
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 1 });
assert!(vm.debug.history.is_empty());
let frame = vm.debug_location().unwrap().frame;
vm.start_immediate(frame, "x%=5").unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 1 });
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(3))));
assert_eq!(
vm.debug
.history
.iter()
.filter(|p| p.procedure == 0 && p.line == 1)
.count(),
3
);
}
#[test]
fn immediate_handler_exit_and_cached_new_imports_are_safe() {
let mut compiler = ProjectCompiler::default();
compiler.debug_symbols = true;
let first = SourceUnit::new("ONE", "one.bas", "x%=1\nSTOP\nx%=x%+1\nEND\n");
let old = SourceUnit::new("TWO", "two.bas", "SUB oldproc()\nPRINT 1\nEND SUB\n");
compiler
.compile("P", &[first.clone(), old], &FormCatalog::default(), &[])
.unwrap();
let second = SourceUnit::new(
"TWO",
"two.bas",
"SUB newproc(n AS INTEGER)\nn=n+5\nEND SUB\n",
);
let code = compiler
.compile("P", &[first, second], &FormCatalog::default(), &[])
.unwrap();
let mut vm = Vm::new(code);
vm.debug.compiler = Some(compiler.debug_compiler());
let mut host = CaptureHost::default();
vm.run(&mut host);
let frame = vm.debug_location().unwrap().frame;
assert_eq!(vm.execution_location().unwrap().line, 3);
vm.start_immediate(frame, "CALL newproc(x%)").unwrap();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(matches!(vm.inspect("ONE!x%"), Some(Value::Int(6))));
let mut vm=self::vm("CALL work\nEND\nSUB work()\nON LOCAL ERROR GOTO handler\nx%=1\nx%=2\nEXIT SUB\nhandler:\nx%=7\nEND SUB\n");
vm.add_source_breakpoint(0, 0, 6);
vm.run(&mut host);
let before = vm.debug_location().unwrap();
vm.start_immediate(before.frame, "ERROR 5").unwrap();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert_eq!(vm.debug_location().unwrap(), before);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(7))));
vm.clear_source_breakpoints();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
}
#[test]
fn set_next_rejects_else_and_case_entry_but_allows_peer_statements() {
let source="x%=1\nIF x% THEN\nx%=2\nELSE\nx%=3\nEND IF\nSELECT CASE x%\nCASE 1\nx%=4\nCASE ELSE\nx%=5\nEND SELECT\nx%=6\nEND\n";
let mut vm = vm(source);
let mut host = CaptureHost::default();
vm.add_source_breakpoint(0, 0, 1);
vm.run(&mut host);
let before = vm.debug_location().unwrap();
for line in [3, 5, 9, 11] {
assert!(
vm.set_next_statement(before.frame, 0, line).is_err(),
"line {line}"
);
assert_eq!(vm.debug_location().unwrap(), before);
}
vm.set_next_statement(before.frame, 0, 13).unwrap();
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("x%"), Some(Value::Int(6))));
}
#[test]
fn watches_never_initialize_arrays_or_run_user_functions_and_keep_selected_context() {
let mut vm=vm("DIM SHARED changed%\nSTOP\nx%=a%(2)\nCALL rec(2)\nEND\nFUNCTION impure%()\nchanged%=changed%+1\nimpure%=changed%\nEND FUNCTION\nSUB rec(n AS INTEGER)\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n");
let mut host = CaptureHost::default();
vm.run(&mut host);
let frame = vm.debug_location().unwrap().frame;
let frames = format!("{:?}", vm.debug_frames());
for _ in 0..4 {
assert!(vm.evaluate_watch(frame, "a%(2)").is_err());
assert!(matches!(vm.inspect("a%"), Some(Value::Empty)));
assert!(vm.evaluate_watch(frame, "impure%").is_err());
assert!(matches!(vm.inspect("changed%"), Some(Value::Int(0))));
assert!(vm.evaluate_watch(frame, "1 / 0").is_err());
assert!(matches!(
vm.evaluate_watch(frame, "ERR").unwrap(),
Value::Lng(0)
));
}
assert_eq!(format!("{:?}", vm.debug_frames()), frames);
vm.add_source_breakpoint(0, 0, 12);
vm.run(&mut host);
let frames = vm.debug_frames();
let older = frames[1].id;
vm.clear_source_breakpoints();
vm.debug.watches.push(tb_vm::interp::DebugWatch {
expression: "n=1".into(),
condition: true,
frame: Some(older),
value: Err("pending".into()),
});
assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. }));
assert!(matches!(vm.debug.watches[0].value, Ok(Value::Int(-1))));
}