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

@@ -0,0 +1,11 @@
[package]
name = "tb-runner"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
tb-vm.workspace = true
tb-runtime.workspace = true
tb-export.workspace = true
tb-ui = { workspace = true, features = ["terminal"] }

View File

@@ -0,0 +1,58 @@
//! Native Runtime: kein IDE- oder BASIC-Quellcompiler-Einstieg.
use std::process::ExitCode;
#[used]
static MARKER: [u8; 32] = tb_export::runtime_marker(if cfg!(target_os = "windows") {
tb_export::Target::WindowsAmd64
} else if cfg!(target_os = "macos") {
tb_export::Target::MacosArm64
} else if cfg!(target_arch = "aarch64") {
tb_export::Target::LinuxArm64
} else {
tb_export::Target::LinuxAmd64
});
fn main() -> ExitCode {
std::hint::black_box(&MARKER);
let path = match std::env::current_exe() {
Ok(p) => p,
Err(e) => {
eprintln!("Executable nicht auffindbar: {e}");
return ExitCode::from(1);
}
};
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(e) => {
eprintln!("Executable nicht lesbar: {e}");
return ExitCode::from(1);
}
};
let target = match tb_export::Target::host() {
Ok(t) => t,
Err(e) => {
eprintln!("{e}");
return ExitCode::from(1);
}
};
if let Err(e) = tb_export::embedded(&bytes, target) {
eprintln!("Ladefehler: {e:#}");
return ExitCode::from(1);
}
let command = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
tb_runner::run(&path, &command, |current| {
let result = if current == path {
tb_export::embedded(&bytes, target).map_err(|e| format!("{e:#}"))
} else if tb_vm::project_io::has_extension(current, "tbc") {
std::fs::read(current)
.map_err(|e| e.to_string())
.and_then(|b| {
tb_vm::bytecode::CompiledModule::from_tbc(&b).map_err(|e| e.to_string())
})
} else {
Err("tbrt benötigt ein vorkompiliertes externes RUN-Ziel (.tbc); kein BASIC-Quellcompiler enthalten".into())
};
result.map_err(|e| {
eprintln!("{}: Ladefehler: {e}", current.display());
ExitCode::from(1)
})
})
}

204
crates/tb-runner/src/lib.rs Normal file
View File

@@ -0,0 +1,204 @@
//! Gemeinsamer CLI-/Standalone-Runner; der Aufrufer bestimmt das Ladeverfahren.
use std::path::Path;
use std::process::ExitCode;
use tb_runtime::host::{Ereignis, Host};
use tb_ui::host::TerminalHost;
use tb_vm::bytecode::CompiledModule;
use tb_vm::interp::{RunEvent, Vm};
use tb_vm::project_io::{new_execution, run_target};
pub fn run(
first: &Path,
command: &str,
mut load: impl FnMut(&Path) -> Result<CompiledModule, ExitCode>,
) -> ExitCode {
let result = match TerminalHost::new() {
Ok(mut host) => {
let size = host.groesse().ok();
let result = run_chain(first, command, &mut load, &mut host, size);
drop(host);
result
}
Err(_) => run_chain(first, command, &mut load, &mut PipeHost::new(), None),
};
finish(result)
}
fn finish(result: Result<(RunEvent, Vm), ExitCode>) -> ExitCode {
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"),
}
}
pub fn run_chain(
first: &Path,
command: &str,
mut load: impl FnMut(&Path) -> Result<CompiledModule, ExitCode>,
host: &mut dyn Host,
size: Option<(usize, usize)>,
) -> Result<(RunEvent, Vm), ExitCode> {
let mut current = first.to_path_buf();
let mut start_line = None;
loop {
let module = load(&current)?;
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(&current, &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<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
}
}