//! `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_vm::project_io::{module_name, SourceLoader}; mod export_cancel; 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("link") => cmd_link(&args[1..]), Some("check") => cmd_check(&args[1..]), Some("convert-frm") => cmd_convert_frm(&args[1..]), _ => { eprintln!( "Aufruf: tbc run|build|link|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|link|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 { if args.len() != 1 { eprintln!("Aufruf: tbc check "); return ExitCode::from(1); } match compile(args.first()) { Ok(_) => ExitCode::SUCCESS, Err(code) => code, } } fn cmd_build(args: &[String]) -> ExitCode { cmd_output(args, false) } fn cmd_link(args: &[String]) -> ExitCode { cmd_output(args, true) } fn cmd_output(args: &[String], link: bool) -> ExitCode { match build(args, link) { Ok(path) => { println!("{}", path.display()); ExitCode::SUCCESS } Err(error) => { eprintln!("{error:#}"); ExitCode::from(1) } } } fn build(args: &[String], link: bool) -> anyhow::Result { use anyhow::{bail, ensure, Context}; let mut inputs = Vec::new(); let mut library = false; let mut exe = false; let mut force = false; let mut output = None; let mut target = None; let mut template = None; let mut args = args.iter(); while let Some(arg) = args.next() { match arg.as_str() { "--library" => { ensure!( !library && !link, "--library ist nur bei build zulässig und darf nicht doppelt sein" ); library = true; } "--exe" => { ensure!(!exe, "Doppeltes --exe"); exe = true; } "--force" => { ensure!(!force, "Doppeltes --force"); force = true; } "-o" | "--output" | "--target" | "--template" => { let value = args .next() .filter(|v| !v.starts_with('-')) .with_context(|| format!("Wert fehlt für {arg}"))?; let slot = match arg.as_str() { "--target" => &mut target, "--template" => &mut template, _ => &mut output, }; ensure!( slot.replace(value.clone()).is_none(), "Doppelte Option {arg}" ); } "--" => inputs.extend(args.by_ref().cloned()), value if value.starts_with('-') => bail!("Unbekannte Build-Option: {value}"), _ => inputs.push(arg.clone()), } } ensure!(!inputs.is_empty(), "Aufruf: tbc build|link [Bibliothek.tbl ...] [--library | --exe] [-o Ziel] [--force]"); ensure!(!(exe && library), "--library und --exe schließen sich aus"); ensure!( exe || (target.is_none() && template.is_none()), "--target/--template benötigen --exe" ); ensure!( exe || library || link || output.is_none(), "Ausgabeoptionen benötigen --exe, --library oder link" ); let source = &inputs[0]; let path = PathBuf::from(source); let tbc = tb_vm::project_io::has_extension(&path, "tbc"); ensure!(!tbc || (!library && inputs.len()==1 && (!link || exe)), "Fertiges TBC kann nur ohne zusätzliche TBL mit link --exe verpackt werden; es ist kein Library-Produkt"); let mut protected = vec![path.clone()]; let mut compiler = tb_vm::project::ProjectCompiler::default(); let loaded = if tbc { None } else { let mut project = SourceLoader::default() .load(&path) .map_err(anyhow::Error::msg)?; for input in &inputs[1..] { project .add_library(Path::new(input)) .map_err(anyhow::Error::msg)?; } protected.extend(project.protected_inputs()); Some(project) }; let has_libraries = loaded.as_ref().is_some_and(|p| !p.libraries.is_empty()); let diags = |errors: Vec| { anyhow::anyhow!(errors .iter() .map(ToString::to_string) .collect::>() .join("\n")) }; let cancel = export_cancel::ExportCancel::new()?; if library { let product = loaded .as_ref() .unwrap() .compile_library(&mut compiler) .map_err(diags)?; let bytes = product.to_tbl().map_err(anyhow::Error::msg)?; let out = output .map(PathBuf::from) .unwrap_or_else(|| path.with_extension("tbl")); tb_export::publish( &out, force, &protected, &|| cancel.cancelled(), |temporary| { std::fs::write(temporary, &bytes)?; tb_vm::library::Library::from_tbl(&std::fs::read(temporary)?) .map_err(anyhow::Error::msg)?; Ok(()) }, )?; return Ok(out); } let selected = if exe { Some(match target { Some(s) => tb_export::Target::parse(&s)?, None => tb_export::Target::host()?, }) } else { None }; let module = if let Some(project) = loaded { project .compile(&mut compiler, &module_name(&path)) .map_err(diags)? } else { tb_vm::bytecode::CompiledModule::from_tbc(&std::fs::read(&path)?) .map_err(|e| anyhow::anyhow!("{e}"))? }; if let Some(target) = selected { let out = output.map(PathBuf::from).unwrap_or_else(|| { path.with_extension(if target == tb_export::Target::WindowsAmd64 { "exe" } else { "" }) }); let template = match template { Some(p) => PathBuf::from(p), None => std::env::current_exe()? .parent() .context("Compilerpfad ohne Verzeichnis")? .join("runtimes") .join(target.triple()) .join(target.runtime_name()), }; tb_export::export( tb_export::Export { module: &module, target, template: &template, output: &out, overwrite: force, protected: &protected, }, &|| cancel.cancelled(), )?; Ok(out) } else { let legacy = !link && !has_libraries && output.is_none() && !force && inputs.len() == 1; let out = output .map(PathBuf::from) .unwrap_or_else(|| path.with_extension("tbc")); if legacy && !tbc && protected.iter().all(|p| { tb_vm::project_io::identity(p).ok() != tb_vm::project_io::identity(&out).ok() }) { std::fs::write(&out, module.to_tbc())?; } else { tb_export::publish( &out, force, &protected, &|| cancel.cancelled(), |temporary| { std::fs::write(temporary, module.to_tbc())?; tb_vm::bytecode::CompiledModule::from_tbc(&std::fs::read(temporary)?) .map_err(|e| anyhow::anyhow!("{e}"))?; Ok(()) }, )?; } Ok(out) } } fn cmd_run(args: &[String]) -> ExitCode { let Some(first) = args.first() else { eprintln!("Aufruf: tbc run "); return ExitCode::from(1); }; tb_runner::run(Path::new(first), &args[1..].join(" "), |path| { compile(Some(&path.display().to_string())).map(|(_, module)| module) }) } #[cfg(test)] fn run_chain( args: &[String], host: &mut dyn tb_runtime::host::Host, size: Option<(usize, usize)>, ) -> Result<(tb_vm::interp::RunEvent, tb_vm::interp::Vm), ExitCode> { let first = args.first().ok_or(ExitCode::from(1))?; tb_runner::run_chain( Path::new(first), &args[1..].join(" "), |path| compile(Some(&path.display().to_string())).map(|(_, module)| module), host, size, ) } #[cfg(test)] mod ide_acceptance;