🐐 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. `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. `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. `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 ### SVBNVLLA
![CVM SVBNVLLA](snippets/svbnvlla.png) ![CVM SVBNVLLA](snippets/svbnvlla.png)

4
cent
View File

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

View File

@@ -1,6 +1,8 @@
import http.server
import re import re
import random
import time import time
import urllib.parse
import urllib.request
from fractions import Fraction from fractions import Fraction
from rply.token import BaseBox from rply.token import BaseBox
@@ -8,6 +10,26 @@ from rply.token import BaseBox
from centvrion.errors import CentvrionError from centvrion.errors import CentvrionError
from centvrion.values import Val, ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac 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 = { NUMERALS = {
"I": 1, "I": 1,
"IV": 4, "IV": 4,
@@ -1175,7 +1197,7 @@ class BuiltIn(Node):
raise CentvrionError("FORTVITVS_NVMERVS requires two numbers") raise CentvrionError("FORTVITVS_NVMERVS requires two numbers")
if a > b: if a > b:
raise CentvrionError(f"FORTVITVS_NVMERVS: first argument ({a}) must be ≤ second ({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": case "FORTVITA_ELECTIO":
if "FORS" not in vtable["#modules"]: if "FORS" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'FORTVITA_ELECTIO' without module 'FORS'") raise CentvrionError("Cannot use 'FORTVITA_ELECTIO' without module 'FORS'")
@@ -1184,14 +1206,14 @@ class BuiltIn(Node):
lst = params[0].value() lst = params[0].value()
if len(lst) == 0: if len(lst) == 0:
raise CentvrionError("FORTVITA_ELECTIO: cannot select from an empty array") 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": case "SEMEN":
if "FORS" not in vtable["#modules"]: if "FORS" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'SEMEN' without module 'FORS'") raise CentvrionError("Cannot use 'SEMEN' without module 'FORS'")
seed = params[0].value() seed = params[0].value()
if not isinstance(seed, int): if not isinstance(seed, int):
raise CentvrionError("SEMEN requires an integer seed") raise CentvrionError("SEMEN requires an integer seed")
random.seed(seed) _cent_rng.seed(seed)
return vtable, ValNul() return vtable, ValNul()
case "DECIMATIO": case "DECIMATIO":
if "FORS" not in vtable["#modules"]: if "FORS" not in vtable["#modules"]:
@@ -1201,7 +1223,7 @@ class BuiltIn(Node):
arr = list(params[0].value()) arr = list(params[0].value())
to_remove = len(arr) // 10 to_remove = len(arr) // 10
for _ in range(to_remove): 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) return vtable, ValList(arr)
case "SENATVS": case "SENATVS":
if len(params) == 1 and isinstance(params[0], ValList): if len(params) == 1 and isinstance(params[0], ValList):
@@ -1301,6 +1323,75 @@ class BuiltIn(Node):
except re.error as e: except re.error as e:
raise CentvrionError(f"Invalid regex: {e}") raise CentvrionError(f"Invalid regex: {e}")
return vtable, ValStr(result) 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 _: case _:
raise NotImplementedError(self.builtin) raise NotImplementedError(self.builtin)
@@ -1331,6 +1422,7 @@ class Program(BaseBox):
"#continue": False, "#continue": False,
"#return": None, "#return": None,
"#modules": [m.module_name for m in self.modules], "#modules": [m.module_name for m in self.modules],
"#routes": [],
} }
last_val = ValNul() last_val = ValNul()
for statement in self.statements: for statement in self.statements:

View File

@@ -303,6 +303,29 @@ def _emit_builtin(node, ctx):
case "SVBSTITVE": case "SVBSTITVE":
lines.append(f"CentValue {tmp} = cent_svbstitve({param_vars[0]}, {param_vars[1]}, {param_vars[2]});") 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 _: case _:
raise NotImplementedError(node.builtin) raise NotImplementedError(node.builtin)

View File

@@ -2,8 +2,12 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdint.h>
#include <time.h> #include <time.h>
#include <unistd.h>
#include <regex.h> #include <regex.h>
#include <curl/curl.h>
#include <microhttpd.h>
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Global arena */ /* Global arena */
@@ -12,6 +16,21 @@
CentArena *cent_arena; CentArena *cent_arena;
int cent_magnvm = 0; 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]; jmp_buf _cent_try_stack[CENT_TRY_STACK_MAX];
int _cent_try_depth = 0; int _cent_try_depth = 0;
const char *_cent_error_msg = NULL; 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; long range = hi.ival - lo.ival + 1;
if (range <= 0) if (range <= 0)
cent_runtime_error("'FORTVITVS_NVMERVS' requires lo <= hi"); 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) { CentValue cent_fortuita_electionis(CentValue lst) {
@@ -637,7 +656,7 @@ CentValue cent_fortuita_electionis(CentValue lst) {
cent_type_error("'FORTVITA_ELECTIO' requires a list"); cent_type_error("'FORTVITA_ELECTIO' requires a list");
if (lst.lval.len == 0) if (lst.lval.len == 0)
cent_runtime_error("'FORTVITA_ELECTIO' requires a non-empty list"); 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) { CentValue cent_senatus(CentValue *args, int n) {
@@ -665,7 +684,7 @@ CentValue cent_decimatio(CentValue lst) {
cent_list_push(&result, lst.lval.items[i]); cent_list_push(&result, lst.lval.items[i]);
int to_remove = result.lval.len / 10; int to_remove = result.lval.len / 10;
for (int i = 0; i < to_remove; i++) { 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 */ /* Shift remaining elements left */
for (int j = idx; j < result.lval.len - 1; j++) for (int j = idx; j < result.lval.len - 1; j++)
result.lval.items[j] = result.lval.items[j + 1]; result.lval.items[j] = result.lval.items[j + 1];
@@ -677,7 +696,8 @@ CentValue cent_decimatio(CentValue lst) {
void cent_semen(CentValue seed) { void cent_semen(CentValue seed) {
if (seed.type != CENT_INT) if (seed.type != CENT_INT)
cent_type_error("'SEMEN' requires an integer seed"); 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) { 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); 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 */ /* Initialisation */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
void cent_init(void) { void cent_init(void) {
cent_arena = cent_arena_new(1024 * 1024); /* 1 MiB initial arena */ 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 */ void cent_adivnge(CentValue path, CentValue content); /* ADIVNGE */
CentValue cent_qvaere(CentValue pattern, CentValue text); /* QVAERE */ CentValue cent_qvaere(CentValue pattern, CentValue text); /* QVAERE */
CentValue cent_svbstitve(CentValue pattern, CentValue replacement, CentValue text); /* SVBSTITVE */ 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 */ /* Array helpers */

View File

@@ -59,7 +59,10 @@ builtin_tokens = [("BUILTIN", i) for i in [
"SCRIBE", "SCRIBE",
"ADIVNGE", "ADIVNGE",
"QVAERE", "QVAERE",
"SVBSTITVE" "SVBSTITVE",
"PETE",
"PETITVR",
"AVSCVLTA"
]] ]]
data_tokens = [ data_tokens = [
@@ -73,7 +76,8 @@ module_tokens = [("MODULE", i) for i in [
"FRACTIO", "FRACTIO",
"MAGNVM", "MAGNVM",
"SCRIPTA", "SCRIPTA",
"SVBNVLLA" "SVBNVLLA",
"RETE"
]] ]]
symbol_tokens = [ 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 \newpage
\begin{itemize} \begin{itemize}
\item \textbf{newline}: \\ Newlines are combined, so a single newline is the same as multiple. \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{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{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. \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 scope: constant.language.centvrion
builtins: 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 scope: support.function.builtin.centvrion
modules: modules:
- match: '\b(FORS|FRACTIO|MAGNVM|SCRIPTA|SVBNVLLA)\b' - match: '\b(FORS|FRACTIO|MAGNVM|RETE|SCRIPTA|SVBNVLLA)\b'
scope: support.class.module.centvrion scope: support.class.module.centvrion
keywords: keywords:

202
tests.py
View File

@@ -1,5 +1,4 @@
import os import os
import random
import subprocess import subprocess
import tempfile import tempfile
import time import time
@@ -17,6 +16,7 @@ from centvrion.ast_nodes import (
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement, ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction, String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string, fraction_to_frac, num_to_int, int_to_num, make_string,
_cent_rng,
) )
from centvrion.compiler.emitter import compile_program from centvrion.compiler.emitter import compile_program
from centvrion.errors import CentvrionError 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=[]): def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]):
random.seed(1) _cent_rng.seed(1)
lexer = Lexer().get_lexer() lexer = Lexer().get_lexer()
tokens = lexer.lex(source + "\n") tokens = lexer.lex(source + "\n")
@@ -83,6 +83,8 @@ def run_test(self, source, target_nodes, target_value, target_output="", input_l
###### Compiler Test ##### ###### Compiler Test #####
########################## ##########################
c_source = compile_program(program) 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: with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
tmp_c.write(c_source) tmp_c.write(c_source)
tmp_c_path = tmp_c.name 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 tmp_bin_path = tmp_bin.name
try: try:
subprocess.run( 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, check=True, capture_output=True,
) )
stdin_data = "".join(f"{l}\n" for l in input_lines) stdin_data = "".join(f"{l}\n" for l in input_lines)
@@ -530,22 +532,22 @@ class TestFunctions(unittest.TestCase):
builtin_tests = [ builtin_tests = [
("AVDI_NVMERVS()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(3), "", ["III"]), ("AVDI_NVMERVS()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(3), "", ["III"]),
("AVDI_NVMERVS()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(10), "", ["X"]), ("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"]), ("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([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([])", Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataArray([])]))]), ValInt(0)),
('LONGITVDO("salve")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("salve")]))]), ValInt(5)), ('LONGITVDO("salve")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("salve")]))]), ValInt(5)),
('LONGITVDO("")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("")]))]), ValInt(0)), ('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\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)\nFORTVITVS_NVMERVS(I, C)", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), Numeral("C")]))]), ValInt(82)), ("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 II) # DECIMATIO: seed 42, 10 elements → removes 1 (element III)
("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\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 # 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 # DECIMATIO: empty array → empty array
("CVM FORS\nDECIMATIO([])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DECIMATIO", [DataArray([])]))]), ValList([])), ("CVM FORS\nDIC(DECIMATIO([]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([])])]))]), ValStr("[]"), "[]\n"),
# DECIMATIO: seed 42, 20 elements → removes 2 (elements I and IV) # DECIMATIO: seed 42, 20 elements → removes 2 (elements XIII and XII)
("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\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: majority true → VERITAS
("SENATVS(VERITAS, VERITAS, FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(True), Bool(False)]))]), ValBool(True)), ("SENATVS(VERITAS, VERITAS, FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(True), Bool(False)]))]), ValBool(True)),
# SENATVS: majority false → FALSITAS # SENATVS: majority false → FALSITAS
@@ -724,6 +726,14 @@ error_tests = [
('SVBSTITVE("a", I, "c")', CentvrionError), # SVBSTITVE requires strings, not int replacement ('SVBSTITVE("a", I, "c")', CentvrionError), # SVBSTITVE requires strings, not int replacement
('SVBSTITVE("a", "b", I)', CentvrionError), # SVBSTITVE requires strings, not int text ('SVBSTITVE("a", "b", I)', CentvrionError), # SVBSTITVE requires strings, not int text
('SVBSTITVE("[", "b", "c")', CentvrionError), # SVBSTITVE invalid regex ('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): class TestErrors(unittest.TestCase):
@@ -748,7 +758,7 @@ def run_compiler_error_test(self, source):
tmp_bin_path = tmp_bin.name tmp_bin_path = tmp_bin.name
try: try:
subprocess.run( 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, check=True, capture_output=True,
) )
proc = subprocess.run([tmp_bin_path], capture_output=True, text=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 tmp_bin_path = tmp_bin.name
try: try:
subprocess.run( 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, check=True, capture_output=True,
) )
start = time.time() start = time.time()
@@ -2520,7 +2530,7 @@ class TestDormi(unittest.TestCase):
tmp_bin_path = tmp_bin.name tmp_bin_path = tmp_bin.name
try: try:
subprocess.run( 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, check=True, capture_output=True,
) )
start = time.time() start = time.time()
@@ -2554,7 +2564,7 @@ class TestScripta(unittest.TestCase):
tmp_bin_path = tmp_bin.name tmp_bin_path = tmp_bin.name
try: try:
subprocess.run( 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, check=True, capture_output=True,
) )
proc = subprocess.run([tmp_bin_path], capture_output=True, text=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) 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__": if __name__ == "__main__":
unittest.main() unittest.main()