Implement and archive Phase 5 debugger
This commit is contained in:
@@ -134,7 +134,7 @@ struct VarInfo {
|
||||
by_ref: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
struct Scope {
|
||||
vars: HashMap<String, VarInfo>,
|
||||
labels: HashMap<String, LabelId>,
|
||||
@@ -421,6 +421,55 @@ pub fn lower_with_forms(
|
||||
(hir, s.diags)
|
||||
}
|
||||
|
||||
/// Kompiliert Debugcode mit denselben Symbolen, DEFtype-Regeln und Slots wie der Originalrumpf.
|
||||
/// Der neue Rumpf ist getrennt; der Projektquelltext wird nicht verändert.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct DebugSymbols {
|
||||
pub globals: Vec<hir::HVar>,
|
||||
pub udts: Vec<hir::HUdt>,
|
||||
pub original_globals: usize,
|
||||
}
|
||||
|
||||
pub fn lower_debug(
|
||||
module: &Module,
|
||||
catalog: &FormCatalog,
|
||||
procedure: &str,
|
||||
text: &str,
|
||||
expression: bool,
|
||||
symbols: DebugSymbols,
|
||||
) -> Result<(hir::HirModule, hir::HProc), Vec<Diagnostic>> {
|
||||
let text = if expression {
|
||||
format!("PRINT {text}\n")
|
||||
} else {
|
||||
format!("{text}\n")
|
||||
};
|
||||
let lexed = crate::lexer::lex(&text);
|
||||
let parsed = crate::parser::parse("<Immediate>", &lexed.tokens);
|
||||
let mut errors = lexed.diagnostics;
|
||||
errors.extend(parsed.diagnostics);
|
||||
if !parsed.module.procs.is_empty() {
|
||||
errors.push(Diagnostic {
|
||||
file: None,
|
||||
pos: SourcePos::default(),
|
||||
message: "Keine Prozedurdefinition im Direktfenster".into(),
|
||||
});
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(errors);
|
||||
}
|
||||
let mut sema = new_sema(module, catalog);
|
||||
sema.debug_symbols = Some(symbols);
|
||||
sema.debug_request = Some((procedure.into(), parsed.module.body, expression));
|
||||
let hir = sema.run(module);
|
||||
if sema.debug_proc.is_none() {
|
||||
sema.err(SourcePos::default(), "Kein erreichbarer Debug-Kontext");
|
||||
}
|
||||
if !sema.diags.is_empty() {
|
||||
return Err(sema.diags);
|
||||
}
|
||||
Ok((hir.unwrap(), sema.debug_proc.unwrap()))
|
||||
}
|
||||
|
||||
/// Semantisch gebundene Objekt-/Ereignisnamen für transaktionale IDE-Umbenennungen.
|
||||
/// Aufrufer validieren die gesamte Übersetzungseinheit einschließlich ihrer Imports.
|
||||
#[derive(Default)]
|
||||
@@ -464,6 +513,9 @@ fn new_sema(module: &Module, catalog: &FormCatalog) -> Sema {
|
||||
module_name: module.name.clone(),
|
||||
event_procs: Vec::new(),
|
||||
references: None,
|
||||
debug_request: None,
|
||||
debug_proc: None,
|
||||
debug_symbols: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,6 +621,9 @@ struct Sema {
|
||||
module_name: String,
|
||||
event_procs: Vec<hir::HEventProc>,
|
||||
references: Option<BoundFormReferences>,
|
||||
debug_request: Option<(String, Vec<Stmt>, bool)>,
|
||||
debug_proc: Option<hir::HProc>,
|
||||
debug_symbols: Option<DebugSymbols>,
|
||||
}
|
||||
|
||||
impl Sema {
|
||||
@@ -636,6 +691,7 @@ impl Sema {
|
||||
};
|
||||
self.prescan(&module.body, &mut scope, true);
|
||||
let body = self.lower_body(&module.body, &mut scope);
|
||||
self.lower_debug_scope(&module.name, &scope);
|
||||
let main = hir::HProc {
|
||||
name: module.name.clone(),
|
||||
kind: hir::HProcKind::Main,
|
||||
@@ -795,6 +851,7 @@ impl Sema {
|
||||
ret_ty = Some(self.h_ty(&ret));
|
||||
}
|
||||
let body = self.lower_body(&proc.body, &mut scope);
|
||||
self.lower_debug_scope(&proc.sig.name, &scope);
|
||||
let hproc = hir::HProc {
|
||||
name: proc.sig.name.clone(),
|
||||
kind: match proc.sig.kind {
|
||||
@@ -814,6 +871,156 @@ impl Sema {
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_debug_scope(&mut self, name: &str, original: &Scope) {
|
||||
let Some((target, statements, expression)) = self.debug_request.clone() else {
|
||||
return;
|
||||
};
|
||||
if !name.eq_ignore_ascii_case(&target) {
|
||||
return;
|
||||
}
|
||||
if original.locals.len() >= u16::MAX as usize {
|
||||
self.err(SourcePos::default(), "Kein freier temporärer Debug-Slot");
|
||||
return;
|
||||
}
|
||||
let mut scope = original.clone();
|
||||
if let Some(symbols) = self.debug_symbols.take() {
|
||||
let offset = self.udt_defs.len() as u16;
|
||||
let mut udts = symbols.udts;
|
||||
for udt in &mut udts {
|
||||
for (_, ty) in &mut udt.fields {
|
||||
if let HTy::Udt(id) = ty {
|
||||
*id += offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i, udt) in udts.iter().enumerate() {
|
||||
self.udt_ids.insert(udt.name.clone(), offset + i as u16);
|
||||
}
|
||||
self.udt_defs.extend(udts);
|
||||
self.globals
|
||||
.resize_with(symbols.original_globals, || hir::HVar {
|
||||
name: "<DebugPad>".into(),
|
||||
ty: HTy::Num(NumTy::Int),
|
||||
array: false,
|
||||
});
|
||||
for mut var in symbols.globals {
|
||||
if let HTy::Udt(id) = &mut var.ty {
|
||||
*id += offset;
|
||||
}
|
||||
let ty = match &var.ty {
|
||||
HTy::Num(n) => ty_of_num(*n),
|
||||
HTy::Str => Ty::Str,
|
||||
HTy::FixedStr(n) => Ty::FixedStr(*n),
|
||||
HTy::Udt(id) => Ty::Udt(self.udt_defs[*id as usize].name.clone()),
|
||||
HTy::Form => Ty::Form,
|
||||
HTy::Control => Ty::Control,
|
||||
};
|
||||
let key = if var.name.ends_with(['%', '&', '!', '#', '$', '@']) {
|
||||
var.name.clone()
|
||||
} else {
|
||||
format!("{}\u{1}AS", var.name)
|
||||
};
|
||||
scope.vars.insert(
|
||||
key,
|
||||
VarInfo {
|
||||
ty,
|
||||
array: var.array,
|
||||
explicit: true,
|
||||
slot: self.globals.len() as u16,
|
||||
global: true,
|
||||
by_ref: false,
|
||||
},
|
||||
);
|
||||
self.globals.push(var);
|
||||
}
|
||||
}
|
||||
let explicit = self.explicit;
|
||||
self.explicit = true;
|
||||
let globals = self.globals.len();
|
||||
let locals = scope.locals.len();
|
||||
let mut proc = hir::HProc {
|
||||
name: "<Immediate>".into(),
|
||||
kind: hir::HProcKind::Sub,
|
||||
params: vec![],
|
||||
locals: vec![],
|
||||
ret_slot: None,
|
||||
ret_ty: None,
|
||||
body: vec![],
|
||||
label_count: 0,
|
||||
};
|
||||
if expression {
|
||||
if let [Stmt::Print { items, .. }] = statements.as_slice() {
|
||||
if let [PrintItem::Expr(expr)] = items.as_slice() {
|
||||
let (value, ty) = self.lower_expr(expr, &mut scope);
|
||||
let ty = self.h_ty(&ty);
|
||||
let slot = VarSlot::Local(scope.locals.len() as u16);
|
||||
proc.body.push(HStmt {
|
||||
pos: SourcePos::default(),
|
||||
line: 0,
|
||||
kind: HStmtKind::Assign {
|
||||
place: HPlace {
|
||||
base: slot,
|
||||
base_is_ref: false,
|
||||
indices: vec![],
|
||||
fields: vec![],
|
||||
ty: ty.clone(),
|
||||
array_elem: None,
|
||||
},
|
||||
value,
|
||||
},
|
||||
});
|
||||
proc.ret_ty = Some(ty.clone());
|
||||
proc.ret_slot = Some(slot);
|
||||
proc.kind = hir::HProcKind::Function;
|
||||
proc.locals = scope.locals.clone();
|
||||
proc.locals.push(hir::HVar {
|
||||
name: "<Watch>".into(),
|
||||
ty,
|
||||
array: false,
|
||||
});
|
||||
} else {
|
||||
self.err(
|
||||
SourcePos::default(),
|
||||
"Genau ein Watch-Ausdruck erforderlich",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.err(
|
||||
SourcePos::default(),
|
||||
"Genau ein Watch-Ausdruck erforderlich",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if statements.iter().any(|s| {
|
||||
!matches!(
|
||||
s,
|
||||
Stmt::Assign { .. }
|
||||
| Stmt::Print { .. }
|
||||
| Stmt::Call { .. }
|
||||
| Stmt::ErrorStmt { .. }
|
||||
)
|
||||
}) {
|
||||
self.err(
|
||||
SourcePos::default(),
|
||||
"Direktfenster erlaubt PRINT, Zuweisungen, Prozeduraufrufe und ERROR",
|
||||
);
|
||||
} else {
|
||||
proc.body = self.lower_body(&statements, &mut scope);
|
||||
}
|
||||
proc.locals = scope.locals.clone();
|
||||
}
|
||||
if self.globals.len() != globals || scope.locals.len() != locals {
|
||||
self.err(
|
||||
SourcePos::default(),
|
||||
"Direktcode darf keine neuen Variablen deklarieren",
|
||||
);
|
||||
}
|
||||
proc.label_count = scope.next_label;
|
||||
self.explicit = explicit;
|
||||
self.debug_proc = Some(proc);
|
||||
self.debug_request = None;
|
||||
}
|
||||
|
||||
/// Prescan eines Rumpfs: Labels/Zeilennummern erhalten `LabelId`s;
|
||||
/// im Modulrumpf werden zusätzlich DATA-Konstanten (statisch, in
|
||||
/// Quellreihenfolge) und RESTORE-Marken eingesammelt.
|
||||
@@ -1159,6 +1366,19 @@ impl Sema {
|
||||
Some((id, object, member.to_string()))
|
||||
}
|
||||
|
||||
fn is_debug_path(&self, scope: &Scope, path: &str) -> bool {
|
||||
self.debug_request.is_some()
|
||||
&& path.contains('!')
|
||||
&& scope.vars.keys().any(|k| {
|
||||
k.trim_end_matches("\u{1}AS")
|
||||
.trim_end_matches(['%', '&', '!', '#', '$', '@'])
|
||||
== path
|
||||
.split('.')
|
||||
.next()
|
||||
.unwrap_or(path)
|
||||
.trim_end_matches(['%', '&', '!', '#', '$', '@'])
|
||||
})
|
||||
}
|
||||
fn is_udt_path(&self, scope: &Scope, path: &str) -> bool {
|
||||
let base = path.split('.').next().unwrap_or(path);
|
||||
let as_key = format!("{base}\u{1}AS");
|
||||
@@ -1669,7 +1889,9 @@ impl Sema {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (name.contains('.') || name.contains('!')) && !self.is_udt_path(scope, name)
|
||||
if (name.contains('.') || name.contains('!'))
|
||||
&& !self.is_udt_path(scope, name)
|
||||
&& !self.is_debug_path(scope, name)
|
||||
{
|
||||
if name.contains('!') && !name.contains('.') {
|
||||
let object_name = name.split_once('!').unwrap().1;
|
||||
@@ -3725,7 +3947,9 @@ impl Sema {
|
||||
return false;
|
||||
}
|
||||
if (suffix.is_none() && self.find_object(name).is_some())
|
||||
|| (name.contains('!') && !self.is_udt_path(scope, name))
|
||||
|| (name.contains('!')
|
||||
&& !self.is_udt_path(scope, name)
|
||||
&& !self.is_debug_path(scope, name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -4720,7 +4944,10 @@ impl Sema {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (name.contains('.') || name.contains('!')) && !self.is_udt_path(scope, name) {
|
||||
if (name.contains('.') || name.contains('!'))
|
||||
&& !self.is_udt_path(scope, name)
|
||||
&& !self.is_debug_path(scope, name)
|
||||
{
|
||||
if name.contains('!') && !name.contains('.') {
|
||||
return match self.object_member_target(name, pos) {
|
||||
Some((object, info, _)) => {
|
||||
|
||||
@@ -192,6 +192,8 @@ pub enum AfterSave {
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DialogKind {
|
||||
Watch(Command),
|
||||
DeleteWatch,
|
||||
DesignProperty,
|
||||
DesignPalette,
|
||||
DesignMenu((String, Option<i32>)),
|
||||
@@ -259,6 +261,7 @@ impl Dialog {
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Hit {
|
||||
DebugFrame(usize),
|
||||
DesignTool(usize),
|
||||
DesignPaint(&'static str, u8),
|
||||
DesignObject(u64),
|
||||
@@ -275,6 +278,7 @@ pub enum Hit {
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub debugger: crate::debugger::Debugger,
|
||||
pub designer: crate::designer::Designer,
|
||||
pub session: crate::execution::Session,
|
||||
pub editor: crate::editor::Editor,
|
||||
@@ -314,6 +318,7 @@ impl App {
|
||||
let (options, errors, config_disk) = Options::load(&config_path);
|
||||
project.include_paths = options.include_paths.clone();
|
||||
let mut app = Self {
|
||||
debugger: Default::default(),
|
||||
designer: Default::default(),
|
||||
session: Default::default(),
|
||||
editor: Default::default(),
|
||||
@@ -570,6 +575,9 @@ impl App {
|
||||
}
|
||||
}
|
||||
fn action(&mut self, command: Command) -> Result<()> {
|
||||
if self.debug_command(command)? {
|
||||
return Ok(());
|
||||
}
|
||||
if self.designer_command(command)? {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1056,6 +1064,7 @@ impl App {
|
||||
self.project.open_project(&path, Decision::Discard)?;
|
||||
}
|
||||
}
|
||||
self.debugger = Default::default();
|
||||
self.session = Default::default();
|
||||
self.designer = Default::default();
|
||||
self.basic_events.clear();
|
||||
@@ -1133,6 +1142,10 @@ impl App {
|
||||
}
|
||||
fn submit(&mut self, d: &mut Dialog) -> Result<bool> {
|
||||
match d.kind.clone() {
|
||||
DialogKind::Watch(_) | DialogKind::DeleteWatch => {
|
||||
self.debug_submit(d)?;
|
||||
return Ok(false);
|
||||
}
|
||||
DialogKind::DesignProperty
|
||||
| DialogKind::DesignPalette
|
||||
| DialogKind::DesignMenu(_)
|
||||
@@ -1491,6 +1504,15 @@ impl App {
|
||||
} else if self.program_focus() {
|
||||
self.basic_events.push(event);
|
||||
} else if let Event::Paste(text) = event {
|
||||
if self.dialog.is_none()
|
||||
&& self.menu.is_none()
|
||||
&& self
|
||||
.active_window()
|
||||
.is_some_and(|w| w.kind == WindowKind::Immediate)
|
||||
{
|
||||
self.debugger.immediate.push_str(&text);
|
||||
return;
|
||||
}
|
||||
if self.dialog.is_none() && self.menu.is_none() && self.mode == Mode::Environment {
|
||||
if let Err(e) = self.editor_insert(&text) {
|
||||
self.message = e.to_string();
|
||||
@@ -1535,6 +1557,11 @@ impl App {
|
||||
return;
|
||||
}
|
||||
match hit {
|
||||
Hit::DebugFrame(index) => {
|
||||
if let Err(e) = self.select_debug_frame(index) {
|
||||
self.message = e.to_string();
|
||||
}
|
||||
}
|
||||
Hit::DesignTool(_) => {}
|
||||
Hit::DesignObject(id) => {
|
||||
if let Err(e) = self.design_select(id, false) {
|
||||
@@ -1924,6 +1951,9 @@ impl App {
|
||||
))
|
||||
}
|
||||
fn code_key(&mut self, key: KeyEvent) -> Result<()> {
|
||||
if self.debug_key(key)? {
|
||||
return Ok(());
|
||||
}
|
||||
if self.mode == Mode::Designer || self.editor_view().is_err() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -101,9 +101,6 @@ impl Command {
|
||||
pub fn feature_phase(self) -> Option<u8> {
|
||||
use Command::*;
|
||||
match self {
|
||||
NextStatement | AddWatch | InstantWatch | Watchpoint | DeleteWatch | DeleteWatches
|
||||
| Trace | History | Breakpoint | ClearBreakpoints | BreakErrors | SetStatement
|
||||
| RunToCursor | Step | ProcedureStep | HistoryBack | HistoryForward => Some(6),
|
||||
HelpIndex | HelpContents | Keyboard | Topic | UsingHelp | Tutorial => Some(7),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
687
crates/tb-ide/src/debugger.rs
Normal file
687
crates/tb-ide/src/debugger.rs
Normal file
@@ -0,0 +1,687 @@
|
||||
//! IDE debugger state and commands on the session's existing VM.
|
||||
use crate::{
|
||||
app::{App, Dialog, DialogKind, Execution, Field, Hit, WindowKind},
|
||||
commands::Command,
|
||||
documents::{BreakpointMark, Document, DocumentId},
|
||||
editor::line_start,
|
||||
};
|
||||
use anyhow::{anyhow, ensure, Result};
|
||||
use crossterm::event::{KeyCode as K, KeyEvent, KeyModifiers as M};
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
use tb_frontend::{Diagnostic, SourcePos};
|
||||
use tb_vm::interp::{DebugLocation, DebugWatch, Vm};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Debugger {
|
||||
pub source_modules: std::collections::BTreeMap<DocumentId, DocumentId>,
|
||||
pub watches: Vec<DebugWatch>,
|
||||
pub frame: Option<u64>,
|
||||
pub trace: bool,
|
||||
pub history: bool,
|
||||
pub break_errors: bool,
|
||||
pub historical: Option<usize>,
|
||||
pub immediate: String,
|
||||
pub motion: Option<Command>,
|
||||
pub instant: Option<DebugWatch>,
|
||||
pub last_location: Option<DebugLocation>,
|
||||
}
|
||||
fn module_name(doc: &Document) -> String {
|
||||
match doc.content() {
|
||||
tb_vm::project_io::Content::Form(form) => form.root.name.clone(),
|
||||
_ => doc
|
||||
.source_path()
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
}
|
||||
}
|
||||
pub fn physical_line(doc: &Document, at: usize) -> u32 {
|
||||
let first = if let tb_vm::project_io::Content::Form(form) = doc.content() {
|
||||
tb_ui::frm::read_text(
|
||||
&doc.source_path().display().to_string(),
|
||||
&tb_ui::frm::write_text(form),
|
||||
)
|
||||
.map_or(1, |f| f.code_line())
|
||||
} else {
|
||||
1
|
||||
};
|
||||
first
|
||||
+ doc.code()[..at.min(doc.code().len())]
|
||||
.bytes()
|
||||
.filter(|c| *c == b'\n')
|
||||
.count() as u32
|
||||
}
|
||||
impl App {
|
||||
pub(crate) fn debug_setup(&mut self, vm: &mut Vm) {
|
||||
vm.debug.enabled = true;
|
||||
vm.debug.compiler = Some(self.editor.compiler.debug_compiler());
|
||||
vm.debug.trace = self.debugger.trace;
|
||||
vm.debug.history_on = self.debugger.history;
|
||||
vm.debug.break_errors = self.debugger.break_errors;
|
||||
for w in &mut self.debugger.watches {
|
||||
w.value = Err("Noch kein aktueller Halt".into());
|
||||
w.frame = None;
|
||||
}
|
||||
vm.debug.watches = self.debugger.watches.clone();
|
||||
self.debugger.frame = None;
|
||||
self.debugger.historical = None;
|
||||
self.debugger.last_location = None;
|
||||
self.debugger.instant = None;
|
||||
self.bind_debug_breakpoints(vm);
|
||||
}
|
||||
fn bind_debug_breakpoints(&mut self, vm: &mut Vm) {
|
||||
vm.clear_source_breakpoints();
|
||||
let ids = self
|
||||
.project
|
||||
.documents()
|
||||
.map(|(id, _)| id)
|
||||
.collect::<Vec<_>>();
|
||||
for id in ids {
|
||||
let doc = self.project.document(id).unwrap();
|
||||
let marks = doc
|
||||
.breakpoints
|
||||
.iter()
|
||||
.map(|m| {
|
||||
(
|
||||
m.valid,
|
||||
self.project
|
||||
.document(m.module)
|
||||
.map(module_name)
|
||||
.unwrap_or_default(),
|
||||
physical_line(doc, m.at),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let path = doc.source_path().display().to_string();
|
||||
let bound = marks
|
||||
.into_iter()
|
||||
.map(|(valid, module, line)| {
|
||||
let source = vm
|
||||
.source_files()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, s)| {
|
||||
s.path == path && vm.module_name(s.module).eq_ignore_ascii_case(&module)
|
||||
})
|
||||
.map(|(id, s)| (s.module, id as u32));
|
||||
valid
|
||||
&& source.is_some_and(|(module, source)| {
|
||||
vm.add_source_breakpoint(module, source, line)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (mark, bound) in self
|
||||
.project
|
||||
.breakpoint_marks_mut(id)
|
||||
.unwrap()
|
||||
.iter_mut()
|
||||
.zip(bound)
|
||||
{
|
||||
mark.bound = bound;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn debug_cursor(&self) -> Result<(String, u32)> {
|
||||
let view = self.project.view(self.editor_view()?)?;
|
||||
ensure!(
|
||||
!self.editor.expansions.contains_key(&self.editor_view()?),
|
||||
"Included File für einen physischen Quellort öffnen"
|
||||
);
|
||||
let doc = self.project.document(view.document())?;
|
||||
Ok((
|
||||
doc.source_path().display().to_string(),
|
||||
physical_line(doc, view.cursor),
|
||||
))
|
||||
}
|
||||
fn debug_target(&self) -> Result<(u16, u32, u32)> {
|
||||
ensure!(self.current_compilation().is_some() && self.editor.revision == self.session.source_revision, "Cursorziele benötigen den unveränderten Quellstand der pausierten Sitzung; zuerst neu starten");
|
||||
let (path, line) = self.debug_cursor()?;
|
||||
let vm = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Keine pausierte Sitzung"))?;
|
||||
let module = vm.current_module();
|
||||
let candidates = vm
|
||||
.source_files()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| s.path == path)
|
||||
.collect::<Vec<_>>();
|
||||
let (source, file) = candidates
|
||||
.iter()
|
||||
.find(|(_, s)| s.module == module)
|
||||
.copied()
|
||||
.or_else(|| (candidates.len() == 1).then(|| candidates[0]))
|
||||
.ok_or_else(|| {
|
||||
anyhow!("Quelldatei gehört nicht eindeutig zur pausierten Kompilatrevision")
|
||||
})?;
|
||||
Ok((file.module, source as u32, line))
|
||||
}
|
||||
pub fn toggle_debug_breakpoint(&mut self) -> Result<()> {
|
||||
let view = self.project.view(self.editor_view()?)?;
|
||||
let id = view.document();
|
||||
let at = line_start(self.project.document(id)?.code(), view.cursor);
|
||||
let doc = self.project.document(id)?;
|
||||
let line = physical_line(doc, at);
|
||||
let module = if self.project.members().contains(&id) {
|
||||
Some(id)
|
||||
} else {
|
||||
self.debugger
|
||||
.source_modules
|
||||
.get(&id)
|
||||
.copied()
|
||||
.or_else(|| self.project.members().get(self.selected_member).copied())
|
||||
}
|
||||
.ok_or_else(|| anyhow!("Kein Ursprungsmodul ausgewählt"))?;
|
||||
let existing = doc
|
||||
.breakpoints
|
||||
.iter()
|
||||
.position(|m| physical_line(doc, m.at) == line && m.module == module);
|
||||
if let Some(index) = existing {
|
||||
self.project.breakpoint_marks_mut(id)?.remove(index);
|
||||
} else {
|
||||
self.project.breakpoint_marks_mut(id)?.push(BreakpointMark {
|
||||
at,
|
||||
module,
|
||||
valid: true,
|
||||
bound: false,
|
||||
});
|
||||
}
|
||||
if self.current_compilation().is_some()
|
||||
&& self.editor.revision == self.session.source_revision
|
||||
{
|
||||
if let Some(mut vm) = self.session.vm.take() {
|
||||
self.bind_debug_breakpoints(&mut vm);
|
||||
self.session.vm = Some(vm);
|
||||
}
|
||||
}
|
||||
self.message =
|
||||
"Breakpoint aktualisiert · ● gebunden, ○ inaktiv bis gültiger Übersetzung".into();
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn debug_command(&mut self, c: Command) -> Result<bool> {
|
||||
use Command::*;
|
||||
match c {
|
||||
Breakpoint => self.toggle_debug_breakpoint()?,
|
||||
ClearBreakpoints => {
|
||||
let ids = self
|
||||
.project
|
||||
.documents()
|
||||
.map(|(id, _)| id)
|
||||
.collect::<Vec<_>>();
|
||||
for id in ids {
|
||||
self.project.breakpoint_marks_mut(id)?.clear();
|
||||
}
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
vm.clear_source_breakpoints();
|
||||
}
|
||||
}
|
||||
Step | ProcedureStep | RunToCursor => {
|
||||
ensure!(
|
||||
self.execution != Execution::Paused || self.session.vm.is_some(),
|
||||
"Keine pausierte VM"
|
||||
);
|
||||
if !matches!(
|
||||
self.execution,
|
||||
Execution::Paused | Execution::Running | Execution::Waiting
|
||||
) {
|
||||
self.start_execution()?;
|
||||
}
|
||||
self.debugger.motion = Some(c);
|
||||
if self.execution == Execution::Paused {
|
||||
self.continue_execution()?;
|
||||
} else {
|
||||
self.debug_resume_motion()?;
|
||||
}
|
||||
}
|
||||
NextStatement => {
|
||||
self.debugger.historical = None;
|
||||
self.debug_navigate(None)?;
|
||||
}
|
||||
SetStatement => {
|
||||
ensure!(
|
||||
self.execution == Execution::Paused,
|
||||
"Set Next Statement benötigt Break-Modus"
|
||||
);
|
||||
let (_, source, line) = self.debug_target()?;
|
||||
let vm = self.session.vm.as_mut().unwrap();
|
||||
let frame = vm
|
||||
.debug_location()
|
||||
.ok_or_else(|| anyhow!("Kein Frame"))?
|
||||
.frame;
|
||||
ensure!(
|
||||
self.debugger.frame.is_none_or(|id| id == frame),
|
||||
"Inspektionsrahmen ist nicht der aktive Frame"
|
||||
);
|
||||
vm.set_next_statement(frame, source, line)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
self.debug_navigate(None)?;
|
||||
}
|
||||
AddWatch | InstantWatch | Watchpoint => self.open_dialog(
|
||||
if c == Watchpoint {
|
||||
"Watchpoint"
|
||||
} else {
|
||||
"Watch"
|
||||
},
|
||||
DialogKind::Watch(c),
|
||||
vec![Field::text("BASIC-Ausdruck", "")],
|
||||
),
|
||||
DeleteWatch => self.open_dialog(
|
||||
"Delete Watch",
|
||||
DialogKind::DeleteWatch,
|
||||
vec![Field::choice(
|
||||
"Watch",
|
||||
self.debugger
|
||||
.watches
|
||||
.iter()
|
||||
.map(|w| w.expression.clone())
|
||||
.collect(),
|
||||
0,
|
||||
)],
|
||||
),
|
||||
DeleteWatches => {
|
||||
self.debugger.watches.clear();
|
||||
self.sync_watchpoints();
|
||||
}
|
||||
Trace | History | BreakErrors => {
|
||||
match c {
|
||||
Trace => self.debugger.trace = !self.debugger.trace,
|
||||
History => self.debugger.history = !self.debugger.history,
|
||||
_ => self.debugger.break_errors = !self.debugger.break_errors,
|
||||
}
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
vm.debug.trace = self.debugger.trace;
|
||||
vm.debug.history_on = self.debugger.history;
|
||||
vm.debug.break_errors = self.debugger.break_errors;
|
||||
}
|
||||
self.message = format!(
|
||||
"Trace {} · History {} · Break on Errors {}",
|
||||
self.debugger.trace, self.debugger.history, self.debugger.break_errors
|
||||
);
|
||||
}
|
||||
HistoryBack | HistoryForward => {
|
||||
let vm = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Keine History"))?;
|
||||
ensure!(!vm.debug.history.is_empty(), "History ist leer");
|
||||
let last = vm.debug.history.len();
|
||||
let at = self.debugger.historical.unwrap_or(last);
|
||||
let at = if c == HistoryBack {
|
||||
at.saturating_sub(1)
|
||||
} else {
|
||||
(at + 1).min(last)
|
||||
};
|
||||
let location = vm.debug.history.get(at).cloned();
|
||||
self.debugger.historical = (at < last).then_some(at);
|
||||
self.debug_navigate(location)?;
|
||||
}
|
||||
Calls | Debug => {
|
||||
self.refresh_debug_watches();
|
||||
self.show_tool(if c == Calls {
|
||||
WindowKind::Calls
|
||||
} else {
|
||||
WindowKind::Debug
|
||||
});
|
||||
}
|
||||
Immediate => self.show_tool(WindowKind::Immediate),
|
||||
_ => return Ok(false),
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
pub(crate) fn debug_resume_motion(&mut self) -> Result<()> {
|
||||
let motion = self.debugger.motion.take();
|
||||
let target = if motion == Some(Command::RunToCursor) {
|
||||
Some(self.debug_target()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
vm.set_step(false);
|
||||
vm.debug.cancel_motion();
|
||||
vm.debug.error = None;
|
||||
match motion {
|
||||
Some(Command::Step) => vm.set_step(true),
|
||||
Some(Command::ProcedureStep) => vm.step_over(),
|
||||
Some(Command::RunToCursor) => {
|
||||
let (m, s, l) = target.unwrap();
|
||||
vm.run_to_cursor(m, s, l).map_err(|e| anyhow!(e))?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.debugger.historical = None;
|
||||
Ok(())
|
||||
}
|
||||
fn sync_watchpoints(&mut self) {
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
vm.debug.watches = self.debugger.watches.clone();
|
||||
}
|
||||
}
|
||||
pub fn refresh_debug_watches(&mut self) {
|
||||
let Some(vm) = &mut self.session.vm else {
|
||||
return;
|
||||
};
|
||||
let Some(active) = vm.debug_location() else {
|
||||
return;
|
||||
};
|
||||
let frame = self
|
||||
.debugger
|
||||
.frame
|
||||
.filter(|id| vm.debug_frames().iter().any(|f| f.id == *id))
|
||||
.unwrap_or(active.frame);
|
||||
self.debugger.frame = Some(frame);
|
||||
for watch in &mut self.debugger.watches {
|
||||
watch.frame = Some(frame);
|
||||
watch.value = if watch.condition {
|
||||
vm.evaluate_condition(frame, &watch.expression)
|
||||
} else {
|
||||
vm.evaluate_watch(frame, &watch.expression)
|
||||
};
|
||||
}
|
||||
self.sync_watchpoints();
|
||||
}
|
||||
pub fn select_debug_frame(&mut self, index: usize) -> Result<()> {
|
||||
ensure!(
|
||||
self.execution == Execution::Paused,
|
||||
"Calls-Inspektion benötigt Break-Modus"
|
||||
);
|
||||
let frame = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.and_then(|vm| vm.debug_frames().get(index).cloned())
|
||||
.ok_or_else(|| anyhow!("Frame nicht erreichbar"))?;
|
||||
self.debugger.frame = Some(frame.id);
|
||||
self.refresh_debug_watches();
|
||||
self.debug_navigate(Some(frame.location))
|
||||
}
|
||||
pub(crate) fn debug_navigate(&mut self, location: Option<DebugLocation>) -> Result<()> {
|
||||
let vm = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Keine laufende Sitzung"))?;
|
||||
let location = location
|
||||
.or_else(|| vm.execution_location())
|
||||
.ok_or_else(|| anyhow!("Kein Quellort"))?;
|
||||
let file = vm.source_files()[location.source as usize].path.clone();
|
||||
if file == "<Immediate>" {
|
||||
self.show_tool(WindowKind::Immediate);
|
||||
self.message = format!("Direktkommando pausiert · Zeile {}", location.line);
|
||||
return Ok(());
|
||||
}
|
||||
self.debugger.last_location = Some(location.clone());
|
||||
let owner_name = vm.module_name(location.module).to_string();
|
||||
let owner = self.project.members().iter().copied().find(|id| {
|
||||
self.project
|
||||
.document(*id)
|
||||
.is_ok_and(|doc| module_name(doc).eq_ignore_ascii_case(&owner_name))
|
||||
});
|
||||
self.session.fullscreen = false;
|
||||
self.goto_diagnostic(&Diagnostic {
|
||||
file: Some(file.clone()),
|
||||
pos: SourcePos {
|
||||
source: location.source,
|
||||
line: location.line,
|
||||
column: location.column,
|
||||
},
|
||||
message: String::new(),
|
||||
})?;
|
||||
if let (Some(id), Some(owner)) = (self.active_document(), owner) {
|
||||
self.debugger.source_modules.insert(id, owner);
|
||||
}
|
||||
self.message = format!(
|
||||
"{} · {}:{} · Werte vom aktuellen Halt{}",
|
||||
if self.debugger.historical.is_some() {
|
||||
"HISTORIE"
|
||||
} else if self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.and_then(|vm| vm.debug_location())
|
||||
.is_some_and(|at| at.frame != location.frame)
|
||||
{
|
||||
"Inspektionsrahmen"
|
||||
} else {
|
||||
"Ausführungsstelle"
|
||||
},
|
||||
file,
|
||||
location.line,
|
||||
if self.session.old_revision {
|
||||
" · ALTES KOMPILAT"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
if let Some(error) = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.and_then(|vm| vm.debug.error.as_ref())
|
||||
{
|
||||
self.message.push_str(&format!(
|
||||
" · Behandelter Fehler {} · Handler {:?}",
|
||||
error.code, error.handler
|
||||
));
|
||||
}
|
||||
if let Some(error) = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.and_then(|vm| vm.debug.immediate_error.as_ref())
|
||||
{
|
||||
self.message.push_str(&format!(" · Direktfehler: {error}"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn submit_immediate(&mut self, text: &str) -> Result<()> {
|
||||
ensure!(
|
||||
self.execution == Execution::Paused,
|
||||
"Direktkommandos benötigen Break-Modus"
|
||||
);
|
||||
let vm = self
|
||||
.session
|
||||
.vm
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("Keine Sitzung"))?;
|
||||
let frame = self
|
||||
.debugger
|
||||
.frame
|
||||
.or_else(|| vm.debug_location().map(|l| l.frame))
|
||||
.ok_or_else(|| anyhow!("Kein Frame"))?;
|
||||
vm.start_immediate(frame, text).map_err(|e| anyhow!(e))?;
|
||||
self.execution = Execution::Running;
|
||||
self.message = "Direktkommando läuft · Unterbrechen über Run/Break".into();
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn debug_submit(&mut self, d: &Dialog) -> Result<()> {
|
||||
match d.kind {
|
||||
DialogKind::Watch(command) => {
|
||||
let text = d.fields[0].string();
|
||||
ensure!(!text.trim().is_empty(), "Ausdruck fehlt");
|
||||
let mut watch = DebugWatch {
|
||||
expression: text,
|
||||
condition: command == Command::Watchpoint,
|
||||
frame: None,
|
||||
value: Err("Noch kein Halt".into()),
|
||||
};
|
||||
if self.execution == Execution::Paused {
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
if let Some(frame) = self
|
||||
.debugger
|
||||
.frame
|
||||
.or_else(|| vm.debug_location().map(|p| p.frame))
|
||||
{
|
||||
watch.frame = Some(frame);
|
||||
watch.value = if watch.condition {
|
||||
vm.evaluate_condition(frame, &watch.expression)
|
||||
} else {
|
||||
vm.evaluate_watch(frame, &watch.expression)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if command == Command::InstantWatch {
|
||||
self.debugger.instant = Some(watch);
|
||||
} else {
|
||||
self.debugger.watches.push(watch);
|
||||
self.sync_watchpoints();
|
||||
}
|
||||
self.show_tool(WindowKind::Debug);
|
||||
}
|
||||
DialogKind::DeleteWatch => {
|
||||
let i = d.fields[0].index();
|
||||
if i < self.debugger.watches.len() {
|
||||
self.debugger.watches.remove(i);
|
||||
self.sync_watchpoints();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn debug_key(&mut self, key: KeyEvent) -> Result<bool> {
|
||||
match self.active_window().map(|w| w.kind) {
|
||||
Some(WindowKind::Immediate) => {
|
||||
match key.code {
|
||||
K::Enter => {
|
||||
let text = self.debugger.immediate.clone();
|
||||
self.submit_immediate(&text)?;
|
||||
self.debugger.immediate.clear();
|
||||
}
|
||||
K::Backspace => {
|
||||
self.debugger.immediate.pop();
|
||||
}
|
||||
K::Char(c) if !key.modifiers.intersects(M::CONTROL | M::ALT) => {
|
||||
self.debugger.immediate.push(c)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
Some(WindowKind::Calls) => {
|
||||
let frames = self
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.map(|vm| vm.debug_frames())
|
||||
.unwrap_or_default();
|
||||
let at = frames
|
||||
.iter()
|
||||
.position(|f| Some(f.id) == self.debugger.frame)
|
||||
.unwrap_or(0);
|
||||
let next = match key.code {
|
||||
K::Down => (at + 1).min(frames.len().saturating_sub(1)),
|
||||
K::Up => at.saturating_sub(1),
|
||||
K::Enter => at,
|
||||
_ => return Ok(true),
|
||||
};
|
||||
self.select_debug_frame(next)?;
|
||||
self.show_tool(WindowKind::Calls);
|
||||
Ok(true)
|
||||
}
|
||||
Some(WindowKind::Debug) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
pub(crate) fn debug_render(&mut self, f: &mut Frame, kind: WindowKind, r: Rect) {
|
||||
let mut lines = Vec::new();
|
||||
match kind {
|
||||
WindowKind::Immediate => {
|
||||
lines.push(format!("> {}", self.debugger.immediate));
|
||||
lines.push("Enter: ausführen · Run/Break: unterbrechen · F5: fortsetzen".into());
|
||||
lines.push(self.message.clone());
|
||||
if let Some(vm) = &self.session.vm {
|
||||
if let Some(error) = &vm.debug.immediate_error {
|
||||
lines.push(error.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowKind::Calls => {
|
||||
if let Some(vm) = &self.session.vm {
|
||||
for (row, frame) in vm.debug_frames().iter().enumerate() {
|
||||
lines.push(format!(
|
||||
"{} {} · {}:{}",
|
||||
if Some(frame.id) == self.debugger.frame {
|
||||
"▶"
|
||||
} else {
|
||||
" "
|
||||
},
|
||||
frame.procedure,
|
||||
frame.file,
|
||||
frame.location.line
|
||||
));
|
||||
if row < r.height as usize {
|
||||
self.hits.push((
|
||||
Rect::new(r.x, r.y + row as u16, r.width, 1),
|
||||
Hit::DebugFrame(row),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
lines.push(if self.debugger.historical.is_some() {
|
||||
"HISTORIE · Werte vom aktuellen Halt".into()
|
||||
} else {
|
||||
"Watches · Werte vom aktuellen Halt (read-only)".into()
|
||||
});
|
||||
for watch in self
|
||||
.debugger
|
||||
.watches
|
||||
.iter()
|
||||
.chain(self.debugger.instant.iter())
|
||||
{
|
||||
lines.push(format!(
|
||||
"{}{} = {}",
|
||||
if watch.condition { "[Watchpoint] " } else { "" },
|
||||
watch.expression,
|
||||
match &watch.value {
|
||||
Ok(v) => format!("{v:?}"),
|
||||
Err(e) => format!("FEHLER: {e}"),
|
||||
}
|
||||
));
|
||||
}
|
||||
if let Some(vm) = &self.session.vm {
|
||||
if let Some(error) = &vm.debug.error {
|
||||
lines.push(format!(
|
||||
"Behandelter Fehler {} · Ursache {:?} · Handler {:?}",
|
||||
error.code, error.origin, error.handler
|
||||
));
|
||||
}
|
||||
}
|
||||
for (_, doc) in self.project.documents() {
|
||||
for mark in &doc.breakpoints {
|
||||
lines.push(format!(
|
||||
"{} {}:{}",
|
||||
if mark.bound { "●" } else { "○ inaktiv" },
|
||||
doc.source_path().display(),
|
||||
physical_line(doc, mark.at)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
f.render_widget(
|
||||
Paragraph::new(lines.join("\n"))
|
||||
.style(Style::default().fg(Color::White).bg(Color::Blue)),
|
||||
r,
|
||||
);
|
||||
if kind == WindowKind::Immediate && r.height > 4 {
|
||||
f.render_widget(
|
||||
tb_ui::screen::ScreenWidget(self.session.screen()),
|
||||
Rect::new(r.x, r.y + 4, r.width, r.height - 4),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,13 +50,23 @@ impl View {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Edit {
|
||||
breakpoints: Vec<BreakpointMark>,
|
||||
content: Content,
|
||||
design_ids: BTreeMap<(String, Option<i32>), u64>,
|
||||
group: Option<(u64, Vec<DocumentId>)>,
|
||||
views: Vec<(ViewId, View)>,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BreakpointMark {
|
||||
pub at: usize,
|
||||
pub module: DocumentId,
|
||||
pub valid: bool,
|
||||
pub bound: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Document {
|
||||
pub breakpoints: Vec<BreakpointMark>,
|
||||
source_path: PathBuf,
|
||||
path: Option<PathBuf>,
|
||||
content: Content,
|
||||
@@ -68,6 +78,29 @@ pub struct Document {
|
||||
undo: Vec<Edit>,
|
||||
design_ids: BTreeMap<(String, Option<i32>), u64>,
|
||||
}
|
||||
fn remap_breakpoints(
|
||||
marks: &[BreakpointMark],
|
||||
edits: &[(Range<usize>, String)],
|
||||
) -> Vec<BreakpointMark> {
|
||||
marks
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut mark| {
|
||||
for (range, text) in edits.iter().rev() {
|
||||
if range.contains(&mark.at) {
|
||||
mark.valid = false;
|
||||
}
|
||||
if mark.at >= range.end {
|
||||
mark.at = mark.at - range.len() + text.len();
|
||||
} else if mark.at > range.start {
|
||||
mark.at = range.start;
|
||||
}
|
||||
}
|
||||
mark.bound = false;
|
||||
mark
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
impl Document {
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
@@ -251,6 +284,7 @@ impl Project {
|
||||
self.documents.insert(
|
||||
id,
|
||||
Document {
|
||||
breakpoints: Vec::new(),
|
||||
source_path: path.clone(),
|
||||
path: Some(path.clone()),
|
||||
saved: Some(read.content.clone()),
|
||||
@@ -303,6 +337,7 @@ impl Project {
|
||||
self.documents.insert(
|
||||
id,
|
||||
Document {
|
||||
breakpoints: Vec::new(),
|
||||
source_path: path.clone(),
|
||||
path: None,
|
||||
content,
|
||||
@@ -542,7 +577,15 @@ impl Project {
|
||||
at
|
||||
}
|
||||
};
|
||||
let marks = remap_breakpoints(
|
||||
&old.breakpoints,
|
||||
&[(
|
||||
removed.clone(),
|
||||
after[removed.start..removed.start + inserted].to_owned(),
|
||||
)],
|
||||
);
|
||||
let undo = Edit {
|
||||
breakpoints: old.breakpoints.clone(),
|
||||
content: old.content.clone(),
|
||||
design_ids: old.design_ids.clone(),
|
||||
group: None,
|
||||
@@ -554,6 +597,7 @@ impl Project {
|
||||
.collect(),
|
||||
};
|
||||
let doc = self.documents.get_mut(&id).unwrap();
|
||||
doc.breakpoints = marks;
|
||||
doc.undo.push(undo);
|
||||
doc.content = content;
|
||||
doc.revision += 1;
|
||||
@@ -567,6 +611,13 @@ impl Project {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn breakpoint_marks_mut(&mut self, id: DocumentId) -> Result<&mut Vec<BreakpointMark>> {
|
||||
Ok(&mut self
|
||||
.documents
|
||||
.get_mut(&id)
|
||||
.ok_or_else(|| anyhow!("Unbekanntes Dokument"))?
|
||||
.breakpoints)
|
||||
}
|
||||
pub fn replace_text(&mut self, id: DocumentId, range: Range<usize>, text: &str) -> Result<()> {
|
||||
self.replace_ranges(id, &[(range, text.to_owned())])
|
||||
}
|
||||
@@ -576,6 +627,7 @@ impl Project {
|
||||
id: DocumentId,
|
||||
edits: &[(Range<usize>, String)],
|
||||
) -> Result<()> {
|
||||
let marks = remap_breakpoints(&self.document(id)?.breakpoints, edits);
|
||||
let mut content = self.document(id)?.content.clone();
|
||||
let mut end = 0;
|
||||
for (range, _) in edits {
|
||||
@@ -597,6 +649,7 @@ impl Project {
|
||||
content.code_mut().replace_range(range.clone(), text);
|
||||
}
|
||||
self.commit_edit(id, content)?;
|
||||
self.documents.get_mut(&id).unwrap().breakpoints = marks;
|
||||
for (view_id, old) in views {
|
||||
let map = |mut at: usize| {
|
||||
for (range, text) in edits.iter().rev() {
|
||||
@@ -659,6 +712,7 @@ impl Project {
|
||||
let Some(edit) = doc.undo.pop() else {
|
||||
return Ok(false);
|
||||
};
|
||||
doc.breakpoints = edit.breakpoints;
|
||||
doc.content = edit.content;
|
||||
doc.design_ids = edit.design_ids;
|
||||
doc.revision += 1;
|
||||
|
||||
@@ -501,7 +501,15 @@ impl App {
|
||||
.resolve(doc.source_path().parent().unwrap(), &path)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
self.editor.history.push((id, view.cursor));
|
||||
let owner = if self.project.members().contains(&id) {
|
||||
Some(id)
|
||||
} else {
|
||||
self.debugger.source_modules.get(&id).copied()
|
||||
};
|
||||
let id = self.project.open_document(&path)?;
|
||||
if let Some(owner) = owner {
|
||||
self.debugger.source_modules.insert(id, owner);
|
||||
}
|
||||
self.show_document(id, false)?;
|
||||
}
|
||||
IncludedLines => {
|
||||
@@ -809,6 +817,7 @@ impl App {
|
||||
}
|
||||
/// Immer aktuelle Quellen einschließlich externer Includes prüfen; alte Erfolge werden nie herausgegeben.
|
||||
pub fn compile_current(&mut self) -> Result<&CompiledModule> {
|
||||
self.editor.compiler.debug_symbols = true;
|
||||
self.editor.compiled = None;
|
||||
self.editor.revision = None;
|
||||
self.editor.diagnostics.clear();
|
||||
|
||||
@@ -51,7 +51,7 @@ pub struct Session {
|
||||
pub forms_phase: bool,
|
||||
pub file_shell: bool,
|
||||
revision: Vec<u8>,
|
||||
source_revision: Option<crate::editor::Revision>,
|
||||
pub(crate) source_revision: Option<crate::editor::Revision>,
|
||||
output: TextScreen,
|
||||
}
|
||||
impl Default for Session {
|
||||
@@ -109,7 +109,7 @@ impl App {
|
||||
.map_err(|e| anyhow!(e))?
|
||||
};
|
||||
let revision = module.to_tbc();
|
||||
let vm = new_execution(
|
||||
let mut vm = new_execution(
|
||||
module,
|
||||
&self.session.command,
|
||||
Some((self.size.0 as usize, self.size.1 as usize)),
|
||||
@@ -126,6 +126,7 @@ impl App {
|
||||
} else {
|
||||
target
|
||||
};
|
||||
self.debug_setup(&mut vm);
|
||||
self.session.finish();
|
||||
self.basic_events.clear();
|
||||
self.session.vm = Some(vm);
|
||||
@@ -165,6 +166,11 @@ impl App {
|
||||
)
|
||||
},
|
||||
);
|
||||
if let Some(vm) = &mut self.session.vm {
|
||||
vm.debug.cancel_motion();
|
||||
}
|
||||
self.debugger.frame = None;
|
||||
self.refresh_debug_watches();
|
||||
}
|
||||
}
|
||||
pub fn continue_execution(&mut self) -> Result<()> {
|
||||
@@ -208,6 +214,10 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
pub fn resume_execution(&mut self, old: bool) {
|
||||
if let Err(e) = self.debug_resume_motion() {
|
||||
self.message = e.to_string();
|
||||
return;
|
||||
}
|
||||
if let Some(vm) = self.session.vm.as_mut() {
|
||||
vm.rt.abbruch = false;
|
||||
self.session.old_revision = old;
|
||||
@@ -272,6 +282,11 @@ impl App {
|
||||
} else {
|
||||
vm.poll(&mut self.session.host, 4096)
|
||||
};
|
||||
for (watch, live) in self.debugger.watches.iter_mut().zip(&vm.debug.watches) {
|
||||
if watch.condition {
|
||||
watch.value = live.value.clone();
|
||||
}
|
||||
}
|
||||
match result {
|
||||
PollResult::Yield => self.execution = Execution::Running,
|
||||
PollResult::Waiting { .. } => self.execution = Execution::Waiting,
|
||||
@@ -289,10 +304,11 @@ impl App {
|
||||
self.execution = Execution::Ended;
|
||||
self.message = "Ended".into();
|
||||
}
|
||||
RunEvent::Stopped { .. }
|
||||
| RunEvent::Interrupted { .. }
|
||||
| RunEvent::Breakpoint { .. }
|
||||
| RunEvent::Stepped { .. } => self.pause_execution(),
|
||||
RunEvent::Stopped { .. } | RunEvent::Interrupted { .. } => self.pause_execution(),
|
||||
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } => {
|
||||
self.pause_execution();
|
||||
let _ = self.debug_navigate(None);
|
||||
}
|
||||
RunEvent::Error {
|
||||
code,
|
||||
line,
|
||||
@@ -323,5 +339,12 @@ impl App {
|
||||
}
|
||||
},
|
||||
}
|
||||
if self.debugger.trace && matches!(self.execution, Execution::Running | Execution::Waiting)
|
||||
{
|
||||
let location = self.session.vm.as_ref().and_then(|vm| vm.debug_location());
|
||||
if location != self.debugger.last_location {
|
||||
let _ = self.debug_navigate(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,3 +12,5 @@ pub mod editor;
|
||||
pub mod execution;
|
||||
|
||||
pub mod designer;
|
||||
|
||||
pub mod debugger;
|
||||
|
||||
@@ -222,6 +222,81 @@ impl App {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
f.render_widget(Paragraph::new(text).style(content_style), inner);
|
||||
if expansion.is_none() {
|
||||
for mark in &doc.breakpoints {
|
||||
let row = doc.code()[..mark.at.min(doc.code().len())]
|
||||
.bytes()
|
||||
.filter(|c| *c == b'\n')
|
||||
.count();
|
||||
if row >= v.scroll_line
|
||||
&& row < v.scroll_line + inner.height as usize
|
||||
{
|
||||
put(
|
||||
f,
|
||||
Rect::new(
|
||||
rect.x,
|
||||
inner.y + (row - v.scroll_line) as u16,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
if mark.bound { "●" } else { "○" },
|
||||
Style::default().fg(Color::Red),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expansion.is_none() {
|
||||
if let Some(vm) = &self.session.vm {
|
||||
let actual = vm.execution_location();
|
||||
let selected = self.debugger.last_location.as_ref();
|
||||
for (location, mark) in
|
||||
actual.as_ref().map(|p| (p, "▶")).into_iter().chain(
|
||||
selected.map(|p| {
|
||||
(
|
||||
p,
|
||||
if self.debugger.historical.is_some() {
|
||||
"H"
|
||||
} else if actual
|
||||
.as_ref()
|
||||
.is_some_and(|a| a.frame != p.frame)
|
||||
{
|
||||
"I"
|
||||
} else {
|
||||
"▶"
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
{
|
||||
if vm.source_files().get(location.source as usize).is_some_and(
|
||||
|source| {
|
||||
std::path::Path::new(&source.path) == doc.source_path()
|
||||
},
|
||||
) {
|
||||
let row = location
|
||||
.line
|
||||
.saturating_sub(crate::debugger::physical_line(doc, 0))
|
||||
as usize;
|
||||
if row >= v.scroll_line
|
||||
&& row < v.scroll_line + inner.height as usize
|
||||
{
|
||||
put(
|
||||
f,
|
||||
Rect::new(
|
||||
rect.x,
|
||||
inner.y + (row - v.scroll_line) as u16,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
mark,
|
||||
Style::default().fg(Color::Yellow),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if count > inner.height as usize {
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||
@@ -335,6 +410,9 @@ impl App {
|
||||
WindowKind::Output => {
|
||||
f.render_widget(tb_ui::screen::ScreenWidget(self.session.screen()), inner)
|
||||
}
|
||||
kind @ (WindowKind::Calls | WindowKind::Debug | WindowKind::Immediate) => {
|
||||
self.debug_render(f, kind, inner)
|
||||
}
|
||||
kind => put(
|
||||
f,
|
||||
inner,
|
||||
|
||||
@@ -252,7 +252,7 @@ fn dispatcher_keeps_function_keys_and_copy_out_of_basic_input() {
|
||||
app.execution = Execution::Paused;
|
||||
plain(&mut app, K::F(10));
|
||||
assert_eq!(app.last_command, Some(Command::ProcedureStep));
|
||||
assert!(app.message.contains("06"));
|
||||
assert_eq!(app.availability(Command::ProcedureStep), None);
|
||||
menu(&mut app, Command::Output);
|
||||
app.execution = Execution::Running;
|
||||
plain(&mut app, K::Char('a'));
|
||||
|
||||
339
crates/tb-ide/tests/debugger.rs
Normal file
339
crates/tb-ide/tests/debugger.rs
Normal file
@@ -0,0 +1,339 @@
|
||||
use crossterm::event::{Event, KeyCode as K, KeyEvent, KeyModifiers as M};
|
||||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
use tb_ide::{
|
||||
app::{App, Execution, Field, WindowKind},
|
||||
commands::Command,
|
||||
};
|
||||
use tb_runtime::value::Value;
|
||||
struct Temp(PathBuf);
|
||||
impl Temp {
|
||||
fn new() -> Self {
|
||||
static N: AtomicUsize = AtomicUsize::new(0);
|
||||
let p = std::env::temp_dir().join(format!(
|
||||
"tb-debug-{}-{}",
|
||||
std::process::id(),
|
||||
N.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir_all(&p).unwrap();
|
||||
Self(p.canonicalize().unwrap())
|
||||
}
|
||||
fn app(&self, source: &str) -> App {
|
||||
let p = self.0.join("main.bas");
|
||||
fs::write(&p, source).unwrap();
|
||||
let mut a = App::new(&self.0, self.0.join("config"), (100, 30)).unwrap();
|
||||
a.load_initial_project(p).unwrap();
|
||||
a.options.syntax_checking = false;
|
||||
a
|
||||
}
|
||||
}
|
||||
impl Drop for Temp {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
fn key(a: &mut App, k: K, m: M) {
|
||||
a.handle(Event::Key(KeyEvent::new(k, m)));
|
||||
}
|
||||
fn tick(a: &mut App) {
|
||||
for _ in 0..30 {
|
||||
a.tick_execution(0);
|
||||
if !matches!(a.execution, Execution::Running | Execution::Waiting) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn cursor(a: &mut App, line: usize) {
|
||||
let (id, v) = a
|
||||
.windows
|
||||
.iter()
|
||||
.find_map(|w| {
|
||||
if let WindowKind::Code(v) = w.kind {
|
||||
Some((w.id, v))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
a.execute(Command::FocusWindow(id));
|
||||
let doc = a.project.view(v).unwrap().document();
|
||||
let code = a.project.document(doc).unwrap().code();
|
||||
let at = code
|
||||
.split_inclusive('\n')
|
||||
.take(line - 1)
|
||||
.map(str::len)
|
||||
.sum();
|
||||
a.project.view_mut(v).unwrap().cursor = at;
|
||||
}
|
||||
fn watch(a: &mut App, c: Command, text: &str) {
|
||||
a.execute(c);
|
||||
a.dialog.as_mut().unwrap().fields[0] = Field::text("BASIC-Ausdruck", text);
|
||||
key(a, K::Enter, M::NONE);
|
||||
assert!(a.dialog.is_none(), "{:?}", a.dialog);
|
||||
}
|
||||
#[test]
|
||||
fn dispatcher_marks_edits_rebind_and_readonly_watches() {
|
||||
let t = Temp::new();
|
||||
let mut a = t.app("x%=1\nx%=2\nPRINT x%\nEND\n");
|
||||
cursor(&mut a, 2);
|
||||
key(&mut a, K::F(9), M::NONE);
|
||||
let doc = a.active_document().unwrap();
|
||||
a.project.replace_text(doc, 0..0, "' inserted\n").unwrap();
|
||||
assert_eq!(
|
||||
a.project.document(doc).unwrap().breakpoints[0].at,
|
||||
"' inserted\nx%=1\n".len()
|
||||
);
|
||||
key(&mut a, K::F(5), M::SHIFT);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3);
|
||||
assert!(a.project.document(doc).unwrap().breakpoints[0].bound);
|
||||
watch(&mut a, Command::AddWatch, "x% + 1");
|
||||
assert!(matches!(a.debugger.watches[0].value, Ok(Value::Int(2))));
|
||||
let before = a.project.document(doc).unwrap().code().to_string();
|
||||
key(&mut a, K::Char('z'), M::NONE);
|
||||
assert_eq!(a.project.document(doc).unwrap().code(), before);
|
||||
watch(&mut a, Command::InstantWatch, "RND");
|
||||
assert!(a.debugger.instant.as_ref().unwrap().value.is_err());
|
||||
a.execute(Command::Immediate);
|
||||
for c in "x%=7".chars() {
|
||||
key(&mut a, K::Char(c), M::NONE);
|
||||
}
|
||||
key(&mut a, K::Enter, M::NONE);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
|
||||
assert!(matches!(
|
||||
a.session.vm.as_ref().unwrap().inspect("x%"),
|
||||
Some(Value::Int(7))
|
||||
));
|
||||
a.execute(Command::DeleteWatches);
|
||||
assert!(a.debugger.watches.is_empty());
|
||||
key(&mut a, K::F(8), M::NONE);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 4);
|
||||
assert!(matches!(
|
||||
a.session.vm.as_ref().unwrap().inspect("x%"),
|
||||
Some(Value::Int(2))
|
||||
));
|
||||
a.execute(Command::ClearBreakpoints);
|
||||
key(&mut a, K::F(5), M::NONE);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Ended);
|
||||
a.project.replace_text(doc, 0..0, "' again\n").unwrap();
|
||||
cursor(&mut a, 1);
|
||||
key(&mut a, K::F(9), M::NONE);
|
||||
key(&mut a, K::F(5), M::SHIFT);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Ended);
|
||||
assert!(!a.project.document(doc).unwrap().breakpoints[0].bound);
|
||||
}
|
||||
#[test]
|
||||
fn calls_procedure_step_cursor_history_and_restart() {
|
||||
let t = Temp::new();
|
||||
let mut a=t.app("CALL rec(2)\nx%=1\nx%=2\nEND\nSUB rec(n AS INTEGER)\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n");
|
||||
a.execute(Command::History);
|
||||
cursor(&mut a, 6);
|
||||
key(&mut a, K::F(9), M::NONE);
|
||||
key(&mut a, K::F(5), M::SHIFT);
|
||||
tick(&mut a);
|
||||
a.execute(Command::ClearBreakpoints);
|
||||
let frame = a
|
||||
.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.debug_location()
|
||||
.unwrap()
|
||||
.frame;
|
||||
key(&mut a, K::F(10), M::NONE);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Paused);
|
||||
assert_eq!(
|
||||
a.session
|
||||
.vm
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.debug_location()
|
||||
.unwrap()
|
||||
.frame,
|
||||
frame
|
||||
);
|
||||
cursor(&mut a, 3);
|
||||
key(&mut a, K::F(7), M::NONE);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3);
|
||||
watch(&mut a, Command::AddWatch, "x%");
|
||||
let current = a.session.vm.as_ref().unwrap().debug_location();
|
||||
key(&mut a, K::F(8), M::SHIFT);
|
||||
key(&mut a, K::F(8), M::SHIFT);
|
||||
assert!(a.debugger.historical.is_some());
|
||||
assert!(a.message.contains("HISTORIE"));
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), current);
|
||||
assert!(matches!(a.debugger.watches[0].value, Ok(Value::Int(1))));
|
||||
key(&mut a, K::F(10), M::SHIFT);
|
||||
a.execute(Command::NextStatement);
|
||||
assert!(a.debugger.historical.is_none());
|
||||
a.execute(Command::Restart);
|
||||
assert!(a.debugger.frame.is_none());
|
||||
assert!(a.debugger.watches[0].value.is_err());
|
||||
assert!(a.session.vm.as_ref().unwrap().debug.history.is_empty());
|
||||
assert_eq!(a.debugger.watches.len(), 1);
|
||||
}
|
||||
#[test]
|
||||
fn includes_and_deleted_marks_do_not_bind_to_foreign_statements() {
|
||||
let t = Temp::new();
|
||||
fs::write(t.0.join("a.bi"), "'a\nx%=1\n").unwrap();
|
||||
fs::write(t.0.join("b.bi"), "'b\nx%=2\n").unwrap();
|
||||
let mut a = t.app("'$INCLUDE: 'a.bi'\n'$INCLUDE: 'b.bi'\nEND\n");
|
||||
cursor(&mut a, 2);
|
||||
a.execute(Command::IncludedFile);
|
||||
let doc = a.active_document().unwrap();
|
||||
let v = match a.active_window().unwrap().kind {
|
||||
WindowKind::Code(v) => v,
|
||||
_ => panic!(),
|
||||
};
|
||||
a.project.view_mut(v).unwrap().cursor = 3;
|
||||
key(&mut a, K::F(9), M::NONE);
|
||||
key(&mut a, K::F(5), M::SHIFT);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
|
||||
let vm = a.session.vm.as_ref().unwrap();
|
||||
assert!(vm.current_file().ends_with("b.bi"));
|
||||
assert!(matches!(vm.inspect("x%"), Some(Value::Int(1))));
|
||||
a.project.replace_text(doc, 3..8, "").unwrap();
|
||||
assert!(!a.project.document(doc).unwrap().breakpoints[0].valid);
|
||||
a.execute(Command::Restart);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Ended);
|
||||
assert!(!a.project.document(doc).unwrap().breakpoints[0].bound);
|
||||
a.project.undo(doc).unwrap();
|
||||
assert!(a.project.document(doc).unwrap().breakpoints[0].valid);
|
||||
}
|
||||
#[test]
|
||||
fn calls_mouse_inspection_instant_key_delete_and_error_window() {
|
||||
let t = Temp::new();
|
||||
let mut a = t.app(
|
||||
"CALL rec(2)\nEND\nSUB rec(n AS INTEGER)\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n",
|
||||
);
|
||||
cursor(&mut a, 5);
|
||||
key(&mut a, K::F(9), M::NONE);
|
||||
key(&mut a, K::F(5), M::SHIFT);
|
||||
tick(&mut a);
|
||||
let current = a.session.vm.as_ref().unwrap().debug_location();
|
||||
a.execute(Command::Calls);
|
||||
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(100, 30)).unwrap();
|
||||
terminal.draw(|f| a.render(f)).unwrap();
|
||||
let rect = a
|
||||
.hits
|
||||
.iter()
|
||||
.find_map(|(r, h)| matches!(h, tb_ide::app::Hit::DebugFrame(1)).then_some(*r))
|
||||
.unwrap();
|
||||
a.handle(Event::Mouse(crossterm::event::MouseEvent {
|
||||
kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
|
||||
column: rect.x,
|
||||
row: rect.y,
|
||||
modifiers: M::NONE,
|
||||
}));
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), current);
|
||||
key(&mut a, K::F(9), M::SHIFT);
|
||||
a.dialog.as_mut().unwrap().fields[0] = Field::text("Ausdruck", "n");
|
||||
key(&mut a, K::Enter, M::NONE);
|
||||
assert!(matches!(
|
||||
a.debugger.instant.as_ref().unwrap().value,
|
||||
Ok(Value::Int(1))
|
||||
));
|
||||
watch(&mut a, Command::AddWatch, "n+2");
|
||||
watch(&mut a, Command::AddWatch, "1/0");
|
||||
assert!(a.debugger.watches[1].value.is_err());
|
||||
a.execute(Command::DeleteWatch);
|
||||
key(&mut a, K::Enter, M::NONE);
|
||||
assert_eq!(a.debugger.watches.len(), 1);
|
||||
terminal.draw(|f| a.render(f)).unwrap();
|
||||
let display = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|c| c.symbol())
|
||||
.collect::<String>();
|
||||
assert!(display.contains("FEHLER"));
|
||||
a.execute(Command::Immediate);
|
||||
for c in "ERROR 5".chars() {
|
||||
key(&mut a, K::Char(c), M::NONE);
|
||||
}
|
||||
key(&mut a, K::Enter, M::NONE);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Paused);
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), current);
|
||||
terminal.draw(|f| a.render(f)).unwrap();
|
||||
let display = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|c| c.symbol())
|
||||
.collect::<String>();
|
||||
assert!(display.contains("Error"));
|
||||
}
|
||||
#[test]
|
||||
fn form_handler_keys_and_old_revision_cursor_guard() {
|
||||
let t = Temp::new();
|
||||
let p = t.0.join("window.frm");
|
||||
fs::write(&p,"VERSION 1.00\nBEGIN Form Form1\nEND\nSUB Form_Load()\nCALL work\nPRINT 2\nEND SUB\nSUB work()\nPRINT 1\nEND SUB\n").unwrap();
|
||||
let mut a = App::new(&t.0, t.0.join("config"), (100, 30)).unwrap();
|
||||
a.load_initial_project(p).unwrap();
|
||||
a.options.syntax_checking = false;
|
||||
cursor(&mut a, 2);
|
||||
key(&mut a, K::F(9), M::NONE);
|
||||
key(&mut a, K::F(5), M::SHIFT);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 5);
|
||||
key(&mut a, K::F(10), M::NONE);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 6);
|
||||
let before = a.session.vm.as_ref().unwrap().debug_location();
|
||||
let doc = a.active_document().unwrap();
|
||||
a.project.replace_text(doc, 0..0, "' edit\n").unwrap();
|
||||
a.execute(Command::SetStatement);
|
||||
assert!(a.message.contains("Quellstand"));
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), before);
|
||||
}
|
||||
#[test]
|
||||
fn immediate_wait_run_transition_watchpoint_and_trace_are_session_owned() {
|
||||
let t = Temp::new();
|
||||
let mut a=t.app("x%=1\nx%=2\nx%=3\nEND\nSUB ask(n AS INTEGER)\nINPUT n\nEND SUB\nSUB restart()\nRUN\nEND SUB\n");
|
||||
watch(&mut a, Command::Watchpoint, "x%=2");
|
||||
a.execute(Command::Trace);
|
||||
key(&mut a, K::F(5), M::SHIFT);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3);
|
||||
let before = a.session.vm.as_ref().unwrap().debug_location();
|
||||
a.execute(Command::DeleteWatches);
|
||||
a.execute(Command::Immediate);
|
||||
a.submit_immediate("CALL ask(x%)").unwrap();
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Waiting);
|
||||
a.execute(Command::Pause);
|
||||
assert_eq!(a.execution, Execution::Paused);
|
||||
assert!(a.session.vm.as_ref().unwrap().immediate_active());
|
||||
key(&mut a, K::F(5), M::NONE);
|
||||
a.execute(Command::Output);
|
||||
key(&mut a, K::Char('9'), M::NONE);
|
||||
key(&mut a, K::Enter, M::NONE);
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Paused);
|
||||
assert_eq!(a.session.vm.as_ref().unwrap().debug_location(), before);
|
||||
assert!(matches!(
|
||||
a.session.vm.as_ref().unwrap().inspect("x%"),
|
||||
Some(Value::Int(9))
|
||||
));
|
||||
a.submit_immediate("CALL restart()").unwrap();
|
||||
tick(&mut a);
|
||||
assert_eq!(a.execution, Execution::Ended);
|
||||
assert!(a.session.vm.is_none());
|
||||
}
|
||||
612
crates/tb-vm/src/debugger.rs
Normal file
612
crates/tb-vm/src/debugger.rs
Normal 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(¤t),
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
420
crates/tb-vm/tests/debugger.rs
Normal file
420
crates/tb-vm/tests/debugger.rs
Normal 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))));
|
||||
}
|
||||
Reference in New Issue
Block a user