Implement and archive Phase 5 debugger

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

View File

@@ -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, _)) => {