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

@@ -153,11 +153,16 @@ fn cmd_run(args: &[String]) -> ExitCode {
struct PipeHost {
puffer: std::collections::VecDeque<Ereignis>,
eof: bool,
start: std::time::Instant,
}
impl PipeHost {
fn new() -> Self {
PipeHost { puffer: std::collections::VecDeque::new(), eof: false }
PipeHost {
puffer: std::collections::VecDeque::new(),
eof: false,
start: std::time::Instant::now(),
}
}
/// Eine Zeile von stdin in Tastendrücke zerlegen.
@@ -177,10 +182,12 @@ impl PipeHost {
zeile.pop();
}
for c in zeile.chars() {
self.puffer.push_back(Ereignis::Taste(c.to_string()));
self.puffer.push_back(Ereignis::Taste(c.to_string(), 0));
}
self.puffer
.push_back(Ereignis::Taste(tb_runtime::host::taste::ENTER.to_string()));
self.puffer.push_back(Ereignis::Taste(
tb_runtime::host::taste::ENTER.to_string(),
0,
));
}
}
}
@@ -195,4 +202,8 @@ impl Host for PipeHost {
}
self.puffer.pop_front()
}
fn jetzt_ms(&mut self) -> u64 {
self.start.elapsed().as_millis() as u64
}
}

View File

@@ -39,7 +39,9 @@ fn groesse_lesen(name: &str, wert: &str) -> (usize, usize) {
/// Wert einer Kopfzeilen-Direktive `' <marke>: <wert>` aus den ersten Zeilen.
fn direktive(src: &str, marke: &str) -> Option<String> {
src.lines()
.take(4)
// Kopfzeilenfenster: Groesse, Zeitzone, Uhr, Tasten, Tempdir,
// Resize passen zusammen in die ersten Zeilen.
.take(8)
.find(|l| l.contains(marke))
.map(|l| l.split(marke).nth(1).unwrap().trim().to_string())
}
@@ -94,6 +96,120 @@ fn resize_direktive(name: &str, src: &str) -> Option<(usize, (usize, usize))> {
}
}
/// Vorbereitete Tastendrücke: `' tb-keys: <folge>`. Sondertasten als
/// `<F1>`…`<F12>`, sonst je ein Zeichen; `+` davor bedeutet Umschalt,
/// `^` Strg, `%` Alt — gebraucht für die benutzerdefinierten Trap-Tasten.
fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
use tb_runtime::host::{taste, umschalt, Ereignis};
let Some(wert) = direktive(src, "tb-keys:") else {
return Vec::new();
};
let mut out = Vec::new();
let zeichen: Vec<char> = wert.chars().collect();
let mut i = 0;
while i < zeichen.len() {
let mut shift = 0u8;
while i < zeichen.len() && matches!(zeichen[i], '+' | '^' | '%') {
shift |= match zeichen[i] {
'+' => umschalt::SHIFT,
'^' => umschalt::CTRL,
_ => umschalt::ALT,
};
i += 1;
}
if i >= zeichen.len() {
panic!("{name}: tb-keys endet mit einem Umschaltzeichen");
}
if zeichen[i] == '<' {
let ende = zeichen[i..]
.iter()
.position(|c| *c == '>')
.unwrap_or_else(|| panic!("{name}: tb-keys: `<` ohne `>`"));
let nm: String = zeichen[i + 1..i + ende].iter().collect();
let code = match nm.to_uppercase().as_str() {
"F1" => 0x3B,
"F2" => 0x3C,
"F3" => 0x3D,
"F4" => 0x3E,
"F5" => 0x3F,
"F6" => 0x40,
"F7" => 0x41,
"F8" => 0x42,
"F9" => 0x43,
"F10" => 0x44,
"F11" => 0x85,
"F12" => 0x86,
"UP" => 0x48,
"LEFT" => 0x4B,
"RIGHT" => 0x4D,
"DOWN" => 0x50,
other => panic!("{name}: tb-keys kennt `<{other}>` nicht"),
};
out.push(Ereignis::Taste(taste::sonder(code), shift));
i += ende + 1;
} else {
out.push(Ereignis::Taste(zeichen[i].to_string(), shift));
i += 1;
}
}
out
}
/// Virtuelle Uhr: `' tb-clock: <n>ms/Zustellpunkt`. Ohne die Direktive
/// steht die Uhr des Testhosts still — ein Programm mit Zeit-Traps liefe
/// dann endlos. Der Harness weist es deshalb ab, statt auf die Systemuhr
/// auszuweichen (design.md, D6).
fn clock_direktive(name: &str, src: &str) -> Option<u64> {
let wert = direktive(src, "tb-clock:")?;
let ms = wert
.trim()
.strip_suffix("ms/Zustellpunkt")
.unwrap_or_else(|| panic!("{name}: tb-clock erwartet `<n>ms/Zustellpunkt`, fand {wert:?}"))
.trim();
Some(
ms.parse()
.unwrap_or_else(|_| panic!("{name}: {ms:?} ist keine Zahl")),
)
}
/// Nutzt das Programm zeitgesteuerte Traps? Grobe, aber ausreichende
/// Prüfung: ohne `ON TIMER` gibt es keine Fälligkeit zu rechnen.
fn braucht_uhr(src: &str) -> bool {
src.to_uppercase().contains("ON TIMER")
}
fn clock_schritt(name: &str, src: &str) -> Option<u64> {
let schritt = clock_direktive(name, src);
assert!(
schritt.is_some() || !braucht_uhr(src),
"{name}: nutzt zeitgesteuerte Traps, deklariert aber keinen Zeitverlauf — \
erste Zeilen brauchen `' tb-clock: <n>ms/Zustellpunkt`"
);
schritt
}
/// Testhost mit virtueller Uhr: sie rückt bei jedem Blick auf die Zeit um
/// einen festen Schritt vor. Damit ist der Lauf reproduzierbar und
/// braucht keine Wartezeit.
struct UhrHost {
inner: CaptureHost,
schritt_ms: u64,
}
impl tb_runtime::host::Host for UhrHost {
fn present(&mut self, s: &tb_runtime::screen::TextScreen) {
self.inner.present(s);
}
fn next_event(&mut self, blockierend: bool) -> Option<tb_runtime::host::Ereignis> {
self.inner.next_event(blockierend)
}
fn jetzt_ms(&mut self) -> u64 {
let jetzt = self.inner.jetzt_ms();
self.inner.uhr_vorruecken(self.schritt_ms);
jetzt
}
}
/// Arbeitsverzeichnis für die Dauer eines Korpusprogramms. Programme mit
/// Datei-E/A laufen in einem frischen temporären Verzeichnis und hinterlassen
/// nichts im Projektbaum; beim Verlassen wird es samt Inhalt entfernt.
@@ -146,6 +262,24 @@ fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String {
tb_runtime::host::Ereignis::Groesse { cols: c, rows: r },
);
}
for t in tasten_direktive(&name, &src) {
host.ereignis(t);
}
let schritt = clock_schritt(&name, &src);
if let Some(schritt_ms) = schritt {
let mut host = UhrHost {
inner: host,
schritt_ms,
};
return match vm.run(&mut host) {
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
other => panic!(
"{}: unerwartetes Laufzeitende {other:?}\nBildschirm bisher:\n{}",
path.display(),
tb_runtime::snapshot::snapshot(&vm.rt.screen)
),
};
}
match vm.run(&mut host) {
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
other => panic!(
@@ -207,12 +341,8 @@ fn assert_output_matches(file: &str, want: &str, got: &str) {
.position(|(a, b)| a != b)
.map(|i| i / 2 + 1)
.unwrap_or(0);
let paar = |s: &str| {
s.chars()
.skip((spalte - 1) * 2)
.take(2)
.collect::<String>()
};
let paar =
|s: &str| s.chars().skip((spalte - 1) * 2).take(2).collect::<String>();
panic!(
"{file}: Farbabweichung in Zeile {}, Spalte {spalte}:\n \
Soll: {}\n Ist: {}",
@@ -257,12 +387,31 @@ fn korpus_laeuft_mit_korrekter_ausgabe() {
let got = run_corpus_file(&path, Some((*cols, *rows)));
let etikett = format!("{name} @ {cols}x{rows}");
assert_output_matches(&etikett, &want, &got);
let wiederholt = run_corpus_file(&path, Some((*cols, *rows)));
assert_eq!(
got, wiederholt,
"{etikett}: zwei Läufe weichen voneinander ab"
);
checked += 1;
}
}
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}");
}
#[test]
fn uhr_direktive_wird_gelesen() {
assert_eq!(
clock_schritt("uhr.bas", "' tb-clock: 25ms/Zustellpunkt\nON TIMER(1)"),
Some(25)
);
}
#[test]
#[should_panic(expected = "deklariert aber keinen Zeitverlauf")]
fn zeittrap_ohne_uhr_direktive_wird_abgewiesen() {
clock_schritt("uhr.bas", "ON TIMER(1) GOSUB Tick");
}
// ---- tbc-Binary (Exit-Codes nach D6) ----------------------------------------
#[test]

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",
}
}

View File

