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 { 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::>() .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| { 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, "
", "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, "
", "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" ); }