Phase 6: Native Executables implementieren und Change archivieren
This commit is contained in:
11
crates/tb-runner/Cargo.toml
Normal file
11
crates/tb-runner/Cargo.toml
Normal 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"] }
|
||||
58
crates/tb-runner/src/bin/tbrt.rs
Normal file
58
crates/tb-runner/src/bin/tbrt.rs
Normal 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
204
crates/tb-runner/src/lib.rs
Normal 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(¤t)?;
|
||||
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(¤t, &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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user