//! 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), /// Formular-/Control-Objekt mit optionalem Index eines Control-Arrays. Obj(u16, Option), Arr(Rc>), Rec(Rc>), /// 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>, u32), /// Record-Feldpfad. Field(Rc>, Vec), } /// 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, } 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, } impl ArrayObj { pub fn new( elem: TypeInit, dims: Vec<(i32, i32)>, udts: &[UdtLayout], ) -> Result { 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 { 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, } // ---- 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 { 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 { 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 { 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 { 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 % 2 == 0 { q } else { q + 1 } } pub fn cur_to_i16(c: i64) -> Result { let v = cur_to_i64(c); i16::try_from(v).map_err(|_| RuntimeError::OVERFLOW) } pub fn cur_to_i32(c: i64) -> Result { 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::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); } }