376 lines
13 KiB
Rust
376 lines
13 KiB
Rust
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"
|
|
);
|
|
}
|