Phase 6: Native Executables implementieren und Change archivieren

This commit is contained in:
2026-09-07 14:16:32 +02:00
parent 993c3e7638
commit 60d37ec79d
28 changed files with 1951 additions and 215 deletions

View File

@@ -12,10 +12,9 @@
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};
use tb_vm::project_io::{module_name, SourceLoader};
mod export_cancel;
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
@@ -126,215 +125,147 @@ fn cmd_check(args: &[String]) -> ExitCode {
}
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());
match build(args) {
Ok(path) => {
println!("{}", path.display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{}: {e}", out.display());
Err(error) => {
eprintln!("{error:#}");
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}");
fn build(args: &[String]) -> anyhow::Result<PathBuf> {
use anyhow::{bail, ensure, Context};
let mut source = None;
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() {
"--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}"
);
}
"--" => {
for value in args.by_ref() {
ensure!(
source.replace(value.clone()).is_none(),
"Mehrere Quelldateien angegeben"
);
}
}
value if value.starts_with('-') => bail!("Unbekannte Build-Option: {value}"),
_ => {
ensure!(
source.replace(arg.clone()).is_none(),
"Mehrere Quelldateien angegeben"
);
}
}
}
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"),
let source = source.context(
"Aufruf: tbc build <Quelle> [--exe --target <Triple> --template <tbrt> -o <Ziel> --force]",
)?;
ensure!(
exe || (target.is_none() && template.is_none() && output.is_none() && !force),
"Native Ausgabeoptionen benötigen --exe"
);
let selected = if exe {
Some(match target {
Some(s) => tb_export::Target::parse(&s)?,
None => tb_export::Target::host()?,
})
} else {
None
};
let (path, module) =
compile(Some(&source)).map_err(|_| anyhow::anyhow!("Build fehlgeschlagen"))?;
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()),
};
let cancel = export_cancel::ExportCancel::new()?;
tb_export::export(
tb_export::Export {
module: &module,
target,
template: &template,
output: &out,
overwrite: force,
protected: &[path],
},
&|| cancel.cancelled(),
)?;
Ok(out)
} else {
let out = path.with_extension("tbc");
std::fs::write(&out, module.to_tbc())?;
Ok(out)
}
}
fn run_chain(
args: &[String],
host: &mut dyn Host,
size: Option<(usize, usize)>,
) -> Result<(RunEvent, Vm), ExitCode> {
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 Err(ExitCode::from(1));
return 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(&current_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)),
}
}
tb_runner::run(Path::new(first), &args[1..].join(" "), |path| {
compile(Some(&path.display().to_string())).map(|(_, module)| module)
})
}
/// 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
}
#[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)]