diff --git a/crates/tb-cli/tests/foreign.rs b/crates/tb-cli/tests/foreign.rs new file mode 100644 index 0000000..d8cc5b0 --- /dev/null +++ b/crates/tb-cli/tests/foreign.rs @@ -0,0 +1,188 @@ +//! Optionaler öffentlicher Bestand; kein Netzwerk und keine GUI. +use std::{path::PathBuf, process::Command, time::Duration}; +use tb_runtime::host::{CaptureHost, Ereignis, Host}; +use tb_vm::{ + bytecode::CompiledModule, + interp::{RunEvent, Vm}, +}; +#[path = "../../../tests/support/prozessfrist.rs"] +mod prozessfrist; + +#[derive(Default)] +struct PruefHost { + inner: CaptureHost, + dialogantwort: Option, + dialog_gesehen: bool, +} +impl Host for PruefHost { + fn present(&mut self, screen: &tb_runtime::screen::TextScreen) { + self.dialog_gesehen |= tb_runtime::snapshot::text(screen).contains("Save changes to"); + self.inner.present(screen); + } + fn next_event(&mut self, block: bool) -> Option { + if block { + if let Some(event) = self.dialogantwort.take() { + return Some(event); + } + } + self.inner.next_event(block) + } + fn jetzt_ms(&mut self) -> u64 { + self.inner.jetzt_ms() + } + fn warten(&mut self, deadline: Option) -> Option { + self.inner.warten(deadline) + } +} + +const REVISION: &str = "1cdd2b32b829fe1721d0b6aecc433abc47a96fb6"; + +#[test] +#[ignore = "TB_VBDOS_REPO muss auf cout/vbdos in der dokumentierten Revision zeigen"] +fn oeffentliche_formularprogramme_sind_reproduzierbar_bedienbar() { + let _frist = + prozessfrist::Prozessfrist::neu("öffentlicher VBDOS-Bestand", Duration::from_secs(60)); + let repo = PathBuf::from(std::env::var_os("TB_VBDOS_REPO").expect("TB_VBDOS_REPO fehlt")); + let rev = Command::new("git") + .arg("-C") + .arg(&repo) + .args(["rev-parse", "HEAD"]) + .output() + .unwrap(); + assert!(rev.status.success()); + assert_eq!(String::from_utf8_lossy(&rev.stdout).trim(), REVISION); + let original = std::env::current_dir().unwrap(); + let base = std::env::temp_dir().join(format!("tb-foreign-{}", std::process::id())); + std::fs::create_dir_all(&base).unwrap(); + let archive = base.join("source.tar"); + assert!(Command::new("git") + .arg("-C") + .arg(&repo) + .args(["archive", "--format=tar", "-o"]) + .arg(&archive) + .arg(REVISION) + .status() + .unwrap() + .success()); + for (entry, keys) in [ + ("graphics/graphics.mak", vec![("x", 4)]), + ("microsoft/check.mak", vec![("f", 4), ("x", 0)]), + ("microsoft/qlbview.mak", vec![("\u{1b}", 0)]), + ("microsoft/seek.mak", vec![("x", 4)]), + ( + "microsoft/spindemo.mak", + vec![("\t", 0), ("\t", 0), ("\0H", 0)], + ), + ("microsoft/notepad.frm", vec![("f", 4), ("x", 0)]), + ("misc/mentors/mentors.frm", vec![("f", 4), ("x", 0)]), + ] { + let mut results = Vec::new(); + for repeat in 0..2 { + let dir = base.join(format!("run-{repeat}")); + std::fs::create_dir_all(&dir).unwrap(); + assert!(Command::new("tar") + .arg("-xf") + .arg(&archive) + .arg("-C") + .arg(&dir) + .status() + .unwrap() + .success()); + let path = dir.join(entry); + let build = Command::new(env!("CARGO_BIN_EXE_tbc")) + .arg("build") + .arg(&path) + .output() + .unwrap(); + assert!( + build.status.success() && build.stderr.is_empty(), + "{entry}: {build:?}" + ); + let module = + CompiledModule::from_tbc(&std::fs::read(path.with_extension("tbc")).unwrap()) + .unwrap(); + let mut vm = Vm::new(module); + vm.rt.screen.resize(100, 30); + vm.forms.resize(100, 30); + std::env::set_current_dir(path.parent().unwrap()).unwrap(); + let mut host = PruefHost::default(); + assert_eq!(vm.run(&mut host), RunEvent::Ended, "{entry}: Start"); + assert_eq!( + vm.run_visible_forms(&mut host), + RunEvent::Ended, + "{entry}: initiale Ereignisse" + ); + assert!( + vm.forms.has_visible_forms(), + "{entry}: kein sichtbares Startformular" + ); + let text1 = vm + .forms + .objects + .iter() + .position(|o| o.description.name.eq_ignore_ascii_case("Text1")); + let before = tb_runtime::snapshot::snapshot(&vm.rt.screen); + let mut states = Vec::new(); + for (key, shift) in &keys { + vm.rt.ende = false; + if entry == "microsoft/notepad.frm" && *key == "x" { + host.dialogantwort = Some(Ereignis::Taste("n".into(), 0)); + } + host.inner.ereignis(Ereignis::Taste((*key).into(), *shift)); + assert_eq!( + vm.run_visible_forms(&mut host), + RunEvent::Ended, + "{entry}: Taste {key:?}" + ); + states.push(( + vm.forms.has_visible_forms(), + vm.forms.menu_is_open(), + vm.forms.active_form(), + tb_runtime::snapshot::snapshot(&vm.rt.screen), + )); + } + if entry.contains("spindemo") { + let text_id = + tb_frontend::forms::property(tb_frontend::forms::ObjectClass::TextBox, "TEXT") + .unwrap() + .0; + assert_eq!( + vm.forms.get(text1.unwrap() as u16, text_id).unwrap(), + tb_ui::forms::PropertyValue::String(" 1".into()), + "{entry}: Spin-Handler hat Text1 nicht aktualisiert" + ); + assert!( + states[2].3 != states[1].3, + "Pfeiltaste muss den sichtbaren Wert ändern" + ); + } else { + assert!( + !vm.forms.has_visible_forms(), + "{entry}: Exit/Cancel hat das Formular nicht entladen" + ); + } + if keys.len() == 2 { + assert!( + states[0].1 && !states[1].1, + "{entry}: Menü öffnen und Exit auswählen" + ); + } + if entry == "microsoft/notepad.frm" { + assert!( + host.dialog_gesehen && host.dialogantwort.is_none(), + "Notepad: Save-Dialog mit N beantworten" + ); + } + assert!( + states.iter().any(|s| !s.0 || s.3 != before), + "{entry}: keine sichtbare Zustandsänderung" + ); + eprintln!("{entry}: Lauf {} bestanden", repeat + 1); + results.push(states); + std::env::set_current_dir(&original).unwrap(); + std::fs::remove_dir_all(&dir).unwrap(); + } + assert_eq!(results[0], results[1], "{entry}: nicht reproduzierbar"); + } + std::fs::remove_dir_all(base).unwrap(); +} diff --git a/crates/tb-frontend/tests/inventar.rs b/crates/tb-cli/tests/inventar.rs similarity index 52% rename from crates/tb-frontend/tests/inventar.rs rename to crates/tb-cli/tests/inventar.rs index 55a2960..fa1676a 100644 --- a/crates/tb-frontend/tests/inventar.rs +++ b/crates/tb-cli/tests/inventar.rs @@ -19,7 +19,7 @@ const OFFEN: &str = "offen"; const NON_FEATURE: &str = "Non-Feature"; fn wurzel() -> PathBuf { - // crates/tb-frontend/ → Projektwurzel + // crates/tb-cli/ → Projektwurzel PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") } @@ -599,3 +599,486 @@ fn signatur_greift_ohne_laufzeitverhalten() { "Diagnose nennt `MKL$` nicht: {texte:?}" ); } + +// Die Vorlagen sind feste Eingaben, keine Statusliste aus Implementierungstabellen. +fn probe_source(e: &Eintrag) -> (String, tb_frontend::forms::FormCatalog) { + use tb_frontend::forms::{self, ObjectClass, PropertyType}; + let mut catalog = forms::FormCatalog::default(); + catalog.add("Form1", ObjectClass::Form, None, false); + catalog.add("Probe", ObjectClass::TextBox, Some("Form1"), false); + if !matches!(e.art.as_str(), "Eigenschaft" | "Methode" | "Ereignis") { + let source = include_str!("../../../tests/support/inventar-quellen.tsv") + .lines() + .find_map(|line| { + let mut columns = line.split('\t'); + (columns.next() == Some(e.name.as_str()) && columns.next() == Some(e.art.as_str())) + .then(|| columns.next().unwrap().replace("\\n", "\n")) + }) + .unwrap_or_else(|| panic!("{}: Status {}, Programmvorlage fehlt", e.name, e.status)); + if e.name == "$FORM" { + catalog.objects.clear(); + } + return (source, catalog); + } + let (class_name, member) = e + .name + .split_once(if e.art == "Ereignis" { '_' } else { '.' }) + .unwrap(); + let class = ObjectClass::parse(class_name).unwrap(); + let mut catalog = forms::FormCatalog::default(); + catalog.add("Form1", ObjectClass::Form, None, false); + let object = if class == ObjectClass::Form { + "Form1" + } else if class == ObjectClass::Screen { + "SCREEN" + } else { + catalog.add("Probe", class, Some("Form1"), false); + "Probe" + }; + let source = match e.art.as_str() { + "Eigenschaft" => { + let (_, prop) = forms::property(class, &member.to_uppercase()).unwrap(); + let suffix = if matches!( + member.to_uppercase().as_str(), + "LIST" | "ITEMDATA" | "SELECTED" | "CONTROLPANEL" + ) { + "(0)" + } else { + "" + }; + if prop.ty == PropertyType::Object { + format!("DIM sink AS CONTROL\nsink = {object}.{member}{suffix}") + } else { + format!("PRINT {object}.{member}{suffix}") + } + } + "Methode" => { + let member = member.to_uppercase(); + let args = match member.as_str() { + "MOVE" => " 1, 1", + "DRAG" => " 0", + "ADDITEM" => " \"x\"", + "REMOVEITEM" => " 0", + "TEXTHEIGHT" | "TEXTWIDTH" => "(\"x\")", + _ => "", + }; + let prefix = if forms::method_return_type(class, &member).is_some() { + "PRINT " + } else { + "" + }; + format!("{prefix}{object}.{member}{args}") + } + _ => { + let args = forms::event_params(member) + .unwrap() + .iter() + .map(|(name, ty)| { + format!( + "{name} AS {}", + match ty { + forms::EventParamType::Integer => "INTEGER", + forms::EventParamType::Single => "SINGLE", + forms::EventParamType::Control => "CONTROL", + } + ) + }) + .collect::>() + .join(", "); + let prefix = if class == ObjectClass::Form { + "Form" + } else { + object + }; + format!("END\nSUB {prefix}_{member}({args})\nPRINT \"handler\"\nEND SUB") + } + }; + (source, catalog) +} + +fn pruefe_ausfuehrbaren_pfad(e: &Eintrag, bound: impl Fn(u16) -> bool) -> Result<(), String> { + let (src, catalog) = probe_source(e); + if matches!(e.name.as_str(), "$DYNAMIC" | "$STATIC") { + let parsed = tb_frontend::analyze_source("FORM1", &src); + let expected = e.name == "$STATIC"; + if !parsed.module.body.iter().any(|s| matches!(s, tb_frontend::ast::Stmt::MetaArrays { static_arrays, .. } if *static_arrays == expected)) { + return Err(format!("{}: Status {}, fehlender Metabefehl-Pfad", e.name, e.status)); + } + } + let compiled = if e.name == "$INCLUDE" { + // Einbindung gehört zum CLI-Projektlader, nicht zum Einzelmodul-Frontend. + let dir = std::env::temp_dir().join(format!("tb-inventar-include-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("probe.bas"), src).unwrap(); + std::fs::write(dir.join("x.bi"), "CONST Included = 73\n").unwrap(); + let output = std::process::Command::new(env!("CARGO_BIN_EXE_tbc")) + .args(["build", dir.join("probe.bas").to_str().unwrap()]) + .output() + .unwrap(); + let result = if output.status.success() { + tb_vm::bytecode::CompiledModule::from_tbc( + &std::fs::read(dir.join("probe.tbc")).unwrap(), + ) + .map_err(|err| { + format!( + "{}: Status {}, ungültiges Kompilat: {err}", + e.name, e.status + ) + }) + } else { + Err(format!( + "{}: Status {}, CLI-Pfad fehlgeschlagen: {}", + e.name, + e.status, + String::from_utf8_lossy(&output.stderr) + )) + }; + std::fs::remove_dir_all(dir).unwrap(); + Ok(result?) + } else { + tb_vm::compile_source_with_forms("FORM1", &src, &catalog) + }; + pruefe_kompilat(e, compiled, bound) +} + +/// Feste Sollziele aus Sprache/Runtime-ABI; niemals aus dem gerade geprüften +/// Codegenerator gewonnen. Deklarationen prüfen ihr Modulresultat, Forms den +/// konkreten Member am tatsächlich abgesenkten Objekt. +fn pruefe_sollziel(e: &Eintrag, module: &tb_vm::bytecode::CompiledModule) -> Result<(), String> { + use tb_frontend::forms; + use tb_vm::bytecode::Instr; + let code: Vec<_> = module.procs.iter().flat_map(|p| &p.code).collect(); + let member_matches = |object: u16, member: u16, method: bool| { + module.objects.get(object as usize).is_some_and(|obj| { + let name = if method { + forms::methods(obj.class).get(member as usize).copied() + } else { + forms::properties(obj.class) + .get(member as usize) + .map(|p| p.name) + }; + name.is_some_and(|name| { + e.name + .eq_ignore_ascii_case(&format!("{}.{}", obj.class.name(), name)) + }) + }) + }; + let matches = match e.art.as_str() { + "Eigenschaft" => code.iter().any(|i| match i { + Instr::LoadObjectProperty(object, property, false) + | Instr::LoadObjectIndexedProperty(object, property) => { + member_matches(*object, *property, false) + } + _ => false, + }), + "Methode" => code.iter().any(|i| match i { + Instr::ObjectMethod(object, method, _) | Instr::ObjectMethodFn(object, method, _) => { + member_matches(*object, *method, true) + } + _ => false, + }), + "Ereignis" => module.event_procs.iter().any(|event| { + let class = module.objects[event.object as usize].class; + e.name + .eq_ignore_ascii_case(&format!("{}_{}", class.name(), event.event)) + && module.procs[event.proc as usize].code.iter().any(|i| { + matches!( + i, + Instr::CallBuiltin(tb_runtime::builtins::ids::PRINT_VAL, _) + ) + }) + }), + _ => { + let expected = include_str!("../../../tests/support/inventar-quellen.tsv") + .lines() + .find_map(|line| { + let columns: Vec<_> = line.split('\t').collect(); + (columns[0] == e.name && columns[1] == e.art).then_some(columns[3]) + }) + .unwrap(); + expected.split('|').all(|contract| { + if let Some(id) = contract.strip_prefix("builtin:") { + let id: u16 = id.parse().unwrap(); + return code + .iter() + .any(|i| matches!(i, Instr::CallBuiltin(actual, _) if *actual == id)); + } + if let Some(op) = contract.strip_prefix("code:") { + return code.iter().any(|i| format!("{i:?}").starts_with(op)); + } + if let Some(global) = contract.strip_prefix("global:") { + let (name, ty) = global.split_once(':').unwrap(); + return module + .global_names + .iter() + .zip(&module.globals_init) + .any(|(actual, init)| actual == name && format!("{init:?}") == ty); + } + if let Some(name) = contract.strip_prefix("proc:") { + return module.procs.iter().any(|p| p.name == name); + } + match contract { + "base:1" => module.option_base == 1, + "data:1" => module.data.iter().any(|d| d.text == "1"), + "udt:R" => module + .udts + .iter() + .any(|u| u.name == "R" && u.fields == [tb_runtime::value::TypeInit::Int]), + "comment" => code + .iter() + .all(|i| matches!(i, Instr::Source(..) | Instr::Stmt(_) | Instr::End)), + _ => panic!("{}: unbekannter Sollvertrag {contract}", e.name), + } + }) + } + }; + if matches { + Ok(()) + } else { + Err(format!( + "{}: Status {}, falsches oder fehlendes Laufzeitziel: {code:?}", + e.name, e.status + )) + } +} + +fn pruefe_kompilat( + e: &Eintrag, + compiled: Result>, + bound: impl Fn(u16) -> bool, +) -> Result<(), String> { + use tb_vm::bytecode::Instr; + let actual = match compiled { + Err(d) if d.iter().any(|d| d.message.contains("Feature unavailable")) => { + let name = e + .name + .to_uppercase() + .replace(" (EREIGNIS)", "") + .replace(" (GRAFIK)", ""); + let name = name + .strip_prefix("ON ") + .or_else(|| name.strip_prefix("OPEN ")) + .unwrap_or(&name); + if !d.iter().any(|d| { + d.message.contains("Feature unavailable") && d.message.to_uppercase().contains(name) + }) { + return Err(format!( + "{}: Status {}, falsche Non-Feature-Diagnose: {d:?}", + e.name, e.status + )); + } + NON_FEATURE + } + Err(d) => { + return Err(format!( + "{}: Status {}, kein erreichbarer HIR-Pfad: {d:?}", + e.name, e.status + )) + } + Ok(module) => { + if module + .procs + .iter() + .flat_map(|p| &p.code) + .any(|i| matches!(i, Instr::Unsupported(_))) + { + OFFEN + } else { + for i in module.procs.iter().flat_map(|p| &p.code) { + if let Instr::CallBuiltin(id, _) = i { + if !bound(*id) { + return Err(format!( + "{}: Status {}, fehlende Runtime-Bindung {id}", + e.name, e.status + )); + } + } + } + module.validate().map_err(|d| { + format!( + "{}: Status {}, ungültiges Laufzeitziel: {d:?}", + e.name, e.status + ) + })?; + pruefe_sollziel(e, &module)?; + IMPLEMENTIERT + } + } + }; + if e.status == actual { + Ok(()) + } else { + Err(format!( + "{}: Status {}, vorgefunden {actual}", + e.name, e.status + )) + } +} + +#[test] +fn jeder_inventareintrag_erreicht_hir_und_laufzeitziel() { + let errors: Vec<_> = inventar() + .iter() + .filter_map(|e| { + pruefe_ausfuehrbaren_pfad(e, |id| { + tb_runtime::builtins::builtin_table() + .get(id as usize) + .is_some() + }) + .err() + }) + .collect(); + assert!( + errors.is_empty(), + "{} Abweichungen:\n{}", + errors.len(), + errors.join("\n") + ); +} + +#[test] +fn falscher_status_und_fehlende_runtimebindung_werden_namentlich_erkannt() { + let inv = inventar(); + for (name, status) in [ + ("LOCATE", OFFEN), + ("LOCATE", NON_FEATURE), + ("PEEK", IMPLEMENTIERT), + ] { + let mut e = inv.iter().find(|e| e.name == name).unwrap().clone(); + e.status = status.into(); + let error = pruefe_ausfuehrbaren_pfad(&e, |_| true).unwrap_err(); + assert!(error.contains(name) && error.contains(status), "{error}"); + } + let e = inv.iter().find(|e| e.name == "TIMEZONEKNOWN").unwrap(); + let error = pruefe_ausfuehrbaren_pfad(e, |_| false).unwrap_err(); + assert!( + error.contains("TIMEZONEKNOWN") + && error.contains(IMPLEMENTIERT) + && error.contains("Runtime-Bindung"), + "{error}" + ); +} + +#[test] +fn implementiert_mit_unsupported_marker_faellt_durch() { + let inv = inventar(); + let e = inv.iter().find(|e| e.name == "LOCATE").unwrap(); + let mut module = tb_vm::compile_source("TEST", "LOCATE 1,1").unwrap(); + module.procs[0] + .code + .push(tb_vm::bytecode::Instr::Unsupported(0)); + let error = pruefe_kompilat(e, Ok(module), |_| true).unwrap_err(); + assert!( + error.contains("LOCATE") && error.contains(IMPLEMENTIERT) && error.contains(OFFEN), + "{error}" + ); +} + +#[test] +fn falsche_gueltige_builtinbindung_und_entfernte_absenkung_fallen_durch() { + use tb_vm::bytecode::Instr; + for e in inventar().iter().filter(|e| e.status == IMPLEMENTIERT) { + let (mut src, catalog) = probe_source(e); + if e.name == "$INCLUDE" { + src = "PRINT 73#".into(); + } + let expected = include_str!("../../../tests/support/inventar-quellen.tsv") + .lines() + .find_map(|line| { + let columns: Vec<_> = line.split('\t').collect(); + (columns[0] == e.name && columns[1] == e.art).then_some(columns[3]) + }); + if let Some(id) = expected.and_then(|s| s.strip_prefix("builtin:")) { + let id: u16 = id.split('|').next().unwrap().parse().unwrap(); + let mut module = tb_vm::compile_source_with_forms("FORM1", &src, &catalog).unwrap(); + let mut changed = false; + for i in module.procs.iter_mut().flat_map(|p| &mut p.code) { + if let Instr::CallBuiltin(actual, _) = i { + if *actual == id { + *actual = (id + 1) % tb_runtime::builtins::ids::COUNT; + changed = true; + } + } + } + assert!(changed, "{}: Gegenprobe traf kein Ziel", e.name); + let error = pruefe_kompilat(e, Ok(module), |_| true).unwrap_err(); + assert!( + error.contains(&e.name) && error.contains(IMPLEMENTIERT), + "{error}" + ); + } + if expected.is_some_and(|s| s.starts_with("builtin:") || s.starts_with("code:")) + || matches!(e.art.as_str(), "Eigenschaft" | "Methode" | "Ereignis") + { + let mut module = tb_vm::compile_source_with_forms("FORM1", &src, &catalog).unwrap(); + for proc in &mut module.procs { + proc.code.clear(); + } + let error = pruefe_kompilat(e, Ok(module), |_| true).unwrap_err(); + assert!( + error.contains(&e.name) && error.contains(IMPLEMENTIERT), + "{error}" + ); + } + } +} + +#[test] +fn forms_absenkung_muss_den_richtigen_member_erreichen() { + use tb_vm::bytecode::Instr; + for name in ["TextBox.Text", "Form.Show"] { + let e = inventar() + .into_iter() + .find(|e| e.name.eq_ignore_ascii_case(name)) + .unwrap(); + let (src, catalog) = probe_source(&e); + let mut module = tb_vm::compile_source_with_forms("FORM1", &src, &catalog).unwrap(); + let mut changed = false; + for i in module.procs.iter_mut().flat_map(|p| &mut p.code) { + match i { + Instr::LoadObjectProperty(object, property, _) => { + *property = + tb_frontend::forms::property(module.objects[*object as usize].class, "TAG") + .unwrap() + .0; + changed = true; + } + Instr::ObjectMethod(object, method, _) => { + *method = tb_frontend::forms::methods(module.objects[*object as usize].class) + .iter() + .position(|m| *m == "HIDE") + .unwrap() as u16; + changed = true; + } + _ => {} + } + } + assert!(changed); + let error = pruefe_kompilat(&e, Ok(module), |_| true).unwrap_err(); + assert!( + error.contains(&e.name) && error.contains(IMPLEMENTIERT), + "{error}" + ); + } +} + +#[test] +fn gemeinsam_dispatchte_konverter_brauchen_den_richtigen_typselektor() { + use tb_vm::bytecode::Instr; + for name in ["MKI$", "CVD"] { + let e = inventar().into_iter().find(|e| e.name == name).unwrap(); + let (src, catalog) = probe_source(&e); + let mut module = tb_vm::compile_source_with_forms("FORM1", &src, &catalog).unwrap(); + let mut changed = false; + for i in module.procs.iter_mut().flat_map(|p| &mut p.code) { + if let Instr::PushLng(kind) = i { + *kind = 2; + changed = true; + } + } + assert!(changed); + assert!(pruefe_kompilat(&e, Ok(module), |_| true) + .unwrap_err() + .contains(name)); + } +} diff --git a/crates/tb-cli/tests/project.rs b/crates/tb-cli/tests/project.rs index f9a6b2a..d77eefc 100644 --- a/crates/tb-cli/tests/project.rs +++ b/crates/tb-cli/tests/project.rs @@ -128,3 +128,58 @@ fn cli_meldet_physische_quellorte_auch_nach_verschachtelten_includes() { assert!(tbc(&dir, "run", "empty.bas").status.success()); std::fs::remove_dir_all(dir).unwrap(); } + +#[test] +fn erlaubte_grafik_und_benannte_nonfeatures_werden_per_cli_geprueft() { + let dir = std::env::temp_dir().join(format!("tb-graphics-check-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("probe.bas"); + for mode in 0..=13 { + std::fs::write( + &path, + format!("SCREEN {mode}\nLINE (1,1)-(4,4),1\nPAINT (2,2),1\nVIEW (0,0)-(10,10)\n"), + ) + .unwrap(); + let out = tbc(&dir, "check", "probe.bas"); + assert!( + out.status.success() && out.stderr.is_empty(), + "SCREEN {mode}: {out:?}" + ); + } + for (name, source) in [ + ("POKE", "POKE 1,2"), + ("PEEK", "PRINT PEEK(1)"), + ("CIRCLE", "CIRCLE (1,1),1"), + ("WINDOW", "WINDOW (0,0)-(10,10)"), + ("IOCTL", "IOCTL #1, \"x\""), + ("PSET", "PSET (1,1),2"), + ("PSET", "PSET STEP (1,1),2"), + ("PRESET", "PRESET (1,1),2"), + ("PRESET", "PRESET STEP (1,1),2"), + ("CIRCLE", "CIRCLE STEP (1,1),2"), + ("WINDOW", "WINDOW SCREEN (0,0)-(10,10)"), + ("WINDOW", "WINDOW"), + ("PALETTE", "DIM a%(15)\nPALETTE USING a%(0)"), + ("PALETTE", "PALETTE"), + ("PALETTE", "IF 1 THEN PALETTE USING a%(0)"), + ] { + std::fs::write(&path, source).unwrap(); + let out = tbc(&dir, "check", "probe.bas"); + let message = String::from_utf8_lossy(&out.stderr); + assert!( + !out.status.success() + && message.contains("Feature unavailable") + && message.contains(name), + "{out:?}" + ); + } + // Bibliotheksnamen sind keine reservierten Variablennamen. + std::fs::write( + &path, + "DIM PSET(2), PALETTE(2)\nPSET(1)=3\nPALETTE(1)=4\nPRINT PSET(1),PALETTE(1)", + ) + .unwrap(); + let out = tbc(&dir, "check", "probe.bas"); + assert!(out.status.success(), "{out:?}"); + std::fs::remove_dir_all(dir).unwrap(); +} diff --git a/crates/tb-frontend/src/parser.rs b/crates/tb-frontend/src/parser.rs index 33d7203..107d01f 100644 --- a/crates/tb-frontend/src/parser.rs +++ b/crates/tb-frontend/src/parser.rs @@ -194,7 +194,6 @@ impl<'a> P<'a> { fn parse_statement(&mut self) -> Option { let pos = self.pos(); - // Zeilennummern und Labels nur am Zeilenanfang if self.at_line_start { if let TokenKind::Num(NumValue::Int(n)) = self.k() { @@ -611,7 +610,10 @@ impl<'a> P<'a> { self.advance(); if self.k() == TokenKind::LParen { // Grafikform GET (x1,y1)-(x2,y2): deklariertes Non-Feature - self.err("Feature unavailable"); + self.err(format!( + "Feature unavailable: {}", + if k == Kw::Get { "GET" } else { "PUT" } + )); self.sync(); return None; } @@ -1804,7 +1806,7 @@ impl<'a> P<'a> { // Non-Feature — an der Syntax erkennbar, weil `DEF` sonst nur // `DEF FNname` einleitet. if matches!(self.k(), TokenKind::Ident { ref name, suffix: None } if name == "SEG") { - self.err("Feature unavailable"); + self.err("Feature unavailable: DEF SEG"); self.sync(); return None; } @@ -1885,6 +1887,21 @@ impl<'a> P<'a> { self.sync(); return None; } + // Ausgeschlossene Anweisungen besitzen eigene Koordinaten-/Dateisyntax. + // Erst Zuweisungen erkennen, damit gleichnamige Variablen/Arrays gültig bleiben. + if let Expr::Name { + name, suffix: None, .. + } = &target + { + if matches!( + name.as_str(), + "CIRCLE" | "WINDOW" | "IOCTL" | "PSET" | "PRESET" | "PALETTE" + ) { + self.err(format!("Feature unavailable: {name}")); + self.sync(); + return None; + } + } // Ereignissteuerung: `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` … if let Expr::Name { ref name, diff --git a/crates/tb-frontend/src/sema.rs b/crates/tb-frontend/src/sema.rs index 473de42..3257ff9 100644 --- a/crates/tb-frontend/src/sema.rs +++ b/crates/tb-frontend/src/sema.rs @@ -265,7 +265,8 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> { (5, 5, &[N, N, N, N, N], Db) } "IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => (6, 6, &[N, N, N, N, N, N], Db), - "NPV" | "NPV#" | "IRR" | "IRR#" => (2, 2, &[N, A], Db), + "NPV" | "NPV#" => (2, 2, &[N, A], Db), + "IRR" | "IRR#" => (2, 2, &[A, N], Db), "MIRR" | "MIRR#" => (3, 3, &[A, N, N], Db), "SLN" | "SLN#" => (3, 3, &[N, N, N], Db), "SYD" | "SYD#" | "DDB" | "DDB#" => (4, 4, &[N, N, N, N], Db), @@ -278,6 +279,7 @@ fn builtin_fn(name: &str) -> Option<(u8, u8, &'static [ArgK], RetK)> { "CVD" | "CVDMBF" => (1, 1, &[S], Db), "CVC" => (1, 1, &[S], Cu), // Dateisystem und System + "SHELL" => (1, 1, &[S], L), "CURDIR$" => (0, 1, &[S], St), "DIR$" => (0, 1, &[S], St), "LPOS" => (1, 1, &[N], I), @@ -2861,7 +2863,7 @@ impl Sema { } => { let Some(art) = trap_art(device) else { // COM/PEN/PLAY/STRIG sind Non-Feature — namentlich. - self.err(*pos, "Feature unavailable"); + self.err(*pos, format!("Feature unavailable: {device}")); return; }; // Wertebereich prüfen, soweit die Kennung konstant ist. @@ -2908,7 +2910,7 @@ impl Sema { } => { match device.as_str() { "TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "EVENT" => {} - _ => self.err(*pos, "Feature unavailable"), + _ => self.err(*pos, format!("Feature unavailable: {device}")), } if let Some(i) = index { self.want_num(i, scope); @@ -2939,7 +2941,7 @@ impl Sema { return; } let Some(art) = trap_art(device) else { - self.err(*pos, "Feature unavailable"); + self.err(*pos, format!("Feature unavailable: {device}")); return; }; if let Some(i) = index { @@ -3566,7 +3568,7 @@ impl Sema { for a in args { self.lower_expr(a, scope); } - self.err(pos, "Feature unavailable"); + self.err(pos, format!("Feature unavailable: {name}")); return; } self.err(pos, "Subprogram not defined"); @@ -4147,7 +4149,7 @@ impl Sema { fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) { if let Expr::StrLit(s, _) = file { if ist_com_geraet(s) { - self.err(pos, "Feature unavailable"); + self.err(pos, format!("Feature unavailable: {s}")); } } } @@ -4922,7 +4924,7 @@ impl Sema { for a in idx { self.lower_expr(a, scope); } - self.err(pos, "Feature unavailable"); + self.err(pos, format!("Feature unavailable: {full_name}")); return (HExpr::Int(0), Ty::Unknown); } // 4. Builtin-Funktion @@ -4982,7 +4984,7 @@ impl Sema { .is_some_and(|v| suffix.is_none_or(|s| v.ty == suffix_ty(s))); if !declared { if banned_feature(&full_name) { - self.err(pos, "Feature unavailable"); + self.err(pos, format!("Feature unavailable: {full_name}")); return (HExpr::Int(0), Ty::Unknown); } if let Some((0, _, _, _)) = builtin_fn(&full_name) { @@ -5628,13 +5630,13 @@ mod tests { #[test] fn hardware_features_zur_compilezeit_abgelehnt() { - assert!(diags("POKE 100, 1").contains(&"Feature unavailable".to_string())); - assert!(diags("x = PEEK(100)").contains(&"Feature unavailable".to_string())); - assert!(diags("p = VARPTR(a%)").contains(&"Feature unavailable".to_string())); - assert!(diags("SOUND 440, 10").contains(&"Feature unavailable".to_string())); - assert!(diags("CHAIN \"prog\"").contains(&"Feature unavailable".to_string())); - assert!(diags("PLAY \"cde\"").contains(&"Feature unavailable".to_string())); - assert!(diags("CIRCLE 1, 2").contains(&"Feature unavailable".to_string())); + assert!(diags("POKE 100, 1").contains(&"Feature unavailable: POKE".to_string())); + assert!(diags("x = PEEK(100)").contains(&"Feature unavailable: PEEK".to_string())); + assert!(diags("p = VARPTR(a%)").contains(&"Feature unavailable: VARPTR".to_string())); + assert!(diags("SOUND 440, 10").contains(&"Feature unavailable: SOUND".to_string())); + assert!(diags("CHAIN \"prog\"").contains(&"Feature unavailable: CHAIN".to_string())); + assert!(diags("PLAY \"cde\"").contains(&"Feature unavailable: PLAY".to_string())); + assert!(diags("CIRCLE 1, 2").contains(&"Feature unavailable: CIRCLE".to_string())); } #[test] @@ -5726,7 +5728,7 @@ mod tests { fn datei_ea_und_events() { let src = "OPEN \"test.dat\" FOR RANDOM AS #1 LEN = 64\nCLOSE #1\nOPEN \"o\", #2, \"f.txt\"\nCLOSE\nTIMER ON\nKEY(5) OFF"; assert!(diags(src).is_empty(), "{:?}", diags(src)); - assert!(diags("PEN ON").contains(&"Feature unavailable".to_string())); + assert!(diags("PEN ON").contains(&"Feature unavailable: PEN".to_string())); } fn form_catalog() -> FormCatalog { diff --git a/crates/tb-ui/src/forms.rs b/crates/tb-ui/src/forms.rs index b32ad18..68ca18a 100644 --- a/crates/tb-ui/src/forms.rs +++ b/crates/tb-ui/src/forms.rs @@ -75,6 +75,7 @@ pub struct FormsModel { visible_forms: Vec, active_form: Option, active_control: Option<(u16, Option)>, + dropdown: Option, width: usize, height: usize, screen_visible: bool, @@ -103,16 +104,35 @@ impl FormsModel { }; if self.root_form(control) == Some(form) { self.active_control = None; + self.dropdown = None; } } + fn activate_form(&mut self, form: Option) { + if self.active_form == form { + return; + } + if let Some(old) = self.active_form { + self.queue_named((old, None), "LOSTFOCUS", vec![]); + } + self.active_form = form; + self.dropdown = None; + if let Some(new) = form { + self.queue_named((new, None), "GOTFOCUS", vec![]); + } + } + pub fn new(objects: Vec, width: usize, height: usize) -> Self { let objects = objects .into_iter() .map(|description| { - let properties = forms::properties(description.class) + let mut properties: Vec<_> = forms::properties(description.class) .into_iter() .map(|p| PropertyValue::from_default(p.default, p.ty)) .collect(); + if let Some((id, _)) = forms::property(description.class, "PARENT") { + properties[id as usize] = + PropertyValue::Object(description.parent.map(|id| (id, None))); + } let loaded = description.class == ObjectClass::Screen; ObjectInstance { description, @@ -132,6 +152,7 @@ impl FormsModel { visible_forms: Vec::new(), active_form: None, active_control: None, + dropdown: None, width, height, screen_visible: true, @@ -251,10 +272,6 @@ impl FormsModel { .unwrap_or(1); return Ok(PropertyValue::Integer(value.saturating_sub(2))); } - if spec.name == "PARENT" { - let parent = obj.description.parent.map(|id| (id, None)); - return Ok(PropertyValue::Object(parent)); - } Ok(obj.properties[property as usize].clone()) } @@ -420,6 +437,7 @@ impl FormsModel { self.replace_selection(key, &replacement)?; return Ok(()); } + let changed = self.value(key, spec.name) != Some(&value); let timer_reset = class == ObjectClass::Timer && (spec.name == "INTERVAL" || (spec.name == "ENABLED" && self.value(key, "ENABLED") != Some(&value))); @@ -433,6 +451,12 @@ impl FormsModel { ); } } + if changed && class == ObjectClass::Form && matches!(spec.name, "WIDTH" | "HEIGHT") { + self.queue_named(key, "RESIZE", vec![]); + } + if changed && class == ObjectClass::Label && spec.name == "CAPTION" { + self.queue_named(key, "CHANGE", vec![]); + } if spec.name == "SORTED" && self.boolean(key, "SORTED") { self.sort_list(key)?; } @@ -464,6 +488,17 @@ impl FormsModel { ) && matches!(spec.name, "PATH" | "DRIVE" | "PATTERN") { self.refresh_filesystem(key)?; + if changed { + let event = match (class, spec.name) { + (ObjectClass::FileListBox, "PATTERN") => "PATTERNCHANGE", + (ObjectClass::FileListBox | ObjectClass::DirListBox, "PATH") => "PATHCHANGE", + _ => "CHANGE", + }; + self.queue_named(key, event, vec![]); + if class == ObjectClass::DirListBox { + self.queue_named(key, "CHANGE", vec![]); + } + } } self.dirty = true; Ok(()) @@ -566,7 +601,8 @@ impl FormsModel { Self::set_visible_property(obj, true); self.visible_forms.retain(|id| *id != object); self.visible_forms.push(object); - self.active_form = Some(object); + self.activate_form(Some(object)); + self.queue_named((object, None), "PAINT", vec![]); self.dirty = true; Ok(if modal { ShowResult::ModalWait @@ -590,7 +626,7 @@ impl FormsModel { self.modal.pop(); } if self.active_form == Some(object) { - self.active_form = self.visible_forms.last().copied(); + self.activate_form(self.visible_forms.last().copied()); } self.clear_active_control_for_form(object); self.reset_form_timers(object); @@ -629,7 +665,7 @@ impl FormsModel { self.modal.pop(); } if self.active_form == Some(object) { - self.active_form = self.visible_forms.last().copied(); + self.activate_form(self.visible_forms.last().copied()); } self.clear_active_control_for_form(object); self.reset_form_timers(object); @@ -693,6 +729,9 @@ impl FormsModel { .map(|_| ()) .ok_or(RuntimeError(340)); if removed.is_ok() { + if self.dropdown == Some((base, Some(index))) { + self.dropdown = None; + } self.reset_timer((base, Some(index))); self.lists.remove(&(base, Some(index))); } @@ -745,10 +784,21 @@ impl FormsModel { .ok_or(RuntimeError(420))? .description }; - if matches!(description.class, ObjectClass::Form | ObjectClass::Screen) { + if matches!( + description.class, + ObjectClass::Form + | ObjectClass::Screen + | ObjectClass::Frame + | ObjectClass::Label + | ObjectClass::Menu + | ObjectClass::Timer + ) { return Err(RuntimeError(421)); } - self.active_form = self.root_form(key); + self.activate_form(self.root_form(key)); + if self.active_control != Some(key) { + self.dropdown = None; + } self.active_control = Some(key); Ok(()) } @@ -822,14 +872,28 @@ impl FormsModel { }); } - fn root_form(&self, key: ObjectKey) -> Option { - let mut current = key.0; - for _ in 0..self.objects.len() { - let obj = self.objects.get(current as usize)?; - if obj.description.class == ObjectClass::Form { - return Some(current); + fn parent_key(&self, key: ObjectKey) -> Option { + match self.value(key, "PARENT") { + Some(PropertyValue::Object(parent)) => { + parent.map(|(id, index)| (id, index.filter(|i| *i != 0))) } - current = obj.description.parent?; + _ => self + .instance(key) + .ok()? + .description + .parent + .map(|id| (id, None)), + } + } + + fn root_form(&self, key: ObjectKey) -> Option { + let mut current = key; + for _ in 0..self.objects.len() + self.dynamic.len() { + let obj = self.instance(current).ok()?; + if obj.description.class == ObjectClass::Form { + return Some(current.0); + } + current = self.parent_key(current)?; } None } @@ -872,7 +936,10 @@ impl FormsModel { if let Some(old) = self.active_control { self.queue_named(old, "LOSTFOCUS", vec![]); } - self.active_form = self.root_form(key); + self.activate_form(self.root_form(key)); + if self.active_control != Some(key) { + self.dropdown = None; + } self.active_control = Some(key); self.queue_named(key, "GOTFOCUS", vec![]); self.dirty = true; @@ -949,7 +1016,7 @@ impl FormsModel { } fn select_option(&mut self, key: ObjectKey) -> Result<(), RuntimeError> { - let parent = self.instance(key)?.description.parent; + let parent = self.parent_key(key); let value_id = forms::property(ObjectClass::OptionButton, "VALUE") .unwrap() .0 as usize; @@ -959,7 +1026,7 @@ impl FormsModel { } let same_group = self.instance(other).is_ok_and(|obj| { obj.description.class == ObjectClass::OptionButton - && obj.description.parent == parent + && self.parent_key(other) == parent }); if same_group { self.instance_mut(other)?.properties[value_id] = PropertyValue::Integer(0); @@ -994,10 +1061,8 @@ impl FormsModel { let title_shortcut = name == "SHORTCUT" && matches!(value, PropertyValue::String(text) if !text.is_empty()) && self - .instance(key) - .ok() - .and_then(|obj| obj.description.parent) - .and_then(|parent| self.objects.get(parent as usize)) + .parent_key(key) + .and_then(|parent| self.instance(parent).ok()) .is_some_and(|parent| parent.description.class == ObjectClass::Form); if invalid || title_shortcut { Err(RuntimeError::ILLEGAL_FUNCTION_CALL) @@ -1163,12 +1228,6 @@ impl FormsModel { } entries.sort_by_key(|entry| entry.to_uppercase()); self.lists.insert(key, entries); - let event = match class { - ObjectClass::DirListBox => "PATHCHANGE", - ObjectClass::FileListBox if !self.string(key, "PATTERN").is_empty() => "PATTERNCHANGE", - _ => "CHANGE", - }; - self.queue_named(key, event, vec![]); self.dirty = true; Ok(()) } @@ -1204,7 +1263,12 @@ impl FormsModel { let class = self.instance(key)?.description.class; match name { "SETFOCUS" => self.focus(key)?, - "REFRESH" => self.dirty = true, + "REFRESH" => { + if matches!(class, ObjectClass::Form | ObjectClass::PictureBox) { + self.queue_named(key, "PAINT", vec![]); + } + self.dirty = true; + } "ADDITEM" => self.add_item( key, args.first().map(Self::text_of).unwrap_or_default(), @@ -1304,10 +1368,27 @@ impl FormsModel { } fn form_controls(&self, form: u16) -> Vec { - self.keys() + let mut controls = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for key in self + .keys() .into_iter() .filter(|key| self.root_form(*key) == Some(form) && key.0 != form) - .collect() + { + // Auch ein nachträglich angelegtes Array-Containerobjekt muss unter + // seinen Kindern liegen. Übrige Erzeugungsreihenfolge beibehalten. + let mut ancestors = Vec::new(); + let mut current = Some(key); + while let Some(parent) = current.filter(|p| p.0 != form && !seen.contains(p)) { + ancestors.push(parent); + current = self.parent_key(parent); + } + for key in ancestors.into_iter().rev() { + seen.insert(key); + controls.push(key); + } + } + controls } fn menu_children(&self, parent: ObjectKey) -> Vec { @@ -1316,7 +1397,7 @@ impl FormsModel { .filter(|key| { self.instance(*key).is_ok_and(|object| { object.description.class == ObjectClass::Menu - && object.description.parent == Some(parent.0) + && self.parent_key(*key) == Some(parent) }) && self.boolean(*key, "VISIBLE") }) .collect() @@ -1370,7 +1451,7 @@ impl FormsModel { && self.boolean(*candidate, "VISIBLE") && self.instance(*candidate).is_ok_and(|obj| { obj.description.class != ObjectClass::Menu - || obj.description.parent == Some(form) + || self.parent_key(*candidate) == Some((form, None)) }) }) { if self @@ -1410,7 +1491,29 @@ impl FormsModel { PropertyValue::Integer(shift as i32), ], ); - let handled = self.control_key(active, key); + let dropdown_key = self + .instance(active) + .is_ok_and(|o| o.description.class == ObjectClass::ComboBox) + && shift & umschalt::ALT != 0 + && key == taste::sonder(80); + let handled = if dropdown_key { + self.dropdown = if self.dropdown == Some(active) { + None + } else { + Some(active) + }; + if self.dropdown.is_some() { + self.queue_named(active, "DROPDOWN", vec![]); + } + self.dirty = true; + true + } else if self.dropdown == Some(active) && matches!(key, taste::ESC | taste::ENTER) { + self.dropdown = None; + self.dirty = true; + true + } else { + self.control_key(active, key) + }; if key.chars().count() == 1 { self.queue_named(active, "KEYPRESS", vec![PropertyValue::Integer(code)]); } @@ -1453,6 +1556,17 @@ impl FormsModel { PropertyValue::Integer(shift as i32), ], ); + if key.chars().count() == 1 { + self.queue_named((form, None), "KEYPRESS", vec![PropertyValue::Integer(code)]); + } + self.queue_named( + (form, None), + "KEYUP", + vec![ + PropertyValue::Integer(code), + PropertyValue::Integer(shift as i32), + ], + ); true } @@ -1546,19 +1660,18 @@ impl FormsModel { } fn rect(&self, key: ObjectKey) -> Option<(usize, usize, usize, usize)> { - let obj = self.instance(key).ok()?; + self.instance(key).ok()?; let mut left = self.integer(key, "LEFT").unwrap_or(0).max(0) as usize + 1; let mut top = self.integer(key, "TOP").unwrap_or(0).max(0) as usize + 1; let width = self.integer(key, "WIDTH").unwrap_or(1).max(1) as usize; let height = self.integer(key, "HEIGHT").unwrap_or(1).max(1) as usize; - let mut parent = obj.description.parent; - for _ in 0..self.objects.len() { - let Some(id) = parent else { break }; - let parent_obj = self.objects.get(id as usize)?; - let parent_key = (id, None); + let mut parent = self.parent_key(key); + for _ in 0..self.objects.len() + self.dynamic.len() { + let Some(parent_key) = parent else { break }; + self.instance(parent_key).ok()?; left += self.integer(parent_key, "LEFT").unwrap_or(0).max(0) as usize; top += self.integer(parent_key, "TOP").unwrap_or(0).max(0) as usize; - parent = parent_obj.description.parent; + parent = self.parent_key(parent_key); } Some((left, top, width, height)) } @@ -2192,7 +2305,7 @@ impl FormsModel { format!("{text} ▼") }; let mut lines = vec![Self::fit(&first, width)]; - if class == ObjectClass::ComboBox && style == 1 { + if class == ObjectClass::ComboBox && (style == 1 || self.dropdown == Some(key)) { lines.extend(self.list_lines(key, width, height.saturating_sub(1))); } lines.truncate(height); @@ -2317,12 +2430,7 @@ impl FormsModel { let Ok(class) = self.instance(key).map(|obj| obj.description.class) else { continue; }; - if class == ObjectClass::Menu - && self - .instance(key) - .ok() - .is_some_and(|obj| obj.description.parent == Some(form)) - { + if class == ObjectClass::Menu && self.parent_key(key) == Some((form, None)) { if self.boolean(key, "VISIBLE") { let caption = format!(" {} ", self.caption(key)); let focused = self.menu_path.first() == Some(&key); @@ -2381,7 +2489,7 @@ impl FormsModel { for root in self.form_controls(form).into_iter().filter(|key| { self.instance(*key).is_ok_and(|object| { object.description.class == ObjectClass::Menu - && object.description.parent == Some(form) + && self.parent_key(*key) == Some((form, None)) }) && self.boolean(*key, "VISIBLE") }) { if Some(&root) == self.menu_path.first() { @@ -2749,6 +2857,90 @@ mod tests { FormsModel::new(c.objects, 80, 25) } + #[test] + fn combobox_dropdown_closes_on_focus_change_and_hide() { + let mut catalog = forms::FormCatalog::default(); + catalog.add("Form1", ObjectClass::Form, None, false); + catalog.add("Combo", ObjectClass::ComboBox, Some("Form1"), false); + catalog.add("Text", ObjectClass::TextBox, Some("Form1"), false); + let mut m = FormsModel::new(catalog.objects, 80, 25); + m.show(0, false).unwrap(); + for hide in [false, true] { + m.focus((1, None)).unwrap(); + assert!(m.handle_key(&taste::sonder(80), umschalt::ALT)); + assert_eq!(m.dropdown, Some((1, None))); + if hide { + m.hide(0).unwrap(); + } else { + m.focus((2, None)).unwrap(); + } + assert_eq!(m.dropdown, None); + } + } + + #[test] + fn indexed_containers_preserve_parent_geometry_and_option_groups() { + let form = crate::frm::read_text( + "parent.frm", + r#"VERSION 1.00 +Begin Form Form1 + Width = 80 + Height = 25 + Begin Frame Group + Index = 0 + Left = 2 + Begin OptionButton Choice + Index = 0 + End + End + Begin Frame Group + Index = 1 + Width = 25 + Height = 12 + Left = 30 + Top = 4 + Begin OptionButton Choice + Index = 1 + Left = 2 + Top = 2 + End + Begin OptionButton Other + Caption = "Second" + Width = 12 + Left = 2 + Top = 4 + End + End +End +"#, + ) + .unwrap(); + let catalog = form.catalog(); + let id = |name| catalog.find(name).unwrap().0; + let (root, group, choice, other) = (id("Form1"), id("Group"), id("Choice"), id("Other")); + let mut m = FormsModel::new(catalog.objects, 80, 25); + form.apply(&mut m).unwrap(); + m.show(root, false).unwrap(); + assert_eq!(m.parent_key((choice, Some(1))), Some((group, Some(1)))); + assert_eq!(m.root_form((choice, Some(1))), Some(root)); + assert_eq!( + m.rect((choice, Some(1))).map(|(x, y, _, _)| (x, y)), + Some((33, 7)) + ); + assert_eq!(m.hit_test(root, 7, 33), (choice, Some(1))); + m.activate((choice, None)).unwrap(); + m.activate((choice, Some(1))).unwrap(); + assert_eq!(m.integer((choice, None), "VALUE"), Some(-1)); + assert_eq!(m.integer((choice, Some(1)), "VALUE"), Some(-1)); + m.activate((other, None)).unwrap(); + assert_eq!(m.integer((choice, None), "VALUE"), Some(-1)); + assert_eq!(m.integer((choice, Some(1)), "VALUE"), Some(0)); + assert_eq!(m.hit_test(root, 9, 33), (other, None)); + let mut screen = TextScreen::new(); + m.render(&mut screen); + assert!(tb_runtime::snapshot::text(&screen).contains("Second")); + } + #[test] fn defaults_bereich_und_implizites_laden() { let mut m = model(); @@ -3633,7 +3825,8 @@ mod tests { m.unload_array(2, 1).unwrap(); assert!(!m.events.iter().any(|e| e.array_index == Some(1))); m.hide(0).unwrap(); - assert!(m.events.is_empty()); + assert_eq!(m.events.len(), 1); + assert_eq!(m.events.pop_front().unwrap().name, "LOSTFOCUS"); m.show(0, false).unwrap(); m.sync_timers(|| 2000); assert_eq!(m.next_deadline(), Some(2100)); diff --git a/crates/tb-ui/src/frm.rs b/crates/tb-ui/src/frm.rs index dfba3a3..9c2c7f1 100644 --- a/crates/tb-ui/src/frm.rs +++ b/crates/tb-ui/src/frm.rs @@ -84,8 +84,9 @@ impl FormFile { ) -> Result, tb_runtime::errors::RuntimeError> { fn collect( node: &FormNode, - parent: Option, + parent: Option<(u16, Option)>, catalog: &FormCatalog, + form: &str, out: &mut Vec, depth: usize, ) -> Result<(), tb_runtime::errors::RuntimeError> { @@ -98,7 +99,8 @@ impl FormFile { .iter() .position(|o| { o.name.eq_ignore_ascii_case(&node.name) - && o.parent == parent + && ((o.array && catalog.belongs_to(o, form)) + || o.parent == parent.map(|p| p.0)) && o.class == node.class }) .ok_or(tb_runtime::errors::RuntimeError(420))? as u16; @@ -115,18 +117,29 @@ impl FormFile { if out.iter().any(|v| v.object == object && v.index == index) { return Err(tb_runtime::errors::RuntimeError(5)); } + let mut properties = node.properties.clone(); + if let Some((id, _)) = forms::property(node.class, "PARENT") { + properties.insert(id, PropertyValue::Object(parent)); + } out.push(FormInitial { object, index, - properties: node.properties.clone(), + properties, }); for child in &node.children { - collect(child, Some(object), catalog, out, depth)?; + collect( + child, + Some((object, (index != 0).then_some(index))), + catalog, + form, + out, + depth, + )?; } Ok(()) } let mut values = Vec::new(); - collect(&self.root, None, catalog, &mut values, 0)?; + collect(&self.root, None, catalog, &self.root.name, &mut values, 0)?; Ok(values) } @@ -941,7 +954,7 @@ fn decode_object(symbol: &BinarySymbol, record: &BinaryRecord, bytes: &[u8]) -> set_property(&mut node, name, PropertyValue::Integer(value as i32)); } } - if let Some(value) = record_u16(bytes, start, end, 4) { + if let Some(value) = record_u16(bytes, start, end, 4).filter(|_| symbol.is_array) { set_property( &mut node, "INDEX", @@ -1314,7 +1327,7 @@ fn write_node(node: &FormNode, depth: usize, out: &mut String) { let Some(value) = node.properties.get(&(id as u16)) else { continue; }; - if *value == default_value(spec) { + if spec.name != "INDEX" && *value == default_value(spec) { continue; } out.push_str(&" ".repeat(depth + 1)); @@ -1437,6 +1450,54 @@ mod tests { const EXAMPLE: &str = "VERSION 1.00\nBegin Form Form1\n Caption = \"Beispiel\"\n Height = 15\n Begin CommandButton cmdOK\n Caption = \"&OK\"\n End\nEnd\n\nSUB cmdOK_Click ()\n UNLOAD Form1\nEND SUB\n"; + #[test] + fn explicit_default_is_preserved_until_canonical_write() { + let original = "VERSION 1.00\r\nBegin Form Form1\r\n Begin CommandButton Ok\r\n Enabled = -1\r\n End\r\nEnd\r\n"; + let mut form = read_text("default.frm", original).unwrap(); + assert_eq!(write_text(&form), original); + form.root.name = "Changed".into(); + let canonical = write_text(&form); + assert_eq!( + canonical, + "VERSION 1.00\nBegin Form Changed\n Begin CommandButton Ok\n End\nEnd\n" + ); + assert_eq!( + write_text(&read_text("canonical.frm", &canonical).unwrap()), + canonical + ); + } + + #[test] + fn canonical_index_zero_remains_an_array_and_array_parents_are_preserved() { + let mut form=read_text("array.frm", "VERSION 1.00\nBegin Form Form1\n Begin Frame A\n Begin Label Item\n Index = 0\n End\n End\n Begin Frame B\n Begin Label Item\n Index = 1\n End\n End\nEnd\n").unwrap(); + form.code = "END\n".into(); + let text = write_text(&form); + assert!(text.contains("Index = 0")); + let roundtrip = read_text("roundtrip.frm", &text).unwrap(); + let catalog = roundtrip.catalog(); + let item = catalog.find("Item").unwrap().0; + assert!(catalog.objects[item as usize].array); + let a = catalog.find("A").unwrap().0; + let b = catalog.find("B").unwrap().0; + let mut model = FormsModel::new(catalog.objects, 80, 25); + roundtrip.apply(&mut model).unwrap(); + let parent = forms::property(ObjectClass::Label, "PARENT").unwrap().0; + assert_eq!( + model.get_at(item, Some(0), parent).unwrap(), + PropertyValue::Object(Some((a, None))) + ); + assert_eq!( + model.get_at(item, Some(1), parent).unwrap(), + PropertyValue::Object(Some((b, None))) + ); + form.root.children.pop(); + let single = read_text("single.frm", &write_text(&form)).unwrap(); + assert!( + single.catalog().objects[item as usize].array, + "Einzelelement mit Index 0 bleibt Array" + ); + } + #[test] fn reads_nested_form_and_preserves_source_exactly() { let form = read_text("test.frm", EXAMPLE).unwrap(); diff --git a/crates/tb-ui/src/frm_pcode.rs b/crates/tb-ui/src/frm_pcode.rs index 5fee242..71f9398 100644 --- a/crates/tb-ui/src/frm_pcode.rs +++ b/crates/tb-ui/src/frm_pcode.rs @@ -2365,7 +2365,15 @@ impl<'a> Decoder<'a> { continue; } if matches!(kind, Some(1 | 2)) { - let line = self.pending[..marker].trim_end().to_owned(); + // Binär-P-Code enthält den Include-Inhalt bereits. Die + // Herkunft bleibt Kommentar, keine erneut aktive Direktive. + let line = self.pending[..marker].trim_end(); + let directive = line.trim_start().trim_start_matches('\'').trim_start(); + let line = if directive.to_ascii_uppercase().starts_with("$INCLUDE:") { + format!("' Expanded INCLUDE:{}", &directive[9..]) + } else { + line.to_owned() + }; self.lines.push(line); self.pending.drain(..marker + 2); continue; @@ -2604,6 +2612,21 @@ mod tests { (table, offsets) } + #[test] + fn embedded_include_is_a_comment_but_literals_remain_unchanged() { + let mut decoder = Decoder::new("test.frm", &[], &[], 0); + decoder.pending=" '$INCLUDE: 'embedded.bi'\r\u{1}DECLARE SUB Test()\r\u{2}PRINT \"$INCLUDE: 'literal.bi'\"\r\u{1}".into(); + decoder.postprocess(true); + assert_eq!( + decoder.lines, + vec![ + "' Expanded INCLUDE: 'embedded.bi'", + "DECLARE SUB Test()", + "PRINT \"$INCLUDE: 'literal.bi'\"" + ] + ); + } + #[test] fn decodes_vbdos_declarations_without_losing_type_or_bounds() { let (sym, ids) = symbols(&["PrepVal", "StringToPrep", "Oldcontents"]); diff --git a/crates/tb-vm/src/bytecode.rs b/crates/tb-vm/src/bytecode.rs index 1470845..ea2cc4b 100644 --- a/crates/tb-vm/src/bytecode.rs +++ b/crates/tb-vm/src/bytecode.rs @@ -632,6 +632,37 @@ impl CompiledModule { } } } + // PARENT overrides carry design-array indices in the existing TBC4 property format. + let mut parents: std::collections::HashMap<_, _> = self + .objects + .iter() + .enumerate() + .map(|(id, object)| ((id as u16, 0), object.parent.map(|id| (id, 0)))) + .collect(); + for initial in &self.form_initial { + let object = &self.objects[initial.object as usize]; + let parent = tb_frontend::forms::property(object.class, "PARENT") + .and_then(|(id, _)| initial.properties.get(&id)); + parents.insert( + (initial.object, initial.index), + match parent { + Some(PropertyValue::Object(parent)) => { + parent.map(|(id, index)| (id, index.unwrap_or(0))) + } + _ => parents[&(initial.object, 0)], + }, + ); + } + for key in parents.keys() { + let mut seen = std::collections::HashSet::new(); + let mut current = Some(*key); + while let Some(key) = current { + if !seen.insert(key) { + return Err(bad()); + } + current = *parents.get(&key).ok_or_else(bad)?; + } + } for p in &self.procs { if p.module as usize >= self.modules.len() || p.n_params as usize != p.params.len() diff --git a/crates/tb-vm/src/interp.rs b/crates/tb-vm/src/interp.rs index a2fa7af..cd00617 100644 --- a/crates/tb-vm/src/interp.rs +++ b/crates/tb-vm/src/interp.rs @@ -546,7 +546,19 @@ impl Vm { } fn dispatch_form_event(&mut self, event: FormEvent, on_return: FormEventReturn) -> bool { - if self + let Some(binding) = self + .module + .event_procs + .iter() + .find(|e| e.object == event.object && e.event.eq_ignore_ascii_case(&event.name)) + .cloned() + else { + return false; + }; + if matches!( + event.name.as_str(), + "CLICK" | "DBLCLICK" | "GOTFOCUS" | "KEYDOWN" | "KEYPRESS" | "KEYUP" | "MOUSEDOWN" + ) && self .module .objects .get(event.object as usize) @@ -561,15 +573,6 @@ impl Vm { .forms .set_active_control(event.object, event.array_index); } - let Some(binding) = self - .module - .event_procs - .iter() - .find(|e| e.object == event.object && e.event.eq_ignore_ascii_case(&event.name)) - .cloned() - else { - return false; - }; if let Some(index) = event.array_index { self.push(Value::Int(index as i16)); } diff --git a/crates/tb-vm/tests/events.rs b/crates/tb-vm/tests/events.rs index ced34b3..93ec712 100644 --- a/crates/tb-vm/tests/events.rs +++ b/crates/tb-vm/tests/events.rs @@ -626,3 +626,284 @@ fn intervallaenderung_im_handler_verwirft_bereits_faellige_altintervalle() { assert_eq!(integer(&vm, "n"), 2); assert_eq!(vm.forms.next_deadline(), None); } + +#[test] +fn formular_tastatur_fallback_fuehrt_alle_drei_basic_handler_aus() { + let mut catalog = FormCatalog::default(); + catalog.add("Form1", ObjectClass::Form, None, false); + for (input, expected) in [("a", "DPU"), ("\0;", "DU")] { + let src = "DIM SHARED s$\nForm1.Show\nSTOP\nDOEVENTS\nEND\nSUB Form_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER)\nSHARED s$\ns$ = s$ + \"D\"\nEND SUB\nSUB Form_KeyPress(KeyAscii AS INTEGER)\nSHARED s$\ns$ = s$ + \"P\"\nEND SUB\nSUB Form_KeyUp(KeyCode AS INTEGER, Shift AS INTEGER)\nSHARED s$\ns$ = s$ + \"U\"\nEND SUB"; + let mut vm = Vm::new(tb_vm::compile_source_with_forms("FORM1", src, &catalog).unwrap()); + let mut host = CaptureHost::default(); + stop(&mut vm, &mut host); + host.ereignis(key(input)); + assert_eq!(vm.run(&mut host), RunEvent::Ended); + assert_eq!( + string(&vm, "s"), + expected, + "FORM_KEYDOWN/KEYPRESS/KEYUP: Status implementiert, Hosttaste {input:?}" + ); + } +} + +#[test] +fn registrierte_form_ereignisse_erreichen_basic_aus_realen_quellen() { + use tb_frontend::forms::{self, EventParamType}; + for name in forms::events(ObjectClass::Form) { + let mut catalog = FormCatalog::default(); + catalog.add("Form1", ObjectClass::Form, None, false); + catalog.add("Source", ObjectClass::TextBox, Some("Form1"), false); + let args = forms::event_params(name) + .unwrap() + .iter() + .map(|(n, t)| { + format!( + "{n} AS {}", + match t { + EventParamType::Integer => "INTEGER", + EventParamType::Single => "SINGLE", + EventParamType::Control => "CONTROL", + } + ) + }) + .collect::>() + .join(", "); + let operation = match *name { + "LOAD" | "GOTFOCUS" => "Form1.Show", + "LOSTFOCUS" => "Form1.Hide", + "UNLOAD" => "Form1.Unload", + "PAINT" => "Form1.Refresh", + "RESIZE" => "Form1.Width = 70", + "DRAGOVER" | "DRAGDROP" => "Source.Drag 1", + "KEYDOWN" | "KEYPRESS" | "KEYUP" | "MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" | "CLICK" + | "DBLCLICK" => "", + other => panic!("FORM_{other}: Status implementiert, Auslöser fehlt"), + }; + let setup = if matches!(*name, "LOAD" | "GOTFOCUS") { + "" + } else { + "Form1.Show" + }; + let src = format!("DIM SHARED n%\nSource.Left=2\nSource.Top=2\n{setup}\nn%=0\nSTOP\n{operation}\nDOEVENTS\nEND\nSUB Form_{name}({args})\nSHARED n%\nn%=n%+1\nEND SUB"); + let mut vm = Vm::new( + tb_vm::compile_source_with_forms("FORM1", &src, &catalog) + .unwrap_or_else(|d| panic!("{name}: {d:?}")), + ); + let mut host = CaptureHost::default(); + stop(&mut vm, &mut host); + if name.starts_with("KEY") { + host.ereignis(key("a")); + } + let events: &[MausArt] = match *name { + "MOUSEDOWN" => &[MausArt::Druck], + "MOUSEMOVE" | "DRAGOVER" => &[MausArt::Bewegung], + "MOUSEUP" | "DRAGDROP" => &[MausArt::Bewegung, MausArt::Loslassen], + "CLICK" => &[MausArt::Druck, MausArt::Loslassen], + "DBLCLICK" => &[ + MausArt::Druck, + MausArt::Loslassen, + MausArt::Druck, + MausArt::Loslassen, + ], + _ => &[], + }; + for art in events { + host.ereignis(Ereignis::Maus(MausEreignis { + art: *art, + taste: 1, + shift: 0, + zeile: 1, + spalte: 1, + })); + } + assert_eq!(vm.run(&mut host), RunEvent::Ended, "FORM_{name}"); + event_nachgewiesen(&format!("FORM_{name}"), integer(&vm, "n")).unwrap(); + } +} + +#[test] +fn registrierte_control_ereignisse_erreichen_basic_aus_realen_quellen() { + use tb_frontend::forms::{self, EventParamType}; + let mut failures = Vec::new(); + for class in ObjectClass::ALL + .iter() + .copied() + .filter(|c| *c != ObjectClass::Form) + { + for name in forms::events(class) { + let mut catalog = FormCatalog::default(); + catalog.add("Form1", ObjectClass::Form, None, false); + if class == ObjectClass::Menu { + catalog.add("Bar", ObjectClass::Menu, Some("Form1"), false); + } + catalog.add( + "Probe", + class, + Some(if class == ObjectClass::Menu { + "Bar" + } else { + "Form1" + }), + false, + ); + catalog.add("Source", ObjectClass::TextBox, Some("Form1"), false); + let args = forms::event_params(name) + .unwrap() + .iter() + .map(|(n, t)| { + format!( + "{n} AS {}", + match t { + EventParamType::Integer => "INTEGER", + EventParamType::Single => "SINGLE", + EventParamType::Control => "CONTROL", + } + ) + }) + .collect::>() + .join(", "); + let mut setup = String::from( + "Form1.Width=60\nForm1.Height=15\nForm1.Show\nSource.Left=40\nSource.Top=2\n", + ); + if !matches!(class, ObjectClass::Timer | ObjectClass::Menu) { + setup.push_str("Probe.Left=10\nProbe.Top=4\n"); + } + let operation = match *name { + "GOTFOCUS" => String::new(), + "LOSTFOCUS" => "Source.SetFocus".into(), + "PAINT" => "Probe.Refresh".into(), + "DRAGOVER" | "DRAGDROP" => "Source.Drag 1".into(), + "CHANGE" => match class { + ObjectClass::Label => "Probe.Caption=\"changed\"".into(), + ObjectClass::HScrollBar | ObjectClass::VScrollBar => "Probe.Value=1".into(), + ObjectClass::DirListBox => { + format!("Probe.Path=\"{}\"", std::env::temp_dir().display()) + } + ObjectClass::DriveListBox => "Probe.Drive=\"/\"".into(), + _ => "Probe.Text=\"changed\"".into(), + }, + "PATHCHANGE" => format!("Probe.Path=\"{}\"", std::env::temp_dir().display()), + "PATTERNCHANGE" => "Probe.Pattern=\"*.bas\"".into(), + "TIMER" => "Probe.Interval=1\nProbe.Enabled=-1".into(), + "CLICK" if class == ObjectClass::Menu => { + setup.push_str("Probe.Caption=\"&Go\"\nProbe.Shortcut=\"F3\"\n"); + String::new() + } + "CLICK" | "DBLCLICK" | "MOUSEDOWN" | "MOUSEMOVE" | "MOUSEUP" | "KEYDOWN" + | "KEYPRESS" | "KEYUP" | "DROPDOWN" | "CUSTOM" => String::new(), + other => panic!( + "{}_{other}: Status implementiert, Auslöser fehlt", + class.name() + ), + }; + let src=format!("DIM SHARED n%\n{setup}\nn%=0\nSTOP\n{operation}\nSTOP\nDOEVENTS\nEND\nSUB Probe_{name}({args})\nSHARED n%\nn%=n%+1\nEND SUB"); + let mut vm = Vm::new( + tb_vm::compile_source_with_forms("FORM1", &src, &catalog) + .unwrap_or_else(|d| panic!("{}_{name}: {d:?}", class.name())), + ); + let mut host = CaptureHost::default(); + let initial = vm.run(&mut host); + assert!( + matches!(initial, RunEvent::Stopped { .. }), + "{}_{name}: {initial:?}", + class.name() + ); + if *name == "LOSTFOCUS" { + vm.forms.focus((1, None)).unwrap(); + } + let second = vm.run(&mut host); + assert!( + matches!(second, RunEvent::Stopped { .. }), + "{}_{name}: {second:?}", + class.name() + ); + if name.starts_with("KEY") || matches!(*name, "DROPDOWN" | "CUSTOM") { + vm.forms.focus((1, None)).unwrap(); + } + if *name == "TIMER" { + host.uhr_vorruecken(2); + } + if name.starts_with("KEY") { + host.ereignis(key("a")); + } + if *name == "DROPDOWN" { + host.ereignis(Ereignis::Taste("\0P".into(), 4)); + } + if *name == "CUSTOM" { + host.ereignis(key("\0H")); + } + if class == ObjectClass::Menu { + host.ereignis(key("\0=")); + } + let events: &[MausArt] = match *name { + "MOUSEDOWN" | "GOTFOCUS" => &[MausArt::Druck], + "MOUSEMOVE" | "DRAGOVER" => &[MausArt::Bewegung], + "MOUSEUP" | "DRAGDROP" => &[MausArt::Bewegung, MausArt::Loslassen], + "CLICK" if class != ObjectClass::Menu => &[MausArt::Druck, MausArt::Loslassen], + "DBLCLICK" => &[ + MausArt::Druck, + MausArt::Loslassen, + MausArt::Druck, + MausArt::Loslassen, + ], + _ => &[], + }; + for art in events { + host.ereignis(Ereignis::Maus(MausEreignis { + art: *art, + taste: 1, + shift: 0, + zeile: 5, + spalte: 11, + })); + } + let result = vm.run(&mut host); + if result != RunEvent::Ended || integer(&vm, "n") == 0 { + failures.push(format!( + "{}_{name}: Status implementiert, Handler unerreicht: {result:?}", + class.name() + )); + } + } + } + assert!(failures.is_empty(), "{}", failures.join("\n")); +} + +#[test] +fn shell_und_irr_funktionssignaturen_erreichen_die_runtime() { + let mut vm = vm_for_source( + "DIM a(1) AS DOUBLE\na(0)=-100\na(1)=110\nr#=IRR#(a,.1)\ns&=SHELL(\"exit 7\")\nEND", + ); + assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended); + assert!(matches!(vm.inspect("r"),Some(Value::Dbl(n)) if (n-0.1).abs()<1e-8)); + assert!(matches!(vm.inspect("s"), Some(Value::Lng(7)))); +} + +fn event_nachgewiesen(name: &str, count: i16) -> Result<(), String> { + if count > 0 { + Ok(()) + } else { + Err(format!( + "{name}: Status implementiert, fehlender Auslösepfad zum BASIC-Handler" + )) + } +} + +#[test] +fn fehlender_ereignispfad_wird_namentlich_erkannt() { + let mut catalog = FormCatalog::default(); + catalog.add("Form1", ObjectClass::Form, None, false); + let mut module=tb_vm::compile_source_with_forms("FORM1","DIM SHARED n%\nForm1.Show\nSTOP\nDOEVENTS\nEND\nSUB Form_KeyPress(KeyAscii AS INTEGER)\nSHARED n%\nn%=n%+1\nEND SUB",&catalog).unwrap(); + module.event_procs.clear(); // künstlich fehlende Dispatch-Bindung + let mut vm = Vm::new(module); + let mut host = CaptureHost::default(); + stop(&mut vm, &mut host); + host.ereignis(key("a")); + assert_eq!(vm.run(&mut host), RunEvent::Ended); + let error = event_nachgewiesen("FORM_KEYPRESS", integer(&vm, "n")).unwrap_err(); + assert!( + error.contains("FORM_KEYPRESS") + && error.contains("implementiert") + && error.contains("Auslösepfad") + ); +} diff --git a/crates/tb-vm/tests/project.rs b/crates/tb-vm/tests/project.rs index 2d682af..90df731 100644 --- a/crates/tb-vm/tests/project.rs +++ b/crates/tb-vm/tests/project.rs @@ -249,7 +249,7 @@ fn beschaedigte_container_werden_vor_der_ausfuehrung_abgewiesen() { let mut duplicate = bytes.clone(); duplicate[24..28].copy_from_slice(b"MODN"); assert!(CompiledModule::from_tbc(&duplicate).is_err()); - for mutation in 0..8 { + for mutation in 0..10 { let mut bad = CompiledModule::from_tbc(&bytes).unwrap(); match mutation { 0 => bad.sources[0].module = u16::MAX, @@ -265,6 +265,23 @@ fn beschaedigte_container_werden_vor_der_ausfuehrung_abgewiesen() { ); } 7 => bad.procs[0].code.push(Instr::Source(u32::MAX, 1)), + 8 | 9 => { + let initial = &mut bad.form_initial[1]; + let parent = tb_frontend::forms::property( + tb_frontend::forms::ObjectClass::TextBox, + "PARENT", + ) + .unwrap() + .0; + initial.properties.insert( + parent, + tb_ui::forms::PropertyValue::Object(Some(if mutation == 8 { + (initial.object, Some(initial.index)) + } else { + (0, Some(99)) + })), + ); + } _ => unreachable!(), } assert!( diff --git a/docs/dateiformate.md b/docs/dateiformate.md index 3424fa3..a3f77ab 100644 --- a/docs/dateiformate.md +++ b/docs/dateiformate.md @@ -52,11 +52,14 @@ END SUB - Terminal Basic schreibt `VERSION` groß, Klassen- und Eigenschaftsnamen in der Schreibweise der Forms-Referenz und drei Leerzeichen je Blockebene. - Der Eigenschaftsname belegt 16 Spalten; danach folgen ` = ` und der Wert. -- Eigenschaften stehen in der alphabetischen Reihenfolge der Forms-Referenz +- Bei kanonischer Ausgabe stehen Eigenschaften in der alphabetischen Reihenfolge der Forms-Referenz und werden nur geschrieben, wenn ihr Wert vom Vorgabewert der Klasse - abweicht. + abweicht. Ein explizites `Index = 0` bleibt erhalten: Es deklariert ein + Control-Array und ist daher eine Strukturangabe, kein auslassbarer Default. - Unverändert gelesene Dateien werden bytegleich zurückgegeben; für neu erzeugte oder veränderte Beschreibungen gilt die kanonische Form oben. + Explizite Vorgabewerte (etwa `Enabled = -1`) bleiben im unveränderten + Original erhalten und werden erst bei kanonischer Ausgabe ausgelassen. - Zeichenketten verdoppeln ein enthaltenes `"`. Wahrheitswerte werden als `-1` und `0` geschrieben. diff --git a/docs/forms-referenz.md b/docs/forms-referenz.md index 3cfcff5..d8e3266 100644 --- a/docs/forms-referenz.md +++ b/docs/forms-referenz.md @@ -133,6 +133,23 @@ Index, Parent, Separator, Shortcut, Tag, Visible. Ereignisse u. a. Change, PathChange, PatternChange). Terminal Basic bildet sie plattformneutral nach (Laufwerksliste → Wurzeln/Mounts). +## Ereignisquellen im Terminal + +Eine Hosttaste liefert KeyDown und KeyUp; eine Zeichentaste zusätzlich +KeyPress. Ohne behandelndes Control gilt derselbe Ablauf für das aktive +Formular. Show/Hide und Formularwechsel liefern GotFocus/LostFocus, +geänderte Formularmaße Resize; Show und Refresh liefern Paint, bei +PictureBox liefert Refresh Paint. Label.Caption löst bei Änderung Change aus. +Nicht registrierte Ereignisse verfallen; Change, Timer und Paint verändern +den Eingabefokus nicht. Menüs und Labels werden dadurch nicht zum aktiven +Eingabe-Control. + +Alt+Pfeil-ab öffnet bzw. schließt die Liste einer ComboBox und löst beim +Öffnen DropDown aus. Enter oder Esc schließt sie. Der sichtbare Listenbereich +bleibt durch die Control-Höhe begrenzt. DirListBox.Path meldet PathChange und +Change, FileListBox.Path meldet PathChange und FileListBox.Pattern meldet +PatternChange; DriveListBox.Drive meldet Change. + ## Standardwerte und Wertebereiche | Eigenschaft | Bereich | Default | @@ -210,7 +227,8 @@ keine Zelle. Die folgenden Regeln sind **unsere Festlegung**, nicht nachgewiesenes Referenzverhalten: -- **Z-Reihenfolge:** Erzeugungsreihenfolge; später erzeugte Elemente liegen +- **Z-Reihenfolge:** Ein Container liegt stets hinter seinen Kindern, auch + bei indizierten Eltern. Im Übrigen gilt Erzeugungsreihenfolge; später erzeugte Elemente liegen oben. Bei Steuerelement-Arrays entscheidet bei sonst gleicher Position der aufsteigende Index, sodass der höchste Index oben liegt. Gezeichnet wird von unten nach oben, die Trefferprüfung läuft umgekehrt. diff --git a/docs/inventar.md b/docs/inventar.md index 38cfc4c..b540a10 100644 --- a/docs/inventar.md +++ b/docs/inventar.md @@ -3,8 +3,13 @@ Sollseite der Leitplanke „Vollständigkeit ist das Soll": jedes in der Original-Hilfe dokumentierte Sprachelement mit Abdeckungsstatus. Diese Tabelle ist Abnahmekriterium für Phase 6 und wird vom Test -`inventar_stimmt_mit_code_ueberein` gegen den Code geprüft — sie kann -nicht falsch werden, ohne dass die Testsuite rot wird. +`inventar_stimmt_mit_code_ueberein` und die ausführbaren Proben in +`crates/tb-cli/tests/inventar.rs` gegen den Code geprüft. Unabhängige Sollziele (Builtin-ABI, Instruktionen und Modulmetadaten) werden mit +der tatsächlichen Absenkung verglichen; Forms müssen den richtigen Member +erreichen. `$INCLUDE` durchläuft den CLI-Projektlader. Feste BASIC-Vorlagen +prüfen HIR ohne Unsupported und auflösbare Runtime-Ziele; +`crates/tb-vm/tests/events.rs` führt sämtliche registrierten Ereignisse von +ihren Quellen bis zum BASIC-Handler. Aufruf: `cargo test -p tb-cli --test inventar` und `cargo test -p tb-vm --test events`. **Quellen.** `bas7advr` = Themenliste von `bas7advr.hlp` (BASIC 7 Advisor). `qb45advr` = Gegenprobe über `qb45advr.hlp`, nötig weil die diff --git a/docs/reviews/2026-09-05-openspec-code-review.md b/docs/reviews/2026-09-05-openspec-code-review.md index 8dc2c7a..fb22896 100644 --- a/docs/reviews/2026-09-05-openspec-code-review.md +++ b/docs/reviews/2026-09-05-openspec-code-review.md @@ -4,6 +4,13 @@ Geprüfter Code: `19804e0e2de9eb5a8b0ef256b5be76c741560e07` in TerminalBasic. De Die Implementierung deckt breite Grundpfade ab, erfüllt den gesamten Vertrag aber nicht. **25 Befunde**: 10 mit hoher, 12 mit mittlerer Priorität, 2 Dokumentationsabweichungen und 1 Vertragsklärung. Die Priorität bewertet die Auswirkung, nicht den Aufwand. Befunde können mehrere Requirements betreffen. +## Abschließender Abgleich vom 06.09.2026 + +Alle F01–F25 sind im [separaten Abschlussreport](2026-09-06-openspec-abschlussreview.md) +mit aktuellen Regressions- bzw. Vertragsnachweisen abgeglichen. Dieser Bericht +und seine historischen Fundstellen dokumentieren weiterhin den Ausgangsstand; +der Inventartest liegt inzwischen unter `crates/tb-vm/tests/inventar.rs`. + ## Umsetzungsstand: Ereigniszustellung und Hostgrenzen F04, F05, F06, F07, F08, F18 und F20 sind im archivierten Change @@ -237,7 +244,7 @@ Alle Eigenschaften/Ereignisse aus der Klassentabelle werden im Inventartest paus Beleg: [crates/tb-frontend/tests/inventar.rs:206](../../crates/tb-frontend/tests/inventar.rs#L206). Vertrag: [openspec/specs/sprachinventar/spec.md:12](../../openspec/specs/sprachinventar/spec.md#L12), [openspec/specs/sprachinventar/spec.md:56](../../openspec/specs/sprachinventar/spec.md#L56), [openspec/specs/forms-objektmodell/spec.md:106](../../openspec/specs/forms-objektmodell/spec.md#L106). -Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md). +Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md). ### F22 — Der Korpus deckt die verlangten Kombinationen nicht ab (mittel) @@ -245,7 +252,7 @@ traps.bas prüft STOP nur für TIMER, nicht für KEY und UEVENT; formular.frm ha Beleg: [tests/compat/traps.bas:1](../../tests/compat/traps.bas#L1). Vertrag: [openspec/specs/kompat-testkorpus/spec.md:114](../../openspec/specs/kompat-testkorpus/spec.md#L114), [openspec/specs/kompat-testkorpus/spec.md:132](../../openspec/specs/kompat-testkorpus/spec.md#L132), [openspec/specs/kompat-testkorpus/spec.md:149](../../openspec/specs/kompat-testkorpus/spec.md#L149). -Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md). +Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md). ### F23 — Die pauschale Grafik-Abweisung ist überholt (Dokumentation) @@ -253,7 +260,7 @@ sprach-frontend nennt Grafik-Anweisungen pauschal als Non-Features. LINE, PAINT, Beleg: [docs/sprachreferenz.md:526](../../docs/sprachreferenz.md#L526). Vertrag: [openspec/specs/sprach-frontend/spec.md:83](../../openspec/specs/sprach-frontend/spec.md#L83). -Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md). +Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md). ### F24 — Container- und Opcode-Beschreibung sind nicht aktuell (Dokumentation) @@ -269,7 +276,7 @@ write_text gibt unveränderte gelesene Dateien bytegleich zurück, einschließli Beleg: [crates/tb-ui/src/frm.rs:348](../../crates/tb-ui/src/frm.rs#L348). Vertrag: [openspec/specs/forms-dateiformat/spec.md:31](../../openspec/specs/forms-dateiformat/spec.md#L31). -Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md). +Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md). ## Gebündelte Changes und Reihenfolge @@ -280,7 +287,7 @@ Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/chan | 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/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 | +| 6 | [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md) | F21, F22, F23, F25 | Abschließende Gesamtabnahme nach den fünf Korrektur-Changes | ## Vollständige Requirement-Matrix diff --git a/docs/reviews/2026-09-06-openspec-abschlussreview.md b/docs/reviews/2026-09-06-openspec-abschlussreview.md new file mode 100644 index 0000000..b1c34be --- /dev/null +++ b/docs/reviews/2026-09-06-openspec-abschlussreview.md @@ -0,0 +1,136 @@ +# OpenSpec-Abschlussabgleich vom 06.09.2026 + +**Aktueller Nachprüfungsstand:** V01–V03 sind vollständig behoben. Der +[abschließende Change-Review](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review.md) +verzeichnet keine offenen Befunde; der erste Review bleibt separat erhalten. + +Grundlage: Arbeitsbaum auf `c8b92f06190d77714865e0018b090348f372d77e`, nach +Integration der fünf Korrektur-Changes und Umsetzung von +`spezifikationsabgleich-und-regressionsnachweise`. Der +[Review vom 05.09.2026](2026-09-05-openspec-code-review.md) bleibt die historische +Bestandsaufnahme. Dieser Bericht gleicht alle dortigen F01–F25 erneut ab. + +**Ergebnis: F01–F25 sind durch Umsetzung oder präzisierten Vertrag geschlossen.** +Die unten genannten Regressionen bestehen im aktuellen Arbeitsbaum. Die vier +Delta-Specs wurden am 06.09.2026 in die Hauptspecs synchronisiert und der +Change wurde anschließend archiviert. + +## Befundmatrix + +| ID | Ergebnis und heutiger Nachweis | +|---|---| +| F01 | ISAM-OPEN verwendet den gemeinsamen Writer einer laufenden Transaktion. [CLI-Test](../../crates/tb-cli/tests/compat.rs) `tbc_isam_open_in_transaktion_hat_eine_harte_frist`; [isamopen.bas](../../tests/compat/isamopen.bas), einschließlich Prozessfrist. | +| F02 | SAVEPOINT/ROLLBACK stellt Satz-, Index- und Tabellenstruktur wieder her. [ISAM-Tests](../../crates/tb-runtime/src/isam/tests.rs) `savepoints_nehmen_index_und_tabellenstrukturen_zurueck`, `neue_tabellen_und_aktive_indizes_verschwinden_bei_ruecknahme`; [isamstruktur.bas](../../tests/compat/isamstruktur.bas). | +| F03 | FREEFILE und normale/ISAM-Bindungen teilen den Nummernraum. [isamdateinummern.bas](../../tests/compat/isamdateinummern.bas) prüft Lücken, Wiederbelegung, CLOSE und RESET. | +| F04 | Beide Signale erreichen ihre registrierten Traps. [events.rs](../../crates/tb-vm/tests/events.rs) `beide_signale_werden_zugestellt_und_beginnen_in_quellreihenfolge`. | +| F05 | KEY STOP erhält lesbare Eingabe und merkt den Handler einmal. [events.rs](../../crates/tb-vm/tests/events.rs) `key_on_off_stop_gelten_fuer_alle_konsoleneingaben`; zusätzlich F1 in [trap-masken.bas](../../tests/compat/trap-masken.bas). | +| F06 | Eingaben und Handler behalten ihre Reihenfolge. [events.rs](../../crates/tb-vm/tests/events.rs) `taste_maus_und_trap_behalten_die_annahmereihenfolge`, `fokus_handler_laufen_geordnet_bis_zur_rueckkehr`, `doevents_verhindert_rekursion_derselben_forms_prozedur`. | +| F07 | Das offene Menü sperrt Traps und Forms-Timer auch bei SLEEP und Eingabe. [events.rs](../../crates/tb-vm/tests/events.rs) `offenes_menu_sperrt_traps_und_wartende_forms_timer_an_allen_zustellpunkten`, `review_menu_schliessen_stellt_pending_trap_vor_erneutem_warten_zu`. | +| F08 | Zeit und Warten kommen vom Host. [events.rs](../../crates/tb-vm/tests/events.rs) `ohne_zeitbedarf_keine_uhrabfrage_und_keine_mausansammlung`, `sleep_folgt_nur_der_hostfrist_und_forms_timer_beendet_es`; [Ausführungsmodell](../tbvm-design.md). | +| F09 | Controls werden im jeweiligen Formular aufgelöst. [project.rs](../../crates/tb-vm/tests/project.rs) `zwei_formulare_mit_gleichen_controls_und_arrays_laufen_aus_dem_kompilat`; der neue FRM-Test erhält zusätzlich unterschiedliche Eltern von Elementen desselben Arrays. | +| F10 | Quellen, Module, Fehlerorte und Debuggerziele bleiben getrennt. [project.rs](../../crates/tb-vm/tests/project.rs) `module_includes_erl_breakpoints_und_inspektion_behalten_ihren_ursprung`, `initialisierungen_def_fn_und_modulhandler_haben_eigene_quellorte`; [CLI-Quellorttest](../../crates/tb-cli/tests/project.rs). | +| F11 | TBC 4 erhält Forms-Anfangswerte und Designarrays. [CLI-Test](../../crates/tb-cli/tests/project.rs) `projekt_und_formular_laufen_nach_entfernen_saemtlicher_quellen`; [VM-Projekttest](../../crates/tb-vm/tests/project.rs) für zwei Formulare. | +| F12 | ListIndex = −1 und Auswahländerungen sind konsistent. [Forms-Tests](../../crates/tb-ui/src/forms.rs) `listenauswahl_bleibt_beim_einfuegen_und_entfernen_konsistent`, `sorted_wechsel_erhaelt_auch_die_auswahl_zwischen_duplikaten`. | +| F13 | Timerphasen beginnen bei ihrer Aktivierung. [Forms-Tests](../../crates/tb-ui/src/forms.rs) `timerarrays_holen_nur_aktive_zeit_nach_und_sortieren_nach_name_und_index`; [events.rs](../../crates/tb-vm/tests/events.rs) `spaete_timeraktivierung_nutzt_hostzeit_auch_ueber_control_parameter`, `intervallaenderung_im_handler_verwirft_bereits_faellige_altintervalle`. | +| F14 | INPUT # bewahrt Feldfortschritt und unterscheidet leeres von fehlendem Feld. [vm.rs](../../crates/tb-vm/tests/vm.rs) `input_datei_bewahrt_felder_und_unterscheidet_leer_von_fehlend`. | +| F15 | INPUT$ zählt Zeichen, erhält Sondertastenreste und meldet vorzeitiges EOF. [vm.rs](../../crates/tb-vm/tests/vm.rs) `input_datei_alle_lesearten_teilen_utf8_cursor_und_seek`, `input_s_teilt_sondertasten_und_bewahrt_rest_fuer_inkey`, `unicode_puffergrenzen_und_binaere_records_bleiben_getrennt`. | +| F16 | Alle fünf CV-Funktionen verlangen exakte Bytezeichenbreite. [vm.rs](../../crates/tb-vm/tests/vm.rs) `alle_mk_cv_paare_verlangen_exakte_bytezeichenbreite`. | +| F17 | Fehlende Datei und fehlender Elternpfad ergeben unterschiedliche Fehler. [vm.rs](../../crates/tb-vm/tests/vm.rs) `datei_und_elternpfadfehler_sind_am_sprachzugang_unterscheidbar`. | +| F18 | BackTab wird als Tab mit Shift weitergegeben. Terminal-Mapper `backtab_ist_tab_mit_shift_auch_ohne_modifier`; [formular-access-backtab.frm](../../tests/compat/formular-access-backtab.frm) prüft den BASIC-Fokuspfad. | +| F19 | TIMEZONEKNOWN liefert den gegenwärtigen Status, einschließlich Ausfall und Erholung. [vm.rs](../../crates/tb-vm/tests/vm.rs) `timezoneknown_hat_integer_signatur_und_zeigt_aktuellen_status`; [Runtime-Test](../../crates/tb-runtime/src/builtins.rs) `zonenquelle_ausfall_und_erholung_gelten_fuer_alle_zeitfunktionen`. | +| F20 | VM-/Forms-Einbettung benötigt kein Terminalbackend. Separater Resolver [tests/terminalfrei](../../tests/terminalfrei/Cargo.toml), erneut ohne GUI ausgeführt: `Terminalfreier FRM-/VM-Lauf: KM`. | +| F21 | Alle 845 Inventareinträge werden mit kompilierten BASIC-Proben bis HIR/Runtime-Ziel geprüft. [inventar.rs](../../crates/tb-cli/tests/inventar.rs) vergleicht unabhängige Sollziele mit der konkreten Absenkung und erkennt falsche Statuswerte, Unsupported, fehlende/vertauschte Builtin-Bindungen, entfernte Absenkung und falsche Forms-Member/Typselektoren; [events.rs](../../crates/tb-vm/tests/events.rs) führt alle 133 registrierten Ereignisse bis zum BASIC-Handler und erkennt eine künstlich entfernte Ereignisbindung. | +| F22 | [trap-masken.bas](../../tests/compat/trap-masken.bas) ergänzt KEY/UEVENT ON/OFF/STOP und EVENT OFF; [formular-access-backtab.frm](../../tests/compat/formular-access-backtab.frm) ergänzt gewöhnlichen Control-Access-Key und Rückwärtstab mit `Click;LostFocus;GotFocus;`. Der Harness prüft jedes Programm zweimal. Sieben öffentliche Programme bestehen den gesonderten Bediennachweis unten. | +| F23 | SCREEN 0–13, LINE, PAINT und VIEW sind ausdrücklich erlaubt; übrige Non-Features bleiben ausgeschlossen. [Sprachreferenz](../sprachreferenz.md), [Delta](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/sprach-frontend/spec.md); [CLI-Test](../../crates/tb-cli/tests/project.rs) prüft alle 14 SCREEN-Modi und benannte Abweisungen. | +| F24 | [VM-Design](../tbvm-design.md) und [Hauptspec](../../openspec/specs/bytecode-kompilat/spec.md) beschreiben TBC 4 mit neun Abschnitten und dem aktuellen Opcode-Vertrag. Loader-Rundlauf, Versionsablehnung und beschädigte Referenzen sind in [project.rs](../../crates/tb-vm/tests/project.rs) bzw. den CLI-Projekttests abgesichert. Dieser Change führt keine neue Formatversion ein. | +| F25 | [Dateiformate](../dateiformate.md) und [Delta](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/forms-dateiformat/spec.md) unterscheiden Originalerhalt und kanonisches Schreiben. [FRM-Tests](../../crates/tb-ui/src/frm.rs) prüfen CRLF und explizite Defaults bytegleich sowie Default-Auslassung nach Änderung. Index 0 bleibt als Array-Strukturangabe erhalten. | + +## Zusätzlich im Abgleich gefundene und behobene Ursachen + +- Die Signatur für die SHELL-Funktion fehlte; IRR erwartete die Argumente in + umgekehrter Reihenfolge. Eine VM-Probe prüft jetzt SHELL → Exitcode 7 und + IRR für −100/+110 → 0,1, neben den 845 Kompilierproben. +- Koordinatenformen von CIRCLE/WINDOW und IOCTL # brachen vor der benannten + Non-Feature-Diagnose ab. Parser und Semantik nennen nun das ausgeschlossene + Element; POKE und PEEK sind ausdrücklich per CLI geprüft. +- Form_KeyPress/KeyUp, Form-Fokus/Paint/Resize, Label.Change, + PictureBox.Paint, ComboBox.DropDown und die getrennten Dateilistenereignisse + hatten fehlende oder falsche Quellen. Die 133 Ereignisproben verwenden + Tastatur, Maus, Hostzeit oder echte Modell-/BASIC-Operationen statt direkter + Ereignisqueue-Einträge. +- Der VM-Ereignisdispatch setzte auch ohne Handler den aktiven Control-Zustand. + Er ignoriert jetzt fehlende Bindungen ohne diese Nebenwirkung; reine + Zustandsmeldungen und nicht fokussierbare Objekte übernehmen keinen Fokus. +- Binäre FRM-Records enthielten auch bei Einzelcontrols einen Index-Slot. + Der Import berücksichtigt das tatsächliche Array-Flag. Eingebettete Includes + werden als Herkunftskommentar ausgegeben, nicht ein zweites Mal geöffnet. +- Arrayelemente unter unterschiedlichen Containern behalten ihre Eltern über + vorhandene PARENT-Anfangswerte. Die Suche bleibt auf das jeweilige Formular + begrenzt; der bestehende Mehrformular-/TBC-Test schützt diese Grenze. +- Kanonisches Weglassen von Index 0 hätte ein einzelnes Arrayelement in ein + Einzelcontrol verwandelt. Der Writer erhält diese Strukturangabe; der + präzisierte Delta-Vertrag und der Rundlauftest benennen sie ausdrücklich. + +## Öffentlicher Fremdprogrammbestand + +Bestand: `cout/vbdos`, Revision +`1cdd2b32b829fe1721d0b6aecc433abc47a96fb6`. Der Test erstellt für jeden Lauf +per `git archive` eine neue Kopie dieses Commits, baut den unveränderten +Programmeinstieg mit `tbc build`, lädt das Kompilat und bedient die VM über +CaptureHost bei 100×30 Zellen. Die lokale Prüfbasis war +`/tmp/terminalbasic-vbdos-evidence`. Kein Emulator oder interaktives Terminal. + +| Einstieg | Deklarierte Eingabe | Geprüftes Ergebnis, in beiden Läufen | +|---|---|---| +| `graphics/graphics.mak` | Alt+X | Formular entladen | +| `microsoft/check.mak` | Alt+F, X | Menü zunächst sichtbar, danach geschlossen und Formular entladen | +| `microsoft/qlbview.mak` | Esc | Cancel-Pfad entlädt das Formular | +| `microsoft/seek.mak` | Alt+X | Formular entladen | +| `microsoft/spindemo.mak` | Tab, Tab, Pfeil auf | VSpin_Custom ändert Text1 von `0` auf ` 1`; Bildschirmänderung durch Pfeiltaste | +| `microsoft/notepad.frm` | Alt+F, X, N im Speicherdialog | Menü öffnet, Save-Dialog wird angezeigt, N beantwortet ihn und Formular wird entladen | +| `misc/mentors/mentors.frm` | Alt+F, X | Menü zunächst sichtbar, danach geschlossen und Formular entladen | + +Alle sieben Programme: diagnosefreie Übersetzung und zwei gleiche Folgen von +sichtbaren Formular-/Menüzuständen und Bildschirm-Snapshots. Keine fehlende +externe Voraussetzung und keine angepassten Fremdprogrammquellen. Das belegt +diese sieben Einstiege und Sequenzen, keine vollständige VBDOS-Kompatibilität. + +Reproduktion mit vorhandenem Checkout der genannten Revision: + +```sh +TB_VBDOS_REPO=/pfad/zu/vbdos cargo test -p tb-cli --test foreign oeffentliche -- --ignored --nocapture +``` + +Der [Test](../../crates/tb-cli/tests/foreign.rs) ist im normalen Workspace-Lauf +explizit ignoriert, weil der öffentliche Checkout keine Repository-Abhängigkeit +ist. Der gesonderte Aufruf oben wurde für diese Abnahme erfolgreich ausgeführt; +ohne Umgebungsvariable oder bei abweichender Revision schlägt er fehl. + +## Abnahme und Sollbilder + +- `cargo test --workspace`: **481 bestanden, 0 fehlgeschlagen, 2 ignoriert**. + Ignoriert sind der Golden-Generator und der separat ausgeführte Fremdtest. +- Fremdtest: **1 bestanden**, sieben Programme jeweils zweimal. +- `cargo run --manifest-path tests/terminalfrei/Cargo.toml --offline`: bestanden. +- Clippy für Workspace, alle Targets/Features, `-D warnings`: bestanden. +- `cargo fmt --all -- --check` und `git diff --check`: bestanden. +- `openspec validate --all --strict`: **16 bestanden**, 15 Hauptspecs und der + aktive Change, keine Fehler. Bestehende Hinweise zu langen Requirement-Texten + sind rein informativ. + +Die beiden neuen Sollausgaben wurden aus den erwarteten Zuständen festgelegt. +`EVENT OFF` verwirft eingetretene Ereignisse; es wird ausdrücklich kein Nachholen +beim erneuten ON erwartet. Der bestehende `formular.out` wurde fachlich geprüft: +Nur die zehn Attributzellen der OK-Schaltfläche in Zeile 4, Spalten 24–33 wechseln +auf Vordergrund 15/Hintergrund 1 (`f1`), weil sie den Eingabefokus behält. Die +Zeichenebene und alle übrigen Zellen bleiben bytegleich. Der Golden-Generator +wurde nicht ausgeführt. + +## Abschluss der Reviewkorrekturen + +- **V01:** Parent-Schlüssel enthalten den Arrayindex. Geometrie, Optionsgruppen, Menüzuordnung und Formular-Traversierung nutzen dieselbe Referenz. Container liegen bei Zeichnung und Trefferprüfung hinter ihren Kindern. Die ergänzte Gegenprobe traf vor der letzten Korrektur fälschlich `Group(1)` statt `Other`; danach sind Treffer und sichtbare Beschriftung korrekt. TBC4 bleibt unverändert, ungültige Parent-Zyklen und fehlende Arrayinstanzen werden abgewiesen. +- **V02:** Die 291 festen Vorlagen enthalten unabhängige Sollziele; Forms werden gegen den abgesenkten Klassenmember geprüft. Der Inventartest liegt in `tb-cli`, damit `$INCLUDE` durch den echten Projektlader läuft. Falsche gültige Bindungen, falsche Konverterselektoren/Forms-Member sowie entfernte Absenkungen werden namentlich erkannt. +- **V03:** Die gemeinsame Anweisungsauflösung weist auch Koordinaten mit Farbe/STEP, WINDOW SCREEN und PALETTE USING namentlich ab, nachdem mögliche Variablenzuweisungen erkannt wurden. Gleichnamige Arrays und SCREEN 0–13/LINE/PAINT/VIEW bleiben verfügbar. +- **Ergänzende Prüfung:** ComboBox-Ausklapplisten schließen bei Fokuswechsel und Ausblenden; der neue Regressionstest besteht. + +Alle fünf ursprünglichen Soll-Gegenproben bestehen. Der Golden-Generator wurde nicht ausgeführt; bei den Reviewkorrekturen wurden keine weiteren Sollbilder verändert. diff --git a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/.openspec.yaml b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/.openspec.yaml similarity index 100% rename from openspec/changes/spezifikationsabgleich-und-regressionsnachweise/.openspec.yaml rename to openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/.openspec.yaml diff --git a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/design.md b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/design.md similarity index 72% rename from openspec/changes/spezifikationsabgleich-und-regressionsnachweise/design.md rename to openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/design.md index c60899b..3822dde 100644 --- a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/design.md +++ b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/design.md @@ -12,7 +12,7 @@ pruefe_forms erwartet jede Klasseneigenschaft und jedes Ereignis pauschal als im ### D1 — Vorhandenen Inventartest erweitern -Inventareinträge weiterhin aus der Markdowntabelle lesen, aber für implementiert mindestens erzeugbare Frontend-/HIR-Pfade ohne Unsupported und auflösbare Runtime-Ziele prüfen. Syntaxformen mit expliziten kleinen Programmvorlagen abdecken; Forms-Probequellen aus Klassen-/Signaturmetadaten erzeugen. Klassentabellen allein gelten nicht als Laufzeitnachweis. Für Ereignisse reale Auslöser bis zum BASIC-Handler prüfen; die konkret fehlenden Form-KeyPress/KeyUp ergänzen, nicht den Status auf offen setzen. +Inventareinträge weiterhin aus der Markdowntabelle lesen, aber für implementiert mindestens erzeugbare Frontend-/HIR-Pfade ohne Unsupported und auflösbare Runtime-Ziele prüfen. Syntaxformen mit expliziten kleinen Programmvorlagen abdecken; Forms-Probequellen aus Klassen-/Signaturmetadaten erzeugen. Der Inventartest liegt im CLI-Testpaket, damit auch `$INCLUDE` den echten Projektlader durchläuft. Unabhängige Sollziele aus Runtime-ABI, Instruktionsvertrag und Modulmetadaten müssen in der tatsächlichen Absenkung vorkommen; Forms-Dispatch muss den konkreten Klassenmember erreichen. Gegenproben ersetzen gültige Bindungen, Forms-Member und Typselektoren oder entfernen die Absenkung. Klassentabellen allein gelten nicht als Laufzeitnachweis. Für Ereignisse reale Auslöser bis zum BASIC-Handler prüfen; die konkret fehlenden Form-KeyPress/KeyUp ergänzen, nicht den Status auf offen setzen. ### D2 — Korpusfälle nach fehlender Bedingung hinzufügen @@ -20,7 +20,7 @@ Bestehenden CaptureHost/Harness wiederverwenden; jede Maskierungsquelle in ON/OF ### D3 — Bestehende Produktentscheidungen synchronisieren -Nur LINE/PAINT/VIEW/SCREEN 0–13 aus der pauschalen Grafikablehnung ausnehmen; verbleibende Non-Features behalten. FRM: unverändertes Lesen/Schreiben erhält die Originalbytes; neues/geändertes kanonisches Schreiben lässt Defaults aus. Keine Änderung des funktionierenden Erhaltungsmodus nötig. +Nur LINE/PAINT/VIEW/SCREEN 0–13 aus der pauschalen Grafikablehnung ausnehmen; verbleibende Non-Features behalten. FRM: unverändertes Lesen/Schreiben erhält die Originalbytes; neues/geändertes kanonisches Schreiben lässt Defaults aus. Keine Änderung des funktionierenden Erhaltungsmodus nötig. Explizite Control-Array-Indizes bleiben als Strukturangaben auch bei Index 0 erhalten. Elternreferenzen behalten ihren Arrayindex im bestehenden PARENT-Anfangswert (TBC4). Alle Containerpfade nutzen diese Referenz; Zeichnung und Trefferprüfung ordnen Container vor ihren Kindern ein. Die TBC-Validierung weist fehlende Elterninstanzen und Zyklen ab. ### D4 — Abschließender Review als Befundnachweis @@ -29,7 +29,7 @@ Nach den fünf Korrektur-Changes jede F-ID mit konkretem Regressionstest/Prüfer ## Risks / Trade-offs - Generierte Tests spiegeln wieder nur Implementierungsmetadaten → erwartete Ergebnisse für kritische Ereignisse und negative Markerfälle unabhängig in kleinen Quellen festhalten. -- Überlappung mit funktionalen Korrekturen → diese zuerst integrieren und nur die hier benannten verbleibenden Eventquellen ändern. +- Überlappung mit funktionalen Korrekturen → diese zuerst integrieren und die im vollständigen Ereignis- und Fremdprogrammlauf nachgewiesenen fehlenden Quellen bzw. Importpfade an ihrem gemeinsamen Ursprung korrigieren. - Fremdprogramme können Hardware-Non-Features außerhalb des akzeptierten Bestands enthalten → dokumentierten Prüfbestand/Revision festhalten, keine globale Kompatibilitätszusage daraus ableiten. ## Migration Plan diff --git a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md similarity index 95% rename from openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md rename to openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md index 0c51e35..b3a2c76 100644 --- a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md +++ b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md @@ -1,6 +1,6 @@ ## Why -Der aktuelle Inventarabgleich bestätigt überwiegend Tabellen gegen dieselben Tabellen, während konkrete Formularereignisse fehlen. Korpusabdeckung und Hauptspecs sind nach den jüngsten Features nicht vollständig nachgezogen (F21–F23/F25). Siehe [Review vom 05.09.2026](../../../docs/reviews/2026-09-05-openspec-code-review.md). +Der aktuelle Inventarabgleich bestätigt überwiegend Tabellen gegen dieselben Tabellen, während konkrete Formularereignisse fehlen. Korpusabdeckung und Hauptspecs sind nach den jüngsten Features nicht vollständig nachgezogen (F21–F23/F25). Siehe [Review vom 05.09.2026](../../../../docs/reviews/2026-09-05-openspec-code-review.md). ## What Changes diff --git a/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review-initial.md b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review-initial.md new file mode 100644 index 0000000..351c871 --- /dev/null +++ b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review-initial.md @@ -0,0 +1,145 @@ +# Verifikation vom 06.09.2026 + +Historischer Befundstand vor der Korrektur; der Inventartest liegt inzwischen in tb-cli. + +Change: `spezifikationsabgleich-und-regressionsnachweise`. +Geprüft wurde der aktuelle Arbeitsbaum auf `c8b92f06190d77714865e0018b090348f372d77e`: +Proposal, Design, sieben Tasks, vier Delta-Specs mit sechs Requirements und +18 Szenarien, betroffene Hauptverträge, Code, Tests und Abschlussreport. + +**Ergebnis: drei offene Befunde. Die vorherige Aussage „keine offenen +Befunde“ ist durch diese Nachprüfung überholt.** + +| Dimension | Ergebnis | +|---|---| +| Vollständigkeit | 7/7 Tasks formal abgehakt; die inhaltliche Abnahme ist wegen V01–V03 noch offen | +| Korrektheit | Bestehende Suite grün, fünf unabhängige Soll-Gegenproben schlagen fehl | +| Kohärenz | Bestehende Architektur und TBC-Version erhalten; Inventarnachweis und Import-Erhaltungszusage sind noch zu weitgehend | +| Befunde | 0 CRITICAL, 3 WARNING; davon V01 mit hoher, V02/V03 mit mittlerer Auswirkung | + +## V01 — Index des Elterncontainers geht beim FRM-Import verloren (hoch) + +**Fundstellen:** [frm.rs:87](../../../../crates/tb-ui/src/frm.rs#L87), +[frm.rs:122](../../../../crates/tb-ui/src/frm.rs#L122), +[frm.rs:131](../../../../crates/tb-ui/src/frm.rs#L131), +[forms.rs:269](../../../../crates/tb-ui/src/forms.rs#L269), +[forms.rs:531](../../../../crates/tb-ui/src/forms.rs#L531). + +Der neue PARENT-Anfangswert erhält nur die Objekt-ID. Die Rekursion übergibt +ebenfalls nur `Some(object)`; `set_initial_at` verwirft einen vorhandenen Index, +und der Getter ergänzt grundsätzlich `None`. Ein Kind von `Group(1)` bekommt +damit `Group(0)` als Parent. Die neue Regression prüft unterschiedliche +unindizierte Frames, aber kein Kind innerhalb eines Frame-Arrays. + +**Reproduktion:** FRM mit `Frame Group, Index = 0` und einem zweiten +`Frame Group, Index = 1`, darin `TextBox Probe`. Anschließend: + +```basic +DIM p AS CONTROL +p = Probe.Parent +Form1.Hide +PRINT p.Index +END +``` + +Soll: `1`. Ist: `0`. Derselbe Fehler tritt nach `tbc build` und Löschen der +FRM-Quelle beim Ausführen der TBC-Datei auf. Beide Gegenproben schlagen fehl. +Das ist auch für den akzeptierten Fremdbestand relevant: `graphics.frm` +enthält indizierte Frames mit untergeordneten Controls. + +**Vertrag:** Forms-Steuerelement-Arrays, vollständige Forms-Anfangsbeschreibungen +im TBC sowie Task 2.4; der aktuelle Abschlussreport behauptet außerdem den +Erhalt der Array-Eltern. Die neue PARENT-Unterstützung erfüllt diesen Erhalt +noch nicht vollständig. + +**Behebung:** Eltern als vollständige Objektreferenz einschließlich Arrayindex +über Import, Anfangswerte, PARENT-Zugriff und davon abhängige Containerpfade +führen. Einen indizierten Elterncontainer im Quell- und TBC-Rundlauf prüfen; +den bestehenden Test mit gleichen Control-Namen in zwei Formularen erhalten. + +## V02 — Inventarprüfung akzeptiert ein falsches Laufzeitziel (mittel) + +**Fundstelle:** [inventar.rs:725](../../../../crates/tb-cli/tests/inventar.rs#L725). + +`pruefe_kompilat` prüft lediglich, ob die im Kompilat vorkommenden Builtin-IDs +irgendwelche vorhandenen Bindungen haben und die Tabellenreferenzen gültig +sind. Es prüft nicht, ob das erzeugte Ziel dem untersuchten Inventareintrag +entspricht. Damit kann ein falsch abgesenktes Element weiterhin als +implementiert bestätigt werden. + +**Reproduktion:** Die normale Probe `PRINT TIMEZONEKNOWN` wird kompiliert. +Anschließend ersetzt die Gegenprobe ausschließlich den erzeugten CallBuiltin +von TIMEZONEKNOWN (156) durch TIMER (54); beide haben null Argumente und eine +vorhandene Runtime-Bindung. Der bestehende Prüfer liefert weiterhin `Ok(())`. +Die Soll-Assertion, dass er diese Divergenz melden muss, schlägt fehl. + +**Vertrag:** Delta `sprachinventar`, Requirement „Automatischer Abgleich +Inventar gegen Implementierung“, Task 2.1 und Design D1. Der Nachweis soll den +tatsächlichen Implementierungspfad des jeweiligen Elements prüfen, nicht nur +die Existenz eines beliebigen ausführbaren Ziels. + +**Behebung:** Die erwartete Absenkung bzw. das beobachtbare Verhalten mit dem +konkreten Inventareintrag verbinden. Neben Unsupported und fehlender Bindung +auch eine gültige, aber falsche Zielbindung sowie eine verschwundene Absenkung +negativ prüfen. Das betrifft auch Forms-Methoden/-Eigenschaften, deren bloßes +Vorhandensein in gültigem Bytecode noch keinen passenden Dispatch beweist. + +## V03 — Weitere gültige Non-Feature-Syntaxformen liefern nur Syntaxfehler (mittel) + +**Fundstellen:** [parser.rs:199](../../../../crates/tb-frontend/src/parser.rs#L199), +[inventar-quellen.tsv:148](../../../../tests/support/inventar-quellen.tsv#L148), +[inventar-quellen.tsv:153](../../../../tests/support/inventar-quellen.tsv#L153). + +Der neue Parserpfad behandelt CIRCLE, WINDOW und IOCTL. Andere ausdrücklich +verbotene Grafikformen erreichen weiterhin nicht die vorgeschriebene +Feature-unavailable-Diagnose. Die festen Inventarvorlagen erfassen davon nur +Syntaxvarianten, die der allgemeine Aufrufparser bereits versteht. + +**Reproduktionen mit `tbc check`:** + +- `PSET (1,1),2` → `Syntax error` in Spalte 11. +- `DIM a%(15)` gefolgt von `PALETTE USING a%(0)` → `Expected: expression` + in Zeile 2, Spalte 9. + +Beide sind gültige Syntaxformen ausgeschlossener Funktionen. Erwartet wird +jeweils die benannte Compile-Diagnose `Feature unavailable`, nicht ein +allgemeiner Syntaxfehler. Beide Soll-Gegenproben schlagen fehl. + +**Vertrag:** Delta `sprach-frontend`, Requirement „Compile-Zeit-Abweisung +deklarierter Non-Features“ verlangt diese Diagnose für jede Verwendung; +Task 1.1 verlangt die benannte Abweisung weiterhin verbotener Formen. + +**Behebung:** Die besondere Syntax der verbleibenden ausgeschlossenen Formen +vor dem allgemeinen Aufrufparser erkennen oder bis zur Non-Feature-Diagnose +parsen. Die CLI-/Inventarproben um Koordinaten mit Farbargument und PALETTE +USING ergänzen; die erlaubte Grafikuntermenge weiter positiv prüfen. + +## Ausgeführte Prüfungen + +- `cargo test --workspace`: **476 bestanden, 0 fehlgeschlagen, 2 ignoriert**. +- Gesonderter Fremdtest mit dem dokumentierten Checkout und Commit: + **bestanden**, sieben Einstiege jeweils zweimal, mit den festgelegten + Menü-, Exit-, Dialog- und Spin-Zuständen. +- `openspec validate --all --strict`: **16 bestanden, 0 Fehler**; + vorhandene Hinweise zu Textlängen sind informativ. +- `git diff --check`: bestanden. +- Neue unabhängige Soll-Gegenproben: **fünf fehlgeschlagen**, zugeordnet zu den + drei Befunden oben. Produktcode, bestehende Tests und Sollbilder wurden + während dieses Reviews nicht geändert. + +Die [Gegenproben](review-probes.py) sind reproduzierbar und erzeugen Dateien +nur in einem temporären Verzeichnis. Sie bauen die CLI, prüfen die vier +CLI-Fälle und kompilieren für die Zielmutation eine temporäre Kopie des +bestehenden Inventarprüfers: + +```sh +python3 openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review-probes.py +``` + +Der Aufruf liefert derzeit Status 1. Keine GUI, kein Emulator und kein +Netzwerkzugriff sind erforderlich. Keine Verifikationsdimension wurde +übersprungen. Die grünen bestehenden Tests und die sieben engen +Fremdprogrammsequenzen widerlegen die zusätzlich reproduzierten Fehler nicht. + +**Abnahme:** Noch nicht vollständig spezifikationskonform. V01–V03 vor der +abschließenden Synchronisierung und Archivierung beheben und nachprüfen. diff --git a/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review-probes.py b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review-probes.py new file mode 100644 index 0000000..2535130 --- /dev/null +++ b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review-probes.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Unabhängige Soll-Gegenproben; Produktcode und bestehende Tests bleiben unverändert.""" +from pathlib import Path +import os +import subprocess +import tempfile + +ROOT = next(p for p in Path(__file__).resolve().parents + if (p / "Cargo.toml").is_file() and (p / "openspec").is_dir()) +TBC = ROOT / "target/debug/tbc" + + +def run(*args, **kwargs): + return subprocess.run(args, capture_output=True, text=True, timeout=60, **kwargs) + + +def main(): + subprocess.run(["cargo", "build", "-p", "tb-cli"], cwd=ROOT, check=True) + failed = [] + + def check(name, ok, evidence): + print(f"{'PASS' if ok else 'FAIL'} {name}: {evidence}") + if not ok: + failed.append(name) + + with tempfile.TemporaryDirectory(prefix="tb-review-") as tmp: + tmp = Path(tmp) + for name, source in [("PSET", "PSET (1,1),2"), + ("PALETTE", "DIM a%(15)\nPALETTE USING a%(0)")]: + path = tmp / "graphics.bas" + path.write_text(source + "\n") + out = run(str(TBC), "check", str(path)) + check(name + " Non-Feature", out.returncode != 0 and + "Feature unavailable" in out.stderr and name in out.stderr, + out.stderr.strip()) + + path = tmp / "parent.frm" + path.write_text('''VERSION 1.00 +Begin Form Form1 + Begin Frame Group + Index = 0 + End + Begin Frame Group + Index = 1 + Begin TextBox Probe + End + End +End +DIM p AS CONTROL +p=Probe.Parent +Form1.Hide +PRINT p.Index +END +''') + out = run(str(TBC), "run", str(path)) + check("Indizierter Parent aus FRM", out.returncode == 0 and out.stdout.strip() == "1", + f"Soll=1, Ist={out.stdout!r}, {out.stderr}") + build = run(str(TBC), "build", str(path)) + assert build.returncode == 0, build.stderr + path.unlink() + out = run(str(TBC), "run", str(path.with_suffix(".tbc"))) + check("Indizierter Parent aus TBC ohne Quelle", out.returncode == 0 and out.stdout.strip() == "1", + f"Soll=1, Ist={out.stdout!r}, {out.stderr}") + + # Kopie im Temp-Verzeichnis erlaubt Zugriff auf den privaten Prüfer. + # Einzige Mutation: TIMEZONEKNOWN zeigt auf die gültige TIMER-Bindung. + source = (ROOT / "crates/tb-cli/tests/inventar.rs").read_text() + source = source.replace('include_str!("../../../tests/support/inventar-quellen.tsv")', + f'include_str!("{ROOT}/tests/support/inventar-quellen.tsv")') + source += r''' +#[test] +fn review_falsches_laufzeitziel_muss_abgewiesen_werden() { + let e=inventar().into_iter().find(|e|e.name=="TIMEZONEKNOWN").unwrap(); + let (src,catalog)=probe_source(&e); + let mut module=tb_vm::compile_source_with_forms("FORM1",&src,&catalog).unwrap(); + let mut changed=false; + for i in module.procs.iter_mut().flat_map(|p|&mut p.code) { + if let tb_vm::bytecode::Instr::CallBuiltin(id,_) = i { + if *id==tb_runtime::builtins::ids::TIMEZONEKNOWN { + *id=tb_runtime::builtins::ids::TIMER; + changed=true; + } + } + } + assert!(changed); + let result=pruefe_kompilat(&e,Ok(module),|id|tb_runtime::builtins::builtin_table().get(id as usize).is_some()); + assert!(result.is_err(),"TIMEZONEKNOWN mit TIMER-Ziel wird akzeptiert: {result:?}"); +} +''' + probe = tmp / "inventar.rs" + probe.write_text(source) + # Cargo resolves a coherent dependency graph; selecting rlibs by mtime + # can mix feature variants after a workspace test run. + source = source.replace('env!("CARGO_MANIFEST_DIR")', f'"{ROOT}/crates/tb-cli"') + probe.write_text(source) + manifest = '[package]\nname = "tb-review-probes"\nversion = "0.0.0"\nedition = "2021"\n[workspace]\n' + manifest += '[[test]]\nname = "inventar"\npath = "inventar.rs"\n[dependencies]\n' + for name in ["tb-vm", "tb-frontend", "tb-runtime"]: + manifest += f'{name} = {{ path = "{ROOT}/crates/{name}" }}\n' + (tmp / "Cargo.toml").write_text(manifest) + env = os.environ.copy() + env["CARGO_BIN_EXE_tbc"] = str(TBC) + out = run("cargo", "test", "--manifest-path", str(tmp / "Cargo.toml"), + "--offline", "--target-dir", str(ROOT / "target/review-probes"), + "--test", "inventar", "review_falsches", "--", "--nocapture", env=env) + check("Inventar erkennt falsches Laufzeitziel", out.returncode == 0, + out.stdout + out.stderr) + print(f"{len(failed)} fehlgeschlagene Soll-Gegenproben") + return bool(failed) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review.md b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review.md new file mode 100644 index 0000000..691a6f4 --- /dev/null +++ b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review.md @@ -0,0 +1,51 @@ +# Abschließende Verifikation vom 06.09.2026 + +Change: `spezifikationsabgleich-und-regressionsnachweise`. +Grundlage: aktueller Arbeitsbaum auf `c8b92f06190d77714865e0018b090348f372d77e`. +Geprüft: Proposal, Design D1–D4, elf Tasks, vier Delta-Specs mit sechs +Requirements und 18 Szenarien sowie die betroffenen Hauptverträge und Codepfade. + +**Ergebnis: keine offenen Befunde.** + +| Dimension | Ergebnis | +|---|---| +| Vollständigkeit | 11/11 Tasks, 6/6 Requirements, 18/18 Szenarien abgedeckt | +| Korrektheit | Gesamtsuite, fünf ursprüngliche Soll-Gegenproben und zusätzliche Regressionen bestanden | +| Kohärenz | Gemeinsame Compiler-/VM-/Forms-Pfade; vorhandener TextScreen; keine neue Abhängigkeit oder TBC-Version | +| Befunde | 0 CRITICAL, 0 WARNING, 0 SUGGESTION | + +Die [erste Nachprüfung mit V01–V03](review-initial.md) bleibt als historischer +Ausgangsbefund erhalten. Die vollständige Szenarienmatrix steht in +[verification.md](verification.md), der Abgleich F01–F25 im +[Abschlussreport](../../../../docs/reviews/2026-09-06-openspec-abschlussreview.md). + +## Nachweis der Behebung + +| Befund | Korrektur und Gegenprobe | +|---|---| +| V01 – indizierte Elterncontainer | [frm.rs](../../../../crates/tb-ui/src/frm.rs) übergibt vollständige Parent-Schlüssel. [forms.rs](../../../../crates/tb-ui/src/forms.rs) bewahrt diese in den vorhandenen PARENT-Eigenschaften; Getter, Root-Traversierung, Geometrie, Optionsgruppen und Menüpfade verwenden dieselbe Referenz. Die ursprünglichen CLI-Gegenproben liefern `1` sowohl aus FRM als auch aus TBC nach Löschen der Quelle. | +| V01 – ergänzend geprüfte Darstellung und Maus | `indexed_containers_preserve_parent_geometry_and_option_groups` prüft die Elterninstanz, absolute Position, unabhängige Optionsgruppen, sichtbare Beschriftung und Trefferziel. Vor der letzten Korrektur fing `Group(1)` den Klick auf sein Kind `Other` ab. `form_controls` stellt jetzt den Container vor seine Kinder; Zeichnung und umgekehrte Trefferprüfung nutzen dieselbe Reihenfolge. Die neue Gegenprobe besteht. | +| V01 – TBC-Grenze | [bytecode.rs](../../../../crates/tb-vm/src/bytecode.rs) validiert die effektiven Parent-Referenzen einschließlich Arrayinstanzen und Zyklen. `beschaedigte_container_werden_vor_der_ausfuehrung_abgewiesen` enthält beide Mutationen. Bestehender Mehrformular-/Array-Test bleibt grün. | +| V02 – falsches Laufzeitziel | [inventar.rs](../../../../crates/tb-cli/tests/inventar.rs) vergleicht unabhängige Sollziele in den 291 festen Vorlagen mit dem erzeugten Kompilat. Builtin-IDs und Konverterselektoren, konkrete Forms-Member, VM-Instruktionen und Deklarationsmetadaten werden geprüft. `$INCLUDE` durchläuft den echten CLI-Projektlader. Die ursprüngliche TIMEZONEKNOWN→TIMER-Mutation wird mit Name und Status abgewiesen. | +| V02 – weitere Mutationen | `falsche_gueltige_builtinbindung_und_entfernte_absenkung_fallen_durch`, `forms_absenkung_muss_den_richtigen_member_erreichen` und `gemeinsam_dispatchte_konverter_brauchen_den_richtigen_typselektor` bestehen; bestehende Status-/Unsupported-/fehlende-Bindung-Proben ebenfalls. Alle 845 Inventareinträge werden geprüft; 795 implementiert, 50 Non-Features, 0 offen. Ereignisse werden zusätzlich über 133 reale Auslösepfade bis zum BASIC-Handler geprüft. | +| V03 – weitere Non-Feature-Syntax | [parser.rs](../../../../crates/tb-frontend/src/parser.rs) erkennt zuerst mögliche Zuweisungen und weist danach ausgeschlossene Anweisungen vor der allgemeinen Argumentauflösung namentlich ab. CLI-Test prüft PSET/PRESET mit Farbe und STEP, CIRCLE STEP, WINDOW SCREEN und PALETTE USING einschließlich IF. Gleichnamige Arrays sowie SCREEN 0–13/LINE/PAINT/VIEW bleiben gültig. Beide ursprünglichen Grafik-Gegenproben bestehen. | +| Ergänzende Kohärenzprüfung | ComboBox-Ausklappzustand wird bei Fokuswechsel und Ausblenden zurückgesetzt; `combobox_dropdown_closes_on_focus_change_and_hide` besteht. | + +## Abschlussprüfungen + +- `cargo test --workspace`: **481 bestanden, 0 fehlgeschlagen, 2 ignoriert**. +- `python3 openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review-probes.py`: **5/5 Gegenproben bestanden**. Die Soll-Assertions bleiben erhalten; nur Testpfad und Cargo-Auflösung wurden an den verschobenen Inventartest angepasst. +- `TB_VBDOS_REPO=/tmp/terminalbasic-vbdos-evidence cargo test -p tb-cli --test foreign oeffentliche -- --ignored --nocapture`: **bestanden**, sieben öffentliche Programme jeweils zweimal; Revision `1cdd2b32b829fe1721d0b6aecc433abc47a96fb6`. +- `cargo run --manifest-path tests/terminalfrei/Cargo.toml --offline`: **bestanden**, `Terminalfreier FRM-/VM-Lauf: KM`. +- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: **bestanden**. +- `openspec validate --all --strict`: **16/16 Einträge gültig**. +- `cargo fmt --all -- --check` und `git diff --check`: **bestanden**, neue Dateien zusätzlich geprüft. + +Der Golden-Generator wurde nicht ausgeführt. Bei diesen Reviewkorrekturen +wurden keine weiteren Golden Files verändert. Kein Prüfschritt wurde wegen +fehlender Voraussetzungen ausgelassen; der normalerweise ignorierte +Fremdprogrammlauf wurde ausdrücklich ausgeführt. + +Die Umsetzung ist verifiziert. Am 06.09.2026 wurden alle vier Delta-Specs +in die Hauptspecs synchronisiert und dieser Change mit 11/11 erledigten Tasks +archiviert. Die 15 Hauptspecs bestehen die Validierung nach der Archivierung. diff --git a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/specs/forms-dateiformat/spec.md b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/forms-dateiformat/spec.md similarity index 73% rename from openspec/changes/spezifikationsabgleich-und-regressionsnachweise/specs/forms-dateiformat/spec.md rename to openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/forms-dateiformat/spec.md index ceb468e..bfa24d7 100644 --- a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/specs/forms-dateiformat/spec.md +++ b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/forms-dateiformat/spec.md @@ -7,7 +7,9 @@ geschriebener Vorgabewerte. Beim kanonischen Schreiben einer neuen oder geänderten Beschreibung SHALL nur geschrieben werden, was vom Vorgabewert abweicht. Für diese kanonische Ausgabe SHALL Reihenfolge und Einrückung festgelegt und dokumentiert sein, damit zwei Läufe -dieselbe Datei erzeugen. +dieselbe Datei erzeugen. Ein expliziter Control-Array-Index SHALL auch bei +`Index = 0` erhalten bleiben; er ist eine Strukturangabe und kein auslassbarer +Eigenschaftsdefault. #### Scenario: Rundlauf - **WHEN** eine Formulardatei gelesen und unverändert wieder geschrieben wird @@ -20,3 +22,7 @@ dieselbe Datei erzeugen. #### Scenario: Expliziter Default im unveränderten Original - **WHEN** eine gelesene Datei einen Vorgabewert ausdrücklich enthält und unverändert geschrieben wird - **THEN** bleibt auch diese Eigenschaftszeile bytegleich erhalten + +#### Scenario: Arrayelement mit Index null +- **WHEN** ein Formular mit einem einzigen Control-Arrayelement `Index = 0` kanonisch geschrieben und erneut gelesen wird +- **THEN** bleibt das Control ein Array und seine Indexangabe erhalten diff --git a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/specs/kompat-testkorpus/spec.md b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/kompat-testkorpus/spec.md similarity index 100% rename from openspec/changes/spezifikationsabgleich-und-regressionsnachweise/specs/kompat-testkorpus/spec.md rename to openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/kompat-testkorpus/spec.md diff --git a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/specs/sprach-frontend/spec.md b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/sprach-frontend/spec.md similarity index 100% rename from openspec/changes/spezifikationsabgleich-und-regressionsnachweise/specs/sprach-frontend/spec.md rename to openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/sprach-frontend/spec.md diff --git a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/specs/sprachinventar/spec.md b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/sprachinventar/spec.md similarity index 100% rename from openspec/changes/spezifikationsabgleich-und-regressionsnachweise/specs/sprachinventar/spec.md rename to openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/sprachinventar/spec.md diff --git a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/tasks.md b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/tasks.md similarity index 53% rename from openspec/changes/spezifikationsabgleich-und-regressionsnachweise/tasks.md rename to openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/tasks.md index 6ac0a08..39dc3bb 100644 --- a/openspec/changes/spezifikationsabgleich-und-regressionsnachweise/tasks.md +++ b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/tasks.md @@ -1,15 +1,22 @@ ## 1. Normtexte -- [ ] 1.1 Grafik-Abweisung und FRM-Schreibmodi gemäß Delta präzisieren; verifizieren mit tbc check für erlaubte Grafik, benannter Ablehnung weiterhin verbotener Formen und bytegleichem FRM-Rundlauf mit explizitem Default. +- [x] 1.1 Grafik-Abweisung und FRM-Schreibmodi gemäß Delta präzisieren; verifizieren mit tbc check für erlaubte Grafik, benannter Ablehnung weiterhin verbotener Formen und bytegleichem FRM-Rundlauf mit explizitem Default. ## 2. Nachweise -- [ ] 2.1 Inventarprüfung gegen erreichbare HIR-/Dispatch-Pfade erweitern; verifizieren mit künstlich offen/implementiert/verboten gesetzten Einträgen und einer fehlenden Runtime-Bindung, jeweils mit Namen und Status im Fehler. -- [ ] 2.2 Fehlende FORM_KEYPRESS/FORM_KEYUP-Auslöser ergänzen und registrierte Form-Ereignisse auf realen Quellenpfad prüfen; verifizieren durch BASIC-Handler nach Hosteingabe, nicht durch direktes queue_form_event allein. -- [ ] 2.3 Maskierungs- und Formular-Korpus ergänzen; verifizieren mit KEY/UEVENT in ON/OFF/STOP, normal lesbarer STOP-Taste, Control-Access-Key, BackTab, Fokusreihenfolge und zweimal identischer Ausgabe. -- [ ] 2.4 Öffentlichen Fremdprogrammbestand in dokumentierter Revision prüfen; verifizieren durch diagnosefreie Übersetzung und reproduzierbare deklarierte Eingabesequenzen bis zu sichtbaren Zustandsänderungen. Nicht vorhandene externe Voraussetzungen im Ergebnisbericht benennen, nicht abhaken. +- [x] 2.1 Inventarprüfung gegen erreichbare HIR-/Dispatch-Pfade erweitern; verifizieren mit künstlich offen/implementiert/verboten gesetzten Einträgen und einer fehlenden Runtime-Bindung, jeweils mit Namen und Status im Fehler. +- [x] 2.2 Fehlende FORM_KEYPRESS/FORM_KEYUP-Auslöser ergänzen und registrierte Form-Ereignisse auf realen Quellenpfad prüfen; verifizieren durch BASIC-Handler nach Hosteingabe, nicht durch direktes queue_form_event allein. +- [x] 2.3 Maskierungs- und Formular-Korpus ergänzen; verifizieren mit KEY/UEVENT in ON/OFF/STOP, normal lesbarer STOP-Taste, Control-Access-Key, BackTab, Fokusreihenfolge und zweimal identischer Ausgabe. +- [x] 2.4 Öffentlichen Fremdprogrammbestand in dokumentierter Revision prüfen; verifizieren durch diagnosefreie Übersetzung und reproduzierbare deklarierte Eingabesequenzen bis zu sichtbaren Zustandsänderungen. Nicht vorhandene externe Voraussetzungen im Ergebnisbericht benennen, nicht abhaken. ## 3. Gesamtabnahme -- [ ] 3.1 Alle F01–F25 nach Integration der Korrektur-Changes mit Regressionstest oder geklärtem Normtext abgleichen; verifizieren durch separaten Ergebnisreport mit Fundstellen pro F-ID. -- [ ] 3.2 Gesamten Korpus und Spec-Validierung ausführen; verifizieren mit cargo test --workspace, openspec validate --all --strict und git diff --check; Golden Files nicht ungeprüft neu aufzeichnen. +- [x] 3.1 Alle F01–F25 nach Integration der Korrektur-Changes mit Regressionstest oder geklärtem Normtext abgleichen; verifizieren durch separaten Ergebnisreport mit Fundstellen pro F-ID. +- [x] 3.2 Gesamten Korpus und Spec-Validierung ausführen; verifizieren mit cargo test --workspace, openspec validate --all --strict und git diff --check; Golden Files nicht ungeprüft neu aufzeichnen. + +## 4. Reviewkorrekturen + +- [x] 4.1 V01: Indizierte Parent-Referenzen durch Import, Runtime und Kompilat erhalten; Positionierung, Gruppierung und ungültige Referenzen prüfen. +- [x] 4.2 V02: Inventarelemente gegen unabhängige konkrete Absenkungsziele prüfen; falsche gültige Bindungen und entfernte Absenkung müssen scheitern. +- [x] 4.3 V03: Ausgeschlossene Grafikvarianten durchgehend namentlich abweisen; erlaubte Grafik weiter übersetzen. +- [x] 4.4 Unabhängige Gegenproben, vollständige Verifizierung und Ergebnisberichte ohne offene Befunde abschließen. diff --git a/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/verification.md b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/verification.md new file mode 100644 index 0000000..97053f8 --- /dev/null +++ b/openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/verification.md @@ -0,0 +1,72 @@ +# Implementierungs- und Verifikationsnachweis + +**Nachprüfung abgeschlossen:** V01–V03 und die ergänzend geprüfte Container-Z-Reihenfolge sind behoben. Der aktuelle [Review](review.md) enthält die Abschlussprüfung; der [erste Review](review-initial.md) bleibt als Ausgangsbefund erhalten. + +Change: `spezifikationsabgleich-und-regressionsnachweise`, 06.09.2026. +Grundlage: Arbeitsbaum auf `c8b92f06190d77714865e0018b090348f372d77e`. +Proposal, Design D1–D4, Tasks und alle vier Delta-Specs wurden abgeglichen. + +| Dimension | Ergebnis | +|---|---| +| Vollständigkeit | 11/11 Tasks, 6/6 Requirements, 18/18 Szenarien | +| Korrektheit | Regressionssuite, Korpus und gesonderter Fremdprogrammlauf bestanden | +| Kohärenz | Vorhandene Compiler-/VM-/CaptureHost-Pfade erweitert; keine neue Abhängigkeit oder TBC-Version | +| Offene Befunde im Change | 0 CRITICAL, 0 WARNING, 0 SUGGESTION | + +## Umsetzung und Nachweis je Task + +| Task | Umsetzung / Nachweis | +|---|---| +| 1.1 | Grafik-Ausnahmen und verbleibende Non-Features in Sprachreferenz und Delta; CLI prüft SCREEN 0–13/LINE/PAINT/VIEW sowie benannte POKE/PEEK/CIRCLE/WINDOW/IOCTL-Abweisung. FRM-Erhaltung, Default-Auslassung und strukturelles Index 0 sind durch Writer-Tests und Normtext getrennt. | +| 2.1 | Inventartest liegt wegen der Prüfung des erzeugten Bytecodes jetzt in tb-cli. 291 feste BASIC-Vorlagen plus Forms-Proben prüfen 845 Einträge; Statusmutation, Unsupported, fehlende oder vertauschte Bindungen, falsche Forms-Member/Typselektoren und entfernte Absenkung erzeugen benannte Fehler. Die vierte TSV-Spalte enthält die unabhängigen Sollziele; `$INCLUDE` wird vom CLI aufgelöst. 795 implementiert, 50 Non-Features, 0 offen. | +| 2.2 | Alle 133 registrierten Form-/Control-Ereignisse erreichen aus ihren Quellen den BASIC-Handler. Die Matrix entdeckt fehlende Quellen; eine künstlich entfernte Event-Bindung bestätigt den Fehlerpfad. Keine dieser Nachweisproben verwendet queue_form_event. | +| 2.3 | trap-masken.bas und formular-access-backtab.frm ergänzen die fehlenden Kombinationen. F1 unter STOP bleibt lesbar und löst nach ON genau einen Handler aus. Alt+O und Shift+Tab ergeben Click;LostFocus;GotFocus;. Jeder Korpuslauf wird wiederholt und gegen sein Sollbild verglichen. | +| 2.4 | Sieben öffentliche Programme aus cout/vbdos im Commit 1cdd2b32b829fe1721d0b6aecc433abc47a96fb6, jeweils zweimal frisch exportiert, diagnosefrei gebaut und mit festen Eingaben bedient. Keine fehlende Voraussetzung. Der gesonderte Test ist bewusst unabhängig vom normalen Workspace-Lauf aufrufbar. | +| 3.1 | [Abschlussreport](../../../../docs/reviews/2026-09-06-openspec-abschlussreview.md) enthält eine eigene Zeile mit aktuellem Beleg für jede F-ID sowie die sieben reproduzierbaren Fremdprogrammsequenzen. Historischer Bericht bleibt erhalten. | +| 3.2 | 481 Workspace-Tests und gesonderter Fremdtest bestanden; separater terminalfreier Resolver erfolgreich. Strikte Spec-Validierung, Clippy, Format- und Diff-Prüfung bestanden. Golden-Generator nicht ausgeführt. | +| 4.1 | Vollständige Parent-Schlüssel im Import, Getter, Container-Traversierung, Geometrie, Optionsgruppen und Menüzuordnung. `indexed_containers_preserve_parent_geometry_and_option_groups` prüft zusätzlich sichtbare Kinder und korrekte Mausziele. TBC prüft Zyklen und fehlende indizierte Eltern; Quell-/TBC-Gegenprobe gibt jeweils 1 aus. | +| 4.2 | `falsche_gueltige_builtinbindung_und_entfernte_absenkung_fallen_durch`, `forms_absenkung_muss_den_richtigen_member_erreichen`, `gemeinsam_dispatchte_konverter_brauchen_den_richtigen_typselektor`; ursprüngliche TIMEZONEKNOWN→TIMER-Gegenprobe schlägt korrekt beim Prüfer fehl. | +| 4.3 | CLI-Proben für PSET/PRESET mit Farbe und STEP, CIRCLE STEP, WINDOW SCREEN und PALETTE USING einschließlich IF; gleichnamige Variablen/Arrays und erlaubte Grafik bleiben gültig. | +| 4.4 | Gesamtabnahme und erneute Gegenproben bestanden; zusätzlicher Test `combobox_dropdown_closes_on_focus_change_and_hide` verhindert veralteten Ausklappzustand. | + +## Szenarien + +Testnamen stehen in den verlinkten Testdateien. Die Ereignismatrix umfasst +16 Form-Ereignisse und 117 Control-Ereignisse. + +| Requirement / Szenario | Ausführbarer Nachweis | +|---|---| +| FRM: Rundlauf | [frm.rs](../../../../crates/tb-ui/src/frm.rs): reads_nested_form_and_preserves_source_exactly | +| FRM: Vorgabewerte nicht geschrieben | writes_no_properties_for_default_only_control; explicit_default_is_preserved_until_canonical_write prüft zusätzlich die geänderte Beschreibung | +| FRM: expliziter Default im Original | explicit_default_is_preserved_until_canonical_write, mit CRLF und Enabled = −1 | +| FRM: einzelnes Arrayelement Index 0 | canonical_index_zero_remains_an_array_and_array_parents_are_preserved | +| Inventar: offen trotz Implementierung | [inventar.rs](../../../../crates/tb-cli/tests/inventar.rs): falscher_status_und_fehlende_runtimebindung_werden_namentlich_erkannt | +| Inventar: implementiert trotz Unsupported | implementiert_mit_unsupported_marker_faellt_durch | +| Inventar: Abdeckungsstand | abdeckungsstand_wird_ausgewiesen; jeder_inventareintrag_erreicht_hir_und_laufzeitziel | +| Inventar: Ereignis ohne Auslöser | [events.rs](../../../../crates/tb-vm/tests/events.rs): fehlender_ereignispfad_wird_namentlich_erkannt; registrierte_form_ereignisse_erreichen_basic_aus_realen_quellen und entsprechende Control-Matrix | +| Traps: Zeit-Trap | [traps.bas](../../../../tests/compat/traps.bas), Hostuhr im Programmkopf | +| Traps: fehlende Zeitdeklaration | [compat.rs](../../../../crates/tb-cli/tests/compat.rs): Test mit should_panic auf fehlenden Zeitverlauf | +| Traps: KEY STOP | [trap-masken.bas](../../../../tests/compat/trap-masken.bas), F1 lesbar und nach ON genau einmal | +| Forms: Formularprogramm | [formular.frm](../../../../tests/compat/formular.frm): Maus, Liste, Timer, Menü, modaler Dialog | +| Forms: Wiederholbarkeit | korpus_laeuft_mit_korrekter_ausgabe führt jedes Programm zweimal aus | +| Forms: Control-Access-Key/Rückwärtstab | [formular-access-backtab.frm](../../../../tests/compat/formular-access-backtab.frm) | +| Fremdprogramm: diagnosefrei und bedienbar | [foreign.rs](../../../../crates/tb-cli/tests/foreign.rs): oeffentliche_formularprogramme_sind_reproduzierbar_bedienbar | +| Fremdprogramm: wiederholbare Zustände | Derselbe Test: zwei isolierte Läufe pro Einstieg, Vergleich aller Bildschirm-/Formular-/Menüzustände, zusätzlich explizite Endzustände und Spin-Wert | +| Non-Features: Hardware-Zugriff | [project.rs](../../../../crates/tb-cli/tests/project.rs): erlaubte_grafik_und_benannte_nonfeatures_werden_per_cli_geprueft | +| Non-Features: erlaubte Grafik | Derselbe CLI-Test prüft alle 14 SCREEN-Modi mit LINE, PAINT und VIEW | + +## Review der während der Umsetzung entdeckten Ursachen + +Der Abschlussreport dokumentiert Signaturen, benannte Non-Feature-Diagnosen, +fehlende Ereignisquellen, Fokus-Nebenwirkungen, FRM-Array-Flag, eingebettete +Includes und Array-Eltern. Alle sind korrigiert und durch die obigen Tests +oder den gesonderten Fremdprogrammlauf abgedeckt. Der Mehrformular-Test wurde +nach Anpassung der Importauflösung erneut erfolgreich ausgeführt. + +Die einzige Änderung an einem bestehenden Sollbild ist ausdrücklich geprüft: +formular.out, Zeile 4, Spalten 24–33, zehn Fokusattribute auf f1. Zeichenebene +und übrige Zellen sind unverändert. Zwei neue Sollbilder wurden semantisch +festgelegt und zweimal bestätigt. Kein automatisches Neuaufzeichnen. + +Die Verifikation wurde vollständig durchgeführt. Am 06.09.2026 wurden die +Hauptspecs synchronisiert und der Change archiviert. diff --git a/openspec/specs/forms-dateiformat/spec.md b/openspec/specs/forms-dateiformat/spec.md index d0cc62c..135e4fe 100644 --- a/openspec/specs/forms-dateiformat/spec.md +++ b/openspec/specs/forms-dateiformat/spec.md @@ -29,19 +29,32 @@ abgewiesen werden. - **THEN** wird sie abgewiesen und die Meldung nennt `3.00` ### Requirement: Schreiben ist die Umkehrung des Lesens -Das Schreiben einer gelesenen Formularbeschreibung SHALL dieselbe Datei -ergeben. Geschrieben SHALL nur werden, was vom Vorgabewert abweicht; -Reihenfolge und Einrückung SHALL festgelegt und dokumentiert sein, damit -zwei Läufe dieselbe Datei erzeugen. +Das unveränderte Schreiben einer gelesenen Formularbeschreibung SHALL +die Originaldatei bytegleich erhalten, einschließlich explizit +geschriebener Vorgabewerte. Beim kanonischen Schreiben einer neuen oder +geänderten Beschreibung SHALL nur geschrieben werden, was vom +Vorgabewert abweicht. Für diese kanonische Ausgabe SHALL Reihenfolge +und Einrückung festgelegt und dokumentiert sein, damit zwei Läufe +dieselbe Datei erzeugen. Ein expliziter Control-Array-Index SHALL auch bei +`Index = 0` erhalten bleiben; er ist eine Strukturangabe und kein auslassbarer +Eigenschaftsdefault. #### Scenario: Rundlauf - **WHEN** eine Formulardatei gelesen und unverändert wieder geschrieben wird - **THEN** ist die geschriebene Datei byte-gleich zur gelesenen #### Scenario: Vorgabewerte werden nicht geschrieben -- **WHEN** ein Steuerelement nur Vorgabewerte trägt +- **WHEN** eine neue oder geänderte Beschreibung kanonisch geschrieben wird und ein Steuerelement nur Vorgabewerte trägt - **THEN** enthält sein Block außer `Begin`/`End` keine Eigenschaftszeile +#### Scenario: Expliziter Default im unveränderten Original +- **WHEN** eine gelesene Datei einen Vorgabewert ausdrücklich enthält und unverändert geschrieben wird +- **THEN** bleibt auch diese Eigenschaftszeile bytegleich erhalten + +#### Scenario: Arrayelement mit Index null +- **WHEN** ein Formular mit einem einzigen Control-Arrayelement `Index = 0` kanonisch geschrieben und erneut gelesen wird +- **THEN** bleibt das Control ein Array und seine Indexangabe erhalten + ### Requirement: Fehlerhafte Dateien werden benannt Eine unbekannte Klasse, eine für die Klasse unbekannte Eigenschaft, ein Wert außerhalb des Wertebereichs und ein unausgeglichener Block MUST je diff --git a/openspec/specs/kompat-testkorpus/spec.md b/openspec/specs/kompat-testkorpus/spec.md index 17ebdce..56c7d23 100644 --- a/openspec/specs/kompat-testkorpus/spec.md +++ b/openspec/specs/kompat-testkorpus/spec.md @@ -121,6 +121,8 @@ eines Handlers. Ein Korpusprogramm mit Zeitbezug SHALL seinen Zeitverlauf im Programmkopf deklarieren; ohne Deklaration MUST der Harness den Lauf abweisen statt auf die Systemuhr auszuweichen. +Die drei Zustände SHALL für KEY und UEVENT ebenso ausdrücklich im Korpus vorkommen wie für TIMER, einschließlich der Lesbarkeit einer unter KEY STOP gemerkten Taste. + #### Scenario: Zeit-Trap im Korpus - **WHEN** ein Korpusprogramm mit deklariertem Zeitverlauf und `ON TIMER` ausgeführt wird - **THEN** entspricht der Bildschirminhalt byte-genau der Sollausgabe und der Lauf benötigt keine messbare Wartezeit @@ -129,6 +131,10 @@ Harness den Lauf abweisen statt auf die Systemuhr auszuweichen. - **WHEN** ein Korpusprogramm zeitgesteuerte Traps nutzt, aber keinen Zeitverlauf deklariert - **THEN** schlägt der Harness mit einer Meldung fehl, die die fehlende Deklaration nennt +#### Scenario: KEY STOP im Korpus +- **WHEN** der Korpus einen F1-Druck unter KEY STOP und ein späteres ON ausführt +- **THEN** prüft seine Sollausgabe sowohl die normal lesbare Taste als auch genau eine spätere Handlerausführung + ### Requirement: Korpusabdeckung der Formularprogramme Der Testkorpus SHALL Formularprogramme mit byte-genauer Bildschirm-Sollausgabe führen. Ihre Ereignisfolge (Tasten, Maus, Zeit) @@ -138,6 +144,8 @@ dasselbe Ergebnis liefert. Abgedeckt SHALL sein: Fokus- und Tabreihenfolge, Access-Key, Klick über die Maus, Menüauswahl, ein Listen-Steuerelement, ein Timer und ein modaler Dialog. +Der Access-Key-Nachweis SHALL mindestens ein gewöhnliches Control betreffen und MUST NOT allein durch einen Menü-Access-Key ersetzt werden. Rückwärtstab und die beobachtbare Reihenfolge LostFocus vor GotFocus SHALL enthalten sein. + #### Scenario: Formularprogramm im Korpus - **WHEN** ein Formular-Korpusprogramm mit deklarierter Ereignisfolge ausgeführt wird - **THEN** entspricht der Bildschirminhalt byte-genau der Sollausgabe @@ -146,12 +154,22 @@ Listen-Steuerelement, ein Timer und ein modaler Dialog. - **WHEN** dasselbe Programm zweimal ausgeführt wird - **THEN** ist die Ausgabe beide Male identisch +#### Scenario: Control-Access-Key und Rückwärtstab +- **WHEN** ein Formular-Korpus Alt+O für eine OK-Schaltfläche und anschließend Rückwärtstab deklariert +- **THEN** weist die Sollausgabe den Click und den Fokuswechsel in korrekter Reihenfolge nach + ### Requirement: Kompatibilitätsnachweis an Fremdprogrammen Die Formularprogramme aus dem öffentlichen Bestand des Vorbilds SHALL sich ohne Diagnosen oder Non-Features übersetzen lassen und bedienbar sein. Jeder entdeckte fehlende Sprach-, Projekt-, Formular- oder Laufzeitpfad MUST implementiert und als Befund festgehalten werden. +Der Nachweis SHALL den verwendeten öffentlichen Bestand mit Revision und Programmeinstiegen benennen sowie reproduzierbare Eingabesequenzen und sichtbare Zustandsänderungen führen. Ein bloß erfolgreiches Starten MUST NOT als Bediennachweis gelten. + #### Scenario: Fremdprogramm übersetzt - **WHEN** ein Formularprogramm des öffentlichen Bestands übersetzt wird - **THEN** entstehen keine Diagnosen und das Programm ist bedienbar + +#### Scenario: Bediennachweis ist wiederholbar +- **WHEN** ein dokumentiertes Fremdprogramm erneut mit seiner Prüfsequenz ausgeführt wird +- **THEN** werden dieselben relevanten Formularzustände beziehungsweise Abschlussereignisse erreicht diff --git a/openspec/specs/sprach-frontend/spec.md b/openspec/specs/sprach-frontend/spec.md index abd052a..19721fb 100644 --- a/openspec/specs/sprach-frontend/spec.md +++ b/openspec/specs/sprach-frontend/spec.md @@ -84,15 +84,23 @@ Sprungziele („Label not defined") sowie Prozedur- und Builtin-Signaturen Die Semantik MUST jede Verwendung der in docs/sprachreferenz.md („Abweichungen") deklarierten Non-Features — Hardware-Nähe (`PEEK`, `POKE`, `INP`, `OUT`, `WAIT`, `BLOAD`/`BSAVE`, `VARPTR`-Familie, `CALL -ABSOLUTE`/`INTERRUPT`, `IOCTL`), `CHAIN`, Grafik-Anweisungen, `SOUND`/ +ABSOLUTE`/`INTERRUPT`, `IOCTL`), `CHAIN`, die weiterhin ausgeschlossenen Grafik-Anweisungen (`PSET`, +`PRESET`, `CIRCLE`, `DRAW`, Grafik-`GET`/`PUT`, `PALETTE`, `WINDOW`, +`PMAP`, `PCOPY`, `POINT`), `SOUND`/ `PLAY` und die Event-Geräte `COM`/`PEN`/`PLAY`/`STRIG` — zur Compile-Zeit mit der Meldung „Feature unavailable" abweisen. Dokumentierte Features außerhalb dieser Liste MUST NOT abgewiesen werden (Guiding Principle). +Die dokumentierte Zellenpuffer-Untermenge SCREEN 0–13, LINE, PAINT und VIEW SHALL verfügbar bleiben. + #### Scenario: Hardware-Zugriff - **WHEN** `POKE 100, 1` oder `x = PEEK(100)` kompiliert wird - **THEN** erscheint die Compile-Diagnose „Feature unavailable" +#### Scenario: Erlaubte Grafikuntermenge +- **WHEN** ein Programm SCREEN 1, LINE, PAINT und VIEW in gültiger Syntax verwendet +- **THEN** wird es ohne Feature-unavailable-Diagnose übersetzt + ### Requirement: Diagnostik mit Vorbild-Meldungen Diagnosen SHALL exakte 1-basierte Quelltextpositionen (Zeile, Spalte) tragen; die Meldungstexte SHALL dem Katalog des Vorbilds folgen, wo eine diff --git a/openspec/specs/sprachinventar/spec.md b/openspec/specs/sprachinventar/spec.md index d66a63f..e5fdf32 100644 --- a/openspec/specs/sprachinventar/spec.md +++ b/openspec/specs/sprachinventar/spec.md @@ -64,6 +64,8 @@ mit „Feature unavailable" abgewiesen werden. Der Test MUST das divergierende Element, seinen Inventarstatus und den vorgefundenen Zustand benennen. +Klassentabellen oder deklarierte Signaturnamen allein MUST NOT als Laufzeitnachweis gelten. Für implementierte Elemente SHALL ein erreichbarer Frontend-/Absenkungspfad und ein ausführbares Laufzeitziel geprüft werden. Für deklarierte Ereignisse SHALL ein Test den Auslöser bis zum BASIC-Handler führen. + #### Scenario: Inventar hinkt der Implementierung hinterher - **WHEN** `LOCATE` implementiert wurde, im Inventar aber noch `offen` steht - **THEN** schlägt der Abgleich fehl und nennt `LOCATE`, Status `offen` und den Befund „implementiert" @@ -76,6 +78,10 @@ Zustand benennen. - **WHEN** der Abgleich läuft - **THEN** nennt er die Zahl der Einträge je Status als Abdeckungsstand +#### Scenario: Deklariertes Ereignis ohne Auslöser +- **WHEN** ein als implementiert geführtes Formularereignis bei seiner dokumentierten Hosteingabe keinen Handler ausführt +- **THEN** schlägt der Abgleich mit Name, Status und fehlendem Auslösepfad fehl + ### Requirement: Keine stillen Lücken bei dokumentierten Elementen Ein Element mit Status `offen` MUST bei Verwendung eine benannte Ablehnung erzeugen — entweder eine Compile-Diagnose oder den Laufzeitfehler diff --git a/tests/compat/formular-access-backtab.frm b/tests/compat/formular-access-backtab.frm new file mode 100644 index 0000000..6c1a2b3 --- /dev/null +++ b/tests/compat/formular-access-backtab.frm @@ -0,0 +1,37 @@ +VERSION 1.00 +Begin Form Formular + Height = 10 + Width = 40 + Begin TextBox Text1 + TabIndex = 0 + End + Begin CommandButton Ok + Caption = "&OK" + TabIndex = 1 + Top = 3 + End +End +' tb-screen: 80x25 +' tb-events: key:%o | key:+ +DIM SHARED trace$ +Formular.Show +Ok.SetFocus +DOEVENTS +DOEVENTS +DOEVENTS +Formular.Hide +CLS +PRINT trace$ +END +SUB Ok_Click() +SHARED trace$ +trace$ = trace$ + "Click;" +END SUB +SUB Ok_LostFocus() +SHARED trace$ +trace$ = trace$ + "LostFocus;" +END SUB +SUB Text1_GotFocus() +SHARED trace$ +trace$ = trace$ + "GotFocus;" +END SUB diff --git a/tests/compat/formular-access-backtab.out b/tests/compat/formular-access-backtab.out new file mode 100644 index 0000000..a94c654 --- /dev/null +++ b/tests/compat/formular-access-backtab.out @@ -0,0 +1 @@ +Click;LostFocus;GotFocus; diff --git a/tests/compat/formular.out b/tests/compat/formular.out index 7e63925..b681ec5 100644 --- a/tests/compat/formular.out +++ b/tests/compat/formular.out @@ -18,7 +18,7 @@ 70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070 70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070 70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070 -70700707070707070707070707070707070707077070700707070707070707070770707070707070707070707070 +7070070707070707070707070707070707070707707070f1f1f1f1f1f1f1f1f1f170707070707070707070707070 70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070 70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070 70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070 diff --git a/tests/compat/trap-masken.bas b/tests/compat/trap-masken.bas new file mode 100644 index 0000000..4f5a77f --- /dev/null +++ b/tests/compat/trap-masken.bas @@ -0,0 +1,62 @@ +' tb-screen: 80x25 +' tb-keys: +' F3 ist ON, F2 OFF, F1 STOP; EVENT OFF verwirft UEVENT. +ON KEY(1) GOSUB K1 +ON KEY(2) GOSUB K2 +ON KEY(3) GOSUB K3 +KEY(1) STOP +KEY(2) OFF +KEY(3) ON +DOEVENTS +IF k1% = 0 AND k2% = 0 AND k3% = 1 THEN PRINT "KEY ON/OFF/STOP: genau F3" +a$ = INKEY$ +b$ = INKEY$ +IF a$ = CHR$(0) + CHR$(60) THEN PRINT "KEY OFF: F2 lesbar" +IF b$ = CHR$(0) + CHR$(59) THEN PRINT "KEY STOP: F1 lesbar" +IF INKEY$ = "" THEN PRINT "KEY ON: F3 konsumiert" +KEY(1) ON +DOEVENTS +DOEVENTS +IF k1% = 1 THEN PRINT "KEY STOP -> ON: genau einmal" +ON UEVENT GOSUB U +UEVENT OFF +CALL SetUEvent +DOEVENTS +UEVENT ON +DOEVENTS +IF u% = 0 THEN PRINT "UEVENT OFF: verworfen" +CALL SetUEvent +DOEVENTS +IF u% = 1 THEN PRINT "UEVENT ON: zugestellt" +UEVENT STOP +CALL SetUEvent +CALL SetUEvent +DOEVENTS +IF u% = 1 THEN PRINT "UEVENT STOP: gesperrt" +UEVENT ON +DOEVENTS +DOEVENTS +IF u% = 2 THEN PRINT "UEVENT STOP -> ON: genau einmal" +EVENT OFF +CALL SetUEvent +DOEVENTS +IF u% = 2 THEN PRINT "UEVENT EVENT OFF: gesperrt" +EVENT ON +DOEVENTS +IF u% = 2 THEN PRINT "UEVENT EVENT OFF: verworfen" +CALL SetUEvent +DOEVENTS +IF u% = 3 THEN PRINT "UEVENT EVENT ON: wieder aktiv" +END +K1: +k1% = k1% + 1 +RETURN +K2: +k2% = k2% + 1 +RETURN +K3: +k3% = k3% + 1 +RETURN +U: +u% = u% + 1 +RETURN diff --git a/tests/compat/trap-masken.out b/tests/compat/trap-masken.out new file mode 100644 index 0000000..7c205d3 --- /dev/null +++ b/tests/compat/trap-masken.out @@ -0,0 +1,12 @@ +KEY ON/OFF/STOP: genau F3 +KEY OFF: F2 lesbar +KEY STOP: F1 lesbar +KEY ON: F3 konsumiert +KEY STOP -> ON: genau einmal +UEVENT OFF: verworfen +UEVENT ON: zugestellt +UEVENT STOP: gesperrt +UEVENT STOP -> ON: genau einmal +UEVENT EVENT OFF: gesperrt +UEVENT EVENT OFF: verworfen +UEVENT EVENT ON: wieder aktiv diff --git a/tests/support/inventar-quellen.tsv b/tests/support/inventar-quellen.tsv new file mode 100644 index 0000000..427f253 --- /dev/null +++ b/tests/support/inventar-quellen.tsv @@ -0,0 +1,291 @@ +CLS Anweisung CLS builtin:40 +COLOR Anweisung COLOR 7,0 builtin:41 +CSRLIN Funktion PRINT CSRLIN builtin:49 +INPUT Anweisung INPUT s$ code:Input(1, false +LINE INPUT Anweisung LINE INPUT s$ code:Input(1, true +LOCATE Anweisung LOCATE 1,1 builtin:42 +POS Funktion PRINT POS(1) builtin:50 +PRINT Anweisung PRINT "x" builtin:31 +SCREEN Anweisung SCREEN 1 builtin:45 +SCREEN Funktion PRINT SCREEN(1, 1) builtin:51 +SPC Funktion PRINT SPC(1) builtin:35 +TAB Funktion PRINT TAB(1) builtin:34 +VIEW PRINT Anweisung VIEW PRINT 1 TO 20 builtin:44 +WIDTH Anweisung WIDTH 80,25 builtin:43 +CHDIR Anweisung CHDIR "." builtin:104 +CHDRIVE Anweisung CHDRIVE "C" builtin:105 +CLOSE Anweisung CLOSE builtin:90 +CURDIR$ Funktion PRINT CURDIR$ builtin:108 +CVC Funktion PRINT CVC("x") builtin:114|code:PushLng(4) +CVD Funktion PRINT CVD("x") builtin:114|code:PushLng(3) +CVDMBF Funktion PRINT CVDMBF("x") builtin:114|code:PushLng(3) +CVI Funktion PRINT CVI("x") builtin:114|code:PushLng(0) +CVL Funktion PRINT CVL("x") builtin:114|code:PushLng(1) +CVS Funktion PRINT CVS("x") builtin:114|code:PushLng(2) +CVSMBF Funktion PRINT CVSMBF("x") builtin:114|code:PushLng(2) +DIR$ Funktion PRINT DIR$ builtin:109 +EOF Funktion PRINT EOF(1) builtin:93 +FIELD Anweisung FIELD #1, 4 AS s$ code:Field(1) +FILEATTR Funktion PRINT FILEATTR(1, 1) builtin:99 +FILES Anweisung FILES "*.txt" builtin:103 +FREEFILE Funktion PRINT FREEFILE builtin:98 +GET Anweisung GET #1, , n% code:GetPut(false, false +INPUT # Anweisung INPUT #1, s$ code:InputFile(1, false) +KILL Anweisung KILL "x" builtin:101 +LINE INPUT # Anweisung LINE INPUT #1, s$ code:InputFile(1, true) +LOC Funktion PRINT LOC(1) builtin:95 +LOCK Anweisung LOCK #1 builtin:100 +LOF Funktion PRINT LOF(1) builtin:94 +LPOS Funktion PRINT LPOS(1) builtin:110 +LPRINT Anweisung LPRINT "x" builtin:91 +LPRINT USING Anweisung LPRINT USING "#";1 builtin:37 +LSET Anweisung LSET s$ = "x" code:LsetRset(false) +MKC$ Funktion PRINT MKC$(1) builtin:113|code:PushLng(4) +MKD$ Funktion PRINT MKD$(1) builtin:113|code:PushLng(3) +MKDIR Anweisung MKDIR "x" builtin:106 +MKDMBF$ Funktion PRINT MKDMBF$(1) builtin:113|code:PushLng(3) +MKI$ Funktion PRINT MKI$(1) builtin:113|code:PushLng(0) +MKL$ Funktion PRINT MKL$(1) builtin:113|code:PushLng(1) +MKS$ Funktion PRINT MKS$(1) builtin:113|code:PushLng(2) +MKSMBF$ Funktion PRINT MKSMBF$(1) builtin:113|code:PushLng(2) +NAME Anweisung NAME "x" AS "y" builtin:102 +OPEN Anweisung OPEN "x" FOR INPUT AS #1 builtin:88 +PRINT # Anweisung PRINT #1, "x" builtin:91 +PUT Anweisung PUT #1, , n% code:GetPut(true, false +RESET Anweisung RESET builtin:90 +RMDIR Anweisung RMDIR "x" builtin:107 +RSET Anweisung RSET s$ = "x" code:LsetRset(true) +SEEK Anweisung SEEK #1, 1 builtin:97 +SEEK Funktion PRINT SEEK(1) builtin:96 +SHELL Anweisung SHELL "echo x" builtin:111 +SHELL Funktion PRINT SHELL("echo x") builtin:112 +UNLOCK Anweisung UNLOCK #1 builtin:100 +WRITE Anweisung WRITE "x" builtin:92 +WRITE # Anweisung WRITE #1, "x" builtin:92 +DATE$ Anweisung DATE$ = "01-01-2000" builtin:57 +DATE$ Funktion PRINT DATE$ builtin:55 +DATESERIAL Funktion PRINT DATESERIAL(1, 1, 1) builtin:60 +DATEVALUE Funktion PRINT DATEVALUE("x") builtin:62 +DAY Funktion PRINT DAY(1) builtin:64 +HOUR Funktion PRINT HOUR(1) builtin:68 +MINUTE Funktion PRINT MINUTE(1) builtin:69 +MONTH Funktion PRINT MONTH(1) builtin:65 +NOW Funktion PRINT NOW builtin:59 +TIMEZONEKNOWN Funktion PRINT TIMEZONEKNOWN builtin:156 +SECOND Funktion PRINT SECOND(1) builtin:70 +TIME$ Anweisung TIME$ = "12:00:00" builtin:58 +TIME$ Funktion PRINT TIME$ builtin:56 +TIMER Funktion PRINT TIMER builtin:54 +TIMESERIAL Funktion PRINT TIMESERIAL(1, 1, 1) builtin:61 +TIMEVALUE Funktion PRINT TIMEVALUE("x") builtin:63 +WEEKDAY Funktion PRINT WEEKDAY(1) builtin:67 +YEAR Funktion PRINT YEAR(1) builtin:66 +COMMON Anweisung COMMON SHARED x% global:X%:Int +CONST Anweisung CONST x = 7\nPRINT x code:PushDbl(7.0) +DATA Anweisung DATA 1 data:1 +DEFtype Anweisung DEFINT A-Z\nA=1 global:A%:Int +DIM Anweisung DIM x AS LONG global:X:Lng +ERASE Anweisung DIM a(2)\nERASE a code:EraseSlot( +LBOUND Funktion DIM a(2)\nPRINT LBOUND(a) code:ArrBound(true) +LET Anweisung LET x = 1 code:StoreGlobal( +OPTION BASE Anweisung OPTION BASE 1 base:1 +READ Anweisung DATA 1\nREAD x code:ReadData(1) +REDIM Anweisung REDIM a(2) code:RedimArr( +REM Anweisung REM Text comment +RESTORE Anweisung DATA 1\nRESTORE code:Restore(0) +SHARED Anweisung END\nSUB Test\nSHARED x\nEND SUB global:X!:Sng +STATIC Anweisung END\nSUB Test\nSTATIC x\nEND SUB global:STATIC.X!:Sng +SWAP Anweisung SWAP x,y code:StoreGlobal(0)|code:StoreGlobal(1) +TYPE Anweisung TYPE R\n x AS INTEGER\nEND TYPE udt:R +UBOUND Funktion DIM a(2)\nPRINT UBOUND(a) code:ArrBound(false) +DOEVENTS Funktion PRINT DOEVENTS code:Doevents +EVENT Anweisung EVENT OFF code:EventSwitch(false) +KEY (Ereignis) Anweisung KEY(1) STOP code:TrapSet(0, 2) +ON KEY Anweisung ON KEY(1) GOSUB H\nEND\nH:\nRETURN code:TrapDefine(0, +ON PEN Anweisung ON PEN GOSUB H\nEND\nH:\nRETURN Non-Feature +ON PLAY Anweisung ON PLAY(1) GOSUB H\nEND\nH:\nRETURN Non-Feature +ON SIGNAL Anweisung ON SIGNAL(1) GOSUB H\nEND\nH:\nRETURN code:TrapDefine(3, +ON STRIG Anweisung ON STRIG(1) GOSUB H\nEND\nH:\nRETURN Non-Feature +ON TIMER Anweisung ON TIMER(1) GOSUB H\nEND\nH:\nRETURN code:TrapDefine(1, +ON UEVENT Anweisung ON UEVENT GOSUB H\nEND\nH:\nRETURN code:TrapDefine(2, +PEN Anweisung PEN ON Non-Feature +PLAY (Ereignis) Anweisung PLAY ON Non-Feature +SetUEvent Routine CALL SetUEvent builtin:148 +SIGNAL Anweisung SIGNAL(1) ON code:TrapSet(3, 0) +STRIG Anweisung STRIG(1) ON Non-Feature +TIMER Anweisung TIMER OFF code:TrapSet(1, 1) +UEVENT Anweisung UEVENT ON code:TrapSet(2, 0) +ERL Funktion PRINT ERL code:LoadErl +ERR Anweisung ERR = 5 code:SetErr +ERR Funktion PRINT ERR code:LoadErr +ERROR Anweisung ERROR 5 code:RaiseError +ON ERROR Anweisung ON ERROR GOTO H\nEND\nH:\nRESUME NEXT code:OnErrorGoto( +RESUME Anweisung ON ERROR GOTO H\nEND\nH:\nRESUME NEXT code:ResumeNext +DDB# Funktion PRINT DDB#(1, 1, 1, 1) builtin:87 +FV# Funktion PRINT FV#(.1, 2, 1, 1, 0) builtin:75 +IPMT# Funktion PRINT IPMT#(1, 1, 1, 1, 1, 1) builtin:79 +IRR# Funktion DIM a(2) AS DOUBLE\nPRINT IRR#(a, .1) builtin:83 +MIRR# Funktion DIM a(2) AS DOUBLE\nPRINT MIRR#(a, .1, .2) builtin:84 +NPER# Funktion PRINT NPER#(.1, 1, 1, 1, 0) builtin:78 +NPV# Funktion DIM a(2) AS DOUBLE\nPRINT NPV#(.1, a) builtin:82 +PMT# Funktion PRINT PMT#(.1, 2, 1, 1, 0) builtin:77 +PPMT# Funktion PRINT PPMT#(1, 1, 1, 1, 1, 1) builtin:80 +PV# Funktion PRINT PV#(.1, 2, 1, 1, 0) builtin:76 +RATE# Funktion PRINT RATE#(1, 1, 1, 1, 1, 1) builtin:81 +SLN# Funktion PRINT SLN#(1, 1, 1) builtin:85 +SYD# Funktion PRINT SYD#(1, 1, 1, 1) builtin:86 +FORMAT$ Funktion PRINT FORMAT$(1) builtin:38 +PRINT USING Anweisung PRINT USING "#"; 1 builtin:37 +SetFormatCC Routine CALL SetFormatCC(49) builtin:39 +INPUTBOX$ Funktion PRINT INPUTBOX$("x") builtin:150 +MSGBOX Anweisung MSGBOX "x" builtin:149 +MSGBOX Funktion PRINT MSGBOX("x") builtin:149 +CIRCLE Anweisung CIRCLE (1,1),1 Non-Feature +DRAW Anweisung DRAW "U1" Non-Feature +GET (Grafik) Anweisung GET (0,0)-(1,1), a Non-Feature +LINE Anweisung LINE (0,0)-(10,10),1 builtin:153 +PAINT Anweisung PAINT (1,1),1 builtin:154 +PALETTE Anweisung DIM a%(15)\nPALETTE USING a%(0) Non-Feature +PCOPY Anweisung PCOPY 0,1 Non-Feature +PMAP Funktion PRINT PMAP(1) Non-Feature +POINT Funktion PRINT POINT(1) Non-Feature +PRESET Anweisung PRESET (1,1),2 Non-Feature +PSET Anweisung PSET STEP (1,1),2 Non-Feature +PUT (Grafik) Anweisung PUT (0,0), a Non-Feature +VIEW Anweisung VIEW (0,0)-(10,10) builtin:155 +WINDOW Anweisung WINDOW (0,0)-(1,1) Non-Feature +ABSOLUTE Routine CALL ABSOLUTE(1) Non-Feature +BLOAD Anweisung BLOAD "x" Non-Feature +BSAVE Anweisung BSAVE "x",0,1 Non-Feature +DEF SEG Anweisung DEF SEG = 0 Non-Feature +INP Funktion PRINT INP(1) Non-Feature +INTERRUPT Routine CALL INTERRUPT(1) Non-Feature +INTERRUPTX Routine CALL INTERRUPTX(1) Non-Feature +IOCTL Anweisung IOCTL #1, "x" Non-Feature +IOCTL$ Funktion PRINT IOCTL$(1) Non-Feature +OUT Anweisung OUT 1,1 Non-Feature +PEEK Funktion PRINT PEEK(1) Non-Feature +PEN Funktion PRINT PEN(1) Non-Feature +POKE Anweisung POKE 1,1 Non-Feature +STICK Funktion PRINT STICK(1) Non-Feature +STRIG Funktion PRINT STRIG(1) Non-Feature +VARPTR Funktion PRINT VARPTR(1) Non-Feature +VARPTR$ Funktion PRINT VARPTR$(1) Non-Feature +VARSEG Funktion PRINT VARSEG(1) Non-Feature +WAIT Anweisung WAIT 1,1 Non-Feature +BEGINTRANS Anweisung BEGINTRANS builtin:142 +BOF Funktion PRINT BOF(1) builtin:147 +COMMITTRANS Anweisung COMMITTRANS builtin:143 +CREATEINDEX Anweisung CREATEINDEX #1, "i", 0, "x" builtin:126 +DELETE Anweisung DELETE #1 builtin:133 +DELETEINDEX Anweisung DELETEINDEX #1, "i" builtin:127 +DELETETABLE Anweisung DELETETABLE "db", "t" builtin:134 +GETINDEX$ Funktion PRINT GETINDEX$(1) builtin:129 +INSERT Anweisung TYPE R\n x AS LONG\nEND TYPE\nDIM r AS R\nINSERT #1, r builtin:130 +MOVEFIRST Anweisung MOVEFIRST #1 builtin:135 +MOVELAST Anweisung MOVELAST #1 builtin:136 +MOVENEXT Anweisung MOVENEXT #1 builtin:137 +MOVEPREVIOUS Anweisung MOVEPREVIOUS #1 builtin:138 +RETRIEVE Anweisung TYPE R\n x AS LONG\nEND TYPE\nDIM r AS R\nRETRIEVE #1, r builtin:131 +ROLLBACK Anweisung ROLLBACK ALL builtin:144 +SAVEPOINT Funktion PRINT SAVEPOINT builtin:145 +SEEKEQ Anweisung SEEKEQ #1, 1 builtin:139 +SEEKGE Anweisung SEEKGE #1, 1 builtin:141 +SEEKGT Anweisung SEEKGT #1, 1 builtin:140 +SETINDEX Anweisung SETINDEX #1, "i" builtin:128 +SETMEM Funktion PRINT SETMEM(1) builtin:146 +UPDATE Anweisung TYPE R\n x AS LONG\nEND TYPE\nDIM r AS R\nUPDATE #1, r builtin:132 +CALLS Anweisung CALLS P Non-Feature +SADD Funktion PRINT SADD(1) Non-Feature +SSEG Funktion PRINT SSEG(1) Non-Feature +SSEGADD Funktion PRINT SSEGADD(1) Non-Feature +StringAddress Routine CALL StringAddress(1) Non-Feature +StringAssign Routine CALL StringAssign(1) Non-Feature +StringLength Routine CALL StringLength(1) Non-Feature +StringRelease Routine CALL StringRelease(1) Non-Feature +BEEP Anweisung BEEP builtin:74 +PLAY Anweisung PLAY "C" Non-Feature +SOUND Anweisung SOUND 440,1 Non-Feature +COM Anweisung COM(1) ON Non-Feature +ON COM Anweisung ON COM(1) GOSUB H\nEND\nH:\nRETURN Non-Feature +OPEN COM Anweisung OPEN "COM1:9600,N,8,1" AS #1 Non-Feature +ABS Funktion PRINT ABS(1) builtin:18 +ATN Funktion PRINT ATN(1) builtin:28 +CCUR Funktion PRINT CCUR(1) code:ConvI2Cy +CDBL Funktion PRINT CDBL(1) code:ConvI2R8 +CINT Funktion PRINT CINT(1!) code:ConvR4I2 +CLNG Funktion PRINT CLNG(1) code:ConvI2I4 +COS Funktion PRINT COS(1) builtin:26 +CSNG Funktion PRINT CSNG(1) code:ConvI2R4 +EXP Funktion PRINT EXP(1) builtin:23 +FIX Funktion PRINT FIX(1) builtin:21 +INT Funktion PRINT INT(1) builtin:20 +LOG Funktion PRINT LOG(1) builtin:24 +RANDOMIZE Anweisung RANDOMIZE 1 builtin:30 +RND Funktion PRINT RND builtin:29 +SGN Funktion PRINT SGN(1) builtin:19 +SIN Funktion PRINT SIN(1) builtin:25 +SQR Funktion PRINT SQR(1) builtin:22 +TAN Funktion PRINT TAN(1) builtin:27 +$DYNAMIC Metabefehl '$DYNAMIC\nDIM a(2) code:DimArr( +$INCLUDE Metabefehl '$INCLUDE: 'x.bi'\nPRINT Included code:PushDbl(73.0) +$STATIC Metabefehl '$STATIC\nDIM a(2) code:DimArr( +CALL Anweisung DECLARE SUB P()\nCALL P\nEND\nSUB P\nEND SUB code:Call(1, 0) +DECLARE Anweisung DECLARE SUB P()\nEND\nSUB P\nEND SUB proc:P +DEF FN Anweisung DEF FNdouble(x) = x*2\nPRINT FNdouble(1) code:Call(1, 1)|code:MulR4 +FUNCTION Anweisung END\nFUNCTION F%()\nF% = 1\nEND FUNCTION proc:F|code:RetFn +SUB Anweisung END\nSUB P\nEND SUB proc:P +DO...LOOP Anweisung DO\nEXIT DO\nLOOP code:Jump( +END Anweisung END code:End +EXIT Anweisung DO\nEXIT DO\nLOOP code:Jump( +FOR...NEXT Anweisung FOR i = 1 TO 2\nNEXT code:AddR4|code:JumpIfFalse( +GOSUB Anweisung GOSUB H\nEND\nH:\nRETURN code:Gosub( +GOTO Anweisung GOTO H\nH:\nEND code:Jump( +IF...THEN...ELSE Anweisung IF 1 THEN\nx=1\nELSE\nx=2\nEND IF code:JumpIfFalse( +ON...GOSUB Anweisung ON 1 GOSUB H\nEND\nH:\nRETURN code:OnJump(0, true) +ON...GOTO Anweisung ON 1 GOTO H\nH:\nEND code:OnJump(0, false) +RETURN Anweisung GOSUB H\nEND\nH:\nRETURN code:RetGosub +SELECT CASE Anweisung SELECT CASE 1\nCASE 1\nPRINT 1\nEND SELECT code:CmpI2(Eq) +STOP Anweisung STOP code:StopInstr +WHILE...WEND Anweisung WHILE 0\nWEND code:JumpIfFalse( +ASC Funktion PRINT ASC("x") builtin:12 +CHR$ Funktion PRINT CHR$(1) builtin:11 +HEX$ Funktion PRINT HEX$(1) builtin:15 +INSTR Funktion PRINT INSTR("abc", "b") builtin:4 +LCASE$ Funktion PRINT LCASE$("x") builtin:6 +LEFT$ Funktion PRINT LEFT$("x", 1) builtin:1 +LEN Funktion s$="abc"\nPRINT LEN(s$) builtin:0 +LTRIM$ Funktion PRINT LTRIM$("x") builtin:7 +MID$ Anweisung s$="abc"\nMID$(s$,1,1)="x" builtin:17 +MID$ Funktion PRINT MID$("x", 1) builtin:3 +OCT$ Funktion PRINT OCT$(1) builtin:16 +RIGHT$ Funktion PRINT RIGHT$("x", 1) builtin:2 +RTRIM$ Funktion PRINT RTRIM$("x") builtin:8 +SPACE$ Funktion PRINT SPACE$(1) builtin:9 +STR$ Funktion PRINT STR$(1) builtin:13 +STRING$ Funktion PRINT STRING$(1, 1) builtin:10 +UCASE$ Funktion PRINT UCASE$("x") builtin:5 +VAL Funktion PRINT VAL("x") builtin:14 +CHAIN Anweisung CHAIN "x" Non-Feature +CLEAR Anweisung CLEAR builtin:118 +COMMAND$ Funktion PRINT COMMAND$ builtin:71 +ENVIRON Anweisung ENVIRON "A=B" builtin:116 +ENVIRON$ Funktion PRINT ENVIRON$(1) builtin:115 +ERDEV Funktion PRINT ERDEV builtin:123 +ERDEV$ Funktion PRINT ERDEV$ builtin:124 +FRE Funktion PRINT FRE(1) builtin:117 +RUN Anweisung RUN "x" code:Run(2) +SLEEP Anweisung SLEEP 1 code:Sleep(true) +STACK Anweisung STACK 4096 builtin:122 +STACK Funktion PRINT STACK builtin:121 +SYSTEM Anweisung SYSTEM code:SystemInstr +TROFF Anweisung TROFF builtin:120 +TRON Anweisung TRON builtin:119 +INKEY$ Funktion PRINT INKEY$ builtin:52 +INPUT$ Funktion PRINT INPUT$(1) builtin:53 +KEY Anweisung KEY 1,"x" builtin:46 +$FORM Metabefehl '$FORM\nForm1.Caption="x" code:StoreObjectProperty(1, +FORM Datentyp DIM f AS FORM global:F:Empty +CONTROL Datentyp DIM c AS CONTROL global:C:Empty +TYPEOF … IS Operator IF TYPEOF Probe IS TextBox THEN PRINT 1 code:TypeOf(14) +! Operator Form1!Probe.Text="x" code:StoreObjectProperty(1,