Files

364 lines
12 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_vm::project_io::{module_name, SourceLoader};
mod export_cancel;
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("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 <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|link|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 {
if args.len() != 1 {
eprintln!("Aufruf: tbc check <Projekt>");
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<PathBuf> {
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 <Quelle> [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<tb_frontend::Diagnostic>| {
anyhow::anyhow!(errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.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 <datei.bas|datei.frm|projekt.mak|datei.tbc>");
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;