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

@@ -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),
}
}