diff --git a/README.md b/README.md index 6d643aa..01b3a94 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,32 @@ Individual elements can be accessed by index using square brackets. Indexing is > I ``` +### Dicts (TABVLA) + +Dicts are key-value maps created with the `TABVLA` keyword and curly braces: + +``` +DESIGNA d VT TABVLA {"nomen" VT "Marcus", "aetas" VT XXV} +``` + +Keys must be strings or integers. Values are accessed and assigned with square brackets: + +``` +DICE(d["nomen"]) // → Marcus +DESIGNA d["aetas"] VT XXVI // update existing key +DESIGNA d["novus"] VT I // insert new key +``` + +Iterating over a dict with `PER` loops over its keys: + +``` +PER k IN d FACE { + DICE(k) +} +``` + +`LONGITVDO(dict)` returns the number of entries. `CLAVES(dict)` returns the keys as an array. + ## Conditionals ### SI/TVNC If-then statements are denoted with the keywords `SI` (if) and `TVNC` (then). Thus, the code @@ -247,9 +273,14 @@ Skips the rest of the current loop body and continues to the next iteration (`DV Breaks out of the current loop (`DVM` or `PER`). Has no meaningful return value. ### LONGITVDO -`LONGITVDO(array)` or `LONGITVDO(string)` +`LONGITVDO(array)`, `LONGITVDO(string)`, or `LONGITVDO(dict)` -Returns the length of `array` (element count) or `string` (character count) as an integer. +Returns the length of `array` (element count), `string` (character count), or `dict` (entry count) as an integer. + +### CLAVES +`CLAVES(dict)` + +Returns the keys of `dict` as an array. ### SENATVS `SENATVS(bool, ...)` or `SENATVS([bool])` diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index 97d5a62..d87ddcb 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -5,7 +5,7 @@ from fractions import Fraction from rply.token import BaseBox from centvrion.errors import CentvrionError -from centvrion.values import Val, ValInt, ValStr, ValBool, ValList, ValNul, ValFunc, ValFrac +from centvrion.values import Val, ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac NUMERALS = { "I": 1, @@ -136,6 +136,14 @@ def make_string(val, magnvm=False, svbnvlla=False) -> str: elif isinstance(val, ValList): inner = ' '.join(make_string(i, magnvm, svbnvlla) for i in val.value()) return f"[{inner}]" + elif isinstance(val, ValDict): + def _key_val(k): + return ValStr(k) if isinstance(k, str) else ValInt(k) + inner = ', '.join( + f"{make_string(_key_val(k), magnvm, svbnvlla)} VT {make_string(v, magnvm, svbnvlla)}" + for k, v in val.value().items() + ) + return "{" + inner + "}" else: raise CentvrionError(f"Cannot display {val!r}") @@ -272,6 +280,32 @@ class DataRangeArray(Node): return vtable, ValList([ValInt(i) for i in range(from_int, to_int)]) +class DataDict(Node): + def __init__(self, pairs) -> None: + self.pairs = pairs + + def __eq__(self, other): + return type(self) == type(other) and self.pairs == other.pairs + + def __repr__(self) -> str: + pair_strs = ', '.join(f"({k!r}, {v!r})" for k, v in self.pairs) + return f"Dict([{pair_strs}])" + + def print(self): + items = ", ".join(f"{k.print()} VT {v.print()}" for k, v in self.pairs) + return "TABVLA {" + items + "}" + + def _eval(self, vtable): + d = {} + for key_node, val_node in self.pairs: + vtable, key = key_node.eval(vtable) + vtable, val = val_node.eval(vtable) + if not isinstance(key, (ValStr, ValInt)): + raise CentvrionError("Dict keys must be strings or integers") + d[key.value()] = val + return vtable, ValDict(d) + + class String(Node): def __init__(self, value) -> None: self.value = value @@ -469,8 +503,15 @@ class DesignaIndex(Node): if self.id.name not in vtable: raise CentvrionError(f"Undefined variable: {self.id.name}") target = vtable[self.id.name] + if isinstance(target, ValDict): + if not isinstance(index, (ValStr, ValInt)): + raise CentvrionError("Dict key must be a string or integer") + d = dict(target.value()) + d[index.value()] = val + vtable[self.id.name] = ValDict(d) + return vtable, ValNul() if not isinstance(target, ValList): - raise CentvrionError(f"{self.id.name} is not an array") + raise CentvrionError(f"{self.id.name} is not an array or dict") i = index.value() lst = list(target.value()) if i < 1 or i > len(lst): @@ -756,6 +797,14 @@ class ArrayIndex(Node): def _eval(self, vtable): vtable, array = self.array.eval(vtable) vtable, index = self.index.eval(vtable) + if isinstance(array, ValDict): + if not isinstance(index, (ValStr, ValInt)): + raise CentvrionError("Dict key must be a string or integer") + k = index.value() + d = array.value() + if k not in d: + raise CentvrionError(f"Key not found in dict") + return vtable, d[k] if not isinstance(array, ValList): raise CentvrionError("Cannot index a non-array value") if isinstance(index, ValInt): @@ -879,8 +928,11 @@ class PerStatement(Node): def _eval(self, vtable): vtable, array = self.data_list.eval(vtable) + if isinstance(array, ValDict): + keys = [ValStr(k) if isinstance(k, str) else ValInt(k) for k in array.value().keys()] + array = ValList(keys) if not isinstance(array, ValList): - raise CentvrionError("PER requires an array") + raise CentvrionError("PER requires an array or dict") variable_name = self.variable_name.name last_val = ValNul() for item in array: @@ -1027,9 +1079,14 @@ class BuiltIn(Node): true_count = sum(1 for p in items if p.value()) return vtable, ValBool(true_count > len(items) / 2) case "LONGITVDO": - if isinstance(params[0], (ValList, ValStr)): + if isinstance(params[0], (ValList, ValStr, ValDict)): return vtable, ValInt(len(params[0].value())) - raise CentvrionError("LONGITVDO requires an array or string") + raise CentvrionError("LONGITVDO requires an array, string, or dict") + case "CLAVES": + if not isinstance(params[0], ValDict): + raise CentvrionError("CLAVES requires a dict") + keys = [ValStr(k) if isinstance(k, str) else ValInt(k) for k in params[0].value().keys()] + return vtable, ValList(keys) case "EVERRO": print("\033[2J\033[H", end="", flush=True) return vtable, ValNul() diff --git a/centvrion/compiler/emit_expr.py b/centvrion/compiler/emit_expr.py index af1a7d0..24d0219 100644 --- a/centvrion/compiler/emit_expr.py +++ b/centvrion/compiler/emit_expr.py @@ -2,7 +2,7 @@ from centvrion.errors import CentvrionError from centvrion.ast_nodes import ( String, InterpolatedString, Numeral, Fractio, Bool, Nullus, ID, BinOp, UnaryMinus, UnaryNot, - ArrayIndex, DataArray, DataRangeArray, + ArrayIndex, DataArray, DataRangeArray, DataDict, BuiltIn, Invoca, num_to_int, frac_to_fraction, ) @@ -144,6 +144,18 @@ def emit_expr(node, ctx): ] return lines, tmp + if isinstance(node, DataDict): + lines = [] + tmp = ctx.fresh_tmp() + lines.append(f"CentValue {tmp} = cent_dict_new({len(node.pairs)});") + for key_node, val_node in node.pairs: + k_lines, k_var = emit_expr(key_node, ctx) + v_lines, v_var = emit_expr(val_node, ctx) + lines.extend(k_lines) + lines.extend(v_lines) + lines.append(f"cent_dict_set(&{tmp}, {k_var}, {v_var});") + return lines, tmp + if isinstance(node, BuiltIn): return _emit_builtin(node, ctx) @@ -233,6 +245,9 @@ def _emit_builtin(node, ctx): lines.append("break;") lines.append(f"CentValue {tmp} = cent_null();") + case "CLAVES": + lines.append(f"CentValue {tmp} = cent_dict_keys({param_vars[0]});") + case "EVERRO": lines.append("cent_everro();") lines.append(f"CentValue {tmp} = cent_null();") diff --git a/centvrion/compiler/emit_stmt.py b/centvrion/compiler/emit_stmt.py index 9fac5bb..877f65e 100644 --- a/centvrion/compiler/emit_stmt.py +++ b/centvrion/compiler/emit_stmt.py @@ -70,12 +70,20 @@ def emit_stmt(node, ctx): var_name = node.variable_name.name body_lines = _emit_body(node.statements, ctx) 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}++) {{", - f' cent_scope_set(&_scope, "{var_name}", {arr_var}.lval.items[{i_var}]);', + 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 += ["}"] + 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/compiler/runtime/cent_runtime.c b/centvrion/compiler/runtime/cent_runtime.c index 927e398..ebbb93b 100644 --- a/centvrion/compiler/runtime/cent_runtime.c +++ b/centvrion/compiler/runtime/cent_runtime.c @@ -290,6 +290,29 @@ static int write_val(CentValue v, char *buf, int bufsz) { return total; } + case CENT_DICT: { + /* "{key VT val, key VT val}" */ + int total = 2; /* '{' + '}' */ + for (int i = 0; i < v.dval.len; i++) { + if (i > 0) total += 2; /* ", " */ + total += write_val(v.dval.keys[i], NULL, 0); + total += 4; /* " VT " */ + total += write_val(v.dval.vals[i], NULL, 0); + } + if (!buf) return total; + int pos = 0; + buf[pos++] = '{'; + for (int i = 0; i < v.dval.len; i++) { + if (i > 0) { buf[pos++] = ','; buf[pos++] = ' '; } + pos += write_val(v.dval.keys[i], buf + pos, bufsz - pos); + memcpy(buf + pos, " VT ", 4); pos += 4; + pos += write_val(v.dval.vals[i], buf + pos, bufsz - pos); + } + buf[pos++] = '}'; + if (pos < bufsz) buf[pos] = '\0'; + return total; + } + default: cent_runtime_error("cannot display value"); return 0; @@ -512,7 +535,8 @@ CentValue cent_avdi_numerus(void) { CentValue cent_longitudo(CentValue v) { if (v.type == CENT_LIST) return cent_int(v.lval.len); if (v.type == CENT_STR) return cent_int((long)strlen(v.sval)); - cent_type_error("'LONGITVDO' requires a list or string"); + if (v.type == CENT_DICT) return cent_int(v.dval.len); + cent_type_error("'LONGITVDO' requires a list, string, or dict"); return cent_null(); /* unreachable; silences warning */ } @@ -595,8 +619,10 @@ void cent_list_push(CentValue *lst, CentValue v) { } CentValue cent_list_index(CentValue lst, CentValue idx) { + if (lst.type == CENT_DICT) + return cent_dict_get(lst, idx); if (lst.type != CENT_LIST) - cent_type_error("index requires a list"); + cent_type_error("index requires a list or dict"); long i; if (idx.type == CENT_INT) i = idx.ival; @@ -610,8 +636,12 @@ CentValue cent_list_index(CentValue lst, CentValue idx) { } void cent_list_index_set(CentValue *lst, CentValue idx, CentValue v) { + if (lst->type == CENT_DICT) { + cent_dict_set(lst, idx, v); + return; + } if (lst->type != CENT_LIST) - cent_type_error("index-assign requires a list"); + cent_type_error("index-assign requires a list or dict"); if (idx.type != CENT_INT) cent_type_error("list index must be an integer"); long i = idx.ival; @@ -620,6 +650,68 @@ void cent_list_index_set(CentValue *lst, CentValue idx, CentValue v) { lst->lval.items[i - 1] = v; } +/* ------------------------------------------------------------------ */ +/* Dict helpers */ +/* ------------------------------------------------------------------ */ + +static int _cent_key_eq(CentValue a, CentValue b) { + if (a.type != b.type) return 0; + if (a.type == CENT_INT) return a.ival == b.ival; + if (a.type == CENT_STR) return strcmp(a.sval, b.sval) == 0; + return 0; +} + +CentValue cent_dict_new(int cap) { + if (cap < 4) cap = 4; + CentValue *keys = cent_arena_alloc(cent_arena, cap * sizeof(CentValue)); + CentValue *vals = cent_arena_alloc(cent_arena, cap * sizeof(CentValue)); + return cent_dict_val(keys, vals, 0, cap); +} + +void cent_dict_set(CentValue *dict, CentValue key, CentValue val) { + if (dict->type != CENT_DICT) + cent_type_error("dict-set requires a dict"); + for (int i = 0; i < dict->dval.len; i++) { + if (_cent_key_eq(dict->dval.keys[i], key)) { + dict->dval.vals[i] = val; + return; + } + } + if (dict->dval.len >= dict->dval.cap) { + int new_cap = dict->dval.cap * 2; + CentValue *new_keys = cent_arena_alloc(cent_arena, new_cap * sizeof(CentValue)); + CentValue *new_vals = cent_arena_alloc(cent_arena, new_cap * sizeof(CentValue)); + memcpy(new_keys, dict->dval.keys, dict->dval.len * sizeof(CentValue)); + memcpy(new_vals, dict->dval.vals, dict->dval.len * sizeof(CentValue)); + dict->dval.keys = new_keys; + dict->dval.vals = new_vals; + dict->dval.cap = new_cap; + } + dict->dval.keys[dict->dval.len] = key; + dict->dval.vals[dict->dval.len] = val; + dict->dval.len++; +} + +CentValue cent_dict_get(CentValue dict, CentValue key) { + if (dict.type != CENT_DICT) + cent_type_error("dict-get requires a dict"); + for (int i = 0; i < dict.dval.len; i++) { + if (_cent_key_eq(dict.dval.keys[i], key)) + return dict.dval.vals[i]; + } + cent_runtime_error("Key not found in dict"); + return cent_null(); +} + +CentValue cent_dict_keys(CentValue dict) { + if (dict.type != CENT_DICT) + cent_type_error("CLAVES requires a dict"); + CentValue result = cent_list_new(dict.dval.len); + for (int i = 0; i < dict.dval.len; i++) + cent_list_push(&result, dict.dval.keys[i]); + return result; +} + /* ------------------------------------------------------------------ */ /* Initialisation */ /* ------------------------------------------------------------------ */ diff --git a/centvrion/compiler/runtime/cent_runtime.h b/centvrion/compiler/runtime/cent_runtime.h index 6ce3ff6..b20eee2 100644 --- a/centvrion/compiler/runtime/cent_runtime.h +++ b/centvrion/compiler/runtime/cent_runtime.h @@ -14,11 +14,13 @@ typedef enum { CENT_BOOL, CENT_LIST, CENT_FRAC, + CENT_DICT, CENT_NULL } CentType; typedef struct CentValue CentValue; typedef struct CentList CentList; +typedef struct CentDict CentDict; /* Duodecimal fraction: num/den stored as exact integers */ typedef struct { @@ -32,6 +34,13 @@ struct CentList { int cap; }; +struct CentDict { + CentValue *keys; + CentValue *vals; + int len; + int cap; +}; + struct CentValue { CentType type; union { @@ -40,6 +49,7 @@ struct CentValue { int bval; /* CENT_BOOL */ CentList lval; /* CENT_LIST */ CentFrac fval; /* CENT_FRAC */ + CentDict dval; /* CENT_DICT */ }; }; @@ -101,6 +111,15 @@ static inline CentValue cent_list(CentValue *items, int len, int cap) { r.lval.cap = cap; return r; } +static inline CentValue cent_dict_val(CentValue *keys, CentValue *vals, int len, int cap) { + CentValue r; + r.type = CENT_DICT; + r.dval.keys = keys; + r.dval.vals = vals; + r.dval.len = len; + r.dval.cap = cap; + return r; +} /* ------------------------------------------------------------------ */ /* Error handling */ @@ -191,6 +210,15 @@ void cent_list_push(CentValue *lst, CentValue v); CentValue cent_list_index(CentValue lst, CentValue idx); /* 1-based */ void cent_list_index_set(CentValue *lst, CentValue idx, CentValue v); +/* ------------------------------------------------------------------ */ +/* Dict helpers */ +/* ------------------------------------------------------------------ */ + +CentValue cent_dict_new(int cap); +void cent_dict_set(CentValue *dict, CentValue key, CentValue val); +CentValue cent_dict_get(CentValue dict, CentValue key); +CentValue cent_dict_keys(CentValue dict); + /* ------------------------------------------------------------------ */ /* Initialisation */ /* ------------------------------------------------------------------ */ diff --git a/centvrion/lexer.py b/centvrion/lexer.py index 9b7eb72..bc85dcc 100644 --- a/centvrion/lexer.py +++ b/centvrion/lexer.py @@ -30,6 +30,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [ "RELIQVVM", "SI", "TVNC", + "TABVLA", "VSQVE", "VT", "VERITAS", @@ -39,6 +40,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [ builtin_tokens = [("BUILTIN", i) for i in [ "AVDI_NVMERVS", "AVDI", + "CLAVES", "DECIMATIO", "DICE", "EVERRO", diff --git a/centvrion/parser.py b/centvrion/parser.py index 7ce4453..ea6e04f 100644 --- a/centvrion/parser.py +++ b/centvrion/parser.py @@ -295,6 +295,21 @@ class Parser(): def parens(tokens): return tokens[1] + @self.pg.production('dict_items : ') + @self.pg.production('dict_items : expression KEYWORD_VT expression') + @self.pg.production('dict_items : expression KEYWORD_VT expression SYMBOL_COMMA dict_items') + def dict_items(calls): + if len(calls) == 0: + return [] + elif len(calls) == 3: + return [(calls[0], calls[2])] + else: + return [(calls[0], calls[2])] + calls[4] + + @self.pg.production('expression : KEYWORD_TABVLA SYMBOL_LCURL dict_items SYMBOL_RCURL') + def dict_literal(tokens): + return ast_nodes.DataDict(tokens[2]) + @self.pg.production('expression : SYMBOL_LBRACKET array_items SYMBOL_RBRACKET') def array(tokens): return ast_nodes.DataArray(tokens[1]) diff --git a/centvrion/values.py b/centvrion/values.py index dd37821..f9068eb 100644 --- a/centvrion/values.py +++ b/centvrion/values.py @@ -66,6 +66,21 @@ class ValList(Val): def __iter__(self): return iter(self._v) +class ValDict(Val): + def __init__(self, v: dict): + assert isinstance(v, dict) + self._v = v + + def value(self): + return self._v + + def __bool__(self): + return len(self._v) > 0 + + def __iter__(self): + return iter(self._v.keys()) + + class ValFrac(Val): def __init__(self, v: Fraction): assert isinstance(v, Fraction) diff --git a/language/main.tex b/language/main.tex index 8bf01d3..99d4502 100644 --- a/language/main.tex +++ b/language/main.tex @@ -65,7 +65,14 @@ \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{]}} \\ \hline \hline + \languageline{literal}{\texttt{[} \textit{expression} \texttt{VSQVE} \textit{expression} \texttt{]}} \\ + \languageline{literal}{\texttt{TABVLA} \texttt{\{} \textit{optional-dict-items} \texttt{\}}} \\ \hline + + \languageline{optional-dict-items}{\textit{dict-items}} \\ + \languageline{optional-dict-items}{} \\ \hline + + \languageline{dict-items}{\textit{expression} \texttt{VT} \textit{expression} \texttt{,} \textit{dict-items}} \\ + \languageline{dict-items}{\textit{expression} \texttt{VT} \textit{expression}} \\ \hline \hline \multicolumn{3}{|c|}{\textbf{Lists}} \\ \hline \languageline{optional-ids}{ids} \\ diff --git a/snippets/syntaxes/centvrion.sublime-syntax b/snippets/syntaxes/centvrion.sublime-syntax index aaf5f92..0390bcf 100644 --- a/snippets/syntaxes/centvrion.sublime-syntax +++ b/snippets/syntaxes/centvrion.sublime-syntax @@ -70,7 +70,7 @@ contexts: scope: constant.language.centvrion builtins: - - match: '\b(AVDI_NVMERVS|AVDI|DECIMATIO|DICE|FORTIS_NVMERVS|FORTIS_ELECTIONIS|LONGITVDO|SEMEN)\b' + - match: '\b(AVDI_NVMERVS|AVDI|CLAVES|DECIMATIO|DICE|EVERRO|FORTIS_NVMERVS|FORTIS_ELECTIONIS|LONGITVDO|SEMEN|SENATVS)\b' scope: support.function.builtin.centvrion modules: @@ -78,7 +78,7 @@ contexts: scope: support.class.module.centvrion keywords: - - match: '\b(AETERNVM|ALVID|AVGE|AVT|CONTINVA|DEFINI|DESIGNA|DONICVM|DVM|ERVMPE|EST|ET|FACE|INVOCA|IN|MINVE|MINVS|NON|PER|PLVS|REDI|RELIQVVM|SI|TVNC|VSQVE|VT|CVM)\b' + - match: '\b(AETERNVM|ALVID|AVGE|AVT|CONTINVA|DEFINI|DESIGNA|DISPAR|DONICVM|DVM|ERVMPE|EST|ET|FACE|INVOCA|IN|MINVE|MINVS|NON|PER|PLVS|REDI|RELIQVVM|SI|TABVLA|TVNC|VSQVE|VT|CVM)\b' scope: keyword.control.centvrion operators: diff --git a/tests.py b/tests.py index 22c3747..dfd2a5d 100644 --- a/tests.py +++ b/tests.py @@ -10,18 +10,18 @@ from parameterized import parameterized from fractions import Fraction from centvrion.ast_nodes import ( - ArrayIndex, Bool, BinOp, BuiltIn, DataArray, DataRangeArray, Defini, - Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement, Erumpe, - ExpressionStatement, ID, InterpolatedString, Invoca, ModuleCall, Nullus, - Numeral, PerStatement, Program, Redi, SiStatement, String, UnaryMinus, - UnaryNot, Fractio, frac_to_fraction, fraction_to_frac, + ArrayIndex, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray, + Defini, Continva, Designa, DesignaDestructure, DesignaIndex, DumStatement, + Erumpe, ExpressionStatement, ID, InterpolatedString, 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, ) from centvrion.compiler.emitter import compile_program from centvrion.errors import CentvrionError from centvrion.lexer import Lexer from centvrion.parser import Parser -from centvrion.values import ValInt, ValStr, ValBool, ValList, ValNul, ValFunc, ValFrac +from centvrion.values import ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac _RUNTIME_C = os.path.join( os.path.dirname(__file__), @@ -1886,5 +1886,166 @@ class TestFractioHelpers(unittest.TestCase): self.assertEqual(fraction_to_frac(frac_to_fraction(s)), s) +# --- Dict (TABVLA) --- + +dict_tests = [ + # empty dict + ("TABVLA {}", + Program([], [ExpressionStatement(DataDict([]))]), + ValDict({})), + # single string key + ('TABVLA {"a" VT I}', + Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I"))]))]), + ValDict({"a": ValInt(1)})), + # multiple entries + ('TABVLA {"a" VT I, "b" VT II}', + Program([], [ExpressionStatement(DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))]))]), + ValDict({"a": ValInt(1), "b": ValInt(2)})), + # integer keys + ('TABVLA {I VT "one", II VT "two"}', + Program([], [ExpressionStatement(DataDict([(Numeral("I"), String("one")), (Numeral("II"), String("two"))]))]), + ValDict({1: ValStr("one"), 2: ValStr("two")})), + # expression values + ('TABVLA {"x" VT I + II}', + Program([], [ExpressionStatement(DataDict([(String("x"), BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS"))]))]), + ValDict({"x": ValInt(3)})), +] + +class TestDict(unittest.TestCase): + @parameterized.expand(dict_tests) + def test_dict(self, source, nodes, value): + run_test(self, source, nodes, value) + + +dict_index_tests = [ + # string key access + ('TABVLA {"a" VT X}["a"]', + Program([], [ExpressionStatement(ArrayIndex(DataDict([(String("a"), Numeral("X"))]), String("a")))]), + ValInt(10)), + # integer key access + ('TABVLA {I VT "one"}[I]', + Program([], [ExpressionStatement(ArrayIndex(DataDict([(Numeral("I"), String("one"))]), Numeral("I")))]), + ValStr("one")), + # access via variable + ('DESIGNA d VT TABVLA {"x" VT V}\nd["x"]', + Program([], [ + Designa(ID("d"), DataDict([(String("x"), Numeral("V"))])), + ExpressionStatement(ArrayIndex(ID("d"), String("x"))), + ]), + ValInt(5)), + # nested dict access + ('TABVLA {"a" VT TABVLA {"b" VT X}}["a"]["b"]', + Program([], [ExpressionStatement( + ArrayIndex(ArrayIndex(DataDict([(String("a"), DataDict([(String("b"), Numeral("X"))]))]), String("a")), String("b")) + )]), + ValInt(10)), +] + +class TestDictIndex(unittest.TestCase): + @parameterized.expand(dict_index_tests) + def test_dict_index(self, source, nodes, value): + run_test(self, source, nodes, value) + + +dict_assign_tests = [ + # update existing key + ('DESIGNA d VT TABVLA {"a" VT I}\nDESIGNA d["a"] VT X\nd["a"]', + Program([], [ + Designa(ID("d"), DataDict([(String("a"), Numeral("I"))])), + DesignaIndex(ID("d"), String("a"), Numeral("X")), + ExpressionStatement(ArrayIndex(ID("d"), String("a"))), + ]), + ValInt(10)), + # insert new key + ('DESIGNA d VT TABVLA {"a" VT I}\nDESIGNA d["b"] VT II\nd["b"]', + Program([], [ + Designa(ID("d"), DataDict([(String("a"), Numeral("I"))])), + DesignaIndex(ID("d"), String("b"), Numeral("II")), + ExpressionStatement(ArrayIndex(ID("d"), String("b"))), + ]), + ValInt(2)), + # original key unaffected after insert + ('DESIGNA d VT TABVLA {"a" VT I}\nDESIGNA d["b"] VT II\nd["a"]', + Program([], [ + Designa(ID("d"), DataDict([(String("a"), Numeral("I"))])), + DesignaIndex(ID("d"), String("b"), Numeral("II")), + ExpressionStatement(ArrayIndex(ID("d"), String("a"))), + ]), + ValInt(1)), +] + +class TestDictAssign(unittest.TestCase): + @parameterized.expand(dict_assign_tests) + def test_dict_assign(self, source, nodes, value): + run_test(self, source, nodes, value) + + +dict_builtin_tests = [ + # LONGITVDO on dict + ('LONGITVDO(TABVLA {"a" VT I, "b" VT II})', + Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))])]))]), + ValInt(2)), + # LONGITVDO on empty dict + ('LONGITVDO(TABVLA {})', + Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataDict([])]))]), + ValInt(0)), + # CLAVES + ('CLAVES(TABVLA {"a" VT I, "b" VT II})', + Program([], [ExpressionStatement(BuiltIn("CLAVES", [DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))])]))]), + ValList([ValStr("a"), ValStr("b")])), + # CLAVES with int keys + ('CLAVES(TABVLA {I VT "x", II VT "y"})', + Program([], [ExpressionStatement(BuiltIn("CLAVES", [DataDict([(Numeral("I"), String("x")), (Numeral("II"), String("y"))])]))]), + ValList([ValInt(1), ValInt(2)])), +] + +class TestDictBuiltins(unittest.TestCase): + @parameterized.expand(dict_builtin_tests) + def test_dict_builtin(self, source, nodes, value): + run_test(self, source, nodes, value) + + +dict_iteration_tests = [ + # PER iterates over keys + ('DESIGNA r VT ""\nPER k IN TABVLA {"a" VT I, "b" VT II} FACE {\nDESIGNA r VT r & k\n}\nr', + Program([], [ + Designa(ID("r"), String("")), + PerStatement( + DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))]), + ID("k"), + [Designa(ID("r"), BinOp(ID("r"), ID("k"), "SYMBOL_AMPERSAND"))], + ), + ExpressionStatement(ID("r")), + ]), + ValStr("ab")), +] + +class TestDictIteration(unittest.TestCase): + @parameterized.expand(dict_iteration_tests) + def test_dict_iteration(self, source, nodes, value): + run_test(self, source, nodes, value) + + +dict_display_tests = [ + # DICE on dict + ('DICE(TABVLA {"a" VT I})', + Program([], [ExpressionStatement(BuiltIn("DICE", [DataDict([(String("a"), Numeral("I"))])]))]), + ValStr("{a VT I}"), "{a VT I}\n"), + # DICE on multi-entry dict + ('DICE(TABVLA {"a" VT I, "b" VT II})', + Program([], [ExpressionStatement(BuiltIn("DICE", [DataDict([(String("a"), Numeral("I")), (String("b"), Numeral("II"))])]))]), + ValStr("{a VT I, b VT II}"), "{a VT I, b VT II}\n"), + # DICE on empty dict + ('DICE(TABVLA {})', + Program([], [ExpressionStatement(BuiltIn("DICE", [DataDict([])]))]), + ValStr("{}"), "{}\n"), +] + +class TestDictDisplay(unittest.TestCase): + @parameterized.expand(dict_display_tests) + def test_dict_display(self, source, nodes, value, output): + run_test(self, source, nodes, value, output) + + if __name__ == "__main__": unittest.main()