From 5418dfa577a8195b89c614afda616d6512f544bb Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Wed, 22 Apr 2026 15:43:32 +0200 Subject: [PATCH] :goat: PER deconstructing --- README.md | 13 ++++++++ centvrion/ast_nodes.py | 25 ++++++++++++++-- centvrion/compiler/emit_stmt.py | 53 +++++++++++++++++++++++---------- centvrion/parser.py | 4 +++ language/main.tex | 1 + tests.py | 41 +++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e40bb48..13551db 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,19 @@ condition. Exit the loop with `ERVMPE` (or `REDI` from inside a function). > V ``` +Variables can be unpacked in `PER` loops, similar to `DESIGNA` destructuring: + +``` +PER a, b IN [[I, II], [III, IV]] FAC { + DIC(a + b) +} +``` + +``` +> III +> VII +``` + ## Error handling Errors can be caught using `TEMPTA` (temptare = to try) and `CAPE` (capere = to catch). The `CAPE` block binds the error message to a variable as a string. diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index 59382e4..be5e911 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -1140,6 +1140,10 @@ class PerStatement(Node): 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 + @property + def destructure(self): + return isinstance(self.variable_name, list) + def __repr__(self) -> str: test = repr(self.data_list) variable_name = repr(self.variable_name) @@ -1149,7 +1153,23 @@ class PerStatement(Node): 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}}" + if self.destructure: + var_str = ", ".join(v.print() for v in self.variable_name) + else: + var_str = self.variable_name.print() + return f"PER {var_str} IN {self.data_list.print()} FAC {{\n{body}\n}}" + + def _assign_loop_var(self, vtable, item): + if self.destructure: + if not isinstance(item, ValList): + raise CentvrionError("Cannot destructure non-array value in PER loop") + if len(item.value()) != len(self.variable_name): + raise CentvrionError( + f"Destructuring mismatch: {len(self.variable_name)} targets, {len(item.value())} values") + for id_node, val in zip(self.variable_name, item.value()): + vtable[id_node.name] = val + else: + vtable[self.variable_name.name] = item def _eval(self, vtable): vtable, array = self.data_list.eval(vtable) @@ -1158,10 +1178,9 @@ class PerStatement(Node): 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 + self._assign_loop_var(vtable, item) for statement in self.statements: vtable, val = statement.eval(vtable) if vtable["#break"] or vtable["#continue"] or vtable["#return"] is not None: diff --git a/centvrion/compiler/emit_stmt.py b/centvrion/compiler/emit_stmt.py index 340b45d..5ba90ce 100644 --- a/centvrion/compiler/emit_stmt.py +++ b/centvrion/compiler/emit_stmt.py @@ -65,23 +65,44 @@ def emit_stmt(node, ctx): if isinstance(node, PerStatement): arr_lines, arr_var = emit_expr(node.data_list, ctx) i_var = ctx.fresh_tmp() - var_name = node.variable_name.name body_lines = _emit_body(node.statements, ctx) - lines = arr_lines + [ - f"if ({arr_var}.type == CENT_DICT) {{", - f" for (int {i_var} = 0; {i_var} < {arr_var}.dval.len; {i_var}++) {{", - f' cent_scope_set(&_scope, "{var_name}", {arr_var}.dval.keys[{i_var}]);', - ] - lines += [f" {l}" for l in body_lines] - lines += [ - " }", - "} else {", - f' if ({arr_var}.type != CENT_LIST) cent_type_error("PER requires an array or dict");', - f" for (int {i_var} = 0; {i_var} < {arr_var}.lval.len; {i_var}++) {{", - f' cent_scope_set(&_scope, "{var_name}", {arr_var}.lval.items[{i_var}]);', - ] - lines += [f" {l}" for l in body_lines] - lines += [" }", "}"] + + if node.destructure: + # Destructuring PER — each element must be a list + elem_var = ctx.fresh_tmp() + assign_lines = [ + f"CentValue {elem_var} = {arr_var}.lval.items[{i_var}];", + f'if ({elem_var}.type != CENT_LIST) cent_type_error("Cannot destructure non-array value in PER loop");', + f'if ({elem_var}.lval.len != {len(node.variable_name)}) cent_runtime_error("Destructuring mismatch");', + ] + for j, id_node in enumerate(node.variable_name): + tmp = ctx.fresh_tmp() + assign_lines.append(f"CentValue {tmp} = cent_list_index({elem_var}, cent_int({j + 1}));") + assign_lines.append(f'cent_scope_set(&_scope, "{id_node.name}", {tmp});') + lines = arr_lines + [ + f'if ({arr_var}.type != CENT_LIST) cent_type_error("PER requires an array");', + f"for (int {i_var} = 0; {i_var} < {arr_var}.lval.len; {i_var}++) {{", + ] + lines += [f" {l}" for l in assign_lines] + lines += [f" {l}" for l in body_lines] + lines += ["}"] + else: + var_name = node.variable_name.name + lines = arr_lines + [ + f"if ({arr_var}.type == CENT_DICT) {{", + f" for (int {i_var} = 0; {i_var} < {arr_var}.dval.len; {i_var}++) {{", + f' cent_scope_set(&_scope, "{var_name}", {arr_var}.dval.keys[{i_var}]);', + ] + lines += [f" {l}" for l in body_lines] + lines += [ + " }", + "} else {", + f' if ({arr_var}.type != CENT_LIST) cent_type_error("PER requires an array or dict");', + f" for (int {i_var} = 0; {i_var} < {arr_var}.lval.len; {i_var}++) {{", + f' cent_scope_set(&_scope, "{var_name}", {arr_var}.lval.items[{i_var}]);', + ] + lines += [f" {l}" for l in body_lines] + lines += [" }", "}"] return lines if isinstance(node, Defini): diff --git a/centvrion/parser.py b/centvrion/parser.py index cfa20ce..10610cf 100644 --- a/centvrion/parser.py +++ b/centvrion/parser.py @@ -242,6 +242,10 @@ class Parser(): def aeternvm(tokens): return ast_nodes.DumStatement(ast_nodes.Bool(False), tokens[3]) + @self.pg.production('per_statement : KEYWORD_PER id SYMBOL_COMMA id_list_rest KEYWORD_IN expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL') + def per_destructure(tokens): + return ast_nodes.PerStatement(tokens[5], [tokens[1]] + tokens[3], tokens[8]) + @self.pg.production('per_statement : KEYWORD_PER id KEYWORD_IN expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL') def per(tokens): return ast_nodes.PerStatement(tokens[3], tokens[1], tokens[6]) diff --git a/language/main.tex b/language/main.tex index b73065a..fd9a2c6 100644 --- a/language/main.tex +++ b/language/main.tex @@ -38,6 +38,7 @@ \languageline{statement}{\texttt{DVM} \textit{expression} \texttt{FAC} \textit{scope}} \\ \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{REDI(} \textit{optional-expressions} \texttt{)}} \\ \languageline{statement}{\texttt{ERVMPE}} \\ diff --git a/tests.py b/tests.py index 3ecbce0..c0307fe 100644 --- a/tests.py +++ b/tests.py @@ -473,6 +473,20 @@ control_tests = [ ("DONICVM i VT I VSQVE V FAC { DIC(i) }", Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("V")), ID("i"), [ExpressionStatement(BuiltIn("DIC", [ID("i")]))])]), ValStr("V"), "I\nII\nIII\nIV\nV\n"), + # PER destructuring + ("PER a, b IN [[I, II], [III, IV]] FAC { DIC(a + b) }", + Program([], [PerStatement( + DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])]), + [ID("a"), ID("b")], + [ExpressionStatement(BuiltIn("DIC", [BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")]))])]), + ValStr("VII"), "III\nVII\n"), + # PER destructuring: three variables + ("PER a, b, c IN [[I, II, III]] FAC { DIC(a + b + c) }", + Program([], [PerStatement( + DataArray([DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]), + [ID("a"), ID("b"), ID("c")], + [ExpressionStatement(BuiltIn("DIC", [BinOp(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"), ID("c"), "SYMBOL_PLUS")]))])]), + ValStr("VI"), "VI\n"), ] class TestControl(unittest.TestCase): @@ -754,6 +768,8 @@ error_tests = [ ("DESIGNA a, b VT III", CentvrionError), # destructure non-array ("DESIGNA a, b VT [I]", CentvrionError), # destructure length mismatch: too many targets ("DESIGNA a, b VT [I, II, III]", CentvrionError), # destructure length mismatch: too few targets + ("PER a, b IN [I, II, III] FAC { DIC(a) }", CentvrionError), # PER destructure: element is not an array + ("PER a, b IN [[I], [II]] FAC { DIC(a) }", CentvrionError), # PER destructure: wrong number of elements ("[I, II, III][II VSQVE IV]", CentvrionError), # slice upper bound out of range ("[I, II, III][NVLLVS VSQVE II]", CentvrionError), # slice with non-integer bound ("I[I VSQVE II]", CentvrionError), # slice on non-array @@ -1549,6 +1565,31 @@ loop_edge_tests = [ [Designa(ID("x"), BinOp(ID("x"), ID("i"), "SYMBOL_PLUS"))]), ExpressionStatement(ID("x"))]), ValNul(), ""), + # PER destructuring with ERVMPE + ("DESIGNA r VT I\nPER a, b IN [[I, II], [III, IV], [V, VI]] FAC {\nSI a EST III TVNC { ERVMPE }\nDESIGNA r VT r + a + b\n}\nr", + Program([], [ + Designa(ID("r"), Numeral("I")), + PerStatement( + DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")]), DataArray([Numeral("V"), Numeral("VI")])]), + [ID("a"), ID("b")], + [SiStatement(BinOp(ID("a"), Numeral("III"), "KEYWORD_EST"), [Erumpe()], None), + Designa(ID("r"), BinOp(BinOp(ID("r"), ID("a"), "SYMBOL_PLUS"), ID("b"), "SYMBOL_PLUS"))], + ), + ExpressionStatement(ID("r")), + ]), + ValInt(4)), # 1 + 1 + 2 = 4, breaks before [III, IV] + # PER destructuring with REDI + ("DEFINI f () VT {\nPER a, b IN [[I, II], [III, IV]] FAC {\nSI a EST III TVNC { REDI (b) }\n}\n}\nINVOCA f ()", + Program([], [ + Defini(ID("f"), [], + [PerStatement( + DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])]), + [ID("a"), ID("b")], + [SiStatement(BinOp(ID("a"), Numeral("III"), "KEYWORD_EST"), [Redi([ID("b")])], None)], + )]), + ExpressionStatement(Invoca(ID("f"), [])), + ]), + ValInt(4)), # returns b=IV when a=III ] class TestLoopEdge(unittest.TestCase):