🐐 Web server

This commit is contained in:
2026-04-22 09:43:58 +02:00
parent 39218485c7
commit 8c69a300a5
13 changed files with 541 additions and 34 deletions

View File

@@ -375,7 +375,7 @@ The `FORS` module allows you to add randomness to your `CENTVRION` program. It a
`FORTVITVS_NVMERVS(int, int)` picks a random int in the (inclusive) range of the two given ints.
`FORTVITA_ELECTIO(['a])` picks a random element from the given array. `FORTVITA_ELECTIO(array)` is identical to ```array[FORTVITVS_NVMERVS NVLLVS ((LONGITVDO array)-I)]```.
`FORTVITA_ELECTIO(['a])` picks a random element from the given array. `FORTVITA_ELECTIO(array)` is identical to ```array[FORTVITVS_NVMERVS(I, LONGITVDO(array))]```.
`DECIMATIO(['a])` returns a copy of the given array with a random tenth of its elements removed. Arrays with fewer than 10 elements are returned unchanged.
@@ -414,6 +414,17 @@ The `SCRIPTA` module adds file I/O to your `CENTVRION` program. It adds 3 new bu
`ADIVNGE(string, string)` appends the second argument to the file at the path given by the first argument.
### RETE
![CVM RETE](snippets/rete.png)
The `RETE` module adds networking to your `CENTVRION` program.
`PETE(string)` performs an HTTP GET request to the given URL and returns the response body as a string.
`PETITVR(string, function)` registers a GET handler for the given path. The handler function receives a single argument: a dictionary with keys `"via"` (the request path), `"quaestio"` (query string), and `"methodus"` (HTTP method). The handler's return value becomes the response body (200 OK). Unmatched paths return a 404.
`AVSCVLTA(integer)` starts an HTTP server on the given port. This call blocks indefinitely, serving registered routes. Routes must be registered with `PETITVR` before calling `AVSCVLTA`. Ports above 3999 require the `MAGNVM` module.
### SVBNVLLA
![CVM SVBNVLLA](snippets/svbnvlla.png)

4
cent
View File

@@ -61,7 +61,7 @@ def main():
with open(tmp_path, "w") as f:
f.write(c_source)
subprocess.run(
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path],
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path, "-lcurl", "-lmicrohttpd"],
check=True,
)
else:
@@ -70,7 +70,7 @@ def main():
tmp_path = tmp.name
try:
subprocess.run(
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path],
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path, "-lcurl", "-lmicrohttpd"],
check=True,
)
finally:

View File

