🐐 Compiler

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

108
tests.py
View File

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