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

2906 lines
82 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.
//! 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::datetime as dt;
use crate::errors::RuntimeError;
use crate::fileio as fio;
use crate::finanz as fin;
use crate::format;
use crate::host::{Ereignis, Host};
use crate::screen::TextScreen;
use crate::value;
use crate::value::{as_f64, cur_to_f64, f64_to_cur, Value};
use std::collections::VecDeque;
use std::rc::Rc;
/// Laufzeitzustand der Bibliothek: Bildschirm, Eingabepuffer, PRNG,
/// Kommandozeile.
pub struct RtState {
/// Der Textbildschirm — Ziel aller Bildschirmwirkungen.
pub screen: TextScreen,
/// Offene Dateien nach Dateinummer.
pub dateien: crate::fileio::Dateien,
/// ISAM-Zustand: Tabellenbindungen, Cursor, laufende Transaktion.
pub isam: crate::isam::Isam,
pub print: PrintState,
/// Noch nicht abgeholte Tastendrücke in `INKEY$`-Form, je mit
/// Umschaltzustand (Bitfeld 1 Umschalt, 2 Strg, 4 Alt). Der Zustand
/// wird nur für die benutzerdefinierten Trap-Tasten gebraucht;
/// `INKEY$` und `INPUT` sehen ihn nicht.
pub tasten: VecDeque<(String, u8)>,
/// Mausereignisse für den Forms-Dispatch; sie werden erst an den
/// bestehenden Zustellpunkten von der VM entnommen.
pub maus: VecDeque<crate::host::MausEreignis>,
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
pub abbruch: bool,
/// Eingabeende wurde gemeldet.
pub ende: bool,
/// Funktionstasten-Makros: F1F10, dann F11/F12 (`KEY 30`/`KEY 31`).
pub key_makros: [String; 12],
/// Ist die Softkey-Zeile eingeblendet (`KEY ON`)?
pub key_zeile: bool,
/// Währungszeichen für `$$` in `PRINT USING` (siehe `SetFormatCC`).
pub waehrung: crate::using::Waehrung,
/// Ablaufverfolgung (`TRON`/`TROFF`) eingeschaltet?
pub trace: bool,
/// Zustände der klassischen Ereignis-Traps (Sprachreferenz §8).
pub traps: crate::traps::Traps,
/// Trefferliste und Zeiger für `DIR$`.
pub dir_treffer: Vec<String>,
pub dir_index: usize,
/// Versatz der Programmuhr in Sekunden. `DATE$ =` und `TIME$ =` können
/// die Systemuhr nicht stellen; sie verschieben stattdessen diesen
/// Versatz (dokumentierte Abweichung). Er wirkt auf die Ortszeit.
pub uhr_offset: i64,
/// Woher der UTC-Versatz kommt. Vorgabe ist die Zone des Rechners;
/// ist sie nicht ermittelbar, steht hier `Unbekannt` und es gilt UTC.
pub zeitzone: dt::Zeitzone,
rng: u32,
rnd_last: f32,
pub command: String,
pub clipboard: String,
}
impl Default for RtState {
fn default() -> Self {
RtState {
screen: TextScreen::new(),
dateien: Default::default(),
isam: Default::default(),
print: PrintState::default(),
tasten: VecDeque::new(),
maus: VecDeque::new(),
abbruch: false,
ende: false,
// Das Vorbild belegt die Makros im Interpreter vor; ein
// kompiliertes Programm startet mit leeren Makros.
key_makros: Default::default(),
key_zeile: false,
waehrung: Default::default(),
trace: false,
traps: crate::traps::Traps::neu(),
dir_treffer: Vec::new(),
dir_index: 0,
uhr_offset: 0,
zeitzone: dt::Zeitzone::ermitteln(),
// 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(),
clipboard: String::new(),
}
}
}
impl RtState {
/// Ereignisse vom Host abholen und einsortieren: Größenänderungen wirken
/// sofort auf den Bildschirm, Abbruch und Eingabeende werden gemerkt,
/// Tastendrücke landen in der Warteschlange für `INKEY$`/`INPUT`.
///
/// Mit `blockierend = true` wird gewartet, bis mindestens ein Ereignis
/// kam oder das Eingabeende erreicht ist.
pub fn pump(&mut self, host: &mut dyn Host, blockierend: bool) {
loop {
let Some(e) = host.next_event(blockierend) else {
return;
};
match e {
Ereignis::Taste(t, shift) => self.tasten.push_back((t, shift)),
Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows),
Ereignis::Abbruch => self.abbruch = true,
Ereignis::Ende => self.ende = true,
Ereignis::Signal(n) => {
if n == 1 && !self.traps.melden(crate::traps::Quelle::Signal(n)) {
self.abbruch = true;
}
}
Ereignis::Maus(m) => self.maus.push_back(m),
}
if blockierend {
return;
}
}
}
/// Bildschirm anzeigen und anstehende Ereignisse einsammeln.
/// Zustellpunkt an Tick-Grenzen und vor blockierender Eingabe.
pub fn tick(&mut self, host: &mut dyn Host) {
host.present(&self.screen);
self.pump(host, false);
self.traps_pruefen(host);
}
/// Was am Zustellpunkt vor der eigentlichen Zustellung zu tun ist:
/// Zeit prüfen und getrappte Tasten aus der Schlange nehmen.
///
/// Die Tasten werden **hier** aussortiert, nicht beim Einsortieren
/// (design.md, D5): so wirkt ein `KEY(n) OFF` sofort und auch auf
/// Tasten, die schon in der Schlange liegen.
pub fn traps_pruefen(&mut self, host: &mut dyn Host) {
if !self.traps.aktiv() {
return;
}
let jetzt = host.jetzt_ms();
self.traps.zeit_pruefen(jetzt);
self.tasten_traps_pruefen();
}
/// Getrappte Tasten aus der Schlange nehmen. Ohne Host, damit die VM
/// das an jeder Anweisungsgrenze tun kann, ohne die Uhr zu lesen.
pub fn tasten_traps_pruefen(&mut self) {
// Nur der Kopf der Schlange: die Reihenfolge der Eingabe bleibt
// erhalten, und eine getrappte Taste hinter einer ungetrappten
// wartet, bis diese gelesen ist.
while let Some((s, shift)) = self.tasten.front() {
let Some(code) = crate::host::taste::scancode_von(s) else {
break;
};
let Some(n) = self.traps.taste_faengt(code, *shift) else {
break;
};
self.tasten.pop_front();
self.traps.melden(crate::traps::Quelle::Key(n));
}
}
/// Nächster Tastendruck in `INKEY$`-Form; blockiert bei `blockierend`.
pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option<String> {
loop {
if let Some((t, _)) = self.tasten.pop_front() {
return Some(t);
}
if self.ende {
return None;
}
self.pump(host, blockierend);
if !blockierend && self.tasten.is_empty() {
return None;
}
if blockierend && self.tasten.is_empty() && self.ende {
return None;
}
}
}
/// Zeileneingabe auf dem Bildschirm: Zeichen werden geechot, Rückschritt
/// löscht, Enter schließt ab. `None` = Eingabeende (Fehler 62).
pub fn read_line(&mut self, host: &mut dyn Host) -> Option<String> {
let mut zeile = String::new();
loop {
host.present(&self.screen);
let t = self.naechste_taste(host, true)?;
match t.as_str() {
crate::host::taste::ENTER => {
self.screen.print("\n");
return Some(zeile);
}
crate::host::taste::BACKSPACE => {
if zeile.pop().is_some() {
self.screen.rueckschritt();
}
}
_ => {
// Sondertasten (Nullzeichen + Kennung) erzeugen keine Eingabe.
if let Some(c) = einzelzeichen(&t) {
zeile.push(c);
self.screen.print(&c.to_string());
}
}
}
}
}
/// Softkey-Zeile ein-/ausblenden (`KEY ON`/`KEY OFF`). Eingeblendet
/// belegt sie die unterste Zeile; der Scrollbereich endet darüber, damit
/// die Ausgabe sie nicht überschreibt.
pub fn softkey_zeile(&mut self, ein: bool) {
if ein == self.key_zeile {
return;
}
self.key_zeile = ein;
let rows = self.screen.rows();
if ein {
let _ = self.screen.view_print(1, rows - 1);
self.softkey_zeile_zeichnen();
} else {
let (z, s) = (self.screen.csrlin(), self.screen.pos());
let _ = self.screen.view_print(1, rows);
let _ = self.screen.locate(rows, 1);
self.screen.print(&" ".repeat(self.screen.cols()));
let _ = self.screen.locate(z, s);
}
}
/// Die Softkey-Zeile neu zeichnen: Tastennummer plus die ersten sechs
/// Zeichen des Makros, wie beim Vorbild.
fn softkey_zeile_zeichnen(&mut self) {
let (z, s) = (self.screen.csrlin(), self.screen.pos());
let breite = self.screen.cols();
let mut zeile = String::new();
for (i, m) in self.key_makros.iter().take(10).enumerate() {
zeile.push_str(&format!("{}", i + 1));
zeile.extend(m.chars().take(6));
zeile.push(' ');
}
zeile.truncate(breite);
let rows = self.screen.rows();
let _ = self.screen.locate(rows, 1);
self.screen.print(&format!("{zeile:<breite$}"));
let _ = self.screen.locate(z.min(rows - 1), s);
}
/// Startwert setzen. Die 16 Bit aus dem Argument bilden die Bits 823
/// des 24-Bit-Zustands; das niederwertige Byte ist fest. Dadurch liefert
/// derselbe Startwert stets dieselbe Folge — unabhängig davon, wie viele
/// `RND`-Aufrufe vorher liefen.
pub fn saat_setzen(&mut self, x: f64) {
let b = x.to_bits();
let m = ((b >> 32) ^ (b >> 48)) as u16;
self.rng = (m as u32) << 8;
}
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
}
}
/// Ein Tastendruck als druckbares Zeichen — Sondertasten (führendes
/// Nullzeichen) und Steuerzeichen liefern `None`.
fn einzelzeichen(t: &str) -> Option<char> {
let mut it = t.chars();
let c = it.next()?;
if it.next().is_some() || c == '\0' || (c as u32) < 0x20 {
return None;
}
Some(c)
}
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 PRINT_USING: u16 = 37;
pub const FORMAT_S: u16 = 38;
pub const SET_FORMAT_CC: u16 = 39;
pub const CLS: u16 = 40;
pub const COLOR: u16 = 41;
pub const LOCATE: u16 = 42;
pub const WIDTH: u16 = 43;
pub const VIEW_PRINT: u16 = 44;
pub const SCREEN_STMT: u16 = 45;
pub const KEY_ASSIGN: u16 = 46;
pub const KEY_LIST: u16 = 47;
pub const KEY_DISPLAY: u16 = 48;
pub const CSRLIN: u16 = 49;
pub const POS_FN: u16 = 50;
pub const SCREEN_FN: u16 = 51;
pub const INKEY_S: u16 = 52;
pub const INPUT_S: u16 = 53;
pub const TIMER: u16 = 54;
pub const DATE_S: u16 = 55;
pub const TIME_S: u16 = 56;
pub const DATE_SET: u16 = 57;
pub const TIME_SET: u16 = 58;
pub const NOW: u16 = 59;
pub const DATE_SERIAL: u16 = 60;
pub const TIME_SERIAL: u16 = 61;
pub const DATE_VALUE: u16 = 62;
pub const TIME_VALUE: u16 = 63;
pub const DAY_F: u16 = 64;
pub const MONTH_F: u16 = 65;
pub const YEAR_F: u16 = 66;
pub const WEEKDAY_F: u16 = 67;
pub const HOUR_F: u16 = 68;
pub const MINUTE_F: u16 = 69;
pub const SECOND_F: u16 = 70;
pub const COMMAND_S: u16 = 71;
pub const DOEVENTS: u16 = 72;
pub const SLEEP: u16 = 73;
pub const BEEP: u16 = 74;
pub const FV: u16 = 75;
pub const PV: u16 = 76;
pub const PMT: u16 = 77;
pub const NPER: u16 = 78;
pub const IPMT: u16 = 79;
pub const PPMT: u16 = 80;
pub const RATE: u16 = 81;
pub const NPV: u16 = 82;
pub const IRR: u16 = 83;
pub const MIRR: u16 = 84;
pub const SLN: u16 = 85;
pub const SYD: u16 = 86;
pub const DDB: u16 = 87;
pub const OPEN: u16 = 88;
pub const CLOSE: u16 = 89;
pub const CLOSE_ALL: u16 = 90;
pub const PRINT_ZIEL: u16 = 91;
pub const WRITE_FILE: u16 = 92;
pub const EOF_F: u16 = 93;
pub const LOF_F: u16 = 94;
pub const LOC_F: u16 = 95;
pub const SEEK_F: u16 = 96;
pub const SEEK_STMT: u16 = 97;
pub const FREEFILE: u16 = 98;
pub const FILEATTR: u16 = 99;
pub const LOCK_STMT: u16 = 100;
pub const KILL: u16 = 101;
pub const NAME_STMT: u16 = 102;
pub const FILES: u16 = 103;
pub const CHDIR: u16 = 104;
pub const CHDRIVE: u16 = 105;
pub const MKDIR: u16 = 106;
pub const RMDIR: u16 = 107;
pub const CURDIR_S: u16 = 108;
pub const DIR_S: u16 = 109;
pub const LPOS: u16 = 110;
pub const SHELL_STMT: u16 = 111;
pub const SHELL_FN: u16 = 112;
pub const MK_S: u16 = 113;
pub const CV_F: u16 = 114;
pub const ENVIRON_S: u16 = 115;
pub const ENVIRON_SET: u16 = 116;
pub const FRE: u16 = 117;
pub const CLEAR: u16 = 118;
pub const TRON: u16 = 119;
pub const TROFF: u16 = 120;
pub const STACK_FN: u16 = 121;
pub const STACK_STMT: u16 = 122;
pub const ERDEV: u16 = 123;
pub const ERDEV_S: u16 = 124;
// ISAM (Change `phase-3-isam`)
pub const ISAM_OPEN: u16 = 125;
pub const ISAM_CREATE_INDEX: u16 = 126;
pub const ISAM_DELETE_INDEX: u16 = 127;
pub const ISAM_SET_INDEX: u16 = 128;
pub const ISAM_GET_INDEX_S: u16 = 129;
pub const ISAM_INSERT: u16 = 130;
pub const ISAM_RETRIEVE: u16 = 131;
pub const ISAM_UPDATE: u16 = 132;
pub const ISAM_DELETE: u16 = 133;
pub const ISAM_DELETE_TABLE: u16 = 134;
pub const ISAM_MOVE_FIRST: u16 = 135;
pub const ISAM_MOVE_LAST: u16 = 136;
pub const ISAM_MOVE_NEXT: u16 = 137;
pub const ISAM_MOVE_PREVIOUS: u16 = 138;
pub const ISAM_SEEK_EQ: u16 = 139;
pub const ISAM_SEEK_GT: u16 = 140;
pub const ISAM_SEEK_GE: u16 = 141;
pub const ISAM_BEGIN_TRANS: u16 = 142;
pub const ISAM_COMMIT_TRANS: u16 = 143;
pub const ISAM_ROLLBACK: u16 = 144;
pub const ISAM_SAVEPOINT: u16 = 145;
pub const ISAM_SETMEM: u16 = 146;
pub const ISAM_BOF: u16 = 147;
/// `SetUEvent` — loest das benutzerdefinierte Ereignis aus (§8).
pub const SETUEVENT: u16 = 148;
pub const MSGBOX: u16 = 149;
pub const INPUTBOX_S: u16 = 150;
pub const CLIPBOARD_ADD: u16 = 151;
pub const CLIPBOARD_GET: u16 = 152;
pub const GRAPHICS_LINE: u16 = 153;
pub const GRAPHICS_PAINT: u16 = 154;
pub const GRAPHICS_VIEW: u16 = 155;
pub const COUNT: u16 = 156;
}
/// 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_print_using,
bi_format_s,
bi_set_format_cc,
bi_cls,
bi_color,
bi_locate,
bi_width,
bi_view_print,
bi_screen_stmt,
bi_key_assign,
bi_key_list,
bi_key_display,
bi_csrlin,
bi_pos,
bi_screen_fn,
bi_inkey,
bi_input_s,
bi_timer,
bi_date,
bi_time,
bi_date_set,
bi_time_set,
bi_now,
bi_date_serial,
bi_time_serial,
bi_date_value,
bi_time_value,
bi_day,
bi_month,
bi_year,
bi_weekday,
bi_hour,
bi_minute,
bi_second,
bi_command,
bi_doevents,
bi_sleep,
bi_beep,
bi_fv,
bi_pv,
bi_pmt,
bi_nper,
bi_ipmt,
bi_ppmt,
bi_rate,
bi_npv,
bi_irr,
bi_mirr,
bi_sln,
bi_syd,
bi_ddb,
bi_open,
bi_close,
bi_close_all,
bi_print_ziel,
bi_write_file,
bi_eof,
bi_lof,
bi_loc,
bi_seek_f,
bi_seek_stmt,
bi_freefile,
bi_fileattr,
bi_lock,
bi_kill,
bi_name,
bi_files,
bi_chdir,
bi_chdrive,
bi_mkdir,
bi_rmdir,
bi_curdir,
bi_dir,
bi_lpos,
bi_shell_stmt,
bi_shell_fn,
bi_mk,
bi_cv,
bi_environ_s,
bi_environ_set,
bi_fre,
bi_clear,
bi_tron,
bi_troff,
bi_stack_fn,
bi_stack_stmt,
bi_erdev,
bi_erdev_s,
// ISAM
bi_isam_open,
bi_isam_create_index,
bi_isam_delete_index,
bi_isam_set_index,
bi_isam_get_index,
bi_isam_insert,
bi_isam_retrieve,
bi_isam_update,
bi_isam_delete,
bi_isam_delete_table,
bi_isam_move_first,
bi_isam_move_last,
bi_isam_move_next,
bi_isam_move_previous,
bi_isam_seek_eq,
bi_isam_seek_gt,
bi_isam_seek_ge,
bi_isam_begin_trans,
bi_isam_commit_trans,
bi_isam_rollback,
bi_isam_savepoint,
bi_isam_setmem,
bi_isam_bof,
bi_setuevent,
bi_msgbox,
bi_inputbox,
bi_clipboard_add,
bi_clipboard_get,
bi_graphics_line,
bi_graphics_paint,
bi_graphics_view,
];
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
TABLE
}
fn bi_clipboard_add(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.clipboard.push_str(&arg_str(a, 0)?);
Ok(None)
}
fn bi_clipboard_get(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Str(Rc::from(st.clipboard.as_str()))))
}
// ---- 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.saat_setzen(-x);
}
st.rng_next()
}
};
Ok(Some(Value::Sng(v)))
}
fn bi_randomize(
st: &mut RtState,
host: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let x = match a.first() {
Some(_) => arg_f64(a, 0)?,
// `RANDOMIZE` ohne Argument erfragt den Startwert (Vorbild).
None => {
st.screen.print("Random Number Seed (-32768 to 32767)? ");
let Some(zeile) = st.read_line(host) else {
return Err(RuntimeError(62)); // Input past end of file
};
crate::format::val(&zeile)
}
};
st.saat_setzen(x);
Ok(None)
}
// ---- Konsole ------------------------------------------------------------------
fn bi_print_val(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let v = a.first().ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
st.print.print_value(&mut st.screen, &mut st.dateien, v)?;
Ok(None)
}
fn bi_print_comma(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.print.print_comma(&mut st.screen, &mut st.dateien)?;
Ok(None)
}
fn bi_print_tab(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
st.print.print_tab(&mut st.screen, &mut st.dateien, n)?;
Ok(None)
}
fn bi_print_spc(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
st.print.print_spc(&mut st.screen, &mut st.dateien, n)?;
Ok(None)
}
fn bi_print_newline(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.print.print_newline(&mut st.screen, &mut st.dateien)?;
Ok(None)
}
// ---- System ----------------------------------------------------------------------
/// Ersatzwerte für Größen ohne Entsprechung auf heutigen Plattformen
/// (dokumentierte Abweichung, s. docs/sprachreferenz.md).
pub const FRE_ERSATZ: i32 = 65_536;
pub const STACK_ERSATZ: i32 = 8_192;
fn bi_environ_s(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Mit String: Wert der Variablen. Mit Zahl: der n-te Eintrag als
// `NAME=WERT` (Vorbild).
match a.first() {
Some(Value::Str(name)) => s_ok(std::env::var(name.as_ref()).unwrap_or_default()),
Some(_) => {
let n = arg_i32(a, 0)?;
if n < 1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let mut alle: Vec<(String, String)> = std::env::vars().collect();
alle.sort();
s_ok(
alle.get(n as usize - 1)
.map(|(k, v)| format!("{k}={v}"))
.unwrap_or_default(),
)
}
None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
}
}
fn bi_environ_set(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// `ENVIRON "NAME=WERT"`; ohne Wert wird die Variable entfernt.
let text = arg_str(a, 0)?;
let (name, wert) = text
.split_once('=')
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
if name.is_empty() {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
if wert.is_empty() {
unsafe { std::env::remove_var(name) };
} else {
unsafe { std::env::set_var(name, wert) };
}
Ok(None)
}
fn bi_fre(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Lng(FRE_ERSATZ)))
}
fn bi_clear(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// `CLEAR` schließt alle Dateien und setzt den Druckzustand zurück.
// Variablen räumt der Codegenerator nicht — das übernimmt der Runner
// beim Neustart (dokumentierte Abweichung).
st.dateien.alle_schliessen()?;
st.print.ziel = crate::console::Ziel::Bildschirm;
st.print.spalte = 0;
Ok(None)
}
fn bi_tron(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.trace = true;
Ok(None)
}
fn bi_troff(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.trace = false;
Ok(None)
}
fn bi_stack_fn(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Lng(STACK_ERSATZ)))
}
fn bi_stack_stmt(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Die Stapelgröße bestimmt die Wirtsplattform; die Anweisung ist
// folgenlos (dokumentierte Abweichung).
Ok(None)
}
fn bi_erdev(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Gerätefehlercodes des Vorbilds gibt es nicht; 0 = kein Fehler.
Ok(Some(Value::Int(0)))
}
fn bi_erdev_s(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
s_ok(String::new())
}
// ---- Datei-E/A --------------------------------------------------------------------
fn bi_open(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// (dateiname$, nummer, modus, reclen)
let pfad = arg_str(a, 0)?;
let nummer = arg_i32(a, 1)?;
let modus = fio::Modus::aus_text(&arg_str(a, 2)?).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
let reclen = arg_i32(a, 3)?.max(1) as usize;
if st.isam.ist_isam(nummer) {
return Err(RuntimeError::FILE_ALREADY_OPEN);
}
st.dateien
.oeffnen(nummer, &pfad, modus, reclen)
.map(|_| None)
}
fn bi_close(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
if st.isam.ist_isam(n) {
return st.isam.schliessen(n).map(|_| None);
}
st.dateien.schliessen(n).map(|_| None)
}
fn bi_close_all(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.isam.bindungen_schliessen();
st.dateien.alle_schliessen().map(|_| None)
}
/// Ausgabeziel von `PRINT` umschalten: 1 = Bildschirm, 2 = Drucker,
/// sonst die Dateinummer.
fn bi_print_ziel(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
st.print.ziel = match n {
-1 => crate::console::Ziel::Bildschirm,
-2 => crate::console::Ziel::Drucker,
n => {
if !st.dateien.ist_offen(n) {
return Err(RuntimeError(52));
}
crate::console::Ziel::Datei(n)
}
};
// Datei- und Druckerspalte beginnen bei jedem Wechsel neu am Zeilenanfang
// nur dann, wenn zuvor der Bildschirm aktiv war.
if n == -1 {
st.print.spalte = 0;
}
Ok(None)
}
fn bi_write_file(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Elemente komma-getrennt, Strings in Anführungszeichen, dann Umbruch.
let mut zeile = String::new();
for (i, v) in a.iter().enumerate() {
if i > 0 {
zeile.push(',');
}
match v {
Value::Str(s) => zeile.push_str(&fio::write_element(s, true)),
_ => zeile.push_str(&fio::write_element(&format::format_print(v), false)),
}
}
zeile.push('\n');
st.print.write(&mut st.screen, &mut st.dateien, &zeile)?;
Ok(None)
}
fn bi_eof(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
// Bei einer ISAM-Dateinummer meldet EOF das Ende der Tabelle in der
// Ordnung des aktiven Index, nicht das Dateiende.
if st.isam.ist_isam(n) {
return Ok(Some(Value::Int(if st.isam.eof(n) { -1 } else { 0 })));
}
let e = st.dateien.get(n)?.eof()?;
Ok(Some(Value::Int(if e { -1 } else { 0 })))
}
fn bi_lof(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
// ISAM: Zahl der Sätze in der Tabelle.
if st.isam.ist_isam(n) {
return Ok(Some(Value::Lng(st.isam.satzzahl(n)? as i32)));
}
let l = st.dateien.get(n)?.laenge()?;
Ok(Some(Value::Lng(l as i32)))
}
fn bi_loc(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
// ISAM: Kennung des aktuellen Satzes, 0 wenn unpositioniert.
if st.isam.ist_isam(n) {
return Ok(Some(Value::Lng(st.isam.satznummer(n)? as i32)));
}
let d = st.dateien.get(n)?;
// Position der letzten Operation (1-basiert): Datensatz bzw. Byte.
Ok(Some(Value::Lng(d.position.saturating_sub(1) as i32)))
}
fn bi_seek_f(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let d = st.dateien.get(arg_i32(a, 0)?)?;
Ok(Some(Value::Lng(d.position as i32)))
}
fn bi_seek_stmt(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let pos = arg_i32(a, 1)?;
if pos < 1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
st.dateien.get(arg_i32(a, 0)?)?.position = pos as u64;
Ok(None)
}
fn bi_freefile(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let nummer = (st.dateien.freie_nummer()..=i16::MAX as i32)
.find(|n| !st.dateien.ist_offen(*n) && !st.isam.ist_isam(*n))
.ok_or(RuntimeError::TOO_MANY_FILES)?;
Ok(Some(Value::Int(nummer as i16)))
}
fn bi_fileattr(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let d = st.dateien.get(arg_i32(a, 0)?)?;
// 1 = Modus. Andere Kennzahlen (Betriebssystem-Handle) bildet der
// Dialekt nicht ab; sie liefern 0 (dokumentierte Abweichung).
let wert = if arg_i32(a, 1)? == 1 {
match d.modus {
fio::Modus::Input => 1,
fio::Modus::Output => 2,
fio::Modus::Random => 4,
fio::Modus::Append => 8,
fio::Modus::Binary => 32,
}
} else {
0
};
Ok(Some(Value::Lng(wert)))
}
fn bi_lock(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Die Datei muss offen sein; echte Sperren bietet die Plattform nicht
// einheitlich (dokumentierte Abweichung).
st.dateien.get(arg_i32(a, 0)?)?;
Ok(None)
}
fn bi_kill(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
fio::loeschen(&arg_str(a, 0)?).map(|_| None)
}
fn bi_name(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
fio::umbenennen(&arg_str(a, 0)?, &arg_str(a, 1)?).map(|_| None)
}
fn bi_files(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let muster = match a.first() {
Some(Value::Str(s)) if !s.is_empty() => s.to_string(),
_ => "*".to_string(),
};
for n in fio::suchen(&muster)? {
st.screen.print(&n);
st.screen.print("\n");
}
Ok(None)
}
fn bi_chdir(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
fio::verzeichnis_wechseln(&arg_str(a, 0)?).map(|_| None)
}
fn bi_chdrive(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Laufwerksbuchstaben gibt es nur auf Windows; anderswo ist die
// Anweisung folgenlos (dokumentierte Abweichung).
let _ = arg_str(a, 0)?;
Ok(None)
}
fn bi_mkdir(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
fio::verzeichnis_anlegen(&arg_str(a, 0)?).map(|_| None)
}
fn bi_rmdir(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
fio::verzeichnis_entfernen(&arg_str(a, 0)?).map(|_| None)
}
fn bi_curdir(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
s_ok(fio::aktuelles_verzeichnis())
}
fn bi_dir(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// `DIR$(muster$)` beginnt eine Suche, `DIR$("")` liefert den nächsten
// Treffer, zuletzt den leeren String.
let muster = arg_str(a, 0)?;
if !muster.is_empty() {
st.dir_treffer = fio::suchen(&muster)?;
st.dir_index = 0;
}
let t = st
.dir_treffer
.get(st.dir_index)
.cloned()
.unwrap_or_default();
if !t.is_empty() {
st.dir_index += 1;
}
s_ok(t)
}
fn bi_lpos(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Spalte im Druckerpuffer (1-basiert).
let spalte = match st.print.drucker.rfind('\n') {
Some(i) => st.print.drucker[i + 1..].chars().count(),
None => st.print.drucker.chars().count(),
};
Ok(Some(Value::Int(spalte as i16 + 1)))
}
/// Gemeinsamer Kern von `SHELL` als Anweisung und als Funktion.
fn shell_ausfuehren(befehl: &str) -> Result<i32, RuntimeError> {
let mut cmd = if cfg!(windows) {
let mut c = std::process::Command::new("cmd");
c.args(["/C", befehl]);
c
} else {
let mut c = std::process::Command::new("sh");
c.args(["-c", befehl]);
c
};
cmd.status()
.map(|s| s.code().unwrap_or(0))
.map_err(|_| RuntimeError(53))
}
fn bi_shell_stmt(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let befehl = match a.first() {
Some(Value::Str(s)) => s.to_string(),
_ => String::new(),
};
if befehl.is_empty() {
return Ok(None);
}
shell_ausfuehren(&befehl).map(|_| None)
}
fn bi_shell_fn(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let code = shell_ausfuehren(&arg_str(a, 0)?)?;
Ok(Some(Value::Lng(code)))
}
/// `MKI$`/`MKL$`/`MKS$`/`MKD$`/`MKC$` — Zahl in ihre Bytedarstellung.
/// Das zweite Argument wählt die Breite (2/4/4/8/8) und den Typ.
fn bi_mk(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let art = arg_i32(a, 1)?;
let bytes: Vec<u8> = match art {
0 => (arg_i32(a, 0)? as i16).to_le_bytes().to_vec(),
1 => arg_i32(a, 0)?.to_le_bytes().to_vec(),
2 => (arg_f64(a, 0)? as f32).to_le_bytes().to_vec(),
3 => arg_f64(a, 0)?.to_le_bytes().to_vec(),
_ => match a.first() {
Some(Value::Cur(c)) => c.to_le_bytes().to_vec(),
_ => value::f64_to_cur(arg_f64(a, 0)?)?.to_le_bytes().to_vec(),
},
};
// Bytes als Zeichen mit Codepoint 0255 ablegen (verlustfrei umkehrbar).
s_ok(bytes.iter().map(|b| *b as char).collect::<String>())
}
/// `CVI`/`CVL`/`CVS`/`CVD`/`CVC` — Bytedarstellung zurück in eine Zahl.
fn bi_cv(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let text = arg_str(a, 0)?;
let art = arg_i32(a, 1)?;
let breite = match art {
0 => 2,
1 | 2 => 4,
_ => 8,
};
let bytes: Vec<u8> = text
.chars()
.map(|c| u8::try_from(c as u32).unwrap_or(0))
.collect();
if bytes.len() < breite {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(Some(match art {
0 => Value::Int(i16::from_le_bytes([bytes[0], bytes[1]])),
1 => Value::Lng(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])),
2 => Value::Sng(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])),
3 => Value::Dbl(f64::from_le_bytes(bytes[..8].try_into().unwrap())),
_ => Value::Cur(i64::from_le_bytes(bytes[..8].try_into().unwrap())),
}))
}
// ---- Finanzmathematik -----------------------------------------------------------
/// Zahlungsreihe aus einem Array-Argument lesen.
fn arg_reihe(a: &[Value], i: usize) -> Result<Vec<f64>, RuntimeError> {
match a.get(i) {
Some(Value::Arr(arr)) => Ok(arr.borrow().data.iter().map(as_f64).collect()),
_ => Err(RuntimeError::TYPE_MISMATCH),
}
}
macro_rules! finanz_fn {
($name:ident, |$a:ident| $ausdruck:expr) => {
fn $name(
_: &mut RtState,
_: &mut dyn Host,
$a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl($ausdruck?)))
}
};
}
finanz_fn!(bi_fv, |a| fin::fv(
arg_f64(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?,
arg_f64(a, 3)?,
arg_f64(a, 4)?
));
finanz_fn!(bi_pv, |a| fin::pv(
arg_f64(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?,
arg_f64(a, 3)?,
arg_f64(a, 4)?
));
finanz_fn!(bi_pmt, |a| fin::pmt(
arg_f64(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?,
arg_f64(a, 3)?,
arg_f64(a, 4)?
));
finanz_fn!(bi_nper, |a| fin::nper(
arg_f64(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?,
arg_f64(a, 3)?,
arg_f64(a, 4)?
));
finanz_fn!(bi_ipmt, |a| fin::ipmt(
arg_f64(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?,
arg_f64(a, 3)?,
arg_f64(a, 4)?,
arg_f64(a, 5)?
));
finanz_fn!(bi_ppmt, |a| fin::ppmt(
arg_f64(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?,
arg_f64(a, 3)?,
arg_f64(a, 4)?,
arg_f64(a, 5)?
));
finanz_fn!(bi_rate, |a| fin::rate(
arg_f64(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?,
arg_f64(a, 3)?,
arg_f64(a, 4)?,
arg_f64(a, 5)?
));
finanz_fn!(bi_npv, |a| fin::npv(arg_f64(a, 0)?, &arg_reihe(a, 1)?));
finanz_fn!(bi_irr, |a| fin::irr(&arg_reihe(a, 0)?, arg_f64(a, 1)?));
finanz_fn!(bi_mirr, |a| fin::mirr(
&arg_reihe(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?
));
finanz_fn!(bi_sln, |a| fin::sln(
arg_f64(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?
));
finanz_fn!(bi_syd, |a| fin::syd(
arg_f64(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?,
arg_f64(a, 3)?
));
finanz_fn!(bi_ddb, |a| fin::ddb(
arg_f64(a, 0)?,
arg_f64(a, 1)?,
arg_f64(a, 2)?,
arg_f64(a, 3)?
));
// ---- Formatierung ---------------------------------------------------------------
fn bi_print_using(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let fmt = arg_str(a, 0)?;
let text = crate::using::using(&fmt, &a[1..], &st.waehrung)?;
st.print.write(&mut st.screen, &mut st.dateien, &text)?;
Ok(None)
}
fn bi_format_s(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// `FORMAT$(wert [, format$])` — ohne Format wie `LTRIM$(STR$(wert))`.
let wert = a.first().cloned().unwrap_or(Value::Int(0));
let text = match a.get(1) {
Some(Value::Str(f)) => crate::using::using(f, std::slice::from_ref(&wert), &st.waehrung)?,
None => format::format_print(&wert).trim().to_string(),
Some(_) => return Err(RuntimeError::TYPE_MISMATCH),
};
Ok(Some(Value::Str(Rc::from(text.as_str()))))
}
fn bi_set_format_cc(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Ländercode → Währungszeichen. Die Tabelle des Vorbilds ist nicht
// vollständig belegt; nicht aufgeführte Codes behalten "$"
// (dokumentierte Abweichung).
let code = arg_i32(a, 0)?;
st.waehrung.zeichen = match code {
1 => "$",
33 => "F",
39 => "L",
44 => "£",
49 => "DM",
81 => "¥",
_ => "$",
}
.to_string();
Ok(None)
}
// ---- Bildschirm -----------------------------------------------------------------
/// Ausgelassenes Argument (`LOCATE , 5`) kommt als -1 herein.
fn opt(a: &[Value], i: usize) -> Result<Option<i32>, RuntimeError> {
match a.get(i) {
None => Ok(None),
Some(_) => {
let v = arg_i32(a, i)?;
Ok(if v < 0 { None } else { Some(v) })
}
}
}
fn bi_cls(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// `CLS`, `CLS 0` und `CLS 2` löschen bei uns dasselbe: den Textbereich.
// Einen getrennten Grafikbereich gibt es nicht (Non-Feature).
st.screen.cls();
Ok(None)
}
fn bi_color(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let fg = opt(a, 0)?.unwrap_or(st.screen.fg as i32);
let bg = opt(a, 1)?.unwrap_or(st.screen.bg as i32);
// Vordergrund 031 (1631 = blinkend, als hell simuliert), Hintergrund 07.
if !(0..=31).contains(&fg) || !(0..=7).contains(&bg) {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
st.screen.set_color(fg as u8, bg as u8);
Ok(None)
}
fn bi_locate(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let zeile = opt(a, 0)?.unwrap_or(st.screen.csrlin() as i32);
let spalte = opt(a, 1)?.unwrap_or(st.screen.pos() as i32);
// Drittes Argument steuert die Cursorsichtbarkeit; Startzeile/Endzeile
// der Cursorform (4./5.) bildet ein Terminal nicht ab und bleibt folgenlos.
if let Some(sichtbar) = opt(a, 2)? {
st.screen.cursor_visible = sichtbar != 0;
}
if zeile < 1 || spalte < 1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
st.screen
.locate(zeile as usize, spalte as usize)
.map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL)?;
Ok(None)
}
fn bi_width(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Die Größe bestimmt das Terminal; WIDTH hebt den Puffer höchstens an
// (dokumentierte Abweichung). Kleinere Werte bleiben folgenlos.
let cols = opt(a, 0)?.unwrap_or(st.screen.cols() as i32);
let rows = opt(a, 1)?.unwrap_or(st.screen.rows() as i32);
if cols < 1 || rows < 1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
st.screen.resize(cols as usize, rows as usize);
Ok(None)
}
fn bi_view_print(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
match (opt(a, 0)?, opt(a, 1)?) {
// `VIEW PRINT` ohne Argumente: voller Bildschirm.
(None, None) => {
let rows = st.screen.rows();
st.screen
.view_print(1, rows)
.map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL)?;
}
(Some(t), Some(b)) if t >= 1 && b >= t => {
st.screen
.view_print(t as usize, b as usize)
.map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL)?;
}
_ => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
}
Ok(None)
}
fn bi_screen_stmt(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Die Terminaldarstellung bildet die historischen Pixelmodi auf Zellen ab;
// Seitenargumente haben im einzelnen Puffer keine zusätzliche Wirkung.
match opt(a, 0)? {
None | Some(0..=13) => {
st.screen.graphics_view(None).unwrap();
st.screen.cls();
Ok(None)
}
Some(_) => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
}
}
fn bi_graphics_line(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let mut from = (arg_i32(a, 0)?, arg_i32(a, 1)?);
if from.0 == i32::MIN {
from = st.screen.graphics_position();
}
let mut to = (arg_i32(a, 2)?, arg_i32(a, 3)?);
if arg_i32(a, 5)? != 0 {
to.0 += from.0;
to.1 += from.1;
}
let color = match arg_i32(a, 4)? {
-1 => i32::from(st.screen.fg),
color @ 0..=15 => color,
_ => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
};
st.screen
.graphics_line(from, to, color, arg_i32(a, 6)? as u8);
Ok(None)
}
fn bi_graphics_paint(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let color = match a.get(2) {
Some(Value::Int(v)) => i32::from(*v),
Some(Value::Lng(v)) => *v,
Some(Value::Str(_)) | None => i32::from(st.screen.fg),
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
if !(0..=15).contains(&color) {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
st.screen
.graphics_paint(arg_i32(a, 0)?, arg_i32(a, 1)?, color);
Ok(None)
}
fn bi_graphics_view(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let x1 = arg_i32(a, 0)?;
if x1 < 0 {
st.screen.graphics_view(None).unwrap();
return Ok(None);
}
let view = (x1, arg_i32(a, 1)?, arg_i32(a, 2)?, arg_i32(a, 3)?);
st.screen
.graphics_view(Some(view))
.map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL)?;
let fill = arg_i32(a, 4)?;
if fill >= 0 {
st.screen
.graphics_line((view.0, view.1), (view.2, view.3), fill, 2);
}
let border = arg_i32(a, 5)?;
if border >= 0 {
st.screen
.graphics_line((view.0, view.1), (view.2, view.3), border, 1);
}
Ok(None)
}
/// Nummer eines Funktionstasten-Makros → Index 011.
fn key_index(n: i32) -> Option<usize> {
match n {
1..=10 => Some(n as usize - 1),
30 => Some(10),
31 => Some(11),
_ => None,
}
}
fn bi_key_assign(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
let text = arg_str(a, 1)?;
// 1525 weisen kein Makro zu, sondern erklären eine Trap-Taste über
// Tastaturflagbyte und Scancode (Sprachreferenz §8).
if (15..=25).contains(&n) {
let mut it = text.chars();
let (Some(flag), Some(scan)) = (it.next(), it.next()) else {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
};
if it.next().is_some() || flag as u32 > 255 || scan as u32 > 255 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
st.traps.benutzertaste(n as u8, flag as u8, scan as u8);
return Ok(None);
}
let i = key_index(n).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
// Höchstens 15 Zeichen; überzählige werden verworfen (Vorbild).
st.key_makros[i] = text.chars().take(15).collect();
if st.key_zeile {
st.softkey_zeile_zeichnen();
}
Ok(None)
}
fn bi_key_list(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Alle zwölf Makros untereinander, wie im Vorbild.
for m in st.key_makros.clone() {
st.screen.print(&m);
st.screen.print("\n");
}
Ok(None)
}
fn bi_key_display(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let ein = arg_i32(a, 0)? != 0;
st.softkey_zeile(ein);
Ok(None)
}
fn bi_csrlin(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Int(st.screen.csrlin() as i16)))
}
fn bi_pos(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Int(st.screen.pos() as i16)))
}
fn bi_screen_fn(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let zeile = arg_i32(a, 0)?;
let spalte = arg_i32(a, 1)?;
let farbe = opt(a, 2)?.unwrap_or(0);
if zeile < 1
|| spalte < 1
|| zeile as usize > st.screen.rows()
|| spalte as usize > st.screen.cols()
{
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
let z = st.screen.cell(zeile as usize, spalte as usize);
let wert = if farbe != 0 {
// Farbattribut wie im Vorbild: Hintergrund*16 + Vordergrund.
(z.bg as i32) * 16 + z.fg as i32
} else {
z.ch as i32
};
Ok(Some(Value::Int(wert.min(i16::MAX as i32) as i16)))
}
fn bi_inkey(
st: &mut RtState,
host: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Nicht blockierend: leerer String, wenn keine Taste anliegt.
let t = st.naechste_taste(host, false).unwrap_or_default();
Ok(Some(Value::Str(Rc::from(t.as_str()))))
}
fn bi_input_s(
st: &mut RtState,
host: &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);
}
if a.len() > 1 {
let nummer = arg_i32(a, 1)?;
let datei = st.dateien.get(nummer)?;
let bytes = datei.bytes_lesen(datei.position, n as usize)?;
return Ok(Some(Value::Str(Rc::from(
String::from_utf8_lossy(&bytes).as_ref(),
))));
}
// Blockierend, ohne Echo auf dem Bildschirm.
let mut s = String::new();
for _ in 0..n {
host.present(&st.screen);
let Some(t) = st.naechste_taste(host, true) else {
return Err(RuntimeError(62)); // Input past end of file
};
s.push_str(&t);
}
Ok(Some(Value::Str(Rc::from(s.as_str()))))
}
// ---- Sonstiges ------------------------------------------------------------------
fn bi_timer(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Sekunden seit der **lokalen** Mitternacht, mit Bruchteil.
let bruchteil = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64().fract())
.unwrap_or(0.0);
let ganze = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
Ok(Some(Value::Sng(ganze as f32 + bruchteil as f32)))
}
fn bi_date(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let secs = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset);
let (y, m, d) = dt::civil_from_days(secs.div_euclid(86_400));
s_ok(format!("{m:02}-{d:02}-{y:04}"))
}
fn bi_time(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let s = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
s_ok(format!(
"{:02}:{:02}:{:02}",
s / 3600,
(s / 60) % 60,
s % 60
))
}
fn bi_date_set(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let t = arg_str(a, 0)?;
let ziel = dt::datevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
// Nur den Datumsanteil verschieben, die Tageszeit bleibt.
let jetzt = dt::jetzt_serial(st.zeitzone, st.uhr_offset);
let diff_tage = ziel.floor() - jetzt.floor();
st.uhr_offset += (diff_tage * 86_400.0) as i64;
Ok(None)
}
fn bi_time_set(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let t = arg_str(a, 0)?;
let ziel = dt::timevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
let jetzt = dt::jetzt_serial(st.zeitzone, st.uhr_offset);
let diff = ziel - (jetzt - jetzt.floor());
st.uhr_offset += (diff * 86_400.0).round() as i64;
Ok(None)
}
fn bi_now(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(dt::jetzt_serial(
st.zeitzone,
st.uhr_offset,
))))
}
fn bi_date_serial(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let (y, m, d) = (
arg_i32(a, 0)? as i64,
arg_i32(a, 1)? as i64,
arg_i32(a, 2)? as i64,
);
// Zweistellige Jahre wie beim Vorbild als 19xx deuten.
let y = if (0..100).contains(&y) { y + 1900 } else { y };
if !dt::gueltig(y, m, d) {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(Some(Value::Dbl(dt::serial_aus_ymd(y, m as u32, d as u32))))
}
fn bi_time_serial(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let (h, m, s) = (
arg_i32(a, 0)? as i64,
arg_i32(a, 1)? as i64,
arg_i32(a, 2)? as i64,
);
if !(0..24).contains(&h) || !(0..60).contains(&m) || !(0..60).contains(&s) {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(Some(Value::Dbl(dt::serial_aus_hms(h, m, s))))
}
fn bi_date_value(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let t = arg_str(a, 0)?;
dt::datevalue(&t)
.map(|v| Some(Value::Dbl(v)))
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)
}
fn bi_time_value(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let t = arg_str(a, 0)?;
dt::timevalue(&t)
.map(|v| Some(Value::Dbl(v)))
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)
}
/// Gemeinsame Auswertung der Zerlegungsfunktionen.
fn dt_teil(a: &[Value], f: impl Fn(f64) -> i32) -> Result<Option<Value>, RuntimeError> {
let s = arg_f64(a, 0)?;
Ok(Some(Value::Int(f(s) as i16)))
}
fn bi_day(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
dt_teil(a, |s| dt::ymd_aus_serial(s).2 as i32)
}
fn bi_month(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
dt_teil(a, |s| dt::ymd_aus_serial(s).1 as i32)
}
fn bi_year(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
dt_teil(a, |s| dt::ymd_aus_serial(s).0 as i32)
}
fn bi_weekday(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
dt_teil(a, |s| dt::weekday(s) as i32)
}
fn bi_hour(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
dt_teil(a, |s| dt::hms_aus_serial(s).0 as i32)
}
fn bi_minute(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
dt_teil(a, |s| dt::hms_aus_serial(s).1 as i32)
}
fn bi_second(
_: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
dt_teil(a, |s| dt::hms_aus_serial(s).2 as i32)
}
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(
st: &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)? };
// SLEEP n wartet n Sekunden; SLEEP ohne Argument bis zum Tastendruck.
st.tick(host);
if secs > 0.0 {
std::thread::sleep(std::time::Duration::from_secs_f64(secs));
} else {
st.naechste_taste(host, true);
}
Ok(None)
}
fn bi_setuevent(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// „sets a flag that BASIC checks before executing the next statement"
st.traps.melden(crate::traps::Quelle::UEvent);
Ok(None)
}
fn bi_beep(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// ponytail: Der Signalton geht direkt an das Terminal, nicht durch den
// Zellenpuffer — er hinterlässt dort nichts. Ceiling: sobald der Host
// mehr als Anzeigen kann, wird daraus ein eigener Host-Aufruf.
eprint!("\u{0007}");
Ok(None)
}
// ---- ISAM -------------------------------------------------------------------
//
// Die Anweisungen legen ihre Argumente in der Reihenfolge der Original-Hilfe
// ab; die Dateinummer ist überall das erste (bei `DELETETABLE` der
// Datenbankname). Die Fachlogik steht in `crate::isam`.
fn bi_isam_open(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// (datenbank$, nummer, tabelle$, spaltennamen$, udt-index)
let datenbank = arg_str(a, 0)?;
let nummer = arg_i32(a, 1)?;
let tabelle = arg_str(a, 2)?;
let spalten = arg_str(a, 3)?;
let udt = arg_i32(a, 4)? as u16;
if st.dateien.ist_offen(nummer) {
return Err(RuntimeError::FILE_ALREADY_OPEN);
}
st.isam
.oeffnen(nummer, &datenbank, &tabelle, &spalten, udt)
.map(|_| None)
}
fn bi_isam_create_index(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// (nummer, indexname$, eindeutig%, spalte$ …)
let nummer = arg_i32(a, 0)?;
let name = arg_str(a, 1)?;
let eindeutig = arg_i32(a, 2)? != 0;
let mut spalten = Vec::new();
for i in 3..a.len() {
spalten.push(arg_str(a, i)?.to_string());
}
st.isam
.index_anlegen(nummer, &name, eindeutig, &spalten)
.map(|_| None)
}
fn bi_isam_delete_index(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let nummer = arg_i32(a, 0)?;
let name = arg_str(a, 1)?;
st.isam.index_loeschen(nummer, &name).map(|_| None)
}
fn bi_isam_set_index(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Ohne Indexnamen gilt der NULL-Index (Einfügereihenfolge).
let nummer = arg_i32(a, 0)?;
let name = if a.len() > 1 {
arg_str(a, 1)?.to_string()
} else {
String::new()
};
st.isam.index_setzen(nummer, &name).map(|_| None)
}
fn bi_isam_get_index(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
st.isam
.index_name(n)
.map(|s| Some(Value::Str(Rc::from(s.as_str()))))
}
fn bi_isam_insert(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let nummer = arg_i32(a, 0)?;
let satz = a.get(1).cloned().ok_or(RuntimeError::TYPE_MISMATCH)?;
st.isam.einfuegen(nummer, &satz).map(|_| None)
}
fn bi_isam_retrieve(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Das Satzargument ist ein geteiltes Record-Handle: die gelesenen
// Feldwerte gehen direkt in die Variable des Programms.
let nummer = arg_i32(a, 0)?;
let ziel = match a.get(1) {
Some(Value::Rec(r)) => r.clone(),
_ => return Err(RuntimeError::TYPE_MISMATCH),
};
let gelesen = st.isam.satz_lesen(nummer)?;
let Value::Rec(quelle) = gelesen else {
return Err(RuntimeError::TYPE_MISMATCH);
};
let werte = quelle.borrow().fields.clone();
let mut z = ziel.borrow_mut();
if z.fields.len() != werte.len() {
return Err(RuntimeError::TYPE_MISMATCH);
}
z.fields = werte;
Ok(None)
}
fn bi_isam_update(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let nummer = arg_i32(a, 0)?;
let satz = a.get(1).cloned().ok_or(RuntimeError::TYPE_MISMATCH)?;
st.isam.aendern(nummer, &satz).map(|_| None)
}
fn bi_isam_delete(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.isam.satz_loeschen(arg_i32(a, 0)?).map(|_| None)
}
fn bi_isam_delete_table(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let datenbank = arg_str(a, 0)?;
let tabelle = arg_str(a, 1)?;
st.isam.tabelle_loeschen(&datenbank, &tabelle).map(|_| None)
}
fn isam_bewegen(
st: &mut RtState,
a: &[Value],
richtung: crate::isam::Richtung,
) -> Result<Option<Value>, RuntimeError> {
st.isam.bewegen(arg_i32(a, 0)?, richtung).map(|_| None)
}
fn bi_isam_move_first(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
isam_bewegen(st, a, crate::isam::Richtung::Erster)
}
fn bi_isam_move_last(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
isam_bewegen(st, a, crate::isam::Richtung::Letzter)
}
fn bi_isam_move_next(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
isam_bewegen(st, a, crate::isam::Richtung::Naechster)
}
fn bi_isam_move_previous(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
isam_bewegen(st, a, crate::isam::Richtung::Voriger)
}
fn isam_suchen(
st: &mut RtState,
a: &[Value],
art: crate::isam::Suchart,
) -> Result<Option<Value>, RuntimeError> {
let nummer = arg_i32(a, 0)?;
st.isam.suchen(nummer, art, &a[1..]).map(|_| None)
}
fn bi_isam_seek_eq(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
isam_suchen(st, a, crate::isam::Suchart::Gleich)
}
fn bi_isam_seek_gt(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
isam_suchen(st, a, crate::isam::Suchart::Groesser)
}
fn bi_isam_seek_ge(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
isam_suchen(st, a, crate::isam::Suchart::GroesserGleich)
}
fn bi_isam_begin_trans(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.isam.trans_beginn().map(|_| None)
}
fn bi_isam_commit_trans(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.isam.trans_festschreiben().map(|_| None)
}
fn bi_isam_rollback(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
// Ohne Argument: bis zum letzten Sicherungspunkt bzw. Transaktionsbeginn.
let kennung = if a.is_empty() { 0 } else { arg_i32(a, 0)? };
st.isam.ruecknahme(kennung).map(|_| None)
}
fn bi_isam_savepoint(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
st.isam.sicherungspunkt().map(|k| Some(Value::Int(k)))
}
fn bi_isam_setmem(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let delta = arg_i32(a, 0)? as i64;
let frei = st.isam.setmem(delta);
Ok(Some(Value::Lng(
frei.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
)))
}
fn bi_isam_bof(
st: &mut RtState,
_: &mut dyn Host,
a: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let n = arg_i32(a, 0)?;
Ok(Some(Value::Int(if st.isam.bof(n) { -1 } else { 0 })))
}
fn bi_msgbox(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Err(RuntimeError::FEATURE_UNAVAILABLE)
}
fn bi_inputbox(
_: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Err(RuntimeError::FEATURE_UNAVAILABLE)
}
#[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);
}
#[test]
fn setuevent_meldet_das_benutzerereignis() {
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = crate::host::CaptureHost::default();
st.traps.definieren(Quelle::UEvent, Some(11));
st.traps.setzen(Quelle::UEvent, Zustand::An, 0);
assert_eq!(st.traps.naechstes(), None);
bi_setuevent(&mut st, &mut h, &mut []).unwrap();
assert_eq!(st.traps.naechstes(), Some((Quelle::UEvent, 11)));
}
#[test]
fn signal_aus_dem_host_meldet_den_trap() {
use crate::host::{Ereignis, Host};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = crate::host::CaptureHost::default();
st.traps.definieren(Quelle::Signal(1), Some(22));
st.traps.setzen(Quelle::Signal(1), Zustand::An, 0);
h.ereignis(Ereignis::Signal(1));
st.pump(&mut h, false);
assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(1), 22)));
assert!(h.next_event(false).is_none());
}
#[test]
fn sigint_ohne_aktiven_trap_bleibt_abbruch() {
use crate::host::Ereignis;
let mut st = RtState::default();
let mut h = crate::host::CaptureHost::default();
h.ereignis(Ereignis::Signal(1));
st.pump(&mut h, false);
assert!(st.abbruch);
}
#[test]
fn mausereignisse_haeufen_sich_nicht_an() {
use crate::host::{Ereignis, MausArt, MausEreignis};
let mut st = RtState::default();
let mut h = crate::host::CaptureHost::default();
for i in 0..500 {
h.ereignis(Ereignis::Maus(MausEreignis {
art: MausArt::Bewegung,
taste: 0,
shift: 0,
zeile: 1 + i % 20,
spalte: 1 + i % 70,
}));
}
st.pump(&mut h, false);
assert!(st.tasten.is_empty(), "Maus landet nicht im Tastenpuffer");
}
#[test]
fn getrappte_taste_verlaesst_den_eingabestrom() {
use crate::host::{taste, CaptureHost, Ereignis};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = CaptureHost::default();
st.traps.definieren(Quelle::Key(1), Some(77));
st.traps.setzen(Quelle::Key(1), Zustand::An, 0);
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0)); // F1
st.pump(&mut h, false);
st.traps_pruefen(&mut h);
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(1), 77)));
assert_eq!(st.naechste_taste(&mut h, false), None, "F1 ist weg");
}
#[test]
fn nicht_getrappte_taste_bleibt_im_strom() {
use crate::host::{taste, CaptureHost, Ereignis};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = CaptureHost::default();
st.traps.definieren(Quelle::Key(1), Some(77));
st.traps.setzen(Quelle::Key(1), Zustand::Aus, 0);
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0));
st.pump(&mut h, false);
st.traps_pruefen(&mut h);
assert_eq!(st.traps.naechstes(), None);
assert_eq!(
st.naechste_taste(&mut h, false),
Some(taste::sonder(0x3B)),
"bei OFF bleibt F1 lesbar"
);
}
#[test]
fn off_wirkt_auf_bereits_wartende_tasten() {
use crate::host::{taste, CaptureHost, Ereignis};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = CaptureHost::default();
st.traps.definieren(Quelle::Key(1), Some(77));
st.traps.setzen(Quelle::Key(1), Zustand::An, 0);
h.ereignis(Ereignis::Taste(taste::sonder(0x3B), 0));
st.pump(&mut h, false); // Taste liegt in der Schlange
st.traps.setzen(Quelle::Key(1), Zustand::Aus, 0);
st.traps_pruefen(&mut h);
assert_eq!(
st.naechste_taste(&mut h, false),
Some(taste::sonder(0x3B)),
"das OFF wirkt auch auf schon Wartendes (design.md D5)"
);
}
#[test]
fn benutzertaste_ueber_key_anweisung() {
use crate::host::{umschalt, CaptureHost};
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = CaptureHost::default();
st.traps.definieren(Quelle::Key(15), Some(88));
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
// KEY 15, CHR$(4) + CHR$(30) — Strg+A
let def = format!("{}{}", 4u8 as char, 0x1Eu8 as char);
bi_key_assign(
&mut st,
&mut h,
&mut [Value::Int(15), Value::Str(def.into())],
)
.unwrap();
h.taste_mit("a", umschalt::CTRL);
st.pump(&mut h, false);
st.traps_pruefen(&mut h);
assert_eq!(st.traps.naechstes(), Some((Quelle::Key(15), 88)));
assert_eq!(st.naechste_taste(&mut h, false), None);
}
#[test]
fn benutzertaste_ohne_umschalt_bleibt_im_strom() {
use crate::host::CaptureHost;
use crate::traps::{Quelle, Zustand};
let mut st = RtState::default();
let mut h = CaptureHost::default();
st.traps.definieren(Quelle::Key(15), Some(88));
st.traps.setzen(Quelle::Key(15), Zustand::An, 0);
let def = format!("{}{}", 4u8 as char, 0x1Eu8 as char);
bi_key_assign(
&mut st,
&mut h,
&mut [Value::Int(15), Value::Str(def.into())],
)
.unwrap();
h.taste_mit("a", 0); // ohne Strg
st.pump(&mut h, false);
st.traps_pruefen(&mut h);
assert_eq!(st.traps.naechstes(), None);
assert_eq!(st.naechste_taste(&mut h, false), Some("a".to_string()));
}
}