364 lines
12 KiB
Rust
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;
|