339 lines
11 KiB
Rust
339 lines
11 KiB
Rust
//! `tbc` — Standalone-Compiler von Terminal Basic.
|
|
//!
|
|
//! Unterbefehle (Phase 2):
|
|
//! - `tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>` Kompilieren und ausführen
|
|
//! - `tbc build <datei.bas|datei.frm|projekt.mak>` Zu `.tbc` kompilieren
|
|
//! - `tbc check <datei.bas|datei.frm|projekt.mak>` Syntax/Semantik prüfen
|
|
//! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln
|
|
//!
|
|
//! Exit-Codes von `run` (Entscheidung D6, docs/tbvm-design.md):
|
|
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
|
|
//! 1 = Compile-Fehler/Bedienfehler.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::ExitCode;
|
|
use tb_runtime::host::{Ereignis, Host};
|
|
use tb_ui::host::TerminalHost;
|
|
use tb_vm::interp::{RunEvent, Vm};
|
|
use tb_vm::project_io::{module_name, new_execution, run_target, SourceLoader};
|
|
|
|
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..]),
|
|
Some("convert-frm") => cmd_convert_frm(&args[1..]),
|
|
_ => {
|
|
eprintln!(
|
|
"Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak|datei.tbc> | tbc convert-frm <quelle.frm> <ziel.frm>"
|
|
);
|
|
ExitCode::from(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cmd_convert_frm(args: &[String]) -> ExitCode {
|
|
if args.len() != 2 {
|
|
eprintln!("Aufruf: tbc convert-frm <quelle.frm> <ziel.frm>");
|
|
return ExitCode::from(1);
|
|
}
|
|
let input = Path::new(&args[0]);
|
|
let output = Path::new(&args[1]);
|
|
let bytes = match std::fs::read(input) {
|
|
Ok(bytes) => bytes,
|
|
Err(error) => {
|
|
eprintln!("{}: {error}", input.display());
|
|
return ExitCode::from(1);
|
|
}
|
|
};
|
|
let converted = match tb_ui::frm::read_binary(&input.display().to_string(), &bytes) {
|
|
Ok(converted) => converted,
|
|
Err(error) => {
|
|
eprintln!("{error}");
|
|
return ExitCode::from(1);
|
|
}
|
|
};
|
|
let text = tb_ui::frm::write_text(&converted.form);
|
|
if let Err(error) = std::fs::write(output, text) {
|
|
eprintln!("{}: {error}", output.display());
|
|
return ExitCode::from(1);
|
|
}
|
|
for warning in &converted.skipped {
|
|
eprintln!(
|
|
"{}: Byte 0x{:04x}: {} nicht übernommen",
|
|
input.display(),
|
|
warning.offset,
|
|
warning.name
|
|
);
|
|
}
|
|
eprintln!(
|
|
"{}: {} nicht übernommene Binärangaben",
|
|
input.display(),
|
|
converted.skipped.len()
|
|
);
|
|
println!("{}", output.display());
|
|
ExitCode::SUCCESS
|
|
}
|
|
|
|
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|datei.frm|projekt.mak|datei.tbc>");
|
|
return Err(ExitCode::from(1));
|
|
};
|
|
let path = PathBuf::from(path);
|
|
if path
|
|
.extension()
|
|
.is_some_and(|ext| ext.eq_ignore_ascii_case("tbc"))
|
|
{
|
|
let bytes = std::fs::read(&path).map_err(|e| {
|
|
eprintln!("{}: {e}", path.display());
|
|
ExitCode::from(1)
|
|
})?;
|
|
let module = tb_vm::bytecode::CompiledModule::from_tbc(&bytes).map_err(|e| {
|
|
eprintln!("{}: {e}", path.display());
|
|
ExitCode::from(1)
|
|
})?;
|
|
return Ok((path, module));
|
|
}
|
|
let input = match SourceLoader::default().load(&path) {
|
|
Ok(input) => input,
|
|
Err(error) => {
|
|
eprintln!("{error}");
|
|
return Err(ExitCode::from(1));
|
|
}
|
|
};
|
|
let compiled = input.compile(&mut Default::default(), &module_name(&path));
|
|
match compiled {
|
|
Ok(m) => Ok((path, m)),
|
|
Err(diags) => {
|
|
for d in &diags {
|
|
eprintln!("{d}");
|
|
}
|
|
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 {
|
|
// Ohne Terminal (Pipe, Skript, CI) läuft das Programm im PipeHost:
|
|
// Eingabe zeilenweise von stdin, Ausgabe am Ende als Snapshot.
|
|
let result = match TerminalHost::new() {
|
|
Ok(mut host) => {
|
|
let size = host.groesse().ok();
|
|
let result = run_chain(args, &mut host, size);
|
|
drop(host); // Alternativschirm verlassen, bevor gedruckt wird
|
|
result
|
|
}
|
|
Err(_) => run_chain(args, &mut PipeHost::new(), None),
|
|
};
|
|
let (ereignis, vm) = match result {
|
|
Ok(result) => result,
|
|
Err(code) => return code,
|
|
};
|
|
print!("{}", tb_runtime::snapshot::text(&vm.rt.screen));
|
|
// `LPRINT` sammelt im Druckerpuffer; am Programmende geht er in die
|
|
// Datei LPT1.TXT im aktuellen Verzeichnis (dokumentierte Abweichung —
|
|
// einen Druckerkanal gibt es plattformübergreifend nicht).
|
|
if !vm.rt.print.drucker.is_empty() {
|
|
if let Err(e) = std::fs::write("LPT1.TXT", &vm.rt.print.drucker) {
|
|
eprintln!("Druckerausgabe nicht schreibbar: {e}");
|
|
}
|
|
}
|
|
|
|
match ereignis {
|
|
RunEvent::Ended => ExitCode::SUCCESS,
|
|
RunEvent::Stopped { line } => {
|
|
// STOP außerhalb der IDE: Meldung + Exit-Code ≠ 0 (D6).
|
|
eprintln!("{}:{line}: STOP in line {line}", vm.current_file());
|
|
ExitCode::from(3)
|
|
}
|
|
RunEvent::Error {
|
|
code,
|
|
line,
|
|
message,
|
|
} => {
|
|
eprintln!(
|
|
"{}:{line}:{}: Runtime error {code}: {message} in line {line}",
|
|
vm.current_file(),
|
|
vm.current_source_pos().column
|
|
);
|
|
ExitCode::from(2)
|
|
}
|
|
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
|
RunEvent::Interrupted { line } => {
|
|
eprintln!("Abgebrochen in Zeile {line}");
|
|
ExitCode::from(3)
|
|
}
|
|
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
|
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } => ExitCode::from(2),
|
|
RunEvent::Restart { .. } => unreachable!("RUN wird vom Runner aufgelöst"),
|
|
}
|
|
}
|
|
|
|
fn run_chain(
|
|
args: &[String],
|
|
host: &mut dyn Host,
|
|
size: Option<(usize, usize)>,
|
|
) -> Result<(RunEvent, Vm), ExitCode> {
|
|
let Some(first) = args.first() else {
|
|
eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak|datei.tbc>");
|
|
return Err(ExitCode::from(1));
|
|
};
|
|
let mut current = PathBuf::from(first);
|
|
let mut start_line = None;
|
|
let command = args[1..].join(" ");
|
|
loop {
|
|
let current_arg = current.display().to_string();
|
|
let (path, module) = compile(Some(¤t_arg))?;
|
|
let mut vm = new_execution(module, &command, size, start_line.take()).map_err(|error| {
|
|
eprintln!("Runtime error {}: {}", error.0, error);
|
|
ExitCode::from(2)
|
|
})?;
|
|
if !vm.rt.zeitpunkt().1 {
|
|
eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
|
|
}
|
|
let event = vm.run(host);
|
|
let event =
|
|
if event == RunEvent::Ended && vm.forms.has_visible_forms() && !vm.is_terminated() {
|
|
vm.run_visible_forms(host)
|
|
} else {
|
|
event
|
|
};
|
|
match event {
|
|
RunEvent::Restart { program, line } => {
|
|
if let Some(program) = program {
|
|
current = run_target(&path, &program).map_err(|error| {
|
|
eprintln!("{error}");
|
|
ExitCode::from(1)
|
|
})?;
|
|
}
|
|
start_line = line;
|
|
}
|
|
event => return Ok((event, vm)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Host ohne Terminal: für Pipes und Skripte (`tbc run x.bas < eingabe.txt`).
|
|
/// Zeigt während des Laufs nichts an; die Ausgabe entsteht am Ende aus dem
|
|
/// Bildschirm-Snapshot. Tastendrücke kommen zeilenweise von stdin.
|
|
struct PipeHost {
|
|
puffer: std::collections::VecDeque<Ereignis>,
|
|
eof: bool,
|
|
start: std::time::Instant,
|
|
}
|
|
|
|
impl PipeHost {
|
|
fn new() -> Self {
|
|
let mut host = PipeHost {
|
|
puffer: std::collections::VecDeque::new(),
|
|
eof: false,
|
|
start: std::time::Instant::now(),
|
|
};
|
|
use std::io::{IsTerminal, Read};
|
|
let mut stdin = std::io::stdin();
|
|
if !stdin.is_terminal() {
|
|
let mut input = String::new();
|
|
let _ = stdin.read_to_string(&mut input);
|
|
for line in input.split_inclusive('\n') {
|
|
for character in line.trim_end_matches(['\r', '\n']).chars() {
|
|
host.puffer
|
|
.push_back(Ereignis::Taste(character.to_string(), 0));
|
|
}
|
|
host.puffer.push_back(Ereignis::Taste(
|
|
tb_runtime::host::taste::ENTER.to_string(),
|
|
0,
|
|
));
|
|
}
|
|
host.puffer.push_back(Ereignis::Ende);
|
|
host.eof = true;
|
|
}
|
|
host
|
|
}
|
|
|
|
/// Eine Zeile von stdin in Tastendrücke zerlegen.
|
|
fn nachfuellen(&mut self) {
|
|
use std::io::BufRead;
|
|
if self.eof {
|
|
return;
|
|
}
|
|
let mut zeile = String::new();
|
|
match std::io::stdin().lock().read_line(&mut zeile) {
|
|
Ok(0) | Err(_) => {
|
|
self.eof = true;
|
|
self.puffer.push_back(Ereignis::Ende);
|
|
}
|
|
Ok(_) => {
|
|
while zeile.ends_with('\n') || zeile.ends_with('\r') {
|
|
zeile.pop();
|
|
}
|
|
for c in zeile.chars() {
|
|
self.puffer.push_back(Ereignis::Taste(c.to_string(), 0));
|
|
}
|
|
self.puffer.push_back(Ereignis::Taste(
|
|
tb_runtime::host::taste::ENTER.to_string(),
|
|
0,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Host for PipeHost {
|
|
fn present(&mut self, _screen: &tb_runtime::screen::TextScreen) {}
|
|
|
|
fn next_event(&mut self, blockierend: bool) -> Option<Ereignis> {
|
|
if self.puffer.is_empty() && blockierend {
|
|
self.nachfuellen();
|
|
}
|
|
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 {
|
|
self.start.elapsed().as_millis() as u64
|
|
}
|
|
}
|