Files
TerminalBasic/crates/tb-runtime/src/traps.rs

544 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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,
/// Menüsperre ohne Änderung der expliziten ON/OFF/STOP-Zustände.
suspendiert: 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
}
pub fn suspendieren(&mut self, gesperrt: bool) {
self.suspendiert = gesperrt;
}
pub fn ist_an(&self, q: Quelle) -> bool {
self.traps
.iter()
.any(|t| t.quelle == q && t.zustand == Zustand::An)
}
/// Nächste Hostfrist, sofern der Zeit-Trap überhaupt Zeit benötigt.
pub fn naechste_frist(&self) -> Option<u64> {
(self.event_an
&& !self.suspendiert
&& self.timer_intervall_ms > 0
&& self.traps.iter().any(|t| {
t.quelle == Quelle::Timer && t.ziel.is_some() && t.zustand != Zustand::Aus
}))
.then_some(self.timer_faellig_ms)
}
/// 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 || self.suspendiert {
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 || self.suspendiert {
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());
}
}