Compare commits

...

4 Commits

Author SHA1 Message Date
940f8d7311 🐐 Escape sequences 2026-04-22 11:27:19 +02:00
8c69a300a5 🐐 Web server 2026-04-22 11:11:58 +02:00
39218485c7 🐐 SVBSTITVE 2026-04-22 09:30:58 +02:00
b9a1ed1bcc 🐐 QVAERE 2026-04-22 09:08:36 +02:00
19 changed files with 866 additions and 318 deletions

View File

@@ -349,6 +349,16 @@ Returns the type of `value` as a string: `NVMERVS` (integer), `LITTERA` (string)
Sleeps for `n` seconds, where `n` is an integer, fraction, or NVLLVS (treated as 0). Returns nothing meaningful.
### QVAERE
`QVAERE(pattern, string)`
Returns an array of all non-overlapping matches of the regex `pattern` in `string`. Both arguments must be strings. Patterns use extended regular expression syntax. Returns an empty array if there are no matches. Raises an error if the pattern is invalid.
### SVBSTITVE
`SVBSTITVE(pattern, replacement, string)`
Replaces all non-overlapping matches of the regex `pattern` in `string` with `replacement`. All three arguments must be strings. The replacement string supports backreferences (`\1`, `\2`, etc.) to captured groups. Returns the resulting string. Raises an error if the pattern is invalid.
## Modules
Modules are additions to the base `CENTVRION` syntax. They add or change certain features. Modules are included in your code by having
@@ -361,15 +371,15 @@ Vnlike many other programming languages with modules, the modules in `CENTVRION`
### FORS
![CVM FORS](snippets/fors.png)
The `FORS` module allows you to add randomness to your `CENTVRION` program. It adds 4 new built-in functions: `FORTVITVS_NVMERVS int int`, `FORTVITA_ELECTIO ['a]`, `DECIMATIO ['a]`, and `SEMEN int`.
The `FORS` module allows you to add randomness to your `CENTVRION` program. It adds 4 new built-in functions: `FORTVITVS_NVMERVS(int, int)`, `FORTVITA_ELECTIO(['a])`, `DECIMATIO(['a])`, and `SEMEN(int)`.
`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.
`SEMEN int` seeds the random number generator for reproducibility.
`SEMEN(int)` seeds the random number generator for reproducibility.
### FRACTIO
![CVM FRACTIO](snippets/fractio.png)
@@ -394,14 +404,26 @@ When `_` is added _after_ a numeric symbol, the symbol becomes 1.000 times large
All integer symbols except `I` may be given a `_`.
### SCRIPTA
![CVM SCRIPTA](snippets/scripta.png)
The `SCRIPTA` module adds file I/O to your `CENTVRION` program. It adds 3 new built-in functions: `LEGE string`, `SCRIBE string string`, and `ADIVNGE string string`.
The `SCRIPTA` module adds file I/O to your `CENTVRION` program. It adds 3 new built-in functions: `LEGE`, `SCRIBE`, and `ADIVNGE`.
`LEGE string` reads the contents of the file at the given path and returns them as a string.
`LEGE(string)` reads the contents of the file at the given path and returns them as a string.
`SCRIBE string string` writes the second argument to the file at the path given by the first argument, overwriting any existing content.
`SCRIBE(string, string)` writes the second argument to the file at the path given by the first argument, overwriting any existing content.
`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
![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,
@@ -321,10 +343,15 @@ class String(Node):
return f"String({self.value})"
def print(self):
v = (self.value
.replace('\\', '\\\\')
.replace('\n', '\\n')
.replace('\t', '\\t')
.replace('\r', '\\r'))
if self.quote == "'":
return f"'{self.value}'"
escaped = self.value.replace('{', '{{').replace('}', '}}')
return f'"{escaped}"'
return f"'{v}'"
v = v.replace('"', '\\"').replace('{', '{{').replace('}', '}}')
return f'"{v}"'
def _eval(self, vtable):
return vtable, ValStr(self.value)
@@ -1175,7 +1202,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 +1211,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 +1228,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):
@@ -1277,6 +1304,99 @@ class BuiltIn(Node):
with open(path, "a") as f:
f.write(content)
return vtable, ValNul()
case "QVAERE":
pattern = params[0]
text = params[1]
if not isinstance(pattern, ValStr) or not isinstance(text, ValStr):
raise CentvrionError("QVAERE requires two strings")
try:
matches = [
ValStr(m.group(0))
for m in re.finditer(pattern.value(), text.value())
]
except re.error as e:
raise CentvrionError(f"Invalid regex: {e}")
return vtable, ValList(matches)
case "SVBSTITVE":
pattern = params[0]
replacement = params[1]
text = params[2]
if not isinstance(pattern, ValStr) or not isinstance(replacement, ValStr) or not isinstance(text, ValStr):
raise CentvrionError("SVBSTITVE requires three strings")
try:
result = re.sub(pattern.value(), replacement.value(), text.value())
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)
@@ -1307,6 +1427,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

