diff --git a/README.md b/README.md index e9a76b3..a52a816 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,16 @@ The keyword `ET` can be used as a boolean "and". The keyword `AVT` can be used a > LV ``` +An optional `GRADV` clause sets the stride. The step must be a nonzero +integer expression; positive values ascend, negative values descend, and +the endpoint is included only when the stride lands on it exactly. + +![DONICVM with GRADV](snippets/donicvm_gradv.png) + +``` +> XXV +``` + ### DVM loops ![DVM loop](snippets/dvm.png) diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index 76a1515..b7229c4 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -369,19 +369,28 @@ class DataArray(Node): class DataRangeArray(Node): - def __init__(self, from_value, to_value) -> None: + def __init__(self, from_value, to_value, step=None) -> None: self.from_value = from_value self.to_value = to_value + self.step = step def __eq__(self, other): - return type(self) == type(other) and self.from_value == other.from_value and self.to_value == other.to_value + return (type(self) == type(other) + and self.from_value == other.from_value + and self.to_value == other.to_value + and self.step == other.step) def __repr__(self) -> str: - content_string = rep_join([self.from_value, self.to_value]) - return f"RangeArray([{content_string}])" + parts = [self.from_value, self.to_value] + if self.step is not None: + parts.append(self.step) + return f"RangeArray([{rep_join(parts)}])" def print(self): - return f"[{self.from_value.print()} VSQVE {self.to_value.print()}]" + base = f"[{self.from_value.print()} VSQVE {self.to_value.print()}" + if self.step is not None: + base += f" GRADV {self.step.print()}" + return base + "]" def _eval(self, vtable): vtable, from_val = self.from_value.eval(vtable) @@ -390,7 +399,25 @@ class DataRangeArray(Node): 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)]) + if self.step is None: + return vtable, ValList([ValInt(i) for i in range(from_int, to_int + 1)]) + vtable, step_val = self.step.eval(vtable) + if not isinstance(step_val, ValInt): + raise CentvrionError("Range step must be a number") + step_int = step_val.value() + if step_int == 0: + raise CentvrionError("Range step cannot be zero") + items = [] + i = from_int + if step_int > 0: + while i <= to_int: + items.append(ValInt(i)) + i += step_int + else: + while i >= to_int: + items.append(ValInt(i)) + i += step_int + return vtable, ValList(items) class DataDict(Node): diff --git a/centvrion/compiler/emit_expr.py b/centvrion/compiler/emit_expr.py index 44ee431..c319727 100644 --- a/centvrion/compiler/emit_expr.py +++ b/centvrion/compiler/emit_expr.py @@ -166,10 +166,21 @@ def emit_expr(node, ctx): hi_lines, hi_var = emit_expr(node.to_value, ctx) tmp = ctx.fresh_tmp() i_var = ctx.fresh_tmp() - cap = f"({hi_var}.ival >= {lo_var}.ival ? (int)({hi_var}.ival - {lo_var}.ival + 1) : 0)" - lines = lo_lines + hi_lines + [ - f"CentValue {tmp} = cent_list_new({cap});", - f"for (long {i_var} = {lo_var}.ival; {i_var} <= {hi_var}.ival; {i_var}++) {{", + if node.step is None: + cap = f"({hi_var}.ival >= {lo_var}.ival ? (int)({hi_var}.ival - {lo_var}.ival + 1) : 0)" + lines = lo_lines + hi_lines + [ + f"CentValue {tmp} = cent_list_new({cap});", + f"for (long {i_var} = {lo_var}.ival; {i_var} <= {hi_var}.ival; {i_var}++) {{", + f" cent_list_push(&{tmp}, cent_int({i_var}));", + "}", + ] + return lines, tmp + step_lines, step_var = emit_expr(node.step, ctx) + lines = lo_lines + hi_lines + step_lines + [ + f'if ({step_var}.type != CENT_INT) cent_type_error("Range step must be a number");', + f'if ({step_var}.ival == 0) cent_runtime_error("Range step cannot be zero");', + f"CentValue {tmp} = cent_list_new(0);", + f"for (long {i_var} = {lo_var}.ival; ({step_var}.ival > 0 ? {i_var} <= {hi_var}.ival : {i_var} >= {hi_var}.ival); {i_var} += {step_var}.ival) {{", f" cent_list_push(&{tmp}, cent_int({i_var}));", "}", ] diff --git a/centvrion/lexer.py b/centvrion/lexer.py index 1370f44..207847e 100644 --- a/centvrion/lexer.py +++ b/centvrion/lexer.py @@ -21,6 +21,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [ "FAC", "FALSITAS", "FVNCTIO", + "GRADV", "HAVD_MINVS", "HAVD_PLVS", "INVOCA", diff --git a/centvrion/parser.py b/centvrion/parser.py index 673ba4a..5110886 100644 --- a/centvrion/parser.py +++ b/centvrion/parser.py @@ -276,6 +276,11 @@ class Parser(): range_array = ast_nodes.DataRangeArray(tokens[3], tokens[5]) return ast_nodes.PerStatement(range_array, tokens[1], tokens[8]) + @self.pg.production('donicum_statement : KEYWORD_DONICVM id KEYWORD_VT expression KEYWORD_VSQVE expression KEYWORD_GRADV expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL') + def donicum_step(tokens): + range_array = ast_nodes.DataRangeArray(tokens[3], tokens[5], tokens[7]) + return ast_nodes.PerStatement(range_array, tokens[1], tokens[10]) + # expressions @self.pg.production('expressions : SYMBOL_LPARENS expression_list') def expressions(tokens): @@ -394,6 +399,10 @@ class Parser(): def range_array(tokens): return ast_nodes.DataRangeArray(tokens[1], tokens[3]) + @self.pg.production('expression : SYMBOL_LBRACKET expression KEYWORD_VSQVE expression KEYWORD_GRADV expression SYMBOL_RBRACKET') + def range_array_step(tokens): + return ast_nodes.DataRangeArray(tokens[1], tokens[3], tokens[5]) + @self.pg.production('expression : expression SYMBOL_LBRACKET expression SYMBOL_RBRACKET', precedence='INDEX') def array_index(tokens): return ast_nodes.ArrayIndex(tokens[0], tokens[2]) diff --git a/language/main.tex b/language/main.tex index 21bdc2d..d2051ec 100644 --- a/language/main.tex +++ b/language/main.tex @@ -41,7 +41,7 @@ \languageline{statement}{\texttt{AETERNVM} \texttt{FAC} \textit{scope}} \\ \languageline{statement}{\texttt{PER} \textbf{id} \texttt{IN} \textit{expression} \texttt{FAC} \textit{scope}} \\ \languageline{statement}{\texttt{PER} \textbf{id}\texttt{,} \textbf{id-list} \texttt{IN} \textit{expression} \texttt{FAC} \textit{scope}} \\ - \languageline{statement}{\texttt{DONICVM} \textbf{id} \texttt{VT} \textit{expression} \texttt{VSQVE} \textit{expression} \texttt{FAC} \textit{scope}} \\ + \languageline{statement}{\texttt{DONICVM} \textbf{id} \texttt{VT} \textit{expression} \texttt{VSQVE} \textit{expression} \textit{optional-step} \texttt{FAC} \textit{scope}} \\ \languageline{statement}{\texttt{REDI(} \textit{optional-expressions} \texttt{)}} \\ \languageline{statement}{\texttt{ERVMPE}} \\ \languageline{statement}{\texttt{CONTINVA}} \\ @@ -73,7 +73,7 @@ \languageline{literal}{\textbf{numeral}} \\ \languageline{literal}{\textbf{bool}} \\ \languageline{literal}{\texttt{[} \textit{optional-expressions} \texttt{]}} \\ - \languageline{literal}{\texttt{[} \textit{expression} \texttt{VSQVE} \textit{expression} \texttt{]} \textnormal{\small\ (inclusive on both ends)}} \\ + \languageline{literal}{\texttt{[} \textit{expression} \texttt{VSQVE} \textit{expression} \textit{optional-step} \texttt{]} \textnormal{\small\ (inclusive on both ends)}} \\ \languageline{literal}{\texttt{TABVLA} \texttt{\{} \textit{optional-dict-items} \texttt{\}}} \\ \hline \languageline{optional-dict-items}{\textit{dict-items}} \\ @@ -94,6 +94,9 @@ \languageline{expressions}{\textit{expression}, \textit{expressions}} \\ \languageline{expressions}{\textit{expression}} \\ \hline + + \languageline{optional-step}{\texttt{GRADV} \textit{expression}} \\ + \languageline{optional-step}{} \\ \hline \end{tabular} \end{center} \end{table} diff --git a/snippets/donicvm_gradv.cent b/snippets/donicvm_gradv.cent new file mode 100644 index 0000000..4136b57 --- /dev/null +++ b/snippets/donicvm_gradv.cent @@ -0,0 +1,5 @@ +DESIGNA s VT NVLLVS +DONICVM i VT I VSQVE X GRADV II FAC { + s AVGE i +} +DIC(s) diff --git a/snippets/donicvm_gradv.png b/snippets/donicvm_gradv.png new file mode 100644 index 0000000..983db8f Binary files /dev/null and b/snippets/donicvm_gradv.png differ diff --git a/snippets/syntaxes/centvrion.sublime-syntax b/snippets/syntaxes/centvrion.sublime-syntax index eafe5e0..52002a3 100644 --- a/snippets/syntaxes/centvrion.sublime-syntax +++ b/snippets/syntaxes/centvrion.sublime-syntax @@ -78,7 +78,7 @@ contexts: scope: support.class.module.centvrion keywords: - - match: '\b(HAVD_PLVS|HAVD_MINVS|AETERNVM|ALIVD|AVGE|AVT|CAPE|CONTINVA|DEFINI|DESIGNA|DISPAR|DIVIDE|DONICVM|DVM|ERVMPE|EST|ET|FAC|FVNCTIO|INVOCA|IN|MINVE|MINVS|MVLTIPLICA|NON|PER|PLVS|REDI|RELIQVVM|SI|TABVLA|TEMPTA|TVNC|VSQVE|VT|CVM)\b' + - match: '\b(HAVD_PLVS|HAVD_MINVS|AETERNVM|ALIVD|AVGE|AVT|CAPE|CONTINVA|DEFINI|DESIGNA|DISPAR|DIVIDE|DONICVM|DVM|ERVMPE|EST|ET|FAC|FVNCTIO|GRADV|INVOCA|IN|MINVE|MINVS|MVLTIPLICA|NON|PER|PLVS|REDI|RELIQVVM|SI|TABVLA|TEMPTA|TVNC|VSQVE|VT|CVM)\b' scope: keyword.control.centvrion operators: diff --git a/tests.py b/tests.py index bcc02d0..7a541da 100644 --- a/tests.py +++ b/tests.py @@ -894,6 +894,8 @@ error_tests = [ ('CVM RETE\nAVSCVLTA(LXXX)', CentvrionError), # AVSCVLTA: no routes registered ('AVSCVLTA(LXXX)', CentvrionError), # RETE required for AVSCVLTA ('CVM RETE\nPETITVR("/", FVNCTIO (r) VT {\nREDI("hi")\n})\nAVSCVLTA("text")', CentvrionError), # AVSCVLTA port must be integer + ("DONICVM i VT I VSQVE X GRADV I - I FAC { DIC(i) }", CentvrionError), # GRADV zero step + ('DONICVM i VT I VSQVE X GRADV "foo" FAC { DIC(i) }', CentvrionError), # GRADV non-integer step ] class TestErrors(unittest.TestCase): @@ -1705,6 +1707,90 @@ loop_edge_tests = [ ExpressionStatement(Invoca(ID("f"), [])), ]), ValInt(4)), # returns b=IV when a=III + # DONICVM GRADV II, endpoint hit exactly: [I, III, V] + ("DONICVM i VT I VSQVE V GRADV II FAC { DIC(i) }", + Program([], [PerStatement( + DataRangeArray(Numeral("I"), Numeral("V"), Numeral("II")), + ID("i"), + [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), + ValStr("V"), "I\nIII\nV\n"), + # DONICVM GRADV II, endpoint overshot: VI excluded, stops at V + ("DONICVM i VT I VSQVE VI GRADV II FAC { DIC(i) }", + Program([], [PerStatement( + DataRangeArray(Numeral("I"), Numeral("VI"), Numeral("II")), + ID("i"), + [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), + ValStr("V"), "I\nIII\nV\n"), + # DONICVM GRADV I is equivalent to default step + ("DONICVM i VT I VSQVE III GRADV I FAC { DIC(i) }", + Program([], [PerStatement( + DataRangeArray(Numeral("I"), Numeral("III"), Numeral("I")), + ID("i"), + [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), + ValStr("III"), "I\nII\nIII\n"), + # DONICVM descending by I + ("CVM SVBNVLLA\nDONICVM i VT V VSQVE I GRADV - I FAC { DIC(i) }", + Program([ModuleCall("SVBNVLLA")], [PerStatement( + DataRangeArray(Numeral("V"), Numeral("I"), UnaryMinus(Numeral("I"))), + ID("i"), + [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), + ValStr("I"), "V\nIV\nIII\nII\nI\n"), + # DONICVM descending by II, endpoint overshot + ("CVM SVBNVLLA\nDONICVM i VT X VSQVE I GRADV - II FAC { DIC(i) }", + Program([ModuleCall("SVBNVLLA")], [PerStatement( + DataRangeArray(Numeral("X"), Numeral("I"), UnaryMinus(Numeral("II"))), + ID("i"), + [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), + ValStr("II"), "X\nVIII\nVI\nIV\nII\n"), + # DONICVM with step bound to a variable + ("DESIGNA s VT II\nDONICVM i VT I VSQVE V GRADV s FAC { DIC(i) }", + Program([], [ + Designa(ID("s"), Numeral("II")), + PerStatement( + DataRangeArray(Numeral("I"), Numeral("V"), ID("s")), + ID("i"), + [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), + ValStr("V"), "I\nIII\nV\n"), + # DONICVM from == to with nonzero step: single iteration + ("DONICVM i VT III VSQVE III GRADV II FAC { DIC(i) }", + Program([], [PerStatement( + DataRangeArray(Numeral("III"), Numeral("III"), Numeral("II")), + ID("i"), + [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), + ValStr("III"), "III\n"), + # DONICVM direction mismatch — negative step with ascending bounds: body never runs + ("CVM SVBNVLLA\nDESIGNA x VT NVLLVS\nDONICVM i VT I VSQVE X GRADV - I FAC { DESIGNA x VT x + i }\nx", + Program([ModuleCall("SVBNVLLA")], [ + Designa(ID("x"), Nullus()), + PerStatement( + DataRangeArray(Numeral("I"), Numeral("X"), UnaryMinus(Numeral("I"))), + ID("i"), + [Designa(ID("x"), BinOp(ID("x"), ID("i"), "SYMBOL_PLUS"))]), + ExpressionStatement(ID("x"))]), + ValNul(), ""), + # Range-array literal with GRADV used via PER + ("PER i IN [I VSQVE V GRADV II] FAC { DIC(i) }", + Program([], [PerStatement( + DataRangeArray(Numeral("I"), Numeral("V"), Numeral("II")), + ID("i"), + [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), + ValStr("V"), "I\nIII\nV\n"), + # DONICVM GRADV II with CONTINVA: skip value V, print the others + ("DONICVM i VT I VSQVE IX GRADV II FAC {\nSI i EST V TVNC { CONTINVA }\nDIC(i)\n}", + Program([], [PerStatement( + DataRangeArray(Numeral("I"), Numeral("IX"), Numeral("II")), + ID("i"), + [SiStatement(BinOp(ID("i"), Numeral("V"), "KEYWORD_EST"), [Continva()], None), + ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), + ValStr("IX"), "I\nIII\nVII\nIX\n"), + # DONICVM GRADV II with ERVMPE: stop at V (last successful DIC was III) + ("DONICVM i VT I VSQVE IX GRADV II FAC {\nSI i EST V TVNC { ERVMPE }\nDIC(i)\n}", + Program([], [PerStatement( + DataRangeArray(Numeral("I"), Numeral("IX"), Numeral("II")), + ID("i"), + [SiStatement(BinOp(ID("i"), Numeral("V"), "KEYWORD_EST"), [Erumpe()], None), + ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), + ValStr("III"), "I\nIII\n"), ] class TestLoopEdge(unittest.TestCase): diff --git a/vscode-extension/snippets/cent.json b/vscode-extension/snippets/cent.json index a16cdb4..358f884 100644 --- a/vscode-extension/snippets/cent.json +++ b/vscode-extension/snippets/cent.json @@ -41,6 +41,7 @@ "FAC": { "prefix": "FAC", "body": "FAC", "description": "block opener for loops and functions" }, "FALSITAS": { "prefix": "FALSITAS", "body": "FALSITAS", "description": "boolean false" }, "FVNCTIO": { "prefix": "FVNCTIO", "body": "FVNCTIO", "description": "anonymous function expression" }, + "GRADV": { "prefix": "GRADV", "body": "GRADV", "description": "DONICVM range step (by a step of)" }, "HAVD_MINVS": { "prefix": "HAVD_MINVS", "body": "HAVD_MINVS", "description": "greater-or-equal (>=)" }, "HAVD_PLVS": { "prefix": "HAVD_PLVS", "body": "HAVD_PLVS", "description": "less-or-equal (<=)" }, "IN": { "prefix": "IN", "body": "IN", "description": "PER-loop iterator separator" }, diff --git a/vscode-extension/syntaxes/cent.tmLanguage.json b/vscode-extension/syntaxes/cent.tmLanguage.json index 0a4bdac..eeea8c8 100644 --- a/vscode-extension/syntaxes/cent.tmLanguage.json +++ b/vscode-extension/syntaxes/cent.tmLanguage.json @@ -45,7 +45,7 @@ "patterns": [ { "name": "keyword.control.cent", - "match": "\\b(AETERNVM|ALIVD|AVGE|AVT|CAPE|CONTINVA|CVM|DEFINI|DESIGNA|DIVIDE|DONICVM|DVM|ERVMPE|ET|FAC|FVNCTIO|IN|INVOCA|MINVE|MVLTIPLICA|NON|PER|REDI|SI|TABVLA|TEMPTA|TVNC|VSQVE|VT)\\b" + "match": "\\b(AETERNVM|ALIVD|AVGE|AVT|CAPE|CONTINVA|CVM|DEFINI|DESIGNA|DIVIDE|DONICVM|DVM|ERVMPE|ET|FAC|FVNCTIO|GRADV|IN|INVOCA|MINVE|MVLTIPLICA|NON|PER|REDI|SI|TABVLA|TEMPTA|TVNC|VSQVE|VT)\\b" }, { "name": "keyword.operator.comparison.cent",