Phase 4: Ereignisschleife und klassische Traps

This commit is contained in:
2026-09-04 15:56:47 +02:00
parent 060674b466
commit 5cf5a6582c
40 changed files with 4024 additions and 176 deletions

View File

@@ -153,11 +153,16 @@ fn cmd_run(args: &[String]) -> ExitCode {
struct PipeHost {
puffer: std::collections::VecDeque<Ereignis>,
eof: bool,
start: std::time::Instant,
}
impl PipeHost {
fn new() -> Self {
PipeHost { puffer: std::collections::VecDeque::new(), eof: false }
PipeHost {
puffer: std::collections::VecDeque::new(),
eof: false,
start: std::time::Instant::now(),
}
}
/// Eine Zeile von stdin in Tastendrücke zerlegen.
@@ -177,10 +182,12 @@ impl PipeHost {
zeile.pop();
}
for c in zeile.chars() {
self.puffer.push_back(Ereignis::Taste(c.to_string()));
self.puffer.push_back(Ereignis::Taste(c.to_string(), 0));
}
self.puffer
.push_back(Ereignis::Taste(tb_runtime::host::taste::ENTER.to_string()));
self.puffer.push_back(Ereignis::Taste(
tb_runtime::host::taste::ENTER.to_string(),
0,
));
}
}
}
@@ -195,4 +202,8 @@ impl Host for PipeHost {
}
self.puffer.pop_front()
}
fn jetzt_ms(&mut self) -> u64 {
self.start.elapsed().as_millis() as u64
}
}

View File

