🐐 Compiler

This commit is contained in:
2026-04-10 13:06:34 +02:00
parent e2688b49ea
commit 4937a95f70
12 changed files with 1280 additions and 36 deletions

34
cent
View File

@@ -2,15 +2,20 @@
""" """
Usage: Usage:
cent (-h|--help) cent (-h|--help)
cent (-i|-c) FILE cent -i FILE
cent -c [--keep-c] FILE
Options: Options:
-h --help Print this help screen -h --help Print this help screen
-i Run the interpreter -i Run the interpreter
-c Run the compiler -c Run the compiler
--keep-c Keep the generated C file alongside the binary
FILE The file to compile/interpret FILE The file to compile/interpret
""" """
import os
import subprocess
import sys import sys
import tempfile
from docopt import docopt from docopt import docopt
from rply.errors import LexingError from rply.errors import LexingError
@@ -19,6 +24,7 @@ from centvrion.errors import CentvrionError
from centvrion.lexer import Lexer from centvrion.lexer import Lexer
from centvrion.parser import Parser from centvrion.parser import Parser
from centvrion.ast_nodes import Program from centvrion.ast_nodes import Program
from centvrion.compiler.emitter import compile_program
def main(): def main():
args = docopt(__doc__) args = docopt(__doc__)
@@ -44,7 +50,31 @@ def main():
except CentvrionError as e: except CentvrionError as e:
sys.exit(f"CENTVRION error: {e}") sys.exit(f"CENTVRION error: {e}")
else: else:
raise Exception("Compiler not implemented") c_source = compile_program(program)
runtime_c = os.path.join(
os.path.dirname(__file__),
"centvrion", "compiler", "runtime", "cent_runtime.c"
)
out_path = os.path.splitext(file_path)[0]
if args["--keep-c"]:
tmp_path = out_path + ".c"
with open(tmp_path, "w") as f:
f.write(c_source)
subprocess.run(
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path],
check=True,
)
else:
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp:
tmp.write(c_source)
tmp_path = tmp.name
try:
subprocess.run(
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path],
check=True,
)
finally:
os.unlink(tmp_path)
else: else:
raise Exception("Output not of type 'Program'", type(program)) raise Exception("Output not of type 'Program'", type(program))

View File

@@ -622,7 +622,9 @@ class UnaryNot(Node):
def _eval(self, vtable): def _eval(self, vtable):
vtable, val = self.expr.eval(vtable) vtable, val = self.expr.eval(vtable)
return vtable, ValBool(not bool(val)) if not isinstance(val, ValBool):
raise CentvrionError("NON requires a boolean")
return vtable, ValBool(not val.value())
class ArrayIndex(Node): class ArrayIndex(Node):
@@ -730,8 +732,6 @@ class DumStatement(Node):
if vtable["#return"] is not None: if vtable["#return"] is not None:
break break
vtable, cond = self.test.eval(vtable) vtable, cond = self.test.eval(vtable)
if not isinstance(cond, ValBool):
raise CentvrionError("DVM condition must be a boolean")
return vtable, last_val return vtable, last_val

View File

@@ -0,0 +1 @@
from centvrion.compiler.emitter import compile_program

View File

@@ -0,0 +1,17 @@
class EmitContext:
def __init__(self):
self._tmp_counter = 0
self.current_function = None
self.modules = set()
# c_func_name → [param_names]; populated by emitter pre-pass
self.functions = {}
# source-level name / alias → c_func_name; populated by emitter pre-pass
self.func_resolve = {}
def fresh_tmp(self):
name = f"_t{self._tmp_counter}"
self._tmp_counter += 1
return name
def has_module(self, name):
return name in self.modules

View File

