Ereigniszustellung und Hostgrenzen korrigieren und Change archivieren

This commit is contained in:
2026-09-05 19:15:31 +02:00
parent 57d6386a5a
commit 644a86212f
39 changed files with 2013 additions and 271 deletions

View File

@@ -14,5 +14,5 @@ path = "src/main.rs"
tb-frontend.workspace = true tb-frontend.workspace = true
tb-vm.workspace = true tb-vm.workspace = true
tb-runtime.workspace = true tb-runtime.workspace = true
tb-ui.workspace = true tb-ui = { workspace = true, features = ["terminal"] }
anyhow.workspace = true anyhow.workspace = true

View File

@@ -551,6 +551,20 @@ impl Host for PipeHost {
self.puffer.pop_front() self.puffer.pop_front()
} }
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
if let Some(event) = self.next_event(false) {
return Some(event);
}
if let Some(deadline) = deadline_ms {
std::thread::sleep(std::time::Duration::from_millis(
deadline.saturating_sub(self.jetzt_ms()),
));
None
} else {
self.next_event(true).or(Some(Ereignis::Ende))
}
}
fn jetzt_ms(&mut self) -> u64 { fn jetzt_ms(&mut self) -> u64 {
self.start.elapsed().as_millis() as u64 self.start.elapsed().as_millis() as u64
} }

View File

@@ -265,6 +265,12 @@ impl tb_runtime::host::Host for EreignisHost {
} }
} }
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
if let Some(event) = self.next_event(false) {
return Some(event);
}
self.inner.warten(deadline_ms)
}
fn jetzt_ms(&mut self) -> u64 { fn jetzt_ms(&mut self) -> u64 {
self.inner.jetzt_ms() self.inner.jetzt_ms()
} }
@@ -318,6 +324,12 @@ impl tb_runtime::host::Host for UhrHost {
fn next_event(&mut self, blockierend: bool) -> Option<tb_runtime::host::Ereignis> { fn next_event(&mut self, blockierend: bool) -> Option<tb_runtime::host::Ereignis> {
self.inner.next_event(blockierend) self.inner.next_event(blockierend)
} }
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
if let Some(event) = self.next_event(false) {
return Some(event);
}
self.inner.warten(deadline_ms)
}
fn jetzt_ms(&mut self) -> u64 { fn jetzt_ms(&mut self) -> u64 {
let jetzt = self.inner.jetzt_ms(); let jetzt = self.inner.jetzt_ms();
self.inner.uhr_vorruecken(self.schritt_ms); self.inner.uhr_vorruecken(self.schritt_ms);

View File

@@ -1882,7 +1882,7 @@ impl<'a> P<'a> {
{ {
if matches!( if matches!(
name.as_str(), name.as_str(),
"TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" | "EVENT" "TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" | "SIGNAL" | "EVENT"
) { ) {
let action = match self.k() { let action = match self.k() {
TokenKind::Kw(Kw::On) => Some(EventAction::On), TokenKind::Kw(Kw::On) => Some(EventAction::On),

View File

@@ -28,14 +28,8 @@ pub struct RtState {
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion. /// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
pub isam: crate::isam::Isam, pub isam: crate::isam::Isam,
pub print: PrintState, pub print: PrintState,
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form, je mit /// Hostreihenfolge bis zur Annahme durch Trap, Formular oder Konsole.
/// Umschaltzustand (Bitfeld 1 Umschalt, 2 Strg, 4 Alt). Der Zustand pub eingaben: VecDeque<Eingabe>,
/// wird nur für die benutzerdefinierten Trap-Tasten gebraucht;
/// `INKEY$` und `INPUT` sehen ihn nicht.
pub tasten: VecDeque<(String, u8)>,
/// Mausereignisse für den Forms-Dispatch; sie werden erst an den
/// bestehenden Zustellpunkten von der VM entnommen.
pub maus: VecDeque<crate::host::MausEreignis>,
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet. /// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
pub abbruch: bool, pub abbruch: bool,
/// Eingabeende wurde gemeldet. /// Eingabeende wurde gemeldet.
@@ -66,6 +60,23 @@ pub struct RtState {
pub clipboard: String, pub clipboard: String,
} }
/// Eine noch nicht verbrauchte Eingabe. Unter KEY STOP wird der Trap
/// einmal gemerkt; die Taste bleibt danach unabhängig vom Trap lesbar.
#[derive(Debug)]
pub struct Eingabe {
pub ereignis: Ereignis,
trap_gemerkt: bool,
}
impl From<Ereignis> for Eingabe {
fn from(ereignis: Ereignis) -> Self {
Self {
ereignis,
trap_gemerkt: false,
}
}
}
impl Default for RtState { impl Default for RtState {
fn default() -> Self { fn default() -> Self {
RtState { RtState {
@@ -73,8 +84,7 @@ impl Default for RtState {
dateien: Default::default(), dateien: Default::default(),
isam: Default::default(), isam: Default::default(),
print: PrintState::default(), print: PrintState::default(),
tasten: VecDeque::new(), eingaben: VecDeque::new(),
maus: VecDeque::new(),
abbruch: false, abbruch: false,
ende: false, ende: false,
// Das Vorbild belegt die Makros im Interpreter vor; ein // Das Vorbild belegt die Makros im Interpreter vor; ein
@@ -111,21 +121,25 @@ impl RtState {
let Some(e) = host.next_event(blockierend) else { let Some(e) = host.next_event(blockierend) else {
return; return;
}; };
self.ereignis(e);
if blockierend {
return;
}
}
}
pub fn ereignis(&mut self, e: Ereignis) {
match e { match e {
Ereignis::Taste(t, shift) => self.tasten.push_back((t, shift)), Ereignis::Taste(..) | Ereignis::Maus(_) => self.eingaben.push_back(e.into()),
Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows), Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows),
Ereignis::Abbruch => self.abbruch = true, Ereignis::Abbruch => self.abbruch = true,
Ereignis::Ende => self.ende = true, Ereignis::Ende => self.ende = true,
Ereignis::Signal(n) => { Ereignis::Signal(n) => {
if n == 1 && !self.traps.melden(crate::traps::Quelle::Signal(n)) { let gemeldet = self.traps.melden(crate::traps::Quelle::Signal(n));
if n == 1 && !gemeldet {
self.abbruch = true; self.abbruch = true;
} }
} }
Ereignis::Maus(m) => self.maus.push_back(m),
}
if blockierend {
return;
}
} }
} }
@@ -147,45 +161,80 @@ impl RtState {
if !self.traps.aktiv() { if !self.traps.aktiv() {
return; return;
} }
let jetzt = host.jetzt_ms(); if self.traps.naechste_frist().is_some() {
self.traps.zeit_pruefen(jetzt); self.traps.zeit_pruefen(host.jetzt_ms());
}
self.tasten_traps_pruefen(); self.tasten_traps_pruefen();
} }
/// Getrappte Tasten aus der Schlange nehmen. Ohne Host, damit die VM /// Getrappte Tasten aus der Schlange nehmen. Ohne Host, damit die VM
/// das an jeder Anweisungsgrenze tun kann, ohne die Uhr zu lesen. /// das an jeder Anweisungsgrenze tun kann, ohne die Uhr zu lesen.
pub fn tasten_traps_pruefen(&mut self) { pub fn tasten_traps_pruefen(&mut self) {
// Nur der Kopf der Schlange: die Reihenfolge der Eingabe bleibt while self.taste_trappen(0) {}
// erhalten, und eine getrappte Taste hinter einer ungetrappten }
// wartet, bis diese gelesen ist.
while let Some((s, shift)) = self.tasten.front() { /// Prüft eine wartende Taste genau bis zu ihrer Annahme. ON verbraucht
/// sie, STOP merkt den Trap einmal und belässt sie im Eingabestrom.
pub fn taste_trappen(&mut self, index: usize) -> bool {
if !self.traps.aktiv() {
return false;
}
let Some(eingabe) = self.eingaben.get_mut(index) else {
return false;
};
let Ereignis::Taste(s, shift) = &eingabe.ereignis else {
return false;
};
if eingabe.trap_gemerkt {
return false;
}
let Some(code) = crate::host::taste::scancode_von(s) else { let Some(code) = crate::host::taste::scancode_von(s) else {
break; return false;
}; };
let Some(n) = self.traps.taste_faengt(code, *shift) else { let Some(n) = self.traps.taste_faengt(code, *shift) else {
break; return false;
}; };
self.tasten.pop_front();
self.traps.melden(crate::traps::Quelle::Key(n)); self.traps.melden(crate::traps::Quelle::Key(n));
if self.traps.ist_an(crate::traps::Quelle::Key(n)) {
self.eingaben.remove(index);
true
} else {
eingabe.trap_gemerkt = true;
false
} }
} }
/// Nächster Tastendruck in `INKEY$`-Form; blockiert bei `blockierend`. /// Nur bereits gepufferte Tasten annehmen, ohne weitere Hostereignisse
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> { /// abzuholen. Die VM kann zwischen Abholen und Blockieren zustellen.
loop { pub fn gepufferte_taste(&mut self) -> Option<String> {
if let Some((t, _)) = self.tasten.pop_front() { while let Some(i) = self
.eingaben
.iter()
.position(|e| matches!(e.ereignis, Ereignis::Taste(..)))
{
if self.taste_trappen(i) {
continue;
}
if let Ereignis::Taste(t, _) = self.eingaben.remove(i).unwrap().ereignis {
return Some(t); return Some(t);
} }
if self.ende { }
None
}
/// Nächster Tastendruck; auch während blockierender Eingabe gelten
/// ON/OFF/STOP. Mausereignisse bleiben dem VM-Zustellpunkt vorbehalten.
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> {
let mut abgeholt = false;
loop {
if let Some(t) = self.gepufferte_taste() {
return Some(t);
}
if self.ende || self.abbruch || (!blockierend && abgeholt) {
return None; return None;
} }
self.pump(host, blockierend); self.pump(host, blockierend);
if !blockierend && self.tasten.is_empty() { abgeholt = true;
return None;
}
if blockierend && self.tasten.is_empty() && self.ende {
return None;
}
} }
} }
@@ -2367,12 +2416,24 @@ fn bi_sleep(
a: &mut [Value], a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> { ) -> Result<Option<Value>, RuntimeError> {
let secs = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? }; let secs = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? };
// SLEEP n wartet n Sekunden; SLEEP ohne Argument bis zum Tastendruck. // Der Bytecode nutzt Instr::Sleep für Handlerzustellung. Auch direkte
// Bibliotheksaufrufe dürfen keine eigene reale Uhr verwenden.
let deadline = (secs > 0.0).then(|| host.jetzt_ms().saturating_add((secs * 1000.0) as u64));
st.tick(host); st.tick(host);
if secs > 0.0 { loop {
std::thread::sleep(std::time::Duration::from_secs_f64(secs)); if st.ende || st.abbruch {
} else { break;
st.naechste_taste(host, true); }
if let Some(deadline) = deadline {
if host.jetzt_ms() >= deadline {
break;
}
} else if st.naechste_taste(host, false).is_some() {
break;
}
if let Some(event) = host.warten(deadline) {
st.ereignis(event);
}
} }
Ok(None) Ok(None)
} }
@@ -2777,6 +2838,60 @@ mod tests {
assert!(h.next_event(false).is_none()); assert!(h.next_event(false).is_none());
} }
#[test]
fn signal_stop_merkt_beide_quellen_und_off_verwirft() {
use crate::host::{CaptureHost, Ereignis};
use crate::traps::{Quelle, Zustand};
for n in [1, 2] {
let mut st = RtState::default();
let mut host = CaptureHost::default();
st.traps.definieren(Quelle::Signal(n), Some(22));
st.traps.setzen(Quelle::Signal(n), Zustand::Gestoppt, 0);
for _ in 0..3 {
host.ereignis(Ereignis::Signal(n));
}
st.pump(&mut host, false);
assert!(!st.abbruch);
assert_eq!(st.traps.naechstes(), None);
st.traps.setzen(Quelle::Signal(n), Zustand::An, 0);
assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(n), 22)));
st.traps.handler_beendet(Quelle::Signal(n));
assert_eq!(st.traps.naechstes(), None);
st.traps.setzen(Quelle::Signal(n), Zustand::Gestoppt, 0);
host.ereignis(Ereignis::Signal(n));
st.pump(&mut host, false);
st.traps.setzen(Quelle::Signal(n), Zustand::Aus, 0);
st.traps.setzen(Quelle::Signal(n), Zustand::An, 0);
assert_eq!(st.traps.naechstes(), None);
}
}
#[test]
fn key_stop_merkt_einmal_und_laesst_alle_tasten_auch_nach_on_lesbar() {
use crate::host::{CaptureHost, Ereignis};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut host = CaptureHost::default();
st.traps.definieren(Quelle::Key(15), Some(22));
st.traps.benutzertaste(15, 0, 30);
st.traps.setzen(Quelle::Key(15), Zustand::Gestoppt, 0);
for _ in 0..3 {
host.ereignis(Ereignis::Taste("a".into(), 0));
}
st.pump(&mut host, false);
for i in 0..3 {
assert!(!st.taste_trappen(i));
}
assert_eq!(st.naechste_taste(&mut host, false).as_deref(), Some("a"));
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(15), 22)));
st.traps.handler_beendet(Quelle::Key(15));
for _ in 0..2 {
assert_eq!(st.naechste_taste(&mut host, false).as_deref(), Some("a"));
}
assert_eq!(st.traps.naechstes(), None);
}
#[test] #[test]
fn sigint_ohne_aktiven_trap_bleibt_abbruch() { fn sigint_ohne_aktiven_trap_bleibt_abbruch() {
use crate::host::Ereignis; use crate::host::Ereignis;
@@ -2802,7 +2917,10 @@ mod tests {
})); }));
} }
st.pump(&mut h, false); st.pump(&mut h, false);
assert!(st.tasten.is_empty(), "Maus landet nicht im Tastenpuffer"); assert!(st
.eingaben
.iter()
.all(|e| matches!(e.ereignis, Ereignis::Maus(_))));
} }
#[test] #[test]

View File

@@ -6,8 +6,8 @@
//! oder das Zurücklesen per `SCREEN(z, s)` nicht transportieren //! oder das Zurücklesen per `SCREEN(z, s)` nicht transportieren
//! (Entscheidung 2026-09-03, D2). //! (Entscheidung 2026-09-03, D2).
//! //!
//! Host-Aufrufe dürfen blockieren; kooperative Zustellpunkte bringt erst die //! Der Host besitzt Zeit und Warten; die VM liefert nur an kooperativen
//! Ereignisschleife der Forms-Engine (Phase 4). //! Zustellpunkten aus.
use std::collections::VecDeque; use std::collections::VecDeque;
@@ -165,6 +165,12 @@ pub trait Host {
/// wenn nichts anliegt (Grundlage von `INKEY$`). /// wenn nichts anliegt (Grundlage von `INKEY$`).
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis>; fn next_event(&mut self, blockierend: bool) -> Option<Ereignis>;
/// Wartet auf Eingabe oder eine absolute Deadline in `jetzt_ms`-Zeit.
/// `None` bedeutet: Deadline erreicht. Ohne Deadline wird bis zu einem
/// Ereignis (gegebenenfalls explizitem Eingabeende) gewartet. Virtuelle
/// Hosts steuern den Zeitfortschritt selbst; keine reale Rückfalluhr.
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis>;
/// Monotone Zeit in Millisekunden seit Programmstart. /// Monotone Zeit in Millisekunden seit Programmstart.
/// ///
/// Die Fälligkeit der Zeit-Traps rechnet die Laufzeit daraus aus; sie /// Die Fälligkeit der Zeit-Traps rechnet die Laufzeit daraus aus; sie
@@ -260,6 +266,19 @@ impl Host for CaptureHost {
self.ereignisse.pop_front() self.ereignisse.pop_front()
} }
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
if let Some(event) = self.next_event(false) {
return Some(event);
}
match deadline_ms {
Some(deadline) => {
self.uhr_ms = self.uhr_ms.max(deadline);
None
}
None => Some(Ereignis::Ende),
}
}
fn jetzt_ms(&mut self) -> u64 { fn jetzt_ms(&mut self) -> u64 {
self.uhr_ms self.uhr_ms
} }

View File