@@ -39,7 +39,9 @@ fn groesse_lesen(name: &str, wert: &str) -> (usize, usize) {
/// Wert einer Kopfzeilen-Direktive `' <marke>: <wert>` aus den ersten Zeilen.
fn direktive(src: &str, marke: &str) -> Option<String> {
src.lines()
.take(4)
// Kopfzeilenfenster: Groesse, Zeitzone, Uhr, Tasten, Tempdir,
// Resize passen zusammen in die ersten Zeilen.
.take(8)
.find(|l| l.contains(marke))
.map(|l| l.split(marke).nth(1).unwrap().trim().to_string())
}
@@ -94,6 +96,120 @@ fn resize_direktive(name: &str, src: &str) -> Option<(usize, (usize, usize))> {
}
}
/// Vorbereitete Tastendrücke: `' tb-keys: <folge>`. Sondertasten als
/// `<F1>`…`<F12>`, sonst je ein Zeichen; `+` davor bedeutet Umschalt,
/// `^` Strg, `%` Alt — gebraucht für die benutzerdefinierten Trap-Tasten.
fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
use tb_runtime::host::{taste, umschalt, Ereignis};
let Some(wert) = direktive(src, "tb-keys:") else {
return Vec::new();
};
let mut out = Vec::new();
let zeichen: Vec<char> = wert.chars().collect();
let mut i = 0;
while i < zeichen.len() {
let mut shift = 0u8;
while i < zeichen.len() && matches!(zeichen[i], '+' | '^' | '%') {
shift |= match zeichen[i] {
'+' => umschalt::SHIFT,
'^' => umschalt::CTRL,
_ => umschalt::ALT,
};
i += 1;
}
if i >= zeichen.len() {
panic!("{name}: tb-keys endet mit einem Umschaltzeichen");
}
if zeichen[i] == '<' {
let ende = zeichen[i..]
.iter()
.position(|c| *c == '>')
.unwrap_or_else(|| panic!("{name}: tb-keys: `<` ohne `>`"));
let nm: String = zeichen[i + 1..i + ende].iter().collect();
let code = match nm.to_uppercase().as_str() {
"F1" => 0x3B,
"F2" => 0x3C,
"F3" => 0x3D,
"F4" => 0x3E,
"F5" => 0x3F,
"F6" => 0x40,
"F7" => 0x41,
"F8" => 0x42,
"F9" => 0x43,
"F10" => 0x44,
"F11" => 0x85,
"F12" => 0x86,
"UP" => 0x48,
"LEFT" => 0x4B,
"RIGHT" => 0x4D,
"DOWN" => 0x50,
other => panic!("{name}: tb-keys kennt `<{other}>` nicht"),
};
out.push(Ereignis::Taste(taste::sonder(code), shift));
i += ende + 1;
} else {
out.push(Ereignis::Taste(zeichen[i].to_string(), shift));
i += 1;
}
}
out
}
/// Virtuelle Uhr: `' tb-clock: <n>ms/Zustellpunkt`. Ohne die Direktive
/// steht die Uhr des Testhosts still — ein Programm mit Zeit-Traps liefe
/// dann endlos. Der Harness weist es deshalb ab, statt auf die Systemuhr
/// auszuweichen (design.md, D6).
fn clock_direktive(name: &str, src: &str) -> Option<u64> {
let wert = direktive(src, "tb-clock:")?;
let ms = wert
.trim()
.strip_suffix("ms/Zustellpunkt")
.unwrap_or_else(|| panic!("{name}: tb-clock erwartet `<n>ms/Zustellpunkt`, fand {wert:?}"))
.trim();
Some(
ms.parse()
.unwrap_or_else(|_| panic!("{name}: {ms:?} ist keine Zahl")),
)
}
/// Nutzt das Programm zeitgesteuerte Traps? Grobe, aber ausreichende
/// Prüfung: ohne `ON TIMER` gibt es keine Fälligkeit zu rechnen.
fn braucht_uhr(src: &str) -> bool {
src.to_uppercase().contains("ON TIMER")
}
fn clock_schritt(name: &str, src: &str) -> Option<u64> {
let schritt = clock_direktive(name, src);
assert!(
schritt.is_some() || !braucht_uhr(src),
"{name}: nutzt zeitgesteuerte Traps, deklariert aber keinen Zeitverlauf — \
erste Zeilen brauchen `' tb-clock: <n>ms/Zustellpunkt`"
);
schritt
}
/// Testhost mit virtueller Uhr: sie rückt bei jedem Blick auf die Zeit um
/// einen festen Schritt vor. Damit ist der Lauf reproduzierbar und
/// braucht keine Wartezeit.
struct UhrHost {
inner: CaptureHost,
schritt_ms: u64,
}
impl tb_runtime::host::Host for UhrHost {
fn present(&mut self, s: &tb_runtime::screen::TextScreen) {
self.inner.present(s);
}
fn next_event(&mut self, blockierend: bool) -> Option<tb_runtime::host::Ereignis> {
self.inner.next_event(blockierend)
}
fn jetzt_ms(&mut self) -> u64 {
let jetzt = self.inner.jetzt_ms();
self.inner.uhr_vorruecken(self.schritt_ms);
jetzt
}
}
/// Arbeitsverzeichnis für die Dauer eines Korpusprogramms. Programme mit
/// Datei-E/A laufen in einem frischen temporären Verzeichnis und hinterlassen
/// nichts im Projektbaum; beim Verlassen wird es samt Inhalt entfernt.
@@ -146,6 +262,24 @@ fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String {
tb_runtime::host::Ereignis::Groesse { cols: c, rows: r },
);
}
for t in tasten_direktive(&name, &src) {
host.ereignis(t);
}
let schritt = clock_schritt(&name, &src);
if let Some(schritt_ms) = schritt {
let mut host = UhrHost {
inner: host,
schritt_ms,
};
return match vm.run(&mut host) {
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
other => panic!(
"{}: unerwartetes Laufzeitende {other:?}\nBildschirm bisher:\n{}",
path.display(),
tb_runtime::snapshot::snapshot(&vm.rt.screen)
),
};
}
match vm.run(&mut host) {
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
other => panic!(
@@ -207,12 +341,8 @@ fn assert_output_matches(file: &str, want: &str, got: &str) {
.position(|(a, b)| a != b)
.map(|i| i / 2 + 1)
.unwrap_or(0);
let paar = |s: &str| {
s.chars()
.skip((spalte - 1) * 2)
.take(2)
.collect::<String>()
};
let paar =
|s: &str| s.chars().skip((spalte - 1) * 2).take(2).collect::<String>();
panic!(
"{file}: Farbabweichung in Zeile {}, Spalte {spalte}:\n \
Soll: {}\n Ist: {}",
@@ -257,12 +387,31 @@ fn korpus_laeuft_mit_korrekter_ausgabe() {
let got = run_corpus_file(&path, Some((*cols, *rows)));
let etikett = format!("{name} @ {cols}x{rows}");
assert_output_matches(&etikett, &want, &got);
let wiederholt = run_corpus_file(&path, Some((*cols, *rows)));
assert_eq!(
got, wiederholt,
"{etikett}: zwei Läufe weichen voneinander ab"
);
checked += 1;
}
}
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}");
}
#[test]
fn uhr_direktive_wird_gelesen() {
assert_eq!(
clock_schritt("uhr.bas", "' tb-clock: 25ms/Zustellpunkt\nON TIMER(1)"),
Some(25)
);
}
#[test]
#[should_panic(expected = "deklariert aber keinen Zeitverlauf")]
fn zeittrap_ohne_uhr_direktive_wird_abgewiesen() {
clock_schritt("uhr.bas", "ON TIMER(1) GOSUB Tick");
}
// ---- tbc-Binary (Exit-Codes nach D6) ----------------------------------------
#[test]