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