🐐 Modulo

This commit is contained in:
2026-04-16 20:34:06 +02:00
parent e2ce25aa24
commit c570d72b02
10 changed files with 122 additions and 7 deletions

View File

@@ -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

View File

@@ -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":

View File

@@ -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

View File

@@ -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)) {

View File

@@ -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 */

View File

@@ -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",

View File

@@ -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')

View File

@@ -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}

View File

@@ -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:

View File

@@ -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")], [