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

View File

@@ -0,0 +1,112 @@
//! Konsolen-Druckzustand: Spaltenverfolgung, PRINT-Formatierung,
//! 14-Zeichen-Druckzonen, TAB/SPC. Wirkung ausschließlich über `Host`.
use crate::format::format_print;
use crate::host::Host;
use crate::value::Value;
/// Breite einer Druckzone (Vorbild: 14 Zeichen).
pub const ZONE_WIDTH: usize = 14;
/// Druckzustand (Spalte 0-basiert, in Zeichen).
#[derive(Default)]
pub struct PrintState {
pub col: usize,
}
impl PrintState {
/// Text ausgeben und Spalte nachführen.
pub fn write(&mut self, host: &mut dyn Host, s: &str) {
host.write(s);
match s.rfind('\n') {
Some(i) => self.col = s[i + 1..].chars().count(),
None => self.col += s.chars().count(),
}
}
/// Ein PRINT-Element: Zahlen mit Vorzeichenspalte und nachgestelltem
/// Leerzeichen, Strings unverändert.
pub fn print_value(&mut self, host: &mut dyn Host, v: &Value) {
match v {
Value::Str(s) => {
let s = s.clone();
self.write(host, &s);
}
_ => {
let s = format_print(v);
self.write(host, &s);
}
}
}
/// `,` — Sprung zur nächsten Druckzone (belegte Zone → übernächste).
pub fn print_comma(&mut self, host: &mut dyn Host) {
let next = (self.col / ZONE_WIDTH + 1) * ZONE_WIDTH;
let pad = next - self.col;
self.write(host, &" ".repeat(pad));
}
/// `TAB(n)` — zur Spalte n (1-basiert); liegt der Cursor bereits
/// dahinter, zuerst Zeilenumbruch (Vorbild).
pub fn print_tab(&mut self, host: &mut dyn Host, n: i32) {
let target = (n.max(1) as usize) - 1;
if self.col > target {
self.write(host, "\n");
}
if target > self.col {
let pad = target - self.col;
self.write(host, &" ".repeat(pad));
}
}
/// `SPC(n)` — n Leerzeichen.
pub fn print_spc(&mut self, host: &mut dyn Host, n: i32) {
if n > 0 {
self.write(host, &" ".repeat(n as usize));
}
}
pub fn print_newline(&mut self, host: &mut dyn Host) {
self.write(host, "\n");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host::CaptureHost;
#[test]
fn druckzonen() {
let mut ps = PrintState::default();
let mut h = CaptureHost::default();
// "a" (1 Zeichen) , → Spalte 14
ps.write(&mut h, "a");
ps.print_comma(&mut h);
ps.write(&mut h, "b");
assert_eq!(h.output, "a b");
assert_eq!(ps.col, 15);
}
#[test]
fn volle_zone_springt_zur_uebernaechsten() {
let mut ps = PrintState::default();
let mut h = CaptureHost::default();
ps.write(&mut h, "12345678901234"); // 14 Zeichen, Zone voll
ps.print_comma(&mut h);
ps.write(&mut h, "x");
// x beginnt in Spalte 29 (1-basiert) = Index 28
assert_eq!(h.output.chars().count(), 29);
assert!(h.output.ends_with("x"));
}
#[test]
fn zahlen_mit_vorzeichenspalte() {
let mut ps = PrintState::default();
let mut h = CaptureHost::default();
ps.print_value(&mut h, &Value::Int(1));
ps.print_value(&mut h, &Value::Int(-2));
ps.print_newline(&mut h);
assert_eq!(h.output, " 1 -2 \n");
}
}

View File

@@ -0,0 +1,240 @@
//! Textdarstellung von Zahlen (PRINT/STR$) und `VAL`-Parsen nach der
//! Matrix in docs/tbvm-design.md: SINGLE bis 7, DOUBLE bis 16
//! signifikante Stellen, keine führende Null vor dem Dezimalpunkt,
//! Exponentialform `E±xx` bzw. `D±xx` außerhalb des Festformat-Bereichs.
use crate::value::Value;
/// Nackte Ziffernfolge ohne Vorzeichenspalte (`1.5`, `-2.5`, `.5`, `1E+08`).
pub fn format_number(v: &Value) -> String {
match v {
Value::Int(x) => x.to_string(),
Value::Lng(x) => x.to_string(),
Value::Sng(x) => fmt_float(*x as f64, 7, 'E'),
Value::Dbl(x) => fmt_float(*x, 16, 'D'),
Value::Cur(c) => fmt_currency(*c),
_ => String::new(),
}
}
/// STR$-Stil: führendes Leerzeichen für nicht-negative Werte, `-` sonst;
/// kein nachgestelltes Leerzeichen.
pub fn format_str_fn(v: &Value) -> String {
let s = format_number(v);
if s.starts_with('-') {
s
} else {
format!(" {s}")
}
}
/// PRINT-Stil: wie STR$ plus nachgestelltes Leerzeichen.
pub fn format_print(v: &Value) -> String {
let mut s = format_str_fn(v);
s.push(' ');
s
}
fn fmt_currency(c: i64) -> String {
let neg = c < 0;
let abs = c.unsigned_abs();
let int = abs / 10_000;
let frac = abs % 10_000;
let mut s = if frac == 0 {
int.to_string()
} else {
let f = format!("{frac:04}");
let f = f.trim_end_matches('0');
if int == 0 {
format!(".{f}")
} else {
format!("{int}.{f}")
}
};
if neg {
s.insert(0, '-');
}
s
}
/// Gleitkomma mit maximal `sig` signifikanten Stellen; Exponentialform
/// (Marke `E` bzw. `D`), wenn der Dezimalexponent < 7 oder ≥ `sig` ist
/// (Schwelle: Festlegung der Matrix in docs/tbvm-design.md; Verifikation
/// gegen die Original-Hilfe ist in PLAN.md Phase 3 eingeplant).
fn fmt_float(x: f64, sig: usize, expch: char) -> String {
if x == 0.0 {
return "0".to_string();
}
if x.is_nan() {
return "NaN".to_string();
}
if x.is_infinite() {
return if x < 0.0 { "-1E+38".into() } else { "1E+38".into() };
}
let neg = x < 0.0;
let ax = x.abs();
// Mantisse/Exponent mit `sig` Stellen bestimmen.
let e = format!("{:.*e}", sig - 1, ax); // z. B. "1.500000e0"
let (mant, exp) = e.split_once('e').unwrap();
let exp: i32 = exp.parse().unwrap();
let mut digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
// Rundungsüberlauf ("9.99…e5" → "10.0e5") normalisiert `format!` bereits.
// Nachgestellte Nullen der Mantisse entfernen.
while digits.len() > 1 && digits.ends_with('0') {
digits.pop();
}
let body = if exp < -7 || exp >= sig as i32 {
// Exponentialform: D. Mantisse "d[.rest]"
let mut m = String::new();
m.push(digits.as_bytes()[0] as char);
if digits.len() > 1 {
m.push('.');
m.push_str(&digits[1..]);
}
format!("{m}{expch}{}{:02}", if exp < 0 { '-' } else { '+' }, exp.abs())
} else if exp >= 0 {
let e = exp as usize;
if (e + 1) >= digits.len() {
// Ganzzahl, ggf. Nullen anhängen
let mut s = digits.clone();
s.push_str(&"0".repeat(e + 1 - digits.len()));
s
} else {
format!("{}.{}", &digits[..e + 1], &digits[e + 1..])
}
} else {
// 0 > exp >= -7: ".0…digits" ohne führende Null
let zeros = (-exp - 1) as usize;
let mut s = String::from(".");
s.push_str(&"0".repeat(zeros));
s.push_str(&digits);
s
};
if neg {
format!("-{body}")
} else {
body
}
}
/// `VAL`: liest das führende Zahlenpräfix (Leerraum wird übersprungen;
/// `&H`/`&O`-Präfixe wie beim Vorbild), ignoriert Restzeichen.
pub fn val(s: &str) -> f64 {
let t = s.trim_start();
// Hex/Oktal
if let Some(rest) = t.strip_prefix("&H").or_else(|| t.strip_prefix("&h")) {
let hex: String = rest.chars().take_while(|c| c.is_ascii_hexdigit()).collect();
return i64::from_str_radix(&hex, 16).unwrap_or(0) as f64;
}
if let Some(rest) = t.strip_prefix("&O").or_else(|| t.strip_prefix("&o")) {
let oct: String = rest.chars().take_while(|c| ('0'..='7').contains(c)).collect();
return i64::from_str_radix(&oct, 8).unwrap_or(0) as f64;
}
let bytes: Vec<char> = t.chars().collect();
let mut i = 0;
let mut num = String::new();
if i < bytes.len() && (bytes[i] == '+' || bytes[i] == '-') {
num.push(bytes[i]);
i += 1;
}
let mut seen_digit = false;
let mut seen_dot = false;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_digit() {
seen_digit = true;
num.push(c);
i += 1;
} else if c == '.' && !seen_dot {
seen_dot = true;
num.push(c);
i += 1;
} else if c == ' ' || c == '\t' {
// Vorbild: VAL ignoriert eingebetteten Leerraum
i += 1;
} else {
break;
}
}
// Exponent (E/D)
if seen_digit && i < bytes.len() && matches!(bytes[i], 'e' | 'E' | 'd' | 'D') {
let mut j = i + 1;
let mut exp = String::new();
if j < bytes.len() && (bytes[j] == '+' || bytes[j] == '-') {
exp.push(bytes[j]);
j += 1;
}
let mut exp_digits = false;
while j < bytes.len() && bytes[j].is_ascii_digit() {
exp.push(bytes[j]);
exp_digits = true;
j += 1;
}
if exp_digits {
num.push('e');
num.push_str(&exp);
}
}
if !seen_digit {
return 0.0;
}
num.parse().unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::Value;
#[test]
fn ganzzahlen() {
assert_eq!(format_number(&Value::Int(42)), "42");
assert_eq!(format_number(&Value::Int(-7)), "-7");
assert_eq!(format_print(&Value::Int(1)), " 1 ");
assert_eq!(format_print(&Value::Int(-2)), "-2 ");
assert_eq!(format_str_fn(&Value::Int(42)), " 42");
}
#[test]
fn single_darstellung() {
assert_eq!(format_number(&Value::Sng(1.5)), "1.5");
assert_eq!(format_number(&Value::Sng(-2.5)), "-2.5");
assert_eq!(format_number(&Value::Sng(0.5)), ".5");
assert_eq!(format_number(&Value::Sng(3.0)), "3");
assert_eq!(format_number(&Value::Sng(1.0 / 3.0)), ".3333333");
assert_eq!(format_number(&Value::Sng(1e8)), "1E+08");
assert_eq!(format_number(&Value::Sng(9_999_999.0)), "9999999");
assert_eq!(format_number(&Value::Sng(1e7)), "1E+07");
}
#[test]
fn double_darstellung() {
assert_eq!(format_number(&Value::Dbl(1.5)), "1.5");
assert_eq!(format_number(&Value::Dbl(1e16)), "1D+16");
assert_eq!(
format_number(&Value::Dbl(0.3333333333333333)),
".3333333333333333"
);
}
#[test]
fn currency_darstellung() {
assert_eq!(format_number(&Value::Cur(15_000)), "1.5");
assert_eq!(format_number(&Value::Cur(10_000)), "1");
assert_eq!(format_number(&Value::Cur(-12_345)), "-1.2345");
assert_eq!(format_number(&Value::Cur(2_500)), ".25");
}
#[test]
fn val_parsen() {
assert_eq!(val(" 12.5abc"), 12.5);
assert_eq!(val("-3"), -3.0);
assert_eq!(val("1e2"), 100.0);
assert_eq!(val("1D2"), 100.0);
assert_eq!(val("&HFF"), 255.0);
assert_eq!(val("&O10"), 8.0);
assert_eq!(val("abc"), 0.0);
assert_eq!(val(" 1 2 3"), 123.0); // eingebetteter Leerraum
}
}

