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:
623
crates/tb-runtime/src/builtins.rs
Normal file
623
crates/tb-runtime/src/builtins.rs
Normal 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 8–23 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);
|
||||
}
|
||||
}
|
||||
112
crates/tb-runtime/src/console.rs
Normal file
112
crates/tb-runtime/src/console.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
240
crates/tb-runtime/src/format.rs
Normal file
240
crates/tb-runtime/src/format.rs
Normal 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
|
||||
}
|
||||
}
|
||||
92
crates/tb-runtime/src/host.rs
Normal file
92
crates/tb-runtime/src/host.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
283
crates/tb-runtime/src/value.rs
Normal file
283
crates/tb-runtime/src/value.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user