@@ -297,6 +297,35 @@ def _emit_builtin(node, ctx):
lines.append(f"cent_adivnge({param_vars[0]}, {param_vars[1]});")
lines.append(f"CentValue {tmp} = cent_null();")
case "QVAERE":
lines.append(f"CentValue {tmp} = cent_qvaere({param_vars[0]}, {param_vars[1]});")
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,7 +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 */
@@ -11,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;
@@ -628,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) {
@@ -636,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) {
@@ -664,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];
@@ -676,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) {
@@ -870,11 +891,299 @@ CentValue cent_dict_keys(CentValue dict) {
return result;
}
/* ------------------------------------------------------------------ */
/* Regex */
/* ------------------------------------------------------------------ */
CentValue cent_qvaere(CentValue pattern, CentValue text) {
if (pattern.type != CENT_STR || text.type != CENT_STR)
cent_type_error("'QVAERE' requires two strings");
regex_t re;
int rc = regcomp(&re, pattern.sval, REG_EXTENDED);
if (rc != 0) {
char errbuf[256];
regerror(rc, &re, errbuf, sizeof(errbuf));
regfree(&re);
cent_runtime_error(errbuf);
}
CentValue result = cent_list_new(8);
const char *cursor = text.sval;
regmatch_t match;
while (*cursor && regexec(&re, cursor, 1, &match, 0) == 0) {
int len = match.rm_eo - match.rm_so;
char *buf = cent_arena_alloc(cent_arena, len + 1);
memcpy(buf, cursor + match.rm_so, len);
buf[len] = '\0';
cent_list_push(&result, cent_str(buf));
cursor += match.rm_eo;
if (len == 0) cursor++; // avoid infinite loop on zero-length match
}
regfree(&re);
return result;
}
/* Expand replacement string, substituting \1..\9 with captured groups */
static void _expand_replacement(const char *repl, const char *subject,
regmatch_t *matches, int ngroups,
char **out, size_t *opos, size_t *ocap) {
for (const char *r = repl; *r; r++) {
if (*r == '\\' && r[1] >= '1' && r[1] <= '9') {
int g = r[1] - '0';
r++;
if (g < ngroups && matches[g].rm_so != -1) {
size_t glen = matches[g].rm_eo - matches[g].rm_so;
while (*opos + glen + 1 > *ocap) {
*ocap *= 2;
char *newbuf = cent_arena_alloc(cent_arena, *ocap);
memcpy(newbuf, *out, *opos);
*out = newbuf;
}
memcpy(*out + *opos, subject + matches[g].rm_so, glen);
*opos += glen;
}
} else if (*r == '\\' && r[1] == '\\') {
/* escaped backslash → literal \ */
if (*opos + 2 > *ocap) {
*ocap *= 2;
char *newbuf = cent_arena_alloc(cent_arena, *ocap);
memcpy(newbuf, *out, *opos);
*out = newbuf;
}
(*out)[(*opos)++] = '\\';
r++;
} else {
if (*opos + 2 > *ocap) {
*ocap *= 2;
char *newbuf = cent_arena_alloc(cent_arena, *ocap);
memcpy(newbuf, *out, *opos);
*out = newbuf;
}
(*out)[(*opos)++] = *r;
}
}
}
CentValue cent_svbstitve(CentValue pattern, CentValue replacement, CentValue text) {
if (pattern.type != CENT_STR || replacement.type != CENT_STR || text.type != CENT_STR)
cent_type_error("'SVBSTITVE' requires three strings");
regex_t re;
int rc = regcomp(&re, pattern.sval, REG_EXTENDED);
if (rc != 0) {
char errbuf[256];
regerror(rc, &re, errbuf, sizeof(errbuf));
regfree(&re);
cent_runtime_error(errbuf);
}
size_t text_len = strlen(text.sval);
size_t repl_len = strlen(replacement.sval);
size_t cap = text_len + repl_len * 4 + 1;
char *result = cent_arena_alloc(cent_arena, cap);
size_t rpos = 0;
const char *cursor = text.sval;
int ngroups = (int)re.re_nsub + 1;
if (ngroups > 10) ngroups = 10;
regmatch_t matches[10];
while (*cursor && regexec(&re, cursor, ngroups, matches, 0) == 0) {
/* copy text before match */
size_t prefix_len = matches[0].rm_so;
while (rpos + prefix_len + 1 > cap) {
cap *= 2;
char *newbuf = cent_arena_alloc(cent_arena, cap);
memcpy(newbuf, result, rpos);
result = newbuf;
}
memcpy(result + rpos, cursor, prefix_len);
rpos += prefix_len;
/* expand replacement with backreferences */
_expand_replacement(replacement.sval, cursor, matches, ngroups,
&result, &rpos, &cap);
cursor += matches[0].rm_eo;
if (matches[0].rm_eo == 0) cursor++;
}
/* copy remaining text */
size_t tail_len = strlen(cursor);
while (rpos + tail_len + 1 > cap) {
cap *= 2;
char *newbuf = cent_arena_alloc(cent_arena, cap);
memcpy(newbuf, result, rpos);
result = newbuf;
}
memcpy(result + rpos, cursor, tail_len);
rpos += tail_len;
result[rpos] = '\0';
regfree(&re);
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

@@ -232,6 +232,11 @@ CentValue cent_ordina(CentValue lst); /* ORDINA */
CentValue cent_lege(CentValue path); /* LEGE */
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

@@ -57,11 +57,16 @@ builtin_tokens = [("BUILTIN", i) for i in [
"TYPVS",
"LEGE",
"SCRIBE",
"ADIVNGE"
"ADIVNGE",
"QVAERE",
"SVBSTITVE",
"PETE",
"PETITVR",
"AVSCVLTA"
]]
data_tokens = [
("DATA_STRING", r"(\".*?\"|'.*?')"),
("DATA_STRING", r'("(?:[^"\\]|\\.)*"|' + r"'(?:[^'\\]|\\.)*')"),
("DATA_FRACTION", r"([IVXLCDM][IVXLCDM_]*)?([S][S:.|]*|:[S:.|]+|\.[S:.|]*)"),
("DATA_NUMERAL", r"[IVXLCDM][IVXLCDM_]*")
]
@@ -71,7 +76,8 @@ module_tokens = [("MODULE", i) for i in [
"FRACTIO",
"MAGNVM",
"SCRIPTA",
"SVBNVLLA"
"SVBNVLLA",
"RETE"
]]
symbol_tokens = [

View File

@@ -7,19 +7,61 @@ from . import ast_nodes
ALL_TOKENS = list(set([i[0] for i in all_tokens]))
_ESCAPE_MAP = {
'n': '\n',
't': '\t',
'r': '\r',
'\\': '\\',
'"': '"',
"'": "'",
}
def _read_escape(s, i):
"""Read a backslash escape at position i (the backslash). Returns (char, new_i)."""
if i + 1 >= len(s):
raise CentvrionError("Trailing backslash in string")
nxt = s[i + 1]
if nxt in _ESCAPE_MAP:
return _ESCAPE_MAP[nxt], i + 2
# unknown escapes pass through literally (e.g. \1 for regex backrefs)
return '\\' + nxt, i + 2
def _unescape(s):
"""Process escape sequences in a string with no interpolation."""
out = []
i = 0
while i < len(s):
if s[i] == '\\':
ch, i = _read_escape(s, i)
out.append(ch)
else:
out.append(s[i])
i += 1
return ''.join(out)
def _parse_interpolated(raw_value):
quote_char = raw_value[0]
inner = raw_value[1:-1]
if quote_char == "'" or len(inner) == 0:
if len(inner) == 0:
return ast_nodes.String(inner)
if quote_char == "'":
return ast_nodes.String(_unescape(inner))
parts = []
i = 0
current = []
while i < len(inner):
ch = inner[i]
if ch == '\\':
c, i = _read_escape(inner, i)
current.append(c)
continue
if ch == '{':
if i + 1 < len(inner) and inner[i + 1] == '{':
current.append('{')

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

@@ -1,2 +0,0 @@
\relax
\gdef \@abspage@last{2}

View File

@@ -1,269 +0,0 @@
This is XeTeX, Version 3.141592653-2.6-0.999998 (TeX Live 2026/Arch Linux) (preloaded format=xelatex 2026.4.8) 21 APR 2026 22:51
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**main.tex
(./main.tex
LaTeX2e <2025-11-01>
L3 programming layer <2026-01-19>
(/usr/share/texmf-dist/tex/latex/base/article.cls
Document Class: article 2025/01/22 v1.4n Standard LaTeX document class
(/usr/share/texmf-dist/tex/latex/base/size10.clo
File: size10.clo 2025/01/22 v1.4n Standard LaTeX file (size option)
)
\c@part=\count271
\c@section=\count272
\c@subsection=\count273
\c@subsubsection=\count274
\c@paragraph=\count275
\c@subparagraph=\count276
\c@figure=\count277
\c@table=\count278
\abovecaptionskip=\skip49
\belowcaptionskip=\skip50
\bibindent=\dimen148
)
(/usr/share/texmf-dist/tex/latex/geometry/geometry.sty
Package: geometry 2020/01/02 v5.9 Page Geometry
(/usr/share/texmf-dist/tex/latex/graphics/keyval.sty
Package: keyval 2022/05/29 v1.15 key=value parser (DPC)
\KV@toks@=\toks17
)
(/usr/share/texmf-dist/tex/generic/iftex/ifvtex.sty
Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead.
(/usr/share/texmf-dist/tex/generic/iftex/iftex.sty
Package: iftex 2024/12/12 v1.0g TeX engine tests
))
\Gm@cnth=\count279
\Gm@cntv=\count280
\c@Gm@tempcnt=\count281
\Gm@bindingoffset=\dimen149
\Gm@wd@mp=\dimen150
\Gm@odd@mp=\dimen151
\Gm@even@mp=\dimen152
\Gm@layoutwidth=\dimen153
\Gm@layoutheight=\dimen154
\Gm@layouthoffset=\dimen155
\Gm@layoutvoffset=\dimen156
\Gm@dimlist=\toks18
)
(/usr/share/texmf-dist/tex/latex/fontspec/fontspec.sty
(/usr/share/texmf-dist/tex/latex/l3packages/xparse/xparse.sty
(/usr/share/texmf-dist/tex/latex/l3kernel/expl3.sty
Package: expl3 2026-01-19 L3 programming layer (loader)
(/usr/share/texmf-dist/tex/latex/l3backend/l3backend-xetex.def
File: l3backend-xetex.def 2025-10-09 L3 backend support: XeTeX
\g__graphics_track_int=\count282
\g__pdfannot_backend_int=\count283
\g__pdfannot_backend_link_int=\count284
))
Package: xparse 2025-10-09 L3 Experimental document command parser
)
Package: fontspec 2025/09/29 v2.9g Font selection for XeLaTeX and LuaLaTeX
(/usr/share/texmf-dist/tex/latex/fontspec/fontspec-xetex.sty
Package: fontspec-xetex 2025/09/29 v2.9g Font selection for XeLaTeX and LuaLaTe
X
\l__fontspec_script_int=\count285
\l__fontspec_language_int=\count286
\l__fontspec_strnum_int=\count287
\l__fontspec_tmp_int=\count288
\l__fontspec_tmpa_int=\count289
\l__fontspec_tmpb_int=\count290
\l__fontspec_tmpc_int=\count291
\l__fontspec_em_int=\count292
\l__fontspec_emdef_int=\count293
\l__fontspec_strong_int=\count294
\l__fontspec_strongdef_int=\count295
\l__fontspec_tmpa_dim=\dimen157
\l__fontspec_tmpb_dim=\dimen158
\l__fontspec_tmpc_dim=\dimen159
(/usr/share/texmf-dist/tex/latex/base/fontenc.sty
Package: fontenc 2025/07/18 v2.1d Standard LaTeX package
)
(/usr/share/texmf-dist/tex/latex/fontspec/fontspec.cfg)))
Package fontspec Info:
(fontspec) Hurmit Nerd Font Mono scale = 0.7.
Package fontspec Info:
(fontspec) Hurmit Nerd Font Mono scale = 0.7.
Package fontspec Info:
(fontspec) Hurmit Nerd Font Mono/B scale = 0.7.
Package fontspec Info:
(fontspec) Hurmit Nerd Font Mono/I scale = 0.7.
Package fontspec Info:
(fontspec) Hurmit Nerd Font Mono/BI scale = 0.7.
Package fontspec Info:
(fontspec) Font family 'HurmitNerdFontMono(0)' created for font
(fontspec) 'Hurmit Nerd Font Mono' with options
(fontspec) [WordSpace={1,0,0},HyphenChar=None,PunctuationSpace=Word
Space,Scale=0.7].
(fontspec)
(fontspec) This font family consists of the following NFSS
(fontspec) series/shapes:
(fontspec)
(fontspec) - 'normal' (m/n) with NFSS spec.: <->s*[0.7]"Hurmit
(fontspec) Nerd Font Mono/OT:script=DFLT;language=dflt;"
(fontspec) - 'bold' (b/n) with NFSS spec.: <->s*[0.7]"Hurmit Nerd
(fontspec) Font Mono/B/OT:script=DFLT;language=dflt;"
(fontspec) - 'italic' (m/it) with NFSS spec.: <->s*[0.7]"Hurmit
(fontspec) Nerd Font Mono/I/OT:script=DFLT;language=dflt;"
(fontspec) - 'bold italic' (b/it) with NFSS spec.:
(fontspec) <->s*[0.7]"Hurmit Nerd Font
(fontspec) Mono/BI/OT:script=DFLT;language=dflt;"
No file main.aux.
\openout1 = `main.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for TU/lmr/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 11.
LaTeX Font Info: ... okay on input line 11.
*geometry* driver: auto-detecting
*geometry* detected driver: xetex
*geometry* verbose mode - [ preamble ] result:
* driver: xetex
* paper: a4paper
* layout: <same size as paper>
* layoutoffset:(h,v)=(0.0pt,0.0pt)
* modes:
* h-part:(L,W,R)=(72.26999pt, 452.9679pt, 72.26999pt)
* v-part:(T,H,B)=(72.26999pt, 700.50687pt, 72.26999pt)
* \paperwidth=597.50787pt
* \paperheight=845.04684pt
* \textwidth=452.9679pt
* \textheight=700.50687pt
* \oddsidemargin=0.0pt
* \evensidemargin=0.0pt
* \topmargin=-37.0pt
* \headheight=12.0pt
* \headsep=25.0pt
* \topskip=10.0pt
* \footskip=30.0pt
* \marginparwidth=57.0pt
* \marginparsep=11.0pt
* \columnsep=10.0pt
* \skip\footins=9.0pt plus 4.0pt minus 2.0pt
* \hoffset=0.0pt
* \voffset=0.0pt
* \mag=1000
* \@twocolumnfalse
* \@twosidefalse
* \@mparswitchfalse
* \@reversemarginfalse
* (1in=72.27pt=25.4mm, 1cm=28.453pt)
Package fontspec Info:
(fontspec) Adjusting the maths setup (use [no-math] to avoid
(fontspec) this).
\symlegacymaths=\mathgroup4
LaTeX Font Info: Overwriting symbol font `legacymaths' in version `bold'
(Font) OT1/cmr/m/n --> OT1/cmr/bx/n on input line 11.
LaTeX Font Info: Redeclaring math accent \acute on input line 11.
LaTeX Font Info: Redeclaring math accent \grave on input line 11.
LaTeX Font Info: Redeclaring math accent \ddot on input line 11.
LaTeX Font Info: Redeclaring math accent \tilde on input line 11.
LaTeX Font Info: Redeclaring math accent \bar on input line 11.
LaTeX Font Info: Redeclaring math accent \breve on input line 11.
LaTeX Font Info: Redeclaring math accent \check on input line 11.
LaTeX Font Info: Redeclaring math accent \hat on input line 11.
LaTeX Font Info: Redeclaring math accent \dot on input line 11.
LaTeX Font Info: Redeclaring math accent \mathring on input line 11.
LaTeX Font Info: Redeclaring math symbol \colon on input line 11.
LaTeX Font Info: Redeclaring math symbol \Gamma on input line 11.
LaTeX Font Info: Redeclaring math symbol \Delta on input line 11.
LaTeX Font Info: Redeclaring math symbol \Theta on input line 11.
LaTeX Font Info: Redeclaring math symbol \Lambda on input line 11.
LaTeX Font Info: Redeclaring math symbol \Xi on input line 11.
LaTeX Font Info: Redeclaring math symbol \Pi on input line 11.
LaTeX Font Info: Redeclaring math symbol \Sigma on input line 11.
LaTeX Font Info: Redeclaring math symbol \Upsilon on input line 11.
LaTeX Font Info: Redeclaring math symbol \Phi on input line 11.
LaTeX Font Info: Redeclaring math symbol \Psi on input line 11.
LaTeX Font Info: Redeclaring math symbol \Omega on input line 11.
LaTeX Font Info: Redeclaring math symbol \mathdollar on input line 11.
LaTeX Font Info: Redeclaring symbol font `operators' on input line 11.
LaTeX Font Info: Encoding `OT1' has changed to `TU' for symbol font
(Font) `operators' in the math version `normal' on input line 11.
LaTeX Font Info: Overwriting symbol font `operators' in version `normal'
(Font) OT1/cmr/m/n --> TU/lmr/m/n on input line 11.
LaTeX Font Info: Encoding `OT1' has changed to `TU' for symbol font
(Font) `operators' in the math version `bold' on input line 11.
LaTeX Font Info: Overwriting symbol font `operators' in version `bold'
(Font) OT1/cmr/bx/n --> TU/lmr/m/n on input line 11.
LaTeX Font Info: Overwriting symbol font `operators' in version `normal'
(Font) TU/lmr/m/n --> TU/lmr/m/n on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathit' in version `normal'
(Font) OT1/cmr/m/it --> TU/lmr/m/it on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathbf' in version `normal'
(Font) OT1/cmr/bx/n --> TU/lmr/b/n on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathsf' in version `normal'
(Font) OT1/cmss/m/n --> TU/lmss/m/n on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathtt' in version `normal'
(Font) OT1/cmtt/m/n --> TU/HurmitNerdFontMono(0)/m/n on input
line 11.
LaTeX Font Info: Overwriting symbol font `operators' in version `bold'
(Font) TU/lmr/m/n --> TU/lmr/b/n on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathit' in version `bold'
(Font) OT1/cmr/bx/it --> TU/lmr/b/it on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathsf' in version `bold'
(Font) OT1/cmss/bx/n --> TU/lmss/b/n on input line 11.
LaTeX Font Info: Overwriting math alphabet `\mathtt' in version `bold'
(Font) OT1/cmtt/m/n --> TU/HurmitNerdFontMono(0)/b/n on input
line 11.
LaTeX Font Info: External font `cmex10' loaded for size
(Font) <7> on input line 14.
LaTeX Font Info: External font `cmex10' loaded for size
(Font) <5> on input line 14.
LaTeX Font Info: Font shape `TU/HurmitNerdFontMono(0)/m/n' will be
(Font) scaled to size 6.99997pt on input line 22.
LaTeX Warning: Float too large for page by 44.293pt on input line 93.
[1
] [2] (./main.aux)
***********
LaTeX2e <2025-11-01>
L3 programming layer <2026-01-19>
***********
)
Here is how much of TeX's memory you used:
3526 strings out of 470191
106539 string characters out of 5479698
562689 words of memory out of 5000000
32135 multiletter control sequences out of 15000+600000
627857 words of font info for 57 fonts, out of 8000000 for 9000
14 hyphenation exceptions out of 8191
73i,9n,93p,432b,328s stack positions out of 10000i,1000n,20000p,200000b,200000s
Output written on main.pdf (2 pages).

Binary file not shown.

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

1
snippets/scripta.cent Normal file
View File

@@ -0,0 +1 @@
CVM SCRIPTA

BIN
snippets/scripta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 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|SCRIBE|SEMEN|SENATVS)\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:

296
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
@@ -606,6 +608,36 @@ builtin_tests = [
("TYPVS(FVNCTIO () VT { REDI(I) })", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Fvnctio([], [Redi([Numeral("I")])])]))]), ValStr("FVNCTIO")),
# TYPVS: null
("TYPVS(NVLLVS)", Program([], [ExpressionStatement(BuiltIn("TYPVS", [Nullus()]))]), ValStr("NVLLVS")),
# QVAERE: basic literal match
('QVAERE("ab", "abcabc")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("ab"), String("abcabc")]))]), ValList([ValStr("ab"), ValStr("ab")])),
# QVAERE: no match → empty list
('QVAERE("xyz", "abc")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("xyz"), String("abc")]))]), ValList([])),
# QVAERE: regex character class
('QVAERE("[a-z]+", "abc123def")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("[a-z]+"), String("abc123def")]))]), ValList([ValStr("abc"), ValStr("def")])),
# QVAERE: empty text → empty list
('QVAERE("a", "")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("a"), String("")]))]), ValList([])),
# QVAERE: capture groups still return full match
('QVAERE("(a)(b)", "ab")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("(a)(b)"), String("ab")]))]), ValList([ValStr("ab")])),
# QVAERE: empty pattern matches between every character
('QVAERE("", "ab")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String(""), String("ab")]))]), ValList([ValStr(""), ValStr(""), ValStr("")])),
# QVAERE: dot matches any character
('QVAERE(".", "ab")', Program([], [ExpressionStatement(BuiltIn("QVAERE", [String("."), String("ab")]))]), ValList([ValStr("a"), ValStr("b")])),
# SVBSTITVE: basic literal replacement
('SVBSTITVE("a", "b", "aaa")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("a"), String("b"), String("aaa")]))]), ValStr("bbb")),
# SVBSTITVE: regex character class
('SVBSTITVE("[0-9]+", "N", "abc123def456")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("[0-9]+"), String("N"), String("abc123def456")]))]), ValStr("abcNdefN")),
# SVBSTITVE: no match → string unchanged
('SVBSTITVE("x", "y", "abc")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("x"), String("y"), String("abc")]))]), ValStr("abc")),
# SVBSTITVE: empty replacement (deletion)
('SVBSTITVE("a", "", "banana")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("a"), String(""), String("banana")]))]), ValStr("bnn")),
# SVBSTITVE: empty text → empty string
('SVBSTITVE("a", "b", "")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("a"), String("b"), String("")]))]), ValStr("")),
# SVBSTITVE: dot matches any character
('SVBSTITVE(".", "x", "ab")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("."), String("x"), String("ab")]))]), ValStr("xx")),
# SVBSTITVE: backreference swaps two groups
('SVBSTITVE("(a)(b)", "\\2\\1", "ab")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("(a)(b)"), String("\\2\\1"), String("ab")]))]), ValStr("ba")),
# SVBSTITVE: backreference with unmatched group (ignored)
('SVBSTITVE("(a)(b)?", "\\1\\2", "a")', Program([], [ExpressionStatement(BuiltIn("SVBSTITVE", [String("(a)(b)?"), String("\\1\\2"), String("a")]))]), ValStr("a")),
]
class TestBuiltins(unittest.TestCase):
@@ -687,6 +719,21 @@ error_tests = [
("CVM FRACTIO\n[I, II, III][I VSQVE IIIS]", CentvrionError), # slice with fractional upper bound
("CVM FRACTIO\n[I, II, III][I / II VSQVE III]", CentvrionError), # slice with division-fraction lower bound
("TEMPTA {\nDESIGNA x VT I / NVLLVS\n} CAPE e {\nDESIGNA y VT I / NVLLVS\n}", CentvrionError), # uncaught error in catch block propagates
('QVAERE(I, "abc")', CentvrionError), # QVAERE requires strings, not int
('QVAERE("abc", I)', CentvrionError), # QVAERE requires strings, not int
('QVAERE("[", "abc")', CentvrionError), # QVAERE invalid regex
('SVBSTITVE(I, "b", "c")', CentvrionError), # SVBSTITVE requires strings, not int pattern
('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):
@@ -711,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)
@@ -1033,6 +1080,63 @@ class TestInterpolation(unittest.TestCase):
run_test(self, source, nodes, value, output)
# --- Escape sequences ---
escape_tests = [
# \n → newline
('"hello\\nworld"',
Program([], [ExpressionStatement(String("hello\nworld"))]),
ValStr("hello\nworld")),
# \t → tab
('"col\\tcol"',
Program([], [ExpressionStatement(String("col\tcol"))]),
ValStr("col\tcol")),
# \r → carriage return
('"line\\rover"',
Program([], [ExpressionStatement(String("line\rover"))]),
ValStr("line\rover")),
# \\ → literal backslash
('"back\\\\slash"',
Program([], [ExpressionStatement(String("back\\slash"))]),
ValStr("back\\slash")),
# \" → literal double quote
('"say \\"salve\\""',
Program([], [ExpressionStatement(String('say "salve"'))]),
ValStr('say "salve"')),
# \' → literal single quote in single-quoted string
("'it\\'s'",
Program([], [ExpressionStatement(String("it's"))]),
ValStr("it's")),
# \n in single-quoted string
("'hello\\nworld'",
Program([], [ExpressionStatement(String("hello\nworld"))]),
ValStr("hello\nworld")),
# escape inside interpolated string
('DESIGNA name VT "Roma"\n"salve\\n{name}"',
Program([], [
Designa(ID("name"), String("Roma")),
ExpressionStatement(InterpolatedString([String("salve\n"), ID("name")]))
]), ValStr("salve\nRoma")),
# DIC with newline escape
('DIC("hello\\nworld")',
Program([], [ExpressionStatement(BuiltIn("DIC", [String("hello\nworld")]))]),
ValStr("hello\nworld"), "hello\nworld\n"),
# multiple escapes in one string
('"\\t\\n\\\\"',
Program([], [ExpressionStatement(String("\t\n\\"))]),
ValStr("\t\n\\")),
# unknown escapes pass through (regex backrefs)
('"\\1\\2"',
Program([], [ExpressionStatement(String("\\1\\2"))]),
ValStr("\\1\\2")),
]
class TestEscapeSequences(unittest.TestCase):
@parameterized.expand(escape_tests)
def test_escape(self, source, nodes, value, output=""):
run_test(self, source, nodes, value, output)
# --- Comparison operators ---
comparison_tests = [
@@ -2446,7 +2550,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()
@@ -2483,7 +2587,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()
@@ -2517,7 +2621,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)
@@ -2760,5 +2864,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()