- 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>
133 lines
4.7 KiB
Rust
133 lines
4.7 KiB
Rust
//! 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);
|
|
}
|