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

@@ -11,6 +11,8 @@ name = "tbc"
path = "src/main.rs"
[dependencies]
tb-runner.workspace = true
tb-export.workspace = true
tb-frontend.workspace = true
tb-vm.workspace = true
tb-runtime.workspace = true
@@ -21,3 +23,9 @@ anyhow.workspace = true
tb-ide = { path = "../tb-ide" }
crossterm.workspace = true
ratatui.workspace = true
[target.'cfg(unix)'.dependencies]
signal-hook.workspace = true
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_System_Console"] }

View File

@@ -0,0 +1,70 @@
//! Prozessabbruch während eines CLI-Exports kontrolliert bis zur Bereinigung führen.
#[cfg(unix)]
pub struct ExportCancel {
flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
handlers: Vec<signal_hook::SigId>,
}
#[cfg(unix)]
impl ExportCancel {
pub fn new() -> std::io::Result<Self> {
let mut result = Self {
flag: Default::default(),
handlers: Vec::new(),
};
for signal in [signal_hook::consts::SIGINT, signal_hook::consts::SIGTERM] {
result
.handlers
.push(signal_hook::flag::register(signal, result.flag.clone())?);
}
Ok(result)
}
pub fn cancelled(&self) -> bool {
self.flag.load(std::sync::atomic::Ordering::Relaxed)
}
}
#[cfg(unix)]
impl Drop for ExportCancel {
fn drop(&mut self) {
for handler in &self.handlers {
signal_hook::low_level::unregister(*handler);
}
}
}
#[cfg(windows)]
static CANCELLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[cfg(windows)]
unsafe extern "system" fn handle(signal: u32) -> i32 {
if signal <= 1 {
CANCELLED.store(true, std::sync::atomic::Ordering::Relaxed);
1
} else {
0
}
}
#[cfg(windows)]
pub struct ExportCancel;
#[cfg(windows)]
impl ExportCancel {
pub fn new() -> std::io::Result<Self> {
CANCELLED.store(false, std::sync::atomic::Ordering::Relaxed);
// Der CLI-Prozess hat genau einen Export. Win32 ruft den statischen Handler auf.
if unsafe { windows_sys::Win32::System::Console::SetConsoleCtrlHandler(Some(handle), 1) }
== 0
{
return Err(std::io::Error::last_os_error());
}
Ok(Self)
}
pub fn cancelled(&self) -> bool {
CANCELLED.load(std::sync::atomic::Ordering::Relaxed)
}
}
#[cfg(windows)]
impl Drop for ExportCancel {
fn drop(&mut self) {
unsafe {
windows_sys::Win32::System::Console::SetConsoleCtrlHandler(Some(handle), 0);
}
}
}

View File

@@ -13,7 +13,10 @@ use tb_ide::{
commands::Command,
export::{ExportStatus, ProjectStamp},
};
use tb_runtime::host::{Ereignis, Host};
use tb_runtime::{snapshot, value::Value};
use tb_vm::interp::RunEvent;
use tb_vm::project_io::new_execution;
struct Temp(PathBuf);
impl Temp {
fn new() -> Self {

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)]

View File

@@ -329,3 +329,58 @@ fn designer_frm_is_accepted_by_the_standalone_cli_compiler() {
tb_vm::project_io::load_program(&dir.join("designed.tbc")).unwrap();
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn native_build_optionen_sind_explizit_und_tbc_bleibt_standard() {
let dir = std::env::temp_dir().join(format!("tb-native-options-{}", std::process::id()));
std::fs::create_dir(&dir).unwrap();
struct Cleanup(std::path::PathBuf);
impl Drop for Cleanup {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
let _cleanup = Cleanup(dir.clone());
std::fs::write(dir.join("main.bas"), "PRINT 42\nEND\n").unwrap();
assert!(tbc(&dir, "build", "main.bas").status.success());
let original = std::fs::read(dir.join("main.tbc")).unwrap();
assert_eq!(&original[..4], b"TBC\0");
for (options, diagnostic) in [
(vec!["--bogus"], "Unbekannte Build-Option"),
(vec!["--exe", "--target"], "Wert fehlt"),
(
vec!["--exe", "--target", "x86_64-apple-darwin"],
"Nicht unterstütztes Target",
),
(vec!["--exe", "--exe"], "Doppeltes --exe"),
(vec!["--output", "program"], "benötigen --exe"),
(
vec!["--exe", "--template", "missing", "-o", "program"],
"Runtime-Vorlage fehlt",
),
(
vec![
"--exe",
"--target",
"aarch64-apple-darwin",
"--target",
"aarch64-apple-darwin",
],
"Doppelte Option",
),
] {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_tbc"))
.current_dir(&dir)
.args(["build", "main.bas"])
.args(options)
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1), "{out:?}");
assert!(
String::from_utf8_lossy(&out.stderr).contains(diagnostic),
"{out:?}"
);
assert_eq!(std::fs::read(dir.join("main.tbc")).unwrap(), original);
assert!(!dir.join("program").exists());
}
}

