Compare commits

...

9 Commits

Author SHA1 Message Date
afb1622b3a 🐐 TEMPTA/CAPE 2026-04-21 23:19:45 +02:00
9003d49b20 🐐 String indexing and slicing 2026-04-21 23:07:40 +02:00
378c28102c 🐐 Array slicing 2026-04-21 22:53:40 +02:00
559b1b100e 🐐 VSQVE change 2026-04-21 22:35:22 +02:00
5961884219 🐐 Snippets 2026-04-21 22:16:03 +02:00
80d430970a 🐐 SCRIPTA 2026-04-21 22:00:19 +02:00
e61009b6ef 🐐 ORDINA 2026-04-21 21:48:56 +02:00
108e69291d 🐐 DORMI 2026-04-21 21:30:59 +02:00
78b1dd7667 🐐 TYPVS 2026-04-21 21:09:59 +02:00
35 changed files with 1391 additions and 88 deletions

106
README.md
View File

@@ -38,16 +38,11 @@ Variable can consist of lower-case letters, numbers, as well as `_`.
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)
```
![Destructuring function](snippets/destructure_fn.png)
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]
```
![Destructuring array](snippets/destructure_array.png)
## Data types
### NVLLVS
@@ -69,17 +64,27 @@ Strings are concatenated with `&`:
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
```
![String interpolation](snippets/string_interp.png)
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}`.
#### String Indexing and Slicing
Strings support the same indexing and slicing syntax as arrays. Indexing is 1-based and returns a single-character string:
```
"SALVTE"[I] @> "S"
"SALVTE"[III] @> "L"
```
Slicing uses `VSQVE` with inclusive bounds, returning a substring:
```
"SALVTE"[II VSQVE IV] @> "ALV"
```
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
@@ -115,10 +120,14 @@ Arrays are defined using square brackets (`[]`) and commas (`,`):
![Array literal](snippets/array_literal.png)
An array of integers can also be initialized with the `VSQVE` keyword:
An array of integers can also be initialized with the `VSQVE` keyword. The range is inclusive on both ends:
![Array with VSQVE](snippets/array_vsqve.png)
```
> [I, II, III, IV, V, VI, VII, VIII, IX, X]
```
Individual elements can be accessed by index using square brackets. Indexing is 1-based, so `I` refers to the first element:
![Array indexing](snippets/array_index.png)
@@ -127,29 +136,27 @@ Individual elements can be accessed by index using square brackets. Indexing is
> I
```
A sub-array can be extracted with `VSQVE` inside the index brackets. Both bounds are inclusive and 1-based:
![Array slicing](snippets/array_slice.png)
```
> [XX, XXX, XL]
```
### 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}
```
![Dict creation](snippets/dict_create.png)
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
```
![Dict access](snippets/dict_access.png)
Iterating over a dict with `PER` loops over its keys:
```
PER k IN d FACE {
DICE(k)
}
```
![Dict iteration](snippets/dict_per.png)
`LONGITVDO(dict)` returns the number of entries. `CLAVES(dict)` returns the keys as an array.
@@ -198,7 +205,7 @@ The keyword `ET` can be used as a boolean "and". The keyword `AVT` can be used a
![DONICVM loop](snippets/donicvm.png)
```
> XLV
> LV
```
### DVM loops
@@ -233,6 +240,24 @@ condition. Exit the loop with `ERVMPE` (or `REDI` from inside a function).
> V
```
## 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.
```
TEMPTA {
DESIGNA x VT I / NVLLVS
} CAPE error {
DICE(error)
}
```
```
> Division by zero
```
If the try block succeeds, the catch block is skipped. If an error occurs in the catch block, it propagates up. `TEMPTA`/`CAPE` blocks can be nested.
## Functions
Functions are defined with the `DEFINI` and `VT` keywords. The `REDI` keyword is used to return. `REDI` can also be used to end the program, if used outside of a function.
@@ -304,11 +329,26 @@ Returns the length of `array` (element count), `string` (character count), or `d
Returns the keys of `dict` as an array.
### ORDINA
`ORDINA(array)`
Sorts an array in ascending order. Returns a new sorted array. All elements must be the same type — integers, fractions, or strings. Integers and fractions sort numerically; strings sort lexicographically.
### SENATVS
`SENATVS(bool, ...)` or `SENATVS([bool])`
Returns VERITAS if a strict majority of the arguments are VERITAS, FALSITAS otherwise. Also accepts a single array of booleans. All values must be booleans. Ties return FALSITAS.
### TYPVS
`TYPVS(value)`
Returns the type of `value` as a string: `NVMERVS` (integer), `LITTERA` (string), `VERAX` (boolean), `CATALOGVS` (list), `FRACTIO` (fraction), `TABVLA` (dict), `FVNCTIO` (function), or `NVLLVS` (null).
### DORMI
`DORMI(n)`
Sleeps for `n` seconds, where `n` is an integer, fraction, or NVLLVS (treated as 0). Returns nothing meaningful.
## Modules
Modules are additions to the base `CENTVRION` syntax. They add or change certain features. Modules are included in your code by having
@@ -353,6 +393,16 @@ When `_` is added _after_ a numeric symbol, the symbol becomes 1.000 times large
All integer symbols except `I` may be given a `_`.
### SCRIPTA
The `SCRIPTA` module adds file I/O to your `CENTVRION` program. It adds 3 new built-in functions: `LEGE string`, `SCRIBE string string`, and `ADIVNGE string string`.
`LEGE string` reads the contents of the file at the given path and returns them as a string.
`SCRIBE string string` writes the second argument to the file at the path given by the first argument, overwriting any existing content.
`ADIVNGE string string` appends the second argument to the file at the path given by the first argument.
### SVBNVLLA
![CVM SVBNVLLA](snippets/svbnvlla.png)

View File

@@ -1,5 +1,6 @@
import re
import random
import time
from fractions import Fraction
from rply.token import BaseBox
@@ -279,7 +280,7 @@ class DataRangeArray(Node):
raise CentvrionError("Range bounds must be numbers")
from_int = from_val.value() or 0
to_int = to_val.value() or 0
return vtable, ValList([ValInt(i) for i in range(from_int, to_int)])
return vtable, ValList([ValInt(i) for i in range(from_int, to_int + 1)])
class DataDict(Node):
@@ -832,6 +833,17 @@ class ArrayIndex(Node):
if k not in d:
raise CentvrionError(f"Key not found in dict")
return vtable, d[k]
if isinstance(array, ValStr):
if isinstance(index, ValInt):
i = index.value()
elif isinstance(index, ValFrac) and index.value().denominator == 1:
i = index.value().numerator
else:
raise CentvrionError("String index must be a number")
s = array.value()
if i < 1 or i > len(s):
raise CentvrionError(f"Index {i} out of range for string of length {len(s)}")
return vtable, ValStr(s[i - 1])
if not isinstance(array, ValList):
raise CentvrionError("Cannot index a non-array value")
if isinstance(index, ValInt):
@@ -846,6 +858,59 @@ class ArrayIndex(Node):
return vtable, lst[i - 1]
def _to_index_int(val):
if isinstance(val, ValInt):
return val.value()
if isinstance(val, ValFrac) and val.value().denominator == 1:
return val.value().numerator
raise CentvrionError("Array index must be a number")
class ArraySlice(Node):
def __init__(self, array, from_index, to_index) -> None:
self.array = array
self.from_index = from_index
self.to_index = to_index
def __eq__(self, other):
return (type(self) == type(other)
and self.array == other.array
and self.from_index == other.from_index
and self.to_index == other.to_index)
def __repr__(self) -> str:
return f"ArraySlice({self.array!r}, {self.from_index!r}, {self.to_index!r})"
def print(self):
return f"{self.array.print()}[{self.from_index.print()} VSQVE {self.to_index.print()}]"
def _eval(self, vtable):
vtable, array = self.array.eval(vtable)
vtable, from_val = self.from_index.eval(vtable)
vtable, to_val = self.to_index.eval(vtable)
if isinstance(array, ValStr):
from_int = _to_index_int(from_val)
to_int = _to_index_int(to_val)
s = array.value()
if from_int < 1 or to_int > len(s) or from_int > to_int:
raise CentvrionError(
f"Slice [{from_int} VSQVE {to_int}] out of range"
f" for string of length {len(s)}"
)
return vtable, ValStr(s[from_int - 1 : to_int])
if not isinstance(array, ValList):
raise CentvrionError("Cannot slice a non-array value")
from_int = _to_index_int(from_val)
to_int = _to_index_int(to_val)
lst = array.value()
if from_int < 1 or to_int > len(lst) or from_int > to_int:
raise CentvrionError(
f"Slice [{from_int} VSQVE {to_int}] out of range"
f" for array of length {len(lst)}"
)
return vtable, ValList(lst[from_int - 1 : to_int])
class SiStatement(Node):
def __init__(self, test, statements, else_part) -> None:
self.test = test
@@ -980,6 +1045,45 @@ class PerStatement(Node):
return vtable, last_val
class TemptaStatement(Node):
def __init__(self, try_statements, error_var, catch_statements) -> None:
self.try_statements = try_statements
self.error_var = error_var
self.catch_statements = catch_statements
def __eq__(self, other):
return (type(self) == type(other)
and self.try_statements == other.try_statements
and self.error_var == other.error_var
and self.catch_statements == other.catch_statements)
def __repr__(self) -> str:
try_stmts = f"try([{rep_join(self.try_statements)}])"
catch_stmts = f"catch([{rep_join(self.catch_statements)}])"
tempta_string = rep_join([try_stmts, repr(self.error_var), catch_stmts])
return f"Tempta({tempta_string})"
def print(self):
try_body = "\n".join(s.print() for s in self.try_statements)
catch_body = "\n".join(s.print() for s in self.catch_statements)
return f"TEMPTA {{\n{try_body}\n}} CAPE {self.error_var.print()} {{\n{catch_body}\n}}"
def _eval(self, vtable):
last_val = ValNul()
try:
for statement in self.try_statements:
vtable, last_val = statement.eval(vtable)
if vtable["#return"] is not None or vtable["#break"] or vtable["#continue"]:
return vtable, last_val
except CentvrionError as e:
vtable[self.error_var.name] = ValStr(str(e))
for statement in self.catch_statements:
vtable, last_val = statement.eval(vtable)
if vtable["#return"] is not None or vtable["#break"] or vtable["#continue"]:
return vtable, last_val
return vtable, last_val
class Invoca(Node):
def __init__(self, callee, parameters) -> None:
self.callee = callee
@@ -1121,6 +1225,58 @@ class BuiltIn(Node):
case "EVERRO":
print("\033[2J\033[H", end="", flush=True)
return vtable, ValNul()
case "ORDINA":
if not isinstance(params[0], ValList):
raise CentvrionError("ORDINA requires an array")
items = list(params[0].value())
if not items:
return vtable, ValList([])
all_numeric = all(isinstance(i, (ValInt, ValFrac)) for i in items)
all_string = all(isinstance(i, ValStr) for i in items)
if not (all_numeric or all_string):
raise CentvrionError("ORDINA requires all elements to be numbers or all strings")
return vtable, ValList(sorted(items, key=lambda v: v.value()))
case "TYPVS":
type_map = {
ValInt: "NVMERVS", ValStr: "LITTERA", ValBool: "VERAX",
ValList: "CATALOGVS", ValFrac: "FRACTIO", ValDict: "TABVLA",
ValFunc: "FVNCTIO", ValNul: "NVLLVS",
}
return vtable, ValStr(type_map[type(params[0])])
case "DORMI":
v = params[0]
if isinstance(v, ValNul):
seconds = 0
elif isinstance(v, ValInt):
seconds = v.value()
elif isinstance(v, ValFrac):
seconds = float(v.value())
else:
raise CentvrionError("DORMI requires a number or NVLLVS")
time.sleep(seconds)
return vtable, ValNul()
case "LEGE":
if "SCRIPTA" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'LEGE' without module 'SCRIPTA'")
path = make_string(params[0], magnvm, svbnvlla)
with open(path, "r") as f:
return vtable, ValStr(f.read())
case "SCRIBE":
if "SCRIPTA" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'SCRIBE' without module 'SCRIPTA'")
path = make_string(params[0], magnvm, svbnvlla)
content = make_string(params[1], magnvm, svbnvlla)
with open(path, "w") as f:
f.write(content)
return vtable, ValNul()
case "ADIVNGE":
if "SCRIPTA" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'ADIVNGE' without module 'SCRIPTA'")
path = make_string(params[0], magnvm, svbnvlla)
content = make_string(params[1], magnvm, svbnvlla)
with open(path, "a") as f:
f.write(content)
return vtable, ValNul()
case _:
raise NotImplementedError(self.builtin)

View File

@@ -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, DataDict,
ArrayIndex, ArraySlice, DataArray, DataRangeArray, DataDict,
BuiltIn, Invoca, Fvnctio,
num_to_int, frac_to_fraction,
)
@@ -120,6 +120,15 @@ def emit_expr(node, ctx):
tmp = ctx.fresh_tmp()
return arr_lines + idx_lines + [f"CentValue {tmp} = cent_list_index({arr_var}, {idx_var});"], tmp
if isinstance(node, ArraySlice):
arr_lines, arr_var = emit_expr(node.array, ctx)
lo_lines, lo_var = emit_expr(node.from_index, ctx)
hi_lines, hi_var = emit_expr(node.to_index, ctx)
tmp = ctx.fresh_tmp()
return arr_lines + lo_lines + hi_lines + [
f"CentValue {tmp} = cent_list_slice({arr_var}, {lo_var}, {hi_var});"
], tmp
if isinstance(node, DataArray):
lines = []
tmp = ctx.fresh_tmp()
@@ -135,10 +144,10 @@ def emit_expr(node, ctx):
hi_lines, hi_var = emit_expr(node.to_value, ctx)
tmp = ctx.fresh_tmp()
i_var = ctx.fresh_tmp()
cap = f"({hi_var}.ival > {lo_var}.ival ? (int)({hi_var}.ival - {lo_var}.ival) : 0)"
cap = f"({hi_var}.ival >= {lo_var}.ival ? (int)({hi_var}.ival - {lo_var}.ival + 1) : 0)"
lines = lo_lines + hi_lines + [
f"CentValue {tmp} = cent_list_new({cap});",
f"for (long {i_var} = {lo_var}.ival; {i_var} < {hi_var}.ival; {i_var}++) {{",
f"for (long {i_var} = {lo_var}.ival; {i_var} <= {hi_var}.ival; {i_var}++) {{",
f" cent_list_push(&{tmp}, cent_int({i_var}));",
"}",
]
@@ -251,10 +260,43 @@ def _emit_builtin(node, ctx):
case "CLAVES":
lines.append(f"CentValue {tmp} = cent_dict_keys({param_vars[0]});")
case "ORDINA":
lines.append(f"CentValue {tmp} = cent_ordina({param_vars[0]});")
case "EVERRO":
lines.append("cent_everro();")
lines.append(f"CentValue {tmp} = cent_null();")
case "TYPVS":
lines.append(f"CentValue {tmp} = cent_typvs({param_vars[0]});")
case "DORMI":
lines.append(f"cent_dormi({param_vars[0]});")
lines.append(f"CentValue {tmp} = cent_null();")
case "LEGE":
if not ctx.has_module("SCRIPTA"):
lines.append('cent_runtime_error("SCRIPTA module required for LEGE");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_lege({param_vars[0]});")
case "SCRIBE":
if not ctx.has_module("SCRIPTA"):
lines.append('cent_runtime_error("SCRIPTA module required for SCRIBE");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"cent_scribe({param_vars[0]}, {param_vars[1]});")
lines.append(f"CentValue {tmp} = cent_null();")
case "ADIVNGE":
if not ctx.has_module("SCRIPTA"):
lines.append('cent_runtime_error("SCRIPTA module required for ADIVNGE");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"cent_adivnge({param_vars[0]}, {param_vars[1]});")
lines.append(f"CentValue {tmp} = cent_null();")
case _:
raise NotImplementedError(node.builtin)

View File

@@ -1,6 +1,7 @@
from centvrion.ast_nodes import (
Designa, DesignaIndex, DesignaDestructure, SiStatement, DumStatement,
PerStatement, Defini, Redi, Erumpe, Continva, ExpressionStatement, ID,
PerStatement, TemptaStatement, Defini, Redi, Erumpe, Continva,
ExpressionStatement, ID,
)
from centvrion.compiler.emit_expr import emit_expr
@@ -125,6 +126,24 @@ def emit_stmt(node, ctx):
if isinstance(node, Continva):
return ["continue;"]
if isinstance(node, TemptaStatement):
lines = [
"_cent_try_depth++;",
"if (setjmp(_cent_try_stack[_cent_try_depth - 1]) == 0) {",
]
try_lines = _emit_body(node.try_statements, ctx)
lines += [f" {l}" for l in try_lines]
lines += [
" _cent_try_depth--;",
"} else {",
" _cent_try_depth--;",
f' cent_scope_set(&_scope, "{node.error_var.name}", cent_str(_cent_error_msg));',
]
catch_lines = _emit_body(node.catch_statements, ctx)
lines += [f" {l}" for l in catch_lines]
lines += ["}"]
return lines
if isinstance(node, ExpressionStatement):
lines, _ = emit_expr(node.expression, ctx)
return lines

View File

@@ -11,6 +11,10 @@
CentArena *cent_arena;
int cent_magnvm = 0;
jmp_buf _cent_try_stack[CENT_TRY_STACK_MAX];
int _cent_try_depth = 0;
const char *_cent_error_msg = NULL;
/* ------------------------------------------------------------------ */
/* Arena allocator */
/* ------------------------------------------------------------------ */
@@ -50,11 +54,19 @@ void *cent_arena_alloc(CentArena *a, size_t n) {
/* ------------------------------------------------------------------ */
void cent_type_error(const char *msg) {
if (_cent_try_depth > 0) {
_cent_error_msg = msg;
longjmp(_cent_try_stack[_cent_try_depth - 1], 1);
}
fprintf(stderr, "CENTVRION type error: %s\n", msg);
exit(1);
}
void cent_runtime_error(const char *msg) {
if (_cent_try_depth > 0) {
_cent_error_msg = msg;
longjmp(_cent_try_stack[_cent_try_depth - 1], 1);
}
fprintf(stderr, "CENTVRION error: %s\n", msg);
exit(1);
}
@@ -545,6 +557,71 @@ CentValue cent_longitudo(CentValue v) {
return cent_null(); /* unreachable; silences warning */
}
CentValue cent_typvs(CentValue v) {
switch (v.type) {
case CENT_INT: return cent_str("NVMERVS");
case CENT_STR: return cent_str("LITTERA");
case CENT_BOOL: return cent_str("VERAX");
case CENT_LIST: return cent_str("CATALOGVS");
case CENT_FRAC: return cent_str("FRACTIO");
case CENT_DICT: return cent_str("TABVLA");
case CENT_FUNC: return cent_str("FVNCTIO");
case CENT_NULL: return cent_str("NVLLVS");
}
return cent_str("IGNOTA"); /* unreachable */
}
void cent_dormi(CentValue n) {
struct timespec ts;
if (n.type == CENT_NULL) {
ts.tv_sec = 0; ts.tv_nsec = 0;
} else if (n.type == CENT_INT) {
ts.tv_sec = n.ival; ts.tv_nsec = 0;
} else if (n.type == CENT_FRAC) {
long sec = n.fval.num / n.fval.den;
long rem = n.fval.num % n.fval.den;
ts.tv_sec = sec;
ts.tv_nsec = rem * 1000000000L / n.fval.den;
} else {
cent_type_error("'DORMI' requires a number or NVLLVS");
}
nanosleep(&ts, NULL);
}
/* ---- SCRIPTA module ---- */
CentValue cent_lege(CentValue path) {
const char *p = cent_make_string(path);
FILE *f = fopen(p, "r");
if (!f) cent_runtime_error("LEGE: cannot open file");
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
char *buf = cent_arena_alloc(cent_arena, len + 1);
fread(buf, 1, len, f);
buf[len] = '\0';
fclose(f);
return cent_str(buf);
}
void cent_scribe(CentValue path, CentValue content) {
const char *p = cent_make_string(path);
const char *c = cent_make_string(content);
FILE *f = fopen(p, "w");
if (!f) cent_runtime_error("SCRIBE: cannot open file");
fputs(c, f);
fclose(f);
}
void cent_adivnge(CentValue path, CentValue content) {
const char *p = cent_make_string(path);
const char *c = cent_make_string(content);
FILE *f = fopen(p, "a");
if (!f) cent_runtime_error("ADIVNGE: cannot open file");
fputs(c, f);
fclose(f);
}
CentValue cent_fortis_numerus(CentValue lo, CentValue hi) {
if (lo.type != CENT_INT || hi.type != CENT_INT)
cent_type_error("'FORTIS_NVMERVS' requires two integers");
@@ -602,6 +679,36 @@ void cent_semen(CentValue seed) {
srand((unsigned)seed.ival);
}
static int _ordina_comparator(const void *a, const void *b) {
const CentValue *va = (const CentValue *)a;
const CentValue *vb = (const CentValue *)b;
if ((va->type == CENT_INT || va->type == CENT_FRAC) &&
(vb->type == CENT_INT || vb->type == CENT_FRAC)) {
long an, ad, bn, bd;
to_frac(*va, &an, &ad);
to_frac(*vb, &bn, &bd);
long lhs = an * bd;
long rhs = bn * ad;
return (lhs > rhs) - (lhs < rhs);
}
if (va->type == CENT_STR && vb->type == CENT_STR)
return strcmp(va->sval, vb->sval);
cent_type_error("'ORDINA' requires all elements to be the same type");
return 0;
}
CentValue cent_ordina(CentValue lst) {
if (lst.type != CENT_LIST)
cent_type_error("'ORDINA' requires a list");
int len = lst.lval.len;
CentValue result = cent_list_new(len);
for (int i = 0; i < len; i++)
cent_list_push(&result, lst.lval.items[i]);
if (len > 1)
qsort(result.lval.items, len, sizeof(CentValue), _ordina_comparator);
return result;
}
/* ------------------------------------------------------------------ */
/* Array helpers */
/* ------------------------------------------------------------------ */
@@ -626,6 +733,22 @@ 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_STR) {
long i;
if (idx.type == CENT_INT)
i = idx.ival;
else if (idx.type == CENT_FRAC && idx.fval.den == 1)
i = idx.fval.num;
else
cent_type_error("string index must be an integer");
long slen = (long)strlen(lst.sval);
if (i < 1 || i > slen)
cent_runtime_error("string index out of range");
char *ch = cent_arena_alloc(cent_arena, 2);
ch[0] = lst.sval[i - 1];
ch[1] = '\0';
return cent_str(ch);
}
if (lst.type != CENT_LIST)
cent_type_error("index requires a list or dict");
long i;
@@ -640,6 +763,36 @@ CentValue cent_list_index(CentValue lst, CentValue idx) {
return lst.lval.items[i - 1];
}
CentValue cent_list_slice(CentValue lst, CentValue lo, CentValue hi) {
if (lst.type == CENT_STR) {
if (lo.type != CENT_INT || hi.type != CENT_INT)
cent_type_error("slice indices must be integers");
long from = lo.ival;
long to = hi.ival;
long slen = (long)strlen(lst.sval);
if (from < 1 || to > slen || from > to)
cent_runtime_error("string slice out of range");
int len = (int)(to - from + 1);
char *buf = cent_arena_alloc(cent_arena, len + 1);
memcpy(buf, lst.sval + from - 1, len);
buf[len] = '\0';
return cent_str(buf);
}
if (lst.type != CENT_LIST)
cent_type_error("slice requires a list");
if (lo.type != CENT_INT || hi.type != CENT_INT)
cent_type_error("slice indices must be integers");
long from = lo.ival;
long to = hi.ival;
if (from < 1 || to > lst.lval.len || from > to)
cent_runtime_error("slice out of range");
int len = (int)(to - from + 1);
CentValue result = cent_list_new(len);
for (long j = from; j <= to; j++)
cent_list_push(&result, lst.lval.items[j - 1]);
return result;
}
void cent_list_index_set(CentValue *lst, CentValue idx, CentValue v) {
if (lst->type == CENT_DICT) {
cent_dict_set(lst, idx, v);

View File

@@ -3,6 +3,7 @@
#include <stddef.h>
#include <string.h>
#include <setjmp.h>
/* ------------------------------------------------------------------ */
/* Types */
@@ -145,8 +146,13 @@ static inline CentValue cent_dict_val(CentValue *keys, CentValue *vals, int len,
/* Error handling */
/* ------------------------------------------------------------------ */
void cent_type_error(const char *msg); /* type mismatch → exit(1) */
void cent_runtime_error(const char *msg); /* runtime fault → exit(1) */
#define CENT_TRY_STACK_MAX 64
extern jmp_buf _cent_try_stack[];
extern int _cent_try_depth;
extern const char *_cent_error_msg;
void cent_type_error(const char *msg); /* type mismatch → longjmp or exit(1) */
void cent_runtime_error(const char *msg); /* runtime fault → longjmp or exit(1) */
/* ------------------------------------------------------------------ */
/* Truthiness — conditions must be booleans; anything else is a fault */
@@ -220,6 +226,12 @@ CentValue cent_decimatio(CentValue lst); /* DECIMATIO */
void cent_semen(CentValue seed); /* SEMEN */
void cent_everro(void); /* EVERRO */
CentValue cent_senatus(CentValue *args, int n); /* SENATVS */
CentValue cent_typvs(CentValue v); /* TYPVS */
void cent_dormi(CentValue n); /* DORMI */
CentValue cent_ordina(CentValue lst); /* ORDINA */
CentValue cent_lege(CentValue path); /* LEGE */
void cent_scribe(CentValue path, CentValue content); /* SCRIBE */
void cent_adivnge(CentValue path, CentValue content); /* ADIVNGE */
/* ------------------------------------------------------------------ */
/* Array helpers */
@@ -228,6 +240,7 @@ CentValue cent_senatus(CentValue *args, int n); /* SENATVS */
CentValue cent_list_new(int cap);
void cent_list_push(CentValue *lst, CentValue v);
CentValue cent_list_index(CentValue lst, CentValue idx); /* 1-based */
CentValue cent_list_slice(CentValue lst, CentValue lo, CentValue hi); /* 1-based, inclusive */
void cent_list_index_set(CentValue *lst, CentValue idx, CentValue v);
/* ------------------------------------------------------------------ */

View File

@@ -6,6 +6,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [
"AETERNVM",
"ALVID",
"AVGE",
"CAPE",
"AVT",
"DEFINI",
"DESIGNA",
@@ -32,6 +33,7 @@ keyword_tokens = [("KEYWORD_"+i, i) for i in [
"SI",
"TVNC",
"TABVLA",
"TEMPTA",
"VSQVE",
"VT",
"VERITAS",
@@ -44,12 +46,18 @@ builtin_tokens = [("BUILTIN", i) for i in [
"CLAVES",
"DECIMATIO",
"DICE",
"DORMI",
"EVERRO",
"FORTIS_NVMERVS",
"FORTIS_ELECTIONIS",
"LONGITVDO",
"ORDINA",
"SEMEN",
"SENATVS"
"SENATVS",
"TYPVS",
"LEGE",
"SCRIBE",
"ADIVNGE"
]]
data_tokens = [
@@ -62,6 +70,7 @@ module_tokens = [("MODULE", i) for i in [
"FORS",
"FRACTIO",
"MAGNVM",
"SCRIPTA",
"SVBNVLLA"
]]

View File

@@ -163,6 +163,7 @@ class Parser():
@self.pg.production('statement : dum_statement')
@self.pg.production('statement : donicum_statement')
@self.pg.production('statement : si_statement')
@self.pg.production('statement : tempta_statement')
def nested_statements(tokens):
return tokens[0]
@@ -203,6 +204,10 @@ class Parser():
def per(tokens):
return ast_nodes.PerStatement(tokens[3], tokens[1], tokens[6])
@self.pg.production('tempta_statement : KEYWORD_TEMPTA SYMBOL_LCURL statements SYMBOL_RCURL KEYWORD_CAPE id SYMBOL_LCURL statements SYMBOL_RCURL')
def tempta(tokens):
return ast_nodes.TemptaStatement(tokens[2], tokens[5], tokens[7])
@self.pg.production('donicum_statement : KEYWORD_DONICVM id KEYWORD_VT expression KEYWORD_VSQVE expression KEYWORD_FACE SYMBOL_LCURL statements SYMBOL_RCURL')
def donicum(tokens):
range_array = ast_nodes.DataRangeArray(tokens[3], tokens[5])
@@ -326,6 +331,10 @@ class Parser():
def array_index(tokens):
return ast_nodes.ArrayIndex(tokens[0], tokens[2])
@self.pg.production('expression : expression SYMBOL_LBRACKET expression KEYWORD_VSQVE expression SYMBOL_RBRACKET', precedence='INDEX')
def array_slice(tokens):
return ast_nodes.ArraySlice(tokens[0], tokens[2], tokens[4])
# ids
@self.pg.production('ids : SYMBOL_LPARENS id_list')
def ids(tokens):

View File

@@ -3,8 +3,8 @@
DESIGNA arr VT [V, III, VIII, I, IX, II, VII, IV, VI, X]
DESIGNA n VT LONGITVDO(arr)
DONICVM i VT I VSQVE n FACE {
DONICVM k VT I VSQVE n - i + I FACE {
DONICVM i VT I VSQVE n - I FACE {
DONICVM k VT I VSQVE n - i FACE {
SI arr[k] PLVS arr[k + I] TVNC {
DESIGNA temp VT arr[k]
DESIGNA arr[k] VT arr[k + I]

View File

@@ -6,7 +6,7 @@
// Returns the bottommost empty row in col, or NVLLVS if full
DEFINI find_slot(b, col) VT {
DESIGNA ans VT NVLLVS
DONICVM r VT I VSQVE VII FACE {
DONICVM r VT I VSQVE VI FACE {
SI b[(r - I) * VII + col] EST NVLLVS TVNC {
DESIGNA ans VT r
}
@@ -16,32 +16,32 @@ DEFINI find_slot(b, col) VT {
// Returns VERITAS if player has four in a row
DEFINI est_victor(b, player) VT {
DONICVM r VT I VSQVE VII FACE {
DONICVM c VT I VSQVE V FACE {
DONICVM r VT I VSQVE VI FACE {
DONICVM c VT I VSQVE IV FACE {
DESIGNA idx VT (r - I) * VII + c
SI b[idx] EST player ET b[idx + I] EST player ET b[idx + II] EST player ET b[idx + III] EST player TVNC {
REDI(VERITAS)
}
}
}
DONICVM r VT I VSQVE IV FACE {
DONICVM c VT I VSQVE VIII FACE {
DONICVM r VT I VSQVE III FACE {
DONICVM c VT I VSQVE VII FACE {
DESIGNA idx VT (r - I) * VII + c
SI b[idx] EST player ET b[idx + VII] EST player ET b[idx + XIV] EST player ET b[idx + XXI] EST player TVNC {
REDI(VERITAS)
}
}
}
DONICVM r VT I VSQVE IV FACE {
DONICVM c VT I VSQVE V FACE {
DONICVM r VT I VSQVE III FACE {
DONICVM c VT I VSQVE IV FACE {
DESIGNA idx VT (r - I) * VII + c
SI b[idx] EST player ET b[idx + VIII] EST player ET b[idx + XVI] EST player ET b[idx + XXIV] EST player TVNC {
REDI(VERITAS)
}
}
}
DONICVM r VT I VSQVE IV FACE {
DONICVM c VT IV VSQVE VIII FACE {
DONICVM r VT I VSQVE III FACE {
DONICVM c VT IV VSQVE VII FACE {
DESIGNA idx VT (r - I) * VII + c
SI b[idx] EST player ET b[idx + VI] EST player ET b[idx + XII] EST player ET b[idx + XVIII] EST player TVNC {
REDI(VERITAS)
@@ -53,9 +53,9 @@ DEFINI est_victor(b, player) VT {
DEFINI print_board(b) VT {
DICE("+---+---+---+---+---+---+---+")
DONICVM r VT I VSQVE VII FACE {
DONICVM r VT I VSQVE VI FACE {
DESIGNA line VT "| "
DONICVM c VT I VSQVE VIII FACE {
DONICVM c VT I VSQVE VII FACE {
DESIGNA cell VT b[(r - I) * VII + c]
SI cell EST I TVNC {
DESIGNA line VT line & "X | "
@@ -101,35 +101,35 @@ DEFINI score_fenestram(a, b, c, d) VT {
DEFINI aestima(b) VT {
DESIGNA score VT NVLLVS
// Center column preference: each AI piece in column IV is worth +1
DONICVM r VT I VSQVE VII FACE {
DONICVM r VT I VSQVE VI FACE {
SI b[(r - I) * VII + IV] EST II TVNC {
DESIGNA score VT score + I
}
}
// Horizontal windows (6 rows x 4 starting columns = 24)
DONICVM r VT I VSQVE VII FACE {
DONICVM c VT I VSQVE V FACE {
DONICVM r VT I VSQVE VI FACE {
DONICVM c VT I VSQVE IV FACE {
DESIGNA idx VT (r - I) * VII + c
DESIGNA score VT score + INVOCA score_fenestram(b[idx], b[idx + I], b[idx + II], b[idx + III])
}
}
// Vertical windows (3 starting rows x 7 columns = 21)
DONICVM r VT I VSQVE IV FACE {
DONICVM c VT I VSQVE VIII FACE {
DONICVM r VT I VSQVE III FACE {
DONICVM c VT I VSQVE VII FACE {
DESIGNA idx VT (r - I) * VII + c
DESIGNA score VT score + INVOCA score_fenestram(b[idx], b[idx + VII], b[idx + XIV], b[idx + XXI])
}
}
// Diagonal up-right windows (3 starting rows x 4 starting columns = 12)
DONICVM r VT I VSQVE IV FACE {
DONICVM c VT I VSQVE V FACE {
DONICVM r VT I VSQVE III FACE {
DONICVM c VT I VSQVE IV FACE {
DESIGNA idx VT (r - I) * VII + c
DESIGNA score VT score + INVOCA score_fenestram(b[idx], b[idx + VIII], b[idx + XVI], b[idx + XXIV])
}
}
// Diagonal up-left windows (3 starting rows x 4 starting columns = 12)
DONICVM r VT I VSQVE IV FACE {
DONICVM c VT IV VSQVE VIII FACE {
DONICVM r VT I VSQVE III FACE {
DONICVM c VT IV VSQVE VII FACE {
DESIGNA idx VT (r - I) * VII + c
DESIGNA score VT score + INVOCA score_fenestram(b[idx], b[idx + VI], b[idx + XII], b[idx + XVIII])
}
@@ -219,8 +219,8 @@ DEFINI ai_move(b) VT {
}
// --- Board setup ---
DESIGNA board VT [I VSQVE XLIII]
DONICVM i VT I VSQVE XLIII FACE {
DESIGNA board VT [I VSQVE XLII]
DONICVM i VT I VSQVE XLII FACE {
DESIGNA board[i] VT NVLLVS
}

View File

@@ -1,9 +1,9 @@
// Prints an X×X multiplication table
DESIGNA n VT X
DONICVM i VT I VSQVE n + I FACE {
DONICVM i VT I VSQVE n FACE {
DESIGNA line VT ""
DONICVM k VT I VSQVE n + I FACE {
DONICVM k VT I VSQVE n FACE {
DESIGNA line VT line & i * k & " "
}
DICE(line)

View File

@@ -2,9 +2,9 @@
DESIGNA n VT L
DONICVM i VT II VSQVE n + I FACE {
DONICVM i VT II VSQVE n FACE {
DESIGNA is_prime VT VERITAS
DONICVM k VT II VSQVE i FACE {
DONICVM k VT II VSQVE i - I FACE {
SI (i / k) * k EST i TVNC {
DESIGNA is_prime VT FALSITAS
}

2
language/main.aux Normal file
View File

@@ -0,0 +1,2 @@
\relax
\gdef \@abspage@last{2}

269
language/main.log Normal file
View File

@@ -0,0 +1,269 @@
This is XeTeX, Version 3.141592653-2.6-0.999998 (TeX Live 2026/Arch Linux) (preloaded format=xelatex 2026.4.8) 21 APR 2026 22:51
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**main.tex
(./main.tex
LaTeX2e <2025-11-01>
L3 programming layer <2026-01-19>
(/usr/share/texmf-dist/tex/latex/base/article.cls
Document Class: article 2025/01/22 v1.4n Standard LaTeX document class
(/usr/share/texmf-dist/tex/latex/base/size10.clo
File: size10.clo 2025/01/22 v1.4n Standard LaTeX file (size option)
)
\c@part=\count271
\c@section=\count272
\c@subsection=\count273
\c@subsubsection=\count274
\c@paragraph=\count275
\c@subparagraph=\count276
\c@figure=\count277
\c@table=\count278
\abovecaptionskip=\skip49
\belowcaptionskip=\skip50
\bibindent=\dimen148
)
(/usr/share/texmf-dist/tex/latex/geometry/geometry.sty
Package: geometry 2020/01/02 v5.9 Page Geometry
(/usr/share/texmf-dist/tex/latex/graphics/keyval.sty
Package: keyval 2022/05/29 v1.15 key=value parser (DPC)
\KV@toks@=\toks17
)
(/usr/share/texmf-dist/tex/generic/iftex/ifvtex.sty
Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead.
(/usr/share/texmf-dist/tex/generic/iftex/iftex.sty
Package: iftex 2024/12/12 v1.0g TeX engine tests
))
\Gm@cnth=\count279
\Gm@cntv=\count280
\c@Gm@tempcnt=\count281
\Gm@bindingoffset=\dimen149
\Gm@wd@mp=\dimen150
\Gm@odd@mp=\dimen151
\Gm@even@mp=\dimen152
\Gm@layoutwidth=\dimen153
\Gm@layoutheight=\dimen154
\Gm@layouthoffset=\dimen155
\Gm@layoutvoffset=\dimen156
\Gm@dimlist=\toks18
)
(/usr/share/texmf-dist/tex/latex/fontspec/fontspec.sty
(/usr/share/texmf-dist/tex/latex/l3packages/xparse/xparse.sty
(/usr/share/texmf-dist/tex/latex/l3kernel/expl3.sty
Package: expl3 2026-01-19 L3 programming layer (loader)
(/usr/share/texmf-dist/tex/latex/l3backend/l3backend-xetex.def
File: l3backend-xetex.def 2025-10-09 L3 backend support: XeTeX
\g__graphics_track_int=\count282
\g__pdfannot_backend_int=\count283
\g__pdfannot_backend_link_int=\count284
))
Package: xparse 2025-10-09 L3 Experimental document command parser
)
Package: fontspec 2025/09/29 v2.9g Font selection for XeLaTeX and LuaLaTeX
(/usr/share/texmf-dist/tex/latex/fontspec/fontspec-xetex.sty
Package: fontspec-xetex 2025/09/29 v2.9g Font selection for XeLaTeX and LuaLaTe
X
\l__fontspec_script_int=\count285
\l__fontspec_language_int=\count286
\l__fontspec_strnum_int=\count287
\l__fontspec_tmp_int=\count288
\l__fontspec_tmpa_int=\count289
\l__fontspec_tmpb_int=\count290
\l__fontspec_tmpc_int=\count291
\l__fontspec_em_int=\count292
\l__fontspec_emdef_int=\count293
\l__fontspec_strong_int=\count294
\l__fontspec_strongdef_int=\count295
\l__fontspec_tmpa_dim=\dimen157
\l__fontspec_tmpb_dim=\dimen158
\l__fontspec_tmpc_dim=\dimen159
(/usr/share/texmf-dist/tex/latex/base/fontenc.sty
Package: fontenc 2025/07/18 v2.1d Standard LaTeX package
)
(/usr/share/texmf-dist/tex/latex/fontspec/fontspec.cfg)))
Package fontspec Info:
(fontspec) Hurmit Nerd Font Mono scale = 0.7.
Package fontspec Info:
(fontspec) Hurmit Nerd Font Mono scale = 0.7.
Package fontspec Info:
(fontspec) Hurmit Nerd Font Mono/B scale = 0.7.
Package fontspec Info:
(fontspec) Hurmit Nerd Font Mono/I scale = 0.7.
Package fontspec Info:
(fontspec) Hurmit Nerd Font Mono/BI scale = 0.7.
Package fontspec Info:
(fontspec) Font family 'HurmitNerdFontMono(0)' created for font
(fontspec) 'Hurmit Nerd Font Mono' with options
(fontspec) [WordSpace={1,0,0},HyphenChar=None,PunctuationSpace=Word
Space,Scale=0.7].
(fontspec)
(fontspec) This font family consists of the following NFSS
(fontspec) series/shapes:
(fontspec)
(fontspec) - 'normal' (m/n) with NFSS spec.: <->s*[0.7]"Hurmit
(fontspec) Nerd Font Mono/OT:script=DFLT;language=dflt;"
(fontspec) - 'bold' (b/n) with NFSS spec.: <->s*[0.7]"Hurmit Nerd
(fontspec) Font Mono/B/OT:script=DFLT;language=dflt;"
(fontspec) - 'italic' (m/it) with NFSS spec.: <->s*[0.7]"Hurmit
(fontspec) Nerd Font Mono/I/OT:script=DFLT;language=dflt;"
(fontspec) - 'bold italic' (b/it) with NFSS spec.:
(fontspec) <->s*[0.7]"Hurmit Nerd Font
(fontspec) Mono/BI/OT:script=DFLT;language=dflt;"
No file main.aux.
\openout1 = `main.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for TU/lmr/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
*geometry* driver: auto-detecting
*geometry* detected driver: xetex
*geometry* verbose mode - [ preamble ] result:
* driver: xetex
* paper: a4paper
* layout: <same size as paper>
* layoutoffset:(h,v)=(0.0pt,0.0pt)
* modes:
* h-part:(L,W,R)=(72.26999pt, 452.9679pt, 72.26999pt)
* v-part:(T,H,B)=(72.26999pt, 700.50687pt, 72.26999pt)
* \paperwidth=597.50787pt
* \paperheight=845.04684pt
* \textwidth=452.9679pt
* \textheight=700.50687pt
* \oddsidemargin=0.0pt
* \evensidemargin=0.0pt
* \topmargin=-37.0pt
* \headheight=12.0pt
* \headsep=25.0pt
* \topskip=10.0pt
* \footskip=30.0pt
* \marginparwidth=57.0pt
* \marginparsep=11.0pt
* \columnsep=10.0pt
* \skip\footins=9.0pt plus 4.0pt minus 2.0pt
* \hoffset=0.0pt
* \voffset=0.0pt
* \mag=1000
* \@twocolumnfalse
* \@twosidefalse
* \@mparswitchfalse
* \@reversemarginfalse
* (1in=72.27pt=25.4mm, 1cm=28.453pt)
Package fontspec Info:
(fontspec) Adjusting the maths setup (use [no-math] to avoid
(fontspec) this).
\symlegacymaths=\mathgroup4
LaTeX Font Info: Overwriting symbol font `legacymaths' in version `bold'
(Font) OT1/cmr/m/n --> OT1/cmr/bx/n on input line 11.
LaTeX Font Info: Redeclaring math accent \acute on input line 11.
LaTeX Font Info: Redeclaring math accent \grave on input line 11.
LaTeX Font Info: Redeclaring math accent \ddot on input line 11.
LaTeX Font Info: Redeclaring math accent \tilde on input line 11.
LaTeX Font Info: Redeclaring math accent \bar on input line 11.
LaTeX Font Info: Redeclaring math accent \breve on input line 11.
LaTeX Font Info: Redeclaring math accent \check on input line 11.
LaTeX Font Info: Redeclaring math accent \hat on input line 11.
LaTeX Font Info: Redeclaring math accent \dot on input line 11.
LaTeX Font Info: Redeclaring math accent \mathring on input line 11.
LaTeX Font Info: Redeclaring math symbol \colon on input line 11.
LaTeX Font Info: Redeclaring math symbol \Gamma on input line 11.
LaTeX Font Info: Redeclaring math symbol \Delta on input line 11.
LaTeX Font Info: Redeclaring math symbol \Theta on input line 11.
LaTeX Font Info: Redeclaring math symbol \Lambda on input line 11.
LaTeX Font Info: Redeclaring math symbol \Xi on input line 11.
LaTeX Font Info: Redeclaring math symbol \Pi on input line 11.
LaTeX Font Info: Redeclaring math symbol \Sigma on input line 11.
LaTeX Font Info: Redeclaring math symbol \Upsilon on input line 11.
LaTeX Font Info: Redeclaring math symbol \Phi on input line 11.
LaTeX Font Info: Redeclaring math symbol \Psi on input line 11.
LaTeX Font Info: Redeclaring math symbol \Omega on input line 11.
LaTeX Font Info: Redeclaring math symbol \mathdollar on input line 11.
LaTeX Font Info: Redeclaring symbol font `operators' on input line 11.
LaTeX Font Info: Encoding `OT1' has changed to `TU' for symbol font
(Font) `operators' in the math version `normal' on input line 11.
LaTeX Font Info: Overwriting symbol font `operators' in version `normal'
(Font) OT1/cmr/m/n --> TU/lmr/m/n on input line 11.
LaTeX Font Info: Encoding `OT1' has changed to `TU' for symbol font
(Font) `operators' in the math version `bold' on input line 11.
LaTeX Font Info: Overwriting symbol font `operators' in version `bold'
(Font) OT1/cmr/bx/n --> TU/lmr/m/n on input line 11.
LaTeX Font Info: Overwriting symbol font `operators' in version `normal'
(Font) TU/lmr/m/n --> TU/lmr/m/n on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathit' in version `normal'
(Font) OT1/cmr/m/it --> TU/lmr/m/it on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathbf' in version `normal'
(Font) OT1/cmr/bx/n --> TU/lmr/b/n on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathsf' in version `normal'
(Font) OT1/cmss/m/n --> TU/lmss/m/n on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathtt' in version `normal'
(Font) OT1/cmtt/m/n --> TU/HurmitNerdFontMono(0)/m/n on input
line 11.
LaTeX Font Info: Overwriting symbol font `operators' in version `bold'
(Font) TU/lmr/m/n --> TU/lmr/b/n on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathit' in version `bold'
(Font) OT1/cmr/bx/it --> TU/lmr/b/it on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathsf' in version `bold'
(Font) OT1/cmss/bx/n --> TU/lmss/b/n on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathtt' in version `bold'
(Font) OT1/cmtt/m/n --> TU/HurmitNerdFontMono(0)/b/n on input
line 11.
LaTeX Font Info: External font `cmex10' loaded for size
(Font) <7> on input line 14.
LaTeX Font Info: External font `cmex10' loaded for size
(Font) <5> on input line 14.
LaTeX Font Info: Font shape `TU/HurmitNerdFontMono(0)/m/n' will be
(Font) scaled to size 6.99997pt on input line 22.
LaTeX Warning: Float too large for page by 44.293pt on input line 93.
[1
] [2] (./main.aux)
***********
LaTeX2e <2025-11-01>
L3 programming layer <2026-01-19>
***********
)
Here is how much of TeX's memory you used:
3526 strings out of 470191
106539 string characters out of 5479698
562689 words of memory out of 5000000
32135 multiletter control sequences out of 15000+600000
627857 words of font info for 57 fonts, out of 8000000 for 9000
14 hyphenation exceptions out of 8191
73i,9n,93p,432b,328s stack positions out of 10000i,1000n,20000p,200000b,200000s
Output written on main.pdf (2 pages).

Binary file not shown.

View File

@@ -41,7 +41,10 @@
\languageline{statement}{\texttt{DONICVM} \textbf{id} \texttt{VT} \textit{expression} \texttt{VSQVE} \textit{expression} \texttt{FACE} \textit{scope}} \\
\languageline{statement}{\texttt{REDI(} \textit{optional-expressions} \texttt{)}} \\
\languageline{statement}{\texttt{ERVMPE}} \\
\languageline{statement}{\texttt{CONTINVA}} \\ \hline
\languageline{statement}{\texttt{CONTINVA}} \\
\languageline{statement}{\textit{try-statement}} \\ \hline
\languageline{try-statement}{\texttt{TEMPTA} \textit{scope} \texttt{CAPE} \textbf{id} \textit{scope}} \\ \hline
\languageline{if-statement}{\texttt{SI} \textit{expression} \texttt{TVNC} \textit{scope}} \\
\languageline{if-statement}{\texttt{SI} \textit{expression} \texttt{TVNC} \textit{scope} \textit{optional-newline} \textit{else-statement}} \\ \hline
@@ -59,6 +62,7 @@
\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} \texttt{[} \textit{expression} \texttt{VSQVE} \textit{expression} \texttt{]} \textnormal{\small\ (inclusive slice)}} \\
\languageline{expression}{\textit{expression} \textbf{binop} \textit{expression}} \\
\languageline{expression}{\textbf{unop} \textit{expression}} \\ \hline
\languageline{literal}{\textbf{string}} \\
@@ -66,7 +70,7 @@
\languageline{literal}{\textbf{numeral}} \\
\languageline{literal}{\textbf{bool}} \\
\languageline{literal}{\texttt{[} \textit{optional-expressions} \texttt{]}} \\
\languageline{literal}{\texttt{[} \textit{expression} \texttt{VSQVE} \textit{expression} \texttt{]}} \\
\languageline{literal}{\texttt{[} \textit{expression} \texttt{VSQVE} \textit{expression} \texttt{]} \textnormal{\small\ (inclusive on both ends)}} \\
\languageline{literal}{\texttt{TABVLA} \texttt{\{} \textit{optional-dict-items} \texttt{\}}} \\ \hline
\languageline{optional-dict-items}{\textit{dict-items}} \\
@@ -94,10 +98,10 @@
\newpage
\begin{itemize}
\item \textbf{newline}: \\ Newlines are combined, so a single newline is the same as multiple.
\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{module-name}: \\ Modules are flags given to the interpreter/compiler, to let it know you want to be using certain rules, functions, or features. Available modules: \texttt{FORS} (randomness), \texttt{FRACTIO} (fractions), \texttt{MAGNVM} (large integers), \texttt{SCRIPTA} (file I/O: \texttt{LEGE}, \texttt{SCRIBE}, \texttt{ADIVNGE}), \texttt{SVBNVLLA} (negative literals).
\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 \texttt{"} or \texttt{'} characters. Single-quoted strings are always literal.
\item \textbf{string}: \\ Any text encased in \texttt{"} or \texttt{'} characters. Single-quoted strings are always literal. Strings support 1-based indexing (\texttt{string[I]}) and inclusive slicing (\texttt{string[I VSQVE III]}), returning single-character strings and substrings respectively.
\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.

BIN
snippets/aeternvm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,2 @@
DESIGNA x VT [X, XX, XXX, XL, L]
DICE(x[II VSQVE IV])

BIN
snippets/array_slice.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1 @@
DESIGNA a, b, c VT [I, II, III]

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,2 @@
DEFINI pair (a, b) VT { REDI (a, b) }
DESIGNA x, y VT INVOCA pair (III, VII)

BIN
snippets/destructure_fn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,3 @@
DICE(d["nomen"])
DESIGNA d["aetas"] VT XXVI
DESIGNA d["novus"] VT I

BIN
snippets/dict_access.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1 @@
DESIGNA d VT TABVLA {"nomen" VT "Marcus", "aetas" VT XXV}

BIN
snippets/dict_create.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

3
snippets/dict_per.cent Normal file
View File

@@ -0,0 +1,3 @@
PER k IN d FACE {
DICE(k)
}

BIN
snippets/dict_per.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,4 @@
DESIGNA nomen VT "Marcus"
DICE("Salve, {nomen}!")
DICE("Sum: {III + IV}")
DICE("{nomen} has {V} cats")

BIN
snippets/string_interp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -9,8 +9,8 @@ contexts:
main:
- include: comments
- include: strings
- include: fractions
- include: keywords
- include: fractions
- include: numerals
- include: constants
- include: builtins
@@ -70,15 +70,15 @@ contexts:
scope: constant.language.centvrion
builtins:
- match: '\b(AVDI_NVMERVS|AVDI|CLAVES|DECIMATIO|DICE|EVERRO|FORTIS_NVMERVS|FORTIS_ELECTIONIS|LONGITVDO|SEMEN|SENATVS)\b'
- match: '\b(ADIVNGE|AVDI_NVMERVS|AVDI|CLAVES|DECIMATIO|DICE|EVERRO|FORTIS_NVMERVS|FORTIS_ELECTIONIS|LEGE|LONGITVDO|ORDINA|SCRIBE|SEMEN|SENATVS)\b'
scope: support.function.builtin.centvrion
modules:
- match: '\b(FORS|FRACTIO|MAGNVM|SVBNVLLA)\b'
- match: '\b(FORS|FRACTIO|MAGNVM|SCRIPTA|SVBNVLLA)\b'
scope: support.class.module.centvrion
keywords:
- 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'
- match: '\b(AETERNVM|ALVID|AVGE|AVT|CAPE|CONTINVA|DEFINI|DESIGNA|DISPAR|DONICVM|DVM|ERVMPE|EST|ET|FACE|FVNCTIO|INVOCA|IN|MINVE|MINVS|NON|PER|PLVS|REDI|RELIQVVM|SI|TABVLA|TEMPTA|TVNC|VSQVE|VT|CVM)\b'
scope: keyword.control.centvrion
operators:

593
tests.py
View File

@@ -2,6 +2,7 @@ import os
import random
import subprocess
import tempfile
import time
import unittest
from io import StringIO
from unittest.mock import patch
@@ -10,12 +11,12 @@ from parameterized import parameterized
from fractions import Fraction
from centvrion.ast_nodes import (
ArrayIndex, Bool, BinOp, BuiltIn, DataArray, DataDict, DataRangeArray,
ArrayIndex, ArraySlice, 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,
String, TemptaStatement, 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
@@ -283,13 +284,13 @@ assignment_tests = [
Designa(ID("x"), BinOp(ID("x"), BinOp(Numeral("II"), Numeral("III"), "SYMBOL_PLUS"), "SYMBOL_PLUS")),
ExpressionStatement(ID("x"))]),
ValInt(6)),
# AVGE inside a loop (DONICVM range is exclusive of upper bound: I VSQVE III = [1, 2])
# AVGE inside a loop (DONICVM range is inclusive: I VSQVE III = [1, 2, 3])
("DESIGNA s VT NVLLVS\nDONICVM i VT I VSQVE III FACE {\ns AVGE i\n}\ns",
Program([], [Designa(ID("s"), Nullus()),
PerStatement(DataRangeArray(Numeral("I"), Numeral("III")), ID("i"),
[Designa(ID("s"), BinOp(ID("s"), ID("i"), "SYMBOL_PLUS"))]),
ExpressionStatement(ID("s"))]),
ValInt(3)),
ValInt(6)),
]
class TestAssignment(unittest.TestCase):
@@ -469,7 +470,7 @@ control_tests = [
# DONICVM range loop
("DONICVM i VT I VSQVE V FACE { DICE(i) }",
Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("V")), ID("i"), [ExpressionStatement(BuiltIn("DICE", [ID("i")]))])]),
ValStr("IV"), "I\nII\nIII\nIV\n"),
ValStr("V"), "I\nII\nIII\nIV\nV\n"),
]
class TestControl(unittest.TestCase):
@@ -567,6 +568,44 @@ builtin_tests = [
("SENATVS([FALSITAS, FALSITAS, VERITAS])", Program([], [ExpressionStatement(BuiltIn("SENATVS", [DataArray([Bool(False), Bool(False), Bool(True)])]))]), ValBool(False)),
# SENATVS: array input, empty → FALSITAS
("SENATVS([])", Program([], [ExpressionStatement(BuiltIn("SENATVS", [DataArray([])]))]), ValBool(False)),
# ORDINA: sort integers
("ORDINA([III, I, II])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("III"), Numeral("I"), Numeral("II")])]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# ORDINA: sort strings
('ORDINA(["c", "a", "b"])', Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([String("c"), String("a"), String("b")])]))]), ValList([ValStr("a"), ValStr("b"), ValStr("c")])),
# ORDINA: empty list
("ORDINA([])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([])]))]), ValList([])),
# ORDINA: single element
("ORDINA([V])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("V")])]))]), ValList([ValInt(5)])),
# ORDINA: already sorted
("ORDINA([I, II, III])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# ORDINA: duplicates
("ORDINA([II, I, II])", Program([], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("II"), Numeral("I"), Numeral("II")])]))]), ValList([ValInt(1), ValInt(2), ValInt(2)])),
# ORDINA: negative numbers
("CVM SVBNVLLA\nORDINA([-II, III, -I])", Program([ModuleCall("SVBNVLLA")], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([UnaryMinus(Numeral("II")), Numeral("III"), UnaryMinus(Numeral("I"))])]))]), ValList([ValInt(-2), ValInt(-1), ValInt(3)])),
# ORDINA: fractions only
("CVM FRACTIO\nORDINA([IIIS, S, IIS])", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Fractio("IIIS"), Fractio("S"), Fractio("IIS")])]))]), ValList([ValFrac(Fraction(1, 2)), ValFrac(Fraction(5, 2)), ValFrac(Fraction(7, 2))])),
# ORDINA: mixed integers and fractions
("CVM FRACTIO\nORDINA([III, S, II])", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("ORDINA", [DataArray([Numeral("III"), Fractio("S"), Numeral("II")])]))]), ValList([ValFrac(Fraction(1, 2)), ValInt(2), ValInt(3)])),
# ORDINA: array passed via variable
("DESIGNA x VT [III, I, II]\nORDINA(x)", Program([], [Designa(ID("x"), DataArray([Numeral("III"), Numeral("I"), Numeral("II")])), ExpressionStatement(BuiltIn("ORDINA", [ID("x")]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
# TYPVS: integer
("TYPVS(V)", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Numeral("V")]))]), ValStr("NVMERVS")),
# TYPVS: string
('TYPVS("hello")', Program([], [ExpressionStatement(BuiltIn("TYPVS", [String("hello")]))]), ValStr("LITTERA")),
# TYPVS: boolean
("TYPVS(VERITAS)", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Bool(True)]))]), ValStr("VERAX")),
# TYPVS: list
("TYPVS([I, II])", Program([], [ExpressionStatement(BuiltIn("TYPVS", [DataArray([Numeral("I"), Numeral("II")])]))]), ValStr("CATALOGVS")),
# TYPVS: empty list
("TYPVS([])", Program([], [ExpressionStatement(BuiltIn("TYPVS", [DataArray([])]))]), ValStr("CATALOGVS")),
# TYPVS: fraction
("CVM FRACTIO\nTYPVS(S)", Program([ModuleCall("FRACTIO")], [ExpressionStatement(BuiltIn("TYPVS", [Fractio("S")]))]), ValStr("FRACTIO")),
# TYPVS: dict
("TYPVS(TABVLA {})", Program([], [ExpressionStatement(BuiltIn("TYPVS", [DataDict([])]))]), ValStr("TABVLA")),
# TYPVS: function
("TYPVS(FVNCTIO () VT { REDI(I) })", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Fvnctio([], [Redi([Numeral("I")])])]))]), ValStr("FVNCTIO")),
# TYPVS: null
("TYPVS(NVLLVS)", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Nullus()]))]), ValStr("NVLLVS")),
]
class TestBuiltins(unittest.TestCase):
@@ -600,6 +639,10 @@ error_tests = [
("I * \"hello\"", CentvrionError), # multiplication with string
("\"hello\" MINVS \"world\"", CentvrionError), # comparison with strings
("I[I]", CentvrionError), # indexing a non-array
('"SALVTE"[VII]', CentvrionError), # string index out of range
('"SALVTE"[NVLLVS]', CentvrionError), # string index with non-integer
('"SALVTE"[II VSQVE VII]', CentvrionError), # string slice out of range
('"SALVTE"[III VSQVE II]', CentvrionError), # string slice from > to
("DESIGNA x VT I\nDESIGNA x[I] VT II", CentvrionError), # index-assign to non-array
("SEMEN(I)", CentvrionError), # requires FORS module
('CVM FORS\nSEMEN("abc")', CentvrionError), # SEMEN requires integer seed
@@ -610,9 +653,15 @@ error_tests = [
("DECIMATIO([I, II, III])", CentvrionError), # FORS required for DECIMATIO
("CVM FORS\nDECIMATIO(I)", CentvrionError), # DECIMATIO requires an array
("LONGITVDO(I)", CentvrionError), # LONGITVDO on non-array
("ORDINA(I)", CentvrionError), # ORDINA on non-array
('ORDINA([I, "a"])', CentvrionError), # ORDINA mixed types
("DESIGNA x VT I\nORDINA(x)", CentvrionError), # ORDINA on id (non-array)
("SENATVS(I)", CentvrionError), # SENATVS requires booleans
("SENATVS(VERITAS, I)", CentvrionError), # SENATVS mixed types
("SENATVS([I, II, III])", CentvrionError), # SENATVS array of non-bools
('LEGE("x.txt")', CentvrionError), # SCRIPTA required for LEGE
('SCRIBE("x.txt", "hi")', CentvrionError), # SCRIPTA required for SCRIBE
('ADIVNGE("x.txt", "hi")', CentvrionError), # SCRIPTA required for ADIVNGE
("DESIGNA x VT I\nINVOCA x ()", CentvrionError), # invoking a non-function
("SI I TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: int
("IIIS", CentvrionError), # fraction without FRACTIO module
@@ -628,6 +677,16 @@ 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
("[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
("[I, II, III][III VSQVE I]", CentvrionError), # slice from > to
("CVM SVBNVLLA\n[I, II, III][-I VSQVE II]", CentvrionError), # slice with negative lower bound
("CVM SVBNVLLA\n[I, II, III][I VSQVE -I]", CentvrionError), # slice with negative upper bound
("CVM FRACTIO\n[I, II, III][IIIS VSQVE III]", CentvrionError), # slice with fractional lower bound
("CVM FRACTIO\n[I, II, III][I VSQVE IIIS]", CentvrionError), # slice with fractional upper bound
("CVM FRACTIO\n[I, II, III][I / II VSQVE III]", CentvrionError), # slice with division-fraction lower bound
("TEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nDESIGNA y VT I / NVLLVS\n}", CentvrionError), # uncaught error in catch block propagates
]
class TestErrors(unittest.TestCase):
@@ -1146,10 +1205,10 @@ class TestFunctionEdge(unittest.TestCase):
# --- Loop edge cases ---
loop_edge_tests = [
# range(3, 3) is empty — body never runs, program returns ValNul
# [III VSQVE III] = [3] — single iteration
("DONICVM i VT III VSQVE III FACE { DICE(i) }",
Program([], [PerStatement(DataRangeArray(Numeral("III"), Numeral("III")), ID("i"), [ExpressionStatement(BuiltIn("DICE", [ID("i")]))])]),
ValNul(), ""),
ValStr("III"), "III\n"),
# empty array — body never runs
("PER i IN [] FACE { DICE(i) }",
Program([], [PerStatement(DataArray([]), ID("i"), [ExpressionStatement(BuiltIn("DICE", [ID("i")]))])]),
@@ -1233,7 +1292,7 @@ loop_edge_tests = [
ExpressionStatement(ID("cnt")),
]),
ValInt(3), ""),
# DONICVM with CONTINVA: skip value III, count remaining
# DONICVM with CONTINVA: skip value III, count remaining (I VSQVE IV = [1,2,3,4], skip 3 → 3 increments)
("DESIGNA cnt VT I\nDONICVM i VT I VSQVE IV FACE {\nSI i EST III TVNC { CONTINVA }\nDESIGNA cnt VT cnt + I\n}\ncnt",
Program([], [
Designa(ID("cnt"), Numeral("I")),
@@ -1245,7 +1304,7 @@ loop_edge_tests = [
),
ExpressionStatement(ID("cnt")),
]),
ValInt(3)),
ValInt(4)),
# DVM condition true from start — body never runs
("DESIGNA x VT I\nDVM VERITAS FACE {\nDESIGNA x VT x + I\n}\nx",
Program([], [
@@ -1254,10 +1313,21 @@ loop_edge_tests = [
ExpressionStatement(ID("x")),
]),
ValInt(1), ""),
# single iteration: [I VSQVE II] = [1]
# two iterations: [I VSQVE II] = [1, 2]
("DONICVM i VT I VSQVE II FACE { DICE(i) }",
Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("II")), ID("i"), [ExpressionStatement(BuiltIn("DICE", [ID("i")]))])]),
ValStr("II"), "I\nII\n"),
# single iteration: [I VSQVE I] = [1]
("DONICVM i VT I VSQVE I FACE { DICE(i) }",
Program([], [PerStatement(DataRangeArray(Numeral("I"), Numeral("I")), ID("i"), [ExpressionStatement(BuiltIn("DICE", [ID("i")]))])]),
ValStr("I"), "I\n"),
# empty range: [V VSQVE I] = []
("DESIGNA x VT NVLLVS\nDONICVM i VT V VSQVE I FACE { DESIGNA x VT x + i }\nx",
Program([], [Designa(ID("x"), Nullus()),
PerStatement(DataRangeArray(Numeral("V"), Numeral("I")), ID("i"),
[Designa(ID("x"), BinOp(ID("x"), ID("i"), "SYMBOL_PLUS"))]),
ExpressionStatement(ID("x"))]),
ValNul(), ""),
]
class TestLoopEdge(unittest.TestCase):
@@ -1382,7 +1452,7 @@ array_index_tests = [
Program([], [Designa(ID("a"), DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX")])), ExpressionStatement(ArrayIndex(ID("a"), Numeral("II")))]),
ValInt(20)), # second element
# index into range array
("[I VSQVE V][II]", Program([], [ExpressionStatement(ArrayIndex(DataRangeArray(Numeral("I"), Numeral("V")), Numeral("II")))]), ValInt(2)), # second element of [1,2,3,4]
("[I VSQVE V][II]", Program([], [ExpressionStatement(ArrayIndex(DataRangeArray(Numeral("I"), Numeral("V")), Numeral("II")))]), ValInt(2)), # second element of [1,2,3,4,5]
# expression as index
("[I, II, III][I + I]",
Program([], [ExpressionStatement(ArrayIndex(
@@ -1461,6 +1531,145 @@ class TestArrayIndexAssign(unittest.TestCase):
run_test(self, source, nodes, value)
# --- Array slicing ---
array_slice_tests = [
# basic slice from middle
("[X, XX, XXX, XL, L][II VSQVE IV]",
Program([], [ExpressionStatement(ArraySlice(
DataArray([Numeral("X"), Numeral("XX"), Numeral("XXX"), Numeral("XL"), Numeral("L")]),
Numeral("II"), Numeral("IV")))]),
ValList([ValInt(20), ValInt(30), ValInt(40)])),
# slice of length 1
("[I, II, III][II VSQVE II]",
Program([], [ExpressionStatement(ArraySlice(
DataArray([Numeral("I"), Numeral("II"), Numeral("III")]),
Numeral("II"), Numeral("II")))]),
ValList([ValInt(2)])),
# full array slice
("[I, II, III][I VSQVE III]",
Program([], [ExpressionStatement(ArraySlice(
DataArray([Numeral("I"), Numeral("II"), Numeral("III")]),
Numeral("I"), Numeral("III")))]),
ValList([ValInt(1), ValInt(2), ValInt(3)])),
# slice on variable
("DESIGNA a VT [I, II, III, IV, V]\na[II VSQVE IV]",
Program([], [
Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V")])),
ExpressionStatement(ArraySlice(ID("a"), Numeral("II"), Numeral("IV"))),
]),
ValList([ValInt(2), ValInt(3), ValInt(4)])),
# slice then index (chained)
("[I, II, III, IV][I VSQVE III][II]",
Program([], [ExpressionStatement(ArrayIndex(
ArraySlice(
DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV")]),
Numeral("I"), Numeral("III")),
Numeral("II")))]),
ValInt(2)),
# slice on range array
("[I VSQVE X][III VSQVE VII]",
Program([], [ExpressionStatement(ArraySlice(
DataRangeArray(Numeral("I"), Numeral("X")),
Numeral("III"), Numeral("VII")))]),
ValList([ValInt(3), ValInt(4), ValInt(5), ValInt(6), ValInt(7)])),
# expression as slice bounds
("[I, II, III, IV, V][I + I VSQVE II + II]",
Program([], [ExpressionStatement(ArraySlice(
DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V")]),
BinOp(Numeral("I"), Numeral("I"), "SYMBOL_PLUS"),
BinOp(Numeral("II"), Numeral("II"), "SYMBOL_PLUS")))]),
ValList([ValInt(2), ValInt(3), ValInt(4)])),
]
class TestArraySlice(unittest.TestCase):
@parameterized.expand(array_slice_tests)
def test_array_slice(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- String indexing ---
string_index_tests = [
# first character
('"SALVTE"[I]',
Program([], [ExpressionStatement(ArrayIndex(String("SALVTE"), Numeral("I")))]),
ValStr("S")),
# last character
('"SALVTE"[VI]',
Program([], [ExpressionStatement(ArrayIndex(String("SALVTE"), Numeral("VI")))]),
ValStr("E")),
# middle character
('"SALVTE"[III]',
Program([], [ExpressionStatement(ArrayIndex(String("SALVTE"), Numeral("III")))]),
ValStr("L")),
# string index via variable
('DESIGNA s VT "SALVTE"\ns[II]',
Program([], [
Designa(ID("s"), String("SALVTE")),
ExpressionStatement(ArrayIndex(ID("s"), Numeral("II"))),
]),
ValStr("A")),
# expression as index
('"SALVTE"[I + II]',
Program([], [ExpressionStatement(ArrayIndex(
String("SALVTE"),
BinOp(Numeral("I"), Numeral("II"), "SYMBOL_PLUS")))]),
ValStr("L")),
]
class TestStringIndex(unittest.TestCase):
@parameterized.expand(string_index_tests)
def test_string_index(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- String slicing ---
string_slice_tests = [
# substring from middle
('"SALVTE"[II VSQVE IV]',
Program([], [ExpressionStatement(ArraySlice(
String("SALVTE"), Numeral("II"), Numeral("IV")))]),
ValStr("ALV")),
# full string slice
('"SALVTE"[I VSQVE VI]',
Program([], [ExpressionStatement(ArraySlice(
String("SALVTE"), Numeral("I"), Numeral("VI")))]),
ValStr("SALVTE")),
# single-char slice
('"SALVTE"[III VSQVE III]',
Program([], [ExpressionStatement(ArraySlice(
String("SALVTE"), Numeral("III"), Numeral("III")))]),
ValStr("L")),
# slice on variable
('DESIGNA s VT "SALVTE"\ns[II VSQVE IV]',
Program([], [
Designa(ID("s"), String("SALVTE")),
ExpressionStatement(ArraySlice(ID("s"), Numeral("II"), Numeral("IV"))),
]),
ValStr("ALV")),
# chaining: slice then index
('"SALVTE"[I VSQVE III][II]',
Program([], [ExpressionStatement(ArrayIndex(
ArraySlice(String("SALVTE"), Numeral("I"), Numeral("III")),
Numeral("II")))]),
ValStr("A")),
# expression as slice bounds
('"SALVTE"[I + I VSQVE II + II]',
Program([], [ExpressionStatement(ArraySlice(
String("SALVTE"),
BinOp(Numeral("I"), Numeral("I"), "SYMBOL_PLUS"),
BinOp(Numeral("II"), Numeral("II"), "SYMBOL_PLUS")))]),
ValStr("ALV")),
]
class TestStringSlice(unittest.TestCase):
@parameterized.expand(string_slice_tests)
def test_string_slice(self, source, nodes, value):
run_test(self, source, nodes, value)
# --- Comments ---
comment_tests = [
@@ -1551,15 +1760,15 @@ scope_tests = [
]),
ValInt(100)),
# DONICVM: counter holds last range value after loop ends
# [I VSQVE IV] = [1,2,3]; last value assigned by loop is III=3
# [I VSQVE IV] = [1,2,3,4]; last value assigned by loop is IV=4
("DONICVM i VT I VSQVE IV FACE { DESIGNA nop VT I }\ni",
Program([], [
PerStatement(DataRangeArray(Numeral("I"), Numeral("IV")), ID("i"), [Designa(ID("nop"), Numeral("I"))]),
ExpressionStatement(ID("i")),
]),
ValInt(3)),
ValInt(4)),
# DONICVM: reassigning counter inside body doesn't reduce the number of iterations
# range [I VSQVE IV] evaluated once; i reset each time; cnt still increments 3 times → 4
# range [I VSQVE IV] evaluated once; i reset each time; cnt still increments 4 times → 5
("DESIGNA cnt VT I\nDONICVM i VT I VSQVE IV FACE { DESIGNA cnt VT cnt + I\nDESIGNA i VT C }\ncnt",
Program([], [
Designa(ID("cnt"), Numeral("I")),
@@ -1569,7 +1778,7 @@ scope_tests = [
]),
ExpressionStatement(ID("cnt")),
]),
ValInt(4)),
ValInt(5)),
# DONICVM: ERVMPE exits loop early; counter persists at break value
("DONICVM i VT I VSQVE X FACE {\nSI i EST III TVNC { ERVMPE }\n}\ni",
Program([], [
@@ -2199,5 +2408,357 @@ class TestFvnctio(unittest.TestCase):
run_test(self, source, nodes, value, output)
# --- DORMI ---
dormi_tests = [
("DORMI(NVLLVS)",
Program([], [ExpressionStatement(BuiltIn("DORMI", [Nullus()]))]),
ValNul()),
]
class TestDormi(unittest.TestCase):
@parameterized.expand(dormi_tests)
def test_dormi(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
def test_dormi_timing_int(self):
source = "DORMI(I)\n"
lexer = Lexer().get_lexer()
tokens = lexer.lex(source)
program = Parser().parse(tokens)
start = time.time()
program.eval()
elapsed = time.time() - start
self.assertAlmostEqual(elapsed, 1.0, delta=0.5)
def test_dormi_timing_int_compiled(self):
source = "DORMI(I)\n"
lexer = Lexer().get_lexer()
tokens = lexer.lex(source)
program = Parser().parse(tokens)
c_source = compile_program(program)
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source)
tmp_c_path = tmp_c.name
with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin:
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path],
check=True, capture_output=True,
)
start = time.time()
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
elapsed = time.time() - start
self.assertEqual(proc.returncode, 0)
self.assertAlmostEqual(elapsed, 1.0, delta=0.5)
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)
def test_dormi_timing_frac(self):
source = "CVM FRACTIO\nDORMI(S)\n"
lexer = Lexer().get_lexer()
tokens = lexer.lex(source)
program = Parser().parse(tokens)
start = time.time()
program.eval()
elapsed = time.time() - start
self.assertAlmostEqual(elapsed, 0.5, delta=0.5)
def test_dormi_timing_frac_compiled(self):
source = "CVM FRACTIO\nDORMI(S)\n"
lexer = Lexer().get_lexer()
tokens = lexer.lex(source)
program = Parser().parse(tokens)
c_source = compile_program(program)
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source)
tmp_c_path = tmp_c.name
with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin:
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path],
check=True, capture_output=True,
)
start = time.time()
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
elapsed = time.time() - start
self.assertEqual(proc.returncode, 0)
self.assertAlmostEqual(elapsed, 0.5, delta=0.5)
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)
class TestScripta(unittest.TestCase):
def _run_scripta(self, source):
lexer = Lexer().get_lexer()
tokens = lexer.lex(source + "\n")
program = Parser().parse(tokens)
# printer round-trip
new_text = program.print()
new_tokens = Lexer().get_lexer().lex(new_text + "\n")
new_nodes = Parser().parse(new_tokens)
self.assertEqual(program, new_nodes, f"Printer test\n{source}\n{new_text}")
return program
def _compile_and_run(self, program):
c_source = compile_program(program)
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source)
tmp_c_path = tmp_c.name
with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_bin:
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path],
check=True, capture_output=True,
)
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
self.assertEqual(proc.returncode, 0, f"Compiler binary exited non-zero:\n{proc.stderr}")
return proc.stdout
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)
def test_scribe_and_lege(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE MVNDE")\nDICE(LEGE("{path}"))'
program = self._run_scripta(source)
captured = StringIO()
with patch("sys.stdout", captured):
program.eval()
self.assertEqual(captured.getvalue(), "SALVE MVNDE\n")
with open(path) as f:
self.assertEqual(f.read(), "SALVE MVNDE")
finally:
if os.path.exists(path):
os.unlink(path)
def test_scribe_and_lege_compiled(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE MVNDE")\nDICE(LEGE("{path}"))'
program = self._run_scripta(source)
output = self._compile_and_run(program)
self.assertEqual(output, "SALVE MVNDE\n")
with open(path) as f:
self.assertEqual(f.read(), "SALVE MVNDE")
finally:
if os.path.exists(path):
os.unlink(path)
def test_adivnge(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE")\nADIVNGE("{path}", " MVNDE")\nDICE(LEGE("{path}"))'
program = self._run_scripta(source)
captured = StringIO()
with patch("sys.stdout", captured):
program.eval()
self.assertEqual(captured.getvalue(), "SALVE MVNDE\n")
finally:
if os.path.exists(path):
os.unlink(path)
def test_adivnge_compiled(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "SALVE")\nADIVNGE("{path}", " MVNDE")\nDICE(LEGE("{path}"))'
program = self._run_scripta(source)
output = self._compile_and_run(program)
self.assertEqual(output, "SALVE MVNDE\n")
finally:
if os.path.exists(path):
os.unlink(path)
def test_scribe_overwrites(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "first")\nSCRIBE("{path}", "second")\nLEGE("{path}")'
program = self._run_scripta(source)
result = program.eval()
self.assertEqual(result, ValStr("second"))
finally:
if os.path.exists(path):
os.unlink(path)
def test_lege_empty_file(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f:
path = f.name
try:
source = f'CVM SCRIPTA\nLEGE("{path}")'
program = self._run_scripta(source)
result = program.eval()
self.assertEqual(result, ValStr(""))
finally:
os.unlink(path)
def test_lege_preexisting_content(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f:
f.write("hello from python")
path = f.name
try:
source = f'CVM SCRIPTA\nLEGE("{path}")'
program = self._run_scripta(source)
result = program.eval()
self.assertEqual(result, ValStr("hello from python"))
finally:
os.unlink(path)
def test_scribe_returns_nullus(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nSCRIBE("{path}", "x")'
program = self._run_scripta(source)
result = program.eval()
self.assertEqual(result, ValNul())
finally:
if os.path.exists(path):
os.unlink(path)
def test_adivnge_returns_nullus(self):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
path = f.name
try:
source = f'CVM SCRIPTA\nADIVNGE("{path}", "x")'
program = self._run_scripta(source)
result = program.eval()
self.assertEqual(result, ValNul())
finally:
if os.path.exists(path):
os.unlink(path)
# --- Tempta/Cape (try/catch) ---
tempta_tests = [
# Try block succeeds — catch not entered
(
"TEMPTA {\nDESIGNA r VT I\n} CAPE e {\nDESIGNA r VT II\n}\nr",
Program([], [
TemptaStatement(
[Designa(ID("r"), Numeral("I"))],
ID("e"),
[Designa(ID("r"), Numeral("II"))],
),
ExpressionStatement(ID("r")),
]),
ValInt(1),
),
# Try block errors — caught by catch
(
"TEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT II\n}\nr",
Program([], [
TemptaStatement(
[Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))],
ID("e"),
[Designa(ID("r"), Numeral("II"))],
),
ExpressionStatement(ID("r")),
]),
ValInt(2),
),
# Error variable contains the error message
(
'DESIGNA e VT NVLLVS\nTEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nNVLLVS\n}\ne',
Program([], [
Designa(ID("e"), Nullus()),
TemptaStatement(
[Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))],
ID("e"),
[ExpressionStatement(Nullus())],
),
ExpressionStatement(ID("e")),
]),
ValStr("Division by zero"),
),
# Nested tempta — inner catches, outer unaffected
(
"DESIGNA r VT NVLLVS\nTEMPTA {\nTEMPTA {\nDESIGNA r VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT I\n}\n} CAPE e {\nDESIGNA r VT II\n}\nr",
Program([], [
Designa(ID("r"), Nullus()),
TemptaStatement(
[TemptaStatement(
[Designa(ID("r"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))],
ID("e"),
[Designa(ID("r"), Numeral("I"))],
)],
ID("e"),
[Designa(ID("r"), Numeral("II"))],
),
ExpressionStatement(ID("r")),
]),
ValInt(1),
),
# REDI inside catch block
(
"DEFINI f () VT {\nTEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nREDI (III)\n}\nREDI (IV)\n}\nINVOCA f ()",
Program([], [
Defini(ID("f"), [], [
TemptaStatement(
[Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))],
ID("e"),
[Redi([Numeral("III")])],
),
Redi([Numeral("IV")]),
]),
ExpressionStatement(Invoca(ID("f"), [])),
]),
ValInt(3),
),
# ERVMPE inside catch block (inside a loop)
(
"DESIGNA r VT NVLLVS\nDVM r EST I FACE {\nTEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nDESIGNA r VT I\nERVMPE\n}\n}\nr",
Program([], [
Designa(ID("r"), Nullus()),
DumStatement(
BinOp(ID("r"), Numeral("I"), "KEYWORD_EST"),
[TemptaStatement(
[Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE"))],
ID("e"),
[Designa(ID("r"), Numeral("I")), Erumpe()],
)],
),
ExpressionStatement(ID("r")),
]),
ValInt(1),
),
# Statement after error in try block is not executed
(
"DESIGNA r VT NVLLVS\nTEMPTA {\nDESIGNA x VT I / NVLLVS\nDESIGNA r VT III\n} CAPE e {\nDESIGNA r VT II\n}\nr",
Program([], [
Designa(ID("r"), Nullus()),
TemptaStatement(
[Designa(ID("x"), BinOp(Numeral("I"), Nullus(), "SYMBOL_DIVIDE")),
Designa(ID("r"), Numeral("III"))],
ID("e"),
[Designa(ID("r"), Numeral("II"))],
),
ExpressionStatement(ID("r")),
]),
ValInt(2),
),
]
class TestTempta(unittest.TestCase):
@parameterized.expand(tempta_tests)
def test_tempta(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
if __name__ == "__main__":
unittest.main()