//! 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, /// Hostreihenfolge bis zur Annahme durch Trap, Formular oder Konsole. pub eingaben: VecDeque, /// Bereits angenommener Rest einer von INPUT$ geteilten Taste. tastenrest: String, /// Abbruchwunsch (Strg+Untbr) wurde gemeldet. pub abbruch: bool, /// Eingabeende wurde gemeldet. pub ende: bool, /// Funktionstasten-Makros: F1–F10, 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, 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; /// bei fehlgeschlagener Auflösung gilt für diese Abfrage UTC. pub zeitzone: dt::Zeitzone, /// Auflösung der Systemzone; durch Einbetter auch ohne Systemänderung ersetzbar. pub zonen_ermittler: fn(i64) -> Option, rng: u32, rnd_last: f32, pub command: String, pub clipboard: String, } /// Eine noch nicht verbrauchte Eingabe. Unter KEY STOP wird der Trap /// einmal gemerkt; die Taste bleibt danach unabhängig vom Trap lesbar. #[derive(Debug)] pub struct Eingabe { pub ereignis: Ereignis, trap_gemerkt: bool, } impl From for Eingabe { fn from(ereignis: Ereignis) -> Self { Self { ereignis, trap_gemerkt: false, } } } impl Default for RtState { fn default() -> Self { RtState { screen: TextScreen::new(), dateien: Default::default(), isam: Default::default(), print: PrintState::default(), eingaben: VecDeque::new(), tastenrest: String::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::System, zonen_ermittler: dt::utc_versatz, // 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 { /// Ortszeit und Gültigkeit stammen aus derselben aktuellen Zonenauflösung. pub fn zeitpunkt(&self) -> (i64, bool) { let utc = dt::jetzt_utc(); let zone = match self.zeitzone { dt::Zeitzone::System => (self.zonen_ermittler)(utc), dt::Zeitzone::Fest(n) => Some(n), dt::Zeitzone::Unbekannt => None, }; ( utc + i64::from(zone.unwrap_or(0)) + self.uhr_offset, zone.is_some(), ) } fn jetzt_serial(&self) -> f64 { self.zeitpunkt().0 as f64 / 86_400.0 + dt::serial_aus_ymd(1970, 1, 1) } /// Höchstens n Codepoints annehmen, übrige Zeichen vor der nächsten Taste lesen. pub fn tastenanteil(&mut self, mut taste: String, n: usize) -> String { if let Some((ende, _)) = taste.char_indices().nth(n) { self.tastenrest = taste.split_off(ende); } taste } /// 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; }; self.ereignis(e); if blockierend { return; } } } pub fn ereignis(&mut self, e: Ereignis) { match e { Ereignis::Taste(..) | Ereignis::Maus(_) => self.eingaben.push_back(e.into()), Ereignis::Groesse { cols, rows } => self.screen.resize(cols, rows), Ereignis::Abbruch => self.abbruch = true, Ereignis::Ende => self.ende = true, Ereignis::Signal(n) => { let gemeldet = self.traps.melden(crate::traps::Quelle::Signal(n)); if n == 1 && !gemeldet { self.abbruch = true; } } } } /// 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; } if self.traps.naechste_frist().is_some() { self.traps.zeit_pruefen(host.jetzt_ms()); } 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) { while self.taste_trappen(0) {} } /// Prüft eine wartende Taste genau bis zu ihrer Annahme. ON verbraucht /// sie, STOP merkt den Trap einmal und belässt sie im Eingabestrom. pub fn taste_trappen(&mut self, index: usize) -> bool { if !self.traps.aktiv() { return false; } let Some(eingabe) = self.eingaben.get_mut(index) else { return false; }; let Ereignis::Taste(s, shift) = &eingabe.ereignis else { return false; }; if eingabe.trap_gemerkt { return false; } let Some(code) = crate::host::taste::scancode_von(s) else { return false; }; let Some(n) = self.traps.taste_faengt(code, *shift) else { return false; }; self.traps.melden(crate::traps::Quelle::Key(n)); if self.traps.ist_an(crate::traps::Quelle::Key(n)) { self.eingaben.remove(index); true } else { eingabe.trap_gemerkt = true; false } } /// Nur bereits gepufferte Tasten annehmen, ohne weitere Hostereignisse /// abzuholen. Die VM kann zwischen Abholen und Blockieren zustellen. pub fn gepufferte_taste(&mut self) -> Option { if !self.tastenrest.is_empty() { return Some(std::mem::take(&mut self.tastenrest)); } while let Some(i) = self .eingaben .iter() .position(|e| matches!(e.ereignis, Ereignis::Taste(..))) { if self.taste_trappen(i) { continue; } if let Ereignis::Taste(t, _) = self.eingaben.remove(i).unwrap().ereignis { return Some(t); } } None } /// Nächster Tastendruck; auch während blockierender Eingabe gelten /// ON/OFF/STOP. Mausereignisse bleiben dem VM-Zustellpunkt vorbehalten. pub fn naechste_taste(&mut self, host: &mut dyn Host, blockierend: bool) -> Option { let mut abgeholt = false; loop { if let Some(t) = self.gepufferte_taste() { return Some(t); } if self.ende || self.abbruch || (!blockierend && abgeholt) { return None; } self.pump(host, blockierend); abgeholt = true; } } /// 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 { 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:> 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 { 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, 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 TIMEZONEKNOWN: u16 = 156; pub const COUNT: u16 = 157; } /// 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, bi_timezoneknown, ]; debug_assert_eq!(TABLE.len(), ids::COUNT as usize); TABLE } fn bi_clipboard_add( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { st.clipboard.push_str(&arg_str(a, 0)?); Ok(None) } fn bi_clipboard_get( st: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, RuntimeError> { Ok(Some(Value::Str(Rc::from(st.clipboard.as_str())))) } // ---- Argument-Hilfen -------------------------------------------------------- fn arg_str(args: &[Value], i: usize) -> Result, RuntimeError> { match args.get(i) { Some(Value::Str(s)) => Ok(s.clone()), _ => Err(RuntimeError::TYPE_MISMATCH), } } fn arg_i32(args: &[Value], i: usize) -> Result { 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 { match args.get(i) { Some(v) => Ok(as_f64(v)), None => Err(RuntimeError::ILLEGAL_FUNCTION_CALL), } } fn s_ok(s: String) -> Result, RuntimeError> { Ok(Some(Value::Str(Rc::from(s.as_str())))) } // ---- Strings ---------------------------------------------------------------- fn bi_len( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, 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, 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, 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, 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 = 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, RuntimeError> { let s = arg_str(a, 0)?; s_ok(s.to_uppercase()) } fn bi_lcase( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { let s = arg_str(a, 0)?; s_ok(s.to_lowercase()) } fn bi_ltrim( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 = target.chars().collect(); let rchars: Vec = 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, 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, 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, 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, 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, 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, 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, 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, RuntimeError> { Ok(Some(Value::Dbl(arg_f64(a, 0)?.sin()))) } fn bi_cos( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { Ok(Some(Value::Dbl(arg_f64(a, 0)?.cos()))) } fn bi_tan( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { Ok(Some(Value::Dbl(arg_f64(a, 0)?.tan()))) } fn bi_atn( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { Ok(Some(Value::Dbl(arg_f64(a, 0)?.atan()))) } fn bi_rnd( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, 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, 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, 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, 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, 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, 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, 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, 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, RuntimeError> { Ok(Some(Value::Lng(FRE_ERSATZ))) } fn bi_clear( st: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, 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, RuntimeError> { st.trace = true; Ok(None) } fn bi_troff( st: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, RuntimeError> { st.trace = false; Ok(None) } fn bi_stack_fn( _: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, RuntimeError> { Ok(Some(Value::Lng(STACK_ERSATZ))) } fn bi_stack_stmt( _: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, 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, 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, RuntimeError> { s_ok(String::new()) } // ---- Datei-E/A -------------------------------------------------------------------- fn bi_open( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, 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, 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, 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, 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, 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, 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, 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, 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, RuntimeError> { let pos = arg_i32(a, 1)?; if pos < 1 { return Err(RuntimeError::ILLEGAL_FUNCTION_CALL); } st.dateien .get(arg_i32(a, 0)?)? .position_setzen(pos as u64)?; Ok(None) } fn bi_freefile( st: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, 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, 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, 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, RuntimeError> { fio::loeschen(&arg_str(a, 0)?).map(|_| None) } fn bi_name( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, 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, RuntimeError> { fio::verzeichnis_wechseln(&arg_str(a, 0)?).map(|_| None) } fn bi_chdrive( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, RuntimeError> { fio::verzeichnis_anlegen(&arg_str(a, 0)?).map(|_| None) } fn bi_rmdir( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { fio::verzeichnis_entfernen(&arg_str(a, 0)?).map(|_| None) } fn bi_curdir( _: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, RuntimeError> { s_ok(fio::aktuelles_verzeichnis()) } fn bi_dir( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, 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))) } fn bi_shell_stmt( _: &mut RtState, host: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { let command = match a.first() { Some(Value::Str(s)) => s.as_ref(), _ => "", }; host.shell(command).map(|_| None) } fn bi_shell_fn( _: &mut RtState, host: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { host.shell(&arg_str(a, 0)?).map(|code| code.map(Value::Lng)) } /// `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, RuntimeError> { let art = arg_i32(a, 1)?; let bytes: Vec = 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 0–255 ablegen (verlustfrei umkehrbar). s_ok(bytes.iter().map(|b| *b as char).collect::()) } /// `CVI`/`CVL`/`CVS`/`CVD`/`CVC` — Bytedarstellung zurück in eine Zahl. fn bi_cv( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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 = text .chars() .map(|c| u8::try_from(c as u32).map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL)) .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, 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, 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, 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, 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, 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, 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, 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, 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 0–31 (16–31 = blinkend, als hell simuliert), Hintergrund 0–7. 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, 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, 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, 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, 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, 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, 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, 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 0–11. fn key_index(n: i32) -> Option { 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, RuntimeError> { let n = arg_i32(a, 0)?; let text = arg_str(a, 1)?; // 15–25 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, 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, 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, RuntimeError> { Ok(Some(Value::Int(st.screen.csrlin() as i16))) } fn bi_pos( st: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, RuntimeError> { Ok(Some(Value::Int(st.screen.pos() as i16))) } fn bi_screen_fn( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, 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, 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)?; return s_ok(datei.zeichen_lesen(n as usize)?); } // Blockierend, ohne Echo auf dem Bildschirm. let mut s = String::new(); let mut rest = n as usize; while rest > 0 { host.present(&st.screen); let Some(t) = st.naechste_taste(host, true) else { return Err(RuntimeError(62)); // Input past end of file }; let t = st.tastenanteil(t, rest); rest -= t.chars().count(); s.push_str(&t); } Ok(Some(Value::Str(Rc::from(s.as_str())))) } // ---- Sonstiges ------------------------------------------------------------------ fn bi_timezoneknown( st: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, RuntimeError> { Ok(Some(Value::Int(if st.zeitpunkt().1 { -1 } else { 0 }))) } fn bi_timer( st: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, 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 = st.zeitpunkt().0.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, RuntimeError> { let secs = st.zeitpunkt().0; 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, RuntimeError> { let s = st.zeitpunkt().0.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, 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 = st.jetzt_serial(); 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, RuntimeError> { let t = arg_str(a, 0)?; let ziel = dt::timevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?; let jetzt = st.jetzt_serial(); 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, RuntimeError> { Ok(Some(Value::Dbl(st.jetzt_serial()))) } fn bi_date_serial( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, 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, 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, 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, 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, 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, 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, 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, RuntimeError> { dt_teil(a, |s| dt::weekday(s) as i32) } fn bi_hour( _: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, 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, 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, RuntimeError> { s_ok(st.command.clone()) } fn bi_doevents( _: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, 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, RuntimeError> { let secs = if a.is_empty() { 0.0 } else { arg_f64(a, 0)? }; // Der Bytecode nutzt Instr::Sleep für Handlerzustellung. Auch direkte // Bibliotheksaufrufe dürfen keine eigene reale Uhr verwenden. let deadline = (secs > 0.0).then(|| host.jetzt_ms().saturating_add((secs * 1000.0) as u64)); st.tick(host); loop { if st.ende || st.abbruch { break; } if let Some(deadline) = deadline { if host.jetzt_ms() >= deadline { break; } } else if st.naechste_taste(host, false).is_some() { break; } if let Some(event) = host.warten(deadline) { st.ereignis(event); } } Ok(None) } fn bi_setuevent( st: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, RuntimeError> { isam_bewegen(st, a, crate::isam::Richtung::Erster) } fn bi_isam_move_last( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { isam_bewegen(st, a, crate::isam::Richtung::Letzter) } fn bi_isam_move_next( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { isam_bewegen(st, a, crate::isam::Richtung::Naechster) } fn bi_isam_move_previous( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { isam_bewegen(st, a, crate::isam::Richtung::Voriger) } fn isam_suchen( st: &mut RtState, a: &[Value], art: crate::isam::Suchart, ) -> Result, 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, RuntimeError> { isam_suchen(st, a, crate::isam::Suchart::Gleich) } fn bi_isam_seek_gt( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { isam_suchen(st, a, crate::isam::Suchart::Groesser) } fn bi_isam_seek_ge( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, RuntimeError> { isam_suchen(st, a, crate::isam::Suchart::GroesserGleich) } fn bi_isam_begin_trans( st: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, RuntimeError> { st.isam.trans_beginn().map(|_| None) } fn bi_isam_commit_trans( st: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, RuntimeError> { st.isam.trans_festschreiben().map(|_| None) } fn bi_isam_rollback( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, RuntimeError> { st.isam.sicherungspunkt().map(|k| Some(Value::Int(k))) } fn bi_isam_setmem( st: &mut RtState, _: &mut dyn Host, a: &mut [Value], ) -> Result, 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, 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, RuntimeError> { Err(RuntimeError::FEATURE_UNAVAILABLE) } fn bi_inputbox( _: &mut RtState, _: &mut dyn Host, _: &mut [Value], ) -> Result, RuntimeError> { Err(RuntimeError::FEATURE_UNAVAILABLE) } #[cfg(test)] mod tests { use super::*; use crate::host::CaptureHost; fn call(id: u16, args: Vec) -> Result, 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 input_s_dispatch_bewahrt_codepointreste_auch_ohne_vm() { let mut st = RtState::default(); let mut host = CaptureHost::default(); host.ereignis(Ereignis::Taste("\0;".into(), 0)); host.ereignis(Ereignis::Taste("ä😀".into(), 0)); for (n, expected) in [(0, ""), (1, "\0"), (2, ";ä"), (1, "😀")] { let got = bi_input_s(&mut st, &mut host, &mut [Value::Int(n)]).unwrap(); assert!(matches!(got, Some(Value::Str(s)) if &*s == expected)); } host.ereignis(Ereignis::Ende); assert_eq!( bi_input_s(&mut st, &mut host, &mut [Value::Int(1)]).unwrap_err(), RuntimeError(62) ); assert_eq!( bi_input_s(&mut st, &mut host, &mut [Value::Int(-1)]).unwrap_err(), RuntimeError(5) ); } #[test] fn zonenquelle_ausfall_und_erholung_gelten_fuer_alle_zeitfunktionen() { thread_local! { static ZONE: std::cell::Cell> = const { std::cell::Cell::new(None) }; } let mut st = RtState { zonen_ermittler: |_| ZONE.get(), uhr_offset: 4321, ..RtState::default() }; let mut host = CaptureHost::default(); for zone in [None, Some(7200), None, Some(0)] { ZONE.set(zone); let before = dt::jetzt_utc(); let (time, known) = st.zeitpunkt(); assert_eq!(known, zone.is_some()); let shift = i64::from(zone.unwrap_or(0)) + st.uhr_offset; assert!((before + shift..=dt::jetzt_utc() + shift).contains(&time)); assert!( matches!(bi_timezoneknown(&mut st, &mut host, &mut []).unwrap(), Some(Value::Int(n)) if n == if known { -1 } else { 0 }) ); let Some(Value::Dbl(now)) = bi_now(&mut st, &mut host, &mut []).unwrap() else { panic!() }; let seconds = ((now - dt::serial_aus_ymd(1970, 1, 1)) * 86_400.0).round() as i64; assert!((before + shift..=dt::jetzt_utc() + shift).contains(&seconds)); let Some(Value::Str(date)) = bi_date(&mut st, &mut host, &mut []).unwrap() else { panic!() }; let Some(Value::Str(clock)) = bi_time(&mut st, &mut host, &mut []).unwrap() else { panic!() }; let date_time = ((dt::datevalue(&date).unwrap() + dt::timevalue(&clock).unwrap() - dt::serial_aus_ymd(1970, 1, 1)) * 86_400.0) .round() as i64; assert!((before + shift..=dt::jetzt_utc() + shift).contains(&date_time)); let Some(Value::Sng(timer)) = bi_timer(&mut st, &mut host, &mut []).unwrap() else { panic!() }; assert!((timer as f64 - seconds.rem_euclid(86_400) as f64).abs() < 2.0); assert_eq!(st.uhr_offset, 4321); } } #[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 signal_stop_merkt_beide_quellen_und_off_verwirft() { use crate::host::{CaptureHost, Ereignis}; use crate::traps::{Quelle, Zustand}; for n in [1, 2] { let mut st = RtState::default(); let mut host = CaptureHost::default(); st.traps.definieren(Quelle::Signal(n), Some(22)); st.traps.setzen(Quelle::Signal(n), Zustand::Gestoppt, 0); for _ in 0..3 { host.ereignis(Ereignis::Signal(n)); } st.pump(&mut host, false); assert!(!st.abbruch); assert_eq!(st.traps.naechstes(), None); st.traps.setzen(Quelle::Signal(n), Zustand::An, 0); assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(n), 22))); st.traps.handler_beendet(Quelle::Signal(n)); assert_eq!(st.traps.naechstes(), None); st.traps.setzen(Quelle::Signal(n), Zustand::Gestoppt, 0); host.ereignis(Ereignis::Signal(n)); st.pump(&mut host, false); st.traps.setzen(Quelle::Signal(n), Zustand::Aus, 0); st.traps.setzen(Quelle::Signal(n), Zustand::An, 0); assert_eq!(st.traps.naechstes(), None); } } #[test] fn key_stop_merkt_einmal_und_laesst_alle_tasten_auch_nach_on_lesbar() { use crate::host::{CaptureHost, Ereignis}; use crate::traps::{Quelle, Zustand}; let mut st = RtState::default(); let mut host = CaptureHost::default(); st.traps.definieren(Quelle::Key(15), Some(22)); st.traps.benutzertaste(15, 0, 30); st.traps.setzen(Quelle::Key(15), Zustand::Gestoppt, 0); for _ in 0..3 { host.ereignis(Ereignis::Taste("a".into(), 0)); } st.pump(&mut host, false); for i in 0..3 { assert!(!st.taste_trappen(i)); } assert_eq!(st.naechste_taste(&mut host, false).as_deref(), Some("a")); st.traps.setzen(Quelle::Key(15), Zustand::An, 0); assert_eq!(st.traps.naechstes(), Some((Quelle::Key(15), 22))); st.traps.handler_beendet(Quelle::Key(15)); for _ in 0..2 { assert_eq!(st.naechste_taste(&mut host, false).as_deref(), Some("a")); } assert_eq!(st.traps.naechstes(), 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 .eingaben .iter() .all(|e| matches!(e.ereignis, Ereignis::Maus(_)))); } #[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())); } }