Compare commits

..

4 Commits

Author SHA1 Message Date
693054491f 🐐 First order functions 2026-04-21 18:12:15 +02:00
db5b7bf144 🐐 Dict 2026-04-21 17:37:12 +02:00
264ea84dfc 🐐 String interpolation 2026-04-21 16:29:40 +02:00
0b8b7c086e 🐐 README fixes 2026-04-21 15:56:16 +02:00
19 changed files with 1116 additions and 72 deletions

View File

@@ -65,6 +65,21 @@ Strings are concatenated with `&`:
`NVLLVS` coerces to an empty string when used with `&`. Note: `+` is for arithmetic only — using it on strings raises an error.
#### String Interpolation
Double-quoted strings support interpolation with `{expression}`:
```
DESIGNA nomen VT "Marcus"
DICE("Salve, {nomen}!") // → Salve, Marcus!
DICE("Sum: {III + IV}") // → Sum: VII
DICE("{nomen} has {V} cats") // → Marcus has V cats
```
Any expression can appear inside `{}`. Values are coerced to strings the same way as with `&` (integers become Roman numerals, booleans become `VERITAS`/`FALSITAS`, etc.).
Single-quoted strings do **not** interpolate — `'{nomen}'` is the literal text `{nomen}`. Use `{{` and `}}` for literal braces in double-quoted strings: `"use {{braces}}"``use {braces}`.
Integer modulo is `RELIQVVM`: `VII RELIQVVM III` evaluates to `I`. Under the `FRACTIO` module it returns a fraction, so `IIIS RELIQVVM IS` is `S` (i.e. 1/2).
### Integers
@@ -112,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
@@ -203,9 +244,31 @@ Calling a function is done with the `INVOCA` keyword.
> CXXI
```
## First-class functions
Functions are first-class values in CENTVRION. They can be assigned to variables, passed as arguments, returned from functions, and stored in arrays or dicts.
Anonymous functions are created with the `FVNCTIO` keyword:
![FVNCTIO](snippets/fvnctio.png)
```
> XIV
```
`INVOCA` accepts any expression as the callee, not just a name:
![INVOCA expressions](snippets/invoca_expr.png)
```
> VI
> VI
> XVI
```
Note: CENTVRION does **not** have closures. When a function is called, it receives a copy of the *caller's* scope, not the scope where it was defined. Variables from a function's definition site are only available if they also exist in the caller's scope at call time.
## Built-ins
### DICE
`DICE value ...`
`DICE(value, ...)`
Prints one or more values to stdout, space-separated, with integers rendered as Roman numerals. Returns the printed string.
@@ -232,9 +295,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])`

View File

