Phase 4: Ereignisschleife und klassische Traps

This commit is contained in:
2026-09-04 15:56:47 +02:00
parent 060674b466
commit 5cf5a6582c
40 changed files with 4024 additions and 176 deletions

View File

@@ -331,10 +331,28 @@ pub enum Stmt {
to: Option<Expr>,
pos: SourcePos,
},
NameStmt { old: Expr, new: Expr, pos: SourcePos },
NameStmt {
old: Expr,
new: Expr,
pos: SourcePos,
},
// ---- Bildschirm/Ereignisse ----
/// `VIEW PRINT [oben TO unten]`.
ViewPrint { top: Option<Expr>, bottom: Option<Expr>, pos: SourcePos },
ViewPrint {
top: Option<Expr>,
bottom: Option<Expr>,
pos: SourcePos,
},
/// `ON TIMER(n&) GOSUB ziel`, `ON KEY(n%) GOSUB ziel`,
/// `ON UEVENT GOSUB ziel`, `ON SIGNAL(n%) GOSUB ziel` — die
/// klassischen Ereignis-Traps (Sprachreferenz §8). `GOSUB 0` schaltet
/// den Trap ab.
TrapDef {
device: String,
index: Option<Expr>,
target: LabelRef,
pos: SourcePos,
},
/// `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` …
EventControl {
device: String,

View File

@@ -329,6 +329,8 @@ pub enum Builtin {
CommandS,
Doevents,
Sleep,
/// `SetUEvent` — löst das benutzerdefinierte Ereignis aus (§8).
SetUEvent,
Beep,
// ISAM (Change `phase-3-isam`)
IsamOpen,
@@ -537,6 +539,22 @@ pub enum HStmtKind {
targets: Vec<LabelId>,
},
ReturnGosub(Option<LabelId>),
/// Ereignis-Trap erklären (Sprachreferenz §8). `art` ist die Quelle
/// (0 KEY, 1 TIMER, 2 UEVENT, 3 SIGNAL), `index` ihre Kennung bzw. bei
/// `TIMER` das Intervall in Sekunden. `ziel = None` = `GOSUB 0`.
TrapDef {
art: u8,
index: HExpr,
ziel: Option<LabelId>,
},
/// `<quelle>(n) ON|OFF|STOP` — `zustand` 0 An, 1 Aus, 2 Gestoppt.
TrapSet {
art: u8,
index: HExpr,
zustand: u8,
},
/// `EVENT ON` / `EVENT OFF`.
EventSwitch(bool),
/// EXIT SUB/FUNCTION/DEF bzw. Rumpfende.
ExitProc,
CallSub {

View File

@@ -666,12 +666,11 @@ impl<'a> P<'a> {
let decls = self.parse_var_decls();
Some(Stmt::StaticDecl { decls, pos })
}
TokenKind::Ident { ref name, .. } if name == "LPRINT" => {
self.parse_print(pos, true)
}
TokenKind::Ident { ref name, suffix: None }
if name == "VIEW" && self.k_at(1) == TokenKind::Kw(Kw::Print) =>
{
TokenKind::Ident { ref name, .. } if name == "LPRINT" => self.parse_print(pos, true),
TokenKind::Ident {
ref name,
suffix: None,
} if name == "VIEW" && self.k_at(1) == TokenKind::Kw(Kw::Print) => {
self.advance(); // VIEW
self.advance(); // PRINT
let (mut top, mut bottom) = (None, None);
@@ -972,9 +971,7 @@ impl<'a> P<'a> {
}
if matches!(self.k(), TokenKind::Eol) {
// Blockform
let stop = |p: &P| {
p.is_kw(Kw::ElseIf) || p.is_kw(Kw::Else) || p.at_end_pair(Kw::If)
};
let stop = |p: &P| p.is_kw(Kw::ElseIf) || p.is_kw(Kw::Else) || p.at_end_pair(Kw::If);
let then_body = self.parse_stmt_list(stop);
let mut elseifs = Vec::new();
while self.eat_kw(Kw::ElseIf) {
@@ -1071,10 +1068,12 @@ impl<'a> P<'a> {
}
}
}
let body = self.parse_stmt_list(|p: &P| {
p.is_kw(Kw::Case) || p.at_end_pair(Kw::Select)
let body = self.parse_stmt_list(|p: &P| p.is_kw(Kw::Case) || p.at_end_pair(Kw::Select));
arms.push(CaseArm {
specs,
body,
pos: arm_pos,
});
arms.push(CaseArm { specs, body, pos: arm_pos });
}
if self.at_end_pair(Kw::Select) {
self.advance();
@@ -1149,6 +1148,46 @@ impl<'a> P<'a> {
Some(Stmt::While { cond, body, pos })
}
/// `ON <quelle>[(n)] GOSUB ziel`. Liefert `None` und lässt den Cursor
/// unberührt, wenn hier kein Trap steht.
fn parse_trap_def(&mut self, pos: SourcePos) -> Option<Stmt> {
let start = self.i;
let TokenKind::Ident { name, suffix: None } = self.k() else {
return None;
};
if !matches!(
name.as_str(),
"TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "COM" | "PEN" | "PLAY" | "STRIG"
) {
return None;
}
let device = name.clone();
self.advance();
let index = if self.eat(&TokenKind::LParen) {
let e = self.parse_expr();
if e.is_none() || !self.eat(&TokenKind::RParen) {
self.i = start;
return None;
}
e
} else {
None
};
if !self.eat_kw(Kw::Gosub) {
// Doch kein Trap — z. B. `ON TIMER GOTO …` gibt es nicht, aber
// die Diagnose dafür gehört an die allgemeine Stelle.
self.i = start;
return None;
}
let target = self.parse_label_ref()?;
Some(Stmt::TrapDef {
device,
index,
target,
pos,
})
}
fn parse_on(&mut self, pos: SourcePos) -> Option<Stmt> {
self.advance(); // ON
let local = self.eat_kw(Kw::Local);
@@ -1186,7 +1225,13 @@ impl<'a> P<'a> {
self.sync();
return None;
}
// ON ausdruck GOTO/GOSUB liste (Event-Traps: Phase 3/4)
// ON <quelle>[(n)] GOSUB ziel — klassische Ereignis-Traps (§8).
// Muss vor dem berechneten `ON ausdruck GOSUB` stehen, sonst
// verschwindet `ON TIMER(5) GOSUB Marke` still darin.
if let Some(st) = self.parse_trap_def(pos) {
return Some(st);
}
// ON ausdruck GOTO/GOSUB liste
let expr = self.parse_expr()?;
let gosub = if self.eat_kw(Kw::Goto) {
false
@@ -1528,26 +1573,34 @@ impl<'a> P<'a> {
return None;
}
// Ereignissteuerung: `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` …
if let Expr::Name { ref name, suffix: None, ref args, .. } = target {
if let Expr::Name {
ref name,
suffix: None,
ref args,
..
} = target
{
if matches!(
name.as_str(),
"TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT"
| "EVENT"
"TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" | "EVENT"
) {
let action = match self.k() {
TokenKind::Kw(Kw::On) => Some(EventAction::On),
TokenKind::Kw(Kw::Stop) => Some(EventAction::Stop),
TokenKind::Ident { ref name, suffix: None }
if name == "OFF" =>
{
Some(EventAction::Off)
}
TokenKind::Ident {
ref name,
suffix: None,
} if name == "OFF" => Some(EventAction::Off),
_ => None,
};
if let Some(action) = action {
self.advance();
let index = args.clone().and_then(|mut a| {
if a.is_empty() { None } else { Some(a.remove(0)) }
if a.is_empty() {
None
} else {
Some(a.remove(0))
}
});
return Some(Stmt::EventControl {
device: name.clone(),

View File

@@ -233,9 +233,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
"NOW" => (0, 0, &[], Db),
"DATESERIAL" | "TIMESERIAL" => (3, 3, &[N, N, N], Db),
"DATEVALUE" | "TIMEVALUE" => (1, 1, &[S], Db),
"DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => {
(1, 1, &[N], I)
}
"DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => (1, 1, &[N], I),
"FORMAT$" => (1, 2, &[A, S], St),
"ERR" | "ERL" => (0, 0, &[], L),
"FRE" => (1, 1, &[A], L),
@@ -253,9 +251,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
"FV" | "FV#" | "PV" | "PV#" | "PMT" | "PMT#" | "NPER" | "NPER#" => {
(5, 5, &[N, N, N, N, N], Db)
}
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => {
(6, 6, &[N, N, N, N, N, N], Db)
}
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => (6, 6, &[N, N, N, N, N, N], Db),
"NPV" | "NPV#" | "IRR" | "IRR#" => (2, 2, &[N, A], Db),
"MIRR" | "MIRR#" => (3, 3, &[A, N, N], Db),
"SLN" | "SLN#" => (3, 3, &[N, N, N], Db),
@@ -636,9 +632,9 @@ impl Sema {
self.prescan(&a.body, scope, module_data);
}
}
Stmt::For { body, .. }
| Stmt::DoLoop { body, .. }
| Stmt::While { body, .. } => self.prescan(body, scope, module_data),
Stmt::For { body, .. } | Stmt::DoLoop { body, .. } | Stmt::While { body, .. } => {
self.prescan(body, scope, module_data)
}
_ => {}
}
}
@@ -880,21 +876,25 @@ impl Sema {
Expr::CurrencyLit(v) => Some(ConstVal::Num(*v as f64 / 10_000.0)),
Expr::StrLit(s) => Some(ConstVal::Str(s.clone())),
Expr::Paren(e) => self.fold_const(e),
Expr::Name { name, args: None, .. } => {
self.consts.get(name).and_then(|(_, v)| v.clone())
}
Expr::Unary { op: UnOp::Neg, operand, .. } => {
match self.fold_const(operand)? {
ConstVal::Num(n) => Some(ConstVal::Num(-n)),
ConstVal::Str(_) => None,
}
}
Expr::Unary { op: UnOp::Not, operand, .. } => {
match self.fold_const(operand)? {
ConstVal::Num(n) => Some(ConstVal::Num(!(n as i64) as f64)),
ConstVal::Str(_) => None,
}
}
Expr::Name {
name, args: None, ..
} => self.consts.get(name).and_then(|(_, v)| v.clone()),
Expr::Unary {
op: UnOp::Neg,
operand,
..
} => match self.fold_const(operand)? {
ConstVal::Num(n) => Some(ConstVal::Num(-n)),
ConstVal::Str(_) => None,
},
Expr::Unary {
op: UnOp::Not,
operand,
..
} => match self.fold_const(operand)? {
ConstVal::Num(n) => Some(ConstVal::Num(!(n as i64) as f64)),
ConstVal::Str(_) => None,
},
Expr::Binary { op, lhs, rhs, .. } => {
let l = self.fold_const(lhs)?;
let r = self.fold_const(rhs)?;
@@ -1513,25 +1513,41 @@ impl Sema {
Some(t) => {
self.check_label_exists(t, scope, *pos);
match t {
LabelRef::Name(n) => {
self.data_marks_name.get(n).copied().unwrap_or(0)
}
LabelRef::Line(n) => {
self.data_marks_line.get(n).copied().unwrap_or(0)
}
LabelRef::Name(n) => self.data_marks_name.get(n).copied().unwrap_or(0),
LabelRef::Line(n) => self.data_marks_line.get(n).copied().unwrap_or(0),
}
}
};
self.push(out, scope, HStmtKind::Restore(idx));
}
Stmt::DefFn { name, suffix, params, body, pos } => {
Stmt::DefFn {
name,
suffix,
params,
body,
pos,
} => {
self.lower_def_fn(name, suffix, params, None, Some(body), scope, *pos);
}
Stmt::DefFnBlock { name, suffix, params, body, pos } => {
Stmt::DefFnBlock {
name,
suffix,
params,
body,
pos,
} => {
self.lower_def_fn(name, suffix, params, Some(body), None, scope, *pos);
}
// ---- Datei-E/A (Grammatik Phase 1, Laufzeit Phase 3) ----
Stmt::Open { file, mode, isam, number, len, pos, .. } => {
Stmt::Open {
file,
mode,
isam,
number,
len,
pos,
..
} => {
let (fe, _) = self.want_str(file, scope);
self.reject_com_device(file, *pos);
let (ne, nt) = self.want_num(number, scope);
@@ -1779,12 +1795,67 @@ impl Sema {
self.push(
out,
scope,
HStmtKind::BuiltinStmt { b: Builtin::ViewPrint, args: a },
HStmtKind::BuiltinStmt {
b: Builtin::ViewPrint,
args: a,
},
);
}
Stmt::EventControl { device, index, action, pos } => {
Stmt::TrapDef {
device,
index,
target,
pos,
} => {
let Some(art) = trap_art(device) else {
// COM/PEN/PLAY/STRIG sind Non-Feature — namentlich.
self.err(*pos, "Feature unavailable");
return;
};
// Wertebereich prüfen, soweit die Kennung konstant ist.
if let Some(i) = index {
if let Some(n) = const_zahl(i) {
if !trap_bereich_ok(art, n) {
self.err(*pos, trap_bereich_text(art));
}
}
} else if art != TRAP_UEVENT {
self.err(*pos, trap_bereich_text(art));
}
if index.is_some() && art == TRAP_UEVENT {
self.err(*pos, "ON UEVENT nimmt keine Kennung");
}
let hindex = match index {
Some(i) => self.lower_num_as(i, scope, NumTy::Lng),
None => HExpr::Lng(0),
};
// `GOSUB 0` schaltet den Trap ab und meint nicht Zeile 0.
let ziel = if matches!(target, LabelRef::Line(0)) {
None
} else {
match self.label_id(target, scope, *pos) {
Some(id) => Some(id),
None => return,
}
};
self.push(
out,
scope,
HStmtKind::TrapDef {
art,
index: hindex,
ziel,
},
);
}
Stmt::EventControl {
device,
index,
action,
pos,
} => {
match device.as_str() {
"TIMER" | "KEY" | "UEVENT" | "EVENT" => {}
"TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "EVENT" => {}
_ => self.err(*pos, "Feature unavailable"),
}
if let Some(i) = index {
@@ -1805,7 +1876,45 @@ impl Sema {
);
return;
}
self.push(out, scope, HStmtKind::Unsupported("Ereignissteuerung"));
if device == "EVENT" {
// Das Vorbild kennt nur ON und OFF — kein STOP.
if *action == EventAction::Stop {
self.err(*pos, "EVENT kennt nur ON und OFF");
return;
}
let an = *action == EventAction::On;
self.push(out, scope, HStmtKind::EventSwitch(an));
return;
}
let Some(art) = trap_art(device) else {
self.err(*pos, "Feature unavailable");
return;
};
if let Some(i) = index {
if let Some(n) = const_zahl(i) {
if !trap_bereich_ok(art, n) {
self.err(*pos, trap_bereich_text(art));
}
}
}
let hindex = match index {
Some(i) => self.lower_num_as(i, scope, NumTy::Lng),
None => HExpr::Lng(0),
};
let zustand = match action {
EventAction::On => 0,
EventAction::Off => 1,
EventAction::Stop => 2,
};
self.push(
out,
scope,
HStmtKind::TrapSet {
art,
index: hindex,
zustand,
},
);
}
}
}
@@ -2343,10 +2452,22 @@ impl Sema {
let lowered = self.check_and_lower_builtin_args(name, args, min, max, spec, scope, pos);
match name {
"BEEP" => self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Beep, args: vec![] }),
"DOEVENTS" => {
self.push(out, scope, HStmtKind::BuiltinStmt { b: Builtin::Doevents, args: vec![] })
}
"BEEP" => self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::Beep,
args: vec![],
},
),
"DOEVENTS" => self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::Doevents,
args: vec![],
},
),
"RANDOMIZE" => {
let a = lowered
.into_iter()
@@ -2494,19 +2615,29 @@ impl Sema {
self.push(out, scope, HStmtKind::BuiltinStmt { b, args: a });
}
// ---- ISAM ------------------------------------------------------
"CREATEINDEX" | "DELETEINDEX" | "SETINDEX" | "INSERT" | "RETRIEVE"
| "UPDATE" | "DELETE" | "DELETETABLE" | "MOVEFIRST" | "MOVELAST"
| "MOVENEXT" | "MOVEPREVIOUS" | "SEEKEQ" | "SEEKGT" | "SEEKGE"
| "BEGINTRANS" | "COMMITTRANS" | "ROLLBACK" => {
"CREATEINDEX" | "DELETEINDEX" | "SETINDEX" | "INSERT" | "RETRIEVE" | "UPDATE"
| "DELETE" | "DELETETABLE" | "MOVEFIRST" | "MOVELAST" | "MOVENEXT" | "MOVEPREVIOUS"
| "SEEKEQ" | "SEEKGT" | "SEEKGE" | "BEGINTRANS" | "COMMITTRANS" | "ROLLBACK" => {
self.lower_isam_stmt(name, args, lowered, scope, out)
}
"SETUEVENT" => self.push(
out,
scope,
HStmtKind::BuiltinStmt {
b: Builtin::SetUEvent,
args: vec![],
},
),
// Bildschirm-/Datei-/System-Anweisungen späterer Phasen.
_ => self.push(out, scope, HStmtKind::Unsupported(match name {
"RUN" => "RUN",
"MSGBOX" => "MSGBOX",
"SETUEVENT" => "SETUEVENT",
_ => "Anweisung",
})),
_ => self.push(
out,
scope,
HStmtKind::Unsupported(match name {
"RUN" => "RUN",
"MSGBOX" => "MSGBOX",
_ => "Anweisung",
}),
),
}
}
@@ -2897,9 +3028,12 @@ impl Sema {
let (re, rt) = self.lower_expr(rhs, scope);
self.lower_binary(*op, le, lt, re, rt, *pos)
}
Expr::Name { name, suffix, args, pos } => {
self.lower_name_expr(name, suffix, args, *pos, scope)
}
Expr::Name {
name,
suffix,
args,
pos,
} => self.lower_name_expr(name, suffix, args, *pos, scope),
}
}
@@ -3742,6 +3876,7 @@ fn stmt_pos(stmt: &Stmt) -> SourcePos {
| Stmt::LockStmt { pos, .. }
| Stmt::NameStmt { pos, .. }
| Stmt::ViewPrint { pos, .. }
| Stmt::TrapDef { pos, .. }
| Stmt::EventControl { pos, .. }
| Stmt::Include { pos, .. }
| Stmt::MetaArrays { pos, .. }
@@ -4082,3 +4217,52 @@ mod tests {
);
}
}
// ---- Ereignis-Traps (Sprachreferenz §8) -------------------------------------
pub const TRAP_KEY: u8 = 0;
pub const TRAP_TIMER: u8 = 1;
pub const TRAP_UEVENT: u8 = 2;
pub const TRAP_SIGNAL: u8 = 3;
/// Quellenname → Art. `None` für die Non-Features.
fn trap_art(device: &str) -> Option<u8> {
Some(match device {
"KEY" => TRAP_KEY,
"TIMER" => TRAP_TIMER,
"UEVENT" => TRAP_UEVENT,
"SIGNAL" => TRAP_SIGNAL,
_ => return None,
})
}
/// Konstanter Zahlenwert eines Ausdrucks, soweit direkt ablesbar.
fn const_zahl(e: &Expr) -> Option<i64> {
match e {
Expr::IntLit(n) => Some(*n as i64),
Expr::LongLit(n) => Some(*n as i64),
Expr::SingleLit(n) => Some(*n as i64),
Expr::DoubleLit(n) => Some(*n as i64),
Expr::Paren(inner) => const_zahl(inner),
_ => None,
}
}
/// Wertebereiche nach der Original-Hilfe (Belege in `umfang-und-form.md`).
fn trap_bereich_ok(art: u8, n: i64) -> bool {
match art {
TRAP_KEY => matches!(n, 0..=25 | 30 | 31),
TRAP_TIMER => (1..=86_400).contains(&n),
TRAP_SIGNAL => (1..=2).contains(&n),
_ => true,
}
}
fn trap_bereich_text(art: u8) -> &'static str {
match art {
TRAP_KEY => "ON KEY: Kennung 0, 1-25 oder 30-31 erwartet",
TRAP_TIMER => "ON TIMER: Intervall 1 bis 86400 Sekunden erwartet",
TRAP_SIGNAL => "ON SIGNAL: Kennung 1 (SIGINT) oder 2 (SIGTERM) erwartet",
_ => "Kennung erwartet",
}
}