@@ -7,13 +7,13 @@
use crate::console::PrintState;
use crate::datetime as dt;
use crate::finanz as fin;
use crate::fileio as fio;
use crate::value;
use crate::errors::RuntimeError;
use crate::fileio as fio;
use crate::finanz as fin;
use crate::format;
use crate::host::{Ereignis, Host};
use crate::screen::TextScreen;
use crate::value;
use crate::value::{as_f64, cur_to_f64, f64_to_cur, Value};
use std::collections::VecDeque;
use std::rc::Rc;
@@ -28,8 +28,11 @@ pub struct RtState {
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
pub isam: crate::isam::Isam,
pub print: PrintState,
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form.
pub tasten: VecDeque<String>,
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form, je mit
/// Umschaltzustand (Bitfeld 1 Umschalt, 2 Strg, 4 Alt). Der Zustand
/// wird nur für die benutzerdefinierten Trap-Tasten gebraucht;
/// `INKEY$` und `INPUT` sehen ihn nicht.
pub tasten: VecDeque<(String, u8)>,
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
pub abbruch: bool,
/// Eingabeende wurde gemeldet.
@@ -42,6 +45,8 @@ pub struct RtState {
pub waehrung: crate::using::Waehrung,
/// Ablaufverfolgung (`TRON`/`TROFF`) eingeschaltet?
pub trace: bool,
/// Zustände der klassischen Ereignis-Traps (Sprachreferenz §8).
pub traps: crate::traps::Traps,
/// Trefferliste und Zeiger für `DIR$`.
pub dir_treffer: Vec<String>,
pub dir_index: usize,
@@ -73,6 +78,7 @@ impl Default for RtState {
key_zeile: false,
waehrung: Default::default(),
trace: false,
traps: crate::traps::Traps::neu(),
dir_treffer: Vec::new(),
dir_index: 0,
uhr_offset: 0,
@@ -100,10 +106,19 @@ impl RtState {
return;
};
match e {
Ereignis::Taste(t) => self.tasten.push_back(t),
Ereignis::Taste(t, shift) => self.tasten.push_back((t, shift)),
Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows),
Ereignis::Abbruch => self.abbruch = true,
Ereignis::Ende => self.ende = true,
Ereignis::Signal(n) => {
if n == 1 && !self.traps.melden(crate::traps::Quelle::Signal(n)) {
self.abbruch = true;
}
}
// ponytail: Mausereignisse bekommen ihren Verbraucher erst
// mit dem Forms-Objektmodell. Bis dahin werden sie hier
// verworfen, damit die Warteschlange nicht wächst.
Ereignis::Maus(_) => {}
}
if blockierend {
return;
@@ -116,12 +131,46 @@ impl RtState {
pub fn tick(&mut self, host: &mut dyn Host) {
host.present(&self.screen);
self.pump(host, false);
self.traps_pruefen(host);
}
/// Was am Zustellpunkt vor der eigentlichen Zustellung zu tun ist:
/// Zeit prüfen und getrappte Tasten aus der Schlange nehmen.
///
/// Die Tasten werden **hier** aussortiert, nicht beim Einsortieren
/// (design.md, D5): so wirkt ein `KEY(n) OFF` sofort und auch auf
/// Tasten, die schon in der Schlange liegen.
pub fn traps_pruefen(&mut self, host: &mut dyn Host) {
if !self.traps.aktiv() {
return;
}
let jetzt = host.jetzt_ms();
self.traps.zeit_pruefen(jetzt);
self.tasten_traps_pruefen();
}
/// Getrappte Tasten aus der Schlange nehmen. Ohne Host, damit die VM
/// das an jeder Anweisungsgrenze tun kann, ohne die Uhr zu lesen.
pub fn tasten_traps_pruefen(&mut self) {
// Nur der Kopf der Schlange: die Reihenfolge der Eingabe bleibt
// erhalten, und eine getrappte Taste hinter einer ungetrappten
// wartet, bis diese gelesen ist.
while let Some((s, shift)) = self.tasten.front() {
let Some(code) = crate::host::taste::scancode_von(s) else {
break;
};
let Some(n) = self.traps.taste_faengt(code, *shift) else {
break;
};
self.tasten.pop_front();
self.traps.melden(crate::traps::Quelle::Key(n));
}
}
/// Nächster Tastendruck in `INKEY$`-Form; blockiert bei `blockierend`.
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> {
loop {
if let Some(t) = self.tasten.pop_front() {
if let Some((t, _)) = self.tasten.pop_front() {
return Some(t);
}
if self.ende {
@@ -386,7 +435,9 @@ pub mod ids {
pub const ISAM_SAVEPOINT: u16 = 145;
pub const ISAM_SETMEM: u16 = 146;
pub const ISAM_BOF: u16 = 147;
pub const COUNT: u16 = 148;
/// `SetUEvent` — loest das benutzerdefinierte Ereignis aus (§8).
pub const SETUEVENT: u16 = 148;
pub const COUNT: u16 = 149;
}
/// Dispatch-Tabelle in Index-Reihenfolge.
@@ -541,6 +592,7 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
bi_isam_savepoint,
bi_isam_setmem,
bi_isam_bof,
bi_setuevent,
];
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
TABLE
@@ -1439,9 +1491,26 @@ fn key_index(n: i32) -> Option<usize> {
}
}
fn bi_key_assign(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
fn bi_key_assign(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
let text = arg_str(a, 1)?;
// 1525 weisen kein Makro zu, sondern erklären eine Trap-Taste über
// Tastaturflagbyte und Scancode (Sprachreferenz §8).
if (15..=25).contains(&n) {
let mut it = text.chars();
let (Some(flag), Some(scan)) = (it.next(), it.next()) else {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
};
if it.next().is_some() || flag as u32 > 255 || scan as u32 > 255 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
st.traps.benutzertaste(n as u8, flag as u8, scan as u8);
return Ok(None);
}
let i = key_index(n).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
// Höchstens 15 Zeichen; überzählige werden verworfen (Vorbild).
st.key_makros[i] = text.chars().take(15).collect();
@@ -1651,7 +1720,21 @@ fn bi_sleep(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Op
Ok(None)
}
fn bi_beep(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
fn bi_setuevent(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// „sets a flag that BASIC checks before executing the next statement"
st.traps.melden(crate::traps::Quelle::UEvent);
Ok(None)
}
fn bi_beep(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// ponytail: Der Signalton geht direkt an das Terminal, nicht durch den
// Zellenpuffer — er hinterlässt dort nichts. Ceiling: sobald der Host
// mehr als Anzeigen kann, wird daraus ein eigener Host-Aufruf.
@@ -1885,4 +1968,155 @@ mod tests {
};
assert_eq!(n, 3);
}
#[test]
fn setuevent_meldet_das_benutzerereignis() {
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = crate::host::CaptureHost::default();
st.traps.definieren(Quelle::UEvent, Some(11));
st.traps.setzen(Quelle::UEvent, Zustand::An, 0);
assert_eq!(st.traps.naechstes(), None);
bi_setuevent(&mut st, &mut h, &mut []).unwrap();
assert_eq!(st.traps.naechstes(), Some((Quelle::UEvent, 11)));
}
#[test]
fn signal_aus_dem_host_meldet_den_trap() {
use crate::host::{Ereignis, Host};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = crate::host::CaptureHost::default();
st.traps.definieren(Quelle::Signal(1), Some(22));
st.traps.setzen(Quelle::Signal(1), Zustand::An, 0);
h.ereignis(Ereignis::Signal(1));
st.pump(&mut h, false);
assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(1), 22)));
assert!(matches!(h.next_event(false), None));
}
#[test]
fn sigint_ohne_aktiven_trap_bleibt_abbruch() {
use crate::host::Ereignis;
let mut st = RtState::default();
let mut h = crate::host::CaptureHost::default();
h.ereignis(Ereignis::Signal(1));
st.pump(&mut h, false);
assert!(st.abbruch);
}
#[test]
fn mausereignisse_haeufen_sich_nicht_an() {
use crate::host::{Ereignis, MausArt, MausEreignis};
let mut st = RtState::default();
let mut h = crate::host::CaptureHost::default();
for i in 0..500 {
h.ereignis(Ereignis::Maus(MausEreignis {
art: MausArt::Bewegung,
taste: 0,
shift: 0,
zeile: 1 + i % 20,
spalte: 1 + i % 70,
}));
}
st.pump(&mut h, false);
assert!(st.tasten.is_empty(), "Maus landet nicht im Tastenpuffer");
}
#[test]
fn getrappte_taste_verlaesst_den_eingabestrom() {
use crate::host::{taste, CaptureHost, Ereignis};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = CaptureHost::default();
st.traps.definieren(Quelle::Key(1), Some(77));
st.traps.setzen(Quelle::Key(1), Zustand::An, 0);
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0)); // F1
st.pump(&mut h, false);
st.traps_pruefen(&mut h);
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(1), 77)));
assert_eq!(st.naechste_taste(&mut h, false), None, "F1 ist weg");
}
#[test]
fn nicht_getrappte_taste_bleibt_im_strom() {
use crate::host::{taste, CaptureHost, Ereignis};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = CaptureHost::default();
st.traps.definieren(Quelle::Key(1), Some(77));
st.traps.setzen(Quelle::Key(1), Zustand::Aus, 0);
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0));
st.pump(&mut h, false);
st.traps_pruefen(&mut h);
assert_eq!(st.traps.naechstes(), None);
assert_eq!(
st.naechste_taste(&mut h, false),
Some(taste::sonder(0x3B)),
"bei OFF bleibt F1 lesbar"
);
}
#[test]
fn off_wirkt_auf_bereits_wartende_tasten() {
use crate::host::{taste, CaptureHost, Ereignis};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = CaptureHost::default();
st.traps.definieren(Quelle::Key(1), Some(77));
st.traps.setzen(Quelle::Key(1), Zustand::An, 0);
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0));
st.pump(&mut h, false); // Taste liegt in der Schlange
st.traps.setzen(Quelle::Key(1), Zustand::Aus, 0);
st.traps_pruefen(&mut h);
assert_eq!(
st.naechste_taste(&mut h, false),
Some(taste::sonder(0x3B)),
"das OFF wirkt auch auf schon Wartendes (design.md D5)"
);
}
#[test]
fn benutzertaste_ueber_key_anweisung() {
use crate::host::{umschalt, CaptureHost};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = CaptureHost::default();
st.traps.definieren(Quelle::Key(15), Some(88));
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
// KEY 15, CHR$(4) + CHR$(30) — Strg+A
let def = format!("{}{}", 4u8 as char, 0x1Eu8 as char);
bi_key_assign(
&mut st,
&mut h,
&mut [Value::Int(15), Value::Str(def.into())],
)
.unwrap();
h.taste_mit("a", umschalt::CTRL);
st.pump(&mut h, false);
st.traps_pruefen(&mut h);
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(15), 88)));
assert_eq!(st.naechste_taste(&mut h, false), None);
}
#[test]
fn benutzertaste_ohne_umschalt_bleibt_im_strom() {
use crate::host::CaptureHost;
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = CaptureHost::default();
st.traps.definieren(Quelle::Key(15), Some(88));
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
let def = format!("{}{}", 4u8 as char, 0x1Eu8 as char);
bi_key_assign(
&mut st,
&mut h,
&mut [Value::Int(15), Value::Str(def.into())],
)
.unwrap();
h.taste_mit("a", 0); // ohne Strg
st.pump(&mut h, false);
st.traps_pruefen(&mut h);
assert_eq!(st.traps.naechstes(), None);
assert_eq!(st.naechste_taste(&mut h, false), Some("a".to_string()));
}
}

View File

