//! `tbc` — Standalone-Compiler von Terminal Basic. //! //! Unterbefehle (Phase 2): //! - `tbc run ` Kompilieren und ausführen //! - `tbc build ` Zu `.tbc` kompilieren //! - `tbc check ` Syntax/Semantik prüfen //! - `tbc convert-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 = 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 | tbc convert-frm " ); ExitCode::from(1) } } } fn cmd_convert_frm(args: &[String]) -> ExitCode { if args.len() != 2 { eprintln!("Aufruf: tbc convert-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 "); 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 "); 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, 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 { if self.puffer.is_empty() && blockierend { self.nachfuellen(); } self.puffer.pop_front() } fn warten(&mut self, deadline_ms: Option) -> Option { 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 } }