Files
centvrion/tests.py
2026-04-22 11:27:19 +02:00

3029 lines
132 KiB
Python
Raw Blame History

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 <20><> 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")),
]
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
("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
('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)})),
]
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()