@@ -16,17 +16,56 @@ use crate::screen::TextScreen;
/// Was vom Terminal hereinkommt.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ereignis {
/// Tastendruck in `INKEY$`-Form: ein Zeichen für Zeichentasten,
/// Nullzeichen + Kennung für Sondertasten.
Taste(String),
/// Tastendruck in `INKEY$`-Form (ein Zeichen für Zeichentasten,
/// Nullzeichen + Kennung für Sondertasten) samt Umschaltzustand.
///
/// Der Umschaltzustand ist ein Bitfeld wie bei der Maus: 1 Umschalt,
/// 2 Strg, 4 Alt. Er wird für `ON KEY(15)``ON KEY(25)` gebraucht —
/// benutzerdefinierte Tasten sind über Umschaltzustand und Scancode
/// erklärt, nicht über das erzeugte Zeichen.
Taste(String, u8),
/// Die Darstellungsfläche hat ihre Größe geändert.
Groesse { cols: usize, rows: usize },
/// Mausereignis in Zellenkoordinaten, 1-basiert wie `LOCATE`.
Maus(MausEreignis),
/// Betriebssystemsignal: 1 = SIGINT, 2 = SIGTERM (Sprachreferenz,
/// Abweichung 8). Andere Signale werden nicht abgebildet.
Signal(u8),
/// Abbruchwunsch (Strg+Untbr).
Abbruch,
/// Eingabeende — es kommt nichts mehr.
Ende,
}
/// Was die Maus getan hat. Die Position ist 1-basiert und in Zellen des
/// Textbildschirms — dieselbe Zählweise wie `LOCATE`, `CSRLIN` und `POS`;
/// die Umrechnung aus der 0-basierten Zählung des Terminals macht die
/// Darstellungsschicht (design.md, D7).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MausEreignis {
pub art: MausArt,
/// Gedrückte Taste: 1 links, 2 rechts, 4 Mitte; 0 bei reiner Bewegung.
pub taste: u8,
/// Umschaltzustand als Bitfeld: 1 Umschalt, 2 Strg, 4 Alt.
pub shift: u8,
pub zeile: usize,
pub spalte: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MausArt {
Druck,
Loslassen,
Bewegung,
}
/// Umschaltzustand: Bitfeld 1 Umschalt, 2 Strg, 4 Alt.
pub mod umschalt {
pub const SHIFT: u8 = 1;
pub const CTRL: u8 = 2;
pub const ALT: u8 = 4;
}
/// Steuerzeichen in `INKEY$`-Form (Kodierung des Vorbilds).
pub mod taste {
pub const ENTER: &str = "\r";
@@ -38,6 +77,84 @@ pub mod taste {
pub fn sonder(code: u8) -> String {
format!("\0{}", code as char)
}
/// Zeichen → Scancode der Tastatur (Tabelle aus `qb45advr.hlp`,
/// „Keyboard Scan Codes"). Gebraucht für die benutzerdefinierten
/// Trap-Tasten `KEY 15``KEY 25`, die über Scancode erklärt sind.
///
/// ponytail: nur die Tasten der Original-Tabelle, keine Ziffernblock-
/// Doppelbelegungen. Ceiling: ein hier nicht geführtes Zeichen kann
/// keinen benutzerdefinierten Trap auslösen; die Tabelle wächst, wenn
/// jemand eine solche Taste braucht.
pub fn scancode(c: char) -> Option<u8> {
let c = c.to_ascii_uppercase();
Some(match c {
'\u{1b}' => 0x01,
'1' | '!' => 0x02,
'2' | '@' => 0x03,
'3' | '#' => 0x04,
'4' | '$' => 0x05,
'5' | '%' => 0x06,
'6' | '^' => 0x07,
'7' | '&' => 0x08,
'8' | '*' => 0x09,
'9' | '(' => 0x0A,
'0' | ')' => 0x0B,
'-' | '_' => 0x0C,
'=' | '+' => 0x0D,
'\u{8}' => 0x0E,
'\t' => 0x0F,
'Q' => 0x10,
'W' => 0x11,
'E' => 0x12,
'R' => 0x13,
'T' => 0x14,
'Y' => 0x15,
'U' => 0x16,
'I' => 0x17,
'O' => 0x18,
'P' => 0x19,
'[' | '{' => 0x1A,
']' | '}' => 0x1B,
'\r' => 0x1C,
'A' => 0x1E,
'S' => 0x1F,
'D' => 0x20,
'F' => 0x21,
'G' => 0x22,
'H' => 0x23,
'J' => 0x24,
'K' => 0x25,
'L' => 0x26,
';' | ':' => 0x27,
'\'' | '"' => 0x28,
'`' | '~' => 0x29,
'\\' | '|' => 0x2B,
'Z' => 0x2C,
'X' => 0x2D,
'C' => 0x2E,
'V' => 0x2F,
'B' => 0x30,
'N' => 0x31,
'M' => 0x32,
',' | '<' => 0x33,
'.' | '>' => 0x34,
'/' | '?' => 0x35,
' ' => 0x39,
_ => return None,
})
}
/// Scancode eines Tastendrucks in `INKEY$`-Form: bei Sondertasten die
/// Kennung hinter dem Nullzeichen, sonst aus der Zeichentabelle.
pub fn scancode_von(s: &str) -> Option<u8> {
let mut it = s.chars();
match (it.next(), it.next()) {
(Some('\0'), Some(k)) => Some(k as u8),
(Some(c), None) => scancode(c),
_ => None,
}
}
}
pub trait Host {
@@ -47,6 +164,16 @@ pub trait Host {
/// Nächstes Ereignis holen. Mit `blockierend = false` sofort `None`,
/// wenn nichts anliegt (Grundlage von `INKEY$`).
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis>;
/// Monotone Zeit in Millisekunden seit Programmstart.
///
/// Die Fälligkeit der Zeit-Traps rechnet die Laufzeit daraus aus; sie
/// greift dafür **nicht** selbst auf die Uhr zu (design.md, D3). Nur so
/// kann ein Host ohne Terminal die Zeit stellen und ein zeitgesteuertes
/// Programm ohne Wartezeit und reproduzierbar prüfen. Von der Ortszeit
/// hinter `TIME$`/`TIMER` ist das unabhängig — hier zählt Dauer, nicht
/// Uhrzeit.
fn jetzt_ms(&mut self) -> u64;
}
/// Host ohne Terminal: hält den Bildschirmzustand fest und speist
@@ -61,6 +188,9 @@ pub struct CaptureHost {
/// Ereignisse, die erst nach der n-ten Anzeige eingereiht werden —
/// so lässt sich eine Größenänderung mitten im Lauf nachstellen.
verzoegert: Vec<(usize, Ereignis)>,
/// Virtuelle Uhr in Millisekunden. Sie läuft nur, wenn der Test sie
/// stellt — von allein vergeht hier keine Zeit.
uhr_ms: u64,
}
impl CaptureHost {
@@ -77,10 +207,16 @@ impl CaptureHost {
/// Text als Folge von Tastendrücken anhängen, abgeschlossen mit Enter.
pub fn tippe(&mut self, text: &str) {
for c in text.chars() {
self.ereignisse.push_back(Ereignis::Taste(c.to_string()));
self.ereignisse.push_back(Ereignis::Taste(c.to_string(), 0));
}
self.ereignisse
.push_back(Ereignis::Taste(taste::ENTER.to_string()));
.push_back(Ereignis::Taste(taste::ENTER.to_string(), 0));
}
/// Tastendruck mit Umschaltzustand einreihen (Strg+A, Alt+F1 …).
pub fn taste_mit(&mut self, s: &str, shift: u8) {
self.ereignisse
.push_back(Ereignis::Taste(s.to_string(), shift));
}
/// Beliebiges Ereignis einreihen (Größenänderung, Abbruch, Sondertaste).
@@ -92,6 +228,16 @@ impl CaptureHost {
pub fn ereignis_nach(&mut self, nach: usize, e: Ereignis) {
self.verzoegert.push((nach, e));
}
/// Virtuelle Uhr vorrücken.
pub fn uhr_vorruecken(&mut self, ms: u64) {
self.uhr_ms += ms;
}
/// Virtuelle Uhr auf einen Wert setzen.
pub fn uhr_setzen(&mut self, ms: u64) {
self.uhr_ms = ms;
}
}
impl Host for CaptureHost {
@@ -113,6 +259,10 @@ impl Host for CaptureHost {
fn next_event(&mut self, _blockierend: bool) -> Option<Ereignis> {
self.ereignisse.pop_front()
}
fn jetzt_ms(&mut self) -> u64 {
self.uhr_ms
}
}
#[cfg(test)]
@@ -137,37 +287,84 @@ mod tests {
#[test]
fn verzoegertes_ereignis_kommt_nach_der_anzeige() {
let mut h = CaptureHost::default();
h.ereignis_nach(2, Ereignis::Groesse { cols: 120, rows: 40 });
h.ereignis_nach(
2,
Ereignis::Groesse {
cols: 120,
rows: 40,
},
);
let s = TextScreen::new();
h.present(&s);
assert_eq!(h.next_event(false), None);
h.present(&s);
assert_eq!(
h.next_event(false),
Some(Ereignis::Groesse { cols: 120, rows: 40 })
Some(Ereignis::Groesse {
cols: 120,
rows: 40
})
);
}
#[test]
fn groessenaenderung_und_abbruch_als_ereignis() {
let mut h = CaptureHost::default();
h.ereignis(Ereignis::Groesse { cols: 120, rows: 40 });
h.ereignis(Ereignis::Groesse {
cols: 120,
rows: 40,
});
h.ereignis(Ereignis::Abbruch);
assert_eq!(
h.next_event(true),
Some(Ereignis::Groesse { cols: 120, rows: 40 })
Some(Ereignis::Groesse {
cols: 120,
rows: 40
})
);
assert_eq!(h.next_event(true), Some(Ereignis::Abbruch));
}
#[test]
fn maus_und_tasten_behalten_ihre_reihenfolge() {
let mut h = CaptureHost::default();
let klick = Ereignis::Maus(MausEreignis {
art: MausArt::Druck,
taste: 1,
shift: 0,
zeile: 3,
spalte: 7,
});
h.ereignis(Ereignis::Taste("a".into(), 0));
h.ereignis(klick.clone());
h.ereignis(Ereignis::Taste("b".into(), 0));
assert_eq!(h.next_event(true), Some(Ereignis::Taste("a".into(), 0)));
assert_eq!(h.next_event(true), Some(klick));
assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into(), 0)));
}
#[test]
fn virtuelle_uhr_laeuft_nur_wenn_gestellt() {
let mut h = CaptureHost::default();
assert_eq!(h.jetzt_ms(), 0);
h.uhr_vorruecken(250);
assert_eq!(h.jetzt_ms(), 250);
h.uhr_vorruecken(250);
assert_eq!(h.jetzt_ms(), 500);
h.uhr_setzen(42);
assert_eq!(h.jetzt_ms(), 42);
// Ohne Zutun vergeht keine Zeit — das traegt die Reproduzierbarkeit.
assert_eq!(h.jetzt_ms(), 42);
}
#[test]
fn vorbereitete_zeile_wird_zu_tasten() {
let mut h = CaptureHost::with_input(&["ab"]);
assert_eq!(h.next_event(true), Some(Ereignis::Taste("a".into())));
assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into())));
assert_eq!(h.next_event(true), Some(Ereignis::Taste("a".into(), 0)));
assert_eq!(h.next_event(true), Some(Ereignis::Taste("b".into(), 0)));
assert_eq!(
h.next_event(true),
Some(Ereignis::Taste(taste::ENTER.into()))
Some(Ereignis::Taste(taste::ENTER.into(), 0))
);
assert_eq!(h.next_event(false), None);
}

View File

@@ -4,18 +4,19 @@
//! gruppiert nach Themen. Ziel ist verhaltensgleiche Nachbildung inklusive
//! Rundungs-, Formatierungs- und Fehlerverhalten (siehe PLAN.md, Phase 3).
pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe)
pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
pub mod isam; // ISAM: Tabellen, Indizes, Cursor, Transaktionen (redb)
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL
pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe)
pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
pub mod datetime; // DATE$, TIME$, TIMER
pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
pub mod finanz; // Finanzmathematik (FV#, PV#, Pmt#, IRR# …)
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL
pub mod host;
pub mod isam; // ISAM: Tabellen, Indizes, Cursor, Transaktionen (redb)
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
pub mod screen;
pub mod finanz; // Finanzmathematik (FV#, PV#, Pmt#, IRR# …)
pub mod using; // PRINT USING / FORMAT$ — Formatzeichenketten
pub mod snapshot; // Bildschirmzustand als vergleichbarer Text (Korpus) // Zellenpuffer des Textbildschirms (ohne Terminalbezug) // Host-Trait (Konsole, Capture) — Entscheidung D3
pub mod datetime; // DATE$, TIME$, TIMER
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
pub mod traps; // Zustaende der klassischen Ereignis-Traps (Sprachreferenz §8)
pub mod using; // PRINT USING / FORMAT$ — Formatzeichenketten
pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix

View File

