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 445ef6e..0852d54 100644 Binary files a/language/main.pdf and b/language/main.pdf differ 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))