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-frontend.workspace = true
|
||||||
tb-vm.workspace = true
|
tb-vm.workspace = true
|
||||||
tb-runtime.workspace = true
|
tb-runtime.workspace = true
|
||||||
tb-ui.workspace = true
|
tb-ui = { workspace = true, features = ["terminal"] }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|||||||
@@ -551,6 +551,20 @@ impl Host for PipeHost {
|
|||||||
self.puffer.pop_front()
|
self.puffer.pop_front()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
|
||||||
|
if let Some(event) = self.next_event(false) {
|
||||||
|
return Some(event);
|
||||||
|
}
|
||||||
|
if let Some(deadline) = deadline_ms {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(
|
||||||
|
deadline.saturating_sub(self.jetzt_ms()),
|
||||||
|
));
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.next_event(true).or(Some(Ereignis::Ende))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn jetzt_ms(&mut self) -> u64 {
|
fn jetzt_ms(&mut self) -> u64 {
|
||||||
self.start.elapsed().as_millis() as u64
|
self.start.elapsed().as_millis() as u64
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,6 +265,12 @@ impl tb_runtime::host::Host for EreignisHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
|
||||||
|
if let Some(event) = self.next_event(false) {
|
||||||
|
return Some(event);
|
||||||
|
}
|
||||||
|
self.inner.warten(deadline_ms)
|
||||||
|
}
|
||||||
fn jetzt_ms(&mut self) -> u64 {
|
fn jetzt_ms(&mut self) -> u64 {
|
||||||
self.inner.jetzt_ms()
|
self.inner.jetzt_ms()
|
||||||
}
|
}
|
||||||
@@ -318,6 +324,12 @@ impl tb_runtime::host::Host for UhrHost {
|
|||||||
fn next_event(&mut self, blockierend: bool) -> Option<tb_runtime::host::Ereignis> {
|
fn next_event(&mut self, blockierend: bool) -> Option<tb_runtime::host::Ereignis> {
|
||||||
self.inner.next_event(blockierend)
|
self.inner.next_event(blockierend)
|
||||||
}
|
}
|
||||||
|
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
|
||||||
|
if let Some(event) = self.next_event(false) {
|
||||||
|
return Some(event);
|
||||||
|
}
|
||||||
|
self.inner.warten(deadline_ms)
|
||||||
|
}
|
||||||
fn jetzt_ms(&mut self) -> u64 {
|
fn jetzt_ms(&mut self) -> u64 {
|
||||||
let jetzt = self.inner.jetzt_ms();
|
let jetzt = self.inner.jetzt_ms();
|
||||||
self.inner.uhr_vorruecken(self.schritt_ms);
|
self.inner.uhr_vorruecken(self.schritt_ms);
|
||||||
|
|||||||
@@ -1882,7 +1882,7 @@ impl<'a> P<'a> {
|
|||||||
{
|
{
|
||||||
if matches!(
|
if matches!(
|
||||||
name.as_str(),
|
name.as_str(),
|
||||||
"TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" | "EVENT"
|
"TIMER" | "KEY" | "COM" | "PEN" | "PLAY" | "STRIG" | "UEVENT" | "SIGNAL" | "EVENT"
|
||||||
) {
|
) {
|
||||||
let action = match self.k() {
|
let action = match self.k() {
|
||||||
TokenKind::Kw(Kw::On) => Some(EventAction::On),
|
TokenKind::Kw(Kw::On) => Some(EventAction::On),
|
||||||
|
|||||||
@@ -28,14 +28,8 @@ pub struct RtState {
|
|||||||
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
|
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
|
||||||
pub isam: crate::isam::Isam,
|
pub isam: crate::isam::Isam,
|
||||||
pub print: PrintState,
|
pub print: PrintState,
|
||||||
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form, je mit
|
/// Hostreihenfolge bis zur Annahme durch Trap, Formular oder Konsole.
|
||||||
/// Umschaltzustand (Bitfeld 1 Umschalt, 2 Strg, 4 Alt). Der Zustand
|
pub eingaben: VecDeque<Eingabe>,
|
||||||
/// wird nur für die benutzerdefinierten Trap-Tasten gebraucht;
|
|
||||||
/// `INKEY$` und `INPUT` sehen ihn nicht.
|
|
||||||
pub tasten: VecDeque<(String, u8)>,
|
|
||||||
/// Mausereignisse für den Forms-Dispatch; sie werden erst an den
|
|
||||||
/// bestehenden Zustellpunkten von der VM entnommen.
|
|
||||||
pub maus: VecDeque<crate::host::MausEreignis>,
|
|
||||||
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
|
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
|
||||||
pub abbruch: bool,
|
pub abbruch: bool,
|
||||||
/// Eingabeende wurde gemeldet.
|
/// Eingabeende wurde gemeldet.
|
||||||
@@ -66,6 +60,23 @@ pub struct RtState {
|
|||||||
pub clipboard: String,
|
pub clipboard: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Eine noch nicht verbrauchte Eingabe. Unter KEY STOP wird der Trap
|
||||||
|
/// einmal gemerkt; die Taste bleibt danach unabhängig vom Trap lesbar.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Eingabe {
|
||||||
|
pub ereignis: Ereignis,
|
||||||
|
trap_gemerkt: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Ereignis> for Eingabe {
|
||||||
|
fn from(ereignis: Ereignis) -> Self {
|
||||||
|
Self {
|
||||||
|
ereignis,
|
||||||
|
trap_gemerkt: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for RtState {
|
impl Default for RtState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
RtState {
|
RtState {
|
||||||
@@ -73,8 +84,7 @@ impl Default for RtState {
|
|||||||
dateien: Default::default(),
|
dateien: Default::default(),
|
||||||
isam: Default::default(),
|
isam: Default::default(),
|
||||||
print: PrintState::default(),
|
print: PrintState::default(),
|
||||||
tasten: VecDeque::new(),
|
eingaben: VecDeque::new(),
|
||||||
maus: VecDeque::new(),
|
|
||||||
abbruch: false,
|
abbruch: false,
|
||||||
ende: false,
|
ende: false,
|
||||||
// Das Vorbild belegt die Makros im Interpreter vor; ein
|
// Das Vorbild belegt die Makros im Interpreter vor; ein
|
||||||
@@ -111,24 +121,28 @@ impl RtState {
|
|||||||
let Some(e) = host.next_event(blockierend) else {
|
let Some(e) = host.next_event(blockierend) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
match e {
|
self.ereignis(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),
|
|
||||||
}
|
|
||||||
if blockierend {
|
if blockierend {
|
||||||
return;
|
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.
|
/// Bildschirm anzeigen und anstehende Ereignisse einsammeln.
|
||||||
/// Zustellpunkt an Tick-Grenzen und vor blockierender Eingabe.
|
/// Zustellpunkt an Tick-Grenzen und vor blockierender Eingabe.
|
||||||
pub fn tick(&mut self, host: &mut dyn Host) {
|
pub fn tick(&mut self, host: &mut dyn Host) {
|
||||||
@@ -147,45 +161,80 @@ impl RtState {
|
|||||||
if !self.traps.aktiv() {
|
if !self.traps.aktiv() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let jetzt = host.jetzt_ms();
|
if self.traps.naechste_frist().is_some() {
|
||||||
self.traps.zeit_pruefen(jetzt);
|
self.traps.zeit_pruefen(host.jetzt_ms());
|
||||||
|
}
|
||||||
self.tasten_traps_pruefen();
|
self.tasten_traps_pruefen();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Getrappte Tasten aus der Schlange nehmen. Ohne Host, damit die VM
|
/// Getrappte Tasten aus der Schlange nehmen. Ohne Host, damit die VM
|
||||||
/// das an jeder Anweisungsgrenze tun kann, ohne die Uhr zu lesen.
|
/// das an jeder Anweisungsgrenze tun kann, ohne die Uhr zu lesen.
|
||||||
pub fn tasten_traps_pruefen(&mut self) {
|
pub fn tasten_traps_pruefen(&mut self) {
|
||||||
// Nur der Kopf der Schlange: die Reihenfolge der Eingabe bleibt
|
while self.taste_trappen(0) {}
|
||||||
// erhalten, und eine getrappte Taste hinter einer ungetrappten
|
}
|
||||||
// wartet, bis diese gelesen ist.
|
|
||||||
while let Some((s, shift)) = self.tasten.front() {
|
/// Prüft eine wartende Taste genau bis zu ihrer Annahme. ON verbraucht
|
||||||
let Some(code) = crate::host::taste::scancode_von(s) else {
|
/// sie, STOP merkt den Trap einmal und belässt sie im Eingabestrom.
|
||||||
break;
|
pub fn taste_trappen(&mut self, index: usize) -> bool {
|
||||||
};
|
if !self.traps.aktiv() {
|
||||||
let Some(n) = self.traps.taste_faengt(code, *shift) else {
|
return false;
|
||||||
break;
|
}
|
||||||
};
|
let Some(eingabe) = self.eingaben.get_mut(index) else {
|
||||||
self.tasten.pop_front();
|
return false;
|
||||||
self.traps.melden(crate::traps::Quelle::Key(n));
|
};
|
||||||
|
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`.
|
/// Nur bereits gepufferte Tasten annehmen, ohne weitere Hostereignisse
|
||||||
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> {
|
/// abzuholen. Die VM kann zwischen Abholen und Blockieren zustellen.
|
||||||
loop {
|
pub fn gepufferte_taste(&mut self) -> Option<String> {
|
||||||
if let Some((t, _)) = self.tasten.pop_front() {
|
while let Some(i) = self
|
||||||
|
.eingaben
|
||||||
|
.iter()
|
||||||
|
.position(|e| matches!(e.ereignis, Ereignis::Taste(..)))
|
||||||
|
{
|
||||||
|
if self.taste_trappen(i) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ereignis::Taste(t, _) = self.eingaben.remove(i).unwrap().ereignis {
|
||||||
return Some(t);
|
return Some(t);
|
||||||
}
|
}
|
||||||
if self.ende {
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nächster Tastendruck; auch während blockierender Eingabe gelten
|
||||||
|
/// ON/OFF/STOP. Mausereignisse bleiben dem VM-Zustellpunkt vorbehalten.
|
||||||
|
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> {
|
||||||
|
let mut abgeholt = false;
|
||||||
|
loop {
|
||||||
|
if let Some(t) = self.gepufferte_taste() {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
if self.ende || self.abbruch || (!blockierend && abgeholt) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.pump(host, blockierend);
|
self.pump(host, blockierend);
|
||||||
if !blockierend && self.tasten.is_empty() {
|
abgeholt = true;
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if blockierend && self.tasten.is_empty() && self.ende {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2367,12 +2416,24 @@ fn bi_sleep(
|
|||||||
a: &mut [Value],
|
a: &mut [Value],
|
||||||
) -> Result<Option<Value>, RuntimeError> {
|
) -> Result<Option<Value>, RuntimeError> {
|
||||||
let secs = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? };
|
let secs = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? };
|
||||||
// SLEEP n wartet n Sekunden; SLEEP ohne Argument bis zum Tastendruck.
|
// Der Bytecode nutzt Instr::Sleep für Handlerzustellung. Auch direkte
|
||||||
|
// Bibliotheksaufrufe dürfen keine eigene reale Uhr verwenden.
|
||||||
|
let deadline = (secs > 0.0).then(|| host.jetzt_ms().saturating_add((secs * 1000.0) as u64));
|
||||||
st.tick(host);
|
st.tick(host);
|
||||||
if secs > 0.0 {
|
loop {
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f64(secs));
|
if st.ende || st.abbruch {
|
||||||
} else {
|
break;
|
||||||
st.naechste_taste(host, true);
|
}
|
||||||
|
if let Some(deadline) = deadline {
|
||||||
|
if host.jetzt_ms() >= deadline {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if st.naechste_taste(host, false).is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(event) = host.warten(deadline) {
|
||||||
|
st.ereignis(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@@ -2777,6 +2838,60 @@ mod tests {
|
|||||||
assert!(h.next_event(false).is_none());
|
assert!(h.next_event(false).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signal_stop_merkt_beide_quellen_und_off_verwirft() {
|
||||||
|
use crate::host::{CaptureHost, Ereignis};
|
||||||
|
use crate::traps::{Quelle, Zustand};
|
||||||
|
for n in [1, 2] {
|
||||||
|
let mut st = RtState::default();
|
||||||
|
let mut host = CaptureHost::default();
|
||||||
|
st.traps.definieren(Quelle::Signal(n), Some(22));
|
||||||
|
st.traps.setzen(Quelle::Signal(n), Zustand::Gestoppt, 0);
|
||||||
|
for _ in 0..3 {
|
||||||
|
host.ereignis(Ereignis::Signal(n));
|
||||||
|
}
|
||||||
|
st.pump(&mut host, false);
|
||||||
|
assert!(!st.abbruch);
|
||||||
|
assert_eq!(st.traps.naechstes(), None);
|
||||||
|
st.traps.setzen(Quelle::Signal(n), Zustand::An, 0);
|
||||||
|
assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(n), 22)));
|
||||||
|
st.traps.handler_beendet(Quelle::Signal(n));
|
||||||
|
assert_eq!(st.traps.naechstes(), None);
|
||||||
|
st.traps.setzen(Quelle::Signal(n), Zustand::Gestoppt, 0);
|
||||||
|
host.ereignis(Ereignis::Signal(n));
|
||||||
|
st.pump(&mut host, false);
|
||||||
|
st.traps.setzen(Quelle::Signal(n), Zustand::Aus, 0);
|
||||||
|
st.traps.setzen(Quelle::Signal(n), Zustand::An, 0);
|
||||||
|
assert_eq!(st.traps.naechstes(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_stop_merkt_einmal_und_laesst_alle_tasten_auch_nach_on_lesbar() {
|
||||||
|
use crate::host::{CaptureHost, Ereignis};
|
||||||
|
use crate::traps::{Quelle, Zustand};
|
||||||
|
let mut st = RtState::default();
|
||||||
|
let mut host = CaptureHost::default();
|
||||||
|
st.traps.definieren(Quelle::Key(15), Some(22));
|
||||||
|
st.traps.benutzertaste(15, 0, 30);
|
||||||
|
st.traps.setzen(Quelle::Key(15), Zustand::Gestoppt, 0);
|
||||||
|
for _ in 0..3 {
|
||||||
|
host.ereignis(Ereignis::Taste("a".into(), 0));
|
||||||
|
}
|
||||||
|
st.pump(&mut host, false);
|
||||||
|
for i in 0..3 {
|
||||||
|
assert!(!st.taste_trappen(i));
|
||||||
|
}
|
||||||
|
assert_eq!(st.naechste_taste(&mut host, false).as_deref(), Some("a"));
|
||||||
|
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
|
||||||
|
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(15), 22)));
|
||||||
|
st.traps.handler_beendet(Quelle::Key(15));
|
||||||
|
for _ in 0..2 {
|
||||||
|
assert_eq!(st.naechste_taste(&mut host, false).as_deref(), Some("a"));
|
||||||
|
}
|
||||||
|
assert_eq!(st.traps.naechstes(), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sigint_ohne_aktiven_trap_bleibt_abbruch() {
|
fn sigint_ohne_aktiven_trap_bleibt_abbruch() {
|
||||||
use crate::host::Ereignis;
|
use crate::host::Ereignis;
|
||||||
@@ -2802,7 +2917,10 @@ mod tests {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
st.pump(&mut h, false);
|
st.pump(&mut h, false);
|
||||||
assert!(st.tasten.is_empty(), "Maus landet nicht im Tastenpuffer");
|
assert!(st
|
||||||
|
.eingaben
|
||||||
|
.iter()
|
||||||
|
.all(|e| matches!(e.ereignis, Ereignis::Maus(_))));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
//! oder das Zurücklesen per `SCREEN(z, s)` nicht transportieren
|
//! oder das Zurücklesen per `SCREEN(z, s)` nicht transportieren
|
||||||
//! (Entscheidung 2026-09-03, D2).
|
//! (Entscheidung 2026-09-03, D2).
|
||||||
//!
|
//!
|
||||||
//! Host-Aufrufe dürfen blockieren; kooperative Zustellpunkte bringt erst die
|
//! Der Host besitzt Zeit und Warten; die VM liefert nur an kooperativen
|
||||||
//! Ereignisschleife der Forms-Engine (Phase 4).
|
//! Zustellpunkten aus.
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
@@ -165,6 +165,12 @@ pub trait Host {
|
|||||||
/// wenn nichts anliegt (Grundlage von `INKEY$`).
|
/// wenn nichts anliegt (Grundlage von `INKEY$`).
|
||||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis>;
|
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis>;
|
||||||
|
|
||||||
|
/// Wartet auf Eingabe oder eine absolute Deadline in `jetzt_ms`-Zeit.
|
||||||
|
/// `None` bedeutet: Deadline erreicht. Ohne Deadline wird bis zu einem
|
||||||
|
/// Ereignis (gegebenenfalls explizitem Eingabeende) gewartet. Virtuelle
|
||||||
|
/// Hosts steuern den Zeitfortschritt selbst; keine reale Rückfalluhr.
|
||||||
|
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis>;
|
||||||
|
|
||||||
/// Monotone Zeit in Millisekunden seit Programmstart.
|
/// Monotone Zeit in Millisekunden seit Programmstart.
|
||||||
///
|
///
|
||||||
/// Die Fälligkeit der Zeit-Traps rechnet die Laufzeit daraus aus; sie
|
/// Die Fälligkeit der Zeit-Traps rechnet die Laufzeit daraus aus; sie
|
||||||
@@ -260,6 +266,19 @@ impl Host for CaptureHost {
|
|||||||
self.ereignisse.pop_front()
|
self.ereignisse.pop_front()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
|
||||||
|
if let Some(event) = self.next_event(false) {
|
||||||
|
return Some(event);
|
||||||
|
}
|
||||||
|
match deadline_ms {
|
||||||
|
Some(deadline) => {
|
||||||
|
self.uhr_ms = self.uhr_ms.max(deadline);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
None => Some(Ereignis::Ende),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn jetzt_ms(&mut self) -> u64 {
|
fn jetzt_ms(&mut self) -> u64 {
|
||||||
self.uhr_ms
|
self.uhr_ms
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ pub struct Traps {
|
|||||||
/// `EVENT OFF` klammert einen Abschnitt ohne Ereignisprüfung. Kein
|
/// `EVENT OFF` klammert einen Abschnitt ohne Ereignisprüfung. Kein
|
||||||
/// dritter Zustand — den kennt das Vorbild nicht.
|
/// dritter Zustand — den kennt das Vorbild nicht.
|
||||||
event_an: bool,
|
event_an: bool,
|
||||||
|
/// Menüsperre ohne Änderung der expliziten ON/OFF/STOP-Zustände.
|
||||||
|
suspendiert: bool,
|
||||||
/// Fälligkeit des Zeit-Traps: Intervall und der Zeitpunkt, an dem er
|
/// Fälligkeit des Zeit-Traps: Intervall und der Zeitpunkt, an dem er
|
||||||
/// das nächste Mal fällig wird (beides in Millisekunden).
|
/// das nächste Mal fällig wird (beides in Millisekunden).
|
||||||
timer_intervall_ms: u64,
|
timer_intervall_ms: u64,
|
||||||
@@ -178,6 +180,27 @@ impl Traps {
|
|||||||
self.aktiv
|
self.aktiv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn suspendieren(&mut self, gesperrt: bool) {
|
||||||
|
self.suspendiert = gesperrt;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ist_an(&self, q: Quelle) -> bool {
|
||||||
|
self.traps
|
||||||
|
.iter()
|
||||||
|
.any(|t| t.quelle == q && t.zustand == Zustand::An)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nächste Hostfrist, sofern der Zeit-Trap überhaupt Zeit benötigt.
|
||||||
|
pub fn naechste_frist(&self) -> Option<u64> {
|
||||||
|
(self.event_an
|
||||||
|
&& !self.suspendiert
|
||||||
|
&& self.timer_intervall_ms > 0
|
||||||
|
&& self.traps.iter().any(|t| {
|
||||||
|
t.quelle == Quelle::Timer && t.ziel.is_some() && t.zustand != Zustand::Aus
|
||||||
|
}))
|
||||||
|
.then_some(self.timer_faellig_ms)
|
||||||
|
}
|
||||||
|
|
||||||
/// Benutzerdefinierte Taste erklären (`KEY n, CHR$(flag)+CHR$(scan)`,
|
/// Benutzerdefinierte Taste erklären (`KEY n, CHR$(flag)+CHR$(scan)`,
|
||||||
/// n = 15–25). `flag` ist das Tastaturflagbyte des Vorbilds.
|
/// n = 15–25). `flag` ist das Tastaturflagbyte des Vorbilds.
|
||||||
pub fn benutzertaste(&mut self, n: u8, flag: u8, scancode: u8) {
|
pub fn benutzertaste(&mut self, n: u8, flag: u8, scancode: u8) {
|
||||||
@@ -193,7 +216,7 @@ impl Traps {
|
|||||||
/// Liefert die Trap-Nummer, nicht nur „ja/nein": der Aufrufer muss
|
/// Liefert die Trap-Nummer, nicht nur „ja/nein": der Aufrufer muss
|
||||||
/// wissen, welche Quelle er melden soll.
|
/// wissen, welche Quelle er melden soll.
|
||||||
pub fn taste_faengt(&self, scancode: u8, shift: u8) -> Option<u8> {
|
pub fn taste_faengt(&self, scancode: u8, shift: u8) -> Option<u8> {
|
||||||
if !self.event_an {
|
if !self.event_an || self.suspendiert {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let treffer = |k: u8| -> bool {
|
let treffer = |k: u8| -> bool {
|
||||||
@@ -218,7 +241,7 @@ impl Traps {
|
|||||||
/// Nächstes zuzustellendes Ereignis: Quelle und Sprungziel. Liefert
|
/// Nächstes zuzustellendes Ereignis: Quelle und Sprungziel. Liefert
|
||||||
/// höchstens eines je Aufruf; die übrigen bleiben anstehend.
|
/// höchstens eines je Aufruf; die übrigen bleiben anstehend.
|
||||||
pub fn naechstes(&mut self) -> Option<(Quelle, u32)> {
|
pub fn naechstes(&mut self) -> Option<(Quelle, u32)> {
|
||||||
if !self.event_an {
|
if !self.event_an || self.suspendiert {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut kandidaten: Vec<usize> = (0..self.traps.len())
|
let mut kandidaten: Vec<usize> = (0..self.traps.len())
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tb-ui"
|
name = "tb-ui"
|
||||||
description = "Terminal Basic: Bildschirm-Runtime und Forms-Engine auf Ratatui"
|
description = "Terminal Basic: Forms-Engine mit optionalem Terminal-Backend"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
terminal = ["dep:ratatui", "dep:crossterm", "dep:signal-hook"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tb-runtime.workspace = true
|
tb-runtime.workspace = true
|
||||||
tb-frontend.workspace = true
|
tb-frontend.workspace = true
|
||||||
ratatui.workspace = true
|
ratatui = { workspace = true, optional = true }
|
||||||
crossterm.workspace = true
|
crossterm = { workspace = true, optional = true }
|
||||||
signal-hook.workspace = true
|
signal-hook = { workspace = true, optional = true }
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -1777,6 +1777,49 @@ impl FormsModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn timer_enabled(&self, key: ObjectKey) -> bool {
|
||||||
|
self.instance(key)
|
||||||
|
.is_ok_and(|obj| obj.description.class == ObjectClass::Timer)
|
||||||
|
&& self.boolean(key, "ENABLED")
|
||||||
|
&& self.integer(key, "INTERVAL").unwrap_or(0) > 0
|
||||||
|
&& self
|
||||||
|
.root_form(key)
|
||||||
|
.is_some_and(|form| self.is_visible(form))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zeitbedarf für Timer und gedrückt gehaltene Spin-Schaltflächen.
|
||||||
|
pub fn next_deadline(&self) -> Option<u64> {
|
||||||
|
if self.menu_is_open() || (!self.has_visible_forms() && self.spin_repeat.is_none()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let timers = self
|
||||||
|
.keys()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|key| self.timer_enabled(*key))
|
||||||
|
.map(|key| {
|
||||||
|
self.timer_last
|
||||||
|
.get(&key)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(0)
|
||||||
|
.saturating_add(self.integer(key, "INTERVAL").unwrap() as u64)
|
||||||
|
});
|
||||||
|
let spin = self.spin_repeat.and_then(|(key, _)| {
|
||||||
|
let interval = self.integer(key, "INTERVAL").unwrap_or(250).max(0) as u64;
|
||||||
|
(interval > 0).then(|| {
|
||||||
|
self.spin_last
|
||||||
|
.map_or(0, |last| last.saturating_add(interval))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
timers.chain(spin).min()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mouse_needs_time(&self, event: MausEreignis) -> bool {
|
||||||
|
self.active_form.is_some()
|
||||||
|
&& !self.menu_is_open()
|
||||||
|
&& event.art == MausArt::Loslassen
|
||||||
|
&& self.pressed.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn timers(&mut self, now_ms: u64) {
|
pub fn timers(&mut self, now_ms: u64) {
|
||||||
if !self.menu_path.is_empty() {
|
if !self.menu_path.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -1784,15 +1827,7 @@ impl FormsModel {
|
|||||||
let mut due: Vec<_> = self
|
let mut due: Vec<_> = self
|
||||||
.keys()
|
.keys()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|key| {
|
.filter(|key| self.timer_enabled(*key))
|
||||||
self.instance(*key)
|
|
||||||
.is_ok_and(|obj| obj.description.class == ObjectClass::Timer)
|
|
||||||
&& self.boolean(*key, "ENABLED")
|
|
||||||
&& self.integer(*key, "INTERVAL").unwrap_or(0) > 0
|
|
||||||
&& self
|
|
||||||
.root_form(*key)
|
|
||||||
.is_some_and(|form| self.is_visible(form))
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
due.sort_by_key(|key| {
|
due.sort_by_key(|key| {
|
||||||
self.instance(*key)
|
self.instance(*key)
|
||||||
@@ -2395,13 +2430,16 @@ fn dialog_event(
|
|||||||
model: &mut FormsModel,
|
model: &mut FormsModel,
|
||||||
screen: &mut TextScreen,
|
screen: &mut TextScreen,
|
||||||
host: &mut dyn Host,
|
host: &mut dyn Host,
|
||||||
queued: &mut VecDeque<Ereignis>,
|
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||||
plain_access_key: bool,
|
plain_access_key: bool,
|
||||||
) -> Option<FormEvent> {
|
) -> Option<FormEvent> {
|
||||||
loop {
|
loop {
|
||||||
model.render(screen);
|
model.render(screen);
|
||||||
host.present(screen);
|
host.present(screen);
|
||||||
let event = queued.pop_front().or_else(|| host.next_event(true))?;
|
let event = queued
|
||||||
|
.pop_front()
|
||||||
|
.map(|e| e.ereignis)
|
||||||
|
.or_else(|| host.next_event(true))?;
|
||||||
match event {
|
match event {
|
||||||
Ereignis::Taste(key, shift) => {
|
Ereignis::Taste(key, shift) => {
|
||||||
let shift = if plain_access_key && key.chars().count() == 1 {
|
let shift = if plain_access_key && key.chars().count() == 1 {
|
||||||
@@ -2412,14 +2450,19 @@ fn dialog_event(
|
|||||||
model.handle_key(&key, shift);
|
model.handle_key(&key, shift);
|
||||||
}
|
}
|
||||||
Ereignis::Maus(event) => {
|
Ereignis::Maus(event) => {
|
||||||
model.handle_mouse_at(event, host.jetzt_ms());
|
let now = if model.mouse_needs_time(event) {
|
||||||
|
host.jetzt_ms()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
model.handle_mouse_at(event, now);
|
||||||
}
|
}
|
||||||
Ereignis::Groesse { cols, rows } => {
|
Ereignis::Groesse { cols, rows } => {
|
||||||
screen.resize(cols, rows);
|
screen.resize(cols, rows);
|
||||||
model.resize(cols, rows);
|
model.resize(cols, rows);
|
||||||
}
|
}
|
||||||
event @ (Ereignis::Abbruch | Ereignis::Ende | Ereignis::Signal(_)) => {
|
event @ (Ereignis::Abbruch | Ereignis::Ende | Ereignis::Signal(_)) => {
|
||||||
queued.push_front(event);
|
queued.push_front(event.into());
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2435,7 +2478,7 @@ fn dialog_event(
|
|||||||
pub fn msgbox_dialog(
|
pub fn msgbox_dialog(
|
||||||
screen: &mut TextScreen,
|
screen: &mut TextScreen,
|
||||||
host: &mut dyn Host,
|
host: &mut dyn Host,
|
||||||
queued: &mut VecDeque<Ereignis>,
|
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||||
text: &str,
|
text: &str,
|
||||||
kind: i32,
|
kind: i32,
|
||||||
title: &str,
|
title: &str,
|
||||||
@@ -2549,7 +2592,7 @@ pub fn msgbox_dialog(
|
|||||||
pub fn inputbox_dialog(
|
pub fn inputbox_dialog(
|
||||||
screen: &mut TextScreen,
|
screen: &mut TextScreen,
|
||||||
host: &mut dyn Host,
|
host: &mut dyn Host,
|
||||||
queued: &mut VecDeque<Ereignis>,
|
queued: &mut VecDeque<tb_runtime::builtins::Eingabe>,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
title: &str,
|
title: &str,
|
||||||
initial: &str,
|
initial: &str,
|
||||||
|
|||||||
@@ -86,7 +86,33 @@ impl Host for TerminalHost {
|
|||||||
self.start.elapsed().as_millis() as u64
|
self.start.elapsed().as_millis() as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<Ereignis> {
|
||||||
|
loop {
|
||||||
|
if let Some(e) = self.next_event(false) {
|
||||||
|
return Some(e);
|
||||||
|
}
|
||||||
|
let timeout = match deadline_ms {
|
||||||
|
Some(deadline) => {
|
||||||
|
let rest = deadline.saturating_sub(self.jetzt_ms());
|
||||||
|
if rest == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Duration::from_millis(rest.min(10))
|
||||||
|
}
|
||||||
|
None => Duration::from_millis(10),
|
||||||
|
};
|
||||||
|
// Begrenzt warten, damit auch SIGINT/SIGTERM ohne Terminaltaste
|
||||||
|
// abgeholt werden. Die VM kennt dieses Backend-Intervall nicht.
|
||||||
|
if event::poll(timeout).is_err() {
|
||||||
|
return Some(Ereignis::Ende);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
|
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
|
||||||
|
if blockierend {
|
||||||
|
return self.warten(None);
|
||||||
|
}
|
||||||
loop {
|
loop {
|
||||||
// Signale vor dem Terminal: sie liegen schon an, wenn wir hier
|
// Signale vor dem Terminal: sie liegen schon an, wenn wir hier
|
||||||
// ankommen, und ein blockierendes `read` wuerde sie liegenlassen.
|
// ankommen, und ein blockierendes `read` wuerde sie liegenlassen.
|
||||||
@@ -181,7 +207,7 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
|||||||
KeyCode::Enter => taste::ENTER.to_string(),
|
KeyCode::Enter => taste::ENTER.to_string(),
|
||||||
KeyCode::Backspace => taste::BACKSPACE.to_string(),
|
KeyCode::Backspace => taste::BACKSPACE.to_string(),
|
||||||
KeyCode::Esc => taste::ESC.to_string(),
|
KeyCode::Esc => taste::ESC.to_string(),
|
||||||
KeyCode::Tab => taste::TAB.to_string(),
|
KeyCode::Tab | KeyCode::BackTab => taste::TAB.to_string(),
|
||||||
// Sondertasten in der Kodierung des Vorbilds: Nullzeichen + Kennung.
|
// Sondertasten in der Kodierung des Vorbilds: Nullzeichen + Kennung.
|
||||||
KeyCode::F(n @ 1..=10) => taste::sonder(58 + n),
|
KeyCode::F(n @ 1..=10) => taste::sonder(58 + n),
|
||||||
KeyCode::Home => taste::sonder(71),
|
KeyCode::Home => taste::sonder(71),
|
||||||
@@ -197,7 +223,7 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
|||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let mut shift = 0u8;
|
let mut shift = 0u8;
|
||||||
if k.modifiers.contains(KeyModifiers::SHIFT) {
|
if k.modifiers.contains(KeyModifiers::SHIFT) || k.code == KeyCode::BackTab {
|
||||||
shift |= umschalt::SHIFT;
|
shift |= umschalt::SHIFT;
|
||||||
}
|
}
|
||||||
if k.modifiers.contains(KeyModifiers::CONTROL) {
|
if k.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
@@ -213,6 +239,16 @@ fn taste_zu_ereignis(k: KeyEvent) -> Option<Ereignis> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backtab_ist_tab_mit_shift_auch_ohne_modifier() {
|
||||||
|
for modifiers in [KeyModifiers::NONE, KeyModifiers::SHIFT, KeyModifiers::ALT] {
|
||||||
|
assert_eq!(
|
||||||
|
taste_zu_ereignis(KeyEvent::new(KeyCode::BackTab, modifiers)),
|
||||||
|
taste_zu_ereignis(KeyEvent::new(KeyCode::Tab, modifiers | KeyModifiers::SHIFT))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zeichentaste_wird_zu_inkey_form() {
|
fn zeichentaste_wird_zu_inkey_form() {
|
||||||
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
|
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
//! Bildschirm-Runtime und Forms-Engine von Terminal Basic, aufbauend auf Ratatui.
|
//! Terminalunabhängiges Forms-Modell, FRM-Reader und Dialoge.
|
||||||
//!
|
//!
|
||||||
//! Zwei Schichten:
|
//! Die Ausführung verwendet `tb_runtime::host::Host` und dessen Zellenpuffer.
|
||||||
//! 1. `screen`: Emulation des 80×25-Textbildschirms (Unicode/UTF-8, 16 Farben,
|
//! Das optionale Feature `terminal` ergänzt Anzeige über Ratatui,
|
||||||
//! Cursor) als Zeichenpuffer, gerendert über Ratatui. Darauf setzen die
|
//! crossterm-Eingaben und Betriebssystemsignale.
|
||||||
//! klassischen Anweisungen `PRINT`, `LOCATE`, `COLOR`, `CLS`, `INPUT` auf.
|
|
||||||
//! 2. `forms`: ereignisgesteuerte Forms-Engine — Fenster, Steuerelemente
|
|
||||||
//! (Schaltflächen, Textfelder, Listen …), Menüs, Fokusreihenfolge,
|
|
||||||
//! Tastatur- und Mausereignisse.
|
|
||||||
|
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod forms;
|
pub mod forms;
|
||||||
pub mod frm;
|
pub mod frm;
|
||||||
mod frm_pcode;
|
mod frm_pcode;
|
||||||
|
#[cfg(feature = "terminal")]
|
||||||
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
|
pub mod host; // Terminal-Host: Anzeige + Tastatur-/Größenereignisse
|
||||||
|
#[cfg(feature = "terminal")]
|
||||||
pub mod screen;
|
pub mod screen;
|
||||||
|
#[cfg(feature = "terminal")]
|
||||||
pub mod signale; // Betriebssystemsignale als Ereignisquelle (SIGNAL)
|
pub mod signale; // Betriebssystemsignale als Ereignisquelle (SIGNAL)
|
||||||
|
|||||||
@@ -78,11 +78,27 @@ struct Frame {
|
|||||||
waiting_form: Option<u16>,
|
waiting_form: Option<u16>,
|
||||||
pending_show: Option<u16>,
|
pending_show: Option<u16>,
|
||||||
form_event: Option<FormEventReturn>,
|
form_event: Option<FormEventReturn>,
|
||||||
|
handler_start: bool,
|
||||||
|
eingabe: Option<KonsolenEingabe>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct KonsolenEingabe {
|
||||||
|
text: String,
|
||||||
|
/// None = Zeile bis Enter; Some = noch erwartete Tasten für INPUT$.
|
||||||
|
rest: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
enum Zustellpunkt {
|
||||||
|
Anweisung,
|
||||||
|
Kooperativ,
|
||||||
|
Konsole,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum FormEventReturn {
|
enum FormEventReturn {
|
||||||
Normal,
|
Normal,
|
||||||
|
Load,
|
||||||
Unload(u16),
|
Unload(u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +241,8 @@ impl Vm {
|
|||||||
waiting_form: None,
|
waiting_form: None,
|
||||||
pending_show: None,
|
pending_show: None,
|
||||||
form_event: None,
|
form_event: None,
|
||||||
|
handler_start: false,
|
||||||
|
eingabe: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,36 +280,15 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_dialog_input(&mut self) -> VecDeque<Ereignis> {
|
fn take_dialog_input(&mut self) -> VecDeque<tb_runtime::builtins::Eingabe> {
|
||||||
let mut events = VecDeque::new();
|
std::mem::take(&mut self.rt.eingaben)
|
||||||
events.extend(
|
|
||||||
self.rt
|
|
||||||
.tasten
|
|
||||||
.drain(..)
|
|
||||||
.map(|(key, shift)| Ereignis::Taste(key, shift)),
|
|
||||||
);
|
|
||||||
events.extend(self.rt.maus.drain(..).map(Ereignis::Maus));
|
|
||||||
events
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_dialog_input(&mut self, events: VecDeque<Ereignis>) {
|
fn restore_dialog_input(&mut self, events: VecDeque<tb_runtime::builtins::Eingabe>) {
|
||||||
for event in events {
|
for input in events {
|
||||||
match event {
|
match input.ereignis {
|
||||||
Ereignis::Taste(key, shift) => self.rt.tasten.push_back((key, shift)),
|
Ereignis::Taste(..) | Ereignis::Maus(_) => self.rt.eingaben.push_back(input),
|
||||||
Ereignis::Maus(event) => self.rt.maus.push_back(event),
|
event => self.rt.ereignis(event),
|
||||||
Ereignis::Abbruch => self.rt.abbruch = true,
|
|
||||||
Ereignis::Ende => self.rt.ende = true,
|
|
||||||
Ereignis::Signal(number) => {
|
|
||||||
if number == 1
|
|
||||||
&& !self
|
|
||||||
.rt
|
|
||||||
.traps
|
|
||||||
.melden(tb_runtime::traps::Quelle::Signal(number))
|
|
||||||
{
|
|
||||||
self.rt.abbruch = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ereignis::Groesse { cols, rows } => self.rt.screen.resize(cols, rows),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,13 +560,47 @@ impl Vm {
|
|||||||
);
|
);
|
||||||
if let Some(frame) = self.frames.last_mut() {
|
if let Some(frame) = self.frames.last_mut() {
|
||||||
frame.form_event = Some(on_return);
|
frame.form_event = Some(on_return);
|
||||||
|
frame.handler_start = true;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_next_form_event(&mut self) -> bool {
|
fn dispatch_next_form_event(&mut self, load_only: bool) -> bool {
|
||||||
while let Some(event) = self.forms.next_event() {
|
let mut i = 0;
|
||||||
if self.dispatch_form_event(event, FormEventReturn::Normal) {
|
while let Some(event) = self.forms.events.get(i) {
|
||||||
|
if load_only && !event.name.eq_ignore_ascii_case("LOAD") {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let laeuft = self
|
||||||
|
.module
|
||||||
|
.event_procs
|
||||||
|
.iter()
|
||||||
|
.find(|binding| {
|
||||||
|
binding.object == event.object
|
||||||
|
&& binding.event.eq_ignore_ascii_case(&event.name)
|
||||||
|
})
|
||||||
|
.is_some_and(|binding| {
|
||||||
|
self.frames.iter().any(|frame| {
|
||||||
|
frame.form_event.is_some() && frame.proc == binding.proc as usize
|
||||||
|
})
|
||||||
|
});
|
||||||
|
// Gesperrte Timer und laufende Prozeduren bleiben anstehend.
|
||||||
|
// Menübedienung und explizite modale Kooperation müssen trotzdem
|
||||||
|
// andere Handler erreichen können.
|
||||||
|
if laeuft || (self.forms.menu_is_open() && event.name.eq_ignore_ascii_case("TIMER")) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let event = self.forms.events.remove(i).unwrap();
|
||||||
|
if self.dispatch_form_event(
|
||||||
|
event,
|
||||||
|
if load_only {
|
||||||
|
FormEventReturn::Load
|
||||||
|
} else {
|
||||||
|
FormEventReturn::Normal
|
||||||
|
},
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -577,6 +608,12 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn request_unload(&mut self, object: u16) -> Result<(), RuntimeError> {
|
fn request_unload(&mut self, object: u16) -> Result<(), RuntimeError> {
|
||||||
|
if self.frames.iter().any(|frame| {
|
||||||
|
matches!(frame.form_event,
|
||||||
|
Some(FormEventReturn::Unload(running)) if running == object)
|
||||||
|
}) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let event = FormEvent {
|
let event = FormEvent {
|
||||||
object,
|
object,
|
||||||
array_index: None,
|
array_index: None,
|
||||||
@@ -590,80 +627,136 @@ impl Vm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn tick(&mut self, host: &mut dyn Host) {
|
fn tick(&mut self, host: &mut dyn Host) {
|
||||||
|
self.rt.traps.suspendieren(self.forms.menu_is_open());
|
||||||
self.forms.render(&mut self.rt.screen);
|
self.forms.render(&mut self.rt.screen);
|
||||||
if self.forms.menu_is_open() {
|
host.present(&self.rt.screen);
|
||||||
host.present(&self.rt.screen);
|
self.rt.pump(host, false);
|
||||||
self.rt.pump(host, false);
|
if !self.forms.menu_is_open() && self.rt.traps.naechste_frist().is_some() {
|
||||||
} else {
|
self.rt.traps.zeit_pruefen(host.jetzt_ms());
|
||||||
self.rt.tick(host);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn forms_zustellen(&mut self, host: &mut dyn Host) -> bool {
|
/// Ein gemeinsamer Zustellpfad für Statements, DOEVENTS, SLEEP und
|
||||||
self.forms
|
/// blockierende Eingabe. Nur explizite Kooperation verschachtelt Forms.
|
||||||
.resize(self.rt.screen.cols(), self.rt.screen.rows());
|
fn zustellen(&mut self, host: &mut dyn Host, punkt: Zustellpunkt) -> bool {
|
||||||
self.forms.timers(host.jetzt_ms());
|
let kooperativ = punkt != Zustellpunkt::Anweisung;
|
||||||
if self.forms.active_form().is_some() {
|
'zustellung: loop {
|
||||||
while let Some((key, shift)) = self.rt.tasten.pop_front() {
|
self.rt.traps.suspendieren(self.forms.menu_is_open());
|
||||||
self.forms.handle_key(&key, shift);
|
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 forms_frei = kooperativ || !self.frames.iter().any(|f| f.form_event.is_some());
|
||||||
let now_ms = host.jetzt_ms();
|
if forms_frei && self.dispatch_next_form_event(false) {
|
||||||
while let Some(m) = self.rt.maus.pop_front() {
|
return true;
|
||||||
self.forms.handle_mouse_at(m, now_ms);
|
}
|
||||||
}
|
if self.rt.traps.aktiv() {
|
||||||
self.forms.render(&mut self.rt.screen);
|
self.rt.tasten_traps_pruefen();
|
||||||
self.dispatch_next_form_event()
|
if (kooperativ || self.tick_zaehler.is_multiple_of(64))
|
||||||
}
|
&& self.rt.traps.naechste_frist().is_some()
|
||||||
|
{
|
||||||
/// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert
|
self.rt.traps.zeit_pruefen(host.jetzt_ms());
|
||||||
/// `true`, wenn ein Handler aufgesetzt wurde.
|
}
|
||||||
fn zustellen(&mut self, host: &mut dyn Host) -> bool {
|
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
||||||
if self.forms.menu_is_open() || !self.rt.traps.aktiv() {
|
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;
|
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
|
fn warten(&mut self, host: &mut dyn Host, deadline: Option<u64>) {
|
||||||
/// Tastendruck. Ein zugestelltes Ereignis beendet das Warten
|
let deadline = deadline
|
||||||
/// (Sprachreferenz §8) — der Handler läuft, und danach geht es hinter
|
.into_iter()
|
||||||
/// dem `SLEEP` weiter.
|
.chain(self.forms.next_deadline())
|
||||||
|
.chain(if self.forms.menu_is_open() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.rt.traps.naechste_frist()
|
||||||
|
})
|
||||||
|
.min();
|
||||||
|
if let Some(event) = host.warten(deadline) {
|
||||||
|
self.rt.ereignis(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ein zugestelltes Ereignis beendet SLEEP; Fristen und Warten gehören
|
||||||
|
/// ausschließlich dem Host, einschließlich einer virtuellen Uhr.
|
||||||
fn schlafen(&mut self, host: &mut dyn Host, sekunden: f64) {
|
fn schlafen(&mut self, host: &mut dyn Host, sekunden: f64) {
|
||||||
let start = host.jetzt_ms();
|
let deadline =
|
||||||
let dauer_ms = (sekunden.max(0.0) * 1000.0) as u64;
|
(sekunden > 0.0).then(|| host.jetzt_ms().saturating_add((sekunden * 1000.0) as u64));
|
||||||
// ponytail: zusätzlich eine echte Zeitschranke, damit ein Host mit
|
|
||||||
// stehender Uhr (Testhost) nicht ewig wartet. Ceiling: bei einer
|
|
||||||
// virtuellen Uhr endet `SLEEP` nach der realen Dauer, wenn die
|
|
||||||
// virtuelle nie vorrückt.
|
|
||||||
let real_start = std::time::Instant::now();
|
|
||||||
loop {
|
loop {
|
||||||
self.tick(host);
|
self.tick(host);
|
||||||
if self.zustellen(host) {
|
if self.zustellen(host, Zustellpunkt::Kooperativ) || self.rt.ende || self.rt.abbruch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.rt.ende || self.rt.abbruch {
|
if let Some(deadline) = deadline {
|
||||||
|
if host.jetzt_ms() >= deadline {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if self.rt.naechste_taste(host, false).is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if sekunden <= 0.0 {
|
self.warten(host, deadline);
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,6 +779,8 @@ impl Vm {
|
|||||||
waiting_form: None,
|
waiting_form: None,
|
||||||
pending_show: None,
|
pending_show: None,
|
||||||
form_event: None,
|
form_event: None,
|
||||||
|
handler_start: true,
|
||||||
|
eingabe: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,7 +794,11 @@ impl Vm {
|
|||||||
|
|
||||||
fn pop_frame(&mut self) {
|
fn pop_frame(&mut self) {
|
||||||
let f = self.frames.pop().expect("Frame-Unterlauf");
|
let f = self.frames.pop().expect("Frame-Unterlauf");
|
||||||
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);
|
self.stack.truncate(f.stack_base);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,14 +905,14 @@ impl Vm {
|
|||||||
pub fn run_visible_forms(&mut self, host: &mut dyn Host) -> RunEvent {
|
pub fn run_visible_forms(&mut self, host: &mut dyn Host) -> RunEvent {
|
||||||
while self.forms.has_visible_forms() {
|
while self.forms.has_visible_forms() {
|
||||||
self.tick(host);
|
self.tick(host);
|
||||||
let dispatched = self.forms_zustellen(host);
|
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
|
||||||
if dispatched {
|
if dispatched {
|
||||||
match self.run(host) {
|
match self.run(host) {
|
||||||
RunEvent::Ended => {}
|
RunEvent::Ended => {}
|
||||||
event => return event,
|
event => return event,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.rt.ende {
|
if !dispatched && self.rt.ende {
|
||||||
return RunEvent::Ended;
|
return RunEvent::Ended;
|
||||||
}
|
}
|
||||||
if self.rt.abbruch {
|
if self.rt.abbruch {
|
||||||
@@ -821,7 +920,9 @@ impl Vm {
|
|||||||
line: self.current_line(),
|
line: self.current_line(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
if !dispatched && self.forms.has_visible_forms() {
|
||||||
|
self.warten(host, None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RunEvent::Ended
|
RunEvent::Ended
|
||||||
}
|
}
|
||||||
@@ -833,11 +934,18 @@ impl Vm {
|
|||||||
self.frames.last_mut().unwrap().waiting_form = None;
|
self.frames.last_mut().unwrap().waiting_form = None;
|
||||||
} else {
|
} else {
|
||||||
self.tick(host);
|
self.tick(host);
|
||||||
self.forms_zustellen(host);
|
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
|
||||||
if self.rt.ende {
|
if !dispatched && self.rt.ende {
|
||||||
return RunEvent::Ended;
|
return RunEvent::Ended;
|
||||||
}
|
}
|
||||||
std::thread::yield_now();
|
if self.rt.abbruch {
|
||||||
|
return RunEvent::Interrupted {
|
||||||
|
line: self.current_line(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if !dispatched {
|
||||||
|
self.warten(host, None);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -898,6 +1006,7 @@ impl Vm {
|
|||||||
(f.stack_base, f.last_stmt_pc, f.proc)
|
(f.stack_base, f.last_stmt_pc, f.proc)
|
||||||
};
|
};
|
||||||
self.stack.truncate(stack_base);
|
self.stack.truncate(stack_base);
|
||||||
|
self.frames.last_mut().unwrap().eingabe = None;
|
||||||
self.err = code;
|
self.err = code;
|
||||||
match handler {
|
match handler {
|
||||||
Handler::Goto(pc) => {
|
Handler::Goto(pc) => {
|
||||||
@@ -1131,27 +1240,11 @@ impl Vm {
|
|||||||
self.tick(host);
|
self.tick(host);
|
||||||
self.rt.screen.veraenderung_quittieren();
|
self.rt.screen.veraenderung_quittieren();
|
||||||
}
|
}
|
||||||
if self.forms_zustellen(host) {
|
let erster_eintritt =
|
||||||
|
std::mem::take(&mut self.frames.last_mut().unwrap().handler_start);
|
||||||
|
if !erster_eintritt && self.zustellen(host, Zustellpunkt::Anweisung) {
|
||||||
return Ok(Flow::Normal);
|
return Ok(Flow::Normal);
|
||||||
}
|
}
|
||||||
// Ereigniszustellung (design.md, D1): nur wenn überhaupt
|
|
||||||
// ein Trap definiert ist — sonst kostet die Grenze nichts.
|
|
||||||
if self.rt.traps.aktiv() {
|
|
||||||
// ponytail: die Uhr nur alle 64 Anweisungen lesen, die
|
|
||||||
// Tastenschlange dagegen jedes Mal. Ceiling: die
|
|
||||||
// Auflösung eines Zeit-Traps ist damit 64 Anweisungen;
|
|
||||||
// reicht das nicht, wird daraus ein eigener Zähler je
|
|
||||||
// Trap.
|
|
||||||
if self.tick_zaehler.is_multiple_of(64) {
|
|
||||||
let jetzt = host.jetzt_ms();
|
|
||||||
self.rt.traps.zeit_pruefen(jetzt);
|
|
||||||
}
|
|
||||||
self.rt.tasten_traps_pruefen();
|
|
||||||
if let Some((q, ziel)) = self.rt.traps.naechstes() {
|
|
||||||
self.trap_frame_aufsetzen(q, ziel);
|
|
||||||
return Ok(Flow::Normal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.flags != 0 {
|
if self.flags != 0 {
|
||||||
if self.flags & F_STEP != 0 {
|
if self.flags & F_STEP != 0 {
|
||||||
return Ok(Flow::Event(RunEvent::Stepped { line }));
|
return Ok(Flow::Event(RunEvent::Stepped { line }));
|
||||||
@@ -1193,7 +1286,7 @@ impl Vm {
|
|||||||
1 => tb_runtime::traps::Zustand::Aus,
|
1 => tb_runtime::traps::Zustand::Aus,
|
||||||
_ => tb_runtime::traps::Zustand::Gestoppt,
|
_ => tb_runtime::traps::Zustand::Gestoppt,
|
||||||
};
|
};
|
||||||
let jetzt = host.jetzt_ms();
|
let jetzt = if art == ART_TIMER { host.jetzt_ms() } else { 0 };
|
||||||
self.rt.traps.setzen(q, z, jetzt);
|
self.rt.traps.setzen(q, z, jetzt);
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
@@ -1203,8 +1296,7 @@ impl Vm {
|
|||||||
// lässt den Wert unberührt.
|
// lässt den Wert unberührt.
|
||||||
self.stack.push(Value::Int(0));
|
self.stack.push(Value::Int(0));
|
||||||
self.tick(host);
|
self.tick(host);
|
||||||
self.forms_zustellen(host);
|
self.zustellen(host, Zustellpunkt::Kooperativ);
|
||||||
self.zustellen(host);
|
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
I::Sleep(mit_arg) => {
|
I::Sleep(mit_arg) => {
|
||||||
@@ -1258,7 +1350,7 @@ impl Vm {
|
|||||||
};
|
};
|
||||||
if !self.forms.is_loaded_at(object, index) {
|
if !self.forms.is_loaded_at(object, index) {
|
||||||
self.forms.ensure_loaded_at(object, index)?;
|
self.forms.ensure_loaded_at(object, index)?;
|
||||||
if index.is_none() && self.dispatch_next_form_event() {
|
if index.is_none() && self.dispatch_next_form_event(true) {
|
||||||
let caller = self.frames.len() - 2;
|
let caller = self.frames.len() - 2;
|
||||||
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
||||||
return Ok(Flow::Normal);
|
return Ok(Flow::Normal);
|
||||||
@@ -1280,7 +1372,7 @@ impl Vm {
|
|||||||
if index.is_none() {
|
if index.is_none() {
|
||||||
self.push(value.clone());
|
self.push(value.clone());
|
||||||
}
|
}
|
||||||
if index.is_none() && self.dispatch_next_form_event() {
|
if index.is_none() && self.dispatch_next_form_event(true) {
|
||||||
let caller = self.frames.len() - 2;
|
let caller = self.frames.len() - 2;
|
||||||
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
||||||
return Ok(Flow::Normal);
|
return Ok(Flow::Normal);
|
||||||
@@ -1304,7 +1396,7 @@ impl Vm {
|
|||||||
};
|
};
|
||||||
self.forms.ensure_loaded_at(object, index)?;
|
self.forms.ensure_loaded_at(object, index)?;
|
||||||
self.push(Value::Obj(object, index));
|
self.push(Value::Obj(object, index));
|
||||||
self.dispatch_next_form_event();
|
self.dispatch_next_form_event(true);
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
I::LoadDynamicObjectProperty(property) => {
|
I::LoadDynamicObjectProperty(property) => {
|
||||||
@@ -1434,7 +1526,7 @@ impl Vm {
|
|||||||
&& !self.forms.is_loaded(object)
|
&& !self.forms.is_loaded(object)
|
||||||
{
|
{
|
||||||
self.forms.ensure_loaded(object)?;
|
self.forms.ensure_loaded(object)?;
|
||||||
if self.dispatch_next_form_event() {
|
if self.dispatch_next_form_event(true) {
|
||||||
let caller = self.frames.len() - 2;
|
let caller = self.frames.len() - 2;
|
||||||
self.frames[caller].pending_show = Some(object);
|
self.frames[caller].pending_show = Some(object);
|
||||||
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
self.frames[caller].pc = self.frames[caller].pc.saturating_sub(1);
|
||||||
@@ -1493,7 +1585,7 @@ impl Vm {
|
|||||||
self.forms.object_method_at(object, index, name, args)?;
|
self.forms.object_method_at(object, index, name, args)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.dispatch_next_form_event();
|
self.dispatch_next_form_event(true);
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
I::ObjectLoad(object, unload, has_index) => {
|
I::ObjectLoad(object, unload, has_index) => {
|
||||||
@@ -1508,7 +1600,7 @@ impl Vm {
|
|||||||
self.request_unload(object)?;
|
self.request_unload(object)?;
|
||||||
} else {
|
} else {
|
||||||
self.forms.ensure_loaded(object)?;
|
self.forms.ensure_loaded(object)?;
|
||||||
self.dispatch_next_form_event();
|
self.dispatch_next_form_event(true);
|
||||||
}
|
}
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
@@ -2058,6 +2150,8 @@ impl Vm {
|
|||||||
self.pop_frame();
|
self.pop_frame();
|
||||||
}
|
}
|
||||||
let f = self.frames.last_mut().unwrap();
|
let f = self.frames.last_mut().unwrap();
|
||||||
|
f.eingabe = None;
|
||||||
|
self.stack.truncate(f.stack_base);
|
||||||
f.pc = t as usize;
|
f.pc = t as usize;
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
@@ -2122,8 +2216,10 @@ impl Vm {
|
|||||||
if let Some(FormEventReturn::Unload(object)) = event {
|
if let Some(FormEventReturn::Unload(object)) = event {
|
||||||
self.forms.unload_with(object, |c| *c = cancel)?;
|
self.forms.unload_with(object, |c| *c = cancel)?;
|
||||||
}
|
}
|
||||||
if event.is_some() {
|
if matches!(event, Some(FormEventReturn::Normal))
|
||||||
self.dispatch_next_form_event();
|
&& !self.frames.iter().any(|f| f.form_event.is_some())
|
||||||
|
{
|
||||||
|
self.dispatch_next_form_event(false);
|
||||||
}
|
}
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
@@ -2134,6 +2230,38 @@ impl Vm {
|
|||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
I::CallBuiltin(id, argc) => {
|
I::CallBuiltin(id, argc) => {
|
||||||
|
if id == ids::RANDOMIZE && argc == 0 {
|
||||||
|
if self.frames.last().unwrap().eingabe.is_none() {
|
||||||
|
self.rt
|
||||||
|
.screen
|
||||||
|
.print("Random Number Seed (-32768 to 32767)? ");
|
||||||
|
}
|
||||||
|
let caller = self.frames.len() - 1;
|
||||||
|
if let Some(text) = self.konsoleneingabe(host, None)? {
|
||||||
|
self.rt.saat_setzen(tb_runtime::format::val(&text));
|
||||||
|
} else {
|
||||||
|
self.frames[caller].pc = pc;
|
||||||
|
}
|
||||||
|
return Ok(Flow::Normal);
|
||||||
|
}
|
||||||
|
if id == ids::INPUT_S && argc == 1 {
|
||||||
|
let n = match self.stack.last() {
|
||||||
|
Some(Value::Int(n)) => i32::from(*n),
|
||||||
|
Some(Value::Lng(n)) => *n,
|
||||||
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||||
|
};
|
||||||
|
if n < 0 {
|
||||||
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||||
|
}
|
||||||
|
let caller = self.frames.len() - 1;
|
||||||
|
if let Some(text) = self.konsoleneingabe(host, Some(n as usize))? {
|
||||||
|
self.pop()?;
|
||||||
|
self.push(Value::Str(Rc::from(text)));
|
||||||
|
} else {
|
||||||
|
self.frames[caller].pc = pc;
|
||||||
|
}
|
||||||
|
return Ok(Flow::Normal);
|
||||||
|
}
|
||||||
let mut args = Vec::with_capacity(argc as usize);
|
let mut args = Vec::with_capacity(argc as usize);
|
||||||
for _ in 0..argc {
|
for _ in 0..argc {
|
||||||
args.push(self.pop()?);
|
args.push(self.pop()?);
|
||||||
@@ -2223,7 +2351,10 @@ impl Vm {
|
|||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
I::Input(argc, line_mode, prompt, question) => {
|
I::Input(argc, line_mode, prompt, question) => {
|
||||||
self.do_input(argc, line_mode, prompt, question, host)?;
|
let caller = self.frames.len() - 1;
|
||||||
|
if !self.do_input(argc, line_mode, prompt, question, host)? {
|
||||||
|
self.frames[caller].pc = pc;
|
||||||
|
}
|
||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
I::InputFile(argc, line_mode) => {
|
I::InputFile(argc, line_mode) => {
|
||||||
@@ -2272,6 +2403,64 @@ impl Vm {
|
|||||||
Ok(Flow::Normal)
|
Ok(Flow::Normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fortsetzbarer Leser: Text und Restlänge gehören zum unterbrochenen
|
||||||
|
/// Frame; Referenzen/Argumente bleiben bis zum Abschluss auf dessen Stack.
|
||||||
|
fn konsoleneingabe(
|
||||||
|
&mut self,
|
||||||
|
host: &mut dyn Host,
|
||||||
|
rest: Option<usize>,
|
||||||
|
) -> Result<Option<String>, RuntimeError> {
|
||||||
|
let caller = self.frames.len() - 1;
|
||||||
|
self.frames[caller]
|
||||||
|
.eingabe
|
||||||
|
.get_or_insert_with(|| KonsolenEingabe {
|
||||||
|
text: String::new(),
|
||||||
|
rest,
|
||||||
|
});
|
||||||
|
loop {
|
||||||
|
if self.frames[caller].eingabe.as_ref().unwrap().rest == Some(0) {
|
||||||
|
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
|
||||||
|
}
|
||||||
|
self.tick(host);
|
||||||
|
if self.zustellen(host, Zustellpunkt::Konsole) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
if let Some(taste) = self.rt.gepufferte_taste() {
|
||||||
|
let eingabe = self.frames[caller].eingabe.as_mut().unwrap();
|
||||||
|
if let Some(rest) = &mut eingabe.rest {
|
||||||
|
eingabe.text.push_str(&taste);
|
||||||
|
*rest -= 1;
|
||||||
|
} else {
|
||||||
|
match taste.as_str() {
|
||||||
|
tb_runtime::host::taste::ENTER => {
|
||||||
|
self.rt.screen.print("\n");
|
||||||
|
return Ok(Some(self.frames[caller].eingabe.take().unwrap().text));
|
||||||
|
}
|
||||||
|
tb_runtime::host::taste::BACKSPACE => {
|
||||||
|
if eingabe.text.pop().is_some() {
|
||||||
|
self.rt.screen.rueckschritt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if taste.chars().count() == 1
|
||||||
|
&& taste.chars().next().is_some_and(|c| c as u32 >= 0x20)
|
||||||
|
{
|
||||||
|
eingabe.text.push_str(&taste);
|
||||||
|
self.rt.screen.print(&taste);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.rt.ende || self.rt.abbruch {
|
||||||
|
self.frames[caller].eingabe = None;
|
||||||
|
return Err(RuntimeError(62));
|
||||||
|
}
|
||||||
|
self.warten(host, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn do_input(
|
fn do_input(
|
||||||
&mut self,
|
&mut self,
|
||||||
argc: u8,
|
argc: u8,
|
||||||
@@ -2279,38 +2468,41 @@ impl Vm {
|
|||||||
prompt: u16,
|
prompt: u16,
|
||||||
question: bool,
|
question: bool,
|
||||||
host: &mut dyn Host,
|
host: &mut dyn Host,
|
||||||
) -> Result<(), RuntimeError> {
|
) -> Result<bool, RuntimeError> {
|
||||||
// Referenzen in Reihenfolge einsammeln.
|
let start = self
|
||||||
let mut refs = Vec::with_capacity(argc as usize);
|
.stack
|
||||||
for _ in 0..argc {
|
.len()
|
||||||
match self.pop()? {
|
.checked_sub(argc as usize)
|
||||||
Value::Ref(r) => refs.push(r),
|
.ok_or(RuntimeError(51))?;
|
||||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
let refs = self.stack[start..]
|
||||||
}
|
.iter()
|
||||||
}
|
.map(|v| match v {
|
||||||
refs.reverse();
|
Value::Ref(r) => Ok(r.clone()),
|
||||||
|
_ => Err(RuntimeError::TYPE_MISMATCH),
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
loop {
|
loop {
|
||||||
if prompt != 0xFFFF {
|
if self.frames.last().unwrap().eingabe.is_none() {
|
||||||
let p = self.module.strings[prompt as usize].clone();
|
if prompt != 0xFFFF {
|
||||||
self.rt.screen.print(&p);
|
self.rt.screen.print(&self.module.strings[prompt as usize]);
|
||||||
|
}
|
||||||
|
if question {
|
||||||
|
self.rt.screen.print("? ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if question {
|
let Some(line) = self.konsoleneingabe(host, None)? else {
|
||||||
self.rt.screen.print("? ");
|
return Ok(false);
|
||||||
}
|
|
||||||
let Some(mut line) = self.rt.read_line(host) else {
|
|
||||||
return Err(RuntimeError(62)); // Input past end of file
|
|
||||||
};
|
};
|
||||||
if line_mode {
|
if line_mode {
|
||||||
let r = refs.first().ok_or(RuntimeError(51))?;
|
let r = refs.first().ok_or(RuntimeError(51))?;
|
||||||
self.write_ref(r, Value::Str(Rc::from(line.as_str())))?;
|
self.write_ref(r, Value::Str(Rc::from(line)))?;
|
||||||
return Ok(());
|
self.stack.truncate(start);
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
// Felder trennen (Quotes respektieren).
|
// Felder trennen (Quotes respektieren).
|
||||||
let fields = split_input_fields(&line);
|
let fields = split_input_fields(&line);
|
||||||
if fields.len() != refs.len() {
|
if fields.len() != refs.len() {
|
||||||
self.rt.screen.print("Redo from start\n");
|
self.rt.screen.print("Redo from start\n");
|
||||||
line.clear();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Konvertieren nach aktuellem Zieltyp; Fehler → Redo.
|
// Konvertieren nach aktuellem Zieltyp; Fehler → Redo.
|
||||||
@@ -2333,7 +2525,8 @@ impl Vm {
|
|||||||
for (r, v) in refs.iter().zip(vals) {
|
for (r, v) in refs.iter().zip(vals) {
|
||||||
self.write_ref(r, v)?;
|
self.write_ref(r, v)?;
|
||||||
}
|
}
|
||||||
return Ok(());
|
self.stack.truncate(self.stack.len() - argc as usize);
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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> {
|
fn next_event(&mut self, b: bool) -> Option<tb_runtime::host::Ereignis> {
|
||||||
self.inner.next_event(b)
|
self.inner.next_event(b)
|
||||||
}
|
}
|
||||||
|
fn warten(&mut self, deadline_ms: Option<u64>) -> Option<tb_runtime::host::Ereignis> {
|
||||||
|
if let Some(event) = self.next_event(false) {
|
||||||
|
return Some(event);
|
||||||
|
}
|
||||||
|
self.inner.warten(deadline_ms)
|
||||||
|
}
|
||||||
fn jetzt_ms(&mut self) -> u64 {
|
fn jetzt_ms(&mut self) -> u64 {
|
||||||
let j = self.inner.jetzt_ms();
|
let j = self.inner.jetzt_ms();
|
||||||
self.inner.uhr_vorruecken(self.schritt);
|
self.inner.uhr_vorruecken(self.schritt);
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ Geprüfter Code: `19804e0e2de9eb5a8b0ef256b5be76c741560e07` in TerminalBasic. De
|
|||||||
|
|
||||||
Die Implementierung deckt breite Grundpfade ab, erfüllt den gesamten Vertrag aber nicht. **25 Befunde**: 10 mit hoher, 12 mit mittlerer Priorität, 2 Dokumentationsabweichungen und 1 Vertragsklärung. Die Priorität bewertet die Auswirkung, nicht den Aufwand. Befunde können mehrere Requirements betreffen.
|
Die Implementierung deckt breite Grundpfade ab, erfüllt den gesamten Vertrag aber nicht. **25 Befunde**: 10 mit hoher, 12 mit mittlerer Priorität, 2 Dokumentationsabweichungen und 1 Vertragsklärung. Die Priorität bewertet die Auswirkung, nicht den Aufwand. Befunde können mehrere Requirements betreffen.
|
||||||
|
|
||||||
|
## Umsetzungsstand: Ereigniszustellung und Hostgrenzen
|
||||||
|
|
||||||
|
F04, F05, F06, F07, F08, F18 und F20 sind im archivierten Change
|
||||||
|
[ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md)
|
||||||
|
implementiert. Die fünf Restfehler der anschließenden
|
||||||
|
[Verifikation](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/review.md)
|
||||||
|
sind behoben: 419 Workspace-Tests und alle sechs ursprünglichen Review-Proben
|
||||||
|
bestehen. Die konkreten Implementierungs- und aktuellen Abnahmenachweise stehen in
|
||||||
|
[verification.md](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/verification.md).
|
||||||
|
Die Befundtexte und die Matrix unten dokumentieren weiterhin den ursprünglichen
|
||||||
|
Reviewstand; die übrigen Changes bleiben eigenständig.
|
||||||
|
|
||||||
## Prüfmethode und Grenzen
|
## Prüfmethode und Grenzen
|
||||||
|
|
||||||
- Alle Requirements/Szenarien gelesen und gegen Einstiegspunkte, Implementierung und vorhandene Tests abgeglichen. Die Matrix unten enthält jeden Requirement-Titel einmal.
|
- Alle Requirements/Szenarien gelesen und gegen Einstiegspunkte, Implementierung und vorhandene Tests abgeglichen. Die Matrix unten enthält jeden Requirement-Titel einmal.
|
||||||
@@ -46,7 +58,7 @@ Ein eingespeistes Signal(2) bei definiertem und eingeschaltetem Trap ergibt kein
|
|||||||
|
|
||||||
Beleg: [crates/tb-runtime/src/builtins.rs:121](../../crates/tb-runtime/src/builtins.rs#L121). Vertrag: [openspec/specs/ereignissteuerung/spec.md:186](../../openspec/specs/ereignissteuerung/spec.md#L186).
|
Beleg: [crates/tb-runtime/src/builtins.rs:121](../../crates/tb-runtime/src/builtins.rs#L121). Vertrag: [openspec/specs/ereignissteuerung/spec.md:186](../../openspec/specs/ereignissteuerung/spec.md#L186).
|
||||||
|
|
||||||
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md).
|
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
|
||||||
|
|
||||||
### F05 — KEY STOP entfernt die Taste aus dem Eingabestrom (hoch)
|
### F05 — KEY STOP entfernt die Taste aus dem Eingabestrom (hoch)
|
||||||
|
|
||||||
@@ -54,7 +66,7 @@ Bei KEY(1) STOP wird F1 zwar für das spätere ON gemerkt, aber aus dem normalen
|
|||||||
|
|
||||||
Beleg: [crates/tb-runtime/src/traps.rs:215](../../crates/tb-runtime/src/traps.rs#L215). Vertrag: [openspec/specs/ereignissteuerung/spec.md:81](../../openspec/specs/ereignissteuerung/spec.md#L81), [openspec/specs/ereignissteuerung/spec.md:143](../../openspec/specs/ereignissteuerung/spec.md#L143).
|
Beleg: [crates/tb-runtime/src/traps.rs:215](../../crates/tb-runtime/src/traps.rs#L215). Vertrag: [openspec/specs/ereignissteuerung/spec.md:81](../../openspec/specs/ereignissteuerung/spec.md#L81), [openspec/specs/ereignissteuerung/spec.md:143](../../openspec/specs/ereignissteuerung/spec.md#L143).
|
||||||
|
|
||||||
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md).
|
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
|
||||||
|
|
||||||
### F06 — Forms-Handler und Eingaben verlieren ihre Reihenfolge (hoch)
|
### F06 — Forms-Handler und Eingaben verlieren ihre Reihenfolge (hoch)
|
||||||
|
|
||||||
@@ -62,7 +74,7 @@ Der Fokuswechsel Text1 nach Text2 ergibt in der VM GL statt LG: GotFocus läuft
|
|||||||
|
|
||||||
Beleg: [crates/tb-vm/src/interp.rs:570](../../crates/tb-vm/src/interp.rs#L570). Vertrag: [openspec/specs/ereignissteuerung/spec.md:32](../../openspec/specs/ereignissteuerung/spec.md#L32), [openspec/specs/forms-steuerelemente/spec.md:30](../../openspec/specs/forms-steuerelemente/spec.md#L30), [openspec/specs/forms-objektmodell/spec.md:106](../../openspec/specs/forms-objektmodell/spec.md#L106).
|
Beleg: [crates/tb-vm/src/interp.rs:570](../../crates/tb-vm/src/interp.rs#L570). Vertrag: [openspec/specs/ereignissteuerung/spec.md:32](../../openspec/specs/ereignissteuerung/spec.md#L32), [openspec/specs/forms-steuerelemente/spec.md:30](../../openspec/specs/forms-steuerelemente/spec.md#L30), [openspec/specs/forms-objektmodell/spec.md:106](../../openspec/specs/forms-objektmodell/spec.md#L106).
|
||||||
|
|
||||||
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md).
|
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
|
||||||
|
|
||||||
### F07 — Offenes Menü verhindert Traps nicht an allen Zustellpunkten (hoch)
|
### F07 — Offenes Menü verhindert Traps nicht an allen Zustellpunkten (hoch)
|
||||||
|
|
||||||
@@ -70,7 +82,7 @@ Ein UEVENT-Trap wird während eines geöffneten Menüs an einer gewöhnlichen An
|
|||||||
|
|
||||||
Beleg: [crates/tb-vm/src/interp.rs:1139](../../crates/tb-vm/src/interp.rs#L1139). Vertrag: [openspec/specs/forms-steuerelemente/spec.md:102](../../openspec/specs/forms-steuerelemente/spec.md#L102), [openspec/specs/ereignissteuerung/spec.md:59](../../openspec/specs/ereignissteuerung/spec.md#L59).
|
Beleg: [crates/tb-vm/src/interp.rs:1139](../../crates/tb-vm/src/interp.rs#L1139). Vertrag: [openspec/specs/forms-steuerelemente/spec.md:102](../../openspec/specs/forms-steuerelemente/spec.md#L102), [openspec/specs/ereignissteuerung/spec.md:59](../../openspec/specs/ereignissteuerung/spec.md#L59).
|
||||||
|
|
||||||
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md).
|
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
|
||||||
|
|
||||||
### F08 — Zeit- und Wartepfade verletzen die Hostgrenze (mittel)
|
### F08 — Zeit- und Wartepfade verletzen die Hostgrenze (mittel)
|
||||||
|
|
||||||
@@ -78,7 +90,7 @@ Drei einfache Anweisungen ohne Trap/Form-Timer lesen die Hostzeit sechsmal. form
|
|||||||
|
|
||||||
Beleg: [crates/tb-vm/src/interp.rs:602](../../crates/tb-vm/src/interp.rs#L602). Vertrag: [openspec/specs/vm-ausfuehrung/spec.md:118](../../openspec/specs/vm-ausfuehrung/spec.md#L118), [openspec/specs/ereignissteuerung/spec.md:15](../../openspec/specs/ereignissteuerung/spec.md#L15), [openspec/specs/ereignissteuerung/spec.md:59](../../openspec/specs/ereignissteuerung/spec.md#L59), [openspec/specs/forms-objektmodell/spec.md:106](../../openspec/specs/forms-objektmodell/spec.md#L106).
|
Beleg: [crates/tb-vm/src/interp.rs:602](../../crates/tb-vm/src/interp.rs#L602). Vertrag: [openspec/specs/vm-ausfuehrung/spec.md:118](../../openspec/specs/vm-ausfuehrung/spec.md#L118), [openspec/specs/ereignissteuerung/spec.md:15](../../openspec/specs/ereignissteuerung/spec.md#L15), [openspec/specs/ereignissteuerung/spec.md:59](../../openspec/specs/ereignissteuerung/spec.md#L59), [openspec/specs/forms-objektmodell/spec.md:106](../../openspec/specs/forms-objektmodell/spec.md#L106).
|
||||||
|
|
||||||
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md).
|
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
|
||||||
|
|
||||||
### F09 — Control-Namen sind faktisch global statt formularlokal (hoch)
|
### F09 — Control-Namen sind faktisch global statt formularlokal (hoch)
|
||||||
|
|
||||||
@@ -158,7 +170,7 @@ Statisch belegt: Der Terminal-Mapper behandelt KeyCode::Tab, aber nicht KeyCode:
|
|||||||
|
|
||||||
Beleg: [crates/tb-ui/src/host.rs:179](../../crates/tb-ui/src/host.rs#L179). Vertrag: [openspec/specs/forms-steuerelemente/spec.md:30](../../openspec/specs/forms-steuerelemente/spec.md#L30).
|
Beleg: [crates/tb-ui/src/host.rs:179](../../crates/tb-ui/src/host.rs#L179). Vertrag: [openspec/specs/forms-steuerelemente/spec.md:30](../../openspec/specs/forms-steuerelemente/spec.md#L30).
|
||||||
|
|
||||||
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md).
|
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
|
||||||
|
|
||||||
### F19 — Der Zonenzustand ist für BASIC nicht abfragbar (mittel)
|
### F19 — Der Zonenzustand ist für BASIC nicht abfragbar (mittel)
|
||||||
|
|
||||||
@@ -174,7 +186,7 @@ tb-vm hängt unmittelbar von tb-ui ab; dort sind ratatui, crossterm und signal-h
|
|||||||
|
|
||||||
Beleg: [crates/tb-vm/Cargo.toml:15](../../crates/tb-vm/Cargo.toml#L15). Vertrag: [openspec/specs/textbildschirm/spec.md:92](../../openspec/specs/textbildschirm/spec.md#L92).
|
Beleg: [crates/tb-vm/Cargo.toml:15](../../crates/tb-vm/Cargo.toml#L15). Vertrag: [openspec/specs/textbildschirm/spec.md:92](../../openspec/specs/textbildschirm/spec.md#L92).
|
||||||
|
|
||||||
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md).
|
Umsetzung: [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md).
|
||||||
|
|
||||||
### F21 — Inventarstatus beweist kein vorhandenes Laufzeitverhalten (mittel)
|
### F21 — Inventarstatus beweist kein vorhandenes Laufzeitverhalten (mittel)
|
||||||
|
|
||||||
@@ -221,7 +233,7 @@ Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/chan
|
|||||||
| Reihenfolge | Change | Befunde | Abhängigkeit |
|
| Reihenfolge | Change | Befunde | Abhängigkeit |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | [isam-transaktionen-und-dateinummern](../../openspec/changes/archive/2026-09-05-isam-transaktionen-und-dateinummern/proposal.md) | F01, F02, F03 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
| 1 | [isam-transaktionen-und-dateinummern](../../openspec/changes/archive/2026-09-05-isam-transaktionen-und-dateinummern/proposal.md) | F01, F02, F03 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
||||||
| 2 | [ereigniszustellung-und-hostgrenzen](../../openspec/changes/ereigniszustellung-und-hostgrenzen/proposal.md) | F04, F05, F06, F07, F08, F18, F20 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
| 2 | [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md) | F04, F05, F06, F07, F08, F18, F20 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
||||||
| 3 | [forms-zustand-und-bedienung](../../openspec/changes/forms-zustand-und-bedienung/proposal.md) | F12, F13 | Nach Ereigniszustellung |
|
| 3 | [forms-zustand-und-bedienung](../../openspec/changes/forms-zustand-und-bedienung/proposal.md) | F12, F13 | Nach Ereigniszustellung |
|
||||||
| 4 | [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md) | F09, F10, F11, F24 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
| 4 | [projektmodule-und-kompilat](../../openspec/changes/projektmodule-und-kompilat/proposal.md) | F09, F10, F11, F24 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
||||||
| 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
| 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
||||||
|
|||||||
@@ -55,15 +55,76 @@ enum Value {
|
|||||||
|
|
||||||
## Ausführungsmodell / Unterbrechbarkeit
|
## Ausführungsmodell / Unterbrechbarkeit
|
||||||
|
|
||||||
- Die Interpreterschleife läuft in **Ticks**: nach jeder Anweisung (Grenze
|
Die VM läuft auf dem aufrufenden Thread. `Vm::run` liefert bei STOP,
|
||||||
aus der Zeilentabelle) prüft sie ein Flag-Set: Breakpoint? Einzelschritt?
|
Breakpoint, Einzelschritt, Programmende oder Fehler ein `RunEvent` an den
|
||||||
Strg+Untbr? Ereignis-Queue nicht leer und Zustellung erlaubt?
|
Runner zurück. `Stmt` markiert Anweisungsgrenzen; es gibt keine Prüfung
|
||||||
- Ereigniszustellung (Forms, Timer) erfolgt kooperativ: nur an
|
nach jedem Opcode.
|
||||||
Zustellpunkten (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer
|
|
||||||
Ereignisprozedur) — wie im Vorbild.
|
Die gemeinsame Zustellentscheidung gilt an Anweisungsgrenzen, bei
|
||||||
- Die VM ist eine gewöhnliche zustandsbehaftete Struktur, `step()`-basiert;
|
`DOEVENTS`, während `SLEEP` und während blockierender Konsoleneingabe. Eine
|
||||||
die einbettende Schleife (IDE-Debugger oder Runner) treibt sie. Kein
|
zugestellte Ereignisprozedur beginnt ihre erste Anweisung, bevor ein
|
||||||
eigener Thread nötig; Terminal-Events werden zwischen Ticks gepollt.
|
weiterer Handler sie unterbrechen darf. Forms-Handler laufen normalerweise
|
||||||
|
bis zur Rückkehr; explizites DOEVENTS und blockierendes Warten können andere
|
||||||
|
Handler verschachteln, aber nie dieselbe laufende Forms-Ereignisprozedur.
|
||||||
|
Die Rückkehr aus dem äußersten normalen Forms-Handler arbeitet wartende
|
||||||
|
Forms-Ereignisse geordnet ab. Objekt-Opcodes stellen nur erforderliche
|
||||||
|
synchrone LOAD-Handler zu; deren Rückkehr zieht keine unabhängigen Ereignisse
|
||||||
|
mitten in eine Anweisung vor. Ein offenes
|
||||||
|
Menü sperrt klassische Traps und Forms-Timer, ohne ON/OFF/STOP zu verändern.
|
||||||
|
Beim Menüschließen wird die Zustellbarkeit vor erneutem Warten geprüft.
|
||||||
|
|
||||||
|
INPUT, LINE INPUT, INPUT$ und RANDOMIZE ohne Argument teilen einen
|
||||||
|
fortsetzbaren Konsolenleser. Teiltext und Restlänge liegen im aufrufenden
|
||||||
|
Frame; Referenzen und Ausdrucksoperanden bleiben bis zum Abschluss auf dem
|
||||||
|
Stack. Nach jedem Hostereignis kann die VM einen Handler ausführen und
|
||||||
|
danach am Eingabe-Opcode fortsetzen, auch über STOP/CONT und verschachtelte
|
||||||
|
Eingaben hinweg. Fehler-Unwinding und RETURN label verwerfen die abgebrochene
|
||||||
|
Eingabe. Während dieses Lesens gehören ungetrappte Tasten der Konsole.
|
||||||
|
|
||||||
|
`RtState::eingaben` hält Tasten und Maus in Hostreihenfolge, auch beim
|
||||||
|
Übergeben an Dialoge. Die VM nimmt eine Forms-Eingabe samt ihren Handlern
|
||||||
|
vor der nächsten Eingabe an. Konsolentasten bleiben lesbar; Maus ohne
|
||||||
|
Formular oder außerhalb der Bildschirmfläche wird verworfen. KEY ON
|
||||||
|
entnimmt getrappte Tasten. KEY STOP merkt höchstens einen Trap, markiert die
|
||||||
|
betroffenen Eingaben als bereits gemerkt und lässt sie lesbar, auch nach ON.
|
||||||
|
Vor der klassischen Quellenauswahl wird der getrappte Tastenanfang der Queue
|
||||||
|
gemeldet: KEY behält seine Priorität vor TIMER/UEVENT/SIGNAL, ohne eine davor
|
||||||
|
wartende Mauseingabe zu überholen. EOF beendet die Quelle; modale und
|
||||||
|
modellose Formularschleifen arbeiten schon angenommene Ereignisse noch ab.
|
||||||
|
|
||||||
|
`Host::jetzt_ms` ist die einzige Uhr für Ereignisfristen. Die VM fragt sie
|
||||||
|
nur für aktive Zeitquellen, SLEEP-Fristen und zeitabhängige Mausauswertung
|
||||||
|
ab. Der klassische TIMER wird weiterhin bei Anzeigen, expliziter
|
||||||
|
Kooperation und ansonsten alle 64 Anweisungsgrenzen geprüft.
|
||||||
|
`Host::warten(Option<u64>)` wartet auf ein Ereignis oder die absolute
|
||||||
|
Deadline derselben Uhr; `None` als Ergebnis meldet eine erreichte Deadline.
|
||||||
|
Ohne Deadline liefert der Host erst ein Ereignis oder explizites
|
||||||
|
`Ereignis::Ende`. Die VM verwendet weder Instant noch Sleep/Yield als
|
||||||
|
Rückfallebene. Auch modale und nach Programmende sichtbare Forms verwenden
|
||||||
|
diesen Wartepfad. CaptureHost liefert vorbereitete Ereignisse zuerst und
|
||||||
|
rückt beim Warten andernfalls bis zur Deadline vor; ohne Deadline meldet
|
||||||
|
er Eingabeende. Seine Uhr bleibt außerhalb dieser ausdrücklichen
|
||||||
|
Warteoperation oder manueller Zeitänderungen stehen.
|
||||||
|
|
||||||
|
`tb-ui` hat standardmäßig keine Terminalabhängigkeiten. Das Feature
|
||||||
|
`terminal` schaltet die Module `host`, `screen`, `signale` und die optionalen
|
||||||
|
Dependencies crossterm, ratatui und signal-hook ein. `tb-cli` aktiviert es
|
||||||
|
explizit; VM, Forms-Modell, FRM-Reader und Dialoge brauchen es nicht. Einbetter
|
||||||
|
müssen die neue Host-Wartemethode implementieren. Dialog-Eingabequeues tragen
|
||||||
|
`Eingabe` statt nackter `Ereignis`-Werte, damit KEY-STOP-Markierungen erhalten
|
||||||
|
bleiben; Ereignisse lassen sich mit `.into()` einreihen.
|
||||||
|
|
||||||
|
Der separate Workspace `tests/terminalfrei` prüft Einbettung ohne
|
||||||
|
Feature-Unifikation mit CLI oder IDE:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --manifest-path tests/terminalfrei/Cargo.toml --offline
|
||||||
|
cargo tree --manifest-path tests/terminalfrei/Cargo.toml --edges normal
|
||||||
|
```
|
||||||
|
|
||||||
|
Der erste Befehl lädt ein FRM-Korpusprogramm und prüft dessen `KM`-Ausgabe
|
||||||
|
mit CaptureHost. Der zweite Baum darf crossterm, ratatui und signal-hook
|
||||||
|
nicht enthalten.
|
||||||
|
|
||||||
## `.tbc`-Container (Stand Phase 2, Formatversion 1)
|
## `.tbc`-Container (Stand Phase 2, Formatversion 1)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## Why
|
## 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
|
## 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
|
können, sodass zeitgesteuerte Programme ohne Wartezeit und mit
|
||||||
reproduzierbarem Ergebnis prüfbar sind.
|
reproduzierbarem Ergebnis prüfbar sind.
|
||||||
|
|
||||||
|
Auch Wartefristen der VM SHALL ausschließlich von der Hostzeit abhängen. Ein Host mit stehender virtueller Zeit MUST NOT durch reale verstrichene Zeit eine Frist scheinbar erreichen.
|
||||||
|
|
||||||
#### Scenario: Zeitgesteuerter Trap ohne echte Wartezeit
|
#### Scenario: Zeitgesteuerter Trap ohne echte Wartezeit
|
||||||
- **WHEN** ein Testhost die Zeit um 5 Sekunden vorstellt und ein Programm `ON TIMER(5) GOSUB Marke` mit `TIMER ON` aktiv hat
|
- **WHEN** ein Testhost die Zeit um 5 Sekunden vorstellt und ein Programm `ON TIMER(5) GOSUB Marke` mit `TIMER ON` aktiv hat
|
||||||
- **THEN** wird der Trap zugestellt, ohne dass der Test tatsächlich wartet
|
- **THEN** wird der Trap zugestellt, ohne dass der Test tatsächlich wartet
|
||||||
@@ -29,6 +31,10 @@ reproduzierbarem Ergebnis prüfbar sind.
|
|||||||
- **WHEN** dasselbe zeitgesteuerte Korpusprogramm zweimal mit demselben Zeitverlauf ausgeführt wird
|
- **WHEN** dasselbe zeitgesteuerte Korpusprogramm zweimal mit demselben Zeitverlauf ausgeführt wird
|
||||||
- **THEN** ist die Ausgabe beide Male identisch
|
- **THEN** ist die Ausgabe beide Male identisch
|
||||||
|
|
||||||
|
#### Scenario: Stehende virtuelle Zeit
|
||||||
|
- **WHEN** ein Host während SLEEP seine Zeit unverändert hält
|
||||||
|
- **THEN** endet SLEEP nicht wegen realer Zeit; eine vom Host gemeldete Frist oder ein zugestelltes Ereignis beendet das Warten
|
||||||
|
|
||||||
### Requirement: Mausereignisse in Zellenkoordinaten
|
### Requirement: Mausereignisse in Zellenkoordinaten
|
||||||
Die Darstellungsschicht SHALL Mausereignisse der Ausführungsumgebung an
|
Die Darstellungsschicht SHALL Mausereignisse der Ausführungsumgebung an
|
||||||
die Laufzeit weitergeben: Drücken, Loslassen und Bewegung, jeweils mit
|
die Laufzeit weitergeben: Drücken, Loslassen und Bewegung, jeweils mit
|
||||||
@@ -40,6 +46,8 @@ SHALL der Reihenfolge ihres Eintreffens entsprechen. Solange kein
|
|||||||
Verbraucher ein Mausereignis annimmt, MUST es am Zustellpunkt verworfen
|
Verbraucher ein Mausereignis annimmt, MUST es am Zustellpunkt verworfen
|
||||||
werden, damit die Warteschlange nicht unbegrenzt wächst.
|
werden, damit die Warteschlange nicht unbegrenzt wächst.
|
||||||
|
|
||||||
|
Die Reihenfolge SHALL bis zur beobachtbaren Ausführung der zugehörigen Handler erhalten bleiben; getrennte Quellarten MUST NOT nachträglich nach ihrer Art umgeordnet werden.
|
||||||
|
|
||||||
#### Scenario: Position als Zelle
|
#### Scenario: Position als Zelle
|
||||||
- **WHEN** in der linken oberen Ecke der Darstellungsfläche die linke Maustaste gedrückt wird
|
- **WHEN** in der linken oberen Ecke der Darstellungsfläche die linke Maustaste gedrückt wird
|
||||||
- **THEN** trägt das Ereignis Zeile 1 und Spalte 1
|
- **THEN** trägt das Ereignis Zeile 1 und Spalte 1
|
||||||
@@ -56,6 +64,14 @@ werden, damit die Warteschlange nicht unbegrenzt wächst.
|
|||||||
- **WHEN** ein Programm ohne Verbraucher für Mausereignisse läuft und die Maus dauerhaft bewegt wird
|
- **WHEN** ein Programm ohne Verbraucher für Mausereignisse läuft und die Maus dauerhaft bewegt wird
|
||||||
- **THEN** wächst die Warteschlange nicht über ihre Grenze und das Programm läuft unverändert weiter
|
- **THEN** wächst die Warteschlange nicht über ihre Grenze und das Programm läuft unverändert weiter
|
||||||
|
|
||||||
|
#### Scenario: Taste vor Maus bis zum Handler
|
||||||
|
- **WHEN** eine Taste vor einem Mausklick eintrifft und beide Handler eine Kennung anhängen
|
||||||
|
- **THEN** steht die Tastenkennung vor der Mauskennung
|
||||||
|
|
||||||
|
#### Scenario: Maus vor Taste bis zum Handler
|
||||||
|
- **WHEN** ein Mausklick vor einer Taste eintrifft
|
||||||
|
- **THEN** wird zuerst der Maushandler wirksam
|
||||||
|
|
||||||
### Requirement: Abschließende Liste der Zustellpunkte
|
### Requirement: Abschließende Liste der Zustellpunkte
|
||||||
Ereignisse SHALL ausschließlich an folgenden Punkten zugestellt werden
|
Ereignisse SHALL ausschließlich an folgenden Punkten zugestellt werden
|
||||||
— das Vorbild prüft die Ereignismarke „before executing the next
|
— das Vorbild prüft die Ereignismarke „before executing the next
|
||||||
@@ -66,6 +82,8 @@ Trap-Handler beginnen; insbesondere MUST NOT eine Anweisung in ihrer
|
|||||||
Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes
|
Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes
|
||||||
`SLEEP` beenden.
|
`SLEEP` beenden.
|
||||||
|
|
||||||
|
Forms-Ereignisse SHALL auch während SLEEP an dessen bestehenden Zustellpunkten verarbeitet werden. Eine für das Menü geltende Zustellsperre SHALL an jedem Zustellpunkt gleichermaßen gelten.
|
||||||
|
|
||||||
#### Scenario: Keine Zustellung innerhalb einer Anweisung
|
#### Scenario: Keine Zustellung innerhalb einer Anweisung
|
||||||
- **WHEN** ein Ereignis fällig wird, während eine mehrteilige Anweisung ausgewertet wird
|
- **WHEN** ein Ereignis fällig wird, während eine mehrteilige Anweisung ausgewertet wird
|
||||||
- **THEN** läuft die Anweisung zu Ende und der Handler beginnt erst an der folgenden Anweisungsgrenze
|
- **THEN** läuft die Anweisung zu Ende und der Handler beginnt erst an der folgenden Anweisungsgrenze
|
||||||
@@ -78,6 +96,10 @@ Mitte unterbrochen werden. Ein zugestelltes Ereignis SHALL ein laufendes
|
|||||||
- **WHEN** ein anstehendes Ereignis vorliegt und `DOEVENTS` ausgewertet wird
|
- **WHEN** ein anstehendes Ereignis vorliegt und `DOEVENTS` ausgewertet wird
|
||||||
- **THEN** läuft der zugehörige Handler, bevor `DOEVENTS` einen Wert liefert
|
- **THEN** läuft der zugehörige Handler, bevor `DOEVENTS` einen Wert liefert
|
||||||
|
|
||||||
|
#### Scenario: Forms-Timer beendet SLEEP
|
||||||
|
- **WHEN** während SLEEP ein aktiver Forms-Timer fällig wird
|
||||||
|
- **THEN** läuft dessen Handler und SLEEP kehrt danach zurück
|
||||||
|
|
||||||
### Requirement: Maskierung je Quelle mit drei Zuständen
|
### Requirement: Maskierung je Quelle mit drei Zuständen
|
||||||
Jede Ereignisquelle SHALL drei Zustände kennen. `ON` stellt Ereignisse
|
Jede Ereignisquelle SHALL drei Zustände kennen. `ON` stellt Ereignisse
|
||||||
zu. `OFF` verwirft sie; ein während `OFF` eingetretenes Ereignis MUST
|
zu. `OFF` verwirft sie; ein während `OFF` eingetretenes Ereignis MUST
|
||||||
@@ -146,6 +168,8 @@ zugestellt werden und MUST NOT zusätzlich über `INKEY$`, `INPUT`,
|
|||||||
`LINE INPUT` oder `INPUT$` sichtbar werden. Bei `OFF` oder `STOP` der
|
`LINE INPUT` oder `INPUT$` sichtbar werden. Bei `OFF` oder `STOP` der
|
||||||
Quelle SHALL die Taste dem normalen Eingabestrom erhalten bleiben.
|
Quelle SHALL die Taste dem normalen Eingabestrom erhalten bleiben.
|
||||||
|
|
||||||
|
Unter STOP SHALL die Taste normal lesbar bleiben und zugleich höchstens ein Ereignis für das spätere ON gemerkt werden.
|
||||||
|
|
||||||
#### Scenario: Getrappte Taste erscheint nicht bei INKEY$
|
#### Scenario: Getrappte Taste erscheint nicht bei INKEY$
|
||||||
- **WHEN** `ON KEY(1) GOSUB Marke` mit `KEY(1) ON` gilt und F1 gedrückt wird
|
- **WHEN** `ON KEY(1) GOSUB Marke` mit `KEY(1) ON` gilt und F1 gedrückt wird
|
||||||
- **THEN** läuft der Handler und ein anschließendes `INKEY$` liefert den leeren String
|
- **THEN** läuft der Handler und ein anschließendes `INKEY$` liefert den leeren String
|
||||||
@@ -154,6 +178,10 @@ Quelle SHALL die Taste dem normalen Eingabestrom erhalten bleiben.
|
|||||||
- **WHEN** derselbe Trap mit `KEY(1) OFF` gilt und F1 gedrückt wird
|
- **WHEN** derselbe Trap mit `KEY(1) OFF` gilt und F1 gedrückt wird
|
||||||
- **THEN** liefert `INKEY$` die Sondertastenfolge für F1
|
- **THEN** liefert `INKEY$` die Sondertastenfolge für F1
|
||||||
|
|
||||||
|
#### Scenario: STOP merkt und erhält Eingabe
|
||||||
|
- **WHEN** unter KEY(1) STOP F1 gedrückt, per INKEY$ gelesen und danach KEY(1) ON ausgeführt wird
|
||||||
|
- **THEN** liefert INKEY$ die Sondertastenfolge und der Handler läuft nach ON genau einmal
|
||||||
|
|
||||||
### Requirement: Benutzerdefiniertes Ereignis
|
### Requirement: Benutzerdefiniertes Ereignis
|
||||||
`SetUEvent` SHALL ein benutzerdefiniertes Ereignis auslösen, das über
|
`SetUEvent` SHALL ein benutzerdefiniertes Ereignis auslösen, das über
|
||||||
`ON UEVENT GOSUB` und die Steueranweisung `UEVENT` denselben Regeln für
|
`ON UEVENT GOSUB` und die Steueranweisung `UEVENT` denselben Regeln für
|
||||||
@@ -208,3 +236,7 @@ ausgewiesen sein.
|
|||||||
#### Scenario: Signal ohne Trap bleibt Abbruch
|
#### Scenario: Signal ohne Trap bleibt Abbruch
|
||||||
- **WHEN** ein `SIGINT` eintrifft, ohne dass ein `SIGNAL(1)`-Trap aktiv ist
|
- **WHEN** ein `SIGINT` eintrifft, ohne dass ein `SIGNAL(1)`-Trap aktiv ist
|
||||||
- **THEN** bleibt es beim bisherigen Abbruchverhalten
|
- **THEN** bleibt es beim bisherigen Abbruchverhalten
|
||||||
|
|
||||||
|
#### Scenario: SIGTERM wird zugestellt
|
||||||
|
- **WHEN** SIGNAL(2) ON gilt und ein SIGTERM über den Host eintrifft
|
||||||
|
- **THEN** wird das Ziel von ON SIGNAL(2) GOSUB am nächsten erlaubten Zustellpunkt ausgeführt
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ auslöst oder ihm den Fokus gibt. Enter SHALL die `Default`-Schaltfläche
|
|||||||
auslösen, Esc die `Cancel`-Schaltfläche. Fokuswechsel MUST `LostFocus`
|
auslösen, Esc die `Cancel`-Schaltfläche. Fokuswechsel MUST `LostFocus`
|
||||||
am alten und `GotFocus` am neuen Element auslösen, in dieser Reihenfolge.
|
am alten und `GotFocus` am neuen Element auslösen, in dieser Reihenfolge.
|
||||||
|
|
||||||
|
Die über das Terminal gelieferte Rückwärtstab-Taste SHALL denselben Fokuswechsel wie Tab mit Umschalt auslösen. Die Reihenfolge LostFocus vor GotFocus SHALL auch an den Wirkungen der BASIC-Handler sichtbar bleiben.
|
||||||
|
|
||||||
#### Scenario: Tab überspringt
|
#### Scenario: Tab überspringt
|
||||||
- **WHEN** das mittlere von drei Elementen `TabStop = 0` trägt und Tab gedrückt wird
|
- **WHEN** das mittlere von drei Elementen `TabStop = 0` trägt und Tab gedrückt wird
|
||||||
- **THEN** erhält das dritte Element den Fokus
|
- **THEN** erhält das dritte Element den Fokus
|
||||||
@@ -48,6 +50,14 @@ am alten und `GotFocus` am neuen Element auslösen, in dieser Reihenfolge.
|
|||||||
- **WHEN** der Fokus von `Text1` auf `Text2` wechselt
|
- **WHEN** der Fokus von `Text1` auf `Text2` wechselt
|
||||||
- **THEN** läuft erst `Text1_LostFocus`, danach `Text2_GotFocus`
|
- **THEN** läuft erst `Text1_LostFocus`, danach `Text2_GotFocus`
|
||||||
|
|
||||||
|
#### Scenario: Handlerwirkungen in Fokusreihenfolge
|
||||||
|
- **WHEN** LostFocus den Text L und GotFocus den Text G an dieselbe Variable anhängen
|
||||||
|
- **THEN** lautet das Ergebnis LG
|
||||||
|
|
||||||
|
#### Scenario: Rückwärtstab vom Terminal
|
||||||
|
- **WHEN** das Terminal eine Rückwärtstab-Taste liefert
|
||||||
|
- **THEN** wechselt der Fokus zum vorherigen zulässigen TabIndex
|
||||||
|
|
||||||
### Requirement: Maussteuerung mit Trefferprüfung
|
### Requirement: Maussteuerung mit Trefferprüfung
|
||||||
Ein Mausereignis SHALL dem obersten Steuerelement an seiner Position
|
Ein Mausereignis SHALL dem obersten Steuerelement an seiner Position
|
||||||
zugestellt werden; liegt dort keines, dem Formular. Klick, Doppelklick
|
zugestellt werden; liegt dort keines, dem Formular. Klick, Doppelklick
|
||||||
@@ -116,6 +126,10 @@ fortgesetzt werden.
|
|||||||
- **WHEN** ein Menü geöffnet ist und ein Zeit-Trap fällig wird
|
- **WHEN** ein Menü geöffnet ist und ein Zeit-Trap fällig wird
|
||||||
- **THEN** läuft sein Handler erst, nachdem das Menü geschlossen wurde
|
- **THEN** läuft sein Handler erst, nachdem das Menü geschlossen wurde
|
||||||
|
|
||||||
|
#### Scenario: UEVENT an gewöhnlicher Anweisungsgrenze
|
||||||
|
- **WHEN** ein Menü offen ist und UEVENT vor einer Zuweisung ansteht
|
||||||
|
- **THEN** läuft dessen Handler erst nach dem Schließen des Menüs
|
||||||
|
|
||||||
### Requirement: Vordefinierte Dialoge
|
### Requirement: Vordefinierte Dialoge
|
||||||
`MSGBOX text$ [, typ% [, titel$]]` SHALL als Anweisung und als Funktion
|
`MSGBOX text$ [, typ% [, titel$]]` SHALL als Anweisung und als Funktion
|
||||||
verfügbar sein; die Funktion SHALL die gedrückte Schaltfläche als
|
verfügbar sein; die Funktion SHALL die gedrückte Schaltfläche als
|
||||||
|
|||||||
@@ -98,10 +98,16 @@ Darstellungsschicht liegen. Damit MUST jedes Bildschirmverhalten in
|
|||||||
automatischen Tests ohne Terminal prüfbar sein, und die Ausführungsschicht
|
automatischen Tests ohne Terminal prüfbar sein, und die Ausführungsschicht
|
||||||
MUST NOT von einer Terminal-Bibliothek abhängen.
|
MUST NOT von einer Terminal-Bibliothek abhängen.
|
||||||
|
|
||||||
|
Ein separater Einbetter SHALL die VM einschließlich Forms ohne Terminalbackend bauen und ausführen können. Der CLI-Runner SHALL das Backend ausdrücklich zuschalten.
|
||||||
|
|
||||||
#### Scenario: Bildschirmverhalten im Test ohne Terminal
|
#### Scenario: Bildschirmverhalten im Test ohne Terminal
|
||||||
- **WHEN** ein Testprogramm in einer Umgebung ohne Terminal `LOCATE 5, 10 : PRINT "x"` ausführt
|
- **WHEN** ein Testprogramm in einer Umgebung ohne Terminal `LOCATE 5, 10 : PRINT "x"` ausführt
|
||||||
- **THEN** trägt die Zelle (5, 10) das Zeichen `x` und der Test benötigt kein Terminal
|
- **THEN** trägt die Zelle (5, 10) das Zeichen `x` und der Test benötigt kein Terminal
|
||||||
|
|
||||||
|
#### Scenario: Terminalfreier Einbetter
|
||||||
|
- **WHEN** ein separates Programm ausschließlich die VM mit einem Capture-Host einbindet
|
||||||
|
- **THEN** enthält sein aufgelöster Abhängigkeitsbaum keine Terminalbibliothek und die Forms-Tests können darin laufen
|
||||||
|
|
||||||
### Requirement: Bildschirmanweisungen des Dialekts
|
### Requirement: Bildschirmanweisungen des Dialekts
|
||||||
`CLS`, `COLOR`, `LOCATE`, `WIDTH`, `VIEW PRINT` und die Anweisungsform von
|
`CLS`, `COLOR`, `LOCATE`, `WIDTH`, `VIEW PRINT` und die Anweisungsform von
|
||||||
`SCREEN` SHALL auf dem Zellenpuffer wirken. `CLS` SHALL den Scrollbereich
|
`SCREEN` SHALL auf dem Zellenpuffer wirken. `CLS` SHALL den Scrollbereich
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ ausführen. Der unterbrochene Zustand — Aufrufkette, Schleifen, Locals,
|
|||||||
Die Prüfung MUST NOT pro Opcode erfolgen; solange kein Trap definiert ist,
|
Die Prüfung MUST NOT pro Opcode erfolgen; solange kein Trap definiert ist,
|
||||||
MUST sie auf einen einzelnen Test hinauslaufen und MUST NOT die Uhr lesen.
|
MUST sie auf einen einzelnen Test hinauslaufen und MUST NOT die Uhr lesen.
|
||||||
|
|
||||||
|
Ohne klassische Traps, aktive Forms-Timer oder zeitabhängige Mausauswertung SHALL auch die Forms-Integration keine Hostzeit abfragen.
|
||||||
|
|
||||||
#### Scenario: Trap aus einer Prozedur heraus
|
#### Scenario: Trap aus einer Prozedur heraus
|
||||||
- **WHEN** ein Ereignis zugestellt wird, während eine `SUB` läuft, und das Trap-Ziel im Modulrumpf liegt
|
- **WHEN** ein Ereignis zugestellt wird, während eine `SUB` läuft, und das Trap-Ziel im Modulrumpf liegt
|
||||||
- **THEN** läuft der Handler und nach `RETURN` setzt die `SUB` an der unterbrochenen Anweisung fort
|
- **THEN** läuft der Handler und nach `RETURN` setzt die `SUB` an der unterbrochenen Anweisung fort
|
||||||
@@ -132,6 +134,10 @@ MUST sie auf einen einzelnen Test hinauslaufen und MUST NOT die Uhr lesen.
|
|||||||
- **WHEN** ein Programm ohne jede `ON …`-Trap-Definition ausgeführt wird
|
- **WHEN** ein Programm ohne jede `ON …`-Trap-Definition ausgeführt wird
|
||||||
- **THEN** fragt die Ausführung an keiner Anweisungsgrenze die Zeit ab, und der Durchsatz bleibt im Rahmen der Messgenauigkeit unverändert
|
- **THEN** fragt die Ausführung an keiner Anweisungsgrenze die Zeit ab, und der Durchsatz bleibt im Rahmen der Messgenauigkeit unverändert
|
||||||
|
|
||||||
|
#### Scenario: Einfaches Programm liest keine Ereigniszeit
|
||||||
|
- **WHEN** ein Programm nur Zuweisungen und END ausführt und ein Testhost Zeitabfragen zählt
|
||||||
|
- **THEN** ist der Zähler am Ende 0
|
||||||
|
|
||||||
### Requirement: Jede Schleife trägt eine Anweisungsgrenze im Kreis
|
### Requirement: Jede Schleife trägt eine Anweisungsgrenze im Kreis
|
||||||
Der zurückspringende Zweig jeder Schleife (`FOR`/`NEXT`, `WHILE`/`WEND`,
|
Der zurückspringende Zweig jeder Schleife (`FOR`/`NEXT`, `WHILE`/`WEND`,
|
||||||
`DO`/`LOOP` in allen Bedingungsformen) MUST über mindestens eine
|
`DO`/`LOOP` in allen Bedingungsformen) MUST über mindestens eine
|
||||||
|
|||||||
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