Projektmodule und vollständiges TBC-Kompilat umsetzen und Change archivieren
This commit is contained in:
299
crates/tb-vm/tests/project.rs
Normal file
299
crates/tb-vm/tests/project.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use tb_frontend::{
|
||||
forms::FormCatalog,
|
||||
source::{SourceSegment, SourceUnit},
|
||||
};
|
||||
use tb_runtime::{host::CaptureHost, value::Value};
|
||||
use tb_ui::frm::{self, FormFile};
|
||||
use tb_vm::{
|
||||
bytecode::CompiledModule,
|
||||
compile_project,
|
||||
interp::{RunEvent, Vm},
|
||||
};
|
||||
|
||||
fn compile(units: &[SourceUnit], forms: &[FormFile]) -> CompiledModule {
|
||||
let mut catalog = FormCatalog::default();
|
||||
for form in forms {
|
||||
catalog.append(&form.catalog());
|
||||
}
|
||||
compile_project("APP", units, &catalog, forms).unwrap_or_else(|d| panic!("{d:#?}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_includes_erl_breakpoints_und_inspektion_behalten_ihren_ursprung() {
|
||||
let a = SourceUnit::new("MAIN", "main.bas", "x%=1\nCALL Fehler\nEND\n");
|
||||
let b = SourceUnit {
|
||||
name: "LIB".into(),
|
||||
segments: vec![
|
||||
SourceSegment {
|
||||
file: "lib.bas".into(),
|
||||
first_line: 1,
|
||||
text: "SUB Fehler\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "nested.bi".into(),
|
||||
first_line: 2,
|
||||
text: "200 ERROR 6\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "lib.bas".into(),
|
||||
first_line: 3,
|
||||
text: "END SUB\n".into(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let module = compile(&[a, b], &[]);
|
||||
let bytes = module.to_tbc();
|
||||
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(loaded.to_tbc(), bytes);
|
||||
let mut vm = Vm::new(loaded);
|
||||
let mut host = CaptureHost::default();
|
||||
vm.add_module_breakpoint(1, 2);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
|
||||
assert_eq!(vm.current_module(), 1);
|
||||
assert_eq!(vm.current_file(), "nested.bi");
|
||||
assert_eq!(vm.current_source_pos().column, 1);
|
||||
assert!(matches!(vm.inspect("MAIN!x"), Some(Value::Int(1))));
|
||||
vm.remove_module_breakpoint(1, 2);
|
||||
vm.set_step(true);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 2 });
|
||||
assert_eq!(vm.current_file(), "nested.bi");
|
||||
assert_eq!(vm.current_source_pos().column, 5);
|
||||
vm.set_step(false);
|
||||
assert!(matches!(
|
||||
vm.run(&mut host),
|
||||
RunEvent::Error {
|
||||
code: 6,
|
||||
line: 2,
|
||||
..
|
||||
}
|
||||
));
|
||||
let mut erl_vm = Vm::new(compile(
|
||||
&[SourceUnit::new(
|
||||
"NUM",
|
||||
"num.bas",
|
||||
"ON ERROR RESUME NEXT\n200 ERROR 6\nn%=ERL\nSTOP",
|
||||
)],
|
||||
&[],
|
||||
));
|
||||
assert!(matches!(erl_vm.run(&mut host), RunEvent::Stopped { .. }));
|
||||
assert!(matches!(erl_vm.inspect("n"), Some(Value::Int(200))));
|
||||
assert_eq!(vm.current_file(), "nested.bi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnose_im_include_und_echte_duplikate_bleiben_sichtbar() {
|
||||
let source = SourceUnit {
|
||||
name: "LIB".into(),
|
||||
segments: vec![SourceSegment {
|
||||
file: "nested.bi".into(),
|
||||
first_line: 2,
|
||||
text: "Text$ = 42\n".into(),
|
||||
}],
|
||||
};
|
||||
let errors = compile_project("APP", &[source], &FormCatalog::default(), &[]).unwrap_err();
|
||||
assert!(errors
|
||||
.iter()
|
||||
.any(|e| e.file.as_deref() == Some("nested.bi") && e.pos.line == 2 && e.pos.column == 1));
|
||||
for text in [
|
||||
"SUB X\nEND SUB\nSUB X\nEND SUB",
|
||||
"CONST N=1\nCONST N=2",
|
||||
"TYPE T\nx AS INTEGER\nEND TYPE\nTYPE T\nx AS INTEGER\nEND TYPE",
|
||||
] {
|
||||
let errors = compile_project(
|
||||
"APP",
|
||||
&[SourceUnit::new("M", "m.bas", text)],
|
||||
&FormCatalog::default(),
|
||||
&[],
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|e| e.message.contains("Duplicate definition")),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialisierungen_def_fn_und_modulhandler_haben_eigene_quellorte() {
|
||||
for (source, expected_line) in [
|
||||
("' Bibliothek\nDIM a%(1 TO 0)", 2),
|
||||
(
|
||||
"' Bibliothek\nDEF FNkaputt(x)=1/x\nSUB Fehler\nPRINT FNkaputt(0)\nEND SUB",
|
||||
2,
|
||||
),
|
||||
] {
|
||||
let module = compile(
|
||||
&[
|
||||
SourceUnit::new("A", "a.bas", "CALL Fehler\nEND"),
|
||||
SourceUnit::new(
|
||||
"B",
|
||||
"b.bas",
|
||||
&format!(
|
||||
"{source}\n{}",
|
||||
if source.contains("SUB") {
|
||||
""
|
||||
} else {
|
||||
"SUB Fehler\nEND SUB"
|
||||
}
|
||||
),
|
||||
),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
|
||||
let event = vm.run(&mut CaptureHost::default());
|
||||
assert!(
|
||||
matches!(event, RunEvent::Error { line, .. } if line == expected_line),
|
||||
"{event:?}"
|
||||
);
|
||||
assert_eq!(vm.current_file(), "b.bas");
|
||||
}
|
||||
let units = [
|
||||
SourceUnit::new(
|
||||
"A",
|
||||
"a.bas",
|
||||
"ON ERROR GOTO H\nCALL Fehler\nEND\nH:\nPRINT ERR;ERL\nEND",
|
||||
),
|
||||
SourceUnit::new(
|
||||
"B",
|
||||
"b.bas",
|
||||
"SUB Fehler\nON ERROR GOTO 0\n200 ERROR 6\nEND SUB",
|
||||
),
|
||||
];
|
||||
let mut vm = Vm::new(compile(&units, &[]));
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 6 200 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lokale_prozeduren_shared_common_und_wiederholte_includes_bleiben_getrennt() {
|
||||
let a = SourceUnit::new("A", "a.bas", "CONST N=1\nDIM SHARED x%\nCOMMON SHARED c%\nx%=10\nCALL Privat\nCALL Zweites\nPRINT x%;c%\nEND\nSUB Privat\nx%=x%+N\nc%=c%+1\nEND SUB");
|
||||
let b = SourceUnit::new("B", "b.bas", "CONST N=2\nDIM SHARED x%\nCOMMON SHARED c%\nSUB Zweites\nx%=20\nCALL Privat\nPRINT x%\nEND SUB\nSUB Privat\nx%=x%+N\nc%=c%+2\nEND SUB");
|
||||
let mut vm = Vm::new(compile(&[a, b], &[]));
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 22 \n 11 3 \n");
|
||||
let include = SourceSegment {
|
||||
file: "shared.bi".into(),
|
||||
first_line: 1,
|
||||
text: "CONST N=7\n".into(),
|
||||
};
|
||||
let units: Vec<_> = ["A", "B"]
|
||||
.iter()
|
||||
.map(|name| SourceUnit {
|
||||
name: (*name).into(),
|
||||
segments: vec![
|
||||
include.clone(),
|
||||
SourceSegment {
|
||||
file: format!("{name}.bas"),
|
||||
first_line: 2,
|
||||
text: "PRINT N\n".into(),
|
||||
},
|
||||
],
|
||||
})
|
||||
.collect();
|
||||
let mut vm = Vm::new(compile(&units, &[]));
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), " 7 \n 7 \n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typreferenzen_signaturen_und_option_base_ueber_modulgrenzen() {
|
||||
use tb_frontend::hir::{HProcKind, HTy, NumTy};
|
||||
use tb_vm::bytecode::Instr;
|
||||
let units = [
|
||||
SourceUnit::new("A", "a.bas", "DECLARE SUB CmnDlgRegister(ok AS INTEGER)\nTYPE Klein\nx AS INTEGER\nEND TYPE\nDIM s AS Gross\nDIM a%(2)\ns.x=4\na%(1)=3\nCmnDlgRegister ok%\nPRINT ok%\nPRINT Summe%(a%(), s, 2)\nPRINT s.x\nCALL Basis\nEND"),
|
||||
SourceUnit::new("B", "b.bas", "OPTION BASE 1\nTYPE Gross\nx AS LONG\ny AS STRING * 8\nEND TYPE\nFUNCTION Summe%(a%(), s AS Gross, n%)\nSumme%=a%(1)+s.x+n%\ns.x=s.x+1\nEND FUNCTION\nSUB Basis\nb%(1)=1\nPRINT LBOUND(b%)\nEND SUB\nSUB Datei\nOPEN \"unbenutzt.isam\" FOR ISAM Gross \"T\" AS #1\nCLOSE #1\nEND SUB"),
|
||||
];
|
||||
let module = compile(&units, &[]);
|
||||
let bytes = module.to_tbc();
|
||||
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(bytes, loaded.to_tbc());
|
||||
let signature = loaded.procs.iter().find(|p| p.name == "B!SUMME").unwrap();
|
||||
assert_eq!(signature.kind, HProcKind::Function);
|
||||
assert_eq!(signature.ret_ty, Some(HTy::Num(NumTy::Int)));
|
||||
assert!(signature.params[0].array && !signature.params[0].by_ref);
|
||||
assert!(signature.params[2].by_ref);
|
||||
assert!(signature.params[1].by_ref);
|
||||
let HTy::Udt(udt) = signature.params[1].ty else {
|
||||
panic!("UDT-Parameter fehlt")
|
||||
};
|
||||
assert_eq!(loaded.udts[udt as usize].name, "GROSS");
|
||||
let file = loaded.procs.iter().find(|p| p.name == "B!DATEI").unwrap();
|
||||
assert!(file.code.contains(&Instr::PushUdtId(udt)));
|
||||
let mut vm = Vm::new(loaded);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(
|
||||
tb_runtime::snapshot::text(&vm.rt.screen),
|
||||
"-1 \n 9 \n 5 \n 1 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beschaedigte_container_werden_vor_der_ausfuehrung_abgewiesen() {
|
||||
use tb_vm::bytecode::Instr;
|
||||
let form = frm::read_text("f.frm", "VERSION 1.00\nBEGIN Form F\n BEGIN TextBox Text1\n Index = 2\n Text = \"hello\"\n END\nEND\n").unwrap();
|
||||
let bytes = compile(&[SourceUnit::new("F", "f.frm", "END")], &[form]).to_tbc();
|
||||
for length in 0..bytes.len() {
|
||||
assert!(
|
||||
CompiledModule::from_tbc(&bytes[..length]).is_err(),
|
||||
"Länge {length}"
|
||||
);
|
||||
}
|
||||
for (offset, value) in [(6, 1u32), (8, u32::MAX), (16, 0), (20, u32::MAX)] {
|
||||
let mut bad = bytes.clone();
|
||||
bad[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
|
||||
assert!(CompiledModule::from_tbc(&bad).is_err(), "Offset {offset}");
|
||||
}
|
||||
let mut duplicate = bytes.clone();
|
||||
duplicate[24..28].copy_from_slice(b"MODN");
|
||||
assert!(CompiledModule::from_tbc(&duplicate).is_err());
|
||||
for mutation in 0..8 {
|
||||
let mut bad = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
match mutation {
|
||||
0 => bad.sources[0].module = u16::MAX,
|
||||
1 => bad.procs[0].module = u16::MAX,
|
||||
2 => bad.procs[0].code.push(Instr::Call(u16::MAX, 0)),
|
||||
3 => bad.procs[0].n_params = 1,
|
||||
4 => bad.objects[1].parent = Some(1),
|
||||
5 => bad.form_initial[1].index = 3,
|
||||
6 => {
|
||||
bad.form_initial[1].properties.insert(
|
||||
0,
|
||||
tb_ui::forms::PropertyValue::Object(Some((u16::MAX, None))),
|
||||
);
|
||||
}
|
||||
7 => bad.procs[0].code.push(Instr::Source(u32::MAX, 1)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert!(
|
||||
CompiledModule::from_tbc(&bad.to_tbc()).is_err(),
|
||||
"Mutation {mutation}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zwei_formulare_mit_gleichen_controls_und_arrays_laufen_aus_dem_kompilat() {
|
||||
let form = |name: &str, text: &str| {
|
||||
frm::read_text(&format!("{name}.frm"), &format!(
|
||||
"VERSION 1.00\nBEGIN Form {name}\n Width = 30\n Height = 10\n BEGIN Frame Frame1\n BEGIN TextBox Text1\n Text = \"{text}\"\n END\n END\n BEGIN TextBox Feld\n Index = 0\n Text = \"null\"\n END\n BEGIN TextBox Feld\n Index = 2\n Text = \"zwei\"\n END\nEND\n\nSUB Form_Load\nText1.Text = Text1.Text + \"!\"\nEND SUB\n"
|
||||
)).unwrap()
|
||||
};
|
||||
let forms = [form("Form1", "a"), form("Form2", "b")];
|
||||
let mut units = vec![SourceUnit::new("MAIN", "main.bas", "Form2.Show\na$=Form1!Text1.Text\nb$=Form2!Text1.Text\nc$=Form2!Feld(2).Text\nForm1.Hide\nForm2.Hide\nCLS\nPRINT a$\nPRINT b$\nPRINT c$\nEND")];
|
||||
units.extend(
|
||||
forms
|
||||
.iter()
|
||||
.map(|f| SourceUnit::new(&f.root.name, &format!("{}.frm", f.root.name), &f.code)),
|
||||
);
|
||||
let module = compile(&units, &forms);
|
||||
assert_eq!(module.event_procs.len(), 2);
|
||||
let bytes = module.to_tbc();
|
||||
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(loaded.to_tbc(), bytes);
|
||||
let mut vm = Vm::new(loaded);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "a!\nb!\nzwei\n");
|
||||
}
|
||||
548
crates/tb-vm/tests/project_regressions.rs
Normal file
548
crates/tb-vm/tests/project_regressions.rs
Normal file
@@ -0,0 +1,548 @@
|
||||
//! Unveränderte ursprüngliche Reviewproben und ergänzende Regressionen des Projektkompilats.
|
||||
use tb_frontend::{
|
||||
forms::FormCatalog,
|
||||
source::{SourceSegment, SourceUnit},
|
||||
};
|
||||
use tb_runtime::{host::CaptureHost, value::Value};
|
||||
use tb_vm::{
|
||||
bytecode::CompiledModule,
|
||||
compile_project,
|
||||
interp::{RunEvent, Vm},
|
||||
};
|
||||
|
||||
fn units(files: &[(&str, &str)]) -> Vec<SourceUnit> {
|
||||
files
|
||||
.iter()
|
||||
.map(|(name, code)| SourceUnit::new(name, &format!("{name}.bas"), code))
|
||||
.collect()
|
||||
}
|
||||
fn compile(files: &[(&str, &str)]) -> Result<CompiledModule, Vec<tb_frontend::Diagnostic>> {
|
||||
compile_project("APP", &units(files), &FormCatalog::default(), &[])
|
||||
}
|
||||
fn run(module: CompiledModule) -> (RunEvent, String) {
|
||||
let bytes = module.to_tbc();
|
||||
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(bytes, loaded.to_tbc());
|
||||
let mut vm = Vm::new(loaded);
|
||||
let event = vm.run(&mut CaptureHost::default());
|
||||
(event, tb_runtime::snapshot::text(&vm.rt.screen))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1_zweige_und_schleifen_behalten_eigene_quellorte() {
|
||||
let mut failures = vec![];
|
||||
for (name, code, line) in [
|
||||
(
|
||||
"ELSEIF",
|
||||
"IF 0 THEN\nPRINT 1\nELSEIF 1/0 THEN\nPRINT 2\nEND IF",
|
||||
3,
|
||||
),
|
||||
("CASE", "SELECT CASE 1\nCASE 1/0\nPRINT 2\nEND SELECT", 2),
|
||||
("NEXT", "FOR i%=32767 TO 32767\nPRINT 1\nNEXT", 3),
|
||||
("LOOP", "DO\nPRINT 1\nLOOP UNTIL 1/0", 3),
|
||||
] {
|
||||
let (event, _) = run(compile(&[("MAIN", code)]).unwrap());
|
||||
if !matches!(event, RunEvent::Error { line: found, .. } if found == line) {
|
||||
failures.push(format!("{name}: erwartet Zeile {line}, erhalten {event:?}"));
|
||||
}
|
||||
}
|
||||
let source = SourceUnit {
|
||||
name: "MAIN".into(),
|
||||
segments: vec![
|
||||
SourceSegment {
|
||||
file: "main.bas".into(),
|
||||
first_line: 1,
|
||||
text: "IF 0 THEN\nPRINT 1\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "cond.bi".into(),
|
||||
first_line: 1,
|
||||
text: "ELSEIF 1/0 THEN\nPRINT 2\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "main.bas".into(),
|
||||
first_line: 4,
|
||||
text: "END IF\n".into(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let mut vm = Vm::new(compile_project("APP", &[source], &FormCatalog::default(), &[]).unwrap());
|
||||
let event = vm.run(&mut CaptureHost::default());
|
||||
if vm.current_file() != "cond.bi" {
|
||||
failures.push(format!(
|
||||
"Include: {event:?} aus {} statt cond.bi",
|
||||
vm.current_file()
|
||||
));
|
||||
}
|
||||
// Das gleiche Problem darf nicht nur durch Umbenennen der Fehlermeldung behoben werden.
|
||||
let mut vm = Vm::new(
|
||||
compile(&[("MAIN", "IF 0 THEN\nPRINT 1\nELSEIF 1 THEN\nPRINT 2\nEND IF")]).unwrap(),
|
||||
);
|
||||
vm.add_module_breakpoint(0, 3);
|
||||
let event = vm.run(&mut CaptureHost::default());
|
||||
if event != (RunEvent::Breakpoint { line: 3 }) {
|
||||
failures.push(format!("Breakpoint ELSEIF: {event:?}"));
|
||||
}
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_importierte_deklarationen_behalten_kontext_und_abhaengigkeiten() {
|
||||
let cases = [
|
||||
(
|
||||
"DEFINT",
|
||||
"PRINT Doppelt(3)\nEND",
|
||||
"DEFINT A-Z\nFUNCTION Doppelt(x)\nDoppelt=x*2\nEND FUNCTION",
|
||||
" 6 \n",
|
||||
),
|
||||
(
|
||||
"CONST",
|
||||
"CONST A=3\nPRINT B\nEND",
|
||||
"CONST A=3\nCONST B=A+1",
|
||||
" 4 \n",
|
||||
),
|
||||
(
|
||||
"TYPE",
|
||||
"TYPE Inner\nx AS INTEGER\nEND TYPE\nDIM a AS Outer\na.i.x=3\nPRINT a.i.x\nEND",
|
||||
"TYPE Inner\nx AS INTEGER\nEND TYPE\nTYPE Outer\ni AS Inner\nEND TYPE",
|
||||
" 3 \n",
|
||||
),
|
||||
];
|
||||
let mut failures = vec![];
|
||||
for (name, main, lib, output) in cases {
|
||||
match compile(&[("MAIN", main), ("LIB", lib)]) {
|
||||
Err(e) => failures.push(format!("{name}: {e:?}")),
|
||||
Ok(module) => {
|
||||
let actual = run(module);
|
||||
if actual != (RunEvent::Ended, output.into()) {
|
||||
failures.push(format!("{name}: {actual:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v3_form_metabefehle_erzeugen_getrennte_formularobjekte() {
|
||||
let module = compile(&[
|
||||
(
|
||||
"A",
|
||||
"'$FORM\nCaption=\"a\"\nCALL SetupB\nPRINT Caption\nEND",
|
||||
),
|
||||
("B", "'$FORM\nSUB SetupB\nCaption=\"b\"\nEND SUB"),
|
||||
])
|
||||
.unwrap();
|
||||
let names: Vec<_> = module.objects.iter().map(|o| o.name.clone()).collect();
|
||||
let actual = run(module);
|
||||
assert_eq!(
|
||||
actual,
|
||||
(RunEvent::Ended, "a\n".into()),
|
||||
"Objekte: {names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v4_common_behaelt_typidentitaet_und_initialisiert_arrays_einmal() {
|
||||
let mut failures = vec![];
|
||||
for (name, files, want) in [
|
||||
(
|
||||
"Suffixe",
|
||||
vec![(
|
||||
"MAIN",
|
||||
"COMMON SHARED c%, c$\nc%=7\nc$=\"ok\"\nPRINT c%;c$\nEND",
|
||||
)],
|
||||
" 7 ok\n",
|
||||
),
|
||||
(
|
||||
"Array",
|
||||
vec![
|
||||
("MAIN", "COMMON SHARED a%(2)\na%(1)=7\nCALL F\nEND"),
|
||||
("LIB", "COMMON SHARED a%(2)\nSUB F\nPRINT a%(1)\nEND SUB"),
|
||||
],
|
||||
" 7 \n",
|
||||
),
|
||||
] {
|
||||
match compile(&files) {
|
||||
Err(e) => failures.push(format!("{name}: {e:?}")),
|
||||
Ok(module) => {
|
||||
let actual = run(module);
|
||||
if actual != (RunEvent::Ended, want.into()) {
|
||||
failures.push(format!("{name}: {actual:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v5_load_und_unload_loesen_den_expliziten_container_auf() {
|
||||
use tb_frontend::forms::ObjectClass;
|
||||
let mut catalog = FormCatalog::default();
|
||||
for name in ["Form1", "Form2"] {
|
||||
catalog.add(name, ObjectClass::Form, None, false);
|
||||
catalog.add("Text1", ObjectClass::TextBox, Some(name), true);
|
||||
}
|
||||
let source = units(&[("MAIN", "LOAD Form2!Text1(3)\nForm2!Text1(3).Text=\"b\"\nPRINT Form2!Text1(3).Text\nUNLOAD Form2!Text1(3)\nEND")]);
|
||||
let module = compile_project("APP", &source, &catalog, &[]).unwrap();
|
||||
assert_eq!(run(module), (RunEvent::Ended, "b\n".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v6_linkerdiagnose_nennt_den_urspruenglichen_dateiort() {
|
||||
let errors = compile(&[
|
||||
("MAIN", "DECLARE SUB F(x%)\nCALL F(1)\nEND"),
|
||||
("LIB", "SUB F(x$)\nEND SUB"),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|d| d.message.contains("Parameter type mismatch")),
|
||||
"{errors:?}"
|
||||
);
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.all(|d| d.file.is_some() && d.pos.line > 0 && d.pos.column > 0),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modulbreakpoint_rundlauf_inspektion_und_numerische_erl_bleiben_getrennt() {
|
||||
let main = format!(
|
||||
"DIM a%(2)\nTYPE T\nx AS INTEGER\nEND TYPE\nDIM r AS T\na%(1)=7\nr.x=8\n{}CALL F\nEND",
|
||||
"\n\nn%=1\n"
|
||||
);
|
||||
let lib = format!("SUB F\n{}200 ERROR 6\nEND SUB", "\n".repeat(8));
|
||||
let module = compile(&[("MAIN", &main), ("LIB", &lib)]).unwrap();
|
||||
let bytes = module.to_tbc();
|
||||
let loaded = CompiledModule::from_tbc(&bytes).unwrap();
|
||||
assert_eq!(bytes, loaded.to_tbc());
|
||||
let mut vm = Vm::new(loaded);
|
||||
vm.add_module_breakpoint(1, 10);
|
||||
let mut host = CaptureHost::default();
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 10 });
|
||||
assert_eq!(vm.current_module(), 1);
|
||||
assert_eq!(vm.current_file(), "LIB.bas");
|
||||
assert!(matches!(
|
||||
vm.inspect_element("MAIN!a%", &[1]),
|
||||
Some(Value::Int(7))
|
||||
));
|
||||
assert!(matches!(
|
||||
vm.inspect_field("MAIN!r", &[0]),
|
||||
Some(Value::Int(8))
|
||||
));
|
||||
vm.remove_module_breakpoint(1, 10);
|
||||
vm.set_step(true);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 10 });
|
||||
assert_eq!(vm.current_source_pos().column, 5);
|
||||
vm.set_step(false);
|
||||
assert!(matches!(
|
||||
vm.run(&mut host),
|
||||
RunEvent::Error {
|
||||
code: 6,
|
||||
line: 10,
|
||||
..
|
||||
}
|
||||
));
|
||||
let source = format!("{}ERROR 6", "\n".repeat(41));
|
||||
assert!(matches!(
|
||||
run(compile(&[("MAIN", &source)]).unwrap()).0,
|
||||
RunEvent::Error {
|
||||
code: 6,
|
||||
line: 42,
|
||||
..
|
||||
}
|
||||
));
|
||||
let module = compile(&[(
|
||||
"MAIN",
|
||||
"ON ERROR RESUME NEXT\n200 ERROR 6\nPRINT ERR;ERL\nEND",
|
||||
)])
|
||||
.unwrap();
|
||||
assert_eq!(run(module), (RunEvent::Ended, " 6 200 \n".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn importkontext_bleibt_auch_bei_abweichenden_lokalen_definitionen_erhalten() {
|
||||
for (files, expected) in [
|
||||
(vec![("MAIN", "CONST A=99\nPRINT B\nEND"), ("LIB", "CONST A=3\nCONST B=A+1")], " 4 \n"),
|
||||
(vec![("MAIN", "PRINT C\nEND"), ("LIB", "CONST C=B+1"), ("BASE", "CONST B=3")], " 4 \n"),
|
||||
(vec![("MAIN", "TYPE Inner\nx AS STRING * 3\nEND TYPE\nDIM a AS Outer\na.i.x=3\nPRINT a.i.x\nEND"), ("LIB", "TYPE Inner\nx AS INTEGER\nEND TYPE\nTYPE Outer\ni AS Inner\nEND TYPE")], " 3 \n"),
|
||||
(vec![("MAIN", "DEFSTR A-Z\nx%=3\nCALL Twice(x%)\nPRINT x%\nPRINT Doppelt(3)\nEND"), ("LIB", "DEFINT A-Z\nSUB Twice(x)\nx=x*2\nEND SUB\nFUNCTION Doppelt(x)\nDoppelt=x*2\nEND FUNCTION")], " 6 \n 6 \n"),
|
||||
] {
|
||||
let actual = run(compile(&files).unwrap_or_else(|e| panic!("{files:?}: {e:?}")));
|
||||
assert_eq!(actual, (RunEvent::Ended, expected.into()), "{files:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_vertraege_und_linkfehler_haben_echte_quellorte() {
|
||||
for (left, right) in [
|
||||
("COMMON SHARED a%(2)", "COMMON SHARED a%(3)"),
|
||||
("COMMON SHARED a%(1 TO 2)", "COMMON SHARED a%(0 TO 2)"),
|
||||
("COMMON SHARED a%(2)", "COMMON SHARED a%(2,2)"),
|
||||
("COMMON SHARED a AS INTEGER", "COMMON SHARED a AS STRING"),
|
||||
("COMMON SHARED a%(2)", "COMMON SHARED a%"),
|
||||
] {
|
||||
let units = [
|
||||
SourceUnit::new("MAIN", "main.bas", &format!("{left}\nEND")),
|
||||
SourceUnit {
|
||||
name: "LIB".into(),
|
||||
segments: vec![SourceSegment {
|
||||
file: "shared.bi".into(),
|
||||
first_line: 42,
|
||||
text: format!(" {right}\n"),
|
||||
}],
|
||||
},
|
||||
];
|
||||
let errors = compile_project("APP", &units, &FormCatalog::default(), &[]).unwrap_err();
|
||||
assert!(
|
||||
errors.iter().any(|e| e.file.as_deref() == Some("shared.bi")
|
||||
&& e.pos.line == 42
|
||||
&& e.pos.column > 1
|
||||
&& e.message.contains("COMMON")),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
let files = [
|
||||
(
|
||||
"MAIN",
|
||||
"COMMON SHARED a%(2),a$(2)\na%(1)=7\na$(1)=\"ok\"\nCALL F\nEND",
|
||||
),
|
||||
(
|
||||
"LIB",
|
||||
"COMMON SHARED a%(2),a$(2)\nSUB F\nPRINT a%(1);a$(1)\nEND SUB",
|
||||
),
|
||||
];
|
||||
assert_eq!(
|
||||
run(compile(&files).unwrap()),
|
||||
(RunEvent::Ended, " 7 ok\n".into())
|
||||
);
|
||||
let errors =
|
||||
compile(&[("MAIN", "' header\nDECLARE SUB F(x$)\nSUB F(x%)\nEND SUB")]).unwrap_err();
|
||||
assert!(
|
||||
errors.iter().any(|e| e.pos.line == 3
|
||||
&& e.file.as_deref() == Some("MAIN.bas")
|
||||
&& e.message.contains("Parameter")),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schleifengrenzen_tragen_breakpoint_include_und_resume() {
|
||||
for (source, line) in [
|
||||
("FOR i%=1 TO 2\nPRINT i%\nNEXT\nEND", 3),
|
||||
("DO\nPRINT 1\nLOOP UNTIL 1\nEND", 3),
|
||||
("i%=1\nWHILE i%\ni%=0\nWEND\nEND", 4),
|
||||
(
|
||||
"SELECT CASE 2\nCASE 1\nPRINT 1\nCASE 2\nPRINT 2\nEND SELECT",
|
||||
4,
|
||||
),
|
||||
] {
|
||||
let module = compile(&[("MAIN", source)]).unwrap();
|
||||
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
|
||||
vm.add_module_breakpoint(0, line);
|
||||
assert_eq!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Breakpoint { line }
|
||||
);
|
||||
assert_eq!(vm.current_file(), "MAIN.bas");
|
||||
assert_eq!(vm.current_source_pos().column, 1);
|
||||
vm.remove_module_breakpoint(0, line);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
}
|
||||
let actual = run(compile(&[("MAIN", "x%=1\nWHILE 1/x%\nx%=0\nWEND")]).unwrap());
|
||||
assert!(
|
||||
matches!(
|
||||
actual.0,
|
||||
RunEvent::Error {
|
||||
code: 11,
|
||||
line: 2,
|
||||
..
|
||||
}
|
||||
),
|
||||
"{actual:?}"
|
||||
);
|
||||
let actual = run(compile(&[(
|
||||
"MAIN",
|
||||
"ON ERROR GOTO H\nDO\nn%=n%+1\nLOOP UNTIL 1/x%\nPRINT n%\nEND\nH:\nx%=1\nRESUME",
|
||||
)])
|
||||
.unwrap());
|
||||
assert_eq!(actual, (RunEvent::Ended, " 1 \n".into()));
|
||||
let actual = run(compile(&[(
|
||||
"MAIN",
|
||||
"ON ERROR RESUME NEXT\nFOR i%=32767 TO 32767\nNEXT\nPRINT ERR\nEND",
|
||||
)])
|
||||
.unwrap());
|
||||
assert_eq!(actual, (RunEvent::Ended, " 6 \n".into()));
|
||||
let unit = SourceUnit {
|
||||
name: "MAIN".into(),
|
||||
segments: vec![
|
||||
SourceSegment {
|
||||
file: "main.bas".into(),
|
||||
first_line: 1,
|
||||
text: "DO\nPRINT 1\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "end.bi".into(),
|
||||
first_line: 42,
|
||||
text: " LOOP UNTIL 1/0\n".into(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let module = compile_project("APP", &[unit], &FormCatalog::default(), &[]).unwrap();
|
||||
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
|
||||
assert!(matches!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Error {
|
||||
code: 11,
|
||||
line: 42,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert_eq!(vm.current_file(), "end.bi");
|
||||
assert_eq!(vm.current_source_pos().column, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debugger_unterscheidet_typisierte_scalar_und_arraynamen() {
|
||||
let mut vm = Vm::new(
|
||||
compile(&[(
|
||||
"MAIN",
|
||||
"COMMON SHARED c%,c$,a%(2),a$(2)\nc%=7\nc$=\"ok\"\na%(1)=8\na$(1)=\"array\"\nSTOP",
|
||||
)])
|
||||
.unwrap(),
|
||||
);
|
||||
assert!(matches!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Stopped { .. }
|
||||
));
|
||||
assert!(matches!(vm.inspect("c%"), Some(Value::Int(7))));
|
||||
assert!(matches!(vm.inspect("c$"),Some(Value::Str(s)) if s.as_ref()=="ok"));
|
||||
assert!(matches!(
|
||||
vm.inspect_element("a%", &[1]),
|
||||
Some(Value::Int(8))
|
||||
));
|
||||
assert!(matches!(vm.inspect_element("a$",&[1]),Some(Value::Str(s)) if s.as_ref()=="array"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_arrays_behalten_dynamische_grenzen_und_kompatible_offene_deklarationen() {
|
||||
for (left, right) in [
|
||||
("COMMON SHARED a%(2)", "COMMON SHARED a%()"),
|
||||
("COMMON SHARED a%(2.4)", "COMMON SHARED a%(2)"),
|
||||
("COMMON SHARED a%(n%+2)", "COMMON SHARED a%(n%+2)"),
|
||||
] {
|
||||
let files = [
|
||||
("MAIN", format!("{left}\na%(1)=7\nCALL F\nEND")),
|
||||
("LIB", format!("{right}\nSUB F\nPRINT a%(1)\nEND SUB")),
|
||||
];
|
||||
let files: Vec<_> = files.iter().map(|(n, s)| (*n, s.as_str())).collect();
|
||||
assert_eq!(
|
||||
run(compile(&files).unwrap()),
|
||||
(RunEvent::Ended, " 7 \n".into()),
|
||||
"{files:?}"
|
||||
);
|
||||
}
|
||||
let module = compile(&[
|
||||
("MAIN", "COMMON SHARED a%(n%+2)\nEND"),
|
||||
("LIB", "COMMON SHARED a%(n%+3)"),
|
||||
])
|
||||
.unwrap();
|
||||
let mut vm = Vm::new(CompiledModule::from_tbc(&module.to_tbc()).unwrap());
|
||||
assert!(matches!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Error {
|
||||
code: 13,
|
||||
line: 1,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert_eq!(vm.current_file(), "LIB.bas");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prozedurlokaler_deftype_veraendert_keine_nachfolgende_signatur() {
|
||||
let files=[("MAIN","PRINT F(3)\nEND"),("LIB","DEFINT A-Z\nSUB Setup\nDEFSTR A-Z\nx=\"local\"\nEND SUB\nFUNCTION F(x)\nF=x*2\nEND FUNCTION")];
|
||||
assert_eq!(
|
||||
run(compile(&files).unwrap()),
|
||||
(RunEvent::Ended, " 6 \n".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemischte_frm_und_form_module_haben_getrennte_ereignisbindungen() {
|
||||
let form=tb_ui::frm::read_text("A.frm","VERSION 1.00\nBEGIN Form A\n Caption = \"design\"\nEND\nSUB Form_Load\nCaption=\"a\"\nEND SUB").unwrap();
|
||||
let sources = units(&[
|
||||
(
|
||||
"MAIN",
|
||||
"B.Show\na$=A.Caption\nb$=B.Caption\nA.Hide\nB.Hide\nCLS\nPRINT a$;b$\nEND",
|
||||
),
|
||||
("A", &form.code),
|
||||
("B", "'$FORM\nSUB Form_Load\nCaption=\"b\"\nEND SUB"),
|
||||
]);
|
||||
let module = compile_project("APP", &sources, &form.catalog(), &[form]).unwrap();
|
||||
assert_eq!(module.event_procs.len(), 2);
|
||||
assert_ne!(module.event_procs[0].object, module.event_procs[1].object);
|
||||
assert_eq!(run(module), (RunEvent::Ended, "ab\n".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostik_bei_literalen_im_include_hat_die_exakte_spalte() {
|
||||
let unit = SourceUnit {
|
||||
name: "MAIN".into(),
|
||||
segments: vec![
|
||||
SourceSegment {
|
||||
file: "main.bas".into(),
|
||||
first_line: 1,
|
||||
text: "DECLARE SUB F(x%)\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "args.bi".into(),
|
||||
first_line: 42,
|
||||
text: " CALL F(\"wrong\")\n".into(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let errors = compile_project("APP", &[unit], &FormCatalog::default(), &[]).unwrap_err();
|
||||
assert!(
|
||||
errors.iter().any(|e| e.file.as_deref() == Some("args.bi")
|
||||
&& e.pos.line == 42
|
||||
&& e.pos.column == 10
|
||||
&& e.message.contains("Parameter type mismatch")),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spaetere_common_konflikte_und_doppelte_module_werden_am_ursprung_gemeldet() {
|
||||
let errors = compile(&[
|
||||
("MAIN", "COMMON a%()\nEND"),
|
||||
("LIB", "COMMON a%(2)"),
|
||||
("THIRD", "' head\nCOMMON a%(3)"),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
errors.iter().any(|e| e.file.as_deref() == Some("THIRD.bas")
|
||||
&& e.pos.line == 2
|
||||
&& e.message.contains("COMMON")),
|
||||
"{errors:?}"
|
||||
);
|
||||
let units = [
|
||||
SourceUnit::new("SAME", "first.bas", "END"),
|
||||
SourceUnit::new("SAME", "second.bas", "' head\n END"),
|
||||
];
|
||||
let errors = compile_project("APP", &units, &FormCatalog::default(), &[]).unwrap_err();
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|e| e.file.as_deref() == Some("second.bas")
|
||||
&& e.pos.line == 2
|
||||
&& e.pos.column == 3
|
||||
&& e.message.contains("Duplicate")),
|
||||
"{errors:?}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user