Ereigniszustellung und Hostgrenzen korrigieren und Change archivieren
This commit is contained in:
@@ -14,5 +14,5 @@ path = "src/main.rs"
|
||||
tb-frontend.workspace = true
|
||||
tb-vm.workspace = true
|
||||
tb-runtime.workspace = true
|
||||
tb-ui.workspace = true
|
||||
tb-ui = { workspace = true, features = ["terminal"] }
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -551,6 +551,20 @@ impl Host for PipeHost {
|
||||
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 {
|
||||
self.start.elapsed().as_millis() as u64
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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> {
|
||||
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 {
|
||||
let jetzt = self.inner.jetzt_ms();
|
||||
self.inner.uhr_vorruecken(self.schritt_ms);
|
||||
|
||||
@@ -1882,7 +1882,7 @@ impl<'a> P<'a> {
|
||||
{
|
||||
if matches!(
|
||||
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() {
|
||||
TokenKind::Kw(Kw::On) => Some(EventAction::On),
|
||||
|
||||
@@ -28,14 +28,8 @@ pub struct RtState {
|
||||
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
|
||||
pub isam: crate::isam::Isam,
|
||||
pub print: PrintState,
|
||||
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form, je mit
|
||||
/// Umschaltzustand (Bitfeld 1 Umschalt, 2 Strg, 4 Alt). Der Zustand
|
||||
/// wird nur für die benutzerdefinierten Trap-Tasten gebraucht;
|
||||
/// `INKEY$` und `INPUT` sehen ihn nicht.
|
||||
pub tasten: VecDeque<(String, u8)>,
|
||||
/// Mausereignisse für den Forms-Dispatch; sie werden erst an den
|
||||
/// bestehenden Zustellpunkten von der VM entnommen.
|
||||
pub maus: VecDeque<crate::host::MausEreignis>,
|
||||
/// Hostreihenfolge bis zur Annahme durch Trap, Formular oder Konsole.
|
||||
pub eingaben: VecDeque<Eingabe>,
|
||||
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
|
||||
pub abbruch: bool,
|
||||
/// Eingabeende wurde gemeldet.
|
||||
@@ -66,6 +60,23 @@ pub struct RtState {
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
RtState {
|
||||
@@ -73,8 +84,7 @@ impl Default for RtState {
|
||||
dateien: Default::default(),
|
||||
isam: Default::default(),
|
||||
print: PrintState::default(),
|
||||
tasten: VecDeque::new(),
|
||||
maus: VecDeque::new(),
|
||||
eingaben: VecDeque::new(),
|
||||
abbruch: false,
|
||||
ende: false,
|
||||
// Das Vorbild belegt die Makros im Interpreter vor; ein
|
||||
@@ -111,24 +121,28 @@ impl RtState {
|
||||
let Some(e) = host.next_event(blockierend) else {
|
||||
return;
|
||||
};
|
||||
match e {
|
||||
Ereignis::Taste(t, shift) => self.tasten.push_back((t, shift)),
|
||||
Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows),
|
||||
Ereignis::Abbruch => self.abbruch = true,
|
||||
Ereignis::Ende => self.ende = true,
|
||||
Ereignis::Signal(n) => {
|
||||
if n == 1 && !self.traps.melden(crate::traps::Quelle::Signal(n)) {
|
||||
self.abbruch = true;
|
||||
}
|
||||
}
|
||||
Ereignis::Maus(m) => self.maus.push_back(m),
|
||||
}
|
||||
self.ereignis(e);
|
||||
if blockierend {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ereignis(&mut self, e: Ereignis) {
|
||||
match e {
|
||||
Ereignis::Taste(..) | Ereignis::Maus(_) => self.eingaben.push_back(e.into()),
|
||||
Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows),
|
||||
Ereignis::Abbruch => self.abbruch = true,
|
||||
Ereignis::Ende => self.ende = true,
|
||||
Ereignis::Signal(n) => {
|
||||
let gemeldet = self.traps.melden(crate::traps::Quelle::Signal(n));
|
||||
if n == 1 && !gemeldet {
|
||||
self.abbruch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bildschirm anzeigen und anstehende Ereignisse einsammeln.
|
||||
/// Zustellpunkt an Tick-Grenzen und vor blockierender Eingabe.
|
||||
pub fn tick(&mut self, host: &mut dyn Host) {
|
||||
@@ -147,45 +161,80 @@ impl RtState {
|
||||
if !self.traps.aktiv() {
|
||||
return;
|
||||
}
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.traps.zeit_pruefen(jetzt);
|
||||
if self.traps.naechste_frist().is_some() {
|
||||
self.traps.zeit_pruefen(host.jetzt_ms());
|
||||
}
|
||||
self.tasten_traps_pruefen();
|
||||
}
|
||||
|
||||
/// Getrappte Tasten aus der Schlange nehmen. Ohne Host, damit die VM
|
||||
/// das an jeder Anweisungsgrenze tun kann, ohne die Uhr zu lesen.
|
||||
pub fn tasten_traps_pruefen(&mut self) {
|
||||
// Nur der Kopf der Schlange: die Reihenfolge der Eingabe bleibt
|
||||
// erhalten, und eine getrappte Taste hinter einer ungetrappten
|
||||
// wartet, bis diese gelesen ist.
|
||||
while let Some((s, shift)) = self.tasten.front() {
|
||||
let Some(code) = crate::host::taste::scancode_von(s) else {
|
||||
break;
|
||||
};
|
||||
let Some(n) = self.traps.taste_faengt(code, *shift) else {
|
||||
break;
|
||||
};
|
||||
self.tasten.pop_front();
|
||||
self.traps.melden(crate::traps::Quelle::Key(n));
|
||||
while self.taste_trappen(0) {}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
return false;
|
||||
};
|
||||
let Some(n) = self.traps.taste_faengt(code, *shift) else {
|
||||
return false;
|
||||
};
|
||||
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`.
|
||||
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> {
|
||||
loop {
|
||||
if let Some((t, _)) = self.tasten.pop_front() {
|
||||
/// Nur bereits gepufferte Tasten annehmen, ohne weitere Hostereignisse
|
||||
/// abzuholen. Die VM kann zwischen Abholen und Blockieren zustellen.
|
||||
pub fn gepufferte_taste(&mut self) -> Option<String> {
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
self.pump(host, blockierend);
|
||||
if !blockierend && self.tasten.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if blockierend && self.tasten.is_empty() && self.ende {
|
||||
return None;
|
||||
}
|
||||
abgeholt = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2367,12 +2416,24 @@ fn bi_sleep(
|
||||
a: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
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);
|
||||
if secs > 0.0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs_f64(secs));
|
||||
} else {
|
||||
st.naechste_taste(host, true);
|
||||
loop {
|
||||
if st.ende || st.abbruch {
|
||||
break;
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -2777,6 +2838,60 @@ mod tests {
|
||||
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]
|
||||
fn sigint_ohne_aktiven_trap_bleibt_abbruch() {
|
||||
use crate::host::Ereignis;
|
||||
@@ -2802,7 +2917,10 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
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]
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
//! oder das Zurücklesen per `SCREEN(z, s)` nicht transportieren
|
||||
//! (Entscheidung 2026-09-03, D2).
|
||||
//!
|
||||
//! Host-Aufrufe dürfen blockieren; kooperative Zustellpunkte bringt erst die
|
||||
//! Ereignisschleife der Forms-Engine (Phase 4).
|
||||
//! Der Host besitzt Zeit und Warten; die VM liefert nur an kooperativen
|
||||
//! Zustellpunkten aus.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
@@ -165,6 +165,12 @@ pub trait Host {
|
||||
/// wenn nichts anliegt (Grundlage von `INKEY$`).
|
||||
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.
|
||||
///
|
||||
/// Die Fälligkeit der Zeit-Traps rechnet die Laufzeit daraus aus; sie
|
||||
@@ -260,6 +266,19 @@ impl Host for CaptureHost {
|
||||
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 {
|
||||
self.uhr_ms
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ pub struct Traps {
|
||||
/// `EVENT OFF` klammert einen Abschnitt ohne Ereignisprüfung. Kein
|
||||
/// dritter Zustand — den kennt das Vorbild nicht.
|
||||
event_an: bool,
|
||||
/// Menüsperre ohne Änderung der expliziten ON/OFF/STOP-Zustände.
|
||||
suspendiert: bool,
|
||||
/// Fälligkeit des Zeit-Traps: Intervall und der Zeitpunkt, an dem er
|
||||
/// das nächste Mal fällig wird (beides in Millisekunden).
|
||||
timer_intervall_ms: u64,
|
||||
@@ -178,6 +180,27 @@ impl Traps {
|
||||
self.aktiv
|
||||
}
|
||||
|
||||
pub fn suspendieren(&mut self, gesperrt: bool) {
|
||||
self.suspendiert = gesperrt;
|
||||
}
|
||||
|
||||
pub fn ist_an(&self, q: Quelle) -> bool {
|
||||
self.traps
|
||||
.iter()
|
||||
.any(|t| t.quelle == q && t.zustand == Zustand::An)
|
||||
}
|
||||
|
||||
/// Nächste Hostfrist, sofern der Zeit-Trap überhaupt Zeit benötigt.
|
||||
pub fn naechste_frist(&self) -> Option<u64> {
|
||||
(self.event_an
|
||||
&& !self.suspendiert
|
||||
&& self.timer_intervall_ms > 0
|
||||
&& self.traps.iter().any(|t| {
|
||||
t.quelle == Quelle::Timer && t.ziel.is_some() && t.zustand != Zustand::Aus
|
||||
}))
|
||||
.then_some(self.timer_faellig_ms)
|
||||
}
|
||||
|
||||
/// Benutzerdefinierte Taste erklären (`KEY n, CHR$(flag)+CHR$(scan)`,
|
||||
/// n = 15–25). `flag` ist das Tastaturflagbyte des Vorbilds.
|
||||
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
|
||||
/// wissen, welche Quelle er melden soll.
|
||||
pub fn taste_faengt(&self, scancode: u8, shift: u8) -> Option<u8> {
|
||||
if !self.event_an {
|
||||
if !self.event_an || self.suspendiert {
|
||||
return None;
|
||||
}
|
||||
let treffer = |k: u8| -> bool {
|
||||
@@ -218,7 +241,7 @@ impl Traps {
|
||||
/// Nächstes zuzustellendes Ereignis: Quelle und Sprungziel. Liefert
|
||||
/// höchstens eines je Aufruf; die übrigen bleiben anstehend.
|
||||
pub fn naechstes(&mut self) -> Option<(Quelle, u32)> {
|
||||
if !self.event_an {
|
||||
if !self.event_an || self.suspendiert {
|
||||
return None;
|
||||
}
|
||||
let mut kandidaten: Vec<usize> = (0..self.traps.len())
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
[package]
|
||||
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
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
terminal = ["dep:ratatui", "dep:crossterm", "dep:signal-hook"]
|
||||
|
||||
[dependencies]
|
||||
tb-runtime.workspace = true
|
||||
tb-frontend.workspace = true
|
||||
ratatui.workspace = true
|
||||
crossterm.workspace = true
|
||||
signal-hook.workspace = true
|
||||
ratatui = { workspace = true, optional = true }
|
||||
crossterm = { workspace = true, optional = true }
|
||||
signal-hook = { workspace = true, optional = true }
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
|
||||
@@ -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) {
|
||||
if !self.menu_path.is_empty() {
|
||||
return;
|
||||
@@ -1784,15 +1827,7 @@ impl FormsModel {
|
||||
let mut due: Vec<_> = self
|
||||
.keys()
|
||||
.into_iter()
|
||||
.filter(|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))
|
||||
})
|
||||
.filter(|key| self.timer_enabled(*key))
|
||||
.collect();
|
||||
due.sort_by_key(|key| {
|
||||
self.instance(*key)
|
||||
@@ -2395,13 +2430,16 @@ fn dialog_event(
|
||||
model: &mut FormsModel,
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<Ereignis>,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
plain_access_key: bool,
|
||||
) -> Option<FormEvent> {
|
||||
loop {
|
||||
model.render(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 {
|
||||
Ereignis::Taste(key, shift) => {
|
||||
let shift = if plain_access_key && key.chars().count() == 1 {
|
||||
@@ -2412,14 +2450,19 @@ fn dialog_event(
|
||||
model.handle_key(&key, shift);
|
||||
}
|
||||
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 } => {
|
||||
screen.resize(cols, rows);
|
||||
model.resize(cols, rows);
|
||||
}
|
||||
event @ (Ereignis::Abbruch | Ereignis::Ende | Ereignis::Signal(_)) => {
|
||||
queued.push_front(event);
|
||||
queued.push_front(event.into());
|
||||
return None;
|
||||
}
|
||||
}
|
||||
@@ -2435,7 +2478,7 @@ fn dialog_event(
|
||||
pub fn msgbox_dialog(
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<Ereignis>,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
text: &str,
|
||||
kind: i32,
|
||||
title: &str,
|
||||
@@ -2549,7 +2592,7 @@ pub fn msgbox_dialog(
|
||||
pub fn inputbox_dialog(
|
||||
screen: &mut TextScreen,
|
||||
host: &mut dyn Host,
|
||||
queued: &mut VecDeque<Ereignis>,
|
||||
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||
prompt: &str,
|
||||
title: &str,
|
||||
initial: &str,
|
||||
|
||||
@@ -86,7 +86,33 @@ impl Host for TerminalHost {
|
||||
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> {
|
||||
if blockierend {
|
||||
return self.warten(None);
|
||||
}
|
||||
loop {
|
||||
// Signale vor dem Terminal: sie liegen schon an, wenn wir hier
|
||||
// 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::Backspace => taste::BACKSPACE.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.
|
||||
KeyCode::F(n @ 1..=10) => taste::sonder(58 + n),
|
||||
KeyCode::Home => taste::sonder(71),
|
||||
@@ -197,7 +223,7 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
||||
_ => return None,
|
||||
};
|
||||
let mut shift = 0u8;
|
||||
if k.modifiers.contains(KeyModifiers::SHIFT) {
|
||||
if k.modifiers.contains(KeyModifiers::SHIFT) || k.code == KeyCode::BackTab {
|
||||
shift |= umschalt::SHIFT;
|
||||
}
|
||||
if k.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
@@ -213,6 +239,16 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
||||
mod tests {
|
||||
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]
|
||||
fn zeichentaste_wird_zu_inkey_form() {
|
||||
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
|
||||
|
||||
@@ -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:
|
||||
//! 1. `screen`: Emulation des 80×25-Textbildschirms (Unicode/UTF-8, 16 Farben,
|
||||
//! Cursor) als Zeichenpuffer, gerendert über Ratatui. Darauf setzen die
|
||||
//! 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.
|
||||
//! Die Ausführung verwendet `tb_runtime::host::Host` und dessen Zellenpuffer.
|
||||
//! Das optionale Feature `terminal` ergänzt Anzeige über Ratatui,
|
||||
//! crossterm-Eingaben und Betriebssystemsignale.
|
||||
|
||||
pub mod events;
|
||||
pub mod forms;
|
||||
pub mod frm;
|
||||
mod frm_pcode;
|
||||
#[cfg(feature = "terminal")]
|
||||
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
|
||||
#[cfg(feature = "terminal")]
|
||||
pub mod screen;
|
||||
#[cfg(feature = "terminal")]
|
||||
pub mod signale; // Betriebssystemsignale als Ereignisquelle (SIGNAL)
|
||||
|
||||
@@ -78,11 +78,27 @@ struct Frame {
|
||||
waiting_form: Option<u16>,
|
||||
pending_show: Option<u16>,
|
||||
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)]
|
||||
enum FormEventReturn {
|
||||
Normal,
|
||||
Load,
|
||||
Unload(u16),
|
||||
}
|
||||
|
||||
@@ -225,6 +241,8 @@ impl Vm {
|
||||
waiting_form: None,
|
||||
pending_show: None,
|
||||
form_event: None,
|
||||
handler_start: false,
|
||||
eingabe: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -262,36 +280,15 @@ impl Vm {
|
||||
}
|
||||
}
|
||||
|
||||
fn take_dialog_input(&mut self) -> VecDeque<Ereignis> {
|
||||
let mut events = VecDeque::new();
|
||||
events.extend(
|
||||
self.rt
|
||||
.tasten
|
||||
.drain(..)
|
||||
.map(|(key, shift)| Ereignis::Taste(key, shift)),
|
||||
);
|
||||
events.extend(self.rt.maus.drain(..).map(Ereignis::Maus));
|
||||
events
|
||||
fn take_dialog_input(&mut self) -> VecDeque<tb_runtime::builtins::Eingabe> {
|
||||
std::mem::take(&mut self.rt.eingaben)
|
||||
}
|
||||
|
||||
fn restore_dialog_input(&mut self, events: VecDeque<Ereignis>) {
|
||||
for event in events {
|
||||
match event {
|
||||
Ereignis::Taste(key, shift) => self.rt.tasten.push_back((key, shift)),
|
||||
Ereignis::Maus(event) => self.rt.maus.push_back(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),
|
||||
fn restore_dialog_input(&mut self, events: VecDeque<tb_runtime::builtins::Eingabe>) {
|
||||
for input in events {
|
||||
match input.ereignis {
|
||||
Ereignis::Taste(..) | Ereignis::Maus(_) => self.rt.eingaben.push_back(input),
|
||||
event => self.rt.ereignis(event),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -563,13 +560,47 @@ impl Vm {
|
||||
);
|
||||
if let Some(frame) = self.frames.last_mut() {
|
||||
frame.form_event = Some(on_return);
|
||||
frame.handler_start = true;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn dispatch_next_form_event(&mut self) -> bool {
|
||||
while let Some(event) = self.forms.next_event() {
|
||||
if self.dispatch_form_event(event, FormEventReturn::Normal) {
|
||||
fn dispatch_next_form_event(&mut self, load_only: bool) -> bool {
|
||||
let mut i = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -577,6 +608,12 @@ impl Vm {
|
||||
}
|
||||
|
||||
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 {
|
||||
object,
|
||||
array_index: None,
|
||||
@@ -590,80 +627,136 @@ impl Vm {
|
||||
}
|
||||
|
||||
fn tick(&mut self, host: &mut dyn Host) {
|
||||
self.rt.traps.suspendieren(self.forms.menu_is_open());
|
||||
self.forms.render(&mut self.rt.screen);
|
||||
if self.forms.menu_is_open() {
|
||||
host.present(&self.rt.screen);
|
||||
self.rt.pump(host, false);
|
||||
} else {
|
||||
self.rt.tick(host);
|
||||
host.present(&self.rt.screen);
|
||||
self.rt.pump(host, false);
|
||||
if !self.forms.menu_is_open() && self.rt.traps.naechste_frist().is_some() {
|
||||
self.rt.traps.zeit_pruefen(host.jetzt_ms());
|
||||
}
|
||||
}
|
||||
|
||||
fn forms_zustellen(&mut self, host: &mut dyn Host) -> bool {
|
||||
self.forms
|
||||
.resize(self.rt.screen.cols(), self.rt.screen.rows());
|
||||
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);
|
||||
/// 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
|
||||
.resize(self.rt.screen.cols(), self.rt.screen.rows());
|
||||
if self.forms.next_deadline().is_some() {
|
||||
self.forms.timers(host.jetzt_ms());
|
||||
}
|
||||
}
|
||||
let now_ms = host.jetzt_ms();
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
if self.rt.traps.aktiv() {
|
||||
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() {
|
||||
self.trap_frame_aufsetzen(q, ziel);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.rt.traps.zeit_pruefen(jetzt);
|
||||
self.rt.tasten_traps_pruefen();
|
||||
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
||||
self.trap_frame_aufsetzen(q, ziel);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// `SLEEP [n]`: wartet `n` Sekunden, ohne Argument bis zum nächsten
|
||||
/// Tastendruck. Ein zugestelltes Ereignis beendet das Warten
|
||||
/// (Sprachreferenz §8) — der Handler läuft, und danach geht es hinter
|
||||
/// dem `SLEEP` weiter.
|
||||
fn warten(&mut self, host: &mut dyn Host, deadline: Option<u64>) {
|
||||
let deadline = deadline
|
||||
.into_iter()
|
||||
.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) {
|
||||
let start = host.jetzt_ms();
|
||||
let dauer_ms = (sekunden.max(0.0) * 1000.0) as u64;
|
||||
// ponytail: zusätzlich eine echte Zeitschranke, damit ein Host mit
|
||||
// stehender Uhr (Testhost) nicht ewig wartet. Ceiling: bei einer
|
||||
// virtuellen Uhr endet `SLEEP` nach der realen Dauer, wenn die
|
||||
// virtuelle nie vorrückt.
|
||||
let real_start = std::time::Instant::now();
|
||||
let deadline =
|
||||
(sekunden > 0.0).then(|| host.jetzt_ms().saturating_add((sekunden * 1000.0) as u64));
|
||||
loop {
|
||||
self.tick(host);
|
||||
if self.zustellen(host) {
|
||||
if self.zustellen(host, Zustellpunkt::Kooperativ) || self.rt.ende || self.rt.abbruch {
|
||||
return;
|
||||
}
|
||||
if self.rt.ende || self.rt.abbruch {
|
||||
if let Some(deadline) = deadline {
|
||||
if host.jetzt_ms() >= deadline {
|
||||
return;
|
||||
}
|
||||
} else if self.rt.naechste_taste(host, false).is_some() {
|
||||
return;
|
||||
}
|
||||
if sekunden <= 0.0 {
|
||||
// Ohne Argument: bis zum Tastendruck.
|
||||
if self.rt.naechste_taste(host, false).is_some() {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let vergangen = host.jetzt_ms().saturating_sub(start);
|
||||
if vergangen >= dauer_ms || real_start.elapsed().as_millis() as u64 >= dauer_ms {
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
self.warten(host, deadline);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,6 +779,8 @@ impl Vm {
|
||||
waiting_form: None,
|
||||
pending_show: None,
|
||||
form_event: None,
|
||||
handler_start: true,
|
||||
eingabe: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -699,7 +794,11 @@ impl Vm {
|
||||
|
||||
fn pop_frame(&mut self) {
|
||||
let f = self.frames.pop().expect("Frame-Unterlauf");
|
||||
self.locals.truncate(f.locals_base);
|
||||
if let Some(q) = f.trap {
|
||||
self.rt.traps.handler_beendet(q);
|
||||
} else {
|
||||
self.locals.truncate(f.locals_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 {
|
||||
while self.forms.has_visible_forms() {
|
||||
self.tick(host);
|
||||
let dispatched = self.forms_zustellen(host);
|
||||
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
|
||||
if dispatched {
|
||||
match self.run(host) {
|
||||
RunEvent::Ended => {}
|
||||
event => return event,
|
||||
}
|
||||
}
|
||||
if self.rt.ende {
|
||||
if !dispatched && self.rt.ende {
|
||||
return RunEvent::Ended;
|
||||
}
|
||||
if self.rt.abbruch {
|
||||
@@ -821,7 +920,9 @@ impl Vm {
|
||||
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
|
||||
}
|
||||
@@ -833,11 +934,18 @@ impl Vm {
|
||||
self.frames.last_mut().unwrap().waiting_form = None;
|
||||
} else {
|
||||
self.tick(host);
|
||||
self.forms_zustellen(host);
|
||||
if self.rt.ende {
|
||||
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
|
||||
if !dispatched && self.rt.ende {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -898,6 +1006,7 @@ impl Vm {
|
||||
(f.stack_base, f.last_stmt_pc, f.proc)
|
||||
};
|
||||
self.stack.truncate(stack_base);
|
||||
self.frames.last_mut().unwrap().eingabe = None;
|
||||
self.err = code;
|
||||
match handler {
|
||||
Handler::Goto(pc) => {
|
||||
@@ -1131,27 +1240,11 @@ impl Vm {
|
||||
self.tick(host);
|
||||
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);
|
||||
}
|
||||
// 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 & F_STEP != 0 {
|
||||
return Ok(Flow::Event(RunEvent::Stepped { line }));
|
||||
@@ -1193,7 +1286,7 @@ impl Vm {
|
||||
1 => tb_runtime::traps::Zustand::Aus,
|
||||
_ => 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);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -1203,8 +1296,7 @@ impl Vm {
|
||||
// lässt den Wert unberührt.
|
||||
self.stack.push(Value::Int(0));
|
||||
self.tick(host);
|
||||
self.forms_zustellen(host);
|
||||
self.zustellen(host);
|
||||
self.zustellen(host, Zustellpunkt::Kooperativ);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::Sleep(mit_arg) => {
|
||||
@@ -1258,7 +1350,7 @@ impl Vm {
|
||||
};
|
||||
if !self.forms.is_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;
|
||||
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
||||
return Ok(Flow::Normal);
|
||||
@@ -1280,7 +1372,7 @@ impl Vm {
|
||||
if index.is_none() {
|
||||
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;
|
||||
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
||||
return Ok(Flow::Normal);
|
||||
@@ -1304,7 +1396,7 @@ impl Vm {
|
||||
};
|
||||
self.forms.ensure_loaded_at(object, index)?;
|
||||
self.push(Value::Obj(object, index));
|
||||
self.dispatch_next_form_event();
|
||||
self.dispatch_next_form_event(true);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::LoadDynamicObjectProperty(property) => {
|
||||
@@ -1434,7 +1526,7 @@ impl Vm {
|
||||
&& !self.forms.is_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;
|
||||
self.frames[caller].pending_show = Some(object);
|
||||
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.dispatch_next_form_event();
|
||||
self.dispatch_next_form_event(true);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::ObjectLoad(object, unload, has_index) => {
|
||||
@@ -1508,7 +1600,7 @@ impl Vm {
|
||||
self.request_unload(object)?;
|
||||
} else {
|
||||
self.forms.ensure_loaded(object)?;
|
||||
self.dispatch_next_form_event();
|
||||
self.dispatch_next_form_event(true);
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -2058,6 +2150,8 @@ impl Vm {
|
||||
self.pop_frame();
|
||||
}
|
||||
let f = self.frames.last_mut().unwrap();
|
||||
f.eingabe = None;
|
||||
self.stack.truncate(f.stack_base);
|
||||
f.pc = t as usize;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -2122,8 +2216,10 @@ impl Vm {
|
||||
if let Some(FormEventReturn::Unload(object)) = event {
|
||||
self.forms.unload_with(object, |c| *c = cancel)?;
|
||||
}
|
||||
if event.is_some() {
|
||||
self.dispatch_next_form_event();
|
||||
if matches!(event, Some(FormEventReturn::Normal))
|
||||
&& !self.frames.iter().any(|f| f.form_event.is_some())
|
||||
{
|
||||
self.dispatch_next_form_event(false);
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -2134,6 +2230,38 @@ impl Vm {
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
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);
|
||||
for _ in 0..argc {
|
||||
args.push(self.pop()?);
|
||||
@@ -2223,7 +2351,10 @@ impl Vm {
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
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)
|
||||
}
|
||||
I::InputFile(argc, line_mode) => {
|
||||
@@ -2272,6 +2403,64 @@ impl Vm {
|
||||
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(
|
||||
&mut self,
|
||||
argc: u8,
|
||||
@@ -2279,38 +2468,41 @@ impl Vm {
|
||||
prompt: u16,
|
||||
question: bool,
|
||||
host: &mut dyn Host,
|
||||
) -> Result<(), RuntimeError> {
|
||||
// Referenzen in Reihenfolge einsammeln.
|
||||
let mut refs = Vec::with_capacity(argc as usize);
|
||||
for _ in 0..argc {
|
||||
match self.pop()? {
|
||||
Value::Ref(r) => refs.push(r),
|
||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
}
|
||||
}
|
||||
refs.reverse();
|
||||
|
||||
) -> Result<bool, RuntimeError> {
|
||||
let start = self
|
||||
.stack
|
||||
.len()
|
||||
.checked_sub(argc as usize)
|
||||
.ok_or(RuntimeError(51))?;
|
||||
let refs = self.stack[start..]
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value::Ref(r) => Ok(r.clone()),
|
||||
_ => Err(RuntimeError::TYPE_MISMATCH),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
loop {
|
||||
if prompt != 0xFFFF {
|
||||
let p = self.module.strings[prompt as usize].clone();
|
||||
self.rt.screen.print(&p);
|
||||
if self.frames.last().unwrap().eingabe.is_none() {
|
||||
if prompt != 0xFFFF {
|
||||
self.rt.screen.print(&self.module.strings[prompt as usize]);
|
||||
}
|
||||
if question {
|
||||
self.rt.screen.print("? ");
|
||||
}
|
||||
}
|
||||
if question {
|
||||
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 {
|
||||
let r = refs.first().ok_or(RuntimeError(51))?;
|
||||
self.write_ref(r, Value::Str(Rc::from(line.as_str())))?;
|
||||
return Ok(());
|
||||
self.write_ref(r, Value::Str(Rc::from(line)))?;
|
||||
self.stack.truncate(start);
|
||||
return Ok(true);
|
||||
}
|
||||
// Felder trennen (Quotes respektieren).
|
||||
let fields = split_input_fields(&line);
|
||||
if fields.len() != refs.len() {
|
||||
self.rt.screen.print("Redo from start\n");
|
||||
line.clear();
|
||||
continue;
|
||||
}
|
||||
// Konvertieren nach aktuellem Zieltyp; Fehler → Redo.
|
||||
@@ -2333,7 +2525,8 @@ impl Vm {
|
||||
for (r, v) in refs.iter().zip(vals) {
|
||||
self.write_ref(r, v)?;
|
||||
}
|
||||
return Ok(());
|
||||
self.stack.truncate(self.stack.len() - argc as usize);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
584
crates/tb-vm/tests/events.rs
Normal file
584
crates/tb-vm/tests/events.rs
Normal 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 V1–V5: 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");
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
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 {
|
||||
let j = self.inner.jetzt_ms();
|
||||
self.inner.uhr_vorruecken(self.schritt);
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
- 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).
|
||||
|
||||
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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
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)
|
||||
|
||||
@@ -221,7 +233,7 @@ Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/chan
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -55,15 +55,76 @@ enum Value {
|
||||
|
||||
## Ausführungsmodell / Unterbrechbarkeit
|
||||
|
||||
- Die Interpreterschleife läuft in **Ticks**: nach jeder Anweisung (Grenze
|
||||
aus der Zeilentabelle) prüft sie ein Flag-Set: Breakpoint? Einzelschritt?
|
||||
Strg+Untbr? Ereignis-Queue nicht leer und Zustellung erlaubt?
|
||||
- Ereigniszustellung (Forms, Timer) erfolgt kooperativ: nur an
|
||||
Zustellpunkten (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer
|
||||
Ereignisprozedur) — wie im Vorbild.
|
||||
- Die VM ist eine gewöhnliche zustandsbehaftete Struktur, `step()`-basiert;
|
||||
die einbettende Schleife (IDE-Debugger oder Runner) treibt sie. Kein
|
||||
eigener Thread nötig; Terminal-Events werden zwischen Ticks gepollt.
|
||||
Die VM läuft auf dem aufrufenden Thread. `Vm::run` liefert bei STOP,
|
||||
Breakpoint, Einzelschritt, Programmende oder Fehler ein `RunEvent` an den
|
||||
Runner zurück. `Stmt` markiert Anweisungsgrenzen; es gibt keine Prüfung
|
||||
nach jedem Opcode.
|
||||
|
||||
Die gemeinsame Zustellentscheidung gilt an Anweisungsgrenzen, bei
|
||||
`DOEVENTS`, während `SLEEP` und während blockierender Konsoleneingabe. Eine
|
||||
zugestellte Ereignisprozedur beginnt ihre erste Anweisung, bevor ein
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Why
|
||||
|
||||
Ereignisse gehen verloren, laufen in falscher Reihenfolge oder umgehen Menüsperren. F04–F08, 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. F04–F08, 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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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 V1–V5-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; V1–V3 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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
V1–V5 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.
|
||||
@@ -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.
|
||||
@@ -21,6 +21,8 @@ Systemuhr zugreifen. Ein Host ohne Terminal MUST die Zeit frei stellen
|
||||
können, sodass zeitgesteuerte Programme ohne Wartezeit und mit
|
||||
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
|
||||
- **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
|
||||
@@ -29,6 +31,10 @@ reproduzierbarem Ergebnis prüfbar sind.
|
||||
- **WHEN** dasselbe zeitgesteuerte Korpusprogramm zweimal mit demselben Zeitverlauf ausgeführt wird
|
||||
- **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
|
||||
Die Darstellungsschicht SHALL Mausereignisse der Ausführungsumgebung an
|
||||
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
|
||||
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
|
||||
- **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
|
||||
@@ -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
|
||||
- **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
|
||||
Ereignisse SHALL ausschließlich an folgenden Punkten zugestellt werden
|
||||
— 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
|
||||
`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
|
||||
- **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
|
||||
@@ -78,6 +96,10 @@ Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes
|
||||
- **WHEN** ein anstehendes Ereignis vorliegt und `DOEVENTS` ausgewertet wird
|
||||
- **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
|
||||
Jede Ereignisquelle SHALL drei Zustände kennen. `ON` stellt Ereignisse
|
||||
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
|
||||
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$
|
||||
- **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
|
||||
@@ -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
|
||||
- **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
|
||||
`SetUEvent` SHALL ein benutzerdefiniertes Ereignis auslösen, das über
|
||||
`ON UEVENT GOSUB` und die Steueranweisung `UEVENT` denselben Regeln für
|
||||
@@ -208,3 +236,7 @@ ausgewiesen sein.
|
||||
#### Scenario: Signal ohne Trap bleibt Abbruch
|
||||
- **WHEN** ein `SIGINT` eintrifft, ohne dass ein `SIGNAL(1)`-Trap aktiv ist
|
||||
- **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
|
||||
|
||||
@@ -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`
|
||||
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
|
||||
- **WHEN** das mittlere von drei Elementen `TabStop = 0` trägt und Tab gedrückt wird
|
||||
- **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
|
||||
- **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
|
||||
Ein Mausereignis SHALL dem obersten Steuerelement an seiner Position
|
||||
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
|
||||
- **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
|
||||
`MSGBOX text$ [, typ% [, titel$]]` SHALL als Anweisung und als Funktion
|
||||
verfügbar sein; die Funktion SHALL die gedrückte Schaltfläche als
|
||||
|
||||
@@ -98,10 +98,16 @@ Darstellungsschicht liegen. Damit MUST jedes Bildschirmverhalten in
|
||||
automatischen Tests ohne Terminal prüfbar sein, und die Ausführungsschicht
|
||||
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
|
||||
- **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
|
||||
|
||||
#### 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
|
||||
`CLS`, `COLOR`, `LOCATE`, `WIDTH`, `VIEW PRINT` und die Anweisungsform von
|
||||
`SCREEN` SHALL auf dem Zellenpuffer wirken. `CLS` SHALL den Scrollbereich
|
||||
|
||||
@@ -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,
|
||||
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
|
||||
- **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
|
||||
@@ -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
|
||||
- **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
|
||||
Der zurückspringende Zweig jeder Schleife (`FOR`/`NEXT`, `WHILE`/`WEND`,
|
||||
`DO`/`LOOP` in allen Bedingungsformen) MUST über mindestens eine
|
||||
|
||||
32
tests/compat/eingabe-maus-taste.frm
Normal file
32
tests/compat/eingabe-maus-taste.frm
Normal 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
|
||||
1
tests/compat/eingabe-maus-taste.out
Normal file
1
tests/compat/eingabe-maus-taste.out
Normal file
@@ -0,0 +1 @@
|
||||
MK
|
||||
32
tests/compat/eingabe-taste-maus.frm
Normal file
32
tests/compat/eingabe-taste-maus.frm
Normal 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
|
||||
1
tests/compat/eingabe-taste-maus.out
Normal file
1
tests/compat/eingabe-taste-maus.out
Normal file
@@ -0,0 +1 @@
|
||||
KM
|
||||
2
tests/terminalfrei/.gitignore
vendored
Normal file
2
tests/terminalfrei/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
13
tests/terminalfrei/Cargo.toml
Normal file
13
tests/terminalfrei/Cargo.toml
Normal 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" }
|
||||
29
tests/terminalfrei/src/main.rs
Normal file
29
tests/terminalfrei/src/main.rs
Normal 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");
|
||||
}
|
||||
Reference in New Issue
Block a user