🐐 Fractions
This commit is contained in:
15
cent
15
cent
@@ -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:
|
||||||
|
|||||||
@@ -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
1
centvrion/errors.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
class CentvrionError(Exception): pass
|
||||||
@@ -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})+")]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
185
tests.py
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user