@@ -0,0 +1,520 @@
//! Zustände der klassischen Ereignis-Traps (Sprachreferenz §8).
//!
//! Die Fachlogik liegt hier, ohne VM und ohne Host: welche Quelle in
//! welchem Zustand ist, was ansteht und was als Nächstes zuzustellen wäre.
//! Wer das Sprungziel anspringt, entscheidet die VM.
//!
//! Belege und Festlegungen: `umfang-und-form.md` des Changes
//! `phase-4-ereignisschleife`.
/// Eine Ereignisquelle. Die Reihenfolge der Varianten ist zugleich die
/// Zustellreihenfolge bei gleichzeitiger Fälligkeit (Festlegung, weil das
/// Vorbild dazu nichts hergibt): Tasten zuerst, weil sie an einer
/// Benutzereingabe hängen.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Quelle {
/// `ON KEY(n)` — n aus 0, 125, 3031.
Key(u8),
/// `ON TIMER(n)`.
Timer,
/// `ON UEVENT`.
UEvent,
/// `ON SIGNAL(n)` — n aus 12.
Signal(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Zustand {
/// Zustellen.
An,
/// Verwerfen — auch das bereits Gemerkte.
#[default]
Aus,
/// Höchstens eines merken und beim nächsten `ON` zustellen.
Gestoppt,
}
/// Ein definierter Trap: Ziel, Zustand, ob etwas ansteht.
#[derive(Debug, Clone, Copy)]
struct Trap {
quelle: Quelle,
/// Sprungziel im Modulrumpf. `None` = `GOSUB 0`, also abgeschaltet.
ziel: Option<u32>,
zustand: Zustand,
/// Höchstens ein gemerktes Ereignis (Vorbild: „remembered").
anstehend: bool,
/// Läuft der Handler dieser Quelle gerade?
laeuft: bool,
/// Wurde im laufenden Handler ausdrücklich `OFF` ausgeführt? Dann
/// unterbleibt das selbsttätige `ON` beim `RETURN`.
off_im_handler: bool,
}
/// Alle Traps eines Moduls.
#[derive(Debug, Default)]
pub struct Traps {
traps: Vec<Trap>,
/// `EVENT OFF` klammert einen Abschnitt ohne Ereignisprüfung. Kein
/// dritter Zustand — den kennt das Vorbild nicht.
event_an: bool,
/// Fälligkeit des Zeit-Traps: Intervall und der Zeitpunkt, an dem er
/// das nächste Mal fällig wird (beides in Millisekunden).
timer_intervall_ms: u64,
timer_faellig_ms: u64,
/// Benutzerdefinierte Tasten 1525: Umschaltzustand und Scancode aus
/// `KEY n, CHR$(flag) + CHR$(scancode)`.
benutzertasten: [Option<(u8, u8)>; 11],
/// Gibt es überhaupt einen nicht abgeschalteten Trap? Gepflegt statt
/// gerechnet: die VM fragt das an jeder Anweisungsgrenze (design.md, D4).
aktiv: bool,
}
impl Traps {
pub fn neu() -> Self {
Traps {
event_an: true,
..Default::default()
}
}
fn finde(&mut self, q: Quelle) -> Option<&mut Trap> {
self.traps.iter_mut().find(|t| t.quelle == q)
}
/// `ON <quelle> GOSUB ziel`. `ziel = None` schaltet den Trap ab
/// (`GOSUB 0`). Der Zustand bleibt, was er war — das Einschalten ist
/// Sache der Steueranweisung.
pub fn definieren(&mut self, q: Quelle, ziel: Option<u32>) {
if let Some(t) = self.finde(q) {
t.ziel = ziel;
if ziel.is_none() {
t.anstehend = false;
}
self.aktiv = self.traps.iter().any(|t| t.ziel.is_some());
return;
}
self.aktiv = self.aktiv || ziel.is_some();
self.traps.push(Trap {
quelle: q,
ziel,
zustand: Zustand::Aus,
anstehend: false,
laeuft: false,
off_im_handler: false,
});
}
/// Intervall des Zeit-Traps setzen (Sekunden, 186 400) und die erste
/// Fälligkeit von `jetzt_ms` aus rechnen.
pub fn timer_intervall(&mut self, sekunden: u64, jetzt_ms: u64) {
self.timer_intervall_ms = sekunden.saturating_mul(1000);
self.timer_faellig_ms = jetzt_ms.saturating_add(self.timer_intervall_ms);
}
/// Steueranweisung `<quelle> ON|OFF|STOP`. Ohne vorangehende
/// Trap-Definition bleibt sie wirkungslos, aber fehlerfrei.
pub fn setzen(&mut self, q: Quelle, z: Zustand, jetzt_ms: u64) {
let neu_faellig = matches!(q, Quelle::Timer) && z == Zustand::An;
let intervall = self.timer_intervall_ms;
if let Some(t) = self.finde(q) {
if z == Zustand::Aus {
// „OFF … Events occurring while trapping is off are
// ignored" und verwirft auch das unter STOP Gemerkte.
t.anstehend = false;
if t.laeuft {
t.off_im_handler = true;
}
}
t.zustand = z;
}
// Ein neu eingeschalteter Zeit-Trap zählt ab jetzt.
if neu_faellig && intervall > 0 {
self.timer_faellig_ms = jetzt_ms.saturating_add(intervall);
}
}
/// `EVENT ON` / `EVENT OFF`.
pub fn event(&mut self, an: bool) {
if !an {
for trap in &mut self.traps {
trap.anstehend = false;
}
}
self.event_an = an;
}
/// Ein Ereignis ist eingetreten. Es wird nur gemerkt, wenn die Quelle
/// einen Trap hat und nicht auf `OFF` steht.
pub fn melden(&mut self, q: Quelle) -> bool {
if !self.event_an {
return false;
}
if let Some(t) = self.finde(q) {
if t.ziel.is_some() && t.zustand != Zustand::Aus {
t.anstehend = true;
return true;
}
}
false
}
/// Zeit prüfen und den Zeit-Trap melden, wenn die Frist verstrichen ist.
/// Übersprungene Fristen zählen als eine — das Vorbild merkt sich nur
/// eines.
pub fn zeit_pruefen(&mut self, jetzt_ms: u64) {
if self.timer_intervall_ms == 0 || jetzt_ms < self.timer_faellig_ms {
return;
}
// Nächste Fälligkeit vom jetzigen Zeitpunkt aus, nicht kumulativ:
// sonst holt ein langer Handler die verpassten Fristen nach.
self.timer_faellig_ms = jetzt_ms.saturating_add(self.timer_intervall_ms);
self.melden(Quelle::Timer);
}
/// Gibt es einen definierten, nicht abgeschalteten Trap? Solange nicht,
/// kostet die Prüfung an der Anweisungsgrenze nichts (design.md, D4).
#[inline]
pub fn aktiv(&self) -> bool {
self.aktiv
}
/// Benutzerdefinierte Taste erklären (`KEY n, CHR$(flag)+CHR$(scan)`,
/// n = 1525). `flag` ist das Tastaturflagbyte des Vorbilds.
pub fn benutzertaste(&mut self, n: u8, flag: u8, scancode: u8) {
if (15..=25).contains(&n) {
self.benutzertasten[n as usize - 15] = Some((flag_zu_umschalt(flag), scancode));
}
}
/// Welcher `KEY`-Trap fängt diesen Tastendruck? Geprüft wird gegen den
/// Scancode und — bei den benutzerdefinierten Tasten — zusätzlich gegen
/// den Umschaltzustand. `KEY(0)` fängt alle Tasten der Tabelle.
///
/// Liefert die Trap-Nummer, nicht nur „ja/nein": der Aufrufer muss
/// wissen, welche Quelle er melden soll.
pub fn taste_faengt(&self, scancode: u8, shift: u8) -> Option<u8> {
if !self.event_an {
return None;
}
let treffer = |k: u8| -> bool {
match k {
0 => vordefinierte_taste(scancode).is_some(),
1..=14 | 30 | 31 => vordefinierte_taste(scancode) == Some(k),
15..=25 => self.benutzertasten[k as usize - 15] == Some((shift, scancode)),
_ => false,
}
};
self.traps
.iter()
.filter(|t| t.ziel.is_some() && t.zustand != Zustand::Aus && !t.laeuft)
.filter_map(|t| match t.quelle {
Quelle::Key(k) if treffer(k) => Some(k),
_ => None,
})
// Ein genauer Trap geht dem Sammeltrap `KEY(0)` vor.
.min_by_key(|&k| if k == 0 { u8::MAX } else { k })
}
/// Nächstes zuzustellendes Ereignis: Quelle und Sprungziel. Liefert
/// höchstens eines je Aufruf; die übrigen bleiben anstehend.
pub fn naechstes(&mut self) -> Option<(Quelle, u32)> {
if !self.event_an {
return None;
}
let mut kandidaten: Vec<usize> = (0..self.traps.len())
.filter(|&i| {
let t = &self.traps[i];
t.anstehend && t.zustand == Zustand::An && !t.laeuft && t.ziel.is_some()
})
.collect();
kandidaten.sort_by_key(|&i| self.traps[i].quelle);
let i = *kandidaten.first()?;
let t = &mut self.traps[i];
t.anstehend = false;
// „an automatic TIMER STOP is executed so that recursive traps
// cannot take place" — gilt für jede Quelle.
t.laeuft = true;
t.off_im_handler = false;
Some((t.quelle, t.ziel.unwrap()))
}
/// `RETURN` aus dem Handler: „automatically performs a TIMER ON
/// statement unless an explicit TIMER OFF was performed inside".
pub fn handler_beendet(&mut self, q: Quelle) {
if let Some(t) = self.finde(q) {
t.laeuft = false;
if t.off_im_handler {
t.zustand = Zustand::Aus;
t.anstehend = false;
} else {
t.zustand = Zustand::An;
}
t.off_im_handler = false;
}
}
}
/// Tastaturflagbyte des Vorbilds → unser Bitfeld (1 Umschalt, 2 Strg,
/// 4 Alt). Links und rechts sind im Terminal nicht unterscheidbar, beide
/// Umschaltbits gelten deshalb gleich; NumLock und CapsLock spielen für
/// einen Trap keine Rolle (Festlegung, siehe Sprachreferenz §8).
fn flag_zu_umschalt(flag: u8) -> u8 {
let mut u = 0;
if flag & 0b11 != 0 {
u |= 1;
}
if flag & 0b100 != 0 {
u |= 2;
}
if flag & 0b1000 != 0 {
u |= 4;
}
u
}
/// Scancode → Nummer des vordefinierten Traps (`ON KEY(1)``ON KEY(14)`,
/// `ON KEY(30)`/`ON KEY(31)`), Tabelle aus der Original-Hilfe.
fn vordefinierte_taste(scancode: u8) -> Option<u8> {
Some(match scancode {
0x3B..=0x44 => scancode - 0x3A, // F1F10 → 110
0x48 => 11, // Pfeil hoch
0x4B => 12, // Pfeil links
0x4D => 13, // Pfeil rechts
0x50 => 14, // Pfeil runter
0x85 => 30, // F11
0x86 => 31, // F12
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn mit_timer() -> Traps {
let mut t = Traps::neu();
t.definieren(Quelle::Timer, Some(100));
t.timer_intervall(1, 0);
t
}
#[test]
fn ohne_trap_kostet_nichts() {
let mut t = Traps::neu();
assert!(!t.aktiv());
assert_eq!(t.naechstes(), None);
t.definieren(Quelle::Timer, Some(7));
assert!(t.aktiv());
}
#[test]
fn aus_verwirft_auch_das_gemerkte() {
let mut t = mit_timer();
t.setzen(Quelle::Timer, Zustand::Gestoppt, 0);
t.zeit_pruefen(1000);
t.setzen(Quelle::Timer, Zustand::Aus, 1000);
t.setzen(Quelle::Timer, Zustand::An, 1000);
assert_eq!(t.naechstes(), None, "OFF verwirft das unter STOP Gemerkte");
}
#[test]
fn gestoppt_merkt_genau_eines() {
let mut t = Traps::neu();
t.definieren(Quelle::Key(1), Some(42));
t.setzen(Quelle::Key(1), Zustand::Gestoppt, 0);
t.melden(Quelle::Key(1));
t.melden(Quelle::Key(1));
t.melden(Quelle::Key(1));
t.setzen(Quelle::Key(1), Zustand::An, 0);
assert_eq!(t.naechstes(), Some((Quelle::Key(1), 42)));
t.handler_beendet(Quelle::Key(1));
assert_eq!(t.naechstes(), None, "gemerkt wird hoechstens eines");
}
#[test]
fn aus_verwirft_kuenftige() {
let mut t = mit_timer();
t.setzen(Quelle::Timer, Zustand::Aus, 0);
t.zeit_pruefen(5000);
t.setzen(Quelle::Timer, Zustand::An, 5000);
assert_eq!(t.naechstes(), None);
}
#[test]
fn zeit_wird_erst_nach_der_frist_faellig() {
let mut t = mit_timer();
t.setzen(Quelle::Timer, Zustand::An, 0);
t.zeit_pruefen(999);
assert_eq!(t.naechstes(), None);
t.zeit_pruefen(1000);
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
}
#[test]
fn uebersprungene_fristen_zaehlen_als_eine() {
let mut t = mit_timer();
t.setzen(Quelle::Timer, Zustand::An, 0);
t.zeit_pruefen(10_000); // zehn Fristen auf einmal
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
t.handler_beendet(Quelle::Timer);
t.zeit_pruefen(10_000);
assert_eq!(t.naechstes(), None, "nicht nachgeholt");
}
#[test]
fn kein_wiedereintritt_waehrend_der_handler_laeuft() {
let mut t = mit_timer();
t.setzen(Quelle::Timer, Zustand::An, 0);
t.zeit_pruefen(1000);
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
t.zeit_pruefen(2000);
assert_eq!(t.naechstes(), None, "kein Wiedereintritt");
t.handler_beendet(Quelle::Timer);
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)), "danach einmal");
}
#[test]
fn off_im_handler_ueberlebt_das_return() {
let mut t = mit_timer();
t.setzen(Quelle::Timer, Zustand::An, 0);
t.zeit_pruefen(1000);
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
t.setzen(Quelle::Timer, Zustand::Aus, 1000); // OFF im Handler
t.handler_beendet(Quelle::Timer);
t.zeit_pruefen(9000);
assert_eq!(t.naechstes(), None, "RETURN schaltet nicht wieder ein");
}
#[test]
fn return_schaltet_sonst_selbsttaetig_ein() {
let mut t = mit_timer();
t.setzen(Quelle::Timer, Zustand::An, 0);
t.zeit_pruefen(1000);
t.naechstes().unwrap();
t.handler_beendet(Quelle::Timer);
t.zeit_pruefen(2000);
assert_eq!(t.naechstes(), Some((Quelle::Timer, 100)));
}
#[test]
fn event_off_verwirft_ereignisse_und_laesst_die_zustaende() {
let mut t = mit_timer();
t.setzen(Quelle::Timer, Zustand::An, 0);
t.event(false);
t.zeit_pruefen(1000);
assert_eq!(t.naechstes(), None);
t.event(true);
assert_eq!(
t.naechstes(),
None,
"EVENT OFF muss Eingetretenes verwerfen"
);
t.zeit_pruefen(2000);
assert_eq!(
t.naechstes(),
Some((Quelle::Timer, 100)),
"TIMER ON blieb erhalten"
);
}
#[test]
fn reihenfolge_key_vor_timer_vor_uevent_vor_signal() {
let mut t = Traps::neu();
for (q, ziel) in [
(Quelle::Signal(1), 4),
(Quelle::UEvent, 3),
(Quelle::Timer, 2),
(Quelle::Key(5), 1),
] {
t.definieren(q, Some(ziel));
t.setzen(q, Zustand::An, 0);
t.melden(q);
}
let mut reihe = Vec::new();
while let Some((q, _)) = t.naechstes() {
reihe.push(q);
t.handler_beendet(q);
}
assert_eq!(
reihe,
vec![
Quelle::Key(5),
Quelle::Timer,
Quelle::UEvent,
Quelle::Signal(1)
]
);
}
#[test]
fn key_null_faengt_alle_tasten_der_tabelle() {
let mut t = Traps::neu();
t.definieren(Quelle::Key(0), Some(9));
t.setzen(Quelle::Key(0), Zustand::An, 0);
assert_eq!(t.taste_faengt(0x3B, 0), Some(0), "F1");
assert_eq!(t.taste_faengt(0x48, 0), Some(0), "Pfeil hoch");
assert_eq!(t.taste_faengt(0x1E, 0), None, "A ist keine Trap-Taste");
t.setzen(Quelle::Key(0), Zustand::Aus, 0);
assert_eq!(t.taste_faengt(0x3B, 0), None, "OFF gibt die Taste frei");
}
#[test]
fn vordefinierte_taste_wird_ueber_den_scancode_erkannt() {
let mut t = Traps::neu();
t.definieren(Quelle::Key(1), Some(1));
t.setzen(Quelle::Key(1), Zustand::An, 0);
assert_eq!(t.taste_faengt(0x3B, 0), Some(1), "F1");
assert_eq!(t.taste_faengt(0x3C, 0), None, "F2 hat keinen Trap");
}
#[test]
fn benutzertaste_braucht_umschaltzustand_und_scancode() {
let mut t = Traps::neu();
t.definieren(Quelle::Key(15), Some(5));
t.setzen(Quelle::Key(15), Zustand::An, 0);
// KEY 15, CHR$(4) + CHR$(30) — Strg+A
t.benutzertaste(15, 4, 0x1E);
assert_eq!(t.taste_faengt(0x1E, 2), Some(15), "Strg+A");
assert_eq!(t.taste_faengt(0x1E, 0), None, "A ohne Strg nicht");
assert_eq!(t.taste_faengt(0x1F, 2), None, "Strg+S nicht");
}
#[test]
fn beide_umschaltbits_gelten_gleich() {
let mut t = Traps::neu();
t.definieren(Quelle::Key(16), Some(6));
t.setzen(Quelle::Key(16), Zustand::An, 0);
t.benutzertaste(16, 1, 0x1E); // rechte Umschalttaste
assert_eq!(t.taste_faengt(0x1E, 1), Some(16));
let mut t2 = Traps::neu();
t2.definieren(Quelle::Key(16), Some(6));
t2.setzen(Quelle::Key(16), Zustand::An, 0);
t2.benutzertaste(16, 2, 0x1E); // linke Umschalttaste
assert_eq!(t2.taste_faengt(0x1E, 1), Some(16));
}
#[test]
fn genauer_trap_geht_dem_sammeltrap_vor() {
let mut t = Traps::neu();
for (q, z) in [(Quelle::Key(0), 9), (Quelle::Key(1), 1)] {
t.definieren(q, Some(z));
t.setzen(q, Zustand::An, 0);
}
assert_eq!(
t.taste_faengt(0x3B, 0),
Some(1),
"F1 hat einen eigenen Trap"
);
assert_eq!(t.taste_faengt(0x3C, 0), Some(0), "F2 faellt an KEY(0)");
}
#[test]
fn gosub_null_schaltet_den_trap_ab() {
let mut t = mit_timer();
t.setzen(Quelle::Timer, Zustand::An, 0);
t.definieren(Quelle::Timer, None);
t.zeit_pruefen(5000);
assert_eq!(t.naechstes(), None);
assert!(!t.aktiv());
}
}

