Phase 2 abgeschlossen: Bytecode, TBVM, Runtime-Scheibe, tbc run

- Sema zum Lowering-Pass umgebaut: typisiertes HIR (Slots, explizite
  Konvertierungsknoten) als Codegen-Eingabe; BYREF verlangt exakten Typ
- Bytecode-Feindesign umgesetzt: monomorpher Opcode-Satz,
  .tbc-Container (Formatversion 1) mit eigenem Writer/Reader
- Codegenerator HIR -> Bytecode (Fixup-Listen, keine globalen Passes)
- TBVM-Interpreter: Kontrollfluss, GOSUB-Stack je Frame, BYREF/BYVAL,
  STATIC, DEF FN, DATA/READ/RESTORE, ON [LOCAL] ERROR/RESUME/ERR/ERL,
  Breakpoints/Einzelschritt/Inspektion, STOP fortsetzbar
- Runtime-Scheibe: Host-Trait (Konsole/Capture), Builtin-Tabelle,
  Konvertierungsmatrix, PRINT-Formatierung/Druckzonen, Stringfunktionen
- tbc run/build/check mit Exit-Codes nach Entscheidung D6
- Korpus-Harness (byte-genauer Vergleich) + 3 neue Korpusdateien
  (konvertierung, fehlerbehandlung, byref); 137 Tests gruen
- Benchmarks: Einzelmodul 1,2 ms / Projekt 49.760 Zeilen 124 ms
  (Budgets eingehalten), VM ~5 Mio Schleifeniterationen/s
- Doku fortgeschrieben (tbvm-design, sprachreferenz, PLAN);
  verlagerte Punkte als explizite Aufgaben in Phase 3
- OpenSpec-Change phase-2-bytecode-vm (27/27 Tasks)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 11:28:07 +02:00
parent da23d52036
commit f7e57b0bd8
42 changed files with 9225 additions and 484 deletions

View File

