🐐 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

15
cent
View File

@@ -10,8 +10,12 @@ Options:
-c Run the compiler -c Run the compiler
FILE The file to compile/interpret FILE The file to compile/interpret
""" """
from docopt import docopt import sys
from docopt import docopt
from rply.errors import LexingError
from centvrion.errors import CentvrionError
from centvrion.lexer import Lexer from centvrion.lexer import Lexer
from centvrion.parser import Parser from centvrion.parser import Parser
from centvrion.ast_nodes import Program from centvrion.ast_nodes import Program
@@ -25,13 +29,20 @@ def main():
lexer = Lexer().get_lexer() lexer = Lexer().get_lexer()
parser = Parser() parser = Parser()
try:
tokens = lexer.lex(program_text) tokens = lexer.lex(program_text)
program = parser.parse(tokens) program = parser.parse(tokens)
except LexingError as e:
pos = e.source_pos
char = program_text[pos.idx] if pos.idx < len(program_text) else "?"
sys.exit(f"CENTVRION error: Invalid character {char!r} at line {pos.lineno}, column {pos.colno}")
if isinstance(program, Program): if isinstance(program, Program):
if args["-i"]: if args["-i"]:
try:
program.eval() program.eval()
except CentvrionError as e:
sys.exit(f"CENTVRION error: {e}")
else: else:
raise Exception("Compiler not implemented") raise Exception("Compiler not implemented")
else: else:

View File

