Phase 2 abgeschlossen: Bytecode, TBVM, Runtime-Scheibe, tbc run

- Sema zum Lowering-Pass umgebaut: typisiertes HIR (Slots, explizite
  Konvertierungsknoten) als Codegen-Eingabe; BYREF verlangt exakten Typ
- Bytecode-Feindesign umgesetzt: monomorpher Opcode-Satz,
  .tbc-Container (Formatversion 1) mit eigenem Writer/Reader
- Codegenerator HIR -> Bytecode (Fixup-Listen, keine globalen Passes)
- TBVM-Interpreter: Kontrollfluss, GOSUB-Stack je Frame, BYREF/BYVAL,
  STATIC, DEF FN, DATA/READ/RESTORE, ON [LOCAL] ERROR/RESUME/ERR/ERL,
  Breakpoints/Einzelschritt/Inspektion, STOP fortsetzbar
- Runtime-Scheibe: Host-Trait (Konsole/Capture), Builtin-Tabelle,
  Konvertierungsmatrix, PRINT-Formatierung/Druckzonen, Stringfunktionen
- tbc run/build/check mit Exit-Codes nach Entscheidung D6
- Korpus-Harness (byte-genauer Vergleich) + 3 neue Korpusdateien
  (konvertierung, fehlerbehandlung, byref); 137 Tests gruen
- Benchmarks: Einzelmodul 1,2 ms / Projekt 49.760 Zeilen 124 ms
  (Budgets eingehalten), VM ~5 Mio Schleifeniterationen/s
- Doku fortgeschrieben (tbvm-design, sprachreferenz, PLAN);
  verlagerte Punkte als explizite Aufgaben in Phase 3
- OpenSpec-Change phase-2-bytecode-vm (27/27 Tasks)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 11:28:07 +02:00
parent da23d52036
commit f7e57b0bd8
42 changed files with 9225 additions and 484 deletions

View File

@@ -0,0 +1,132 @@
//! Kompatibilitäts-Harness (Phase-2-Meilenstein): jede Korpusdatei
//! `tests/compat/*.bas` wird kompiliert, im Capture-Host ausgeführt und
//! byte-genau gegen ihre `.out` verglichen. Bei Abweichung nennt der
//! Test Datei, erste abweichende Zeile sowie Soll und Ist.
use std::path::{Path, PathBuf};
use std::process::Command;
use tb_runtime::host::CaptureHost;
use tb_vm::interp::{RunEvent, Vm};
fn compat_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat")
}
fn run_corpus_file(path: &Path) -> String {
let src = std::fs::read_to_string(path).unwrap();
let name = path.file_stem().unwrap().to_string_lossy().to_uppercase();
let module = tb_vm::compile_source(&name, &src)
.unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display()));
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
match vm.run(&mut host) {
RunEvent::Ended => host.output,
other => panic!(
"{}: unerwartetes Laufzeitende {other:?}\nAusgabe bisher:\n{}",
path.display(),
host.output
),
}
}
/// Erste abweichende Zeile melden (byte-genau, inkl. Leerzeichen am Ende).
fn assert_output_matches(file: &str, want: &str, got: &str) {
if want == got {
return;
}
let want_lines: Vec<&str> = want.split('\n').collect();
let got_lines: Vec<&str> = got.split('\n').collect();
for (i, (w, g)) in want_lines.iter().zip(got_lines.iter()).enumerate() {
if w != g {
panic!(
"{file}: Abweichung in Zeile {}:\n Soll: {w:?}\n Ist: {g:?}",
i + 1
);
}
}
panic!(
"{file}: Zeilenanzahl weicht ab (Soll {} / Ist {}).\nSoll:\n{want}\nIst:\n{got}",
want_lines.len(),
got_lines.len()
);
}
#[test]
fn korpus_laeuft_mit_korrekter_ausgabe() {
let dir = compat_dir();
let mut checked = 0;
let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
.expect("tests/compat fehlt")
.map(|e| e.unwrap().path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("bas"))
.collect();
entries.sort();
for path in entries {
let name = path.file_name().unwrap().to_string_lossy().to_string();
let out_path = path.with_extension("out");
let want = std::fs::read_to_string(&out_path)
.unwrap_or_else(|_| panic!("{name}: Sollausgabe {} fehlt", out_path.display()));
// .out-Dateien sind LF-normiert (.gitattributes); zur Sicherheit
// CRLF des Checkouts entfernen.
let want = want.replace("\r\n", "\n");
let got = run_corpus_file(&path);
assert_output_matches(&name, &want, &got);
checked += 1;
}
assert!(checked >= 5, "zu wenige Korpusdateien gefunden: {checked}");
}
// ---- tbc-Binary (Exit-Codes nach D6) ----------------------------------------
#[test]
fn tbc_run_hello() {
let exe = env!("CARGO_BIN_EXE_tbc");
let out = Command::new(exe)
.args(["run"])
.arg(compat_dir().join("hello.bas"))
.output()
.expect("tbc startet");
assert!(out.status.success(), "{out:?}");
assert_eq!(String::from_utf8_lossy(&out.stdout), "Hallo, Welt!\n");
}
#[test]
fn tbc_run_stop_exitcode() {
let exe = env!("CARGO_BIN_EXE_tbc");
let dir = std::env::temp_dir();
let f = dir.join("tb_phase2_stop_test.bas");
std::fs::write(&f, "PRINT \"x\"\nSTOP\n").unwrap();
let out = Command::new(exe).args(["run"]).arg(&f).output().unwrap();
assert_eq!(out.status.code(), Some(3), "{out:?}");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("STOP in line 2"), "{err}");
let _ = std::fs::remove_file(&f);
}
#[test]
fn tbc_run_laufzeitfehler_exitcode() {
let exe = env!("CARGO_BIN_EXE_tbc");
let dir = std::env::temp_dir();
let f = dir.join("tb_phase2_err_test.bas");
std::fs::write(&f, "i% = 40000\n").unwrap();
let out = Command::new(exe).args(["run"]).arg(&f).output().unwrap();
assert_eq!(out.status.code(), Some(2), "{out:?}");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("Overflow"), "{err}");
let _ = std::fs::remove_file(&f);
}
#[test]
fn tbc_build_erzeugt_tbc() {
let exe = env!("CARGO_BIN_EXE_tbc");
let dir = std::env::temp_dir();
let f = dir.join("tb_phase2_build_test.bas");
std::fs::write(&f, "PRINT 1\n").unwrap();
let out = Command::new(exe).args(["build"]).arg(&f).output().unwrap();
assert!(out.status.success(), "{out:?}");
let tbc = f.with_extension("tbc");
let bytes = std::fs::read(&tbc).unwrap();
assert_eq!(&bytes[..4], b"TBC\0");
let _ = std::fs::remove_file(&f);
let _ = std::fs::remove_file(&tbc);
}