2906 lines
82 KiB
Rust
2906 lines
82 KiB
Rust
//! 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: 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<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 8–23
|
||
/// 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 0–255 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 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<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 0–11.
|
||
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)?;
|
||
// 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<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()));
|
||
}
|
||
}
|