@@ -0,0 +1,623 @@
//! Builtin-Dispatch-Tabelle (`CALL_BUILTIN`-ABI): Argumente kommen vom
//! Operandenstack der VM, der Index steht im Opcode. Die Tabelle ist ohne
//! Änderung am Opcode-Satz erweiterbar — Phase 3 füllt sie auf.
//!
//! Die Indizes (`ids::*`) sind stabil; der Codegenerator (`tb-vm`)
//! bildet `hir::Builtin` über ein erschöpfendes `match` darauf ab.
use crate::console::PrintState;
use crate::errors::RuntimeError;
use crate::format;
use crate::host::Host;
use crate::value::{as_f64, cur_to_f64, f64_to_cur, Value};
use std::rc::Rc;
/// Laufzeitzustand der Bibliothek (PRNG, Druckspalte, Kommandozeile).
pub struct RtState {
pub print: PrintState,
rng: u32,
rnd_last: f32,
pub command: String,
}
impl Default for RtState {
fn default() -> Self {
RtState {
print: PrintState::default(),
// Startzustand des Vorbild-PRNG; die exakte
// PRNG-Kompatibilität ist Aufgabe in PLAN.md Phase 3
// („RND/RANDOMIZE — kompatibler PRNG").
rng: 0x50000,
rnd_last: 0.0,
command: String::new(),
}
}
}
impl RtState {
fn rng_next(&mut self) -> f32 {
self.rng = self.rng.wrapping_mul(0xFD43FD).wrapping_add(0xC39EC3) & 0xFF_FFFF;
self.rnd_last = self.rng as f32 / 16_777_216.0;
self.rnd_last
}
}
pub type BuiltinFn =
fn(&mut RtState, &mut dyn Host, &mut [Value]) -> Result<Option<Value>, RuntimeError>;
/// Stabile Tabellenindizes (Ordnung = `hir::Builtin` des Frontends).
pub mod ids {
pub const LEN: u16 = 0;
pub const LEFT_S: u16 = 1;
pub const RIGHT_S: u16 = 2;
pub const MID_S: u16 = 3;
pub const INSTR: u16 = 4;
pub const UCASE_S: u16 = 5;
pub const LCASE_S: u16 = 6;
pub const LTRIM_S: u16 = 7;
pub const RTRIM_S: u16 = 8;
pub const SPACE_S: u16 = 9;
pub const STRING_S: u16 = 10;
pub const CHR_S: u16 = 11;
pub const ASC: u16 = 12;
pub const STR_S: u16 = 13;
pub const VAL: u16 = 14;
pub const HEX_S: u16 = 15;
pub const OCT_S: u16 = 16;
pub const MID_ASSIGN: u16 = 17;
pub const ABS: u16 = 18;
pub const SGN: u16 = 19;
pub const INT_F: u16 = 20;
pub const FIX: u16 = 21;
pub const SQR: u16 = 22;
pub const EXP: u16 = 23;
pub const LOG: u16 = 24;
pub const SIN: u16 = 25;
pub const COS: u16 = 26;
pub const TAN: u16 = 27;
pub const ATN: u16 = 28;
pub const RND: u16 = 29;
pub const RANDOMIZE: u16 = 30;
pub const PRINT_VAL: u16 = 31;
pub const PRINT_STR_LIT: u16 = 32;
pub const PRINT_COMMA: u16 = 33;
pub const PRINT_TAB: u16 = 34;
pub const PRINT_SPC: u16 = 35;
pub const PRINT_NEWLINE: u16 = 36;
pub const TIMER: u16 = 37;
pub const DATE_S: u16 = 38;
pub const TIME_S: u16 = 39;
pub const COMMAND_S: u16 = 40;
pub const DOEVENTS: u16 = 41;
pub const SLEEP: u16 = 42;
pub const BEEP: u16 = 43;
pub const COUNT: u16 = 44;
}
/// Dispatch-Tabelle in Index-Reihenfolge.
pub fn builtin_table() -> &'static [BuiltinFn] {
const TABLE: &[BuiltinFn] = &[
bi_len,
bi_left,
bi_right,
bi_mid,
bi_instr,
bi_ucase,
bi_lcase,
bi_ltrim,
bi_rtrim,
bi_space,
bi_string,
bi_chr,
bi_asc,
bi_str,
bi_val,
bi_hex,
bi_oct,
bi_mid_assign,
bi_abs,
bi_sgn,
bi_int,
bi_fix,
bi_sqr,
bi_exp,
bi_log,
bi_sin,
bi_cos,
bi_tan,
bi_atn,
bi_rnd,
bi_randomize,
bi_print_val,
bi_print_val, // PRINT_STR_LIT: identisch (Strings per Tag)
bi_print_comma,
bi_print_tab,
bi_print_spc,
bi_print_newline,
bi_timer,
bi_date,
bi_time,
bi_command,
bi_doevents,
bi_sleep,
bi_beep,
];
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
TABLE
}
// ---- Argument-Hilfen --------------------------------------------------------
fn arg_str(args: &[Value], i: usize) -> Result<Rc<str>, RuntimeError> {
match args.get(i) {
Some(Value::Str(s)) => Ok(s.clone()),
_ => Err(RuntimeError::TYPE_MISMATCH),
}
}
fn arg_i32(args: &[Value], i: usize) -> Result<i32, RuntimeError> {
match args.get(i) {
Some(Value::Lng(v)) => Ok(*v),
Some(Value::Int(v)) => Ok(*v as i32),
Some(v) => Ok(as_f64(v) as i32),
None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
}
}
fn arg_f64(args: &[Value], i: usize) -> Result<f64, RuntimeError> {
match args.get(i) {
Some(v) => Ok(as_f64(v)),
None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
}
}
fn s_ok(s: String) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Str(Rc::from(s.as_str()))))
}
// ---- Strings ----------------------------------------------------------------
fn bi_len(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
let n = s.chars().count();
Ok(Some(Value::Int(i16::try_from(n).unwrap_or(i16::MAX))))
}
fn bi_left(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
let n = arg_i32(a, 1)?;
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
s_ok(s.chars().take(n as usize).collect())
}
fn bi_right(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
let n = arg_i32(a, 1)?;
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let len = s.chars().count();
let skip = len.saturating_sub(n as usize);
s_ok(s.chars().skip(skip).collect())
}
fn bi_mid(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
let start = arg_i32(a, 1)?;
let len = arg_i32(a, 2)?; // -1 = Rest
if start < 1 || len < -1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let iter = s.chars().skip((start - 1) as usize);
if len < 0 {
s_ok(iter.collect())
} else {
s_ok(iter.take(len as usize).collect())
}
}
fn bi_instr(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let start = arg_i32(a, 0)?;
let s = arg_str(a, 1)?;
let t = arg_str(a, 2)?;
if start < 1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let chars: Vec<char> = s.chars().collect();
let slen = chars.len();
if start as usize > slen {
// Vorbild: Start hinter Stringende → 0
return Ok(Some(Value::Int(0)));
}
if t.is_empty() {
return Ok(Some(Value::Int(start as i16)));
}
let hay: String = chars[(start - 1) as usize..].iter().collect();
match hay.find(&*t) {
Some(byte_pos) => {
let char_pos = hay[..byte_pos].chars().count();
Ok(Some(Value::Int((start as usize + char_pos) as i16)))
}
None => Ok(Some(Value::Int(0))),
}
}
fn bi_ucase(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
s_ok(s.to_uppercase())
}
fn bi_lcase(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
s_ok(s.to_lowercase())
}
fn bi_ltrim(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
s_ok(s.trim_start_matches(' ').to_string())
}
fn bi_rtrim(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
s_ok(s.trim_end_matches(' ').to_string())
}
fn bi_space(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
s_ok(" ".repeat(n as usize))
}
fn bi_string(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
if n < 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let ch = match a.get(1) {
Some(Value::Str(s)) => s.chars().next().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?,
Some(v) => {
let code = as_f64(v) as i64;
char::from_u32(u32::try_from(code).map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL)?)
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?
}
None => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
};
s_ok(ch.to_string().repeat(n as usize))
}
fn bi_chr(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
let ch = u32::try_from(n)
.ok()
.and_then(char::from_u32)
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
s_ok(ch.to_string())
}
fn bi_asc(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
match s.chars().next() {
// Codepoints > 32767 passen nicht in INTEGER → Overflow wie beim
// Vorbild bei Bereichsüberschreitung.
Some(c) => i16::try_from(c as u32)
.map(|v| Some(Value::Int(v)))
.map_err(|_| RuntimeError::OVERFLOW),
None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
}
}
fn bi_str(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let v = a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
s_ok(format::format_str_fn(v))
}
fn bi_val(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let s = arg_str(a, 0)?;
Ok(Some(Value::Dbl(format::val(&s))))
}
fn bi_hex(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
s_ok(format!("{:X}", n as u32))
}
fn bi_oct(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
s_ok(format!("{:o}", n as u32))
}
/// MID$-Anweisung als Funktion: (ziel, start, länge, ersatz) → neuer
/// String; die Länge des Ziels bleibt unverändert.
fn bi_mid_assign(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let target = arg_str(a, 0)?;
let start = arg_i32(a, 1)?;
let len = arg_i32(a, 2)?; // -1 = Länge des Ersatzes
let repl = arg_str(a, 3)?;
if start < 1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let tchars: Vec<char> = target.chars().collect();
let rchars: Vec<char> = repl.chars().collect();
let start0 = (start - 1) as usize;
if start0 >= tchars.len() {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let max_repl = if len < 0 { rchars.len() } else { (len as usize).min(rchars.len()) };
let n = max_repl.min(tchars.len() - start0);
let mut out = tchars.clone();
out[start0..start0 + n].copy_from_slice(&rchars[..n]);
s_ok(out.into_iter().collect())
}
// ---- Mathematik --------------------------------------------------------------
fn bi_abs(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(match a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)? {
Value::Int(v) => Value::Int(v.checked_abs().ok_or(RuntimeError::OVERFLOW)?),
Value::Lng(v) => Value::Lng(v.checked_abs().ok_or(RuntimeError::OVERFLOW)?),
Value::Sng(v) => Value::Sng(v.abs()),
Value::Dbl(v) => Value::Dbl(v.abs()),
Value::Cur(v) => Value::Cur(v.checked_abs().ok_or(RuntimeError::OVERFLOW)?),
_ => return Err(RuntimeError::TYPE_MISMATCH),
}))
}
fn bi_sgn(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let x = arg_f64(a, 0)?;
Ok(Some(Value::Int(if x > 0.0 {
1
} else if x < 0.0 {
-1
} else {
0
})))
}
fn bi_int(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(match a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)? {
v @ (Value::Int(_) | Value::Lng(_)) => v.clone(),
Value::Sng(v) => Value::Sng(v.floor()),
Value::Dbl(v) => Value::Dbl(v.floor()),
Value::Cur(v) => Value::Cur(f64_to_cur(cur_to_f64(*v).floor())?),
_ => return Err(RuntimeError::TYPE_MISMATCH),
}))
}
fn bi_fix(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(match a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)? {
v @ (Value::Int(_) | Value::Lng(_)) => v.clone(),
Value::Sng(v) => Value::Sng(v.trunc()),
Value::Dbl(v) => Value::Dbl(v.trunc()),
Value::Cur(v) => Value::Cur(f64_to_cur(cur_to_f64(*v).trunc())?),
_ => return Err(RuntimeError::TYPE_MISMATCH),
}))
}
fn bi_sqr(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let x = arg_f64(a, 0)?;
if x < 0.0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(Some(Value::Dbl(x.sqrt())))
}
fn bi_exp(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let x = arg_f64(a, 0)?;
let r = x.exp();
if !r.is_finite() {
return Err(RuntimeError::OVERFLOW);
}
Ok(Some(Value::Dbl(r)))
}
fn bi_log(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let x = arg_f64(a, 0)?;
if x <= 0.0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(Some(Value::Dbl(x.ln())))
}
fn bi_sin(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(arg_f64(a, 0)?.sin())))
}
fn bi_cos(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(arg_f64(a, 0)?.cos())))
}
fn bi_tan(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(arg_f64(a, 0)?.tan())))
}
fn bi_atn(_: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(arg_f64(a, 0)?.atan())))
}
fn bi_rnd(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let v = if a.is_empty() {
st.rng_next()
} else {
let x = arg_f64(a, 0)?;
if x == 0.0 {
st.rnd_last
} else {
if x < 0.0 {
// Neu aussäen aus dem Argument (deterministisch).
st.rng = ((-x).to_bits() >> 20) as u32 & 0xFF_FFFF;
}
st.rng_next()
}
};
Ok(Some(Value::Sng(v)))
}
fn bi_randomize(st: &mut RtState, _: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let x = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? };
// 16 Bit aus dem Argument in Bits 823 des Zustands (Vorbild-Schema;
// die exakte Mischung klärt die Phase-3-Aufgabe „RND/RANDOMIZE —
// kompatibler PRNG" in PLAN.md).
let b = x.to_bits();
let m = ((b >> 32) ^ (b >> 48)) as u16;
st.rng = ((m as u32) << 8) | (st.rng & 0xFF);
Ok(None)
}
// ---- Konsole ------------------------------------------------------------------
fn bi_print_val(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let v = a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
st.print.print_value(host, v);
Ok(None)
}
fn bi_print_comma(st: &mut RtState, host: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
st.print.print_comma(host);
Ok(None)
}
fn bi_print_tab(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
st.print.print_tab(host, n);
Ok(None)
}
fn bi_print_spc(st: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
st.print.print_spc(host, n);
Ok(None)
}
fn bi_print_newline(st: &mut RtState, host: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
st.print.print_newline(host);
Ok(None)
}
// ---- Sonstiges ------------------------------------------------------------------
fn bi_timer(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
// Sekunden seit Mitternacht (UTC-basiert; lokale Zeitzone: Phase 3).
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64() % 86_400.0)
.unwrap_or(0.0);
Ok(Some(Value::Sng(secs as f32)))
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
// Howard Hinnant, days→(y,m,d)
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
(y + if m <= 2 { 1 } else { 0 }, m, d)
}
fn bi_date(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let (y, m, d) = civil_from_days((secs / 86_400) as i64);
s_ok(format!("{m:02}-{d:02}-{y:04}"))
}
fn bi_time(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let s = secs % 86_400;
s_ok(format!("{:02}:{:02}:{:02}", s / 3600, (s / 60) % 60, s % 60))
}
fn bi_command(st: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
s_ok(st.command.clone())
}
fn bi_doevents(_: &mut RtState, _: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
// Ereigniszustellung kommt mit Phase 4.
Ok(Some(Value::Int(0)))
}
fn bi_sleep(_: &mut RtState, host: &mut dyn Host, a: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
let secs = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? };
host.sleep(secs);
Ok(None)
}
fn bi_beep(_: &mut RtState, host: &mut dyn Host, _: &mut [Value]) -> Result<Option<Value>, RuntimeError> {
host.write("\u{0007}");
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host::CaptureHost;
fn call(id: u16, args: Vec<Value>) -> Result<Option<Value>, RuntimeError> {
let mut st = RtState::default();
let mut host = CaptureHost::default();
let mut a = args;
builtin_table()[id as usize](&mut st, &mut host, &mut a)
}
fn s(v: &str) -> Value {
Value::Str(Rc::from(v))
}
#[test]
fn stringfunktionen_randfaelle() {
// VAL liest Präfix
let Some(Value::Dbl(v)) = call(ids::VAL, vec![s(" 12.5abc")]).unwrap() else {
panic!()
};
assert_eq!(v, 12.5);
// STR$ mit führendem Leerzeichen
let Some(Value::Str(r)) = call(ids::STR_S, vec![Value::Int(42)]).unwrap() else {
panic!()
};
assert_eq!(&*r, " 42");
// ASC("") → Fehler 5
assert_eq!(
call(ids::ASC, vec![s("")]).unwrap_err(),
RuntimeError::ILLEGAL_FUNCTION_CALL
);
// MID$-Anweisung
let Some(Value::Str(r)) =
call(ids::MID_ASSIGN, vec![s("hallo"), Value::Lng(2), Value::Lng(2), s("EY")])
.unwrap()
else {
panic!()
};
assert_eq!(&*r, "hEYlo");
// LEN zählt Zeichen (Unicode)
let Some(Value::Int(n)) = call(ids::LEN, vec![s("äöü")]).unwrap() else {
panic!()
};
assert_eq!(n, 3);
// INSTR
let Some(Value::Int(p)) =
call(ids::INSTR, vec![Value::Lng(1), s("Terminal Basic"), s("Basic")]).unwrap()
else {
panic!()
};
assert_eq!(p, 10);
}
#[test]
fn builtin_ueber_tabelle() {
// Spec-Szenario: LEN über Tabellenindex liefert 3.
let Some(Value::Int(n)) = call(ids::LEN, vec![s("abc")]).unwrap() else {
panic!()
};
assert_eq!(n, 3);
}
}