@@ -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,16 @@ 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 + "}"
elif isinstance(val, ValFunc):
return "FVNCTIO"
else:
raise CentvrionError(f"Cannot display {val!r}")
@@ -272,9 +282,36 @@ 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
self.quote = '"'
def __eq__(self, other):
return type(self) == type(other) and self.value == other.value
@@ -283,12 +320,60 @@ class String(Node):
return f"String({self.value})"
def print(self):
return f'"{self.value}"'
if self.quote == "'":
return f"'{self.value}'"
escaped = self.value.replace('{', '{{').replace('}', '}}')
return f'"{escaped}"'
def _eval(self, vtable):
return vtable, ValStr(self.value)
def _flip_quotes(node, quote):
"""Recursively set quote style on all String nodes in an expression tree."""
if isinstance(node, String):
node.quote = quote
for attr in vars(node).values():
if isinstance(attr, Node):
_flip_quotes(attr, quote)
elif isinstance(attr, list):
for item in attr:
if isinstance(item, Node):
_flip_quotes(item, quote)
class InterpolatedString(Node):
def __init__(self, parts) -> None:
self.parts = parts
def __eq__(self, other):
return type(self) == type(other) and self.parts == other.parts
def __repr__(self):
return f"InterpolatedString([{rep_join(self.parts)}])"
def print(self):
result = '"'
for part in self.parts:
if isinstance(part, String):
result += part.value.replace('{', '{{').replace('}', '}}')
else:
_flip_quotes(part, "'")
result += '{' + part.print() + '}'
_flip_quotes(part, '"')
result += '"'
return result
def _eval(self, vtable):
magnvm = "MAGNVM" in vtable["#modules"]
svbnvlla = "SVBNVLLA" in vtable["#modules"]
pieces = []
for part in self.parts:
vtable, val = part.eval(vtable)
pieces.append(make_string(val, magnvm, svbnvlla))
return vtable, ValStr(''.join(pieces))
class Numeral(Node):
def __init__(self, value: str) -> None:
self.value = value
@@ -420,8 +505,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):
@@ -487,6 +579,31 @@ class Defini(Node):
return vtable, ValNul()
class Fvnctio(Node):
def __init__(self, parameters: list[ID], statements: list[Node]) -> None:
self.parameters = parameters
self.statements = statements
def __eq__(self, other):
return (type(self) == type(other)
and self.parameters == other.parameters
and self.statements == other.statements)
def __repr__(self) -> str:
parameter_string = f"parameters([{rep_join(self.parameters)}])"
statements_string = f"statements([{rep_join(self.statements)}])"
fvn_string = rep_join([parameter_string, statements_string])
return f"Fvnctio({fvn_string})"
def print(self):
params = ", ".join(p.print() for p in self.parameters)
body = "\n".join(s.print() for s in self.statements)
return f"FVNCTIO ({params}) VT {{\n{body}\n}}"
def _eval(self, vtable):
return vtable, ValFunc(self.parameters, self.statements)
class Redi(Node):
def __init__(self, values: list[Node]) -> None:
self.values = values
@@ -707,6 +824,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):
@@ -830,8 +955,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:
@@ -853,32 +981,36 @@ class PerStatement(Node):
class Invoca(Node):
def __init__(self, name, parameters) -> None:
self.name = name
def __init__(self, callee, parameters) -> None:
self.callee = callee
self.parameters = parameters
def __eq__(self, other):
return type(self) == type(other) and self.name == other.name and self.parameters == other.parameters
return (type(self) == type(other)
and self.callee == other.callee
and self.parameters == other.parameters)
def __repr__(self) -> str:
parameters_string = f"parameters([{rep_join(self.parameters)}])"
invoca_string = rep_join([self.name, parameters_string])
invoca_string = rep_join([self.callee, parameters_string])
return f"Invoca({invoca_string})"
def print(self):
args = ", ".join(p.print() for p in self.parameters)
return f"INVOCA {self.name.print()} ({args})"
return f"INVOCA {self.callee.print()} ({args})"
def _eval(self, vtable):
params = [p.eval(vtable)[1] for p in self.parameters]
if self.name.name not in vtable:
raise CentvrionError(f"Undefined function: {self.name.name}")
func = vtable[self.name.name]
vtable, func = self.callee.eval(vtable)
if not isinstance(func, ValFunc):
raise CentvrionError(f"{self.name.name} is not a function")
callee_desc = (self.callee.name
if isinstance(self.callee, ID) else "expression")
raise CentvrionError(f"{callee_desc} is not a function")
if len(params) != len(func.params):
callee_desc = (self.callee.name
if isinstance(self.callee, ID) else "FVNCTIO")
raise CentvrionError(
f"{self.name.name} expects {len(func.params)} argument(s), got {len(params)}"
f"{callee_desc} expects {len(func.params)} argument(s), got {len(params)}"
)
func_vtable = vtable.copy()
for i, param in enumerate(func.params):
@@ -978,9 +1110,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()

View File

@@ -7,6 +7,10 @@ class EmitContext:
self.functions = {}
# source-level name / alias → c_func_name; populated by emitter pre-pass
self.func_resolve = {}
# id(Fvnctio_node) → c_func_name; populated by lambda lifting pass
self.lambda_names = {}
# [(c_name, Fvnctio_node), ...]; populated by lambda lifting pass
self.lambdas = []
def fresh_tmp(self):
name = f"_t{self._tmp_counter}"

View File

