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"); }