🐐 Fractions

This commit is contained in:
2026-04-01 14:45:48 +02:00
parent 2f138093e3
commit bd27857472
8 changed files with 378 additions and 67 deletions

189
tests.py
View File

@@ -4,16 +4,20 @@ from io import StringIO
from unittest.mock import patch
from parameterized import parameterized
from fractions import Fraction
from centvrion.ast_nodes import (
ArrayIndex, Bool, BinOp, BuiltIn, DataArray, DataRangeArray, Defini,
Designa, DesignaIndex, DumStatement, Erumpe, ExpressionStatement, ID,
Invoca, ModuleCall, Nullus, Numeral, PerStatement,
Program, Redi, SiStatement, String, UnaryMinus, UnaryNot,
Fractio, frac_to_fraction, fraction_to_frac,
num_to_int, int_to_num, make_string,
)
from centvrion.errors import CentvrionError
from centvrion.lexer import Lexer
from centvrion.parser import Parser
from centvrion.values import ValInt, ValStr, ValBool, ValList, ValNul, ValFunc
from centvrion.values import ValInt, ValStr, ValBool, ValList, ValNul, ValFunc, ValFrac
def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]):
random.seed(1)
@@ -368,22 +372,35 @@ class TestBuiltins(unittest.TestCase):
# --- Errors ---
error_tests = [
("x", KeyError), # undefined variable
("INVOCA f ()", KeyError), # undefined function
("DESIGNA VT III", SyntaxError), # parse error: missing id after DESIGNA
("DESIGNA x III", SyntaxError), # parse error: missing VT
("DICE(M + M + M + M)", ValueError), # output > 3999 without MAGNVM
("IIII", ValueError), # invalid Roman numeral in source
("FORTIS_NVMERVS(I, X)", ValueError), # requires FORS module
("DEFINI f (x) VT { REDI(x) }\nINVOCA f (I, II)", TypeError), # too many args
("DEFINI f (x, y) VT { REDI(x) }\nINVOCA f (I)", TypeError), # too few args
("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", TypeError), # args to zero-param function
("SI NVLLVS TVNC { DESIGNA r VT I }", TypeError), # NVLLVS cannot be used as boolean
("NVLLVS AVT VERITAS", TypeError), # NVLLVS cannot be used as boolean in AVT
('"hello" + " world"', TypeError), # use : for string concatenation, not +
("[I, II][III]", IndexError), # index too high
("CVM SVBNVLLA\n[I, II][-I]", IndexError), # negative index
("[I, II][-I]", ValueError), # negative value
("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
("DICE(M + M + M + M)", CentvrionError), # output > 3999 without MAGNVM
("IIII", CentvrionError), # invalid Roman numeral in source
("FORTIS_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
("DESIGNA x VT I\nDESIGNA x[I] VT II", CentvrionError), # index-assign to non-array
("FORTIS_ELECTIONIS([])", CentvrionError), # FORS required for FORTIS_ELECTIONIS
("CVM FORS\nFORTIS_ELECTIONIS([])", CentvrionError), # FORTIS_ELECTIONIS on empty array
("CVM FORS\nFORTIS_NVMERVS(X, I)", CentvrionError), # FORTIS_NVMERVS a > b
("PER i IN I FACE { DICE(i) }", CentvrionError), # PER over non-array
("LONGITVDO(I)", CentvrionError), # LONGITVDO on non-array
("DESIGNA x VT I\nINVOCA x ()", CentvrionError), # invoking a non-function
]
class TestErrors(unittest.TestCase):
@@ -463,7 +480,7 @@ class TestNumerals(unittest.TestCase):
num_to_int("IM", False)
def test_negative_without_svbnvlla_raises(self):
with self.assertRaises(ValueError):
with self.assertRaises(CentvrionError):
num_to_int("-IV", False)
def test_negative_with_svbnvlla(self):
@@ -606,14 +623,14 @@ class TestArithmeticEdge(unittest.TestCase):
# --- String concatenation ---
string_concat_tests = [
('"hello" : " world"', Program([], [ExpressionStatement(BinOp(String("hello"), String(" world"), "SYMBOL_COLON"))]), ValStr("hello world")),
('"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_COLON"))]), ValStr("hello")),
('"hello" : NVLLVS', Program([], [ExpressionStatement(BinOp(String("hello"), Nullus(), "SYMBOL_COLON"))]), ValStr("hello")),
('NVLLVS : NVLLVS', Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_COLON"))]), ValStr("")),
('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_COLON"))]), ValStr("value: V")),
('X : " items"', Program([], [ExpressionStatement(BinOp(Numeral("X"), String(" items"), "SYMBOL_COLON"))]), ValStr("X items")),
('"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):
@@ -1174,5 +1191,129 @@ class TestNon(unittest.TestCase):
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",
None, ValFrac(Fraction(4))),
("CVM FRACTIO\nIIIS - S",
None, ValFrac(Fraction(3))),
("CVM FRACTIO\nS * IV",
None, ValFrac(Fraction(2))),
# Division returns fraction
("CVM FRACTIO\nI / IV",
None, ValFrac(Fraction(1, 4))),
("CVM FRACTIO\nI / III",
None, ValFrac(Fraction(1, 3))),
# Integer division still works without fractions in operands... but with FRACTIO returns ValFrac
("CVM FRACTIO\nX / II",
None, ValFrac(Fraction(5))),
# String concatenation with fraction
("CVM FRACTIO\nDICE(IIIS & \"!\")",
None, ValStr("IIIS!"), "IIIS!\n"),
]
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", None, ValBool(True)), # 3.5 > 3
("CVM FRACTIO\nIII MINVS IIIS", None, ValBool(True)), # 3 < 3.5
("CVM FRACTIO\nIIIS MINVS IV", None, ValBool(True)), # 3.5 < 4
("CVM FRACTIO\nIV PLVS IIIS", None, ValBool(True)), # 4 > 3.5
("CVM FRACTIO\nIIIS PLVS IIIS", None, ValBool(False)), # 3.5 > 3.5 is False
("CVM FRACTIO\nIIIS MINVS IIIS", None, ValBool(False)), # 3.5 < 3.5 is False
# equality: fraction == fraction
("CVM FRACTIO\nIIIS EST IIIS", None, ValBool(True)),
("CVM FRACTIO\nIIIS EST IV", None, ValBool(False)),
# equality: fraction == whole number (ValFrac(4) vs ValInt(4))
("CVM FRACTIO\nIIIS + S EST IV", None, ValBool(True)), # 3.5+0.5 == 4
("CVM FRACTIO\nS + S EST I", None, ValBool(True)), # 0.5+0.5 == 1
]
class TestFractioComparisons(unittest.TestCase):
@parameterized.expand(fractio_comparison_tests)
def test_fractio_comparison(self, source, nodes, value):
run_test(self, source, nodes, value)
class TestFractioErrors(unittest.TestCase):
def test_fraction_without_module(self):
source = "IIIS\n"
lexer = Lexer().get_lexer()
tokens = lexer.lex(source)
program = Parser().parse(tokens)
with self.assertRaises(CentvrionError):
program.eval()
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") # 6*S = 36/12 >= 1 per level... wait S can only appear once
# Actually "SS" means S twice, which is 12/12 = 1, violating < 12/12 constraint
class TestFractioHelpers(unittest.TestCase):
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)
if __name__ == "__main__":
unittest.main()