🐐 Web server
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import http.server
|
||||
import re
|
||||
import random
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from fractions import Fraction
|
||||
|
||||
from rply.token import BaseBox
|
||||
@@ -8,6 +10,26 @@ from rply.token import BaseBox
|
||||
from centvrion.errors import CentvrionError
|
||||
from centvrion.values import Val, ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac
|
||||
|
||||
class _CentRng:
|
||||
"""Xorshift32 RNG — identical algorithm in Python and C runtime."""
|
||||
def __init__(self, seed=None):
|
||||
if seed is None:
|
||||
seed = int(time.time())
|
||||
self.state = seed & 0xFFFFFFFF or 1
|
||||
def seed(self, s):
|
||||
self.state = s & 0xFFFFFFFF or 1
|
||||
def next(self):
|
||||
x = self.state
|
||||
x ^= (x << 13) & 0xFFFFFFFF
|
||||
x ^= x >> 17
|
||||
x ^= (x << 5) & 0xFFFFFFFF
|
||||
self.state = x
|
||||
return x
|
||||
def randint(self, a, b):
|
||||
return a + self.next() % (b - a + 1)
|
||||
|
||||
_cent_rng = _CentRng()
|
||||
|
||||
NUMERALS = {
|
||||
"I": 1,
|
||||
"IV": 4,
|
||||
@@ -1175,7 +1197,7 @@ class BuiltIn(Node):
|
||||
raise CentvrionError("FORTVITVS_NVMERVS requires two numbers")
|
||||
if a > b:
|
||||
raise CentvrionError(f"FORTVITVS_NVMERVS: first argument ({a}) must be ≤ second ({b})")
|
||||
return vtable, ValInt(random.randint(a, b))
|
||||
return vtable, ValInt(_cent_rng.randint(a, b))
|
||||
case "FORTVITA_ELECTIO":
|
||||
if "FORS" not in vtable["#modules"]:
|
||||
raise CentvrionError("Cannot use 'FORTVITA_ELECTIO' without module 'FORS'")
|
||||
@@ -1184,14 +1206,14 @@ class BuiltIn(Node):
|
||||
lst = params[0].value()
|
||||
if len(lst) == 0:
|
||||
raise CentvrionError("FORTVITA_ELECTIO: cannot select from an empty array")
|
||||
return vtable, lst[random.randint(0, len(lst) - 1)]
|
||||
return vtable, lst[_cent_rng.randint(0, len(lst) - 1)]
|
||||
case "SEMEN":
|
||||
if "FORS" not in vtable["#modules"]:
|
||||
raise CentvrionError("Cannot use 'SEMEN' without module 'FORS'")
|
||||
seed = params[0].value()
|
||||
if not isinstance(seed, int):
|
||||
raise CentvrionError("SEMEN requires an integer seed")
|
||||
random.seed(seed)
|
||||
_cent_rng.seed(seed)
|
||||
return vtable, ValNul()
|
||||
case "DECIMATIO":
|
||||
if "FORS" not in vtable["#modules"]:
|
||||
@@ -1201,7 +1223,7 @@ class BuiltIn(Node):
|
||||
arr = list(params[0].value())
|
||||
to_remove = len(arr) // 10
|
||||
for _ in range(to_remove):
|
||||
arr.pop(random.randint(0, len(arr) - 1))
|
||||
arr.pop(_cent_rng.randint(0, len(arr) - 1))
|
||||
return vtable, ValList(arr)
|
||||
case "SENATVS":
|
||||
if len(params) == 1 and isinstance(params[0], ValList):
|
||||
@@ -1301,6 +1323,75 @@ class BuiltIn(Node):
|
||||
except re.error as e:
|
||||
raise CentvrionError(f"Invalid regex: {e}")
|
||||
return vtable, ValStr(result)
|
||||
case "PETE":
|
||||
if "RETE" not in vtable["#modules"]:
|
||||
raise CentvrionError("Cannot use 'PETE' without module 'RETE'")
|
||||
url = params[0]
|
||||
if not isinstance(url, ValStr):
|
||||
raise CentvrionError("PETE requires a string URL")
|
||||
try:
|
||||
with urllib.request.urlopen(url.value()) as resp:
|
||||
return vtable, ValStr(resp.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
raise CentvrionError(f"PETE: {e}")
|
||||
case "PETITVR":
|
||||
if "RETE" not in vtable["#modules"]:
|
||||
raise CentvrionError("Cannot use 'PETITVR' without module 'RETE'")
|
||||
path, handler = params[0], params[1]
|
||||
if not isinstance(path, ValStr):
|
||||
raise CentvrionError("PETITVR requires a string path")
|
||||
if not isinstance(handler, ValFunc):
|
||||
raise CentvrionError("PETITVR requires a function handler")
|
||||
vtable["#routes"].append((path.value(), handler))
|
||||
return vtable, ValNul()
|
||||
case "AVSCVLTA":
|
||||
if "RETE" not in vtable["#modules"]:
|
||||
raise CentvrionError("Cannot use 'AVSCVLTA' without module 'RETE'")
|
||||
port = params[0]
|
||||
if not isinstance(port, ValInt):
|
||||
raise CentvrionError("AVSCVLTA requires an integer port")
|
||||
routes = vtable["#routes"]
|
||||
if not routes:
|
||||
raise CentvrionError("AVSCVLTA: no routes registered")
|
||||
captured_vtable = vtable.copy()
|
||||
route_map = {p: h for p, h in routes}
|
||||
class _CentHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
handler = route_map.get(parsed.path)
|
||||
if handler is None:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
request_dict = ValDict({
|
||||
"via": ValStr(parsed.path),
|
||||
"quaestio": ValStr(parsed.query),
|
||||
"methodus": ValStr("GET"),
|
||||
})
|
||||
func_vtable = captured_vtable.copy()
|
||||
if len(handler.params) == 1:
|
||||
func_vtable[handler.params[0].name] = request_dict
|
||||
func_vtable["#return"] = None
|
||||
for statement in handler.body:
|
||||
func_vtable, _ = statement.eval(func_vtable)
|
||||
if func_vtable["#return"] is not None:
|
||||
break
|
||||
result = func_vtable["#return"]
|
||||
if isinstance(result, ValStr):
|
||||
body = result.value().encode("utf-8")
|
||||
elif result is not None:
|
||||
body = make_string(result, magnvm, svbnvlla).encode("utf-8")
|
||||
else:
|
||||
body = b""
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
server = http.server.HTTPServer(("0.0.0.0", port.value()), _CentHandler)
|
||||
server.serve_forever()
|
||||
return vtable, ValNul()
|
||||
case _:
|
||||
raise NotImplementedError(self.builtin)
|
||||
|
||||
@@ -1331,6 +1422,7 @@ class Program(BaseBox):
|
||||
"#continue": False,
|
||||
"#return": None,
|
||||
"#modules": [m.module_name for m in self.modules],
|
||||
"#routes": [],
|
||||
}
|
||||
last_val = ValNul()
|
||||
for statement in self.statements:
|
||||
|
||||
@@ -303,6 +303,29 @@ def _emit_builtin(node, ctx):
|
||||
case "SVBSTITVE":
|
||||
lines.append(f"CentValue {tmp} = cent_svbstitve({param_vars[0]}, {param_vars[1]}, {param_vars[2]});")
|
||||
|
||||
case "PETE":
|
||||
if not ctx.has_module("RETE"):
|
||||
lines.append('cent_runtime_error("RETE module required for PETE");')
|
||||
lines.append(f"CentValue {tmp} = cent_null();")
|
||||
else:
|
||||
lines.append(f"CentValue {tmp} = cent_pete({param_vars[0]});")
|
||||
|
||||
case "PETITVR":
|
||||
if not ctx.has_module("RETE"):
|
||||
lines.append('cent_runtime_error("RETE module required for PETITVR");')
|
||||
lines.append(f"CentValue {tmp} = cent_null();")
|
||||
else:
|
||||
lines.append(f"cent_petitvr({param_vars[0]}, {param_vars[1]}, _scope);")
|
||||
lines.append(f"CentValue {tmp} = cent_null();")
|
||||
|
||||
case "AVSCVLTA":
|
||||
if not ctx.has_module("RETE"):
|
||||
lines.append('cent_runtime_error("RETE module required for AVSCVLTA");')
|
||||
lines.append(f"CentValue {tmp} = cent_null();")
|
||||
else:
|
||||
lines.append(f"cent_avscvlta({param_vars[0]});")
|
||||
lines.append(f"CentValue {tmp} = cent_null();")
|
||||
|
||||
case _:
|
||||
raise NotImplementedError(node.builtin)
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <regex.h>
|
||||
#include <curl/curl.h>
|
||||
#include <microhttpd.h>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Global arena */
|
||||
@@ -12,6 +16,21 @@
|
||||
CentArena *cent_arena;
|
||||
int cent_magnvm = 0;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Portable xorshift32 RNG (matches Python _CentRng) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static uint32_t cent_rng_state = 1;
|
||||
|
||||
static uint32_t cent_rng_next(void) {
|
||||
uint32_t x = cent_rng_state;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
cent_rng_state = x;
|
||||
return x;
|
||||
}
|
||||
|
||||
jmp_buf _cent_try_stack[CENT_TRY_STACK_MAX];
|
||||
int _cent_try_depth = 0;
|
||||
const char *_cent_error_msg = NULL;
|
||||
@@ -629,7 +648,7 @@ CentValue cent_fortuitus_numerus(CentValue lo, CentValue hi) {
|
||||
long range = hi.ival - lo.ival + 1;
|
||||
if (range <= 0)
|
||||
cent_runtime_error("'FORTVITVS_NVMERVS' requires lo <= hi");
|
||||
return cent_int(lo.ival + rand() % range);
|
||||
return cent_int(lo.ival + cent_rng_next() % range);
|
||||
}
|
||||
|
||||
CentValue cent_fortuita_electionis(CentValue lst) {
|
||||
@@ -637,7 +656,7 @@ CentValue cent_fortuita_electionis(CentValue lst) {
|
||||
cent_type_error("'FORTVITA_ELECTIO' requires a list");
|
||||
if (lst.lval.len == 0)
|
||||
cent_runtime_error("'FORTVITA_ELECTIO' requires a non-empty list");
|
||||
return lst.lval.items[rand() % lst.lval.len];
|
||||
return lst.lval.items[cent_rng_next() % lst.lval.len];
|
||||
}
|
||||
|
||||
CentValue cent_senatus(CentValue *args, int n) {
|
||||
@@ -665,7 +684,7 @@ CentValue cent_decimatio(CentValue lst) {
|
||||
cent_list_push(&result, lst.lval.items[i]);
|
||||
int to_remove = result.lval.len / 10;
|
||||
for (int i = 0; i < to_remove; i++) {
|
||||
int idx = rand() % result.lval.len;
|
||||
int idx = cent_rng_next() % result.lval.len;
|
||||
/* Shift remaining elements left */
|
||||
for (int j = idx; j < result.lval.len - 1; j++)
|
||||
result.lval.items[j] = result.lval.items[j + 1];
|
||||
@@ -677,7 +696,8 @@ CentValue cent_decimatio(CentValue lst) {
|
||||
void cent_semen(CentValue seed) {
|
||||
if (seed.type != CENT_INT)
|
||||
cent_type_error("'SEMEN' requires an integer seed");
|
||||
srand((unsigned)seed.ival);
|
||||
cent_rng_state = (uint32_t)seed.ival;
|
||||
if (cent_rng_state == 0) cent_rng_state = 1;
|
||||
}
|
||||
|
||||
static int _ordina_comparator(const void *a, const void *b) {
|
||||
@@ -995,11 +1015,175 @@ CentValue cent_svbstitve(CentValue pattern, CentValue replacement, CentValue tex
|
||||
return cent_str(result);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Networking (RETE) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
char *buf;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
} CurlBuf;
|
||||
|
||||
static size_t _curl_write_cb(void *data, size_t size, size_t nmemb, void *userp) {
|
||||
size_t bytes = size * nmemb;
|
||||
CurlBuf *cb = (CurlBuf *)userp;
|
||||
while (cb->len + bytes + 1 > cb->cap) {
|
||||
cb->cap = cb->cap ? cb->cap * 2 : 4096;
|
||||
cb->buf = realloc(cb->buf, cb->cap);
|
||||
if (!cb->buf) cent_runtime_error("PETE: out of memory");
|
||||
}
|
||||
memcpy(cb->buf + cb->len, data, bytes);
|
||||
cb->len += bytes;
|
||||
cb->buf[cb->len] = '\0';
|
||||
return bytes;
|
||||
}
|
||||
|
||||
CentValue cent_pete(CentValue url) {
|
||||
if (url.type != CENT_STR)
|
||||
cent_type_error("'PETE' requires a string URL");
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) cent_runtime_error("PETE: failed to init curl");
|
||||
CurlBuf cb = {0};
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.sval);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _curl_write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &cb);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_easy_cleanup(curl);
|
||||
if (res != CURLE_OK) {
|
||||
free(cb.buf);
|
||||
cent_runtime_error(curl_easy_strerror(res));
|
||||
}
|
||||
char *arena_buf = cent_arena_alloc(cent_arena, cb.len + 1);
|
||||
memcpy(arena_buf, cb.buf, cb.len + 1);
|
||||
free(cb.buf);
|
||||
return cent_str(arena_buf);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Server (RETE) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define CENT_MAX_ROUTES 64
|
||||
|
||||
typedef struct {
|
||||
const char *path;
|
||||
CentFuncInfo handler;
|
||||
CentScope scope;
|
||||
} CentRoute;
|
||||
|
||||
static CentRoute _cent_routes[CENT_MAX_ROUTES];
|
||||
static int _cent_route_count = 0;
|
||||
|
||||
void cent_petitvr(CentValue path, CentValue handler, CentScope scope) {
|
||||
if (path.type != CENT_STR)
|
||||
cent_type_error("PETITVR requires a string path");
|
||||
if (handler.type != CENT_FUNC)
|
||||
cent_type_error("PETITVR requires a function handler");
|
||||
if (_cent_route_count >= CENT_MAX_ROUTES)
|
||||
cent_runtime_error("PETITVR: too many routes");
|
||||
_cent_routes[_cent_route_count].path = path.sval;
|
||||
_cent_routes[_cent_route_count].handler = handler.fnval;
|
||||
_cent_routes[_cent_route_count].scope = cent_scope_copy(&scope);
|
||||
_cent_route_count++;
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
_cent_request_handler(void *cls, struct MHD_Connection *conn,
|
||||
const char *url, const char *method,
|
||||
const char *version, const char *upload_data,
|
||||
size_t *upload_data_size, void **con_cls) {
|
||||
(void)cls; (void)version; (void)upload_data;
|
||||
(void)upload_data_size; (void)con_cls;
|
||||
|
||||
/* Find matching route */
|
||||
CentFuncInfo *fi = NULL;
|
||||
CentScope *captured_scope = NULL;
|
||||
/* Split url at '?' to match path only */
|
||||
const char *qmark = strchr(url, '?');
|
||||
size_t path_len = qmark ? (size_t)(qmark - url) : strlen(url);
|
||||
for (int i = 0; i < _cent_route_count; i++) {
|
||||
if (strlen(_cent_routes[i].path) == path_len
|
||||
&& strncmp(_cent_routes[i].path, url, path_len) == 0) {
|
||||
fi = &_cent_routes[i].handler;
|
||||
captured_scope = &_cent_routes[i].scope;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fi) {
|
||||
const char *msg = "CDIV — not found\n";
|
||||
struct MHD_Response *resp = MHD_create_response_from_buffer(
|
||||
strlen(msg), (void *)msg, MHD_RESPMEM_PERSISTENT);
|
||||
enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_NOT_FOUND, resp);
|
||||
MHD_destroy_response(resp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Build request dict: via, quaestio, methodus */
|
||||
char *via_buf = cent_arena_alloc(cent_arena, path_len + 1);
|
||||
memcpy(via_buf, url, path_len);
|
||||
via_buf[path_len] = '\0';
|
||||
|
||||
const char *qs = qmark ? qmark + 1 : "";
|
||||
size_t qs_len = strlen(qs);
|
||||
char *qs_buf = cent_arena_alloc(cent_arena, qs_len + 1);
|
||||
memcpy(qs_buf, qs, qs_len + 1);
|
||||
|
||||
CentValue dict = cent_dict_new(4);
|
||||
cent_dict_set(&dict, cent_str("via"), cent_str(via_buf));
|
||||
cent_dict_set(&dict, cent_str("quaestio"), cent_str(qs_buf));
|
||||
cent_dict_set(&dict, cent_str("methodus"), cent_str(method));
|
||||
|
||||
/* Call handler — start from the scope captured at registration time */
|
||||
CentScope call_scope = cent_scope_copy(captured_scope);
|
||||
if (fi->param_count == 1) {
|
||||
cent_scope_set(&call_scope, fi->param_names[0], dict);
|
||||
}
|
||||
CentValue result = fi->fn(call_scope);
|
||||
|
||||
/* Build response */
|
||||
const char *body = "";
|
||||
size_t body_len = 0;
|
||||
if (result.type == CENT_STR) {
|
||||
body = result.sval;
|
||||
body_len = strlen(body);
|
||||
} else if (result.type != CENT_NULL) {
|
||||
char *s = cent_make_string(result);
|
||||
body = s;
|
||||
body_len = strlen(s);
|
||||
}
|
||||
|
||||
struct MHD_Response *resp = MHD_create_response_from_buffer(
|
||||
body_len, (void *)body, MHD_RESPMEM_PERSISTENT);
|
||||
MHD_add_response_header(resp, "Content-Type", "text/plain");
|
||||
enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_OK, resp);
|
||||
MHD_destroy_response(resp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void cent_avscvlta(CentValue port) {
|
||||
if (port.type != CENT_INT)
|
||||
cent_type_error("AVSCVLTA requires an integer port");
|
||||
if (_cent_route_count == 0)
|
||||
cent_runtime_error("AVSCVLTA: no routes registered");
|
||||
struct MHD_Daemon *d = MHD_start_daemon(
|
||||
MHD_USE_INTERNAL_POLLING_THREAD,
|
||||
(uint16_t)port.ival, NULL, NULL,
|
||||
_cent_request_handler, NULL,
|
||||
MHD_OPTION_END);
|
||||
if (!d) cent_runtime_error("AVSCVLTA: failed to start server");
|
||||
/* Block forever */
|
||||
while (1) pause();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Initialisation */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void cent_init(void) {
|
||||
cent_arena = cent_arena_new(1024 * 1024); /* 1 MiB initial arena */
|
||||
srand((unsigned)time(NULL));
|
||||
cent_rng_state = (uint32_t)time(NULL);
|
||||
if (cent_rng_state == 0) cent_rng_state = 1;
|
||||
}
|
||||
|
||||
@@ -234,6 +234,9 @@ void cent_scribe(CentValue path, CentValue content); /* SCRIBE */
|
||||
void cent_adivnge(CentValue path, CentValue content); /* ADIVNGE */
|
||||
CentValue cent_qvaere(CentValue pattern, CentValue text); /* QVAERE */
|
||||
CentValue cent_svbstitve(CentValue pattern, CentValue replacement, CentValue text); /* SVBSTITVE */
|
||||
CentValue cent_pete(CentValue url); /* PETE */
|
||||
void cent_petitvr(CentValue path, CentValue handler, CentScope scope); /* PETITVR */
|
||||
void cent_avscvlta(CentValue port); /* AVSCVLTA */
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Array helpers */
|
||||
|
||||
@@ -59,7 +59,10 @@ builtin_tokens = [("BUILTIN", i) for i in [
|
||||
"SCRIBE",
|
||||
"ADIVNGE",
|
||||
"QVAERE",
|
||||
"SVBSTITVE"
|
||||
"SVBSTITVE",
|
||||
"PETE",
|
||||
"PETITVR",
|
||||
"AVSCVLTA"
|
||||
]]
|
||||
|
||||
data_tokens = [
|
||||
@@ -73,7 +76,8 @@ module_tokens = [("MODULE", i) for i in [
|
||||
"FRACTIO",
|
||||
"MAGNVM",
|
||||
"SCRIPTA",
|
||||
"SVBNVLLA"
|
||||
"SVBNVLLA",
|
||||
"RETE"
|
||||
]]
|
||||
|
||||
symbol_tokens = [
|
||||
|
||||
Reference in New Issue
Block a user