Phase 6: P-Code-Bibliotheken und Linker abschließen, Cross-Buildplan festhalten
This commit is contained in:
375
crates/tb-vm/tests/library.rs
Normal file
375
crates/tb-vm/tests/library.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
use tb_frontend::{forms::FormCatalog, source::SourceUnit};
|
||||
use tb_runtime::host::CaptureHost;
|
||||
use tb_vm::{
|
||||
bytecode::CompiledModule,
|
||||
interp::{RunEvent, Vm},
|
||||
library::Library,
|
||||
project::ProjectCompiler,
|
||||
};
|
||||
fn unit(name: &str, text: &str) -> SourceUnit {
|
||||
SourceUnit::new(name, &format!("{name}.bas"), text)
|
||||
}
|
||||
fn library(units: &[SourceUnit], libs: &[Library]) -> Library {
|
||||
let l = ProjectCompiler::default()
|
||||
.compile_library(units, &FormCatalog::default(), &[], libs)
|
||||
.unwrap();
|
||||
let bytes = l.to_tbl().unwrap();
|
||||
let l = Library::from_tbl(&bytes).unwrap();
|
||||
assert_eq!(bytes, l.to_tbl().unwrap());
|
||||
l
|
||||
}
|
||||
fn link(main: &str, libs: &[Library]) -> Result<CompiledModule, String> {
|
||||
let mut c = ProjectCompiler::default();
|
||||
c.compile_library(&[unit("APP", main)], &FormCatalog::default(), &[], libs)
|
||||
.and_then(|l| c.link_library("APP", l, None))
|
||||
.map_err(|ds| {
|
||||
ds.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
})
|
||||
}
|
||||
fn output(code: CompiledModule) -> String {
|
||||
let mut vm = Vm::new(code);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
tb_runtime::snapshot::text(&vm.rt.screen)
|
||||
}
|
||||
#[test]
|
||||
fn two_source_free_libraries_with_typed_open_imports_and_diagnostics() {
|
||||
let a = library(
|
||||
&[unit(
|
||||
"A",
|
||||
"DECLARE SUB Second(n AS INTEGER)\nSUB First(n AS INTEGER)\nSecond n\nEND SUB",
|
||||
)],
|
||||
&[],
|
||||
);
|
||||
assert!(link("CALL First(n%)\nEND", std::slice::from_ref(&a))
|
||||
.unwrap_err()
|
||||
.contains("SECOND"));
|
||||
let b = library(
|
||||
&[unit("B", "SUB Second(n AS INTEGER)\nn=n+3\nEND SUB")],
|
||||
&[],
|
||||
);
|
||||
assert_eq!(
|
||||
output(
|
||||
link(
|
||||
"n%=2\nCALL First(n%)\nPRINT n%\nEND",
|
||||
&[a.clone(), b.clone()]
|
||||
)
|
||||
.unwrap()
|
||||
),
|
||||
" 5 \n"
|
||||
);
|
||||
let incompatible = library(&[unit("B", "SUB Second(n AS LONG)\nEND SUB")], &[]);
|
||||
assert!(link("CALL First(n%)\nEND", &[a.clone(), incompatible])
|
||||
.unwrap_err()
|
||||
.contains("Parameter type mismatch"));
|
||||
let c = library(&[unit("C", "SUB Second(n AS INTEGER)\nEND SUB")], &[]);
|
||||
let direct = link("CALL Second(n%)\nEND", &[b.clone(), c.clone()]).unwrap_err();
|
||||
assert!(
|
||||
direct.contains("Ambiguous subprogram: SECOND (B, C)"),
|
||||
"{direct}"
|
||||
);
|
||||
let error = link("CALL First(n%)\nEND", &[a.clone(), b.clone(), c]).unwrap_err();
|
||||
assert!(
|
||||
error.contains("Ambiguous") && error.contains("B") && error.contains("C"),
|
||||
"{error}"
|
||||
);
|
||||
assert!(link("END", &[b.clone(), b.clone()])
|
||||
.unwrap_err()
|
||||
.contains("Duplicate definition: module B"));
|
||||
let combined = library(&[], &[a, b]);
|
||||
assert_eq!(combined.modules.len(), 2);
|
||||
assert_eq!(
|
||||
output(link("CALL First(n%)\nPRINT n%\nEND", &[combined]).unwrap()),
|
||||
" 3 \n"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn source_and_library_have_identical_types_constants_common_data_byref_and_init_order() {
|
||||
let a=unit("A","CONST N=3\nTYPE Record\nx AS INTEGER\ns AS STRING * 4\nEND TYPE\nCOMMON SHARED shared%\nDIM SHARED ready%(N)\nDATA 7\nPRINT \"A\"\nDATA 7\nSUB Work(a%(), r AS Record, n%)\nready%(N)=1\nshared%=shared%+1\na%(1)=N\nr.x=r.x+n%\nn%=9\nRESTORE\nREAD d%\nPRINT d%\nEND SUB\n");
|
||||
let b = unit("B", "PRINT \"B\"\n");
|
||||
let main="COMMON SHARED shared%\nDIM a%(N)\nDIM r AS Record\nn%=2\nCALL Work(a%(),r,n%)\nPRINT a%(1);r.x;n%;shared%\nCALL Work(a%(),r,(n%))\nPRINT r.x;n%;shared%\n";
|
||||
let source = ProjectCompiler::default()
|
||||
.compile(
|
||||
"APP",
|
||||
&[unit("APP", main), a.clone(), b.clone()],
|
||||
&FormCatalog::default(),
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
let binary = link(main, &[library(&[a, b], &[])]).unwrap();
|
||||
assert_eq!(output(source), output(binary));
|
||||
}
|
||||
#[test]
|
||||
fn library_source_locations_and_run_restart_are_preserved() {
|
||||
let l = library(&[unit("LIB", "SUB Fail\n200 ERROR 6\nEND SUB")], &[]);
|
||||
let mut vm = Vm::new(link("CALL Fail\nEND", &[l]).unwrap());
|
||||
assert!(matches!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Error {
|
||||
code: 6,
|
||||
line: 2,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert_eq!(vm.current_file(), "LIB.bas");
|
||||
let l = library(
|
||||
&[unit(
|
||||
"LIB",
|
||||
"DIM SHARED n%\nSUB Increment\nn%=n%+1\nPRINT n%\nEND SUB",
|
||||
)],
|
||||
&[],
|
||||
);
|
||||
let code = link("CALL Increment\nEND", &[l]).unwrap();
|
||||
for _ in 0..2 {
|
||||
assert_eq!(output(code.clone()), " 1 \n");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn library_container_rejects_truncation_versions_lengths_and_references() {
|
||||
let l = library(
|
||||
&[unit("LIB", "CONST N=3\nSUB Work(n%)\nn%=N\nEND SUB")],
|
||||
&[],
|
||||
);
|
||||
let bytes = l.to_tbl().unwrap();
|
||||
let seal = |mut payload: Vec<u8>| {
|
||||
payload.extend_from_slice(&tb_vm::bytecode::checksum(&payload).to_le_bytes());
|
||||
payload
|
||||
};
|
||||
for end in 0..bytes.len() - 8 {
|
||||
assert!(
|
||||
Library::from_tbl(&bytes[..end]).is_err(),
|
||||
"accepted prefix {end}"
|
||||
);
|
||||
assert!(
|
||||
Library::from_tbl(&seal(bytes[..end].to_vec())).is_err(),
|
||||
"accepted incomplete payload {end}"
|
||||
);
|
||||
}
|
||||
for offset in [0, 4, 6, 8, 12, 16] {
|
||||
let mut bad = bytes[..bytes.len() - 8].to_vec();
|
||||
bad[offset] = 255;
|
||||
assert!(
|
||||
Library::from_tbl(&seal(bad)).is_err(),
|
||||
"accepted offset {offset}"
|
||||
);
|
||||
}
|
||||
let mut bad = bytes[..bytes.len() - 8].to_vec();
|
||||
bad.push(0);
|
||||
assert!(Library::from_tbl(&seal(bad)).is_err());
|
||||
for offset in (0..bytes.len() - 8).step_by(7) {
|
||||
let mut bad = bytes[..bytes.len() - 8].to_vec();
|
||||
bad[offset] ^= 255;
|
||||
let bad = seal(bad);
|
||||
std::panic::catch_unwind(|| {
|
||||
if let Ok(l) = Library::from_tbl(&bad) {
|
||||
let _ = link("END", &[l]);
|
||||
}
|
||||
})
|
||||
.expect("beschädigte TBL darf nicht paniken");
|
||||
}
|
||||
let mut bad = l.clone();
|
||||
if let tb_frontend::ast::Stmt::ConstDecl { items, .. } = &mut bad.modules[0].metadata.exports[0]
|
||||
{
|
||||
items.clear();
|
||||
}
|
||||
assert!(bad.to_tbl().is_err());
|
||||
let mut bad = l.clone();
|
||||
bad.modules[0].metadata.defined.push("ABSENT".into());
|
||||
assert!(bad.to_tbl().is_err());
|
||||
let mut bad = l;
|
||||
bad.modules[0].code.procs[0]
|
||||
.code
|
||||
.push(tb_vm::bytecode::Instr::Call(u16::MAX, 0));
|
||||
assert!(bad.to_tbl().is_err());
|
||||
}
|
||||
#[test]
|
||||
fn separate_form_libraries_remap_nested_objects_initials_arrays_and_events() {
|
||||
let form = |name: &str, text: &str| {
|
||||
tb_ui::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\nSUB Form_Load\nText1.Text=Text1.Text+\"!\"\nEND SUB\n")).unwrap()
|
||||
};
|
||||
let forms = [form("Form1", "a"), form("Form2", "b")];
|
||||
let mut catalog = FormCatalog::default();
|
||||
for f in &forms {
|
||||
catalog.append(&f.catalog());
|
||||
}
|
||||
let units: Vec<_> = forms.iter().map(|f| unit(&f.root.name, &f.code)).collect();
|
||||
let libs: Vec<_> = forms
|
||||
.iter()
|
||||
.zip(&units)
|
||||
.map(|(f, u)| {
|
||||
let l = ProjectCompiler::default()
|
||||
.compile_library(
|
||||
std::slice::from_ref(u),
|
||||
&f.catalog(),
|
||||
std::slice::from_ref(f),
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
Library::from_tbl(&l.to_tbl().unwrap()).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let main=unit("APP","Form1.Show\nForm2.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");
|
||||
let mut all = vec![main.clone()];
|
||||
all.extend(units);
|
||||
let mut c = ProjectCompiler::default();
|
||||
let mut source = c.compile("APP", &all, &catalog, &forms).unwrap();
|
||||
source.startup_form = None;
|
||||
let l = c
|
||||
.compile_library(&[main], &FormCatalog::default(), &[], &libs)
|
||||
.unwrap();
|
||||
let linked = c.link_library("APP", l, None).unwrap();
|
||||
assert_eq!(linked.event_procs.len(), 2);
|
||||
assert_eq!(output(source), output(linked));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contracts_reject_result_common_bounds_and_invalid_declarations() {
|
||||
let a = library(
|
||||
&[unit(
|
||||
"A",
|
||||
"DECLARE FUNCTION Value%()\nSUB Work\nPRINT Value%\nEND SUB",
|
||||
)],
|
||||
&[],
|
||||
);
|
||||
let b = library(&[unit("B", "FUNCTION Value&\nValue&=4\nEND FUNCTION")], &[]);
|
||||
assert!(link("CALL Work\nEND", &[a, b])
|
||||
.unwrap_err()
|
||||
.contains("Parameter type mismatch"));
|
||||
let a = library(&[unit("A", "COMMON SHARED n%(1 TO 3)\n")], &[]);
|
||||
let b = library(&[unit("B", "COMMON SHARED n%(1 TO 4)\n")], &[]);
|
||||
assert!(link("END", &[a, b])
|
||||
.unwrap_err()
|
||||
.contains("COMMON type or bounds mismatch"));
|
||||
for source in [
|
||||
"DECLARE SUB Bad(x AS Missing)",
|
||||
"DECLARE SUB Bad(x%)\nDECLARE SUB Bad(x&)",
|
||||
"CONST N=Missing\n",
|
||||
"TYPE X\nn AS Missing\nEND TYPE",
|
||||
] {
|
||||
assert!(
|
||||
ProjectCompiler::default()
|
||||
.compile_library(&[unit("BAD", source)], &FormCatalog::default(), &[], &[])
|
||||
.is_err(),
|
||||
"{source}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_debug_context_keeps_module_ids_when_library_is_between_sources() {
|
||||
let l = library(&[unit("LIB", "DIM n%\nSUB LibraryCall\nEND SUB")], &[]);
|
||||
let mut compiler = ProjectCompiler::default();
|
||||
compiler.debug_symbols = true;
|
||||
let mut product = compiler
|
||||
.compile_library(
|
||||
&[unit("APP", "PRINT 0"), unit("LAST", "x%=7\nSTOP")],
|
||||
&FormCatalog::default(),
|
||||
&[],
|
||||
&[l],
|
||||
)
|
||||
.unwrap();
|
||||
product.modules.swap(1, 2);
|
||||
let code = compiler.link_library("APP", product, None).unwrap();
|
||||
let debug = compiler.debug_compiler();
|
||||
assert!(debug.compile(1, "<main>", "x%", true).is_err());
|
||||
let mut vm = Vm::new(code);
|
||||
vm.debug.enabled = true;
|
||||
vm.debug.compiler = Some(debug);
|
||||
assert!(matches!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Stopped { .. }
|
||||
));
|
||||
let frame = vm.debug_location().unwrap().frame;
|
||||
assert!(matches!(
|
||||
vm.evaluate_watch(frame, "x%").unwrap(),
|
||||
tb_runtime::value::Value::Int(7)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unused_declare_is_not_an_import_but_references_in_any_body_must_resolve() {
|
||||
let l = library(
|
||||
&[unit(
|
||||
"LIB",
|
||||
"DECLARE FUNCTION Missing%(s$)\nSUB Work\nPRINT 7\nEND SUB",
|
||||
)],
|
||||
&[],
|
||||
);
|
||||
let code = link("CALL Work\nEND", &[l]).unwrap();
|
||||
assert!(!code.procs.iter().any(|p| p.name.ends_with("MISSING")));
|
||||
assert_eq!(output(code), " 7 \n");
|
||||
let l = library(
|
||||
&[unit(
|
||||
"LIB",
|
||||
"DECLARE SUB Missing\nSUB Unused\nCALL Missing\nEND SUB",
|
||||
)],
|
||||
&[],
|
||||
);
|
||||
assert!(link("END", &[l])
|
||||
.unwrap_err()
|
||||
.contains("Subprogram not defined: MISSING"));
|
||||
let mut compiler = ProjectCompiler::default();
|
||||
compiler.debug_symbols = true;
|
||||
compiler
|
||||
.compile(
|
||||
"APP",
|
||||
&[unit("APP", "DECLARE SUB Missing\nSTOP")],
|
||||
&FormCatalog::default(),
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(compiler
|
||||
.debug_compiler()
|
||||
.compile(0, "<main>", "CALL Missing", false)
|
||||
.err()
|
||||
.unwrap()
|
||||
.contains("Subprogram not defined"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_include_locations_keep_physical_lines_and_basic_erl() {
|
||||
use tb_frontend::source::SourceSegment;
|
||||
let source = SourceUnit {
|
||||
name: "LIB".into(),
|
||||
segments: vec![
|
||||
SourceSegment {
|
||||
file: "lib.bas".into(),
|
||||
first_line: 1,
|
||||
text: "SUB Fail\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "nested.bi".into(),
|
||||
first_line: 9,
|
||||
text: "200 ERROR 6\n".into(),
|
||||
},
|
||||
SourceSegment {
|
||||
file: "lib.bas".into(),
|
||||
first_line: 3,
|
||||
text: "END SUB\n".into(),
|
||||
},
|
||||
],
|
||||
};
|
||||
let l = library(&[source], &[]);
|
||||
let mut vm = Vm::new(link("CALL Fail\nEND", std::slice::from_ref(&l)).unwrap());
|
||||
assert!(matches!(
|
||||
vm.run(&mut CaptureHost::default()),
|
||||
RunEvent::Error {
|
||||
code: 6,
|
||||
line: 9,
|
||||
..
|
||||
}
|
||||
));
|
||||
assert_eq!(vm.current_file(), "nested.bi");
|
||||
assert_eq!(
|
||||
output(
|
||||
link(
|
||||
"ON ERROR GOTO Handler\nCALL Fail\nEND\nHandler: PRINT ERR;ERL\nEND",
|
||||
&[l]
|
||||
)
|
||||
.unwrap()
|
||||
),
|
||||
" 6 200 \n"
|
||||
);
|
||||
}
|
||||
@@ -191,3 +191,107 @@ fn case_insensitive_parent_lookup_works_from_relative_base_without_changing_cwd(
|
||||
let found = tb_vm::project_io::relative_case_insensitive(&base, "../SIBLING.BAS").unwrap();
|
||||
assert_eq!(identity(&found).unwrap(), identity(&sibling).unwrap());
|
||||
}
|
||||
#[test]
|
||||
fn tbl_members_move_save_as_order_startup_and_same_size_cache_replacement() {
|
||||
use tb_frontend::{forms::FormCatalog, source::SourceUnit};
|
||||
use tb_vm::{
|
||||
interp::{RunEvent, Vm},
|
||||
project::ProjectCompiler,
|
||||
};
|
||||
let t = Temp::new();
|
||||
let library = |n, value| {
|
||||
ProjectCompiler::default()
|
||||
.compile_library(
|
||||
&[SourceUnit::new(
|
||||
"LIB",
|
||||
"removed.bas",
|
||||
&format!("CONST N={n}\nSUB Work\nPRINT {value}\nEND SUB\nPRINT \"LIB\""),
|
||||
)],
|
||||
&FormCatalog::default(),
|
||||
&[],
|
||||
&[],
|
||||
)
|
||||
.unwrap()
|
||||
.to_tbl()
|
||||
.unwrap()
|
||||
};
|
||||
let first = library(1, 1);
|
||||
let second = library(2, 2);
|
||||
assert_eq!(first.len(), second.len());
|
||||
t.write("old/main.bas", "PRINT N\nCALL Work\n");
|
||||
t.write("old/last.bas", "PRINT \"LAST\"\n");
|
||||
let lib = t.0.join("old/Mixed.tbl");
|
||||
fs::write(&lib, &first).unwrap();
|
||||
let mak = t.write(
|
||||
"old/app.mak",
|
||||
"mixed.TBL\nlast.bas\nmain.bas\n' $STARTUP: \"main.bas\"\n",
|
||||
);
|
||||
let loader = SourceLoader::default();
|
||||
let input = loader.load(&mak).unwrap();
|
||||
assert_eq!(input.units.len(), 2);
|
||||
assert_eq!(input.libraries.len(), 1);
|
||||
let run = |code| {
|
||||
let mut vm = Vm::new(code);
|
||||
assert_eq!(
|
||||
vm.run(&mut tb_runtime::host::CaptureHost::default()),
|
||||
RunEvent::Ended
|
||||
);
|
||||
tb_runtime::snapshot::text(&vm.rt.screen)
|
||||
};
|
||||
let mut c = ProjectCompiler::default();
|
||||
assert_eq!(
|
||||
run(input.compile(&mut c, "APP").unwrap()),
|
||||
" 1 \n 1 \nLIB\nLAST\n"
|
||||
);
|
||||
fs::write(&lib, &second).unwrap();
|
||||
assert_eq!(
|
||||
run(input.compile(&mut c, "APP").unwrap()),
|
||||
" 2 \n 2 \nLIB\nLAST\n"
|
||||
);
|
||||
let body_only = library(2, 3);
|
||||
assert_eq!(second.len(), body_only.len());
|
||||
fs::write(&lib, &body_only).unwrap();
|
||||
assert_eq!(
|
||||
run(input.compile(&mut c, "APP").unwrap()),
|
||||
" 2 \n 3 \nLIB\nLAST\n"
|
||||
);
|
||||
assert_eq!(
|
||||
c.stats.compiled, 0,
|
||||
"Unveränderte Deklarationen dürfen Quellprodukte wiederverwenden"
|
||||
);
|
||||
fs::create_dir_all(t.0.join("save")).unwrap();
|
||||
let saved = t.0.join("save/app.mak");
|
||||
fs::write(&saved, input.manifest.text(&saved).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
identity(&loader.load(&saved).unwrap().libraries[0]).unwrap(),
|
||||
identity(&input.libraries[0]).unwrap()
|
||||
);
|
||||
fs::rename(t.0.join("old"), t.0.join("moved")).unwrap();
|
||||
assert_eq!(
|
||||
run(loader
|
||||
.load(&t.0.join("moved/app.mak"))
|
||||
.unwrap()
|
||||
.compile(&mut c, "APP")
|
||||
.unwrap()),
|
||||
" 2 \n 3 \nLIB\nLAST\n"
|
||||
);
|
||||
let moved = t.0.join("moved/app.mak");
|
||||
for text in [
|
||||
"Mixed.tbl\n' $STARTUP: \"Mixed.tbl\"",
|
||||
"Mixed.tbl\nmixed.TBL\n",
|
||||
"absent.tbl\n",
|
||||
] {
|
||||
assert!(Manifest::parse(&moved, text, &loader).is_err(), "{text}");
|
||||
}
|
||||
assert!(
|
||||
tb_vm::project_io::read_document(&t.0.join("moved/Mixed.tbl"))
|
||||
.unwrap_err()
|
||||
.contains("kein Textdokument")
|
||||
);
|
||||
fs::remove_file(t.0.join("moved/Mixed.tbl")).unwrap();
|
||||
let error = loader.load(&moved).unwrap_err();
|
||||
assert!(
|
||||
error.contains("app.mak") && error.contains("mixed.TBL"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user