@@ -1,6 +1,8 @@
import http.server
import re
import random
import time
import urllib.parse
import urllib.request
from fractions import Fraction
from rply.token import BaseBox
@@ -8,6 +10,26 @@ from rply.token import BaseBox
from centvrion.errors import CentvrionError
from centvrion.values import Val, ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac
class _CentRng:
"""Xorshift32 RNG — identical algorithm in Python and C runtime."""
def __init__(self, seed=None):
if seed is None:
seed = int(time.time())
self.state = seed & 0xFFFFFFFF or 1
def seed(self, s):
self.state = s & 0xFFFFFFFF or 1
def next(self):
x = self.state
x ^= (x << 13) & 0xFFFFFFFF
x ^= x >> 17
x ^= (x << 5) & 0xFFFFFFFF
self.state = x
return x
def randint(self, a, b):
return a + self.next() % (b - a + 1)
_cent_rng = _CentRng()
NUMERALS = {
"I": 1,
"IV": 4,
@@ -1175,7 +1197,7 @@ class BuiltIn(Node):
raise CentvrionError("FORTVITVS_NVMERVS requires two numbers")
if a > b:
raise CentvrionError(f"FORTVITVS_NVMERVS: first argument ({a}) must be ≤ second ({b})")
return vtable, ValInt(random.randint(a, b))
return vtable, ValInt(_cent_rng.randint(a, b))
case "FORTVITA_ELECTIO":
if "FORS" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'FORTVITA_ELECTIO' without module 'FORS'")
@@ -1184,14 +1206,14 @@ class BuiltIn(Node):
lst = params[0].value()
if len(lst) == 0:
raise CentvrionError("FORTVITA_ELECTIO: cannot select from an empty array")
return vtable, lst[random.randint(0, len(lst) - 1)]
return vtable, lst[_cent_rng.randint(0, len(lst) - 1)]
case "SEMEN":
if "FORS" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'SEMEN' without module 'FORS'")
seed = params[0].value()
if not isinstance(seed, int):
raise CentvrionError("SEMEN requires an integer seed")
random.seed(seed)
_cent_rng.seed(seed)
return vtable, ValNul()
case "DECIMATIO":
if "FORS" not in vtable["#modules"]:
@@ -1201,7 +1223,7 @@ class BuiltIn(Node):
arr = list(params[0].value())
to_remove = len(arr) // 10
for _ in range(to_remove):
arr.pop(random.randint(0, len(arr) - 1))
arr.pop(_cent_rng.randint(0, len(arr) - 1))
return vtable, ValList(arr)
case "SENATVS":
if len(params) == 1 and isinstance(params[0], ValList):
@@ -1301,6 +1323,75 @@ class BuiltIn(Node):
except re.error as e:
raise CentvrionError(f"Invalid regex: {e}")
return vtable, ValStr(result)
case "PETE":
if "RETE" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'PETE' without module 'RETE'")
url = params[0]
if not isinstance(url, ValStr):
raise CentvrionError("PETE requires a string URL")
try:
with urllib.request.urlopen(url.value()) as resp:
return vtable, ValStr(resp.read().decode("utf-8"))
except Exception as e:
raise CentvrionError(f"PETE: {e}")
case "PETITVR":
if "RETE" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'PETITVR' without module 'RETE'")
path, handler = params[0], params[1]
if not isinstance(path, ValStr):
raise CentvrionError("PETITVR requires a string path")
if not isinstance(handler, ValFunc):
raise CentvrionError("PETITVR requires a function handler")
vtable["#routes"].append((path.value(), handler))
return vtable, ValNul()
case "AVSCVLTA":
if "RETE" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'AVSCVLTA' without module 'RETE'")
port = params[0]
if not isinstance(port, ValInt):
raise CentvrionError("AVSCVLTA requires an integer port")
routes = vtable["#routes"]
if not routes:
raise CentvrionError("AVSCVLTA: no routes registered")
captured_vtable = vtable.copy()
route_map = {p: h for p, h in routes}
class _CentHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
handler = route_map.get(parsed.path)
if handler is None:
self.send_response(404)
self.end_headers()
return
request_dict = ValDict({
"via": ValStr(parsed.path),
"quaestio": ValStr(parsed.query),
"methodus": ValStr("GET"),
})
func_vtable = captured_vtable.copy()
if len(handler.params) == 1:
func_vtable[handler.params[0].name] = request_dict
func_vtable["#return"] = None
for statement in handler.body:
func_vtable, _ = statement.eval(func_vtable)
if func_vtable["#return"] is not None:
break
result = func_vtable["#return"]
if isinstance(result, ValStr):
body = result.value().encode("utf-8")
elif result is not None:
body = make_string(result, magnvm, svbnvlla).encode("utf-8")
else:
body = b""
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
server = http.server.HTTPServer(("0.0.0.0", port.value()), _CentHandler)
server.serve_forever()
return vtable, ValNul()
case _:
raise NotImplementedError(self.builtin)
@@ -1331,6 +1422,7 @@ class Program(BaseBox):
"#continue": False,
"#return": None,
"#modules": [m.module_name for m in self.modules],
"#routes": [],
}
last_val = ValNul()
for statement in self.statements:

View File