@@ -1,9 +1,9 @@
from centvrion.errors import CentvrionError
from centvrion.ast_nodes import (
String, Numeral, Fractio, Bool, Nullus, ID,
String, InterpolatedString, Numeral, Fractio, Bool, Nullus, ID,
BinOp, UnaryMinus, UnaryNot,
ArrayIndex, DataArray, DataRangeArray,
BuiltIn, Invoca,
ArrayIndex, DataArray, DataRangeArray, DataDict,
BuiltIn, Invoca, Fvnctio,
num_to_int, frac_to_fraction,
)
@@ -51,6 +51,25 @@ def emit_expr(node, ctx):
tmp = ctx.fresh_tmp()
return [f'CentValue {tmp} = cent_str("{_escape(node.value)}");'], tmp
if isinstance(node, InterpolatedString):
if len(node.parts) == 0:
tmp = ctx.fresh_tmp()
return [f'CentValue {tmp} = cent_str("");'], tmp
if len(node.parts) == 1:
return emit_expr(node.parts[0], ctx)
l_lines, l_var = emit_expr(node.parts[0], ctx)
r_lines, r_var = emit_expr(node.parts[1], ctx)
lines = l_lines + r_lines
acc = ctx.fresh_tmp()
lines.append(f"CentValue {acc} = cent_concat({l_var}, {r_var});")
for part in node.parts[2:]:
p_lines, p_var = emit_expr(part, ctx)
lines.extend(p_lines)
new_acc = ctx.fresh_tmp()
lines.append(f"CentValue {new_acc} = cent_concat({acc}, {p_var});")
acc = new_acc
return lines, acc
if isinstance(node, Bool):
tmp = ctx.fresh_tmp()
v = "1" if node.value else "0"
@@ -125,12 +144,27 @@ 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)
if isinstance(node, Invoca):
return _emit_invoca(node, ctx)
if isinstance(node, Fvnctio):
return _emit_fvnctio(node, ctx)
raise NotImplementedError(type(node).__name__)
@@ -214,6 +248,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();")
@@ -227,7 +264,8 @@ def _emit_builtin(node, ctx):
def _emit_invoca(node, ctx):
"""
Emits a user-defined function call.
Requires ctx.functions[name] = [param_names] populated by the emitter pre-pass.
Supports both static resolution (ID callee with known function) and
dynamic dispatch (arbitrary expression callee via CENT_FUNC values).
"""
lines = []
param_vars = []
@@ -236,21 +274,59 @@ def _emit_invoca(node, ctx):
lines.extend(p_lines)
param_vars.append(p_var)
func_name = node.name.name
c_func_name = ctx.func_resolve.get(func_name)
if c_func_name is None:
raise CentvrionError(f"Undefined function: {func_name}")
# Try static resolution for simple ID callees
if isinstance(node.callee, ID):
c_func_name = ctx.func_resolve.get(node.callee.name)
if c_func_name is not None:
call_scope_var = ctx.fresh_tmp() + "_sc"
lines.append(f"CentScope {call_scope_var} = cent_scope_copy(&_scope);")
param_names = ctx.functions[c_func_name]
if len(param_vars) != len(param_names):
raise CentvrionError(
f"Function '{node.callee.name}' expects {len(param_names)} argument(s), "
f"got {len(param_vars)}"
)
for i, pname in enumerate(param_names):
lines.append(f'cent_scope_set(&{call_scope_var}, "{pname}", {param_vars[i]});')
tmp = ctx.fresh_tmp()
lines.append(f"CentValue {tmp} = {c_func_name}({call_scope_var});")
return lines, tmp
# Dynamic dispatch: evaluate callee, call via function pointer
callee_lines, callee_var = emit_expr(node.callee, ctx)
lines.extend(callee_lines)
lines.append(f'if ({callee_var}.type != CENT_FUNC) cent_type_error("cannot call non-function");')
call_scope_var = ctx.fresh_tmp() + "_sc"
lines.append(f"CentScope {call_scope_var} = cent_scope_copy(&_scope);")
param_names = ctx.functions[c_func_name]
if len(param_vars) != len(param_names):
raise CentvrionError(
f"Function '{func_name}' expects {len(param_names)} argument(s), got {len(param_vars)}"
nargs = len(param_vars)
lines.append(
f"if ({callee_var}.fnval.param_count != {nargs}) "
f'cent_runtime_error("wrong number of arguments");'
)
for i, pv in enumerate(param_vars):
lines.append(
f'cent_scope_set(&{call_scope_var}, '
f'{callee_var}.fnval.param_names[{i}], {pv});'
)
for i, pname in enumerate(param_names):
lines.append(f'cent_scope_set(&{call_scope_var}, "{pname}", {param_vars[i]});')
tmp = ctx.fresh_tmp()
lines.append(f"CentValue {tmp} = {c_func_name}({call_scope_var});")
lines.append(f"CentValue {tmp} = {callee_var}.fnval.fn({call_scope_var});")
return lines, tmp
def _emit_fvnctio(node, ctx):
"""Emit a FVNCTIO lambda expression as a CENT_FUNC value."""
c_name = ctx.lambda_names[id(node)]
param_names = ctx.functions[c_name]
tmp = ctx.fresh_tmp()
lines = []
# Build static param name array
params_arr = ctx.fresh_tmp() + "_pn"
lines.append(
f"static const char *{params_arr}[] = {{"
+ ", ".join(f'"{p}"' for p in param_names)
+ "};"
)
lines.append(
f"CentValue {tmp} = cent_func_val({c_name}, {params_arr}, {len(param_names)});"
)
return lines, tmp

View File

@@ -11,9 +11,6 @@ def emit_stmt(node, ctx):
Returns lines — list of C statements.
"""
if isinstance(node, Designa):
# Function alias: resolved at compile time, no runtime code needed
if isinstance(node.value, ID) and node.value.name in ctx.func_resolve:
return []
val_lines, val_var = emit_expr(node.value, ctx)
return val_lines + [f'cent_scope_set(&_scope, "{node.id.name}", {val_var});']
@@ -70,16 +67,37 @@ 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):
# Function definitions are hoisted by emitter.py; no-op here.
# Top-level definitions are handled by emitter.py (hoisted + scope-set).
# Nested definitions (inside another function) need runtime scope-set.
if ctx.current_function is not None:
name = node.name.name
c_name = ctx.func_resolve[name]
param_names = ctx.functions[c_name]
pn_var = ctx.fresh_tmp() + "_pn"
return [
f"static const char *{pn_var}[] = {{"
+ ", ".join(f'"{p}"' for p in param_names)
+ "};",
f'cent_scope_set(&_scope, "{name}", '
f"cent_func_val({c_name}, {pn_var}, {len(param_names)}));",
]
return []
if isinstance(node, Redi):

View File

@@ -1,11 +1,32 @@
import os
from centvrion.ast_nodes import Defini, Designa, ID
from centvrion.ast_nodes import Defini, Designa, Fvnctio, ID, Node
from centvrion.compiler.context import EmitContext
from centvrion.compiler.emit_stmt import emit_stmt, _emit_body
_RUNTIME_DIR = os.path.join(os.path.dirname(__file__), "runtime")
def _collect_lambdas(node, ctx, counter):
"""Walk AST recursively, find all Fvnctio nodes, assign C names."""
if isinstance(node, Fvnctio):
c_name = f"_cent_lambda_{counter[0]}"
counter[0] += 1
ctx.lambda_names[id(node)] = c_name
ctx.functions[c_name] = [p.name for p in node.parameters]
ctx.lambdas.append((c_name, node))
for attr in vars(node).values():
if isinstance(attr, Node):
_collect_lambdas(attr, ctx, counter)
elif isinstance(attr, list):
for item in attr:
if isinstance(item, Node):
_collect_lambdas(item, ctx, counter)
elif isinstance(item, tuple):
for elem in item:
if isinstance(elem, Node):
_collect_lambdas(elem, ctx, counter)
def compile_program(program):
"""Return a complete C source string for the given Program AST node."""
ctx = EmitContext()
@@ -26,10 +47,11 @@ def compile_program(program):
ctx.functions[c_name] = [p.name for p in stmt.parameters]
ctx.func_resolve[name] = c_name
func_definitions.append((c_name, stmt))
elif isinstance(stmt, Designa) and isinstance(stmt.value, ID):
rhs = stmt.value.name
if rhs in ctx.func_resolve:
ctx.func_resolve[stmt.id.name] = ctx.func_resolve[rhs]
# Lambda lifting: find all Fvnctio nodes in the entire AST
counter = [0]
for stmt in program.statements:
_collect_lambdas(stmt, ctx, counter)
lines = []
@@ -39,13 +61,13 @@ def compile_program(program):
"",
]
# Forward declarations
# Forward declarations (named functions + lambdas)
for c_name in ctx.functions:
lines.append(f"CentValue {c_name}(CentScope _scope);")
if ctx.functions:
lines.append("")
# Hoisted function definitions
# Hoisted named function definitions
for c_name, stmt in func_definitions:
ctx.current_function = c_name
lines.append(f"CentValue {c_name}(CentScope _scope) {{")
@@ -55,6 +77,16 @@ def compile_program(program):
lines += ["_func_return:", " return _return_val;", "}", ""]
ctx.current_function = None
# Hoisted lambda definitions
for c_name, fvnctio_node in ctx.lambdas:
ctx.current_function = c_name
lines.append(f"CentValue {c_name}(CentScope _scope) {{")
lines.append(" CentValue _return_val = cent_null();")
for l in _emit_body(fvnctio_node.statements, ctx):
lines.append(f" {l}")
lines += ["_func_return:", " return _return_val;", "}", ""]
ctx.current_function = None
# main()
lines.append("int main(void) {")
lines.append(" cent_init();")
@@ -62,8 +94,25 @@ def compile_program(program):
lines.append(" cent_magnvm = 1;")
lines.append(" CentScope _scope = {0};")
lines.append(" CentValue _return_val = cent_null();")
# Build a map from id(Defini_node) → c_name for scope registration
defini_c_names = {id(stmt): c_name for c_name, stmt in func_definitions}
for stmt in program.statements:
if isinstance(stmt, Defini):
name = stmt.name.name
c_name = defini_c_names[id(stmt)]
param_names = ctx.functions[c_name]
pn_var = f"_pn_{c_name}"
lines.append(
f" static const char *{pn_var}[] = {{"
+ ", ".join(f'"{p}"' for p in param_names)
+ "};"
)
lines.append(
f' cent_scope_set(&_scope, "{name}", '
f"cent_func_val({c_name}, {pn_var}, {len(param_names)}));"
)
continue
for l in emit_stmt(stmt, ctx):
lines.append(f" {l}")

View File

@@ -290,6 +290,33 @@ static int write_val(CentValue v, char *buf, int bufsz) {
return total;
}
case CENT_FUNC:
if (buf && bufsz > 7) { memcpy(buf, "FVNCTIO", 7); buf[7] = '\0'; }
return 7;
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;
@@ -431,6 +458,7 @@ CentValue cent_eq(CentValue a, CentValue b) {
switch (a.type) {
case CENT_STR: return cent_bool(strcmp(a.sval, b.sval) == 0);
case CENT_BOOL: return cent_bool(a.bval == b.bval);
case CENT_FUNC: return cent_bool(a.fnval.fn == b.fnval.fn);
case CENT_NULL: return cent_bool(1);
default:
cent_type_error("'EST' not supported for this type");
@@ -512,7 +540,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 +624,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 +641,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 +655,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 */
/* ------------------------------------------------------------------ */

View File

@@ -14,11 +14,24 @@ typedef enum {
CENT_BOOL,
CENT_LIST,
CENT_FRAC,
CENT_DICT,
CENT_FUNC,
CENT_NULL
} CentType;
typedef struct CentValue CentValue;
typedef struct CentList CentList;
typedef struct CentDict CentDict;
struct CentScope; /* forward declaration */
/* First-class function value */
typedef CentValue (*CentFuncPtr)(struct CentScope);
typedef struct {
CentFuncPtr fn;
const char **param_names;
int param_count;
} CentFuncInfo;
/* Duodecimal fraction: num/den stored as exact integers */
typedef struct {
@@ -32,6 +45,13 @@ struct CentList {
int cap;
};
struct CentDict {
CentValue *keys;
CentValue *vals;
int len;
int cap;
};
struct CentValue {
CentType type;
union {
@@ -39,13 +59,15 @@ struct CentValue {
char *sval; /* CENT_STR */
int bval; /* CENT_BOOL */
CentList lval; /* CENT_LIST */
CentFrac fval; /* CENT_FRAC */
CentFrac fval; /* CENT_FRAC */
CentDict dval; /* CENT_DICT */
CentFuncInfo fnval; /* CENT_FUNC */
};
};
/* Scope: flat name→value array. Stack-allocated by the caller;
cent_scope_set uses cent_arena when it needs to grow. */
typedef struct {
typedef struct CentScope {
const char **names;
CentValue *vals;
int len;
@@ -101,6 +123,23 @@ static inline CentValue cent_list(CentValue *items, int len, int cap) {
r.lval.cap = cap;
return r;
}
static inline CentValue cent_func_val(CentFuncPtr fn, const char **param_names, int param_count) {
CentValue r;
r.type = CENT_FUNC;
r.fnval.fn = fn;
r.fnval.param_names = param_names;
r.fnval.param_count = param_count;
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 +230,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 */
/* ------------------------------------------------------------------ */

View File

@@ -18,6 +18,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [
"ET",
"FACE",
"FALSITAS",
"FVNCTIO",
"INVOCA",
"IN",
"MINVE",
@@ -30,6 +31,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [
"RELIQVVM",
"SI",
"TVNC",
"TABVLA",
"VSQVE",
"VT",
"VERITAS",
@@ -39,6 +41,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [
builtin_tokens = [("BUILTIN", i) for i in [
"AVDI_NVMERVS",
"AVDI",
"CLAVES",
"DECIMATIO",
"DICE",
"EVERRO",

View File

@@ -1,10 +1,71 @@
from rply import ParserGenerator
from centvrion.lexer import all_tokens
from centvrion.errors import CentvrionError
from centvrion.lexer import Lexer, all_tokens
from . import ast_nodes
ALL_TOKENS = list(set([i[0] for i in all_tokens]))
def _parse_interpolated(raw_value):
quote_char = raw_value[0]
inner = raw_value[1:-1]
if quote_char == "'" or len(inner) == 0:
return ast_nodes.String(inner)
parts = []
i = 0
current = []
while i < len(inner):
ch = inner[i]
if ch == '{':
if i + 1 < len(inner) and inner[i + 1] == '{':
current.append('{')
i += 2
continue
if current:
parts.append(ast_nodes.String(''.join(current)))
current = []
j = i + 1
depth = 1
while j < len(inner) and depth > 0:
if inner[j] == '{':
depth += 1
elif inner[j] == '}':
depth -= 1
j += 1
if depth != 0:
raise CentvrionError("Unclosed '{' in interpolated string")
expr_src = inner[i + 1:j - 1]
tokens = Lexer().get_lexer().lex(expr_src + "\n")
program = Parser().parse(tokens)
if len(program.statements) != 1:
raise CentvrionError("Interpolation must contain exactly one expression")
stmt = program.statements[0]
if not isinstance(stmt, ast_nodes.ExpressionStatement):
raise CentvrionError("Interpolation must contain an expression, not a statement")
parts.append(stmt.expression)
i = j
elif ch == '}':
if i + 1 < len(inner) and inner[i + 1] == '}':
current.append('}')
i += 2
continue
raise CentvrionError("Unmatched '}' in string (use '}}' for literal '}')")
else:
current.append(ch)
i += 1
if current:
parts.append(ast_nodes.String(''.join(current)))
if len(parts) == 1 and isinstance(parts[0], ast_nodes.String):
return parts[0]
return ast_nodes.InterpolatedString(parts)
class Parser():
def __init__(self):
self.pg = ParserGenerator(
@@ -184,7 +245,7 @@ class Parser():
@self.pg.production('expression : DATA_STRING')
def expression_string(tokens):
return ast_nodes.String(tokens[0].value[1:-1])
return _parse_interpolated(tokens[0].value)
@self.pg.production('expression : DATA_NUMERAL')
def expression_numeral(tokens):
@@ -226,14 +287,33 @@ class Parser():
def unary_not(tokens):
return ast_nodes.UnaryNot(tokens[1])
@self.pg.production('expression : KEYWORD_INVOCA id expressions')
@self.pg.production('expression : KEYWORD_INVOCA expression expressions')
def invoca(tokens):
return ast_nodes.Invoca(tokens[1], tokens[2])
@self.pg.production('expression : KEYWORD_FVNCTIO ids KEYWORD_VT SYMBOL_LCURL statements SYMBOL_RCURL')
def fvnctio(tokens):
return ast_nodes.Fvnctio(tokens[1], tokens[4])
@self.pg.production('expression : SYMBOL_LPARENS expression SYMBOL_RPARENS')
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])

View File

@@ -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)

View File

@@ -55,16 +55,25 @@
\languageline{expression}{\texttt{(} \textit{expression} \texttt{)}} \\
\languageline{expression}{\textbf{id}} \\
\languageline{expression}{\textbf{builtin} \texttt{(} \textit{optional-expressions} \texttt{)}} \\
\languageline{expression}{\texttt{INVOCA} \textbf{id} \texttt{(} \textit{optional-expressions} \texttt{)}} \\
\languageline{expression}{\texttt{INVOCA} \textit{expression} \texttt{(} \textit{optional-expressions} \texttt{)}} \\
\languageline{expression}{\texttt{FVNCTIO} \texttt{(} \textit{optional-ids} \texttt{)} \texttt{VT} \textit{scope}} \\
\languageline{expression}{\textit{literal}} \\
\languageline{expression}{\textit{expression} \texttt{[} \textit{expression} \texttt{]}} \\
\languageline{expression}{\textit{expression} \textbf{binop} \textit{expression}} \\
\languageline{expression}{\textbf{unop} \textit{expression}} \\ \hline
\languageline{literal}{\textbf{string}} \\
\languageline{literal}{\textbf{interpolated-string}} \\
\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} \\
@@ -88,7 +97,8 @@
\item \textbf{module-name}: \\ Modules are flags given to the interpreter/compiler, to let it know you want to be using certain rules, functions, or features.
\item \textbf{id}: \\ Variable. Can only consist of lowercase characters and underscores, but not the letters j, u, or w.
\item \textbf{builtin}: \\ Builtin functions are uppercase latin words.
\item \textbf{string}: \\ Any text encased in " characters.
\item \textbf{string}: \\ Any text encased in \texttt{"} or \texttt{'} characters. Single-quoted strings are always literal.
\item \textbf{interpolated-string}: \\ A double-quoted string containing \texttt{\{}\textit{expression}\texttt{\}} segments. Each expression is evaluated and coerced to a string. Use \texttt{\{\{} and \texttt{\}\}} for literal braces.
\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{RELIQVVM} (modulo), \texttt{EST} (equality), \texttt{DISPAR} (not-equal), \texttt{MINVS} (<), \texttt{PLVS} (>), \texttt{ET} (and), \texttt{AVT} (or), \texttt{\&} (string concatenation).

9
snippets/fvnctio.cent Normal file
View File

@@ -0,0 +1,9 @@
DEFINI apply (f, x) VT {
REDI (INVOCA f (x))
}
DESIGNA dbl VT FVNCTIO (n) VT {
REDI (n * II)
}
DICE(INVOCA apply (dbl, VII))

BIN
snippets/fvnctio.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

11
snippets/invoca_expr.cent Normal file
View File

@@ -0,0 +1,11 @@
// Immediately invoked
DICE(INVOCA FVNCTIO (x) VT { REDI (x + I) } (V))
// From an array
DESIGNA fns VT [FVNCTIO (x) VT { REDI (x + I) }]
DICE(INVOCA fns[I] (V))
// Passing a named function as an argument
DEFINI apply (f, x) VT { REDI (INVOCA f (x)) }
DEFINI sqr (x) VT { REDI (x * x) }
DICE(INVOCA apply (sqr, IV))

BIN
snippets/invoca_expr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -33,6 +33,19 @@ contexts:
scope: string.quoted.double.centvrion
push:
- meta_scope: string.quoted.double.centvrion
- match: '\{\{'
scope: constant.character.escape.centvrion
- match: '\}\}'
scope: constant.character.escape.centvrion
- match: '\{'
scope: punctuation.section.interpolation.begin.centvrion
push:
- clear_scopes: 1
- meta_scope: meta.interpolation.centvrion
- match: '\}'
scope: punctuation.section.interpolation.end.centvrion
pop: true
- include: main
- match: '"'
pop: true
- match: "'"
@@ -57,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:
@@ -65,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|FVNCTIO|INVOCA|IN|MINVE|MINVS|NON|PER|PLVS|REDI|RELIQVVM|SI|TABVLA|TVNC|VSQVE|VT|CVM)\b'
scope: keyword.control.centvrion
operators:

418
tests.py
View File

@@ -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, 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, Fvnctio, 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__),
@@ -881,6 +881,99 @@ class TestStringConcat(unittest.TestCase):
run_test(self, source, nodes, value)
# --- String interpolation ---
interpolation_tests = [
# basic variable interpolation
('DESIGNA nomen VT "Marcus"\n"Salve, {nomen}!"',
Program([], [
Designa(ID("nomen"), String("Marcus")),
ExpressionStatement(InterpolatedString([String("Salve, "), ID("nomen"), String("!")]))
]), ValStr("Salve, Marcus!")),
# arithmetic expression inside interpolation
('DESIGNA x VT III\n"Sum: {x + II}"',
Program([], [
Designa(ID("x"), Numeral("III")),
ExpressionStatement(InterpolatedString([String("Sum: "), BinOp(ID("x"), Numeral("II"), "SYMBOL_PLUS")]))
]), ValStr("Sum: V")),
# multiple interpolations
('DESIGNA a VT I\nDESIGNA b VT II\n"{a} + {b} = {a + b}"',
Program([], [
Designa(ID("a"), Numeral("I")),
Designa(ID("b"), Numeral("II")),
ExpressionStatement(InterpolatedString([
ID("a"), String(" + "), ID("b"), String(" = "),
BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"),
]))
]), ValStr("I + II = III")),
# escaped braces become literal
('"use {{braces}}"',
Program([], [ExpressionStatement(String("use {braces}"))]),
ValStr("use {braces}")),
# single-quoted strings ignore braces
("'hello {world}'",
Program([], [ExpressionStatement(String("hello {world}"))]),
ValStr("hello {world}")),
# integer coercion
('DESIGNA n VT V\n"n is {n}"',
Program([], [
Designa(ID("n"), Numeral("V")),
ExpressionStatement(InterpolatedString([String("n is "), ID("n")]))
]), ValStr("n is V")),
# boolean coercion
('DESIGNA b VT VERITAS\n"value: {b}"',
Program([], [
Designa(ID("b"), Bool(True)),
ExpressionStatement(InterpolatedString([String("value: "), ID("b")]))
]), ValStr("value: VERITAS")),
# NVLLVS coercion
('"value: {NVLLVS}"',
Program([], [
ExpressionStatement(InterpolatedString([String("value: "), Nullus()]))
]), ValStr("value: NVLLVS")),
# expression-only string (no literal parts around it)
('DESIGNA x VT "hi"\n"{x}"',
Program([], [
Designa(ID("x"), String("hi")),
ExpressionStatement(InterpolatedString([ID("x")]))
]), ValStr("hi")),
# adjacent interpolations
('DESIGNA a VT "x"\nDESIGNA b VT "y"\n"{a}{b}"',
Program([], [
Designa(ID("a"), String("x")),
Designa(ID("b"), String("y")),
ExpressionStatement(InterpolatedString([ID("a"), ID("b")]))
]), ValStr("xy")),
# function call inside interpolation
("DEFINI f () VT {\nREDI (V)\n}\n\"result: {INVOCA f()}\"",
Program([], [
Defini(ID("f"), [], [Redi([Numeral("V")])]),
ExpressionStatement(InterpolatedString([String("result: "), Invoca(ID("f"), [])]))
]), ValStr("result: V")),
# single-quoted string inside interpolation
("DESIGNA x VT 'hello'\n\"{x & '!'}\"",
Program([], [
Designa(ID("x"), String("hello")),
ExpressionStatement(InterpolatedString([BinOp(ID("x"), String("!"), "SYMBOL_AMPERSAND")]))
]), ValStr("hello!")),
# plain double-quoted string (no braces) still works
('"hello world"',
Program([], [ExpressionStatement(String("hello world"))]),
ValStr("hello world")),
# interpolation in DICE output
('DESIGNA name VT "Roma"\nDICE("Salve, {name}!")',
Program([], [
Designa(ID("name"), String("Roma")),
ExpressionStatement(BuiltIn("DICE", [InterpolatedString([String("Salve, "), ID("name"), String("!")])]))
]), ValStr("Salve, Roma!"), "Salve, Roma!\n"),
]
class TestInterpolation(unittest.TestCase):
@parameterized.expand(interpolation_tests)
def test_interpolation(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
# --- Comparison operators ---
comparison_tests = [
@@ -1793,5 +1886,318 @@ 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)
# --- First-class functions / FVNCTIO ---
fvnctio_tests = [
# Lambda assigned to variable, then called
(
"DESIGNA f VT FVNCTIO (x) VT { REDI (x + I) }\nINVOCA f (V)",
Program([], [
Designa(ID("f"), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")])])),
ExpressionStatement(Invoca(ID("f"), [Numeral("V")])),
]),
ValInt(6),
),
# IIFE: immediately invoked lambda
(
"INVOCA FVNCTIO (x) VT { REDI (x * II) } (III)",
Program([], [
ExpressionStatement(Invoca(
Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("II"), "SYMBOL_TIMES")])]),
[Numeral("III")],
)),
]),
ValInt(6),
),
# Zero-arg lambda
(
"INVOCA FVNCTIO () VT { REDI (XLII) } ()",
Program([], [
ExpressionStatement(Invoca(
Fvnctio([], [Redi([Numeral("XLII")])]),
[],
)),
]),
ValInt(42),
),
# Function passed as argument
(
"DEFINI apply (f, x) VT { REDI (INVOCA f (x)) }\n"
"DESIGNA dbl VT FVNCTIO (n) VT { REDI (n * II) }\n"
"INVOCA apply (dbl, V)",
Program([], [
Defini(ID("apply"), [ID("f"), ID("x")], [
Redi([Invoca(ID("f"), [ID("x")])])
]),
Designa(ID("dbl"), Fvnctio([ID("n")], [
Redi([BinOp(ID("n"), Numeral("II"), "SYMBOL_TIMES")])
])),
ExpressionStatement(Invoca(ID("apply"), [ID("dbl"), Numeral("V")])),
]),
ValInt(10),
),
# Lambda uses caller-scope variable (copy-caller semantics)
(
"DESIGNA n VT III\n"
"DESIGNA f VT FVNCTIO (x) VT { REDI (x + n) }\n"
"INVOCA f (V)",
Program([], [
Designa(ID("n"), Numeral("III")),
Designa(ID("f"), Fvnctio([ID("x")], [
Redi([BinOp(ID("x"), ID("n"), "SYMBOL_PLUS")])
])),
ExpressionStatement(Invoca(ID("f"), [Numeral("V")])),
]),
ValInt(8),
),
# Named function passed as value
(
"DEFINI sqr (x) VT { REDI (x * x) }\n"
"DESIGNA f VT sqr\n"
"INVOCA f (IV)",
Program([], [
Defini(ID("sqr"), [ID("x")], [Redi([BinOp(ID("x"), ID("x"), "SYMBOL_TIMES")])]),
Designa(ID("f"), ID("sqr")),
ExpressionStatement(Invoca(ID("f"), [Numeral("IV")])),
]),
ValInt(16),
),
# Nested lambdas
(
"INVOCA FVNCTIO (x) VT { REDI (INVOCA FVNCTIO (y) VT { REDI (y + I) } (x)) } (V)",
Program([], [
ExpressionStatement(Invoca(
Fvnctio([ID("x")], [
Redi([Invoca(
Fvnctio([ID("y")], [Redi([BinOp(ID("y"), Numeral("I"), "SYMBOL_PLUS")])]),
[ID("x")],
)])
]),
[Numeral("V")],
)),
]),
ValInt(6),
),
# DICE on a function value
(
"DESIGNA f VT FVNCTIO (x) VT { REDI (x) }\nDICE(f)",
Program([], [
Designa(ID("f"), Fvnctio([ID("x")], [Redi([ID("x")])])),
ExpressionStatement(BuiltIn("DICE", [ID("f")])),
]),
ValStr("FVNCTIO"),
"FVNCTIO\n",
),
# Lambda stored in array, called via index
(
"DESIGNA fns VT [FVNCTIO (x) VT { REDI (x + I) }, FVNCTIO (x) VT { REDI (x * II) }]\n"
"INVOCA fns[I] (V)",
Program([], [
Designa(ID("fns"), DataArray([
Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")])]),
Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("II"), "SYMBOL_TIMES")])]),
])),
ExpressionStatement(Invoca(
ArrayIndex(ID("fns"), Numeral("I")),
[Numeral("V")],
)),
]),
ValInt(6),
),
# Lambda stored in dict, called via key
(
'DESIGNA d VT TABVLA {"add" VT FVNCTIO (x) VT { REDI (x + I) }}\n'
'INVOCA d["add"] (V)',
Program([], [
Designa(ID("d"), DataDict([
(String("add"), Fvnctio([ID("x")], [Redi([BinOp(ID("x"), Numeral("I"), "SYMBOL_PLUS")])])),
])),
ExpressionStatement(Invoca(
ArrayIndex(ID("d"), String("add")),
[Numeral("V")],
)),
]),
ValInt(6),
),
# Multi-param lambda
(
"DESIGNA add VT FVNCTIO (a, b) VT { REDI (a + b) }\nINVOCA add (III, IV)",
Program([], [
Designa(ID("add"), Fvnctio([ID("a"), ID("b")], [
Redi([BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")])
])),
ExpressionStatement(Invoca(ID("add"), [Numeral("III"), Numeral("IV")])),
]),
ValInt(7),
),
]
class TestFvnctio(unittest.TestCase):
@parameterized.expand(fvnctio_tests)
def test_fvnctio(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
if __name__ == "__main__":
unittest.main()

View File

@@ -45,7 +45,7 @@
"patterns": [
{
"name": "keyword.control.cent",
"match": "\\b(AETERNVM|ALVID|AVT|CONTINVA|CVM|DEFINI|DESIGNA|DONICVM|DVM|ERVMPE|ET|FACE|IN|INVOCA|NON|PER|REDI|SI|TVNC|VSQVE|VT)\\b"
"match": "\\b(AETERNVM|ALVID|AVT|CONTINVA|CVM|DEFINI|DESIGNA|DONICVM|DVM|ERVMPE|ET|FACE|FVNCTIO|IN|INVOCA|NON|PER|REDI|SI|TVNC|VSQVE|VT)\\b"
},
{
"name": "keyword.operator.comparison.cent",