Files
TerminalBasic/crates/tb-runtime/src/value.rs
Chili Palmer f7e57b0bd8 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>
2026-09-02 11:28:07 +02:00

284 lines
8.2 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>),
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);
}
}