Spezifikationsabgleich und Regressionsnachweise abschließen
This commit is contained in:
188
crates/tb-cli/tests/foreign.rs
Normal file
188
crates/tb-cli/tests/foreign.rs
Normal 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();
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,13 @@ Geprüfter Code: `19804e0e2de9eb5a8b0ef256b5be76c741560e07` in TerminalBasic. De
|
||||
|
||||
Die Implementierung deckt breite Grundpfade ab, erfüllt den gesamten Vertrag aber nicht. **25 Befunde**: 10 mit hoher, 12 mit mittlerer Priorität, 2 Dokumentationsabweichungen und 1 Vertragsklärung. Die Priorität bewertet die Auswirkung, nicht den Aufwand. Befunde können mehrere Requirements betreffen.
|
||||
|
||||
## Abschließender Abgleich vom 06.09.2026
|
||||
|
||||
Alle F01–F25 sind im [separaten Abschlussreport](2026-09-06-openspec-abschlussreview.md)
|
||||
mit aktuellen Regressions- bzw. Vertragsnachweisen abgeglichen. Dieser Bericht
|
||||
und seine historischen Fundstellen dokumentieren weiterhin den Ausgangsstand;
|
||||
der Inventartest liegt inzwischen unter `crates/tb-vm/tests/inventar.rs`.
|
||||
|
||||
## Umsetzungsstand: Ereigniszustellung und Hostgrenzen
|
||||
|
||||
F04, F05, F06, F07, F08, F18 und F20 sind im archivierten Change
|
||||
@@ -237,7 +244,7 @@ Alle Eigenschaften/Ereignisse aus der Klassentabelle werden im Inventartest paus
|
||||
|
||||
Beleg: [crates/tb-frontend/tests/inventar.rs:206](../../crates/tb-frontend/tests/inventar.rs#L206). Vertrag: [openspec/specs/sprachinventar/spec.md:12](../../openspec/specs/sprachinventar/spec.md#L12), [openspec/specs/sprachinventar/spec.md:56](../../openspec/specs/sprachinventar/spec.md#L56), [openspec/specs/forms-objektmodell/spec.md:106](../../openspec/specs/forms-objektmodell/spec.md#L106).
|
||||
|
||||
Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md).
|
||||
Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md).
|
||||
|
||||
### F22 — Der Korpus deckt die verlangten Kombinationen nicht ab (mittel)
|
||||
|
||||
@@ -245,7 +252,7 @@ traps.bas prüft STOP nur für TIMER, nicht für KEY und UEVENT; formular.frm ha
|
||||
|
||||
Beleg: [tests/compat/traps.bas:1](../../tests/compat/traps.bas#L1). Vertrag: [openspec/specs/kompat-testkorpus/spec.md:114](../../openspec/specs/kompat-testkorpus/spec.md#L114), [openspec/specs/kompat-testkorpus/spec.md:132](../../openspec/specs/kompat-testkorpus/spec.md#L132), [openspec/specs/kompat-testkorpus/spec.md:149](../../openspec/specs/kompat-testkorpus/spec.md#L149).
|
||||
|
||||
Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md).
|
||||
Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md).
|
||||
|
||||
### F23 — Die pauschale Grafik-Abweisung ist überholt (Dokumentation)
|
||||
|
||||
@@ -253,7 +260,7 @@ sprach-frontend nennt Grafik-Anweisungen pauschal als Non-Features. LINE, PAINT,
|
||||
|
||||
Beleg: [docs/sprachreferenz.md:526](../../docs/sprachreferenz.md#L526). Vertrag: [openspec/specs/sprach-frontend/spec.md:83](../../openspec/specs/sprach-frontend/spec.md#L83).
|
||||
|
||||
Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md).
|
||||
Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md).
|
||||
|
||||
### F24 — Container- und Opcode-Beschreibung sind nicht aktuell (Dokumentation)
|
||||
|
||||
@@ -269,7 +276,7 @@ write_text gibt unveränderte gelesene Dateien bytegleich zurück, einschließli
|
||||
|
||||
Beleg: [crates/tb-ui/src/frm.rs:348](../../crates/tb-ui/src/frm.rs#L348). Vertrag: [openspec/specs/forms-dateiformat/spec.md:31](../../openspec/specs/forms-dateiformat/spec.md#L31).
|
||||
|
||||
Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md).
|
||||
Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md).
|
||||
|
||||
## Gebündelte Changes und Reihenfolge
|
||||
|
||||
@@ -280,7 +287,7 @@ Umsetzung: [spezifikationsabgleich-und-regressionsnachweise](../../openspec/chan
|
||||
| 3 | [forms-zustand-und-bedienung](../../openspec/changes/archive/2026-09-05-forms-zustand-und-bedienung/proposal.md) | F12, F13 | Nach Ereigniszustellung |
|
||||
| 4 | [projektmodule-und-kompilat](../../openspec/changes/archive/2026-09-05-projektmodule-und-kompilat/proposal.md) | F09, F10, F11, F24 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
||||
| 5 | [laufzeit-eingabe-und-systemstatus](../../openspec/changes/archive/2026-09-06-laufzeit-eingabe-und-systemstatus/proposal.md) | F14, F15, F16, F17, F19 | Eigenständig; bei gemeinsamen Dateien sequenziell integrieren |
|
||||
| 6 | [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/spezifikationsabgleich-und-regressionsnachweise/proposal.md) | F21, F22, F23, F25 | Abschließende Gesamtabnahme nach den fünf Korrektur-Changes |
|
||||
| 6 | [spezifikationsabgleich-und-regressionsnachweise](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/proposal.md) | F21, F22, F23, F25 | Abschließende Gesamtabnahme nach den fünf Korrektur-Changes |
|
||||
|
||||
## Vollständige Requirement-Matrix
|
||||
|
||||
|
||||
136
docs/reviews/2026-09-06-openspec-abschlussreview.md
Normal file
136
docs/reviews/2026-09-06-openspec-abschlussreview.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# OpenSpec-Abschlussabgleich vom 06.09.2026
|
||||
|
||||
**Aktueller Nachprüfungsstand:** V01–V03 sind vollständig behoben. Der
|
||||
[abschließende Change-Review](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review.md)
|
||||
verzeichnet keine offenen Befunde; der erste Review bleibt separat erhalten.
|
||||
|
||||
Grundlage: Arbeitsbaum auf `c8b92f06190d77714865e0018b090348f372d77e`, nach
|
||||
Integration der fünf Korrektur-Changes und Umsetzung von
|
||||
`spezifikationsabgleich-und-regressionsnachweise`. Der
|
||||
[Review vom 05.09.2026](2026-09-05-openspec-code-review.md) bleibt die historische
|
||||
Bestandsaufnahme. Dieser Bericht gleicht alle dortigen F01–F25 erneut ab.
|
||||
|
||||
**Ergebnis: F01–F25 sind durch Umsetzung oder präzisierten Vertrag geschlossen.**
|
||||
Die unten genannten Regressionen bestehen im aktuellen Arbeitsbaum. Die vier
|
||||
Delta-Specs wurden am 06.09.2026 in die Hauptspecs synchronisiert und der
|
||||
Change wurde anschließend archiviert.
|
||||
|
||||
## Befundmatrix
|
||||
|
||||
| ID | Ergebnis und heutiger Nachweis |
|
||||
|---|---|
|
||||
| F01 | ISAM-OPEN verwendet den gemeinsamen Writer einer laufenden Transaktion. [CLI-Test](../../crates/tb-cli/tests/compat.rs) `tbc_isam_open_in_transaktion_hat_eine_harte_frist`; [isamopen.bas](../../tests/compat/isamopen.bas), einschließlich Prozessfrist. |
|
||||
| F02 | SAVEPOINT/ROLLBACK stellt Satz-, Index- und Tabellenstruktur wieder her. [ISAM-Tests](../../crates/tb-runtime/src/isam/tests.rs) `savepoints_nehmen_index_und_tabellenstrukturen_zurueck`, `neue_tabellen_und_aktive_indizes_verschwinden_bei_ruecknahme`; [isamstruktur.bas](../../tests/compat/isamstruktur.bas). |
|
||||
| F03 | FREEFILE und normale/ISAM-Bindungen teilen den Nummernraum. [isamdateinummern.bas](../../tests/compat/isamdateinummern.bas) prüft Lücken, Wiederbelegung, CLOSE und RESET. |
|
||||
| F04 | Beide Signale erreichen ihre registrierten Traps. [events.rs](../../crates/tb-vm/tests/events.rs) `beide_signale_werden_zugestellt_und_beginnen_in_quellreihenfolge`. |
|
||||
| F05 | KEY STOP erhält lesbare Eingabe und merkt den Handler einmal. [events.rs](../../crates/tb-vm/tests/events.rs) `key_on_off_stop_gelten_fuer_alle_konsoleneingaben`; zusätzlich F1 in [trap-masken.bas](../../tests/compat/trap-masken.bas). |
|
||||
| F06 | Eingaben und Handler behalten ihre Reihenfolge. [events.rs](../../crates/tb-vm/tests/events.rs) `taste_maus_und_trap_behalten_die_annahmereihenfolge`, `fokus_handler_laufen_geordnet_bis_zur_rueckkehr`, `doevents_verhindert_rekursion_derselben_forms_prozedur`. |
|
||||
| F07 | Das offene Menü sperrt Traps und Forms-Timer auch bei SLEEP und Eingabe. [events.rs](../../crates/tb-vm/tests/events.rs) `offenes_menu_sperrt_traps_und_wartende_forms_timer_an_allen_zustellpunkten`, `review_menu_schliessen_stellt_pending_trap_vor_erneutem_warten_zu`. |
|
||||
| F08 | Zeit und Warten kommen vom Host. [events.rs](../../crates/tb-vm/tests/events.rs) `ohne_zeitbedarf_keine_uhrabfrage_und_keine_mausansammlung`, `sleep_folgt_nur_der_hostfrist_und_forms_timer_beendet_es`; [Ausführungsmodell](../tbvm-design.md). |
|
||||
| F09 | Controls werden im jeweiligen Formular aufgelöst. [project.rs](../../crates/tb-vm/tests/project.rs) `zwei_formulare_mit_gleichen_controls_und_arrays_laufen_aus_dem_kompilat`; der neue FRM-Test erhält zusätzlich unterschiedliche Eltern von Elementen desselben Arrays. |
|
||||
| F10 | Quellen, Module, Fehlerorte und Debuggerziele bleiben getrennt. [project.rs](../../crates/tb-vm/tests/project.rs) `module_includes_erl_breakpoints_und_inspektion_behalten_ihren_ursprung`, `initialisierungen_def_fn_und_modulhandler_haben_eigene_quellorte`; [CLI-Quellorttest](../../crates/tb-cli/tests/project.rs). |
|
||||
| F11 | TBC 4 erhält Forms-Anfangswerte und Designarrays. [CLI-Test](../../crates/tb-cli/tests/project.rs) `projekt_und_formular_laufen_nach_entfernen_saemtlicher_quellen`; [VM-Projekttest](../../crates/tb-vm/tests/project.rs) für zwei Formulare. |
|
||||
| F12 | ListIndex = −1 und Auswahländerungen sind konsistent. [Forms-Tests](../../crates/tb-ui/src/forms.rs) `listenauswahl_bleibt_beim_einfuegen_und_entfernen_konsistent`, `sorted_wechsel_erhaelt_auch_die_auswahl_zwischen_duplikaten`. |
|
||||
| F13 | Timerphasen beginnen bei ihrer Aktivierung. [Forms-Tests](../../crates/tb-ui/src/forms.rs) `timerarrays_holen_nur_aktive_zeit_nach_und_sortieren_nach_name_und_index`; [events.rs](../../crates/tb-vm/tests/events.rs) `spaete_timeraktivierung_nutzt_hostzeit_auch_ueber_control_parameter`, `intervallaenderung_im_handler_verwirft_bereits_faellige_altintervalle`. |
|
||||
| F14 | INPUT # bewahrt Feldfortschritt und unterscheidet leeres von fehlendem Feld. [vm.rs](../../crates/tb-vm/tests/vm.rs) `input_datei_bewahrt_felder_und_unterscheidet_leer_von_fehlend`. |
|
||||
| F15 | INPUT$ zählt Zeichen, erhält Sondertastenreste und meldet vorzeitiges EOF. [vm.rs](../../crates/tb-vm/tests/vm.rs) `input_datei_alle_lesearten_teilen_utf8_cursor_und_seek`, `input_s_teilt_sondertasten_und_bewahrt_rest_fuer_inkey`, `unicode_puffergrenzen_und_binaere_records_bleiben_getrennt`. |
|
||||
| F16 | Alle fünf CV-Funktionen verlangen exakte Bytezeichenbreite. [vm.rs](../../crates/tb-vm/tests/vm.rs) `alle_mk_cv_paare_verlangen_exakte_bytezeichenbreite`. |
|
||||
| F17 | Fehlende Datei und fehlender Elternpfad ergeben unterschiedliche Fehler. [vm.rs](../../crates/tb-vm/tests/vm.rs) `datei_und_elternpfadfehler_sind_am_sprachzugang_unterscheidbar`. |
|
||||
| F18 | BackTab wird als Tab mit Shift weitergegeben. Terminal-Mapper `backtab_ist_tab_mit_shift_auch_ohne_modifier`; [formular-access-backtab.frm](../../tests/compat/formular-access-backtab.frm) prüft den BASIC-Fokuspfad. |
|
||||
| F19 | TIMEZONEKNOWN liefert den gegenwärtigen Status, einschließlich Ausfall und Erholung. [vm.rs](../../crates/tb-vm/tests/vm.rs) `timezoneknown_hat_integer_signatur_und_zeigt_aktuellen_status`; [Runtime-Test](../../crates/tb-runtime/src/builtins.rs) `zonenquelle_ausfall_und_erholung_gelten_fuer_alle_zeitfunktionen`. |
|
||||
| F20 | VM-/Forms-Einbettung benötigt kein Terminalbackend. Separater Resolver [tests/terminalfrei](../../tests/terminalfrei/Cargo.toml), erneut ohne GUI ausgeführt: `Terminalfreier FRM-/VM-Lauf: KM`. |
|
||||
| F21 | Alle 845 Inventareinträge werden mit kompilierten BASIC-Proben bis HIR/Runtime-Ziel geprüft. [inventar.rs](../../crates/tb-cli/tests/inventar.rs) vergleicht unabhängige Sollziele mit der konkreten Absenkung und erkennt falsche Statuswerte, Unsupported, fehlende/vertauschte Builtin-Bindungen, entfernte Absenkung und falsche Forms-Member/Typselektoren; [events.rs](../../crates/tb-vm/tests/events.rs) führt alle 133 registrierten Ereignisse bis zum BASIC-Handler und erkennt eine künstlich entfernte Ereignisbindung. |
|
||||
| F22 | [trap-masken.bas](../../tests/compat/trap-masken.bas) ergänzt KEY/UEVENT ON/OFF/STOP und EVENT OFF; [formular-access-backtab.frm](../../tests/compat/formular-access-backtab.frm) ergänzt gewöhnlichen Control-Access-Key und Rückwärtstab mit `Click;LostFocus;GotFocus;`. Der Harness prüft jedes Programm zweimal. Sieben öffentliche Programme bestehen den gesonderten Bediennachweis unten. |
|
||||
| F23 | SCREEN 0–13, LINE, PAINT und VIEW sind ausdrücklich erlaubt; übrige Non-Features bleiben ausgeschlossen. [Sprachreferenz](../sprachreferenz.md), [Delta](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/sprach-frontend/spec.md); [CLI-Test](../../crates/tb-cli/tests/project.rs) prüft alle 14 SCREEN-Modi und benannte Abweisungen. |
|
||||
| F24 | [VM-Design](../tbvm-design.md) und [Hauptspec](../../openspec/specs/bytecode-kompilat/spec.md) beschreiben TBC 4 mit neun Abschnitten und dem aktuellen Opcode-Vertrag. Loader-Rundlauf, Versionsablehnung und beschädigte Referenzen sind in [project.rs](../../crates/tb-vm/tests/project.rs) bzw. den CLI-Projekttests abgesichert. Dieser Change führt keine neue Formatversion ein. |
|
||||
| F25 | [Dateiformate](../dateiformate.md) und [Delta](../../openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/specs/forms-dateiformat/spec.md) unterscheiden Originalerhalt und kanonisches Schreiben. [FRM-Tests](../../crates/tb-ui/src/frm.rs) prüfen CRLF und explizite Defaults bytegleich sowie Default-Auslassung nach Änderung. Index 0 bleibt als Array-Strukturangabe erhalten. |
|
||||
|
||||
## Zusätzlich im Abgleich gefundene und behobene Ursachen
|
||||
|
||||
- Die Signatur für die SHELL-Funktion fehlte; IRR erwartete die Argumente in
|
||||
umgekehrter Reihenfolge. Eine VM-Probe prüft jetzt SHELL → Exitcode 7 und
|
||||
IRR für −100/+110 → 0,1, neben den 845 Kompilierproben.
|
||||
- Koordinatenformen von CIRCLE/WINDOW und IOCTL # brachen vor der benannten
|
||||
Non-Feature-Diagnose ab. Parser und Semantik nennen nun das ausgeschlossene
|
||||
Element; POKE und PEEK sind ausdrücklich per CLI geprüft.
|
||||
- Form_KeyPress/KeyUp, Form-Fokus/Paint/Resize, Label.Change,
|
||||
PictureBox.Paint, ComboBox.DropDown und die getrennten Dateilistenereignisse
|
||||
hatten fehlende oder falsche Quellen. Die 133 Ereignisproben verwenden
|
||||
Tastatur, Maus, Hostzeit oder echte Modell-/BASIC-Operationen statt direkter
|
||||
Ereignisqueue-Einträge.
|
||||
- Der VM-Ereignisdispatch setzte auch ohne Handler den aktiven Control-Zustand.
|
||||
Er ignoriert jetzt fehlende Bindungen ohne diese Nebenwirkung; reine
|
||||
Zustandsmeldungen und nicht fokussierbare Objekte übernehmen keinen Fokus.
|
||||
- Binäre FRM-Records enthielten auch bei Einzelcontrols einen Index-Slot.
|
||||
Der Import berücksichtigt das tatsächliche Array-Flag. Eingebettete Includes
|
||||
werden als Herkunftskommentar ausgegeben, nicht ein zweites Mal geöffnet.
|
||||
- Arrayelemente unter unterschiedlichen Containern behalten ihre Eltern über
|
||||
vorhandene PARENT-Anfangswerte. Die Suche bleibt auf das jeweilige Formular
|
||||
begrenzt; der bestehende Mehrformular-/TBC-Test schützt diese Grenze.
|
||||
- Kanonisches Weglassen von Index 0 hätte ein einzelnes Arrayelement in ein
|
||||
Einzelcontrol verwandelt. Der Writer erhält diese Strukturangabe; der
|
||||
präzisierte Delta-Vertrag und der Rundlauftest benennen sie ausdrücklich.
|
||||
|
||||
## Öffentlicher Fremdprogrammbestand
|
||||
|
||||
Bestand: `cout/vbdos`, Revision
|
||||
`1cdd2b32b829fe1721d0b6aecc433abc47a96fb6`. Der Test erstellt für jeden Lauf
|
||||
per `git archive` eine neue Kopie dieses Commits, baut den unveränderten
|
||||
Programmeinstieg mit `tbc build`, lädt das Kompilat und bedient die VM über
|
||||
CaptureHost bei 100×30 Zellen. Die lokale Prüfbasis war
|
||||
`/tmp/terminalbasic-vbdos-evidence`. Kein Emulator oder interaktives Terminal.
|
||||
|
||||
| Einstieg | Deklarierte Eingabe | Geprüftes Ergebnis, in beiden Läufen |
|
||||
|---|---|---|
|
||||
| `graphics/graphics.mak` | Alt+X | Formular entladen |
|
||||
| `microsoft/check.mak` | Alt+F, X | Menü zunächst sichtbar, danach geschlossen und Formular entladen |
|
||||
| `microsoft/qlbview.mak` | Esc | Cancel-Pfad entlädt das Formular |
|
||||
| `microsoft/seek.mak` | Alt+X | Formular entladen |
|
||||
| `microsoft/spindemo.mak` | Tab, Tab, Pfeil auf | VSpin_Custom ändert Text1 von `0` auf ` 1`; Bildschirmänderung durch Pfeiltaste |
|
||||
| `microsoft/notepad.frm` | Alt+F, X, N im Speicherdialog | Menü öffnet, Save-Dialog wird angezeigt, N beantwortet ihn und Formular wird entladen |
|
||||
| `misc/mentors/mentors.frm` | Alt+F, X | Menü zunächst sichtbar, danach geschlossen und Formular entladen |
|
||||
|
||||
Alle sieben Programme: diagnosefreie Übersetzung und zwei gleiche Folgen von
|
||||
sichtbaren Formular-/Menüzuständen und Bildschirm-Snapshots. Keine fehlende
|
||||
externe Voraussetzung und keine angepassten Fremdprogrammquellen. Das belegt
|
||||
diese sieben Einstiege und Sequenzen, keine vollständige VBDOS-Kompatibilität.
|
||||
|
||||
Reproduktion mit vorhandenem Checkout der genannten Revision:
|
||||
|
||||
```sh
|
||||
TB_VBDOS_REPO=/pfad/zu/vbdos cargo test -p tb-cli --test foreign oeffentliche -- --ignored --nocapture
|
||||
```
|
||||
|
||||
Der [Test](../../crates/tb-cli/tests/foreign.rs) ist im normalen Workspace-Lauf
|
||||
explizit ignoriert, weil der öffentliche Checkout keine Repository-Abhängigkeit
|
||||
ist. Der gesonderte Aufruf oben wurde für diese Abnahme erfolgreich ausgeführt;
|
||||
ohne Umgebungsvariable oder bei abweichender Revision schlägt er fehl.
|
||||
|
||||
## Abnahme und Sollbilder
|
||||
|
||||
- `cargo test --workspace`: **481 bestanden, 0 fehlgeschlagen, 2 ignoriert**.
|
||||
Ignoriert sind der Golden-Generator und der separat ausgeführte Fremdtest.
|
||||
- Fremdtest: **1 bestanden**, sieben Programme jeweils zweimal.
|
||||
- `cargo run --manifest-path tests/terminalfrei/Cargo.toml --offline`: bestanden.
|
||||
- Clippy für Workspace, alle Targets/Features, `-D warnings`: bestanden.
|
||||
- `cargo fmt --all -- --check` und `git diff --check`: bestanden.
|
||||
- `openspec validate --all --strict`: **16 bestanden**, 15 Hauptspecs und der
|
||||
aktive Change, keine Fehler. Bestehende Hinweise zu langen Requirement-Texten
|
||||
sind rein informativ.
|
||||
|
||||
Die beiden neuen Sollausgaben wurden aus den erwarteten Zuständen festgelegt.
|
||||
`EVENT OFF` verwirft eingetretene Ereignisse; es wird ausdrücklich kein Nachholen
|
||||
beim erneuten ON erwartet. Der bestehende `formular.out` wurde fachlich geprüft:
|
||||
Nur die zehn Attributzellen der OK-Schaltfläche in Zeile 4, Spalten 24–33 wechseln
|
||||
auf Vordergrund 15/Hintergrund 1 (`f1`), weil sie den Eingabefokus behält. Die
|
||||
Zeichenebene und alle übrigen Zellen bleiben bytegleich. Der Golden-Generator
|
||||
wurde nicht ausgeführt.
|
||||
|
||||
## Abschluss der Reviewkorrekturen
|
||||
|
||||
- **V01:** Parent-Schlüssel enthalten den Arrayindex. Geometrie, Optionsgruppen, Menüzuordnung und Formular-Traversierung nutzen dieselbe Referenz. Container liegen bei Zeichnung und Trefferprüfung hinter ihren Kindern. Die ergänzte Gegenprobe traf vor der letzten Korrektur fälschlich `Group(1)` statt `Other`; danach sind Treffer und sichtbare Beschriftung korrekt. TBC4 bleibt unverändert, ungültige Parent-Zyklen und fehlende Arrayinstanzen werden abgewiesen.
|
||||
- **V02:** Die 291 festen Vorlagen enthalten unabhängige Sollziele; Forms werden gegen den abgesenkten Klassenmember geprüft. Der Inventartest liegt in `tb-cli`, damit `$INCLUDE` durch den echten Projektlader läuft. Falsche gültige Bindungen, falsche Konverterselektoren/Forms-Member sowie entfernte Absenkungen werden namentlich erkannt.
|
||||
- **V03:** Die gemeinsame Anweisungsauflösung weist auch Koordinaten mit Farbe/STEP, WINDOW SCREEN und PALETTE USING namentlich ab, nachdem mögliche Variablenzuweisungen erkannt wurden. Gleichnamige Arrays und SCREEN 0–13/LINE/PAINT/VIEW bleiben verfügbar.
|
||||
- **Ergänzende Prüfung:** ComboBox-Ausklapplisten schließen bei Fokuswechsel und Ausblenden; der neue Regressionstest besteht.
|
||||
|
||||
Alle fünf ursprünglichen Soll-Gegenproben bestehen. Der Golden-Generator wurde nicht ausgeführt; bei den Reviewkorrekturen wurden keine weiteren Sollbilder verändert.
|
||||
@@ -12,7 +12,7 @@ pruefe_forms erwartet jede Klasseneigenschaft und jedes Ereignis pauschal als im
|
||||
|
||||
### D1 — Vorhandenen Inventartest erweitern
|
||||
|
||||
Inventareinträge weiterhin aus der Markdowntabelle lesen, aber für implementiert mindestens erzeugbare Frontend-/HIR-Pfade ohne Unsupported und auflösbare Runtime-Ziele prüfen. Syntaxformen mit expliziten kleinen Programmvorlagen abdecken; Forms-Probequellen aus Klassen-/Signaturmetadaten erzeugen. Klassentabellen allein gelten nicht als Laufzeitnachweis. Für Ereignisse reale Auslöser bis zum BASIC-Handler prüfen; die konkret fehlenden Form-KeyPress/KeyUp ergänzen, nicht den Status auf offen setzen.
|
||||
Inventareinträge weiterhin aus der Markdowntabelle lesen, aber für implementiert mindestens erzeugbare Frontend-/HIR-Pfade ohne Unsupported und auflösbare Runtime-Ziele prüfen. Syntaxformen mit expliziten kleinen Programmvorlagen abdecken; Forms-Probequellen aus Klassen-/Signaturmetadaten erzeugen. Der Inventartest liegt im CLI-Testpaket, damit auch `$INCLUDE` den echten Projektlader durchläuft. Unabhängige Sollziele aus Runtime-ABI, Instruktionsvertrag und Modulmetadaten müssen in der tatsächlichen Absenkung vorkommen; Forms-Dispatch muss den konkreten Klassenmember erreichen. Gegenproben ersetzen gültige Bindungen, Forms-Member und Typselektoren oder entfernen die Absenkung. Klassentabellen allein gelten nicht als Laufzeitnachweis. Für Ereignisse reale Auslöser bis zum BASIC-Handler prüfen; die konkret fehlenden Form-KeyPress/KeyUp ergänzen, nicht den Status auf offen setzen.
|
||||
|
||||
### D2 — Korpusfälle nach fehlender Bedingung hinzufügen
|
||||
|
||||
@@ -20,7 +20,7 @@ Bestehenden CaptureHost/Harness wiederverwenden; jede Maskierungsquelle in ON/OF
|
||||
|
||||
### D3 — Bestehende Produktentscheidungen synchronisieren
|
||||
|
||||
Nur LINE/PAINT/VIEW/SCREEN 0–13 aus der pauschalen Grafikablehnung ausnehmen; verbleibende Non-Features behalten. FRM: unverändertes Lesen/Schreiben erhält die Originalbytes; neues/geändertes kanonisches Schreiben lässt Defaults aus. Keine Änderung des funktionierenden Erhaltungsmodus nötig.
|
||||
Nur LINE/PAINT/VIEW/SCREEN 0–13 aus der pauschalen Grafikablehnung ausnehmen; verbleibende Non-Features behalten. FRM: unverändertes Lesen/Schreiben erhält die Originalbytes; neues/geändertes kanonisches Schreiben lässt Defaults aus. Keine Änderung des funktionierenden Erhaltungsmodus nötig. Explizite Control-Array-Indizes bleiben als Strukturangaben auch bei Index 0 erhalten. Elternreferenzen behalten ihren Arrayindex im bestehenden PARENT-Anfangswert (TBC4). Alle Containerpfade nutzen diese Referenz; Zeichnung und Trefferprüfung ordnen Container vor ihren Kindern ein. Die TBC-Validierung weist fehlende Elterninstanzen und Zyklen ab.
|
||||
|
||||
### D4 — Abschließender Review als Befundnachweis
|
||||
|
||||
@@ -29,7 +29,7 @@ Nach den fünf Korrektur-Changes jede F-ID mit konkretem Regressionstest/Prüfer
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Generierte Tests spiegeln wieder nur Implementierungsmetadaten → erwartete Ergebnisse für kritische Ereignisse und negative Markerfälle unabhängig in kleinen Quellen festhalten.
|
||||
- Überlappung mit funktionalen Korrekturen → diese zuerst integrieren und nur die hier benannten verbleibenden Eventquellen ändern.
|
||||
- Überlappung mit funktionalen Korrekturen → diese zuerst integrieren und die im vollständigen Ereignis- und Fremdprogrammlauf nachgewiesenen fehlenden Quellen bzw. Importpfade an ihrem gemeinsamen Ursprung korrigieren.
|
||||
- Fremdprogramme können Hardware-Non-Features außerhalb des akzeptierten Bestands enthalten → dokumentierten Prüfbestand/Revision festhalten, keine globale Kompatibilitätszusage daraus ableiten.
|
||||
|
||||
## Migration Plan
|
||||
@@ -1,6 +1,6 @@
|
||||
## Why
|
||||
|
||||
Der aktuelle Inventarabgleich bestätigt überwiegend Tabellen gegen dieselben Tabellen, während konkrete Formularereignisse fehlen. Korpusabdeckung und Hauptspecs sind nach den jüngsten Features nicht vollständig nachgezogen (F21–F23/F25). Siehe [Review vom 05.09.2026](../../../docs/reviews/2026-09-05-openspec-code-review.md).
|
||||
Der aktuelle Inventarabgleich bestätigt überwiegend Tabellen gegen dieselben Tabellen, während konkrete Formularereignisse fehlen. Korpusabdeckung und Hauptspecs sind nach den jüngsten Features nicht vollständig nachgezogen (F21–F23/F25). Siehe [Review vom 05.09.2026](../../../../docs/reviews/2026-09-05-openspec-code-review.md).
|
||||
|
||||
## What Changes
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# Verifikation vom 06.09.2026
|
||||
|
||||
Historischer Befundstand vor der Korrektur; der Inventartest liegt inzwischen in tb-cli.
|
||||
|
||||
Change: `spezifikationsabgleich-und-regressionsnachweise`.
|
||||
Geprüft wurde der aktuelle Arbeitsbaum auf `c8b92f06190d77714865e0018b090348f372d77e`:
|
||||
Proposal, Design, sieben Tasks, vier Delta-Specs mit sechs Requirements und
|
||||
18 Szenarien, betroffene Hauptverträge, Code, Tests und Abschlussreport.
|
||||
|
||||
**Ergebnis: drei offene Befunde. Die vorherige Aussage „keine offenen
|
||||
Befunde“ ist durch diese Nachprüfung überholt.**
|
||||
|
||||
| Dimension | Ergebnis |
|
||||
|---|---|
|
||||
| Vollständigkeit | 7/7 Tasks formal abgehakt; die inhaltliche Abnahme ist wegen V01–V03 noch offen |
|
||||
| Korrektheit | Bestehende Suite grün, fünf unabhängige Soll-Gegenproben schlagen fehl |
|
||||
| Kohärenz | Bestehende Architektur und TBC-Version erhalten; Inventarnachweis und Import-Erhaltungszusage sind noch zu weitgehend |
|
||||
| Befunde | 0 CRITICAL, 3 WARNING; davon V01 mit hoher, V02/V03 mit mittlerer Auswirkung |
|
||||
|
||||
## V01 — Index des Elterncontainers geht beim FRM-Import verloren (hoch)
|
||||
|
||||
**Fundstellen:** [frm.rs:87](../../../../crates/tb-ui/src/frm.rs#L87),
|
||||
[frm.rs:122](../../../../crates/tb-ui/src/frm.rs#L122),
|
||||
[frm.rs:131](../../../../crates/tb-ui/src/frm.rs#L131),
|
||||
[forms.rs:269](../../../../crates/tb-ui/src/forms.rs#L269),
|
||||
[forms.rs:531](../../../../crates/tb-ui/src/forms.rs#L531).
|
||||
|
||||
Der neue PARENT-Anfangswert erhält nur die Objekt-ID. Die Rekursion übergibt
|
||||
ebenfalls nur `Some(object)`; `set_initial_at` verwirft einen vorhandenen Index,
|
||||
und der Getter ergänzt grundsätzlich `None`. Ein Kind von `Group(1)` bekommt
|
||||
damit `Group(0)` als Parent. Die neue Regression prüft unterschiedliche
|
||||
unindizierte Frames, aber kein Kind innerhalb eines Frame-Arrays.
|
||||
|
||||
**Reproduktion:** FRM mit `Frame Group, Index = 0` und einem zweiten
|
||||
`Frame Group, Index = 1`, darin `TextBox Probe`. Anschließend:
|
||||
|
||||
```basic
|
||||
DIM p AS CONTROL
|
||||
p = Probe.Parent
|
||||
Form1.Hide
|
||||
PRINT p.Index
|
||||
END
|
||||
```
|
||||
|
||||
Soll: `1`. Ist: `0`. Derselbe Fehler tritt nach `tbc build` und Löschen der
|
||||
FRM-Quelle beim Ausführen der TBC-Datei auf. Beide Gegenproben schlagen fehl.
|
||||
Das ist auch für den akzeptierten Fremdbestand relevant: `graphics.frm`
|
||||
enthält indizierte Frames mit untergeordneten Controls.
|
||||
|
||||
**Vertrag:** Forms-Steuerelement-Arrays, vollständige Forms-Anfangsbeschreibungen
|
||||
im TBC sowie Task 2.4; der aktuelle Abschlussreport behauptet außerdem den
|
||||
Erhalt der Array-Eltern. Die neue PARENT-Unterstützung erfüllt diesen Erhalt
|
||||
noch nicht vollständig.
|
||||
|
||||
**Behebung:** Eltern als vollständige Objektreferenz einschließlich Arrayindex
|
||||
über Import, Anfangswerte, PARENT-Zugriff und davon abhängige Containerpfade
|
||||
führen. Einen indizierten Elterncontainer im Quell- und TBC-Rundlauf prüfen;
|
||||
den bestehenden Test mit gleichen Control-Namen in zwei Formularen erhalten.
|
||||
|
||||
## V02 — Inventarprüfung akzeptiert ein falsches Laufzeitziel (mittel)
|
||||
|
||||
**Fundstelle:** [inventar.rs:725](../../../../crates/tb-cli/tests/inventar.rs#L725).
|
||||
|
||||
`pruefe_kompilat` prüft lediglich, ob die im Kompilat vorkommenden Builtin-IDs
|
||||
irgendwelche vorhandenen Bindungen haben und die Tabellenreferenzen gültig
|
||||
sind. Es prüft nicht, ob das erzeugte Ziel dem untersuchten Inventareintrag
|
||||
entspricht. Damit kann ein falsch abgesenktes Element weiterhin als
|
||||
implementiert bestätigt werden.
|
||||
|
||||
**Reproduktion:** Die normale Probe `PRINT TIMEZONEKNOWN` wird kompiliert.
|
||||
Anschließend ersetzt die Gegenprobe ausschließlich den erzeugten CallBuiltin
|
||||
von TIMEZONEKNOWN (156) durch TIMER (54); beide haben null Argumente und eine
|
||||
vorhandene Runtime-Bindung. Der bestehende Prüfer liefert weiterhin `Ok(())`.
|
||||
Die Soll-Assertion, dass er diese Divergenz melden muss, schlägt fehl.
|
||||
|
||||
**Vertrag:** Delta `sprachinventar`, Requirement „Automatischer Abgleich
|
||||
Inventar gegen Implementierung“, Task 2.1 und Design D1. Der Nachweis soll den
|
||||
tatsächlichen Implementierungspfad des jeweiligen Elements prüfen, nicht nur
|
||||
die Existenz eines beliebigen ausführbaren Ziels.
|
||||
|
||||
**Behebung:** Die erwartete Absenkung bzw. das beobachtbare Verhalten mit dem
|
||||
konkreten Inventareintrag verbinden. Neben Unsupported und fehlender Bindung
|
||||
auch eine gültige, aber falsche Zielbindung sowie eine verschwundene Absenkung
|
||||
negativ prüfen. Das betrifft auch Forms-Methoden/-Eigenschaften, deren bloßes
|
||||
Vorhandensein in gültigem Bytecode noch keinen passenden Dispatch beweist.
|
||||
|
||||
## V03 — Weitere gültige Non-Feature-Syntaxformen liefern nur Syntaxfehler (mittel)
|
||||
|
||||
**Fundstellen:** [parser.rs:199](../../../../crates/tb-frontend/src/parser.rs#L199),
|
||||
[inventar-quellen.tsv:148](../../../../tests/support/inventar-quellen.tsv#L148),
|
||||
[inventar-quellen.tsv:153](../../../../tests/support/inventar-quellen.tsv#L153).
|
||||
|
||||
Der neue Parserpfad behandelt CIRCLE, WINDOW und IOCTL. Andere ausdrücklich
|
||||
verbotene Grafikformen erreichen weiterhin nicht die vorgeschriebene
|
||||
Feature-unavailable-Diagnose. Die festen Inventarvorlagen erfassen davon nur
|
||||
Syntaxvarianten, die der allgemeine Aufrufparser bereits versteht.
|
||||
|
||||
**Reproduktionen mit `tbc check`:**
|
||||
|
||||
- `PSET (1,1),2` → `Syntax error` in Spalte 11.
|
||||
- `DIM a%(15)` gefolgt von `PALETTE USING a%(0)` → `Expected: expression`
|
||||
in Zeile 2, Spalte 9.
|
||||
|
||||
Beide sind gültige Syntaxformen ausgeschlossener Funktionen. Erwartet wird
|
||||
jeweils die benannte Compile-Diagnose `Feature unavailable`, nicht ein
|
||||
allgemeiner Syntaxfehler. Beide Soll-Gegenproben schlagen fehl.
|
||||
|
||||
**Vertrag:** Delta `sprach-frontend`, Requirement „Compile-Zeit-Abweisung
|
||||
deklarierter Non-Features“ verlangt diese Diagnose für jede Verwendung;
|
||||
Task 1.1 verlangt die benannte Abweisung weiterhin verbotener Formen.
|
||||
|
||||
**Behebung:** Die besondere Syntax der verbleibenden ausgeschlossenen Formen
|
||||
vor dem allgemeinen Aufrufparser erkennen oder bis zur Non-Feature-Diagnose
|
||||
parsen. Die CLI-/Inventarproben um Koordinaten mit Farbargument und PALETTE
|
||||
USING ergänzen; die erlaubte Grafikuntermenge weiter positiv prüfen.
|
||||
|
||||
## Ausgeführte Prüfungen
|
||||
|
||||
- `cargo test --workspace`: **476 bestanden, 0 fehlgeschlagen, 2 ignoriert**.
|
||||
- Gesonderter Fremdtest mit dem dokumentierten Checkout und Commit:
|
||||
**bestanden**, sieben Einstiege jeweils zweimal, mit den festgelegten
|
||||
Menü-, Exit-, Dialog- und Spin-Zuständen.
|
||||
- `openspec validate --all --strict`: **16 bestanden, 0 Fehler**;
|
||||
vorhandene Hinweise zu Textlängen sind informativ.
|
||||
- `git diff --check`: bestanden.
|
||||
- Neue unabhängige Soll-Gegenproben: **fünf fehlgeschlagen**, zugeordnet zu den
|
||||
drei Befunden oben. Produktcode, bestehende Tests und Sollbilder wurden
|
||||
während dieses Reviews nicht geändert.
|
||||
|
||||
Die [Gegenproben](review-probes.py) sind reproduzierbar und erzeugen Dateien
|
||||
nur in einem temporären Verzeichnis. Sie bauen die CLI, prüfen die vier
|
||||
CLI-Fälle und kompilieren für die Zielmutation eine temporäre Kopie des
|
||||
bestehenden Inventarprüfers:
|
||||
|
||||
```sh
|
||||
python3 openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review-probes.py
|
||||
```
|
||||
|
||||
Der Aufruf liefert derzeit Status 1. Keine GUI, kein Emulator und kein
|
||||
Netzwerkzugriff sind erforderlich. Keine Verifikationsdimension wurde
|
||||
übersprungen. Die grünen bestehenden Tests und die sieben engen
|
||||
Fremdprogrammsequenzen widerlegen die zusätzlich reproduzierten Fehler nicht.
|
||||
|
||||
**Abnahme:** Noch nicht vollständig spezifikationskonform. V01–V03 vor der
|
||||
abschließenden Synchronisierung und Archivierung beheben und nachprüfen.
|
||||
@@ -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())
|
||||
@@ -0,0 +1,51 @@
|
||||
# Abschließende Verifikation vom 06.09.2026
|
||||
|
||||
Change: `spezifikationsabgleich-und-regressionsnachweise`.
|
||||
Grundlage: aktueller Arbeitsbaum auf `c8b92f06190d77714865e0018b090348f372d77e`.
|
||||
Geprüft: Proposal, Design D1–D4, elf Tasks, vier Delta-Specs mit sechs
|
||||
Requirements und 18 Szenarien sowie die betroffenen Hauptverträge und Codepfade.
|
||||
|
||||
**Ergebnis: keine offenen Befunde.**
|
||||
|
||||
| Dimension | Ergebnis |
|
||||
|---|---|
|
||||
| Vollständigkeit | 11/11 Tasks, 6/6 Requirements, 18/18 Szenarien abgedeckt |
|
||||
| Korrektheit | Gesamtsuite, fünf ursprüngliche Soll-Gegenproben und zusätzliche Regressionen bestanden |
|
||||
| Kohärenz | Gemeinsame Compiler-/VM-/Forms-Pfade; vorhandener TextScreen; keine neue Abhängigkeit oder TBC-Version |
|
||||
| Befunde | 0 CRITICAL, 0 WARNING, 0 SUGGESTION |
|
||||
|
||||
Die [erste Nachprüfung mit V01–V03](review-initial.md) bleibt als historischer
|
||||
Ausgangsbefund erhalten. Die vollständige Szenarienmatrix steht in
|
||||
[verification.md](verification.md), der Abgleich F01–F25 im
|
||||
[Abschlussreport](../../../../docs/reviews/2026-09-06-openspec-abschlussreview.md).
|
||||
|
||||
## Nachweis der Behebung
|
||||
|
||||
| Befund | Korrektur und Gegenprobe |
|
||||
|---|---|
|
||||
| V01 – indizierte Elterncontainer | [frm.rs](../../../../crates/tb-ui/src/frm.rs) übergibt vollständige Parent-Schlüssel. [forms.rs](../../../../crates/tb-ui/src/forms.rs) bewahrt diese in den vorhandenen PARENT-Eigenschaften; Getter, Root-Traversierung, Geometrie, Optionsgruppen und Menüpfade verwenden dieselbe Referenz. Die ursprünglichen CLI-Gegenproben liefern `1` sowohl aus FRM als auch aus TBC nach Löschen der Quelle. |
|
||||
| V01 – ergänzend geprüfte Darstellung und Maus | `indexed_containers_preserve_parent_geometry_and_option_groups` prüft die Elterninstanz, absolute Position, unabhängige Optionsgruppen, sichtbare Beschriftung und Trefferziel. Vor der letzten Korrektur fing `Group(1)` den Klick auf sein Kind `Other` ab. `form_controls` stellt jetzt den Container vor seine Kinder; Zeichnung und umgekehrte Trefferprüfung nutzen dieselbe Reihenfolge. Die neue Gegenprobe besteht. |
|
||||
| V01 – TBC-Grenze | [bytecode.rs](../../../../crates/tb-vm/src/bytecode.rs) validiert die effektiven Parent-Referenzen einschließlich Arrayinstanzen und Zyklen. `beschaedigte_container_werden_vor_der_ausfuehrung_abgewiesen` enthält beide Mutationen. Bestehender Mehrformular-/Array-Test bleibt grün. |
|
||||
| V02 – falsches Laufzeitziel | [inventar.rs](../../../../crates/tb-cli/tests/inventar.rs) vergleicht unabhängige Sollziele in den 291 festen Vorlagen mit dem erzeugten Kompilat. Builtin-IDs und Konverterselektoren, konkrete Forms-Member, VM-Instruktionen und Deklarationsmetadaten werden geprüft. `$INCLUDE` durchläuft den echten CLI-Projektlader. Die ursprüngliche TIMEZONEKNOWN→TIMER-Mutation wird mit Name und Status abgewiesen. |
|
||||
| V02 – weitere Mutationen | `falsche_gueltige_builtinbindung_und_entfernte_absenkung_fallen_durch`, `forms_absenkung_muss_den_richtigen_member_erreichen` und `gemeinsam_dispatchte_konverter_brauchen_den_richtigen_typselektor` bestehen; bestehende Status-/Unsupported-/fehlende-Bindung-Proben ebenfalls. Alle 845 Inventareinträge werden geprüft; 795 implementiert, 50 Non-Features, 0 offen. Ereignisse werden zusätzlich über 133 reale Auslösepfade bis zum BASIC-Handler geprüft. |
|
||||
| V03 – weitere Non-Feature-Syntax | [parser.rs](../../../../crates/tb-frontend/src/parser.rs) erkennt zuerst mögliche Zuweisungen und weist danach ausgeschlossene Anweisungen vor der allgemeinen Argumentauflösung namentlich ab. CLI-Test prüft PSET/PRESET mit Farbe und STEP, CIRCLE STEP, WINDOW SCREEN und PALETTE USING einschließlich IF. Gleichnamige Arrays sowie SCREEN 0–13/LINE/PAINT/VIEW bleiben gültig. Beide ursprünglichen Grafik-Gegenproben bestehen. |
|
||||
| Ergänzende Kohärenzprüfung | ComboBox-Ausklappzustand wird bei Fokuswechsel und Ausblenden zurückgesetzt; `combobox_dropdown_closes_on_focus_change_and_hide` besteht. |
|
||||
|
||||
## Abschlussprüfungen
|
||||
|
||||
- `cargo test --workspace`: **481 bestanden, 0 fehlgeschlagen, 2 ignoriert**.
|
||||
- `python3 openspec/changes/archive/2026-09-06-spezifikationsabgleich-und-regressionsnachweise/review-probes.py`: **5/5 Gegenproben bestanden**. Die Soll-Assertions bleiben erhalten; nur Testpfad und Cargo-Auflösung wurden an den verschobenen Inventartest angepasst.
|
||||
- `TB_VBDOS_REPO=/tmp/terminalbasic-vbdos-evidence cargo test -p tb-cli --test foreign oeffentliche -- --ignored --nocapture`: **bestanden**, sieben öffentliche Programme jeweils zweimal; Revision `1cdd2b32b829fe1721d0b6aecc433abc47a96fb6`.
|
||||
- `cargo run --manifest-path tests/terminalfrei/Cargo.toml --offline`: **bestanden**, `Terminalfreier FRM-/VM-Lauf: KM`.
|
||||
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: **bestanden**.
|
||||
- `openspec validate --all --strict`: **16/16 Einträge gültig**.
|
||||
- `cargo fmt --all -- --check` und `git diff --check`: **bestanden**, neue Dateien zusätzlich geprüft.
|
||||
|
||||
Der Golden-Generator wurde nicht ausgeführt. Bei diesen Reviewkorrekturen
|
||||
wurden keine weiteren Golden Files verändert. Kein Prüfschritt wurde wegen
|
||||
fehlender Voraussetzungen ausgelassen; der normalerweise ignorierte
|
||||
Fremdprogrammlauf wurde ausdrücklich ausgeführt.
|
||||
|
||||
Die Umsetzung ist verifiziert. Am 06.09.2026 wurden alle vier Delta-Specs
|
||||
in die Hauptspecs synchronisiert und dieser Change mit 11/11 erledigten Tasks
|
||||
archiviert. Die 15 Hauptspecs bestehen die Validierung nach der Archivierung.
|
||||
@@ -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
|
||||
@@ -1,15 +1,22 @@
|
||||
## 1. Normtexte
|
||||
|
||||
- [ ] 1.1 Grafik-Abweisung und FRM-Schreibmodi gemäß Delta präzisieren; verifizieren mit tbc check für erlaubte Grafik, benannter Ablehnung weiterhin verbotener Formen und bytegleichem FRM-Rundlauf mit explizitem Default.
|
||||
- [x] 1.1 Grafik-Abweisung und FRM-Schreibmodi gemäß Delta präzisieren; verifizieren mit tbc check für erlaubte Grafik, benannter Ablehnung weiterhin verbotener Formen und bytegleichem FRM-Rundlauf mit explizitem Default.
|
||||
|
||||
## 2. Nachweise
|
||||
|
||||
- [ ] 2.1 Inventarprüfung gegen erreichbare HIR-/Dispatch-Pfade erweitern; verifizieren mit künstlich offen/implementiert/verboten gesetzten Einträgen und einer fehlenden Runtime-Bindung, jeweils mit Namen und Status im Fehler.
|
||||
- [ ] 2.2 Fehlende FORM_KEYPRESS/FORM_KEYUP-Auslöser ergänzen und registrierte Form-Ereignisse auf realen Quellenpfad prüfen; verifizieren durch BASIC-Handler nach Hosteingabe, nicht durch direktes queue_form_event allein.
|
||||
- [ ] 2.3 Maskierungs- und Formular-Korpus ergänzen; verifizieren mit KEY/UEVENT in ON/OFF/STOP, normal lesbarer STOP-Taste, Control-Access-Key, BackTab, Fokusreihenfolge und zweimal identischer Ausgabe.
|
||||
- [ ] 2.4 Öffentlichen Fremdprogrammbestand in dokumentierter Revision prüfen; verifizieren durch diagnosefreie Übersetzung und reproduzierbare deklarierte Eingabesequenzen bis zu sichtbaren Zustandsänderungen. Nicht vorhandene externe Voraussetzungen im Ergebnisbericht benennen, nicht abhaken.
|
||||
- [x] 2.1 Inventarprüfung gegen erreichbare HIR-/Dispatch-Pfade erweitern; verifizieren mit künstlich offen/implementiert/verboten gesetzten Einträgen und einer fehlenden Runtime-Bindung, jeweils mit Namen und Status im Fehler.
|
||||
- [x] 2.2 Fehlende FORM_KEYPRESS/FORM_KEYUP-Auslöser ergänzen und registrierte Form-Ereignisse auf realen Quellenpfad prüfen; verifizieren durch BASIC-Handler nach Hosteingabe, nicht durch direktes queue_form_event allein.
|
||||
- [x] 2.3 Maskierungs- und Formular-Korpus ergänzen; verifizieren mit KEY/UEVENT in ON/OFF/STOP, normal lesbarer STOP-Taste, Control-Access-Key, BackTab, Fokusreihenfolge und zweimal identischer Ausgabe.
|
||||
- [x] 2.4 Öffentlichen Fremdprogrammbestand in dokumentierter Revision prüfen; verifizieren durch diagnosefreie Übersetzung und reproduzierbare deklarierte Eingabesequenzen bis zu sichtbaren Zustandsänderungen. Nicht vorhandene externe Voraussetzungen im Ergebnisbericht benennen, nicht abhaken.
|
||||
|
||||
## 3. Gesamtabnahme
|
||||
|
||||
- [ ] 3.1 Alle F01–F25 nach Integration der Korrektur-Changes mit Regressionstest oder geklärtem Normtext abgleichen; verifizieren durch separaten Ergebnisreport mit Fundstellen pro F-ID.
|
||||
- [ ] 3.2 Gesamten Korpus und Spec-Validierung ausführen; verifizieren mit cargo test --workspace, openspec validate --all --strict und git diff --check; Golden Files nicht ungeprüft neu aufzeichnen.
|
||||
- [x] 3.1 Alle F01–F25 nach Integration der Korrektur-Changes mit Regressionstest oder geklärtem Normtext abgleichen; verifizieren durch separaten Ergebnisreport mit Fundstellen pro F-ID.
|
||||
- [x] 3.2 Gesamten Korpus und Spec-Validierung ausführen; verifizieren mit cargo test --workspace, openspec validate --all --strict und git diff --check; Golden Files nicht ungeprüft neu aufzeichnen.
|
||||
|
||||
## 4. Reviewkorrekturen
|
||||
|
||||
- [x] 4.1 V01: Indizierte Parent-Referenzen durch Import, Runtime und Kompilat erhalten; Positionierung, Gruppierung und ungültige Referenzen prüfen.
|
||||
- [x] 4.2 V02: Inventarelemente gegen unabhängige konkrete Absenkungsziele prüfen; falsche gültige Bindungen und entfernte Absenkung müssen scheitern.
|
||||
- [x] 4.3 V03: Ausgeschlossene Grafikvarianten durchgehend namentlich abweisen; erlaubte Grafik weiter übersetzen.
|
||||
- [x] 4.4 Unabhängige Gegenproben, vollständige Verifizierung und Ergebnisberichte ohne offene Befunde abschließen.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Implementierungs- und Verifikationsnachweis
|
||||
|
||||
**Nachprüfung abgeschlossen:** V01–V03 und die ergänzend geprüfte Container-Z-Reihenfolge sind behoben. Der aktuelle [Review](review.md) enthält die Abschlussprüfung; der [erste Review](review-initial.md) bleibt als Ausgangsbefund erhalten.
|
||||
|
||||
Change: `spezifikationsabgleich-und-regressionsnachweise`, 06.09.2026.
|
||||
Grundlage: Arbeitsbaum auf `c8b92f06190d77714865e0018b090348f372d77e`.
|
||||
Proposal, Design D1–D4, Tasks und alle vier Delta-Specs wurden abgeglichen.
|
||||
|
||||
| Dimension | Ergebnis |
|
||||
|---|---|
|
||||
| Vollständigkeit | 11/11 Tasks, 6/6 Requirements, 18/18 Szenarien |
|
||||
| Korrektheit | Regressionssuite, Korpus und gesonderter Fremdprogrammlauf bestanden |
|
||||
| Kohärenz | Vorhandene Compiler-/VM-/CaptureHost-Pfade erweitert; keine neue Abhängigkeit oder TBC-Version |
|
||||
| Offene Befunde im Change | 0 CRITICAL, 0 WARNING, 0 SUGGESTION |
|
||||
|
||||
## Umsetzung und Nachweis je Task
|
||||
|
||||
| Task | Umsetzung / Nachweis |
|
||||
|---|---|
|
||||
| 1.1 | Grafik-Ausnahmen und verbleibende Non-Features in Sprachreferenz und Delta; CLI prüft SCREEN 0–13/LINE/PAINT/VIEW sowie benannte POKE/PEEK/CIRCLE/WINDOW/IOCTL-Abweisung. FRM-Erhaltung, Default-Auslassung und strukturelles Index 0 sind durch Writer-Tests und Normtext getrennt. |
|
||||
| 2.1 | Inventartest liegt wegen der Prüfung des erzeugten Bytecodes jetzt in tb-cli. 291 feste BASIC-Vorlagen plus Forms-Proben prüfen 845 Einträge; Statusmutation, Unsupported, fehlende oder vertauschte Bindungen, falsche Forms-Member/Typselektoren und entfernte Absenkung erzeugen benannte Fehler. Die vierte TSV-Spalte enthält die unabhängigen Sollziele; `$INCLUDE` wird vom CLI aufgelöst. 795 implementiert, 50 Non-Features, 0 offen. |
|
||||
| 2.2 | Alle 133 registrierten Form-/Control-Ereignisse erreichen aus ihren Quellen den BASIC-Handler. Die Matrix entdeckt fehlende Quellen; eine künstlich entfernte Event-Bindung bestätigt den Fehlerpfad. Keine dieser Nachweisproben verwendet queue_form_event. |
|
||||
| 2.3 | trap-masken.bas und formular-access-backtab.frm ergänzen die fehlenden Kombinationen. F1 unter STOP bleibt lesbar und löst nach ON genau einen Handler aus. Alt+O und Shift+Tab ergeben Click;LostFocus;GotFocus;. Jeder Korpuslauf wird wiederholt und gegen sein Sollbild verglichen. |
|
||||
| 2.4 | Sieben öffentliche Programme aus cout/vbdos im Commit 1cdd2b32b829fe1721d0b6aecc433abc47a96fb6, jeweils zweimal frisch exportiert, diagnosefrei gebaut und mit festen Eingaben bedient. Keine fehlende Voraussetzung. Der gesonderte Test ist bewusst unabhängig vom normalen Workspace-Lauf aufrufbar. |
|
||||
| 3.1 | [Abschlussreport](../../../../docs/reviews/2026-09-06-openspec-abschlussreview.md) enthält eine eigene Zeile mit aktuellem Beleg für jede F-ID sowie die sieben reproduzierbaren Fremdprogrammsequenzen. Historischer Bericht bleibt erhalten. |
|
||||
| 3.2 | 481 Workspace-Tests und gesonderter Fremdtest bestanden; separater terminalfreier Resolver erfolgreich. Strikte Spec-Validierung, Clippy, Format- und Diff-Prüfung bestanden. Golden-Generator nicht ausgeführt. |
|
||||
| 4.1 | Vollständige Parent-Schlüssel im Import, Getter, Container-Traversierung, Geometrie, Optionsgruppen und Menüzuordnung. `indexed_containers_preserve_parent_geometry_and_option_groups` prüft zusätzlich sichtbare Kinder und korrekte Mausziele. TBC prüft Zyklen und fehlende indizierte Eltern; Quell-/TBC-Gegenprobe gibt jeweils 1 aus. |
|
||||
| 4.2 | `falsche_gueltige_builtinbindung_und_entfernte_absenkung_fallen_durch`, `forms_absenkung_muss_den_richtigen_member_erreichen`, `gemeinsam_dispatchte_konverter_brauchen_den_richtigen_typselektor`; ursprüngliche TIMEZONEKNOWN→TIMER-Gegenprobe schlägt korrekt beim Prüfer fehl. |
|
||||
| 4.3 | CLI-Proben für PSET/PRESET mit Farbe und STEP, CIRCLE STEP, WINDOW SCREEN und PALETTE USING einschließlich IF; gleichnamige Variablen/Arrays und erlaubte Grafik bleiben gültig. |
|
||||
| 4.4 | Gesamtabnahme und erneute Gegenproben bestanden; zusätzlicher Test `combobox_dropdown_closes_on_focus_change_and_hide` verhindert veralteten Ausklappzustand. |
|
||||
|
||||
## Szenarien
|
||||
|
||||
Testnamen stehen in den verlinkten Testdateien. Die Ereignismatrix umfasst
|
||||
16 Form-Ereignisse und 117 Control-Ereignisse.
|
||||
|
||||
| Requirement / Szenario | Ausführbarer Nachweis |
|
||||
|---|---|
|
||||
| FRM: Rundlauf | [frm.rs](../../../../crates/tb-ui/src/frm.rs): reads_nested_form_and_preserves_source_exactly |
|
||||
| FRM: Vorgabewerte nicht geschrieben | writes_no_properties_for_default_only_control; explicit_default_is_preserved_until_canonical_write prüft zusätzlich die geänderte Beschreibung |
|
||||
| FRM: expliziter Default im Original | explicit_default_is_preserved_until_canonical_write, mit CRLF und Enabled = −1 |
|
||||
| FRM: einzelnes Arrayelement Index 0 | canonical_index_zero_remains_an_array_and_array_parents_are_preserved |
|
||||
| Inventar: offen trotz Implementierung | [inventar.rs](../../../../crates/tb-cli/tests/inventar.rs): falscher_status_und_fehlende_runtimebindung_werden_namentlich_erkannt |
|
||||
| Inventar: implementiert trotz Unsupported | implementiert_mit_unsupported_marker_faellt_durch |
|
||||
| Inventar: Abdeckungsstand | abdeckungsstand_wird_ausgewiesen; jeder_inventareintrag_erreicht_hir_und_laufzeitziel |
|
||||
| Inventar: Ereignis ohne Auslöser | [events.rs](../../../../crates/tb-vm/tests/events.rs): fehlender_ereignispfad_wird_namentlich_erkannt; registrierte_form_ereignisse_erreichen_basic_aus_realen_quellen und entsprechende Control-Matrix |
|
||||
| Traps: Zeit-Trap | [traps.bas](../../../../tests/compat/traps.bas), Hostuhr im Programmkopf |
|
||||
| Traps: fehlende Zeitdeklaration | [compat.rs](../../../../crates/tb-cli/tests/compat.rs): Test mit should_panic auf fehlenden Zeitverlauf |
|
||||
| Traps: KEY STOP | [trap-masken.bas](../../../../tests/compat/trap-masken.bas), F1 lesbar und nach ON genau einmal |
|
||||
| Forms: Formularprogramm | [formular.frm](../../../../tests/compat/formular.frm): Maus, Liste, Timer, Menü, modaler Dialog |
|
||||
| Forms: Wiederholbarkeit | korpus_laeuft_mit_korrekter_ausgabe führt jedes Programm zweimal aus |
|
||||
| Forms: Control-Access-Key/Rückwärtstab | [formular-access-backtab.frm](../../../../tests/compat/formular-access-backtab.frm) |
|
||||
| Fremdprogramm: diagnosefrei und bedienbar | [foreign.rs](../../../../crates/tb-cli/tests/foreign.rs): oeffentliche_formularprogramme_sind_reproduzierbar_bedienbar |
|
||||
| Fremdprogramm: wiederholbare Zustände | Derselbe Test: zwei isolierte Läufe pro Einstieg, Vergleich aller Bildschirm-/Formular-/Menüzustände, zusätzlich explizite Endzustände und Spin-Wert |
|
||||
| Non-Features: Hardware-Zugriff | [project.rs](../../../../crates/tb-cli/tests/project.rs): erlaubte_grafik_und_benannte_nonfeatures_werden_per_cli_geprueft |
|
||||
| Non-Features: erlaubte Grafik | Derselbe CLI-Test prüft alle 14 SCREEN-Modi mit LINE, PAINT und VIEW |
|
||||
|
||||
## Review der während der Umsetzung entdeckten Ursachen
|
||||
|
||||
Der Abschlussreport dokumentiert Signaturen, benannte Non-Feature-Diagnosen,
|
||||
fehlende Ereignisquellen, Fokus-Nebenwirkungen, FRM-Array-Flag, eingebettete
|
||||
Includes und Array-Eltern. Alle sind korrigiert und durch die obigen Tests
|
||||
oder den gesonderten Fremdprogrammlauf abgedeckt. Der Mehrformular-Test wurde
|
||||
nach Anpassung der Importauflösung erneut erfolgreich ausgeführt.
|
||||
|
||||
Die einzige Änderung an einem bestehenden Sollbild ist ausdrücklich geprüft:
|
||||
formular.out, Zeile 4, Spalten 24–33, zehn Fokusattribute auf f1. Zeichenebene
|
||||
und übrige Zellen sind unverändert. Zwei neue Sollbilder wurden semantisch
|
||||
festgelegt und zweimal bestätigt. Kein automatisches Neuaufzeichnen.
|
||||
|
||||
Die Verifikation wurde vollständig durchgeführt. Am 06.09.2026 wurden die
|
||||
Hauptspecs synchronisiert und der Change archiviert.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -84,15 +84,23 @@ Sprungziele („Label not defined") sowie Prozedur- und Builtin-Signaturen
|
||||
Die Semantik MUST jede Verwendung der in docs/sprachreferenz.md
|
||||
(„Abweichungen") deklarierten Non-Features — Hardware-Nähe (`PEEK`,
|
||||
`POKE`, `INP`, `OUT`, `WAIT`, `BLOAD`/`BSAVE`, `VARPTR`-Familie, `CALL
|
||||
ABSOLUTE`/`INTERRUPT`, `IOCTL`), `CHAIN`, Grafik-Anweisungen, `SOUND`/
|
||||
ABSOLUTE`/`INTERRUPT`, `IOCTL`), `CHAIN`, die weiterhin ausgeschlossenen Grafik-Anweisungen (`PSET`,
|
||||
`PRESET`, `CIRCLE`, `DRAW`, Grafik-`GET`/`PUT`, `PALETTE`, `WINDOW`,
|
||||
`PMAP`, `PCOPY`, `POINT`), `SOUND`/
|
||||
`PLAY` und die Event-Geräte `COM`/`PEN`/`PLAY`/`STRIG` — zur Compile-Zeit
|
||||
mit der Meldung „Feature unavailable" abweisen. Dokumentierte Features
|
||||
außerhalb dieser Liste MUST NOT abgewiesen werden (Guiding Principle).
|
||||
|
||||
Die dokumentierte Zellenpuffer-Untermenge SCREEN 0–13, LINE, PAINT und VIEW SHALL verfügbar bleiben.
|
||||
|
||||
#### Scenario: Hardware-Zugriff
|
||||
- **WHEN** `POKE 100, 1` oder `x = PEEK(100)` kompiliert wird
|
||||
- **THEN** erscheint die Compile-Diagnose „Feature unavailable"
|
||||
|
||||
#### Scenario: Erlaubte Grafikuntermenge
|
||||
- **WHEN** ein Programm SCREEN 1, LINE, PAINT und VIEW in gültiger Syntax verwendet
|
||||
- **THEN** wird es ohne Feature-unavailable-Diagnose übersetzt
|
||||
|
||||
### Requirement: Diagnostik mit Vorbild-Meldungen
|
||||
Diagnosen SHALL exakte 1-basierte Quelltextpositionen (Zeile, Spalte)
|
||||
tragen; die Meldungstexte SHALL dem Katalog des Vorbilds folgen, wo eine
|
||||
|
||||
@@ -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
|
||||
|
||||
37
tests/compat/formular-access-backtab.frm
Normal file
37
tests/compat/formular-access-backtab.frm
Normal 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
|
||||
1
tests/compat/formular-access-backtab.out
Normal file
1
tests/compat/formular-access-backtab.out
Normal file
@@ -0,0 +1 @@
|
||||
Click;LostFocus;GotFocus;
|
||||
@@ -18,7 +18,7 @@
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
||||
70700707070707070707070707070707070707077070700707070707070707070770707070707070707070707070
|
||||
7070070707070707070707070707070707070707707070f1f1f1f1f1f1f1f1f1f170707070707070707070707070
|
||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
||||
|
||||
62
tests/compat/trap-masken.bas
Normal file
62
tests/compat/trap-masken.bas
Normal 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
|
||||
12
tests/compat/trap-masken.out
Normal file
12
tests/compat/trap-masken.out
Normal 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
|
||||
291
tests/support/inventar-quellen.tsv
Normal file
291
tests/support/inventar-quellen.tsv
Normal 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.
|
Reference in New Issue
Block a user