//! 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 { files .iter() .map(|(name, code)| SourceUnit::new(name, &format!("{name}.bas"), code)) .collect() } fn compile(files: &[(&str, &str)]) -> Result> { 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:?}" ); }