From 4937a95f70a97152425c95babc2d1f7553da5668 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Fri, 10 Apr 2026 13:06:34 +0200 Subject: [PATCH] :goat: Compiler --- cent | 34 +- centvrion/ast_nodes.py | 6 +- centvrion/compiler/__init__.py | 1 + centvrion/compiler/context.py | 17 + centvrion/compiler/emit_expr.py | 225 +++++++++ centvrion/compiler/emit_stmt.py | 106 +++++ centvrion/compiler/emitter.py | 72 +++ centvrion/compiler/runtime/cent_runtime.c | 551 ++++++++++++++++++++++ centvrion/compiler/runtime/cent_runtime.h | 194 ++++++++ language/main.pdf | Bin 20111 -> 21167 bytes language/main.tex | 2 +- tests.py | 108 +++-- 12 files changed, 1280 insertions(+), 36 deletions(-) create mode 100644 centvrion/compiler/__init__.py create mode 100644 centvrion/compiler/context.py create mode 100644 centvrion/compiler/emit_expr.py create mode 100644 centvrion/compiler/emit_stmt.py create mode 100644 centvrion/compiler/emitter.py create mode 100644 centvrion/compiler/runtime/cent_runtime.c create mode 100644 centvrion/compiler/runtime/cent_runtime.h diff --git a/cent b/cent index 28d50d6..c639e8e 100755 --- a/cent +++ b/cent @@ -2,15 +2,20 @@ """ Usage: cent (-h|--help) - cent (-i|-c) FILE + cent -i FILE + cent -c [--keep-c] FILE Options: -h --help Print this help screen -i Run the interpreter -c Run the compiler + --keep-c Keep the generated C file alongside the binary FILE The file to compile/interpret """ +import os +import subprocess import sys +import tempfile from docopt import docopt from rply.errors import LexingError @@ -19,6 +24,7 @@ from centvrion.errors import CentvrionError from centvrion.lexer import Lexer from centvrion.parser import Parser from centvrion.ast_nodes import Program +from centvrion.compiler.emitter import compile_program def main(): args = docopt(__doc__) @@ -44,7 +50,31 @@ def main(): except CentvrionError as e: sys.exit(f"CENTVRION error: {e}") 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: raise Exception("Output not of type 'Program'", type(program)) diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index a732b14..dcc6211 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -622,7 +622,9 @@ class UnaryNot(Node): def _eval(self, 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): @@ -730,8 +732,6 @@ class DumStatement(Node): if vtable["#return"] is not None: break vtable, cond = self.test.eval(vtable) - if not isinstance(cond, ValBool): - raise CentvrionError("DVM condition must be a boolean") return vtable, last_val diff --git a/centvrion/compiler/__init__.py b/centvrion/compiler/__init__.py new file mode 100644 index 0000000..f68664a --- /dev/null +++ b/centvrion/compiler/__init__.py @@ -0,0 +1 @@ +from centvrion.compiler.emitter import compile_program diff --git a/centvrion/compiler/context.py b/centvrion/compiler/context.py new file mode 100644 index 0000000..fb26b27 --- /dev/null +++ b/centvrion/compiler/context.py @@ -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 diff --git a/centvrion/compiler/emit_expr.py b/centvrion/compiler/emit_expr.py new file mode 100644 index 0000000..979c4b7 --- /dev/null +++ b/centvrion/compiler/emit_expr.py @@ -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 diff --git a/centvrion/compiler/emit_stmt.py b/centvrion/compiler/emit_stmt.py new file mode 100644 index 0000000..60ad23e --- /dev/null +++ b/centvrion/compiler/emit_stmt.py @@ -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 diff --git a/centvrion/compiler/emitter.py b/centvrion/compiler/emitter.py new file mode 100644 index 0000000..72df9b9 --- /dev/null +++ b/centvrion/compiler/emitter.py @@ -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" diff --git a/centvrion/compiler/runtime/cent_runtime.c b/centvrion/compiler/runtime/cent_runtime.c new file mode 100644 index 0000000..58ac326 --- /dev/null +++ b/centvrion/compiler/runtime/cent_runtime.c @@ -0,0 +1,551 @@ +#include "cent_runtime.h" +#include +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* 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)); +} diff --git a/centvrion/compiler/runtime/cent_runtime.h b/centvrion/compiler/runtime/cent_runtime.h new file mode 100644 index 0000000..276910d --- /dev/null +++ b/centvrion/compiler/runtime/cent_runtime.h @@ -0,0 +1,194 @@ +#ifndef CENT_RUNTIME_H +#define CENT_RUNTIME_H + +#include +#include + +/* ------------------------------------------------------------------ */ +/* 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 (1–3999, 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 */ diff --git a/language/main.pdf b/language/main.pdf index 445ef6e31c57c0e2e7c5ecfbda8f5eee9954a95b..0852d54c836016d2d6384526d211d4c2b456ef42 100644 GIT binary patch delta 13416 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 Y#m&pb+yai7o0FXjj)Fp5Q3CG&02DW>fB*mh diff --git a/language/main.tex b/language/main.tex index c42c22f..11a02d6 100644 --- a/language/main.tex +++ b/language/main.tex @@ -86,7 +86,7 @@ \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{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). \end{itemize} diff --git a/tests.py b/tests.py index 4e75d45..6cc4d98 100644 --- a/tests.py +++ b/tests.py @@ -1,4 +1,7 @@ +import os import random +import subprocess +import tempfile import unittest from io import StringIO from unittest.mock import patch @@ -14,11 +17,17 @@ from centvrion.ast_nodes import ( Fractio, frac_to_fraction, fraction_to_frac, num_to_int, int_to_num, make_string, ) +from centvrion.compiler.emitter import compile_program from centvrion.errors import CentvrionError from centvrion.lexer import Lexer from centvrion.parser import Parser from centvrion.values import ValInt, ValStr, ValBool, ValList, ValNul, ValFunc, ValFrac +_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=[]): random.seed(1) @@ -72,11 +81,27 @@ def run_test(self, source, target_nodes, target_value, target_output="", input_l ########################## ###### Compiler Test ##### ########################## - # try: - # bytecode = program.compile() - # ... - # except Exception as e: - # raise Exception("###Compiler test###") from e + 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, + ) + 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 --- @@ -384,7 +409,7 @@ error_tests = [ ("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 ("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 ("CVM SVBNVLLA\n[I, II][-I]", CentvrionError), # negative index ("[I, II][-I]", CentvrionError), # negative value @@ -402,10 +427,14 @@ error_tests = [ ("LONGITVDO(I)", CentvrionError), # LONGITVDO on non-array ("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 ("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 [] 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 + ("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): @@ -415,6 +444,39 @@ class TestErrors(unittest.TestCase): 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_tests = [ @@ -1178,21 +1240,18 @@ non_tests = [ ("NON NON VERITAS", Program([], [ExpressionStatement(UnaryNot(UnaryNot(Bool(True))))]), ValBool(True)), - ("NON I", - Program([], [ExpressionStatement(UnaryNot(Numeral("I")))]), - 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")))]), + ("DESIGNA b VT I EST II\nNON b", + Program([], [Designa(ID("b"), BinOp(Numeral("I"), Numeral("II"), "KEYWORD_EST")), ExpressionStatement(UnaryNot(ID("b")))]), 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", Program([], [ExpressionStatement(BinOp(UnaryNot(Bool(True)), Bool(False), "KEYWORD_AVT"))]), ValBool(False)), - # NON binds tighter than EST: (NON I) EST I → FALSITAS EST I → False - ("NON I EST I", - Program([], [ExpressionStatement(BinOp(UnaryNot(Numeral("I")), Numeral("I"), "KEYWORD_EST"))]), - ValBool(False)), + ("NON VERITAS EST FALSITAS", + Program([], [ExpressionStatement(BinOp(UnaryNot(Bool(True)), Bool(False), "KEYWORD_EST"))]), + ValBool(True)), ] class TestNon(unittest.TestCase): @@ -1272,26 +1331,15 @@ class TestFractioComparisons(unittest.TestCase): run_test(self, source, nodes, value) -class TestFractioErrors(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() - +class TestFractioHelpers(unittest.TestCase): def test_frac_to_fraction_ordering(self): with self.assertRaises(CentvrionError): frac_to_fraction(".S") # . before S violates highest-to-lowest def test_frac_to_fraction_level_overflow(self): with self.assertRaises(CentvrionError): - frac_to_fraction("SSSSSS") # 6*S = 36/12 >= 1 per level... wait S can only appear once - # Actually "SS" means S twice, which is 12/12 = 1, violating < 12/12 constraint + frac_to_fraction("SSSSSS") # SS means S twice = 12/12 = 1, violating < 12/12 constraint - -class TestFractioHelpers(unittest.TestCase): def test_frac_to_fraction_iiis(self): self.assertEqual(frac_to_fraction("IIIS"), Fraction(7, 2))