diff --git a/README.md b/README.md index edb68c6..f7aa77a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,21 @@ Variable can consist of lower-case letters, numbers, as well as `_`. `x AVGE III` is equivalent to `DESIGNA x VT x + III`. +### Destructuring + +Multiple variables can be assigned at once by unpacking an array or multi-return function: + +``` +DEFINI pair (a, b) VT { REDI (a, b) } +DESIGNA x, y VT INVOCA pair (III, VII) +``` + +The number of targets must match the length of the array. This also works with array literals: + +``` +DESIGNA a, b, c VT [I, II, III] +``` + ## Data types ### NVLLVS `NVLLVS` is a special kind of data type in `CENTVRION`, similar to the `null` value in many other languages. `NVLLVS` can be 0 if evaluated as an int or float, or an empty string if evaluated as a string. `NVLLVS` cannot be evaluated as a boolean. diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index c55fb0a..4adae8b 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -431,6 +431,35 @@ class DesignaIndex(Node): return vtable, ValNul() +class DesignaDestructure(Node): + def __init__(self, variables: list, value: Node) -> None: + self.ids = variables + self.value = value + + def __eq__(self, other): + return type(self) == type(other) and self.ids == other.ids and self.value == other.value + + def __repr__(self) -> str: + ids_string = ", ".join(repr(i) for i in self.ids) + value_string = repr(self.value).replace('\n', '\n ') + return f"DesignaDestructure(\n [{ids_string}],\n {value_string}\n)" + + def print(self): + ids_str = ", ".join(i.print() for i in self.ids) + return f"DESIGNA {ids_str} VT {self.value.print()}" + + def _eval(self, vtable): + vtable, val = self.value.eval(vtable) + if not isinstance(val, ValList): + raise CentvrionError("Cannot destructure non-array value") + if len(val.value()) != len(self.ids): + raise CentvrionError( + f"Destructuring mismatch: {len(self.ids)} targets, {len(val.value())} values") + for id_node, item in zip(self.ids, val.value()): + vtable[id_node.name] = item + return vtable, ValNul() + + class Defini(Node): def __init__(self, name: ID, parameters: list[ID], statements: list[Node]) -> None: self.name = name diff --git a/centvrion/compiler/emit_stmt.py b/centvrion/compiler/emit_stmt.py index 2f856a4..9fac5bb 100644 --- a/centvrion/compiler/emit_stmt.py +++ b/centvrion/compiler/emit_stmt.py @@ -1,6 +1,6 @@ from centvrion.ast_nodes import ( - Designa, DesignaIndex, SiStatement, DumStatement, PerStatement, - Defini, Redi, Erumpe, Continva, ExpressionStatement, ID, + Designa, DesignaIndex, DesignaDestructure, SiStatement, DumStatement, + PerStatement, Defini, Redi, Erumpe, Continva, ExpressionStatement, ID, ) from centvrion.compiler.emit_expr import emit_expr @@ -29,6 +29,18 @@ def emit_stmt(node, ctx): ] ) + if isinstance(node, DesignaDestructure): + n = len(node.ids) + val_lines, val_var = emit_expr(node.value, ctx) + lines = val_lines[:] + lines.append(f'if ({val_var}.type != CENT_LIST) cent_type_error("Cannot destructure non-array value");') + lines.append(f'if ({val_var}.lval.len != {n}) cent_runtime_error("Destructuring mismatch");') + for i, id_node in enumerate(node.ids): + tmp = ctx.fresh_tmp() + lines.append(f"CentValue {tmp} = cent_list_index({val_var}, cent_int({i + 1}));") + lines.append(f'cent_scope_set(&_scope, "{id_node.name}", {tmp});') + return lines + if isinstance(node, SiStatement): cond_lines, cond_var = emit_expr(node.test, ctx) then_lines = _emit_body(node.statements, ctx) diff --git a/centvrion/parser.py b/centvrion/parser.py index 6f7fc81..6c34d48 100644 --- a/centvrion/parser.py +++ b/centvrion/parser.py @@ -74,6 +74,10 @@ class Parser(): def statement_designa_index(tokens): return ast_nodes.DesignaIndex(tokens[1], tokens[3], tokens[6]) + @self.pg.production('statement : KEYWORD_DESIGNA id SYMBOL_COMMA id_list_rest KEYWORD_VT expression') + def statement_designa_destructure(tokens): + return ast_nodes.DesignaDestructure([tokens[1]] + tokens[3], tokens[5]) + @self.pg.production('statement : id KEYWORD_AVGE expression') def statement_avge(tokens): return ast_nodes.Designa(tokens[0], ast_nodes.BinOp(tokens[0], tokens[2], "SYMBOL_PLUS")) @@ -258,6 +262,14 @@ class Parser(): else: return [calls[0]] + calls[2] + @self.pg.production('id_list_rest : id') + @self.pg.production('id_list_rest : id SYMBOL_COMMA id_list_rest') + def id_list_rest(calls): + if len(calls) == 1: + return [calls[0]] + else: + return [calls[0]] + calls[2] + @self.pg.production("id : ID") def id_expression(tokens): return ast_nodes.ID(tokens[0].value) diff --git a/language/main.pdf b/language/main.pdf index 0852d54..6d031a8 100644 Binary files a/language/main.pdf and b/language/main.pdf differ diff --git a/language/main.tex b/language/main.tex index 5410620..304b9ea 100644 --- a/language/main.tex +++ b/language/main.tex @@ -30,6 +30,7 @@ \multicolumn{3}{|c|}{\textbf{Statements}} \\ \hline \languageline{statement}{\textit{expression}} \\ \languageline{statement}{\texttt{DESIGNA} \textbf{id} \texttt{VT} \textit{expression}} \\ + \languageline{statement}{\texttt{DESIGNA} \textbf{id} \texttt{,} \textit{ids} \texttt{VT} \textit{expression}} \\ \languageline{statement}{\textbf{id} \texttt{AVGE} \textit{expression}} \\ \languageline{statement}{\textbf{id} \texttt{MINVE} \textit{expression}} \\ \languageline{statement}{\texttt{DEFINI} \textbf{id} \texttt{(} \textit{optional-ids} \texttt{)} \texttt{VT} \textit{scope}} \\ diff --git a/tests.py b/tests.py index 34c669b..e16d192 100644 --- a/tests.py +++ b/tests.py @@ -11,8 +11,8 @@ from fractions import Fraction from centvrion.ast_nodes import ( ArrayIndex, Bool, BinOp, BuiltIn, DataArray, DataRangeArray, Defini, - Continva, Designa, DesignaIndex, DumStatement, Erumpe, ExpressionStatement, ID, - Invoca, ModuleCall, Nullus, Numeral, PerStatement, + Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement, Erumpe, + ExpressionStatement, ID, Invoca, ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement, String, UnaryMinus, UnaryNot, Fractio, frac_to_fraction, fraction_to_frac, num_to_int, int_to_num, make_string, @@ -298,6 +298,57 @@ class TestAssignment(unittest.TestCase): run_test(self, source, nodes, value) +# --- Destructuring --- + +destructuring_tests = [ + # basic: unpack multi-return function + ( + "DEFINI pair (a, b) VT { REDI (a, b) }\nDESIGNA x, y VT INVOCA pair (III, VII)\nx + y", + Program([], [ + Defini(ID("pair"), [ID("a"), ID("b")], [Redi([ID("a"), ID("b")])]), + DesignaDestructure([ID("x"), ID("y")], Invoca(ID("pair"), [Numeral("III"), Numeral("VII")])), + ExpressionStatement(BinOp(ID("x"), ID("y"), "SYMBOL_PLUS")), + ]), + ValInt(10), + ), + # unpack array literal + ( + "DESIGNA a, b VT [I, II]\na + b", + Program([], [ + DesignaDestructure([ID("a"), ID("b")], DataArray([Numeral("I"), Numeral("II")])), + ExpressionStatement(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")), + ]), + ValInt(3), + ), + # three variables + ( + "DESIGNA a, b, c VT [X, XX, XXX]\na + b + c", + Program([], [ + DesignaDestructure([ID("a"), ID("b"), ID("c")], DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX")])), + ExpressionStatement(BinOp(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"), ID("c"), "SYMBOL_PLUS")), + ]), + ValInt(60), + ), + # destructure into individual use + ( + "DEFINI pair (a, b) VT { REDI (a, b) }\nDESIGNA x, y VT INVOCA pair (V, II)\nDICE(x)\nDICE(y)", + Program([], [ + Defini(ID("pair"), [ID("a"), ID("b")], [Redi([ID("a"), ID("b")])]), + DesignaDestructure([ID("x"), ID("y")], Invoca(ID("pair"), [Numeral("V"), Numeral("II")])), + ExpressionStatement(BuiltIn("DICE", [ID("x")])), + ExpressionStatement(BuiltIn("DICE", [ID("y")])), + ]), + ValStr("II"), + "V\nII\n", + ), +] + +class TestDestructuring(unittest.TestCase): + @parameterized.expand(destructuring_tests) + def test_destructuring(self, source, nodes, value, output=""): + run_test(self, source, nodes, value, output) + + # --- Control flow --- control_tests = [ @@ -539,6 +590,9 @@ error_tests = [ ("NON I", CentvrionError), # NON on integer ("DESIGNA z VT I - I\nNON z", CentvrionError), # NON on zero integer ('NON "hello"', CentvrionError), # NON on string + ("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 ] class TestErrors(unittest.TestCase):