@@ -0,0 +1,225 @@
from centvrion.errors import CentvrionError
from centvrion.ast_nodes import (
String, Numeral, Fractio, Bool, Nullus, ID,
BinOp, UnaryMinus, UnaryNot,
ArrayIndex, DataArray, DataRangeArray,
BuiltIn, Invoca,
num_to_int, frac_to_fraction,
)
_BINOP_FN = {
"SYMBOL_PLUS": "cent_add",
"SYMBOL_MINUS": "cent_sub",
"SYMBOL_TIMES": "cent_mul",
"SYMBOL_DIVIDE": "cent_div",
"SYMBOL_AMPERSAND": "cent_concat",
"KEYWORD_EST": "cent_eq",
"KEYWORD_MINVS": "cent_lt",
"KEYWORD_PLVS": "cent_gt",
"KEYWORD_ET": "cent_and",
"KEYWORD_AVT": "cent_or",
}
def _escape(s):
return (s
.replace("\\", "\\\\")
.replace('"', '\\"')
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t"))
def emit_expr(node, ctx):
"""
Emit C code for a CENTVRION expression node.
Returns (lines, result_var):
lines — list of C statements that compute the expression
result_var — name of the CentValue variable holding the result
"""
if isinstance(node, Numeral):
tmp = ctx.fresh_tmp()
magnvm = "MAGNVM" in ctx.modules
svbnvlla = "SVBNVLLA" in ctx.modules
n = num_to_int(node.value, magnvm, svbnvlla)
return [f"CentValue {tmp} = cent_int({n}L);"], tmp
if isinstance(node, String):
tmp = ctx.fresh_tmp()
return [f'CentValue {tmp} = cent_str("{_escape(node.value)}");'], tmp
if isinstance(node, Bool):
tmp = ctx.fresh_tmp()
v = "1" if node.value else "0"
return [f"CentValue {tmp} = cent_bool({v});"], tmp
if isinstance(node, Nullus):
tmp = ctx.fresh_tmp()
return [f"CentValue {tmp} = cent_null();"], tmp
if isinstance(node, Fractio):
if not ctx.has_module("FRACTIO"):
raise CentvrionError("Cannot use fraction literals without 'FRACTIO' module")
tmp = ctx.fresh_tmp()
magnvm = "MAGNVM" in ctx.modules
svbnvlla = "SVBNVLLA" in ctx.modules
frac = frac_to_fraction(node.value, magnvm, svbnvlla)
return [f"CentValue {tmp} = cent_frac({frac.numerator}L, {frac.denominator}L);"], tmp
if isinstance(node, ID):
tmp = ctx.fresh_tmp()
return [f'CentValue {tmp} = cent_scope_get(&_scope, "{node.name}");'], tmp
if isinstance(node, BinOp):
l_lines, l_var = emit_expr(node.left, ctx)
r_lines, r_var = emit_expr(node.right, ctx)
tmp = ctx.fresh_tmp()
if node.op == "SYMBOL_DIVIDE" and ctx.has_module("FRACTIO"):
fn = "cent_div_frac"
else:
fn = _BINOP_FN[node.op]
return l_lines + r_lines + [f"CentValue {tmp} = {fn}({l_var}, {r_var});"], tmp
if isinstance(node, UnaryMinus):
inner_lines, inner_var = emit_expr(node.expr, ctx)
tmp = ctx.fresh_tmp()
return inner_lines + [f"CentValue {tmp} = cent_int(-{inner_var}.ival);"], tmp
if isinstance(node, UnaryNot):
inner_lines, inner_var = emit_expr(node.expr, ctx)
tmp = ctx.fresh_tmp()
return inner_lines + [f"CentValue {tmp} = cent_bool(!cent_truthy({inner_var}));"], tmp
if isinstance(node, ArrayIndex):
arr_lines, arr_var = emit_expr(node.array, ctx)
idx_lines, idx_var = emit_expr(node.index, ctx)
tmp = ctx.fresh_tmp()
return arr_lines + idx_lines + [f"CentValue {tmp} = cent_list_index({arr_var}, {idx_var});"], tmp
if isinstance(node, DataArray):
lines = []
tmp = ctx.fresh_tmp()
lines.append(f"CentValue {tmp} = cent_list_new({len(node.content)});")
for item in node.content:
item_lines, item_var = emit_expr(item, ctx)
lines.extend(item_lines)
lines.append(f"cent_list_push(&{tmp}, {item_var});")
return lines, tmp
if isinstance(node, DataRangeArray):
lo_lines, lo_var = emit_expr(node.from_value, 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)"
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" cent_list_push(&{tmp}, cent_int({i_var}));",
"}",
]
return lines, tmp
if isinstance(node, BuiltIn):
return _emit_builtin(node, ctx)
if isinstance(node, Invoca):
return _emit_invoca(node, ctx)
raise NotImplementedError(type(node).__name__)
def _emit_builtin(node, ctx):
lines = []
param_vars = []
for p in node.parameters:
p_lines, p_var = emit_expr(p, ctx)
lines.extend(p_lines)
param_vars.append(p_var)
tmp = ctx.fresh_tmp()
match node.builtin:
case "DICE":
if not param_vars:
lines.append('cent_dice(cent_str(""));')
lines.append(f'CentValue {tmp} = cent_str("");')
elif len(param_vars) == 1:
lines.append(f"cent_dice({param_vars[0]});")
lines.append(f"CentValue {tmp} = {param_vars[0]};")
else:
acc = param_vars[0]
for pv in param_vars[1:]:
space_tmp = ctx.fresh_tmp()
joined_tmp = ctx.fresh_tmp()
lines.append(f'CentValue {space_tmp} = cent_concat({acc}, cent_str(" "));')
lines.append(f"CentValue {joined_tmp} = cent_concat({space_tmp}, {pv});")
acc = joined_tmp
lines.append(f"cent_dice({acc});")
lines.append(f"CentValue {tmp} = {acc};")
case "AVDI":
lines.append(f"CentValue {tmp} = cent_avdi();")
case "AVDI_NVMERVS":
lines.append(f"CentValue {tmp} = cent_avdi_numerus();")
case "LONGITVDO":
lines.append(f"CentValue {tmp} = cent_longitudo({param_vars[0]});")
case "FORTIS_NVMERVS":
if not ctx.has_module("FORS"):
lines.append('cent_runtime_error("FORS module required for FORTIS_NVMERVS");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_fortis_numerus({param_vars[0]}, {param_vars[1]});")
case "FORTIS_ELECTIONIS":
if not ctx.has_module("FORS"):
lines.append('cent_runtime_error("FORS module required for FORTIS_ELECTIONIS");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_fortis_electionis({param_vars[0]});")
case "ERVMPE":
# break as expression (side-effecting; result is unused)
lines.append("break;")
lines.append(f"CentValue {tmp} = cent_null();")
case _:
raise NotImplementedError(node.builtin)
return lines, tmp
def _emit_invoca(node, ctx):
"""
Emits a user-defined function call.
Requires ctx.functions[name] = [param_names] populated by the emitter pre-pass.
"""
lines = []
param_vars = []
for p in node.parameters:
p_lines, p_var = emit_expr(p, 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}")
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)}"
)
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

View File