View File

@@ -10,6 +10,7 @@ authors.workspace = true
tb-runtime.workspace = true
ratatui.workspace = true
crossterm.workspace = true
signal-hook.workspace = true
thiserror.workspace = true
log.workspace = true

View File

@@ -5,11 +5,11 @@
//! Terminal-Abhängigkeit bleibt (Entscheidung 2026-09-03, D1/D2).
use std::io::{self, Stdout};
use std::time::Duration;
use std::time::{Duration, Instant};
use crossterm::event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
KeyModifiers,
KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use crossterm::execute;
use crossterm::terminal::{
@@ -18,10 +18,11 @@ use crossterm::terminal::{
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use tb_runtime::host::{taste, Ereignis, Host};
use tb_runtime::host::{taste, umschalt, Ereignis, Host, MausArt, MausEreignis};
use tb_runtime::screen::TextScreen;
use crate::screen::ScreenWidget;
use crate::signale::Signalquelle;
/// Host für laufende Programme im Terminal.
///
@@ -29,6 +30,12 @@ use crate::screen::ScreenWidget;
/// `Drop` stellt beides wieder her — auch bei Panik oder Laufzeitfehler.
pub struct TerminalHost {
terminal: Terminal<CrosstermBackend<Stdout>>,
/// Nullpunkt der monotonen Zeit (`Host::jetzt_ms`).
start: Instant,
signale: Signalquelle,
/// Zuletzt bekannte Darstellungsgröße in Zellen. Mausereignisse
/// außerhalb dieser Fläche werden verworfen (design.md, D7).
flaeche: (usize, usize),
}
impl TerminalHost {
@@ -37,7 +44,16 @@ impl TerminalHost {
let mut out = io::stdout();
execute!(out, EnterAlternateScreen, EnableMouseCapture)?;
let terminal = Terminal::new(CrosstermBackend::new(out))?;
Ok(TerminalHost { terminal })
let flaeche = terminal
.size()
.map(|s| (s.width as usize, s.height as usize))
.unwrap_or((80, 25));
Ok(TerminalHost {
terminal,
start: Instant::now(),
signale: Signalquelle::neu(),
flaeche,
})
}
/// Aktuelle Terminalgröße, damit der Bildschirm sie von Beginn an nutzt.
@@ -66,8 +82,17 @@ impl Host for TerminalHost {
.draw(|f| f.render_widget(ScreenWidget(screen), f.area()));
}
fn jetzt_ms(&mut self) -> u64 {
self.start.elapsed().as_millis() as u64
}
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
loop {
// Signale vor dem Terminal: sie liegen schon an, wenn wir hier
// ankommen, und ein blockierendes `read` wuerde sie liegenlassen.
if let Some(n) = self.signale.abholen() {
return Some(Ereignis::Signal(n));
}
if !blockierend && !event::poll(Duration::ZERO).unwrap_or(false) {
return None;
}
@@ -78,10 +103,16 @@ impl Host for TerminalHost {
}
}
Ok(Event::Resize(cols, rows)) => {
self.flaeche = (cols as usize, rows as usize);
return Some(Ereignis::Groesse {
cols: cols as usize,
rows: rows as usize,
})
});
}
Ok(Event::Mouse(m)) => {
if let Some(e) = maus_zu_ereignis(m, self.flaeche) {
return Some(e);
}
}
Ok(_) => {}
Err(_) => return Some(Ereignis::Ende),
@@ -90,6 +121,53 @@ impl Host for TerminalHost {
}
}
/// crossterm-Maus → Ereignis in Zellenkoordinaten (design.md, D7).
///
/// crossterm zählt ab 0, der Dialekt ab 1 — umgerechnet wird hier, damit es
/// in der Laufzeit nur eine Zählweise gibt. Ereignisse außerhalb der
/// Darstellungsfläche und Ereignisse ohne Entsprechung liefern `None`.
fn maus_zu_ereignis(m: MouseEvent, flaeche: (usize, usize)) -> Option<Ereignis> {
let (cols, rows) = flaeche;
let spalte = m.column as usize + 1;
let zeile = m.row as usize + 1;
if spalte > cols || zeile > rows {
return None;
}
let knopf = |b: MouseButton| match b {
MouseButton::Left => 1,
MouseButton::Right => 2,
MouseButton::Middle => 4,
};
let (art, taste) = match m.kind {
MouseEventKind::Down(b) => (MausArt::Druck, knopf(b)),
MouseEventKind::Up(b) => (MausArt::Loslassen, knopf(b)),
MouseEventKind::Drag(b) => (MausArt::Bewegung, knopf(b)),
MouseEventKind::Moved => (MausArt::Bewegung, 0),
// Rollrad kennt das Vorbild nicht — Stufe 2 (PLAN.md).
MouseEventKind::ScrollDown
| MouseEventKind::ScrollUp
| MouseEventKind::ScrollLeft
| MouseEventKind::ScrollRight => return None,
};
let mut shift = 0u8;
if m.modifiers.contains(KeyModifiers::SHIFT) {
shift |= 1;
}
if m.modifiers.contains(KeyModifiers::CONTROL) {
shift |= 2;
}
if m.modifiers.contains(KeyModifiers::ALT) {
shift |= 4;
}
Some(Ereignis::Maus(MausEreignis {
art,
taste,
shift,
zeile,
spalte,
}))
}
/// crossterm-Taste → `INKEY$`-Form. `None` für Tasten ohne Entsprechung.
fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
// Strg+Untbr bzw. Strg+C: Abbruchwunsch.
@@ -118,7 +196,17 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
KeyCode::Delete => taste::sonder(83),
_ => return None,
};
Some(Ereignis::Taste(s))
let mut shift = 0u8;
if k.modifiers.contains(KeyModifiers::SHIFT) {
shift |= umschalt::SHIFT;
}
if k.modifiers.contains(KeyModifiers::CONTROL) {
shift |= umschalt::CTRL;
}
if k.modifiers.contains(KeyModifiers::ALT) {
shift |= umschalt::ALT;
}
Some(Ereignis::Taste(s, shift))
}
#[cfg(test)]
@@ -128,13 +216,13 @@ mod tests {
#[test]
fn zeichentaste_wird_zu_inkey_form() {
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Taste("a".into())));
assert_eq!(taste_zu_ereignis(k), Some(Ereignis::Taste("a".into(), 0)));
}
#[test]
fn sondertaste_traegt_fuehrendes_nullzeichen() {
let k = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE);
let Some(Ereignis::Taste(s)) = taste_zu_ereignis(k) else {
let Some(Ereignis::Taste(s, _)) = taste_zu_ereignis(k) else {
panic!("keine Taste");
};
assert_eq!(s.chars().count(), 2);
@@ -142,6 +230,123 @@ mod tests {
assert_eq!(s.chars().nth(1), Some(59 as char)); // F1 = 59
}
fn maus(kind: MouseEventKind, col: u16, row: u16, mods: KeyModifiers) -> MouseEvent {
MouseEvent {
kind,
column: col,
row,
modifiers: mods,
}
}
#[test]
fn maus_zaehlt_ab_eins() {
let e = maus_zu_ereignis(
maus(
MouseEventKind::Down(MouseButton::Left),
0,
0,
KeyModifiers::NONE,
),
(80, 25),
);
assert_eq!(
e,
Some(Ereignis::Maus(MausEreignis {
art: MausArt::Druck,
taste: 1,
shift: 0,
zeile: 1,
spalte: 1,
}))
);
}
#[test]
fn letzte_zelle_liegt_noch_drin() {
let e = maus_zu_ereignis(
maus(
MouseEventKind::Up(MouseButton::Right),
79,
24,
KeyModifiers::NONE,
),
(80, 25),
);
let Some(Ereignis::Maus(m)) = e else {
panic!("kein Mausereignis")
};
assert_eq!((m.zeile, m.spalte), (25, 80));
assert_eq!((m.art, m.taste), (MausArt::Loslassen, 2));
}
#[test]
fn ausserhalb_der_flaeche_wird_verworfen() {
let e = maus_zu_ereignis(
maus(MouseEventKind::Moved, 80, 0, KeyModifiers::NONE),
(80, 25),
);
assert_eq!(e, None);
let e = maus_zu_ereignis(
maus(MouseEventKind::Moved, 0, 25, KeyModifiers::NONE),
(80, 25),
);
assert_eq!(e, None);
}
#[test]
fn nach_groessenaenderung_gilt_die_neue_flaeche() {
// Dieselbe Position: bei 80x25 draussen, bei 120x40 drin.
let p = maus(MouseEventKind::Moved, 100, 30, KeyModifiers::NONE);
assert_eq!(maus_zu_ereignis(p, (80, 25)), None);
let Some(Ereignis::Maus(m)) = maus_zu_ereignis(p, (120, 40)) else {
panic!("kein Mausereignis")
};
assert_eq!((m.zeile, m.spalte), (31, 101));
}
#[test]
fn umschaltzustand_als_bitfeld() {
let e = maus_zu_ereignis(
maus(
MouseEventKind::Down(MouseButton::Middle),
5,
5,
KeyModifiers::SHIFT | KeyModifiers::ALT,
),
(80, 25),
);
let Some(Ereignis::Maus(m)) = e else {
panic!("kein Mausereignis")
};
assert_eq!(m.shift, 1 | 4);
assert_eq!(m.taste, 4);
}
#[test]
fn rollrad_hat_keine_entsprechung() {
let e = maus_zu_ereignis(
maus(MouseEventKind::ScrollUp, 1, 1, KeyModifiers::NONE),
(80, 25),
);
assert_eq!(e, None);
}
#[test]
fn umschaltzustand_der_taste_wird_mitgefuehrt() {
// Strg+A: Strg allein ist kein Abbruch, nur Strg+C ist es.
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
assert_eq!(
taste_zu_ereignis(k),
Some(Ereignis::Taste("a".into(), umschalt::CTRL))
);
let k = KeyEvent::new(KeyCode::F(1), KeyModifiers::ALT | KeyModifiers::SHIFT);
let Some(Ereignis::Taste(_, shift)) = taste_zu_ereignis(k) else {
panic!("keine Taste")
};
assert_eq!(shift, umschalt::ALT | umschalt::SHIFT);
}
#[test]
fn strg_c_ist_abbruch() {
let k = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);

View File

@@ -8,7 +8,8 @@
//! (Schaltflächen, Textfelder, Listen …), Menüs, Fokusreihenfolge,
//! Tastatur- und Mausereignisse.
pub mod screen;
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
pub mod forms;
pub mod events;
pub mod forms;
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
pub mod screen;
pub mod signale; // Betriebssystemsignale als Ereignisquelle (SIGNAL)

112
crates/tb-ui/src/signale.rs Normal file
View File

@@ -0,0 +1,112 @@
//! Betriebssystemsignale als Ereignisquelle (design.md, D8).
//!
//! Der Signalhandler tut nur eines: er setzt eine Marke. Übersetzt wird sie
//! erst beim Abholen durch den Host, an einem gewöhnlichen Zustellpunkt —
//! im Signalkontext läuft damit kein Programmcode und nichts von der
//! Laufzeit.
//!
//! Abgebildet wird nur, was auf allen Zielplattformen besteht:
//! `SIGNAL(1)` = `SIGINT`, `SIGNAL(2)` = `SIGTERM` (Sprachreferenz,
//! Abweichung 8). Unter Windows nutzt `signal-hook` das `signal` der
//! C-Laufzeit; `SIGTERM` ist dort von außen nicht zustellbar.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use signal_hook::consts::{SIGINT, SIGTERM};
/// Kennung von `SIGNAL(n%)`.
pub const SIGNAL_INT: u8 = 1;
pub const SIGNAL_TERM: u8 = 2;
pub struct Signalquelle {
int: Arc<AtomicBool>,
term: Arc<AtomicBool>,
}
impl Signalquelle {
/// Handler eintragen. Schlägt das fehl, bleibt die Quelle stumm — ein
/// Programm ohne `SIGNAL`-Trap merkt davon nichts, und eines mit Trap
/// verhält sich wie unter `SIGNAL(n) OFF`.
pub fn neu() -> Self {
let int = Arc::new(AtomicBool::new(false));
let term = Arc::new(AtomicBool::new(false));
let _ = signal_hook::flag::register(SIGINT, Arc::clone(&int));
let _ = signal_hook::flag::register(SIGTERM, Arc::clone(&term));
Signalquelle { int, term }
}
/// Höchstens ein anstehendes Signal abholen und die Marke zurücksetzen.
/// `SIGINT` hat Vorrang: es ist das dringendere der beiden.
pub fn abholen(&self) -> Option<u8> {
if self.int.swap(false, Ordering::SeqCst) {
return Some(SIGNAL_INT);
}
if self.term.swap(false, Ordering::SeqCst) {
return Some(SIGNAL_TERM);
}
None
}
}
#[cfg(test)]
impl Signalquelle {
/// Quelle ohne eingetragene Handler. Ein echtes Signal erreicht **jede**
/// im Prozess registrierte Quelle; die reinen Übersetzungstests müssen
/// deshalb aus dem Weg bleiben.
fn ohne_registrierung() -> Self {
Signalquelle {
int: Arc::new(AtomicBool::new(false)),
term: Arc::new(AtomicBool::new(false)),
}
}
}
impl Default for Signalquelle {
fn default() -> Self {
Signalquelle::neu()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Marke → Ereignis, ohne echtes Signal: prüft die Übersetzung.
#[test]
fn marke_wird_zu_kennung_und_verfaellt_danach() {
let q = Signalquelle::ohne_registrierung();
assert_eq!(q.abholen(), None);
q.int.store(true, Ordering::SeqCst);
assert_eq!(q.abholen(), Some(SIGNAL_INT));
assert_eq!(q.abholen(), None, "eine Marke gilt genau einmal");
q.term.store(true, Ordering::SeqCst);
assert_eq!(q.abholen(), Some(SIGNAL_TERM));
}
#[test]
fn sigint_hat_vorrang_vor_sigterm() {
let q = Signalquelle::ohne_registrierung();
q.int.store(true, Ordering::SeqCst);
q.term.store(true, Ordering::SeqCst);
assert_eq!(q.abholen(), Some(SIGNAL_INT));
assert_eq!(q.abholen(), Some(SIGNAL_TERM));
}
/// Ganze Kette: echtes Signal an den eigenen Prozess. Der eingetragene
/// Handler ersetzt die Vorgabewirkung, der Test überlebt es also.
#[test]
fn echtes_signal_setzt_die_marke() {
let q = Signalquelle::neu();
signal_hook::low_level::raise(SIGINT).expect("raise");
// Zustellung ist asynchron; ein paar Versuche genuegen.
for _ in 0..1000 {
if let Some(n) = q.abholen() {
assert_eq!(n, SIGNAL_INT);
return;
}
std::thread::yield_now();
}
panic!("SIGINT kam nicht an");
}
}

View File

@@ -304,6 +304,14 @@ instrs! {
0xB2 RetFn;
0xB3 CallBuiltin(a: u16, b: u8);
// 0xE0 — Ereignis-Traps (Sprachreferenz §8); Kennung vom Stack
0xE0 TrapDefine(a: u8, b: u32); // Quellenart, Sprungziel
0xE1 TrapDisable(a: u8); // GOSUB 0
0xE2 TrapSet(a: u8, b: u8); // Quellenart, Zustand
0xE3 EventSwitch(a: bool);
0xE4 Doevents; // Zustellpunkt; legt 0 ab
0xE5 Sleep(a: bool); // hat Argument? Dauer vom Stack (Sekunden)
// 0xC0 — Fehlerbehandlung
0xC0 OnErrorGoto(a: u32);
0xC1 OnErrorLocal(a: u32);
@@ -614,6 +622,12 @@ mod tests {
Instr::Call(2, 3),
Instr::Input(2, false, 0xFFFF, true),
Instr::ConvCyR8,
Instr::TrapDefine(1, 4242),
Instr::TrapDisable(3),
Instr::TrapSet(0, 2),
Instr::EventSwitch(false),
Instr::Doevents,
Instr::Sleep(true),
Instr::RetFn,
];
let mut buf = Vec::new();

View File

@@ -95,6 +95,24 @@ impl ProcCtx {
fn here(&self) -> u32 {
self.code.len() as u32
}
/// Label auf die unmittelbar zuvor emittierte `Stmt`-Grenze binden.
///
/// Schleifen brauchen im Kreis eine Anweisungsgrenze — sonst wird dort
/// nie ein Ereignis zugestellt, kein Breakpoint erreicht und kein
/// Abbruch bemerkt (`FOR i = 1 TO 10000: NEXT` als Warteschleife).
/// Wo die Grenze ohnehin direkt vor dem Schleifenkopf liegt, zeigt der
/// Rücksprung einfach auf sie, statt eine zweite zu emittieren.
fn bind_auf_stmt(&mut self, label: u16) {
let pc = self.code.len().saturating_sub(1) as u32;
debug_assert!(
matches!(self.code.last(), Some(Instr::Stmt(_))),
"bind_auf_stmt ohne vorangehende Anweisungsgrenze"
);
if (label as usize) >= self.label_pc.len() {
self.label_pc.resize(label as usize + 1, None);
}
self.label_pc[label as usize] = Some(pc);
}
fn bind(&mut self, label: u16) {
let pc = self.here();
if (label as usize) >= self.label_pc.len() {
@@ -170,7 +188,8 @@ impl Codegen {
| Instr::RetGosubTo(t)
| Instr::ResumeLabel(t)
| Instr::OnErrorGoto(t)
| Instr::OnErrorLocal(t) => *t = pc,
| Instr::OnErrorLocal(t)
| Instr::TrapDefine(_, t) => *t = pc,
other => unreachable!("Fixup auf {other:?}"),
}
}
@@ -330,9 +349,14 @@ impl Codegen {
ctx.bind(l_end);
}
}
HStmtKind::Loop { pre, post, body, exit_label } => {
HStmtKind::Loop {
pre,
post,
body,
exit_label,
} => {
let l_start = ctx.new_label();
ctx.bind(l_start);
ctx.bind_auf_stmt(l_start);
if let Some((is_until, cond)) = pre {
self.expr(ctx, cond);
if *is_until {
@@ -370,19 +394,49 @@ impl Codegen {
exit_label,
} => {
self.gen_for(
ctx, proc, var, *ty, from, to, step.as_ref(), *limit_slot, *step_slot, body,
ctx,
proc,
var,
*ty,
from,
to,
step.as_ref(),
*limit_slot,
*step_slot,
body,
*exit_label,
stmt.line,
);
}
HStmtKind::Goto(l) => ctx.emit_jump(Instr::Jump(0), *l),
HStmtKind::Gosub(l) => ctx.emit_jump(Instr::Gosub(0), *l),
HStmtKind::OnGoto { sel, gosub, targets } => {
HStmtKind::OnGoto {
sel,
gosub,
targets,
} => {
self.expr(ctx, sel);
let table = self.jump_tables.len();
self.jump_tables.push(Vec::new());
ctx.table_fixups.push((table, targets.clone()));
ctx.emit(Instr::OnJump(table as u16, *gosub));
}
HStmtKind::TrapDef { art, index, ziel } => {
self.expr(ctx, index);
match ziel {
Some(l) => ctx.emit_jump(Instr::TrapDefine(*art, 0), *l),
None => ctx.emit(Instr::TrapDisable(*art)),
}
}
HStmtKind::TrapSet {
art,
index,
zustand,
} => {
self.expr(ctx, index);
ctx.emit(Instr::TrapSet(*art, *zustand));
}
HStmtKind::EventSwitch(an) => ctx.emit(Instr::EventSwitch(*an)),
HStmtKind::ReturnGosub(target) => match target {
None => ctx.emit(Instr::RetGosub),
Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l),
@@ -398,6 +452,17 @@ impl Codegen {
for a in args {
self.expr(ctx, a);
}
// `DOEVENTS` und `SLEEP` sind Zustellpunkte und damit Sache
// der Ausführung, nicht der Bibliothek: ein Builtin kann
// keine Ereignisprozedur des Programms aufrufen
// (design.md, D1).
if let Some(i) = zustellpunkt_instr(*b, args.len()) {
ctx.emit(i);
if matches!(b, Builtin::Doevents) {
ctx.emit(Instr::Pop);
}
return;
}
let id = builtin_id(*b);
ctx.emit(Instr::CallBuiltin(id, args.len() as u8));
if builtin_returns_value(*b) {
@@ -484,6 +549,7 @@ impl Codegen {
step_slot: Option<VarSlot>,
body: &[HStmt],
exit_label: u16,
line: u32,
) {
// Startwert, Grenze, ggf. Schritt einmal auswerten.
self.store_place(ctx, var, |cg, ctx| cg.expr(ctx, from));
@@ -501,6 +567,15 @@ impl Codegen {
let l_test = ctx.new_label();
let l_body = ctx.new_label();
ctx.bind(l_test);
// Anweisungsgrenze für den Rücksprung von `NEXT`. Sie kann nicht
// auf die Grenze des `FOR` zeigen — dort stünde die Initialisierung
// noch einmal.
//
// ponytail: gemeldet wird die Zeile des `FOR`, nicht die des
// `NEXT` — die trägt das HIR nicht. Ceiling: braucht der Debugger
// in Phase 5 die genaue Zeile, bekommt `HStmtKind::For` ein Feld
// `next_line`.
ctx.emit(Instr::Stmt(line));
match const_step {
Some(s) => {
// Vergleichsrichtung zur Compilezeit.
@@ -625,6 +700,10 @@ impl Codegen {
for a in args {
self.expr(ctx, a);
}
if let Some(i) = zustellpunkt_instr(*b, args.len()) {
ctx.emit(i);
return;
}
ctx.emit(Instr::CallBuiltin(builtin_id(*b), args.len() as u8));
}
HExpr::ArrayBound { lower, place, dim } => {
@@ -934,6 +1013,15 @@ fn emit_conv(ctx: &mut ProcCtx, from: NumTy, to: NumTy) {
/// Abbildung `hir::Builtin` → stabiler Tabellenindex der Laufzeit.
/// Erschöpfendes `match`: neue Builtins zwingen hier zur Pflege.
/// `DOEVENTS`/`SLEEP` → eigene Instruktion statt Builtin-Aufruf.
fn zustellpunkt_instr(b: Builtin, argc: usize) -> Option<Instr> {
match b {
Builtin::Doevents => Some(Instr::Doevents),
Builtin::Sleep => Some(Instr::Sleep(argc > 0)),
_ => None,
}
}
fn builtin_id(b: Builtin) -> u16 {
match b {
Builtin::Len => ids::LEN,
@@ -1060,6 +1148,7 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::CommandS => ids::COMMAND_S,
Builtin::Doevents => ids::DOEVENTS,
Builtin::Sleep => ids::SLEEP,
Builtin::SetUEvent => ids::SETUEVENT,
Builtin::Beep => ids::BEEP,
// ISAM
Builtin::IsamOpen => ids::ISAM_OPEN,

View File

@@ -15,9 +15,8 @@ use std::rc::Rc;
use tb_runtime::builtins::{builtin_table, RtState};
use tb_runtime::errors::RuntimeError;
use tb_runtime::host::Host;
use tb_runtime::value::{
self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef,
};
use tb_runtime::traps::Quelle;
use tb_runtime::value::{self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef};
/// Warum die VM die Kontrolle abgibt.
#[derive(Debug, Clone, PartialEq)]
@@ -54,6 +53,11 @@ struct Frame {
/// Instruktionsindex der zuletzt begonnenen Anweisung (`Stmt`).
last_stmt_pc: usize,
line: u32,
/// Gesetzt, wenn dieser Frame der Handler eines Ereignis-Traps ist.
/// Er läuft im Modulrumpf und **teilt dessen Locals** — ein eigener
/// Satz würde dem Handler leere Modulvariablen zeigen. Sein `RETURN`
/// beendet den Handler (design.md, D2).
trap: Option<Quelle>,
}
pub struct Vm {
@@ -65,7 +69,12 @@ pub struct Vm {
pub rt: RtState,
// Fehlerzustand
err: u16,
/// Wert von `ERL`: die Zeilennummer, in der der Fehler auftrat. Wird
/// **nur** beim Auslösen gesetzt — liefe sie mit `zeile_nr` mit, meldete
/// ein Handler auf einer nummerierten Zeile seine eigene Nummer.
erl: u32,
/// Zuletzt durchlaufene numerische Zeilennummer (0 = keine).
zeile_nr: u32,
module_handler: Handler,
in_handler: bool,
resume_pc: usize,
@@ -77,6 +86,34 @@ pub struct Vm {
data_ptr: usize,
}
/// Quellenarten, wie der Codegenerator sie kodiert.
const ART_KEY: u8 = 0;
const ART_TIMER: u8 = 1;
const ART_UEVENT: u8 = 2;
const ART_SIGNAL: u8 = 3;
/// Art + Kennung → Quelle. Die Kennung kann ein Ausdruck sein, deshalb
/// wird der Wertebereich hier noch einmal geprüft (Fehler 5).
///
/// Das Zeitintervall gehört **nicht** hierher: `TIMER ON` trägt keine
/// Kennung, geprüft wird es nur an `ON TIMER(n&)`.
fn trap_quelle(art: u8, n: i32) -> Result<Quelle, RuntimeError> {
let ok = match art {
ART_KEY => matches!(n, 0..=25 | 30 | 31),
ART_SIGNAL => (1..=2).contains(&n),
_ => true,
};
if !ok {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(match art {
ART_KEY => Quelle::Key(n as u8),
ART_TIMER => Quelle::Timer,
ART_UEVENT => Quelle::UEvent,
_ => Quelle::Signal(n as u8),
})
}
enum Flow {
Normal,
Event(RunEvent),
@@ -97,6 +134,7 @@ impl Vm {
rt: RtState::default(),
err: 0,
erl: 0,
zeile_nr: 0,
module_handler: Handler::None,
in_handler: false,
resume_pc: 0,
@@ -134,9 +172,88 @@ impl Vm {
local_handler: Handler::None,
last_stmt_pc: 0,
line: 0,
trap: None,
});
}
/// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert
/// `true`, wenn ein Handler aufgesetzt wurde.
fn zustellen(&mut self, host: &mut dyn Host) -> bool {
if !self.rt.traps.aktiv() {
return false;
}
let jetzt = host.jetzt_ms();
self.rt.traps.zeit_pruefen(jetzt);
self.rt.tasten_traps_pruefen();
if let Some((q, ziel)) = self.rt.traps.naechstes() {
self.trap_frame_aufsetzen(q, ziel);
return true;
}
false
}
/// `SLEEP [n]`: wartet `n` Sekunden, ohne Argument bis zum nächsten
/// Tastendruck. Ein zugestelltes Ereignis beendet das Warten
/// (Sprachreferenz §8) — der Handler läuft, und danach geht es hinter
/// dem `SLEEP` weiter.
fn schlafen(&mut self, host: &mut dyn Host, sekunden: f64) {
let start = host.jetzt_ms();
let dauer_ms = (sekunden.max(0.0) * 1000.0) as u64;
// ponytail: zusätzlich eine echte Zeitschranke, damit ein Host mit
// stehender Uhr (Testhost) nicht ewig wartet. Ceiling: bei einer
// virtuellen Uhr endet `SLEEP` nach der realen Dauer, wenn die
// virtuelle nie vorrückt.
let real_start = std::time::Instant::now();
loop {
self.rt.tick(host);
if self.zustellen(host) {
return;
}
if self.rt.ende || self.rt.abbruch {
return;
}
if sekunden <= 0.0 {
// Ohne Argument: bis zum Tastendruck.
if self.rt.naechste_taste(host, false).is_some() {
return;
}
} else {
let vergangen = host.jetzt_ms().saturating_sub(start);
if vergangen >= dauer_ms || real_start.elapsed().as_millis() as u64 >= dauer_ms {
return;
}
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
/// Trap-Handler aufsetzen: ein Frame auf den Modulrumpf, der dessen
/// Locals mitbenutzt, mit eigenem `GOSUB`-Stapel und einer Rückmarke
/// auf die unterbrochene Stelle (design.md, D2).
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.frames.push(Frame {
proc: 0,
pc: ziel as usize,
locals_base,
stack_base,
gosub: Vec::new(),
local_handler: Handler::None,
last_stmt_pc: 0,
line: 0,
trap: Some(q),
});
}
/// Trap-Frame abräumen. Anders als `pop_frame` bleiben die Locals
/// stehen — sie gehören dem Modulrumpf, nicht dem Handler.
fn trap_frame_abraeumen(&mut self) -> Option<Quelle> {
let f = self.frames.pop()?;
self.stack.truncate(f.stack_base);
f.trap
}
fn pop_frame(&mut self) {
let f = self.frames.pop().expect("Frame-Unterlauf");
self.locals.truncate(f.locals_base);
@@ -273,6 +390,8 @@ impl Vm {
// Fehler im Handler: fatal, keine Kaskade.
return Some(self.error_event(code, line));
}
// `ERL` friert auf der Zeile ein, in der es passiert ist.
self.erl = self.zeile_nr;
// Frame-Kette von innen nach außen nach LOCAL-Handlern absuchen.
let mut target: Option<(usize, Handler)> = None;
for (depth, f) in self.frames.iter().enumerate().rev() {
@@ -523,6 +642,24 @@ impl Vm {
self.rt.tick(host);
self.rt.screen.veraenderung_quittieren();
}
// Ereigniszustellung (design.md, D1): nur wenn überhaupt
// ein Trap definiert ist — sonst kostet die Grenze nichts.
if self.rt.traps.aktiv() {
// ponytail: die Uhr nur alle 64 Anweisungen lesen, die
// Tastenschlange dagegen jedes Mal. Ceiling: die
// Auflösung eines Zeit-Traps ist damit 64 Anweisungen;
// reicht das nicht, wird daraus ein eigener Zähler je
// Trap.
if self.tick_zaehler % 64 == 0 {
let jetzt = host.jetzt_ms();
self.rt.traps.zeit_pruefen(jetzt);
}
self.rt.tasten_traps_pruefen();
if let Some((q, ziel)) = self.rt.traps.naechstes() {
self.trap_frame_aufsetzen(q, ziel);
return Ok(Flow::Normal);
}
}
if self.flags != 0 {
if self.flags & F_STEP != 0 {
return Ok(Flow::Event(RunEvent::Stepped { line }));
@@ -536,12 +673,64 @@ impl Vm {
}
Ok(Flow::Normal)
}
// ---- Ereignis-Traps (Sprachreferenz §8) ----
I::TrapDefine(art, ziel) => {
let n = self.pop_i32()?;
let q = trap_quelle(art, n)?;
if art == ART_TIMER {
if !(1..=86_400).contains(&n) {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let jetzt = host.jetzt_ms();
self.rt.traps.timer_intervall(n as u64, jetzt);
}
self.rt.traps.definieren(q, Some(ziel));
Ok(Flow::Normal)
}
I::TrapDisable(art) => {
let n = self.pop_i32()?;
let q = trap_quelle(art, n)?;
self.rt.traps.definieren(q, None);
Ok(Flow::Normal)
}
I::TrapSet(art, zustand) => {
let n = self.pop_i32()?;
let q = trap_quelle(art, n)?;
let z = match zustand {
0 => tb_runtime::traps::Zustand::An,
1 => tb_runtime::traps::Zustand::Aus,
_ => tb_runtime::traps::Zustand::Gestoppt,
};
let jetzt = host.jetzt_ms();
self.rt.traps.setzen(q, z, jetzt);
Ok(Flow::Normal)
}
I::Doevents => {
// Wert zuerst ablegen: ein hier zugestellter Handler
// bekommt seinen eigenen Stapelabschnitt darüber und
// lässt den Wert unberührt.
self.stack.push(Value::Int(0));
self.rt.tick(host);
self.zustellen(host);
Ok(Flow::Normal)
}
I::Sleep(mit_arg) => {
let sekunden = if mit_arg { self.pop_f64()? } else { 0.0 };
self.schlafen(host, sekunden);
Ok(Flow::Normal)
}
I::EventSwitch(an) => {
self.rt.traps.event(an);
Ok(Flow::Normal)
}
I::SetErl(n) => {
self.erl = n;
self.zeile_nr = n;
Ok(Flow::Normal)
}
I::End | I::SystemInstr => Ok(Flow::Event(RunEvent::Ended)),
I::StopInstr => Ok(Flow::Event(RunEvent::Stopped { line: self.current_line() })),
I::StopInstr => Ok(Flow::Event(RunEvent::Stopped {
line: self.current_line(),
})),
I::Unsupported(_) => Err(RuntimeError(73)), // Advanced feature
I::PushInt(v) => {
@@ -1086,6 +1275,14 @@ impl Vm {
f.pc = ret;
Ok(Flow::Normal)
}
// Leerer GOSUB-Stapel in einem Trap-Frame: das `RETURN`
// beendet den Handler und führt an der unterbrochenen
// Stelle weiter.
None if f.trap.is_some() => {
let q = self.trap_frame_abraeumen().unwrap();
self.rt.traps.handler_beendet(q);
Ok(Flow::Normal)
}
None => Err(RuntimeError::RETURN_WITHOUT_GOSUB),
}
}
@@ -1096,6 +1293,20 @@ impl Vm {
f.pc = t as usize;
Ok(Flow::Normal)
}
// `RETURN label` aus einem Trap-Handler: der Handler
// endet, die unterbrochene Aufrufkette wird verworfen
// und der Modulrumpf läuft am Label weiter. Das ist die
// klassische — und bewusst rabiate — Bedeutung.
None if f.trap.is_some() => {
let q = self.trap_frame_abraeumen().unwrap();
self.rt.traps.handler_beendet(q);
while self.frames.len() > 1 {
self.pop_frame();
}
let f = self.frames.last_mut().unwrap();
f.pc = t as usize;
Ok(Flow::Normal)
}
None => Err(RuntimeError::RETURN_WITHOUT_GOSUB),
}
}

View File

@@ -476,9 +476,10 @@ fn print_zonen_und_tab() {
#[test]
fn unsupported_feature_fehler_73() {
// Dokumentiert, aber noch offen: `SETUEVENT` kommt mit Phase 4
// Dokumentiert, aber noch offen: `RUN` kommt mit Phase 5
// → Laufzeitfehler 73 mit dem Katalogtext des Vorbilds (VBDOS).
let (ev, _) = run("SETUEVENT");
// (`SETUEVENT` stand hier bis zum Change `phase-4-ereignisschleife`.)
let (ev, _) = run("RUN");
match ev {
RunEvent::Error { code, message, .. } => {
assert_eq!(code, 73);
@@ -510,9 +511,8 @@ fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
/// die Eingabeaufforderung unsichtbar da.
#[test]
fn ausgabe_erscheint_vor_der_eingabe() {
use tb_runtime::host::{CaptureHost, Host};
let module =
tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
use tb_runtime::host::CaptureHost;
let module = tb_vm::compile_source("TEST", "PRINT \"Frage:\"\nINPUT a%\nPRINT a%\n").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::with_input(&["7"]);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
@@ -534,10 +534,10 @@ fn eingabe_wird_geechot_und_rueckschritt_loescht() {
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
for c in ['a', 'b', 'x'] {
host.ereignis(Ereignis::Taste(c.to_string()));
host.ereignis(Ereignis::Taste(c.to_string(), 0));
}
host.ereignis(Ereignis::Taste(taste::BACKSPACE.to_string()));
host.ereignis(Ereignis::Taste(taste::ENTER.to_string()));
host.ereignis(Ereignis::Taste(taste::BACKSPACE.to_string(), 0));
host.ereignis(Ereignis::Taste(taste::ENTER.to_string(), 0));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
let text = tb_runtime::snapshot::text(&vm.rt.screen);
// Erste Zeile: das Echo ohne das gelöschte Zeichen.
@@ -609,8 +609,8 @@ fn inkey_liefert_zeichen_und_sondertaste() {
.unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
host.ereignis(Ereignis::Taste("q".into()));
host.ereignis(Ereignis::Taste(taste::sonder(59))); // F1
host.ereignis(Ereignis::Taste("q".into(), 0));
host.ereignis(Ereignis::Taste(taste::sonder(59), 0)); // F1
assert_eq!(vm.run(&mut host), RunEvent::Ended);
let t = tb_runtime::snapshot::text(&vm.rt.screen);
// "q", dann Länge 2 und führendes Nullzeichen.
@@ -624,7 +624,7 @@ fn input_s_liest_genau_n_zeichen_ohne_echo() {
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
for c in ['x', 'y', 'z', 'w'] {
host.ereignis(Ereignis::Taste(c.to_string()));
host.ereignis(Ereignis::Taste(c.to_string(), 0));
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
// Nur das PRINT erscheint — INPUT$ echot nicht.
@@ -774,3 +774,322 @@ fn rueckfall_auf_utc_laeuft_fehlerfrei() {
);
assert_eq!(t.trim(), "10 8 -1 -1", "{t:?}");
}
// ---- Ereignis-Traps (Change `phase-4-ereignisschleife`) ---------------------
/// Programm mit virtueller Uhr laufen lassen: die Uhr rückt an jedem
/// Zustellpunkt um `schritt_ms` vor (design.md, D6).
fn run_mit_uhr(src: &str, schritt_ms: u64) -> (RunEvent, String) {
struct UhrHost {
inner: CaptureHost,
schritt: u64,
}
impl tb_runtime::host::Host for UhrHost {
fn present(&mut self, s: &tb_runtime::screen::TextScreen) {
self.inner.present(s);
}
fn next_event(&mut self, b: bool) -> Option<tb_runtime::host::Ereignis> {
self.inner.next_event(b)
}
fn jetzt_ms(&mut self) -> u64 {
let j = self.inner.jetzt_ms();
self.inner.uhr_vorruecken(self.schritt);
j
}
}
let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| {
panic!("Compile-Fehler: {d:?}");
});
let mut vm = Vm::new(module);
let mut host = UhrHost {
inner: CaptureHost::default(),
schritt: schritt_ms,
};
let ev = vm.run(&mut host);
(ev, tb_runtime::snapshot::text(&vm.rt.screen))
}
#[test]
fn zeit_trap_laeuft_und_kehrt_zurueck() {
// Die Schleife druckt x, der Trap dazwischen T. Nach RETURN zaehlt die
// Schleife unveraendert weiter — der unterbrochene Zustand bleibt.
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
FOR i% = 1 TO 200\n\
NEXT\n\
PRINT \"i=\"; i%\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(out.contains('T'), "Zeit-Trap lief nicht: {out:?}");
assert!(
out.contains("i= 201"),
"Schleife lief nicht sauber weiter: {out:?}"
);
}
#[test]
fn ohne_timer_on_kein_trap() {
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
FOR i% = 1 TO 200\n\
NEXT\n\
PRINT \"fertig\"\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(!out.contains('T'), "Trap lief ohne TIMER ON: {out:?}");
assert!(out.contains("fertig"));
}
#[test]
fn trap_aus_einer_sub_heraus_kehrt_dorthin_zurueck() {
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
CALL Rechne\n\
PRINT \"ende\"\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n\
SUB Rechne\n\
FOR j% = 1 TO 200\n\
NEXT\n\
PRINT \"sub\"; j%\n\
END SUB\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(out.contains('T'), "Trap lief nicht: {out:?}");
assert!(
out.contains("sub 201"),
"SUB lief nicht sauber weiter: {out:?}"
);
assert!(out.contains("ende"));
}
#[test]
fn event_off_klammert_die_zustellung() {
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
EVENT OFF\n\
FOR i% = 1 TO 200\n\
NEXT\n\
PRINT \"|\";\n\
EVENT ON\n\
FOR i% = 1 TO 200\n\
NEXT\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
let text = out.trim();
let strich = text.find('|').expect("Trennzeichen fehlt");
assert!(
!text[..strich].contains('T'),
"waehrend EVENT OFF wurde zugestellt: {text:?}"
);
assert!(
text[strich..].contains('T'),
"nach EVENT ON wurde nicht zugestellt: {text:?}"
);
}
#[test]
fn timer_off_im_handler_ueberlebt_das_return() {
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
FOR i% = 1 TO 400\n\
NEXT\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
TIMER OFF\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert_eq!(
out.trim().matches('T').count(),
1,
"RETURN hat den Trap wieder eingeschaltet: {out:?}"
);
}
#[test]
fn gosub_null_schaltet_den_trap_ab() {
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
ON TIMER(1) GOSUB 0\n\
FOR i% = 1 TO 300\n\
NEXT\n\
PRINT \"fertig\"\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(!out.contains('T'), "abgeschalteter Trap lief: {out:?}");
}
#[test]
fn setuevent_loest_den_uevent_trap_aus() {
let (ev, out) = run("ON UEVENT GOSUB Nutzer\n\
UEVENT ON\n\
CALL SetUEvent\n\
FOR i% = 1 TO 20\n\
NEXT\n\
PRINT \"|\";\n\
END\n\
Nutzer:\n\
PRINT \"U\";\n\
RETURN\n");
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(out.contains('U'), "UEVENT-Trap lief nicht: {out:?}");
}
#[test]
fn kennung_ausserhalb_des_bereichs_ist_fehler_5() {
// Nicht konstant, also erst zur Laufzeit pruefbar.
let src = "n% = 99\nON KEY(n%) GOSUB M\nEND\nM:\nRETURN\n";
assert_eq!(err_code(src), 5);
}
#[test]
fn leere_warteschleife_hat_eine_anweisungsgrenze() {
// `FOR i = 1 TO n: NEXT` ist die klassische Warteschleife. Ohne
// Anweisungsgrenze im Kreis würde dort nie ein Ereignis zugestellt,
// kein Breakpoint erreicht und kein Abbruch bemerkt.
use tb_vm::bytecode::Instr;
for src in [
"FOR i% = 1 TO 3\nNEXT\n",
"WHILE i% < 3\nWEND\n",
"DO WHILE i% < 3\nLOOP\n",
"DO\nLOOP UNTIL i% > 3\n",
] {
let m = tb_vm::compile_source("T", src).unwrap();
let code = &m.procs[0].code;
let mut kreise = 0;
for (j, i) in code.iter().enumerate() {
let ziel = match i {
Instr::Jump(t) | Instr::JumpIfFalse(t) | Instr::JumpIfTrue(t) => *t as usize,
_ => continue,
};
if ziel >= j {
continue;
}
kreise += 1;
assert!(
code[ziel..=j].iter().any(|i| matches!(i, Instr::Stmt(_))),
"Schleife ohne Anweisungsgrenze im Kreis: {src:?}"
);
}
assert!(kreise > 0, "kein Rücksprung gefunden: {src:?}");
}
}
#[test]
fn doevents_stellt_zu() {
// Die Warteschleife ruft DOEVENTS; der Trap muss darin laufen.
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
FOR i% = 1 TO 5\n\
x% = DOEVENTS\n\
NEXT\n\
PRINT \"|\";\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
let t = out.trim();
let strich = t.find('|').expect("Trennzeichen fehlt");
assert!(
t[..strich].contains('T'),
"DOEVENTS stellte nicht zu: {t:?}"
);
}
#[test]
fn doevents_liefert_null() {
assert_eq!(out("PRINT DOEVENTS").trim(), "0");
}
#[test]
fn sleep_endet_durch_ein_ereignis() {
// SLEEP 60 wuerde ohne Zustellung eine Minute warten. Der faellige
// Trap beendet es; danach laeuft das Programm hinter dem SLEEP weiter.
let t = std::time::Instant::now();
let (ev, out) = run_mit_uhr(
"ON TIMER(1) GOSUB Tick\n\
TIMER ON\n\
SLEEP 60\n\
PRINT \"danach\"\n\
END\n\
Tick:\n\
PRINT \"T\";\n\
RETURN\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(out.contains('T'), "Trap lief nicht: {out:?}");
assert!(
out.contains("danach"),
"SLEEP kehrte nicht zurueck: {out:?}"
);
assert!(
t.elapsed().as_secs() < 5,
"SLEEP wartete die vollen 60 Sekunden ab"
);
}
#[test]
fn fehler_im_trap_handler_erreicht_den_modulweiten_handler() {
// `ERL` nennt die zuletzt ausgefuehrte **Zeilennummer** — deshalb ist
// das Programm nummeriert. Der Fehler entsteht in Zeile 90 (im
// Handler), nicht an der unterbrochenen Stelle.
let (ev, out) = run_mit_uhr(
"10 ON ERROR GOTO 200\n 20 ON TIMER(1) GOSUB 90\n 30 TIMER ON\n 40 FOR i% = 1 TO 300\n 50 NEXT\n 60 PRINT \"|\";\n 70 END\n 90 ERROR 7\n 100 RETURN\n 200 PRINT \"E\"; ERR; ERL;\n 210 RESUME NEXT\n",
600,
);
assert_eq!(ev, RunEvent::Ended, "{out}");
assert!(out.contains("E 7"), "Fehlerbehandlung lief nicht: {out:?}");
assert!(
out.contains(" 90"),
"ERL nennt nicht die Zeile im Handler: {out:?}"
);
}
#[test]
fn erl_nennt_die_fehlerzeile_nicht_die_handlerzeile() {
// Regression: der Handler steht selbst auf einer nummerierten Zeile.
// `ERL` muss trotzdem die Zeile des Fehlers melden (Spec
// vm-fehlerbehandlung, „ERR und ERL").
let out = out("10 ON ERROR GOTO 100\n\
20 ERROR 7\n\
30 END\n\
100 PRINT ERL;\n\
110 RESUME NEXT\n");
assert_eq!(out.trim(), "20");
}