@@ -57,6 +57,8 @@ pub struct Traps {
/// `EVENT OFF` klammert einen Abschnitt ohne Ereignisprüfung. Kein /// `EVENT OFF` klammert einen Abschnitt ohne Ereignisprüfung. Kein
/// dritter Zustand — den kennt das Vorbild nicht. /// dritter Zustand — den kennt das Vorbild nicht.
event_an: bool, 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 /// Fälligkeit des Zeit-Traps: Intervall und der Zeitpunkt, an dem er
/// das nächste Mal fällig wird (beides in Millisekunden). /// das nächste Mal fällig wird (beides in Millisekunden).
timer_intervall_ms: u64, timer_intervall_ms: u64,
@@ -178,6 +180,27 @@ impl Traps {
self.aktiv 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)`, /// Benutzerdefinierte Taste erklären (`KEY n, CHR$(flag)+CHR$(scan)`,
/// n = 1525). `flag` ist das Tastaturflagbyte des Vorbilds. /// n = 1525). `flag` ist das Tastaturflagbyte des Vorbilds.
pub fn benutzertaste(&mut self, n: u8, flag: u8, scancode: u8) { pub fn benutzertaste(&mut self, n: u8, flag: u8, scancode: u8) {
@@ -193,7 +216,7 @@ impl Traps {
/// Liefert die Trap-Nummer, nicht nur „ja/nein": der Aufrufer muss /// Liefert die Trap-Nummer, nicht nur „ja/nein": der Aufrufer muss
/// wissen, welche Quelle er melden soll. /// wissen, welche Quelle er melden soll.
pub fn taste_faengt(&self, scancode: u8, shift: u8) -> Option<u8> { pub fn taste_faengt(&self, scancode: u8, shift: u8) -> Option<u8> {
if !self.event_an { if !self.event_an || self.suspendiert {
return None; return None;
} }
let treffer = |k: u8| -> bool { let treffer = |k: u8| -> bool {
@@ -218,7 +241,7 @@ impl Traps {
/// Nächstes zuzustellendes Ereignis: Quelle und Sprungziel. Liefert /// Nächstes zuzustellendes Ereignis: Quelle und Sprungziel. Liefert
/// höchstens eines je Aufruf; die übrigen bleiben anstehend. /// höchstens eines je Aufruf; die übrigen bleiben anstehend.
pub fn naechstes(&mut self) -> Option<(Quelle, u32)> { pub fn naechstes(&mut self) -> Option<(Quelle, u32)> {
if !self.event_an { if !self.event_an || self.suspendiert {
return None; return None;
} }
let mut kandidaten: Vec<usize> = (0..self.traps.len()) let mut kandidaten: Vec<usize> = (0..self.traps.len())

View File

@@ -1,17 +1,21 @@
[package] [package]
name = "tb-ui" name = "tb-ui"
description = "Terminal Basic: Bildschirm-Runtime und Forms-Engine auf Ratatui" description = "Terminal Basic: Forms-Engine mit optionalem Terminal-Backend"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
authors.workspace = true authors.workspace = true
[features]
default = []
terminal = ["dep:ratatui", "dep:crossterm", "dep:signal-hook"]
[dependencies] [dependencies]
tb-runtime.workspace = true tb-runtime.workspace = true
tb-frontend.workspace = true tb-frontend.workspace = true
ratatui.workspace = true ratatui = { workspace = true, optional = true }
crossterm.workspace = true crossterm = { workspace = true, optional = true }
signal-hook.workspace = true signal-hook = { workspace = true, optional = true }
thiserror.workspace = true thiserror.workspace = true
log.workspace = true log.workspace = true

View File

@@ -1777,6 +1777,49 @@ impl FormsModel {
} }
} }
fn timer_enabled(&self, key: ObjectKey) -> bool {
self.instance(key)
.is_ok_and(|obj| obj.description.class == ObjectClass::Timer)
&& self.boolean(key, "ENABLED")
&& self.integer(key, "INTERVAL").unwrap_or(0) > 0
&& self
.root_form(key)
.is_some_and(|form| self.is_visible(form))
}
/// Zeitbedarf für Timer und gedrückt gehaltene Spin-Schaltflächen.
pub fn next_deadline(&self) -> Option<u64> {
if self.menu_is_open() || (!self.has_visible_forms() && self.spin_repeat.is_none()) {
return None;
}
let timers = self
.keys()
.into_iter()
.filter(|key| self.timer_enabled(*key))
.map(|key| {
self.timer_last
.get(&key)
.copied()
.unwrap_or(0)
.saturating_add(self.integer(key, "INTERVAL").unwrap() as u64)
});
let spin = self.spin_repeat.and_then(|(key, _)| {
let interval = self.integer(key, "INTERVAL").unwrap_or(250).max(0) as u64;
(interval > 0).then(|| {
self.spin_last
.map_or(0, |last| last.saturating_add(interval))
})
});
timers.chain(spin).min()
}
pub fn mouse_needs_time(&self, event: MausEreignis) -> bool {
self.active_form.is_some()
&& !self.menu_is_open()
&& event.art == MausArt::Loslassen
&& self.pressed.is_some()
}
pub fn timers(&mut self, now_ms: u64) { pub fn timers(&mut self, now_ms: u64) {
if !self.menu_path.is_empty() { if !self.menu_path.is_empty() {
return; return;
@@ -1784,15 +1827,7 @@ impl FormsModel {
let mut due: Vec<_> = self let mut due: Vec<_> = self
.keys() .keys()
.into_iter() .into_iter()
.filter(|key| { .filter(|key| self.timer_enabled(*key))
self.instance(*key)
.is_ok_and(|obj| obj.description.class == ObjectClass::Timer)
&& self.boolean(*key, "ENABLED")
&& self.integer(*key, "INTERVAL").unwrap_or(0) > 0
&& self
.root_form(*key)
.is_some_and(|form| self.is_visible(form))
})
.collect(); .collect();
due.sort_by_key(|key| { due.sort_by_key(|key| {
self.instance(*key) self.instance(*key)
@@ -2395,13 +2430,16 @@ fn dialog_event(
model: &mut FormsModel, model: &mut FormsModel,
screen: &mut TextScreen, screen: &mut TextScreen,
host: &mut dyn Host, host: &mut dyn Host,
queued: &mut VecDeque<Ereignis>, queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
plain_access_key: bool, plain_access_key: bool,
) -> Option<FormEvent> { ) -> Option<FormEvent> {
loop { loop {
model.render(screen); model.render(screen);
host.present(screen); host.present(screen);
let event = queued.pop_front().or_else(|| host.next_event(true))?; let event = queued
.pop_front()
.map(|e| e.ereignis)
.or_else(|| host.next_event(true))?;
match event { match event {
Ereignis::Taste(key, shift) => { Ereignis::Taste(key, shift) => {
let shift = if plain_access_key && key.chars().count() == 1 { let shift = if plain_access_key && key.chars().count() == 1 {
@@ -2412,14 +2450,19 @@ fn dialog_event(
model.handle_key(&key, shift); model.handle_key(&key, shift);
} }
Ereignis::Maus(event) => { Ereignis::Maus(event) => {
model.handle_mouse_at(event, host.jetzt_ms()); let now = if model.mouse_needs_time(event) {
host.jetzt_ms()
} else {
0
};
model.handle_mouse_at(event, now);
} }
Ereignis::Groesse { cols, rows } => { Ereignis::Groesse { cols, rows } => {
screen.resize(cols, rows); screen.resize(cols, rows);
model.resize(cols, rows); model.resize(cols, rows);
} }
event @ (Ereignis::Abbruch | Ereignis::Ende | Ereignis::Signal(_)) => { event @ (Ereignis::Abbruch | Ereignis::Ende | Ereignis::Signal(_)) => {
queued.push_front(event); queued.push_front(event.into());
return None; return None;
} }
} }
@@ -2435,7 +2478,7 @@ fn dialog_event(
pub fn msgbox_dialog( pub fn msgbox_dialog(
screen: &mut TextScreen, screen: &mut TextScreen,
host: &mut dyn Host, host: &mut dyn Host,
queued: &mut VecDeque<Ereignis>, queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
text: &str, text: &str,
kind: i32, kind: i32,
title: &str, title: &str,
@@ -2549,7 +2592,7 @@ pub fn msgbox_dialog(
pub fn inputbox_dialog( pub fn inputbox_dialog(
screen: &mut TextScreen, screen: &mut TextScreen,
host: &mut dyn Host, host: &mut dyn Host,
queued: &mut VecDeque<Ereignis>, queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
prompt: &str, prompt: &str,
title: &str, title: &str,
initial: &str, initial: &str,

View File

@@ -86,7 +86,33 @@ impl Host for TerminalHost {
self.start.elapsed().as_millis() as u64 self.start.elapsed().as_millis() as u64
} }
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
loop {
if let Some(e) = self.next_event(false) {
return Some(e);
}
let timeout = match deadline_ms {
Some(deadline) => {
let rest = deadline.saturating_sub(self.jetzt_ms());
if rest == 0 {
return None;
}
Duration::from_millis(rest.min(10))
}
None => Duration::from_millis(10),
};
// Begrenzt warten, damit auch SIGINT/SIGTERM ohne Terminaltaste
// abgeholt werden. Die VM kennt dieses Backend-Intervall nicht.
if event::poll(timeout).is_err() {
return Some(Ereignis::Ende);
}
}
}
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> { fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
if blockierend {
return self.warten(None);
}
loop { loop {
// Signale vor dem Terminal: sie liegen schon an, wenn wir hier // Signale vor dem Terminal: sie liegen schon an, wenn wir hier
// ankommen, und ein blockierendes `read` wuerde sie liegenlassen. // ankommen, und ein blockierendes `read` wuerde sie liegenlassen.
@@ -181,7 +207,7 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
KeyCode::Enter => taste::ENTER.to_string(), KeyCode::Enter => taste::ENTER.to_string(),
KeyCode::Backspace => taste::BACKSPACE.to_string(), KeyCode::Backspace => taste::BACKSPACE.to_string(),
KeyCode::Esc => taste::ESC.to_string(), KeyCode::Esc => taste::ESC.to_string(),
KeyCode::Tab => taste::TAB.to_string(), KeyCode::Tab | KeyCode::BackTab => taste::TAB.to_string(),
// Sondertasten in der Kodierung des Vorbilds: Nullzeichen + Kennung. // Sondertasten in der Kodierung des Vorbilds: Nullzeichen + Kennung.
KeyCode::F(n @ 1..=10) => taste::sonder(58 + n), KeyCode::F(n @ 1..=10) => taste::sonder(58 + n),
KeyCode::Home => taste::sonder(71), KeyCode::Home => taste::sonder(71),
@@ -197,7 +223,7 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
_ => return None, _ => return None,
}; };
let mut shift = 0u8; let mut shift = 0u8;
if k.modifiers.contains(KeyModifiers::SHIFT) { if k.modifiers.contains(KeyModifiers::SHIFT) || k.code == KeyCode::BackTab {
shift |= umschalt::SHIFT; shift |= umschalt::SHIFT;
} }
if k.modifiers.contains(KeyModifiers::CONTROL) { if k.modifiers.contains(KeyModifiers::CONTROL) {
@@ -213,6 +239,16 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn backtab_ist_tab_mit_shift_auch_ohne_modifier() {
for modifiers in [KeyModifiers::NONE, KeyModifiers::SHIFT, KeyModifiers::ALT] {
assert_eq!(
taste_zu_ereignis(KeyEvent::new(KeyCode::BackTab, modifiers)),
taste_zu_ereignis(KeyEvent::new(KeyCode::Tab, modifiers | KeyModifiers::SHIFT))
);
}
}
#[test] #[test]
fn zeichentaste_wird_zu_inkey_form() { fn zeichentaste_wird_zu_inkey_form() {
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE); let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);

View File

@@ -1,17 +1,16 @@
//! Bildschirm-Runtime und Forms-Engine von Terminal Basic, aufbauend auf Ratatui. //! Terminalunabhängiges Forms-Modell, FRM-Reader und Dialoge.
//! //!
//! Zwei Schichten: //! Die Ausführung verwendet `tb_runtime::host::Host` und dessen Zellenpuffer.
//! 1. `screen`: Emulation des 80×25-Textbildschirms (Unicode/UTF-8, 16 Farben, //! Das optionale Feature `terminal` ergänzt Anzeige über Ratatui,
//! Cursor) als Zeichenpuffer, gerendert über Ratatui. Darauf setzen die //! crossterm-Eingaben und Betriebssystemsignale.
//! klassischen Anweisungen `PRINT`, `LOCATE`, `COLOR`, `CLS`, `INPUT` auf.
//! 2. `forms`: ereignisgesteuerte Forms-Engine — Fenster, Steuerelemente
//! (Schaltflächen, Textfelder, Listen …), Menüs, Fokusreihenfolge,
//! Tastatur- und Mausereignisse.
pub mod events; pub mod events;
pub mod forms; pub mod forms;
pub mod frm; pub mod frm;
mod frm_pcode; mod frm_pcode;
#[cfg(feature = "terminal")]
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
#[cfg(feature = "terminal")]
pub mod screen; pub mod screen;
#[cfg(feature = "terminal")]
pub mod signale; // Betriebssystemsignale als Ereignisquelle (SIGNAL) pub mod signale; // Betriebssystemsignale als Ereignisquelle (SIGNAL)

View File

@@ -78,11 +78,27 @@ struct Frame {
waiting_form: Option<u16>, waiting_form: Option<u16>,
pending_show: Option<u16>, pending_show: Option<u16>,
form_event: Option<FormEventReturn>, form_event: Option<FormEventReturn>,
handler_start: bool,
eingabe: Option<KonsolenEingabe>,
}
struct KonsolenEingabe {
text: String,
/// None = Zeile bis Enter; Some = noch erwartete Tasten für INPUT$.
rest: Option<usize>,
}
#[derive(Clone, Copy, PartialEq)]
enum Zustellpunkt {
Anweisung,
Kooperativ,
Konsole,
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum FormEventReturn { enum FormEventReturn {
Normal, Normal,
Load,
Unload(u16), Unload(u16),
} }
@@ -225,6 +241,8 @@ impl Vm {
waiting_form: None, waiting_form: None,
pending_show: None, pending_show: None,
form_event: None, form_event: None,
handler_start: false,
eingabe: None,
}); });
} }
@@ -262,36 +280,15 @@ impl Vm {
} }
} }
fn take_dialog_input(&mut self) -> VecDeque<Ereignis> { fn take_dialog_input(&mut self) -> VecDeque<tb_runtime::builtins::Eingabe> {
let mut events = VecDeque::new(); std::mem::take(&mut self.rt.eingaben)
events.extend(
self.rt
.tasten
.drain(..)
.map(|(key, shift)| Ereignis::Taste(key, shift)),
);
events.extend(self.rt.maus.drain(..).map(Ereignis::Maus));
events
} }
fn restore_dialog_input(&mut self, events: VecDeque<Ereignis>) { fn restore_dialog_input(&mut self, events: VecDeque<tb_runtime::builtins::Eingabe>) {
for event in events { for input in events {
match event { match input.ereignis {
Ereignis::Taste(key, shift) => self.rt.tasten.push_back((key, shift)), Ereignis::Taste(..) | Ereignis::Maus(_) => self.rt.eingaben.push_back(input),
Ereignis::Maus(event) => self.rt.maus.push_back(event), event => self.rt.ereignis(event),
Ereignis::Abbruch => self.rt.abbruch = true,
Ereignis::Ende => self.rt.ende = true,
Ereignis::Signal(number) => {
if number == 1
&& !self
.rt
.traps
.melden(tb_runtime::traps::Quelle::Signal(number))
{
self.rt.abbruch = true;
}
}
Ereignis::Groesse { cols, rows } => self.rt.screen.resize(cols, rows),
} }
} }
} }
@@ -563,13 +560,47 @@ impl Vm {
); );
if let Some(frame) = self.frames.last_mut() { if let Some(frame) = self.frames.last_mut() {
frame.form_event = Some(on_return); frame.form_event = Some(on_return);
frame.handler_start = true;
} }
true true
} }
fn dispatch_next_form_event(&mut self) -> bool { fn dispatch_next_form_event(&mut self, load_only: bool) -> bool {
while let Some(event) = self.forms.next_event() { let mut i = 0;
if self.dispatch_form_event(event, FormEventReturn::Normal) { while let Some(event) = self.forms.events.get(i) {
if load_only && !event.name.eq_ignore_ascii_case("LOAD") {
i += 1;
continue;
}
let laeuft = self
.module
.event_procs
.iter()
.find(|binding| {
binding.object == event.object
&& binding.event.eq_ignore_ascii_case(&event.name)
})
.is_some_and(|binding| {
self.frames.iter().any(|frame| {
frame.form_event.is_some() && frame.proc == binding.proc as usize
})
});
// Gesperrte Timer und laufende Prozeduren bleiben anstehend.
// Menübedienung und explizite modale Kooperation müssen trotzdem
// andere Handler erreichen können.
if laeuft || (self.forms.menu_is_open() && event.name.eq_ignore_ascii_case("TIMER")) {
i += 1;
continue;
}
let event = self.forms.events.remove(i).unwrap();
if self.dispatch_form_event(
event,
if load_only {
FormEventReturn::Load
} else {
FormEventReturn::Normal
},
) {
return true; return true;
} }
} }
@@ -577,6 +608,12 @@ impl Vm {
} }
fn request_unload(&mut self, object: u16) -> Result<(), RuntimeError> { fn request_unload(&mut self, object: u16) -> Result<(), RuntimeError> {
if self.frames.iter().any(|frame| {
matches!(frame.form_event,
Some(FormEventReturn::Unload(running)) if running == object)
}) {
return Ok(());
}
let event = FormEvent { let event = FormEvent {
object, object,
array_index: None, array_index: None,
@@ -590,80 +627,136 @@ impl Vm {
} }
fn tick(&mut self, host: &mut dyn Host) { fn tick(&mut self, host: &mut dyn Host) {
self.rt.traps.suspendieren(self.forms.menu_is_open());
self.forms.render(&mut self.rt.screen); self.forms.render(&mut self.rt.screen);
if self.forms.menu_is_open() {
host.present(&self.rt.screen); host.present(&self.rt.screen);
self.rt.pump(host, false); self.rt.pump(host, false);
} else { if !self.forms.menu_is_open() && self.rt.traps.naechste_frist().is_some() {
self.rt.tick(host); self.rt.traps.zeit_pruefen(host.jetzt_ms());
} }
} }
fn forms_zustellen(&mut self, host: &mut dyn Host) -> bool { /// Ein gemeinsamer Zustellpfad für Statements, DOEVENTS, SLEEP und
/// blockierende Eingabe. Nur explizite Kooperation verschachtelt Forms.
fn zustellen(&mut self, host: &mut dyn Host, punkt: Zustellpunkt) -> bool {
let kooperativ = punkt != Zustellpunkt::Anweisung;
'zustellung: loop {
self.rt.traps.suspendieren(self.forms.menu_is_open());
self.forms self.forms
.resize(self.rt.screen.cols(), self.rt.screen.rows()); .resize(self.rt.screen.cols(), self.rt.screen.rows());
if self.forms.next_deadline().is_some() {
self.forms.timers(host.jetzt_ms()); self.forms.timers(host.jetzt_ms());
if self.forms.active_form().is_some() {
while let Some((key, shift)) = self.rt.tasten.pop_front() {
self.forms.handle_key(&key, shift);
} }
let forms_frei = kooperativ || !self.frames.iter().any(|f| f.form_event.is_some());
if forms_frei && self.dispatch_next_form_event(false) {
return true;
} }
let now_ms = host.jetzt_ms(); if self.rt.traps.aktiv() {
while let Some(m) = self.rt.maus.pop_front() {
self.forms.handle_mouse_at(m, now_ms);
}
self.forms.render(&mut self.rt.screen);
self.dispatch_next_form_event()
}
/// 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.forms.menu_is_open() || !self.rt.traps.aktiv() {
return false;
}
let jetzt = host.jetzt_ms();
self.rt.traps.zeit_pruefen(jetzt);
self.rt.tasten_traps_pruefen(); self.rt.tasten_traps_pruefen();
if (kooperativ || self.tick_zaehler.is_multiple_of(64))
&& self.rt.traps.naechste_frist().is_some()
{
self.rt.traps.zeit_pruefen(host.jetzt_ms());
}
if let Some((q, ziel)) = self.rt.traps.naechstes() { if let Some((q, ziel)) = self.rt.traps.naechstes() {
self.trap_frame_aufsetzen(q, ziel); self.trap_frame_aufsetzen(q, ziel);
return true; return true;
} }
false }
// Unverbrauchte Konsolentasten bleiben liegen. Maus ohne Formular
// wird verworfen; mit Formular wird jede Eingabe samt ihrer Handler
// abgearbeitet, bevor die nächste Eingabe angenommen wird.
let mut i = 0;
while i < self.rt.eingaben.len() {
if !self.forms.menu_is_open() && self.rt.taste_trappen(i) {
if let Some((q, ziel)) = self.rt.traps.naechstes() {
self.trap_frame_aufsetzen(q, ziel);
return true;
}
continue;
}
if punkt == Zustellpunkt::Konsole
&& matches!(self.rt.eingaben[i].ereignis, Ereignis::Taste(..))
{
i += 1;
continue;
}
if self.forms.active_form().is_some() {
if !forms_frei {
break;
}
match self.rt.eingaben.remove(i).unwrap().ereignis {
Ereignis::Taste(key, shift) => {
let menu_war_offen = self.forms.menu_is_open();
self.forms.handle_key(&key, shift);
self.rt.traps.suspendieren(self.forms.menu_is_open());
if menu_war_offen && !self.forms.menu_is_open() {
continue 'zustellung;
}
}
Ereignis::Maus(m) => {
if m.zeile > 0
&& m.zeile <= self.rt.screen.rows()
&& m.spalte > 0
&& m.spalte <= self.rt.screen.cols()
{
let now = if self.forms.mouse_needs_time(m) {
host.jetzt_ms()
} else {
0
};
self.forms.handle_mouse_at(m, now);
}
}
_ => unreachable!(),
}
if self.dispatch_next_form_event(false) {
return true;
}
} else if matches!(self.rt.eingaben[i].ereignis, Ereignis::Maus(_)) {
self.rt.eingaben.remove(i);
} else {
i += 1;
}
}
self.forms.render(&mut self.rt.screen);
return false;
}
} }
/// `SLEEP [n]`: wartet `n` Sekunden, ohne Argument bis zum nächsten fn warten(&mut self, host: &mut dyn Host, deadline: Option<u64>) {
/// Tastendruck. Ein zugestelltes Ereignis beendet das Warten let deadline = deadline
/// (Sprachreferenz §8) — der Handler läuft, und danach geht es hinter .into_iter()
/// dem `SLEEP` weiter. .chain(self.forms.next_deadline())
.chain(if self.forms.menu_is_open() {
None
} else {
self.rt.traps.naechste_frist()
})
.min();
if let Some(event) = host.warten(deadline) {
self.rt.ereignis(event);
}
}
/// Ein zugestelltes Ereignis beendet SLEEP; Fristen und Warten gehören
/// ausschließlich dem Host, einschließlich einer virtuellen Uhr.
fn schlafen(&mut self, host: &mut dyn Host, sekunden: f64) { fn schlafen(&mut self, host: &mut dyn Host, sekunden: f64) {
let start = host.jetzt_ms(); let deadline =
let dauer_ms = (sekunden.max(0.0) * 1000.0) as u64; (sekunden > 0.0).then(|| host.jetzt_ms().saturating_add((sekunden * 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 { loop {
self.tick(host); self.tick(host);
if self.zustellen(host) { if self.zustellen(host, Zustellpunkt::Kooperativ) || self.rt.ende || self.rt.abbruch {
return; return;
} }
if self.rt.ende || self.rt.abbruch { if let Some(deadline) = deadline {
if host.jetzt_ms() >= deadline {
return; return;
} }
if sekunden <= 0.0 { } else if self.rt.naechste_taste(host, false).is_some() {
// Ohne Argument: bis zum Tastendruck.
if self.rt.naechste_taste(host, false).is_some() {
return; return;
} }
} else { self.warten(host, deadline);
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));
} }
} }
@@ -686,6 +779,8 @@ impl Vm {
waiting_form: None, waiting_form: None,
pending_show: None, pending_show: None,
form_event: None, form_event: None,
handler_start: true,
eingabe: None,
}); });
} }
@@ -699,7 +794,11 @@ impl Vm {
fn pop_frame(&mut self) { fn pop_frame(&mut self) {
let f = self.frames.pop().expect("Frame-Unterlauf"); let f = self.frames.pop().expect("Frame-Unterlauf");
if let Some(q) = f.trap {
self.rt.traps.handler_beendet(q);
} else {
self.locals.truncate(f.locals_base); self.locals.truncate(f.locals_base);
}
self.stack.truncate(f.stack_base); self.stack.truncate(f.stack_base);
} }
@@ -806,14 +905,14 @@ impl Vm {
pub fn run_visible_forms(&mut self, host: &mut dyn Host) -> RunEvent { pub fn run_visible_forms(&mut self, host: &mut dyn Host) -> RunEvent {
while self.forms.has_visible_forms() { while self.forms.has_visible_forms() {
self.tick(host); self.tick(host);
let dispatched = self.forms_zustellen(host); let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
if dispatched { if dispatched {
match self.run(host) { match self.run(host) {
RunEvent::Ended => {} RunEvent::Ended => {}
event => return event, event => return event,
} }
} }
if self.rt.ende { if !dispatched && self.rt.ende {
return RunEvent::Ended; return RunEvent::Ended;
} }
if self.rt.abbruch { if self.rt.abbruch {
@@ -821,7 +920,9 @@ impl Vm {
line: self.current_line(), line: self.current_line(),
}; };
} }
std::thread::sleep(std::time::Duration::from_millis(1)); if !dispatched && self.forms.has_visible_forms() {
self.warten(host, None);
}
} }
RunEvent::Ended RunEvent::Ended
} }
@@ -833,11 +934,18 @@ impl Vm {
self.frames.last_mut().unwrap().waiting_form = None; self.frames.last_mut().unwrap().waiting_form = None;
} else { } else {
self.tick(host); self.tick(host);
self.forms_zustellen(host); let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
if self.rt.ende { if !dispatched && self.rt.ende {
return RunEvent::Ended; return RunEvent::Ended;
} }
std::thread::yield_now(); if self.rt.abbruch {
return RunEvent::Interrupted {
line: self.current_line(),
};
}
if !dispatched {
self.warten(host, None);
}
continue; continue;
} }
} }
@@ -898,6 +1006,7 @@ impl Vm {
(f.stack_base, f.last_stmt_pc, f.proc) (f.stack_base, f.last_stmt_pc, f.proc)
}; };
self.stack.truncate(stack_base); self.stack.truncate(stack_base);
self.frames.last_mut().unwrap().eingabe = None;
self.err = code; self.err = code;
match handler { match handler {
Handler::Goto(pc) => { Handler::Goto(pc) => {
@@ -1131,27 +1240,11 @@ impl Vm {
self.tick(host); self.tick(host);
self.rt.screen.veraenderung_quittieren(); self.rt.screen.veraenderung_quittieren();
} }
if self.forms_zustellen(host) { let erster_eintritt =
std::mem::take(&mut self.frames.last_mut().unwrap().handler_start);
if !erster_eintritt && self.zustellen(host, Zustellpunkt::Anweisung) {
return Ok(Flow::Normal); return Ok(Flow::Normal);
} }
// 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.is_multiple_of(64) {
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 != 0 {
if self.flags & F_STEP != 0 { if self.flags & F_STEP != 0 {
return Ok(Flow::Event(RunEvent::Stepped { line })); return Ok(Flow::Event(RunEvent::Stepped { line }));
@@ -1193,7 +1286,7 @@ impl Vm {
1 => tb_runtime::traps::Zustand::Aus, 1 => tb_runtime::traps::Zustand::Aus,
_ => tb_runtime::traps::Zustand::Gestoppt, _ => tb_runtime::traps::Zustand::Gestoppt,
}; };
let jetzt = host.jetzt_ms(); let jetzt = if art == ART_TIMER { host.jetzt_ms() } else { 0 };
self.rt.traps.setzen(q, z, jetzt); self.rt.traps.setzen(q, z, jetzt);
Ok(Flow::Normal) Ok(Flow::Normal)
} }
@@ -1203,8 +1296,7 @@ impl Vm {
// lässt den Wert unberührt. // lässt den Wert unberührt.
self.stack.push(Value::Int(0)); self.stack.push(Value::Int(0));
self.tick(host); self.tick(host);
self.forms_zustellen(host); self.zustellen(host, Zustellpunkt::Kooperativ);
self.zustellen(host);
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::Sleep(mit_arg) => { I::Sleep(mit_arg) => {
@@ -1258,7 +1350,7 @@ impl Vm {
}; };
if !self.forms.is_loaded_at(object, index) { if !self.forms.is_loaded_at(object, index) {
self.forms.ensure_loaded_at(object, index)?; self.forms.ensure_loaded_at(object, index)?;
if index.is_none() && self.dispatch_next_form_event() { if index.is_none() && self.dispatch_next_form_event(true) {
let caller = self.frames.len() - 2; let caller = self.frames.len() - 2;
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1); self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
return Ok(Flow::Normal); return Ok(Flow::Normal);
@@ -1280,7 +1372,7 @@ impl Vm {
if index.is_none() { if index.is_none() {
self.push(value.clone()); self.push(value.clone());
} }
if index.is_none() && self.dispatch_next_form_event() { if index.is_none() && self.dispatch_next_form_event(true) {
let caller = self.frames.len() - 2; let caller = self.frames.len() - 2;
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1); self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
return Ok(Flow::Normal); return Ok(Flow::Normal);
@@ -1304,7 +1396,7 @@ impl Vm {
}; };
self.forms.ensure_loaded_at(object, index)?; self.forms.ensure_loaded_at(object, index)?;
self.push(Value::Obj(object, index)); self.push(Value::Obj(object, index));
self.dispatch_next_form_event(); self.dispatch_next_form_event(true);
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::LoadDynamicObjectProperty(property) => { I::LoadDynamicObjectProperty(property) => {
@@ -1434,7 +1526,7 @@ impl Vm {
&& !self.forms.is_loaded(object) && !self.forms.is_loaded(object)
{ {
self.forms.ensure_loaded(object)?; self.forms.ensure_loaded(object)?;
if self.dispatch_next_form_event() { if self.dispatch_next_form_event(true) {
let caller = self.frames.len() - 2; let caller = self.frames.len() - 2;
self.frames[caller].pending_show = Some(object); self.frames[caller].pending_show = Some(object);
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1); self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
@@ -1493,7 +1585,7 @@ impl Vm {
self.forms.object_method_at(object, index, name, args)?; self.forms.object_method_at(object, index, name, args)?;
} }
} }
self.dispatch_next_form_event(); self.dispatch_next_form_event(true);
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::ObjectLoad(object, unload, has_index) => { I::ObjectLoad(object, unload, has_index) => {
@@ -1508,7 +1600,7 @@ impl Vm {
self.request_unload(object)?; self.request_unload(object)?;
} else { } else {
self.forms.ensure_loaded(object)?; self.forms.ensure_loaded(object)?;
self.dispatch_next_form_event(); self.dispatch_next_form_event(true);
} }
Ok(Flow::Normal) Ok(Flow::Normal)
} }
@@ -2058,6 +2150,8 @@ impl Vm {
self.pop_frame(); self.pop_frame();
} }
let f = self.frames.last_mut().unwrap(); let f = self.frames.last_mut().unwrap();
f.eingabe = None;
self.stack.truncate(f.stack_base);
f.pc = t as usize; f.pc = t as usize;
Ok(Flow::Normal) Ok(Flow::Normal)
} }
@@ -2122,8 +2216,10 @@ impl Vm {
if let Some(FormEventReturn::Unload(object)) = event { if let Some(FormEventReturn::Unload(object)) = event {
self.forms.unload_with(object, |c| *c = cancel)?; self.forms.unload_with(object, |c| *c = cancel)?;
} }
if event.is_some() { if matches!(event, Some(FormEventReturn::Normal))
self.dispatch_next_form_event(); && !self.frames.iter().any(|f| f.form_event.is_some())
{
self.dispatch_next_form_event(false);
} }
Ok(Flow::Normal) Ok(Flow::Normal)
} }
@@ -2134,6 +2230,38 @@ impl Vm {
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::CallBuiltin(id, argc) => { I::CallBuiltin(id, argc) => {
if id == ids::RANDOMIZE && argc == 0 {
if self.frames.last().unwrap().eingabe.is_none() {
self.rt
.screen
.print("Random Number Seed (-32768 to 32767)? ");
}
let caller = self.frames.len() - 1;
if let Some(text) = self.konsoleneingabe(host, None)? {
self.rt.saat_setzen(tb_runtime::format::val(&text));
} else {
self.frames[caller].pc = pc;
}
return Ok(Flow::Normal);
}
if id == ids::INPUT_S && argc == 1 {
let n = match self.stack.last() {
Some(Value::Int(n)) => i32::from(*n),
Some(Value::Lng(n)) => *n,
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let caller = self.frames.len() - 1;
if let Some(text) = self.konsoleneingabe(host, Some(n as usize))? {
self.pop()?;
self.push(Value::Str(Rc::from(text)));
} else {
self.frames[caller].pc = pc;
}
return Ok(Flow::Normal);
}
let mut args = Vec::with_capacity(argc as usize); let mut args = Vec::with_capacity(argc as usize);
for _ in 0..argc { for _ in 0..argc {
args.push(self.pop()?); args.push(self.pop()?);
@@ -2223,7 +2351,10 @@ impl Vm {
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::Input(argc, line_mode, prompt, question) => { I::Input(argc, line_mode, prompt, question) => {
self.do_input(argc, line_mode, prompt, question, host)?; let caller = self.frames.len() - 1;
if !self.do_input(argc, line_mode, prompt, question, host)? {
self.frames[caller].pc = pc;
}
Ok(Flow::Normal) Ok(Flow::Normal)
} }
I::InputFile(argc, line_mode) => { I::InputFile(argc, line_mode) => {
@@ -2272,6 +2403,64 @@ impl Vm {
Ok(Flow::Normal) Ok(Flow::Normal)
} }
/// Fortsetzbarer Leser: Text und Restlänge gehören zum unterbrochenen
/// Frame; Referenzen/Argumente bleiben bis zum Abschluss auf dessen Stack.
fn konsoleneingabe(
&mut self,
host: &mut dyn Host,
rest: Option<usize>,
) -> Result<Option<String>, RuntimeError> {
let caller = self.frames.len() - 1;
self.frames[caller]
.eingabe
.get_or_insert_with(|| KonsolenEingabe {
text: String::new(),
rest,
});
loop {
if self.frames[caller].eingabe.as_ref().unwrap().rest == Some(0) {
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
}
self.tick(host);
if self.zustellen(host, Zustellpunkt::Konsole) {
return Ok(None);
}
if let Some(taste) = self.rt.gepufferte_taste() {
let eingabe = self.frames[caller].eingabe.as_mut().unwrap();
if let Some(rest) = &mut eingabe.rest {
eingabe.text.push_str(&taste);
*rest -= 1;
} else {
match taste.as_str() {
tb_runtime::host::taste::ENTER => {
self.rt.screen.print("\n");
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
}
tb_runtime::host::taste::BACKSPACE => {
if eingabe.text.pop().is_some() {
self.rt.screen.rueckschritt();
}
}
_ => {
if taste.chars().count() == 1
&& taste.chars().next().is_some_and(|c| c as u32 >= 0x20)
{
eingabe.text.push_str(&taste);
self.rt.screen.print(&taste);
}
}
}
}
continue;
}
if self.rt.ende || self.rt.abbruch {
self.frames[caller].eingabe = None;
return Err(RuntimeError(62));
}
self.warten(host, None);
}
}
fn do_input( fn do_input(
&mut self, &mut self,
argc: u8, argc: u8,
@@ -2279,38 +2468,41 @@ impl Vm {
prompt: u16, prompt: u16,
question: bool, question: bool,
host: &mut dyn Host, host: &mut dyn Host,
) -> Result<(), RuntimeError> { ) -> Result<bool, RuntimeError> {
// Referenzen in Reihenfolge einsammeln. let start = self
let mut refs = Vec::with_capacity(argc as usize); .stack
for _ in 0..argc { .len()
match self.pop()? { .checked_sub(argc as usize)
Value::Ref(r) => refs.push(r), .ok_or(RuntimeError(51))?;
_ => return Err(RuntimeError::TYPE_MISMATCH), let refs = self.stack[start..]
} .iter()
} .map(|v| match v {
refs.reverse(); Value::Ref(r) => Ok(r.clone()),
_ => Err(RuntimeError::TYPE_MISMATCH),
})
.collect::<Result<Vec<_>, _>>()?;
loop { loop {
if self.frames.last().unwrap().eingabe.is_none() {
if prompt != 0xFFFF { if prompt != 0xFFFF {
let p = self.module.strings[prompt as usize].clone(); self.rt.screen.print(&self.module.strings[prompt as usize]);
self.rt.screen.print(&p);
} }
if question { if question {
self.rt.screen.print("? "); self.rt.screen.print("? ");
} }
let Some(mut line) = self.rt.read_line(host) else { }
return Err(RuntimeError(62)); // Input past end of file let Some(line) = self.konsoleneingabe(host, None)? else {
return Ok(false);
}; };
if line_mode { if line_mode {
let r = refs.first().ok_or(RuntimeError(51))?; let r = refs.first().ok_or(RuntimeError(51))?;
self.write_ref(r, Value::Str(Rc::from(line.as_str())))?; self.write_ref(r, Value::Str(Rc::from(line)))?;
return Ok(()); self.stack.truncate(start);
return Ok(true);
} }
// Felder trennen (Quotes respektieren). // Felder trennen (Quotes respektieren).
let fields = split_input_fields(&line); let fields = split_input_fields(&line);
if fields.len() != refs.len() { if fields.len() != refs.len() {
self.rt.screen.print("Redo from start\n"); self.rt.screen.print("Redo from start\n");
line.clear();
continue; continue;
} }
// Konvertieren nach aktuellem Zieltyp; Fehler → Redo. // Konvertieren nach aktuellem Zieltyp; Fehler → Redo.
@@ -2333,7 +2525,8 @@ impl Vm {
for (r, v) in refs.iter().zip(vals) { for (r, v) in refs.iter().zip(vals) {
self.write_ref(r, v)?; self.write_ref(r, v)?;
} }
return Ok(()); self.stack.truncate(self.stack.len() - argc as usize);
return Ok(true);
} }
} }

View File

@@ -0,0 +1,584 @@
//! Regressionen zum Change ereigniszustellung-und-hostgrenzen.
use tb_frontend::forms::{FormCatalog, ObjectClass};
use tb_runtime::host::{CaptureHost, Ereignis, Host, MausArt, MausEreignis};
use tb_runtime::screen::TextScreen;
use tb_runtime::value::Value;
use tb_vm::interp::{RunEvent, Vm};
#[path = "../../../tests/support/prozessfrist.rs"]
mod prozessfrist;
fn vm(src: &str) -> Vm {
let mut catalog = FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
catalog.add("Text2", ObjectClass::TextBox, Some("Form1"), false);
catalog.add("Timer1", ObjectClass::Timer, Some("Form1"), false);
catalog.add("Menu1", ObjectClass::Menu, Some("Form1"), false);
catalog.add("Item1", ObjectClass::Menu, Some("Menu1"), false);
Vm::new(
tb_vm::compile_source_with_forms("FORM1", src, &catalog)
.unwrap_or_else(|d| panic!("{d:?}")),
)
}
fn stop(vm: &mut Vm, host: &mut dyn Host) {
assert!(matches!(vm.run(host), RunEvent::Stopped { .. }));
}
fn string(vm: &Vm, name: &str) -> String {
match vm.inspect(name) {
Some(Value::Str(s)) => s.to_string(),
other => panic!("{other:?}"),
}
}
fn integer(vm: &Vm, name: &str) -> i16 {
match vm.inspect(name) {
Some(Value::Int(n)) => n,
other => panic!("{other:?}"),
}
}
fn key(s: &str) -> Ereignis {
Ereignis::Taste(s.into(), 0)
}
fn mouse() -> Ereignis {
Ereignis::Maus(MausEreignis {
art: MausArt::Druck,
taste: 1,
shift: 0,
zeile: 1,
spalte: 1,
})
}
#[test]
fn beide_signale_werden_zugestellt_und_beginnen_in_quellreihenfolge() {
let mut vm = vm("ON SIGNAL(1) GOSUB IntHandler\nON SIGNAL(2) GOSUB TermHandler\nSIGNAL(1) ON\nSIGNAL(2) ON\nSTOP\nDOEVENTS\nEND\nIntHandler:\ns$ = s$ + \"1\"\nRETURN\nTermHandler:\ns$ = s$ + \"2\"\nRETURN");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(Ereignis::Signal(2));
host.ereignis(Ereignis::Signal(1));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "12");
for n in [0, 3] {
assert!(
tb_vm::compile_source("TEST", &format!("ON SIGNAL({n}) GOSUB H\nEND\nH:\nRETURN"))
.is_err()
);
let mut bad = vm_for_source(&format!("n% = {n}\nON SIGNAL(n%) GOSUB H\nEND\nH:\nRETURN"));
assert!(matches!(
bad.run(&mut host),
RunEvent::Error { code: 5, .. }
));
}
}
fn vm_for_source(src: &str) -> Vm {
Vm::new(tb_vm::compile_source("TEST", src).unwrap())
}
#[test]
fn key_on_off_stop_gelten_fuer_alle_konsoleneingaben() {
for state in ["ON", "OFF", "STOP"] {
for input in ["s$ = INKEY$", "INPUT s$", "LINE INPUT s$", "s$ = INPUT$(1)"] {
let mut vm = vm_for_source(&format!("ON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(30)\nKEY(15) {state}\nSTOP\n{input}\nKEY(15) ON\nDOEVENTS\nEND\nH:\nn% = n% + 1\nRETURN"));
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("a"));
if state == "ON" {
host.ereignis(key("b"));
}
host.ereignis(key("\r"));
assert_eq!(vm.run(&mut host), RunEvent::Ended, "{state}: {input}");
assert_eq!(
string(&vm, "s"),
if state == "ON" { "b" } else { "a" },
"{state}: {input}"
);
assert_eq!(
integer(&vm, "n"),
if state == "OFF" { 0 } else { 1 },
"{state}: {input}"
);
}
}
}
#[test]
fn taste_maus_und_trap_behalten_die_annahmereihenfolge() {
for (events, expected) in [
(vec![key("a"), mouse()], "aM"),
(vec![mouse(), key("a")], "Ma"),
(
vec![key("a"), mouse(), key("b"), key("t"), mouse()],
"aMbTM",
),
] {
let mut vm = vm("DIM SHARED s$\nON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nForm1.Show\nText1.SetFocus\nSTOP\nn% = DOEVENTS + LEN(s$)\nDOEVENTS\nDOEVENTS\nDOEVENTS\nDOEVENTS\nEND\nH:\ns$ = s$ + \"T\"\nRETURN\nSUB Text1_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + CHR$(KeyAscii)\nEND SUB\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
for event in events {
host.ereignis(event);
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), expected);
assert!(
matches!(vm.inspect("n"), Some(Value::Int(n)) if n > 0),
"DOEVENTS muss vor der Auswertung von LEN zustellen"
);
}
}
#[test]
fn fokus_handler_laufen_geordnet_bis_zur_rueckkehr() {
let mut vm = vm("DIM SHARED s$\nText1.TabIndex = 0\nText2.TabIndex = 1\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_LostFocus()\nSHARED s$\ns$ = s$ + \"L\"\ns$ = s$ + \"l\"\nEND SUB\nSUB Text2_GotFocus()\nSHARED s$\ns$ = s$ + \"G\"\ns$ = s$ + \"g\"\nEND SUB");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("\t"));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "LlGg");
}
#[test]
fn doevents_verhindert_rekursion_derselben_forms_prozedur() {
let mut vm = vm("DIM SHARED s$\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + CHR$(KeyAscii)\nDOEVENTS\ns$ = s$ + \"r\"\nEND SUB");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("a"));
host.ereignis(key("b"));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "arbr");
}
#[test]
fn offenes_menu_sperrt_traps_und_wartende_forms_timer_an_allen_zustellpunkten() {
let mut vm = vm("DIM SHARED n%\nON UEVENT GOSUB H\nUEVENT ON\nMenu1.Caption = \"&Datei\"\nItem1.Caption = \"&Ende\"\nForm1.Show\nSTOP\nCALL SetUEvent\nn% = n% + 1\nDOEVENTS\nSLEEP .001\nLINE INPUT s$\nSTOP\nDOEVENTS\nEND\nH:\nn% = n% + 10\nRETURN\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 100\nEND SUB");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
vm.forms.handle_key("d", 4);
assert!(vm.forms.menu_is_open());
vm.queue_form_event(tb_ui::forms::FormEvent {
object: 3,
array_index: None,
name: "TIMER".into(),
args: vec![],
});
// Erst die blockierende Konsole holt Enter ab, damit das Menü offen bleibt.
struct InputHost(CaptureHost);
impl Host for InputHost {
fn present(&mut self, s: &TextScreen) {
self.0.present(s);
}
fn next_event(&mut self, block: bool) -> Option<Ereignis> {
block.then(|| key("\r"))
}
fn jetzt_ms(&mut self) -> u64 {
self.0.jetzt_ms()
}
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
if deadline.is_none() {
self.next_event(true)
} else {
self.0.warten(deadline)
}
}
}
stop(&mut vm, &mut InputHost(CaptureHost::default()));
assert_eq!(integer(&vm, "n"), 1);
assert!(vm.forms.menu_is_open());
host.ereignis(key("\u{1b}"));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(integer(&vm, "n"), 111);
}
#[test]
fn ohne_zeitbedarf_keine_uhrabfrage_und_keine_mausansammlung() {
struct OhneUhr(CaptureHost);
impl Host for OhneUhr {
fn present(&mut self, s: &TextScreen) {
self.0.present(s);
}
fn next_event(&mut self, b: bool) -> Option<Ereignis> {
self.0.next_event(b)
}
fn jetzt_ms(&mut self) -> u64 {
panic!("unerwartete Uhrabfrage")
}
fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
panic!("unerwartetes Warten")
}
}
for src in ["a% = 1\na% = 2\nEND", "Form1.Show\na% = 1\nDOEVENTS\nEND"] {
let mut vm = vm(src);
let mut host = OhneUhr(CaptureHost::default());
for _ in 0..500 {
host.0.ereignis(mouse());
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(vm.rt.eingaben.is_empty());
}
}
#[test]
fn sleep_folgt_nur_der_hostfrist_und_forms_timer_beendet_es() {
let _frist = prozessfrist::Prozessfrist::neu("Host-SLEEP", std::time::Duration::from_secs(5));
struct WarteHost {
now: u64,
deadlines: Vec<Option<u64>>,
timer: bool,
}
impl Host for WarteHost {
fn present(&mut self, _: &TextScreen) {}
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
None
}
fn jetzt_ms(&mut self) -> u64 {
self.now
}
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
self.deadlines.push(deadline);
if self.deadlines.len() == 1 && !self.timer {
// Reale Zeit verstreicht bei eingefrorener Hostuhr. Erst die
// folgende Host-Wartephase darf die Deadline erreichen.
std::thread::sleep(std::time::Duration::from_millis(10));
return Some(Ereignis::Groesse { cols: 80, rows: 25 });
}
self.now = deadline.expect("Deadline fehlt");
None
}
}
let mut host = WarteHost {
now: 0,
deadlines: vec![],
timer: false,
};
let mut plain = vm_for_source("SLEEP .001\nEND");
assert_eq!(plain.run(&mut host), RunEvent::Ended);
assert_eq!(host.deadlines, [Some(1), Some(1)]);
let mut vm = vm("DIM SHARED n%\nForm1.Show\nTimer1.Interval = 100\nTimer1.Enabled = -1\nSLEEP 60\nEND\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 1\nEND SUB");
let mut host = WarteHost {
now: 0,
deadlines: vec![],
timer: true,
};
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(host.deadlines, [Some(100)]);
assert_eq!(integer(&vm, "n"), 1);
}
#[test]
fn dialog_erhaelt_stop_markierung_unverbrauchter_tasten() {
let mut vm = vm_for_source("ON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(30)\nKEY(15) STOP\nSTOP\nx% = MSGBOX(\"Weiter\")\nKEY(15) ON\nDOEVENTS\ns$ = INKEY$\nEND\nH:\nn% = n% + 1\nRETURN");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("\r"));
host.ereignis(key("a"));
vm.rt.pump(&mut host, false);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "a");
assert_eq!(integer(&vm, "n"), 1);
}
#[test]
fn unload_aus_unload_handler_ruft_dieselbe_prozedur_nicht_rekursiv_auf() {
let mut vm = vm("DIM SHARED s$\nForm1.Show\nUNLOAD Form1\nEND\nSUB Form_Unload(Cancel AS INTEGER)\nSHARED s$\ns$ = s$ + \"u\"\nUNLOAD Form1\ns$ = s$ + \"r\"\nEND SUB");
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "ur");
assert!(!vm.forms.is_loaded(0));
}
// Nachverifikation V1V5: unveränderte Sollwerte aus den Review-Proben.
#[test]
fn review_signal_waehrend_blockierender_eingabe() {
struct Input {
reads: usize,
}
impl Host for Input {
fn present(&mut self, _: &TextScreen) {}
fn next_event(&mut self, block: bool) -> Option<Ereignis> {
if !block {
return None;
}
self.reads += 1;
Some(if self.reads == 1 {
Ereignis::Signal(2)
} else {
Ereignis::Ende
})
}
fn jetzt_ms(&mut self) -> u64 {
0
}
fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
self.next_event(true)
}
}
let mut results = vec![];
for statement in ["INPUT s$", "LINE INPUT s$", "s$ = INPUT$(1)"] {
let src =
format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\n{statement}\nEND\nH:\nPRINT \"H\"\nEND");
let mut vm = Vm::new(tb_vm::compile_source("TEST", &src).unwrap());
let result = vm.run(&mut Input { reads: 0 });
println!(
"{statement}: {result:?}; Ausgabe={:?}",
tb_runtime::snapshot::text(&vm.rt.screen)
);
results.push(result);
}
assert!(results.iter().all(|result| *result == RunEvent::Ended));
}
#[test]
fn review_objektargument_darf_gotfocus_nicht_vorziehen() {
for action in [
"CALL Mark(Text1)",
"n% = Text1.TabIndex\nCALL Mark(Text1)",
"Text1.Text = \"neu\"\nCALL Mark(Text1)",
"Text1.Refresh\nCALL Mark(Text1)",
"LOAD Form1\nCALL Mark(Text1)",
] {
let mut vm = vm(&format!("DIM SHARED s$\nText1.TabIndex = 0\nText2.TabIndex = 1\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_LostFocus()\nSHARED s$\n{action}\ns$ = s$ + \"l\"\nEND SUB\nSUB Mark(c AS CONTROL)\nSHARED s$\ns$ = s$ + \"L\"\nEND SUB\nSUB Text2_GotFocus()\nSHARED s$\ns$ = s$ + \"G\"\ns$ = s$ + \"g\"\nEND SUB"));
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("\t"));
assert_eq!(vm.run(&mut host), RunEvent::Ended, "{action}");
assert_eq!(string(&vm, "s"), "LlGg", "{action}");
}
}
#[test]
fn review_gleichzeitig_key_und_uevent_behalten_prioritaet() {
let mut vm = vm("ON KEY(15) GOSUB K\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nON UEVENT GOSUB U\nUEVENT ON\nSTOP\nDOEVENTS\nEND\nK:\ns$ = s$ + \"K\"\nRETURN\nU:\ns$ = s$ + \"U\"\nRETURN");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("t"));
vm.rt.pump(&mut host, false);
vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "KU");
}
#[test]
fn review_modale_eingabe_vor_eof_wird_zugestellt() {
let mut vm = vm("DIM SHARED s$\nForm1.Show 1\nEND\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = \"M\"\nUNLOAD Form1\nEND SUB");
let mut host = CaptureHost::default();
host.ereignis_nach(2, mouse());
host.ereignis_nach(2, Ereignis::Ende);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "M");
}
#[test]
fn eof_leert_angenommene_eingaben_bei_modalen_und_modellosen_forms() {
for modal in [false, true] {
let show = if modal { "Form1.Show 1" } else { "Form1.Show" };
let mut vm = vm(&format!("DIM SHARED s$\n{show}\nEND\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB"));
let mut host = CaptureHost::default();
if !modal {
assert_eq!(vm.run(&mut host), RunEvent::Ended);
}
host.ereignis_nach(2, mouse());
host.ereignis_nach(2, mouse());
host.ereignis_nach(2, Ereignis::Ende);
let result = if modal {
vm.run(&mut host)
} else {
vm.run_visible_forms(&mut host)
};
assert_eq!(result, RunEvent::Ended, "modal={modal}");
assert_eq!(string(&vm, "s"), "MM", "modal={modal}");
}
}
#[test]
fn review_menu_schliessen_stellt_pending_trap_vor_erneutem_warten_zu() {
struct MenuHost {
time: u64,
waits: usize,
}
impl Host for MenuHost {
fn present(&mut self, _: &TextScreen) {}
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
None
}
fn jetzt_ms(&mut self) -> u64 {
self.time
}
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
self.waits += 1;
if self.waits == 1 {
return Some(key("\u{1b}"));
}
self.time = deadline.unwrap();
None
}
}
let mut vm = vm("ON UEVENT GOSUB H\nUEVENT ON\nMenu1.Caption = \"&Datei\"\nItem1.Caption = \"&Ende\"\nForm1.Show\nSTOP\nSLEEP 60\nEND\nH:\ns$ = \"U\"\nRETURN");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
vm.forms.handle_key("d", 4);
vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent);
let mut host = MenuHost { time: 0, waits: 0 };
assert_eq!(vm.run(&mut host), RunEvent::Ended);
println!(
"Menü/SLEEP: Zeit={}, Warteaufrufe={}, Handler={}",
host.time,
host.waits,
string(&vm, "s")
);
assert_eq!(host.waits, 1);
}
#[test]
fn review_maus_vor_getrappter_taste() {
let mut vm = vm("DIM SHARED s$\nON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nForm1.Show\nSTOP\nDOEVENTS\nDOEVENTS\nEND\nH:\ns$ = s$ + \"K\"\nRETURN\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(mouse());
host.ereignis(key("t"));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "MK");
}
/// Ereignisse treffen erst beim Warten ein, auch mitten in einer Eingabe.
struct EingabeHost {
events: std::collections::VecDeque<Ereignis>,
now: u64,
deadlines: Vec<Option<u64>>,
}
impl Host for EingabeHost {
fn present(&mut self, _: &TextScreen) {}
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
None
}
fn jetzt_ms(&mut self) -> u64 {
self.now
}
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
self.deadlines.push(deadline);
if let Some(deadline) = deadline {
self.now = deadline;
None
} else {
Some(self.events.pop_front().unwrap_or(Ereignis::Ende))
}
}
}
fn eingabe_host(events: Vec<Ereignis>) -> EingabeHost {
EingabeHost {
events: events.into(),
now: 0,
deadlines: vec![],
}
}
#[test]
fn input_fortsetzung_erhaelt_teiltext_ausdruck_und_byref_ziel() {
for (statement, expected, enter) in [
("INPUT s$", "ab", true),
("LINE INPUT s$", "ab", true),
("s$ = \"x\" + INPUT$(2) + \"y\"", "xaby", false),
] {
let src = format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nCALL Lesen(s$)\nEND\nH:\nn% = n% + 1\nRETURN\nSUB Lesen(s$)\n{statement}\nEND SUB");
let mut vm = vm_for_source(&src);
let mut events = vec![key("a"), Ereignis::Signal(2), key("b")];
if enter {
events.push(key("\r"));
}
let mut host = eingabe_host(events);
assert_eq!(vm.run(&mut host), RunEvent::Ended, "{statement}");
assert_eq!(string(&vm, "s"), expected);
assert_eq!(integer(&vm, "n"), 1);
assert_eq!(host.deadlines.len(), if enter { 4 } else { 3 });
}
}
#[test]
fn handler_eingabe_und_stop_bewahren_die_aeussere_eingabe() {
let mut vm = vm_for_source(
"ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nINPUT s$\nEND\nH:\nSTOP\nINPUT t$\nRETURN",
);
let mut host = eingabe_host(vec![
key("a"),
Ereignis::Signal(2),
key("h"),
key("\r"),
key("b"),
key("\r"),
]);
stop(&mut vm, &mut host);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "ab");
assert_eq!(string(&vm, "t"), "h");
}
#[test]
fn timer_waehrend_input_wird_vor_erneutem_warten_zugestellt() {
let mut vm = vm_for_source(
"ON TIMER(1) GOSUB H\nTIMER ON\nINPUT s$\nEND\nH:\nn% = n% + 1\nTIMER OFF\nRETURN",
);
let mut host = eingabe_host(vec![key("a"), key("\r")]);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "a");
assert_eq!(integer(&vm, "n"), 1);
assert_eq!(host.deadlines, [Some(1000), None, None]);
}
#[test]
fn input_redo_verliert_weder_referenzen_noch_teiltext_beim_trap() {
let mut vm =
vm_for_source("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nINPUT \"Zahl:\"; n%\nEND\nH:\nRETURN");
let mut host = eingabe_host(vec![
key("x"),
key("\r"),
key("4"),
Ereignis::Signal(2),
key("2"),
key("\r"),
]);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(integer(&vm, "n"), 42);
assert_eq!(
tb_runtime::snapshot::text(&vm.rt.screen)
.matches("Zahl:")
.count(),
2
);
}
#[test]
fn lokaler_error_handler_nach_trap_verwirft_abgebrochene_teileingabe() {
let mut vm = vm_for_source("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nCALL Lesen(s$)\nEND\nH:\nERROR 5\nRETURN\nSUB Lesen(s$)\nON LOCAL ERROR GOTO Fehler\nINPUT s$\nEXIT SUB\nFehler:\nINPUT s$\nRESUME NEXT\nEND SUB");
let mut host = eingabe_host(vec![key("a"), Ereignis::Signal(2), key("b"), key("\r")]);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "b");
}
#[test]
fn randomize_eingabe_teilt_den_fortsetzbaren_konsolenpfad() {
let mut vm = vm_for_source(
"ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\nRANDOMIZE\nEND\nH:\nn% = n% + 1\nRETURN",
);
let mut host = eingabe_host(vec![key("4"), Ereignis::Signal(2), key("2"), key("\r")]);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(integer(&vm, "n"), 1);
assert_eq!(
tb_runtime::snapshot::text(&vm.rt.screen)
.matches("Random Number Seed")
.count(),
1
);
}
#[test]
fn return_label_verwirft_abgebrochene_eingabe_und_ausdruck() {
for statement in ["INPUT s$", "s$ = \"alt\" + INPUT$(2)"] {
let mut vm = vm_for_source(&format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\n{statement}\nEND\nWeiter:\nINPUT t$\nEND\nH:\nRETURN Weiter"));
let mut host = eingabe_host(vec![key("a"), Ereignis::Signal(2), key("b"), key("\r")]);
assert_eq!(vm.run(&mut host), RunEvent::Ended, "{statement}");
assert_eq!(string(&vm, "s"), "");
assert_eq!(string(&vm, "t"), "b");
}
}

View File

@@ -859,6 +859,12 @@ fn run_mit_uhr(src: &str, schritt_ms: u64) -> (RunEvent, String) {
fn next_event(&mut self, b: bool) -> Option<tb_runtime::host::Ereignis> { fn next_event(&mut self, b: bool) -> Option<tb_runtime::host::Ereignis> {
self.inner.next_event(b) self.inner.next_event(b)
} }
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
if let Some(event) = self.next_event(false) {
return Some(event);
}
self.inner.warten(deadline_ms)
}
fn jetzt_ms(&mut self) -> u64 { fn jetzt_ms(&mut self) -> u64 {
let j = self.inner.jetzt_ms(); let j = self.inner.jetzt_ms();
self.inner.uhr_vorruecken(self.schritt); self.inner.uhr_vorruecken(self.schritt);

View File

@@ -4,6 +4,18 @@ Geprüfter Code: `19804e0e2de9eb5a8b0ef256b5be76c741560e07` in TerminalBasic. De
Die Implementierung deckt breite Grundpfade ab, erfüllt den gesamten Vertrag aber nicht. **25 Befunde**: 10 mit hoher, 12 mit mittlerer Priorität, 2 Dokumentationsabweichungen und 1 Vertragsklärung. Die Priorität bewertet die Auswirkung, nicht den Aufwand. Befunde können mehrere Requirements betreffen. Die Implementierung deckt breite Grundpfade ab, erfüllt den gesamten Vertrag aber nicht. **25 Befunde**: 10 mit hoher, 12 mit mittlerer Priorität, 2 Dokumentationsabweichungen und 1 Vertragsklärung. Die Priorität bewertet die Auswirkung, nicht den Aufwand. Befunde können mehrere Requirements betreffen.
## Umsetzungsstand: Ereigniszustellung und Hostgrenzen
F04, F05, F06, F07, F08, F18 und F20 sind im archivierten Change
[ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md)
implementiert. Die fünf Restfehler der anschließenden
[Verifikation](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/review.md)
sind behoben: 419 Workspace-Tests und alle sechs ursprünglichen Review-Proben
bestehen. Die konkreten Implementierungs- und aktuellen Abnahmenachweise stehen in
[verification.md](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/verification.md).
Die Befundtexte und die Matrix unten dokumentieren weiterhin den ursprünglichen
Reviewstand; die übrigen Changes bleiben eigenständig.
## Prüfmethode und Grenzen ## Prüfmethode und Grenzen
- Alle Requirements/Szenarien gelesen und gegen Einstiegspunkte, Implementierung und vorhandene Tests abgeglichen. Die Matrix unten enthält jeden Requirement-Titel einmal. - Alle Requirements/Szenarien gelesen und gegen Einstiegspunkte, Implementierung und vorhandene Tests abgeglichen. Die Matrix unten enthält jeden Requirement-Titel einmal.
@@ -46,7 +58,7 @@ Ein eingespeistes Signal(2) bei definiertem und eingeschaltetem Trap ergibt kein
Beleg: [crates/tb-runtime/src/builtins.rs:121](../../crates/tb-runtime/src/builtins.rs#L121). Vertrag: [openspec/specs/ereignissteuerung/spec.md:186](../../openspec/specs/ereignissteuerung/spec.md#L186). Beleg: [crates/tb-runtime/src/builtins.rs:121](../../crates/tb-runtime/src/builtins.rs#L121). Vertrag: [openspec/specs/ereignissteuerung/spec.md:186](../../openspec/specs/ereignissteuerung/spec.md#L186).
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md). Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
### F05 — KEY STOP entfernt die Taste aus dem Eingabestrom (hoch) ### F05 — KEY STOP entfernt die Taste aus dem Eingabestrom (hoch)
@@ -54,7 +66,7 @@ Bei KEY(1) STOP wird F1 zwar für das spätere ON gemerkt, aber aus dem normalen
Beleg: [crates/tb-runtime/src/traps.rs:215](../../crates/tb-runtime/src/traps.rs#L215). Vertrag: [openspec/specs/ereignissteuerung/spec.md:81](../../openspec/specs/ereignissteuerung/spec.md#L81), [openspec/specs/ereignissteuerung/spec.md:143](../../openspec/specs/ereignissteuerung/spec.md#L143). Beleg: [crates/tb-runtime/src/traps.rs:215](../../crates/tb-runtime/src/traps.rs#L215). Vertrag: [openspec/specs/ereignissteuerung/spec.md:81](../../openspec/specs/ereignissteuerung/spec.md#L81), [openspec/specs/ereignissteuerung/spec.md:143](../../openspec/specs/ereignissteuerung/spec.md#L143).
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md). Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
### F06 — Forms-Handler und Eingaben verlieren ihre Reihenfolge (hoch) ### F06 — Forms-Handler und Eingaben verlieren ihre Reihenfolge (hoch)
@@ -62,7 +74,7 @@ Der Fokuswechsel Text1 nach Text2 ergibt in der VM GL statt LG: GotFocus läuft
Beleg: [crates/tb-vm/src/interp.rs:570](../../crates/tb-vm/src/interp.rs#L570). Vertrag: [openspec/specs/ereignissteuerung/spec.md:32](../../openspec/specs/ereignissteuerung/spec.md#L32), [openspec/specs/forms-steuerelemente/spec.md:30](../../openspec/specs/forms-steuerelemente/spec.md#L30), [openspec/specs/forms-objektmodell/spec.md:106](../../openspec/specs/forms-objektmodell/spec.md#L106). Beleg: [crates/tb-vm/src/interp.rs:570](../../crates/tb-vm/src/interp.rs#L570). Vertrag: [openspec/specs/ereignissteuerung/spec.md:32](../../openspec/specs/ereignissteuerung/spec.md#L32), [openspec/specs/forms-steuerelemente/spec.md:30](../../openspec/specs/forms-steuerelemente/spec.md#L30), [openspec/specs/forms-objektmodell/spec.md:106](../../openspec/specs/forms-objektmodell/spec.md#L106).
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md). Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
### F07 — Offenes Menü verhindert Traps nicht an allen Zustellpunkten (hoch) ### F07 — Offenes Menü verhindert Traps nicht an allen Zustellpunkten (hoch)
@@ -70,7 +82,7 @@ Ein UEVENT-Trap wird während eines geöffneten Menüs an einer gewöhnlichen An
Beleg: [crates/tb-vm/src/interp.rs:1139](../../crates/tb-vm/src/interp.rs#L1139). Vertrag: [openspec/specs/forms-steuerelemente/spec.md:102](../../openspec/specs/forms-steuerelemente/spec.md#L102), [openspec/specs/ereignissteuerung/spec.md:59](../../openspec/specs/ereignissteuerung/spec.md#L59). Beleg: [crates/tb-vm/src/interp.rs:1139](../../crates/tb-vm/src/interp.rs#L1139). Vertrag: [openspec/specs/forms-steuerelemente/spec.md:102](../../openspec/specs/forms-steuerelemente/spec.md#L102), [openspec/specs/ereignissteuerung/spec.md:59](../../openspec/specs/ereignissteuerung/spec.md#L59).
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md). Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
### F08 — Zeit- und Wartepfade verletzen die Hostgrenze (mittel) ### F08 — Zeit- und Wartepfade verletzen die Hostgrenze (mittel)
@@ -78,7 +90,7 @@ Drei einfache Anweisungen ohne Trap/Form-Timer lesen die Hostzeit sechsmal. form
Beleg: [crates/tb-vm/src/interp.rs:602](../../crates/tb-vm/src/interp.rs#L602). Vertrag: [openspec/specs/vm-ausfuehrung/spec.md:118](../../openspec/specs/vm-ausfuehrung/spec.md#L118), [openspec/specs/ereignissteuerung/spec.md:15](../../openspec/specs/ereignissteuerung/spec.md#L15), [openspec/specs/ereignissteuerung/spec.md:59](../../openspec/specs/ereignissteuerung/spec.md#L59), [openspec/specs/forms-objektmodell/spec.md:106](../../openspec/specs/forms-objektmodell/spec.md#L106). Beleg: [crates/tb-vm/src/interp.rs:602](../../crates/tb-vm/src/interp.rs#L602). Vertrag: [openspec/specs/vm-ausfuehrung/spec.md:118](../../openspec/specs/vm-ausfuehrung/spec.md#L118), [openspec/specs/ereignissteuerung/spec.md:15](../../openspec/specs/ereignissteuerung/spec.md#L15), [openspec/specs/ereignissteuerung/spec.md:59](../../openspec/specs/ereignissteuerung/spec.md#L59), [openspec/specs/forms-objektmodell/spec.md:106](../../openspec/specs/forms-objektmodell/spec.md#L106).
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md). Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
### F09 — Control-Namen sind faktisch global statt formularlokal (hoch) ### F09 — Control-Namen sind faktisch global statt formularlokal (hoch)
@@ -158,7 +170,7 @@ Statisch belegt: Der Terminal-Mapper behandelt KeyCode::Tab, aber nicht KeyCode:
Beleg: [crates/tb-ui/src/host.rs:179](../../crates/tb-ui/src/host.rs#L179). Vertrag: [openspec/specs/forms-steuerelemente/spec.md:30](../../openspec/specs/forms-steuerelemente/spec.md#L30). Beleg: [crates/tb-ui/src/host.rs:179](../../crates/tb-ui/src/host.rs#L179). Vertrag: [openspec/specs/forms-steuerelemente/spec.md:30](../../openspec/specs/forms-steuerelemente/spec.md#L30).
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md). Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
### F19 — Der Zonenzustand ist für BASIC nicht abfragbar (mittel) ### F19 — Der Zonenzustand ist für BASIC nicht abfragbar (mittel)
@@ -174,7 +186,7 @@ tb-vm hängt unmittelbar von tb-ui ab; dort sind ratatui, crossterm und signal-h
Beleg: [crates/tb-vm/Cargo.toml:15](../../crates/tb-vm/Cargo.toml#L15). Vertrag: [openspec/specs/textbildschirm/spec.md:92](../../openspec/specs/textbildschirm/spec.md#L92). Beleg: [crates/tb-vm/Cargo.toml:15](../../crates/tb-vm/Cargo.toml#L15). Vertrag: [openspec/specs/textbildschirm/spec.md:92](../../openspec/specs/textbildschirm/spec.md#L92).
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md). Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
### F21 — Inventarstatus beweist kein vorhandenes Laufzeitverhalten (mittel) ### F21 — Inventarstatus beweist kein vorhandenes Laufzeitverhalten (mittel)
@@ -221,7 +233,7 @@ Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/chan
| Reihenfolge | Change | Befunde | Abhängigkeit | | Reihenfolge | Change | Befunde | Abhängigkeit |
|---|---|---|---| |---|---|---|---|
| 1 | [isam-transaktionen-und-dateinummern](../../openspec/changes/archive/2026-09-05-isam-transaktionen-und-dateinummern/proposal.md) | F01, F02, F03 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren | | 1 | [isam-transaktionen-und-dateinummern](../../openspec/changes/archive/2026-09-05-isam-transaktionen-und-dateinummern/proposal.md) | F01, F02, F03 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 2 | [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md) | F04, F05, F06, F07, F08, F18, F20 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren | | 2 | [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md) | F04, F05, F06, F07, F08, F18, F20 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 3 | [forms-zustand-und-bedienung](../../openspec/changes/forms-zustand-und-bedienung/proposal.md) | F12, F13 | Nach Ereigniszustellung | | 3 | [forms-zustand-und-bedienung](../../openspec/changes/forms-zustand-und-bedienung/proposal.md) | F12, F13 | Nach Ereigniszustellung |
| 4 | [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md) | F09, F10, F11, F24 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren | | 4 | [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md) | F09, F10, F11, F24 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren | | 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |

View File

@@ -55,15 +55,76 @@ enum Value {
## Ausführungsmodell / Unterbrechbarkeit ## Ausführungsmodell / Unterbrechbarkeit
- Die Interpreterschleife läuft in **Ticks**: nach jeder Anweisung (Grenze Die VM läuft auf dem aufrufenden Thread. `Vm::run` liefert bei STOP,
aus der Zeilentabelle) prüft sie ein Flag-Set: Breakpoint? Einzelschritt? Breakpoint, Einzelschritt, Programmende oder Fehler ein `RunEvent` an den
Strg+Untbr? Ereignis-Queue nicht leer und Zustellung erlaubt? Runner zurück. `Stmt` markiert Anweisungsgrenzen; es gibt keine Prüfung
- Ereigniszustellung (Forms, Timer) erfolgt kooperativ: nur an nach jedem Opcode.
Zustellpunkten (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer
Ereignisprozedur) — wie im Vorbild. Die gemeinsame Zustellentscheidung gilt an Anweisungsgrenzen, bei
- Die VM ist eine gewöhnliche zustandsbehaftete Struktur, `step()`-basiert; `DOEVENTS`, während `SLEEP` und während blockierender Konsoleneingabe. Eine
die einbettende Schleife (IDE-Debugger oder Runner) treibt sie. Kein zugestellte Ereignisprozedur beginnt ihre erste Anweisung, bevor ein
eigener Thread nötig; Terminal-Events werden zwischen Ticks gepollt. weiterer Handler sie unterbrechen darf. Forms-Handler laufen normalerweise
bis zur Rückkehr; explizites DOEVENTS und blockierendes Warten können andere
Handler verschachteln, aber nie dieselbe laufende Forms-Ereignisprozedur.
Die Rückkehr aus dem äußersten normalen Forms-Handler arbeitet wartende
Forms-Ereignisse geordnet ab. Objekt-Opcodes stellen nur erforderliche
synchrone LOAD-Handler zu; deren Rückkehr zieht keine unabhängigen Ereignisse
mitten in eine Anweisung vor. Ein offenes
Menü sperrt klassische Traps und Forms-Timer, ohne ON/OFF/STOP zu verändern.
Beim Menüschließen wird die Zustellbarkeit vor erneutem Warten geprüft.
INPUT, LINE INPUT, INPUT$ und RANDOMIZE ohne Argument teilen einen
fortsetzbaren Konsolenleser. Teiltext und Restlänge liegen im aufrufenden
Frame; Referenzen und Ausdrucksoperanden bleiben bis zum Abschluss auf dem
Stack. Nach jedem Hostereignis kann die VM einen Handler ausführen und
danach am Eingabe-Opcode fortsetzen, auch über STOP/CONT und verschachtelte
Eingaben hinweg. Fehler-Unwinding und RETURN label verwerfen die abgebrochene
Eingabe. Während dieses Lesens gehören ungetrappte Tasten der Konsole.
`RtState::eingaben` hält Tasten und Maus in Hostreihenfolge, auch beim
Übergeben an Dialoge. Die VM nimmt eine Forms-Eingabe samt ihren Handlern
vor der nächsten Eingabe an. Konsolentasten bleiben lesbar; Maus ohne
Formular oder außerhalb der Bildschirmfläche wird verworfen. KEY ON
entnimmt getrappte Tasten. KEY STOP merkt höchstens einen Trap, markiert die
betroffenen Eingaben als bereits gemerkt und lässt sie lesbar, auch nach ON.
Vor der klassischen Quellenauswahl wird der getrappte Tastenanfang der Queue
gemeldet: KEY behält seine Priorität vor TIMER/UEVENT/SIGNAL, ohne eine davor
wartende Mauseingabe zu überholen. EOF beendet die Quelle; modale und
modellose Formularschleifen arbeiten schon angenommene Ereignisse noch ab.
`Host::jetzt_ms` ist die einzige Uhr für Ereignisfristen. Die VM fragt sie
nur für aktive Zeitquellen, SLEEP-Fristen und zeitabhängige Mausauswertung
ab. Der klassische TIMER wird weiterhin bei Anzeigen, expliziter
Kooperation und ansonsten alle 64 Anweisungsgrenzen geprüft.
`Host::warten(Option<u64>)` wartet auf ein Ereignis oder die absolute
Deadline derselben Uhr; `None` als Ergebnis meldet eine erreichte Deadline.
Ohne Deadline liefert der Host erst ein Ereignis oder explizites
`Ereignis::Ende`. Die VM verwendet weder Instant noch Sleep/Yield als
Rückfallebene. Auch modale und nach Programmende sichtbare Forms verwenden
diesen Wartepfad. CaptureHost liefert vorbereitete Ereignisse zuerst und
rückt beim Warten andernfalls bis zur Deadline vor; ohne Deadline meldet
er Eingabeende. Seine Uhr bleibt außerhalb dieser ausdrücklichen
Warteoperation oder manueller Zeitänderungen stehen.
`tb-ui` hat standardmäßig keine Terminalabhängigkeiten. Das Feature
`terminal` schaltet die Module `host`, `screen`, `signale` und die optionalen
Dependencies crossterm, ratatui und signal-hook ein. `tb-cli` aktiviert es
explizit; VM, Forms-Modell, FRM-Reader und Dialoge brauchen es nicht. Einbetter
müssen die neue Host-Wartemethode implementieren. Dialog-Eingabequeues tragen
`Eingabe` statt nackter `Ereignis`-Werte, damit KEY-STOP-Markierungen erhalten
bleiben; Ereignisse lassen sich mit `.into()` einreihen.
Der separate Workspace `tests/terminalfrei` prüft Einbettung ohne
Feature-Unifikation mit CLI oder IDE:
```sh
cargo run --manifest-path tests/terminalfrei/Cargo.toml --offline
cargo tree --manifest-path tests/terminalfrei/Cargo.toml --edges normal
```
Der erste Befehl lädt ein FRM-Korpusprogramm und prüft dessen `KM`-Ausgabe
mit CaptureHost. Der zweite Baum darf crossterm, ratatui und signal-hook
nicht enthalten.
## `.tbc`-Container (Stand Phase 2, Formatversion 1) ## `.tbc`-Container (Stand Phase 2, Formatversion 1)

View File

@@ -1,6 +1,6 @@
## Why ## Why
Ereignisse gehen verloren, laufen in falscher Reihenfolge oder umgehen Menüsperren. F04F08, F18 und F20 zeigen außerdem Zeitabfragen ohne Bedarf und eine unerwünschte Terminalabhängigkeit der VM. Siehe [Review vom 05.09.2026](../../../docs/reviews/2026-09-05-openspec-code-review.md). Ereignisse gehen verloren, laufen in falscher Reihenfolge oder umgehen Menüsperren. F04F08, F18 und F20 zeigen außerdem Zeitabfragen ohne Bedarf und eine unerwünschte Terminalabhängigkeit der VM. Siehe [Review vom 05.09.2026](../../../../docs/reviews/2026-09-05-openspec-code-review.md).
## What Changes ## What Changes

View File

@@ -0,0 +1,167 @@
//! Ursprüngliche Reproduktionen: fünf behobene Befunde und eine Kontrollprobe.
//! Separater Cargo-Treiber; siehe review.md. Alle sechs Prüfungen müssen bestehen.
use tb_frontend::forms::{FormCatalog, ObjectClass};
use tb_runtime::host::{CaptureHost, Ereignis, Host, MausArt, MausEreignis};
use tb_runtime::screen::TextScreen;
use tb_runtime::value::Value;
use tb_vm::interp::{RunEvent, Vm};
fn vm(src: &str) -> Vm {
let mut catalog = FormCatalog::default();
catalog.add("Form1", ObjectClass::Form, None, false);
catalog.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
catalog.add("Text2", ObjectClass::TextBox, Some("Form1"), false);
catalog.add("Timer1", ObjectClass::Timer, Some("Form1"), false);
catalog.add("Menu1", ObjectClass::Menu, Some("Form1"), false);
catalog.add("Item1", ObjectClass::Menu, Some("Menu1"), false);
Vm::new(
tb_vm::compile_source_with_forms("FORM1", src, &catalog)
.unwrap_or_else(|d| panic!("{d:?}")),
)
}
fn stop(vm: &mut Vm, host: &mut dyn Host) {
assert!(matches!(vm.run(host), RunEvent::Stopped { .. }));
}
fn string(vm: &Vm, name: &str) -> String {
match vm.inspect(name) {
Some(Value::Str(s)) => s.to_string(),
other => panic!("{other:?}"),
}
}
fn key(s: &str) -> Ereignis {
Ereignis::Taste(s.into(), 0)
}
fn mouse() -> Ereignis {
Ereignis::Maus(MausEreignis {
art: MausArt::Druck,
taste: 1,
shift: 0,
zeile: 1,
spalte: 1,
})
}
#[test]
fn review_signal_waehrend_blockierender_eingabe() {
struct Input {
reads: usize,
}
impl Host for Input {
fn present(&mut self, _: &TextScreen) {}
fn next_event(&mut self, block: bool) -> Option<Ereignis> {
if !block {
return None;
}
self.reads += 1;
Some(if self.reads == 1 {
Ereignis::Signal(2)
} else {
Ereignis::Ende
})
}
fn jetzt_ms(&mut self) -> u64 {
0
}
fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
self.next_event(true)
}
}
let mut results = vec![];
for statement in ["INPUT s$", "LINE INPUT s$", "s$ = INPUT$(1)"] {
let src =
format!("ON SIGNAL(2) GOSUB H\nSIGNAL(2) ON\n{statement}\nEND\nH:\nPRINT \"H\"\nEND");
let mut vm = Vm::new(tb_vm::compile_source("TEST", &src).unwrap());
let result = vm.run(&mut Input { reads: 0 });
println!(
"{statement}: {result:?}; Ausgabe={:?}",
tb_runtime::snapshot::text(&vm.rt.screen)
);
results.push(result);
}
assert!(results.iter().all(|result| *result == RunEvent::Ended));
}
#[test]
fn review_objektargument_darf_gotfocus_nicht_vorziehen() {
let mut vm = vm("DIM SHARED s$\nText1.TabIndex = 0\nText2.TabIndex = 1\nForm1.Show\nText1.SetFocus\nSTOP\nDOEVENTS\nEND\nSUB Text1_LostFocus()\nSHARED s$\nCALL Mark(Text1)\ns$ = s$ + \"l\"\nEND SUB\nSUB Mark(c AS CONTROL)\nSHARED s$\ns$ = s$ + \"L\"\nEND SUB\nSUB Text2_GotFocus()\nSHARED s$\ns$ = s$ + \"G\"\ns$ = s$ + \"g\"\nEND SUB");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("\t"));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "LlGg");
}
#[test]
fn review_gleichzeitig_key_und_uevent_behalten_prioritaet() {
let mut vm = vm("ON KEY(15) GOSUB K\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nON UEVENT GOSUB U\nUEVENT ON\nSTOP\nDOEVENTS\nEND\nK:\ns$ = s$ + \"K\"\nRETURN\nU:\ns$ = s$ + \"U\"\nRETURN");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(key("t"));
vm.rt.pump(&mut host, false);
vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "KU");
}
#[test]
fn review_modale_eingabe_vor_eof_wird_zugestellt() {
let mut vm = vm("DIM SHARED s$\nForm1.Show 1\nEND\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = \"M\"\nUNLOAD Form1\nEND SUB");
let mut host = CaptureHost::default();
host.ereignis_nach(2, mouse());
host.ereignis_nach(2, Ereignis::Ende);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "M");
}
#[test]
fn review_menu_schliessen_stellt_pending_trap_vor_erneutem_warten_zu() {
struct MenuHost {
time: u64,
waits: usize,
}
impl Host for MenuHost {
fn present(&mut self, _: &TextScreen) {}
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
None
}
fn jetzt_ms(&mut self) -> u64 {
self.time
}
fn warten(&mut self, deadline: Option<u64>) -> Option<Ereignis> {
self.waits += 1;
if self.waits == 1 {
return Some(key("\u{1b}"));
}
self.time = deadline.unwrap();
None
}
}
let mut vm = vm("ON UEVENT GOSUB H\nUEVENT ON\nMenu1.Caption = \"&Datei\"\nItem1.Caption = \"&Ende\"\nForm1.Show\nSTOP\nSLEEP 60\nEND\nH:\ns$ = \"U\"\nRETURN");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
vm.forms.handle_key("d", 4);
vm.rt.traps.melden(tb_runtime::traps::Quelle::UEvent);
let mut host = MenuHost { time: 0, waits: 0 };
assert_eq!(vm.run(&mut host), RunEvent::Ended);
println!(
"Menü/SLEEP: Zeit={}, Warteaufrufe={}, Handler={}",
host.time,
host.waits,
string(&vm, "s")
);
assert_eq!(host.waits, 1);
}
#[test]
fn review_maus_vor_getrappter_taste() {
let mut vm = vm("DIM SHARED s$\nON KEY(15) GOSUB H\nKEY 15, CHR$(0) + CHR$(20)\nKEY(15) ON\nForm1.Show\nSTOP\nDOEVENTS\nDOEVENTS\nEND\nH:\ns$ = s$ + \"K\"\nRETURN\nSUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\nSHARED s$\ns$ = s$ + \"M\"\nEND SUB");
let mut host = CaptureHost::default();
stop(&mut vm, &mut host);
host.ereignis(mouse());
host.ereignis(key("t"));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(string(&vm, "s"), "MK");
}

View File

@@ -0,0 +1,191 @@
# Verifikation: ereigniszustellung-und-hostgrenzen
Stand nach Behebung: 05.09.2026, aktueller Arbeitsbaum auf `57d6386`.
**Alle fünf Verifikationsbefunde sind behoben.** Die ursprünglichen sechs
Review-Proben bestehen unverändert in ihren Erwartungen; zusätzlich wurden
sie in die Workspace-Regressionen übernommen und um verwandte Pfade ergänzt.
| Dimension | Ergebnis |
| --- | --- |
| Vollständigkeit | 12/12 Tasks erledigt; ursprüngliche Zuordnung der 9 Delta-Requirements und 33 Szenarien um Korrektur-Nachweise ergänzt. |
| Korrektheit | 419 Workspace-Tests und alle sechs separaten Review-Proben bestanden; keine offenen V1V5-Befunde. |
| Kohärenz | Gemeinsame Zustellung auch während Eingabe und nach Menüschluss; synchrone LOAD-Wirkungen getrennt, klassische Quellpriorität erhalten. |
## Korrekturen und Regressionen
| Befund | Behebung | Nachweis in `crates/tb-vm/tests/events.rs` |
| --- | --- | --- |
| V1 | Fortsetzbarer Konsolenleser für INPUT, LINE INPUT, INPUT$ und RANDOMIZE ohne Argument. Frame hält Teiltext/Restlänge, Stack hält Operanden und Referenzen. Trap-Unwinding erhält gemeinsam genutzte Locals; Fehler und RETURN label verwerfen abgebrochene Eingaben. | Ursprüngliche Signalprobe; Teiltext/BYREF/Ausdruck, verschachtelte Eingaben mit STOP/CONT, Timer, Redo, lokaler Fehlerhandler, RANDOMIZE und RETURN label. |
| V2 | Implizite Objektaufrufe stellen ausschließlich notwendige LOAD-Handler zu. Deren Rückkehr zieht keine unabhängigen Forms-Ereignisse vor. | LlGg mit Control-Argument, Property-Lesen/-Schreiben, Refresh und LOAD; bestehende Load-vor-Show- und Unload-Tests. |
| V3 | Nach dem Schließen des Menüs wird die gemeinsame Auswahl vor jedem erneuten Warten wiederholt. | Esc bei Zeit 0, UEVENT zugestellt nach genau einem Warteaufruf; bestehende Menüsperrtests einschließlich Forms-Timer. |
| V4 | Modale und modellose Schleifen beenden bei EOF erst, wenn kein Handler mehr zugestellt wurde. | Ursprüngliche MouseDown/Unload-Probe; jeweils zwei Mausereignisse vor EOF ergeben MM in beiden Schleifen. |
| V5 | Getrappte Tasten am Queue-Anfang werden vor der klassischen Quellenauswahl gemeldet. | Gleichzeitiges KEY/UEVENT ergibt KU; Maus vor getrappter Taste bleibt MK. |
## Ursprüngliche Befunde — historischer Stand vor der Behebung
Die folgenden Ist-Beobachtungen und Ursachen dokumentieren den fehlgeschlagenen
Review vor diesen Korrekturen. Alle fünf sind durch die Nachweise oben geschlossen.
### V1 — Während blockierender Eingabe werden eintreffende Traps nicht zugestellt (hoch)
**Reproduktion:** `ON SIGNAL(2) GOSUB H`, `SIGNAL(2) ON`, danach INPUT.
Der Host liefert erst im blockierenden `next_event(true)` Signal(2), beim
nächsten Aufruf Eingabeende. Der Handler H würde `PRINT "H"` und END ausführen.
**Ist:** INPUT, LINE INPUT und INPUT$(1) enden jeweils mit Fehler 62, H läuft
nicht. Ohne nachfolgende Eingabe kann die Laufzeit stattdessen erneut
blockieren, obwohl der Signal-Trap schon ansteht.
**Soll:** Vor dem erneuten Blockieren muss der erlaubte Zustellpunkt genutzt
werden; H beendet das Programm, Ausgabe H, kein Fehler 62.
**Ursache:** Der einmalige VM-Check vor der Eingabeinstruktion genügt nicht.
[RtState::naechste_taste](../../../../crates/tb-runtime/src/builtins.rs#L209)
pumpt innerhalb seiner Schleife weitere Ereignisse und blockiert erneut,
ohne zur VM-Zustellung zurückzukehren. Dasselbe betrifft die drei genannten
Eingabefunktionen über ihren gemeinsamen Leser.
**Vertrag:** `ereignissteuerung` / Abschließende Liste der Zustellpunkte,
Signal-Trap auf Betriebssystemsignalen; Design D2; Task 2.1.
**Korrektur:** Blockierende Eingabe so fortsetzbar machen, dass nach einem
Nicht-Tasten-Ereignis und vor dem nächsten Host-Warten die gemeinsame
VM-Zustellung laufen kann. Eingabepuffer, Referenzen und Operanden erhalten.
### V2 — Control-Argumente stellen wartende Forms-Handler mitten in einer Anweisung zu (hoch)
**Reproduktion:** Fokuswechsel Text1 → Text2. Text1_LostFocus ruft als erste
wirksame Anweisung `CALL Mark(Text1)` auf; Mark hängt L an eine gemeinsame
Variable, LostFocus danach l. Text2_GotFocus hängt G und g an.
**Ist:** `GgLl`. **Soll:** `LlGg`.
**Ursache:** [Instr::PushObject](../../../../crates/tb-vm/src/interp.rs#L1344)
ruft nach dem Ablegen eines bereits geladenen Control-Objekts bedingungslos
`dispatch_next_form_event` auf. Dadurch läuft GotFocus noch vor dem eigentlichen
CALL, obwohl weder DOEVENTS noch modales Warten vorliegt. Der Schutz am ersten
Stmt-Eintritt und `forms_frei` werden damit umgangen.
**Vertrag:** Fokus, Tabreihenfolge und Access-Keys / Handlerwirkungen in
Fokusreihenfolge; Abschließende Liste der Zustellpunkte; Design D1/D2;
Tasks 1.2 und 2.1.
**Korrektur:** Unabhängig wartende Ereignisse aus Objekt-Opcodes heraus nicht
zustellen. Notwendige synchrone Load-/Unload-Wirkungen gezielt behandeln;
alle Aufrufer von `dispatch_next_form_event` gegen dieselben Zustellregeln prüfen.
### V3 — Menüschließen während SLEEP führt zu erneutem Warten trotz bereitem Trap (hoch)
**Reproduktion:** Menü offen, UEVENT anstehend, `SLEEP 60`. Der erste
Host-Warteaufruf liefert Esc bei unveränderter virtueller Zeit 0.
**Ist:** Esc schließt das Menü; die VM wartet ein zweites Mal bis Hostzeit
60.000 ms. Erst dann läuft der UEVENT-Handler.
**Soll:** Nach Esc läuft der freigegebene Handler ohne zweiten Warteaufruf
und beendet SLEEP sofort. Bei einer Wartephase ohne eigene Deadline droht
entsprechend Warten auf ein weiteres externes Ereignis.
**Ursache:** Die klassische Trap-Auswahl in
[Vm::zustellen](../../../../crates/tb-vm/src/interp.rs#L625) erfolgt vor der
Eingabeverarbeitung. Nachdem Esc die Menüsperre aufgehoben hat, kehrt die
Methode ohne erneute Auswahl zurück;
[SLEEP](../../../../crates/tb-vm/src/interp.rs#L707) ruft unmittelbar wieder
`warten` auf.
**Vertrag:** Menüsystem / Traps ruhen im geöffneten Menü und werden danach
fortgesetzt; Abschließende Liste der Zustellpunkte / SLEEP endet durch
Ereignis; Design D2/D3; Tasks 2.1/2.2.
**Korrektur:** Nach einer Eingabe, die die Zustellbarkeit verändert, bereite
Ereignisse vor jedem weiteren blockierenden Warten erneut auswählen.
### V4 — Modale Schleife verliert bereits angenommenes Ereignis unmittelbar vor EOF (hoch)
**Reproduktion:** `Form1.Show 1`; nach der zweiten Anzeige liefert der Host
MouseDown und unmittelbar danach Eingabeende. Der MouseDown-Handler hängt M
an und entlädt das Formular.
**Ist:** VM meldet Ended, die gemeinsame Variable bleibt leer; der Handler
wurde nicht ausgeführt. **Soll:** Das vor EOF eingetroffene Mausereignis wird
noch wirksam, Ergebnis M, Formular entladen.
**Ursache:** In der [modalen Run-Schleife](../../../../crates/tb-vm/src/interp.rs#L890)
setzt `zustellen` einen Handlerframe auf. Direkt danach wird ungeachtet von
`dispatched` wegen `rt.ende` zurückgekehrt. Das Ereignis ist bereits aus der
Queue entfernt und wird stillschweigend verloren.
**Vertrag:** Mausereignisse in Zellenkoordinaten / Reihenfolge bis zur
beobachtbaren Handlerausführung; Design D1/D2; Tasks 1.2/2.1.
**Korrektur:** EOF als Ende der Eingabequelle behandeln und bereits angenommene
Ereignisse/Handler geordnet abarbeiten, bevor die modale Ausführung endet.
## Ursprüngliche weitere Designabweichung
### V5 — Gleichzeitig wartende klassische Quellen erhalten eine andere Priorität (mittel)
**Reproduktion:** KEY(15) ON und UEVENT ON. Vor demselben Zustellpunkt liegen
eine passende Taste in der Eingabequeue und ein gemeldeter UEVENT vor.
Die Handler hängen K bzw. U an.
**Ist:** `UK`. **Soll:** `KU` gemäß der bestehenden klassischen Quellpriorität.
**Ursache:** [Vm::zustellen](../../../../crates/tb-vm/src/interp.rs#L631) wählt
bereits gemeldete Traps aus, bevor die wartende Taste durch `taste_trappen`
gemeldet wird. Die Prioritätsauswahl in Traps sieht die Taste deshalb noch
nicht. Früher erfolgte die Tastenprüfung vor der gemeinsamen Trap-Auswahl.
**Vertrag:** Design / Non-Goals: keine Änderung der festgelegten Priorität
gleichzeitig fälliger klassischer Quellen; D1/D2; Task 2.1.
**Korrektur:** Fälligkeitserfassung und Trap-Auswahl so koordinieren, dass die
bestehende Quellpriorität erhalten bleibt, ohne die Reihenfolge zwischen
Maus- und Tasteneingaben wieder nach Quellarten umzusortieren.
## Abdeckung und positive Nachweise
| Delta-Requirement | Nachweis / Einschränkung |
| --- | --- |
| Ereignisquellen mit Zeit vom Host | Host-Warteschnittstelle, eingefrorene virtuelle Zeit, klassischer Timer und Forms-Timer-SLEEP getestet. Keine reale Rückfalluhr in der VM; Zustellung nach Menüschluss durch V3-Regression abgesichert. |
| Mausereignisse in Zellenkoordinaten | Mapper-/Bounds-Tests, leere Verbraucher, KM/MK-Korpus und gemischte Eingaben. V4-Regressionen sichern EOF in modalen und modellosen Schleifen ab. |
| Abschließende Liste der Zustellpunkte | DOEVENTS-Wert, SLEEP, vorbereitete und erst beim Warten eintreffende INPUT-Ereignisse getestet; V1V3 geschlossen. |
| Getrappte Tasten verlassen den Eingabestrom | ON/OFF/STOP × INKEY$/INPUT/LINE INPUT/INPUT$, mehrfaches STOP-Merken und Dialogrestpuffer getestet. |
| Signal-Trap auf Betriebssystemsignalen | Beide Kennungen, Maskierung und ungültige Werte getestet. V1-Regression sichert Signalzustellung während blockierender Eingabe ab. |
| Fokus, Tabreihenfolge und Access-Keys | Tab/Shift-Tab, BackTab-Mapper, Access-Keys und einfacher LostFocus→GotFocus-Lauf belegt. V2-Regressionen sichern Control-Argumente und verwandte Objektaufrufe ab. |
| Menüsystem | Menüeigenschaften/-auswahl und Sperre an normalen Grenzen getestet. V3-Regression sichert Freigabe innerhalb von SLEEP ab. |
| Zellenpuffer ohne Terminalabhängigkeit | Separater Workspace kompiliert FRM/VM und liefert KM; 16 Pakete im normalen Dependency-Graph, keines davon crossterm/ratatui/signal-hook. CLI schaltet das Feature ausdrücklich ein. |
| Ereigniszustellung an Anweisungsgrenzen | Bestehende SUB-/GOSUB-/BYREF-/RESUME-Tests und Host ohne erlaubte Uhrabfragen grün. V2/V5-Regressionen sichern zulässige Handlerbeginn-Reihenfolge und klassische Priorität ab. |
Erneut ausgeführt:
- `cargo test --workspace`: **419 bestanden, 0 fehlgeschlagen, 1 ignoriert**.
- `openspec validate ereigniszustellung-und-hostgrenzen --strict`: gültig.
- Isolierter Einbetter mit CaptureHost: **KM**, Dependency-Prüfung erfolgreich.
- Separate [Review-Proben](review-probes.rs): **6 bestanden, 0 fehlgeschlagen**.
Die Kontrollprobe prüft Maus unmittelbar vor getrappter Taste.
- Workspace-Ereignistests: **25 bestanden**, einschließlich Frist-Watchdog.
- Keine bestehenden Sollausgaben neu aufgezeichnet oder geändert.
Warnings-denied Clippy, Formatprüfungen und Compile-/VM-Benchmarks wurden
nach der Behebung erneut erfolgreich ausgeführt; Zahlen siehe
[verification.md](verification.md). Kein interaktiver Terminaltest und keine
Windows-/Linux-Matrix. Die Proben speisen Ereignisse
gezielt über Host ein und benötigen weder echte Signale noch reale Wartefristen.
## Review-Proben reproduzieren
Im Repository-Wurzelverzeichnis ausführen. Das erzeugt ausschließlich einen
separaten temporären Cargo-Treiber und referenziert den aktuellen Produktcode:
```sh
python3 - <<'PY'
from pathlib import Path
import json, subprocess, tempfile
root = Path.cwd()
p = Path(tempfile.mkdtemp(prefix='tb-review-'))
manifest = '[package]\nname="tb-review"\nversion="0.0.0"\nedition="2021"\n[workspace]\n'
manifest += '[[test]]\nname="befunde"\npath=' + json.dumps(str(root / 'openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/review-probes.rs')) + '\n[dependencies]\n'
for name in ['tb-vm', 'tb-runtime', 'tb-ui', 'tb-frontend']:
manifest += name + ' = { path = ' + json.dumps(str(root / 'crates' / name)) + ' }\n'
(p / 'Cargo.toml').write_text(manifest)
subprocess.run(['cargo', 'test', '--manifest-path', str(p / 'Cargo.toml'), '--test', 'befunde', '--', '--nocapture'], check=True, timeout=120)
PY
```
Alle sechs Proben müssen auf dem korrigierten Stand bestehen. Ihre Erwartungen
wurden auch in den regulären Workspace-Testlauf übernommen.

View File

@@ -0,0 +1,25 @@
## 1. Hosteingabe und Maskierung
- [x] 1.1 SIGNAL(2), KEY STOP und BackTab am gemeinsamen Eingang berichtigen; verifizieren durch Tests für beide Signale, alle drei KEY-Zustände mit INKEY$/INPUT und den Terminal-Key-Mapper.
- [x] 1.2 Geordnete Tasten-/Mausannahme implementieren; verifizieren mit K→M, M→K und gemischten Folgen einschließlich ungetrappter und getrappter Tasten.
## 2. Dispatch und Warten
- [x] 2.1 Handlerbeginn/Rückkehr und Menüsperre an allen Zustellpunkten vereinheitlichen; verifizieren mit LostFocus→GotFocus, gleichzeitig wartenden Handlern, UEVENT bei offenem Menü, DOEVENTS und den bestehenden Fehler-/Modalitätstests.
- [x] 2.2 Hostgesteuertes Warten und bedarfsgerechte Zeitabfrage implementieren; verifizieren mit einem Host, der unerwartete Uhrabfragen zählt/abweist, eingefrorener und vorrückender Zeit sowie einem Forms-Timer, der SLEEP beendet.
- [x] 2.3 Terminal-Feature in tb-ui abgrenzen und CLI ausdrücklich anschließen; verifizieren durch isolierten terminalfreien VM-Einbetter, Dependency-Graph ohne crossterm/ratatui/signal-hook und normalen CLI-Build.
## 3. Abnahme
- [x] 3.1 Gezielte geordnete Ereignisprogramme zum Korpus ergänzen; verifizieren mit cargo test --workspace und Vergleich der bestehenden Snapshots ohne automatisches Neuaufzeichnen.
- [x] 3.2 Zeit-/Zustellverträge und Backend-Feature dokumentieren; verifizieren mit Compile-/VM-Benchmarks und openspec validate ereigniszustellung-und-hostgrenzen --strict.
Abnahmenachweise: [verification.md](verification.md).
## 4. Verifikationsbefunde
- [x] 4.1 V1: Blockierende Konsoleneingabe fortsetzbar machen; Signale, Timer, Teiltexte und Handler-Rückkehr absichern.
- [x] 4.2 V2: Synchrone Load-Wirkungen von wartenden Forms-Ereignissen trennen; Control-Argumente und Geschwisterpfade prüfen.
- [x] 4.3 V3: Nach Menüschluss vor erneutem Warten bereit gewordene Ereignisse zustellen.
- [x] 4.4 V4: Vor EOF angenommene Ereignisse im modalen Lauf vollständig abarbeiten.
- [x] 4.5 V5: Klassische Quellpriorität bei gleichzeitig wartenden Tasten und UEVENT erhalten.

View File

@@ -0,0 +1,82 @@
# Implementierungsnachweis — 05.09.2026
Change: `ereigniszustellung-und-hostgrenzen`, Ausgangscommit `57d6386`.
Alle zwölf Implementierungs-/Abnahmetasks einschließlich der fünf
[Verifikationsbefunde](review.md) sind abgeschlossen. Die vier betroffenen
Hauptspezifikationen sind synchronisiert; der Change wurde am 05.09.2026 archiviert.
## Befunde und Umsetzung
| Befund / Tasks | Umsetzung | Ausführbarer Nachweis |
| --- | --- | --- |
| F04 / 1.1 | SIGNAL(1) und SIGNAL(2) werden im gemeinsamen RtState-Eingang gemeldet; nur ungetrapptes SIGINT wird Abbruch. Zusätzlich erkennt der Parser SIGNAL(n) ON/OFF/STOP. | VM-Test `beide_signale_werden_zugestellt_und_beginnen_in_quellreihenfolge` einschließlich ungültiger konstanter/dynamischer Kennungen; Runtime-Tests für STOP/OFF beider Quellen und SIGINT ohne Trap. |
| F05 / 1.1 | ON verbraucht die Taste; STOP merkt den Trap einmal und belässt die Eingabe auch nach erneutem ON lesbar. Die Markierung bleibt beim Dialogaufruf erhalten. | VM-Test `key_on_off_stop_gelten_fuer_alle_konsoleneingaben`: drei Zustände × INKEY$, INPUT, LINE INPUT, INPUT$. Runtime-Test mit drei STOP-Tasten; VM-Regression für verbleibende Dialogeingaben. |
| F06 / 1.2, 2.1 | Eine Eingabequeue für Tasten/Maus bis zur Annahme. Forms-Eingaben und ihre Handler werden geordnet verarbeitet; erster Handler-Statement-Eintritt geschützt, gleiche Forms-Prozedur nicht rekursiv zugestellt. | VM-Tests für aM, Ma, aMbTM einschließlich KEY-Trap; Fokusfolge LlGg; explizites DOEVENTS ergibt arbr; UNLOAD im eigenen Handler ergibt ur. |
| F07 / 2.1 | Gemeinsame Zustellung; separate Menüsperre in Traps ohne Änderung der ON/OFF/STOP-Zustände. Bereits wartende Forms-Timer bleiben gesperrt, Menübedienung kann fortfahren. | VM-Menütest über gewöhnliche Zuweisung, DOEVENTS, SLEEP und LINE INPUT: Zähler bleibt 1, nach Esc werden UEVENT und Forms-Timer nachgeholt, Ergebnis 111. |
| F08 / 2.2 | Host besitzt Zeit und Deadline-Warten; keine Instant-/Sleep-/Yield-Rückfallebene in der VM. Auch Forms-SLEEP, modales Warten und sichtbare Forms nach Programmende nutzen den Host. Zeitabfragen nur bei tatsächlichem Zeitbedarf. | Host, der jede unerwartete Uhrabfrage/Warteoperation ablehnt; eingefrorene Uhr über reale 10 ms mit zwei identischen Hostdeadlines; Forms-Timer beendet SLEEP 60 bei Hostzeit 100 ms. Bestehender klassischer SLEEP-/TIMER-Test. |
| F18 / 1.1 | BackTab wird als TAB mit SHIFT normalisiert, auch wenn crossterm kein separates SHIFT-Flag liefert. | Terminal-Mapper-Test `backtab_ist_tab_mit_shift_auch_ohne_modifier`; bestehende Forms-Tests für Rückwärtstab, übersprungene Controls und Access-Keys. |
| F20 / 2.3 | tb-ui ist standardmäßig terminalfrei; `terminal` aktiviert Backendmodule und optionale Dependencies. CLI aktiviert dieses Feature ausdrücklich. | Separater Workspace `tests/terminalfrei`: FRM laden/kompilieren, Forms ausführen, CaptureHost-Ausgabe KM prüfen. Normaler Dependency-Graph: 16 Pakete, keines davon crossterm, ratatui oder signal-hook. CLI-Kompilation und CLI-Prozesstests erfolgreich. |
| 3.1 | Zwei kleine geordnete Ereignisprogramme mit ausdrücklich festgelegten Ausgaben ergänzt. | `eingabe-taste-maus.frm` → KM; `eingabe-maus-taste.frm` → MK. Beide laufen zweimal im bestehenden Korpusharness. Keine bestehende .out-Datei verändert. |
| 3.2 | Zeit-, Zustell-, Eingabequeue- und Backend-Verträge einschließlich Einbetter-Migration dokumentiert. | [VM-Design](../../../../docs/tbvm-design.md#ausführungsmodell--unterbrechbarkeit), Checks und Benchmarks unten. |
Die neuen VM-Regressionen liegen in
[crates/tb-vm/tests/events.rs](../../../../crates/tb-vm/tests/events.rs).
Bestehende Trap-Rückkehr-, GOSUB-, Fehler-/RESUME-, BYREF- und Modalitätstests
wurden im vollständigen Workspace-Testlauf mit ausgeführt.
## Behebung der Verifikationsbefunde
V1V5 sind geschlossen. Der [aktualisierte Report](review.md) ordnet jede
Korrektur den ursprünglichen Beobachtungen und ihren Regressionen zu.
Die 25 Tests in `events.rs` decken nun auch fortsetzbare Eingabe samt Teiltext,
Ausdrucksoperanden, BYREF, verschachtelten Handlern, STOP/CONT, Timer,
Fehler-Unwinding und RETURN label ab. Objektaufrufe bewahren die Fokusfolge,
Menüschluss gibt Traps sofort frei, beide Formularschleifen arbeiten Eingaben
vor EOF ab und die klassische KEY-/UEVENT-Priorität bleibt erhalten.
## Ausgeführte Prüfungen
- `cargo test --workspace`: **419 bestanden, 0 fehlgeschlagen, 1 ignoriert**.
Ignoriert bleibt ausschließlich `erzeuge_sollausgaben`; keine automatische
Neuaufzeichnung von Golden Files.
- Separater Treiber der ursprünglichen `review-probes.rs`: **6 bestanden, 0 fehlgeschlagen**.
- `cargo clippy -p tb-runtime -p tb-ui -p tb-vm -p tb-cli --all-targets --all-features -- -D warnings`: erfolgreich, ohne Warnungen.
- `cargo fmt --all -- --check` und Formatprüfung des separaten Einbetters: erfolgreich.
- `cargo run --manifest-path tests/terminalfrei/Cargo.toml --offline`: erfolgreich,
`Terminalfreier FRM-/VM-Lauf: KM`.
- `cargo tree --manifest-path tests/terminalfrei/Cargo.toml --offline --edges normal --prefix none --format '{p}'`:
mit expliziter Prüfung auf ausgeschlossene Terminalpakete, erfolgreich.
- `openspec validate ereigniszustellung-und-hostgrenzen --strict`: erfolgreich.
- `git diff --check`: erfolgreich.
## Release-Benchmarks
`cargo bench -p tb-vm --bench compile --bench vm`, auf diesem Rechner:
| Last | Ergebnis |
| --- | --- |
| Compile: 508 Zeilen | 0,83 ms, Budget 50 ms eingehalten |
| Compile: 49.760 Zeilen in 20 Modulen | 58 ms, Budget 1.000 ms eingehalten |
| INTEGER: 10 Mio. Iterationen | 1.048 ms, 9,5 Mio. Iterationen/s |
| DOUBLE: 5 Mio. Iterationen | 504 ms, 9,9 Mio. Iterationen/s |
| SUB/BYREF: 1 Mio. Aufrufe | 123 ms, 8,2 Mio. Aufrufe/s |
| String-Funktionen: 200.000 Runden | 99 ms, 2,0 Mio. Runden/s |
Der Compile-Projektbenchmark übersetzt 20 Module einzeln; er prüft keinen
Mehrmodul-Linker. Die VM-Zahlen sind lokale Durchsatzmessungen, keine
zugesicherte Performance auf anderer Hardware.
## Synchronisation und Archivierung
Neun bestehende Requirements in vier Hauptspezifikationen wurden um elf
Szenarien ergänzt; alle bisherigen Szenarien bleiben erhalten. Vollständiger
Abgleich aller Delta-Requirements erfolgreich; `openspec validate --specs
--strict`: 15 bestanden, 0 fehlgeschlagen.
## Grenzen
Terminaleingaben wurden über den Mapper und vorbereitete Hostereignisse
geprüft. Kein interaktiver Terminal-/Fremdprogrammvergleich und keine
Windows-/Linux-Ausführung. Der neue Vertrag verlangt von Einbettern eine
Implementierung von Host::warten; CaptureHost modelliert eine endliche
Ereignisquelle und explizites virtuelles Fortschreiten bis zur Deadline.

View File

@@ -1,15 +0,0 @@
## 1. Hosteingabe und Maskierung
- [ ] 1.1 SIGNAL(2), KEY STOP und BackTab am gemeinsamen Eingang berichtigen; verifizieren durch Tests für beide Signale, alle drei KEY-Zustände mit INKEY$/INPUT und den Terminal-Key-Mapper.
- [ ] 1.2 Geordnete Tasten-/Mausannahme implementieren; verifizieren mit K→M, M→K und gemischten Folgen einschließlich ungetrappter und getrappter Tasten.
## 2. Dispatch und Warten
- [ ] 2.1 Handlerbeginn/Rückkehr und Menüsperre an allen Zustellpunkten vereinheitlichen; verifizieren mit LostFocus→GotFocus, gleichzeitig wartenden Handlern, UEVENT bei offenem Menü, DOEVENTS und den bestehenden Fehler-/Modalitätstests.
- [ ] 2.2 Hostgesteuertes Warten und bedarfsgerechte Zeitabfrage implementieren; verifizieren mit einem Host, der unerwartete Uhrabfragen zählt/abweist, eingefrorener und vorrückender Zeit sowie einem Forms-Timer, der SLEEP beendet.
- [ ] 2.3 Terminal-Feature in tb-ui abgrenzen und CLI ausdrücklich anschließen; verifizieren durch isolierten terminalfreien VM-Einbetter, Dependency-Graph ohne crossterm/ratatui/signal-hook und normalen CLI-Build.
## 3. Abnahme
- [ ] 3.1 Gezielte geordnete Ereignisprogramme zum Korpus ergänzen; verifizieren mit cargo test --workspace und Vergleich der bestehenden Snapshots ohne automatisches Neuaufzeichnen.
- [ ] 3.2 Zeit-/Zustellverträge und Backend-Feature dokumentieren; verifizieren mit Compile-/VM-Benchmarks und openspec validate ereigniszustellung-und-hostgrenzen --strict.

View File

@@ -21,6 +21,8 @@ Systemuhr zugreifen. Ein Host ohne Terminal MUST die Zeit frei stellen
können, sodass zeitgesteuerte Programme ohne Wartezeit und mit können, sodass zeitgesteuerte Programme ohne Wartezeit und mit
reproduzierbarem Ergebnis prüfbar sind. reproduzierbarem Ergebnis prüfbar sind.
Auch Wartefristen der VM SHALL ausschließlich von der Hostzeit abhängen. Ein Host mit stehender virtueller Zeit MUST NOT durch reale verstrichene Zeit eine Frist scheinbar erreichen.
#### Scenario: Zeitgesteuerter Trap ohne echte Wartezeit #### Scenario: Zeitgesteuerter Trap ohne echte Wartezeit
- **WHEN** ein Testhost die Zeit um 5 Sekunden vorstellt und ein Programm `ON TIMER(5) GOSUB Marke` mit `TIMER ON` aktiv hat - **WHEN** ein Testhost die Zeit um 5 Sekunden vorstellt und ein Programm `ON TIMER(5) GOSUB Marke` mit `TIMER ON` aktiv hat
- **THEN** wird der Trap zugestellt, ohne dass der Test tatsächlich wartet - **THEN** wird der Trap zugestellt, ohne dass der Test tatsächlich wartet
@@ -29,6 +31,10 @@ reproduzierbarem Ergebnis prüfbar sind.
- **WHEN** dasselbe zeitgesteuerte Korpusprogramm zweimal mit demselben Zeitverlauf ausgeführt wird - **WHEN** dasselbe zeitgesteuerte Korpusprogramm zweimal mit demselben Zeitverlauf ausgeführt wird
- **THEN** ist die Ausgabe beide Male identisch - **THEN** ist die Ausgabe beide Male identisch
#### Scenario: Stehende virtuelle Zeit
- **WHEN** ein Host während SLEEP seine Zeit unverändert hält
- **THEN** endet SLEEP nicht wegen realer Zeit; eine vom Host gemeldete Frist oder ein zugestelltes Ereignis beendet das Warten
### Requirement: Mausereignisse in Zellenkoordinaten ### Requirement: Mausereignisse in Zellenkoordinaten
Die Darstellungsschicht SHALL Mausereignisse der Ausführungsumgebung an Die Darstellungsschicht SHALL Mausereignisse der Ausführungsumgebung an
die Laufzeit weitergeben: Drücken, Loslassen und Bewegung, jeweils mit die Laufzeit weitergeben: Drücken, Loslassen und Bewegung, jeweils mit
@@ -40,6 +46,8 @@ SHALL der Reihenfolge ihres Eintreffens entsprechen. Solange kein
Verbraucher ein Mausereignis annimmt, MUST es am Zustellpunkt verworfen Verbraucher ein Mausereignis annimmt, MUST es am Zustellpunkt verworfen
werden, damit die Warteschlange nicht unbegrenzt wächst. werden, damit die Warteschlange nicht unbegrenzt wächst.
Die Reihenfolge SHALL bis zur beobachtbaren Ausführung der zugehörigen Handler erhalten bleiben; getrennte Quellarten MUST NOT nachträglich nach ihrer Art umgeordnet werden.
#### Scenario: Position als Zelle #### Scenario: Position als Zelle
- **WHEN** in der linken oberen Ecke der Darstellungsfläche die linke Maustaste gedrückt wird - **WHEN** in der linken oberen Ecke der Darstellungsfläche die linke Maustaste gedrückt wird
- **THEN** trägt das Ereignis Zeile 1 und Spalte 1 - **THEN** trägt das Ereignis Zeile 1 und Spalte 1
@@ -56,6 +64,14 @@ werden, damit die Warteschlange nicht unbegrenzt wächst.
- **WHEN** ein Programm ohne Verbraucher für Mausereignisse läuft und die Maus dauerhaft bewegt wird - **WHEN** ein Programm ohne Verbraucher für Mausereignisse läuft und die Maus dauerhaft bewegt wird
- **THEN** wächst die Warteschlange nicht über ihre Grenze und das Programm läuft unverändert weiter - **THEN** wächst die Warteschlange nicht über ihre Grenze und das Programm läuft unverändert weiter
#### Scenario: Taste vor Maus bis zum Handler
- **WHEN** eine Taste vor einem Mausklick eintrifft und beide Handler eine Kennung anhängen
- **THEN** steht die Tastenkennung vor der Mauskennung
#### Scenario: Maus vor Taste bis zum Handler
- **WHEN** ein Mausklick vor einer Taste eintrifft
- **THEN** wird zuerst der Maushandler wirksam
### Requirement: Abschließende Liste der Zustellpunkte ### Requirement: Abschließende Liste der Zustellpunkte
Ereignisse SHALL ausschließlich an folgenden Punkten zugestellt werden Ereignisse SHALL ausschließlich an folgenden Punkten zugestellt werden
— das Vorbild prüft die Ereignismarke „before executing the next — das Vorbild prüft die Ereignismarke „before executing the next
@@ -66,6 +82,8 @@ Trap-Handler beginnen; insbesondere MUST NOT eine Anweisung in ihrer
Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes
`SLEEP` beenden. `SLEEP` beenden.
Forms-Ereignisse SHALL auch während SLEEP an dessen bestehenden Zustellpunkten verarbeitet werden. Eine für das Menü geltende Zustellsperre SHALL an jedem Zustellpunkt gleichermaßen gelten.
#### Scenario: Keine Zustellung innerhalb einer Anweisung #### Scenario: Keine Zustellung innerhalb einer Anweisung
- **WHEN** ein Ereignis fällig wird, während eine mehrteilige Anweisung ausgewertet wird - **WHEN** ein Ereignis fällig wird, während eine mehrteilige Anweisung ausgewertet wird
- **THEN** läuft die Anweisung zu Ende und der Handler beginnt erst an der folgenden Anweisungsgrenze - **THEN** läuft die Anweisung zu Ende und der Handler beginnt erst an der folgenden Anweisungsgrenze
@@ -78,6 +96,10 @@ Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes
- **WHEN** ein anstehendes Ereignis vorliegt und `DOEVENTS` ausgewertet wird - **WHEN** ein anstehendes Ereignis vorliegt und `DOEVENTS` ausgewertet wird
- **THEN** läuft der zugehörige Handler, bevor `DOEVENTS` einen Wert liefert - **THEN** läuft der zugehörige Handler, bevor `DOEVENTS` einen Wert liefert
#### Scenario: Forms-Timer beendet SLEEP
- **WHEN** während SLEEP ein aktiver Forms-Timer fällig wird
- **THEN** läuft dessen Handler und SLEEP kehrt danach zurück
### Requirement: Maskierung je Quelle mit drei Zuständen ### Requirement: Maskierung je Quelle mit drei Zuständen
Jede Ereignisquelle SHALL drei Zustände kennen. `ON` stellt Ereignisse Jede Ereignisquelle SHALL drei Zustände kennen. `ON` stellt Ereignisse
zu. `OFF` verwirft sie; ein während `OFF` eingetretenes Ereignis MUST zu. `OFF` verwirft sie; ein während `OFF` eingetretenes Ereignis MUST
@@ -146,6 +168,8 @@ zugestellt werden und MUST NOT zusätzlich über `INKEY$`, `INPUT`,
`LINE INPUT` oder `INPUT$` sichtbar werden. Bei `OFF` oder `STOP` der `LINE INPUT` oder `INPUT$` sichtbar werden. Bei `OFF` oder `STOP` der
Quelle SHALL die Taste dem normalen Eingabestrom erhalten bleiben. Quelle SHALL die Taste dem normalen Eingabestrom erhalten bleiben.
Unter STOP SHALL die Taste normal lesbar bleiben und zugleich höchstens ein Ereignis für das spätere ON gemerkt werden.
#### Scenario: Getrappte Taste erscheint nicht bei INKEY$ #### Scenario: Getrappte Taste erscheint nicht bei INKEY$
- **WHEN** `ON KEY(1) GOSUB Marke` mit `KEY(1) ON` gilt und F1 gedrückt wird - **WHEN** `ON KEY(1) GOSUB Marke` mit `KEY(1) ON` gilt und F1 gedrückt wird
- **THEN** läuft der Handler und ein anschließendes `INKEY$` liefert den leeren String - **THEN** läuft der Handler und ein anschließendes `INKEY$` liefert den leeren String
@@ -154,6 +178,10 @@ Quelle SHALL die Taste dem normalen Eingabestrom erhalten bleiben.
- **WHEN** derselbe Trap mit `KEY(1) OFF` gilt und F1 gedrückt wird - **WHEN** derselbe Trap mit `KEY(1) OFF` gilt und F1 gedrückt wird
- **THEN** liefert `INKEY$` die Sondertastenfolge für F1 - **THEN** liefert `INKEY$` die Sondertastenfolge für F1
#### Scenario: STOP merkt und erhält Eingabe
- **WHEN** unter KEY(1) STOP F1 gedrückt, per INKEY$ gelesen und danach KEY(1) ON ausgeführt wird
- **THEN** liefert INKEY$ die Sondertastenfolge und der Handler läuft nach ON genau einmal
### Requirement: Benutzerdefiniertes Ereignis ### Requirement: Benutzerdefiniertes Ereignis
`SetUEvent` SHALL ein benutzerdefiniertes Ereignis auslösen, das über `SetUEvent` SHALL ein benutzerdefiniertes Ereignis auslösen, das über
`ON UEVENT GOSUB` und die Steueranweisung `UEVENT` denselben Regeln für `ON UEVENT GOSUB` und die Steueranweisung `UEVENT` denselben Regeln für
@@ -208,3 +236,7 @@ ausgewiesen sein.
#### Scenario: Signal ohne Trap bleibt Abbruch #### Scenario: Signal ohne Trap bleibt Abbruch
- **WHEN** ein `SIGINT` eintrifft, ohne dass ein `SIGNAL(1)`-Trap aktiv ist - **WHEN** ein `SIGINT` eintrifft, ohne dass ein `SIGNAL(1)`-Trap aktiv ist
- **THEN** bleibt es beim bisherigen Abbruchverhalten - **THEN** bleibt es beim bisherigen Abbruchverhalten
#### Scenario: SIGTERM wird zugestellt
- **WHEN** SIGNAL(2) ON gilt und ein SIGTERM über den Host eintrifft
- **THEN** wird das Ziel von ON SIGNAL(2) GOSUB am nächsten erlaubten Zustellpunkt ausgeführt

View File

@@ -36,6 +36,8 @@ auslöst oder ihm den Fokus gibt. Enter SHALL die `Default`-Schaltfläche
auslösen, Esc die `Cancel`-Schaltfläche. Fokuswechsel MUST `LostFocus` auslösen, Esc die `Cancel`-Schaltfläche. Fokuswechsel MUST `LostFocus`
am alten und `GotFocus` am neuen Element auslösen, in dieser Reihenfolge. am alten und `GotFocus` am neuen Element auslösen, in dieser Reihenfolge.
Die über das Terminal gelieferte Rückwärtstab-Taste SHALL denselben Fokuswechsel wie Tab mit Umschalt auslösen. Die Reihenfolge LostFocus vor GotFocus SHALL auch an den Wirkungen der BASIC-Handler sichtbar bleiben.
#### Scenario: Tab überspringt #### Scenario: Tab überspringt
- **WHEN** das mittlere von drei Elementen `TabStop = 0` trägt und Tab gedrückt wird - **WHEN** das mittlere von drei Elementen `TabStop = 0` trägt und Tab gedrückt wird
- **THEN** erhält das dritte Element den Fokus - **THEN** erhält das dritte Element den Fokus
@@ -48,6 +50,14 @@ am alten und `GotFocus` am neuen Element auslösen, in dieser Reihenfolge.
- **WHEN** der Fokus von `Text1` auf `Text2` wechselt - **WHEN** der Fokus von `Text1` auf `Text2` wechselt
- **THEN** läuft erst `Text1_LostFocus`, danach `Text2_GotFocus` - **THEN** läuft erst `Text1_LostFocus`, danach `Text2_GotFocus`
#### Scenario: Handlerwirkungen in Fokusreihenfolge
- **WHEN** LostFocus den Text L und GotFocus den Text G an dieselbe Variable anhängen
- **THEN** lautet das Ergebnis LG
#### Scenario: Rückwärtstab vom Terminal
- **WHEN** das Terminal eine Rückwärtstab-Taste liefert
- **THEN** wechselt der Fokus zum vorherigen zulässigen TabIndex
### Requirement: Maussteuerung mit Trefferprüfung ### Requirement: Maussteuerung mit Trefferprüfung
Ein Mausereignis SHALL dem obersten Steuerelement an seiner Position Ein Mausereignis SHALL dem obersten Steuerelement an seiner Position
zugestellt werden; liegt dort keines, dem Formular. Klick, Doppelklick zugestellt werden; liegt dort keines, dem Formular. Klick, Doppelklick
@@ -116,6 +126,10 @@ fortgesetzt werden.
- **WHEN** ein Menü geöffnet ist und ein Zeit-Trap fällig wird - **WHEN** ein Menü geöffnet ist und ein Zeit-Trap fällig wird
- **THEN** läuft sein Handler erst, nachdem das Menü geschlossen wurde - **THEN** läuft sein Handler erst, nachdem das Menü geschlossen wurde
#### Scenario: UEVENT an gewöhnlicher Anweisungsgrenze
- **WHEN** ein Menü offen ist und UEVENT vor einer Zuweisung ansteht
- **THEN** läuft dessen Handler erst nach dem Schließen des Menüs
### Requirement: Vordefinierte Dialoge ### Requirement: Vordefinierte Dialoge
`MSGBOX text$ [, typ% [, titel$]]` SHALL als Anweisung und als Funktion `MSGBOX text$ [, typ% [, titel$]]` SHALL als Anweisung und als Funktion
verfügbar sein; die Funktion SHALL die gedrückte Schaltfläche als verfügbar sein; die Funktion SHALL die gedrückte Schaltfläche als

View File

@@ -98,10 +98,16 @@ Darstellungsschicht liegen. Damit MUST jedes Bildschirmverhalten in
automatischen Tests ohne Terminal prüfbar sein, und die Ausführungsschicht automatischen Tests ohne Terminal prüfbar sein, und die Ausführungsschicht
MUST NOT von einer Terminal-Bibliothek abhängen. MUST NOT von einer Terminal-Bibliothek abhängen.
Ein separater Einbetter SHALL die VM einschließlich Forms ohne Terminalbackend bauen und ausführen können. Der CLI-Runner SHALL das Backend ausdrücklich zuschalten.
#### Scenario: Bildschirmverhalten im Test ohne Terminal #### Scenario: Bildschirmverhalten im Test ohne Terminal
- **WHEN** ein Testprogramm in einer Umgebung ohne Terminal `LOCATE 5, 10 : PRINT "x"` ausführt - **WHEN** ein Testprogramm in einer Umgebung ohne Terminal `LOCATE 5, 10 : PRINT "x"` ausführt
- **THEN** trägt die Zelle (5, 10) das Zeichen `x` und der Test benötigt kein Terminal - **THEN** trägt die Zelle (5, 10) das Zeichen `x` und der Test benötigt kein Terminal
#### Scenario: Terminalfreier Einbetter
- **WHEN** ein separates Programm ausschließlich die VM mit einem Capture-Host einbindet
- **THEN** enthält sein aufgelöster Abhängigkeitsbaum keine Terminalbibliothek und die Forms-Tests können darin laufen
### Requirement: Bildschirmanweisungen des Dialekts ### Requirement: Bildschirmanweisungen des Dialekts
`CLS`, `COLOR`, `LOCATE`, `WIDTH`, `VIEW PRINT` und die Anweisungsform von `CLS`, `COLOR`, `LOCATE`, `WIDTH`, `VIEW PRINT` und die Anweisungsform von
`SCREEN` SHALL auf dem Zellenpuffer wirken. `CLS` SHALL den Scrollbereich `SCREEN` SHALL auf dem Zellenpuffer wirken. `CLS` SHALL den Scrollbereich

View File

@@ -124,6 +124,8 @@ ausführen. Der unterbrochene Zustand — Aufrufkette, Schleifen, Locals,
Die Prüfung MUST NOT pro Opcode erfolgen; solange kein Trap definiert ist, Die Prüfung MUST NOT pro Opcode erfolgen; solange kein Trap definiert ist,
MUST sie auf einen einzelnen Test hinauslaufen und MUST NOT die Uhr lesen. MUST sie auf einen einzelnen Test hinauslaufen und MUST NOT die Uhr lesen.
Ohne klassische Traps, aktive Forms-Timer oder zeitabhängige Mausauswertung SHALL auch die Forms-Integration keine Hostzeit abfragen.
#### Scenario: Trap aus einer Prozedur heraus #### Scenario: Trap aus einer Prozedur heraus
- **WHEN** ein Ereignis zugestellt wird, während eine `SUB` läuft, und das Trap-Ziel im Modulrumpf liegt - **WHEN** ein Ereignis zugestellt wird, während eine `SUB` läuft, und das Trap-Ziel im Modulrumpf liegt
- **THEN** läuft der Handler und nach `RETURN` setzt die `SUB` an der unterbrochenen Anweisung fort - **THEN** läuft der Handler und nach `RETURN` setzt die `SUB` an der unterbrochenen Anweisung fort
@@ -132,6 +134,10 @@ MUST sie auf einen einzelnen Test hinauslaufen und MUST NOT die Uhr lesen.
- **WHEN** ein Programm ohne jede `ON …`-Trap-Definition ausgeführt wird - **WHEN** ein Programm ohne jede `ON …`-Trap-Definition ausgeführt wird
- **THEN** fragt die Ausführung an keiner Anweisungsgrenze die Zeit ab, und der Durchsatz bleibt im Rahmen der Messgenauigkeit unverändert - **THEN** fragt die Ausführung an keiner Anweisungsgrenze die Zeit ab, und der Durchsatz bleibt im Rahmen der Messgenauigkeit unverändert
#### Scenario: Einfaches Programm liest keine Ereigniszeit
- **WHEN** ein Programm nur Zuweisungen und END ausführt und ein Testhost Zeitabfragen zählt
- **THEN** ist der Zähler am Ende 0
### Requirement: Jede Schleife trägt eine Anweisungsgrenze im Kreis ### Requirement: Jede Schleife trägt eine Anweisungsgrenze im Kreis
Der zurückspringende Zweig jeder Schleife (`FOR`/`NEXT`, `WHILE`/`WEND`, Der zurückspringende Zweig jeder Schleife (`FOR`/`NEXT`, `WHILE`/`WEND`,
`DO`/`LOOP` in allen Bedingungsformen) MUST über mindestens eine `DO`/`LOOP` in allen Bedingungsformen) MUST über mindestens eine

View File

@@ -0,0 +1,32 @@
VERSION 1.00
Begin Form Form1
Width = 40
Height = 12
Begin TextBox Text1
Left = 5
Top = 5
Width = 10
Height = 1
End
End
' tb-screen: 80x25
' tb-events: time:0ms | mouse:down,1,0,2,2 | key:K
' Erwartete Handlerwirkungen in Hostreihenfolge; keine Snapshot-Neuaufnahme.
Text1.SetFocus
DIM SHARED s$
DOEVENTS
DOEVENTS
DOEVENTS
Form1.Hide
CLS
PRINT s$
END
SUB Text1_KeyPress(KeyAscii AS INTEGER)
SHARED s$
s$ = s$ + CHR$(KeyAscii)
END SUB
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)
SHARED s$
s$ = s$ + "M"
END SUB

View File

@@ -0,0 +1 @@
MK

View File

@@ -0,0 +1,32 @@
VERSION 1.00
Begin Form Form1
Width = 40
Height = 12
Begin TextBox Text1
Left = 5
Top = 5
Width = 10
Height = 1
End
End
' tb-screen: 80x25
' tb-events: time:0ms | key:K | mouse:down,1,0,2,2
' Erwartete Handlerwirkungen in Hostreihenfolge; keine Snapshot-Neuaufnahme.
Text1.SetFocus
DIM SHARED s$
DOEVENTS
DOEVENTS
DOEVENTS
Form1.Hide
CLS
PRINT s$
END
SUB Text1_KeyPress(KeyAscii AS INTEGER)
SHARED s$
s$ = s$ + CHR$(KeyAscii)
END SUB
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)
SHARED s$
s$ = s$ + "M"
END SUB

View File

@@ -0,0 +1 @@
KM

2
tests/terminalfrei/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
/Cargo.lock

View File

@@ -0,0 +1,13 @@
[package]
name = "tb-terminalfrei-check"
version = "0.0.0"
edition = "2021"
publish = false
# Eigener Resolver: keine Feature-Unifikation mit CLI/IDE im Hauptworkspace.
[workspace]
[dependencies]
tb-vm = { path = "../../crates/tb-vm" }
tb-runtime = { path = "../../crates/tb-runtime" }
tb-ui = { path = "../../crates/tb-ui" }

View File

@@ -0,0 +1,29 @@
use tb_runtime::host::{CaptureHost, Ereignis, MausArt, MausEreignis};
use tb_vm::interp::{RunEvent, Vm};
fn main() {
let form = tb_ui::frm::read_text(
"eingabe-taste-maus.frm",
include_str!("../../compat/eingabe-taste-maus.frm"),
)
.unwrap();
let module = tb_vm::compile_source_with_forms("FORM1", &form.code, &form.catalog()).unwrap();
let mut vm = Vm::new(module);
form.apply(&mut vm.forms).unwrap();
vm.forms.show(0, false).unwrap();
let mut host = CaptureHost::default();
host.ereignis_nach(2, Ereignis::Taste("K".into(), 0));
host.ereignis_nach(
2,
Ereignis::Maus(MausEreignis {
art: MausArt::Druck,
taste: 1,
shift: 0,
zeile: 2,
spalte: 2,
}),
);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "KM\n");
println!("Terminalfreier FRM-/VM-Lauf: KM");
}