Phase 4: Formularmodell und Objektsprache

This commit is contained in:
2026-09-04 17:30:32 +02:00
parent 5cf5a6582c
commit 54488b5b67
35 changed files with 7195 additions and 627 deletions

View File

@@ -99,7 +99,10 @@ fn gemischte_arithmetik() {
#[test]
fn logik_bitweise() {
assert_eq!(out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"), " 2 7 5 -1 \n");
assert_eq!(
out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"),
" 2 7 5 -1 \n"
);
// Operanden werden gerundet: 1.5 AND 1 → 2 AND 1 = 0
assert_eq!(out("PRINT 1.5 AND 1"), " 0 \n");
}
@@ -122,12 +125,18 @@ fn stringvergleich_und_verkettung() {
#[test]
fn for_ohne_durchlauf() {
// Spec-Szenario: FOR i% = 3 TO 1 → Körper wird nicht betreten
assert_eq!(out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""), "ende\n");
assert_eq!(
out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""),
"ende\n"
);
}
#[test]
fn for_mit_negativem_step() {
assert_eq!(out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"), " 3 2 1 \n");
assert_eq!(
out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"),
" 3 2 1 \n"
);
}
#[test]
@@ -172,11 +181,13 @@ fn exit_for_und_do() {
fn kontrollfluss_korpusdatei() {
// 5.2-Verifikation: kontrollfluss.bas byte-genau korrekt.
let src = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.bas"),
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/compat/kontrollfluss.bas"),
)
.unwrap();
let want = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.out"),
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/compat/kontrollfluss.out"),
)
.unwrap();
assert_eq!(out(&src), want);
@@ -268,7 +279,8 @@ fn data_read_restore() {
// Anmerkung: unquotierte DATA-Texte verlieren derzeit die
// Groß-/Kleinschreibung (Lexer normalisiert Bezeichner);
// Rohtext-Erhalt ist als Aufgabe in PLAN.md Phase 3 eingeplant.
let src = "DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
let src =
"DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%";
assert_eq!(out(src), " 1 2.5 hallo\n 1 \n");
}
@@ -444,10 +456,7 @@ fn end_und_system() {
#[test]
fn input_mit_redo() {
let (ev, output) = run_with_input(
"INPUT \"Zahl\"; n%\nPRINT n% * 2",
&["abc", "21"],
);
let (ev, output) = run_with_input("INPUT \"Zahl\"; n%\nPRINT n% * 2", &["abc", "21"]);
assert_eq!(ev, RunEvent::Ended);
assert!(output.contains("Redo from start"));
assert!(output.ends_with(" 42 \n"), "{output}");
@@ -464,7 +473,10 @@ fn line_input_liest_ganze_zeile() {
#[test]
fn mid_anweisung_mutiert() {
assert_eq!(out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"), "hEYlo\n");
assert_eq!(
out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"),
"hEYlo\n"
);
}
#[test]
@@ -495,12 +507,10 @@ fn unsupported_feature_fehler_73() {
#[test]
fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
let _dir = TempVerzeichnis::neu("open_isam");
let (ev, ausgabe) = run(
"TYPE T\n f AS INTEGER\nEND TYPE\n\
let (ev, ausgabe) = run("TYPE T\n f AS INTEGER\nEND TYPE\n\
OPEN \"db.isam\" FOR ISAM T \"Tab\" AS #1\n\
PRINT \"offen\"\n\
CLOSE #1",
);
CLOSE #1");
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
assert!(ausgabe.contains("offen"), "{ausgabe}");
}
@@ -552,7 +562,10 @@ fn groessenaenderung_wirkt_zur_laufzeit() {
let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nFOR i% = 1 TO 50\nNEXT i%\n").unwrap();
let mut vm = Vm::new(module);
let mut host = CaptureHost::default();
host.ereignis(Ereignis::Groesse { cols: 120, rows: 40 });
host.ereignis(Ereignis::Groesse {
cols: 120,
rows: 40,
});
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert_eq!((vm.rt.screen.cols(), vm.rt.screen.rows()), (120, 40));
// Inhalt bleibt oben links erhalten.
@@ -639,10 +652,7 @@ fn cls_setzt_cursor_zurueck() {
#[test]
fn width_hebt_die_groesse_an() {
assert_eq!(
out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"),
" 32 \n"
);
assert_eq!(out("WIDTH 120, 40\nPRINT SCREEN(40, 120)"), " 32 \n");
}
// ---- ON ERROR-Scoping (Spec sprach-frontend) -------------------------------
@@ -736,8 +746,14 @@ fn time_und_now_stimmen_ueberein() {
#[test]
fn fester_versatz_verschiebt_die_uhr() {
let src = "PRINT INT(TIMER)";
let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt).trim().parse().unwrap();
let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600)).trim().parse().unwrap();
let ohne: f64 = out_mit_zone(src, Zeitzone::Unbekannt)
.trim()
.parse()
.unwrap();
let mit: f64 = out_mit_zone(src, Zeitzone::Fest(3600))
.trim()
.parse()
.unwrap();
// Modulo Tageslänge, damit ein Mitternachtsübergang nichts kaputt macht.
let diff = (mit - ohne).rem_euclid(86_400.0);
assert!((diff - 3600.0).abs() <= 2.0, "Differenz {diff}");
@@ -746,7 +762,10 @@ fn fester_versatz_verschiebt_die_uhr() {
/// `TIMER` zählt ab der lokalen Mitternacht, nicht ab der UTC-Mitternacht.
#[test]
fn timer_zaehlt_ab_lokaler_mitternacht() {
let t = out_mit_zone("PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))", Zeitzone::Fest(-18000));
let t = out_mit_zone(
"PRINT INT(TIMER / 3600); VAL(LEFT$(TIME$, 2))",
Zeitzone::Fest(-18000),
);
let zahlen: Vec<f64> = t
.split_whitespace()
.filter_map(|w| w.parse().ok())
@@ -1093,3 +1112,169 @@ fn erl_nennt_die_fehlerzeile_nicht_die_handlerzeile() {
110 RESUME NEXT\n");
assert_eq!(out.trim(), "20");
}
fn forms_catalog() -> tb_frontend::forms::FormCatalog {
use tb_frontend::forms::ObjectClass;
let mut c = tb_frontend::forms::FormCatalog::default();
c.add("Form1", ObjectClass::Form, None, false);
c.add("Text1", ObjectClass::TextBox, Some("Form1"), false);
c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true);
c
}
fn form_vm(src: &str) -> Vm {
let module = tb_vm::compile_source_with_forms("FORM1", src, &forms_catalog())
.unwrap_or_else(|d| panic!("Compile-Fehler: {d:?}"));
Vm::new(module)
}
#[test]
fn objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex() {
let mut vm = form_vm("Form1!Text1.Text = \"hallo\"\nPRINT Form1!Text1.Text");
let ev = vm.run(&mut CaptureHost::default());
assert_eq!(ev, RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "hallo");
}
#[test]
fn steuerarray_eigenschaften_und_objektparameter_funktionieren() {
let mut vm = form_vm(
"LOAD Command1(3)\nCommand1(3).Caption = \"drei\"\nPRINT Command1(3).Caption\n\
CALL Aus(Text1)\nPRINT Text1.Visible\nCALL FormAus(Form1)\nPRINT Form1.Caption\nEND\n\
SUB Aus(c AS CONTROL)\nPRINT TYPEOF c IS TextBox\nc.Visible = 0\nEND SUB\n\
SUB FormAus(f AS FORM)\nPRINT TYPEOF f IS Form\nf.Caption = \"Formular\"\nEND SUB",
);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(
tb_runtime::snapshot::text(&vm.rt.screen).trim(),
"drei\n-1 \n 0 \n-1 \nFormular"
);
}
#[test]
fn modales_show_setzt_nach_unload_fort() {
let mut vm = form_vm(
"DIM SHARED gesehen%\nForm1.Show 1\nPRINT gesehen%\nEND\n\
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
SHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
);
let mut host = CaptureHost::default();
host.ereignis_nach(
2,
tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis {
art: tb_runtime::host::MausArt::Druck,
taste: 1,
shift: 0,
zeile: 1,
spalte: 1,
}),
);
let ev = vm.run(&mut host);
assert_eq!(ev, RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "1");
assert!(host.presents >= 2);
}
#[test]
fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() {
let mut vm = form_vm("Form1.Show\nIF TYPEOF Text1 IS TextBox THEN PRINT \"ja\"");
vm.queue_form_event(tb_ui::forms::FormEvent {
object: 0,
array_index: None,
name: "CLICK".into(),
args: vec![],
});
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "ja");
}
#[test]
fn mausargumente_kommen_in_zellen_an() {
use tb_runtime::host::{Ereignis, MausArt, MausEreignis};
let mut vm = form_vm(
"DIM SHARED b%, s%, gx!, gy!\nForm1.Show\nSTOP\nDOEVENTS\nEND\n\
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
SHARED b%, s%, gx!, gy!\nb% = Button: s% = Shift: gx! = X: gy! = Y\nEND SUB",
);
let mut host = CaptureHost::default();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
host.ereignis(Ereignis::Maus(MausEreignis {
art: MausArt::Druck,
taste: 1,
shift: 2,
zeile: 5,
spalte: 7,
}));
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("b"), Some(Value::Int(1))));
assert!(matches!(vm.inspect("s"), Some(Value::Int(2))));
assert!(matches!(vm.inspect("gx"), Some(Value::Sng(7.0))));
assert!(matches!(vm.inspect("gy"), Some(Value::Sng(5.0))));
}
#[test]
fn form_load_laeuft_vor_dem_sichtbarsetzen() {
let mut vm = form_vm(
"Form1.Show\nEND\n\
SUB Form_Load()\nPRINT Form1.Visible\nEND SUB",
);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "0");
}
#[test]
fn steuerarray_ereignis_stellt_index_voran() {
let mut vm = form_vm(
"DIM SHARED seen%\nSTOP\nDOEVENTS\nEND\n\
SUB Command1_Click(Index AS INTEGER)\nSHARED seen%\nseen% = Index\nEND SUB",
);
let mut host = CaptureHost::default();
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
vm.forms.load_array(2, 3).unwrap();
vm.queue_form_event(tb_ui::forms::FormEvent {
object: 2,
array_index: Some(3),
name: "CLICK".into(),
args: vec![],
});
assert_eq!(vm.run(&mut host), RunEvent::Ended);
assert!(matches!(vm.inspect("seen"), Some(Value::Int(3))));
let active =
tb_frontend::forms::property(tb_frontend::forms::ObjectClass::Screen, "ACTIVECONTROL")
.unwrap()
.0;
assert_eq!(
vm.forms.get(3, active).unwrap(),
tb_ui::forms::PropertyValue::Object(Some((2, Some(3))))
);
}
#[test]
fn form_load_laeuft_einmal_und_unload_cancel_verhindert_entladen() {
let mut vm = form_vm(
"DIM SHARED loads%\nx$ = Form1.Caption\nForm1.Hide\nx$ = Form1.Caption\n\
Form1.Show\nUNLOAD Form1\nEND\n\
SUB Form_Load()\nSHARED loads%\nloads% = loads% + 1\nEND SUB\n\
SUB Form_Unload(Cancel AS INTEGER)\nCancel = 1\nEND SUB",
);
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
assert!(matches!(vm.inspect("loads"), Some(Value::Int(1))));
assert!(vm.forms.objects[0].loaded);
assert!(vm.forms.objects[0].visible);
}
#[test]
fn objektcode_erzeugt_keine_zusaetzlichen_zustellopcodes() {
let module = tb_vm::compile_source_with_forms(
"FORM1",
"Form1.Caption = \"x\"\nDOEVENTS\nx$ = Form1.Caption",
&forms_catalog(),
)
.unwrap();
let n = module.procs[0]
.code
.iter()
.filter(|i| matches!(i, tb_vm::bytecode::Instr::Doevents))
.count();
assert_eq!(n, 1, "nur das explizite DOEVENTS ist ein Zustellopcode");
}