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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user