import random import unittest from io import StringIO from unittest.mock import patch from parameterized import parameterized from centvrion.ast_nodes import ( Bool, BinOp, BuiltIn, DataArray, DataRangeArray, Defini, Designa, DumStatement, Erumpe, ExpressionStatement, ID, Invoca, ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement, String, num_to_int, int_to_num, make_string, ) from centvrion.lexer import Lexer from centvrion.parser import Parser from centvrion.values import ValInt, ValStr, ValBool, ValList, ValNul, ValFunc def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]): random.seed(1) lexer = Lexer().get_lexer() tokens = lexer.lex(source + "\n") program = Parser().parse(tokens) ########################## ####### Parser Test ###### (commented out — no __eq__ on AST nodes yet) ########################## # self.assertEqual( # program, # target_nodes, # f"Parser test:\n{program}\n{target_nodes}" # ) ########################## #### Interpreter Test #### ########################## captured = StringIO() try: if input_lines: inputs = iter(input_lines) with patch("builtins.input", lambda: next(inputs)), patch("sys.stdout", captured): result = program.eval() else: with patch("sys.stdout", captured): result = program.eval() except Exception as e: raise e self.assertEqual(result, target_value, "Return value test") self.assertEqual(captured.getvalue(), target_output, "Output test") ########################## ###### Printer Test ###### (commented out — no print() on AST nodes yet) ########################## # try: # new_text = program.print() # new_tokens = Lexer().get_lexer().lex(new_text + "\n") # new_nodes = Parser().parse(new_tokens) # except Exception as e: # raise Exception(f"###Printer test###\n{new_text}") from e # self.assertEqual( # program, # new_nodes, # f"Printer test\n{source}\n{new_text}" # ) ########################## ###### Compiler Test ##### ########################## # try: # bytecode = program.compile() # ... # except Exception as e: # raise Exception("###Compiler test###") from e # --- Output --- output_tests = [ ("DICE(\"hello\")", None, ValStr("hello"), "hello\n"), ("DICE(\"world\")", None, ValStr("world"), "world\n"), ("DICE(III)", None, ValStr("III"), "III\n"), ("DICE(X)", None, ValStr("X"), "X\n"), ("DICE(MMXXV)", None, ValStr("MMXXV"), "MMXXV\n"), ("DICE('hello')", None, ValStr("hello"), "hello\n"), ("DICE('world')", None, ValStr("world"), "world\n"), ("DICE(\"a\", \"b\")", None, ValStr("a b"), "a b\n"), ("DICE(\"line one\")\nDICE(\"line two\")", None, ValStr("line two"), "line one\nline two\n"), ("DICE(DICE(II))", None, ValStr("II"), "II\nII\n"), ] class TestOutput(unittest.TestCase): @parameterized.expand(output_tests) def test_output(self, source, nodes, value, output): run_test(self, source, nodes, value, output) # --- Arithmetic --- arithmetic_tests = [ ("I + I", None, ValInt(2)), ("X - III", None, ValInt(7)), ("III * IV", None, ValInt(12)), ("X / II", None, ValInt(5)), ("X / III", None, ValInt(3)), # integer division: 10 // 3 = 3 ("II + III * IV", None, ValInt(14)), # precedence: 2 + (3*4) = 14 ("(II + III) * IV", None, ValInt(20)), # parens: (2+3)*4 = 20 ] class TestArithmetic(unittest.TestCase): @parameterized.expand(arithmetic_tests) def test_arithmetic(self, source, nodes, value): run_test(self, source, nodes, value) # --- Assignment --- assignment_tests = [ ("DESIGNA x VT III\nx", None, ValInt(3)), ("DESIGNA msg VT \"hello\"\nmsg", None, ValStr("hello")), ("DESIGNA msg VT 'hello'\nmsg", None, ValStr("hello")), ("DESIGNA a VT V\nDESIGNA b VT X\na + b", None, ValInt(15)), ("DESIGNA x VT II\nDESIGNA x VT x + I\nx", None, ValInt(3)), ] class TestAssignment(unittest.TestCase): @parameterized.expand(assignment_tests) def test_assignment(self, source, nodes, value): run_test(self, source, nodes, value) # --- Control flow --- control_tests = [ # SI without ALVID — true branch ("SI VERITAS TVNC { DESIGNA r VT I }\nr", None, ValInt(1)), # SI without ALVID — false branch ("SI FALSITAS TVNC { DESIGNA r VT I }", None, ValNul()), # SI with ALVID — true branch ("SI VERITAS TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(1)), # SI with ALVID — false branch ("SI FALSITAS TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(2)), # SI with comparison — equal ("SI I EST I TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(1)), # SI with comparison — unequal ("SI I EST II TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(2)), # SI MINVS ("SI I MINVS II TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(1)), # SI PLVS ("SI II PLVS I TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(1)), # ALVID SI chain ( "SI I EST II TVNC { DESIGNA r VT I } ALVID SI I EST I TVNC { DESIGNA r VT II } ALVID { DESIGNA r VT III }\nr", None, ValInt(2), ), # DVM (while not): loops until condition is true ( "DESIGNA x VT I\nDVM x EST III FACE {\nDESIGNA x VT x + I\n}\nx", None, ValInt(3), ), # DVM with ERVMPE — loop body prints (testing DICE + ERVMPE together) ("DESIGNA x VT I\nDVM FALSITAS FACE {\nDICE(x)\nERVMPE\n}", None, ValStr("I"), "I\n"), # PER foreach ("PER i IN [(I, II, III)] FACE { DICE(i) }", None, ValStr("III"), "I\nII\nIII\n"), # DONICVM range loop ("DONICVM i VT I VSQVE V FACE { DICE(i) }", None, ValStr("IV"), "I\nII\nIII\nIV\n"), ] class TestControl(unittest.TestCase): @parameterized.expand(control_tests) def test_control(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) # --- Functions --- function_tests = [ ( "DEFINI bis (n) VT { REDI (n * II) }\nINVOCA bis (III)", None, ValInt(6), ), ( "DEFINI add (a, b) VT { REDI (a + b) }\nINVOCA add (III, IV)", None, ValInt(7), ), # Fibonacci: fib(n<3)=1, fib(n)=fib(n-1)+fib(n-2) ( "DEFINI fib (n) VT {\nSI n MINVS III TVNC { REDI (I) } ALVID { REDI (INVOCA fib (n - I) + INVOCA fib (n - II)) }\n}\nINVOCA fib (VII)", None, ValInt(13), ), ] class TestFunctions(unittest.TestCase): @parameterized.expand(function_tests) def test_functions(self, source, nodes, value): run_test(self, source, nodes, value) # --- Builtins --- builtin_tests = [ ("AVDI_NVMERVS()", None, ValInt(3), "", ["III"]), ("AVDI_NVMERVS()", None, ValInt(10), "", ["X"]), ("CVM FORS\nFORTIS_NVMERVS(I, X)", None, ValInt(3)), ("AVDI()", None, ValStr("hello"), "", ["hello"]), ("LONGITVDO([(I, II, III)])", None, ValInt(3)), ("CVM FORS\nFORTIS_ELECTIONIS([(I, II, III)])", None, ValInt(1)), ] class TestBuiltins(unittest.TestCase): @parameterized.expand(builtin_tests) def test_builtins(self, source, nodes, value, output="", input_lines=[]): run_test(self, source, nodes, value, output, input_lines) # --- Errors --- error_tests = [ ("x", KeyError), # undefined variable ("INVOCA f ()", KeyError), # undefined function ("DESIGNA VT III", SyntaxError), # parse error: missing id after DESIGNA ("DESIGNA x III", SyntaxError), # parse error: missing VT ("DICE(M + M + M + M)", ValueError), # output > 3999 without MAGNVM ("IIII", ValueError), # invalid Roman numeral in source ("FORTIS_NVMERVS(I, X)", ValueError), # requires FORS module ("DEFINI f (x) VT { REDI(x) }\nINVOCA f (I, II)", TypeError), # too many args ("DEFINI f (x, y) VT { REDI(x) }\nINVOCA f (I)", TypeError), # too few args ("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", TypeError), # args to zero-param function ("SI NVLLVS TVNC { DESIGNA r VT I }", TypeError), # NVLLVS cannot be used as boolean ] class TestErrors(unittest.TestCase): @parameterized.expand(error_tests) def test_errors(self, source, error_type): with self.assertRaises(error_type): run_test(self, source, None, None) # --- Repr --- repr_tests = [ ("string", String("hello"), "String(hello)"), ("numeral", Numeral("III"), "Numeral(III)"), ("bool_true", Bool(True), "Bool(True)"), ("bool_false", Bool(False), "Bool(False)"), ("nullus", Nullus(), "Nullus()"), ("erumpe", Erumpe(), "Erumpe()"), ("module_call", ModuleCall("FORS"), "FORS"), ("id", ID("x"), "ID(x)"), ("expression_stmt", ExpressionStatement(String("hi")), "String(hi)"), ("data_array", DataArray([Numeral("I"), Numeral("II")]), "Array([\n Numeral(I),\n Numeral(II)\n])"), ("data_range_array", DataRangeArray(Numeral("I"), Numeral("X")), "RangeArray([\n Numeral(I),\n Numeral(X)\n])"), ("designa", Designa(ID("x"), Numeral("III")), "Designa(\n ID(x),\n Numeral(III)\n)"), ("binop", BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS"), "BinOp(\n Numeral(I),\n Numeral(II),\n SYMBOL_PLUS\n)"), ("redi", Redi([Numeral("I")]), "Redi([\n Numeral(I)\n])"), ("si_no_else", SiStatement(Bool(True), [ExpressionStatement(Erumpe())], None), "Si(\n Bool(True),\n statements([\n Erumpe()\n ]),\n statements([])\n)"), ("si_with_else", SiStatement(Bool(True), [ExpressionStatement(Erumpe())], [ExpressionStatement(Erumpe())]), "Si(\n Bool(True),\n statements([\n Erumpe()\n ]),\n statements([\n Erumpe()\n ])\n)"), ("dum", DumStatement(Bool(False), [ExpressionStatement(Erumpe())]), "Dum(\n Bool(False),\n statements([\n Erumpe()\n ])\n)"), ("per", PerStatement(DataArray([Numeral("I")]), ID("i"), [ExpressionStatement(Erumpe())]), "Per(\n Array([\n Numeral(I)\n ]),\n ID(i),\n statements([\n Erumpe()\n ])\n)"), ("invoca", Invoca(ID("f"), [Numeral("I")]), "Invoca(\n ID(f),\n parameters([\n Numeral(I)\n ])\n)"), ("builtin", BuiltIn("DICE", [String("hi")]), "Builtin(\n DICE,\n parameters([\n String(hi)\n ])\n)"), ("defini", Defini(ID("f"), [ID("n")], [ExpressionStatement(Redi([Numeral("I")]))]), "Defini(\n ID(f),\n parameters([\n ID(n)\n ]),\n statements([\n Redi([\n Numeral(I)\n ])\n ])\n)"), ("program_no_modules", Program([], [ExpressionStatement(Numeral("I"))]), "modules([]),\nstatements([\n Numeral(I)\n])"), ("program_with_module", Program([ModuleCall("FORS")], [ExpressionStatement(Numeral("I"))]), "modules([\n FORS\n]),\nstatements([\n Numeral(I)\n])"), ] class TestRepr(unittest.TestCase): @parameterized.expand(repr_tests) def test_repr(self, _, node, expected): self.assertEqual(repr(node), expected) # --- Roman numeral utilities --- class TestNumerals(unittest.TestCase): # num_to_int: valid cases def test_simple_numerals(self): for s, n in [("I",1),("V",5),("X",10),("L",50),("C",100),("D",500),("M",1000)]: self.assertEqual(num_to_int(s, False), n) def test_subtractive_forms(self): for s, n in [("IV",4),("IX",9),("XL",40),("XC",90),("CD",400),("CM",900)]: self.assertEqual(num_to_int(s, False), n) def test_complex_numerals(self): for s, n in [("XLII",42),("XCIX",99),("MCMXCIX",1999),("MMMCMXCIX",3999)]: self.assertEqual(num_to_int(s, False), n) # num_to_int: invalid cases def test_four_in_a_row_raises(self): with self.assertRaises(Exception): num_to_int("IIII", False) def test_four_x_in_a_row_raises(self): with self.assertRaises(Exception): num_to_int("XXXX", False) def test_invalid_subtractive_iix_raises(self): # IIX is non-standard — I can't appear twice before X with self.assertRaises(Exception): num_to_int("IIX", False) def test_invalid_subtractive_im_raises(self): # I can only subtract from V and X, not M with self.assertRaises(Exception): num_to_int("IM", False) # int_to_num: valid cases def test_int_to_num(self): for n, s in [(1,"I"),(4,"IV"),(9,"IX"),(40,"XL"),(42,"XLII"),(3999,"MMMCMXCIX")]: self.assertEqual(int_to_num(n, False), s) def test_int_to_num_above_3999_raises(self): with self.assertRaises(Exception): int_to_num(4000, False) def test_int_to_num_magnvm(self): # 4000 with MAGNVM enabled self.assertEqual(int_to_num(4000, True), "MV_") def test_num_to_int_magnvm_required(self): # Numbers parsed from strings with _ require MAGNVM with self.assertRaises(Exception): num_to_int("V_", False) # --- make_string --- class TestMakeString(unittest.TestCase): def test_str(self): self.assertEqual(make_string(ValStr("hello")), "hello") def test_int(self): self.assertEqual(make_string(ValInt(3)), "III") def test_bool_true(self): self.assertEqual(make_string(ValBool(True)), "VERVS") def test_bool_false(self): self.assertEqual(make_string(ValBool(False)), "FALSVS") def test_nul(self): self.assertEqual(make_string(ValNul()), "NVLLVS") def test_list(self): self.assertEqual(make_string(ValList([ValInt(1), ValInt(2)])), "[I II]") def test_empty_list(self): self.assertEqual(make_string(ValList([])), "[]") def test_nested_list(self): self.assertEqual( make_string(ValList([ValStr("a"), ValBool(True)])), "[a VERVS]" ) # --- DICE with non-integer types --- dice_type_tests = [ ("DICE(VERITAS)", None, ValStr("VERVS"), "VERVS\n"), ("DICE(FALSITAS)", None, ValStr("FALSVS"), "FALSVS\n"), ("DICE(NVLLVS)", None, ValStr("NVLLVS"), "NVLLVS\n"), ('DICE([(I, II)])', None, ValStr("[I II]"), "[I II]\n"), ('DICE("")', None, ValStr(""), "\n"), # arithmetic result printed as numeral ("DICE(II + III)", None, ValStr("V"), "V\n"), # multiple args of mixed types ('DICE("x", VERITAS)', None, ValStr("x VERVS"), "x VERVS\n"), ] class TestDiceTypes(unittest.TestCase): @parameterized.expand(dice_type_tests) def test_dice_types(self, source, nodes, value, output): run_test(self, source, nodes, value, output) # --- SI/DVM: truthiness of non-bool conditions --- truthiness_tests = [ # nonzero int is truthy ("SI I TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(1)), # zero int is falsy ("DESIGNA z VT I - I\nSI z TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(2)), # non-empty list is truthy ("SI [(I)] TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(1)), # empty list is falsy ("SI [()] TVNC { DESIGNA r VT II } ALVID { DESIGNA r VT I }\nr", None, ValInt(1)), # DVM exits when condition becomes truthy ( "DESIGNA x VT I\nDVM x PLVS III FACE {\nDESIGNA x VT x + I\n}\nx", None, ValInt(4), ), ] class TestTruthiness(unittest.TestCase): @parameterized.expand(truthiness_tests) def test_truthiness(self, source, nodes, value): run_test(self, source, nodes, value) # --- Arithmetic: edge cases --- arithmetic_edge_tests = [ ("I - I", None, ValInt(0)), # result zero ("I - V", None, ValInt(-4)), # negative result ("I / V", None, ValInt(0)), # integer division → 0 ("M * M", None, ValInt(1000000)), # large intermediate (not displayed) ("(I + II) * (IV - I)", None, ValInt(9)), # nested parens ] class TestArithmeticEdge(unittest.TestCase): @parameterized.expand(arithmetic_edge_tests) def test_arithmetic_edge(self, source, nodes, value): run_test(self, source, nodes, value) # --- Comparison operators --- comparison_tests = [ # EST on strings ('\"hello\" EST \"hello\"', None, ValBool(True)), ('\"hello\" EST \"world\"', None, ValBool(False)), # chain comparisons as conditions ("SI III PLVS II TVNC { DESIGNA r VT I }\nr", None, ValInt(1)), ("SI II PLVS III TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(2)), # result of comparison is ValBool ("I EST I", None, ValBool(True)), ("I EST II", None, ValBool(False)), ("I MINVS II", None, ValBool(True)), ("II MINVS I", None, ValBool(False)), ("II PLVS I", None, ValBool(True)), ("I PLVS II", None, ValBool(False)), ] class TestComparisons(unittest.TestCase): @parameterized.expand(comparison_tests) def test_comparisons(self, source, nodes, value): run_test(self, source, nodes, value) # --- Function edge cases --- function_edge_tests = [ # no explicit REDI → returns ValNul ("DEFINI f () VT { I }\nINVOCA f ()", None, ValNul()), # REDI multiple values → ValList ( "DEFINI pair (a, b) VT { REDI (a, b) }\nINVOCA pair (I, II)", None, ValList([ValInt(1), ValInt(2)]), ), # function doesn't mutate outer vtable ( "DESIGNA x VT I\nDEFINI f () VT { DESIGNA x VT V\nREDI (x) }\nINVOCA f ()\nx", None, ValInt(1), ), # function can read outer vtable (closure-like) ( "DESIGNA x VT VII\nDEFINI f () VT { REDI (x) }\nINVOCA f ()", None, ValInt(7), ), # function defined after use is still a parse error (definition must precede call at runtime) # (skipped — ftable is populated at eval time, so definition order matters) # parameter shadows outer variable inside function ( "DESIGNA n VT I\nDEFINI f (n) VT { REDI (n * II) }\nINVOCA f (X)\nn", None, ValInt(1), ), # function aliasing: assign f to g, invoke via g ( "DEFINI f (n) VT { REDI (n * II) }\nDESIGNA g VT f\nINVOCA g (V)", None, ValInt(10), ), # alias is independent: redefining f doesn't affect g ( "DEFINI f (n) VT { REDI (n * II) }\nDESIGNA g VT f\nDEFINI f (n) VT { REDI (n * III) }\nINVOCA g (V)", None, ValInt(10), ), ] class TestFunctionEdge(unittest.TestCase): @parameterized.expand(function_edge_tests) def test_function_edge(self, source, nodes, value): run_test(self, source, nodes, value) # --- Loop edge cases --- loop_edge_tests = [ # range(3, 3) is empty — body never runs, program returns ValNul ("DONICVM i VT III VSQVE III FACE { DICE(i) }", None, ValNul(), ""), # empty array — body never runs ("PER i IN [()] FACE { DICE(i) }", None, ValNul(), ""), # PER breaks on element 2 — last assigned i is 2 ("PER i IN [(I, II, III)] FACE { SI i EST II TVNC { ERVMPE } }\ni", None, ValInt(2), ""), # nested DVM: inner always breaks; outer runs until btr==3 ("DESIGNA btr VT I\nDVM btr EST III FACE {\nDVM FALSITAS FACE {\nERVMPE\n}\nDESIGNA btr VT btr + I\n}\nbtr", None, ValInt(3), ""), # nested PER: inner always breaks on first element; outer completes both iterations # cnt starts at 1, increments twice → 3 ("DESIGNA cnt VT I\nPER i IN [(I, II)] FACE {\nPER k IN [(I, II)] FACE {\nERVMPE\n}\nDESIGNA cnt VT cnt + I\n}\ncnt", None, ValInt(3), ""), # DVM condition true from start — body never runs ("DESIGNA x VT I\nDVM VERITAS FACE {\nDESIGNA x VT x + I\n}\nx", None, ValInt(1), ""), # single iteration: [I VSQVE II] = [1] ("DONICVM i VT I VSQVE II FACE { DICE(i) }", None, ValStr("I"), "I\n"), ] class TestLoopEdge(unittest.TestCase): @parameterized.expand(loop_edge_tests) def test_loop_edge(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) # --- Values: equality and truthiness --- class TestValues(unittest.TestCase): def test_valint_equality(self): self.assertEqual(ValInt(3), ValInt(3)) self.assertNotEqual(ValInt(3), ValInt(4)) def test_valstr_equality(self): self.assertEqual(ValStr("hi"), ValStr("hi")) self.assertNotEqual(ValStr("hi"), ValStr("bye")) def test_valbool_equality(self): self.assertEqual(ValBool(True), ValBool(True)) self.assertNotEqual(ValBool(True), ValBool(False)) def test_valnul_equality(self): self.assertEqual(ValNul(), ValNul()) def test_vallist_equality(self): self.assertEqual(ValList([ValInt(1)]), ValList([ValInt(1)])) self.assertNotEqual(ValList([ValInt(1)]), ValList([ValInt(2)])) self.assertNotEqual(ValList([ValInt(1)]), ValList([])) def test_valint_truthiness(self): self.assertTrue(bool(ValInt(1))) self.assertTrue(bool(ValInt(-1))) self.assertFalse(bool(ValInt(0))) def test_valstr_truthiness(self): self.assertTrue(bool(ValStr("x"))) self.assertFalse(bool(ValStr(""))) def test_valbool_truthiness(self): self.assertTrue(bool(ValBool(True))) self.assertFalse(bool(ValBool(False))) def test_vallist_truthiness(self): self.assertTrue(bool(ValList([ValInt(1)]))) self.assertFalse(bool(ValList([]))) def test_cross_type_inequality(self): self.assertNotEqual(ValInt(1), ValBool(True)) self.assertNotEqual(ValInt(0), ValNul()) self.assertNotEqual(ValStr(""), ValNul()) # --- MAGNVM module --- # (ValueError for 4000 without MAGNVM is already in error_tests) magnvm_tests = [ # M+M+M+M = 4000; MAGNVM allows display as "MV_" ("CVM MAGNVM\nDICE(M + M + M + M)", None, ValStr("MV_"), "MV_\n"), # I_ = 1000 with MAGNVM (same value as M, but written with thousands operator) ("CVM MAGNVM\nI_", None, ValInt(1000), ""), # I_ + I_ = 2000; displayed as MM with MAGNVM ("CVM MAGNVM\nDICE(I_ + I_)", None, ValStr("MM"), "MM\n"), ] class TestMAGNVM(unittest.TestCase): @parameterized.expand(magnvm_tests) def test_magnvm(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) # --- ET and AVT (boolean and/or) --- et_avt_tests = [ ("VERITAS ET VERITAS", None, ValBool(True)), ("VERITAS ET FALSITAS", None, ValBool(False)), ("FALSITAS ET VERITAS", None, ValBool(False)), ("FALSITAS ET FALSITAS", None, ValBool(False)), ("VERITAS AVT VERITAS", None, ValBool(True)), ("VERITAS AVT FALSITAS", None, ValBool(True)), ("FALSITAS AVT VERITAS", None, ValBool(True)), ("FALSITAS AVT FALSITAS", None, ValBool(False)), # short-circuit behaviour: combined with comparisons ("(I EST I) ET (II EST II)", None, ValBool(True)), ("(I EST II) AVT (II EST II)", None, ValBool(True)), # used as SI condition ("SI VERITAS ET VERITAS TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(1)), ("SI FALSITAS AVT FALSITAS TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", None, ValInt(2)), ] class TestEtAvt(unittest.TestCase): @parameterized.expand(et_avt_tests) def test_et_avt(self, source, nodes, value): run_test(self, source, nodes, value) # --- Array indexing --- # Indexing is 0-based; NVLLVS serves as index 0 array_index_tests = [ # basic indexing ("[(I, II, III)][NVLLVS]", None, ValInt(1)), # index 0 → first element ("[(I, II, III)][I]", None, ValInt(2)), # index 1 → second element ("[(I, II, III)][II]", None, ValInt(3)), # index 2 → third element # index into a variable ("DESIGNA a VT [(X, XX, XXX)]\na[I]", None, ValInt(20)), # index into range array ("[I VSQVE V][II]", None, ValInt(3)), ] class TestArrayIndex(unittest.TestCase): @parameterized.expand(array_index_tests) def test_array_index(self, source, nodes, value): run_test(self, source, nodes, value) # --- Comments --- comment_tests = [ # trailing line comment ('DICE("hello") // this is ignored', None, ValStr("hello"), "hello\n"), # line comment on its own line before code ('// ignored\nDICE("hi")', None, ValStr("hi"), "hi\n"), # inline block comment ('DICE(/* ignored */ "hi")', None, ValStr("hi"), "hi\n"), # block comment spanning multiple lines ('/* line one\nline two */\nDICE("hi")', None, ValStr("hi"), "hi\n"), # block comment mid-expression ("II /* ignored */ + III", None, ValInt(5)), # line comment after expression (no output) ("II + III // ignored", None, ValInt(5)), # division still works (/ token not confused with //) ("X / II", None, ValInt(5)), # multiple line comments ('// first\n// second\nDICE("ok")', None, ValStr("ok"), "ok\n"), # comment-only line between two statements ('DESIGNA x VT I\n// set y\nDESIGNA y VT II\nx + y', None, ValInt(3)), # blank line between two statements (double newline) ('DESIGNA x VT I\n\nDESIGNA y VT II\nx + y', None, ValInt(3)), # multiple comment-only lines between statements ('DESIGNA x VT I\n// one\n// two\nDESIGNA y VT III\nx + y', None, ValInt(4)), ] class TestComments(unittest.TestCase): @parameterized.expand(comment_tests) def test_comments(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) # --- Scope --- scope_tests = [ # SI: variable assigned in true branch persists in outer scope ("SI VERITAS TVNC { DESIGNA r VT X }\nr", None, ValInt(10)), # SI: variable assigned in ALVID branch persists in outer scope ("SI FALSITAS TVNC { DESIGNA r VT X } ALVID { DESIGNA r VT V }\nr", None, ValInt(5)), # DVM: variable assigned in body persists after loop exits # x goes 1→2→3→4→5; r tracks x each iteration; loop exits when x==5 ("DESIGNA x VT I\nDVM x EST V FACE { DESIGNA x VT x + I\nDESIGNA r VT x }\nr", None, ValInt(5)), # PER: loop variable holds last array element after loop (no ERVMPE) ("PER i IN [(I, II, III)] FACE { DESIGNA nop VT I }\ni", None, ValInt(3)), # PER: reassigning loop var in body doesn't prevent remaining iterations from running # cnt increments once per iteration (all 3); C=100 doesn't replace next element assignment ("DESIGNA cnt VT I\nPER i IN [(I, II, III)] FACE { DESIGNA i VT C\nDESIGNA cnt VT cnt + I }\ncnt", None, ValInt(4)), # PER: loop var after loop reflects the last body assignment, not the last array element # body sets i=C=100 on every iteration; after loop ends, i stays at 100 ("PER i IN [(I, II, III)] FACE { DESIGNA i VT C }\ni", None, ValInt(100)), # DONICVM: counter holds last range value after loop ends # [I VSQVE IV] = [1,2,3]; last value assigned by loop is III=3 ("DONICVM i VT I VSQVE IV FACE { DESIGNA nop VT I }\ni", None, ValInt(3)), # DONICVM: reassigning counter inside body doesn't reduce the number of iterations # range [I VSQVE IV] evaluated once; i reset each time; cnt still increments 3 times → 4 ("DESIGNA cnt VT I\nDONICVM i VT I VSQVE IV FACE { DESIGNA cnt VT cnt + I\nDESIGNA i VT C }\ncnt", None, ValInt(4)), # DONICVM: ERVMPE exits loop early; counter persists at break value ("DONICVM i VT I VSQVE X FACE {\nSI i EST III TVNC { ERVMPE }\n}\ni", None, ValInt(3)), # Function: modifying parameter inside function does not affect outer variable of same name # outer n=1; f receives n=5 and modifies its local copy; outer n unchanged ("DESIGNA n VT I\nDEFINI f (n) VT { DESIGNA n VT n + X\nREDI (n) }\nINVOCA f (V)\nn", None, ValInt(1)), # Function: mutating outer variable inside function (via DESIGNA) is not visible outside # Invoca creates func_vtable = vtable.copy(); mutations to func_vtable don't propagate back ("DESIGNA x VT I\nDEFINI f () VT { DESIGNA x VT C\nREDI (x) }\nINVOCA f ()\nx", None, ValInt(1)), # Function: two successive calls with same parameter name don't share state ("DEFINI f (n) VT { REDI (n * II) }\nINVOCA f (III) + INVOCA f (IV)", None, ValInt(14)), # Function: calling f(I) with param named n does not overwrite outer n=II # f is defined before n is designated; INVOCA creates a local copy, outer vtable unchanged ("DEFINI f (n) VT { REDI (n * II) }\nDESIGNA n VT II\nINVOCA f (I)\nn", None, ValInt(2)), ] class TestScope(unittest.TestCase): @parameterized.expand(scope_tests) def test_scope(self, source, nodes, value): run_test(self, source, nodes, value) if __name__ == "__main__": unittest.main()