import os import subprocess import tempfile import time import unittest from io import StringIO from unittest.mock import patch from parameterized import parameterized from fractions import Fraction from centvrion.ast_nodes import ( ArrayIndex, ArraySlice, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray, Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement, Erumpe, ExpressionStatement, Fvnctio, ID, InterpolatedString, Invoca, ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement, String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction, fraction_to_frac, num_to_int, int_to_num, make_string, _cent_rng, ) from centvrion.compiler.emitter import compile_program from centvrion.errors import CentvrionError from centvrion.lexer import Lexer from centvrion.parser import Parser from centvrion.values import ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac _RUNTIME_C = os.path.join( os.path.dirname(__file__), "centvrion", "compiler", "runtime", "cent_runtime.c" ) def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]): _cent_rng.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 ##### ########################## c_source = compile_program(program) # Force deterministic RNG seed=1 for test reproducibility c_source = c_source.replace("cent_init();", "cent_init(); cent_semen((CentValue){.type=CENT_INT, .ival=1});", 1) with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c: tmp_c.write(c_source) tmp_c_path = tmp_c.name with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin: tmp_bin_path = tmp_bin.name try: subprocess.run( ["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"], check=True, capture_output=True, ) stdin_data = "".join(f"{l}\n" for l in input_lines) proc = subprocess.run( [tmp_bin_path], input=stdin_data, capture_output=True, text=True, ) self.assertEqual(proc.returncode, 0, f"Compiler binary exited non-zero:\n{proc.stderr}") self.assertEqual(proc.stdout, target_output, "Compiler output test") finally: os.unlink(tmp_c_path) os.unlink(tmp_bin_path) assert target_nodes is not None, "All tests must have target nodes" # --- Output --- output_tests = [ ("DIC(\"hello\")", Program([], [ExpressionStatement(BuiltIn("DIC", [String("hello")]))]), ValStr("hello"), "hello\n"), ("DIC(\"world\")", Program([], [ExpressionStatement(BuiltIn("DIC", [String("world")]))]), ValStr("world"), "world\n"), ("DIC(III)", Program([], [ExpressionStatement(BuiltIn("DIC", [Numeral("III")]))]), ValStr("III"), "III\n"), ("DIC(X)", Program([], [ExpressionStatement(BuiltIn("DIC", [Numeral("X")]))]), ValStr("X"), "X\n"), ("DIC(MMXXV)", Program([], [ExpressionStatement(BuiltIn("DIC", [Numeral("MMXXV")]))]), ValStr("MMXXV"), "MMXXV\n"), ("DIC('hello')", Program([], [ExpressionStatement(BuiltIn("DIC", [String("hello")]))]), ValStr("hello"), "hello\n"), ("DIC('world')", Program([], [ExpressionStatement(BuiltIn("DIC", [String("world")]))]), ValStr("world"), "world\n"), ("DIC(\"a\", \"b\")", Program([], [ExpressionStatement(BuiltIn("DIC", [String("a"), String("b")]))]), ValStr("a b"), "a b\n"), ("DIC(\"line one\")\nDIC(\"line two\")", Program([], [ExpressionStatement(BuiltIn("DIC", [String("line one")])), ExpressionStatement(BuiltIn("DIC", [String("line two")]))]), ValStr("line two"), "line one\nline two\n"), ("DIC(DIC(II))", Program([], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("DIC", [Numeral("II")])]))]), ValStr("II"), "II\nII\n"), ("EVERRE()", Program([], [ExpressionStatement(BuiltIn("EVERRE", []))]), ValNul(), "\033[2J\033[H"), ] 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 ("X RELIQVVM III", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "KEYWORD_RELIQVVM"))]), ValInt(1)), # 10 % 3 = 1 ("IX RELIQVVM III", Program([], [ExpressionStatement(BinOp(Numeral("IX"), Numeral("III"), "KEYWORD_RELIQVVM"))]), ValInt(0)), # exact divisor ("VII RELIQVVM X", Program([], [ExpressionStatement(BinOp(Numeral("VII"), Numeral("X"), "KEYWORD_RELIQVVM"))]), ValInt(7)), # dividend < divisor ("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, DISPAR, PLVS, MINVS) < (+ -) < (* / RELIQVVM) < 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)), # + binds tighter than DISPAR: (2+3)!=5 = False, not 2+(3!=5) = type error ("II + III DISPAR V", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), Numeral("V"), "KEYWORD_DISPAR"))]), ValBool(False)), # DISPAR binds tighter than ET: (1!=2) AND (2!=2) = True AND False = False ("I DISPAR II ET II DISPAR II", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_DISPAR"), BinOp(Numeral("II"), Numeral("II"), "KEYWORD_DISPAR"), "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)), # RELIQVVM same precedence as *, /; left-associative: (17 % 5) % 2 = 0 ("XVII RELIQVVM V RELIQVVM II", Program([], [ExpressionStatement( BinOp(BinOp(Numeral("XVII"), Numeral("V"), "KEYWORD_RELIQVVM"), Numeral("II"), "KEYWORD_RELIQVVM"))]), ValInt(0)), # RELIQVVM binds tighter than +: 2 + (7 % 3) = 3, not (2+7) % 3 = 0 ("II + VII RELIQVVM III", Program([], [ExpressionStatement( BinOp(Numeral("II"), BinOp(Numeral("VII"), Numeral("III"), "KEYWORD_RELIQVVM"), "SYMBOL_PLUS"))]), ValInt(3)), # 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)), # Compound assignment — AVGE (+=) ("DESIGNA x VT V\nx AVGE III\nx", Program([], [Designa(ID("x"), Numeral("V")), Designa(ID("x"), BinOp(ID("x"), Numeral("III"), "SYMBOL_PLUS")), ExpressionStatement(ID("x"))]), ValInt(8)), # Compound assignment — MINVE (-=) ("DESIGNA x VT X\nx MINVE III\nx", Program([], [Designa(ID("x"), Numeral("X")), Designa(ID("x"), BinOp(ID("x"), Numeral("III"), "SYMBOL_MINUS")), ExpressionStatement(ID("x"))]), ValInt(7)), # AVGE with complex expression ("DESIGNA x VT I\nx AVGE II + III\nx", Program([], [Designa(ID("x"), Numeral("I")), Designa(ID("x"), BinOp(ID("x"), BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), "SYMBOL_PLUS")), ExpressionStatement(ID("x"))]), ValInt(6)), # AVGE inside a loop (DONICVM range is inclusive: I VSQVE III = [1, 2, 3]) ("DESIGNA s VT NVLLVS\nDONICVM i VT I VSQVE III FAC {\ns AVGE i\n}\ns", Program([], [Designa(ID("s"), Nullus()), PerStatement(DataRangeArray(Numeral("I"), Numeral("III")), ID("i"), [Designa(ID("s"), BinOp(ID("s"), ID("i"), "SYMBOL_PLUS"))]), ExpressionStatement(ID("s"))]), ValInt(6)), ] class TestAssignment(unittest.TestCase): @parameterized.expand(assignment_tests) def test_assignment(self, source, nodes, value): run_test(self, source, nodes, value) # --- Destructuring --- destructuring_tests = [ # basic: unpack multi-return function ( "DEFINI pair (a, b) VT { REDI (a, b) }\nDESIGNA x, y VT INVOCA pair (III, VII)\nx + y", Program([], [ Defini(ID("pair"), [ID("a"), ID("b")], [Redi([ID("a"), ID("b")])]), DesignaDestructure([ID("x"), ID("y")], Invoca(ID("pair"), [Numeral("III"), Numeral("VII")])), ExpressionStatement(BinOp(ID("x"), ID("y"), "SYMBOL_PLUS")), ]), ValInt(10), ), # unpack array literal ( "DESIGNA a, b VT [I, II]\na + b", Program([], [ DesignaDestructure([ID("a"), ID("b")], DataArray([Numeral("I"), Numeral("II")])), ExpressionStatement(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")), ]), ValInt(3), ), # three variables ( "DESIGNA a, b, c VT [X, XX, XXX]\na + b + c", Program([], [ DesignaDestructure([ID("a"), ID("b"), ID("c")], DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX")])), ExpressionStatement(BinOp(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"), ID("c"), "SYMBOL_PLUS")), ]), ValInt(60), ), # destructure into individual use ( "DEFINI pair (a, b) VT { REDI (a, b) }\nDESIGNA x, y VT INVOCA pair (V, II)\nDIC(x)\nDIC(y)", Program([], [ Defini(ID("pair"), [ID("a"), ID("b")], [Redi([ID("a"), ID("b")])]), DesignaDestructure([ID("x"), ID("y")], Invoca(ID("pair"), [Numeral("V"), Numeral("II")])), ExpressionStatement(BuiltIn("DIC", [ID("x")])), ExpressionStatement(BuiltIn("DIC", [ID("y")])), ]), ValStr("II"), "V\nII\n", ), ] class TestDestructuring(unittest.TestCase): @parameterized.expand(destructuring_tests) def test_destructuring(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) # --- Control flow --- control_tests = [ # SI without ALIVD — 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 ALIVD — false branch ("SI FALSITAS TVNC { DESIGNA r VT I }", Program([], [SiStatement(Bool(False), [Designa(ID("r"), Numeral("I"))], None)]), ValNul()), # SI with ALIVD — true branch ("SI VERITAS TVNC { DESIGNA r VT I } ALIVD { 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 ALIVD — false branch ("SI FALSITAS TVNC { DESIGNA r VT I } ALIVD { 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 } ALIVD { 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 } ALIVD { 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 } ALIVD { 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 } ALIVD { 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)), # ALIVD SI chain ( "SI I EST II TVNC { DESIGNA r VT I } ALIVD SI I EST I TVNC { DESIGNA r VT II } ALIVD { 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 FAC {\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 DIC + ERVMPE together) ("DESIGNA x VT I\nDVM FALSITAS FAC {\nDIC(x)\nERVMPE\n}", Program([], [ Designa(ID("x"), Numeral("I")), DumStatement(Bool(False), [ExpressionStatement(BuiltIn("DIC", [ID("x")])), Erumpe()]), ]), ValStr("I"), "I\n"), # AETERNVM is sugar for DVM FALSITAS — must produce the same AST. ("DESIGNA x VT I\nAETERNVM FAC {\nDIC(x)\nERVMPE\n}", Program([], [ Designa(ID("x"), Numeral("I")), DumStatement(Bool(False), [ExpressionStatement(BuiltIn("DIC", [ID("x")])), Erumpe()]), ]), ValStr("I"), "I\n"), # AETERNVM with counter + ERVMPE on condition ("DESIGNA x VT I\nAETERNVM FAC {\nSI x EST III TVNC { ERVMPE }\nDESIGNA x VT x + I\n}\nx", Program([], [ Designa(ID("x"), Numeral("I")), DumStatement(Bool(False), [ SiStatement(BinOp(ID("x"), Numeral("III"), "KEYWORD_EST"), [Erumpe()], None), Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")), ]), ExpressionStatement(ID("x")), ]), ValInt(3)), # AETERNVM with CONTINVA — skip printing III; ERVMPE after V. # Return value is ValNul because the iteration that triggers ERVMPE runs # Designa first (resetting last_val); we test on output, which is the point. ("DESIGNA x VT NVLLVS\nAETERNVM FAC {\nDESIGNA x VT x + I\nSI x PLVS V TVNC { ERVMPE }\nSI x EST III TVNC { CONTINVA }\nDIC(x)\n}", Program([], [ Designa(ID("x"), Nullus()), DumStatement(Bool(False), [ Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")), SiStatement(BinOp(ID("x"), Numeral("V"), "KEYWORD_PLVS"), [Erumpe()], None), SiStatement(BinOp(ID("x"), Numeral("III"), "KEYWORD_EST"), [Continva()], None), ExpressionStatement(BuiltIn("DIC", [ID("x")])), ]), ]), ValNul(), "I\nII\nIV\nV\n"), # REDI inside AETERNVM (inside DEFINI) — exits both loop and function ( "DEFINI f () VT {\nDESIGNA x VT I\nAETERNVM FAC {\nREDI (x)\n}\n}\nINVOCA f ()", Program([], [ Defini(ID("f"), [], [ Designa(ID("x"), Numeral("I")), DumStatement(Bool(False), [Redi([ID("x")])]), ]), ExpressionStatement(Invoca(ID("f"), [])), ]), ValInt(1), ), # PER foreach ("PER i IN [I, II, III] FAC { DIC(i) }", Program([], [PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), ValStr("III"), "I\nII\nIII\n"), # DONICVM range loop ("DONICVM i VT I VSQVE V FAC { DIC(i) }", Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("V")), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), ValStr("V"), "I\nII\nIII\nIV\nV\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) } ALIVD { 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\nDESIGNA a VT [I, II, III]\nDIC(a[FORTVITVS_NVMERVS(I, LONGITVDO(a))])", Program([ModuleCall("FORS")], [Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])), ExpressionStatement(BuiltIn("DIC", [ArrayIndex(ID("a"), BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), BuiltIn("LONGITVDO", [ID("a")])]))]))]), ValStr("I"), "I\n"), ("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)), ("LONGITVDO([])", Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataArray([])]))]), ValInt(0)), ('LONGITVDO("salve")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("salve")]))]), ValInt(5)), ('LONGITVDO("")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("")]))]), ValInt(0)), ("CVM FORS\nDIC(FORTVITA_ELECTIO([I, II, III]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("FORTVITA_ELECTIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])])]))]), ValStr("I"), "I\n"), ("CVM FORS\nSEMEN(XLII)\nDIC(FORTVITVS_NVMERVS(I, C))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), Numeral("C")])]))]), ValStr("XXXIII"), "XXXIII\n"), # DECIMATIO: seed 42, 10 elements → removes 1 (element III) ("CVM FORS\nSEMEN(XLII)\nDIC(DECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X")])])]))]), ValStr("[I II IV V VI VII VIII IX X]"), "[I II IV V VI VII VIII IX X]\n"), # DECIMATIO: seed 1, 3 elements → 3//10=0, nothing removed ("CVM FORS\nSEMEN(I)\nDIC(DECIMATIO([I, II, III]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("I")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])])]))]), ValStr("[I II III]"), "[I II III]\n"), # DECIMATIO: empty array → empty array ("CVM FORS\nDIC(DECIMATIO([]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([])])]))]), ValStr("[]"), "[]\n"), # DECIMATIO: seed 42, 20 elements → removes 2 (elements XIII and XII) ("CVM FORS\nSEMEN(XLII)\nDIC(DECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X"), Numeral("XI"), Numeral("XII"), Numeral("XIII"), Numeral("XIV"), Numeral("XV"), Numeral("XVI"), Numeral("XVII"), Numeral("XVIII"), Numeral("XIX"), Numeral("XX")])])]))]), ValStr("[I II III IV V VI VII VIII IX X XI XIV XV XVI XVII XVIII XIX XX]"), "[I II III IV V VI VII VIII IX X XI XIV XV XVI XVII XVIII XIX XX]\n"), # SENATVS: majority true → VERITAS ("SENATVS(VERITAS, VERITAS, FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(True), Bool(False)]))]), ValBool(True)), # SENATVS: majority false → FALSITAS ("SENATVS(FALSITAS, FALSITAS, VERITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(False), Bool(False), Bool(True)]))]), ValBool(False)), # SENATVS: tie → FALSITAS ("SENATVS(VERITAS, FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(False)]))]), ValBool(False)), # SENATVS: 4-arg tie → FALSITAS ("SENATVS(VERITAS, VERITAS, FALSITAS, FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(True), Bool(False), Bool(False)]))]), ValBool(False)), # SENATVS: single true → VERITAS ("SENATVS(VERITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True)]))]), ValBool(True)), # SENATVS: single false → FALSITAS ("SENATVS(FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(False)]))]), ValBool(False)), # SENATVS: empty → FALSITAS (vacuous) ("SENATVS()", Program([], [ExpressionStatement(BuiltIn("SENATVS", []))]), ValBool(False)), # SENATVS: all true �� VERITAS ("SENATVS(VERITAS, VERITAS, VERITAS, VERITAS, VERITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(True), Bool(True), Bool(True), Bool(True)]))]), ValBool(True)), # SENATVS: array input, majority true → VERITAS ("SENATVS([VERITAS, VERITAS, FALSITAS])", Program([], [ExpressionStatement(BuiltIn("SENATVS", [DataArray([Bool(True), Bool(True), Bool(False)])]))]), ValBool(True)), # SENATVS: array input, majority false → FALSITAS ("SENATVS([FALSITAS, FALSITAS, VERITAS])", Program([], [ExpressionStatement(BuiltIn("SENATVS", [DataArray([Bool(False), Bool(False), Bool(True)])]))]), ValBool(False)), # SENATVS: array input, empty → FALSITAS ("SENATVS([])", Program([], [ExpressionStatement(BuiltIn("SENATVS", [DataArray([])]))]), ValBool(False)), # ORDINA: sort integers ("ORDINA([III, I, II])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("III"), Numeral("I"), Numeral("II")])]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])), # ORDINA: sort strings ('ORDINA(["c", "a", "b"])', Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([String("c"), String("a"), String("b")])]))]), ValList([ValStr("a"), ValStr("b"), ValStr("c")])), # ORDINA: empty list ("ORDINA([])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([])]))]), ValList([])), # ORDINA: single element ("ORDINA([V])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("V")])]))]), ValList([ValInt(5)])), # ORDINA: already sorted ("ORDINA([I, II, III])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])), # ORDINA: duplicates ("ORDINA([II, I, II])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("II"), Numeral("I"), Numeral("II")])]))]), ValList([ValInt(1), ValInt(2), ValInt(2)])), # ORDINA: negative numbers ("CVM SVBNVLLA\nORDINA([-II, III, -I])", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([UnaryMinus(Numeral("II")), Numeral("III"), UnaryMinus(Numeral("I"))])]))]), ValList([ValInt(-2), ValInt(-1), ValInt(3)])), # ORDINA: fractions only ("CVM FRACTIO\nORDINA([IIIS, S, IIS])", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Fractio("IIIS"), Fractio("S"), Fractio("IIS")])]))]), ValList([ValFrac(Fraction(1, 2)), ValFrac(Fraction(5, 2)), ValFrac(Fraction(7, 2))])), # ORDINA: mixed integers and fractions ("CVM FRACTIO\nORDINA([III, S, II])", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("III"), Fractio("S"), Numeral("II")])]))]), ValList([ValFrac(Fraction(1, 2)), ValInt(2), ValInt(3)])), # ORDINA: array passed via variable ("DESIGNA x VT [III, I, II]\nORDINA(x)", Program([], [Designa(ID("x"), DataArray([Numeral("III"), Numeral("I"), Numeral("II")])), ExpressionStatement(BuiltIn("ORDINA", [ID("x")]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])), # TYPVS: integer ("TYPVS(V)", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Numeral("V")]))]), ValStr("NVMERVS")), # TYPVS: string ('TYPVS("hello")', Program([], [ExpressionStatement(BuiltIn("TYPVS", [String("hello")]))]), ValStr("LITTERA")), # TYPVS: boolean ("TYPVS(VERITAS)", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Bool(True)]))]), ValStr("VERAX")), # TYPVS: list ("TYPVS([I, II])", Program([], [ExpressionStatement(BuiltIn("TYPVS", [DataArray([Numeral("I"), Numeral("II")])]))]), ValStr("CATALOGVS")), # TYPVS: empty list ("TYPVS([])", Program([], [ExpressionStatement(BuiltIn("TYPVS", [DataArray([])]))]), ValStr("CATALOGVS")), # TYPVS: fraction ("CVM FRACTIO\nTYPVS(S)", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("TYPVS", [Fractio("S")]))]), ValStr("FRACTIO")), # TYPVS: dict ("TYPVS(TABVLA {})", Program([], [ExpressionStatement(BuiltIn("TYPVS", [DataDict([])]))]), ValStr("TABVLA")), # TYPVS: function ("TYPVS(FVNCTIO () VT { REDI(I) })", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Fvnctio([], [Redi([Numeral("I")])])]))]), ValStr("FVNCTIO")), # TYPVS: null ("TYPVS(NVLLVS)", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Nullus()]))]), ValStr("NVLLVS")), # QVAERE: basic literal match ('QVAERE("ab", "abcabc")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("ab"), String("abcabc")]))]), ValList([ValStr("ab"), ValStr("ab")])), # QVAERE: no match → empty list ('QVAERE("xyz", "abc")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("xyz"), String("abc")]))]), ValList([])), # QVAERE: regex character class ('QVAERE("[a-z]+", "abc123def")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("[a-z]+"), String("abc123def")]))]), ValList([ValStr("abc"), ValStr("def")])), # QVAERE: empty text → empty list ('QVAERE("a", "")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("a"), String("")]))]), ValList([])), # QVAERE: capture groups still return full match ('QVAERE("(a)(b)", "ab")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("(a)(b)"), String("ab")]))]), ValList([ValStr("ab")])), # QVAERE: empty pattern matches between every character ('QVAERE("", "ab")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String(""), String("ab")]))]), ValList([ValStr(""), ValStr(""), ValStr("")])), # QVAERE: dot matches any character ('QVAERE(".", "ab")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("."), String("ab")]))]), ValList([ValStr("a"), ValStr("b")])), # SVBSTITVE: basic literal replacement ('SVBSTITVE("a", "b", "aaa")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("a"), String("b"), String("aaa")]))]), ValStr("bbb")), # SVBSTITVE: regex character class ('SVBSTITVE("[0-9]+", "N", "abc123def456")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("[0-9]+"), String("N"), String("abc123def456")]))]), ValStr("abcNdefN")), # SVBSTITVE: no match → string unchanged ('SVBSTITVE("x", "y", "abc")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("x"), String("y"), String("abc")]))]), ValStr("abc")), # SVBSTITVE: empty replacement (deletion) ('SVBSTITVE("a", "", "banana")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("a"), String(""), String("banana")]))]), ValStr("bnn")), # SVBSTITVE: empty text → empty string ('SVBSTITVE("a", "b", "")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("a"), String("b"), String("")]))]), ValStr("")), # SVBSTITVE: dot matches any character ('SVBSTITVE(".", "x", "ab")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("."), String("x"), String("ab")]))]), ValStr("xx")), # SVBSTITVE: backreference swaps two groups ('SVBSTITVE("(a)(b)", "\\2\\1", "ab")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("(a)(b)"), String("\\2\\1"), String("ab")]))]), ValStr("ba")), # SVBSTITVE: backreference with unmatched group (ignored) ('SVBSTITVE("(a)(b)?", "\\1\\2", "a")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("(a)(b)?"), String("\\1\\2"), String("a")]))]), ValStr("a")), # NVMERVS: basic conversion ('NVMERVS("XIV")', Program([], [ExpressionStatement(BuiltIn("NVMERVS", [String("XIV")]))]), ValInt(14)), # NVMERVS: simple single numeral ('NVMERVS("I")', Program([], [ExpressionStatement(BuiltIn("NVMERVS", [String("I")]))]), ValInt(1)), # NVMERVS: large numeral ('NVMERVS("MMMCMXCIX")', Program([], [ExpressionStatement(BuiltIn("NVMERVS", [String("MMMCMXCIX")]))]), ValInt(3999)), # NVMERVS: subtractive form ('NVMERVS("IX")', Program([], [ExpressionStatement(BuiltIn("NVMERVS", [String("IX")]))]), ValInt(9)), # SCINDE: basic split ('SCINDE("a,b,c", ",")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String("a,b,c"), String(",")]))]), ValList([ValStr("a"), ValStr("b"), ValStr("c")])), # SCINDE: no match (delimiter not found) ('SCINDE("abc", ",")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String("abc"), String(",")]))]), ValList([ValStr("abc")])), # SCINDE: empty string ('SCINDE("", ",")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String(""), String(",")]))]), ValList([ValStr("")])), # SCINDE: multi-char delimiter ('SCINDE("a::b::c", "::")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String("a::b::c"), String("::")]))]), ValList([ValStr("a"), ValStr("b"), ValStr("c")])), # SCINDE: delimiter at edges ('SCINDE(",a,", ",")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String(",a,"), String(",")]))]), ValList([ValStr(""), ValStr("a"), ValStr("")])), # SCINDE: empty delimiter (split into chars) ('SCINDE("abc", "")', Program([], [ExpressionStatement(BuiltIn("SCINDE", [String("abc"), String("")]))]), ValList([ValStr("a"), ValStr("b"), ValStr("c")])), ] 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", CentvrionError), # undefined variable ("INVOCA f ()", CentvrionError), # undefined function ("DESIGNA VT III", SyntaxError), # parse error: missing id after DESIGNA ("DESIGNA x III", SyntaxError), # parse error: missing VT ("DIC(M + M + M + M)", CentvrionError), # output > 3999 without MAGNVM ("IIII", CentvrionError), # invalid Roman numeral in source ("FORTVITVS_NVMERVS(I, X)", CentvrionError), # requires FORS module ('NVMERVS(I)', CentvrionError), # NVMERVS expects a string, not int ('NVMERVS("ABC")', CentvrionError), # invalid Roman numeral string ('NVMERVS("XIV", "IX")', CentvrionError), # too many args ("DEFINI f (x) VT { REDI(x) }\nINVOCA f (I, II)", CentvrionError), # too many args ("DEFINI f (x, y) VT { REDI(x) }\nINVOCA f (I)", CentvrionError), # too few args ("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", CentvrionError), # args to zero-param function ("SI NVLLVS TVNC { DESIGNA r VT I }", CentvrionError), # NVLLVS cannot be used as boolean ("NVLLVS AVT VERITAS", CentvrionError), # NVLLVS cannot be used as boolean in AVT ('"hello" + " world"', CentvrionError), # use & for string concatenation, not + ("[I, II][III]", CentvrionError), # index too high ("CVM SVBNVLLA\n[I, II][-I]", CentvrionError), # negative index ("[I, II][-I]", CentvrionError), # negative value ("I / NVLLVS", CentvrionError), # division by zero (NVLLVS coerces to 0) ("I / [I, II]", CentvrionError), # division with array operand ("I - \"hello\"", CentvrionError), # subtraction with string ("I * \"hello\"", CentvrionError), # multiplication with string ("\"hello\" MINVS \"world\"", CentvrionError), # comparison with strings ("I[I]", CentvrionError), # indexing a non-array ('"SALVTE"[VII]', CentvrionError), # string index out of range ('"SALVTE"[NVLLVS]', CentvrionError), # string index with non-integer ('"SALVTE"[II VSQVE VII]', CentvrionError), # string slice out of range ('"SALVTE"[III VSQVE II]', CentvrionError), # string slice from > to ("DESIGNA x VT I\nDESIGNA x[I] VT II", CentvrionError), # index-assign to non-array ("SEMEN(I)", CentvrionError), # requires FORS module ('CVM FORS\nSEMEN("abc")', CentvrionError), # SEMEN requires integer seed ("FORTVITA_ELECTIO([])", CentvrionError), # FORS required for FORTVITA_ELECTIO ("CVM FORS\nFORTVITA_ELECTIO([])", CentvrionError), # FORTVITA_ELECTIO on empty array ("CVM FORS\nFORTVITVS_NVMERVS(X, I)", CentvrionError), # FORTVITVS_NVMERVS a > b ("PER i IN I FAC { DIC(i) }", CentvrionError), # PER over non-array ("DECIMATIO([I, II, III])", CentvrionError), # FORS required for DECIMATIO ("CVM FORS\nDECIMATIO(I)", CentvrionError), # DECIMATIO requires an array ("LONGITVDO(I)", CentvrionError), # LONGITVDO on non-array ("ORDINA(I)", CentvrionError), # ORDINA on non-array ('ORDINA([I, "a"])', CentvrionError), # ORDINA mixed types ("DESIGNA x VT I\nORDINA(x)", CentvrionError), # ORDINA on id (non-array) ("SENATVS(I)", CentvrionError), # SENATVS requires booleans ("SENATVS(VERITAS, I)", CentvrionError), # SENATVS mixed types ("SENATVS([I, II, III])", CentvrionError), # SENATVS array of non-bools ('LEGE("x.txt")', CentvrionError), # SCRIPTA required for LEGE ('SCRIBE("x.txt", "hi")', CentvrionError), # SCRIPTA required for SCRIBE ('ADIVNGE("x.txt", "hi")', CentvrionError), # SCRIPTA required for ADIVNGE ("DESIGNA x VT I\nINVOCA x ()", CentvrionError), # invoking a non-function ("SI I TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: int ("IIIS", CentvrionError), # fraction without FRACTIO module ("CVM FRACTIO\n[I, II, III][IIIS]", CentvrionError), # fractional index (IIIS = 7/2) ("CVM FRACTIO\n[I, II, III][I / II]", CentvrionError), # fractional index from division (1/2) ("DESIGNA z VT I - I\nSI z TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: zero int ("SI [I] TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: non-empty list ("SI [] TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: empty list ("DESIGNA x VT I\nDVM x FAC {\nDESIGNA x VT x + I\n}", CentvrionError), # non-bool DVM condition: int ("NON I", CentvrionError), # NON on integer ("DESIGNA z VT I - I\nNON z", CentvrionError), # NON on zero integer ('NON "hello"', CentvrionError), # NON on string ("DESIGNA a, b VT III", CentvrionError), # destructure non-array ("DESIGNA a, b VT [I]", CentvrionError), # destructure length mismatch: too many targets ("DESIGNA a, b VT [I, II, III]", CentvrionError), # destructure length mismatch: too few targets ("[I, II, III][II VSQVE IV]", CentvrionError), # slice upper bound out of range ("[I, II, III][NVLLVS VSQVE II]", CentvrionError), # slice with non-integer bound ("I[I VSQVE II]", CentvrionError), # slice on non-array ("[I, II, III][III VSQVE I]", CentvrionError), # slice from > to ("CVM SVBNVLLA\n[I, II, III][-I VSQVE II]", CentvrionError), # slice with negative lower bound ("CVM SVBNVLLA\n[I, II, III][I VSQVE -I]", CentvrionError), # slice with negative upper bound ("CVM FRACTIO\n[I, II, III][IIIS VSQVE III]", CentvrionError), # slice with fractional lower bound ("CVM FRACTIO\n[I, II, III][I VSQVE IIIS]", CentvrionError), # slice with fractional upper bound ("CVM FRACTIO\n[I, II, III][I / II VSQVE III]", CentvrionError), # slice with division-fraction lower bound ("TEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nDESIGNA y VT I / NVLLVS\n}", CentvrionError), # uncaught error in catch block propagates ('QVAERE(I, "abc")', CentvrionError), # QVAERE requires strings, not int ('QVAERE("abc", I)', CentvrionError), # QVAERE requires strings, not int ('QVAERE("[", "abc")', CentvrionError), # QVAERE invalid regex ('SVBSTITVE(I, "b", "c")', CentvrionError), # SVBSTITVE requires strings, not int pattern ('SVBSTITVE("a", I, "c")', CentvrionError), # SVBSTITVE requires strings, not int replacement ('SVBSTITVE("a", "b", I)', CentvrionError), # SVBSTITVE requires strings, not int text ('SVBSTITVE("[", "b", "c")', CentvrionError), # SVBSTITVE invalid regex ('SCINDE(I, ",")', CentvrionError), # SCINDE requires strings, not int ('SCINDE("a", I)', CentvrionError), # SCINDE requires strings, not int delimiter ('PETE("http://example.com")', CentvrionError), # RETE required for PETE ('CVM RETE\nPETE(I)', CentvrionError), # PETE requires a string URL ('PETITVR("/", FVNCTIO (r) VT {\nREDI("hi")\n})', CentvrionError), # RETE required for PETITVR ('CVM RETE\nPETITVR(I, FVNCTIO (r) VT {\nREDI("hi")\n})', CentvrionError), # PETITVR path must be string ('CVM RETE\nPETITVR("/", "not a func")', CentvrionError), # PETITVR handler must be function ('CVM RETE\nAVSCVLTA(LXXX)', CentvrionError), # AVSCVLTA: no routes registered ('AVSCVLTA(LXXX)', CentvrionError), # RETE required for AVSCVLTA ('CVM RETE\nPETITVR("/", FVNCTIO (r) VT {\nREDI("hi")\n})\nAVSCVLTA("text")', CentvrionError), # AVSCVLTA port must be integer ] 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) def run_compiler_error_test(self, source): lexer = Lexer().get_lexer() tokens = lexer.lex(source + "\n") program = Parser().parse(tokens) try: c_source = compile_program(program) except CentvrionError: return # compile-time detection is valid with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c: tmp_c.write(c_source) tmp_c_path = tmp_c.name with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin: tmp_bin_path = tmp_bin.name try: subprocess.run( ["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"], check=True, capture_output=True, ) proc = subprocess.run([tmp_bin_path], capture_output=True, text=True) self.assertNotEqual(proc.returncode, 0, "Expected non-zero exit for error program") self.assertTrue(proc.stderr.strip(), "Expected error message on stderr") finally: os.unlink(tmp_c_path) os.unlink(tmp_bin_path) compiler_error_tests = [(s, e) for s, e in error_tests if e == CentvrionError] class TestCompilerErrors(unittest.TestCase): @parameterized.expand(compiler_error_tests) def test_compiler_errors(self, source, error_type): run_compiler_error_test(self, source) # --- 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")), "ExpressionStatement(\n String(hi)\n)"), ("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 ExpressionStatement(\n Erumpe()\n )\n ]),\n None\n)"), ("si_with_else", SiStatement(Bool(True), [ExpressionStatement(Erumpe())], [ExpressionStatement(Erumpe())]), "Si(\n Bool(True),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ]),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ])\n)"), ("si_empty_else", SiStatement(Bool(True), [ExpressionStatement(Erumpe())], []), "Si(\n Bool(True),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ]),\n statements([])\n)"), ("dum", DumStatement(Bool(False), [ExpressionStatement(Erumpe())]), "Dum(\n Bool(False),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ])\n)"), ("per", PerStatement(DataArray([Numeral("I")]), ID("i"), [ExpressionStatement(Erumpe())]), "Per(\n Array([\n Numeral(I)\n ]),\n ID(i),\n statements([\n ExpressionStatement(\n Erumpe()\n )\n ])\n)"), ("invoca", Invoca(ID("f"), [Numeral("I")]), "Invoca(\n ID(f),\n parameters([\n Numeral(I)\n ])\n)"), ("builtin", BuiltIn("DIC", [String("hi")]), "Builtin(\n DIC,\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 ExpressionStatement(\n Redi([\n Numeral(I)\n ])\n )\n ])\n)"), ("program_no_modules", Program([], [ExpressionStatement(Numeral("I"))]), "modules([]),\nstatements([\n ExpressionStatement(\n Numeral(I)\n )\n])"), ("program_with_module", Program([ModuleCall("FORS")], [ExpressionStatement(Numeral("I"))]), "modules([\n FORS\n]),\nstatements([\n ExpressionStatement(\n Numeral(I)\n )\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(CentvrionError): 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)), "VERITAS") def test_bool_false(self): self.assertEqual(make_string(ValBool(False)), "FALSITAS") 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 VERITAS]" ) # --- DIC with non-integer types --- dic_type_tests = [ ("DIC(VERITAS)", Program([], [ExpressionStatement(BuiltIn("DIC", [Bool(True)]))]), ValStr("VERITAS"), "VERITAS\n"), ("DIC(FALSITAS)", Program([], [ExpressionStatement(BuiltIn("DIC", [Bool(False)]))]), ValStr("FALSITAS"), "FALSITAS\n"), ("DIC(NVLLVS)", Program([], [ExpressionStatement(BuiltIn("DIC", [Nullus()]))]), ValStr("NVLLVS"), "NVLLVS\n"), ('DIC([I, II])', Program([], [ExpressionStatement(BuiltIn("DIC", [DataArray([Numeral("I"), Numeral("II")])]))]), ValStr("[I II]"), "[I II]\n"), ('DIC("")', Program([], [ExpressionStatement(BuiltIn("DIC", [String("")]))]), ValStr(""), "\n"), # arithmetic result printed as numeral ("DIC(II + III)", Program([], [ExpressionStatement(BuiltIn("DIC", [BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS")]))]), ValStr("V"), "V\n"), # multiple args of mixed types ('DIC("x", VERITAS)', Program([], [ExpressionStatement(BuiltIn("DIC", [String("x"), Bool(True)]))]), ValStr("x VERITAS"), "x VERITAS\n"), ] class TestDicTypes(unittest.TestCase): @parameterized.expand(dic_type_tests) def test_dic_types(self, source, nodes, value, output): run_test(self, source, nodes, value, output) # --- SI/DVM: boolean condition enforcement --- dvm_bool_condition_tests = [ # DVM exits when condition becomes true (boolean comparison) ( "DESIGNA x VT I\nDVM x PLVS III FAC {\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 TestDvmBoolCondition(unittest.TestCase): @parameterized.expand(dvm_bool_condition_tests) def test_dvm_bool_condition(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_AMPERSAND"))]), ValStr("hello world")), # NVLLVS coerces to "" in string context ('NVLLVS & "hello"', Program([], [ExpressionStatement(BinOp(Nullus(), String("hello"), "SYMBOL_AMPERSAND"))]), ValStr("hello")), ('"hello" & NVLLVS', Program([], [ExpressionStatement(BinOp(String("hello"), Nullus(), "SYMBOL_AMPERSAND"))]), ValStr("hello")), ('NVLLVS & NVLLVS', Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_AMPERSAND"))]), ValStr("")), # integers coerce to Roman numerals in string context ('"value: " & V', Program([], [ExpressionStatement(BinOp(String("value: "), Numeral("V"), "SYMBOL_AMPERSAND"))]), ValStr("value: V")), ('X & " items"', Program([], [ExpressionStatement(BinOp(Numeral("X"), String(" items"), "SYMBOL_AMPERSAND"))]), 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) # --- String interpolation --- interpolation_tests = [ # basic variable interpolation ('DESIGNA nomen VT "Marcus"\n"Salve, {nomen}!"', Program([], [ Designa(ID("nomen"), String("Marcus")), ExpressionStatement(InterpolatedString([String("Salve, "), ID("nomen"), String("!")])) ]), ValStr("Salve, Marcus!")), # arithmetic expression inside interpolation ('DESIGNA x VT III\n"Sum: {x + II}"', Program([], [ Designa(ID("x"), Numeral("III")), ExpressionStatement(InterpolatedString([String("Sum: "), BinOp(ID("x"), Numeral("II"), "SYMBOL_PLUS")])) ]), ValStr("Sum: V")), # multiple interpolations ('DESIGNA a VT I\nDESIGNA b VT II\n"{a} + {b} = {a + b}"', Program([], [ Designa(ID("a"), Numeral("I")), Designa(ID("b"), Numeral("II")), ExpressionStatement(InterpolatedString([ ID("a"), String(" + "), ID("b"), String(" = "), BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"), ])) ]), ValStr("I + II = III")), # escaped braces become literal ('"use {{braces}}"', Program([], [ExpressionStatement(String("use {braces}"))]), ValStr("use {braces}")), # single-quoted strings ignore braces ("'hello {world}'", Program([], [ExpressionStatement(String("hello {world}"))]), ValStr("hello {world}")), # integer coercion ('DESIGNA n VT V\n"n is {n}"', Program([], [ Designa(ID("n"), Numeral("V")), ExpressionStatement(InterpolatedString([String("n is "), ID("n")])) ]), ValStr("n is V")), # boolean coercion ('DESIGNA b VT VERITAS\n"value: {b}"', Program([], [ Designa(ID("b"), Bool(True)), ExpressionStatement(InterpolatedString([String("value: "), ID("b")])) ]), ValStr("value: VERITAS")), # NVLLVS coercion ('"value: {NVLLVS}"', Program([], [ ExpressionStatement(InterpolatedString([String("value: "), Nullus()])) ]), ValStr("value: NVLLVS")), # expression-only string (no literal parts around it) ('DESIGNA x VT "hi"\n"{x}"', Program([], [ Designa(ID("x"), String("hi")), ExpressionStatement(InterpolatedString([ID("x")])) ]), ValStr("hi")), # adjacent interpolations ('DESIGNA a VT "x"\nDESIGNA b VT "y"\n"{a}{b}"', Program([], [ Designa(ID("a"), String("x")), Designa(ID("b"), String("y")), ExpressionStatement(InterpolatedString([ID("a"), ID("b")])) ]), ValStr("xy")), # function call inside interpolation ("DEFINI f () VT {\nREDI (V)\n}\n\"result: {INVOCA f()}\"", Program([], [ Defini(ID("f"), [], [Redi([Numeral("V")])]), ExpressionStatement(InterpolatedString([String("result: "), Invoca(ID("f"), [])])) ]), ValStr("result: V")), # single-quoted string inside interpolation ("DESIGNA x VT 'hello'\n\"{x & '!'}\"", Program([], [ Designa(ID("x"), String("hello")), ExpressionStatement(InterpolatedString([BinOp(ID("x"), String("!"), "SYMBOL_AMPERSAND")])) ]), ValStr("hello!")), # plain double-quoted string (no braces) still works ('"hello world"', Program([], [ExpressionStatement(String("hello world"))]), ValStr("hello world")), # interpolation in DIC output ('DESIGNA name VT "Roma"\nDIC("Salve, {name}!")', Program([], [ Designa(ID("name"), String("Roma")), ExpressionStatement(BuiltIn("DIC", [InterpolatedString([String("Salve, "), ID("name"), String("!")])])) ]), ValStr("Salve, Roma!"), "Salve, Roma!\n"), ] class TestInterpolation(unittest.TestCase): @parameterized.expand(interpolation_tests) def test_interpolation(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) # --- Escape sequences --- escape_tests = [ # \n → newline ('"hello\\nworld"', Program([], [ExpressionStatement(String("hello\nworld"))]), ValStr("hello\nworld")), # \t → tab ('"col\\tcol"', Program([], [ExpressionStatement(String("col\tcol"))]), ValStr("col\tcol")), # \r → carriage return ('"line\\rover"', Program([], [ExpressionStatement(String("line\rover"))]), ValStr("line\rover")), # \\ → literal backslash ('"back\\\\slash"', Program([], [ExpressionStatement(String("back\\slash"))]), ValStr("back\\slash")), # \" → literal double quote ('"say \\"salve\\""', Program([], [ExpressionStatement(String('say "salve"'))]), ValStr('say "salve"')), # \' → literal single quote in single-quoted string ("'it\\'s'", Program([], [ExpressionStatement(String("it's"))]), ValStr("it's")), # \n in single-quoted string ("'hello\\nworld'", Program([], [ExpressionStatement(String("hello\nworld"))]), ValStr("hello\nworld")), # escape inside interpolated string ('DESIGNA name VT "Roma"\n"salve\\n{name}"', Program([], [ Designa(ID("name"), String("Roma")), ExpressionStatement(InterpolatedString([String("salve\n"), ID("name")])) ]), ValStr("salve\nRoma")), # DIC with newline escape ('DIC("hello\\nworld")', Program([], [ExpressionStatement(BuiltIn("DIC", [String("hello\nworld")]))]), ValStr("hello\nworld"), "hello\nworld\n"), # multiple escapes in one string ('"\\t\\n\\\\"', Program([], [ExpressionStatement(String("\t\n\\"))]), ValStr("\t\n\\")), # unknown escapes pass through (regex backrefs) ('"\\1\\2"', Program([], [ExpressionStatement(String("\\1\\2"))]), ValStr("\\1\\2")), ] class TestEscapeSequences(unittest.TestCase): @parameterized.expand(escape_tests) def test_escape(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) # --- 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 } ALIVD { 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)), # DISPAR (not-equal): mirrors EST semantics, negated ("I DISPAR II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_DISPAR"))]), ValBool(True)), ("I DISPAR I", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("I"), "KEYWORD_DISPAR"))]), ValBool(False)), ('"hello" DISPAR "hello"', Program([], [ExpressionStatement(BinOp(String("hello"), String("hello"), "KEYWORD_DISPAR"))]), ValBool(False)), ('"hello" DISPAR "world"', Program([], [ExpressionStatement(BinOp(String("hello"), String("world"), "KEYWORD_DISPAR"))]), ValBool(True)), ("VERITAS DISPAR FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(False), "KEYWORD_DISPAR"))]), ValBool(True)), ("NVLLVS DISPAR NVLLVS", Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "KEYWORD_DISPAR"))]), ValBool(False)), # cross-type: an int and a string are never equal ('I DISPAR "I"', Program([], [ExpressionStatement(BinOp(Numeral("I"), String("I"), "KEYWORD_DISPAR"))]), 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), ), # 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), ), # REDI inside SI exits function, skips remaining statements in block ( "DEFINI f () VT {\nSI VERITAS TVNC {\nREDI (I)\nREDI (II)\n}\n}\nINVOCA f ()", Program([],[ Defini(ID("f"), [], [SiStatement(Bool(True),[Redi([Numeral("I")]),Redi([Numeral("II")])],None)]), ExpressionStatement(Invoca(ID("f"),[])) ]), ValInt(1), ), # REDI inside DVM exits loop and function ( "DEFINI f () VT {\nDESIGNA x VT I\nDVM FALSITAS FAC {\nREDI (x)\n}\n}\nINVOCA f ()", Program([],[ Defini(ID("f"), [], [ Designa(ID("x"), Numeral("I")), DumStatement(Bool(False), [Redi([ID("x")])]) ]), ExpressionStatement(Invoca(ID("f"),[])) ]), ValInt(1), ), # REDI inside PER exits loop and function ( "DEFINI f () VT {\nPER x IN [I, II, III] FAC {\nSI x EST II TVNC {\nREDI (x)\n}\n}\n}\nINVOCA f ()", Program([],[ Defini(ID("f"), [], [ PerStatement(DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), ID("x"), [ SiStatement(BinOp(ID("x"), Numeral("II"), "KEYWORD_EST"), [ Redi([ID("x")]) ], None) ]) ]), ExpressionStatement(Invoca(ID("f"),[])) ]), ValInt(2), ), # REDI inside nested loops exits all loops and function ( "DEFINI f () VT {\nDESIGNA x VT I\nDVM FALSITAS FAC {\nDVM FALSITAS FAC {\nREDI (x)\n}\n}\n}\nINVOCA f ()", Program([],[ Defini(ID("f"), [], [ Designa(ID("x"), Numeral("I")), DumStatement(Bool(False), [ DumStatement(Bool(False), [ Redi([ID("x")]) ]) ]) ]), ExpressionStatement(Invoca(ID("f"),[])) ]), ValInt(1), ), ] 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 = [ # [III VSQVE III] = [3] — single iteration ("DONICVM i VT III VSQVE III FAC { DIC(i) }", Program([], [PerStatement(DataRangeArray(Numeral("III"), Numeral("III")), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), ValStr("III"), "III\n"), # empty array — body never runs ("PER i IN [] FAC { DIC(i) }", Program([], [PerStatement(DataArray([]), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), ValNul(), ""), # PER breaks on element 2 — last assigned i is 2 ("PER i IN [I, II, III] FAC { 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 FAC {\nDVM FALSITAS FAC {\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] FAC {\nPER k IN [I, II] FAC {\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), ""), # PER with CONTINVA: skip odd numbers, sum evens # [I,II,III,IV] → skip I and III; cnt increments on II and IV → cnt = III ("DESIGNA cnt VT I\nPER i IN [I, II, III, IV] FAC {\nSI i EST I AVT i EST III TVNC { CONTINVA }\nDESIGNA cnt VT cnt + I\n}\ncnt", Program([], [ Designa(ID("cnt"), Numeral("I")), PerStatement( DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV")]), ID("i"), [SiStatement(BinOp(BinOp(ID("i"), Numeral("I"), "KEYWORD_EST"), BinOp(ID("i"), Numeral("III"), "KEYWORD_EST"), "KEYWORD_AVT"), [Continva()], None), Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS"))], ), ExpressionStatement(ID("cnt")), ]), ValInt(3), ""), # DVM with CONTINVA: skip body when x is II, increment regardless # x goes 1→2→3; on x=2 we continue (no DIC); DIC fires for x=1 and x=3 ("DESIGNA x VT I\nDVM x EST IV FAC {\nSI x EST II TVNC { DESIGNA x VT x + I\nCONTINVA }\nDIC(x)\nDESIGNA x VT x + I\n}\nx", Program([], [ Designa(ID("x"), Numeral("I")), DumStatement( BinOp(ID("x"), Numeral("IV"), "KEYWORD_EST"), [SiStatement(BinOp(ID("x"), Numeral("II"), "KEYWORD_EST"), [Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")), Continva()], None), ExpressionStatement(BuiltIn("DIC", [ID("x")])), Designa(ID("x"), BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS"))], ), ExpressionStatement(ID("x")), ]), ValInt(4), "I\nIII\n"), # nested PER: CONTINVA in inner only skips rest of inner body; outer still increments ("DESIGNA cnt VT I\nPER i IN [I, II] FAC {\nPER k IN [I, II] FAC {\nCONTINVA\nDESIGNA cnt VT cnt + I\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"), [Continva(), Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS"))]), Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS"))], ), ExpressionStatement(ID("cnt")), ]), ValInt(3), ""), # DONICVM with CONTINVA: skip value III, count remaining (I VSQVE IV = [1,2,3,4], skip 3 → 3 increments) ("DESIGNA cnt VT I\nDONICVM i VT I VSQVE IV FAC {\nSI i EST III TVNC { CONTINVA }\nDESIGNA cnt VT cnt + I\n}\ncnt", Program([], [ Designa(ID("cnt"), Numeral("I")), PerStatement( DataRangeArray(Numeral("I"), Numeral("IV")), ID("i"), [SiStatement(BinOp(ID("i"), Numeral("III"), "KEYWORD_EST"), [Continva()], None), Designa(ID("cnt"), BinOp(ID("cnt"), Numeral("I"), "SYMBOL_PLUS"))], ), ExpressionStatement(ID("cnt")), ]), ValInt(4)), # DVM condition true from start — body never runs ("DESIGNA x VT I\nDVM VERITAS FAC {\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), ""), # two iterations: [I VSQVE II] = [1, 2] ("DONICVM i VT I VSQVE II FAC { DIC(i) }", Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("II")), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), ValStr("II"), "I\nII\n"), # single iteration: [I VSQVE I] = [1] ("DONICVM i VT I VSQVE I FAC { DIC(i) }", Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("I")), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), ValStr("I"), "I\n"), # empty range: [V VSQVE I] = [] ("DESIGNA x VT NVLLVS\nDONICVM i VT V VSQVE I FAC { DESIGNA x VT x + i }\nx", Program([], [Designa(ID("x"), Nullus()), PerStatement(DataRangeArray(Numeral("V"), Numeral("I")), ID("i"), [Designa(ID("x"), BinOp(ID("x"), ID("i"), "SYMBOL_PLUS"))]), ExpressionStatement(ID("x"))]), ValNul(), ""), ] 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\nDIC(M + M + M + M)", Program([ModuleCall("MAGNVM")], [ExpressionStatement(BuiltIn("DIC", [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\nDIC(I_ + I_)", Program([ModuleCall("MAGNVM")], [ExpressionStatement(BuiltIn("DIC", [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 behavior: 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 } ALIVD { 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 } ALIVD { 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,5] # expression as index ("[I, II, III][I + I]", Program([], [ExpressionStatement(ArrayIndex( DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), BinOp(Numeral("I"), Numeral("I"), "SYMBOL_PLUS")))]), ValInt(2)), # division result as index (no FRACTIO): IV / II = 2 ("[X, XX, XXX][IV / II]", Program([], [ExpressionStatement(ArrayIndex( DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX")]), BinOp(Numeral("IV"), Numeral("II"), "SYMBOL_DIVIDE")))]), ValInt(20)), # whole-number fraction (from division) as index, with FRACTIO imported ("CVM FRACTIO\n[X, XX, XXX][IV / II]", Program([ModuleCall("FRACTIO")], [ExpressionStatement(ArrayIndex( DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX")]), BinOp(Numeral("IV"), Numeral("II"), "SYMBOL_DIVIDE")))]), ValInt(20)), ] 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) # --- Array slicing --- array_slice_tests = [ # basic slice from middle ("[X, XX, XXX, XL, L][II VSQVE IV]", Program([], [ExpressionStatement(ArraySlice( DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX"), Numeral("XL"), Numeral("L")]), Numeral("II"), Numeral("IV")))]), ValList([ValInt(20), ValInt(30), ValInt(40)])), # slice of length 1 ("[I, II, III][II VSQVE II]", Program([], [ExpressionStatement(ArraySlice( DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("II"), Numeral("II")))]), ValList([ValInt(2)])), # full array slice ("[I, II, III][I VSQVE III]", Program([], [ExpressionStatement(ArraySlice( DataArray([Numeral("I"), Numeral("II"), Numeral("III")]), Numeral("I"), Numeral("III")))]), ValList([ValInt(1), ValInt(2), ValInt(3)])), # slice on variable ("DESIGNA a VT [I, II, III, IV, V]\na[II VSQVE IV]", Program([], [ Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V")])), ExpressionStatement(ArraySlice(ID("a"), Numeral("II"), Numeral("IV"))), ]), ValList([ValInt(2), ValInt(3), ValInt(4)])), # slice then index (chained) ("[I, II, III, IV][I VSQVE III][II]", Program([], [ExpressionStatement(ArrayIndex( ArraySlice( DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV")]), Numeral("I"), Numeral("III")), Numeral("II")))]), ValInt(2)), # slice on range array ("[I VSQVE X][III VSQVE VII]", Program([], [ExpressionStatement(ArraySlice( DataRangeArray(Numeral("I"), Numeral("X")), Numeral("III"), Numeral("VII")))]), ValList([ValInt(3), ValInt(4), ValInt(5), ValInt(6), ValInt(7)])), # expression as slice bounds ("[I, II, III, IV, V][I + I VSQVE II + II]", Program([], [ExpressionStatement(ArraySlice( DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V")]), BinOp(Numeral("I"), Numeral("I"), "SYMBOL_PLUS"), BinOp(Numeral("II"), Numeral("II"), "SYMBOL_PLUS")))]), ValList([ValInt(2), ValInt(3), ValInt(4)])), ] class TestArraySlice(unittest.TestCase): @parameterized.expand(array_slice_tests) def test_array_slice(self, source, nodes, value): run_test(self, source, nodes, value) # --- String indexing --- string_index_tests = [ # first character ('"SALVTE"[I]', Program([], [ExpressionStatement(ArrayIndex(String("SALVTE"), Numeral("I")))]), ValStr("S")), # last character ('"SALVTE"[VI]', Program([], [ExpressionStatement(ArrayIndex(String("SALVTE"), Numeral("VI")))]), ValStr("E")), # middle character ('"SALVTE"[III]', Program([], [ExpressionStatement(ArrayIndex(String("SALVTE"), Numeral("III")))]), ValStr("L")), # string index via variable ('DESIGNA s VT "SALVTE"\ns[II]', Program([], [ Designa(ID("s"), String("SALVTE")), ExpressionStatement(ArrayIndex(ID("s"), Numeral("II"))), ]), ValStr("A")), # expression as index ('"SALVTE"[I + II]', Program([], [ExpressionStatement(ArrayIndex( String("SALVTE"), BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS")))]), ValStr("L")), ] class TestStringIndex(unittest.TestCase): @parameterized.expand(string_index_tests) def test_string_index(self, source, nodes, value): run_test(self, source, nodes, value) # --- String slicing --- string_slice_tests = [ # substring from middle ('"SALVTE"[II VSQVE IV]', Program([], [ExpressionStatement(ArraySlice( String("SALVTE"), Numeral("II"), Numeral("IV")))]), ValStr("ALV")), # full string slice ('"SALVTE"[I VSQVE VI]', Program([], [ExpressionStatement(ArraySlice( String("SALVTE"), Numeral("I"), Numeral("VI")))]), ValStr("SALVTE")), # single-char slice ('"SALVTE"[III VSQVE III]', Program([], [ExpressionStatement(ArraySlice( String("SALVTE"), Numeral("III"), Numeral("III")))]), ValStr("L")), # slice on variable ('DESIGNA s VT "SALVTE"\ns[II VSQVE IV]', Program([], [ Designa(ID("s"), String("SALVTE")), ExpressionStatement(ArraySlice(ID("s"), Numeral("II"), Numeral("IV"))), ]), ValStr("ALV")), # chaining: slice then index ('"SALVTE"[I VSQVE III][II]', Program([], [ExpressionStatement(ArrayIndex( ArraySlice(String("SALVTE"), Numeral("I"), Numeral("III")), Numeral("II")))]), ValStr("A")), # expression as slice bounds ('"SALVTE"[I + I VSQVE II + II]', Program([], [ExpressionStatement(ArraySlice( String("SALVTE"), BinOp(Numeral("I"), Numeral("I"), "SYMBOL_PLUS"), BinOp(Numeral("II"), Numeral("II"), "SYMBOL_PLUS")))]), ValStr("ALV")), ] class TestStringSlice(unittest.TestCase): @parameterized.expand(string_slice_tests) def test_string_slice(self, source, nodes, value): run_test(self, source, nodes, value) # --- Comments --- comment_tests = [ # trailing line comment ('DIC("hello") // this is ignored', Program([], [ExpressionStatement(BuiltIn("DIC", [String("hello")]))]), ValStr("hello"), "hello\n"), # line comment on its own line before code ('// ignored\nDIC("hi")', Program([], [ExpressionStatement(BuiltIn("DIC", [String("hi")]))]), ValStr("hi"), "hi\n"), # inline block comment ('DIC(/* ignored */ "hi")', Program([], [ExpressionStatement(BuiltIn("DIC", [String("hi")]))]), ValStr("hi"), "hi\n"), # block comment spanning multiple lines ('/* line one\nline two */\nDIC("hi")', Program([], [ExpressionStatement(BuiltIn("DIC", [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\nDIC("ok")', Program([], [ExpressionStatement(BuiltIn("DIC", [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 ALIVD branch persists in outer scope ("SI FALSITAS TVNC { DESIGNA r VT X } ALIVD { 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 FAC { 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] FAC { 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] FAC { 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] FAC { 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,4]; last value assigned by loop is IV=4 ("DONICVM i VT I VSQVE IV FAC { DESIGNA nop VT I }\ni", Program([], [ PerStatement(DataRangeArray(Numeral("I"), Numeral("IV")), ID("i"), [Designa(ID("nop"), Numeral("I"))]), ExpressionStatement(ID("i")), ]), ValInt(4)), # 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 4 times → 5 ("DESIGNA cnt VT I\nDONICVM i VT I VSQVE IV FAC { 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(5)), # DONICVM: ERVMPE exits loop early; counter persists at break value ("DONICVM i VT I VSQVE X FAC {\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)), ("DESIGNA b VT I EST II\nNON b", Program([], [Designa(ID("b"), BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST")), ExpressionStatement(UnaryNot(ID("b")))]), ValBool(True)), ("DESIGNA z VT I EST I\nNON z", Program([], [Designa(ID("z"), BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST")), ExpressionStatement(UnaryNot(ID("z")))]), ValBool(False)), ("NON VERITAS AVT FALSITAS", Program([], [ExpressionStatement(BinOp(UnaryNot(Bool(True)), Bool(False), "KEYWORD_AVT"))]), ValBool(False)), ("NON VERITAS EST FALSITAS", Program([], [ExpressionStatement(BinOp(UnaryNot(Bool(True)), Bool(False), "KEYWORD_EST"))]), ValBool(True)), ] class TestNon(unittest.TestCase): @parameterized.expand(non_tests) def test_non(self, source, nodes, value): run_test(self, source, nodes, value) # --- FRACTIO module --- fractio_tests = [ # Basic fraction literals ("CVM FRACTIO\nIIIS", Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("IIIS"))]), ValFrac(Fraction(7, 2))), ("CVM FRACTIO\nS", Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("S"))]), ValFrac(Fraction(1, 2))), ("CVM FRACTIO\nS:.", Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("S:."))]), ValFrac(Fraction(3, 4))), ("CVM FRACTIO\n.", Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("."))]), ValFrac(Fraction(1, 12))), ("CVM FRACTIO\n:.", Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio(":."))]), ValFrac(Fraction(1, 4))), # Integer part with fraction ("CVM FRACTIO\nVIIS:|::", Program([ModuleCall("FRACTIO")], [ExpressionStatement(Fractio("VIIS:|::"))]), ValFrac(Fraction(7) + Fraction(100, 144))), # Arithmetic ("CVM FRACTIO\nIIIS + S", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("S"), "SYMBOL_PLUS")) ]), ValFrac(Fraction(4)) ), ("CVM FRACTIO\nIIIS - S", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("S"), "SYMBOL_MINUS")) ]), ValFrac(Fraction(3)) ), ("CVM FRACTIO\nS * IV", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("S"), Numeral("IV"), "SYMBOL_TIMES")) ]), ValFrac(Fraction(2)) ), # Division returns fraction ("CVM FRACTIO\nI / IV", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Numeral("I"), Numeral("IV"), "SYMBOL_DIVIDE")) ]), ValFrac(Fraction(1, 4)) ), ("CVM FRACTIO\nI / III", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Numeral("I"), Numeral("III"), "SYMBOL_DIVIDE")) ]), ValFrac(Fraction(1, 3)) ), # Integer division still works without fractions in operands... but with FRACTIO returns ValFrac ("CVM FRACTIO\nX / II", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Numeral("X"), Numeral("II"), "SYMBOL_DIVIDE")) ]), ValFrac(Fraction(5)) ), # Modulo on fractions: 7/2 RELIQVVM 3/2 = 1/2 (7/2 / 3/2 = 7/3, floor=2, 7/2 - 3 = 1/2) ("CVM FRACTIO\nIIIS RELIQVVM IS", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IS"), "KEYWORD_RELIQVVM")) ]), ValFrac(Fraction(1, 2)) ), # Modulo with mixed operand types: 5/2 RELIQVVM 1 = 1/2 ("CVM FRACTIO\nIIS RELIQVVM I", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("IIS"), Numeral("I"), "KEYWORD_RELIQVVM")) ]), ValFrac(Fraction(1, 2)) ), # Int operands under FRACTIO still return a fraction: 10 RELIQVVM 3 = 1 (as Fraction) ("CVM FRACTIO\nX RELIQVVM III", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "KEYWORD_RELIQVVM")) ]), ValFrac(Fraction(1)) ), # Exact multiple under FRACTIO: 3 RELIQVVM 3/2 = 0 ("CVM FRACTIO\nIII RELIQVVM IS", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Numeral("III"), Fractio("IS"), "KEYWORD_RELIQVVM")) ]), ValFrac(Fraction(0)) ), # String concatenation with fraction ("CVM FRACTIO\nDIC(IIIS & \"!\")", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BuiltIn("DIC", [BinOp(Fractio("IIIS"), String("!"), "SYMBOL_AMPERSAND")])) ]), ValStr("IIIS!"), "IIIS!\n" ), # Negative fractions ("CVM FRACTIO\nCVM SVBNVLLA\n-IIS", Program([ModuleCall("FRACTIO"),ModuleCall("SVBNVLLA")],[ ExpressionStatement(UnaryMinus(Fractio("IIS"))) ]), ValFrac(Fraction(-5,2)) ) ] class TestFractio(unittest.TestCase): @parameterized.expand(fractio_tests) def test_fractio(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) fractio_comparison_tests = [ # fraction vs fraction ("CVM FRACTIO\nIIIS PLVS III", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("IIIS"), Numeral("III"), "KEYWORD_PLVS")) ]), ValBool(True) ), ("CVM FRACTIO\nIII MINVS IIIS", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Numeral("III"), Fractio("IIIS"), "KEYWORD_MINVS")) ]), ValBool(True) ), ("CVM FRACTIO\nIIIS MINVS IV", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("IIIS"), Numeral("IV"), "KEYWORD_MINVS")) ]), ValBool(True) ), ("CVM FRACTIO\nIV PLVS IIIS", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Numeral("IV"), Fractio("IIIS"), "KEYWORD_PLVS")) ]), ValBool(True) ), ("CVM FRACTIO\nIIIS PLVS IIIS", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IIIS"), "KEYWORD_PLVS")) ]), ValBool(False) ), ("CVM FRACTIO\nIIIS MINVS IIIS", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IIIS"), "KEYWORD_MINVS")) ]), ValBool(False) ), # equality: fraction == fraction ("CVM FRACTIO\nIIIS EST IIIS", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IIIS"), "KEYWORD_EST")) ]), ValBool(True) ), ("CVM FRACTIO\nIIIS EST IV", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp(Fractio("IIIS"), Numeral("IV"), "KEYWORD_EST")) ]), ValBool(False) ), # equality: fraction == whole number (ValFrac(4) vs ValInt(4)) ("CVM FRACTIO\nIIIS + S EST IV", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp( BinOp(Fractio("IIIS"), Fractio("S"), "SYMBOL_PLUS"), Numeral("IV"), "KEYWORD_EST")) ]), ValBool(True) ), ("CVM FRACTIO\nS + S EST I", Program([ModuleCall("FRACTIO")], [ ExpressionStatement(BinOp( BinOp(Fractio("S"), Fractio("S"), "SYMBOL_PLUS"), Numeral("I"), "KEYWORD_EST")) ]), ValBool(True) ), ] class TestFractioComparisons(unittest.TestCase): @parameterized.expand(fractio_comparison_tests) def test_fractio_comparison(self, source, nodes, value): run_test(self, source, nodes, value) class TestFractioHelpers(unittest.TestCase): def test_frac_to_fraction_ordering(self): with self.assertRaises(CentvrionError): frac_to_fraction(".S") # . before S violates highest-to-lowest def test_frac_to_fraction_level_overflow(self): with self.assertRaises(CentvrionError): frac_to_fraction("SSSSSS") # SS means S twice = 12/12 = 1, violating < 12/12 constraint def test_frac_to_fraction_iiis(self): self.assertEqual(frac_to_fraction("IIIS"), Fraction(7, 2)) def test_frac_to_fraction_s_colon_dot(self): self.assertEqual(frac_to_fraction("S:."), Fraction(3, 4)) def test_frac_to_fraction_dot(self): self.assertEqual(frac_to_fraction("."), Fraction(1, 12)) def test_frac_to_fraction_multilevel(self): self.assertEqual(frac_to_fraction("VIIS:|::"), Fraction(7) + Fraction(100, 144)) def test_fraction_to_frac_iiis(self): self.assertEqual(fraction_to_frac(Fraction(7, 2)), "IIIS") def test_fraction_to_frac_s_colon_dot(self): self.assertEqual(fraction_to_frac(Fraction(3, 4)), "S:.") def test_fraction_to_frac_dot(self): self.assertEqual(fraction_to_frac(Fraction(1, 12)), ".") def test_fraction_to_frac_multilevel(self): self.assertEqual( fraction_to_frac(Fraction(7) + Fraction(100, 144)), "VIIS:|::" ) def test_roundtrip(self): # Only canonical forms roundtrip — fraction_to_frac always uses max colons before dots for s in ["IIIS", "S:.", ".", "::", "VIIS:|::", "S"]: self.assertEqual(fraction_to_frac(frac_to_fraction(s)), s) # --- Dict (TABVLA) --- dict_tests = [ # empty dict ("TABVLA {}", Program([], [ExpressionStatement(DataDict([]))]), ValDict({})), # single string key ('TABVLA {"a" VT I}', Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I"))]))]), ValDict({"a": ValInt(1)})), # multiple entries ('TABVLA {"a" VT I, "b" VT II}', Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))]))]), ValDict({"a": ValInt(1), "b": ValInt(2)})), # integer keys ('TABVLA {I VT "one", II VT "two"}', Program([], [ExpressionStatement(DataDict([(Numeral("I"), String("one")), (Numeral("II"), String("two"))]))]), ValDict({1: ValStr("one"), 2: ValStr("two")})), # expression values ('TABVLA {"x" VT I + II}', Program([], [ExpressionStatement(DataDict([(String("x"), BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS"))]))]), ValDict({"x": ValInt(3)})), # empty dict with newline ('TABVLA {\n}', Program([], [ExpressionStatement(DataDict([]))]), ValDict({})), # single entry with surrounding newlines ('TABVLA {\n"a" VT I\n}', Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I"))]))]), ValDict({"a": ValInt(1)})), # multiple entries with newlines after commas ('TABVLA {\n"a" VT I,\n"b" VT II\n}', Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))]))]), ValDict({"a": ValInt(1), "b": ValInt(2)})), # newlines around every delimiter ('TABVLA {\n"a" VT I,\n"b" VT II,\n"c" VT III\n}', Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II")), (String("c"), Numeral("III"))]))]), ValDict({"a": ValInt(1), "b": ValInt(2), "c": ValInt(3)})), ] class TestDict(unittest.TestCase): @parameterized.expand(dict_tests) def test_dict(self, source, nodes, value): run_test(self, source, nodes, value) dict_index_tests = [ # string key access ('TABVLA {"a" VT X}["a"]', Program([], [ExpressionStatement(ArrayIndex(DataDict([(String("a"), Numeral("X"))]), String("a")))]), ValInt(10)), # integer key access ('TABVLA {I VT "one"}[I]', Program([], [ExpressionStatement(ArrayIndex(DataDict([(Numeral("I"), String("one"))]), Numeral("I")))]), ValStr("one")), # access via variable ('DESIGNA d VT TABVLA {"x" VT V}\nd["x"]', Program([], [ Designa(ID("d"), DataDict([(String("x"), Numeral("V"))])), ExpressionStatement(ArrayIndex(ID("d"), String("x"))), ]), ValInt(5)), # nested dict access ('TABVLA {"a" VT TABVLA {"b" VT X}}["a"]["b"]', Program([], [ExpressionStatement( ArrayIndex(ArrayIndex(DataDict([(String("a"), DataDict([(String("b"), Numeral("X"))]))]), String("a")), String("b")) )]), ValInt(10)), ] class TestDictIndex(unittest.TestCase): @parameterized.expand(dict_index_tests) def test_dict_index(self, source, nodes, value): run_test(self, source, nodes, value) dict_assign_tests = [ # update existing key ('DESIGNA d VT TABVLA {"a" VT I}\nDESIGNA d["a"] VT X\nd["a"]', Program([], [ Designa(ID("d"), DataDict([(String("a"), Numeral("I"))])), DesignaIndex(ID("d"), String("a"), Numeral("X")), ExpressionStatement(ArrayIndex(ID("d"), String("a"))), ]), ValInt(10)), # insert new key ('DESIGNA d VT TABVLA {"a" VT I}\nDESIGNA d["b"] VT II\nd["b"]', Program([], [ Designa(ID("d"), DataDict([(String("a"), Numeral("I"))])), DesignaIndex(ID("d"), String("b"), Numeral("II")), ExpressionStatement(ArrayIndex(ID("d"), String("b"))), ]), ValInt(2)), # original key unaffected after insert ('DESIGNA d VT TABVLA {"a" VT I}\nDESIGNA d["b"] VT II\nd["a"]', Program([], [ Designa(ID("d"), DataDict([(String("a"), Numeral("I"))])), DesignaIndex(ID("d"), String("b"), Numeral("II")), ExpressionStatement(ArrayIndex(ID("d"), String("a"))), ]), ValInt(1)), ] class TestDictAssign(unittest.TestCase): @parameterized.expand(dict_assign_tests) def test_dict_assign(self, source, nodes, value): run_test(self, source, nodes, value) dict_builtin_tests = [ # LONGITVDO on dict ('LONGITVDO(TABVLA {"a" VT I, "b" VT II})', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))])]))]), ValInt(2)), # LONGITVDO on empty dict ('LONGITVDO(TABVLA {})', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataDict([])]))]), ValInt(0)), # CLAVES ('CLAVES(TABVLA {"a" VT I, "b" VT II})', Program([], [ExpressionStatement(BuiltIn("CLAVES", [DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))])]))]), ValList([ValStr("a"), ValStr("b")])), # CLAVES with int keys ('CLAVES(TABVLA {I VT "x", II VT "y"})', Program([], [ExpressionStatement(BuiltIn("CLAVES", [DataDict([(Numeral("I"), String("x")), (Numeral("II"), String("y"))])]))]), ValList([ValInt(1), ValInt(2)])), ] class TestDictBuiltins(unittest.TestCase): @parameterized.expand(dict_builtin_tests) def test_dict_builtin(self, source, nodes, value): run_test(self, source, nodes, value) dict_iteration_tests = [ # PER iterates over keys ('DESIGNA r VT ""\nPER k IN TABVLA {"a" VT I, "b" VT II} FAC {\nDESIGNA r VT r & k\n}\nr', Program([], [ Designa(ID("r"), String("")), PerStatement( DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))]), ID("k"), [Designa(ID("r"), BinOp(ID("r"), ID("k"), "SYMBOL_AMPERSAND"))], ), ExpressionStatement(ID("r")), ]), ValStr("ab")), ] class TestDictIteration(unittest.TestCase): @parameterized.expand(dict_iteration_tests) def test_dict_iteration(self, source, nodes, value): run_test(self, source, nodes, value) dict_display_tests = [ # DIC on dict ('DIC(TABVLA {"a" VT I})', Program([], [ExpressionStatement(BuiltIn("DIC", [DataDict([(String("a"), Numeral("I"))])]))]), ValStr("{a VT I}"), "{a VT I}\n"), # DIC on multi-entry dict ('DIC(TABVLA {"a" VT I, "b" VT II})', Program([], [ExpressionStatement(BuiltIn("DIC", [DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))])]))]), ValStr("{a VT I, b VT II}"), "{a VT I, b VT II}\n"), # DIC on empty dict ('DIC(TABVLA {})', Program([], [ExpressionStatement(BuiltIn("DIC", [DataDict([])]))]), ValStr("{}"), "{}\n"), ] class TestDictDisplay(unittest.TestCase): @parameterized.expand(dict_display_tests) def test_dict_display(self, source, nodes, value, output): run_test(self, source, nodes, value, output) # --- First-class functions / FVNCTIO --- fvnctio_tests = [ # Lambda assigned to variable, then called ( "DESIGNA f VT FVNCTIO (x) VT { REDI (x + I) }\nINVOCA f (V)", Program([], [ Designa(ID("f"), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")])])), ExpressionStatement(Invoca(ID("f"), [Numeral("V")])), ]), ValInt(6), ), # IIFE: immediately invoked lambda ( "INVOCA FVNCTIO (x) VT { REDI (x * II) } (III)", Program([], [ ExpressionStatement(Invoca( Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("II"), "SYMBOL_TIMES")])]), [Numeral("III")], )), ]), ValInt(6), ), # Zero-arg lambda ( "INVOCA FVNCTIO () VT { REDI (XLII) } ()", Program([], [ ExpressionStatement(Invoca( Fvnctio([], [Redi([Numeral("XLII")])]), [], )), ]), ValInt(42), ), # Function passed as argument ( "DEFINI apply (f, x) VT { REDI (INVOCA f (x)) }\n" "DESIGNA dbl VT FVNCTIO (n) VT { REDI (n * II) }\n" "INVOCA apply (dbl, V)", Program([], [ Defini(ID("apply"), [ID("f"), ID("x")], [ Redi([Invoca(ID("f"), [ID("x")])]) ]), Designa(ID("dbl"), Fvnctio([ID("n")], [ Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")]) ])), ExpressionStatement(Invoca(ID("apply"), [ID("dbl"), Numeral("V")])), ]), ValInt(10), ), # Lambda uses caller-scope variable (copy-caller semantics) ( "DESIGNA n VT III\n" "DESIGNA f VT FVNCTIO (x) VT { REDI (x + n) }\n" "INVOCA f (V)", Program([], [ Designa(ID("n"), Numeral("III")), Designa(ID("f"), Fvnctio([ID("x")], [ Redi([BinOp(ID("x"), ID("n"), "SYMBOL_PLUS")]) ])), ExpressionStatement(Invoca(ID("f"), [Numeral("V")])), ]), ValInt(8), ), # Named function passed as value ( "DEFINI sqr (x) VT { REDI (x * x) }\n" "DESIGNA f VT sqr\n" "INVOCA f (IV)", Program([], [ Defini(ID("sqr"), [ID("x")], [Redi([BinOp(ID("x"), ID("x"), "SYMBOL_TIMES")])]), Designa(ID("f"), ID("sqr")), ExpressionStatement(Invoca(ID("f"), [Numeral("IV")])), ]), ValInt(16), ), # Nested lambdas ( "INVOCA FVNCTIO (x) VT { REDI (INVOCA FVNCTIO (y) VT { REDI (y + I) } (x)) } (V)", Program([], [ ExpressionStatement(Invoca( Fvnctio([ID("x")], [ Redi([Invoca( Fvnctio([ID("y")], [Redi([BinOp(ID("y"), Numeral("I"), "SYMBOL_PLUS")])]), [ID("x")], )]) ]), [Numeral("V")], )), ]), ValInt(6), ), # DIC on a function value ( "DESIGNA f VT FVNCTIO (x) VT { REDI (x) }\nDIC(f)", Program([], [ Designa(ID("f"), Fvnctio([ID("x")], [Redi([ID("x")])])), ExpressionStatement(BuiltIn("DIC", [ID("f")])), ]), ValStr("FVNCTIO"), "FVNCTIO\n", ), # Lambda stored in array, called via index ( "DESIGNA fns VT [FVNCTIO (x) VT { REDI (x + I) }, FVNCTIO (x) VT { REDI (x * II) }]\n" "INVOCA fns[I] (V)", Program([], [ Designa(ID("fns"), DataArray([ Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")])]), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("II"), "SYMBOL_TIMES")])]), ])), ExpressionStatement(Invoca( ArrayIndex(ID("fns"), Numeral("I")), [Numeral("V")], )), ]), ValInt(6), ), # Lambda stored in dict, called via key ( 'DESIGNA d VT TABVLA {"add" VT FVNCTIO (x) VT { REDI (x + I) }}\n' 'INVOCA d["add"] (V)', Program([], [ Designa(ID("d"), DataDict([ (String("add"), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")])])), ])), ExpressionStatement(Invoca( ArrayIndex(ID("d"), String("add")), [Numeral("V")], )), ]), ValInt(6), ), # Multi-param lambda ( "DESIGNA add VT FVNCTIO (a, b) VT { REDI (a + b) }\nINVOCA add (III, IV)", Program([], [ Designa(ID("add"), Fvnctio([ID("a"), ID("b")], [ Redi([BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")]) ])), ExpressionStatement(Invoca(ID("add"), [Numeral("III"), Numeral("IV")])), ]), ValInt(7), ), ] class TestFvnctio(unittest.TestCase): @parameterized.expand(fvnctio_tests) def test_fvnctio(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) # --- DORMI --- dormi_tests = [ ("DORMI(NVLLVS)", Program([], [ExpressionStatement(BuiltIn("DORMI", [Nullus()]))]), ValNul()), ] class TestDormi(unittest.TestCase): @parameterized.expand(dormi_tests) def test_dormi(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) def test_dormi_timing_int(self): source = "DORMI(I)\n" lexer = Lexer().get_lexer() tokens = lexer.lex(source) program = Parser().parse(tokens) start = time.time() program.eval() elapsed = time.time() - start self.assertAlmostEqual(elapsed, 1.0, delta=0.5) def test_dormi_timing_int_compiled(self): source = "DORMI(I)\n" lexer = Lexer().get_lexer() tokens = lexer.lex(source) program = Parser().parse(tokens) c_source = compile_program(program) with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c: tmp_c.write(c_source) tmp_c_path = tmp_c.name with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin: tmp_bin_path = tmp_bin.name try: subprocess.run( ["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"], check=True, capture_output=True, ) start = time.time() proc = subprocess.run([tmp_bin_path], capture_output=True, text=True) elapsed = time.time() - start self.assertEqual(proc.returncode, 0) self.assertAlmostEqual(elapsed, 1.0, delta=0.5) finally: os.unlink(tmp_c_path) os.unlink(tmp_bin_path) def test_dormi_timing_frac(self): source = "CVM FRACTIO\nDORMI(S)\n" lexer = Lexer().get_lexer() tokens = lexer.lex(source) program = Parser().parse(tokens) start = time.time() program.eval() elapsed = time.time() - start self.assertAlmostEqual(elapsed, 0.5, delta=0.5) def test_dormi_timing_frac_compiled(self): source = "CVM FRACTIO\nDORMI(S)\n" lexer = Lexer().get_lexer() tokens = lexer.lex(source) program = Parser().parse(tokens) c_source = compile_program(program) with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c: tmp_c.write(c_source) tmp_c_path = tmp_c.name with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin: tmp_bin_path = tmp_bin.name try: subprocess.run( ["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"], check=True, capture_output=True, ) start = time.time() proc = subprocess.run([tmp_bin_path], capture_output=True, text=True) elapsed = time.time() - start self.assertEqual(proc.returncode, 0) self.assertAlmostEqual(elapsed, 0.5, delta=0.5) finally: os.unlink(tmp_c_path) os.unlink(tmp_bin_path) class TestScripta(unittest.TestCase): def _run_scripta(self, source): lexer = Lexer().get_lexer() tokens = lexer.lex(source + "\n") program = Parser().parse(tokens) # printer round-trip new_text = program.print() new_tokens = Lexer().get_lexer().lex(new_text + "\n") new_nodes = Parser().parse(new_tokens) self.assertEqual(program, new_nodes, f"Printer test\n{source}\n{new_text}") return program def _compile_and_run(self, program): c_source = compile_program(program) with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c: tmp_c.write(c_source) tmp_c_path = tmp_c.name with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin: tmp_bin_path = tmp_bin.name try: subprocess.run( ["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"], check=True, capture_output=True, ) proc = subprocess.run([tmp_bin_path], capture_output=True, text=True) self.assertEqual(proc.returncode, 0, f"Compiler binary exited non-zero:\n{proc.stderr}") return proc.stdout finally: os.unlink(tmp_c_path) os.unlink(tmp_bin_path) def test_scribe_and_lege(self): with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f: path = f.name try: source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE MVNDE")\nDIC(LEGE("{path}"))' program = self._run_scripta(source) captured = StringIO() with patch("sys.stdout", captured): program.eval() self.assertEqual(captured.getvalue(), "SALVE MVNDE\n") with open(path) as f: self.assertEqual(f.read(), "SALVE MVNDE") finally: if os.path.exists(path): os.unlink(path) def test_scribe_and_lege_compiled(self): with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f: path = f.name try: source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE MVNDE")\nDIC(LEGE("{path}"))' program = self._run_scripta(source) output = self._compile_and_run(program) self.assertEqual(output, "SALVE MVNDE\n") with open(path) as f: self.assertEqual(f.read(), "SALVE MVNDE") finally: if os.path.exists(path): os.unlink(path) def test_adivnge(self): with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f: path = f.name try: source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE")\nADIVNGE("{path}", " MVNDE")\nDIC(LEGE("{path}"))' program = self._run_scripta(source) captured = StringIO() with patch("sys.stdout", captured): program.eval() self.assertEqual(captured.getvalue(), "SALVE MVNDE\n") finally: if os.path.exists(path): os.unlink(path) def test_adivnge_compiled(self): with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f: path = f.name try: source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE")\nADIVNGE("{path}", " MVNDE")\nDIC(LEGE("{path}"))' program = self._run_scripta(source) output = self._compile_and_run(program) self.assertEqual(output, "SALVE MVNDE\n") finally: if os.path.exists(path): os.unlink(path) def test_scribe_overwrites(self): with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f: path = f.name try: source = f'CVM SCRIPTA\nSCRIBE("{path}", "first")\nSCRIBE("{path}", "second")\nLEGE("{path}")' program = self._run_scripta(source) result = program.eval() self.assertEqual(result, ValStr("second")) finally: if os.path.exists(path): os.unlink(path) def test_lege_empty_file(self): with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f: path = f.name try: source = f'CVM SCRIPTA\nLEGE("{path}")' program = self._run_scripta(source) result = program.eval() self.assertEqual(result, ValStr("")) finally: os.unlink(path) def test_lege_preexisting_content(self): with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f: f.write("hello from python") path = f.name try: source = f'CVM SCRIPTA\nLEGE("{path}")' program = self._run_scripta(source) result = program.eval() self.assertEqual(result, ValStr("hello from python")) finally: os.unlink(path) def test_scribe_returns_nullus(self): with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f: path = f.name try: source = f'CVM SCRIPTA\nSCRIBE("{path}", "x")' program = self._run_scripta(source) result = program.eval() self.assertEqual(result, ValNul()) finally: if os.path.exists(path): os.unlink(path) def test_adivnge_returns_nullus(self): with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f: path = f.name try: source = f'CVM SCRIPTA\nADIVNGE("{path}", "x")' program = self._run_scripta(source) result = program.eval() self.assertEqual(result, ValNul()) finally: if os.path.exists(path): os.unlink(path) # --- Tempta/Cape (try/catch) --- tempta_tests = [ # Try block succeeds — catch not entered ( "TEMPTA {\nDESIGNA r VT I\n} CAPE e {\nDESIGNA r VT II\n}\nr", Program([], [ TemptaStatement( [Designa(ID("r"), Numeral("I"))], ID("e"), [Designa(ID("r"), Numeral("II"))], ), ExpressionStatement(ID("r")), ]), ValInt(1), ), # Try block errors — caught by catch ( "TEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT II\n}\nr", Program([], [ TemptaStatement( [Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))], ID("e"), [Designa(ID("r"), Numeral("II"))], ), ExpressionStatement(ID("r")), ]), ValInt(2), ), # Error variable contains the error message ( 'DESIGNA e VT NVLLVS\nTEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nNVLLVS\n}\ne', Program([], [ Designa(ID("e"), Nullus()), TemptaStatement( [Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))], ID("e"), [ExpressionStatement(Nullus())], ), ExpressionStatement(ID("e")), ]), ValStr("Division by zero"), ), # Nested tempta — inner catches, outer unaffected ( "DESIGNA r VT NVLLVS\nTEMPTA {\nTEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT I\n}\n} CAPE e {\nDESIGNA r VT II\n}\nr", Program([], [ Designa(ID("r"), Nullus()), TemptaStatement( [TemptaStatement( [Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))], ID("e"), [Designa(ID("r"), Numeral("I"))], )], ID("e"), [Designa(ID("r"), Numeral("II"))], ), ExpressionStatement(ID("r")), ]), ValInt(1), ), # REDI inside catch block ( "DEFINI f () VT {\nTEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nREDI (III)\n}\nREDI (IV)\n}\nINVOCA f ()", Program([], [ Defini(ID("f"), [], [ TemptaStatement( [Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))], ID("e"), [Redi([Numeral("III")])], ), Redi([Numeral("IV")]), ]), ExpressionStatement(Invoca(ID("f"), [])), ]), ValInt(3), ), # ERVMPE inside catch block (inside a loop) ( "DESIGNA r VT NVLLVS\nDVM r EST I FAC {\nTEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT I\nERVMPE\n}\n}\nr", Program([], [ Designa(ID("r"), Nullus()), DumStatement( BinOp(ID("r"), Numeral("I"), "KEYWORD_EST"), [TemptaStatement( [Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))], ID("e"), [Designa(ID("r"), Numeral("I")), Erumpe()], )], ), ExpressionStatement(ID("r")), ]), ValInt(1), ), # Statement after error in try block is not executed ( "DESIGNA r VT NVLLVS\nTEMPTA {\nDESIGNA x VT I / NVLLVS\nDESIGNA r VT III\n} CAPE e {\nDESIGNA r VT II\n}\nr", Program([], [ Designa(ID("r"), Nullus()), TemptaStatement( [Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE")), Designa(ID("r"), Numeral("III"))], ID("e"), [Designa(ID("r"), Numeral("II"))], ), ExpressionStatement(ID("r")), ]), ValInt(2), ), ] class TestTempta(unittest.TestCase): @parameterized.expand(tempta_tests) def test_tempta(self, source, nodes, value, output=""): run_test(self, source, nodes, value, output) class TestRete(unittest.TestCase): @classmethod def setUpClass(cls): import http.server, threading class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(b"SALVE MVNDE") def log_message(self, *args): pass cls.server = http.server.HTTPServer(("127.0.0.1", 0), Handler) cls.port = cls.server.server_address[1] cls.thread = threading.Thread(target=cls.server.serve_forever) cls.thread.daemon = True cls.thread.start() @classmethod def tearDownClass(cls): cls.server.shutdown() def test_pete(self): url = f"http://127.0.0.1:{self.port}/" source = f'CVM RETE\nPETE("{url}")' run_test(self, source, Program([ModuleCall("RETE")], [ExpressionStatement(BuiltIn("PETE", [String(url)]))]), ValStr("SALVE MVNDE")) def test_pete_dic(self): url = f"http://127.0.0.1:{self.port}/" source = f'CVM RETE\nDIC(PETE("{url}"))' run_test(self, source, Program([ModuleCall("RETE")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("PETE", [String(url)])]))]), ValStr("SALVE MVNDE"), "SALVE MVNDE\n") class TestReteServer(unittest.TestCase): """Integration tests for PETITVR + AVSCVLTA server functionality.""" def _wait_for_server(self, port, timeout=2.0): """Poll until the server is accepting connections.""" import socket deadline = time.time() + timeout while time.time() < deadline: try: with socket.create_connection(("127.0.0.1", port), timeout=0.1): return except OSError: time.sleep(0.05) self.fail(f"Server on port {port} did not start within {timeout}s") def _free_port(self): """Find a free port in range 1024-3999 (representable without MAGNVM).""" import socket, random for _ in range(100): port = random.randint(1024, 3999) try: with socket.socket() as s: s.bind(("127.0.0.1", port)) return port except OSError: continue raise RuntimeError("Could not find a free port in range 1024-3999") def _run_server(self, source): """Parse and eval source in a daemon thread. Returns when server is ready.""" import threading lexer = Lexer().get_lexer() tokens = lexer.lex(source + "\n") program = Parser().parse(tokens) t = threading.Thread(target=program.eval, daemon=True) t.start() def test_basic_handler(self): port = self._free_port() source = ( f'CVM RETE\n' f'PETITVR("/", FVNCTIO (petitio) VT {{\n' f'REDI("SALVE MVNDE")\n' f'}})\n' f'AVSCVLTA({int_to_num(port, False)})' ) self._run_server(source) self._wait_for_server(port) import urllib.request resp = urllib.request.urlopen(f"http://127.0.0.1:{port}/") self.assertEqual(resp.read().decode(), "SALVE MVNDE") def test_multiple_routes(self): port = self._free_port() source = ( f'CVM RETE\n' f'PETITVR("/", FVNCTIO (p) VT {{\nREDI("RADIX")\n}})\n' f'PETITVR("/nomen", FVNCTIO (p) VT {{\nREDI("MARCVS")\n}})\n' f'AVSCVLTA({int_to_num(port, False)})' ) self._run_server(source) self._wait_for_server(port) import urllib.request resp1 = urllib.request.urlopen(f"http://127.0.0.1:{port}/") self.assertEqual(resp1.read().decode(), "RADIX") resp2 = urllib.request.urlopen(f"http://127.0.0.1:{port}/nomen") self.assertEqual(resp2.read().decode(), "MARCVS") def test_404_unmatched(self): port = self._free_port() source = ( f'CVM RETE\n' f'PETITVR("/", FVNCTIO (p) VT {{\nREDI("ok")\n}})\n' f'AVSCVLTA({int_to_num(port, False)})' ) self._run_server(source) self._wait_for_server(port) import urllib.request, urllib.error with self.assertRaises(urllib.error.HTTPError) as ctx: urllib.request.urlopen(f"http://127.0.0.1:{port}/nonexistent") self.assertEqual(ctx.exception.code, 404) def test_request_dict_via(self): port = self._free_port() source = ( f'CVM RETE\n' f'PETITVR("/echo", FVNCTIO (petitio) VT {{\n' f'REDI(petitio["via"])\n' f'}})\n' f'AVSCVLTA({int_to_num(port, False)})' ) self._run_server(source) self._wait_for_server(port) import urllib.request resp = urllib.request.urlopen(f"http://127.0.0.1:{port}/echo") self.assertEqual(resp.read().decode(), "/echo") def test_request_dict_quaestio(self): port = self._free_port() source = ( f'CVM RETE\n' f'PETITVR("/q", FVNCTIO (petitio) VT {{\n' f'REDI(petitio["quaestio"])\n' f'}})\n' f'AVSCVLTA({int_to_num(port, False)})' ) self._run_server(source) self._wait_for_server(port) import urllib.request resp = urllib.request.urlopen(f"http://127.0.0.1:{port}/q?nomen=Marcus") self.assertEqual(resp.read().decode(), "nomen=Marcus") def test_petitvr_stores_route(self): """PETITVR alone (without AVSCVLTA) just stores a route and returns NVLLVS.""" source = 'CVM RETE\nPETITVR("/", FVNCTIO (p) VT {\nREDI("hi")\n})' run_test(self, source, Program([ModuleCall("RETE")], [ExpressionStatement(BuiltIn("PETITVR", [ String("/"), Fvnctio([ID("p")], [Redi([String("hi")])]) ]))]), ValNul()) if __name__ == "__main__": unittest.main()