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:
@@ -1,13 +1,110 @@
|
||||
//! `tbc` — Standalone-Compiler von Terminal Basic.
|
||||
//!
|
||||
//! Wandelt Quelldateien und Projekte in binäre Ergebnisse (`.tbc`-Bytecode
|
||||
//! bzw. eigenständig ausführbare Programme) um. Geplante Unterbefehle
|
||||
//! (siehe PLAN.md):
|
||||
//! - `tbc build <projekt|datei.bas>` Kompilieren zu binärem Ergebnis
|
||||
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
|
||||
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
|
||||
//! Unterbefehle (Phase 2):
|
||||
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
|
||||
//! - `tbc build <datei.bas>` Kompilieren zu `datei.tbc`
|
||||
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
|
||||
//!
|
||||
//! Exit-Codes von `run` (Entscheidung D6, docs/tbvm-design.md):
|
||||
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
|
||||
//! 1 = Compile-Fehler/Bedienfehler.
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
println!("tbc — Terminal Basic Compiler (Projektrahmen, noch ohne Funktion)");
|
||||
Ok(())
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use tb_runtime::host::ConsoleHost;
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
match args.first().map(String::as_str) {
|
||||
Some("run") => cmd_run(&args[1..]),
|
||||
Some("build") => cmd_build(&args[1..]),
|
||||
Some("check") => cmd_check(&args[1..]),
|
||||
_ => {
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas>");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn module_name(path: &Path) -> String {
|
||||
path.file_stem()
|
||||
.map(|s| s.to_string_lossy().to_uppercase())
|
||||
.unwrap_or_else(|| "MODUL".into())
|
||||
}
|
||||
|
||||
fn compile(path_arg: Option<&String>) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
||||
let Some(path) = path_arg else {
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas>");
|
||||
return Err(ExitCode::from(1));
|
||||
};
|
||||
let path = PathBuf::from(path);
|
||||
let source = match std::fs::read_to_string(&path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("{}: {e}", path.display());
|
||||
return Err(ExitCode::from(1));
|
||||
}
|
||||
};
|
||||
match tb_vm::compile_source(&module_name(&path), &source) {
|
||||
Ok(m) => Ok((path, m)),
|
||||
Err(diags) => {
|
||||
for d in &diags {
|
||||
eprintln!("{}:{d}", path.display());
|
||||
}
|
||||
eprintln!("{} Fehler.", diags.len());
|
||||
Err(ExitCode::from(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_check(args: &[String]) -> ExitCode {
|
||||
match compile(args.first()) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(code) => code,
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_build(args: &[String]) -> ExitCode {
|
||||
let (path, module) = match compile(args.first()) {
|
||||
Ok(x) => x,
|
||||
Err(code) => return code,
|
||||
};
|
||||
let out = path.with_extension("tbc");
|
||||
match std::fs::write(&out, module.to_tbc()) {
|
||||
Ok(()) => {
|
||||
println!("{}", out.display());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}: {e}", out.display());
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_run(args: &[String]) -> ExitCode {
|
||||
let (_path, module) = match compile(args.first()) {
|
||||
Ok(x) => x,
|
||||
Err(code) => return code,
|
||||
};
|
||||
let mut vm = Vm::new(module);
|
||||
vm.rt.command = args[1..].join(" ");
|
||||
let mut host = ConsoleHost;
|
||||
match vm.run(&mut host) {
|
||||
RunEvent::Ended => ExitCode::SUCCESS,
|
||||
RunEvent::Stopped { line } => {
|
||||
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
|
||||
eprintln!("STOP in line {line}");
|
||||
ExitCode::from(3)
|
||||
}
|
||||
RunEvent::Error { code, line, message } => {
|
||||
eprintln!("Runtime error {code}: {message} in line {line}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } | RunEvent::Interrupted { .. } => {
|
||||
ExitCode::from(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
132
crates/tb-cli/tests/compat.rs
Normal file
132
crates/tb-cli/tests/compat.rs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user