//! 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, 1–25, 30–31. Key(u8), /// `ON TIMER(n)`. Timer, /// `ON UEVENT`. UEvent, /// `ON SIGNAL(n)` — n aus 1–2. 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, 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, /// `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 15–25: 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 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) { 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, 1–86 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 ` 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 { (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 = 15–25). `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 { 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 = (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 { Some(match scancode { 0x3B..=0x44 => scancode - 0x3A, // F1–F10 → 1–10 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()); } }