From 8c69a300a5454cfb203e6bc3124845a54b37a090 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Wed, 22 Apr 2026 09:43:58 +0200 Subject: [PATCH] :goat: Web server --- README.md | 13 +- cent | 4 +- centvrion/ast_nodes.py | 102 ++++++++++- centvrion/compiler/emit_expr.py | 23 +++ centvrion/compiler/runtime/cent_runtime.c | 194 +++++++++++++++++++- centvrion/compiler/runtime/cent_runtime.h | 3 + centvrion/lexer.py | 8 +- examples/web_server.cent | 19 ++ language/main.tex | 2 +- snippets/rete.cent | 1 + snippets/rete.png | Bin 0 -> 3362 bytes snippets/syntaxes/centvrion.sublime-syntax | 4 +- tests.py | 202 +++++++++++++++++++-- 13 files changed, 541 insertions(+), 34 deletions(-) create mode 100644 examples/web_server.cent create mode 100644 snippets/rete.cent create mode 100644 snippets/rete.png diff --git a/README.md b/README.md index bf63326..493fb79 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/cent b/cent index 0593552..594e6a2 100755 --- a/cent +++ b/cent @@ -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: diff --git a/centvrion/ast_nodes.py b/centvrion/ast_nodes.py index e2070aa..937a3a5 100644 --- a/centvrion/ast_nodes.py +++ b/centvrion/ast_nodes.py @@ -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: diff --git a/centvrion/compiler/emit_expr.py b/centvrion/compiler/emit_expr.py index 4000619..5872adb 100644 --- a/centvrion/compiler/emit_expr.py +++ b/centvrion/compiler/emit_expr.py @@ -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) diff --git a/centvrion/compiler/runtime/cent_runtime.c b/centvrion/compiler/runtime/cent_runtime.c index e91e9dc..cef3f14 100644 --- a/centvrion/compiler/runtime/cent_runtime.c +++ b/centvrion/compiler/runtime/cent_runtime.c @@ -2,8 +2,12 @@ #include #include #include +#include #include +#include #include +#include +#include /* ------------------------------------------------------------------ */ /* 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; } diff --git a/centvrion/compiler/runtime/cent_runtime.h b/centvrion/compiler/runtime/cent_runtime.h index 94e93b8..f44b97c 100644 --- a/centvrion/compiler/runtime/cent_runtime.h +++ b/centvrion/compiler/runtime/cent_runtime.h @@ -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 */ diff --git a/centvrion/lexer.py b/centvrion/lexer.py index 5e65a23..d2d2301 100644 --- a/centvrion/lexer.py +++ b/centvrion/lexer.py @@ -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 = [ diff --git a/examples/web_server.cent b/examples/web_server.cent new file mode 100644 index 0000000..dc35e2c --- /dev/null +++ b/examples/web_server.cent @@ -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) diff --git a/language/main.tex b/language/main.tex index 3ed992b..d2bbe35 100644 --- a/language/main.tex +++ b/language/main.tex @@ -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. diff --git a/snippets/rete.cent b/snippets/rete.cent new file mode 100644 index 0000000..bf5fc55 --- /dev/null +++ b/snippets/rete.cent @@ -0,0 +1 @@ +CVM RETE diff --git a/snippets/rete.png b/snippets/rete.png new file mode 100644 index 0000000000000000000000000000000000000000..01782638a67333b628b89568441591b6f3c175d0 GIT binary patch literal 3362 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 literal 0 HcmV?d00001 diff --git a/snippets/syntaxes/centvrion.sublime-syntax b/snippets/syntaxes/centvrion.sublime-syntax index 94d3a6f..3ce4715 100644 --- a/snippets/syntaxes/centvrion.sublime-syntax +++ b/snippets/syntaxes/centvrion.sublime-syntax @@ -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: diff --git a/tests.py b/tests.py index 5e2cd3c..34fe3e3 100644 --- a/tests.py +++ b/tests.py @@ -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()