From 83c9a5682133dc275a993fd582755e65678402d3 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Wed, 1 Apr 2026 12:40:39 +0200 Subject: [PATCH] :goat: NVLLVS and string concatenation --- README.md | 8 ++++++++ centvrion/ast_nodes.py | 25 +++++++++++++++++-------- centvrion/lexer.py | 1 + centvrion/parser.py | 3 ++- language/main.tex | 2 ++ tests.py | 27 +++++++++++++++++++++++++++ 6 files changed, 57 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5ea5a96..59fd098 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,14 @@ Strings are written as text in quotes (`'` or `"`). DESIGNA x VT "this is a string" ``` +Strings are concatenated with `:`: + +``` +DESIGNA greeting VT "Hello, " : "world!" +``` + +`NVLLVS` coerces to an empty string when used with `:`. Note: `+` is for arithmetic only — using it on strings raises an error. + ### Integers Integers must be written in roman numerals using the following symbols: diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index febc60d..723596c 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -34,6 +34,7 @@ def rep_join(l): OP_STR = { "SYMBOL_PLUS": "+", "SYMBOL_MINUS": "-", "SYMBOL_TIMES": "*", "SYMBOL_DIVIDE": "/", + "SYMBOL_COLON": ":", "KEYWORD_EST": "EST", "KEYWORD_MINVS": "MINVS", "KEYWORD_PLVS": "PLVS", "KEYWORD_ET": "ET", "KEYWORD_AVT": "AVT", } @@ -411,24 +412,32 @@ class BinOp(Node): lv, rv = left.value(), right.value() match self.op: case "SYMBOL_PLUS": - return vtable, ValInt(lv + rv) + if isinstance(lv, str) or isinstance(rv, str): + raise TypeError("Use : for string concatenation, not +") + if lv is None and rv is None: + return vtable, ValNul() + return vtable, ValInt((lv or 0) + (rv or 0)) + case "SYMBOL_COLON": + lv = lv if lv is not None else "" + rv = rv if rv is not None else "" + return vtable, ValStr(lv + rv) case "SYMBOL_MINUS": - return vtable, ValInt(lv - rv) + return vtable, ValInt((lv or 0) - (rv or 0)) case "SYMBOL_TIMES": - return vtable, ValInt(lv * rv) + return vtable, ValInt((lv or 0) * (rv or 0)) case "SYMBOL_DIVIDE": # TODO: Fractio - return vtable, ValInt(lv // rv) + return vtable, ValInt((lv or 0) // (rv or 0)) case "KEYWORD_MINVS": - return vtable, ValBool(lv < rv) + return vtable, ValBool((lv or 0) < (rv or 0)) case "KEYWORD_PLVS": - return vtable, ValBool(lv > rv) + return vtable, ValBool((lv or 0) > (rv or 0)) case "KEYWORD_EST": return vtable, ValBool(lv == rv) case "KEYWORD_ET": - return vtable, ValBool(bool(lv) and bool(rv)) + return vtable, ValBool(bool(left) and bool(right)) case "KEYWORD_AVT": - return vtable, ValBool(bool(lv) or bool(rv)) + return vtable, ValBool(bool(left) or bool(right)) case _: raise Exception(self.op) diff --git a/centvrion/lexer.py b/centvrion/lexer.py index d782962..b6258c9 100644 --- a/centvrion/lexer.py +++ b/centvrion/lexer.py @@ -61,6 +61,7 @@ symbol_tokens = [ ("SYMBOL_MINUS", r"\-"), ("SYMBOL_TIMES", r"\*"), ("SYMBOL_DIVIDE", r"\/"), + ("SYMBOL_COLON", r":"), ("SYMBOL_COMMA", r",") ] diff --git a/centvrion/parser.py b/centvrion/parser.py index 6aa0622..63bac9b 100644 --- a/centvrion/parser.py +++ b/centvrion/parser.py @@ -13,7 +13,7 @@ class Parser(): ('left', ["KEYWORD_AVT"]), ('left', ["KEYWORD_ET"]), ('left', ["KEYWORD_PLVS", "KEYWORD_MINVS", "KEYWORD_EST"]), - ('left', ["SYMBOL_PLUS", "SYMBOL_MINUS"]), + ('left', ["SYMBOL_COLON", "SYMBOL_PLUS", "SYMBOL_MINUS"]), ('left', ["SYMBOL_TIMES", "SYMBOL_DIVIDE"]), ('right', ["UMINUS"]), ('left', ["INDEX"]), @@ -174,6 +174,7 @@ class Parser(): def expression_nullus(_): return ast_nodes.Nullus() + @self.pg.production('expression : expression SYMBOL_COLON expression') @self.pg.production('expression : expression SYMBOL_MINUS expression') @self.pg.production('expression : expression SYMBOL_PLUS expression') @self.pg.production('expression : expression SYMBOL_TIMES expression') diff --git a/language/main.tex b/language/main.tex index 44bbb2a..c42c22f 100644 --- a/language/main.tex +++ b/language/main.tex @@ -86,6 +86,8 @@ \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{unop}: \\ Unary operators: \texttt{-} (negation), \texttt{NON} (boolean not). \end{itemize} \end{document} \ No newline at end of file diff --git a/tests.py b/tests.py index 41ecd72..b76dde3 100644 --- a/tests.py +++ b/tests.py @@ -375,6 +375,8 @@ error_tests = [ ("DEFINI f (x, y) VT { REDI(x) }\nINVOCA f (I)", TypeError), # too few args ("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", TypeError), # args to zero-param function ("SI NVLLVS TVNC { DESIGNA r VT I }", TypeError), # NVLLVS cannot be used as boolean + ("NVLLVS AVT VERITAS", TypeError), # NVLLVS cannot be used as boolean in AVT + ('"hello" + " world"', TypeError), # use : for string concatenation, not + ("[I, II][III]", IndexError), # index too high ("CVM SVBNVLLA\n[I, II][-I]", IndexError), # negative index ("[I, II][-I]", ValueError), # negative value @@ -583,6 +585,12 @@ arithmetic_edge_tests = [ ("I / V", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("V"), "SYMBOL_DIVIDE"))]), ValInt(0)), # integer division → 0 ("M * M", Program([], [ExpressionStatement(BinOp(Numeral("M"), Numeral("M"), "SYMBOL_TIMES"))]), ValInt(1000000)), # large intermediate (not displayed) ("(I + II) * (IV - I)", Program([], [ExpressionStatement(BinOp(BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS"), BinOp(Numeral("IV"), Numeral("I"), "SYMBOL_MINUS"), "SYMBOL_TIMES"))]), ValInt(9)), # nested parens + # NVLLVS coerces to 0 in integer arithmetic + ("NVLLVS + V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "SYMBOL_PLUS"))]), ValInt(5)), + ("V + NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("V"), Nullus(), "SYMBOL_PLUS"))]), ValInt(5)), + ("NVLLVS + NVLLVS", Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_PLUS"))]), ValNul()), + ("NVLLVS - V", Program([], [ExpressionStatement(BinOp(Nullus(), Numeral("V"), "SYMBOL_MINUS"))]), ValInt(-5)), + ("V - NVLLVS", Program([], [ExpressionStatement(BinOp(Numeral("V"), Nullus(), "SYMBOL_MINUS"))]), ValInt(5)), ] class TestArithmeticEdge(unittest.TestCase): @@ -591,6 +599,22 @@ class TestArithmeticEdge(unittest.TestCase): run_test(self, source, nodes, value) +# --- String concatenation --- + +string_concat_tests = [ + ('"hello" : " world"', Program([], [ExpressionStatement(BinOp(String("hello"), String(" world"), "SYMBOL_COLON"))]), ValStr("hello world")), + # NVLLVS coerces to "" in string context + ('NVLLVS : "hello"', Program([], [ExpressionStatement(BinOp(Nullus(), String("hello"), "SYMBOL_COLON"))]), ValStr("hello")), + ('"hello" : NVLLVS', Program([], [ExpressionStatement(BinOp(String("hello"), Nullus(), "SYMBOL_COLON"))]), ValStr("hello")), + ('NVLLVS : NVLLVS', Program([], [ExpressionStatement(BinOp(Nullus(), Nullus(), "SYMBOL_COLON"))]), ValStr("")), +] + +class TestStringConcat(unittest.TestCase): + @parameterized.expand(string_concat_tests) + def test_string_concat(self, source, nodes, value): + run_test(self, source, nodes, value) + + # --- Comparison operators --- comparison_tests = [ @@ -611,6 +635,9 @@ comparison_tests = [ ("II MINVS I", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("I"), "KEYWORD_MINVS"))]), ValBool(False)), ("II PLVS I", Program([], [ExpressionStatement(BinOp(Numeral("II"), Numeral("I"), "KEYWORD_PLVS"))]), ValBool(True)), ("I PLVS II", Program([], [ExpressionStatement(BinOp(Numeral("I"), Numeral("II"), "KEYWORD_PLVS"))]), ValBool(False)), + # 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)), ] class TestComparisons(unittest.TestCase):