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";
|
const NON_FEATURE: &str = "Non-Feature";
|
||||||
|
|
||||||
fn wurzel() -> PathBuf {
|
fn wurzel() -> PathBuf {
|
||||||
// crates/tb-frontend/ → Projektwurzel
|
// crates/tb-cli/ → Projektwurzel
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,3 +599,486 @@ fn signatur_greift_ohne_laufzeitverhalten() {
|
|||||||
"Diagnose nennt `MKL$` nicht: {texte:?}"
|
"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());
|
assert!(tbc(&dir, "run", "empty.bas").status.success());
|
||||||
std::fs::remove_dir_all(dir).unwrap();
|
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> {
|
fn parse_statement(&mut self) -> Option<Stmt> {
|
||||||
let pos = self.pos();
|
let pos = self.pos();
|
||||||
|
|
||||||
// Zeilennummern und Labels nur am Zeilenanfang
|
// Zeilennummern und Labels nur am Zeilenanfang
|
||||||
if self.at_line_start {
|
if self.at_line_start {
|
||||||
if let TokenKind::Num(NumValue::Int(n)) = self.k() {
|
if let TokenKind::Num(NumValue::Int(n)) = self.k() {
|
||||||
@@ -611,7 +610,10 @@ impl<'a> P<'a> {
|
|||||||
self.advance();
|
self.advance();
|
||||||
if self.k() == TokenKind::LParen {
|
if self.k() == TokenKind::LParen {
|
||||||
// Grafikform GET (x1,y1)-(x2,y2): deklariertes Non-Feature
|
// 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();
|
self.sync();
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -1804,7 +1806,7 @@ impl<'a> P<'a> {
|
|||||||
// Non-Feature — an der Syntax erkennbar, weil `DEF` sonst nur
|
// Non-Feature — an der Syntax erkennbar, weil `DEF` sonst nur
|
||||||
// `DEF FNname` einleitet.
|
// `DEF FNname` einleitet.
|
||||||
if matches!(self.k(), TokenKind::Ident { ref name, suffix: None } if name == "SEG") {
|
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();
|
self.sync();
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -1885,6 +1887,21 @@ impl<'a> P<'a> {
|
|||||||
self.sync();
|
self.sync();
|
||||||
return None;
|
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` …
|
// Ereignissteuerung: `TIMER ON`, `KEY(5) OFF`, `UEVENT STOP` …
|
||||||
if let Expr::Name {
|
if let Expr::Name {
|
||||||
ref 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)
|
(5, 5, &[N, N, N, N, N], Db)
|
||||||
}
|
}
|
||||||
"IPMT" | "IPMT#" | "PPMT" | "PPMT#" | "RATE" | "RATE#" => (6, 6, &[N, 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),
|
"MIRR" | "MIRR#" => (3, 3, &[A, N, N], Db),
|
||||||
"SLN" | "SLN#" => (3, 3, &[N, N, N], Db),
|
"SLN" | "SLN#" => (3, 3, &[N, N, N], Db),
|
||||||
"SYD" | "SYD#" | "DDB" | "DDB#" => (4, 4, &[N, 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),
|
"CVD" | "CVDMBF" => (1, 1, &[S], Db),
|
||||||
"CVC" => (1, 1, &[S], Cu),
|
"CVC" => (1, 1, &[S], Cu),
|
||||||
// Dateisystem und System
|
// Dateisystem und System
|
||||||
|
"SHELL" => (1, 1, &[S], L),
|
||||||
"CURDIR$" => (0, 1, &[S], St),
|
"CURDIR$" => (0, 1, &[S], St),
|
||||||
"DIR$" => (0, 1, &[S], St),
|
"DIR$" => (0, 1, &[S], St),
|
||||||
"LPOS" => (1, 1, &[N], I),
|
"LPOS" => (1, 1, &[N], I),
|
||||||
@@ -2861,7 +2863,7 @@ impl Sema {
|
|||||||
} => {
|
} => {
|
||||||
let Some(art) = trap_art(device) else {
|
let Some(art) = trap_art(device) else {
|
||||||
// COM/PEN/PLAY/STRIG sind Non-Feature — namentlich.
|
// COM/PEN/PLAY/STRIG sind Non-Feature — namentlich.
|
||||||
self.err(*pos, "Feature unavailable");
|
self.err(*pos, format!("Feature unavailable: {device}"));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// Wertebereich prüfen, soweit die Kennung konstant ist.
|
// Wertebereich prüfen, soweit die Kennung konstant ist.
|
||||||
@@ -2908,7 +2910,7 @@ impl Sema {
|
|||||||
} => {
|
} => {
|
||||||
match device.as_str() {
|
match device.as_str() {
|
||||||
"TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "EVENT" => {}
|
"TIMER" | "KEY" | "UEVENT" | "SIGNAL" | "EVENT" => {}
|
||||||
_ => self.err(*pos, "Feature unavailable"),
|
_ => self.err(*pos, format!("Feature unavailable: {device}")),
|
||||||
}
|
}
|
||||||
if let Some(i) = index {
|
if let Some(i) = index {
|
||||||
self.want_num(i, scope);
|
self.want_num(i, scope);
|
||||||
@@ -2939,7 +2941,7 @@ impl Sema {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(art) = trap_art(device) else {
|
let Some(art) = trap_art(device) else {
|
||||||
self.err(*pos, "Feature unavailable");
|
self.err(*pos, format!("Feature unavailable: {device}"));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Some(i) = index {
|
if let Some(i) = index {
|
||||||
@@ -3566,7 +3568,7 @@ impl Sema {
|
|||||||
for a in args {
|
for a in args {
|
||||||
self.lower_expr(a, scope);
|
self.lower_expr(a, scope);
|
||||||
}
|
}
|
||||||
self.err(pos, "Feature unavailable");
|
self.err(pos, format!("Feature unavailable: {name}"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.err(pos, "Subprogram not defined");
|
self.err(pos, "Subprogram not defined");
|
||||||
@@ -4147,7 +4149,7 @@ impl Sema {
|
|||||||
fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) {
|
fn reject_com_device(&mut self, file: &Expr, pos: SourcePos) {
|
||||||
if let Expr::StrLit(s, _) = file {
|
if let Expr::StrLit(s, _) = file {
|
||||||
if ist_com_geraet(s) {
|
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 {
|
for a in idx {
|
||||||
self.lower_expr(a, scope);
|
self.lower_expr(a, scope);
|
||||||
}
|
}
|
||||||
self.err(pos, "Feature unavailable");
|
self.err(pos, format!("Feature unavailable: {full_name}"));
|
||||||
return (HExpr::Int(0), Ty::Unknown);
|
return (HExpr::Int(0), Ty::Unknown);
|
||||||
}
|
}
|
||||||
// 4. Builtin-Funktion
|
// 4. Builtin-Funktion
|
||||||
@@ -4982,7 +4984,7 @@ impl Sema {
|
|||||||
.is_some_and(|v| suffix.is_none_or(|s| v.ty == suffix_ty(s)));
|
.is_some_and(|v| suffix.is_none_or(|s| v.ty == suffix_ty(s)));
|
||||||
if !declared {
|
if !declared {
|
||||||
if banned_feature(&full_name) {
|
if banned_feature(&full_name) {
|
||||||
self.err(pos, "Feature unavailable");
|
self.err(pos, format!("Feature unavailable: {full_name}"));
|
||||||
return (HExpr::Int(0), Ty::Unknown);
|
return (HExpr::Int(0), Ty::Unknown);
|
||||||
}
|
}
|
||||||
if let Some((0, _, _, _)) = builtin_fn(&full_name) {
|
if let Some((0, _, _, _)) = builtin_fn(&full_name) {
|
||||||
@@ -5628,13 +5630,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hardware_features_zur_compilezeit_abgelehnt() {
|
fn hardware_features_zur_compilezeit_abgelehnt() {
|
||||||
assert!(diags("POKE 100, 1").contains(&"Feature unavailable".to_string()));
|
assert!(diags("POKE 100, 1").contains(&"Feature unavailable: POKE".to_string()));
|
||||||
assert!(diags("x = PEEK(100)").contains(&"Feature unavailable".to_string()));
|
assert!(diags("x = PEEK(100)").contains(&"Feature unavailable: PEEK".to_string()));
|
||||||
assert!(diags("p = VARPTR(a%)").contains(&"Feature unavailable".to_string()));
|
assert!(diags("p = VARPTR(a%)").contains(&"Feature unavailable: VARPTR".to_string()));
|
||||||
assert!(diags("SOUND 440, 10").contains(&"Feature unavailable".to_string()));
|
assert!(diags("SOUND 440, 10").contains(&"Feature unavailable: SOUND".to_string()));
|
||||||
assert!(diags("CHAIN \"prog\"").contains(&"Feature unavailable".to_string()));
|
assert!(diags("CHAIN \"prog\"").contains(&"Feature unavailable: CHAIN".to_string()));
|
||||||
assert!(diags("PLAY \"cde\"").contains(&"Feature unavailable".to_string()));
|
assert!(diags("PLAY \"cde\"").contains(&"Feature unavailable: PLAY".to_string()));
|
||||||
assert!(diags("CIRCLE 1, 2").contains(&"Feature unavailable".to_string()));
|
assert!(diags("CIRCLE 1, 2").contains(&"Feature unavailable: CIRCLE".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -5726,7 +5728,7 @@ mod tests {
|
|||||||
fn datei_ea_und_events() {
|
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";
|
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(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 {
|
fn form_catalog() -> FormCatalog {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ pub struct FormsModel {
|
|||||||
visible_forms: Vec<u16>,
|
visible_forms: Vec<u16>,
|
||||||
active_form: Option<u16>,
|
active_form: Option<u16>,
|
||||||
active_control: Option<(u16, Option<i32>)>,
|
active_control: Option<(u16, Option<i32>)>,
|
||||||
|
dropdown: Option<ObjectKey>,
|
||||||
width: usize,
|
width: usize,
|
||||||
height: usize,
|
height: usize,
|
||||||
screen_visible: bool,
|
screen_visible: bool,
|
||||||
@@ -103,16 +104,35 @@ impl FormsModel {
|
|||||||
};
|
};
|
||||||
if self.root_form(control) == Some(form) {
|
if self.root_form(control) == Some(form) {
|
||||||
self.active_control = None;
|
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 {
|
pub fn new(objects: Vec<FormObject>, width: usize, height: usize) -> Self {
|
||||||
let objects = objects
|
let objects = objects
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|description| {
|
.map(|description| {
|
||||||
let properties = forms::properties(description.class)
|
let mut properties: Vec<_> = forms::properties(description.class)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| PropertyValue::from_default(p.default, p.ty))
|
.map(|p| PropertyValue::from_default(p.default, p.ty))
|
||||||
.collect();
|
.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;
|
let loaded = description.class == ObjectClass::Screen;
|
||||||
ObjectInstance {
|
ObjectInstance {
|
||||||
description,
|
description,
|
||||||
@@ -132,6 +152,7 @@ impl FormsModel {
|
|||||||
visible_forms: Vec::new(),
|
visible_forms: Vec::new(),
|
||||||
active_form: None,
|
active_form: None,
|
||||||
active_control: None,
|
active_control: None,
|
||||||
|
dropdown: None,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
screen_visible: true,
|
screen_visible: true,
|
||||||
@@ -251,10 +272,6 @@ impl FormsModel {
|
|||||||
.unwrap_or(1);
|
.unwrap_or(1);
|
||||||
return Ok(PropertyValue::Integer(value.saturating_sub(2)));
|
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())
|
Ok(obj.properties[property as usize].clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,6 +437,7 @@ impl FormsModel {
|
|||||||
self.replace_selection(key, &replacement)?;
|
self.replace_selection(key, &replacement)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let changed = self.value(key, spec.name) != Some(&value);
|
||||||
let timer_reset = class == ObjectClass::Timer
|
let timer_reset = class == ObjectClass::Timer
|
||||||
&& (spec.name == "INTERVAL"
|
&& (spec.name == "INTERVAL"
|
||||||
|| (spec.name == "ENABLED" && self.value(key, "ENABLED") != Some(&value)));
|
|| (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") {
|
if spec.name == "SORTED" && self.boolean(key, "SORTED") {
|
||||||
self.sort_list(key)?;
|
self.sort_list(key)?;
|
||||||
}
|
}
|
||||||
@@ -464,6 +488,17 @@ impl FormsModel {
|
|||||||
) && matches!(spec.name, "PATH" | "DRIVE" | "PATTERN")
|
) && matches!(spec.name, "PATH" | "DRIVE" | "PATTERN")
|
||||||
{
|
{
|
||||||
self.refresh_filesystem(key)?;
|
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;
|
self.dirty = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -566,7 +601,8 @@ impl FormsModel {
|
|||||||
Self::set_visible_property(obj, true);
|
Self::set_visible_property(obj, true);
|
||||||
self.visible_forms.retain(|id| *id != object);
|
self.visible_forms.retain(|id| *id != object);
|
||||||
self.visible_forms.push(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;
|
self.dirty = true;
|
||||||
Ok(if modal {
|
Ok(if modal {
|
||||||
ShowResult::ModalWait
|
ShowResult::ModalWait
|
||||||
@@ -590,7 +626,7 @@ impl FormsModel {
|
|||||||
self.modal.pop();
|
self.modal.pop();
|
||||||
}
|
}
|
||||||
if self.active_form == Some(object) {
|
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.clear_active_control_for_form(object);
|
||||||
self.reset_form_timers(object);
|
self.reset_form_timers(object);
|
||||||
@@ -629,7 +665,7 @@ impl FormsModel {
|
|||||||
self.modal.pop();
|
self.modal.pop();
|
||||||
}
|
}
|
||||||
if self.active_form == Some(object) {
|
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.clear_active_control_for_form(object);
|
||||||
self.reset_form_timers(object);
|
self.reset_form_timers(object);
|
||||||
@@ -693,6 +729,9 @@ impl FormsModel {
|
|||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.ok_or(RuntimeError(340));
|
.ok_or(RuntimeError(340));
|
||||||
if removed.is_ok() {
|
if removed.is_ok() {
|
||||||
|
if self.dropdown == Some((base, Some(index))) {
|
||||||
|
self.dropdown = None;
|
||||||
|
}
|
||||||
self.reset_timer((base, Some(index)));
|
self.reset_timer((base, Some(index)));
|
||||||
self.lists.remove(&(base, Some(index)));
|
self.lists.remove(&(base, Some(index)));
|
||||||
}
|
}
|
||||||
@@ -745,10 +784,21 @@ impl FormsModel {
|
|||||||
.ok_or(RuntimeError(420))?
|
.ok_or(RuntimeError(420))?
|
||||||
.description
|
.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));
|
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);
|
self.active_control = Some(key);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -822,14 +872,28 @@ impl FormsModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn root_form(&self, key: ObjectKey) -> Option<u16> {
|
fn parent_key(&self, key: ObjectKey) -> Option<ObjectKey> {
|
||||||
let mut current = key.0;
|
match self.value(key, "PARENT") {
|
||||||
for _ in 0..self.objects.len() {
|
Some(PropertyValue::Object(parent)) => {
|
||||||
let obj = self.objects.get(current as usize)?;
|
parent.map(|(id, index)| (id, index.filter(|i| *i != 0)))
|
||||||
if obj.description.class == ObjectClass::Form {
|
|
||||||
return Some(current);
|
|
||||||
}
|
}
|
||||||
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
|
None
|
||||||
}
|
}
|
||||||
@@ -872,7 +936,10 @@ impl FormsModel {
|
|||||||
if let Some(old) = self.active_control {
|
if let Some(old) = self.active_control {
|
||||||
self.queue_named(old, "LOSTFOCUS", vec![]);
|
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.active_control = Some(key);
|
||||||
self.queue_named(key, "GOTFOCUS", vec![]);
|
self.queue_named(key, "GOTFOCUS", vec![]);
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
@@ -949,7 +1016,7 @@ impl FormsModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn select_option(&mut self, key: ObjectKey) -> Result<(), RuntimeError> {
|
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")
|
let value_id = forms::property(ObjectClass::OptionButton, "VALUE")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.0 as usize;
|
.0 as usize;
|
||||||
@@ -959,7 +1026,7 @@ impl FormsModel {
|
|||||||
}
|
}
|
||||||
let same_group = self.instance(other).is_ok_and(|obj| {
|
let same_group = self.instance(other).is_ok_and(|obj| {
|
||||||
obj.description.class == ObjectClass::OptionButton
|
obj.description.class == ObjectClass::OptionButton
|
||||||
&& obj.description.parent == parent
|
&& self.parent_key(other) == parent
|
||||||
});
|
});
|
||||||
if same_group {
|
if same_group {
|
||||||
self.instance_mut(other)?.properties[value_id] = PropertyValue::Integer(0);
|
self.instance_mut(other)?.properties[value_id] = PropertyValue::Integer(0);
|
||||||
@@ -994,10 +1061,8 @@ impl FormsModel {
|
|||||||
let title_shortcut = name == "SHORTCUT"
|
let title_shortcut = name == "SHORTCUT"
|
||||||
&& matches!(value, PropertyValue::String(text) if !text.is_empty())
|
&& matches!(value, PropertyValue::String(text) if !text.is_empty())
|
||||||
&& self
|
&& self
|
||||||
.instance(key)
|
.parent_key(key)
|
||||||
.ok()
|
.and_then(|parent| self.instance(parent).ok())
|
||||||
.and_then(|obj| obj.description.parent)
|
|
||||||
.and_then(|parent| self.objects.get(parent as usize))
|
|
||||||
.is_some_and(|parent| parent.description.class == ObjectClass::Form);
|
.is_some_and(|parent| parent.description.class == ObjectClass::Form);
|
||||||
if invalid || title_shortcut {
|
if invalid || title_shortcut {
|
||||||
Err(RuntimeError::ILLEGAL_FUNCTION_CALL)
|
Err(RuntimeError::ILLEGAL_FUNCTION_CALL)
|
||||||
@@ -1163,12 +1228,6 @@ impl FormsModel {
|
|||||||
}
|
}
|
||||||
entries.sort_by_key(|entry| entry.to_uppercase());
|
entries.sort_by_key(|entry| entry.to_uppercase());
|
||||||
self.lists.insert(key, entries);
|
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;
|
self.dirty = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1204,7 +1263,12 @@ impl FormsModel {
|
|||||||
let class = self.instance(key)?.description.class;
|
let class = self.instance(key)?.description.class;
|
||||||
match name {
|
match name {
|
||||||
"SETFOCUS" => self.focus(key)?,
|
"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(
|
"ADDITEM" => self.add_item(
|
||||||
key,
|
key,
|
||||||
args.first().map(Self::text_of).unwrap_or_default(),
|
args.first().map(Self::text_of).unwrap_or_default(),
|
||||||
@@ -1304,10 +1368,27 @@ impl FormsModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn form_controls(&self, form: u16) -> Vec<ObjectKey> {
|
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()
|
.into_iter()
|
||||||
.filter(|key| self.root_form(*key) == Some(form) && key.0 != form)
|
.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> {
|
fn menu_children(&self, parent: ObjectKey) -> Vec<ObjectKey> {
|
||||||
@@ -1316,7 +1397,7 @@ impl FormsModel {
|
|||||||
.filter(|key| {
|
.filter(|key| {
|
||||||
self.instance(*key).is_ok_and(|object| {
|
self.instance(*key).is_ok_and(|object| {
|
||||||
object.description.class == ObjectClass::Menu
|
object.description.class == ObjectClass::Menu
|
||||||
&& object.description.parent == Some(parent.0)
|
&& self.parent_key(*key) == Some(parent)
|
||||||
}) && self.boolean(*key, "VISIBLE")
|
}) && self.boolean(*key, "VISIBLE")
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -1370,7 +1451,7 @@ impl FormsModel {
|
|||||||
&& self.boolean(*candidate, "VISIBLE")
|
&& self.boolean(*candidate, "VISIBLE")
|
||||||
&& self.instance(*candidate).is_ok_and(|obj| {
|
&& self.instance(*candidate).is_ok_and(|obj| {
|
||||||
obj.description.class != ObjectClass::Menu
|
obj.description.class != ObjectClass::Menu
|
||||||
|| obj.description.parent == Some(form)
|
|| self.parent_key(*candidate) == Some((form, None))
|
||||||
})
|
})
|
||||||
}) {
|
}) {
|
||||||
if self
|
if self
|
||||||
@@ -1410,7 +1491,29 @@ impl FormsModel {
|
|||||||
PropertyValue::Integer(shift as i32),
|
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 {
|
if key.chars().count() == 1 {
|
||||||
self.queue_named(active, "KEYPRESS", vec![PropertyValue::Integer(code)]);
|
self.queue_named(active, "KEYPRESS", vec![PropertyValue::Integer(code)]);
|
||||||
}
|
}
|
||||||
@@ -1453,6 +1556,17 @@ impl FormsModel {
|
|||||||
PropertyValue::Integer(shift as i32),
|
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1546,19 +1660,18 @@ impl FormsModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn rect(&self, key: ObjectKey) -> Option<(usize, usize, usize, usize)> {
|
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 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 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 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 height = self.integer(key, "HEIGHT").unwrap_or(1).max(1) as usize;
|
||||||
let mut parent = obj.description.parent;
|
let mut parent = self.parent_key(key);
|
||||||
for _ in 0..self.objects.len() {
|
for _ in 0..self.objects.len() + self.dynamic.len() {
|
||||||
let Some(id) = parent else { break };
|
let Some(parent_key) = parent else { break };
|
||||||
let parent_obj = self.objects.get(id as usize)?;
|
self.instance(parent_key).ok()?;
|
||||||
let parent_key = (id, None);
|
|
||||||
left += self.integer(parent_key, "LEFT").unwrap_or(0).max(0) as usize;
|
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;
|
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))
|
Some((left, top, width, height))
|
||||||
}
|
}
|
||||||
@@ -2192,7 +2305,7 @@ impl FormsModel {
|
|||||||
format!("{text} ▼")
|
format!("{text} ▼")
|
||||||
};
|
};
|
||||||
let mut lines = vec![Self::fit(&first, width)];
|
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.extend(self.list_lines(key, width, height.saturating_sub(1)));
|
||||||
}
|
}
|
||||||
lines.truncate(height);
|
lines.truncate(height);
|
||||||
@@ -2317,12 +2430,7 @@ impl FormsModel {
|
|||||||
let Ok(class) = self.instance(key).map(|obj| obj.description.class) else {
|
let Ok(class) = self.instance(key).map(|obj| obj.description.class) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if class == ObjectClass::Menu
|
if class == ObjectClass::Menu && self.parent_key(key) == Some((form, None)) {
|
||||||
&& self
|
|
||||||
.instance(key)
|
|
||||||
.ok()
|
|
||||||
.is_some_and(|obj| obj.description.parent == Some(form))
|
|
||||||
{
|
|
||||||
if self.boolean(key, "VISIBLE") {
|
if self.boolean(key, "VISIBLE") {
|
||||||
let caption = format!(" {} ", self.caption(key));
|
let caption = format!(" {} ", self.caption(key));
|
||||||
let focused = self.menu_path.first() == Some(&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| {
|
for root in self.form_controls(form).into_iter().filter(|key| {
|
||||||
self.instance(*key).is_ok_and(|object| {
|
self.instance(*key).is_ok_and(|object| {
|
||||||
object.description.class == ObjectClass::Menu
|
object.description.class == ObjectClass::Menu
|
||||||
&& object.description.parent == Some(form)
|
&& self.parent_key(*key) == Some((form, None))
|
||||||
}) && self.boolean(*key, "VISIBLE")
|
}) && self.boolean(*key, "VISIBLE")
|
||||||
}) {
|
}) {
|
||||||
if Some(&root) == self.menu_path.first() {
|
if Some(&root) == self.menu_path.first() {
|
||||||
@@ -2749,6 +2857,90 @@ mod tests {
|
|||||||
FormsModel::new(c.objects, 80, 25)
|
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]
|
#[test]
|
||||||
fn defaults_bereich_und_implizites_laden() {
|
fn defaults_bereich_und_implizites_laden() {
|
||||||
let mut m = model();
|
let mut m = model();
|
||||||
@@ -3633,7 +3825,8 @@ mod tests {
|
|||||||
m.unload_array(2, 1).unwrap();
|
m.unload_array(2, 1).unwrap();
|
||||||
assert!(!m.events.iter().any(|e| e.array_index == Some(1)));
|
assert!(!m.events.iter().any(|e| e.array_index == Some(1)));
|
||||||
m.hide(0).unwrap();
|
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.show(0, false).unwrap();
|
||||||
m.sync_timers(|| 2000);
|
m.sync_timers(|| 2000);
|
||||||
assert_eq!(m.next_deadline(), Some(2100));
|
assert_eq!(m.next_deadline(), Some(2100));
|
||||||
|
|||||||
@@ -84,8 +84,9 @@ impl FormFile {
|
|||||||
) -> Result<Vec<FormInitial>, tb_runtime::errors::RuntimeError> {
|
) -> Result<Vec<FormInitial>, tb_runtime::errors::RuntimeError> {
|
||||||
fn collect(
|
fn collect(
|
||||||
node: &FormNode,
|
node: &FormNode,
|
||||||
parent: Option<u16>,
|
parent: Option<(u16, Option<i32>)>,
|
||||||
catalog: &FormCatalog,
|
catalog: &FormCatalog,
|
||||||
|
form: &str,
|
||||||
out: &mut Vec<FormInitial>,
|
out: &mut Vec<FormInitial>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
) -> Result<(), tb_runtime::errors::RuntimeError> {
|
) -> Result<(), tb_runtime::errors::RuntimeError> {
|
||||||
@@ -98,7 +99,8 @@ impl FormFile {
|
|||||||
.iter()
|
.iter()
|
||||||
.position(|o| {
|
.position(|o| {
|
||||||
o.name.eq_ignore_ascii_case(&node.name)
|
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
|
&& o.class == node.class
|
||||||
})
|
})
|
||||||
.ok_or(tb_runtime::errors::RuntimeError(420))? as u16;
|
.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) {
|
if out.iter().any(|v| v.object == object && v.index == index) {
|
||||||
return Err(tb_runtime::errors::RuntimeError(5));
|
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 {
|
out.push(FormInitial {
|
||||||
object,
|
object,
|
||||||
index,
|
index,
|
||||||
properties: node.properties.clone(),
|
properties,
|
||||||
});
|
});
|
||||||
for child in &node.children {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
let mut values = Vec::new();
|
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)
|
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));
|
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(
|
set_property(
|
||||||
&mut node,
|
&mut node,
|
||||||
"INDEX",
|
"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 {
|
let Some(value) = node.properties.get(&(id as u16)) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if *value == default_value(spec) {
|
if spec.name != "INDEX" && *value == default_value(spec) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
out.push_str(&" ".repeat(depth + 1));
|
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";
|
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]
|
#[test]
|
||||||
fn reads_nested_form_and_preserves_source_exactly() {
|
fn reads_nested_form_and_preserves_source_exactly() {
|
||||||
let form = read_text("test.frm", EXAMPLE).unwrap();
|
let form = read_text("test.frm", EXAMPLE).unwrap();
|
||||||
|
|||||||
@@ -2365,7 +2365,15 @@ impl<'a> Decoder<'a> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if matches!(kind, Some(1 | 2)) {
|
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.lines.push(line);
|
||||||
self.pending.drain(..marker + 2);
|
self.pending.drain(..marker + 2);
|
||||||
continue;
|
continue;
|
||||||
@@ -2604,6 +2612,21 @@ mod tests {
|
|||||||
(table, offsets)
|
(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]
|
#[test]
|
||||||
fn decodes_vbdos_declarations_without_losing_type_or_bounds() {
|
fn decodes_vbdos_declarations_without_losing_type_or_bounds() {
|
||||||
let (sym, ids) = symbols(&["PrepVal", "StringToPrep", "Oldcontents"]);
|
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 {
|
for p in &self.procs {
|
||||||
if p.module as usize >= self.modules.len()
|
if p.module as usize >= self.modules.len()
|
||||||
|| p.n_params as usize != p.params.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 {
|
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
|
.module
|
||||||
.objects
|
.objects
|
||||||
.get(event.object as usize)
|
.get(event.object as usize)
|
||||||
@@ -561,15 +573,6 @@ impl Vm {
|
|||||||
.forms
|
.forms
|
||||||
.set_active_control(event.object, event.array_index);
|
.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 {
|
if let Some(index) = event.array_index {
|
||||||
self.push(Value::Int(index as i16));
|
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!(integer(&vm, "n"), 2);
|
||||||
assert_eq!(vm.forms.next_deadline(), None);
|
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();
|
let mut duplicate = bytes.clone();
|
||||||
duplicate[24..28].copy_from_slice(b"MODN");
|
duplicate[24..28].copy_from_slice(b"MODN");
|
||||||
assert!(CompiledModule::from_tbc(&duplicate).is_err());
|
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();
|
let mut bad = CompiledModule::from_tbc(&bytes).unwrap();
|
||||||
match mutation {
|
match mutation {
|
||||||
0 => bad.sources[0].module = u16::MAX,
|
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)),
|
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!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -52,11 +52,14 @@ END SUB
|
|||||||
- Terminal Basic schreibt `VERSION` groß, Klassen- und Eigenschaftsnamen in der
|
- Terminal Basic schreibt `VERSION` groß, Klassen- und Eigenschaftsnamen in der
|
||||||
Schreibweise der Forms-Referenz und drei Leerzeichen je Blockebene.
|
Schreibweise der Forms-Referenz und drei Leerzeichen je Blockebene.
|
||||||
- Der Eigenschaftsname belegt 16 Spalten; danach folgen ` = ` und der Wert.
|
- 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
|
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
|
- Unverändert gelesene Dateien werden bytegleich zurückgegeben; für neu
|
||||||
erzeugte oder veränderte Beschreibungen gilt die kanonische Form oben.
|
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`
|
- Zeichenketten verdoppeln ein enthaltenes `"`. Wahrheitswerte werden als `-1`
|
||||||
und `0` geschrieben.
|
und `0` geschrieben.
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,23 @@ Index, Parent, Separator, Shortcut, Tag, Visible.
|
|||||||
Ereignisse u. a. Change, PathChange, PatternChange). Terminal Basic bildet
|
Ereignisse u. a. Change, PathChange, PatternChange). Terminal Basic bildet
|
||||||
sie plattformneutral nach (Laufwerksliste → Wurzeln/Mounts).
|
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
|
## Standardwerte und Wertebereiche
|
||||||
|
|
||||||
| Eigenschaft | Bereich | Default |
|
| Eigenschaft | Bereich | Default |
|
||||||
@@ -210,7 +227,8 @@ keine Zelle.
|
|||||||
Die folgenden Regeln sind **unsere Festlegung**, nicht nachgewiesenes
|
Die folgenden Regeln sind **unsere Festlegung**, nicht nachgewiesenes
|
||||||
Referenzverhalten:
|
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
|
oben. Bei Steuerelement-Arrays entscheidet bei sonst gleicher Position der
|
||||||
aufsteigende Index, sodass der höchste Index oben liegt. Gezeichnet wird von
|
aufsteigende Index, sodass der höchste Index oben liegt. Gezeichnet wird von
|
||||||
unten nach oben, die Trefferprüfung läuft umgekehrt.
|
unten nach oben, die Trefferprüfung läuft umgekehrt.
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
Sollseite der Leitplanke „Vollständigkeit ist das Soll": jedes in der
|
Sollseite der Leitplanke „Vollständigkeit ist das Soll": jedes in der
|
||||||
Original-Hilfe dokumentierte Sprachelement mit Abdeckungsstatus. Diese
|
Original-Hilfe dokumentierte Sprachelement mit Abdeckungsstatus. Diese
|
||||||
Tabelle ist Abnahmekriterium für Phase 6 und wird vom Test
|
Tabelle ist Abnahmekriterium für Phase 6 und wird vom Test
|
||||||
`inventar_stimmt_mit_code_ueberein` gegen den Code geprüft — sie kann
|
`inventar_stimmt_mit_code_ueberein` und die ausführbaren Proben in
|
||||||
nicht falsch werden, ohne dass die Testsuite rot wird.
|
`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
|
**Quellen.** `bas7advr` = Themenliste von `bas7advr.hlp` (BASIC 7
|
||||||
Advisor). `qb45advr` = Gegenprobe über `qb45advr.hlp`, nötig weil die
|
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.
|
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
|
## Umsetzungsstand: Ereigniszustellung und Hostgrenzen
|
||||||
|
|
||||||
F04, F05, F06, F07, F08, F18 und F20 sind im archivierten Change
|
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).
|
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)
|
### 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).
|
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)
|
### 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).
|
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)
|
### 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).
|
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
|
## 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 |
|
| 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 |
|
| 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 |
|
| 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
|
## 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
|
### 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
|
### 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
|
### 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
|
### 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
|
## 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.
|
- 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.
|
- 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
|
## Migration Plan
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
## Why
|
## 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
|
## 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
|
geänderten Beschreibung SHALL nur geschrieben werden, was vom
|
||||||
Vorgabewert abweicht. Für diese kanonische Ausgabe SHALL Reihenfolge
|
Vorgabewert abweicht. Für diese kanonische Ausgabe SHALL Reihenfolge
|
||||||
und Einrückung festgelegt und dokumentiert sein, damit zwei Läufe
|
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
|
#### Scenario: Rundlauf
|
||||||
- **WHEN** eine Formulardatei gelesen und unverändert wieder geschrieben wird
|
- **WHEN** eine Formulardatei gelesen und unverändert wieder geschrieben wird
|
||||||
@@ -20,3 +22,7 @@ dieselbe Datei erzeugen.
|
|||||||
#### Scenario: Expliziter Default im unveränderten Original
|
#### Scenario: Expliziter Default im unveränderten Original
|
||||||
- **WHEN** eine gelesene Datei einen Vorgabewert ausdrücklich enthält und unverändert geschrieben wird
|
- **WHEN** eine gelesene Datei einen Vorgabewert ausdrücklich enthält und unverändert geschrieben wird
|
||||||
- **THEN** bleibt auch diese Eigenschaftszeile bytegleich erhalten
|
- **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. 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. 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.
|
- [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.
|
||||||
- [ ] 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.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.
|
- [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.
|
||||||
- [ ] 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.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. 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.
|
- [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.
|
||||||
- [ ] 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.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`
|
- **THEN** wird sie abgewiesen und die Meldung nennt `3.00`
|
||||||
|
|
||||||
### Requirement: Schreiben ist die Umkehrung des Lesens
|
### Requirement: Schreiben ist die Umkehrung des Lesens
|
||||||
Das Schreiben einer gelesenen Formularbeschreibung SHALL dieselbe Datei
|
Das unveränderte Schreiben einer gelesenen Formularbeschreibung SHALL
|
||||||
ergeben. Geschrieben SHALL nur werden, was vom Vorgabewert abweicht;
|
die Originaldatei bytegleich erhalten, einschließlich explizit
|
||||||
Reihenfolge und Einrückung SHALL festgelegt und dokumentiert sein, damit
|
geschriebener Vorgabewerte. Beim kanonischen Schreiben einer neuen oder
|
||||||
zwei Läufe dieselbe Datei erzeugen.
|
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
|
#### Scenario: Rundlauf
|
||||||
- **WHEN** eine Formulardatei gelesen und unverändert wieder geschrieben wird
|
- **WHEN** eine Formulardatei gelesen und unverändert wieder geschrieben wird
|
||||||
- **THEN** ist die geschriebene Datei byte-gleich zur gelesenen
|
- **THEN** ist die geschriebene Datei byte-gleich zur gelesenen
|
||||||
|
|
||||||
#### Scenario: Vorgabewerte werden nicht geschrieben
|
#### 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
|
- **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
|
### Requirement: Fehlerhafte Dateien werden benannt
|
||||||
Eine unbekannte Klasse, eine für die Klasse unbekannte Eigenschaft, ein
|
Eine unbekannte Klasse, eine für die Klasse unbekannte Eigenschaft, ein
|
||||||
Wert außerhalb des Wertebereichs und ein unausgeglichener Block MUST je
|
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
|
Zeitverlauf im Programmkopf deklarieren; ohne Deklaration MUST der
|
||||||
Harness den Lauf abweisen statt auf die Systemuhr auszuweichen.
|
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
|
#### Scenario: Zeit-Trap im Korpus
|
||||||
- **WHEN** ein Korpusprogramm mit deklariertem Zeitverlauf und `ON TIMER` ausgeführt wird
|
- **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
|
- **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
|
- **WHEN** ein Korpusprogramm zeitgesteuerte Traps nutzt, aber keinen Zeitverlauf deklariert
|
||||||
- **THEN** schlägt der Harness mit einer Meldung fehl, die die fehlende Deklaration nennt
|
- **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
|
### Requirement: Korpusabdeckung der Formularprogramme
|
||||||
Der Testkorpus SHALL Formularprogramme mit byte-genauer
|
Der Testkorpus SHALL Formularprogramme mit byte-genauer
|
||||||
Bildschirm-Sollausgabe führen. Ihre Ereignisfolge (Tasten, Maus, Zeit)
|
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
|
Tabreihenfolge, Access-Key, Klick über die Maus, Menüauswahl, ein
|
||||||
Listen-Steuerelement, ein Timer und ein modaler Dialog.
|
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
|
#### Scenario: Formularprogramm im Korpus
|
||||||
- **WHEN** ein Formular-Korpusprogramm mit deklarierter Ereignisfolge ausgeführt wird
|
- **WHEN** ein Formular-Korpusprogramm mit deklarierter Ereignisfolge ausgeführt wird
|
||||||
- **THEN** entspricht der Bildschirminhalt byte-genau der Sollausgabe
|
- **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
|
- **WHEN** dasselbe Programm zweimal ausgeführt wird
|
||||||
- **THEN** ist die Ausgabe beide Male identisch
|
- **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
|
### Requirement: Kompatibilitätsnachweis an Fremdprogrammen
|
||||||
Die Formularprogramme aus dem öffentlichen Bestand des Vorbilds SHALL
|
Die Formularprogramme aus dem öffentlichen Bestand des Vorbilds SHALL
|
||||||
sich ohne Diagnosen oder Non-Features übersetzen lassen und bedienbar
|
sich ohne Diagnosen oder Non-Features übersetzen lassen und bedienbar
|
||||||
sein. Jeder entdeckte fehlende Sprach-, Projekt-, Formular- oder
|
sein. Jeder entdeckte fehlende Sprach-, Projekt-, Formular- oder
|
||||||
Laufzeitpfad MUST implementiert und als Befund festgehalten werden.
|
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
|
#### Scenario: Fremdprogramm übersetzt
|
||||||
- **WHEN** ein Formularprogramm des öffentlichen Bestands übersetzt wird
|
- **WHEN** ein Formularprogramm des öffentlichen Bestands übersetzt wird
|
||||||
- **THEN** entstehen keine Diagnosen und das Programm ist bedienbar
|
- **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
|
Die Semantik MUST jede Verwendung der in docs/sprachreferenz.md
|
||||||
(„Abweichungen") deklarierten Non-Features — Hardware-Nähe (`PEEK`,
|
(„Abweichungen") deklarierten Non-Features — Hardware-Nähe (`PEEK`,
|
||||||
`POKE`, `INP`, `OUT`, `WAIT`, `BLOAD`/`BSAVE`, `VARPTR`-Familie, `CALL
|
`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
|
`PLAY` und die Event-Geräte `COM`/`PEN`/`PLAY`/`STRIG` — zur Compile-Zeit
|
||||||
mit der Meldung „Feature unavailable" abweisen. Dokumentierte Features
|
mit der Meldung „Feature unavailable" abweisen. Dokumentierte Features
|
||||||
außerhalb dieser Liste MUST NOT abgewiesen werden (Guiding Principle).
|
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
|
#### Scenario: Hardware-Zugriff
|
||||||
- **WHEN** `POKE 100, 1` oder `x = PEEK(100)` kompiliert wird
|
- **WHEN** `POKE 100, 1` oder `x = PEEK(100)` kompiliert wird
|
||||||
- **THEN** erscheint die Compile-Diagnose „Feature unavailable"
|
- **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
|
### Requirement: Diagnostik mit Vorbild-Meldungen
|
||||||
Diagnosen SHALL exakte 1-basierte Quelltextpositionen (Zeile, Spalte)
|
Diagnosen SHALL exakte 1-basierte Quelltextpositionen (Zeile, Spalte)
|
||||||
tragen; die Meldungstexte SHALL dem Katalog des Vorbilds folgen, wo eine
|
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
|
divergierende Element, seinen Inventarstatus und den vorgefundenen
|
||||||
Zustand benennen.
|
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
|
#### Scenario: Inventar hinkt der Implementierung hinterher
|
||||||
- **WHEN** `LOCATE` implementiert wurde, im Inventar aber noch `offen` steht
|
- **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"
|
- **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
|
- **WHEN** der Abgleich läuft
|
||||||
- **THEN** nennt er die Zahl der Einträge je Status als Abdeckungsstand
|
- **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
|
### Requirement: Keine stillen Lücken bei dokumentierten Elementen
|
||||||
Ein Element mit Status `offen` MUST bei Verwendung eine benannte
|
Ein Element mit Status `offen` MUST bei Verwendung eine benannte
|
||||||
Ablehnung erzeugen — entweder eine Compile-Diagnose oder den Laufzeitfehler
|
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
|
||||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
||||||
70700707070707070707070707070707070707077070700707070707070707070770707070707070707070707070
|
7070070707070707070707070707070707070707707070f1f1f1f1f1f1f1f1f1f170707070707070707070707070
|
||||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
||||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
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