Laufzeit-Eingabe und Zonenzustand korrigieren und Change archivieren

This commit is contained in:
2026-09-06 08:04:22 +02:00
parent 815825dde7
commit c8b92f0619
36 changed files with 1104 additions and 91 deletions

View File

@@ -405,7 +405,7 @@ fn run_chain(
}
vm.rt.command.clone_from(&command);
vm.set_poll_interrupt(true);
if vm.rt.zeitzone == tb_runtime::datetime::Zeitzone::Unbekannt {
if !vm.rt.zeitpunkt().1 {
eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
}
let event = vm.run(host);

View File

@@ -63,11 +63,15 @@ fn screen_groessen(name: &str, src: &str) -> Vec<(usize, usize)> {
wert.split(',').map(|w| groesse_lesen(name, w)).collect()
}
/// Optionale Zeitzone `' tb-tz: ±HH:MM`. Ohne Angabe gilt die Zone des
/// Optionale Zeitzone `' tb-tz: ±HH:MM` oder `unknown` für UTC-Rückfall.
/// Ohne Angabe gilt die Zone des
/// Rechners — Zeitprogramme im Korpus setzen sie, damit ihre Sollausgabe
/// unabhängig vom Standort des Testrechners bleibt.
fn tz_direktive(name: &str, src: &str) -> Option<tb_runtime::datetime::Zeitzone> {
let wert = direktive(src, "tb-tz:")?;
if wert == "unknown" {
return Some(tb_runtime::datetime::Zeitzone::Unbekannt);
}
let (vorzeichen, rest) = match wert.strip_prefix('-') {
Some(r) => (-1i32, r),
None => (1i32, wert.trim_start_matches('+')),

View File

@@ -340,6 +340,7 @@ pub enum Builtin {
DateSet,
TimeSet,
Now,
TimezoneKnown,
DateSerial,
TimeSerial,
DateValue,

View File

@@ -243,6 +243,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> {
"DATE$" | "TIME$" => (0, 0, &[], St),
"TIMER" => (0, 0, &[], Sg),
"NOW" => (0, 0, &[], Db),
"TIMEZONEKNOWN" => (0, 0, &[], I),
"DATESERIAL" | "TIMESERIAL" => (3, 3, &[N, N, N], Db),
"DATEVALUE" | "TIMEVALUE" => (1, 1, &[S], Db),
"DAY" | "MONTH" | "YEAR" | "WEEKDAY" | "HOUR" | "MINUTE" | "SECOND" => (1, 1, &[N], I),
@@ -5374,6 +5375,7 @@ impl Sema {
b(bt, a, Ty::Dbl, self)
}
"NOW" => b(Builtin::Now, vec![], Ty::Dbl, self),
"TIMEZONEKNOWN" => b(Builtin::TimezoneKnown, vec![], Ty::Int, self),
"DATESERIAL" | "TIMESERIAL" => {
let x = conv_arg!(0, NumTy::Lng);
let y = conv_arg!(1, NumTy::Lng);
@@ -5663,6 +5665,19 @@ mod tests {
assert!(diags("PRINT CHR$(\"x\")").contains(&"Type mismatch".to_string()));
assert!(diags("x$ = INKEY$").is_empty());
assert!(diags("t! = TIMER").is_empty());
assert!(matches!(
super::builtin_fn("TIMEZONEKNOWN"),
Some((0, 0, [], super::RetK::I))
));
assert!(diags("PRINT TIMEZONEKNOWN").is_empty());
for name in ["TIMEZONEKNOWN(1)", "MKL$()"] {
let d = diags(&format!("PRINT {name}"));
assert!(
d.iter().any(|m| m.contains("Argument-count mismatch")
&& m.contains(name.split('(').next().unwrap())),
"{d:?}"
);
}
}
#[test]

View File

@@ -30,6 +30,8 @@ pub struct RtState {
pub print: PrintState,
/// Hostreihenfolge bis zur Annahme durch Trap, Formular oder Konsole.
pub eingaben: VecDeque<Eingabe>,
/// Bereits angenommener Rest einer von INPUT$ geteilten Taste.
tastenrest: String,
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
pub abbruch: bool,
/// Eingabeende wurde gemeldet.
@@ -52,8 +54,10 @@ pub struct RtState {
/// 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.
/// 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<i32>,
rng: u32,
rnd_last: f32,
pub command: String,
@@ -85,6 +89,7 @@ impl Default for RtState {
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
@@ -97,7 +102,8 @@ impl Default for RtState {
dir_treffer: Vec::new(),
dir_index: 0,
uhr_offset: 0,
zeitzone: dt::Zeitzone::ermitteln(),
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").
@@ -110,6 +116,32 @@ impl Default for RtState {
}
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`.
@@ -207,6 +239,9 @@ impl RtState {
/// Nur bereits gepufferte Tasten annehmen, ohne weitere Hostereignisse
/// abzuholen. Die VM kann zwischen Abholen und Blockieren zustellen.
pub fn gepufferte_taste(&mut self) -> Option<String> {
if !self.tastenrest.is_empty() {
return Some(std::mem::take(&mut self.tastenrest));
}
while let Some(i) = self
.eingaben
.iter()
@@ -496,7 +531,8 @@ pub mod ids {
pub const GRAPHICS_LINE: u16 = 153;
pub const GRAPHICS_PAINT: u16 = 154;
pub const GRAPHICS_VIEW: u16 = 155;
pub const COUNT: u16 = 156;
pub const TIMEZONEKNOWN: u16 = 156;
pub const COUNT: u16 = 157;
}
/// Dispatch-Tabelle in Index-Reihenfolge.
@@ -659,6 +695,7 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
bi_graphics_line,
bi_graphics_paint,
bi_graphics_view,
bi_timezoneknown,
];
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
TABLE
@@ -1470,7 +1507,9 @@ fn bi_seek_stmt(
if pos < 1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
st.dateien.get(arg_i32(a, 0)?)?.position = pos as u64;
st.dateien
.get(arg_i32(a, 0)?)?
.position_setzen(pos as u64)?;
Ok(None)
}
@@ -1706,9 +1745,9 @@ fn bi_cv(
};
let bytes: Vec<u8> = text
.chars()
.map(|c| u8::try_from(c as u32).unwrap_or(0))
.collect();
if bytes.len() < breite {
.map(|c| u8::try_from(c as u32).map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL))
.collect::<Result<_, _>>()?;
if bytes.len() != breite {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(Some(match art {
@@ -2180,18 +2219,18 @@ fn bi_input_s(
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(),
))));
return s_ok(datei.zeichen_lesen(n as usize)?);
}
// Blockierend, ohne Echo auf dem Bildschirm.
let mut s = String::new();
for _ in 0..n {
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()))))
@@ -2199,6 +2238,14 @@ fn bi_input_s(
// ---- Sonstiges ------------------------------------------------------------------
fn bi_timezoneknown(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Int(if st.zeitpunkt().1 { -1 } else { 0 })))
}
fn bi_timer(
st: &mut RtState,
_: &mut dyn Host,
@@ -2209,7 +2256,7 @@ fn bi_timer(
.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);
let ganze = st.zeitpunkt().0.rem_euclid(86_400);
Ok(Some(Value::Sng(ganze as f32 + bruchteil as f32)))
}
@@ -2218,7 +2265,7 @@ fn bi_date(
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let secs = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset);
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}"))
}
@@ -2228,7 +2275,7 @@ fn bi_time(
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let s = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
let s = st.zeitpunkt().0.rem_euclid(86_400);
s_ok(format!(
"{:02}:{:02}:{:02}",
s / 3600,
@@ -2245,7 +2292,7 @@ fn bi_date_set(
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 jetzt = st.jetzt_serial();
let diff_tage = ziel.floor() - jetzt.floor();
st.uhr_offset += (diff_tage * 86_400.0) as i64;
Ok(None)
@@ -2258,7 +2305,7 @@ fn bi_time_set(
) -> 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 jetzt = st.jetzt_serial();
let diff = ziel - (jetzt - jetzt.floor());
st.uhr_offset += (diff * 86_400.0).round() as i64;
Ok(None)
@@ -2269,10 +2316,7 @@ fn bi_now(
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(dt::jetzt_serial(
st.zeitzone,
st.uhr_offset,
))))
Ok(Some(Value::Dbl(st.jetzt_serial())))
}
fn bi_date_serial(
@@ -2762,6 +2806,72 @@ mod tests {
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<Option<i32>> = 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

View File

@@ -132,8 +132,8 @@ pub enum Zeitzone {
impl Zeitzone {
/// Versatz zu einem Zeitpunkt. Bei `System` ohne ermittelbare Zone
/// fällt der Wert auf 0 (UTC) zurück — die Rückfallebene ist über
/// [`Zeitzone::ermitteln`] erkennbar.
/// fällt der Wert auf 0 (UTC) zurück. BASIC und Einbetter verwenden
/// [`crate::builtins::RtState::zeitpunkt`] für Zeit und aktuellen Status.
pub fn versatz(&self, zeitpunkt: i64) -> i32 {
match self {
Zeitzone::System => utc_versatz(zeitpunkt).unwrap_or(0),
@@ -142,8 +142,9 @@ impl Zeitzone {
}
}
/// Einmal beim Start: `System`, wenn die Zone lesbar ist, sonst
/// `Unbekannt`. Damit ist der Rückfall auf UTC sichtbar statt still.
/// Momentaufnahme: `System`, wenn die Zone lesbar ist, sonst `Unbekannt`.
/// Die Laufzeit behält `System` als Quelle, damit sie nach Ausfällen
/// erneut auflösen kann; `RtState::zeitpunkt` liefert den aktuellen Status.
pub fn ermitteln() -> Zeitzone {
match utc_versatz(jetzt_utc()) {
Some(_) => Zeitzone::System,

View File

@@ -52,7 +52,7 @@ pub struct Feld {
pub struct Datei {
pub modus: Modus,
pub pfad: PathBuf,
datei: File,
datei: BufReader<File>,
/// Recordlänge (`RANDOM`); bei `BINARY` immer 1.
pub reclen: usize,
/// Recordpuffer für `FIELD`/`GET`/`PUT`.
@@ -61,8 +61,8 @@ pub struct Datei {
/// Nächste Position: Datensatz bei `RANDOM`, Byte bei `BINARY`,
/// jeweils 1-basiert.
pub position: u64,
/// Zeilenpuffer für sequenzielles Lesen.
leser: Option<BufReader<File>>,
/// Ein Komma kann auch vor EOF noch ein leeres Feld eröffnen.
leeres_feld: bool,
}
impl Datei {
@@ -82,11 +82,7 @@ impl Datei {
opt.read(true).write(true).create(true);
}
}
let datei = opt.open(pfad).map_err(fehler_aus_io)?;
let leser = match modus {
Modus::Input => Some(BufReader::new(File::open(pfad).map_err(fehler_aus_io)?)),
_ => None,
};
let datei = BufReader::new(opt.open(pfad).map_err(|e| fehler_am_pfad(e, pfad))?);
let reclen = if modus == Modus::Random {
reclen.max(1)
} else {
@@ -100,13 +96,14 @@ impl Datei {
puffer: vec![b' '; reclen],
felder: Vec::new(),
position: 1,
leser,
leeres_feld: false,
})
}
/// Dateigröße in Bytes.
pub fn laenge(&self) -> Result<u64, RuntimeError> {
self.datei
.get_ref()
.metadata()
.map(|m| m.len())
.map_err(fehler_aus_io)
@@ -117,8 +114,7 @@ impl Datei {
pub fn eof(&mut self) -> Result<bool, RuntimeError> {
match self.modus {
Modus::Input => {
let l = self.leser.as_mut().ok_or(RuntimeError(52))?;
Ok(l.fill_buf().map_err(fehler_aus_io)?.is_empty())
Ok(!self.leeres_feld && self.datei.fill_buf().map_err(fehler_aus_io)?.is_empty())
}
Modus::Random => Ok((self.position - 1) * self.reclen as u64 >= self.laenge()?),
Modus::Binary => Ok(self.position > self.laenge()?),
@@ -132,24 +128,143 @@ impl Datei {
if !matches!(self.modus, Modus::Output | Modus::Append) {
return Err(RuntimeError(54)); // Bad file mode
}
self.datei.write_all(text.as_bytes()).map_err(fehler_aus_io)
self.datei
.get_mut()
.write_all(text.as_bytes())
.map_err(fehler_aus_io)
}
/// Eine Zeile lesen (ohne Zeilenende); `None` = Dateiende.
/// `SEEK`: auch den Lesepuffer verwerfen; Position bleibt 1-basiert.
pub fn position_setzen(&mut self, pos: u64) -> Result<(), RuntimeError> {
if pos == 0 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
if self.modus == Modus::Input {
self.datei
.seek(SeekFrom::Start(pos - 1))
.map_err(fehler_aus_io)?;
}
self.position = pos;
self.leeres_feld = false;
Ok(())
}
fn byte_lesen(&mut self) -> Result<Option<u8>, RuntimeError> {
let mut b = [0];
if self.datei.read(&mut b).map_err(fehler_aus_io)? == 0 {
return Ok(None);
}
self.position += 1;
self.leeres_feld = false;
Ok(Some(b[0]))
}
fn crlf_beenden(&mut self, b: u8) -> Result<(), RuntimeError> {
if b == b'\r' && self.datei.fill_buf().map_err(fehler_aus_io)?.first() == Some(&b'\n') {
self.byte_lesen()?;
}
Ok(())
}
/// Eine Zeile ab dem gemeinsamen Cursor, ohne genau ein Zeilenende.
pub fn zeile_lesen(&mut self) -> Result<Option<String>, RuntimeError> {
if self.modus != Modus::Input {
return Err(RuntimeError(54));
}
let l = self.leser.as_mut().ok_or(RuntimeError(52))?;
let mut roh = Vec::new();
let n = l.read_until(b'\n', &mut roh).map_err(fehler_aus_io)?;
if n == 0 {
let mut gelesen = false;
self.leeres_feld = false;
while let Some(b) = self.byte_lesen()? {
gelesen = true;
if matches!(b, b'\r' | b'\n') {
self.crlf_beenden(b)?;
break;
}
roh.push(b);
}
if !gelesen {
return Ok(None);
}
while roh.last() == Some(&b'\n') || roh.last() == Some(&b'\r') {
roh.pop();
String::from_utf8(roh)
.map(Some)
.map_err(|_| RuntimeError(57))
}
/// Nächstes INPUT-#-Feld, ohne nachfolgende Felder oder Zeilen zu verbrauchen.
pub fn feld_lesen(&mut self) -> Result<String, RuntimeError> {
if self.modus != Modus::Input {
return Err(RuntimeError(54));
}
Ok(Some(String::from_utf8_lossy(&roh).into_owned()))
let mut roh = Vec::new();
let mut in_quote = false;
let mut hatte_quote = false;
let mut gelesen = std::mem::take(&mut self.leeres_feld);
while let Some(b) = self.byte_lesen()? {
gelesen = true;
match b {
b'"' if !hatte_quote && roh.iter().all(u8::is_ascii_whitespace) => {
roh.clear();
in_quote = true;
hatte_quote = true;
}
b'"' if in_quote => {
if self.datei.fill_buf().map_err(fehler_aus_io)?.first() == Some(&b'"') {
self.byte_lesen()?;
roh.push(b'"');
} else {
in_quote = false;
}
}
b',' if !in_quote => {
self.leeres_feld = true;
break;
}
b'\r' | b'\n' if !in_quote => {
self.crlf_beenden(b)?;
break;
}
b if hatte_quote && !in_quote && b.is_ascii_whitespace() => {}
_ => roh.push(b),
}
}
if !gelesen || in_quote {
return Err(RuntimeError(62));
}
let text = String::from_utf8(roh).map_err(|_| RuntimeError(57))?;
Ok(if hatte_quote {
text
} else {
text.trim().to_owned()
})
}
/// INPUT$: exakt n UTF-8-Codepoints; beschädigte Daten sind Fehler 57.
pub fn zeichen_lesen(&mut self, n: usize) -> Result<String, RuntimeError> {
if !matches!(self.modus, Modus::Input | Modus::Binary) {
return Err(RuntimeError(54));
}
if self.modus == Modus::Binary {
self.datei
.seek(SeekFrom::Start(self.position - 1))
.map_err(fehler_aus_io)?;
}
let mut text = String::new();
for _ in 0..n {
let mut bytes = [0; 4];
bytes[0] = self.byte_lesen()?.ok_or(RuntimeError(62))?;
let breite = match bytes[0] {
0..=0x7f => 1,
0xc2..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf4 => 4,
_ => return Err(RuntimeError(57)),
};
for b in &mut bytes[1..breite] {
*b = self.byte_lesen()?.ok_or(RuntimeError(57))?;
}
text.push_str(std::str::from_utf8(&bytes[..breite]).map_err(|_| RuntimeError(57))?);
}
Ok(text)
}
/// Datensatz `nr` (1-basiert) in den Puffer lesen.
@@ -179,7 +294,10 @@ impl Datei {
.seek(SeekFrom::Start(offset))
.map_err(fehler_aus_io)?;
let puffer = std::mem::take(&mut self.puffer);
self.datei.write_all(&puffer).map_err(fehler_aus_io)?;
self.datei
.get_mut()
.write_all(&puffer)
.map_err(fehler_aus_io)?;
self.puffer = puffer;
self.position = nr + 1;
Ok(())
@@ -211,13 +329,16 @@ impl Datei {
self.datei
.seek(SeekFrom::Start(pos - 1))
.map_err(fehler_aus_io)?;
self.datei.write_all(daten).map_err(fehler_aus_io)?;
self.datei
.get_mut()
.write_all(daten)
.map_err(fehler_aus_io)?;
self.position = pos + daten.len() as u64;
Ok(())
}
fn schliessen(&mut self) -> Result<(), RuntimeError> {
self.datei.flush().map_err(fehler_aus_io)
self.datei.get_mut().flush().map_err(fehler_aus_io)
}
}
@@ -243,9 +364,6 @@ impl Dateien {
return Err(RuntimeError(55)); // File already open
}
let p = pfad_normieren(pfad);
if modus == Modus::Input && !p.exists() {
return Err(RuntimeError(53)); // File not found
}
self.offen
.insert(nummer, Datei::oeffnen(&p, modus, reclen)?);
Ok(())
@@ -478,6 +596,7 @@ pub fn pfad_normieren(p: &str) -> PathBuf {
fn fehler_aus_io(e: std::io::Error) -> RuntimeError {
use std::io::ErrorKind::*;
match e.kind() {
NotADirectory => RuntimeError(76), // Path not found
NotFound => RuntimeError(53), // File not found
PermissionDenied => RuntimeError(70), // Permission denied
AlreadyExists => RuntimeError(58), // File already exists
@@ -485,32 +604,39 @@ fn fehler_aus_io(e: std::io::Error) -> RuntimeError {
}
}
/// NotFound am Dateieintrag (53) gegenüber fehlendem Elternpfad (76).
fn fehler_am_pfad(e: std::io::Error, pfad: &Path) -> RuntimeError {
if e.kind() == std::io::ErrorKind::NotFound
&& pfad
.parent()
.is_some_and(|p| !p.as_os_str().is_empty() && !p.is_dir())
{
RuntimeError(76)
} else {
fehler_aus_io(e)
}
}
/// `KILL` — Datei löschen.
pub fn loeschen(pfad: &str) -> Result<(), RuntimeError> {
std::fs::remove_file(pfad_normieren(pfad)).map_err(fehler_aus_io)
let p = pfad_normieren(pfad);
std::fs::remove_file(&p).map_err(|e| fehler_am_pfad(e, &p))
}
/// `NAME alt AS neu`.
pub fn umbenennen(alt: &str, neu: &str) -> Result<(), RuntimeError> {
let (a, n) = (pfad_normieren(alt), pfad_normieren(neu));
if !a.exists() {
return Err(RuntimeError(53));
}
std::fs::metadata(&a).map_err(|e| fehler_am_pfad(e, &a))?;
if n.exists() {
return Err(RuntimeError(58)); // File already exists
}
std::fs::rename(a, n).map_err(fehler_aus_io)
std::fs::rename(&a, &n).map_err(|e| fehler_am_pfad(e, &n))
}
/// `MKDIR` / `RMDIR` / `CHDIR`.
pub fn verzeichnis_anlegen(pfad: &str) -> Result<(), RuntimeError> {
let p = pfad_normieren(pfad);
if p.parent()
.is_some_and(|e| !e.as_os_str().is_empty() && !e.exists())
{
return Err(RuntimeError(76)); // Path not found
}
std::fs::create_dir(p).map_err(fehler_aus_io)
std::fs::create_dir(&p).map_err(|e| fehler_am_pfad(e, &p))
}
pub fn verzeichnis_entfernen(pfad: &str) -> Result<(), RuntimeError> {
@@ -613,7 +739,7 @@ pub fn felder_trennen(zeile: &str) -> Vec<String> {
/// Ein Element für `WRITE #` aufbereiten: Strings in Anführungszeichen.
pub fn write_element(text: &str, ist_string: bool) -> String {
if ist_string {
format!("\"{text}\"")
format!("\"{}\"", text.replace('"', "\"\""))
} else {
text.trim().to_string()
}

View File

@@ -1375,6 +1375,7 @@ fn builtin_id(b: Builtin) -> u16 {
Builtin::DateSet => ids::DATE_SET,
Builtin::TimeSet => ids::TIME_SET,
Builtin::Now => ids::NOW,
Builtin::TimezoneKnown => ids::TIMEZONEKNOWN,
Builtin::DateSerial => ids::DATE_SERIAL,
Builtin::TimeSerial => ids::TIME_SERIAL,
Builtin::DateValue => ids::DATE_VALUE,

View File

@@ -2570,8 +2570,9 @@ impl Vm {
if let Some(taste) = self.rt.gepufferte_taste() {
let eingabe = self.frames[caller].eingabe.as_mut().unwrap();
if let Some(rest) = &mut eingabe.rest {
let taste = self.rt.tastenanteil(taste, *rest);
*rest -= taste.chars().count();
eingabe.text.push_str(&taste);
*rest -= 1;
} else {
match taste.as_str() {
tb_runtime::host::taste::ENTER => {
@@ -2872,19 +2873,26 @@ impl Vm {
refs.reverse();
let nummer = self.pop_i32()?;
let Some(zeile) = self.rt.dateien.get(nummer)?.zeile_lesen()? else {
return Err(RuntimeError(62)); // Input past end of file
};
if line_mode {
let zeile = self
.rt
.dateien
.get(nummer)?
.zeile_lesen()?
.ok_or(RuntimeError(62))?;
let r = refs.first().ok_or(RuntimeError(51))?;
self.write_ref(r, Value::Str(Rc::from(zeile.as_str())))?;
return Ok(());
}
let felder = tb_runtime::fileio::felder_trennen(&zeile);
for (i, r) in refs.iter().enumerate() {
let text = felder.get(i).map(String::as_str).unwrap_or("");
for r in &refs {
let text = self.rt.dateien.get(nummer)?.feld_lesen()?;
let ziel = self.read_ref(r)?;
let wert = input_value(&ziel, text).ok_or(RuntimeError(64))?; // Bad file name
// Der Feldleser hat Quotes und äußeren Leerraum bereits ausgewertet.
let wert = if matches!(ziel, Value::Str(_)) {
Value::Str(Rc::from(text))
} else {
input_value(&ziel, &text).ok_or(RuntimeError(64))?
};
self.write_ref(r, wert)?;
}
Ok(())

View File

@@ -1516,3 +1516,203 @@ fn klassischer_timer_trap_ruht_solange_das_menu_offen_ist() {
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("n"), Some(Value::Int(1))));
}
// ---- Laufzeit-Eingabe und Systemstatus ------------------------------------
#[test]
fn input_datei_bewahrt_felder_und_unterscheidet_leer_von_fehlend() {
let tmp = TempVerzeichnis::neu("input_felder");
let p = tmp.pfad("daten.txt");
assert_eq!(out(&format!(
"OPEN \"{p}\" FOR OUTPUT AS #1\nWRITE #1, \"a\", \"b\"\nWRITE #1, \"a,b\", 5\nWRITE #1, CHR$(34) + \"ä\" + CHR$(34), \"\"\nCLOSE #1\nOPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$\nINPUT #1, b$, c$, n%, d$, e$\nPRINT a$; b$; c$; n%; d$; LEN(e$); EOF(1)\nCLOSE"
)), "aba,b 5 \"ä\" 0 -1 \n");
for (data, expected) in [("\"a\"\n", 62), ("a", 62), ("", 62), ("\"a", 62)] {
std::fs::write(&p, data).unwrap();
assert_eq!(
err_code(&format!("OPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$, b$")),
expected
);
}
std::fs::write(&p, "a,\r\n\r\n\"b\n,c\"\r\n").unwrap();
assert_eq!(out(&format!("OPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$, b$, c$, d$\nPRINT a$; LEN(b$); LEN(c$); LEN(d$); EOF(1)")), "a 0 0 4 -1 \n");
std::fs::write(&p, "a,").unwrap();
assert_eq!(out(&format!("OPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, a$\nPRINT EOF(1);\nINPUT #1, b$\nPRINT LEN(b$); EOF(1)")), " 0 0 -1 \n");
}
#[test]
fn input_datei_alle_lesearten_teilen_utf8_cursor_und_seek() {
let tmp = TempVerzeichnis::neu("input_cursor");
let p = tmp.pfad("daten.txt");
std::fs::write(&p, "ä😀,rest\r\n\"a,b\",Ende\r\nZ").unwrap();
assert_eq!(out(&format!(
"OPEN \"{p}\" FOR INPUT AS #1\nPRINT INPUT$(1, #1); INPUT$(1, #1); SEEK(1)\nLINE INPUT #1, s$\nPRINT s$\nINPUT #1, s$\nPRINT s$; INPUT$(2, #1)\nLINE INPUT #1, s$\nPRINT s$\nPRINT INPUT$(1, #1); EOF(1); LEN(INPUT$(0, #1))\nSEEK #1, 1\nINPUT #1, s$\nPRINT s$; SEEK(1)\nCLOSE"
)), "ä😀 7 \n,rest\na,bEn\nde\nZ-1 0 \nä😀 8 \n");
for mode in ["INPUT", "BINARY"] {
std::fs::write(&p, "ä😀x").unwrap();
assert_eq!(
out(&format!(
"OPEN \"{p}\" FOR {mode} AS #1\nPRINT INPUT$(2, #1); INPUT$(1, #1); EOF(1)"
)),
"ä😀x-1 \n"
);
assert_eq!(
err_code(&format!(
"OPEN \"{p}\" FOR {mode} AS #1\ns$ = INPUT$(4, #1)"
)),
62
);
for bad in [&[0xc3][..], &[0xff], &[0xe0, 0x80, 0x80], &[0xf0, 0x9f]] {
std::fs::write(&p, bad).unwrap();
assert_eq!(
err_code(&format!(
"OPEN \"{p}\" FOR {mode} AS #1\ns$ = INPUT$(1, #1)"
)),
57
);
}
}
for input in ["INPUT #1, s$", "LINE INPUT #1, s$"] {
assert_eq!(
err_code(&format!("OPEN \"{p}\" FOR INPUT AS #1\n{input}")),
57
);
}
for mode in ["OUTPUT", "APPEND", "RANDOM"] {
assert_eq!(
err_code(&format!(
"OPEN \"{p}\" FOR {mode} AS #1\ns$ = INPUT$(1, #1)"
)),
54
);
}
}
#[test]
fn input_s_teilt_sondertasten_und_bewahrt_rest_fuer_inkey() {
use tb_runtime::host::Ereignis;
for second in ["INPUT$(1)", "INKEY$"] {
let src = format!("a$ = INPUT$(1)\nb$ = {second}\nc$ = INPUT$(2)\nPRINT LEN(a$); ASC(a$); LEN(b$); ASC(b$); c$; LEN(INKEY$)");
let mut vm = Vm::new(tb_vm::compile_source("T", &src).unwrap());
let mut host = CaptureHost::default();
for text in ["\0;", "ä", "😀"] {
host.ereignis(Ereignis::Taste(text.into(), 0));
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(
tb_runtime::snapshot::text(&vm.rt.screen),
" 1 0 1 59 ä😀 0 \n"
);
}
}
#[test]
fn alle_mk_cv_paare_verlangen_exakte_bytezeichenbreite() {
for (mk, cv, n, breite) in [
("MKI$", "CVI", "-123", 2),
("MKL$", "CVL", "123456", 4),
("MKS$", "CVS", "1.5", 4),
("MKD$", "CVD", "-12.25", 8),
("MKC$", "CVC", "12.3456@", 8),
] {
assert_eq!(out(&format!("PRINT {cv}({mk}({n})) = {n}")), "-1 \n");
for arg in [
format!("LEFT$({mk}({n}), {})", breite - 1),
format!("{mk}({n}) + CHR$(0)"),
format!("STRING$({breite}, \"😀\")"),
] {
assert_eq!(err_code(&format!("PRINT {cv}({arg})")), 5, "{cv}({arg})");
}
}
}
#[test]
fn datei_und_elternpfadfehler_sind_am_sprachzugang_unterscheidbar() {
let tmp = TempVerzeichnis::neu("pfadfehler");
let leaf = tmp.pfad("fehlt.txt");
let parent = tmp.pfad("fehlt/datei.txt");
let source = tmp.pfad("quelle.txt");
std::fs::write(&source, "erhalten").unwrap();
for (path, code) in [(&leaf, 53), (&parent, 76)] {
for stmt in [
format!("OPEN \"{path}\" FOR INPUT AS #1"),
format!("KILL \"{path}\""),
format!("NAME \"{path}\" AS \"{source}\""),
] {
assert_eq!(err_code(&stmt), code, "{stmt}");
}
}
for stmt in [
format!("MKDIR \"{parent}\""),
format!("NAME \"{source}\" AS \"{parent}\""),
format!("CHDIR \"{parent}\""),
format!("RMDIR \"{parent}\""),
format!("PRINT DIR$(\"{parent}\")"),
] {
assert_eq!(err_code(&stmt), 76, "{stmt}");
}
for mode in ["OUTPUT", "APPEND", "BINARY", "RANDOM"] {
assert_eq!(err_code(&format!("OPEN \"{parent}\" FOR {mode} AS #1")), 76);
}
assert_eq!(std::fs::read_to_string(&source).unwrap(), "erhalten");
let sub = tmp.pfad("unter");
let a = format!("{sub}\\a.txt");
let b = format!("{sub}/b.txt");
assert_eq!(out(&format!("MKDIR \"{sub}\"\nOPEN \"{a}\" FOR OUTPUT AS #1\nWRITE #1, \"ok\"\nCLOSE\nNAME \"{a}\" AS \"{b}\"\nPRINT DIR$(\"{sub}/*.txt\"); LEN(DIR$(\"\"))\nOPEN \"{b}\" FOR INPUT AS #1\nINPUT #1, s$\nPRINT s$\nCLOSE\nKILL \"{b}\"\nRMDIR \"{sub}\"")), "b.txt 0 \nok\n");
}
#[test]
fn timezoneknown_hat_integer_signatur_und_zeigt_aktuellen_status() {
for (zone, expected) in [(Zeitzone::Fest(0), "-1 \n"), (Zeitzone::Unbekannt, " 0 \n")] {
assert_eq!(out_mit_zone("PRINT TIMEZONEKNOWN", zone), expected);
}
let diagnostics = tb_vm::compile_source("T", "PRINT TIMEZONEKNOWN(1)").unwrap_err();
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("TIMEZONEKNOWN")),
"{diagnostics:?}"
);
let mut vm = Vm::new(
tb_vm::compile_source(
"T",
"x = TIMEZONEKNOWN\nSTOP\ny = TIMEZONEKNOWN\nSTOP\nz = TIMEZONEKNOWN\nEND",
)
.unwrap(),
);
let mut host = CaptureHost::default();
vm.rt.zonen_ermittler = |_| Some(7200);
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(matches!(vm.inspect("x"), Some(Value::Sng(-1.0))));
vm.rt.zonen_ermittler = |_| None;
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
assert!(matches!(vm.inspect("y"), Some(Value::Sng(0.0))));
vm.rt.zonen_ermittler = |_| Some(0);
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("z"), Some(Value::Sng(-1.0))));
}
#[test]
fn unicode_puffergrenzen_und_binaere_records_bleiben_getrennt() {
let tmp = TempVerzeichnis::neu("input_puffer");
let p = tmp.pfad("daten.txt");
for n in [8190, 8191, 8192] {
std::fs::write(&p, format!("{}ä😀\r\nZ", "x".repeat(n))).unwrap();
assert_eq!(out(&format!("OPEN \"{p}\" FOR INPUT AS #1\na$ = INPUT$({n}, #1)\nPRINT INPUT$(2, #1)\nLINE INPUT #1, b$\nINPUT #1, c$\nPRINT LEN(b$); c$; EOF(1)")), "ä😀\n 0 Z-1 \n");
}
let mut bytes = "ä😀".as_bytes().to_vec();
bytes.extend_from_slice(&513i16.to_le_bytes());
bytes.push(b'Z');
std::fs::write(&p, bytes).unwrap();
assert_eq!(out(&format!("OPEN \"{p}\" FOR BINARY AS #1\nPRINT INPUT$(2, #1);\nGET #1, , n%\nPRINT n%; INPUT$(1, #1); EOF(1)\nSEEK #1, 7\nn% = 1027\nPUT #1, , n%\nSEEK #1, 7\nGET #1, , n%\nPRINT n%; INPUT$(1, #1)")), "ä😀 513 Z-1 \n 1027 Z\n");
}
#[test]
fn sondertastenrest_wird_nicht_erneut_getrappt() {
use tb_runtime::host::Ereignis;
let src = "ON KEY(1) GOSUB H\nKEY(1) STOP\na$ = INPUT$(1)\nKEY(1) ON\nb$ = INPUT$(1)\nDOEVENTS\nPRINT ASC(a$); ASC(b$); n%\nEND\nH:\nn% = n% + 1\nRETURN";
let mut vm = Vm::new(tb_vm::compile_source("T", src).unwrap());
let mut host = CaptureHost::default();
host.ereignis(Ereignis::Taste("\0;".into(), 0));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 0 59 1 \n");
}

View File

@@ -58,6 +58,7 @@ Bildschirm zurück.
| `TIMER` | Funktion |
| `TIMESERIAL` | Funktion |
| `TIMEVALUE` | Funktion |
| `TIMEZONEKNOWN` | Funktion |
| `WEEKDAY` | Funktion |
| `YEAR` | Funktion |
@@ -72,6 +73,13 @@ Zeitpunkt bestimmt, nicht einmal beim Programmstart — ein Programm, das
Lässt sich die Zone nicht ermitteln, gilt UTC; das Programm läuft weiter
und der Runner meldet es einmal beim Start auf der Fehlerausgabe.
`TIMEZONEKNOWN` ist eine parameterlose TerminalBasic-Erweiterung mit
Ergebnistyp `INTEGER`: `-1` bei ermittelter Zone (auch UTC), `0` bei
UTC-Rückfall. Jede Abfrage ermittelt den aktuellen Zustand, einschließlich
späterer Ausfälle und Wiederherstellung. Sie ändert weder die Systemuhr
noch den programmeigenen Versatz. Rust-Einbetter erhalten Zeitwert und
Status gemeinsam über `RtState::zeitpunkt()`; `Zeitzone::Unbekannt` kann
weiterhin einen festen UTC-Rückfall vorgeben.
`DATE$ = …` und `TIME$ = …` stellen nicht die Systemuhr, sondern einen
programmeigenen Versatz auf die Ortszeit, den alle Zeitfunktionen
@@ -384,9 +392,39 @@ einen dokumentierten Ersatzwert statt still zu variieren.
`INKEY$` blockiert nicht: leerer String bei leerem Puffer, ein Zeichen
bei einer Zeichentaste, zwei Zeichen mit führendem Nullzeichen bei
einer Sondertaste. `INPUT$(n)` liest genau `n` Zeichen und blockiert
einer Sondertaste. `INPUT$(n)` liest genau `n` Unicode-Codepoints und blockiert
dabei, ohne sie am Bildschirm zu wiederholen.
`KEY n, text$` belegt ein Funktionstasten-Makro (n = 110 sowie 30/31,
höchstens 15 Zeichen), `KEY LIST` gibt alle zwölf untereinander aus,
`KEY ON`/`KEY OFF` blendet die Softkey-Zeile ein und aus.
## Fortlaufende Datei-Eingabe und Record-Konvertierung
`INPUT #` liest Felder fortlaufend, auch über Zeilengrenzen hinweg. Nicht
benötigte Felder bleiben für spätere Aufrufe erhalten. `WRITE #` setzt
Strings in Anführungszeichen und verdoppelt enthaltene Anführungszeichen;
`INPUT #` erhält deren Inhalt einschließlich Kommas und Leerraum.
Fehlende geforderte Felder lösen Fehler 62 aus; ein ausdrücklich leeres
Feld bleibt zulässig.
`INPUT$(n, #f)` decodiert genau `n` UTF-8-Codepoints aus einer INPUT- oder
BINARY-Datei. `INPUT #`, `LINE INPUT #` und `INPUT$` verwenden dieselbe
Leseposition; `LINE INPUT #` entfernt ein LF-, CR- oder CRLF-Zeilenende.
`SEEK` und `LOC` beziehen sich dabei weiterhin auf Bytes. `INPUT$(0)`
verbraucht nichts; negative Längen lösen Fehler 5 aus. EOF vor dem
geforderten Zeichen führt zu Fehler 62; ungültige oder abgeschnittene
UTF-8-Sequenzen führen zu Fehler 57 statt zu Ersatzzeichen.
Bei Tastatureingabe bleibt der unbenutzte Rest einer erweiterten Taste
für `INPUT$` bzw. `INKEY$` erhalten: zweimal `INPUT$(1)` liest aus F1
zuerst `CHR$(0)`, dann `CHR$(59)`.
`CVI`, `CVL`, `CVS`, `CVD`, `CVC` verlangen exakt 2, 4, 4, 8 bzw. 8
Bytezeichen (Codepoints 0255), entsprechend `MKI$`, `MKL$`, `MKS$`,
`MKD$`, `MKC$`. Kürzere, längere oder nicht als Byte darstellbare
Argumente lösen Fehler 5 aus. GET/PUT und UTF-32-Recordfelder bleiben
unverändert; die MK/CV-Bytezeichen sind keine UTF-8-Dateidecodierung.
Fehlt bei `OPEN FOR INPUT`, `KILL` oder der Quelle von `NAME` nur die
Datei, gilt Fehler 53. Ein fehlender Elternpfad führt zu Fehler 76,
auch beim Erzeugen, Umbenennen oder Anlegen von Verzeichnissen.

View File

@@ -22,7 +22,7 @@ namentlich abgewiesen. `Non-Feature` = abgelehnt, gelistet unter
**Fundstelle.** Bei `implementiert` das Modul, bei `Non-Feature` der
Abschnitt der Sprachreferenz, bei `offen` ein Strich.
**Abdeckung.** implementiert 794 · offen 0 · Non-Feature 50 · gesamt 844
**Abdeckung.** implementiert 795 · offen 0 · Non-Feature 50 · gesamt 845
| Name | Art | Gruppe | Status | Fundstelle | Quelle |
|---|---|---|---|---|---|
@@ -99,6 +99,7 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
| `MINUTE` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
| `MONTH` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
| `NOW` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
| `TIMEZONEKNOWN` | Funktion | Datum/Zeit | implementiert | tb-runtime::builtins | TerminalBasic-Erweiterung |
| `SECOND` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
| `TIME$` | Anweisung | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |
| `TIME$` | Funktion | Datum/Zeit | implementiert | tb-runtime::datetime | bas7advr |

View File

@@ -46,6 +46,19 @@ archiviert. Details und
Szenariozuordnung: [Implementierungsnachweis](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/verification.md).
Die folgenden Befundtexte bleiben die historische Bestandsaufnahme.
## Umsetzungsstand: Laufzeit-Eingabe und Systemstatus
F14, F15, F16, F17 und F19 sind im archivierten Change
[laufzeit-eingabe-und-systemstatus](../../openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus/proposal.md)
umgesetzt. Der Abschlussabgleich hat keine offenen Befunde im Change:
463 Workspace-Tests, Clippy, Format- und Spec-Prüfung bestehen. Alle sechs
Tasks sind abgeschlossen; die bestehenden Sollausgaben bleiben unverändert.
Szenarien und Randfälle stehen im
[Implementierungsnachweis](../../openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus/verification.md).
Die vier Hauptspecs wurden am 06.09.2026 synchronisiert, alle 15 Hauptspecs
streng validiert und der Change archiviert. Die Befundtexte unten bleiben
die historische Bestandsaufnahme.
## Prüfmethode und Grenzen
- Alle Requirements/Szenarien gelesen und gegen Einstiegspunkte, Implementierung und vorhandene Tests abgeglichen. Die Matrix unten enthält jeden Requirement-Titel einmal.
@@ -168,7 +181,7 @@ Nach WRITE #1, "a", "b" liest INPUT #1, a$ die ganze Zeile und verwirft b; das z
Beleg: [crates/tb-vm/src/interp.rs:2529](../../crates/tb-vm/src/interp.rs#L2529). Vertrag: [openspec/specs/datei-eio/spec.md:35](../../openspec/specs/datei-eio/spec.md#L35).
Umsetzung: [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md).
Umsetzung: [laufzeit-eingabe-und-systemstatus](../../openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus/proposal.md).
### F15 — INPUT$ zählt weder durchgehend Zeichen noch die verlangte Länge (mittel)
@@ -176,7 +189,7 @@ INPUT$(1) mit F1 liefert zwei Zeichen (Nullzeichen und Scancode). INPUT$(1,#1) a
Beleg: [crates/tb-runtime/src/builtins.rs:2116](../../crates/tb-runtime/src/builtins.rs#L2116). Vertrag: [openspec/specs/textbildschirm/spec.md:144](../../openspec/specs/textbildschirm/spec.md#L144).
Umsetzung: [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md).
Umsetzung: [laufzeit-eingabe-und-systemstatus](../../openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus/proposal.md).
### F16 — CV-Funktionen akzeptieren zu lange Byte-Strings (mittel)
@@ -184,7 +197,7 @@ CVI("abc") läuft erfolgreich und liefert 25185. Die Implementierung prüft nur
Beleg: [crates/tb-runtime/src/builtins.rs:1640](../../crates/tb-runtime/src/builtins.rs#L1640). Vertrag: [openspec/specs/datei-eio/spec.md:68](../../openspec/specs/datei-eio/spec.md#L68).
Umsetzung: [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md).
Umsetzung: [laufzeit-eingabe-und-systemstatus](../../openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus/proposal.md).
### F17 — Fehlender Elternpfad wird als fehlende Datei gemeldet (mittel)
@@ -192,7 +205,7 @@ OPEN "absentdir/file.txt" FOR INPUT liefert Fehler 53; der Vertrag unterscheidet
Beleg: [crates/tb-runtime/src/fileio.rs:69](../../crates/tb-runtime/src/fileio.rs#L69). Vertrag: [openspec/specs/datei-eio/spec.md:135](../../openspec/specs/datei-eio/spec.md#L135).
Umsetzung: [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md).
Umsetzung: [laufzeit-eingabe-und-systemstatus](../../openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus/proposal.md).
### F18 — Umschalt-Tab wird am Terminal verworfen (mittel)
@@ -208,7 +221,7 @@ Zeitzone::Unbekannt ist auf Rust-Ebene vorhanden und der CLI-Runner schreibt ein
Beleg: [crates/tb-runtime/src/datetime.rs:147](../../crates/tb-runtime/src/datetime.rs#L147). Vertrag: [openspec/specs/konsolen-basisbibliothek/spec.md:260](../../openspec/specs/konsolen-basisbibliothek/spec.md#L260).
Umsetzung: [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md).
Umsetzung: [laufzeit-eingabe-und-systemstatus](../../openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus/proposal.md).
### F20 — VM hängt transitiv von Terminalbibliotheken ab (mittel)
@@ -266,7 +279,7 @@ Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/chan
| 2 | [ereigniszustellung-und-hostgrenzen](../../openspec/changes/archive/2026-09-05-ereigniszustellung-und-hostgrenzen/proposal.md) | F04, F05, F06, F07, F08, F18, F20 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 3 | [forms-zustand-und-bedienung](../../openspec/changes/archive/2026-09-05-forms-zustand-und-bedienung/proposal.md) | F12, F13 | Nach Ereigniszustellung |
| 4 | [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md) | F09, F10, F11, F24 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
| 6 | [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md) | F21, F22, F23, F25 | Abschließende Gesamtabnahme nach den fünf Korrektur-Changes |
## Vollständige Requirement-Matrix

View File

@@ -277,7 +277,9 @@ verweist dazu auf ein nicht vorliegendes Kapitel des Programmer's Guide):
`LINE INPUT` (ganze Zeile in String).
- `INKEY$` (nicht blockierend; "" wenn leer; erweiterte Tasten:
2-Zeichen-Sequenz `CHR$(0)+code` im Vorbild — Abweichung s. u.),
`INPUT$(n [,#f])`.
`INPUT$(n [,#f])` liest genau `n` Unicode-Codepoints, ohne Echo. Der
Rest einer Sondertastenfolge bleibt lesbar. Dateien werden als UTF-8
decodiert; zu wenige Zeichen → Fehler 62, beschädigtes UTF-8 → Fehler 57.
- `LOCATE [zeile][,spalte][,cursor an/aus][,start,ende]`, `CSRLIN`, `POS(0)`
— arbeiten auf der tatsächlichen Bildschirmgröße (dynamisch, s. Abweichungen).
- `COLOR [vg][,hg]` (vg 031: 1631 = blinkend — bei uns als „hell"
@@ -310,14 +312,21 @@ verweist dazu auf ein nicht vorliegendes Kapitel des Programmer's Guide):
`BINARY`.
- `CLOSE [#n, …]`, `RESET` (alle schließen).
- Sequenziell: `PRINT #`, `PRINT # USING`, `WRITE #` (CSV-artig, Strings in
`"…"`), `INPUT #`, `LINE INPUT #`, `EOF(n)`.
`"…"`, innere Anführungszeichen verdoppelt), `INPUT #`, `LINE INPUT #`,
`EOF(n)`. `INPUT #` bewahrt übrige Felder und liest über Zeilen hinweg;
fehlende geforderte Felder lösen Fehler 62 aus. Die drei Datei-Lesearten
`INPUT #`, `LINE INPUT #`, `INPUT$` teilen einen Cursor.
- Random: `FIELD #n, breite AS var$…` (klassisch) **und** `GET/PUT #n
[,satznr] [,var]` mit TYPE-Variablen; `LSET`/`RSET` für Feldpuffer.
- Binary: `GET/PUT #n, [pos], var`, `SEEK #n, pos` / `SEEK(n)`,
`LOC(n)`, `LOF(n)`.
- Verwaltung: `NAME alt$ AS neu$`, `KILL datei$`, `FILES [muster$]`,
`CHDIR`, `MKDIR`, `RMDIR`, `FILEATTR`, `FREEFILE`.
- Pfade: plattformneutral; `/` und `\` werden akzeptiert.
- Pfade: plattformneutral; `/` und `\` werden akzeptiert. Fehlende Datei
bei vorhandenem Elternverzeichnis → Fehler 53; fehlender Elternpfad → 76.
- `CVI/CVL/CVS/CVD/CVC` verlangen exakt 2/4/4/8/8 Bytezeichen mit
Codepoints 0255, sonst Fehler 5. Die zugehörigen `MK…$`-Funktionen
liefern diese Darstellung; Recordfelder bleiben UTF-32.
## 10a. ISAM (indizierter Satzzugriff)
@@ -547,7 +556,11 @@ Dialoge:
Programm, das über eine Zeitumstellung hinweg läuft, folgt ihr.
Lässt sich die Zone des Rechners nicht ermitteln, gilt UTC; das
Programm läuft weiter, und der Runner meldet es einmal beim Start auf
der Fehlerausgabe („Zeitzone nicht ermittelbar …"). `DATE$ = …` und
der Fehlerausgabe („Zeitzone nicht ermittelbar …"). Die additive
TerminalBasic-Erweiterung `TIMEZONEKNOWN` (ohne Argument, `INTEGER`)
liefert `-1` für eine ermittelte Zone, `0` für UTC-Rückfall. Der Status
folgt jeder aktuellen Ermittlung, auch späterem Ausfall und Erholung;
eine ermittelte UTC-Zone liefert ebenfalls `-1`. `DATE$ = …` und
`TIME$ = …` stellen nicht die Systemuhr, sondern einen programmeigenen
Versatz auf die Ortszeit; alle Zeitfunktionen rechnen ihn mit ein.
6b. **Serielle Datumswerte** zählen Tage ab dem 30.12.1899 (Serie 2 =

View File

@@ -1,6 +1,6 @@
## Why
Die Laufzeit verwirft INPUT-Felder, liest INPUT$ mit falscher Zeichenzählung und weicht bei CV-Längen sowie Dateifehlern ab. Zusätzlich fehlt die spezifizierte BASIC-Abfrage des Zeitzonenrückfalls (F14F17/F19). Siehe [Review vom 05.09.2026](../../../docs/reviews/2026-09-05-openspec-code-review.md).
Die Laufzeit verwirft INPUT-Felder, liest INPUT$ mit falscher Zeichenzählung und weicht bei CV-Längen sowie Dateifehlern ab. Zusätzlich fehlt die spezifizierte BASIC-Abfrage des Zeitzonenrückfalls (F14F17/F19). Siehe [Review vom 05.09.2026](../../../../docs/reviews/2026-09-05-openspec-code-review.md).
## What Changes

View File

@@ -0,0 +1,194 @@
//! Unabhängige Reviewproben, 06.09.2026. Als Rust-Testbinary gegen tb-vm/tb-runtime.
use std::{path::PathBuf, rc::Rc};
use tb_runtime::{
builtins::{builtin_table, ids, RtState},
datetime::Zeitzone,
host::{CaptureHost, Ereignis},
value::Value,
};
use tb_vm::{
bytecode::CompiledModule,
interp::{RunEvent, Vm},
};
struct Temp(PathBuf);
impl Temp {
fn new(name: &str) -> Self {
let p =
std::env::temp_dir().join(format!("tb_runtime_review_{name}_{}", std::process::id()));
std::fs::create_dir_all(&p).unwrap();
Self(p)
}
fn file(&self) -> String {
self.0.join("data.txt").to_str().unwrap().to_owned()
}
}
impl Drop for Temp {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn text(vm: &Vm, name: &str) -> String {
match vm.inspect(name).unwrap() {
Value::Str(s) => s.to_string(),
other => panic!("{other:?}"),
}
}
fn expr(s: &str) -> String {
let escaped = s
.replace('"', "\"\"")
.replace('\r', "\" + CHR$(13) + \"")
.replace('\n', "\" + CHR$(10) + \"")
.replace('\0', "\" + CHR$(0) + \"");
format!("\"{escaped}\"")
}
#[test]
fn write_input_roundtrip_string_matrix() {
let tmp = Temp::new("roundtrip");
let p = tmp.file();
let parts = ["", "a", " ", "\t", "\"", ",", "\r\n", "ä😀", "\0", "\u{a0}"];
for a in parts {
for b in parts {
let expected = format!("{a}{b}");
let src = format!("OPEN \"{p}\" FOR OUTPUT AS #1\nWRITE #1, {}, \"next\"\nCLOSE\nOPEN \"{p}\" FOR INPUT AS #1\nINPUT #1, got$\nINPUT #1, next$\nEND", expr(&expected));
let mut vm = Vm::new(tb_vm::compile_source("REVIEW", &src).unwrap());
assert_eq!(
vm.run(&mut CaptureHost::default()),
RunEvent::Ended,
"{expected:?}"
);
assert_eq!(text(&vm, "got"), expected);
assert_eq!(text(&vm, "next"), "next");
}
}
}
#[test]
fn all_nine_file_read_transitions() {
let tmp = Temp::new("transitions");
let p = tmp.file();
for first in 0..3 {
for second in 0..3 {
std::fs::write(&p, "ä,😀\r\nZ").unwrap();
let statement = |mode, variable| match mode {
0 => format!("INPUT #1, {variable}$"),
1 => format!("{variable}$ = INPUT$(1, #1)"),
_ => format!("LINE INPUT #1, {variable}$"),
};
let src = format!(
"OPEN \"{p}\" FOR INPUT AS #1\n{}\n{}\nEND",
statement(first, "a"),
statement(second, "b")
);
let mut vm = Vm::new(tb_vm::compile_source("REVIEW", &src).unwrap());
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(text(&vm, "a"), if first == 2 { "ä,😀" } else { "ä" });
let expected = match (first, second) {
(0, _) => "😀",
(1, 0) => "",
(1, 1) => ",",
(1, 2) => ",😀",
_ => "Z",
};
assert_eq!(text(&vm, "b"), expected, "transition {first}/{second}");
}
}
}
#[test]
fn every_mk_cv_pair_preserves_target_values_and_width() {
let mut st = RtState::default();
let mut host = CaptureHost::default();
let cases = [
(0, Value::Int(i16::MIN)),
(0, Value::Int(i16::MAX)),
(1, Value::Lng(i32::MIN)),
(1, Value::Lng(i32::MAX)),
(2, Value::Sng(-0.0)),
(2, Value::Sng(f32::MAX)),
(3, Value::Dbl(-0.0)),
(3, Value::Dbl(f64::MIN_POSITIVE)),
(4, Value::Cur(i64::MIN)),
(4, Value::Cur(i64::MAX)),
];
for (kind, value) in cases {
let encoded = builtin_table()[ids::MK_S as usize](
&mut st,
&mut host,
&mut [value.clone(), Value::Lng(kind)],
)
.unwrap()
.unwrap();
let decoded = builtin_table()[ids::CV_F as usize](
&mut st,
&mut host,
&mut [encoded.clone(), Value::Lng(kind)],
)
.unwrap()
.unwrap();
match (&value, &decoded) {
(Value::Int(a), Value::Int(b)) => assert_eq!(a, b),
(Value::Lng(a), Value::Lng(b)) => assert_eq!(a, b),
(Value::Cur(a), Value::Cur(b)) => assert_eq!(a, b),
(Value::Sng(a), Value::Sng(b)) => assert_eq!(a.to_bits(), b.to_bits()),
(Value::Dbl(a), Value::Dbl(b)) => assert_eq!(a.to_bits(), b.to_bits()),
_ => panic!("{value:?} -> {decoded:?}"),
}
let Value::Str(encoded) = encoded else {
panic!()
};
let len = encoded.chars().count();
for wrong in [
encoded.chars().take(len - 1).collect::<String>(),
format!("{encoded}\0"),
] {
let err = builtin_table()[ids::CV_F as usize](
&mut st,
&mut host,
&mut [Value::Str(Rc::from(wrong)), Value::Lng(kind)],
)
.unwrap_err();
assert_eq!(err.0, 5);
}
}
}
#[test]
fn timezoneknown_survives_tbc_and_source_failure_recovery() {
thread_local! { static ZONE: std::cell::Cell<Option<i32>> = const { std::cell::Cell::new(None) }; }
let module = tb_vm::compile_source(
"REVIEW",
"a% = TIMEZONEKNOWN\nSTOP\nb% = TIMEZONEKNOWN\nSTOP\nc% = TIMEZONEKNOWN\nEND",
)
.unwrap();
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
vm.rt.zonen_ermittler = |_| ZONE.get();
let mut host = CaptureHost::default();
for (name, zone, expected) in [("a", Some(3600), -1), ("b", None, 0), ("c", Some(0), -1)] {
ZONE.set(zone);
let ev = vm.run(&mut host);
assert!(matches!(ev, RunEvent::Stopped { .. } | RunEvent::Ended));
assert!(matches!(vm.inspect(name), Some(Value::Int(n)) if n == expected));
assert_eq!(vm.rt.zeitzone, Zeitzone::System);
assert_eq!(vm.rt.uhr_offset, 0);
}
}
#[test]
fn successive_function_keys_are_split_in_order() {
let module = tb_vm::compile_source(
"REVIEW",
"a$ = INPUT$(3)\nb$ = INPUT$(1)\nc$ = INPUT$(1)\nEND",
)
.unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
for s in ["\0;", "\0<", "😀"] {
host.ereignis(Ereignis::Taste(s.into(), 0));
}
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!(text(&vm, "a"), "\0;\0");
assert_eq!(text(&vm, "b"), "<");
assert_eq!(text(&vm, "c"), "😀");
}

View File

@@ -0,0 +1,76 @@
# Verification Report: laufzeit-eingabe-und-systemstatus
Erneute Prüfung am 06.09.2026. Grundlage ist der aktuelle, noch uncommittete
Arbeitsbaum auf `815825dde76eb8f4b48cb27d83f21732afe707c2`. Die Aussage aus
`verification.md` wurde gegen Proposal, Design, Tasks, alle vier Delta-Specs,
die tatsächlichen Aufrufpfade und zusätzlich erstellte Reviewproben geprüft.
## Summary
| Dimension | Ergebnis |
|---|---|
| Vollständigkeit | 6/6 Tasks abgeschlossen; 6 Requirements mit 21 Szenarien abgeglichen |
| Korrektheit | 463 Workspace-Tests und 5 unabhängige Reviewproben bestanden |
| Kohärenz | D1D4 eingehalten; gemeinsame Dateileseposition, Codepoint-/Byte-Trennung, Pfadfehler und aktuelle Zonenauflösung |
| CRITICAL | 0 |
| WARNING | 0 |
| SUGGESTION | 0 |
**Keine offenen Befunde im geprüften Change.** Die vorhandene
[Szenariozuordnung](verification.md#szenarien-und-ausführbare-nachweise)
ist durch die erneute Codeprüfung und den aktuellen Testlauf bestätigt.
## Zusätzliche unabhängige Proben
[review-probes.rs](review-probes.rs) wurde separat mit `rustc --test` gegen
die aktuell gebauten `tb_vm`- und `tb_runtime`-Bibliotheken aus
`target/debug/deps` übersetzt und ausgeführt. Alle fünf Tests bestanden.
Die Proben wurden nicht aus den vorhandenen Tests kopiert.
| Probe | Abdeckung / Ergebnis |
|---|---|
| `write_input_roundtrip_string_matrix` | 100 Kombinationen aus leeren Strings, ASCII, Leerzeichen, Tab, Anführungszeichen, Komma, CRLF, ä/Emoji, NUL und geschütztem Leerzeichen: WRITE und zwei folgende INPUT-#-Aufrufe erhalten Inhalt und nächstes Feld exakt |
| `all_nine_file_read_transitions` | Sämtliche 3×3 Übergänge zwischen INPUT #, INPUT$ und LINE INPUT # auf derselben UTF-8-/CRLF-Datei liefern den erwarteten Cursorfortschritt |
| `every_mk_cv_pair_preserves_target_values_and_width` | INTEGER-/LONG-/CURRENCY-Grenzen, negative Null und Fließkomma-Grenzwerte bleiben typ- und bitgetreu; jeweils zu kurze und zu lange Argumente erzeugen Fehler 5 |
| `timezoneknown_survives_tbc_and_source_failure_recovery` | Neuer Builtin über Kompilat-Speicherung und -Laden ausführbar; derselbe Resolver wechselt bekannt → ausgefallen → bekannte UTC. BASIC liefert INTEGER -1/0/-1; Zonenquelle und Programmversatz bleiben unverändert |
| `successive_function_keys_are_split_in_order` | Zwei aufeinanderfolgende erweiterte Tasten und Emoji werden durch INPUT$(3), INPUT$(1), INPUT$(1) ohne Verlust oder Umordnung gelesen |
Statisch zusätzlich abgeglichen: gemeinsame Leser-/SEEK-/EOF-Aufrufpfade,
Binärzugriff nach Einführung von BufReader, beide INPUT$-Dispatchwege,
Trap-Behandlung bereits angenommener Reste, alle fünf CV-Zieltypen,
OPEN/KILL/NAME/MKDIR-Fehlerzuordnung sowie Frontend/HIR/Builtin-ID/CLI
und alle Zeitfunktionen einschließlich der Zuweisungen an DATE$/TIME$.
Die neue Builtin-ID wird angehängt; bestehende IDs werden nicht verschoben.
## Erneut ausgeführte Abnahme
- `cargo test --workspace --quiet`: **463 bestanden, 0 fehlgeschlagen,
1 ignoriert**; der Golden-Generator blieb ignoriert.
- Separate Reviewproben: **5 bestanden, 0 fehlgeschlagen**.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: bestanden.
- `cargo fmt --all -- --check`: bestanden.
- `git diff --check`: bestanden vor dem Staging. Die abschließende
Prüfung des gestagten Changes mit `git diff --cached --check -- .
':(exclude)tests/compat/*.out'` besteht ebenfalls. Die neuen `.out`-Dateien
enthalten absichtlich signifikante PRINT-Leerzeichen am Zeilenende.
- `openspec validate laufzeit-eingabe-und-systemstatus --strict`: bestanden.
- `openspec status` und `openspec instructions apply`: Planung vollständig,
6/6 Tasks, `all_done`. Die Häkchen wurden gegen Code und Tests geprüft.
- Keine bestehenden Korpus-Sollausgaben verändert; die drei neuen
Eingabe-/Zonenprogramme bestehen im regulären Korpuslauf jeweils zweimal.
Während dieser Verifikation wurden nur dieser Bericht und die zusätzlichen
Reviewproben angelegt. Produktcode, bestehende Tests und Sollausgaben blieben
unverändert. Headless auf macOS geprüft; kein neuer Windows-/Linux-Lauf oder
interaktiver Fremdprogrammvergleich. Die Prüfung betrifft diesen Change und
ersetzt nicht den noch offenen übergreifenden Spezifikationsabgleich.
Alle Prüfungen bestanden. Der Change ist zur Synchronisierung und
anschließenden Archivierung bereit.
## Archivabschluss
Am 06.09.2026 wurden alle sechs Delta-Requirements mit den vier Hauptspecs
synchronisiert und vollständig abgeglichen. Alle 15 Hauptspecs bestehen
die strenge Validierung. Der Change wurde anschließend einschließlich
der fünf Reviewproben archiviert.

View File

@@ -1,14 +1,14 @@
## 1. Eingabe und Recordfunktionen
- [ ] 1.1 Fortlaufenden INPUT-#-Lesefortschritt implementieren; verifizieren mit zwei Einzelreads nach WRITE zweier Felder, mehreren Zeilen, Quotes/Kommas und Fehler 62 bei zu wenigen Feldern.
- [ ] 1.2 INPUT$ auf exakt n Codepoints führen und Reste erhalten; verifizieren mit F1 und zwei INPUT$(1)-Aufrufen, UTF-8 ä/Emoji aus Datei, gemischten Lesearten und EOF.
- [ ] 1.3 CV-Zielbreiten exakt prüfen; verifizieren mit allen fünf MK/CV-Paaren und jeweils zu kurzen/passenden/zu langen Argumenten.
- [ ] 1.4 Datei-/Elternpfadfehler unterscheiden; verifizieren mit fehlender Datei in vorhandenem Verzeichnis, fehlendem Elternpfad und bestehenden erfolgreichen Dateisystemfällen.
- [x] 1.1 Fortlaufenden INPUT-#-Lesefortschritt implementieren; verifizieren mit zwei Einzelreads nach WRITE zweier Felder, mehreren Zeilen, Quotes/Kommas und Fehler 62 bei zu wenigen Feldern.
- [x] 1.2 INPUT$ auf exakt n Codepoints führen und Reste erhalten; verifizieren mit F1 und zwei INPUT$(1)-Aufrufen, UTF-8 ä/Emoji aus Datei, gemischten Lesearten und EOF.
- [x] 1.3 CV-Zielbreiten exakt prüfen; verifizieren mit allen fünf MK/CV-Paaren und jeweils zu kurzen/passenden/zu langen Argumenten.
- [x] 1.4 Datei-/Elternpfadfehler unterscheiden; verifizieren mit fehlender Datei in vorhandenem Verzeichnis, fehlendem Elternpfad und bestehenden erfolgreichen Dateisystemfällen.
## 2. Zonenzustand
- [ ] 2.1 TIMEZONEKNOWN durch Frontend, HIR, Dispatch und Inventar ergänzen und dynamischen Rückfall sichtbar halten; verifizieren mit ermittelter Zone, UTC-Rückfall, späterem Ausfall/Wiederherstellung und unverändertem DATE$/TIME$/NOW/TIMER-Versatz.
- [x] 2.1 TIMEZONEKNOWN durch Frontend, HIR, Dispatch und Inventar ergänzen und dynamischen Rückfall sichtbar halten; verifizieren mit ermittelter Zone, UTC-Rückfall, späterem Ausfall/Wiederherstellung und unverändertem DATE$/TIME$/NOW/TIMER-Versatz.
## 3. Abnahme
- [ ] 3.1 Sprach-/Bibliotheksreferenz und Eingabe-/Zonenkorpus ergänzen; verifizieren mit cargo test --workspace, unveränderten vorhandenen Snapshots und openspec validate laufzeit-eingabe-und-systemstatus --strict.
- [x] 3.1 Sprach-/Bibliotheksreferenz und Eingabe-/Zonenkorpus ergänzen; verifizieren mit cargo test --workspace, unveränderten vorhandenen Snapshots und openspec validate laufzeit-eingabe-und-systemstatus --strict.

View File

@@ -0,0 +1,110 @@
# Implementierungs- und Verifikationsnachweis
Change: `laufzeit-eingabe-und-systemstatus`, Stand 06.09.2026.
Grundlage: Arbeitsbaum auf `815825dde76eb8f4b48cb27d83f21732afe707c2`;
Proposal, Design, vier Delta-Specs und alle sechs Tasks abgeglichen.
## Ergebnis
| Dimension | Ergebnis |
|---|---|
| Vollständigkeit | 6/6 Tasks, 6/6 Requirements, 21/21 Szenarien abgeglichen |
| Korrektheit | Alle zugeordneten Regressionen und Workspace-Tests bestanden |
| Kohärenz | D1D4 umgesetzt; keine zusätzlichen Dependencies |
| Offene Befunde | 0 CRITICAL, 0 WARNING, 0 SUGGESTION im Change |
F14F17 und F19 sind umgesetzt. Die vier Hauptspecs wurden am 06.09.2026
synchronisiert und alle 15 Hauptspecs streng validiert. Der Change ist unter
`openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus` archiviert.
## Umsetzung und Design
- **D1:** `Datei` verwendet einen `BufReader<File>` für den gemeinsamen Cursor.
`feld_lesen`, `zeile_lesen`, `zeichen_lesen`, EOF und SEEK erhalten den
Lesefortschritt. INPUT # liest einzelne Felder statt vollständiger Zeilen;
fehlende Felder erzeugen Fehler 62. Dekodierte Strings werden in der VM
nicht erneut von Anführungszeichen oder Rand-Leerraum befreit.
- **D2:** INPUT$ zählt UTF-8-Codepoints. Beschädigte oder abgeschnittene
UTF-8-Sequenzen führen zu Fehler 57; EOF vor einem geforderten Zeichen
zu Fehler 62. Ein bereits angenommener Tastaturrest wird vor weiteren
Tasten geliefert und nicht nochmals als Trap-/Forms-Ereignis eingereiht.
Direkter Builtin-Dispatch und resumierbare VM-Eingabe verwenden dieselbe
Aufteilung. Alle fünf CV-Funktionen prüfen exakte Breite und Bytezeichen.
GET/PUT und UTF-32-Recorddarstellung bleiben eigenständige Binärpfade.
- **D3:** `fehler_am_pfad` unterscheidet fehlenden Dateieintrag (53) von
fehlendem Elternverzeichnis (76) bei OPEN, KILL, NAME und MKDIR.
Vorhandene Berechtigungs-/Existenzfehler bleiben erhalten; ein Pfadbestandteil,
der kein Verzeichnis ist, wird ebenfalls als Pfadfehler eingeordnet.
- **D4:** TIMEZONEKNOWN ist als parameterlose INTEGER-Funktion durch Semantik,
HIR, Codegenerator, stabile neue Builtin-ID 156 und Dispatch geführt.
`RtState::zeitpunkt` löst Zeit und Status gemeinsam auf. Die Systemquelle
bleibt nach Ausfällen aktiv, sodass spätere Erholung sichtbar wird.
Tests können die Zonenquelle ersetzen, ohne die Rechnerzone zu verändern.
Programmversatz, monotone Ereigniszeit und Betriebssystemuhr werden durch
die Statusabfrage nicht verändert. CLI-Startwarnung und Inventar sind angepasst.
## Szenarien und ausführbare Nachweise
Testnamen ohne Präfix stehen in `crates/tb-vm/tests/vm.rs`. `runtime` meint
`crates/tb-runtime/src/builtins.rs`, `frontend` die Semantik bzw. Inventartests.
Die Korpusprogramme liegen in `tests/compat`; der Harness führt jedes zweimal
gegen seine festgeschriebene Sollausgabe aus.
| Requirement / Szenario | Nachweis |
|---|---|
| Sequenzielle Datenübertragung: WRITE # und INPUT # sind gegenläufig | `input_datei_bewahrt_felder_und_unterscheidet_leer_von_fehlend`; `eingabefortschritt.bas` |
| Lesen über das Dateiende | Derselbe Test: leere Datei, fehlendes zweites Feld und EOF; `input_datei_alle_lesearten_teilen_utf8_cursor_und_seek` |
| Felder über zwei Aufrufe | `input_datei_bewahrt_felder_und_unterscheidet_leer_von_fehlend`: WRITE a/b, anschließende getrennte Reads |
| Zu wenige Felder | Derselbe Test: mit/ohne finales Zeilenende Fehler 62; explizit leere Felder bleiben lesbar |
| Record-Konvertierungen: MK und CV sind gegenläufig | `alle_mk_cv_paare_verlangen_exakte_bytezeichenbreite`: alle fünf Paare |
| Falsche Argumentlänge | Derselbe Test: jeweils Zielbreite minus ein Zeichen → Fehler 5 |
| Zu langes CVI-Argument | Derselbe Test: jeweils Zielbreite plus ein Zeichen → Fehler 5; zusätzlich Nicht-Bytezeichen |
| Dateisystem: Rückwärts-Schrägstrich auf Unix | `datei_und_elternpfadfehler_sind_am_sprachzugang_unterscheidbar`: OPEN über Backslash, anschließendes NAME/Lesen/Löschen |
| Verzeichnis durchlaufen | Derselbe Test: DIR$ mit Muster und Folgeaufruf; vorhandener Dateikorpus |
| Fehlender Pfad | Derselbe Test: MKDIR, NAME-Ziel, CHDIR, RMDIR, DIR$ → 76 |
| OPEN mit fehlendem Elternpfad | Derselbe Test: INPUT mit fehlender Datei → 53, fehlendem Elternpfad → 76; zusätzlich OUTPUT/APPEND/BINARY/RANDOM |
| Zonenermittlung: Zone nicht ermittelbar | runtime `zonenquelle_ausfall_und_erholung_gelten_fuer_alle_zeitfunktionen`; `timezonefallback.bas` |
| Rückfallebene ist erkennbar | `timezoneknown_hat_integer_signatur_und_zeigt_aktuellen_status`; `timezoneknown.bas` und `timezonefallback.bas` |
| Rückfall nach Programmstart | Derselbe VM-Test über STOP/Fortsetzung und runtime-Zonenquelle mit Ausfall und Erholung; bekannte UTC liefert -1 |
| Signaturen: Element ohne Laufzeitverhalten wird namentlich abgewiesen | Vorhandener frontend-Test `dokumentierte_elemente_werden_namentlich_abgewiesen`; Inventar hat derzeit 0 offene Elemente |
| Argumentprüfung ohne Laufzeitverhalten | frontend `builtins_typen`: MKL$() hat benannte Argumentanzahl-Diagnose |
| Signatur der Zonenzustandsabfrage | frontend `builtins_typen`: 0 Argumente, INTEGER; TIMEZONEKNOWN(1) benannt abgewiesen; runtime-Test prüft tatsächliches `Value::Int` |
| Tastatur: INKEY$ bei leerem Puffer | `inkey_ohne_taste_ist_leer` |
| Sondertaste als zwei Zeichen | `inkey_liefert_zeichen_und_sondertaste` |
| Sondertastenfolge in zwei Reads | `input_s_teilt_sondertasten_und_bewahrt_rest_fuer_inkey`; `eingabefortschritt.bas`; direkter runtime-Dispatch |
| Unicode aus Datei | `input_datei_alle_lesearten_teilen_utf8_cursor_und_seek`: ä/Emoji, INPUT und BINARY |
Zusätzliche Randfälle: CR/LF/CRLF, Leerzeilen, eingebettete Kommas,
verdoppelte Anführungszeichen und Zeilenumbrüche, Null-Länge, negative Länge,
ungültiges UTF-8, falscher Dateimodus, SEEK nach EOF und gemischte Lesearten.
`unicode_puffergrenzen_und_binaere_records_bleiben_getrennt` prüft UTF-8 an
8-KiB-Puffergrenzen sowie GET/PUT zwischen INPUT$-Aufrufen.
`sondertastenrest_wird_nicht_erneut_getrappt` prüft F1 unter KEY STOP/ON.
Die bestehenden Programmversatz- und Ereignistests bestehen weiterhin.
## Abnahme
- `cargo test --workspace --quiet`: **463 bestanden, 0 fehlgeschlagen,
1 ignoriert**. Der ignorierte Golden-Generator wurde nicht ausgeführt.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: bestanden.
- `cargo fmt --all -- --check`: bestanden.
- `git diff --check`: bestanden vor dem Staging. Die abschließende
Prüfung des gestagten Changes mit `git diff --cached --check -- .
':(exclude)tests/compat/*.out'` besteht ebenfalls. Die neuen `.out`-Dateien
enthalten absichtlich signifikante PRINT-Leerzeichen am Zeilenende.
- `openspec validate laufzeit-eingabe-und-systemstatus --strict`: bestanden.
- Bestehende `tests/compat/*.out` unverändert; drei neue Programme mit
vorab festgelegten Sollausgaben ergänzt. Die Inventar-Abdeckungszahl
berücksichtigt die neue Erweiterung: 795 implementiert, 0 offen,
50 Non-Features, insgesamt 845 Einträge.
Die anfänglichen Testfehler (doppelte Quote-Auswertung, fehlende Korpus-
Rückfalldirektive, Test-Namensauflösung, Inventar-Gesamtzahl) sind behoben;
der abschließende Workspace-Lauf ist grün. Keine Tests oder vorhandenen
Sollausgaben wurden zum Übergehen dieser Fehler abgeschwächt.
Geprüft wurde headless auf macOS. Es erfolgte kein neuer interaktiver
Fremdprogrammvergleich und keine Windows-/Linux-Ausführung. Die ergänzenden
Korpusfälle belegen den erklärten TerminalBasic-Vertrag, keine historische
Bytekompatibilität. Der verbleibende Change
`spezifikationsabgleich-und-regressionsnachweise` bleibt eigenständig.

View File

@@ -46,6 +46,8 @@ und dabei Anführungszeichen auswerten, `LINE INPUT #` die ganze Zeile ohne
Zeilenende. `EOF` SHALL das Erreichen des Dateiendes melden; Lesen über das
Dateiende hinaus MUST Laufzeitfehler 62 „Input past end of file" auslösen.
INPUT # SHALL nicht konsumierte Felder einer Zeile für spätere Aufrufe bewahren und bei Bedarf über Zeilen hinweg weiterlesen. Fehlende geforderte Felder am Dateiende MUST Fehler 62 auslösen statt als leere Ersatzwerte zu gelten.
#### Scenario: WRITE # und INPUT # sind gegenläufig
- **WHEN** `WRITE #1, "a,b", 5` geschrieben und die Zeile mit `INPUT #1, s$, n` gelesen wird
- **THEN** enthält `s$` den Text `a,b` und `n` den Wert 5
@@ -54,6 +56,14 @@ Dateiende hinaus MUST Laufzeitfehler 62 „Input past end of file" auslösen.
- **WHEN** `INPUT #1, x` nach dem letzten Datensatz ausgeführt wird
- **THEN** tritt Laufzeitfehler 62 auf
#### Scenario: Felder über zwei Aufrufe
- **WHEN** WRITE #1, "a", "b" geschrieben und nach Wiederöffnung zweimal INPUT #1 mit je einer Stringvariablen ausgeführt wird
- **THEN** liefern die Aufrufe a und b
#### Scenario: Zu wenige Felder
- **WHEN** nur ein Feld vorhanden ist und INPUT #1 zwei Felder verlangt
- **THEN** tritt Fehler 62 auf
### Requirement: Wahlfreier Zugriff mit Record-Puffern
`OPEN … FOR RANDOM … LEN = n` SHALL Datensätze fester Länge bereitstellen.
`GET` und `PUT` SHALL einen Datensatz an einer 1-basierten Recordnummer
@@ -83,6 +93,8 @@ Microsoft-Binärformat älterer BASIC-Versionen erfüllen; ist dieses
Format ein deklariertes Non-Feature, MUST die Ablehnung namentlich
erfolgen und in der Sprachreferenz unter „Abweichungen" stehen.
Die Argumentlänge der CV-Funktionen SHALL exakt ihrer Zielbreite entsprechen, nicht nur einer Mindestbreite.
#### Scenario: MK und CV sind gegenläufig
- **WHEN** `CVL(MKL$(123456))` ausgewertet wird
- **THEN** ist das Ergebnis 123456
@@ -91,6 +103,10 @@ erfolgen und in der Sprachreferenz unter „Abweichungen" stehen.
- **WHEN** `CVI("x")` mit einem ein Zeichen langen String ausgewertet wird
- **THEN** tritt Laufzeitfehler 5 auf
#### Scenario: Zu langes CVI-Argument
- **WHEN** CVI("abc") ausgewertet wird
- **THEN** tritt Laufzeitfehler 5 auf
### Requirement: Festtextfelder als UTF-32 mit fester Recordlänge
Feste Strings (`STRING * n`) in Recordpuffern SHALL als UTF-32 gespeichert
werden — 4 Bytes je Zeichen, damit `STRING * n` stets genau `4 * n` Bytes
@@ -165,6 +181,10 @@ Pfade Fehler 76 „Path not found".
- **WHEN** `MKDIR` in einem nicht existierenden Elternverzeichnis aufgerufen wird
- **THEN** tritt Laufzeitfehler 76 auf
#### Scenario: OPEN mit fehlendem Elternpfad
- **WHEN** OPEN FOR INPUT einen Dateinamen in einem nicht vorhandenen Elternverzeichnis verwendet
- **THEN** tritt Fehler 76 auf; fehlt nur die Datei in einem vorhandenen Verzeichnis, tritt Fehler 53 auf
### Requirement: LPRINT als Druckausgabe
`LPRINT` und `LPRINT USING` SHALL wie `PRINT` formatieren und in einen
Druckerkanal schreiben. `LPOS` SHALL die aktuelle Spalte im

View File

@@ -265,6 +265,8 @@ Rückfallebene greift, MUST für ein Programm erkennbar sein und in
docs/sprachreferenz.md beschrieben stehen; ein stiller Wechsel auf UTC
MUST NOT vorkommen.
Die parameterlose INTEGER-Funktion TIMEZONEKNOWN SHALL -1 für eine ermittelte Systemzone und 0 für den UTC-Rückfall liefern. Auch eine nach Programmstart scheiternde Zonenermittlung SHALL diesen Zustand sichtbar ändern; die Abfrage SHALL keine Zeitzone oder Systemuhr verstellen.
#### Scenario: Zone nicht ermittelbar
- **WHEN** die Zeitzone des Rechners nicht bestimmt werden kann
- **THEN** rechnen die Zeitfunktionen in UTC und das Programm läuft ohne Fehler weiter
@@ -273,6 +275,10 @@ MUST NOT vorkommen.
- **WHEN** ein Programm den Zonenzustand abfragt
- **THEN** unterscheidet die Antwort erkennbar zwischen ermittelter Zone und Rückfall auf UTC
#### Scenario: Rückfall nach Programmstart
- **WHEN** zunächst eine Zone ermittelt wurde, eine spätere Ermittlung aber scheitert
- **THEN** verwenden die Zeitfunktionen UTC und TIMEZONEKNOWN liefert 0
### Requirement: Programmversatz wirkt auf die Ortszeit
`DATE$ = text$` und `TIME$ = text$` SHALL weiterhin nicht die Systemuhr
stellen, sondern einen programmeigenen Versatz setzen. Dieser Versatz

View File

@@ -139,6 +139,8 @@ Funktionen, die Record-Konvertierungsfunktionen (`MK…$`/`CV…`), die
Dateisystem-Funktionen `CURDIR$`, `DIR$`, `CHDRIVE`, sowie `LPOS`,
`STACK`, `SYSTEM`, `ERDEV`/`ERDEV$` und `SetFormatCC`.
Die TerminalBasic-Erweiterung TIMEZONEKNOWN SHALL als parameterlose INTEGER-Funktion eine Signatur besitzen und im Inventar mit ihrer tatsächlichen Umsetzung geführt sein.
#### Scenario: Element ohne Laufzeitverhalten wird namentlich abgewiesen
- **WHEN** ein Modul eine im Inventar als `offen` geführte Funktion verwendet
- **THEN** nennt die Ablehnung deren Namen und nicht „unbekannter Bezeichner"
@@ -147,6 +149,10 @@ Dateisystem-Funktionen `CURDIR$`, `DIR$`, `CHDRIVE`, sowie `LPOS`,
- **WHEN** ein Modul `MKL$()` ohne Argument enthält
- **THEN** meldet die Semantik eine Argumentanzahl-Diagnose, die `MKL$` nennt
#### Scenario: Signatur der Zonenzustandsabfrage
- **WHEN** PRINT TIMEZONEKNOWN übersetzt wird
- **THEN** entsteht eine INTEGER-Funktionsabfrage; TIMEZONEKNOWN(1) wird mit benannter Argumentanzahl-Diagnose abgewiesen
### Requirement: Modulweites ON ERROR ist aus Prozeduren ansprechbar
`ON ERROR GOTO label` ohne `LOCAL` SHALL einen modulweiten Fehlerhandler
setzen, dessen Sprungziel auch dann aufgelöst wird, wenn die Anweisung in

View File

@@ -154,6 +154,8 @@ Zeichentaste, eine zwei Zeichen lange Folge mit führendem Nullzeichen bei
einer Sondertaste. `INPUT$(n [, #dateinummer])` SHALL genau `n` Zeichen
lesen und dabei blockieren, ohne sie am Bildschirm zu wiederholen.
INPUT$ SHALL n Unicode-Codepoints liefern. Nicht verbrauchte Zeichen einer Sondertastenfolge und noch nicht gelesene Dateizeichen SHALL für Folgeaufrufe erhalten bleiben. Dateieingabe SHALL UTF-8 vor der Zeichenzählung decodieren.
#### Scenario: INKEY$ bei leerem Tastaturpuffer
- **WHEN** `INKEY$` ohne anstehende Taste ausgewertet wird
- **THEN** liefert es den leeren String und blockiert nicht
@@ -162,6 +164,14 @@ lesen und dabei blockieren, ohne sie am Bildschirm zu wiederholen.
- **WHEN** F1 gedrückt wurde und `INKEY$` ausgewertet wird
- **THEN** hat das Ergebnis die Länge 2 und beginnt mit dem Nullzeichen
#### Scenario: Sondertastenfolge in zwei Reads
- **WHEN** F1 ansteht und zweimal INPUT$(1) ausgeführt wird
- **THEN** liefert der erste Aufruf das Nullzeichen und der zweite den Scancode, jeweils genau ein Zeichen
#### Scenario: Unicode aus Datei
- **WHEN** eine UTF-8-Datei mit ä beginnt und INPUT$(1,#1) aufgerufen wird
- **THEN** liefert der Aufruf ä ohne Ersatzzeichen
### Requirement: Keine feste Bildschirmgröße im Verhalten
Kein beobachtbares Verhalten SHALL eine feste Spalten- oder Zeilenzahl
voraussetzen. Wo die Referenz des Vorbilds von 80×25 spricht, ist stets

View File

@@ -18,3 +18,10 @@ Konventionen:
- Nur Konsolen-E/A (keine Forms) bis Phase 4
- Kein Zufall/Zeit, außer der Test fixiert die Saat (`RANDOMIZE` mit
Konstante) bzw. prüft nur Invarianten
Die Programme `eingabefortschritt`, `timezoneknown` und `timezonefallback`
prüfen den TerminalBasic-Vertrag für Codepoints, gemeinsame Dateileseposition
und den sichtbaren Zonenzustand. `tb-tz: +00:00` setzt eine bekannte UTC-Zone,
`tb-tz: unknown` erzwingt den UTC-Rückfall. Späterer Ausfall und Erholung
werden zusätzlich mit einer ersetzbaren Zonenquelle in den Laufzeit- und
VM-Tests geprüft, ohne die Zeitzone des Rechners zu verändern.

View File

@@ -0,0 +1,21 @@
' tb-screen: 80x25
' tb-tempdir
' tb-keys: <F1>ä😀
' TerminalBasic-Codepointvertrag; keine historische Bytekompatibilitaet.
a$ = INPUT$(1)
b$ = INPUT$(1)
PRINT LEN(a$); ASC(a$); LEN(b$); ASC(b$); INPUT$(2)
OPEN "eingabe.txt" FOR OUTPUT AS #1
WRITE #1, "a,b", "ä😀", 5
WRITE #1, "Ende"
CLOSE
OPEN "eingabe.txt" FOR INPUT AS #1
INPUT #1, a$
INPUT #1, b$, n%
PRINT a$; ":"; b$; n%
PRINT INPUT$(1, #1)
LINE INPUT #1, a$
PRINT a$; EOF(1)
CLOSE
KILL "eingabe.txt"
END

View File

@@ -0,0 +1,4 @@
1 0 1 59 ä😀
a,b:ä😀 5
"
Ende"-1

View File

@@ -0,0 +1,10 @@
' tb-screen: 80x25
' tb-tz: unknown
' Expliziter UTC-Rueckfall bleibt auch mit programmeigenem Versatz sichtbar.
PRINT TIMEZONEKNOWN
DATE$ = "03-15-1992"
TIME$ = "13:45:30"
PRINT TIMEZONEKNOWN
PRINT DATE$; " "; TIME$
PRINT YEAR(NOW); HOUR(NOW); INT(TIMER)
END

View File

@@ -0,0 +1,4 @@
0
0
03-15-1992 13:45:30
1992 13 49530

View File

@@ -0,0 +1,10 @@
' tb-screen: 80x25
' tb-tz: +00:00
' Additive TerminalBasic-Erweiterung: bekannte UTC ist kein Rueckfall.
PRINT TIMEZONEKNOWN
DATE$ = "03-15-1992"
TIME$ = "13:45:30"
PRINT TIMEZONEKNOWN
PRINT DATE$; " "; TIME$
PRINT YEAR(NOW); HOUR(NOW); INT(TIMER)
END

View File

@@ -0,0 +1,4 @@
-1
-1
03-15-1992 13:45:30
1992 13 49530