import re import random import time from fractions import Fraction from rply.token import BaseBox from centvrion.errors import CentvrionError from centvrion.values import Val, ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac NUMERALS = { "I": 1, "IV": 4, "V": 5, "IX": 9, "X": 10, "XL": 40, "L": 50, "XC": 90, "C": 100, "CD": 400, "D": 500, "CM": 900, "M": 1000 } def rep_join(l): format_string = ',\n'.join( [repr(i) if not isinstance(i, str) else i for i in l] ).replace('\n', '\n ') if format_string != "": format_string = f"\n {format_string}\n" return format_string OP_STR = { "SYMBOL_PLUS": "+", "SYMBOL_MINUS": "-", "SYMBOL_TIMES": "*", "SYMBOL_DIVIDE": "/", "SYMBOL_AMPERSAND": "&", "KEYWORD_RELIQVVM": "RELIQVVM", "KEYWORD_EST": "EST", "KEYWORD_DISPAR": "DISPAR", "KEYWORD_MINVS": "MINVS", "KEYWORD_PLVS": "PLVS", "KEYWORD_ET": "ET", "KEYWORD_AVT": "AVT", } def single_num_to_int(i, m): if i[-1] == "_": if not m: raise CentvrionError( "Cannot calculate numbers above 3999 without 'MAGNVM' module" ) if i[0] != "I": raise CentvrionError( "Cannot use 'I' with thousands operator, use 'M' instead" ) return 1000 * single_num_to_int(i[:-1], m) else: return NUMERALS[i] def num_to_int(n, m, s=False): if n.startswith("-"): if not s: 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 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 CentvrionError(f"{n!r} is not a valid roman numeral") while True: for x, num in enumerate(nums[:-1]): if num < nums[x+1]: if (not nums[x+1] % 5) and (num in [nums[x+1]/5, nums[x+1]/10]): new_nums[x] = nums[x+1] - num new_nums[x+1] = 0 break else: 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] new_nums = nums.copy() else: break if nums != sorted(nums)[::-1]: raise CentvrionError(f"{n!r} is not a valid roman numeral") return sum(nums) def int_to_num(n, m, s=False) -> str: if n < 0: if not s: raise CentvrionError("Cannot display negative numbers without 'SVBNVLLA' module") return "-" + int_to_num(-n, m, s) if n > 3999: if not m: raise CentvrionError( "Cannot display numbers above 3999 without 'MAGNVM' module" ) thousands_chars = re.findall(r"[IVXLCDM]_*", int_to_num(n//1000, m, s)) thousands = ''.join([ "M" if i == "I" else i+"_" for i in thousands_chars ]) return thousands + int_to_num(n % 1000, m, s) else: nums = [] while n > 0: for num, i in list(NUMERALS.items())[::-1]: if n >= i: nums.append(num) n -= i break return ''.join(nums) def make_string(val, magnvm=False, svbnvlla=False) -> str: if isinstance(val, ValStr): 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 "VERITAS" if val.value() else "FALSITAS" elif isinstance(val, ValNul): return "NVLLVS" elif isinstance(val, ValList): inner = ' '.join(make_string(i, magnvm, svbnvlla) for i in val.value()) return f"[{inner}]" elif isinstance(val, ValDict): def _key_val(k): return ValStr(k) if isinstance(k, str) else ValInt(k) inner = ', '.join( f"{make_string(_key_val(k), magnvm, svbnvlla)} VT {make_string(v, magnvm, svbnvlla)}" for k, v in val.value().items() ) return "{" + inner + "}" elif isinstance(val, ValFunc): return "FVNCTIO" else: 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) -> str: if f < 0: if not svbnvlla: raise CentvrionError("Cannot display negative numbers 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): def eval(self, vtable): return self._eval(vtable.copy()) def _eval(self, vtable): raise NotImplementedError def print(self): return "Node()" class ExpressionStatement(Node): def __init__(self, expression: Node) -> None: self.expression = expression def __eq__(self, other): return type(self) == type(other) and self.expression == other.expression def __repr__(self) -> str: expr = repr(self.expression).replace('\n', '\n ') return f"ExpressionStatement(\n {expr}\n)" def print(self): return self.expression.print() def _eval(self, vtable): return self.expression.eval(vtable) class DataArray(Node): def __init__(self, content) -> None: self.content = content def __eq__(self, other): return type(self) == type(other) and self.content == other.content def __repr__(self) -> str: content_string = rep_join(self.content) return f"Array([{content_string}])" def print(self): items = ", ".join(i.print() for i in self.content) return f"[{items}]" def _eval(self, vtable): vals = [] for item in self.content: vtable, val = item.eval(vtable) vals.append(val) return vtable, ValList(vals) class DataRangeArray(Node): def __init__(self, from_value, to_value) -> None: self.from_value = from_value self.to_value = to_value def __eq__(self, other): return type(self) == type(other) and self.from_value == other.from_value and self.to_value == other.to_value def __repr__(self) -> str: content_string = rep_join([self.from_value, self.to_value]) return f"RangeArray([{content_string}])" def print(self): return f"[{self.from_value.print()} VSQVE {self.to_value.print()}]" 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, ValNul)) or not isinstance(to_val, (ValInt, ValNul)): raise CentvrionError("Range bounds must be numbers") from_int = from_val.value() or 0 to_int = to_val.value() or 0 return vtable, ValList([ValInt(i) for i in range(from_int, to_int + 1)]) class DataDict(Node): def __init__(self, pairs) -> None: self.pairs = pairs def __eq__(self, other): return type(self) == type(other) and self.pairs == other.pairs def __repr__(self) -> str: pair_strs = ', '.join(f"({k!r}, {v!r})" for k, v in self.pairs) return f"Dict([{pair_strs}])" def print(self): items = ", ".join(f"{k.print()} VT {v.print()}" for k, v in self.pairs) return "TABVLA {" + items + "}" def _eval(self, vtable): d = {} for key_node, val_node in self.pairs: vtable, key = key_node.eval(vtable) vtable, val = val_node.eval(vtable) if not isinstance(key, (ValStr, ValInt)): raise CentvrionError("Dict keys must be strings or integers") d[key.value()] = val return vtable, ValDict(d) class String(Node): def __init__(self, value) -> None: self.value = value self.quote = '"' def __eq__(self, other): return type(self) == type(other) and self.value == other.value def __repr__(self): return f"String({self.value})" def print(self): if self.quote == "'": return f"'{self.value}'" escaped = self.value.replace('{', '{{').replace('}', '}}') return f'"{escaped}"' def _eval(self, vtable): return vtable, ValStr(self.value) def _flip_quotes(node, quote): """Recursively set quote style on all String nodes in an expression tree.""" if isinstance(node, String): node.quote = quote for attr in vars(node).values(): if isinstance(attr, Node): _flip_quotes(attr, quote) elif isinstance(attr, list): for item in attr: if isinstance(item, Node): _flip_quotes(item, quote) class InterpolatedString(Node): def __init__(self, parts) -> None: self.parts = parts def __eq__(self, other): return type(self) == type(other) and self.parts == other.parts def __repr__(self): return f"InterpolatedString([{rep_join(self.parts)}])" def print(self): result = '"' for part in self.parts: if isinstance(part, String): result += part.value.replace('{', '{{').replace('}', '}}') else: _flip_quotes(part, "'") result += '{' + part.print() + '}' _flip_quotes(part, '"') result += '"' return result def _eval(self, vtable): magnvm = "MAGNVM" in vtable["#modules"] svbnvlla = "SVBNVLLA" in vtable["#modules"] pieces = [] for part in self.parts: vtable, val = part.eval(vtable) pieces.append(make_string(val, magnvm, svbnvlla)) return vtable, ValStr(''.join(pieces)) class Numeral(Node): def __init__(self, value: str) -> None: self.value = value def __eq__(self, other): return type(self) == type(other) and self.value == other.value def __repr__(self): return f"Numeral({self.value})" def print(self): return self.value def _eval(self, vtable): return vtable, ValInt(num_to_int(self.value, "MAGNVM" in vtable["#modules"], "SVBNVLLA" in vtable["#modules"])) class Fractio(Node): def __init__(self, value: str) -> 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 def __eq__(self, other): return type(self) == type(other) and self.value == other.value def __repr__(self): return f"Bool({self.value})" def print(self): return "VERITAS" if self.value else "FALSITAS" def _eval(self, vtable): return vtable, ValBool(self.value) class ModuleCall(BaseBox): def __init__(self, module_name) -> None: self.module_name = module_name def __eq__(self, other): return type(self) == type(other) and self.module_name == other.module_name def __repr__(self) -> str: return f"{self.module_name}" def print(self): return f"CVM {self.module_name}" class ID(Node): def __init__(self, name: str) -> None: self.name = name def __eq__(self, other): return type(self) == type(other) and self.name == other.name def __repr__(self) -> str: return f"ID({self.name})" def print(self): 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] class Designa(Node): def __init__(self, variable: ID, value: Node) -> None: self.id = variable self.value = value def __eq__(self, other): return type(self) == type(other) and self.id == other.id and self.value == other.value def __repr__(self) -> str: id_string = repr(self.id).replace('\n', '\n ') value_string = repr(self.value).replace('\n', '\n ') return f"Designa(\n {id_string},\n {value_string}\n)" def print(self): return f"DESIGNA {self.id.print()} VT {self.value.print()}" def _eval(self, vtable): vtable, val = self.value.eval(vtable) vtable[self.id.name] = val return vtable, ValNul() class DesignaIndex(Node): def __init__(self, variable: ID, index, value) -> None: self.id = variable self.index = index self.value = value def __eq__(self, other): return type(self) == type(other) and self.id == other.id and self.index == other.index and self.value == other.value def __repr__(self) -> str: return f"DesignaIndex({self.id!r}, {self.index!r}, {self.value!r})" def print(self): return f"DESIGNA {self.id.print()}[{self.index.print()}] VT {self.value.print()}" 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 isinstance(target, ValDict): if not isinstance(index, (ValStr, ValInt)): raise CentvrionError("Dict key must be a string or integer") d = dict(target.value()) d[index.value()] = val vtable[self.id.name] = ValDict(d) return vtable, ValNul() if not isinstance(target, ValList): raise CentvrionError(f"{self.id.name} is not an array or dict") i = index.value() lst = list(target.value()) if i < 1 or i > 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() class DesignaDestructure(Node): def __init__(self, variables: list, value: Node) -> None: self.ids = variables self.value = value def __eq__(self, other): return type(self) == type(other) and self.ids == other.ids and self.value == other.value def __repr__(self) -> str: ids_string = ", ".join(repr(i) for i in self.ids) value_string = repr(self.value).replace('\n', '\n ') return f"DesignaDestructure(\n [{ids_string}],\n {value_string}\n)" def print(self): ids_str = ", ".join(i.print() for i in self.ids) return f"DESIGNA {ids_str} VT {self.value.print()}" def _eval(self, vtable): vtable, val = self.value.eval(vtable) if not isinstance(val, ValList): raise CentvrionError("Cannot destructure non-array value") if len(val.value()) != len(self.ids): raise CentvrionError( f"Destructuring mismatch: {len(self.ids)} targets, {len(val.value())} values") for id_node, item in zip(self.ids, val.value()): vtable[id_node.name] = item return vtable, ValNul() class Defini(Node): def __init__(self, name: ID, parameters: list[ID], statements: list[Node]) -> None: self.name = name self.parameters = parameters self.statements = statements def __eq__(self, other): return type(self) == type(other) and self.name == other.name and self.parameters == other.parameters and self.statements == other.statements def __repr__(self) -> str: parameter_string = f"parameters([{rep_join(self.parameters)}])" statements_string = f"statements([{rep_join(self.statements)}])" def_string = rep_join( [f"{repr(self.name)}", parameter_string, statements_string] ) return f"Defini({def_string})" def print(self): params = ", ".join(p.print() for p in self.parameters) body = "\n".join(s.print() for s in self.statements) return f"DEFINI {self.name.print()} ({params}) VT {{\n{body}\n}}" def _eval(self, vtable): vtable[self.name.name] = ValFunc(self.parameters, self.statements) return vtable, ValNul() class Fvnctio(Node): def __init__(self, parameters: list[ID], statements: list[Node]) -> None: self.parameters = parameters self.statements = statements def __eq__(self, other): return (type(self) == type(other) and self.parameters == other.parameters and self.statements == other.statements) def __repr__(self) -> str: parameter_string = f"parameters([{rep_join(self.parameters)}])" statements_string = f"statements([{rep_join(self.statements)}])" fvn_string = rep_join([parameter_string, statements_string]) return f"Fvnctio({fvn_string})" def print(self): params = ", ".join(p.print() for p in self.parameters) body = "\n".join(s.print() for s in self.statements) return f"FVNCTIO ({params}) VT {{\n{body}\n}}" def _eval(self, vtable): return vtable, ValFunc(self.parameters, self.statements) class Redi(Node): def __init__(self, values: list[Node]) -> None: self.values = values def __eq__(self, other): return type(self) == type(other) and self.values == other.values def __repr__(self) -> str: values_string = f"[{rep_join(self.values)}]" return f"Redi({values_string})" def print(self): exprs = ", ".join(v.print() for v in self.values) return f"REDI ({exprs})" def _eval(self, vtable): vals = [] for v in self.values: vtable, val = v.eval(vtable) vals.append(val) if len(vals) == 1: vtable["#return"] = vals[0] else: vtable["#return"] = ValList(vals) return vtable, ValNul() class Erumpe(Node): def __eq__(self, other): return type(self) == type(other) def __repr__(self) -> str: return "Erumpe()" def print(self): return "ERVMPE" def _eval(self, vtable): vtable["#break"] = True return vtable, ValNul() class Continva(Node): def __eq__(self, other): return type(self) == type(other) def __repr__(self) -> str: return "Continva()" def print(self): return "CONTINVA" def _eval(self, vtable): vtable["#continue"] = True return vtable, ValNul() class Nullus(Node): def __eq__(self, other): return type(self) == type(other) def __repr__(self) -> str: return "Nullus()" def print(self): return "NVLLVS" def _eval(self, vtable): return vtable, ValNul() class BinOp(Node): def __init__(self, left: Node, right: Node, op: str) -> None: self.left = left self.right = right self.op = op def __eq__(self, other): return type(self) == type(other) and self.left == other.left and self.right == other.right and self.op == other.op def __repr__(self) -> str: binop_string = rep_join([self.left, self.right, self.op]) return f"BinOp({binop_string})" def print(self): return f"({self.left.print()} {OP_STR[self.op]} {self.right.print()})" def _eval(self, vtable): vtable, left = self.left.eval(vtable) vtable, right = self.right.eval(vtable) lv, rv = left.value(), right.value() match self.op: case "SYMBOL_PLUS": if isinstance(lv, str) or isinstance(rv, str): 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() 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": 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": 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": 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_RELIQVVM": if isinstance(lv, (str, list)) or isinstance(rv, (str, list)): raise CentvrionError("Cannot use RELIQVVM on strings or arrays") if (rv or 0) == 0: raise CentvrionError("Modulo 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) case "KEYWORD_DISPAR": return vtable, ValBool(lv != rv) case "KEYWORD_ET": return vtable, ValBool(bool(left) and bool(right)) case "KEYWORD_AVT": return vtable, ValBool(bool(left) or bool(right)) case _: raise Exception(self.op) class UnaryMinus(Node): def __init__(self, expr: Node): self.expr = expr def __eq__(self, other): return type(self) == type(other) and self.expr == other.expr def __repr__(self): return f"UnaryMinus({self.expr!r})" def print(self): return f"(- {self.expr.print()})" def _eval(self, vtable): if "SVBNVLLA" not in vtable["#modules"]: raise CentvrionError("Cannot use unary minus without 'SVBNVLLA' module") vtable, val = self.expr.eval(vtable) if isinstance(val, ValInt): return vtable, ValInt(-val.value()) elif isinstance(val, ValFrac): return vtable, ValFrac(-val.value()) else: raise CentvrionError("Unary minus requires a number") class UnaryNot(Node): def __init__(self, expr): self.expr = expr def __eq__(self, other): return type(self) == type(other) and self.expr == other.expr def __repr__(self): return f"UnaryNot({self.expr!r})" def print(self): return f"(NON {self.expr.print()})" def _eval(self, vtable): vtable, val = self.expr.eval(vtable) if not isinstance(val, ValBool): raise CentvrionError("NON requires a boolean") return vtable, ValBool(not val.value()) class ArrayIndex(Node): def __init__(self, array, index) -> None: self.array = array self.index = index def __eq__(self, other): return type(self) == type(other) and self.array == other.array and self.index == other.index def __repr__(self) -> str: return f"ArrayIndex({self.array!r}, {self.index!r})" def print(self): return f"{self.array.print()}[{self.index.print()}]" def _eval(self, vtable): vtable, array = self.array.eval(vtable) vtable, index = self.index.eval(vtable) if isinstance(array, ValDict): if not isinstance(index, (ValStr, ValInt)): raise CentvrionError("Dict key must be a string or integer") k = index.value() d = array.value() if k not in d: raise CentvrionError(f"Key not found in dict") return vtable, d[k] if isinstance(array, ValStr): if isinstance(index, ValInt): i = index.value() elif isinstance(index, ValFrac) and index.value().denominator == 1: i = index.value().numerator else: raise CentvrionError("String index must be a number") s = array.value() if i < 1 or i > len(s): raise CentvrionError(f"Index {i} out of range for string of length {len(s)}") return vtable, ValStr(s[i - 1]) if not isinstance(array, ValList): raise CentvrionError("Cannot index a non-array value") if isinstance(index, ValInt): i = index.value() elif isinstance(index, ValFrac) and index.value().denominator == 1: i = index.value().numerator else: raise CentvrionError("Array index must be a number") lst = array.value() if i < 1 or i > len(lst): raise CentvrionError(f"Index {i} out of range for array of length {len(lst)}") return vtable, lst[i - 1] def _to_index_int(val): if isinstance(val, ValInt): return val.value() if isinstance(val, ValFrac) and val.value().denominator == 1: return val.value().numerator raise CentvrionError("Array index must be a number") class ArraySlice(Node): def __init__(self, array, from_index, to_index) -> None: self.array = array self.from_index = from_index self.to_index = to_index def __eq__(self, other): return (type(self) == type(other) and self.array == other.array and self.from_index == other.from_index and self.to_index == other.to_index) def __repr__(self) -> str: return f"ArraySlice({self.array!r}, {self.from_index!r}, {self.to_index!r})" def print(self): return f"{self.array.print()}[{self.from_index.print()} VSQVE {self.to_index.print()}]" def _eval(self, vtable): vtable, array = self.array.eval(vtable) vtable, from_val = self.from_index.eval(vtable) vtable, to_val = self.to_index.eval(vtable) if isinstance(array, ValStr): from_int = _to_index_int(from_val) to_int = _to_index_int(to_val) s = array.value() if from_int < 1 or to_int > len(s) or from_int > to_int: raise CentvrionError( f"Slice [{from_int} VSQVE {to_int}] out of range" f" for string of length {len(s)}" ) return vtable, ValStr(s[from_int - 1 : to_int]) if not isinstance(array, ValList): raise CentvrionError("Cannot slice a non-array value") from_int = _to_index_int(from_val) to_int = _to_index_int(to_val) lst = array.value() if from_int < 1 or to_int > len(lst) or from_int > to_int: raise CentvrionError( f"Slice [{from_int} VSQVE {to_int}] out of range" f" for array of length {len(lst)}" ) return vtable, ValList(lst[from_int - 1 : to_int]) class SiStatement(Node): def __init__(self, test, statements, else_part) -> None: self.test = test self.statements = statements self.else_part = else_part def __eq__(self, other): return type(self) == type(other) and self.test == other.test and self.statements == other.statements and self.else_part == other.else_part def __repr__(self) -> str: test = repr(self.test) statements = f"statements([{rep_join(self.statements)}])" if self.else_part is None: else_part = "None" else: else_part = f"statements([{rep_join(self.else_part)}])" si_string = rep_join([test, statements, else_part]) return f"Si({si_string})" def print(self): body = "\n".join(s.print() for s in self.statements) result = f"SI {self.test.print()} TVNC {{\n{body}\n}}" if self.else_part: else_body = "\n".join(s.print() for s in self.else_part) result += f" ALIVD {{\n{else_body}\n}}" return result def _eval(self, vtable): vtable, cond = self.test.eval(vtable) if not isinstance(cond, ValBool): raise CentvrionError("SI condition must be a boolean") last_val = ValNul() if cond: for statement in self.statements: vtable, last_val = statement.eval(vtable) if vtable["#return"] is not None: break elif self.else_part: for statement in self.else_part: vtable, last_val = statement.eval(vtable) if vtable["#return"] is not None: break return vtable, last_val class DumStatement(Node): def __init__(self, test: Node, statements: list[Node]) -> None: self.test = test self.statements = statements def __eq__(self, other): return type(self) == type(other) and self.test == other.test and self.statements == other.statements def __repr__(self) -> str: test = repr(self.test) statements = f"statements([{rep_join(self.statements)}])" dum_string = rep_join([test, statements]) return f"Dum({dum_string})" def print(self): body = "\n".join(s.print() for s in self.statements) return f"DVM {self.test.print()} FAC {{\n{body}\n}}" def _eval(self, vtable): last_val = ValNul() vtable, cond = self.test.eval(vtable) if not isinstance(cond, ValBool): raise CentvrionError("DVM condition must be a boolean") while not cond: for statement in self.statements: vtable, val = statement.eval(vtable) if vtable["#break"] or vtable["#continue"] or vtable["#return"] is not None: break last_val = val if vtable["#break"]: vtable["#break"] = False break if vtable["#continue"]: vtable["#continue"] = False vtable, cond = self.test.eval(vtable) continue if vtable["#return"] is not None: break vtable, cond = self.test.eval(vtable) return vtable, last_val class PerStatement(Node): def __init__(self, data_list, variable_name, statements) -> None: self.data_list = data_list self.variable_name = variable_name self.statements = statements def __eq__(self, other): return type(self) == type(other) and self.data_list == other.data_list and self.variable_name == other.variable_name and self.statements == other.statements def __repr__(self) -> str: test = repr(self.data_list) variable_name = repr(self.variable_name) statements = f"statements([{rep_join(self.statements)}])" dum_string = rep_join([test, variable_name, statements]) return f"Per({dum_string})" def print(self): body = "\n".join(s.print() for s in self.statements) return f"PER {self.variable_name.print()} IN {self.data_list.print()} FAC {{\n{body}\n}}" def _eval(self, vtable): vtable, array = self.data_list.eval(vtable) if isinstance(array, ValDict): keys = [ValStr(k) if isinstance(k, str) else ValInt(k) for k in array.value().keys()] array = ValList(keys) if not isinstance(array, ValList): raise CentvrionError("PER requires an array or dict") variable_name = self.variable_name.name last_val = ValNul() for item in array: vtable[variable_name] = item for statement in self.statements: vtable, val = statement.eval(vtable) if vtable["#break"] or vtable["#continue"] or vtable["#return"] is not None: break last_val = val if vtable["#break"]: vtable["#break"] = False break if vtable["#continue"]: vtable["#continue"] = False continue if vtable["#return"] is not None: break return vtable, last_val class TemptaStatement(Node): def __init__(self, try_statements, error_var, catch_statements) -> None: self.try_statements = try_statements self.error_var = error_var self.catch_statements = catch_statements def __eq__(self, other): return (type(self) == type(other) and self.try_statements == other.try_statements and self.error_var == other.error_var and self.catch_statements == other.catch_statements) def __repr__(self) -> str: try_stmts = f"try([{rep_join(self.try_statements)}])" catch_stmts = f"catch([{rep_join(self.catch_statements)}])" tempta_string = rep_join([try_stmts, repr(self.error_var), catch_stmts]) return f"Tempta({tempta_string})" def print(self): try_body = "\n".join(s.print() for s in self.try_statements) catch_body = "\n".join(s.print() for s in self.catch_statements) return f"TEMPTA {{\n{try_body}\n}} CAPE {self.error_var.print()} {{\n{catch_body}\n}}" def _eval(self, vtable): last_val = ValNul() try: for statement in self.try_statements: vtable, last_val = statement.eval(vtable) if vtable["#return"] is not None or vtable["#break"] or vtable["#continue"]: return vtable, last_val except CentvrionError as e: vtable[self.error_var.name] = ValStr(str(e)) for statement in self.catch_statements: vtable, last_val = statement.eval(vtable) if vtable["#return"] is not None or vtable["#break"] or vtable["#continue"]: return vtable, last_val return vtable, last_val class Invoca(Node): def __init__(self, callee, parameters) -> None: self.callee = callee self.parameters = parameters def __eq__(self, other): return (type(self) == type(other) and self.callee == other.callee and self.parameters == other.parameters) def __repr__(self) -> str: parameters_string = f"parameters([{rep_join(self.parameters)}])" invoca_string = rep_join([self.callee, parameters_string]) return f"Invoca({invoca_string})" def print(self): args = ", ".join(p.print() for p in self.parameters) return f"INVOCA {self.callee.print()} ({args})" def _eval(self, vtable): params = [p.eval(vtable)[1] for p in self.parameters] vtable, func = self.callee.eval(vtable) if not isinstance(func, ValFunc): callee_desc = (self.callee.name if isinstance(self.callee, ID) else "expression") raise CentvrionError(f"{callee_desc} is not a function") if len(params) != len(func.params): callee_desc = (self.callee.name if isinstance(self.callee, ID) else "FVNCTIO") raise CentvrionError( f"{callee_desc} expects {len(func.params)} argument(s), got {len(params)}" ) func_vtable = vtable.copy() for i, param in enumerate(func.params): func_vtable[param.name] = params[i] func_vtable["#return"] = None for statement in func.body: func_vtable, _ = statement.eval(func_vtable) if func_vtable["#return"] is not None: return vtable, func_vtable["#return"] return vtable, ValNul() class BuiltIn(Node): def __init__(self, builtin: str, parameters: list[Node]) -> None: self.builtin = builtin self.parameters = parameters def __eq__(self, other): return type(self) == type(other) and self.builtin == other.builtin and self.parameters == other.parameters def __repr__(self) -> str: parameter_string = f"parameters([{rep_join(self.parameters)}])" builtin_string = rep_join([self.builtin, parameter_string]) return f"Builtin({builtin_string})" def print(self): args = ", ".join(p.print() for p in self.parameters) return f"{self.builtin}({args})" def _eval(self, vtable): params = [p.eval(vtable)[1] for p in self.parameters] magnvm = "MAGNVM" in vtable["#modules"] svbnvlla = "SVBNVLLA" in vtable["#modules"] match self.builtin: case "AVDI_NVMERVS": 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 "DIC": print_string = ' '.join( make_string(i, magnvm, svbnvlla) for i in params ) print(print_string) return vtable, ValStr(print_string) case "ERVMPE": vtable["#break"] = True return vtable, ValNul() case "FORTVITVS_NVMERVS": if "FORS" not in vtable["#modules"]: raise CentvrionError("Cannot use 'FORTVITVS_NVMERVS' without module 'FORS'") a, b = params[0].value(), params[1].value() if not isinstance(a, int) or not isinstance(b, int): raise CentvrionError("FORTVITVS_NVMERVS requires two numbers") if a > b: raise CentvrionError(f"FORTVITVS_NVMERVS: first argument ({a}) must be ≤ second ({b})") return vtable, ValInt(random.randint(a, b)) case "FORTVITA_ELECTIO": if "FORS" not in vtable["#modules"]: raise CentvrionError("Cannot use 'FORTVITA_ELECTIO' without module 'FORS'") if not isinstance(params[0], ValList): raise CentvrionError("FORTVITA_ELECTIO requires an array") lst = params[0].value() if len(lst) == 0: raise CentvrionError("FORTVITA_ELECTIO: cannot select from an empty array") return vtable, lst[random.randint(0, len(lst) - 1)] case "SEMEN": if "FORS" not in vtable["#modules"]: raise CentvrionError("Cannot use 'SEMEN' without module 'FORS'") seed = params[0].value() if not isinstance(seed, int): raise CentvrionError("SEMEN requires an integer seed") random.seed(seed) return vtable, ValNul() case "DECIMATIO": if "FORS" not in vtable["#modules"]: raise CentvrionError("Cannot use 'DECIMATIO' without module 'FORS'") if not isinstance(params[0], ValList): raise CentvrionError("DECIMATIO requires an array") arr = list(params[0].value()) to_remove = len(arr) // 10 for _ in range(to_remove): arr.pop(random.randint(0, len(arr) - 1)) return vtable, ValList(arr) case "SENATVS": if len(params) == 1 and isinstance(params[0], ValList): items = params[0].value() else: items = params for p in items: if not isinstance(p, ValBool): raise CentvrionError("SENATVS requires boolean arguments") true_count = sum(1 for p in items if p.value()) return vtable, ValBool(true_count > len(items) / 2) case "LONGITVDO": if isinstance(params[0], (ValList, ValStr, ValDict)): return vtable, ValInt(len(params[0].value())) raise CentvrionError("LONGITVDO requires an array, string, or dict") case "CLAVES": if not isinstance(params[0], ValDict): raise CentvrionError("CLAVES requires a dict") keys = [ValStr(k) if isinstance(k, str) else ValInt(k) for k in params[0].value().keys()] return vtable, ValList(keys) case "EVERRE": print("\033[2J\033[H", end="", flush=True) return vtable, ValNul() case "ORDINA": if not isinstance(params[0], ValList): raise CentvrionError("ORDINA requires an array") items = list(params[0].value()) if not items: return vtable, ValList([]) all_numeric = all(isinstance(i, (ValInt, ValFrac)) for i in items) all_string = all(isinstance(i, ValStr) for i in items) if not (all_numeric or all_string): raise CentvrionError("ORDINA requires all elements to be numbers or all strings") return vtable, ValList(sorted(items, key=lambda v: v.value())) case "TYPVS": type_map = { ValInt: "NVMERVS", ValStr: "LITTERA", ValBool: "VERAX", ValList: "CATALOGVS", ValFrac: "FRACTIO", ValDict: "TABVLA", ValFunc: "FVNCTIO", ValNul: "NVLLVS", } return vtable, ValStr(type_map[type(params[0])]) case "DORMI": v = params[0] if isinstance(v, ValNul): seconds = 0 elif isinstance(v, ValInt): seconds = v.value() elif isinstance(v, ValFrac): seconds = float(v.value()) else: raise CentvrionError("DORMI requires a number or NVLLVS") time.sleep(seconds) return vtable, ValNul() case "LEGE": if "SCRIPTA" not in vtable["#modules"]: raise CentvrionError("Cannot use 'LEGE' without module 'SCRIPTA'") path = make_string(params[0], magnvm, svbnvlla) with open(path, "r") as f: return vtable, ValStr(f.read()) case "SCRIBE": if "SCRIPTA" not in vtable["#modules"]: raise CentvrionError("Cannot use 'SCRIBE' without module 'SCRIPTA'") path = make_string(params[0], magnvm, svbnvlla) content = make_string(params[1], magnvm, svbnvlla) with open(path, "w") as f: f.write(content) return vtable, ValNul() case "ADIVNGE": if "SCRIPTA" not in vtable["#modules"]: raise CentvrionError("Cannot use 'ADIVNGE' without module 'SCRIPTA'") path = make_string(params[0], magnvm, svbnvlla) content = make_string(params[1], magnvm, svbnvlla) with open(path, "a") as f: f.write(content) return vtable, ValNul() case _: raise NotImplementedError(self.builtin) class Program(BaseBox): def __init__(self, module_calls: list[ModuleCall], statements: list[Node]) -> None: self.modules = module_calls self.statements = statements def __eq__(self, other): return type(self) == type(other) and self.modules == other.modules and self.statements == other.statements def __repr__(self) -> str: modules_string = f"modules([{rep_join(self.modules)}])" statements_string = f"statements([{rep_join(self.statements)}])" return f"{modules_string},\n{statements_string}" def print(self): stmts = "\n".join(s.print() for s in self.statements) if self.modules: mods = "\n".join(m.print() for m in self.modules) return f"{mods}\n\n{stmts}" return stmts def eval(self, *_): vtable = { "#break": False, "#continue": False, "#return": None, "#modules": [m.module_name for m in self.modules], } last_val = ValNul() for statement in self.statements: vtable, last_val = statement.eval(vtable) if vtable["#return"] is not None: return vtable["#return"] return last_val