From c570d72b02ab2df24f2eaddd89a606993f243b05 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Thu, 16 Apr 2026 20:34:06 +0200 Subject: [PATCH] :goat: Modulo --- README.md | 4 +- centvrion/ast_nodes.py | 14 ++++- centvrion/compiler/emit_expr.py | 4 ++ centvrion/compiler/runtime/cent_runtime.c | 29 ++++++++++ centvrion/compiler/runtime/cent_runtime.h | 3 ++ centvrion/lexer.py | 2 + centvrion/parser.py | 6 ++- language/main.tex | 2 +- snippets/syntaxes/centvrion.sublime-syntax | 2 +- tests.py | 63 +++++++++++++++++++++- 10 files changed, 122 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5c62ede..fd812f9 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Strings are concatenated with `&`: `NVLLVS` coerces to an empty string when used with `&`. Note: `+` is for arithmetic only — using it on strings raises an error. +Integer modulo is `RELIQVVM`: `VII RELIQVVM III` evaluates to `I`. Under the `FRACTIO` module it returns a fraction, so `IIIS RELIQVVM IS` is `S` (i.e. 1/2). + ### Integers Integers must be written in roman numerals using the following symbols: @@ -92,7 +94,7 @@ If-then statements are denoted with the keywords `SI` (if) and `TVNC` (then). Th Will return `I` (1), as the conditional evaluates `x` to be true. ### Boolean expressions -In conditionals, `EST` functions as an equality evaluation, and `MINVS` (<) and `PLVS` (>) function as inequality evaluation. +In conditionals, `EST` functions as an equality evaluation, `DISPAR` as not-equal, and `MINVS` (<) and `PLVS` (>) function as inequality evaluation. ### ALVID diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index baae3f5..f9ffb6d 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -37,7 +37,9 @@ OP_STR = { "SYMBOL_PLUS": "+", "SYMBOL_MINUS": "-", "SYMBOL_TIMES": "*", "SYMBOL_DIVIDE": "/", "SYMBOL_AMPERSAND": "&", - "KEYWORD_EST": "EST", "KEYWORD_MINVS": "MINVS", + "KEYWORD_RELIQVVM": "RELIQVVM", + "KEYWORD_EST": "EST", "KEYWORD_DISPAR": "DISPAR", + "KEYWORD_MINVS": "MINVS", "KEYWORD_PLVS": "PLVS", "KEYWORD_ET": "ET", "KEYWORD_AVT": "AVT", } @@ -586,6 +588,14 @@ class BinOp(Node): 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") @@ -596,6 +606,8 @@ class BinOp(Node): 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": diff --git a/centvrion/compiler/emit_expr.py b/centvrion/compiler/emit_expr.py index 65e487c..fe2b433 100644 --- a/centvrion/compiler/emit_expr.py +++ b/centvrion/compiler/emit_expr.py @@ -13,7 +13,9 @@ _BINOP_FN = { "SYMBOL_TIMES": "cent_mul", "SYMBOL_DIVIDE": "cent_div", "SYMBOL_AMPERSAND": "cent_concat", + "KEYWORD_RELIQVVM": "cent_mod", "KEYWORD_EST": "cent_eq", + "KEYWORD_DISPAR": "cent_neq", "KEYWORD_MINVS": "cent_lt", "KEYWORD_PLVS": "cent_gt", "KEYWORD_ET": "cent_and", @@ -77,6 +79,8 @@ def emit_expr(node, ctx): tmp = ctx.fresh_tmp() if node.op == "SYMBOL_DIVIDE" and ctx.has_module("FRACTIO"): fn = "cent_div_frac" + elif node.op == "KEYWORD_RELIQVVM" and ctx.has_module("FRACTIO"): + fn = "cent_mod_frac" else: fn = _BINOP_FN[node.op] return l_lines + r_lines + [f"CentValue {tmp} = {fn}({l_var}, {r_var});"], tmp diff --git a/centvrion/compiler/runtime/cent_runtime.c b/centvrion/compiler/runtime/cent_runtime.c index eca116b..553bd10 100644 --- a/centvrion/compiler/runtime/cent_runtime.c +++ b/centvrion/compiler/runtime/cent_runtime.c @@ -396,6 +396,30 @@ CentValue cent_div_frac(CentValue a, CentValue b) { return frac_reduce(an * bd, ad * bn); } +CentValue cent_mod(CentValue a, CentValue b) { + if (a.type != CENT_INT || b.type != CENT_INT) + cent_type_error("'RELIQVVM' requires two integers"); + if (b.ival == 0) + cent_runtime_error("modulo by zero"); + return cent_int(a.ival % b.ival); +} + +CentValue cent_mod_frac(CentValue a, CentValue b) { + long an, ad, bn, bd; + to_frac(a, &an, &ad); to_frac(b, &bn, &bd); + if (bn == 0) cent_runtime_error("modulo by zero"); + /* a/b mod c/d over a common denominator ad*bd: + num_a = an*bd, num_b = bn*ad + result = (num_a - floor(num_a/num_b) * num_b) / (ad*bd) + Use floored division so the result matches Python's Fraction.__mod__. */ + long num_a = an * bd; + long num_b = bn * ad; + long q = num_a / num_b; + if ((num_a % num_b != 0) && ((num_a < 0) != (num_b < 0))) q -= 1; + long new_num = num_a - q * num_b; + return frac_reduce(new_num, ad * bd); +} + CentValue cent_eq(CentValue a, CentValue b) { if ((a.type == CENT_INT || a.type == CENT_FRAC) && (b.type == CENT_INT || b.type == CENT_FRAC)) { @@ -414,6 +438,11 @@ CentValue cent_eq(CentValue a, CentValue b) { } } +CentValue cent_neq(CentValue a, CentValue b) { + CentValue r = cent_eq(a, b); + return cent_bool(!r.bval); +} + CentValue cent_lt(CentValue a, CentValue b) { if ((a.type == CENT_INT || a.type == CENT_FRAC || a.type == CENT_NULL) && (b.type == CENT_INT || b.type == CENT_FRAC || b.type == CENT_NULL)) { diff --git a/centvrion/compiler/runtime/cent_runtime.h b/centvrion/compiler/runtime/cent_runtime.h index 2ce7ee0..4612309 100644 --- a/centvrion/compiler/runtime/cent_runtime.h +++ b/centvrion/compiler/runtime/cent_runtime.h @@ -158,7 +158,10 @@ CentValue cent_sub(CentValue a, CentValue b); /* INT-INT or FRAC-FRAC/INT */ CentValue cent_mul(CentValue a, CentValue b); /* INT*INT or FRAC*FRAC/INT */ CentValue cent_div(CentValue a, CentValue b); /* INT/INT integer div */ CentValue cent_div_frac(CentValue a, CentValue b); /* FRACTIO: exact div → FRAC */ +CentValue cent_mod(CentValue a, CentValue b); /* INT%INT integer modulo */ +CentValue cent_mod_frac(CentValue a, CentValue b); /* FRACTIO: floored mod → FRAC */ CentValue cent_eq (CentValue a, CentValue b); /* EST → BOOL */ +CentValue cent_neq(CentValue a, CentValue b); /* DISPAR → BOOL */ CentValue cent_lt (CentValue a, CentValue b); /* MINVS → BOOL */ CentValue cent_gt (CentValue a, CentValue b); /* PLVS → BOOL */ CentValue cent_and(CentValue a, CentValue b); /* ET → BOOL */ diff --git a/centvrion/lexer.py b/centvrion/lexer.py index 7a4bfd9..554dd18 100644 --- a/centvrion/lexer.py +++ b/centvrion/lexer.py @@ -7,6 +7,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [ "AVT", "DEFINI", "DESIGNA", + "DISPAR", "DONICVM", "DVM", "CONTINVA", @@ -23,6 +24,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [ "PER", "PLVS", "REDI", + "RELIQVVM", "SI", "TVNC", "VSQVE", diff --git a/centvrion/parser.py b/centvrion/parser.py index 7970288..e95ab0c 100644 --- a/centvrion/parser.py +++ b/centvrion/parser.py @@ -12,9 +12,9 @@ class Parser(): precedence=[ ('left', ["KEYWORD_AVT"]), ('left', ["KEYWORD_ET"]), - ('left', ["KEYWORD_PLVS", "KEYWORD_MINVS", "KEYWORD_EST"]), + ('left', ["KEYWORD_PLVS", "KEYWORD_MINVS", "KEYWORD_EST", "KEYWORD_DISPAR"]), ('left', ["SYMBOL_AMPERSAND", "SYMBOL_PLUS", "SYMBOL_MINUS"]), - ('left', ["SYMBOL_TIMES", "SYMBOL_DIVIDE"]), + ('left', ["SYMBOL_TIMES", "SYMBOL_DIVIDE", "KEYWORD_RELIQVVM"]), ('right', ["UMINUS", "UNOT"]), ('left', ["SYMBOL_LBRACKET", "INDEX"]), ] @@ -191,7 +191,9 @@ class Parser(): @self.pg.production('expression : expression SYMBOL_PLUS expression') @self.pg.production('expression : expression SYMBOL_TIMES expression') @self.pg.production('expression : expression SYMBOL_DIVIDE expression') + @self.pg.production('expression : expression KEYWORD_RELIQVVM expression') @self.pg.production('expression : expression KEYWORD_EST expression') + @self.pg.production('expression : expression KEYWORD_DISPAR expression') @self.pg.production('expression : expression KEYWORD_MINVS expression') @self.pg.production('expression : expression KEYWORD_PLVS expression') @self.pg.production('expression : expression KEYWORD_ET expression') diff --git a/language/main.tex b/language/main.tex index 6312f74..15dd90b 100644 --- a/language/main.tex +++ b/language/main.tex @@ -87,7 +87,7 @@ \item \textbf{string}: \\ Any text encased in " characters. \item \textbf{numeral}: \\ Roman numerals consisting of the uppercase characters I, V, X, L, C, D, and M. Can also include underscore if the module MAGNVM. \item \textbf{bool}: \\ VERITAS or FALSITAS. - \item \textbf{binop}: \\ Binary operators: \texttt{+}, \texttt{-}, \texttt{*}, \texttt{/}, \texttt{EST} (equality), \texttt{MINVS} (<), \texttt{PLVS} (>), \texttt{ET} (and), \texttt{AVT} (or), \texttt{\&} (string concatenation). + \item \textbf{binop}: \\ Binary operators: \texttt{+}, \texttt{-}, \texttt{*}, \texttt{/}, \texttt{RELIQVVM} (modulo), \texttt{EST} (equality), \texttt{DISPAR} (not-equal), \texttt{MINVS} (<), \texttt{PLVS} (>), \texttt{ET} (and), \texttt{AVT} (or), \texttt{\&} (string concatenation). \item \textbf{unop}: \\ Unary operators: \texttt{-} (negation), \texttt{NON} (boolean not). \end{itemize} diff --git a/snippets/syntaxes/centvrion.sublime-syntax b/snippets/syntaxes/centvrion.sublime-syntax index b6af211..f98a1f6 100644 --- a/snippets/syntaxes/centvrion.sublime-syntax +++ b/snippets/syntaxes/centvrion.sublime-syntax @@ -65,7 +65,7 @@ contexts: scope: support.class.module.centvrion keywords: - - match: '\b(ALVID|AVT|DEFINI|DESIGNA|DONICVM|DVM|ERVMPE|EST|ET|FACE|INVOCA|IN|MINVS|NON|PER|PLVS|REDI|SI|TVNC|VSQVE|VT|CVM)\b' + - match: '\b(ALVID|AVT|DEFINI|DESIGNA|DONICVM|DVM|ERVMPE|EST|ET|FACE|INVOCA|IN|MINVS|NON|PER|PLVS|REDI|RELIQVVM|SI|TVNC|VSQVE|VT|CVM)\b' scope: keyword.control.centvrion operators: diff --git a/tests.py b/tests.py index 65baed2..089533d 100644 --- a/tests.py +++ b/tests.py @@ -136,6 +136,9 @@ arithmetic_tests = [ ("III * IV", Program([], [ExpressionStatement(BinOp(Numeral("III"), Numeral("IV"), "SYMBOL_TIMES"))]), ValInt(12)), ("X / II", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("II"), "SYMBOL_DIVIDE"))]), ValInt(5)), ("X / III", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "SYMBOL_DIVIDE"))]), ValInt(3)), # integer division: 10 // 3 = 3 + ("X RELIQVVM III", Program([], [ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "KEYWORD_RELIQVVM"))]), ValInt(1)), # 10 % 3 = 1 + ("IX RELIQVVM III", Program([], [ExpressionStatement(BinOp(Numeral("IX"), Numeral("III"), "KEYWORD_RELIQVVM"))]), ValInt(0)), # exact divisor + ("VII RELIQVVM X", Program([], [ExpressionStatement(BinOp(Numeral("VII"), Numeral("X"), "KEYWORD_RELIQVVM"))]), ValInt(7)), # dividend < divisor ("II + III * IV", Program([], [ExpressionStatement(BinOp(Numeral("II"), BinOp(Numeral("III"), Numeral("IV"), "SYMBOL_TIMES"), "SYMBOL_PLUS"))]), ValInt(14)), # precedence: 2 + (3*4) = 14 ("(II + III) * IV", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), Numeral("IV"), "SYMBOL_TIMES"))]), ValInt(20)), # parens: (2+3)*4 = 20 ("CVM SVBNVLLA\n- III", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(UnaryMinus(Numeral("III")))]), ValInt(-3)), # unary negation @@ -153,7 +156,7 @@ class TestArithmetic(unittest.TestCase): # --- Precedence and associativity --- # # Precedence (lowest → highest): -# AVT < ET < (EST, PLVS, MINVS) < (+ -) < (* /) < UMINUS < INDEX +# AVT < ET < (EST, DISPAR, PLVS, MINVS) < (+ -) < (* / RELIQVVM) < UMINUS < INDEX precedence_tests = [ # * binds tighter than -: 10 - (2*3) = 4, not (10-2)*3 = 24 @@ -176,6 +179,14 @@ precedence_tests = [ ("I EST II ET II EST II", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST"), BinOp(Numeral("II"), Numeral("II"), "KEYWORD_EST"), "KEYWORD_ET"))]), ValBool(False)), + # + binds tighter than DISPAR: (2+3)!=5 = False, not 2+(3!=5) = type error + ("II + III DISPAR V", + Program([], [ExpressionStatement(BinOp(BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), Numeral("V"), "KEYWORD_DISPAR"))]), + ValBool(False)), + # DISPAR binds tighter than ET: (1!=2) AND (2!=2) = True AND False = False + ("I DISPAR II ET II DISPAR II", + Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_DISPAR"), BinOp(Numeral("II"), Numeral("II"), "KEYWORD_DISPAR"), "KEYWORD_ET"))]), + ValBool(False)), # ET binds tighter than AVT: True OR (False AND False) = True ("VERITAS AVT FALSITAS ET FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(True), BinOp(Bool(False), Bool(False), "KEYWORD_ET"), "KEYWORD_AVT"))]), @@ -208,6 +219,19 @@ precedence_tests = [ ("XII / II / III", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("XII"), Numeral("II"), "SYMBOL_DIVIDE"), Numeral("III"), "SYMBOL_DIVIDE"))]), ValInt(2)), + # RELIQVVM same precedence as *, /; left-associative: (17 % 5) % 2 = 0 + ("XVII RELIQVVM V RELIQVVM II", + Program([], [ExpressionStatement( + BinOp(BinOp(Numeral("XVII"), Numeral("V"), "KEYWORD_RELIQVVM"), + Numeral("II"), "KEYWORD_RELIQVVM"))]), + ValInt(0)), + # RELIQVVM binds tighter than +: 2 + (7 % 3) = 3, not (2+7) % 3 = 0 + ("II + VII RELIQVVM III", + Program([], [ExpressionStatement( + BinOp(Numeral("II"), + BinOp(Numeral("VII"), Numeral("III"), "KEYWORD_RELIQVVM"), + "SYMBOL_PLUS"))]), + ValInt(3)), # left-associativity of AVT: (False OR True) OR False = True ("FALSITAS AVT VERITAS AVT FALSITAS", Program([], [ExpressionStatement(BinOp(BinOp(Bool(False), Bool(True), "KEYWORD_AVT"), Bool(False), "KEYWORD_AVT"))]), @@ -719,6 +743,15 @@ comparison_tests = [ # NVLLVS coerces to 0 in comparisons ("V PLVS NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("V"), Nullus(), "KEYWORD_PLVS"))]), ValBool(True)), ("NVLLVS MINVS V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "KEYWORD_MINVS"))]), ValBool(True)), + # DISPAR (not-equal): mirrors EST semantics, negated + ("I DISPAR II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_DISPAR"))]), ValBool(True)), + ("I DISPAR I", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("I"), "KEYWORD_DISPAR"))]), ValBool(False)), + ('"hello" DISPAR "hello"', Program([], [ExpressionStatement(BinOp(String("hello"), String("hello"), "KEYWORD_DISPAR"))]), ValBool(False)), + ('"hello" DISPAR "world"', Program([], [ExpressionStatement(BinOp(String("hello"), String("world"), "KEYWORD_DISPAR"))]), ValBool(True)), + ("VERITAS DISPAR FALSITAS", Program([], [ExpressionStatement(BinOp(Bool(True), Bool(False), "KEYWORD_DISPAR"))]), ValBool(True)), + ("NVLLVS DISPAR NVLLVS", Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "KEYWORD_DISPAR"))]), ValBool(False)), + # cross-type: an int and a string are never equal + ('I DISPAR "I"', Program([], [ExpressionStatement(BinOp(Numeral("I"), String("I"), "KEYWORD_DISPAR"))]), ValBool(True)), ] class TestComparisons(unittest.TestCase): @@ -1431,6 +1464,34 @@ fractio_tests = [ ]), ValFrac(Fraction(5)) ), + # Modulo on fractions: 7/2 RELIQVVM 3/2 = 1/2 (7/2 / 3/2 = 7/3, floor=2, 7/2 - 3 = 1/2) + ("CVM FRACTIO\nIIIS RELIQVVM IS", + Program([ModuleCall("FRACTIO")], [ + ExpressionStatement(BinOp(Fractio("IIIS"), Fractio("IS"), "KEYWORD_RELIQVVM")) + ]), + ValFrac(Fraction(1, 2)) + ), + # Modulo with mixed operand types: 5/2 RELIQVVM 1 = 1/2 + ("CVM FRACTIO\nIIS RELIQVVM I", + Program([ModuleCall("FRACTIO")], [ + ExpressionStatement(BinOp(Fractio("IIS"), Numeral("I"), "KEYWORD_RELIQVVM")) + ]), + ValFrac(Fraction(1, 2)) + ), + # Int operands under FRACTIO still return a fraction: 10 RELIQVVM 3 = 1 (as Fraction) + ("CVM FRACTIO\nX RELIQVVM III", + Program([ModuleCall("FRACTIO")], [ + ExpressionStatement(BinOp(Numeral("X"), Numeral("III"), "KEYWORD_RELIQVVM")) + ]), + ValFrac(Fraction(1)) + ), + # Exact multiple under FRACTIO: 3 RELIQVVM 3/2 = 0 + ("CVM FRACTIO\nIII RELIQVVM IS", + Program([ModuleCall("FRACTIO")], [ + ExpressionStatement(BinOp(Numeral("III"), Fractio("IS"), "KEYWORD_RELIQVVM")) + ]), + ValFrac(Fraction(0)) + ), # String concatenation with fraction ("CVM FRACTIO\nDICE(IIIS & \"!\")", Program([ModuleCall("FRACTIO")], [