@@ -303,6 +303,29 @@ def _emit_builtin(node, ctx):
case "SVBSTITVE":
lines.append(f"CentValue {tmp} = cent_svbstitve({param_vars[0]}, {param_vars[1]}, {param_vars[2]});")
case "PETE":
if not ctx.has_module("RETE"):
lines.append('cent_runtime_error("RETE module required for PETE");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"CentValue {tmp} = cent_pete({param_vars[0]});")
case "PETITVR":
if not ctx.has_module("RETE"):
lines.append('cent_runtime_error("RETE module required for PETITVR");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"cent_petitvr({param_vars[0]}, {param_vars[1]}, _scope);")
lines.append(f"CentValue {tmp} = cent_null();")
case "AVSCVLTA":
if not ctx.has_module("RETE"):
lines.append('cent_runtime_error("RETE module required for AVSCVLTA");')
lines.append(f"CentValue {tmp} = cent_null();")
else:
lines.append(f"cent_avscvlta({param_vars[0]});")
lines.append(f"CentValue {tmp} = cent_null();")
case _:
raise NotImplementedError(node.builtin)

View File

@@ -2,8 +2,12 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <unistd.h>
#include <regex.h>
#include <curl/curl.h>
#include <microhttpd.h>
/* ------------------------------------------------------------------ */
/* Global arena */
@@ -12,6 +16,21 @@
CentArena *cent_arena;
int cent_magnvm = 0;
/* ------------------------------------------------------------------ */
/* Portable xorshift32 RNG (matches Python _CentRng) */
/* ------------------------------------------------------------------ */
static uint32_t cent_rng_state = 1;
static uint32_t cent_rng_next(void) {
uint32_t x = cent_rng_state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
cent_rng_state = x;
return x;
}
jmp_buf _cent_try_stack[CENT_TRY_STACK_MAX];
int _cent_try_depth = 0;
const char *_cent_error_msg = NULL;
@@ -629,7 +648,7 @@ CentValue cent_fortuitus_numerus(CentValue lo, CentValue hi) {
long range = hi.ival - lo.ival + 1;
if (range <= 0)
cent_runtime_error("'FORTVITVS_NVMERVS' requires lo <= hi");
return cent_int(lo.ival + rand() % range);
return cent_int(lo.ival + cent_rng_next() % range);
}
CentValue cent_fortuita_electionis(CentValue lst) {
@@ -637,7 +656,7 @@ CentValue cent_fortuita_electionis(CentValue lst) {
cent_type_error("'FORTVITA_ELECTIO' requires a list");
if (lst.lval.len == 0)
cent_runtime_error("'FORTVITA_ELECTIO' requires a non-empty list");
return lst.lval.items[rand() % lst.lval.len];
return lst.lval.items[cent_rng_next() % lst.lval.len];
}
CentValue cent_senatus(CentValue *args, int n) {
@@ -665,7 +684,7 @@ CentValue cent_decimatio(CentValue lst) {
cent_list_push(&result, lst.lval.items[i]);
int to_remove = result.lval.len / 10;
for (int i = 0; i < to_remove; i++) {
int idx = rand() % result.lval.len;
int idx = cent_rng_next() % result.lval.len;
/* Shift remaining elements left */
for (int j = idx; j < result.lval.len - 1; j++)
result.lval.items[j] = result.lval.items[j + 1];
@@ -677,7 +696,8 @@ CentValue cent_decimatio(CentValue lst) {
void cent_semen(CentValue seed) {
if (seed.type != CENT_INT)
cent_type_error("'SEMEN' requires an integer seed");
srand((unsigned)seed.ival);
cent_rng_state = (uint32_t)seed.ival;
if (cent_rng_state == 0) cent_rng_state = 1;
}
static int _ordina_comparator(const void *a, const void *b) {
@@ -995,11 +1015,175 @@ CentValue cent_svbstitve(CentValue pattern, CentValue replacement, CentValue tex
return cent_str(result);
}
/* ------------------------------------------------------------------ */
/* Networking (RETE) */
/* ------------------------------------------------------------------ */
typedef struct {
char *buf;
size_t len;
size_t cap;
} CurlBuf;
static size_t _curl_write_cb(void *data, size_t size, size_t nmemb, void *userp) {
size_t bytes = size * nmemb;
CurlBuf *cb = (CurlBuf *)userp;
while (cb->len + bytes + 1 > cb->cap) {
cb->cap = cb->cap ? cb->cap * 2 : 4096;
cb->buf = realloc(cb->buf, cb->cap);
if (!cb->buf) cent_runtime_error("PETE: out of memory");
}
memcpy(cb->buf + cb->len, data, bytes);
cb->len += bytes;
cb->buf[cb->len] = '\0';
return bytes;
}
CentValue cent_pete(CentValue url) {
if (url.type != CENT_STR)
cent_type_error("'PETE' requires a string URL");
CURL *curl = curl_easy_init();
if (!curl) cent_runtime_error("PETE: failed to init curl");
CurlBuf cb = {0};
curl_easy_setopt(curl, CURLOPT_URL, url.sval);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _curl_write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &cb);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
free(cb.buf);
cent_runtime_error(curl_easy_strerror(res));
}
char *arena_buf = cent_arena_alloc(cent_arena, cb.len + 1);
memcpy(arena_buf, cb.buf, cb.len + 1);
free(cb.buf);
return cent_str(arena_buf);
}
/* ------------------------------------------------------------------ */
/* Server (RETE) */
/* ------------------------------------------------------------------ */
#define CENT_MAX_ROUTES 64
typedef struct {
const char *path;
CentFuncInfo handler;
CentScope scope;
} CentRoute;
static CentRoute _cent_routes[CENT_MAX_ROUTES];
static int _cent_route_count = 0;
void cent_petitvr(CentValue path, CentValue handler, CentScope scope) {
if (path.type != CENT_STR)
cent_type_error("PETITVR requires a string path");
if (handler.type != CENT_FUNC)
cent_type_error("PETITVR requires a function handler");
if (_cent_route_count >= CENT_MAX_ROUTES)
cent_runtime_error("PETITVR: too many routes");
_cent_routes[_cent_route_count].path = path.sval;
_cent_routes[_cent_route_count].handler = handler.fnval;
_cent_routes[_cent_route_count].scope = cent_scope_copy(&scope);
_cent_route_count++;
}
static enum MHD_Result
_cent_request_handler(void *cls, struct MHD_Connection *conn,
const char *url, const char *method,
const char *version, const char *upload_data,
size_t *upload_data_size, void **con_cls) {
(void)cls; (void)version; (void)upload_data;
(void)upload_data_size; (void)con_cls;
/* Find matching route */
CentFuncInfo *fi = NULL;
CentScope *captured_scope = NULL;
/* Split url at '?' to match path only */
const char *qmark = strchr(url, '?');
size_t path_len = qmark ? (size_t)(qmark - url) : strlen(url);
for (int i = 0; i < _cent_route_count; i++) {
if (strlen(_cent_routes[i].path) == path_len
&& strncmp(_cent_routes[i].path, url, path_len) == 0) {
fi = &_cent_routes[i].handler;
captured_scope = &_cent_routes[i].scope;
break;
}
}
if (!fi) {
const char *msg = "CDIV — not found\n";
struct MHD_Response *resp = MHD_create_response_from_buffer(
strlen(msg), (void *)msg, MHD_RESPMEM_PERSISTENT);
enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_NOT_FOUND, resp);
MHD_destroy_response(resp);
return ret;
}
/* Build request dict: via, quaestio, methodus */
char *via_buf = cent_arena_alloc(cent_arena, path_len + 1);
memcpy(via_buf, url, path_len);
via_buf[path_len] = '\0';
const char *qs = qmark ? qmark + 1 : "";
size_t qs_len = strlen(qs);
char *qs_buf = cent_arena_alloc(cent_arena, qs_len + 1);
memcpy(qs_buf, qs, qs_len + 1);
CentValue dict = cent_dict_new(4);
cent_dict_set(&dict, cent_str("via"), cent_str(via_buf));
cent_dict_set(&dict, cent_str("quaestio"), cent_str(qs_buf));
cent_dict_set(&dict, cent_str("methodus"), cent_str(method));
/* Call handler — start from the scope captured at registration time */
CentScope call_scope = cent_scope_copy(captured_scope);
if (fi->param_count == 1) {
cent_scope_set(&call_scope, fi->param_names[0], dict);
}
CentValue result = fi->fn(call_scope);
/* Build response */
const char *body = "";
size_t body_len = 0;
if (result.type == CENT_STR) {
body = result.sval;
body_len = strlen(body);
} else if (result.type != CENT_NULL) {
char *s = cent_make_string(result);
body = s;
body_len = strlen(s);
}
struct MHD_Response *resp = MHD_create_response_from_buffer(
body_len, (void *)body, MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "text/plain");
enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_OK, resp);
MHD_destroy_response(resp);
return ret;
}
void cent_avscvlta(CentValue port) {
if (port.type != CENT_INT)
cent_type_error("AVSCVLTA requires an integer port");
if (_cent_route_count == 0)
cent_runtime_error("AVSCVLTA: no routes registered");
struct MHD_Daemon *d = MHD_start_daemon(
MHD_USE_INTERNAL_POLLING_THREAD,
(uint16_t)port.ival, NULL, NULL,
_cent_request_handler, NULL,
MHD_OPTION_END);
if (!d) cent_runtime_error("AVSCVLTA: failed to start server");
/* Block forever */
while (1) pause();
}
/* ------------------------------------------------------------------ */
/* Initialisation */
/* ------------------------------------------------------------------ */
void cent_init(void) {
cent_arena = cent_arena_new(1024 * 1024); /* 1 MiB initial arena */
srand((unsigned)time(NULL));
cent_rng_state = (uint32_t)time(NULL);
if (cent_rng_state == 0) cent_rng_state = 1;
}

View File

@@ -234,6 +234,9 @@ void cent_scribe(CentValue path, CentValue content); /* SCRIBE */
void cent_adivnge(CentValue path, CentValue content); /* ADIVNGE */
CentValue cent_qvaere(CentValue pattern, CentValue text); /* QVAERE */
CentValue cent_svbstitve(CentValue pattern, CentValue replacement, CentValue text); /* SVBSTITVE */
CentValue cent_pete(CentValue url); /* PETE */
void cent_petitvr(CentValue path, CentValue handler, CentScope scope); /* PETITVR */
void cent_avscvlta(CentValue port); /* AVSCVLTA */
/* ------------------------------------------------------------------ */
/* Array helpers */

View File

@@ -59,7 +59,10 @@ builtin_tokens = [("BUILTIN", i) for i in [
"SCRIBE",
"ADIVNGE",
"QVAERE",
"SVBSTITVE"
"SVBSTITVE",
"PETE",
"PETITVR",
"AVSCVLTA"
]]
data_tokens = [
@@ -73,7 +76,8 @@ module_tokens = [("MODULE", i) for i in [
"FRACTIO",
"MAGNVM",
"SCRIPTA",
"SVBNVLLA"
"SVBNVLLA",
"RETE"
]]
symbol_tokens = [

19
examples/web_server.cent Normal file
View File

@@ -0,0 +1,19 @@
// A simple web server with dynamic content
// Run: ./cent -i examples/web_server.cent
// Then visit http://localhost:80/ in your browser
CVM RETE
CVM FORS
DESIGNA salve VT ["SALVE", "AVE", "HAVETE", "SALVETE"]
DESIGNA sententiae VT ["Alea iacta est.", "Veni, vidi, vici.", "Carpe diem.", "Cogito, ergo svm."]
PETITVR("/", FVNCTIO (petitio) VT {
DESIGNA verbvm VT FORTVITA_ELECTIO(salve)
DESIGNA sententia VT FORTVITA_ELECTIO(sententiae)
DESIGNA a VT FORTVITVS_NVMERVS(I, VI)
DESIGNA b VT FORTVITVS_NVMERVS(I, VI)
REDI("{verbvm} MVNDE!\n\n{sententia}\n\nAlea: {a} + {b} = {a + b}")
})
DIC("Avscvlta in port MLXXX...")
AVSCVLTA(MLXXX)

View File

@@ -98,7 +98,7 @@
\newpage
\begin{itemize}
\item \textbf{newline}: \\ Newlines are combined, so a single newline is the same as multiple.
\item \textbf{module-name}: \\ Modules are flags given to the interpreter/compiler, to let it know you want to be using certain rules, functions, or features. Available modules: \texttt{FORS} (randomness), \texttt{FRACTIO} (fractions), \texttt{MAGNVM} (large integers), \texttt{SCRIPTA} (file I/O: \texttt{LEGE}, \texttt{SCRIBE}, \texttt{ADIVNGE}), \texttt{SVBNVLLA} (negative literals).
\item \textbf{module-name}: \\ Modules are flags given to the interpreter/compiler, to let it know you want to be using certain rules, functions, or features. Available modules: \texttt{FORS} (randomness), \texttt{FRACTIO} (fractions), \texttt{MAGNVM} (large integers), \texttt{SCRIPTA} (file I/O: \texttt{LEGE}, \texttt{SCRIBE}, \texttt{ADIVNGE}), \texttt{SVBNVLLA} (negative literals), \texttt{RETE} (networking: \texttt{PETE}, \texttt{PETITVR}, \texttt{AVSCVLTA}).
\item \textbf{id}: \\ Variable. Can only consist of lowercase characters and underscores, but not the letters j, u, or w.
\item \textbf{builtin}: \\ Builtin functions are uppercase latin words.
\item \textbf{string}: \\ Any text encased in \texttt{"} or \texttt{'} characters. Single-quoted strings are always literal. Strings support 1-based indexing (\texttt{string[I]}) and inclusive slicing (\texttt{string[I VSQVE III]}), returning single-character strings and substrings respectively.

1
snippets/rete.cent Normal file
View File

@@ -0,0 +1 @@
CVM RETE

BIN
snippets/rete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -70,11 +70,11 @@ contexts:
scope: constant.language.centvrion
builtins:
- match: '\b(ADIVNGE|AVDI_NVMERVS|AVDI|CLAVES|DECIMATIO|DIC|EVERRE|FORTVITVS_NVMERVS|FORTVITA_ELECTIO|LEGE|LONGITVDO|ORDINA|QVAERE|SCRIBE|SEMEN|SENATVS|SVBSTITVE)\b'
- match: '\b(ADIVNGE|AVDI_NVMERVS|AVDI|CLAVES|DECIMATIO|DIC|EVERRE|FORTVITVS_NVMERVS|FORTVITA_ELECTIO|LEGE|LONGITVDO|ORDINA|PETE|QVAERE|SCRIBE|SEMEN|SENATVS|SVBSTITVE)\b'
scope: support.function.builtin.centvrion
modules:
- match: '\b(FORS|FRACTIO|MAGNVM|SCRIPTA|SVBNVLLA)\b'
- match: '\b(FORS|FRACTIO|MAGNVM|RETE|SCRIPTA|SVBNVLLA)\b'
scope: support.class.module.centvrion
keywords:

202
tests.py
View File

@@ -1,5 +1,4 @@
import os
import random
import subprocess
import tempfile
import time
@@ -17,6 +16,7 @@ from centvrion.ast_nodes import (
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string,
_cent_rng,
)
from centvrion.compiler.emitter import compile_program
from centvrion.errors import CentvrionError
@@ -30,7 +30,7 @@ _RUNTIME_C = os.path.join(
)
def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]):
random.seed(1)
_cent_rng.seed(1)
lexer = Lexer().get_lexer()
tokens = lexer.lex(source + "\n")
@@ -83,6 +83,8 @@ def run_test(self, source, target_nodes, target_value, target_output="", input_l
###### Compiler Test #####
##########################
c_source = compile_program(program)
# Force deterministic RNG seed=1 for test reproducibility
c_source = c_source.replace("cent_init();", "cent_init(); cent_semen((CentValue){.type=CENT_INT, .ival=1});", 1)
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source)
tmp_c_path = tmp_c.name
@@ -90,7 +92,7 @@ def run_test(self, source, target_nodes, target_value, target_output="", input_l
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path],
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"],
check=True, capture_output=True,
)
stdin_data = "".join(f"{l}\n" for l in input_lines)
@@ -530,22 +532,22 @@ class TestFunctions(unittest.TestCase):
builtin_tests = [
("AVDI_NVMERVS()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(3), "", ["III"]),
("AVDI_NVMERVS()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(10), "", ["X"]),
("CVM FORS\nFORTVITVS_NVMERVS(I, X)", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), Numeral("X")]))]), ValInt(3)),
("CVM FORS\nDESIGNA a VT [I, II, III]\nDIC(a[FORTVITVS_NVMERVS(I, LONGITVDO(a))])", Program([ModuleCall("FORS")], [Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])), ExpressionStatement(BuiltIn("DIC", [ArrayIndex(ID("a"), BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), BuiltIn("LONGITVDO", [ID("a")])]))]))]), ValStr("I"), "I\n"),
("AVDI()", Program([], [ExpressionStatement(BuiltIn("AVDI", []))]), ValStr("hello"), "", ["hello"]),
("LONGITVDO([I, II, III])", Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValInt(3)),
("LONGITVDO([])", Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataArray([])]))]), ValInt(0)),
('LONGITVDO("salve")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("salve")]))]), ValInt(5)),
('LONGITVDO("")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("")]))]), ValInt(0)),
("CVM FORS\nFORTVITA_ELECTIO([I, II, III])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("FORTVITA_ELECTIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValInt(1)),
("CVM FORS\nSEMEN(XLII)\nFORTVITVS_NVMERVS(I, C)", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), Numeral("C")]))]), ValInt(82)),
# DECIMATIO: seed 42, 10 elements → removes 1 (element II)
("CVM FORS\nSEMEN(XLII)\nDECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X")])]))]), ValList([ValInt(1), ValInt(3), ValInt(4), ValInt(5), ValInt(6), ValInt(7), ValInt(8), ValInt(9), ValInt(10)])),
("CVM FORS\nDIC(FORTVITA_ELECTIO([I, II, III]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("FORTVITA_ELECTIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])])]))]), ValStr("I"), "I\n"),
("CVM FORS\nSEMEN(XLII)\nDIC(FORTVITVS_NVMERVS(I, C))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), Numeral("C")])]))]), ValStr("XXXIII"), "XXXIII\n"),
# DECIMATIO: seed 42, 10 elements → removes 1 (element III)
("CVM FORS\nSEMEN(XLII)\nDIC(DECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X")])])]))]), ValStr("[I II IV V VI VII VIII IX X]"), "[I II IV V VI VII VIII IX X]\n"),
# DECIMATIO: seed 1, 3 elements → 3//10=0, nothing removed
("CVM FORS\nSEMEN(I)\nDECIMATIO([I, II, III])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("I")])), ExpressionStatement(BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
("CVM FORS\nSEMEN(I)\nDIC(DECIMATIO([I, II, III]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("I")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])])]))]), ValStr("[I II III]"), "[I II III]\n"),
# DECIMATIO: empty array → empty array
("CVM FORS\nDECIMATIO([])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DECIMATIO", [DataArray([])]))]), ValList([])),
# DECIMATIO: seed 42, 20 elements → removes 2 (elements I and IV)
("CVM FORS\nSEMEN(XLII)\nDECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X"), Numeral("XI"), Numeral("XII"), Numeral("XIII"), Numeral("XIV"), Numeral("XV"), Numeral("XVI"), Numeral("XVII"), Numeral("XVIII"), Numeral("XIX"), Numeral("XX")])]))]), ValList([ValInt(2), ValInt(3), ValInt(5), ValInt(6), ValInt(7), ValInt(8), ValInt(9), ValInt(10), ValInt(11), ValInt(12), ValInt(13), ValInt(14), ValInt(15), ValInt(16), ValInt(17), ValInt(18), ValInt(19), ValInt(20)])),
("CVM FORS\nDIC(DECIMATIO([]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([])])]))]), ValStr("[]"), "[]\n"),
# DECIMATIO: seed 42, 20 elements → removes 2 (elements XIII and XII)
("CVM FORS\nSEMEN(XLII)\nDIC(DECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X"), Numeral("XI"), Numeral("XII"), Numeral("XIII"), Numeral("XIV"), Numeral("XV"), Numeral("XVI"), Numeral("XVII"), Numeral("XVIII"), Numeral("XIX"), Numeral("XX")])])]))]), ValStr("[I II III IV V VI VII VIII IX X XI XIV XV XVI XVII XVIII XIX XX]"), "[I II III IV V VI VII VIII IX X XI XIV XV XVI XVII XVIII XIX XX]\n"),
# SENATVS: majority true → VERITAS
("SENATVS(VERITAS, VERITAS, FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(True), Bool(False)]))]), ValBool(True)),
# SENATVS: majority false → FALSITAS
@@ -724,6 +726,14 @@ error_tests = [
('SVBSTITVE("a", I, "c")', CentvrionError), # SVBSTITVE requires strings, not int replacement
('SVBSTITVE("a", "b", I)', CentvrionError), # SVBSTITVE requires strings, not int text
('SVBSTITVE("[", "b", "c")', CentvrionError), # SVBSTITVE invalid regex
('PETE("http://example.com")', CentvrionError), # RETE required for PETE
('CVM RETE\nPETE(I)', CentvrionError), # PETE requires a string URL
('PETITVR("/", FVNCTIO (r) VT {\nREDI("hi")\n})', CentvrionError), # RETE required for PETITVR
('CVM RETE\nPETITVR(I, FVNCTIO (r) VT {\nREDI("hi")\n})', CentvrionError), # PETITVR path must be string
('CVM RETE\nPETITVR("/", "not a func")', CentvrionError), # PETITVR handler must be function
('CVM RETE\nAVSCVLTA(LXXX)', CentvrionError), # AVSCVLTA: no routes registered
('AVSCVLTA(LXXX)', CentvrionError), # RETE required for AVSCVLTA
('CVM RETE\nPETITVR("/", FVNCTIO (r) VT {\nREDI("hi")\n})\nAVSCVLTA("text")', CentvrionError), # AVSCVLTA port must be integer
]
class TestErrors(unittest.TestCase):
@@ -748,7 +758,7 @@ def run_compiler_error_test(self, source):
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path],
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"],
check=True, capture_output=True,
)
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
@@ -2483,7 +2493,7 @@ class TestDormi(unittest.TestCase):
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path],
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"],
check=True, capture_output=True,
)
start = time.time()
@@ -2520,7 +2530,7 @@ class TestDormi(unittest.TestCase):
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path],
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"],
check=True, capture_output=True,
)
start = time.time()
@@ -2554,7 +2564,7 @@ class TestScripta(unittest.TestCase):
tmp_bin_path = tmp_bin.name
try:
subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path],
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"],
check=True, capture_output=True,
)
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)
@@ -2797,5 +2807,165 @@ class TestTempta(unittest.TestCase):
run_test(self, source, nodes, value, output)
class TestRete(unittest.TestCase):
@classmethod
def setUpClass(cls):
import http.server, threading
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"SALVE MVNDE")
def log_message(self, *args):
pass
cls.server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
cls.port = cls.server.server_address[1]
cls.thread = threading.Thread(target=cls.server.serve_forever)
cls.thread.daemon = True
cls.thread.start()
@classmethod
def tearDownClass(cls):
cls.server.shutdown()
def test_pete(self):
url = f"http://127.0.0.1:{self.port}/"
source = f'CVM RETE\nPETE("{url}")'
run_test(self, source,
Program([ModuleCall("RETE")], [ExpressionStatement(BuiltIn("PETE", [String(url)]))]),
ValStr("SALVE MVNDE"))
def test_pete_dic(self):
url = f"http://127.0.0.1:{self.port}/"
source = f'CVM RETE\nDIC(PETE("{url}"))'
run_test(self, source,
Program([ModuleCall("RETE")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("PETE", [String(url)])]))]),
ValStr("SALVE MVNDE"), "SALVE MVNDE\n")
class TestReteServer(unittest.TestCase):
"""Integration tests for PETITVR + AVSCVLTA server functionality."""
def _wait_for_server(self, port, timeout=2.0):
"""Poll until the server is accepting connections."""
import socket
deadline = time.time() + timeout
while time.time() < deadline:
try:
with socket.create_connection(("127.0.0.1", port), timeout=0.1):
return
except OSError:
time.sleep(0.05)
self.fail(f"Server on port {port} did not start within {timeout}s")
def _free_port(self):
"""Find a free port in range 1024-3999 (representable without MAGNVM)."""
import socket, random
for _ in range(100):
port = random.randint(1024, 3999)
try:
with socket.socket() as s:
s.bind(("127.0.0.1", port))
return port
except OSError:
continue
raise RuntimeError("Could not find a free port in range 1024-3999")
def _run_server(self, source):
"""Parse and eval source in a daemon thread. Returns when server is ready."""
import threading
lexer = Lexer().get_lexer()
tokens = lexer.lex(source + "\n")
program = Parser().parse(tokens)
t = threading.Thread(target=program.eval, daemon=True)
t.start()
def test_basic_handler(self):
port = self._free_port()
source = (
f'CVM RETE\n'
f'PETITVR("/", FVNCTIO (petitio) VT {{\n'
f'REDI("SALVE MVNDE")\n'
f'}})\n'
f'AVSCVLTA({int_to_num(port, False)})'
)
self._run_server(source)
self._wait_for_server(port)
import urllib.request
resp = urllib.request.urlopen(f"http://127.0.0.1:{port}/")
self.assertEqual(resp.read().decode(), "SALVE MVNDE")
def test_multiple_routes(self):
port = self._free_port()
source = (
f'CVM RETE\n'
f'PETITVR("/", FVNCTIO (p) VT {{\nREDI("RADIX")\n}})\n'
f'PETITVR("/nomen", FVNCTIO (p) VT {{\nREDI("MARCVS")\n}})\n'
f'AVSCVLTA({int_to_num(port, False)})'
)
self._run_server(source)
self._wait_for_server(port)
import urllib.request
resp1 = urllib.request.urlopen(f"http://127.0.0.1:{port}/")
self.assertEqual(resp1.read().decode(), "RADIX")
resp2 = urllib.request.urlopen(f"http://127.0.0.1:{port}/nomen")
self.assertEqual(resp2.read().decode(), "MARCVS")
def test_404_unmatched(self):
port = self._free_port()
source = (
f'CVM RETE\n'
f'PETITVR("/", FVNCTIO (p) VT {{\nREDI("ok")\n}})\n'
f'AVSCVLTA({int_to_num(port, False)})'
)
self._run_server(source)
self._wait_for_server(port)
import urllib.request, urllib.error
with self.assertRaises(urllib.error.HTTPError) as ctx:
urllib.request.urlopen(f"http://127.0.0.1:{port}/nonexistent")
self.assertEqual(ctx.exception.code, 404)
def test_request_dict_via(self):
port = self._free_port()
source = (
f'CVM RETE\n'
f'PETITVR("/echo", FVNCTIO (petitio) VT {{\n'
f'REDI(petitio["via"])\n'
f'}})\n'
f'AVSCVLTA({int_to_num(port, False)})'
)
self._run_server(source)
self._wait_for_server(port)
import urllib.request
resp = urllib.request.urlopen(f"http://127.0.0.1:{port}/echo")
self.assertEqual(resp.read().decode(), "/echo")
def test_request_dict_quaestio(self):
port = self._free_port()
source = (
f'CVM RETE\n'
f'PETITVR("/q", FVNCTIO (petitio) VT {{\n'
f'REDI(petitio["quaestio"])\n'
f'}})\n'
f'AVSCVLTA({int_to_num(port, False)})'
)
self._run_server(source)
self._wait_for_server(port)
import urllib.request
resp = urllib.request.urlopen(f"http://127.0.0.1:{port}/q?nomen=Marcus")
self.assertEqual(resp.read().decode(), "nomen=Marcus")
def test_petitvr_stores_route(self):
"""PETITVR alone (without AVSCVLTA) just stores a route and returns NVLLVS."""
source = 'CVM RETE\nPETITVR("/", FVNCTIO (p) VT {\nREDI("hi")\n})'
run_test(self, source,
Program([ModuleCall("RETE")], [ExpressionStatement(BuiltIn("PETITVR", [
String("/"),
Fvnctio([ID("p")], [Redi([String("hi")])])
]))]),
ValNul())
if __name__ == "__main__":
unittest.main()