@@ -0,0 +1,106 @@
from centvrion.ast_nodes import (
Designa, DesignaIndex, SiStatement, DumStatement, PerStatement,
Defini, Redi, Erumpe, ExpressionStatement, ID,
)
from centvrion.compiler.emit_expr import emit_expr
def emit_stmt(node, ctx):
"""
Emit C code for a CENTVRION statement node.
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});']
if isinstance(node, DesignaIndex):
idx_lines, idx_var = emit_expr(node.index, ctx)
val_lines, val_var = emit_expr(node.value, ctx)
arr_tmp = ctx.fresh_tmp()
return (
idx_lines + val_lines + [
f'CentValue {arr_tmp} = cent_scope_get(&_scope, "{node.id.name}");',
f"cent_list_index_set(&{arr_tmp}, {idx_var}, {val_var});",
f'cent_scope_set(&_scope, "{node.id.name}", {arr_tmp});',
]
)
if isinstance(node, SiStatement):
cond_lines, cond_var = emit_expr(node.test, ctx)
then_lines = _emit_body(node.statements, ctx)
lines = cond_lines + [f"if (cent_truthy({cond_var})) {{"]
lines += [f" {l}" for l in then_lines]
if node.else_part:
else_lines = _emit_body(node.else_part, ctx)
lines += ["} else {"]
lines += [f" {l}" for l in else_lines]
lines += ["}"]
return lines
if isinstance(node, DumStatement):
# DVM loops UNTIL condition is true (inverted while)
lines = ["while (1) {"]
cond_lines, cond_var = emit_expr(node.test, ctx)
lines += [f" {l}" for l in cond_lines]
lines += [f" if (cent_truthy({cond_var})) break;"]
body_lines = _emit_body(node.statements, ctx)
lines += [f" {l}" for l in body_lines]
lines += ["}"]
return lines
if isinstance(node, PerStatement):
arr_lines, arr_var = emit_expr(node.data_list, ctx)
i_var = ctx.fresh_tmp()
var_name = node.variable_name.name
body_lines = _emit_body(node.statements, ctx)
lines = arr_lines + [
f'if ({arr_var}.type != CENT_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}]);',
]
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.
return []
if isinstance(node, Redi):
lines = []
val_vars = []
for v in node.values:
v_lines, v_var = emit_expr(v, ctx)
lines.extend(v_lines)
val_vars.append(v_var)
if len(val_vars) == 1:
lines.append(f"_return_val = {val_vars[0]};")
else:
# multiple return values → pack into a list
lst_tmp = ctx.fresh_tmp()
lines.append(f"CentValue {lst_tmp} = cent_list_new({len(val_vars)});")
for vv in val_vars:
lines.append(f"cent_list_push(&{lst_tmp}, {vv});")
lines.append(f"_return_val = {lst_tmp};")
lines.append("goto _func_return;")
return lines
if isinstance(node, Erumpe):
return ["break;"]
if isinstance(node, ExpressionStatement):
lines, _ = emit_expr(node.expression, ctx)
return lines
raise NotImplementedError(type(node).__name__)
def _emit_body(stmts, ctx):
lines = []
for s in stmts:
lines.extend(emit_stmt(s, ctx))
return lines

View File

@@ -0,0 +1,72 @@
import os
from centvrion.ast_nodes import Defini, Designa, ID
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 compile_program(program):
"""Return a complete C source string for the given Program AST node."""
ctx = EmitContext()
# Module pre-pass
for mc in program.modules:
ctx.modules.add(mc.module_name)
# Function pre-pass: assign unique C names, track aliases in order
func_version = {}
func_definitions = [] # [(c_name, Defini_stmt), ...]
for stmt in program.statements:
if isinstance(stmt, Defini):
name = stmt.name.name
ver = func_version.get(name, 0)
c_name = f"_cent_{name}" if ver == 0 else f"_cent_{name}_{ver}"
func_version[name] = ver + 1
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]
lines = []
# Includes
lines += [
f'#include "{_RUNTIME_DIR}/cent_runtime.h"',
"",
]
# Forward declarations
for c_name in ctx.functions:
lines.append(f"CentValue {c_name}(CentScope _scope);")
if ctx.functions:
lines.append("")
# Hoisted function definitions
for c_name, stmt in func_definitions:
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(stmt.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();")
if "MAGNVM" in ctx.modules:
lines.append(" cent_magnvm = 1;")
lines.append(" CentScope _scope = {0};")
lines.append(" CentValue _return_val = cent_null();")
for stmt in program.statements:
if isinstance(stmt, Defini):
continue
for l in emit_stmt(stmt, ctx):
lines.append(f" {l}")
lines += ["_func_return:", " return 0;", "}"]
return "\n".join(lines) + "\n"

View File

@@ -0,0 +1,551 @@
#include "cent_runtime.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* ------------------------------------------------------------------ */
/* Global arena */
/* ------------------------------------------------------------------ */
CentArena *cent_arena;
int cent_magnvm = 0;
/* ------------------------------------------------------------------ */
/* Arena allocator */
/* ------------------------------------------------------------------ */
CentArena *cent_arena_new(size_t cap) {
CentArena *a = malloc(sizeof(CentArena));
if (!a) { fputs("cent: out of memory\n", stderr); exit(1); }
a->buf = malloc(cap);
if (!a->buf) { fputs("cent: out of memory\n", stderr); exit(1); }
a->used = 0;
a->cap = cap;
a->next = NULL;
return a;
}
static size_t align8(size_t n) { return (n + 7) & ~(size_t)7; }
void *cent_arena_alloc(CentArena *a, size_t n) {
n = align8(n);
CentArena *cur = a;
for (;;) {
if (cur->used + n <= cur->cap) {
void *p = cur->buf + cur->used;
cur->used += n;
return p;
}
if (!cur->next) {
size_t new_cap = cur->cap > n ? cur->cap : n;
cur->next = cent_arena_new(new_cap);
}
cur = cur->next;
}
}
/* ------------------------------------------------------------------ */
/* Error handling */
/* ------------------------------------------------------------------ */
void cent_type_error(const char *msg) {
fprintf(stderr, "CENTVRION type error: %s\n", msg);
exit(1);
}
void cent_runtime_error(const char *msg) {
fprintf(stderr, "CENTVRION error: %s\n", msg);
exit(1);
}
/* ------------------------------------------------------------------ */
/* Scope operations */
/* ------------------------------------------------------------------ */
CentValue cent_scope_get(CentScope *s, const char *name) {
for (int i = 0; i < s->len; i++) {
if (strcmp(s->names[i], name) == 0)
return s->vals[i];
}
fprintf(stderr, "CENTVRION error: undefined variable '%s'\n", name);
exit(1);
}
void cent_scope_set(CentScope *s, const char *name, CentValue v) {
for (int i = 0; i < s->len; i++) {
if (strcmp(s->names[i], name) == 0) {
s->vals[i] = v;
return;
}
}
if (s->len >= s->cap) {
int new_cap = s->cap ? s->cap * 2 : 8;
const char **new_names = cent_arena_alloc(cent_arena, new_cap * sizeof(char *));
CentValue *new_vals = cent_arena_alloc(cent_arena, new_cap * sizeof(CentValue));
if (s->len > 0) {
memcpy(new_names, s->names, s->len * sizeof(char *));
memcpy(new_vals, s->vals, s->len * sizeof(CentValue));
}
s->names = new_names;
s->vals = new_vals;
s->cap = new_cap;
}
s->names[s->len] = name;
s->vals[s->len] = v;
s->len++;
}
CentScope cent_scope_copy(CentScope *s) {
CentScope dst;
dst.len = s->len;
dst.cap = s->cap;
if (s->cap > 0) {
dst.names = cent_arena_alloc(cent_arena, s->cap * sizeof(char *));
dst.vals = cent_arena_alloc(cent_arena, s->cap * sizeof(CentValue));
if (s->len > 0) {
memcpy(dst.names, s->names, s->len * sizeof(char *));
memcpy(dst.vals, s->vals, s->len * sizeof(CentValue));
}
} else {
dst.names = NULL;
dst.vals = NULL;
}
return dst;
}
/* ------------------------------------------------------------------ */
/* Roman numeral conversion */
/* ------------------------------------------------------------------ */
/* Descending table used for both int→roman and roman→int */
static const struct { const char *str; long val; } ROMAN_TABLE[] = {
{"M", 1000}, {"CM", 900}, {"D", 500}, {"CD", 400},
{"C", 100}, {"XC", 90}, {"L", 50}, {"XL", 40},
{"X", 10}, {"IX", 9}, {"V", 5}, {"IV", 4},
{"I", 1}
};
#define ROMAN_TABLE_LEN 13
/* Transform a 1-3999 Roman string into its x1000 equivalent.
Each char: 'I' -> "M" (since I*1000 = M), others -> char + "_". */
static void transform_thousands(const char *src, char *dst, size_t dstsz) {
size_t pos = 0;
for (const char *p = src; *p; p++) {
if (*p == 'I') {
if (pos + 1 >= dstsz) cent_runtime_error("Roman numeral buffer overflow");
dst[pos++] = 'M';
} else {
if (pos + 2 >= dstsz) cent_runtime_error("Roman numeral buffer overflow");
dst[pos++] = *p;
dst[pos++] = '_';
}
}
dst[pos] = '\0';
}
void cent_int_to_roman(long n, char *buf, size_t bufsz) {
if (n <= 0 || (n > 3999 && !cent_magnvm))
cent_runtime_error("number out of range for Roman numerals (1-3999)");
size_t pos = 0;
if (n > 3999) {
char base[64];
cent_int_to_roman(n / 1000, base, sizeof(base));
char transformed[128];
transform_thousands(base, transformed, sizeof(transformed));
size_t tlen = strlen(transformed);
if (tlen >= bufsz) cent_runtime_error("Roman numeral buffer overflow");
memcpy(buf, transformed, tlen);
pos = tlen;
n = n % 1000;
if (n == 0) { buf[pos] = '\0'; return; }
}
for (int i = 0; i < ROMAN_TABLE_LEN && n > 0; i++) {
while (n >= ROMAN_TABLE[i].val) {
size_t slen = strlen(ROMAN_TABLE[i].str);
if (pos + slen >= bufsz)
cent_runtime_error("Roman numeral buffer overflow");
memcpy(buf + pos, ROMAN_TABLE[i].str, slen);
pos += slen;
n -= ROMAN_TABLE[i].val;
}
}
buf[pos] = '\0';
}
long cent_roman_to_int(const char *s) {
long result = 0;
int pos = 0;
int slen = (int)strlen(s);
if (slen == 0)
cent_runtime_error("empty Roman numeral");
while (pos < slen) {
int matched = 0;
for (int i = 0; i < ROMAN_TABLE_LEN; i++) {
int tlen = (int)strlen(ROMAN_TABLE[i].str);
if (strncmp(s + pos, ROMAN_TABLE[i].str, tlen) == 0) {
result += ROMAN_TABLE[i].val;
pos += tlen;
matched = 1;
break;
}
}
if (!matched) {
fprintf(stderr, "CENTVRION error: invalid Roman numeral: %s\n", s);
exit(1);
}
}
return result;
}
/* ------------------------------------------------------------------ */
/* Display */
/* ------------------------------------------------------------------ */
/* Write value as string into buf[0..bufsz-1]; return chars needed */
/* (not counting null). buf may be NULL (just count mode). */
static int write_val(CentValue v, char *buf, int bufsz) {
char tmp[64];
int n;
switch (v.type) {
case CENT_INT:
cent_int_to_roman(v.ival, tmp, sizeof(tmp));
n = (int)strlen(tmp);
if (buf && n < bufsz) { memcpy(buf, tmp, n); buf[n] = '\0'; }
return n;
case CENT_STR:
n = (int)strlen(v.sval);
if (buf && n < bufsz) { memcpy(buf, v.sval, n); buf[n] = '\0'; }
return n;
case CENT_BOOL:
if (v.bval) {
if (buf && bufsz > 5) { memcpy(buf, "VERVS", 5); buf[5] = '\0'; }
return 5;
} else {
if (buf && bufsz > 6) { memcpy(buf, "FALSVS", 6); buf[6] = '\0'; }
return 6;
}
case CENT_NULL:
if (buf && bufsz > 6) { memcpy(buf, "NVLLVS", 6); buf[6] = '\0'; }
return 6;
case CENT_FRAC: {
long num = v.fval.num, den = v.fval.den;
if (den < 0) { num = -num; den = -den; }
if (num < 0)
cent_runtime_error("cannot display negative fraction without SVBNVLLA");
long int_part = num / den;
long rem_num = num % den;
/* Integer part (omit leading zero for pure fractions) */
char int_buf[64] = {0};
int int_len = 0;
if (int_part > 0 || rem_num == 0) {
cent_int_to_roman(int_part, int_buf, sizeof(int_buf));
int_len = (int)strlen(int_buf);
}
/* Duodecimal fractional expansion — mirrors fraction_to_frac() */
char frac_buf[64] = {0};
int frac_pos = 0;
long cur_num = rem_num, cur_den = den;
for (int lvl = 0; lvl < 6 && cur_num != 0; lvl++) {
if (lvl > 0) frac_buf[frac_pos++] = '|';
long level_int = (cur_num * 12) / cur_den;
cur_num = (cur_num * 12) % cur_den;
for (int i = 0; i < (int)(level_int / 6); i++) frac_buf[frac_pos++] = 'S';
for (int i = 0; i < (int)((level_int % 6) / 2); i++) frac_buf[frac_pos++] = ':';
for (int i = 0; i < (int)((level_int % 6) % 2); i++) frac_buf[frac_pos++] = '.';
}
n = int_len + frac_pos;
if (buf && n < bufsz) {
memcpy(buf, int_buf, int_len);
memcpy(buf + int_len, frac_buf, frac_pos);
buf[n] = '\0';
}
return n;
}
case CENT_LIST: {
/* "[elem1 elem2 ...]" */
int total = 2; /* '[' + ']' */
for (int i = 0; i < v.lval.len; i++) {
if (i > 0) total++; /* space separator */
total += write_val(v.lval.items[i], NULL, 0);
}
if (!buf) return total;
int pos = 0;
buf[pos++] = '[';
for (int i = 0; i < v.lval.len; i++) {
if (i > 0) buf[pos++] = ' ';
int written = write_val(v.lval.items[i], buf + pos, bufsz - pos);
pos += written;
}
buf[pos++] = ']';
if (pos < bufsz) buf[pos] = '\0';
return total;
}
default:
cent_runtime_error("cannot display value");
return 0;
}
}
char *cent_make_string(CentValue v) {
int len = write_val(v, NULL, 0);
char *buf = cent_arena_alloc(cent_arena, len + 1);
write_val(v, buf, len + 1);
return buf;
}
/* ------------------------------------------------------------------ */
/* Arithmetic and comparison operators */
/* ------------------------------------------------------------------ */
static long gcd(long a, long b) {
a = a < 0 ? -a : a;
b = b < 0 ? -b : b;
while (b) { long t = b; b = a % b; a = t; }
return a ? a : 1;
}
static CentValue frac_reduce(long num, long den) {
if (den < 0) { num = -num; den = -den; }
long g = gcd(num < 0 ? -num : num, den);
return cent_frac(num / g, den / g);
}
static void to_frac(CentValue v, long *num, long *den) {
if (v.type == CENT_INT) { *num = v.ival; *den = 1; }
else if (v.type == CENT_NULL) { *num = 0; *den = 1; }
else { *num = v.fval.num; *den = v.fval.den; }
}
CentValue cent_add(CentValue a, CentValue b) {
if (a.type == CENT_INT && b.type == CENT_INT)
return cent_int(a.ival + b.ival);
if (a.type == CENT_NULL && b.type == CENT_NULL)
return cent_null();
if ((a.type == CENT_INT || a.type == CENT_FRAC || a.type == CENT_NULL) &&
(b.type == CENT_INT || b.type == CENT_FRAC || b.type == CENT_NULL)) {
long an, ad, bn, bd;
to_frac(a, &an, &ad); to_frac(b, &bn, &bd);
return frac_reduce(an * bd + bn * ad, ad * bd);
}
if (a.type == CENT_STR || b.type == CENT_STR)
cent_type_error("'+' cannot be used with strings; use '&' for concatenation");
else
cent_type_error("'+' requires two integers");
return cent_null();
}
CentValue cent_concat(CentValue a, CentValue b) {
const char *sa = (a.type == CENT_NULL) ? "" : cent_make_string(a);
const char *sb = (b.type == CENT_NULL) ? "" : cent_make_string(b);
int la = (int)strlen(sa), lb = (int)strlen(sb);
char *s = cent_arena_alloc(cent_arena, la + lb + 1);
memcpy(s, sa, la);
memcpy(s + la, sb, lb);
s[la + lb] = '\0';
return cent_str(s);
}
CentValue cent_sub(CentValue a, CentValue b) {
if (a.type == CENT_INT && b.type == CENT_INT)
return cent_int(a.ival - b.ival);
if ((a.type == CENT_INT || a.type == CENT_FRAC || a.type == CENT_NULL) &&
(b.type == CENT_INT || b.type == CENT_FRAC || b.type == CENT_NULL)) {
long an, ad, bn, bd;
to_frac(a, &an, &ad); to_frac(b, &bn, &bd);
return frac_reduce(an * bd - bn * ad, ad * bd);
}
cent_type_error("'-' requires two integers");
return cent_null();
}
CentValue cent_mul(CentValue a, CentValue b) {
if (a.type == CENT_INT && b.type == CENT_INT)
return cent_int(a.ival * b.ival);
if ((a.type == CENT_INT || a.type == CENT_FRAC || a.type == CENT_NULL) &&
(b.type == CENT_INT || b.type == CENT_FRAC || b.type == CENT_NULL)) {
long an, ad, bn, bd;
to_frac(a, &an, &ad); to_frac(b, &bn, &bd);
return frac_reduce(an * bn, ad * bd);
}
cent_type_error("'*' requires two integers");
return cent_null();
}
CentValue cent_div(CentValue a, CentValue b) {
if (a.type != CENT_INT || b.type != CENT_INT)
cent_type_error("'/' requires two integers");
if (b.ival == 0)
cent_runtime_error("division by zero");
return cent_int(a.ival / b.ival);
}
CentValue cent_div_frac(CentValue a, CentValue b) {
long an, ad, bn, bd;
to_frac(a, &an, &ad); to_frac(b, &bn, &bd);
if (bn == 0) cent_runtime_error("division by zero");
return frac_reduce(an * bd, ad * bn);
}
CentValue cent_eq(CentValue a, CentValue b) {
if ((a.type == CENT_INT || a.type == CENT_FRAC) &&
(b.type == CENT_INT || b.type == CENT_FRAC)) {
long an, ad, bn, bd;
to_frac(a, &an, &ad); to_frac(b, &bn, &bd);
return cent_bool(an * bd == bn * ad);
}
if (a.type != b.type) return cent_bool(0);
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_NULL: return cent_bool(1);
default:
cent_type_error("'EST' not supported for this type");
return cent_null();
}
}
CentValue cent_lt(CentValue a, CentValue b) {
if ((a.type == CENT_INT || a.type == CENT_FRAC || a.type == CENT_NULL) &&
(b.type == CENT_INT || b.type == CENT_FRAC || b.type == CENT_NULL)) {
long an, ad, bn, bd;
to_frac(a, &an, &ad); to_frac(b, &bn, &bd);
return cent_bool(an * bd < bn * ad);
}
cent_type_error("'MINVS' requires two integers");
return cent_null();
}
CentValue cent_gt(CentValue a, CentValue b) {
if ((a.type == CENT_INT || a.type == CENT_FRAC || a.type == CENT_NULL) &&
(b.type == CENT_INT || b.type == CENT_FRAC || b.type == CENT_NULL)) {
long an, ad, bn, bd;
to_frac(a, &an, &ad); to_frac(b, &bn, &bd);
return cent_bool(an * bd > bn * ad);
}
cent_type_error("'PLVS' requires two integers");
return cent_null();
}
CentValue cent_and(CentValue a, CentValue b) {
if (a.type != CENT_BOOL || b.type != CENT_BOOL)
cent_type_error("'ET' requires two booleans");
return cent_bool(a.bval && b.bval);
}
CentValue cent_or(CentValue a, CentValue b) {
if (a.type != CENT_BOOL || b.type != CENT_BOOL)
cent_type_error("'AVT' requires two booleans");
return cent_bool(a.bval || b.bval);
}
/* ------------------------------------------------------------------ */
/* Builtin functions */
/* ------------------------------------------------------------------ */
void cent_dice(CentValue v) {
char *s = cent_make_string(v);
fputs(s, stdout);
fputc('\n', stdout);
}
CentValue cent_avdi(void) {
char *buf = cent_arena_alloc(cent_arena, 1024);
if (!fgets(buf, 1024, stdin)) {
buf[0] = '\0';
return cent_str(buf);
}
int len = (int)strlen(buf);
if (len > 0 && buf[len - 1] == '\n')
buf[len - 1] = '\0';
return cent_str(buf);
}
CentValue cent_avdi_numerus(void) {
CentValue s = cent_avdi();
return cent_int(cent_roman_to_int(s.sval));
}
CentValue cent_longitudo(CentValue v) {
if (v.type != CENT_LIST)
cent_type_error("'LONGITVDO' requires a list");
return cent_int(v.lval.len);
}
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");
long range = hi.ival - lo.ival + 1;
if (range <= 0)
cent_runtime_error("'FORTIS_NVMERVS' requires lo <= hi");
return cent_int(lo.ival + rand() % range);
}
CentValue cent_fortis_electionis(CentValue lst) {
if (lst.type != CENT_LIST)
cent_type_error("'FORTIS_ELECTIONIS' requires a list");
if (lst.lval.len == 0)
cent_runtime_error("'FORTIS_ELECTIONIS' requires a non-empty list");
return lst.lval.items[rand() % lst.lval.len];
}
/* ------------------------------------------------------------------ */
/* Array helpers */
/* ------------------------------------------------------------------ */
CentValue cent_list_new(int cap) {
CentValue *items = cent_arena_alloc(cent_arena, cap * sizeof(CentValue));
return cent_list(items, 0, cap);
}
void cent_list_push(CentValue *lst, CentValue v) {
if (lst->lval.len >= lst->lval.cap) {
int new_cap = lst->lval.cap ? lst->lval.cap * 2 : 8;
CentValue *new_items = cent_arena_alloc(cent_arena, new_cap * sizeof(CentValue));
if (lst->lval.len > 0)
memcpy(new_items, lst->lval.items, lst->lval.len * sizeof(CentValue));
lst->lval.items = new_items;
lst->lval.cap = new_cap;
}
lst->lval.items[lst->lval.len++] = v;
}
CentValue cent_list_index(CentValue lst, CentValue idx) {
if (lst.type != CENT_LIST)
cent_type_error("index requires a list");
if (idx.type != CENT_INT)
cent_type_error("list index must be an integer");
long i = idx.ival;
if (i < 1 || i > lst.lval.len)
cent_runtime_error("list index out of range");
return lst.lval.items[i - 1];
}
void cent_list_index_set(CentValue *lst, CentValue idx, CentValue v) {
if (lst->type != CENT_LIST)
cent_type_error("index-assign requires a list");
if (idx.type != CENT_INT)
cent_type_error("list index must be an integer");
long i = idx.ival;
if (i < 1 || i > lst->lval.len)
cent_runtime_error("list index out of range");
lst->lval.items[i - 1] = v;
}
/* ------------------------------------------------------------------ */
/* Initialisation */
/* ------------------------------------------------------------------ */
void cent_init(void) {
cent_arena = cent_arena_new(1024 * 1024); /* 1 MiB initial arena */
srand((unsigned)time(NULL));
}

View File

@@ -0,0 +1,194 @@
#ifndef CENT_RUNTIME_H
#define CENT_RUNTIME_H
#include <stddef.h>
#include <string.h>
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
typedef enum {
CENT_INT,
CENT_STR,
CENT_BOOL,
CENT_LIST,
CENT_FRAC,
CENT_NULL
} CentType;
typedef struct CentValue CentValue;
typedef struct CentList CentList;
/* Duodecimal fraction: num/den stored as exact integers */
typedef struct {
long num;
long den;
} CentFrac;
struct CentList {
CentValue *items;
int len;
int cap;
};
struct CentValue {
CentType type;
union {
long ival; /* CENT_INT */
char *sval; /* CENT_STR */
int bval; /* CENT_BOOL */
CentList lval; /* CENT_LIST */
CentFrac fval; /* CENT_FRAC */
};
};
/* Scope: flat name→value array. Stack-allocated by the caller;
cent_scope_set uses cent_arena when it needs to grow. */
typedef struct {
const char **names;
CentValue *vals;
int len;
int cap;
} CentScope;
/* ------------------------------------------------------------------ */
/* Arena allocator — no free() during a program run */
/* ------------------------------------------------------------------ */
typedef struct CentArena CentArena;
struct CentArena {
char *buf;
size_t used;
size_t cap;
CentArena *next; /* overflow chain */
};
CentArena *cent_arena_new(size_t cap);
void *cent_arena_alloc(CentArena *a, size_t n);
/* Global arena; initialised in cent_runtime.c main() setup */
extern CentArena *cent_arena;
/* Set to 1 when CVM MAGNVM is active; enables extended numeral display */
extern int cent_magnvm;
/* ------------------------------------------------------------------ */
/* Value constructors */
/* ------------------------------------------------------------------ */
static inline CentValue cent_int(long v) {
CentValue r; r.type = CENT_INT; r.ival = v; return r;
}
static inline CentValue cent_str(const char *s) {
CentValue r; r.type = CENT_STR; r.sval = (char *)s; return r;
}
static inline CentValue cent_bool(int v) {
CentValue r; r.type = CENT_BOOL; r.bval = !!v; return r;
}
static inline CentValue cent_null(void) {
CentValue r; r.type = CENT_NULL; r.ival = 0; return r;
}
static inline CentValue cent_frac(long num, long den) {
CentValue r; r.type = CENT_FRAC; r.fval.num = num; r.fval.den = den; return r;
}
static inline CentValue cent_list(CentValue *items, int len, int cap) {
CentValue r;
r.type = CENT_LIST;
r.lval.items = items;
r.lval.len = len;
r.lval.cap = cap;
return r;
}
/* ------------------------------------------------------------------ */
/* Error handling */
/* ------------------------------------------------------------------ */
void cent_type_error(const char *msg); /* type mismatch → exit(1) */
void cent_runtime_error(const char *msg); /* runtime fault → exit(1) */
/* ------------------------------------------------------------------ */
/* Truthiness — conditions must be booleans; anything else is a fault */
/* ------------------------------------------------------------------ */
static inline int cent_truthy(CentValue v) {
if (v.type != CENT_BOOL)
cent_type_error("condition must be a boolean (VERITAS/FALSITAS)");
return v.bval;
}
/* ------------------------------------------------------------------ */
/* Scope operations */
/* ------------------------------------------------------------------ */
/* Look up name; returns cent_null() if not found */
CentValue cent_scope_get(CentScope *s, const char *name);
/* Insert or update name→v; grows via cent_arena if needed */
void cent_scope_set(CentScope *s, const char *name, CentValue v);
/* Shallow copy of a scope (matches vtable.copy() semantics) */
CentScope cent_scope_copy(CentScope *s);
/* ------------------------------------------------------------------ */
/* Roman numeral conversion */
/* ------------------------------------------------------------------ */
/* int → Roman numeral string (13999, or any positive int if cent_magnvm set) */
void cent_int_to_roman(long n, char *buf, size_t bufsz);
/* Roman numeral string → int; exits on invalid input */
long cent_roman_to_int(const char *s);
/* ------------------------------------------------------------------ */
/* Display */
/* ------------------------------------------------------------------ */
/* Returns arena-allocated string mirroring Python make_string() */
char *cent_make_string(CentValue v);
/* ------------------------------------------------------------------ */
/* Arithmetic and comparison operators */
/* ------------------------------------------------------------------ */
CentValue cent_add(CentValue a, CentValue b); /* INT+INT or FRAC+FRAC/INT */
CentValue cent_concat(CentValue a, CentValue b); /* & operator: coerce all types to str */
CentValue cent_sub(CentValue a, CentValue b); /* INT-INT or FRAC-FRAC/INT */
CentValue cent_mul(CentValue a, CentValue b); /* INT*INT or FRAC*FRAC/INT */
CentValue cent_div(CentValue a, CentValue b); /* INT/INT integer div */
CentValue cent_div_frac(CentValue a, CentValue b); /* FRACTIO: exact div → FRAC */
CentValue cent_eq (CentValue a, CentValue b); /* EST → BOOL */
CentValue cent_lt (CentValue a, CentValue b); /* MINVS → BOOL */
CentValue cent_gt (CentValue a, CentValue b); /* PLVS → BOOL */
CentValue cent_and(CentValue a, CentValue b); /* ET → BOOL */
CentValue cent_or (CentValue a, CentValue b); /* AVT → BOOL */
/* ------------------------------------------------------------------ */
/* Builtin functions */
/* ------------------------------------------------------------------ */
void cent_dice(CentValue v); /* DICE */
CentValue cent_avdi(void); /* AVDI */
CentValue cent_avdi_numerus(void); /* AVDI_NVMERVS */
CentValue cent_longitudo(CentValue v); /* LONGITVDO */
CentValue cent_fortis_numerus(CentValue lo, CentValue hi); /* FORTIS_NVMERVS */
CentValue cent_fortis_electionis(CentValue lst); /* FORTIS_ELECTIONIS */
/* ------------------------------------------------------------------ */
/* Array helpers */
/* ------------------------------------------------------------------ */
CentValue cent_list_new(int cap);
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);
/* ------------------------------------------------------------------ */
/* Initialisation */
/* ------------------------------------------------------------------ */
/* Call once at program start: sets up arena + seeds RNG */
void cent_init(void);
#endif /* CENT_RUNTIME_H */

Binary file not shown.

View File

@@ -86,7 +86,7 @@
\item \textbf{string}: \\ Any text encased in " characters. \item \textbf{string}: \\ Any text encased in " characters.
\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{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{bool}: \\ VERITAS or FALSITAS.
\item \textbf{binop}: \\ Binary operators: \texttt{+}, \texttt{-}, \texttt{*}, \texttt{/}, \texttt{EST} (equality), \texttt{MINVS} (<), \texttt{PLVS} (>), \texttt{ET} (and), \texttt{AVT} (or), \texttt{:} (string concatenation). \item \textbf{binop}: \\ Binary operators: \texttt{+}, \texttt{-}, \texttt{*}, \texttt{/}, \texttt{EST} (equality), \texttt{MINVS} (<), \texttt{PLVS} (>), \texttt{ET} (and), \texttt{AVT} (or), \texttt{\&} (string concatenation).
\item \textbf{unop}: \\ Unary operators: \texttt{-} (negation), \texttt{NON} (boolean not). \item \textbf{unop}: \\ Unary operators: \texttt{-} (negation), \texttt{NON} (boolean not).
\end{itemize} \end{itemize}

108
tests.py
View File

@@ -1,4 +1,7 @@
import os
import random import random
import subprocess
import tempfile
import unittest import unittest
from io import StringIO from io import StringIO
from unittest.mock import patch from unittest.mock import patch
@@ -14,11 +17,17 @@ from centvrion.ast_nodes import (
Fractio, frac_to_fraction, fraction_to_frac, Fractio, frac_to_fraction, fraction_to_frac,
num_to_int, int_to_num, make_string, num_to_int, int_to_num, make_string,
) )
from centvrion.compiler.emitter import compile_program
from centvrion.errors import CentvrionError from centvrion.errors import CentvrionError
from centvrion.lexer import Lexer from centvrion.lexer import Lexer
from centvrion.parser import Parser from centvrion.parser import Parser
from centvrion.values import ValInt, ValStr, ValBool, ValList, ValNul, ValFunc, ValFrac from centvrion.values import ValInt, ValStr, ValBool, ValList, ValNul, ValFunc, ValFrac
_RUNTIME_C = os.path.join(
os.path.dirname(__file__),
"centvrion", "compiler", "runtime", "cent_runtime.c"
)
def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]): def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]):
random.seed(1) random.seed(1)
@@ -72,11 +81,27 @@ def run_test(self, source, target_nodes, target_value, target_output="", input_l
########################## ##########################
###### Compiler Test ##### ###### Compiler Test #####
########################## ##########################
# try: c_source = compile_program(program)
# bytecode = program.compile() with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
# ... tmp_c.write(c_source)
# except Exception as e: tmp_c_path = tmp_c.name
# raise Exception("###Compiler test###") from e 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,
)
stdin_data = "".join(f"{l}\n" for l in input_lines)
proc = subprocess.run(
[tmp_bin_path],
input=stdin_data, capture_output=True, text=True,
)
self.assertEqual(proc.returncode, 0, f"Compiler binary exited non-zero:\n{proc.stderr}")
self.assertEqual(proc.stdout, target_output, "Compiler output test")
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)
# --- Output --- # --- Output ---
@@ -384,7 +409,7 @@ error_tests = [
("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", CentvrionError), # args to zero-param function ("DEFINI f () VT { REDI(I) }\nINVOCA f (I)", CentvrionError), # args to zero-param function
("SI NVLLVS TVNC { DESIGNA r VT I }", CentvrionError), # NVLLVS cannot be used as boolean ("SI NVLLVS TVNC { DESIGNA r VT I }", CentvrionError), # NVLLVS cannot be used as boolean
("NVLLVS AVT VERITAS", CentvrionError), # NVLLVS cannot be used as boolean in AVT ("NVLLVS AVT VERITAS", CentvrionError), # NVLLVS cannot be used as boolean in AVT
('"hello" + " world"', CentvrionError), # use : for string concatenation, not + ('"hello" + " world"', CentvrionError), # use & for string concatenation, not +
("[I, II][III]", CentvrionError), # index too high ("[I, II][III]", CentvrionError), # index too high
("CVM SVBNVLLA\n[I, II][-I]", CentvrionError), # negative index ("CVM SVBNVLLA\n[I, II][-I]", CentvrionError), # negative index
("[I, II][-I]", CentvrionError), # negative value ("[I, II][-I]", CentvrionError), # negative value
@@ -402,10 +427,14 @@ error_tests = [
("LONGITVDO(I)", CentvrionError), # LONGITVDO on non-array ("LONGITVDO(I)", CentvrionError), # LONGITVDO on non-array
("DESIGNA x VT I\nINVOCA x ()", CentvrionError), # invoking a non-function ("DESIGNA x VT I\nINVOCA x ()", CentvrionError), # invoking a non-function
("SI I TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: int ("SI I TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: int
("IIIS", CentvrionError), # fraction without FRACTIO module
("DESIGNA z VT I - I\nSI z TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: zero int ("DESIGNA z VT I - I\nSI z TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: zero int
("SI [I] TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: non-empty list ("SI [I] TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: non-empty list
("SI [] TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: empty list ("SI [] TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: empty list
("DESIGNA x VT I\nDVM x FACE {\nDESIGNA x VT x + I\n}", CentvrionError), # non-bool DVM condition: int ("DESIGNA x VT I\nDVM x FACE {\nDESIGNA x VT x + I\n}", CentvrionError), # non-bool DVM condition: int
("NON I", CentvrionError), # NON on integer
("DESIGNA z VT I - I\nNON z", CentvrionError), # NON on zero integer
('NON "hello"', CentvrionError), # NON on string
] ]
class TestErrors(unittest.TestCase): class TestErrors(unittest.TestCase):
@@ -415,6 +444,39 @@ class TestErrors(unittest.TestCase):
run_test(self, source, None, None) run_test(self, source, None, None)
def run_compiler_error_test(self, source):
lexer = Lexer().get_lexer()
tokens = lexer.lex(source + "\n")
program = Parser().parse(tokens)
try:
c_source = compile_program(program)
except CentvrionError:
return # compile-time detection is valid
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.assertNotEqual(proc.returncode, 0, "Expected non-zero exit for error program")
self.assertTrue(proc.stderr.strip(), "Expected error message on stderr")
finally:
os.unlink(tmp_c_path)
os.unlink(tmp_bin_path)
compiler_error_tests = [(s, e) for s, e in error_tests if e == CentvrionError]
class TestCompilerErrors(unittest.TestCase):
@parameterized.expand(compiler_error_tests)
def test_compiler_errors(self, source, error_type):
run_compiler_error_test(self, source)
# --- Repr --- # --- Repr ---
repr_tests = [ repr_tests = [
@@ -1178,21 +1240,18 @@ non_tests = [
("NON NON VERITAS", ("NON NON VERITAS",
Program([], [ExpressionStatement(UnaryNot(UnaryNot(Bool(True))))]), Program([], [ExpressionStatement(UnaryNot(UnaryNot(Bool(True))))]),
ValBool(True)), ValBool(True)),
("NON I", ("DESIGNA b VT I EST II\nNON b",
Program([], [ExpressionStatement(UnaryNot(Numeral("I")))]), Program([], [Designa(ID("b"), BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST")), ExpressionStatement(UnaryNot(ID("b")))]),
ValBool(False)),
# zero int is falsy, so NON gives True
("DESIGNA z VT I - I\nNON z",
Program([], [Designa(ID("z"), BinOp(Numeral("I"), Numeral("I"), "SYMBOL_MINUS")), ExpressionStatement(UnaryNot(ID("z")))]),
ValBool(True)), ValBool(True)),
# NON binds tighter than AVT: (NON VERITAS) AVT FALSITAS → FALSITAS AVT FALSITAS → False ("DESIGNA z VT I EST I\nNON z",
Program([], [Designa(ID("z"), BinOp(Numeral("I"), Numeral("I"), "KEYWORD_EST")), ExpressionStatement(UnaryNot(ID("z")))]),
ValBool(False)),
("NON VERITAS AVT FALSITAS", ("NON VERITAS AVT FALSITAS",
Program([], [ExpressionStatement(BinOp(UnaryNot(Bool(True)), Bool(False), "KEYWORD_AVT"))]), Program([], [ExpressionStatement(BinOp(UnaryNot(Bool(True)), Bool(False), "KEYWORD_AVT"))]),
ValBool(False)), ValBool(False)),
# NON binds tighter than EST: (NON I) EST I → FALSITAS EST I → False ("NON VERITAS EST FALSITAS",
("NON I EST I", Program([], [ExpressionStatement(BinOp(UnaryNot(Bool(True)), Bool(False), "KEYWORD_EST"))]),
Program([], [ExpressionStatement(BinOp(UnaryNot(Numeral("I")), Numeral("I"), "KEYWORD_EST"))]), ValBool(True)),
ValBool(False)),
] ]
class TestNon(unittest.TestCase): class TestNon(unittest.TestCase):
@@ -1272,26 +1331,15 @@ class TestFractioComparisons(unittest.TestCase):
run_test(self, source, nodes, value) run_test(self, source, nodes, value)
class TestFractioErrors(unittest.TestCase): class TestFractioHelpers(unittest.TestCase):
def test_fraction_without_module(self):
source = "IIIS\n"
lexer = Lexer().get_lexer()
tokens = lexer.lex(source)
program = Parser().parse(tokens)
with self.assertRaises(CentvrionError):
program.eval()
def test_frac_to_fraction_ordering(self): def test_frac_to_fraction_ordering(self):
with self.assertRaises(CentvrionError): with self.assertRaises(CentvrionError):
frac_to_fraction(".S") # . before S violates highest-to-lowest frac_to_fraction(".S") # . before S violates highest-to-lowest
def test_frac_to_fraction_level_overflow(self): def test_frac_to_fraction_level_overflow(self):
with self.assertRaises(CentvrionError): with self.assertRaises(CentvrionError):
frac_to_fraction("SSSSSS") # 6*S = 36/12 >= 1 per level... wait S can only appear once frac_to_fraction("SSSSSS") # SS means S twice = 12/12 = 1, violating < 12/12 constraint
# Actually "SS" means S twice, which is 12/12 = 1, violating < 12/12 constraint
class TestFractioHelpers(unittest.TestCase):
def test_frac_to_fraction_iiis(self): def test_frac_to_fraction_iiis(self):
self.assertEqual(frac_to_fraction("IIIS"), Fraction(7, 2)) self.assertEqual(frac_to_fraction("IIIS"), Fraction(7, 2))