use tb_frontend::{forms::FormCatalog, source::SourceUnit}; use tb_runtime::{host::CaptureHost, value::Value}; use tb_vm::{ interp::{RunEvent, Vm}, project::ProjectCompiler, }; fn vm(source: &str) -> Vm { let mut compiler = ProjectCompiler::default(); let code = compiler .compile( "TEST", &[SourceUnit::new("TEST", "test.bas", source)], &FormCatalog::default(), &[], ) .unwrap(); let mut vm = Vm::new(code); vm.debug.enabled = true; vm.debug.compiler = Some(compiler.debug_compiler()); vm } #[test] fn break_continue_loop_and_statement_steps() { let mut vm = vm("FOR i% = 1 TO 2\nx% = x% + 1: y% = y% + 1\nNEXT\nEND\n"); let mut host = CaptureHost::default(); assert!(vm.add_source_breakpoint(0, 0, 2)); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 }); assert!(matches!(vm.inspect("x%"), Some(Value::Int(0)))); vm.set_step(true); assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 2 }); assert!(matches!(vm.inspect("x%"), Some(Value::Int(1)))); vm.set_step(false); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 }); assert!(matches!(vm.inspect("y%"), Some(Value::Int(1)))); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("x%"), Some(Value::Int(2)))); } #[test] fn watches_immediate_byref_and_errors_preserve_continuation() { let mut vm=vm("DIM SHARED x AS INTEGER\nx = 3\nCALL work(x)\nEND\nSUB work(n AS INTEGER)\nDIM a(1 TO 2) AS INTEGER\na(1)=n\nn=n+1\nEND SUB\nSUB bump(n AS INTEGER)\nn=n+5\nEND SUB\n"); let mut host = CaptureHost::default(); assert!(vm.add_source_breakpoint(0, 0, 8)); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 8 }); let location = vm.debug_location().unwrap(); let frame = location.frame; for _ in 0..3 { assert!(matches!( vm.evaluate_watch(frame, "a(1) + CINT(2.5)").unwrap(), Value::Int(5) )); assert!(vm.evaluate_watch(frame, "RND").is_err()); assert_eq!(vm.debug_location().unwrap(), location); } assert!(vm.start_immediate(frame, "n =").is_err()); vm.start_immediate(frame, "n = 10: CALL bump(n): PRINT n") .unwrap(); assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 8 }); assert_eq!(vm.debug_location().unwrap(), location); assert!(matches!(vm.inspect("n"), Some(Value::Int(15)))); vm.start_immediate(frame, "ERROR 5").unwrap(); assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 8 }); assert!(vm.debug.immediate_error.is_some()); assert_eq!(vm.debug_location().unwrap(), location); vm.clear_source_breakpoints(); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("x"), Some(Value::Int(16)))); } #[test] fn immediate_handler_resume_and_break_on_errors() { let mut vm = vm("ON ERROR GOTO handler\nx%=1\nx%=x%+1\nEND\nhandler:\ny%=ERR\nRESUME NEXT\n"); let mut host = CaptureHost::default(); vm.add_source_breakpoint(0, 0, 3); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 3 }); let location = vm.debug_location().unwrap(); vm.debug.break_errors = true; vm.start_immediate(location.frame, "ERROR 5: x%=10") .unwrap(); assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. })); assert_eq!(vm.debug.error.as_ref().unwrap().code, 5); assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 3 }); assert_eq!(vm.debug_location().unwrap(), location); assert!(matches!(vm.inspect("y%"), Some(Value::Int(5)))); assert!(matches!(vm.inspect("x%"), Some(Value::Int(10)))); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("x%"), Some(Value::Int(11)))); } #[test] fn step_over_recursive_frames_history_and_safe_jump() { let mut vm=vm("CALL rec(2)\nx%=1\nx%=2\nEND\nSUB rec(n AS INTEGER)\nIF n>0 THEN\nCALL rec(n-1)\nEND IF\nPRINT n\nEND SUB\n"); let mut host = CaptureHost::default(); vm.debug.history_on = true; vm.add_source_breakpoint(0, 0, 7); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 7 }); let first = vm.debug_location().unwrap(); vm.clear_source_breakpoints(); vm.step_over(); assert!(matches!(vm.run(&mut host), RunEvent::Stepped { .. })); assert_eq!(vm.debug_location().unwrap().frame, first.frame); vm.set_step(false); vm.run_to_cursor(0, 0, 2).unwrap(); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 }); let at = vm.debug_location().unwrap(); vm.set_next_statement(at.frame, 0, 3).unwrap(); let moved = vm.debug_location().unwrap(); assert!(vm.set_next_statement(at.frame, 0, 7).is_err()); assert_eq!(vm.debug_location().unwrap(), moved); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("x%"), Some(Value::Int(2)))); assert!(!vm.debug.history.is_empty()); } #[test] fn include_identity_qualified_globals_udts_and_older_frames() { use tb_frontend::source::SourceSegment; let mut compiler = ProjectCompiler::default(); let first=SourceUnit{name:"FIRST".into(),segments:vec![SourceSegment{file:"one.bi".into(),first_line:1,text:"'one\nx%=1\n".into()},SourceSegment{file:"two.bi".into(),first_line:1,text:"'two\nx%=2\nCALL rec(2)\nEND\nSUB rec(n AS INTEGER)\nDIM a(1 TO 2) AS INTEGER\na(1)=n\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n".into()}]}; let second=SourceUnit::new("SECOND","second.bas","TYPE pair\nvalue AS INTEGER\nEND TYPE\nDIM SHARED r AS pair\nDIM SHARED a(1 TO 2) AS INTEGER\n"); let code = compiler .compile("P", &[first, second], &FormCatalog::default(), &[]) .unwrap(); let mut vm = Vm::new(code); vm.debug.compiler = Some(compiler.debug_compiler()); vm.add_source_breakpoint(0, 1, 2); let mut host = CaptureHost::default(); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 }); assert_eq!(vm.current_file(), "two.bi"); assert!(matches!(vm.inspect("x%"), Some(Value::Int(1)))); vm.clear_source_breakpoints(); vm.add_source_breakpoint(0, 1, 9); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 9 }); let frames = vm.debug_frames(); assert!(frames.len() >= 4); let top = vm.debug_location(); assert!(matches!( vm.evaluate_watch(frames[0].id, "a(1)").unwrap(), Value::Int(0) )); assert!(matches!( vm.evaluate_watch(frames[1].id, "a(1)").unwrap(), Value::Int(1) )); assert!(matches!( vm.evaluate_watch(frames[1].id, "SECOND!r.value + SECOND!a(1)") .unwrap(), Value::Int(0) )); assert_eq!(vm.debug_location(), top); vm.start_immediate(frames[1].id, "SECOND!r.value=7: SECOND!a(1)=8") .unwrap(); assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. })); assert!(matches!( vm.evaluate_watch(frames[1].id, "SECOND!r.value + SECOND!a(1)") .unwrap(), Value::Int(15) )); } #[test] fn watchpoint_history_ring_and_error_differential() { use tb_vm::interp::DebugWatch; let mut vm = vm("FOR i%=1 TO 1500\nx%=i%\nNEXT\nEND\n"); vm.debug.enabled = true; vm.debug.history_on = true; vm.debug.watches.push(DebugWatch { expression: "x%=2".into(), condition: true, frame: None, value: Err("pending".into()), }); let mut host = CaptureHost::default(); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 3 }); assert!(matches!(vm.inspect("x%"), Some(Value::Int(2)))); vm.debug.watches.clear(); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(vm.debug.history.len(), 1024); let source="10 ON ERROR GOTO handler\n20 ERROR 6\n30 x%=x%+1\n40 END\nhandler:\ne%=ERR: l&=ERL\nRESUME NEXT\n"; let run = |break_errors| { let mut vm = self::vm(source); vm.debug.break_errors = break_errors; let mut host = CaptureHost::default(); if break_errors { assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. })); assert_eq!(vm.debug.error.as_ref().unwrap().code, 6); assert!(matches!(vm.inspect("e%"), Some(Value::Int(0)))); } assert_eq!(vm.run(&mut host), RunEvent::Ended); format!( "{:?}{:?}{:?}", vm.inspect("e%"), vm.inspect("l&"), vm.inspect("x%") ) }; assert_eq!(run(true), run(false)); } #[test] fn gosub_step_over_breakpoint_priority_and_temporary_target_cleanup() { let mut vm = vm("GOSUB work\nx%=4\nEND\nwork:\nx%=1\nx%=2\nRETURN\n"); let mut host = CaptureHost::default(); vm.set_step(true); assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 1 }); vm.step_over(); vm.add_source_breakpoint(0, 0, 6); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 6 }); vm.clear_source_breakpoints(); vm.set_step(false); assert_eq!(vm.run(&mut host), RunEvent::Ended); let mut vm = self::vm("FOR i%=1 TO 2\nx%=x%+1\ny%=y%+1\nNEXT\nEND\n"); vm.add_source_breakpoint(0, 0, 2); vm.run_to_cursor(0, 0, 3).unwrap(); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 }); vm.clear_source_breakpoints(); assert_eq!(vm.run(&mut host), RunEvent::Ended); } #[test] fn immediate_wait_interrupt_nested_error_end_and_run() { use tb_vm::interp::PollResult; let source="x%=1\nx%=x%+1\nEND\nSUB ask(n AS INTEGER)\nINPUT n\nEND SUB\nSUB fail()\nON LOCAL ERROR GOTO handler\nERROR 6\nEXIT SUB\nhandler:\nx%=ERR\nRESUME NEXT\nEND SUB\nSUB done()\nEND\nEND SUB\nSUB restart()\nRUN\nEND SUB\n"; let mut vm = vm(source); let mut host = CaptureHost::default(); vm.add_source_breakpoint(0, 0, 2); vm.run(&mut host); let location = vm.debug_location().unwrap(); vm.start_immediate(location.frame, "CALL ask(x%)").unwrap(); assert!(matches!( vm.poll(&mut host, 1000), PollResult::Waiting { .. } )); assert!(vm.immediate_active()); vm.rt.abbruch = true; assert!(matches!( vm.poll(&mut host, 1000), PollResult::Event(RunEvent::Interrupted { .. }) )); vm.rt.abbruch = false; host.tippe("9\r"); assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. })); assert_eq!(vm.debug_location().unwrap(), location); assert!(matches!(vm.inspect("x%"), Some(Value::Int(9)))); vm.start_immediate(location.frame, "CALL fail()").unwrap(); assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. })); assert_eq!(vm.debug_location().unwrap(), location); vm.start_immediate(location.frame, "CALL restart()") .unwrap(); assert_eq!( vm.run(&mut host), RunEvent::Restart { program: None, line: None } ); let mut vm = self::vm(source); vm.add_source_breakpoint(0, 0, 2); vm.run(&mut host); let frame = vm.debug_location().unwrap().frame; vm.start_immediate(frame, "CALL done()").unwrap(); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(vm.is_terminated()); } #[test] fn form_event_breakpoint_and_step_over_keep_the_event_frame() { use tb_frontend::forms::ObjectClass; let mut catalog = FormCatalog::default(); catalog.add("Form1", ObjectClass::Form, None, false); let source="Form1.Show\nx%=1\nEND\nSUB Form_Load()\nCALL bump\nPRINT 4\nEND SUB\nSUB bump()\nPRINT 3\nEND SUB\n"; let mut compiler = ProjectCompiler::default(); let code = compiler .compile( "Form1", &[SourceUnit::new("Form1", "form1.frm", source)], &catalog, &[], ) .unwrap(); let mut vm = Vm::new(code); vm.debug.compiler = Some(compiler.debug_compiler()); vm.add_source_breakpoint(0, 0, 5); let mut host = CaptureHost::default(); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 5 }); let frame = vm.debug_location().unwrap().frame; vm.clear_source_breakpoints(); vm.step_over(); assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 6 }); assert_eq!(vm.debug_location().unwrap().frame, frame); vm.add_source_breakpoint(0, 0, 2); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 }); vm.clear_source_breakpoints(); assert_eq!(vm.run(&mut host), RunEvent::Ended); } #[test] fn set_next_rejects_loop_gosub_handler_and_pending_operations_atomically() { let source = "x%=1\nFOR i%=1 TO 2\nx%=x%+1\nNEXT\nGOSUB worker\nEND\nworker:\nx%=3\nRETURN\n"; let mut vm = vm(source); let mut host = CaptureHost::default(); vm.add_source_breakpoint(0, 0, 1); vm.run(&mut host); let before = vm.debug_location().unwrap(); for line in [3, 4, 8] { assert!( vm.set_next_statement(before.frame, 0, line).is_err(), "line {line}" ); assert_eq!(vm.debug_location().unwrap(), before); } vm.clear_source_breakpoints(); vm.add_source_breakpoint(0, 0, 8); vm.run(&mut host); let before = vm.debug_location().unwrap(); assert!(vm.set_next_statement(before.frame, 0, 6).is_err()); assert_eq!(vm.debug_location().unwrap(), before); } #[test] fn same_line_calls_and_immediate_do_not_retrigger_a_line_breakpoint() { let mut vm = vm("x%=1: CALL bump(x%): x%=x%+1\nEND\nSUB bump(n AS INTEGER)\nn=n+1\nEND SUB\n"); vm.debug.history_on = true; vm.add_source_breakpoint(0, 0, 1); let mut host = CaptureHost::default(); assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 1 }); assert!(vm.debug.history.is_empty()); let frame = vm.debug_location().unwrap().frame; vm.start_immediate(frame, "x%=5").unwrap(); assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 1 }); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("x%"), Some(Value::Int(3)))); assert_eq!( vm.debug .history .iter() .filter(|p| p.procedure == 0 && p.line == 1) .count(), 3 ); } #[test] fn immediate_handler_exit_and_cached_new_imports_are_safe() { let mut compiler = ProjectCompiler::default(); compiler.debug_symbols = true; let first = SourceUnit::new("ONE", "one.bas", "x%=1\nSTOP\nx%=x%+1\nEND\n"); let old = SourceUnit::new("TWO", "two.bas", "SUB oldproc()\nPRINT 1\nEND SUB\n"); compiler .compile("P", &[first.clone(), old], &FormCatalog::default(), &[]) .unwrap(); let second = SourceUnit::new( "TWO", "two.bas", "SUB newproc(n AS INTEGER)\nn=n+5\nEND SUB\n", ); let code = compiler .compile("P", &[first, second], &FormCatalog::default(), &[]) .unwrap(); let mut vm = Vm::new(code); vm.debug.compiler = Some(compiler.debug_compiler()); let mut host = CaptureHost::default(); vm.run(&mut host); let frame = vm.debug_location().unwrap().frame; assert_eq!(vm.execution_location().unwrap().line, 3); vm.start_immediate(frame, "CALL newproc(x%)").unwrap(); assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. })); assert!(matches!(vm.inspect("ONE!x%"), Some(Value::Int(6)))); let mut vm=self::vm("CALL work\nEND\nSUB work()\nON LOCAL ERROR GOTO handler\nx%=1\nx%=2\nEXIT SUB\nhandler:\nx%=7\nEND SUB\n"); vm.add_source_breakpoint(0, 0, 6); vm.run(&mut host); let before = vm.debug_location().unwrap(); vm.start_immediate(before.frame, "ERROR 5").unwrap(); assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. })); assert_eq!(vm.debug_location().unwrap(), before); assert!(matches!(vm.inspect("x%"), Some(Value::Int(7)))); vm.clear_source_breakpoints(); assert_eq!(vm.run(&mut host), RunEvent::Ended); } #[test] fn set_next_rejects_else_and_case_entry_but_allows_peer_statements() { let source="x%=1\nIF x% THEN\nx%=2\nELSE\nx%=3\nEND IF\nSELECT CASE x%\nCASE 1\nx%=4\nCASE ELSE\nx%=5\nEND SELECT\nx%=6\nEND\n"; let mut vm = vm(source); let mut host = CaptureHost::default(); vm.add_source_breakpoint(0, 0, 1); vm.run(&mut host); let before = vm.debug_location().unwrap(); for line in [3, 5, 9, 11] { assert!( vm.set_next_statement(before.frame, 0, line).is_err(), "line {line}" ); assert_eq!(vm.debug_location().unwrap(), before); } vm.set_next_statement(before.frame, 0, 13).unwrap(); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert!(matches!(vm.inspect("x%"), Some(Value::Int(6)))); } #[test] fn watches_never_initialize_arrays_or_run_user_functions_and_keep_selected_context() { let mut vm=vm("DIM SHARED changed%\nSTOP\nx%=a%(2)\nCALL rec(2)\nEND\nFUNCTION impure%()\nchanged%=changed%+1\nimpure%=changed%\nEND FUNCTION\nSUB rec(n AS INTEGER)\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n"); let mut host = CaptureHost::default(); vm.run(&mut host); let frame = vm.debug_location().unwrap().frame; let frames = format!("{:?}", vm.debug_frames()); for _ in 0..4 { assert!(vm.evaluate_watch(frame, "a%(2)").is_err()); assert!(matches!(vm.inspect("a%"), Some(Value::Empty))); assert!(vm.evaluate_watch(frame, "impure%").is_err()); assert!(matches!(vm.inspect("changed%"), Some(Value::Int(0)))); assert!(vm.evaluate_watch(frame, "1 / 0").is_err()); assert!(matches!( vm.evaluate_watch(frame, "ERR").unwrap(), Value::Lng(0) )); } assert_eq!(format!("{:?}", vm.debug_frames()), frames); vm.add_source_breakpoint(0, 0, 12); vm.run(&mut host); let frames = vm.debug_frames(); let older = frames[1].id; vm.clear_source_breakpoints(); vm.debug.watches.push(tb_vm::interp::DebugWatch { expression: "n=1".into(), condition: true, frame: Some(older), value: Err("pending".into()), }); assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. })); assert!(matches!(vm.debug.watches[0].value, Ok(Value::Int(-1)))); }