@@ -1,9 +1,11 @@
import re import re
import random import random
from fractions import Fraction
from rply.token import BaseBox from rply.token import BaseBox
from centvrion.values import Val, ValInt, ValStr, ValBool, ValList, ValNul, ValFunc from centvrion.errors import CentvrionError
from centvrion.values import Val, ValInt, ValStr, ValBool, ValList, ValNul, ValFunc, ValFrac
NUMERALS = { NUMERALS = {
"I": 1, "I": 1,
@@ -34,7 +36,7 @@ def rep_join(l):
OP_STR = { OP_STR = {
"SYMBOL_PLUS": "+", "SYMBOL_MINUS": "-", "SYMBOL_PLUS": "+", "SYMBOL_MINUS": "-",
"SYMBOL_TIMES": "*", "SYMBOL_DIVIDE": "/", "SYMBOL_TIMES": "*", "SYMBOL_DIVIDE": "/",
"SYMBOL_COLON": ":", "SYMBOL_AMPERSAND": "&",
"KEYWORD_EST": "EST", "KEYWORD_MINVS": "MINVS", "KEYWORD_EST": "EST", "KEYWORD_MINVS": "MINVS",
"KEYWORD_PLVS": "PLVS", "KEYWORD_ET": "ET", "KEYWORD_AVT": "AVT", "KEYWORD_PLVS": "PLVS", "KEYWORD_ET": "ET", "KEYWORD_AVT": "AVT",
} }
@@ -42,12 +44,12 @@ OP_STR = {
def single_num_to_int(i, m): def single_num_to_int(i, m):
if i[-1] == "_": if i[-1] == "_":
if not m: if not m:
raise ValueError( raise CentvrionError(
"Cannot calculate numbers above 3999 without 'MAGNVM' module" "Cannot calculate numbers above 3999 without 'MAGNVM' module"
) )
if i[0] != "I": if i[0] != "I":
raise ValueError( raise CentvrionError(
"Cannot use 'I' with thousands operator, use 'M' instead" "Cannot use 'I' with thousands operator, use 'M' instead"
) )
@@ -58,16 +60,16 @@ def single_num_to_int(i, m):
def num_to_int(n, m, s=False): def num_to_int(n, m, s=False):
if n.startswith("-"): if n.startswith("-"):
if not s: if not s:
raise ValueError("Cannot use negative numbers without 'SVBNVLLA' module") raise CentvrionError("Cannot use negative numbers without 'SVBNVLLA' module")
return -num_to_int(n[1:], m, s) return -num_to_int(n[1:], m, s)
chars = re.findall(r"[IVXLCDM]_*", n) chars = re.findall(r"[IVXLCDM]_*", n)
if ''.join(chars) != n: if ''.join(chars) != n:
raise ValueError("Invalid numeral", n) raise CentvrionError(f"Invalid numeral: {n!r}")
nums = [single_num_to_int(i, m) for i in chars] nums = [single_num_to_int(i, m) for i in chars]
new_nums = nums.copy() new_nums = nums.copy()
for x, num in enumerate(nums[:-3]): for x, num in enumerate(nums[:-3]):
if all(num == nums[x+i] for i in range(0,4)): if all(num == nums[x+i] for i in range(0,4)):
raise ValueError(n, "is not a valid roman numeral") raise CentvrionError(f"{n!r} is not a valid roman numeral")
while True: while True:
for x, num in enumerate(nums[:-1]): for x, num in enumerate(nums[:-1]):
@@ -77,7 +79,7 @@ def num_to_int(n, m, s=False):
new_nums[x+1] = 0 new_nums[x+1] = 0
break break
else: else:
raise ValueError(n, "is not a valid roman numeral") raise CentvrionError(f"{n!r} is not a valid roman numeral")
if new_nums != nums: if new_nums != nums:
nums = [i for i in new_nums if i != 0] nums = [i for i in new_nums if i != 0]
@@ -86,18 +88,18 @@ def num_to_int(n, m, s=False):
break break
if nums != sorted(nums)[::-1]: if nums != sorted(nums)[::-1]:
raise Exception(n, "is not a valid roman numeral") raise CentvrionError(f"{n!r} is not a valid roman numeral")
return sum(nums) return sum(nums)
def int_to_num(n, m, s=False): def int_to_num(n, m, s=False):
if n < 0: if n < 0:
if not s: if not s:
raise ValueError("Cannot display negative numbers without 'SVBNVLLA' module") raise CentvrionError("Cannot display negative numbers without 'SVBNVLLA' module")
return "-" + int_to_num(-n, m, s) return "-" + int_to_num(-n, m, s)
if n > 3999: if n > 3999:
if not m: if not m:
raise ValueError( raise CentvrionError(
"Cannot display numbers above 3999 without 'MAGNVM' module" "Cannot display numbers above 3999 without 'MAGNVM' module"
) )
thousands_chars = re.findall(r"[IVXLCDM]_*", int_to_num(n//1000, m, s)) thousands_chars = re.findall(r"[IVXLCDM]_*", int_to_num(n//1000, m, s))
@@ -123,6 +125,8 @@ def make_string(val, magnvm=False, svbnvlla=False):
return val.value() return val.value()
elif isinstance(val, ValInt): elif isinstance(val, ValInt):
return int_to_num(val.value(), magnvm, svbnvlla) return int_to_num(val.value(), magnvm, svbnvlla)
elif isinstance(val, ValFrac):
return fraction_to_frac(val.value(), magnvm, svbnvlla)
elif isinstance(val, ValBool): elif isinstance(val, ValBool):
return "VERVS" if val.value() else "FALSVS" return "VERVS" if val.value() else "FALSVS"
elif isinstance(val, ValNul): elif isinstance(val, ValNul):
@@ -131,7 +135,62 @@ def make_string(val, magnvm=False, svbnvlla=False):
inner = ' '.join(make_string(i, magnvm, svbnvlla) for i in val.value()) inner = ' '.join(make_string(i, magnvm, svbnvlla) for i in val.value())
return f"[{inner}]" return f"[{inner}]"
else: else:
raise TypeError(f"Cannot display {val!r}") raise CentvrionError(f"Cannot display {val!r}")
FRAC_SYMBOLS = [("S", 6), (":", 2), (".", 1)]
def frac_to_fraction(s, magnvm=False, svbnvlla=False):
match = re.match(r'^([IVXLCDM][IVXLCDM_]*)(.*)', s)
if match:
int_str, frac_str = match.group(1), match.group(2)
total = Fraction(num_to_int(int_str, magnvm, svbnvlla))
else:
total = Fraction(0)
frac_str = s
for level_idx, level in enumerate(frac_str.split('|')):
divisor = 12 ** (level_idx + 1)
level_val = 0
prev_weight = 12
for ch in level:
weight = next((w for sym, w in FRAC_SYMBOLS if sym == ch), None)
if weight is None:
raise CentvrionError(f"Invalid fraction symbol: {ch!r}")
if weight > prev_weight:
raise CentvrionError("Fraction symbols must be written highest to lowest")
prev_weight = weight
level_val += weight
if level_val >= 12:
raise CentvrionError("Fraction level value must be less than 1 (< 12/12)")
total += Fraction(level_val, divisor)
return total
def fraction_to_frac(f, magnvm=False, svbnvlla=False):
if f < 0:
if not svbnvlla:
raise CentvrionError("Cannot display negative fractions without 'SVBNVLLA' module")
return "-" + fraction_to_frac(-f, magnvm, svbnvlla)
integer_part = int(f)
remainder = f - integer_part
int_str = int_to_num(integer_part, magnvm, svbnvlla) if integer_part > 0 else ""
levels = []
current = remainder
for _ in range(6):
if current == 0:
break
current = current * 12
level_int = int(current)
current = current - level_int
s_count = level_int // 6
remaining = level_int % 6
colon_count = remaining // 2
dot_count = remaining % 2
levels.append("S" * s_count + ":" * colon_count + "." * dot_count)
return int_str + "|".join(levels)
class Node(BaseBox): class Node(BaseBox):
@@ -200,6 +259,8 @@ class DataRangeArray(Node):
def _eval(self, vtable): def _eval(self, vtable):
vtable, from_val = self.from_value.eval(vtable) vtable, from_val = self.from_value.eval(vtable)
vtable, to_val = self.to_value.eval(vtable) vtable, to_val = self.to_value.eval(vtable)
if not isinstance(from_val, ValInt) or not isinstance(to_val, ValInt):
raise CentvrionError("Range bounds must be numbers")
return vtable, ValList([ValInt(i) for i in range(from_val.value(), to_val.value())]) return vtable, ValList([ValInt(i) for i in range(from_val.value(), to_val.value())])
@@ -237,6 +298,27 @@ class Numeral(Node):
return vtable, ValInt(num_to_int(self.value, "MAGNVM" in vtable["#modules"], "SVBNVLLA" in vtable["#modules"])) return vtable, ValInt(num_to_int(self.value, "MAGNVM" in vtable["#modules"], "SVBNVLLA" in vtable["#modules"]))
class Fractio(Node):
def __init__(self, value) -> None:
self.value = value
def __eq__(self, other):
return type(self) == type(other) and self.value == other.value
def __repr__(self):
return f"Fractio({self.value!r})"
def print(self):
return self.value
def _eval(self, vtable):
if "FRACTIO" not in vtable["#modules"]:
raise CentvrionError("Cannot use fraction literals without 'FRACTIO' module")
m = "MAGNVM" in vtable["#modules"]
s = "SVBNVLLA" in vtable["#modules"]
return vtable, ValFrac(frac_to_fraction(self.value, m, s))
class Bool(Node): class Bool(Node):
def __init__(self, value) -> None: def __init__(self, value) -> None:
self.value = value self.value = value
@@ -282,6 +364,8 @@ class ID(Node):
return self.name return self.name
def _eval(self, vtable): def _eval(self, vtable):
if self.name not in vtable:
raise CentvrionError(f"Undefined variable: {self.name}")
return vtable, vtable[self.name] return vtable, vtable[self.name]
@@ -325,10 +409,15 @@ class DesignaIndex(Node):
def _eval(self, vtable): def _eval(self, vtable):
vtable, index = self.index.eval(vtable) vtable, index = self.index.eval(vtable)
vtable, val = self.value.eval(vtable) vtable, val = self.value.eval(vtable)
if self.id.name not in vtable:
raise CentvrionError(f"Undefined variable: {self.id.name}")
target = vtable[self.id.name]
if not isinstance(target, ValList):
raise CentvrionError(f"{self.id.name} is not an array")
i = index.value() i = index.value()
lst = list(vtable[self.id.name].value()) lst = list(target.value())
if i < 1 or i > len(lst): if i < 1 or i > len(lst):
raise IndexError(f"Index {i} out of range for array of length {len(lst)}") raise CentvrionError(f"Index {i} out of range for array of length {len(lst)}")
lst[i - 1] = val lst[i - 1] = val
vtable[self.id.name] = ValList(lst) vtable[self.id.name] = ValList(lst)
return vtable, ValNul() return vtable, ValNul()
@@ -440,29 +529,49 @@ class BinOp(Node):
match self.op: match self.op:
case "SYMBOL_PLUS": case "SYMBOL_PLUS":
if isinstance(lv, str) or isinstance(rv, str): if isinstance(lv, str) or isinstance(rv, str):
raise TypeError("Use : for string concatenation, not +") raise CentvrionError("Use & for string concatenation, not +")
if isinstance(lv, list) or isinstance(rv, list):
raise CentvrionError("Cannot use + on arrays")
if lv is None and rv is None: if lv is None and rv is None:
return vtable, ValNul() return vtable, ValNul()
return vtable, ValInt((lv or 0) + (rv or 0)) result = (lv or 0) + (rv or 0)
case "SYMBOL_COLON": return vtable, ValFrac(result) if isinstance(result, Fraction) else ValInt(result)
case "SYMBOL_AMPERSAND":
magnvm = "MAGNVM" in vtable["#modules"] magnvm = "MAGNVM" in vtable["#modules"]
svbnvlla = "SVBNVLLA" in vtable["#modules"] svbnvlla = "SVBNVLLA" in vtable["#modules"]
def _coerce(v): def _coerce(v):
if v is None: return "" if v is None: return ""
if isinstance(v, bool): return "VERITAS" if v else "FALSITAS" if isinstance(v, bool): return "VERITAS" if v else "FALSITAS"
if isinstance(v, int): return int_to_num(v, magnvm, svbnvlla) if isinstance(v, int): return int_to_num(v, magnvm, svbnvlla)
if isinstance(v, Fraction): return fraction_to_frac(v, magnvm, svbnvlla)
if isinstance(v, list): return make_string(ValList(v), magnvm, svbnvlla)
return v return v
return vtable, ValStr(_coerce(lv) + _coerce(rv)) return vtable, ValStr(_coerce(lv) + _coerce(rv))
case "SYMBOL_MINUS": case "SYMBOL_MINUS":
return vtable, ValInt((lv or 0) - (rv or 0)) if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
raise CentvrionError("Cannot use - on strings or arrays")
result = (lv or 0) - (rv or 0)
return vtable, ValFrac(result) if isinstance(result, Fraction) else ValInt(result)
case "SYMBOL_TIMES": case "SYMBOL_TIMES":
return vtable, ValInt((lv or 0) * (rv or 0)) if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
raise CentvrionError("Cannot use * on strings or arrays")
result = (lv or 0) * (rv or 0)
return vtable, ValFrac(result) if isinstance(result, Fraction) else ValInt(result)
case "SYMBOL_DIVIDE": case "SYMBOL_DIVIDE":
# TODO: Fractio if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
raise CentvrionError("Cannot use / on strings or arrays")
if (rv or 0) == 0:
raise CentvrionError("Division by zero")
if isinstance(lv, Fraction) or isinstance(rv, Fraction) or "FRACTIO" in vtable["#modules"]:
return vtable, ValFrac(Fraction(lv or 0) / Fraction(rv or 0))
return vtable, ValInt((lv or 0) // (rv or 0)) return vtable, ValInt((lv or 0) // (rv or 0))
case "KEYWORD_MINVS": case "KEYWORD_MINVS":
if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
raise CentvrionError("Cannot compare strings or arrays with MINVS")
return vtable, ValBool((lv or 0) < (rv or 0)) return vtable, ValBool((lv or 0) < (rv or 0))
case "KEYWORD_PLVS": case "KEYWORD_PLVS":
if isinstance(lv, (str, list)) or isinstance(rv, (str, list)):
raise CentvrionError("Cannot compare strings or arrays with PLVS")
return vtable, ValBool((lv or 0) > (rv or 0)) return vtable, ValBool((lv or 0) > (rv or 0))
case "KEYWORD_EST": case "KEYWORD_EST":
return vtable, ValBool(lv == rv) return vtable, ValBool(lv == rv)
@@ -489,8 +598,10 @@ class UnaryMinus(Node):
def _eval(self, vtable): def _eval(self, vtable):
if "SVBNVLLA" not in vtable["#modules"]: if "SVBNVLLA" not in vtable["#modules"]:
raise ValueError("Cannot use unary minus without 'SVBNVLLA' module") raise CentvrionError("Cannot use unary minus without 'SVBNVLLA' module")
vtable, val = self.expr.eval(vtable) vtable, val = self.expr.eval(vtable)
if not isinstance(val, ValInt):
raise CentvrionError("Unary minus requires a number")
return vtable, ValInt(-val.value()) return vtable, ValInt(-val.value())
@@ -529,10 +640,14 @@ class ArrayIndex(Node):
def _eval(self, vtable): def _eval(self, vtable):
vtable, array = self.array.eval(vtable) vtable, array = self.array.eval(vtable)
vtable, index = self.index.eval(vtable) vtable, index = self.index.eval(vtable)
if not isinstance(array, ValList):
raise CentvrionError("Cannot index a non-array value")
if not isinstance(index, ValInt):
raise CentvrionError("Array index must be a number")
i = index.value() i = index.value()
lst = array.value() lst = array.value()
if i < 1 or i > len(lst): if i < 1 or i > len(lst):
raise IndexError(f"Index {i} out of range for array of length {len(lst)}") raise CentvrionError(f"Index {i} out of range for array of length {len(lst)}")
return vtable, lst[i - 1] return vtable, lst[i - 1]
@@ -628,6 +743,8 @@ class PerStatement(Node):
def _eval(self, vtable): def _eval(self, vtable):
vtable, array = self.data_list.eval(vtable) vtable, array = self.data_list.eval(vtable)
if not isinstance(array, ValList):
raise CentvrionError("PER requires an array")
variable_name = self.variable_name.name variable_name = self.variable_name.name
last_val = ValNul() last_val = ValNul()
for item in array: for item in array:
@@ -662,9 +779,13 @@ class Invoca(Node):
def _eval(self, vtable): def _eval(self, vtable):
params = [p.eval(vtable)[1] for p in self.parameters] params = [p.eval(vtable)[1] for p in self.parameters]
if self.name.name not in vtable:
raise CentvrionError(f"Undefined function: {self.name.name}")
func = vtable[self.name.name] func = vtable[self.name.name]
if not isinstance(func, ValFunc):
raise CentvrionError(f"{self.name.name} is not a function")
if len(params) != len(func.params): if len(params) != len(func.params):
raise TypeError( raise CentvrionError(
f"{self.name.name} expects {len(func.params)} argument(s), got {len(params)}" f"{self.name.name} expects {len(func.params)} argument(s), got {len(params)}"
) )
func_vtable = vtable.copy() func_vtable = vtable.copy()
@@ -702,7 +823,11 @@ class BuiltIn(Node):
match self.builtin: match self.builtin:
case "AVDI_NVMERVS": case "AVDI_NVMERVS":
return vtable, ValInt(num_to_int(input(), magnvm, svbnvlla)) raw = input()
try:
return vtable, ValInt(num_to_int(raw, magnvm, svbnvlla))
except ValueError:
raise CentvrionError(f"Invalid numeral input: {raw!r}")
case "AVDI": case "AVDI":
return vtable, ValStr(input()) return vtable, ValStr(input())
case "DICE": case "DICE":
@@ -716,17 +841,25 @@ class BuiltIn(Node):
return vtable, ValNul() return vtable, ValNul()
case "FORTIS_NVMERVS": case "FORTIS_NVMERVS":
if "FORS" not in vtable["#modules"]: if "FORS" not in vtable["#modules"]:
raise ValueError( raise CentvrionError("Cannot use 'FORTIS_NVMERVS' without module 'FORS'")
"Cannot use 'FORTIS_NVMERVS' without module 'FORS'" a, b = params[0].value(), params[1].value()
) if not isinstance(a, int) or not isinstance(b, int):
return vtable, ValInt(random.randint(params[0].value(), params[1].value())) raise CentvrionError("FORTIS_NVMERVS requires two numbers")
if a > b:
raise CentvrionError(f"FORTIS_NVMERVS: first argument ({a}) must be ≤ second ({b})")
return vtable, ValInt(random.randint(a, b))
case "FORTIS_ELECTIONIS": case "FORTIS_ELECTIONIS":
if "FORS" not in vtable["#modules"]: if "FORS" not in vtable["#modules"]:
raise ValueError( raise CentvrionError("Cannot use 'FORTIS_ELECTIONIS' without module 'FORS'")
"Cannot use 'FORTIS_ELECTIONIS' without module 'FORS'" if not isinstance(params[0], ValList):
) raise CentvrionError("FORTIS_ELECTIONIS requires an array")
return vtable, params[0].value()[random.randint(0, len(params[0].value()) - 1)] lst = params[0].value()
if len(lst) == 0:
raise CentvrionError("FORTIS_ELECTIONIS: cannot select from an empty array")
return vtable, lst[random.randint(0, len(lst) - 1)]
case "LONGITVDO": case "LONGITVDO":
if not isinstance(params[0], ValList):
raise CentvrionError("LONGITVDO requires an array")
return vtable, ValInt(len(params[0].value())) return vtable, ValInt(len(params[0].value()))
case _: case _:
raise NotImplementedError(self.builtin) raise NotImplementedError(self.builtin)

1
centvrion/errors.py Normal file
View File

@@ -0,0 +1 @@
class CentvrionError(Exception): pass

View File

@@ -41,6 +41,7 @@ builtin_tokens = [("BUILTIN", i) for i in [
data_tokens = [ data_tokens = [
("DATA_STRING", r"(\".*?\"|'.*?')"), ("DATA_STRING", r"(\".*?\"|'.*?')"),
("DATA_FRACTION", r"([IVXLCDM][IVXLCDM_]*)?([S][S:.|]*|:[S:.|]+|\.[S:.|]*)"),
("DATA_NUMERAL", r"[IVXLCDM][IVXLCDM_]*") ("DATA_NUMERAL", r"[IVXLCDM][IVXLCDM_]*")
] ]
@@ -62,7 +63,7 @@ symbol_tokens = [
("SYMBOL_MINUS", r"\-"), ("SYMBOL_MINUS", r"\-"),
("SYMBOL_TIMES", r"\*"), ("SYMBOL_TIMES", r"\*"),
("SYMBOL_DIVIDE", r"\/"), ("SYMBOL_DIVIDE", r"\/"),
("SYMBOL_COLON", r":"), ("SYMBOL_AMPERSAND", r"&"),
("SYMBOL_COMMA", r",") ("SYMBOL_COMMA", r",")
] ]
@@ -74,8 +75,8 @@ all_tokens = (
keyword_tokens + keyword_tokens +
builtin_tokens + builtin_tokens +
module_tokens + module_tokens +
symbol_tokens +
data_tokens + data_tokens +
symbol_tokens +
whitespace_tokens + whitespace_tokens +
[("ID", f"({valid_characters})+")] [("ID", f"({valid_characters})+")]
) )

View File

@@ -13,7 +13,7 @@ class Parser():
('left', ["KEYWORD_AVT"]), ('left', ["KEYWORD_AVT"]),
('left', ["KEYWORD_ET"]), ('left', ["KEYWORD_ET"]),
('left', ["KEYWORD_PLVS", "KEYWORD_MINVS", "KEYWORD_EST"]), ('left', ["KEYWORD_PLVS", "KEYWORD_MINVS", "KEYWORD_EST"]),
('left', ["SYMBOL_COLON", "SYMBOL_PLUS", "SYMBOL_MINUS"]), ('left', ["SYMBOL_AMPERSAND", "SYMBOL_PLUS", "SYMBOL_MINUS"]),
('left', ["SYMBOL_TIMES", "SYMBOL_DIVIDE"]), ('left', ["SYMBOL_TIMES", "SYMBOL_DIVIDE"]),
('right', ["UMINUS", "UNOT"]), ('right', ["UMINUS", "UNOT"]),
('left', ["SYMBOL_LBRACKET", "INDEX"]), ('left', ["SYMBOL_LBRACKET", "INDEX"]),
@@ -169,6 +169,10 @@ class Parser():
def expression_numeral(tokens): def expression_numeral(tokens):
return ast_nodes.Numeral(tokens[0].value) return ast_nodes.Numeral(tokens[0].value)
@self.pg.production('expression : DATA_FRACTION')
def expression_fraction(tokens):
return ast_nodes.Fractio(tokens[0].value)
@self.pg.production('expression : KEYWORD_FALSITAS') @self.pg.production('expression : KEYWORD_FALSITAS')
@self.pg.production('expression : KEYWORD_VERITAS') @self.pg.production('expression : KEYWORD_VERITAS')
def expression_bool(tokens): def expression_bool(tokens):
@@ -178,7 +182,7 @@ class Parser():
def expression_nullus(_): def expression_nullus(_):
return ast_nodes.Nullus() return ast_nodes.Nullus()
@self.pg.production('expression : expression SYMBOL_COLON expression') @self.pg.production('expression : expression SYMBOL_AMPERSAND expression')
@self.pg.production('expression : expression SYMBOL_MINUS expression') @self.pg.production('expression : expression SYMBOL_MINUS expression')
@self.pg.production('expression : expression SYMBOL_PLUS expression') @self.pg.production('expression : expression SYMBOL_PLUS expression')
@self.pg.production('expression : expression SYMBOL_TIMES expression') @self.pg.production('expression : expression SYMBOL_TIMES expression')

View File

@@ -1,4 +1,7 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from fractions import Fraction
from centvrion.errors import CentvrionError
class Val(ABC): class Val(ABC):
@abstractmethod @abstractmethod
@@ -63,12 +66,29 @@ class ValList(Val):
def __iter__(self): def __iter__(self):
return iter(self._v) return iter(self._v)
class ValFrac(Val):
def __init__(self, v: Fraction):
assert isinstance(v, Fraction)
self._v = v
def value(self):
return self._v
def __bool__(self):
return self._v != 0
def __lt__(self, other):
return self._v < other.value()
def __gt__(self, other):
return self._v > other.value()
class ValNul(Val): class ValNul(Val):
def value(self): def value(self):
return None return None
def __bool__(self): def __bool__(self):
raise TypeError("NVLLVS cannot be evaluated as a boolean") raise CentvrionError("NVLLVS cannot be evaluated as a boolean")
class ValFunc(Val): class ValFunc(Val):
def __init__(self, params: list, body: list): def __init__(self, params: list, body: list):

View File

@@ -4,7 +4,7 @@ DESIGNA n VT X
DONICVM i VT I VSQVE n + I FACE { DONICVM i VT I VSQVE n + I FACE {
DESIGNA line VT "" DESIGNA line VT ""
DONICVM k VT I VSQVE n + I FACE { DONICVM k VT I VSQVE n + I FACE {
DESIGNA line VT line : i * k : " " DESIGNA line VT line & i * k & " "
} }
DICE(line) DICE(line)
} }

185
tests.py
View File

@@ -4,16 +4,20 @@ from io import StringIO
from unittest.mock import patch from unittest.mock import patch
from parameterized import parameterized from parameterized import parameterized
from fractions import Fraction
from centvrion.ast_nodes import ( from centvrion.ast_nodes import (
ArrayIndex, Bool, BinOp, BuiltIn, DataArray, DataRangeArray, Defini, ArrayIndex, Bool, BinOp, BuiltIn, DataArray, DataRangeArray, Defini,
Designa, DesignaIndex, DumStatement, Erumpe, ExpressionStatement, ID, Designa, DesignaIndex, DumStatement, Erumpe, ExpressionStatement, ID,
Invoca, ModuleCall, Nullus, Numeral, PerStatement, Invoca, ModuleCall, Nullus, Numeral, PerStatement,
Program, Redi, SiStatement, String, UnaryMinus, UnaryNot, Program, Redi, SiStatement, String, UnaryMinus, UnaryNot,
Fractio, frac_to_fraction, fraction_to_frac,
num_to_int, int_to_num, make_string, num_to_int, int_to_num, make_string,
) )
from centvrion.errors import CentvrionError
from centvrion.lexer import Lexer from centvrion.lexer import Lexer
from centvrion.parser import Parser 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=[]): def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]):
random.seed(1) random.seed(1)
@@ -368,22 +372,35 @@ class TestBuiltins(unittest.TestCase):
# --- Errors --- # --- Errors ---
error_tests = [ error_tests = [
("x", KeyError), # undefined variable ("x", CentvrionError), # undefined variable
("INVOCA f ()", KeyError), # undefined function ("INVOCA f ()", CentvrionError), # undefined function
("DESIGNA VT III", SyntaxError), # parse error: missing id after DESIGNA ("DESIGNA VT III", SyntaxError), # parse error: missing id after DESIGNA
("DESIGNA x III", SyntaxError), # parse error: missing VT ("DESIGNA x III", SyntaxError), # parse error: missing VT
("DICE(M + M + M + M)", ValueError), # output > 3999 without MAGNVM ("DICE(M + M + M + M)", CentvrionError), # output > 3999 without MAGNVM
("IIII", ValueError), # invalid Roman numeral in source ("IIII", CentvrionError), # invalid Roman numeral in source
("FORTIS_NVMERVS(I, X)", ValueError), # requires FORS module ("FORTIS_NVMERVS(I, X)", CentvrionError), # requires FORS module
("DEFINI f (x) VT { REDI(x) }\nINVOCA f (I, II)", TypeError), # too many args ("DEFINI f (x) VT { REDI(x) }\nINVOCA f (I, II)", CentvrionError), # too many args
("DEFINI f (x, y) VT { REDI(x) }\nINVOCA f (I)", TypeError), # too few args ("DEFINI f (x, y) VT { REDI(x) }\nINVOCA f (I)", CentvrionError), # too few args
("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", TypeError), # args to zero-param function ("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", CentvrionError), # args to zero-param function
("SI NVLLVS TVNC { DESIGNA r VT I }", TypeError), # NVLLVS cannot be used as boolean ("SI NVLLVS TVNC { DESIGNA r VT I }", CentvrionError), # NVLLVS cannot be used as boolean
("NVLLVS AVT VERITAS", TypeError), # NVLLVS cannot be used as boolean in AVT ("NVLLVS AVT VERITAS", CentvrionError), # NVLLVS cannot be used as boolean in AVT
('"hello" + " world"', TypeError), # use : for string concatenation, not + ('"hello" + " world"', CentvrionError), # use : for string concatenation, not +
("[I, II][III]", IndexError), # index too high ("[I, II][III]", CentvrionError), # index too high
("CVM SVBNVLLA\n[I, II][-I]", IndexError), # negative index ("CVM SVBNVLLA\n[I, II][-I]", CentvrionError), # negative index
("[I, II][-I]", ValueError), # negative value ("[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): class TestErrors(unittest.TestCase):
@@ -463,7 +480,7 @@ class TestNumerals(unittest.TestCase):
num_to_int("IM", False) num_to_int("IM", False)
def test_negative_without_svbnvlla_raises(self): def test_negative_without_svbnvlla_raises(self):
with self.assertRaises(ValueError): with self.assertRaises(CentvrionError):
num_to_int("-IV", False) num_to_int("-IV", False)
def test_negative_with_svbnvlla(self): def test_negative_with_svbnvlla(self):
@@ -606,14 +623,14 @@ class TestArithmeticEdge(unittest.TestCase):
# --- String concatenation --- # --- String concatenation ---
string_concat_tests = [ 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 coerces to "" in string context
('NVLLVS : "hello"', Program([], [ExpressionStatement(BinOp(Nullus(), String("hello"), "SYMBOL_COLON"))]), ValStr("hello")), ('NVLLVS & "hello"', Program([], [ExpressionStatement(BinOp(Nullus(), String("hello"), "SYMBOL_AMPERSAND"))]), ValStr("hello")),
('"hello" : NVLLVS', Program([], [ExpressionStatement(BinOp(String("hello"), Nullus(), "SYMBOL_COLON"))]), ValStr("hello")), ('"hello" & NVLLVS', Program([], [ExpressionStatement(BinOp(String("hello"), Nullus(), "SYMBOL_AMPERSAND"))]), ValStr("hello")),
('NVLLVS : NVLLVS', Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_COLON"))]), ValStr("")), ('NVLLVS & NVLLVS', Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_AMPERSAND"))]), ValStr("")),
# integers coerce to Roman numerals in string context # integers coerce to Roman numerals in string context
('"value: " : V', Program([], [ExpressionStatement(BinOp(String("value: "), Numeral("V"), "SYMBOL_COLON"))]), ValStr("value: V")), ('"value: " & V', Program([], [ExpressionStatement(BinOp(String("value: "), Numeral("V"), "SYMBOL_AMPERSAND"))]), ValStr("value: V")),
('X : " items"', Program([], [ExpressionStatement(BinOp(Numeral("X"), String(" items"), "SYMBOL_COLON"))]), ValStr("X items")), ('X & " items"', Program([], [ExpressionStatement(BinOp(Numeral("X"), String(" items"), "SYMBOL_AMPERSAND"))]), ValStr("X items")),
] ]
class TestStringConcat(unittest.TestCase): class TestStringConcat(unittest.TestCase):
@@ -1174,5 +1191,129 @@ class TestNon(unittest.TestCase):
run_test(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",
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__": if __name__ == "__main__":
unittest.main() unittest.main()