View File

@@ -0,0 +1,92 @@
//! `Host`-Abstraktion für Konsolen-E/A (Design-Entscheidung D3):
//! Alle Konsolenwirkungen der VM laufen über dieses Trait. Host-Aufrufe
//! dürfen blockieren; die Abbruchprüfung (Strg+Untbr) obliegt dem Host.
use std::collections::VecDeque;
use std::io::{BufRead, Write as _};
pub trait Host {
/// Text ausgeben (ohne implizite Zeilenumbrüche).
fn write(&mut self, s: &str);
/// Eine Eingabezeile lesen (ohne Zeilenende); `None` = Eingabeende.
fn read_line(&mut self) -> Option<String>;
/// Abbruchwunsch (Strg+Untbr)? Wird an Anweisungsgrenzen geprüft.
fn interrupted(&mut self) -> bool {
false
}
/// `SLEEP` — blockierend im Host.
fn sleep(&mut self, _secs: f64) {}
}
/// Konsolen-Host für `tbc run`: stdout/stdin.
#[derive(Default)]
pub struct ConsoleHost;
impl Host for ConsoleHost {
fn write(&mut self, s: &str) {
let mut out = std::io::stdout().lock();
let _ = out.write_all(s.as_bytes());
let _ = out.flush();
}
fn read_line(&mut self) -> Option<String> {
let mut line = String::new();
match std::io::stdin().lock().read_line(&mut line) {
Ok(0) => None,
Ok(_) => {
while line.ends_with('\n') || line.ends_with('\r') {
line.pop();
}
Some(line)
}
Err(_) => None,
}
}
fn sleep(&mut self, secs: f64) {
if secs > 0.0 {
std::thread::sleep(std::time::Duration::from_secs_f64(secs));
}
}
}
/// Capture-Host für Tests: zeichnet die Ausgabe byte-genau auf und
/// liefert vorbereitete Eingabezeilen.
#[derive(Default)]
pub struct CaptureHost {
pub output: String,
pub input: VecDeque<String>,
}
impl CaptureHost {
pub fn with_input(lines: &[&str]) -> Self {
CaptureHost {
output: String::new(),
input: lines.iter().map(|s| s.to_string()).collect(),
}
}
}
impl Host for CaptureHost {
fn write(&mut self, s: &str) {
self.output.push_str(s);
}
fn read_line(&mut self) -> Option<String> {
self.input.pop_front()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capture_host_zeichnet_bytegenau_auf() {
let mut h = CaptureHost::default();
h.write(" 1 2 ");
h.write("\n");
h.write("x");
assert_eq!(h.output, " 1 2 \nx");
}
}

View File

@@ -4,8 +4,13 @@
//! gruppiert nach Themen. Ziel ist verhaltensgleiche Nachbildung inklusive
//! Rundungs-, Formatierungs- und Fehlerverhalten (siehe PLAN.md, Phase 3).
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
pub mod datetime; // DATE$, TIME$, TIMER
pub mod builtins; // Dispatch-Tabelle für CALL_BUILTIN (Phase-2-Scheibe)
pub mod console; // Druckzustand: Zonen, TAB/SPC, Zahlenausgabe
pub mod errors; // Laufzeitfehler-Codes und -Meldungen des Vorbilds
pub mod fileio; // OPEN/CLOSE/PRINT#/INPUT#/GET/PUT, sequenziell/random/binär
pub mod format; // Zahlendarstellung (PRINT/STR$) und VAL
pub mod host; // Host-Trait (Konsole, Capture) — Entscheidung D3
pub mod datetime; // DATE$, TIME$, TIMER
pub mod math; // Arithmetik, Rundung (Banker's Rounding), RND/RANDOMIZE …
pub mod strings; // LEFT$, MID$, INSTR, STR$, VAL, Formatierung mit PRINT USING …
pub mod value; // Laufzeitwerte, Arrays/Records, Konvertierungsmatrix

View File

@@ -0,0 +1,283 @@
//! Laufzeitwerte der TBVM: Tagged Enum (Entwurf docs/tbvm-design.md),
//! `Rc` statt GC (der Dialekt kennt keine Zyklen), Arrays/Records als
//! geteilte Handles, Referenzwerte für BYREF-Parameter.
//!
//! Hier lebt außerdem die **Zahlenkonvertierungs-Matrix** (Banker's
//! Rounding, Überlauf → Fehler 6) — die einzige Implementierung der in
//! docs/tbvm-design.md dokumentierten Semantik.
use crate::errors::RuntimeError;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone)]
pub enum Value {
Int(i16),
Lng(i32),
Sng(f32),
Dbl(f64),
Cur(i64),
Str(Rc<str>),
Arr(Rc<RefCell<ArrayObj>>),
Rec(Rc<RefCell<RecordObj>>),
/// Referenz (BYREF-Parameter-Slot).
Ref(VarRef),
/// Nicht-initialisiertes Array-/Record-Handle (Auto-DIM bei Zugriff).
Empty,
}
/// Referenzziel eines BYREF-Parameters.
#[derive(Debug, Clone)]
pub enum VarRef {
Global(u16),
/// Absoluter Index in den Locals-Stack der VM.
Stack(u32),
/// Arrayelement (flacher Index).
Elem(Rc<RefCell<ArrayObj>>, u32),
/// Record-Feldpfad.
Field(Rc<RefCell<RecordObj>>, Vec<u16>),
}
/// Initialisierungstyp eines Slots/Elements (serialisierbar im `.tbc`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeInit {
Int,
Lng,
Sng,
Dbl,
Cur,
Str,
FixedStr(u32),
Udt(u16),
/// Array-/Record-Slot ohne Vorbelegung (Auto-DIM).
Empty,
}
/// UDT-Layout für die Default-Erzeugung.
#[derive(Debug, Clone)]
pub struct UdtLayout {
pub name: String,
pub fields: Vec<TypeInit>,
}
pub fn default_value(init: &TypeInit, udts: &[UdtLayout]) -> Value {
match init {
TypeInit::Int => Value::Int(0),
TypeInit::Lng => Value::Lng(0),
TypeInit::Sng => Value::Sng(0.0),
TypeInit::Dbl => Value::Dbl(0.0),
TypeInit::Cur => Value::Cur(0),
TypeInit::Str => Value::Str(Rc::from("")),
TypeInit::FixedStr(n) => Value::Str(Rc::from(" ".repeat(*n as usize).as_str())),
TypeInit::Udt(id) => {
let layout = &udts[*id as usize];
let fields = layout
.fields
.iter()
.map(|f| default_value(f, udts))
.collect();
Value::Rec(Rc::new(RefCell::new(RecordObj { fields })))
}
TypeInit::Empty => Value::Empty,
}
}
#[derive(Debug)]
pub struct ArrayObj {
pub elem: TypeInit,
/// (Untergrenze, Obergrenze) je Dimension.
pub dims: Vec<(i32, i32)>,
pub data: Vec<Value>,
}
impl ArrayObj {
pub fn new(elem: TypeInit, dims: Vec<(i32, i32)>, udts: &[UdtLayout]) -> Result<Self, RuntimeError> {
let mut len: usize = 1;
for (lo, hi) in &dims {
if hi < lo {
return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE);
}
let n = (*hi as i64 - *lo as i64 + 1) as usize;
len = len.checked_mul(n).ok_or(RuntimeError::OUT_OF_MEMORY)?;
if len > 64 * 1024 * 1024 {
return Err(RuntimeError::OUT_OF_MEMORY);
}
}
let mut data = Vec::with_capacity(len);
for _ in 0..len {
data.push(default_value(&elem, udts));
}
Ok(ArrayObj { elem, dims, data })
}
/// Flacher Index (zeilenweise, letzte Dimension läuft am schnellsten);
/// Bereichsprüfung → Fehler 9.
pub fn flat_index(&self, idx: &[i32]) -> Result<u32, RuntimeError> {
if idx.len() != self.dims.len() {
return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE);
}
let mut flat: u64 = 0;
for (i, (lo, hi)) in idx.iter().zip(self.dims.iter()) {
if i < lo || i > hi {
return Err(RuntimeError::SUBSCRIPT_OUT_OF_RANGE);
}
let span = (*hi as i64 - *lo as i64 + 1) as u64;
flat = flat * span + (*i as i64 - *lo as i64) as u64;
}
Ok(flat as u32)
}
}
#[derive(Debug)]
pub struct RecordObj {
pub fields: Vec<Value>,
}
// ---- Konvertierungsmatrix ---------------------------------------------------
/// Kaufmännische Rundung zur nächsten geraden Zahl (Banker's Rounding,
/// Verhalten von CINT/CLNG des Vorbilds).
pub fn banker_round(x: f64) -> f64 {
let floor = x.floor();
let diff = x - floor;
if diff > 0.5 {
floor + 1.0
} else if diff < 0.5 {
floor
} else {
// exakt .5 → zur geraden Zahl
if (floor as i64) % 2 == 0 {
floor
} else {
floor + 1.0
}
}
}
pub fn f64_to_i16(x: f64) -> Result<i16, RuntimeError> {
let r = banker_round(x);
if !(i16::MIN as f64..=i16::MAX as f64).contains(&r) {
return Err(RuntimeError::OVERFLOW);
}
Ok(r as i16)
}
pub fn f64_to_i32(x: f64) -> Result<i32, RuntimeError> {
let r = banker_round(x);
if !(i32::MIN as f64..=i32::MAX as f64).contains(&r) {
return Err(RuntimeError::OVERFLOW);
}
Ok(r as i32)
}
pub fn f64_to_f32(x: f64) -> Result<f32, RuntimeError> {
if x.is_finite() && x.abs() > f32::MAX as f64 {
return Err(RuntimeError::OVERFLOW);
}
Ok(x as f32)
}
/// f64 → CURRENCY (Festkomma ×10 000, Banker's auf der 4. Nachkommastelle).
pub fn f64_to_cur(x: f64) -> Result<i64, RuntimeError> {
let scaled = banker_round(x * 10_000.0);
if !((i64::MIN as f64) < scaled && scaled < (i64::MAX as f64)) {
return Err(RuntimeError::OVERFLOW);
}
Ok(scaled as i64)
}
/// CURRENCY → Ganzzahl (÷10 000, Banker's; ganzzahlig exakt).
pub fn cur_to_i64(c: i64) -> i64 {
let q = c.div_euclid(10_000);
let r = c.rem_euclid(10_000);
// r in 0..10000; runde halb-zu-gerade
if r > 5_000 {
q + 1
} else if r < 5_000 {
q
} else if q % 2 == 0 {
q
} else {
q + 1
}
}
pub fn cur_to_i16(c: i64) -> Result<i16, RuntimeError> {
let v = cur_to_i64(c);
i16::try_from(v).map_err(|_| RuntimeError::OVERFLOW)
}
pub fn cur_to_i32(c: i64) -> Result<i32, RuntimeError> {
let v = cur_to_i64(c);
i32::try_from(v).map_err(|_| RuntimeError::OVERFLOW)
}
pub fn cur_to_f64(c: i64) -> f64 {
c as f64 / 10_000.0
}
/// i32 → i16 mit Bereichsprüfung.
pub fn i32_to_i16(v: i32) -> Result<i16, RuntimeError> {
i16::try_from(v).map_err(|_| RuntimeError::OVERFLOW)
}
/// Numerischer Wert als f64 (für Builtins, die per Tag dispatchen).
pub fn as_f64(v: &Value) -> f64 {
match v {
Value::Int(x) => *x as f64,
Value::Lng(x) => *x as f64,
Value::Sng(x) => *x as f64,
Value::Dbl(x) => *x,
Value::Cur(x) => cur_to_f64(*x),
_ => 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn banker_rounding() {
assert_eq!(banker_round(0.5), 0.0);
assert_eq!(banker_round(1.5), 2.0);
assert_eq!(banker_round(2.5), 2.0);
assert_eq!(banker_round(-0.5), 0.0);
assert_eq!(banker_round(-1.5), -2.0);
assert_eq!(banker_round(2.4), 2.0);
assert_eq!(banker_round(2.6), 3.0);
}
#[test]
fn overflow_bei_konvertierung() {
assert!(f64_to_i16(40_000.0).is_err());
assert!(f64_to_i16(32_767.4).is_ok());
assert!(f64_to_i32(3e9).is_err());
assert!(f64_to_f32(1e39).is_err());
}
#[test]
fn currency_rundung() {
assert_eq!(f64_to_cur(1.5).unwrap(), 15_000);
assert_eq!(f64_to_cur(-2.25).unwrap(), -22_500);
// Halb-zu-gerade nur bei exakt darstellbarem .5-Fall (Skalierung
// 10000.5 ist binär exakt, wenn der Ausgangswert es hergibt):
assert_eq!(banker_round(10_000.5), 10_000.0);
assert_eq!(cur_to_i64(15_000), 2); // 1.5 → 2
assert_eq!(cur_to_i64(25_000), 2); // 2.5 → 2
assert_eq!(cur_to_i64(-15_000), -2);
}
#[test]
fn array_indexpruefung() {
let a = ArrayObj::new(TypeInit::Int, vec![(0, 10)], &[]).unwrap();
assert_eq!(a.flat_index(&[0]).unwrap(), 0);
assert_eq!(a.flat_index(&[10]).unwrap(), 10);
assert!(a.flat_index(&[11]).is_err());
assert!(a.flat_index(&[-1]).is_err());
let b = ArrayObj::new(TypeInit::Int, vec![(1, 3), (1, 2)], &[]).unwrap();
assert_eq!(b.flat_index(&[1, 1]).unwrap(), 0);
assert_eq!(b.flat_index(&[1, 2]).unwrap(), 1);
assert_eq!(b.flat_index(&[2, 1]).unwrap(), 2);
}
}