import random import unittest from io import StringIO from unittest.mock import patch from parameterized import parameterized from centvrion.ast_nodes import ( ArrayIndex, Bool, BinOp, BuiltIn, DataArray, DataRangeArray, Defini, Designa, DesignaIndex, DumStatement, Erumpe, ExpressionStatement, ID, Invoca, ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement, String, UnaryMinus, UnaryNot, 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 ###### ########################## if target_nodes is not None: 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 ###### ########################## 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\")", Program([], [ExpressionStatement(BuiltIn("DICE", [String("hello")]))]), ValStr("hello"), "hello\n"), ("DICE(\"world\")", Program([], [ExpressionStatement(BuiltIn("DICE", [String("world")]))]), ValStr("world"), "world\n"), ("DICE(III)", Program([], [ExpressionStatement(BuiltIn("DICE", [Numeral("III")]))]), ValStr("III"), "III\n"), ("DICE(X)", Program([], [ExpressionStatement(BuiltIn("DICE", [Numeral("X")]))]), ValStr("X"), "X\n"), ("DICE(MMXXV)", Program([], [ExpressionStatement(BuiltIn("DICE", [Numeral("MMXXV")]))]), ValStr("MMXXV"), "MMXXV\n"), ("DICE('hello')", Program([], [ExpressionStatement(BuiltIn("DICE", [String("hello")]))]), ValStr("hello"), "hello\n"), ("DICE('world')", Program([], [ExpressionStatement(BuiltIn("DICE", [String("world")]))]), ValStr("world"), "world\n"), ("DICE(\"a\", \"b\")", Program([], [ExpressionStatement(BuiltIn("DICE", [String("a"), String("b")]))]), ValStr("a b"), "a b\n"), ("DICE(\"line one\")\nDICE(\"line two\")", Program([], [ExpressionStatement(BuiltIn("DICE", [String("line one")])), ExpressionStatement(BuiltIn("DICE", [String("line two")]))]), ValStr("line two"), "line one\nline two\n"), ("DICE(DICE(II))", Program([], [ExpressionStatement(BuiltIn("DICE", [BuiltIn("DICE", [Numeral("II")])]))]), 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", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("I"), "SYMBOL_PLUS"))]), ValInt(2)), ("X - III", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "SYMBOL_MINUS"))]), ValInt(7)), ("III * IV", Program([], [ExpressionStatement(BinOp(Numeral("III"), Numeral("IV"), "SYMBOL_TIMES"))]), ValInt(12)), ("X / II", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("II"), "SYMBOL_DIVIDE"))]), ValInt(5)), ("X / III", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "SYMBOL_DIVIDE"))]), ValInt(3)), # integer division: 10 // 3 = 3 ("II + III * IV", Program([], [ExpressionStatement(BinOp(Numeral("II"), BinOp(Numeral("III"), Numeral("IV"), "SYMBOL_TIMES"), "SYMBOL_PLUS"))]), ValInt(14)), # precedence: 2 + (3*4) = 14 ("(II + III) * IV", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), Numeral("IV"), "SYMBOL_TIMES"))]), ValInt(20)), # parens: (2+3)*4 = 20 ("CVM SVBNVLLA\n- III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(UnaryMinus(Numeral("III")))]), ValInt(-3)), # unary negation ("CVM SVBNVLLA\n- (II + III)", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(UnaryMinus(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS")))]), ValInt(-5)), # unary negation of expression ("CVM SVBNVLLA\n- - II", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(UnaryMinus(UnaryMinus(Numeral("II"))))]), ValInt(2)), # double negation ("CVM SVBNVLLA\nIII + - II", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(Numeral("III"), UnaryMinus(Numeral("II")), "SYMBOL_PLUS"))]), ValInt(1)), # unary in binary context ] class TestArithmetic(unittest.TestCase): @parameterized.expand(arithmetic_tests) def test_arithmetic(self, source, nodes, value): run_test(self, source, nodes, value) # --- Precedence and associativity --- # # Precedence (lowest → highest): # AVT < ET < (EST, PLVS, MINVS) < (+ -) < (* /) < UMINUS < INDEX precedence_tests = [ # * binds tighter than -: 10 - (2*3) = 4, not (10-2)*3 = 24 ("X - II * III", Program([], [ExpressionStatement(BinOp(Numeral("X"), BinOp(Numeral("II"), Numeral("III"), "SYMBOL_TIMES"), "SYMBOL_MINUS"))]), ValInt(4)), # / binds tighter than +: (10/2) + 3 = 8, not 10/(2+3) = 2 ("X / II + III", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("X"), Numeral("II"), "SYMBOL_DIVIDE"), Numeral("III"), "SYMBOL_PLUS"))]), ValInt(8)), # + binds tighter than EST: (2+3)==5 = True, not 2+(3==5) = type error ("II + III EST V", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), Numeral("V"), "KEYWORD_EST"))]), ValBool(True)), # * binds tighter than PLVS: (2*3)>4 = True ("II * III PLVS IV", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_TIMES"), Numeral("IV"), "KEYWORD_PLVS"))]), ValBool(True)), # comparison binds tighter than ET: (1==2) AND (2==2) = False AND True = False ("I EST II ET II EST II", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"), BinOp(Numeral("II"), Numeral("II"), "KEYWORD_EST"), "KEYWORD_ET"))]), ValBool(False)), # ET binds tighter than AVT: True OR (False AND False) = True ("VERITAS AVT FALSITAS ET FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(True), BinOp(Bool(False), Bool(False), "KEYWORD_ET"), "KEYWORD_AVT"))]), ValBool(True)), # UMINUS binds tighter than *: (-2)*3 = -6, not -(2*3) = -6 (same value, different tree) ("CVM SVBNVLLA\n- II * III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(UnaryMinus(Numeral("II")), Numeral("III"), "SYMBOL_TIMES"))]), ValInt(-6)), # UMINUS binds tighter than +: (-2)+3 = 1, not -(2+3) = -5 ("CVM SVBNVLLA\n- II + III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BinOp(UnaryMinus(Numeral("II")), Numeral("III"), "SYMBOL_PLUS"))]), ValInt(1)), # INDEX binds tighter than UMINUS: -(arr[I]) = -1 ("CVM SVBNVLLA\n- [I, II, III][I]", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(UnaryMinus(ArrayIndex(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("I"))))]), ValInt(-1)), # INDEX binds tighter than NON: NON (arr[I]) = NON VERITAS = False ("NON [VERITAS, FALSITAS][I]", Program([], [ExpressionStatement(UnaryNot(ArrayIndex(DataArray([Bool(True), Bool(False)]), Numeral("I"))))]), ValBool(False)), # INDEX binds tighter than +: (arr[II]) + X = 2 + 10 = 12 ("[I, II, III][II] + X", Program([], [ExpressionStatement(BinOp(ArrayIndex(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("II")), Numeral("X"), "SYMBOL_PLUS"))]), ValInt(12)), # left-associativity of -: (10-3)-2 = 5, not 10-(3-2) = 9 ("X - III - II", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("X"), Numeral("III"), "SYMBOL_MINUS"), Numeral("II"), "SYMBOL_MINUS"))]), ValInt(5)), # left-associativity of /: (12/2)/3 = 2, not 12/(2/3) = 18 ("XII / II / III", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("XII"), Numeral("II"), "SYMBOL_DIVIDE"), Numeral("III"), "SYMBOL_DIVIDE"))]), ValInt(2)), # left-associativity of AVT: (False OR True) OR False = True ("FALSITAS AVT VERITAS AVT FALSITAS", Program([], [ExpressionStatement(BinOp(BinOp(Bool(False), Bool(True), "KEYWORD_AVT"), Bool(False), "KEYWORD_AVT"))]), ValBool(True)), ] class TestPrecedence(unittest.TestCase): @parameterized.expand(precedence_tests) def test_precedence(self, source, nodes, value): run_test(self, source, nodes, value) # --- Assignment --- assignment_tests = [ ("DESIGNA x VT III\nx", Program([], [Designa(ID("x"), Numeral("III")), ExpressionStatement(ID("x"))]), ValInt(3)), ("DESIGNA msg VT \"hello\"\nmsg", Program([], [Designa(ID("msg"), String("hello")), ExpressionStatement(ID("msg"))]), ValStr("hello")), ("DESIGNA msg VT 'hello'\nmsg", Program([], [Designa(ID("msg"), String("hello")), ExpressionStatement(ID("msg"))]), ValStr("hello")), ("DESIGNA a VT V\nDESIGNA b VT X\na + b", Program([], [Designa(ID("a"), Numeral("V")), Designa(ID("b"), Numeral("X")), ExpressionStatement(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"))]), ValInt(15)), ("DESIGNA x VT II\nDESIGNA x VT x + I\nx", Program([], [Designa(ID("x"), Numeral("II")), Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")), ExpressionStatement(ID("x"))]), 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", Program([], [SiStatement(Bool(True), [Designa(ID("r"), Numeral("I"))], None), ExpressionStatement(ID("r"))]), ValInt(1)), # SI without ALVID — false branch ("SI FALSITAS TVNC { DESIGNA r VT I }", Program([], [SiStatement(Bool(False), [Designa(ID("r"), Numeral("I"))], None)]), ValNul()), # SI with ALVID — true branch ("SI VERITAS TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [SiStatement(Bool(True), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), ValInt(1)), # SI with ALVID — false branch ("SI FALSITAS TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [SiStatement(Bool(False), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), ValInt(2)), # SI with comparison — equal ("SI I EST I TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [SiStatement(BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), ValInt(1)), # SI with comparison — unequal ("SI I EST II TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [SiStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), ValInt(2)), # SI MINVS ("SI I MINVS II TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [SiStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_MINVS"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), ValInt(1)), # SI PLVS ("SI II PLVS I TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [SiStatement(BinOp(Numeral("II"), Numeral("I"), "KEYWORD_PLVS"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), 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", Program([], [ SiStatement( BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"), [Designa(ID("r"), Numeral("I"))], [SiStatement( BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST"), [Designa(ID("r"), Numeral("II"))], [Designa(ID("r"), Numeral("III"))], )], ), ExpressionStatement(ID("r")), ]), 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", Program([], [ Designa(ID("x"), Numeral("I")), DumStatement(BinOp(ID("x"), Numeral("III"), "KEYWORD_EST"), [Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS"))]), ExpressionStatement(ID("x")), ]), ValInt(3), ), # DVM with ERVMPE — loop body prints (testing DICE + ERVMPE together) ("DESIGNA x VT I\nDVM FALSITAS FACE {\nDICE(x)\nERVMPE\n}", Program([], [ Designa(ID("x"), Numeral("I")), DumStatement(Bool(False), [ExpressionStatement(BuiltIn("DICE", [ID("x")])), Erumpe()]), ]), ValStr("I"), "I\n"), # PER foreach ("PER i IN [I, II, III] FACE { DICE(i) }", Program([], [PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("i"), [ExpressionStatement(BuiltIn("DICE", [ID("i")]))])]), ValStr("III"), "I\nII\nIII\n"), # DONICVM range loop ("DONICVM i VT I VSQVE V FACE { DICE(i) }", Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("V")), ID("i"), [ExpressionStatement(BuiltIn("DICE", [ID("i")]))])]), 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)", Program([], [ Defini(ID("bis"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]), ExpressionStatement(Invoca(ID("bis"), [Numeral("III")])), ]), ValInt(6), ), ( "DEFINI add (a, b) VT { REDI (a + b) }\nINVOCA add (III, IV)", Program([], [ Defini(ID("add"), [ID("a"), ID("b")], [Redi([BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")])]), ExpressionStatement(Invoca(ID("add"), [Numeral("III"), Numeral("IV")])), ]), 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)", Program([], [ Defini(ID("fib"), [ID("n")], [ SiStatement( BinOp(ID("n"), Numeral("III"), "KEYWORD_MINVS"), [Redi([Numeral("I")])], [Redi([BinOp( Invoca(ID("fib"), [BinOp(ID("n"), Numeral("I"), "SYMBOL_MINUS")]), Invoca(ID("fib"), [BinOp(ID("n"), Numeral("II"), "SYMBOL_MINUS")]), "SYMBOL_PLUS", )])], ), ]), ExpressionStatement(Invoca(ID("fib"), [Numeral("VII")])), ]), 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()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(3), "", ["III"]), ("AVDI_NVMERVS()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(10), "", ["X"]), ("CVM FORS\nFORTIS_NVMERVS(I, X)", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("FORTIS_NVMERVS", [Numeral("I"), Numeral("X")]))]), ValInt(3)), ("AVDI()", Program([], [ExpressionStatement(BuiltIn("AVDI", []))]), ValStr("hello"), "", ["hello"]), ("LONGITVDO([I, II, III])", Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValInt(3)), ("CVM FORS\nFORTIS_ELECTIONIS([I, II, III])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("FORTIS_ELECTIONIS", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), 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 ("NVLLVS AVT VERITAS", TypeError), # NVLLVS cannot be used as boolean in AVT ('"hello" + " world"', TypeError), # use : for string concatenation, not + ("[I, II][III]", IndexError), # index too high ("CVM SVBNVLLA\n[I, II][-I]", IndexError), # negative index ("[I, II][-I]", ValueError), # negative value ] 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) def test_negative_without_svbnvlla_raises(self): with self.assertRaises(ValueError): num_to_int("-IV", False) def test_negative_with_svbnvlla(self): self.assertEqual(num_to_int("-IV", False, True), -4) self.assertEqual(num_to_int("-XLII", False, True), -42) # 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)", Program([], [ExpressionStatement(BuiltIn("DICE", [Bool(True)]))]), ValStr("VERVS"), "VERVS\n"), ("DICE(FALSITAS)", Program([], [ExpressionStatement(BuiltIn("DICE", [Bool(False)]))]), ValStr("FALSVS"), "FALSVS\n"), ("DICE(NVLLVS)", Program([], [ExpressionStatement(BuiltIn("DICE", [Nullus()]))]), ValStr("NVLLVS"), "NVLLVS\n"), ('DICE([I, II])', Program([], [ExpressionStatement(BuiltIn("DICE", [DataArray([Numeral("I"), Numeral("II")])]))]), ValStr("[I II]"), "[I II]\n"), ('DICE("")', Program([], [ExpressionStatement(BuiltIn("DICE", [String("")]))]), ValStr(""), "\n"), # arithmetic result printed as numeral ("DICE(II + III)", Program([], [ExpressionStatement(BuiltIn("DICE", [BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS")]))]), ValStr("V"), "V\n"), # multiple args of mixed types ('DICE("x", VERITAS)', Program([], [ExpressionStatement(BuiltIn("DICE", [String("x"), Bool(True)]))]), 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", Program([], [SiStatement(Numeral("I"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), ValInt(1)), # zero int is falsy ("DESIGNA z VT I - I\nSI z TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [ Designa(ID("z"), BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS")), SiStatement(ID("z"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r")), ]), ValInt(2)), # non-empty list is truthy ("SI [I] TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [SiStatement(DataArray([Numeral("I")]), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), ValInt(1)), # empty list is falsy ("SI [] TVNC { DESIGNA r VT II } ALVID { DESIGNA r VT I }\nr", Program([], [SiStatement(DataArray([]), [Designa(ID("r"), Numeral("II"))], [Designa(ID("r"), Numeral("I"))]), ExpressionStatement(ID("r"))]), ValInt(1)), # DVM exits when condition becomes truthy ( "DESIGNA x VT I\nDVM x PLVS III FACE {\nDESIGNA x VT x + I\n}\nx", Program([], [ Designa(ID("x"), Numeral("I")), DumStatement(BinOp(ID("x"), Numeral("III"), "KEYWORD_PLVS"), [Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS"))]), ExpressionStatement(ID("x")), ]), 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", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS"))]), ValInt(0)), # result zero ("I - V", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("V"), "SYMBOL_MINUS"))]), ValInt(-4)), # negative result ("I / V", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("V"), "SYMBOL_DIVIDE"))]), ValInt(0)), # integer division → 0 ("M * M", Program([], [ExpressionStatement(BinOp(Numeral("M"), Numeral("M"), "SYMBOL_TIMES"))]), ValInt(1000000)), # large intermediate (not displayed) ("(I + II) * (IV - I)", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS"), BinOp(Numeral("IV"), Numeral("I"), "SYMBOL_MINUS"), "SYMBOL_TIMES"))]), ValInt(9)), # nested parens # NVLLVS coerces to 0 in integer arithmetic ("NVLLVS + V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "SYMBOL_PLUS"))]), ValInt(5)), ("V + NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("V"), Nullus(), "SYMBOL_PLUS"))]), ValInt(5)), ("NVLLVS + NVLLVS", Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_PLUS"))]), ValNul()), ("NVLLVS - V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "SYMBOL_MINUS"))]), ValInt(-5)), ("V - NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("V"), Nullus(), "SYMBOL_MINUS"))]), ValInt(5)), ] class TestArithmeticEdge(unittest.TestCase): @parameterized.expand(arithmetic_edge_tests) def test_arithmetic_edge(self, source, nodes, value): run_test(self, source, nodes, value) # --- String concatenation --- string_concat_tests = [ ('"hello" : " world"', Program([], [ExpressionStatement(BinOp(String("hello"), String(" world"), "SYMBOL_COLON"))]), ValStr("hello world")), # NVLLVS coerces to "" in string context ('NVLLVS : "hello"', Program([], [ExpressionStatement(BinOp(Nullus(), String("hello"), "SYMBOL_COLON"))]), ValStr("hello")), ('"hello" : NVLLVS', Program([], [ExpressionStatement(BinOp(String("hello"), Nullus(), "SYMBOL_COLON"))]), ValStr("hello")), ('NVLLVS : NVLLVS', Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_COLON"))]), ValStr("")), # integers coerce to Roman numerals in string context ('"value: " : V', Program([], [ExpressionStatement(BinOp(String("value: "), Numeral("V"), "SYMBOL_COLON"))]), ValStr("value: V")), ('X : " items"', Program([], [ExpressionStatement(BinOp(Numeral("X"), String(" items"), "SYMBOL_COLON"))]), ValStr("X items")), ] class TestStringConcat(unittest.TestCase): @parameterized.expand(string_concat_tests) def test_string_concat(self, source, nodes, value): run_test(self, source, nodes, value) # --- Comparison operators --- comparison_tests = [ # EST on strings ('\"hello\" EST \"hello\"', Program([], [ExpressionStatement(BinOp(String("hello"), String("hello"), "KEYWORD_EST"))]), ValBool(True)), ('\"hello\" EST \"world\"', Program([], [ExpressionStatement(BinOp(String("hello"), String("world"), "KEYWORD_EST"))]), ValBool(False)), # chain comparisons as conditions ("SI III PLVS II TVNC { DESIGNA r VT I }\nr", Program([], [SiStatement(BinOp(Numeral("III"), Numeral("II"), "KEYWORD_PLVS"), [Designa(ID("r"), Numeral("I"))], None), ExpressionStatement(ID("r"))]), ValInt(1)), ("SI II PLVS III TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [SiStatement(BinOp(Numeral("II"), Numeral("III"), "KEYWORD_PLVS"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), ValInt(2)), # result of comparison is ValBool ("I EST I", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST"))]), ValBool(True)), ("I EST II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"))]), ValBool(False)), ("I MINVS II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_MINVS"))]), ValBool(True)), ("II MINVS I", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("I"), "KEYWORD_MINVS"))]), ValBool(False)), ("II PLVS I", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("I"), "KEYWORD_PLVS"))]), ValBool(True)), ("I PLVS II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_PLVS"))]), ValBool(False)), # NVLLVS coerces to 0 in comparisons ("V PLVS NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("V"), Nullus(), "KEYWORD_PLVS"))]), ValBool(True)), ("NVLLVS MINVS V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "KEYWORD_MINVS"))]), ValBool(True)), ] 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 ()", Program([], [Defini(ID("f"), [], [ExpressionStatement(Numeral("I"))]), ExpressionStatement(Invoca(ID("f"), []))]), ValNul()), # REDI multiple values → ValList ( "DEFINI pair (a, b) VT { REDI (a, b) }\nINVOCA pair (I, II)", Program([], [ Defini(ID("pair"), [ID("a"), ID("b")], [Redi([ID("a"), ID("b")])]), ExpressionStatement(Invoca(ID("pair"), [Numeral("I"), Numeral("II")])), ]), 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", Program([], [ Designa(ID("x"), Numeral("I")), Defini(ID("f"), [], [Designa(ID("x"), Numeral("V")), Redi([ID("x")])]), ExpressionStatement(Invoca(ID("f"), [])), ExpressionStatement(ID("x")), ]), ValInt(1), ), # function can read outer vtable (closure-like) ( "DESIGNA x VT VII\nDEFINI f () VT { REDI (x) }\nINVOCA f ()", Program([], [ Designa(ID("x"), Numeral("VII")), Defini(ID("f"), [], [Redi([ID("x")])]), ExpressionStatement(Invoca(ID("f"), [])), ]), 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", Program([], [ Designa(ID("n"), Numeral("I")), Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]), ExpressionStatement(Invoca(ID("f"), [Numeral("X")])), ExpressionStatement(ID("n")), ]), 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)", Program([], [ Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]), Designa(ID("g"), ID("f")), ExpressionStatement(Invoca(ID("g"), [Numeral("V")])), ]), 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)", Program([], [ Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]), Designa(ID("g"), ID("f")), Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("III"), "SYMBOL_TIMES")])]), ExpressionStatement(Invoca(ID("g"), [Numeral("V")])), ]), 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) }", Program([], [PerStatement(DataRangeArray(Numeral("III"), Numeral("III")), ID("i"), [ExpressionStatement(BuiltIn("DICE", [ID("i")]))])]), ValNul(), ""), # empty array — body never runs ("PER i IN [] FACE { DICE(i) }", Program([], [PerStatement(DataArray([]), ID("i"), [ExpressionStatement(BuiltIn("DICE", [ID("i")]))])]), 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", Program([], [ PerStatement( DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("i"), [SiStatement(BinOp(ID("i"), Numeral("II"), "KEYWORD_EST"), [Erumpe()], None)], ), ExpressionStatement(ID("i")), ]), 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", Program([], [ Designa(ID("btr"), Numeral("I")), DumStatement( BinOp(ID("btr"), Numeral("III"), "KEYWORD_EST"), [DumStatement(Bool(False), [Erumpe()]), Designa(ID("btr"), BinOp(ID("btr"), Numeral("I"), "SYMBOL_PLUS"))], ), ExpressionStatement(ID("btr")), ]), 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", Program([], [ Designa(ID("cnt"), Numeral("I")), PerStatement( DataArray([Numeral("I"), Numeral("II")]), ID("i"), [PerStatement(DataArray([Numeral("I"), Numeral("II")]), ID("k"), [Erumpe()]), Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS"))], ), ExpressionStatement(ID("cnt")), ]), ValInt(3), ""), # DVM condition true from start — body never runs ("DESIGNA x VT I\nDVM VERITAS FACE {\nDESIGNA x VT x + I\n}\nx", Program([], [ Designa(ID("x"), Numeral("I")), DumStatement(Bool(True), [Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS"))]), ExpressionStatement(ID("x")), ]), ValInt(1), ""), # single iteration: [I VSQVE II] = [1] ("DONICVM i VT I VSQVE II FACE { DICE(i) }", Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("II")), ID("i"), [ExpressionStatement(BuiltIn("DICE", [ID("i")]))])]), 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)", Program([ModuleCall("MAGNVM")], [ExpressionStatement(BuiltIn("DICE", [BinOp(BinOp(BinOp(Numeral("M"), Numeral("M"), "SYMBOL_PLUS"), Numeral("M"), "SYMBOL_PLUS"), Numeral("M"), "SYMBOL_PLUS")]))]), ValStr("MV_"), "MV_\n"), # I_ = 1000 with MAGNVM (same value as M, but written with thousands operator) ("CVM MAGNVM\nI_", Program([ModuleCall("MAGNVM")], [ExpressionStatement(Numeral("I_"))]), ValInt(1000), ""), # I_ + I_ = 2000; displayed as MM with MAGNVM ("CVM MAGNVM\nDICE(I_ + I_)", Program([ModuleCall("MAGNVM")], [ExpressionStatement(BuiltIn("DICE", [BinOp(Numeral("I_"), Numeral("I_"), "SYMBOL_PLUS")]))]), 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", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(True), "KEYWORD_ET"))]), ValBool(True)), ("VERITAS ET FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(False), "KEYWORD_ET"))]), ValBool(False)), ("FALSITAS ET VERITAS", Program([], [ExpressionStatement(BinOp(Bool(False), Bool(True), "KEYWORD_ET"))]), ValBool(False)), ("FALSITAS ET FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(False), Bool(False), "KEYWORD_ET"))]), ValBool(False)), ("VERITAS AVT VERITAS", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(True), "KEYWORD_AVT"))]), ValBool(True)), ("VERITAS AVT FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(False), "KEYWORD_AVT"))]), ValBool(True)), ("FALSITAS AVT VERITAS", Program([], [ExpressionStatement(BinOp(Bool(False), Bool(True), "KEYWORD_AVT"))]), ValBool(True)), ("FALSITAS AVT FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(False), Bool(False), "KEYWORD_AVT"))]), ValBool(False)), # short-circuit behaviour: combined with comparisons ("(I EST I) ET (II EST II)", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST"), BinOp(Numeral("II"), Numeral("II"), "KEYWORD_EST"), "KEYWORD_ET"))]), ValBool(True)), ("(I EST II) AVT (II EST II)", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"), BinOp(Numeral("II"), Numeral("II"), "KEYWORD_EST"), "KEYWORD_AVT"))]), ValBool(True)), # used as SI condition ("SI VERITAS ET VERITAS TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [SiStatement(BinOp(Bool(True), Bool(True), "KEYWORD_ET"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), ValInt(1)), ("SI FALSITAS AVT FALSITAS TVNC { DESIGNA r VT I } ALVID { DESIGNA r VT II }\nr", Program([], [SiStatement(BinOp(Bool(False), Bool(False), "KEYWORD_AVT"), [Designa(ID("r"), Numeral("I"))], [Designa(ID("r"), Numeral("II"))]), ExpressionStatement(ID("r"))]), 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 1-based; I is the first element array_index_tests = [ # basic indexing ("[I, II, III][I]", Program([], [ExpressionStatement(ArrayIndex(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("I")))]), ValInt(1)), # first element ("[I, II, III][II]", Program([], [ExpressionStatement(ArrayIndex(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("II")))]), ValInt(2)), # second element ("[I, II, III][III]", Program([], [ExpressionStatement(ArrayIndex(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("III")))]), ValInt(3)), # third element # index into a variable ("DESIGNA a VT [X, XX, XXX]\na[II]", Program([], [Designa(ID("a"), DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX")])), ExpressionStatement(ArrayIndex(ID("a"), Numeral("II")))]), ValInt(20)), # second element # index into range array ("[I VSQVE V][II]", Program([], [ExpressionStatement(ArrayIndex(DataRangeArray(Numeral("I"), Numeral("V")), Numeral("II")))]), ValInt(2)), # second element of [1,2,3,4] ] class TestArrayIndex(unittest.TestCase): @parameterized.expand(array_index_tests) def test_array_index(self, source, nodes, value): run_test(self, source, nodes, value) # --- Array index assignment --- array_index_assign_tests = [ # assign to middle element ("DESIGNA a VT [I, II, III]\nDESIGNA a[II] VT X\na[II]", Program([], [ Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])), DesignaIndex(ID("a"), Numeral("II"), Numeral("X")), ExpressionStatement(ArrayIndex(ID("a"), Numeral("II"))), ]), ValInt(10)), # assign to first element ("DESIGNA a VT [I, II, III]\nDESIGNA a[I] VT V\na[I]", Program([], [ Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])), DesignaIndex(ID("a"), Numeral("I"), Numeral("V")), ExpressionStatement(ArrayIndex(ID("a"), Numeral("I"))), ]), ValInt(5)), # assign to last element ("DESIGNA a VT [I, II, III]\nDESIGNA a[III] VT L\na[III]", Program([], [ Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])), DesignaIndex(ID("a"), Numeral("III"), Numeral("L")), ExpressionStatement(ArrayIndex(ID("a"), Numeral("III"))), ]), ValInt(50)), # other elements unaffected ("DESIGNA a VT [I, II, III]\nDESIGNA a[II] VT X\na[I]", Program([], [ Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])), DesignaIndex(ID("a"), Numeral("II"), Numeral("X")), ExpressionStatement(ArrayIndex(ID("a"), Numeral("I"))), ]), ValInt(1)), # expression as index ("DESIGNA a VT [I, II, III]\nDESIGNA i VT II\nDESIGNA a[i] VT X\na[II]", Program([], [ Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])), Designa(ID("i"), Numeral("II")), DesignaIndex(ID("a"), ID("i"), Numeral("X")), ExpressionStatement(ArrayIndex(ID("a"), Numeral("II"))), ]), ValInt(10)), ] class TestArrayIndexAssign(unittest.TestCase): @parameterized.expand(array_index_assign_tests) def test_array_index_assign(self, source, nodes, value): run_test(self, source, nodes, value) # --- Comments --- comment_tests = [ # trailing line comment ('DICE("hello") // this is ignored', Program([], [ExpressionStatement(BuiltIn("DICE", [String("hello")]))]), ValStr("hello"), "hello\n"), # line comment on its own line before code ('// ignored\nDICE("hi")', Program([], [ExpressionStatement(BuiltIn("DICE", [String("hi")]))]), ValStr("hi"), "hi\n"), # inline block comment ('DICE(/* ignored */ "hi")', Program([], [ExpressionStatement(BuiltIn("DICE", [String("hi")]))]), ValStr("hi"), "hi\n"), # block comment spanning multiple lines ('/* line one\nline two */\nDICE("hi")', Program([], [ExpressionStatement(BuiltIn("DICE", [String("hi")]))]), ValStr("hi"), "hi\n"), # block comment mid-expression ("II /* ignored */ + III", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"))]), ValInt(5)), # line comment after expression (no output) ("II + III // ignored", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"))]), ValInt(5)), # division still works (/ token not confused with //) ("X / II", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("II"), "SYMBOL_DIVIDE"))]), ValInt(5)), # multiple line comments ('// first\n// second\nDICE("ok")', Program([], [ExpressionStatement(BuiltIn("DICE", [String("ok")]))]), ValStr("ok"), "ok\n"), # comment-only line between two statements ('DESIGNA x VT I\n// set y\nDESIGNA y VT II\nx + y', Program([], [Designa(ID("x"), Numeral("I")), Designa(ID("y"), Numeral("II")), ExpressionStatement(BinOp(ID("x"), ID("y"), "SYMBOL_PLUS"))]), ValInt(3)), # blank line between two statements (double newline) ('DESIGNA x VT I\n\nDESIGNA y VT II\nx + y', Program([], [Designa(ID("x"), Numeral("I")), Designa(ID("y"), Numeral("II")), ExpressionStatement(BinOp(ID("x"), ID("y"), "SYMBOL_PLUS"))]), ValInt(3)), # multiple comment-only lines between statements ('DESIGNA x VT I\n// one\n// two\nDESIGNA y VT III\nx + y', Program([], [Designa(ID("x"), Numeral("I")), Designa(ID("y"), Numeral("III")), ExpressionStatement(BinOp(ID("x"), ID("y"), "SYMBOL_PLUS"))]), 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", Program([], [SiStatement(Bool(True), [Designa(ID("r"), Numeral("X"))], None), ExpressionStatement(ID("r"))]), 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", Program([], [SiStatement(Bool(False), [Designa(ID("r"), Numeral("X"))], [Designa(ID("r"), Numeral("V"))]), ExpressionStatement(ID("r"))]), 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", Program([], [ Designa(ID("x"), Numeral("I")), DumStatement(BinOp(ID("x"), Numeral("V"), "KEYWORD_EST"), [ Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")), Designa(ID("r"), ID("x")), ]), ExpressionStatement(ID("r")), ]), 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", Program([], [ PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("i"), [Designa(ID("nop"), Numeral("I"))]), ExpressionStatement(ID("i")), ]), 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", Program([], [ Designa(ID("cnt"), Numeral("I")), PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("i"), [ Designa(ID("i"), Numeral("C")), Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS")), ]), ExpressionStatement(ID("cnt")), ]), 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", Program([], [ PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("i"), [Designa(ID("i"), Numeral("C"))]), ExpressionStatement(ID("i")), ]), 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", Program([], [ PerStatement(DataRangeArray(Numeral("I"), Numeral("IV")), ID("i"), [Designa(ID("nop"), Numeral("I"))]), ExpressionStatement(ID("i")), ]), 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", Program([], [ Designa(ID("cnt"), Numeral("I")), PerStatement(DataRangeArray(Numeral("I"), Numeral("IV")), ID("i"), [ Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS")), Designa(ID("i"), Numeral("C")), ]), ExpressionStatement(ID("cnt")), ]), 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", Program([], [ PerStatement(DataRangeArray(Numeral("I"), Numeral("X")), ID("i"), [ SiStatement(BinOp(ID("i"), Numeral("III"), "KEYWORD_EST"), [Erumpe()], None), ]), ExpressionStatement(ID("i")), ]), 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", Program([], [ Designa(ID("n"), Numeral("I")), Defini(ID("f"), [ID("n")], [Designa(ID("n"), BinOp(ID("n"), Numeral("X"), "SYMBOL_PLUS")), Redi([ID("n")])]), ExpressionStatement(Invoca(ID("f"), [Numeral("V")])), ExpressionStatement(ID("n")), ]), 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", Program([], [ Designa(ID("x"), Numeral("I")), Defini(ID("f"), [], [Designa(ID("x"), Numeral("C")), Redi([ID("x")])]), ExpressionStatement(Invoca(ID("f"), [])), ExpressionStatement(ID("x")), ]), 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)", Program([], [ Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]), ExpressionStatement(BinOp(Invoca(ID("f"), [Numeral("III")]), Invoca(ID("f"), [Numeral("IV")]), "SYMBOL_PLUS")), ]), 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", Program([], [ Defini(ID("f"), [ID("n")], [Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])]), Designa(ID("n"), Numeral("II")), ExpressionStatement(Invoca(ID("f"), [Numeral("I")])), ExpressionStatement(ID("n")), ]), ValInt(2)), ] class TestScope(unittest.TestCase): @parameterized.expand(scope_tests) def test_scope(self, source, nodes, value): run_test(self, source, nodes, value) # --- NON (boolean not) --- non_tests = [ ("NON VERITAS", Program([], [ExpressionStatement(UnaryNot(Bool(True)))]), ValBool(False)), ("NON FALSITAS", Program([], [ExpressionStatement(UnaryNot(Bool(False)))]), ValBool(True)), ("NON NON VERITAS", Program([], [ExpressionStatement(UnaryNot(UnaryNot(Bool(True))))]), ValBool(True)), ("NON I", Program([], [ExpressionStatement(UnaryNot(Numeral("I")))]), ValBool(False)), # zero int is falsy, so NON gives True ("DESIGNA z VT I - I\nNON z", Program([], [Designa(ID("z"), BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS")), ExpressionStatement(UnaryNot(ID("z")))]), ValBool(True)), # NON binds tighter than AVT: (NON VERITAS) AVT FALSITAS → FALSITAS AVT FALSITAS → False ("NON VERITAS AVT FALSITAS", Program([], [ExpressionStatement(BinOp(UnaryNot(Bool(True)), Bool(False), "KEYWORD_AVT"))]), ValBool(False)), # NON binds tighter than EST: (NON I) EST I → FALSITAS EST I → False ("NON I EST I", Program([], [ExpressionStatement(BinOp(UnaryNot(Numeral("I")), Numeral("I"), "KEYWORD_EST"))]), ValBool(False)), ] class TestNon(unittest.TestCase): @parameterized.expand(non_tests) def test_non(self, source, nodes, value): run_test(self, source, nodes, value) if __name__ == "__main__": unittest.main()