//! Interpreter-Tests: Spec-Szenarien aus vm-ausfuehrung und //! vm-fehlerbehandlung (Phase-2-Änderung). use tb_runtime::host::CaptureHost; use tb_runtime::value::Value; use tb_vm::interp::{RunEvent, Vm}; fn run(src: &str) -> (RunEvent, String) { run_with_input(src, &[]) } fn run_with_input(src: &str, input: &[&str]) -> (RunEvent, String) { let module = tb_vm::compile_source("TEST", src).unwrap_or_else(|d| { panic!("Compile-Fehler: {d:?}"); }); let mut vm = Vm::new(module); let mut host = CaptureHost::with_input(input); let ev = vm.run(&mut host); (ev, host.output) } fn out(src: &str) -> String { let (ev, output) = run(src); assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{output}"); output } fn err_code(src: &str) -> u16 { match run(src).0 { RunEvent::Error { code, .. } => code, other => panic!("Fehler erwartet, war {other:?}"), } } // ---- 5.1 Ausdrücke und Konvertierungsmatrix -------------------------------- #[test] fn print_hallo_welt() { assert_eq!(out("PRINT \"Hallo, Welt!\"\nEND"), "Hallo, Welt!\n"); } #[test] fn banker_rounding_cint() { // Spec-Szenario: PRINT CINT(0.5); CINT(1.5); CINT(2.5) → " 0 2 2 " assert_eq!(out("PRINT CINT(0.5); CINT(1.5); CINT(2.5)"), " 0 2 2 \n"); } #[test] fn overflow_bei_zuweisung() { // Spec-Szenario: INTEGER-Variable = 40000 → Fehler 6 assert_eq!(err_code("i% = 40000"), 6); } #[test] fn intdiv_rundet_operanden_vor() { // Spec-Szenario: PRINT 7.5 \ 2 → 8 \ 2 = 4 assert_eq!(out("PRINT 7.5 \\ 2"), " 4 \n"); } #[test] fn gemischte_arithmetik() { assert_eq!(out("i% = 2\nd# = i% + 1.5#\nPRINT d#"), " 3.5 \n"); assert_eq!(out("PRINT 1 / 3"), " .3333333 \n"); // SINGLE-Division assert_eq!(out("PRINT 7 MOD 3; -7 MOD 3"), " 1 -1 \n"); // Vorzeichen wie Dividend assert_eq!(out("PRINT 2 ^ 10"), " 1024 \n"); } #[test] fn logik_bitweise() { assert_eq!(out("PRINT 6 AND 3; 6 OR 3; 6 XOR 3; NOT 0"), " 2 7 5 -1 \n"); // Operanden werden gerundet: 1.5 AND 1 → 2 AND 1 = 0 assert_eq!(out("PRINT 1.5 AND 1"), " 0 \n"); } #[test] fn division_durch_null() { assert_eq!(err_code("PRINT 1 / 0"), 11); assert_eq!(err_code("PRINT 1 \\ 0"), 11); assert_eq!(err_code("PRINT 0 ^ -1"), 11); } #[test] fn stringvergleich_und_verkettung() { assert_eq!(out("PRINT \"a\" + \"b\""), "ab\n"); assert_eq!(out("PRINT (\"abc\" < \"abd\")"), "-1 \n"); } // ---- 5.2 Kontrollfluss ------------------------------------------------------- #[test] fn for_ohne_durchlauf() { // Spec-Szenario: FOR i% = 3 TO 1 → Körper wird nicht betreten assert_eq!(out("FOR i% = 3 TO 1\nPRINT i%\nNEXT\nPRINT \"ende\""), "ende\n"); } #[test] fn for_mit_negativem_step() { assert_eq!(out("FOR i% = 3 TO 1 STEP -1\nPRINT i%;\nNEXT\nPRINT"), " 3 2 1 \n"); } #[test] fn for_mit_dynamischem_step() { assert_eq!( out("s% = -2\nFOR i% = 5 TO 1 STEP s%\nPRINT i%;\nNEXT\nPRINT"), " 5 3 1 \n" ); } #[test] fn select_case_bereiche() { let src = "FOR i% = 1 TO 4\nSELECT CASE i%\nCASE 1: PRINT \"eins\"\nCASE 2 TO 3: PRINT \"mittel\"\nCASE ELSE: PRINT \"rest\"\nEND SELECT\nNEXT"; assert_eq!(out(src), "eins\nmittel\nmittel\nrest\n"); } #[test] fn do_loop_varianten() { assert_eq!( out("n% = 3\nDO WHILE n% > 0\nPRINT n%;\nn% = n% - 1\nLOOP\nPRINT"), " 3 2 1 \n" ); assert_eq!( out("n% = 0\nDO\nn% = n% + 1\nLOOP UNTIL n% >= 3\nPRINT n%"), " 3 \n" ); assert_eq!( out("n% = 5\nWHILE n% > 3\nn% = n% - 1\nWEND\nPRINT n%"), " 3 \n" ); } #[test] fn exit_for_und_do() { assert_eq!( out("FOR i% = 1 TO 10\nIF i% = 3 THEN EXIT FOR\nNEXT\nPRINT i%"), " 3 \n" ); } #[test] fn kontrollfluss_korpusdatei() { // 5.2-Verifikation: kontrollfluss.bas byte-genau korrekt. let src = std::fs::read_to_string( std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.bas"), ) .unwrap(); let want = std::fs::read_to_string( std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/compat/kontrollfluss.out"), ) .unwrap(); assert_eq!(out(&src), want); } #[test] fn gosub_return_und_fehler_3() { assert_eq!( out("GOSUB U\nPRINT \"zurueck\"\nEND\nU:\nPRINT \"unten\"\nRETURN"), "unten\nzurueck\n" ); // Spec-Szenario: RETURN ohne GOSUB → Fehler 3 assert_eq!(err_code("RETURN"), 3); } #[test] fn on_goto_berechnet() { let src = "FOR i% = 0 TO 3\nON i% GOTO A, B\nPRINT \"kein\";\nGOTO W\nA:\nPRINT \"a\";\nGOTO W\nB:\nPRINT \"b\";\nW:\nNEXT\nPRINT"; assert_eq!(out(src), "keinabkein\n"); } // ---- 5.3 Prozeduren ------------------------------------------------------------ #[test] fn byref_wirkt_zurueck() { // Spec-Szenario BYREF assert_eq!( out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc n%\nPRINT n%"), " 2 \n" ); } #[test] fn klammern_erzwingen_byval() { // Spec-Szenario BYVAL assert_eq!( out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nn% = 1\nInc (n%)\nPRINT n%"), " 1 \n" ); } #[test] fn byref_auf_arrayelement() { assert_eq!( out("SUB Inc (x%)\nx% = x% + 1\nEND SUB\nDIM a%(5)\na%(2) = 7\nInc a%(2)\nPRINT a%(2)"), " 8 \n" ); } #[test] fn function_und_rekursion() { assert_eq!( out("FUNCTION Quad (x)\nQuad = x * x\nEND FUNCTION\nPRINT Quad(3)"), " 9 \n" ); let fak = "FUNCTION Fak& (n%)\nIF n% <= 1 THEN\nFak& = 1\nELSE\nFak& = n% * Fak&(n% - 1)\nEND IF\nEND FUNCTION\nPRINT Fak&(10)"; assert_eq!(out(fak), " 3628800 \n"); } #[test] fn static_behaelt_werte() { let src = "SUB Zaehl\nSTATIC n%\nn% = n% + 1\nPRINT n%;\nEND SUB\nZaehl\nZaehl\nZaehl\nPRINT"; assert_eq!(out(src), " 1 2 3 \n"); } #[test] fn def_fn_im_modulkontext() { // DEF FN: Parameter lokal (BYVAL), freie Namen binden an Modulvariablen. let src = "faktor = 10\nDEF FNmal (x) = x * faktor\nPRINT FNmal(3)"; assert_eq!(out(src), " 30 \n"); } #[test] fn ganzes_array_uebergeben() { let src = "SUB Summe (a%(), s%)\ns% = 0\nFOR i% = LBOUND(a%) TO UBOUND(a%)\ns% = s% + a%(i%)\nNEXT\nEND SUB\nDIM w%(3)\nFOR i% = 0 TO 3\nw%(i%) = i%\nNEXT\nSumme w%(), erg%\nPRINT erg%"; assert_eq!(out(src), " 6 \n"); } #[test] fn udt_wertsemantik_und_felder() { let src = "TYPE Punkt\nx AS INTEGER\ny AS INTEGER\nEND TYPE\nDIM a AS Punkt, b AS Punkt\na.x = 1\nb = a\nb.x = 9\nPRINT a.x; b.x"; assert_eq!(out(src), " 1 9 \n"); } // ---- 5.4 DATA/READ/RESTORE ----------------------------------------------------- #[test] fn data_read_restore() { // Anmerkung: unquotierte DATA-Texte verlieren derzeit die // Groß-/Kleinschreibung (Lexer normalisiert Bezeichner); // Rohtext-Erhalt ist als Aufgabe in PLAN.md Phase 3 eingeplant. let src = "DATA 1, 2.5, \"hallo\"\nREAD a%, b!, c$\nPRINT a%; b!; c$\nRESTORE\nREAD x%\nPRINT x%"; assert_eq!(out(src), " 1 2.5 hallo\n 1 \n"); } #[test] fn out_of_data_fehler_4() { assert_eq!(err_code("DATA 1\nREAD a%, b%"), 4); } #[test] fn data_typkonflikt_fehler_13() { assert_eq!(err_code("DATA hallo\nREAD a%"), 13); } #[test] fn restore_mit_label() { let src = "DATA 1\nMarke:\nDATA 2\nREAD a%\nRESTORE Marke\nREAD b%\nPRINT a%; b%"; assert_eq!(out(src), " 1 2 \n"); } // ---- 5.5 Fehlerbehandlung ------------------------------------------------------- #[test] fn modulweiter_handler_faengt_prozedurfehler() { // Spec-Szenario: Hauptprogramm setzt Handler, SUB löst Fehler 6 aus. let src = "SUB Knall\ni% = 40000\nEND SUB\nON ERROR GOTO Fehler\nKnall\nPRINT \"nie\"\nEND\nFehler:\nPRINT \"ERR=\"; ERR\nEND"; assert_eq!(out(src), "ERR= 6 \n"); } #[test] fn lokaler_handler_verdeckt_modulweiten() { let src = "SUB Tu\nON LOCAL ERROR GOTO L\nERROR 5\nEXIT SUB\nL:\nPRINT \"lokal\"; ERR\nRESUME Weiter\nWeiter:\nEND SUB\nON ERROR GOTO M\nTu\nEND\nM:\nPRINT \"modul\"\nEND"; assert_eq!(out(src), "lokal 5 \n"); } #[test] fn ohne_handler_bricht_ab() { // Spec-Szenario: Fehler 9 ohne Handler → Abbruch mit Meldung. let (ev, _) = run("DIM a%(3)\nPRINT a%(7)"); match ev { RunEvent::Error { code, message, .. } => { assert_eq!(code, 9); assert_eq!(message, "Subscript out of range"); } other => panic!("{other:?}"), } } #[test] fn erl_liefert_zeilennummer() { let src = "ON ERROR GOTO H\n10 ERROR 5\nEND\nH:\nPRINT ERL\nEND"; assert_eq!(out(src), " 10 \n"); } #[test] fn erl_null_ohne_zeilennummern() { // Spec-Szenario: keine numerischen Zeilennummern → ERL = 0 let src = "ON ERROR GOTO H\nERROR 5\nEND\nH:\nPRINT ERL\nEND"; assert_eq!(out(src), " 0 \n"); } #[test] fn error_anweisung() { // Spec-Szenario: ERROR 53 → Handler mit ERR = 53 let src = "ON ERROR GOTO H\nERROR 53\nEND\nH:\nPRINT ERR\nEND"; assert_eq!(out(src), " 53 \n"); } #[test] fn resume_wiederholt_anweisung() { let src = "ON ERROR GOTO H\nn% = 0\nversuch% = 0\n10 versuch% = versuch% + 1\nIF versuch% < 3 THEN ERROR 5\nPRINT versuch%\nEND\nH:\nRESUME"; // RESUME wiederholt die IF-Anweisung; versuch% bleibt 1? Nein: // Fehler in IF-Zeile, RESUME wiederholt IF — Endlosschleife ohne // Zählerénderung wäre falsch. Zähler steht in Zeile 10, daher: der // Fehler passiert im IF, RESUME wiederholt das IF, versuch% ist noch // < 3 … Um Determinismus zu sichern, zählt der Handler mit. let _ = src; let src2 = "ON ERROR GOTO H\nversuch% = 0\nERROR 5\nPRINT \"nach\"; versuch%\nEND\nH:\nversuch% = versuch% + 1\nIF versuch% < 3 THEN RESUME\nRESUME NEXT"; assert_eq!(out(src2), "nach 3 \n"); } #[test] fn resume_next_faehrt_fort() { // Spec-Szenario: RESUME NEXT nach Division durch 0. let src = "ON ERROR GOTO H\nx = 1 / 0\nPRINT \"weiter\"\nEND\nH:\nRESUME NEXT"; assert_eq!(out(src), "weiter\n"); } #[test] fn resume_ohne_fehler_20() { assert_eq!(err_code("RESUME"), 20); } #[test] fn fehler_im_handler_ist_fatal() { // Spec-Szenario: kein Kaskadieren. let src = "ON ERROR GOTO H\nERROR 5\nEND\nH:\ni% = 40000\nEND"; let (ev, _) = run(src); match ev { RunEvent::Error { code, .. } => assert_eq!(code, 6), other => panic!("{other:?}"), } } #[test] fn on_error_goto_0_deaktiviert() { let src = "ON ERROR GOTO H\nON ERROR GOTO 0\nERROR 5\nEND\nH:\nPRINT \"nie\"\nEND"; assert_eq!(err_code(src), 5); } // ---- 5.6 Unterbrechbarkeit ------------------------------------------------------- #[test] fn breakpoint_haelt_an_und_setzt_fort() { let module = tb_vm::compile_source("TEST", "a% = 1\nb% = 2\nc% = 3\nPRINT a% + b% + c%").unwrap(); let mut vm = Vm::new(module); let mut host = CaptureHost::default(); vm.add_breakpoint(3); // Spec-Szenario: hält VOR der Anweisung in Zeile 3. match vm.run(&mut host) { RunEvent::Breakpoint { line } => assert_eq!(line, 3), other => panic!("{other:?}"), } // Spec-Szenario Inspektion: b% ist gesetzt, c% noch 0. assert!(matches!(vm.inspect("b%"), Some(Value::Int(2)))); assert!(matches!(vm.inspect("c%"), Some(Value::Int(0)))); // Fortsetzen (Breakpoint entfernen, sonst hält Zeile 3 erneut). vm.remove_breakpoint(3); assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(host.output, " 6 \n"); } #[test] fn einzelschritt() { let module = tb_vm::compile_source("TEST", "a% = 1\nb% = 2\nEND").unwrap(); let mut vm = Vm::new(module); let mut host = CaptureHost::default(); vm.set_step(true); let mut lines = Vec::new(); loop { match vm.run(&mut host) { RunEvent::Stepped { line } => lines.push(line), RunEvent::Ended => break, other => panic!("{other:?}"), } } assert_eq!(lines, vec![1, 2, 3]); } // ---- 5.7 Programmende -------------------------------------------------------------- #[test] fn stop_liefert_zeile_und_ist_fortsetzbar() { let module = tb_vm::compile_source("TEST", "PRINT \"a\"\nSTOP\nPRINT \"b\"").unwrap(); let mut vm = Vm::new(module); let mut host = CaptureHost::default(); match vm.run(&mut host) { RunEvent::Stopped { line } => assert_eq!(line, 2), other => panic!("{other:?}"), } // IDE-Semantik: CONT = weiterlaufen. assert_eq!(vm.run(&mut host), RunEvent::Ended); assert_eq!(host.output, "a\nb\n"); } #[test] fn end_und_system() { assert_eq!(run("PRINT \"x\"\nEND").0, RunEvent::Ended); assert_eq!(run("SYSTEM").0, RunEvent::Ended); } // ---- Eingabe ----------------------------------------------------------------------- #[test] fn input_mit_redo() { let (ev, output) = run_with_input( "INPUT \"Zahl\"; n%\nPRINT n% * 2", &["abc", "21"], ); assert_eq!(ev, RunEvent::Ended); assert!(output.contains("Redo from start")); assert!(output.ends_with(" 42 \n"), "{output}"); } #[test] fn line_input_liest_ganze_zeile() { let (ev, output) = run_with_input("LINE INPUT s$\nPRINT s$", &["a, b, c"]); assert_eq!(ev, RunEvent::Ended); assert!(output.ends_with("a, b, c\n")); } // ---- Strings/PRINT über die VM ------------------------------------------------------- #[test] fn mid_anweisung_mutiert() { assert_eq!(out("s$ = \"hallo\"\nMID$(s$, 2, 2) = \"EY\"\nPRINT s$"), "hEYlo\n"); } #[test] fn print_zonen_und_tab() { assert_eq!(out("PRINT \"a\", \"b\""), "a b\n"); assert_eq!(out("PRINT TAB(5); \"x\""), " x\n"); assert_eq!(out("PRINT \"a\"; SPC(3); \"b\""), "a b\n"); } #[test] fn unsupported_feature_fehler_73() { // Dokumentiert, aber Phase 3: Datei-E/A → Laufzeitfehler 73. let (ev, _) = run("OPEN \"x.txt\" FOR INPUT AS #1"); match ev { RunEvent::Error { code, message, .. } => { assert_eq!(code, 73); // Katalogtext des Vorbilds (VBDOS) für Code 73: assert_eq!(message, "Feature unavailable"); } other => panic!("{other:?}"), } }