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, _)) => {
|
||||
|
||||
Reference in New Issue
Block a user