|
|
|
|
@@ -1,9 +1,11 @@
|
|
|
|
|
import re
|
|
|
|
|
import random
|
|
|
|
|
from fractions import Fraction
|
|
|
|
|
|
|
|
|
|
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 = {
|
|
|
|
|
"I": 1,
|
|
|
|
|
@@ -34,7 +36,7 @@ def rep_join(l):
|
|
|
|
|
OP_STR = {
|
|
|
|
|
"SYMBOL_PLUS": "+", "SYMBOL_MINUS": "-",
|
|
|
|
|
"SYMBOL_TIMES": "*", "SYMBOL_DIVIDE": "/",
|
|
|
|
|
"SYMBOL_COLON": ":",
|
|
|
|
|
"SYMBOL_AMPERSAND": "&",
|
|
|
|
|
"KEYWORD_EST": "EST", "KEYWORD_MINVS": "MINVS",
|
|
|
|
|
"KEYWORD_PLVS": "PLVS", "KEYWORD_ET": "ET", "KEYWORD_AVT": "AVT",
|
|
|
|
|
}
|
|
|
|
|
@@ -42,12 +44,12 @@ OP_STR = {
|
|
|
|
|
def single_num_to_int(i, m):
|
|
|
|
|
if i[-1] == "_":
|
|
|
|
|
if not m:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
raise CentvrionError(
|
|
|
|
|
"Cannot calculate numbers above 3999 without 'MAGNVM' module"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if i[0] != "I":
|
|
|
|
|
raise ValueError(
|
|
|
|
|
raise CentvrionError(
|
|
|
|
|
"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):
|
|
|
|
|
if n.startswith("-"):
|
|
|
|
|
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)
|
|
|
|
|
chars = re.findall(r"[IVXLCDM]_*", 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]
|
|
|
|
|
new_nums = nums.copy()
|
|
|
|
|
for x, num in enumerate(nums[:-3]):
|
|
|
|
|
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:
|
|
|
|
|
for x, num in enumerate(nums[:-1]):
|
|
|
|
|
@@ -77,7 +79,7 @@ def num_to_int(n, m, s=False):
|
|
|
|
|
new_nums[x+1] = 0
|
|
|
|
|
break
|
|
|
|
|
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:
|
|
|
|
|
nums = [i for i in new_nums if i != 0]
|
|
|
|
|
@@ -86,18 +88,18 @@ def num_to_int(n, m, s=False):
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
def int_to_num(n, m, s=False):
|
|
|
|
|
if n < 0:
|
|
|
|
|
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)
|
|
|
|
|
if n > 3999:
|
|
|
|
|
if not m:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
raise CentvrionError(
|
|
|
|
|
"Cannot display numbers above 3999 without 'MAGNVM' module"
|
|
|
|
|
)
|
|
|
|
|
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()
|
|
|
|
|
elif isinstance(val, ValInt):
|
|
|
|
|
return int_to_num(val.value(), magnvm, svbnvlla)
|
|
|
|
|
elif isinstance(val, ValFrac):
|
|
|
|
|
return fraction_to_frac(val.value(), magnvm, svbnvlla)
|
|
|
|
|
elif isinstance(val, ValBool):
|
|
|
|
|
return "VERVS" if val.value() else "FALSVS"
|
|
|
|
|
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())
|
|
|
|
|
return f"[{inner}]"
|
|
|
|
|
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):
|
|
|
|
|
@@ -200,6 +259,8 @@ class DataRangeArray(Node):
|
|
|
|
|
def _eval(self, vtable):
|
|
|
|
|
vtable, from_val = self.from_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())])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -237,6 +298,27 @@ class Numeral(Node):
|
|
|
|
|
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):
|
|
|
|
|
def __init__(self, value) -> None:
|
|
|
|
|
self.value = value
|
|
|
|
|
@@ -282,6 +364,8 @@ class ID(Node):
|
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
def _eval(self, vtable):
|
|
|
|
|
if self.name not in vtable:
|
|
|
|
|
raise CentvrionError(f"Undefined variable: {self.name}")
|
|
|
|
|
return vtable, vtable[self.name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -325,10 +409,15 @@ class DesignaIndex(Node):
|
|
|
|
|
def _eval(self, vtable):
|
|
|
|
|
vtable, index = self.index.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()
|
|
|
|
|
lst = list(vtable[self.id.name].value())
|
|
|
|
|
lst = list(target.value())
|
|
|
|
|
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
|
|
|
|
|
vtable[self.id.name] = ValList(lst)
|
|
|
|
|
return vtable, ValNul()
|
|
|
|
|
@@ -440,29 +529,49 @@ class BinOp(Node):
|
|
|
|
|
match self.op:
|
|
|
|
|
case "SYMBOL_PLUS":
|
|
|
|
|
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:
|
|
|
|
|
return vtable, ValNul()
|
|
|
|
|
return vtable, ValInt((lv or 0) + (rv or 0))
|
|
|
|
|
case "SYMBOL_COLON":
|
|
|
|
|
result = (lv or 0) + (rv or 0)
|
|
|
|
|
return vtable, ValFrac(result) if isinstance(result, Fraction) else ValInt(result)
|
|
|
|
|
case "SYMBOL_AMPERSAND":
|
|
|
|
|
magnvm = "MAGNVM" in vtable["#modules"]
|
|
|
|
|
svbnvlla = "SVBNVLLA" in vtable["#modules"]
|
|
|
|
|
def _coerce(v):
|
|
|
|
|
if v is None: return ""
|
|
|
|
|
if isinstance(v, bool): return "VERITAS" if v else "FALSITAS"
|
|
|
|
|
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 vtable, ValStr(_coerce(lv) + _coerce(rv))
|
|
|
|
|
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":
|
|
|
|
|
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":
|
|
|
|
|
# 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))
|
|
|
|
|
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))
|
|
|
|
|
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))
|
|
|
|
|
case "KEYWORD_EST":
|
|
|
|
|
return vtable, ValBool(lv == rv)
|
|
|
|
|
@@ -489,8 +598,10 @@ class UnaryMinus(Node):
|
|
|
|
|
|
|
|
|
|
def _eval(self, vtable):
|
|
|
|
|
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)
|
|
|
|
|
if not isinstance(val, ValInt):
|
|
|
|
|
raise CentvrionError("Unary minus requires a number")
|
|
|
|
|
return vtable, ValInt(-val.value())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -529,10 +640,14 @@ class ArrayIndex(Node):
|
|
|
|
|
def _eval(self, vtable):
|
|
|
|
|
vtable, array = self.array.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()
|
|
|
|
|
lst = array.value()
|
|
|
|
|
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]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -628,6 +743,8 @@ class PerStatement(Node):
|
|
|
|
|
|
|
|
|
|
def _eval(self, 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
|
|
|
|
|
last_val = ValNul()
|
|
|
|
|
for item in array:
|
|
|
|
|
@@ -662,9 +779,13 @@ class Invoca(Node):
|
|
|
|
|
|
|
|
|
|
def _eval(self, vtable):
|
|
|
|
|
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]
|
|
|
|
|
if not isinstance(func, ValFunc):
|
|
|
|
|
raise CentvrionError(f"{self.name.name} is not a function")
|
|
|
|
|
if len(params) != len(func.params):
|
|
|
|
|
raise TypeError(
|
|
|
|
|
raise CentvrionError(
|
|
|
|
|
f"{self.name.name} expects {len(func.params)} argument(s), got {len(params)}"
|
|
|
|
|
)
|
|
|
|
|
func_vtable = vtable.copy()
|
|
|
|
|
@@ -702,7 +823,11 @@ class BuiltIn(Node):
|
|
|
|
|
|
|
|
|
|
match self.builtin:
|
|
|
|
|
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":
|
|
|
|
|
return vtable, ValStr(input())
|
|
|
|
|
case "DICE":
|
|
|
|
|
@@ -716,17 +841,25 @@ class BuiltIn(Node):
|
|
|
|
|
return vtable, ValNul()
|
|
|
|
|
case "FORTIS_NVMERVS":
|
|
|
|
|
if "FORS" not in vtable["#modules"]:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"Cannot use 'FORTIS_NVMERVS' without module 'FORS'"
|
|
|
|
|
)
|
|
|
|
|
return vtable, ValInt(random.randint(params[0].value(), params[1].value()))
|
|
|
|
|
raise CentvrionError("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):
|
|
|
|
|
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":
|
|
|
|
|
if "FORS" not in vtable["#modules"]:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"Cannot use 'FORTIS_ELECTIONIS' without module 'FORS'"
|
|
|
|
|
)
|
|
|
|
|
return vtable, params[0].value()[random.randint(0, len(params[0].value()) - 1)]
|
|
|
|
|
raise CentvrionError("Cannot use 'FORTIS_ELECTIONIS' without module 'FORS'")
|
|
|
|
|
if not isinstance(params[0], ValList):
|
|
|
|
|
raise CentvrionError("FORTIS_ELECTIONIS requires an array")
|
|
|
|
|
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":
|
|
|
|
|
if not isinstance(params[0], ValList):
|
|
|
|
|
raise CentvrionError("LONGITVDO requires an array")
|
|
|
|
|
return vtable, ValInt(len(params[0].value()))
|
|
|
|
|
case _:
|
|
|
|
|
raise NotImplementedError(self.builtin)
|
|
|
|
|
|