View File

@@ -0,0 +1,9 @@
[package]
name = "tb-export"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
tb-vm.workspace = true
anyhow.workspace = true

View File

@@ -0,0 +1,12 @@
//! Builder-Hilfe: geprüfte Metadaten für eine frisch gebaute Runtime schreiben.
fn main() -> anyhow::Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
anyhow::ensure!(
args.len() == 2,
"Aufruf: tb-template <tbrt-Pfad> <Target-Triple>"
);
tb_export::prepare_template(
std::path::Path::new(&args[0]),
tb_export::Target::parse(&args[1])?,
)
}

514
crates/tb-export/src/lib.rs Normal file
View File

@@ -0,0 +1,514 @@
//! Geprüfte Runtime-Vorlagen, Nutzlastcontainer und geschützte Veröffentlichung.
use anyhow::{bail, ensure, Context, Result};
use std::{
fs,
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
};
use tb_vm::bytecode::{CompiledModule, TBC_VERSION};
pub const RUNTIME_VERSION: u32 = 1;
const CONTAINER_VERSION: u32 = 1;
const MAGIC: &[u8; 8] = b"TBPCODE!";
const FOOTER: usize = 48;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum Target {
WindowsAmd64 = 1,
MacosArm64 = 2,
LinuxAmd64 = 3,
LinuxArm64 = 4,
}
impl Target {
pub const ALL: [Self; 4] = [
Self::WindowsAmd64,
Self::MacosArm64,
Self::LinuxAmd64,
Self::LinuxArm64,
];
pub fn triple(self) -> &'static str {
match self {
Self::WindowsAmd64 => "x86_64-pc-windows-msvc",
Self::MacosArm64 => "aarch64-apple-darwin",
Self::LinuxAmd64 => "x86_64-unknown-linux-gnu",
Self::LinuxArm64 => "aarch64-unknown-linux-gnu",
}
}
pub fn parse(s: &str) -> Result<Self> {
Self::ALL
.into_iter()
.find(|t| t.triple() == s)
.with_context(|| format!("Nicht unterstütztes Target: {s}"))
}
pub fn host() -> Result<Self> {
match (std::env::consts::OS, std::env::consts::ARCH) {
("windows", "x86_64") => Ok(Self::WindowsAmd64),
("macos", "aarch64") => Ok(Self::MacosArm64),
("linux", "x86_64") => Ok(Self::LinuxAmd64),
("linux", "aarch64") => Ok(Self::LinuxArm64),
other => bail!("Kein unterstütztes Host-Target: {other:?}"),
}
}
pub fn runtime_name(self) -> &'static str {
if self == Self::WindowsAmd64 {
"tbrt.exe"
} else {
"tbrt"
}
}
}
fn part(b: &[u8], at: usize, len: usize) -> Result<&[u8]> {
b.get(at..at.checked_add(len).context("Längenüberlauf")?)
.context("Abgeschnittenes natives Format/Nutzlast")
}
fn u16_at(b: &[u8], at: usize) -> Result<u16> {
Ok(u16::from_le_bytes(part(b, at, 2)?.try_into()?))
}
fn u32_at(b: &[u8], at: usize) -> Result<u32> {
Ok(u32::from_le_bytes(part(b, at, 4)?.try_into()?))
}
fn u64_at(b: &[u8], at: usize) -> Result<u64> {
Ok(u64::from_le_bytes(part(b, at, 8)?.try_into()?))
}
fn size_at(b: &[u8], at: usize) -> Result<usize> {
usize::try_from(u64_at(b, at)?).context("Nutzlastlänge nicht darstellbar")
}
fn put64(b: &mut [u8], at: usize, n: usize) {
b[at..at + 8].copy_from_slice(&(n as u64).to_le_bytes());
}
/// FNV-1a-64 gegen Übertragungs-/Dateibeschädigung; keine Herkunftsauthentisierung.
pub fn checksum(b: &[u8]) -> u64 {
b.iter().fold(0xcbf29ce484222325, |h, v| {
(h ^ u64::from(*v)).wrapping_mul(0x100000001b3)
})
}
fn macho_commands(b: &[u8]) -> Result<Vec<(u32, usize)>> {
let end = 32usize
.checked_add(u32_at(b, 20)? as usize)
.context("Mach-O-Headerlänge")?;
part(b, 0, end)?;
let mut at = 32;
let mut commands = Vec::new();
for _ in 0..u32_at(b, 16)? {
ensure!(at + 8 <= end, "Ungültige Mach-O-Ladekommandos");
let cmd = u32_at(b, at)?;
let len = u32_at(b, at + 4)? as usize;
ensure!(
len >= 8 && len.is_multiple_of(8) && len <= end - at,
"Ungültige Mach-O-Kommandolänge"
);
commands.push((cmd, at));
at += len;
}
ensure!(at == end, "Mach-O-Kommandotabelle unvollständig");
Ok(commands)
}
/// Format-/Architekturprüfung; synthetische Header sind kein Zielausführungsnachweis.
pub fn native_target(b: &[u8]) -> Result<Target> {
part(b, 0, 64)?;
if b.starts_with(b"MZ") {
let pe = u32_at(b, 0x3c)? as usize;
ensure!(part(b, pe, 4)? == b"PE\0\0", "Ungültiges PE-Format");
ensure!(
u16_at(b, pe + 4)? == 0x8664,
"PE-Architektur muss amd64 sein"
);
ensure!(
u16_at(b, pe + 24)? == 0x20b && u16_at(b, pe + 92)? == 3,
"PE muss PE32+ Terminalanwendung sein"
);
ensure!(
u16_at(b, pe + 22)? & 0x2002 == 2,
"PE muss Executable statt DLL sein"
);
return Ok(Target::WindowsAmd64);
}
if b.starts_with(b"\x7fELF") {
ensure!(
matches!(b[7], 0 | 3),
"ELF-System-ABI muss System V/Linux sein"
);
ensure!(
part(b, 4, 3)? == [2, 1, 1],
"ELF muss 64-Bit Little Endian sein"
);
ensure!(matches!(u16_at(b, 16)?, 2 | 3), "ELF muss ausführbar sein");
part(b, 0, 64)?;
return match u16_at(b, 18)? {
62 => Ok(Target::LinuxAmd64),
183 => Ok(Target::LinuxArm64),
_ => bail!("Nicht unterstützte ELF-Architektur"),
};
}
if b.starts_with(&[0xcf, 0xfa, 0xed, 0xfe]) {
ensure!(
u32_at(b, 4)? == 0x0100000c,
"Mach-O-Architektur muss arm64 sein"
);
ensure!(u32_at(b, 12)? == 2, "Mach-O muss Executable sein");
macho_commands(b)?;
return Ok(Target::MacosArm64);
}
bail!("Unbekanntes natives Zielformat (PE/ELF/Mach-O erwartet)")
}
/// Mach-O-Signaturen folgen der Nutzlast. Ihr Ladekommando ist die stabile Grenze.
fn content_end(b: &[u8], target: Target) -> Result<usize> {
if target == Target::MacosArm64 {
let signatures: Vec<_> = macho_commands(b)?
.into_iter()
.filter(|(cmd, _)| *cmd == 0x1d)
.collect();
ensure!(signatures.len() <= 1, "Doppelte Mach-O-Signatur");
if let Some((_, at)) = signatures.first() {
ensure!(u32_at(b, at + 4)? == 16, "Ungültiges Signaturkommando");
let off = u32_at(b, at + 8)? as usize;
let len = u32_at(b, at + 12)? as usize;
part(b, off, len)?;
ensure!(off + len == b.len(), "Daten hinter Mach-O-Signatur");
return Ok(off);
}
}
Ok(b.len())
}
pub fn embedded(b: &[u8], expected: Target) -> Result<CompiledModule> {
ensure!(
native_target(b)? == expected,
"Executable-Target passt nicht zum Host"
);
let end = content_end(b, expected)?;
// codesign richtet die Signatur auf 16 Bytes aus; ausschließlich Nullpadding zulassen.
let footer = (0..=15)
.find_map(|padding| {
let at = end.checked_sub(FOOTER + padding)?;
(b.get(at..at + 8) == Some(MAGIC) && b[at + FOOTER..end].iter().all(|v| *v == 0))
.then_some(at)
})
.context("Fehlende oder beschädigte eingebettete Nutzlast (leere tbrt-Vorlage)")?;
ensure!(
u32_at(b, footer + 8)? == CONTAINER_VERSION,
"Inkompatible Container-Version"
);
ensure!(
u32_at(b, footer + 12)? == RUNTIME_VERSION,
"Inkompatible Runtime-Version"
);
ensure!(
u32_at(b, footer + 16)? == u32::from(TBC_VERSION),
"Inkompatible TBC-Version"
);
ensure!(
u32_at(b, footer + 20)? == expected as u32,
"Nutzlast-Target passt nicht zum Executable"
);
let offset = size_at(b, footer + 24)?;
let len = size_at(b, footer + 32)?;
ensure!(
offset >= 64 && offset <= footer && len == footer - offset,
"Ungültige Nutzlastgrenzen"
);
let payload = part(b, offset, len)?;
ensure!(
checksum(payload) == u64_at(b, footer + 40)?,
"Nutzlast-Prüfsumme stimmt nicht"
);
CompiledModule::from_tbc(payload)
.map_err(|e| anyhow::anyhow!("Eingebettetes Programm nicht ladbar: {e}"))
}
/// Im nativen Runner vorhanden; verhindert Verwechslung mit beliebigen Executables.
pub const fn runtime_marker(target: Target) -> [u8; 32] {
let mut marker = [0; 32];
let magic = *b"TBRT-RUNTIME-v1!";
let mut i = 0;
while i < 16 {
marker[i] = magic[i];
i += 1;
}
let values = [
CONTAINER_VERSION,
RUNTIME_VERSION,
TBC_VERSION as u32,
target as u32,
];
i = 0;
while i < 4 {
let bytes = values[i].to_le_bytes();
let mut j = 0;
while j < 4 {
marker[16 + i * 4 + j] = bytes[j];
j += 1;
}
i += 1;
}
marker
}
fn check_runner(b: &[u8], target: Target) -> Result<()> {
ensure!(
b.windows(32).any(|w| w == runtime_marker(target)),
"Keine kompatible tbrt-Runtime-Vorlage (Runtime-/Format-/Target-Markierung fehlt)"
);
Ok(())
}
fn manifest(b: &[u8], target: Target) -> String {
format!("TBRT-TEMPLATE 1\nruntime={RUNTIME_VERSION}\npackage={}\ntbc={TBC_VERSION}\ntarget={}\nchecksum={:016x}\n",env!("CARGO_PKG_VERSION"),target.triple(),checksum(b))
}
pub fn manifest_path(path: &Path) -> PathBuf {
let mut p = path.as_os_str().to_owned();
p.push(".meta");
PathBuf::from(p)
}
/// Für den Runtime-Builder; kein impliziter Build beim Export.
pub fn prepare_template(path: &Path, target: Target) -> Result<()> {
let b = fs::read(path)?;
check_runner(&b, target)?;
ensure!(
native_target(&b)? == target,
"Vorlagen-Zielformat/Architektur widerspricht {}",
target.triple()
);
ensure!(
embedded(&b, target).is_err(),
"Bereits eingebettetes Programm ist keine leere Vorlage"
);
publish(
&manifest_path(path),
false,
&[path.to_path_buf()],
&|| false,
|temp| {
fs::write(temp, manifest(&b, target))?;
Ok(())
},
)
}
fn template(path: &Path, target: Target) -> Result<Vec<u8>> {
let b = fs::read(path).with_context(|| format!("Runtime-Vorlage fehlt: {}", path.display()))?;
ensure!(
native_target(&b)? == target,
"Vorlagen-Zielformat/Architektur passt nicht zu {}",
target.triple()
);
let found = fs::read_to_string(manifest_path(path)).context("Vorlagenmetadaten fehlen")?;
check_runner(&b, target)?;
let want = manifest(&b, target);
for (index, (a, z)) in found.lines().zip(want.lines()).enumerate() {
ensure!(
a == z,
"Inkompatible Vorlagenmetadaten, Zeile {}: Soll {z}, Ist {a}",
index + 1
);
}
ensure!(
found == want,
"Unvollständige Vorlagenmetadaten oder Integritätsdaten"
);
Ok(b)
}
static NEXT: AtomicU64 = AtomicU64::new(0);
struct Temporary(PathBuf);
impl Drop for Temporary {
fn drop(&mut self) {
let _ = fs::remove_file(&self.0);
}
}
fn destination(path: &Path, protected: &[PathBuf]) -> Result<Option<Vec<u8>>> {
let key = tb_vm::project_io::identity(path)?;
for source in protected {
ensure!(
tb_vm::project_io::identity(source)? != key,
"Ausgabe würde Projektdaten/Vorlage überschreiben: {}",
path.display()
);
}
match fs::symlink_metadata(path) {
Ok(meta) => {
ensure!(
meta.is_file() && !meta.permissions().readonly(),
"Ziel ist keine beschreibbare reguläre Datei: {}",
path.display()
);
Ok(Some(fs::read(path)?))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
/// No-clobber per hard_link; Ersetzen nur nach expliziter Freigabe und unverändertem Ziel.
/// prepare darf finalisieren; cancellation wird vor Arbeit und Veröffentlichung geprüft.
pub fn publish(
path: &Path,
overwrite: bool,
protected: &[PathBuf],
cancelled: &dyn Fn() -> bool,
prepare: impl FnOnce(&Path) -> Result<()>,
) -> Result<()> {
ensure!(!cancelled(), "Export abgebrochen");
let initial = destination(path, protected)?;
ensure!(
initial.is_none() || overwrite,
"Ziel existiert; --force zum Ersetzen: {}",
path.display()
);
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(Path::new("."));
let temporary = loop {
let p = parent.join(format!(
".tb-export-{}-{}.tmp",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
match fs::OpenOptions::new().write(true).create_new(true).open(&p) {
Ok(_) => break Temporary(p),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e.into()),
}
};
prepare(&temporary.0)?;
fs::File::open(&temporary.0)?.sync_all()?;
ensure!(!cancelled(), "Export abgebrochen");
ensure!(
destination(path, protected)? == initial,
"Ziel wurde während des Exports verändert"
);
if initial.is_some() {
fs::rename(&temporary.0, path)?;
} else {
fs::hard_link(&temporary.0, path)
.context("Ziel inzwischen belegt oder Veröffentlichung nicht möglich")?;
}
Ok(())
}
fn codesign(path: &Path, args: &[&str]) -> Result<()> {
ensure!(
cfg!(target_os = "macos"),
"macOS-Finalisierung benötigt macOS mit /usr/bin/codesign"
);
let result = std::process::Command::new("/usr/bin/codesign")
.args(args)
.arg(path)
.output()
.context("Finalisierung: /usr/bin/codesign fehlt oder ist nicht ausführbar")?;
ensure!(
result.status.success(),
"Finalisierung fehlgeschlagen: {}",
String::from_utf8_lossy(&result.stderr)
);
Ok(())
}
fn append_payload(b: &mut Vec<u8>, module: &CompiledModule, target: Target) -> Result<()> {
module.validate().map_err(|e| anyhow::anyhow!("{e}"))?;
if target == Target::WindowsAmd64 {
let pe = u32_at(b, 0x3c)? as usize;
// Eine bestehende Authenticode-Signatur wäre nach dem Einbetten ungültig.
if u32_at(b, pe + 132)? > 4 {
ensure!(
part(b, pe + 168, 8)?.iter().all(|v| *v == 0),
"Signierte PE-Vorlagen werden nicht unterstützt; unsigned tbrt benötigt"
);
}
// Für Benutzerprogramme optional; keine veraltete PE-Prüfsumme übernehmen.
b[pe + 88..pe + 92].fill(0);
}
let payload = module.to_tbc();
let offset = b.len();
b.extend_from_slice(&payload);
b.extend_from_slice(MAGIC);
for n in [
CONTAINER_VERSION,
RUNTIME_VERSION,
u32::from(TBC_VERSION),
target as u32,
] {
b.extend_from_slice(&n.to_le_bytes());
}
for n in [offset as u64, payload.len() as u64, checksum(&payload)] {
b.extend_from_slice(&n.to_le_bytes());
}
if target == Target::MacosArm64 {
let link = macho_commands(b)?
.into_iter()
.find(|(cmd, at)| {
*cmd == 0x19 && b.get(at + 8..at + 24) == Some(b"__LINKEDIT\0\0\0\0\0\0")
})
.context("Mach-O __LINKEDIT fehlt")?
.1;
ensure!(
u32_at(b, link + 4)? as usize >= 72,
"Mach-O-Segment zu kurz"
);
let start = size_at(b, link + 40)?;
ensure!(start <= offset, "Ungültige __LINKEDIT-Grenze");
let len = b.len() - start;
put64(b, link + 48, len);
put64(
b,
link + 32,
len.checked_add(16383).context("Mach-O-Länge")? & !16383,
);
}
Ok(())
}
pub struct Export<'a> {
pub module: &'a CompiledModule,
pub target: Target,
pub template: &'a Path,
pub output: &'a Path,
pub overwrite: bool,
pub protected: &'a [PathBuf],
}
pub fn export(request: Export<'_>, cancelled: &dyn Fn() -> bool) -> Result<()> {
let mut b = template(request.template, request.target)?;
let mut protected = request.protected.to_vec();
protected.extend([
request.template.to_path_buf(),
manifest_path(request.template),
]);
protected.extend(
request
.module
.sources
.iter()
.map(|s| PathBuf::from(&s.path)),
);
publish(
request.output,
request.overwrite,
&protected,
cancelled,
|temp| {
if request.target == Target::MacosArm64 {
fs::write(temp, &b)?;
codesign(temp, &["--remove-signature"])?;
b = fs::read(temp)?;
}
append_payload(&mut b, request.module, request.target)?;
fs::write(temp, &b)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(temp, fs::Permissions::from_mode(0o755))?;
}
if request.target == Target::MacosArm64 {
codesign(temp, &["--force", "--sign", "-", "--timestamp=none"])?;
codesign(temp, &["--verify", "--strict"])?;
}
embedded(&fs::read(temp)?, request.target)?;
Ok(())
},
)
}
#[cfg(test)]
mod tests;

View File

@@ -0,0 +1,226 @@
use super::*;
use std::cell::Cell;
fn header(t: Target) -> Vec<u8> {
let mut b = vec![0; 256];
match t {
Target::WindowsAmd64 => {
b[..2].copy_from_slice(b"MZ");
b[0x3c..0x40].copy_from_slice(&64u32.to_le_bytes());
b[64..68].copy_from_slice(b"PE\0\0");
b[68..70].copy_from_slice(&0x8664u16.to_le_bytes());
b[86..88].copy_from_slice(&2u16.to_le_bytes());
b[88..90].copy_from_slice(&0x20bu16.to_le_bytes());
b[156..158].copy_from_slice(&3u16.to_le_bytes());
}
Target::MacosArm64 => {
b[..4].copy_from_slice(&[0xcf, 0xfa, 0xed, 0xfe]);
b[4..8].copy_from_slice(&0x0100000cu32.to_le_bytes());
b[12..16].copy_from_slice(&2u32.to_le_bytes());
}
Target::LinuxAmd64 | Target::LinuxArm64 => {
b[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
b[16..18].copy_from_slice(&2u16.to_le_bytes());
let cpu: u16 = if t == Target::LinuxAmd64 { 62 } else { 183 };
b[18..20].copy_from_slice(&cpu.to_le_bytes());
}
}
b.extend_from_slice(&runtime_marker(t));
b
}
struct Dir(PathBuf);
impl Dir {
fn new() -> Self {
let p = std::env::temp_dir().join(format!(
"tb-export-test-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir(&p).unwrap();
Self(p)
}
}
impl Drop for Dir {
fn drop(&mut self) {
fs::remove_dir_all(&self.0).unwrap();
}
}
#[test]
fn targets_versions_integrity_and_payload_bounds_are_enforced() {
let d = Dir::new();
for t in Target::ALL {
let b = header(t);
assert_eq!(native_target(&b).unwrap(), t);
let path = d.0.join(t.runtime_name());
fs::write(&path, &b).unwrap();
prepare_template(&path, t).unwrap();
assert_eq!(template(&path, t).unwrap(), b);
let other = if t == Target::LinuxAmd64 {
Target::LinuxArm64
} else {
Target::LinuxAmd64
};
assert!(template(&path, other)
.unwrap_err()
.to_string()
.contains("Architektur"));
let meta = manifest(&b, t);
for wrong in [
meta.replace("runtime=1", "runtime=2"),
meta.replace("package=0.1.0", "package=9"),
meta.replace("tbc=4", "tbc=99"),
meta.replace("checksum=", "checksum=0"),
] {
fs::write(manifest_path(&path), wrong).unwrap();
assert!(template(&path, t).is_err());
}
fs::remove_file(manifest_path(&path)).unwrap();
assert!(template(&path, t).is_err());
fs::remove_file(&path).unwrap();
assert!(template(&path, t)
.unwrap_err()
.to_string()
.contains("fehlt"));
for len in 0..64 {
assert!(native_target(&b[..len]).is_err());
}
}
assert!(Target::parse("aarch64-pc-windows-msvc").is_err());
assert!(Target::parse("x86_64-apple-darwin").is_err());
let mut foreign_abi = header(Target::LinuxAmd64);
foreign_abi[7] = 9;
assert!(native_target(&foreign_abi).is_err());
let module = tb_vm::compile_source("TEST", "PRINT 42\nEND\n").unwrap();
for t in [Target::WindowsAmd64, Target::LinuxAmd64, Target::LinuxArm64] {
let mut b = header(t);
if t == Target::WindowsAmd64 {
let mut signed = b.clone();
signed[196..200].copy_from_slice(&5u32.to_le_bytes());
signed[232] = 1;
assert!(append_payload(&mut signed, &module, t)
.unwrap_err()
.to_string()
.contains("Signierte PE"));
b[152..156].copy_from_slice(&123u32.to_le_bytes());
}
append_payload(&mut b, &module, t).unwrap();
if t == Target::WindowsAmd64 {
assert_eq!(&b[152..156], &[0; 4]);
}
assert_eq!(embedded(&b, t).unwrap().to_tbc(), module.to_tbc());
let at = b.len() - FOOTER;
for field in [8, 12, 16, 20, 24, 32, 40] {
let mut bad = b.clone();
bad[at + field] ^= 0xff;
assert!(embedded(&bad, t).is_err(), "{field}");
}
for cut in 1..=FOOTER + 1 {
assert!(embedded(&b[..b.len() - cut], t).is_err());
}
let mut bad = b.clone();
bad[300] ^= 1;
assert!(embedded(&bad, t).is_err());
}
}
#[test]
fn publication_preserves_originals_on_conflict_failure_race_and_cancel() {
let d = Dir::new();
let out = d.0.join("result");
let source = d.0.join("source.bas");
fs::write(&source, b"source").unwrap();
fs::write(&out, b"old").unwrap();
assert!(publish(&out, false, &[], &|| false, |_| panic!(
"darf nicht schreiben"
))
.is_err());
assert!(publish(
&source,
true,
std::slice::from_ref(&source),
&|| false,
|_| panic!("Projektschutz")
)
.is_err());
assert!(publish(&out, true, &[], &|| false, |p| {
fs::write(p, b"new")?;
bail!("Finalisierungsfehler")
})
.is_err());
assert_eq!(fs::read(&out).unwrap(), b"old");
let cancel = Cell::new(false);
assert!(publish(&out, true, &[], &|| cancel.get(), |p| {
fs::write(p, b"new")?;
cancel.set(true);
Ok(())
})
.is_err());
assert_eq!(fs::read(&out).unwrap(), b"old");
assert!(publish(&out, true, &[], &|| true, |_| panic!("abgebrochen")).is_err());
assert!(publish(&out, true, &[], &|| false, |p| {
fs::write(p, b"new")?;
fs::write(&out, b"external")?;
Ok(())
})
.is_err());
assert_eq!(fs::read(&out).unwrap(), b"external");
fs::remove_file(&out).unwrap();
assert!(publish(&out, true, &[], &|| false, |p| {
fs::write(p, b"new")?;
fs::write(&out, b"racer")?;
Ok(())
})
.is_err());
assert_eq!(fs::read(&out).unwrap(), b"racer");
assert!(
publish(&d.0.join("missing/result"), false, &[], &|| false, |_| Ok(
()
))
.is_err()
);
publish(&out, true, &[], &|| false, |p| {
fs::write(p, b"approved")?;
Ok(())
})
.unwrap();
assert_eq!(fs::read(&out).unwrap(), b"approved");
assert_eq!(fs::read(&source).unwrap(), b"source");
assert_eq!(fs::read_dir(&d.0).unwrap().count(), 2);
#[cfg(unix)]
{
std::os::unix::fs::symlink(&source, d.0.join("alias")).unwrap();
assert!(
publish(&d.0.join("alias"), true, &[], &|| false, |_| panic!(
"Symlink"
))
.is_err()
);
}
}
#[test]
fn fehlgeschlagene_native_finalisierung_erhaelt_das_ziel() {
let dir = Dir::new();
let runtime = dir.0.join("tbrt");
let out = dir.0.join("program");
fs::write(&runtime, header(Target::MacosArm64)).unwrap();
prepare_template(&runtime, Target::MacosArm64).unwrap();
fs::write(&out, b"original").unwrap();
let module = tb_vm::compile_source("TEST", "END\n").unwrap();
let error = export(
Export {
module: &module,
target: Target::MacosArm64,
template: &runtime,
output: &out,
overwrite: true,
protected: &[],
},
&|| false,
)
.unwrap_err();
assert!(error.to_string().contains("Finalisierung"), "{error:#}");
assert_eq!(fs::read(&out).unwrap(), b"original");
assert_eq!(fs::read_dir(&dir.0).unwrap().count(), 3);
}

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
}
}

View File

@@ -849,9 +849,14 @@ fn link(
Ok((result, debug_maps))
}
// Erst ein ausdrücklich erzeugter DebugCompiler bindet den Quellcompiler ein.
// Die reine Runtime kann dadurch den gleichen VM-Pfad ohne Parser/Codegenerator linken.
type DebugCompileFn = fn(&DebugCompiler, u16, &str, &str, bool) -> Result<DebugCode, String>;
/// Ephemeral symbol/link context, deliberately not part of TBC serialization.
#[derive(Clone)]
pub struct DebugCompiler {
compile_fn: DebugCompileFn,
modules: Vec<(Module, FormCatalog, DebugMap)>,
symbols: tb_frontend::sema::DebugSymbols,
slots: Vec<u16>,
@@ -914,6 +919,7 @@ impl ProjectCompiler {
}
}
DebugCompiler {
compile_fn: DebugCompiler::compile_impl,
modules,
symbols,
slots,
@@ -928,6 +934,15 @@ impl DebugCompiler {
procedure: &str,
text: &str,
expression: bool,
) -> Result<DebugCode, String> {
(self.compile_fn)(self, module, procedure, text, expression)
}
fn compile_impl(
&self,
module: u16,
procedure: &str,
text: &str,
expression: bool,
) -> Result<DebugCode, String> {
if let Some(error) = &self.error {
return Err(error.clone());