Compare commits
4 Commits
ebea9f942b
...
940f8d7311
| Author | SHA1 | Date | |
|---|---|---|---|
| 940f8d7311 | |||
| 8c69a300a5 | |||
| 39218485c7 | |||
| b9a1ed1bcc |
40
README.md
40
README.md
@@ -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
|
||||

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

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

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

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

|
||||
|
||||
4
cent
4
cent
@@ -61,7 +61,7 @@ def main():
|
||||
with open(tmp_path, "w") as f:
|
||||
f.write(c_source)
|
||||
subprocess.run(
|
||||
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path],
|
||||
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path, "-lcurl", "-lmicrohttpd"],
|
||||
check=True,
|
||||
)
|
||||
else:
|
||||
@@ -70,7 +70,7 @@ def main():
|
||||
tmp_path = tmp.name
|
||||
try:
|
||||
subprocess.run(
|
||||
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path],
|
||||
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path, "-lcurl", "-lmicrohttpd"],
|
||||
check=True,
|
||||
)
|
||||
finally:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
19
examples/web_server.cent
Normal 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)
|
||||
@@ -1,2 +0,0 @@
|
||||
\relax
|
||||
\gdef \@abspage@last{2}
|
||||
@@ -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.
@@ -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
1
snippets/rete.cent
Normal file
@@ -0,0 +1 @@
|
||||
CVM RETE
|
||||
BIN
snippets/rete.png
Normal file
BIN
snippets/rete.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
1
snippets/scripta.cent
Normal file
1
snippets/scripta.cent
Normal file
@@ -0,0 +1 @@
|
||||
CVM SCRIPTA
|
||||
BIN
snippets/scripta.png
Normal file
BIN
snippets/scripta.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
@@ -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
296
tests.py
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
@@ -17,6 +16,7 @@ from centvrion.ast_nodes import (
|
||||
ModuleCall, Nullus, Numeral, PerStatement, Program, Redi, SiStatement,
|
||||
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
|
||||
fraction_to_frac, num_to_int, int_to_num, make_string,
|
||||
_cent_rng,
|
||||
)
|
||||
from centvrion.compiler.emitter import compile_program
|
||||
from centvrion.errors import CentvrionError
|
||||
@@ -30,7 +30,7 @@ _RUNTIME_C = os.path.join(
|
||||
)
|
||||
|
||||
def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]):
|
||||
random.seed(1)
|
||||
_cent_rng.seed(1)
|
||||
|
||||
lexer = Lexer().get_lexer()
|
||||
tokens = lexer.lex(source + "\n")
|
||||
@@ -83,6 +83,8 @@ def run_test(self, source, target_nodes, target_value, target_output="", input_l
|
||||
###### Compiler Test #####
|
||||
##########################
|
||||
c_source = compile_program(program)
|
||||
# Force deterministic RNG seed=1 for test reproducibility
|
||||
c_source = c_source.replace("cent_init();", "cent_init(); cent_semen((CentValue){.type=CENT_INT, .ival=1});", 1)
|
||||
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as tmp_c:
|
||||
tmp_c.write(c_source)
|
||||
tmp_c_path = tmp_c.name
|
||||
@@ -90,7 +92,7 @@ def run_test(self, source, target_nodes, target_value, target_output="", input_l
|
||||
tmp_bin_path = tmp_bin.name
|
||||
try:
|
||||
subprocess.run(
|
||||
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path],
|
||||
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"],
|
||||
check=True, capture_output=True,
|
||||
)
|
||||
stdin_data = "".join(f"{l}\n" for l in input_lines)
|
||||
@@ -530,22 +532,22 @@ class TestFunctions(unittest.TestCase):
|
||||
builtin_tests = [
|
||||
("AVDI_NVMERVS()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(3), "", ["III"]),
|
||||
("AVDI_NVMERVS()", Program([], [ExpressionStatement(BuiltIn("AVDI_NVMERVS", []))]), ValInt(10), "", ["X"]),
|
||||
("CVM FORS\nFORTVITVS_NVMERVS(I, X)", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), Numeral("X")]))]), ValInt(3)),
|
||||
("CVM FORS\nDESIGNA a VT [I, II, III]\nDIC(a[FORTVITVS_NVMERVS(I, LONGITVDO(a))])", Program([ModuleCall("FORS")], [Designa(ID("a"), DataArray([Numeral("I"), Numeral("II"), Numeral("III")])), ExpressionStatement(BuiltIn("DIC", [ArrayIndex(ID("a"), BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), BuiltIn("LONGITVDO", [ID("a")])]))]))]), ValStr("I"), "I\n"),
|
||||
("AVDI()", Program([], [ExpressionStatement(BuiltIn("AVDI", []))]), ValStr("hello"), "", ["hello"]),
|
||||
("LONGITVDO([I, II, III])", Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValInt(3)),
|
||||
("LONGITVDO([])", Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [DataArray([])]))]), ValInt(0)),
|
||||
('LONGITVDO("salve")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("salve")]))]), ValInt(5)),
|
||||
('LONGITVDO("")', Program([], [ExpressionStatement(BuiltIn("LONGITVDO", [String("")]))]), ValInt(0)),
|
||||
("CVM FORS\nFORTVITA_ELECTIO([I, II, III])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("FORTVITA_ELECTIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValInt(1)),
|
||||
("CVM FORS\nSEMEN(XLII)\nFORTVITVS_NVMERVS(I, C)", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), Numeral("C")]))]), ValInt(82)),
|
||||
# DECIMATIO: seed 42, 10 elements → removes 1 (element II)
|
||||
("CVM FORS\nSEMEN(XLII)\nDECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X")])]))]), ValList([ValInt(1), ValInt(3), ValInt(4), ValInt(5), ValInt(6), ValInt(7), ValInt(8), ValInt(9), ValInt(10)])),
|
||||
("CVM FORS\nDIC(FORTVITA_ELECTIO([I, II, III]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("FORTVITA_ELECTIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])])]))]), ValStr("I"), "I\n"),
|
||||
("CVM FORS\nSEMEN(XLII)\nDIC(FORTVITVS_NVMERVS(I, C))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("FORTVITVS_NVMERVS", [Numeral("I"), Numeral("C")])]))]), ValStr("XXXIII"), "XXXIII\n"),
|
||||
# DECIMATIO: seed 42, 10 elements → removes 1 (element III)
|
||||
("CVM FORS\nSEMEN(XLII)\nDIC(DECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X")])])]))]), ValStr("[I II IV V VI VII VIII IX X]"), "[I II IV V VI VII VIII IX X]\n"),
|
||||
# DECIMATIO: seed 1, 3 elements → 3//10=0, nothing removed
|
||||
("CVM FORS\nSEMEN(I)\nDECIMATIO([I, II, III])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("I")])), ExpressionStatement(BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]))]), ValList([ValInt(1), ValInt(2), ValInt(3)])),
|
||||
("CVM FORS\nSEMEN(I)\nDIC(DECIMATIO([I, II, III]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("I")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III")])])]))]), ValStr("[I II III]"), "[I II III]\n"),
|
||||
# DECIMATIO: empty array → empty array
|
||||
("CVM FORS\nDECIMATIO([])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DECIMATIO", [DataArray([])]))]), ValList([])),
|
||||
# DECIMATIO: seed 42, 20 elements → removes 2 (elements I and IV)
|
||||
("CVM FORS\nSEMEN(XLII)\nDECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX])", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X"), Numeral("XI"), Numeral("XII"), Numeral("XIII"), Numeral("XIV"), Numeral("XV"), Numeral("XVI"), Numeral("XVII"), Numeral("XVIII"), Numeral("XIX"), Numeral("XX")])]))]), ValList([ValInt(2), ValInt(3), ValInt(5), ValInt(6), ValInt(7), ValInt(8), ValInt(9), ValInt(10), ValInt(11), ValInt(12), ValInt(13), ValInt(14), ValInt(15), ValInt(16), ValInt(17), ValInt(18), ValInt(19), ValInt(20)])),
|
||||
("CVM FORS\nDIC(DECIMATIO([]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([])])]))]), ValStr("[]"), "[]\n"),
|
||||
# DECIMATIO: seed 42, 20 elements → removes 2 (elements XIII and XII)
|
||||
("CVM FORS\nSEMEN(XLII)\nDIC(DECIMATIO([I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX]))", Program([ModuleCall("FORS")], [ExpressionStatement(BuiltIn("SEMEN", [Numeral("XLII")])), ExpressionStatement(BuiltIn("DIC", [BuiltIn("DECIMATIO", [DataArray([Numeral("I"), Numeral("II"), Numeral("III"), Numeral("IV"), Numeral("V"), Numeral("VI"), Numeral("VII"), Numeral("VIII"), Numeral("IX"), Numeral("X"), Numeral("XI"), Numeral("XII"), Numeral("XIII"), Numeral("XIV"), Numeral("XV"), Numeral("XVI"), Numeral("XVII"), Numeral("XVIII"), Numeral("XIX"), Numeral("XX")])])]))]), ValStr("[I II III IV V VI VII VIII IX X XI XIV XV XVI XVII XVIII XIX XX]"), "[I II III IV V VI VII VIII IX X XI XIV XV XVI XVII XVIII XIX XX]\n"),
|
||||
# SENATVS: majority true → VERITAS
|
||||
("SENATVS(VERITAS, VERITAS, FALSITAS)", Program([], [ExpressionStatement(BuiltIn("SENATVS", [Bool(True), Bool(True), Bool(False)]))]), ValBool(True)),
|
||||
# SENATVS: majority false → FALSITAS
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user