Files
TerminalBasic/crates/tb-runtime/src/value.rs

288 lines
8.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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>),
/// Formular-/Control-Objekt mit optionalem Index eines Control-Arrays.
Obj(u16, Option<i32>),
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 % 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);
}
}