Spezifikationsabgleich und Regressionsnachweise abschließen

This commit is contained in:
2026-09-06 10:31:16 +02:00
parent c8b92f0619
commit 024336e29c
39 changed files with 2485 additions and 120 deletions

View File

@@ -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<Ereignis>,
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<Ereignis> {
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<u64>) -> Option<Ereignis> {
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();
}

View File

@@ -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::<Vec<_>>()
.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<tb_vm::bytecode::CompiledModule, Vec<tb_frontend::Diagnostic>>,
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));
}
}

View File

@@ -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();
}

View File

@@ -194,7 +194,6 @@ impl<'a> P<'a> {
fn parse_statement(&mut self) -> Option<Stmt> {
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,

View File

@@ -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 {

View File

@@ -75,6 +75,7 @@ pub struct FormsModel {
visible_forms: Vec<u16>,
active_form: Option<u16>,
active_control: Option<(u16, Option<i32>)>,
dropdown: Option<ObjectKey>,
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<u16>) {
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<FormObject>, 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<u16> {
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<ObjectKey> {
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<u16> {
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<ObjectKey> {
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<ObjectKey> {
@@ -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));

View File

@@ -84,8 +84,9 @@ impl FormFile {
) -> Result<Vec<FormInitial>, tb_runtime::errors::RuntimeError> {
fn collect(
node: &FormNode,
parent: Option<u16>,
parent: Option<(u16, Option<i32>)>,
catalog: &FormCatalog,
form: &str,
out: &mut Vec<FormInitial>,
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();

View File

@@ -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"]);

View File

@@ -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()

View File

@@ -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));
}

View File

@@ -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::<Vec<_>>()
.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::<Vec<_>>()
.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")
);
}

View File

@@ -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!(

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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 F01F25 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

View File

@@ -0,0 +1,136 @@
# OpenSpec-Abschlussabgleich vom 06.09.2026
**Aktueller Nachprüfungsstand:** V01V03 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 F01F25 erneut ab.
**Ergebnis: F01F25 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 013, 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 2433 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 013/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.

View File

@@ -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 013 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 013 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

View File

@@ -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 (F21F23/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 (F21F23/F25). Siehe [Review vom 05.09.2026](../../../../docs/reviews/2026-09-05-openspec-code-review.md).
## What Changes

View File

@@ -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 V01V03 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. V01V03 vor der
abschließenden Synchronisierung und Archivierung beheben und nachprüfen.

View File

@@ -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())

View File

@@ -0,0 +1,51 @@
# Abschließende Verifikation vom 06.09.2026
Change: `spezifikationsabgleich-und-regressionsnachweise`.
Grundlage: aktueller Arbeitsbaum auf `c8b92f06190d77714865e0018b090348f372d77e`.
Geprüft: Proposal, Design D1D4, 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 V01V03](review-initial.md) bleibt als historischer
Ausgangsbefund erhalten. Die vollständige Szenarienmatrix steht in
[verification.md](verification.md), der Abgleich F01F25 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 013/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.

View File

@@ -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

View File

@@ -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 F01F25 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 F01F25 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.

View File

@@ -0,0 +1,72 @@
# Implementierungs- und Verifikationsnachweis
**Nachprüfung abgeschlossen:** V01V03 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 D1D4, 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 013/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 2433, 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.

View File

@@ -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

View File

@@ -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

View File

@@ -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 013, 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

View File

@@ -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

View File

@@ -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:+<TAB>
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

View File

@@ -0,0 +1 @@
Click;LostFocus;GotFocus;

View File

@@ -18,7 +18,7 @@
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
70700707070707070707070707070707070707077070700707070707070707070770707070707070707070707070
7070070707070707070707070707070707070707707070f1f1f1f1f1f1f1f1f1f170707070707070707070707070
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070

View File

@@ -0,0 +1,62 @@
' tb-screen: 80x25
' tb-keys: <F3><F2><F1>
' 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

View File

@@ -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

View File

@@ -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,
Can't render this file because it contains an unexpected character in line 8 and column 23.