This commit is contained in:
2026-04-25 22:03:30 +02:00
parent ff1c392dd6
commit d2d09c770d
20 changed files with 782 additions and 16 deletions

28
DOCS.md
View File

@@ -509,6 +509,34 @@ The symbol `|` can be used to denote that the following fraction symbols are 1 "
A single "set" of fraction symbols can only represent up to 11/12, as 12/12 can be written as 1. A single "set" of fraction symbols can only represent up to 11/12, as 12/12 can be written as 1.
### IASON
> ⚠ **Warning.** The `IASON` module enables your program to read and write non-Roman numerals. Numbers handled by `IASON_LEGE` and `IASON_SCRIBE` use the decimal digits `0``9` (e.g. `42`, `1789`, `30`), not Roman numerals. This goes against the design philosophy of CENTVRION and should not be used unless absolutely necessary.
![CVM IASON](snippets/iason.png)
The `IASON` module adds two builtins for converting between `CENTVRION` values and JSON strings.
`IASON_LEGE(string)` parses a JSON string and returns the corresponding `CENTVRION` value. Mappings: JSON `null` → `NVLLVS`, `true`/`false` → `VERITAS`/`FALSITAS`, integer → numeral, string → string, array → array, object → `TABVLA` (string keys).
JSON floats with no fractional part (e.g. `3.0`) come back as integers. Other floats depend on whether the `FRACTIO` module is also loaded: with `FRACTIO`, `0.1` parses to the exact fraction `I:|::|::|S:.|S.|:` (1/10); without it, the value is floored to the nearest integer.
![IASON_LEGE example](snippets/iason_lege.png)
```
> Marcus
> XXX
> [gladius scutum]
```
`IASON_SCRIBE(value)` serializes a `CENTVRION` value to a JSON string. Integers and fractions become JSON numbers (fractions via shortest-round-trip float), strings become JSON strings (with the standard escapes), arrays become arrays, dicts become objects (insertion order preserved). Functions and dicts with non-string keys raise an error.
![IASON_SCRIBE example](snippets/iason_scribe.png)
```
> {"nomen": "Marcus", "anni": 30}
```
### MAGNVM ### MAGNVM
![CVM MAGNVM](snippets/magnvm.png) ![CVM MAGNVM](snippets/magnvm.png)

10
cent
View File

@@ -53,17 +53,19 @@ def main():
sys.exit(f"CENTVRION error: {e}") sys.exit(f"CENTVRION error: {e}")
else: else:
c_source = compile_program(program) c_source = compile_program(program)
runtime_c = os.path.join( runtime_dir = os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
"centvrion", "compiler", "runtime", "cent_runtime.c" "centvrion", "compiler", "runtime"
) )
runtime_c = os.path.join(runtime_dir, "cent_runtime.c")
iason_c = os.path.join(runtime_dir, "cent_iason.c")
out_path = os.path.splitext(file_path)[0] out_path = os.path.splitext(file_path)[0]
if args["--keep-c"]: if args["--keep-c"]:
tmp_path = out_path + ".c" tmp_path = out_path + ".c"
with open(tmp_path, "w") as f: with open(tmp_path, "w") as f:
f.write(c_source) f.write(c_source)
subprocess.run( subprocess.run(
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path, "-lcurl", "-lmicrohttpd"], ["gcc", "-O2", tmp_path, runtime_c, iason_c, "-o", out_path, "-lcurl", "-lmicrohttpd", "-lm"],
check=True, check=True,
) )
else: else:
@@ -72,7 +74,7 @@ def main():
tmp_path = tmp.name tmp_path = tmp.name
try: try:
subprocess.run( subprocess.run(
["gcc", "-O2", tmp_path, runtime_c, "-o", out_path, "-lcurl", "-lmicrohttpd"], ["gcc", "-O2", tmp_path, runtime_c, iason_c, "-o", out_path, "-lcurl", "-lmicrohttpd", "-lm"],
check=True, check=True,
) )
finally: finally:

View File

@@ -1,5 +1,7 @@
import functools import functools
import http.server import http.server
import json
import math
import re import re
import time import time
import urllib.parse import urllib.parse
@@ -290,6 +292,51 @@ def frac_to_fraction(s, magnvm=False, svbnvlla=False):
return total return total
def _json_to_val(obj):
if obj is None:
return ValNul()
if isinstance(obj, bool):
return ValBool(obj)
if isinstance(obj, int):
return ValInt(obj)
if isinstance(obj, Fraction):
if obj.denominator == 1:
return ValInt(obj.numerator)
return ValFrac(obj)
if isinstance(obj, str):
return ValStr(obj)
if isinstance(obj, list):
return ValList([_json_to_val(x) for x in obj])
if isinstance(obj, dict):
return ValDict({k: _json_to_val(v) for k, v in obj.items()})
raise CentvrionError(f"IASON_LEGE: unsupported JSON value of type {type(obj).__name__}")
def _val_to_json(val):
if isinstance(val, ValNul):
return None
if isinstance(val, ValBool):
return val.value()
if isinstance(val, ValInt):
return val.value()
if isinstance(val, ValFrac):
return float(val.value())
if isinstance(val, ValStr):
return val.value()
if isinstance(val, ValList):
return [_val_to_json(x) for x in val.value()]
if isinstance(val, ValDict):
out = {}
for k, v in val.value().items():
if not isinstance(k, str):
raise CentvrionError("IASON_SCRIBE: dict keys must be strings to serialize as JSON")
out[k] = _val_to_json(v)
return out
if isinstance(val, ValFunc):
raise CentvrionError("IASON_SCRIBE: cannot serialize a function")
raise CentvrionError(f"IASON_SCRIBE: cannot serialize value of type {type(val).__name__}")
def fraction_to_frac(f, magnvm=False, svbnvlla=False) -> str: def fraction_to_frac(f, magnvm=False, svbnvlla=False) -> str:
if f < 0: if f < 0:
if not svbnvlla: if not svbnvlla:
@@ -1782,6 +1829,30 @@ class BuiltIn(Node):
server = http.server.HTTPServer(("0.0.0.0", port.value()), _CentHandler) server = http.server.HTTPServer(("0.0.0.0", port.value()), _CentHandler)
server.serve_forever() server.serve_forever()
return vtable, ValNul() return vtable, ValNul()
case "IASON_LEGE":
if "IASON" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'IASON_LEGE' without module 'IASON'")
if len(params) != 1:
raise CentvrionError("IASON_LEGE takes exactly I argument")
s = params[0]
if not isinstance(s, ValStr):
raise CentvrionError("IASON_LEGE requires a string")
fractio = "FRACTIO" in vtable["#modules"]
try:
if fractio:
parsed = json.loads(s.value(), parse_float=Fraction)
else:
parsed = json.loads(s.value(), parse_float=lambda x: math.floor(float(x)))
except json.JSONDecodeError as e:
raise CentvrionError(f"IASON_LEGE: invalid JSON: {e.msg}")
return vtable, _json_to_val(parsed)
case "IASON_SCRIBE":
if "IASON" not in vtable["#modules"]:
raise CentvrionError("Cannot use 'IASON_SCRIBE' without module 'IASON'")
if len(params) != 1:
raise CentvrionError("IASON_SCRIBE takes exactly I argument")
obj = _val_to_json(params[0])
return vtable, ValStr(json.dumps(obj, ensure_ascii=False))
case _: case _:
raise NotImplementedError(self.builtin) raise NotImplementedError(self.builtin)

View File

@@ -437,6 +437,25 @@ def _emit_builtin(node, ctx):
lines.append(f"cent_avscvlta({param_vars[0]});") lines.append(f"cent_avscvlta({param_vars[0]});")
lines.append(f"CentValue {tmp} = cent_null();") lines.append(f"CentValue {tmp} = cent_null();")
case "IASON_LEGE":
if not ctx.has_module("IASON"):
lines.append('cent_runtime_error("IASON module required for IASON_LEGE");')
lines.append(f"CentValue {tmp} = cent_null();")
elif len(param_vars) != 1:
raise _err(node, "IASON_LEGE takes exactly I argument")
else:
fractio_flag = "1" if ctx.has_module("FRACTIO") else "0"
lines.append(f"CentValue {tmp} = cent_iason_lege({param_vars[0]}, {fractio_flag});")
case "IASON_SCRIBE":
if not ctx.has_module("IASON"):
lines.append('cent_runtime_error("IASON module required for IASON_SCRIBE");')
lines.append(f"CentValue {tmp} = cent_null();")
elif len(param_vars) != 1:
raise _err(node, "IASON_SCRIBE takes exactly I argument")
else:
lines.append(f"CentValue {tmp} = cent_iason_scribe({param_vars[0]});")
case _: case _:
raise NotImplementedError(node.builtin) raise NotImplementedError(node.builtin)

View File

@@ -58,6 +58,7 @@ def compile_program(program):
# Includes # Includes
lines += [ lines += [
f'#include "{_RUNTIME_DIR}/cent_runtime.h"', f'#include "{_RUNTIME_DIR}/cent_runtime.h"',
f'#include "{_RUNTIME_DIR}/cent_iason.h"',
"", "",
] ]

View File

@@ -0,0 +1,426 @@
#include "cent_iason.h"
#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---------- shared helpers ----------------------------------------- */
static long iason_gcd(long a, long b) {
if (a < 0) a = -a;
if (b < 0) b = -b;
while (b) { long t = b; b = a % b; a = t; }
return a ? a : 1;
}
static CentValue iason_frac_reduce(long num, long den) {
if (den < 0) { num = -num; den = -den; }
long g = iason_gcd(num, den);
num /= g; den /= g;
if (den == 1) return cent_int(num);
return cent_frac(num, den);
}
/* ---------- parser ------------------------------------------------- */
typedef struct {
const char *src;
size_t pos;
size_t len;
int fractio;
} IasonParser;
static void iason_die(const char *msg) {
cent_runtime_error(msg);
}
static void iason_skip_ws(IasonParser *p) {
while (p->pos < p->len) {
char c = p->src[p->pos];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') p->pos++;
else break;
}
}
static int iason_peek(IasonParser *p) {
return (p->pos < p->len) ? (unsigned char)p->src[p->pos] : -1;
}
static void iason_expect(IasonParser *p, char c, const char *msg) {
if (p->pos >= p->len || p->src[p->pos] != c)
iason_die(msg);
p->pos++;
}
static void iason_expect_word(IasonParser *p, const char *word) {
size_t n = strlen(word);
if (p->len - p->pos < n || memcmp(p->src + p->pos, word, n) != 0)
iason_die("IASON_LEGE: invalid JSON literal");
p->pos += n;
}
/* Encode a Unicode codepoint as UTF-8 into buf; returns bytes written. */
static int iason_utf8_encode(unsigned cp, char *buf) {
if (cp <= 0x7F) { buf[0] = (char)cp; return 1; }
if (cp <= 0x7FF) { buf[0] = (char)(0xC0 | (cp >> 6));
buf[1] = (char)(0x80 | (cp & 0x3F)); return 2; }
if (cp <= 0xFFFF) { buf[0] = (char)(0xE0 | (cp >> 12));
buf[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
buf[2] = (char)(0x80 | (cp & 0x3F)); return 3; }
if (cp <= 0x10FFFF) { buf[0] = (char)(0xF0 | (cp >> 18));
buf[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
buf[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
buf[3] = (char)(0x80 | (cp & 0x3F)); return 4; }
iason_die("IASON_LEGE: codepoint out of range");
return 0;
}
static unsigned iason_read_hex4(IasonParser *p) {
if (p->len - p->pos < 4) iason_die("IASON_LEGE: truncated \\u escape");
unsigned v = 0;
for (int i = 0; i < 4; i++) {
char c = p->src[p->pos++];
v <<= 4;
if (c >= '0' && c <= '9') v |= c - '0';
else if (c >= 'a' && c <= 'f') v |= c - 'a' + 10;
else if (c >= 'A' && c <= 'F') v |= c - 'A' + 10;
else iason_die("IASON_LEGE: invalid hex in \\u escape");
}
return v;
}
/* Parses a JSON string literal at p->pos (positioned at the opening "),
returns an arena-allocated NUL-terminated UTF-8 string. */
static char *iason_parse_string(IasonParser *p) {
iason_expect(p, '"', "IASON_LEGE: expected string");
/* upper bound on output: same as remaining input (escapes shrink). */
size_t cap = (p->len - p->pos) + 1;
char *buf = cent_arena_alloc(cent_arena, cap);
size_t out = 0;
while (p->pos < p->len) {
unsigned char c = (unsigned char)p->src[p->pos++];
if (c == '"') { buf[out] = '\0'; return buf; }
if (c == '\\') {
if (p->pos >= p->len) iason_die("IASON_LEGE: trailing \\ in string");
char esc = p->src[p->pos++];
switch (esc) {
case '"': buf[out++] = '"'; break;
case '\\': buf[out++] = '\\'; break;
case '/': buf[out++] = '/'; break;
case 'b': buf[out++] = '\b'; break;
case 'f': buf[out++] = '\f'; break;
case 'n': buf[out++] = '\n'; break;
case 'r': buf[out++] = '\r'; break;
case 't': buf[out++] = '\t'; break;
case 'u': {
unsigned cp = iason_read_hex4(p);
if (cp >= 0xD800 && cp <= 0xDBFF) {
/* high surrogate; expect \uXXXX low surrogate */
if (p->len - p->pos < 6 || p->src[p->pos] != '\\' || p->src[p->pos + 1] != 'u')
iason_die("IASON_LEGE: missing low surrogate after high surrogate");
p->pos += 2;
unsigned lo = iason_read_hex4(p);
if (lo < 0xDC00 || lo > 0xDFFF)
iason_die("IASON_LEGE: invalid low surrogate");
cp = 0x10000 + (((cp - 0xD800) << 10) | (lo - 0xDC00));
} else if (cp >= 0xDC00 && cp <= 0xDFFF) {
iason_die("IASON_LEGE: stray low surrogate");
}
out += iason_utf8_encode(cp, buf + out);
break;
}
default: iason_die("IASON_LEGE: invalid escape sequence");
}
} else if (c < 0x20) {
iason_die("IASON_LEGE: unescaped control character in string");
} else {
buf[out++] = (char)c;
}
}
iason_die("IASON_LEGE: unterminated string");
return NULL;
}
/* Cap on fractional digits parsed exactly; beyond this we truncate to
keep `long` arithmetic safe (10^18 fits in int64). */
#define IASON_MAX_FRAC_DIGITS 18
static CentValue iason_parse_number(IasonParser *p) {
size_t start = p->pos;
int negative = 0;
if (p->src[p->pos] == '-') { negative = 1; p->pos++; }
/* Integer part. */
if (p->pos >= p->len || !isdigit((unsigned char)p->src[p->pos]))
iason_die("IASON_LEGE: invalid number");
if (p->src[p->pos] == '0') {
p->pos++;
} else {
while (p->pos < p->len && isdigit((unsigned char)p->src[p->pos])) p->pos++;
}
int has_frac = 0, has_exp = 0;
size_t frac_start = 0, frac_end = 0;
if (p->pos < p->len && p->src[p->pos] == '.') {
has_frac = 1;
p->pos++;
frac_start = p->pos;
if (p->pos >= p->len || !isdigit((unsigned char)p->src[p->pos]))
iason_die("IASON_LEGE: invalid number");
while (p->pos < p->len && isdigit((unsigned char)p->src[p->pos])) p->pos++;
frac_end = p->pos;
}
long exp = 0;
if (p->pos < p->len && (p->src[p->pos] == 'e' || p->src[p->pos] == 'E')) {
has_exp = 1;
p->pos++;
int esign = 1;
if (p->pos < p->len && (p->src[p->pos] == '+' || p->src[p->pos] == '-')) {
if (p->src[p->pos] == '-') esign = -1;
p->pos++;
}
if (p->pos >= p->len || !isdigit((unsigned char)p->src[p->pos]))
iason_die("IASON_LEGE: invalid number");
while (p->pos < p->len && isdigit((unsigned char)p->src[p->pos])) {
exp = exp * 10 + (p->src[p->pos] - '0');
p->pos++;
}
exp *= esign;
}
size_t end = p->pos;
if (!has_frac && !has_exp) {
/* Pure integer. Use strtol-style parse (we already validated digits). */
long v = 0;
for (size_t i = (negative ? start + 1 : start); i < end; i++) {
v = v * 10 + (p->src[i] - '0');
}
return cent_int(negative ? -v : v);
}
if (!p->fractio) {
/* Floor to int. strtod handles the full grammar here. */
char *tmp = cent_arena_alloc(cent_arena, end - start + 1);
memcpy(tmp, p->src + start, end - start);
tmp[end - start] = '\0';
double d = strtod(tmp, NULL);
return cent_int((long)floor(d));
}
/* FRACTIO loaded: build an exact fraction from the decimal/exponent form. */
long num = 0;
/* Integer-part digits */
size_t int_start = negative ? start + 1 : start;
size_t int_end = has_frac ? (frac_start - 1) : (has_exp ? end - 0 : end);
/* If we have an exponent without a fraction part, find where digits end. */
if (has_exp && !has_frac) {
int_end = int_start;
while (int_end < p->len && isdigit((unsigned char)p->src[int_end])) int_end++;
}
for (size_t i = int_start; i < int_end; i++) {
num = num * 10 + (p->src[i] - '0');
}
/* Fractional digits, capped */
long den = 1;
if (has_frac) {
size_t take = frac_end - frac_start;
if (take > IASON_MAX_FRAC_DIGITS) take = IASON_MAX_FRAC_DIGITS;
for (size_t i = 0; i < take; i++) {
num = num * 10 + (p->src[frac_start + i] - '0');
den *= 10;
}
}
if (negative) num = -num;
/* Apply exponent: positive shifts num, negative shifts den. */
while (exp > 0) { num *= 10; exp--; }
while (exp < 0) { den *= 10; exp++; }
return iason_frac_reduce(num, den);
}
static CentValue iason_parse_value(IasonParser *p);
static CentValue iason_parse_array(IasonParser *p) {
iason_expect(p, '[', "IASON_LEGE: expected [");
iason_skip_ws(p);
CentValue lst = cent_list_new(4);
if (iason_peek(p) == ']') { p->pos++; return lst; }
for (;;) {
iason_skip_ws(p);
CentValue elem = iason_parse_value(p);
cent_list_push(&lst, elem);
iason_skip_ws(p);
int c = iason_peek(p);
if (c == ',') { p->pos++; continue; }
if (c == ']') { p->pos++; return lst; }
iason_die("IASON_LEGE: expected , or ] in array");
}
}
static CentValue iason_parse_object(IasonParser *p) {
iason_expect(p, '{', "IASON_LEGE: expected {");
iason_skip_ws(p);
CentValue d = cent_dict_new(4);
if (iason_peek(p) == '}') { p->pos++; return d; }
for (;;) {
iason_skip_ws(p);
if (iason_peek(p) != '"') iason_die("IASON_LEGE: object key must be a string");
char *key = iason_parse_string(p);
iason_skip_ws(p);
iason_expect(p, ':', "IASON_LEGE: expected : after object key");
iason_skip_ws(p);
CentValue val = iason_parse_value(p);
cent_dict_set(&d, cent_str(key), val);
iason_skip_ws(p);
int c = iason_peek(p);
if (c == ',') { p->pos++; continue; }
if (c == '}') { p->pos++; return d; }
iason_die("IASON_LEGE: expected , or } in object");
}
}
static CentValue iason_parse_value(IasonParser *p) {
iason_skip_ws(p);
int c = iason_peek(p);
if (c < 0) iason_die("IASON_LEGE: unexpected end of input");
if (c == '{') return iason_parse_object(p);
if (c == '[') return iason_parse_array(p);
if (c == '"') return cent_str(iason_parse_string(p));
if (c == 't') { iason_expect_word(p, "true"); return cent_bool(1); }
if (c == 'f') { iason_expect_word(p, "false"); return cent_bool(0); }
if (c == 'n') { iason_expect_word(p, "null"); return cent_null(); }
if (c == '-' || (c >= '0' && c <= '9')) return iason_parse_number(p);
iason_die("IASON_LEGE: unexpected character");
return cent_null();
}
CentValue cent_iason_lege(CentValue s, int fractio_loaded) {
if (s.type != CENT_STR) cent_type_error("IASON_LEGE requires a string");
IasonParser p = { s.sval, 0, strlen(s.sval), fractio_loaded };
CentValue v = iason_parse_value(&p);
iason_skip_ws(&p);
if (p.pos != p.len) iason_die("IASON_LEGE: trailing data after JSON value");
return v;
}
/* ---------- serializer --------------------------------------------- */
typedef struct {
char *buf;
size_t len;
size_t cap;
} IasonBuf;
static void iason_buf_reserve(IasonBuf *b, size_t extra) {
if (b->len + extra <= b->cap) return;
size_t new_cap = b->cap ? b->cap * 2 : 64;
while (new_cap < b->len + extra) new_cap *= 2;
char *nb = cent_arena_alloc(cent_arena, new_cap);
if (b->len) memcpy(nb, b->buf, b->len);
b->buf = nb;
b->cap = new_cap;
}
static void iason_buf_putc(IasonBuf *b, char c) {
iason_buf_reserve(b, 1);
b->buf[b->len++] = c;
}
static void iason_buf_puts(IasonBuf *b, const char *s) {
size_t n = strlen(s);
iason_buf_reserve(b, n);
memcpy(b->buf + b->len, s, n);
b->len += n;
}
static void iason_buf_putn(IasonBuf *b, const char *s, size_t n) {
iason_buf_reserve(b, n);
memcpy(b->buf + b->len, s, n);
b->len += n;
}
static void iason_emit_string(IasonBuf *b, const char *s) {
iason_buf_putc(b, '"');
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
unsigned char c = *p;
switch (c) {
case '"': iason_buf_puts(b, "\\\""); break;
case '\\': iason_buf_puts(b, "\\\\"); break;
case '\b': iason_buf_puts(b, "\\b"); break;
case '\f': iason_buf_puts(b, "\\f"); break;
case '\n': iason_buf_puts(b, "\\n"); break;
case '\r': iason_buf_puts(b, "\\r"); break;
case '\t': iason_buf_puts(b, "\\t"); break;
default:
if (c < 0x20) {
char tmp[8];
snprintf(tmp, sizeof tmp, "\\u%04x", c);
iason_buf_puts(b, tmp);
} else {
iason_buf_putc(b, (char)c);
}
}
}
iason_buf_putc(b, '"');
}
static void iason_emit_value(IasonBuf *b, CentValue v) {
switch (v.type) {
case CENT_NULL: iason_buf_puts(b, "null"); return;
case CENT_BOOL: iason_buf_puts(b, v.bval ? "true" : "false"); return;
case CENT_INT: {
char tmp[32];
int n = snprintf(tmp, sizeof tmp, "%ld", v.ival);
iason_buf_putn(b, tmp, (size_t)n);
return;
}
case CENT_FRAC: {
double d = (double)v.fval.num / (double)v.fval.den;
/* Shortest round-trippable representation, like Python's float repr. */
char tmp[64];
int n = 0;
for (int prec = 15; prec <= 17; prec++) {
n = snprintf(tmp, sizeof tmp, "%.*g", prec, d);
if (strtod(tmp, NULL) == d) break;
}
iason_buf_putn(b, tmp, (size_t)n);
return;
}
case CENT_STR: iason_emit_string(b, v.sval); return;
case CENT_LIST: {
iason_buf_putc(b, '[');
for (int i = 0; i < v.lval.len; i++) {
if (i > 0) iason_buf_puts(b, ", ");
iason_emit_value(b, v.lval.items[i]);
}
iason_buf_putc(b, ']');
return;
}
case CENT_DICT: {
iason_buf_putc(b, '{');
for (int i = 0; i < v.dval.len; i++) {
if (i > 0) iason_buf_puts(b, ", ");
CentValue k = v.dval.keys[i];
if (k.type != CENT_STR)
cent_runtime_error("IASON_SCRIBE: dict keys must be strings to serialize as JSON");
iason_emit_string(b, k.sval);
iason_buf_puts(b, ": ");
iason_emit_value(b, v.dval.vals[i]);
}
iason_buf_putc(b, '}');
return;
}
case CENT_FUNC:
cent_runtime_error("IASON_SCRIBE: cannot serialize a function");
return;
}
}
CentValue cent_iason_scribe(CentValue v) {
IasonBuf b = { NULL, 0, 0 };
iason_emit_value(&b, v);
iason_buf_putc(&b, '\0');
return cent_str(b.buf);
}

View File

@@ -0,0 +1,14 @@
#ifndef CENT_IASON_H
#define CENT_IASON_H
#include "cent_runtime.h"
/* IASON_LEGE — parse a JSON string into a CENTVRION value tree.
When fractio_loaded != 0, JSON floats become exact fractions; otherwise
they are floored to ints. */
CentValue cent_iason_lege(CentValue s, int fractio_loaded);
/* IASON_SCRIBE — serialize a CENTVRION value to a JSON string. */
CentValue cent_iason_scribe(CentValue v);
#endif /* CENT_IASON_H */

View File

@@ -80,7 +80,9 @@ builtin_tokens = [("BUILTIN", i) for i in [
"SCINDE", "SCINDE",
"PETE", "PETE",
"PETITVR", "PETITVR",
"AVSCVLTA" "AVSCVLTA",
"IASON_LEGE",
"IASON_SCRIBE"
]] ]]
data_tokens = [ data_tokens = [
@@ -92,6 +94,7 @@ data_tokens = [
module_tokens = [("MODULE", i) for i in [ module_tokens = [("MODULE", i) for i in [
"FORS", "FORS",
"FRACTIO", "FRACTIO",
"IASON",
"MAGNVM", "MAGNVM",
"SCRIPTA", "SCRIPTA",
"SVBNVLLA", "SVBNVLLA",

View File

@@ -104,7 +104,7 @@
\newpage \newpage
\begin{itemize} \begin{itemize}
\item \textbf{newline}: \\ Newlines are combined, so a single newline is the same as multiple. \item \textbf{newline}: \\ Newlines are combined, so a single newline is the same as multiple.
\item \textbf{module-name}: \\ Modules are flags given to the interpreter/compiler, to let it know you want to be using certain rules, functions, or features. Available modules: \texttt{FORS} (randomness), \texttt{FRACTIO} (fractions), \texttt{MAGNVM} (large integers), \texttt{SCRIPTA} (file I/O: \texttt{LEGE}, \texttt{SCRIBE}, \texttt{ADIVNGE}), \texttt{SVBNVLLA} (negative literals), \texttt{RETE} (networking: \texttt{PETE}, \texttt{PETITVR}, \texttt{AVSCVLTA}). \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{IASON} (JSON I/O: \texttt{IASON\_LEGE}, \texttt{IASON\_SCRIBE}), \texttt{MAGNVM} (large integers), \texttt{SCRIPTA} (file I/O: \texttt{LEGE}, \texttt{SCRIBE}, \texttt{ADIVNGE}), \texttt{SVBNVLLA} (negative literals), \texttt{RETE} (networking: \texttt{PETE}, \texttt{PETITVR}, \texttt{AVSCVLTA}).
\item \textbf{id}: \\ Variable. Can only consist of lowercase characters and underscores, but not the letters j, u, or w. \item \textbf{id}: \\ Variable. Can only consist of lowercase characters and underscores, but not the letters j, u, or w.
\item \textbf{builtin}: \\ Builtin functions are uppercase latin words. \item \textbf{builtin}: \\ Builtin functions are uppercase latin words.
\item \textbf{string}: \\ Any text encased in \texttt{"} or \texttt{'} characters. Single-quoted strings are always literal. Strings support 1-based indexing (\texttt{string[I]}) and inclusive slicing (\texttt{string[I VSQVE III]}), returning single-character strings and substrings respectively. \item \textbf{string}: \\ Any text encased in \texttt{"} or \texttt{'} characters. Single-quoted strings are always literal. Strings support 1-based indexing (\texttt{string[I]}) and inclusive slicing (\texttt{string[I VSQVE III]}), returning single-character strings and substrings respectively.

1
snippets/iason.cent Normal file
View File

@@ -0,0 +1 @@
CVM IASON

BIN
snippets/iason.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

5
snippets/iason_lege.cent Normal file
View File

@@ -0,0 +1,5 @@
CVM IASON
DESIGNA data VT IASON_LEGE('{"nomen": "Marcus", "anni": 30, "armorum": ["gladius", "scutum"]}')
DIC(data["nomen"])
DIC(data["anni"])
DIC(data["armorum"])

BIN
snippets/iason_lege.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -0,0 +1,3 @@
CVM IASON
DESIGNA persona VT TABVLA {"nomen" VT "Marcus", "anni" VT XXX}
DIC(IASON_SCRIBE(persona))

BIN
snippets/iason_scribe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -70,11 +70,11 @@ contexts:
scope: constant.language.centvrion scope: constant.language.centvrion
builtins: builtins:
- match: '\b(ADDE|ADIVNGE|AVDI_NVMERVS|AVDI|AVSCVLTA|CLAVES|CONFLA|CRIBRA|DECIMATIO|DIC|DORMI|EVERRE|FORTVITVS_NVMERVS|FORTVITA_ELECTIO|INSERE|IVNGE|LEGE|LITTERA|LONGITVDO|MAIVSCVLA|MINVSCVLA|MVTA|NECTE|NVMERVS|ORDINA|PETE|PETITVR|QVAERE|SCINDE|SCRIBE|SEMEN|SENATVS|SVBSTITVE|TOLLE|TYPVS)\b' - match: '\b(ADDE|ADIVNGE|AVDI_NVMERVS|AVDI|AVSCVLTA|CLAVES|CONFLA|CRIBRA|DECIMATIO|DIC|DORMI|EVERRE|FORTVITVS_NVMERVS|FORTVITA_ELECTIO|IASON_LEGE|IASON_SCRIBE|INSERE|IVNGE|LEGE|LITTERA|LONGITVDO|MAIVSCVLA|MINVSCVLA|MVTA|NECTE|NVMERVS|ORDINA|PETE|PETITVR|QVAERE|SCINDE|SCRIBE|SEMEN|SENATVS|SVBSTITVE|TOLLE|TYPVS)\b'
scope: support.function.builtin.centvrion scope: support.function.builtin.centvrion
modules: modules:
- match: '\b(FORS|FRACTIO|MAGNVM|RETE|SCRIPTA|SVBNVLLA)\b' - match: '\b(FORS|FRACTIO|IASON|MAGNVM|RETE|SCRIPTA|SVBNVLLA)\b'
scope: support.class.module.centvrion scope: support.class.module.centvrion
keywords: keywords:

View File

@@ -8,7 +8,7 @@ from tests._helpers import (
String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction, String, TemptaStatement, UnaryMinus, UnaryNot, Fractio, frac_to_fraction,
fraction_to_frac, num_to_int, int_to_num, make_string, fraction_to_frac, num_to_int, int_to_num, make_string,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac, ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac,
CentvrionError, _RUNTIME_C, _cent_rng, CentvrionError, _RUNTIME_C, _IASON_C, _cent_rng,
Lexer, Parser, compile_program, Lexer, Parser, compile_program,
os, subprocess, tempfile, StringIO, patch, os, subprocess, tempfile, StringIO, patch,
) )
@@ -160,6 +160,24 @@ error_tests = [
('IVNGE(["a"], II)', CentvrionError), # IVNGE second arg not a list ('IVNGE(["a"], II)', CentvrionError), # IVNGE second arg not a list
("IVNGE([VERITAS], [I])", CentvrionError), # IVNGE invalid key type (bool) ("IVNGE([VERITAS], [I])", CentvrionError), # IVNGE invalid key type (bool)
("IVNGE([[I]], [II])", CentvrionError), # IVNGE invalid key type (list) ("IVNGE([[I]], [II])", CentvrionError), # IVNGE invalid key type (list)
("IASON_LEGE('null')", CentvrionError), # IASON module required for IASON_LEGE
("IASON_SCRIBE(NVLLVS)", CentvrionError), # IASON module required for IASON_SCRIBE
("CVM IASON\nIASON_LEGE(I)", CentvrionError), # IASON_LEGE non-string arg
("CVM IASON\nIASON_LEGE()", CentvrionError), # IASON_LEGE no args
("CVM IASON\nIASON_LEGE('null', 'null')", CentvrionError), # IASON_LEGE too many args
("CVM IASON\nIASON_LEGE('not json')", CentvrionError), # invalid JSON
("CVM IASON\nIASON_LEGE('[1,]')", CentvrionError), # trailing comma in array
("CVM IASON\nIASON_LEGE('{\"a\":}')", CentvrionError), # missing value in object
("CVM IASON\nIASON_LEGE('{\"a\" 1}')", CentvrionError), # missing colon in object
("CVM IASON\nIASON_LEGE('[1, 2')", CentvrionError), # unterminated array
("CVM IASON\nIASON_LEGE('{')", CentvrionError), # unterminated object
("CVM IASON\nIASON_LEGE('\"abc')", CentvrionError), # unterminated string
("CVM IASON\nIASON_LEGE('[1] junk')", CentvrionError), # trailing data
("CVM IASON\nIASON_LEGE('[\"a\\\\x\"]')", CentvrionError), # invalid escape
("CVM IASON\nIASON_SCRIBE()", CentvrionError), # IASON_SCRIBE no args
("CVM IASON\nIASON_SCRIBE(I, II)", CentvrionError), # IASON_SCRIBE too many args
('CVM IASON\nIASON_SCRIBE(IVNGE([I], ["v"]))', CentvrionError), # IASON_SCRIBE int dict keys
("CVM IASON\nIASON_SCRIBE(FVNCTIO (x) VT { REDI(x) })", CentvrionError), # IASON_SCRIBE function
] ]
class TestErrors(unittest.TestCase): class TestErrors(unittest.TestCase):
@@ -196,7 +214,7 @@ class TestErrorLineNumbers(unittest.TestCase):
tmp_bin_path = tmp_bin.name tmp_bin_path = tmp_bin.name
try: try:
subprocess.run( subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"], ["gcc", "-O2", tmp_c_path, _RUNTIME_C, _IASON_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd", "-lm"],
check=True, capture_output=True, check=True, capture_output=True,
) )
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True) proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)

173
tests/13_test_iason___.py Normal file
View File

@@ -0,0 +1,173 @@
from tests._helpers import (
unittest, parameterized, Fraction,
run_test,
Bool, BuiltIn, DataArray, DataDict, Designa, ExpressionStatement, ID,
ModuleCall, Nullus, Numeral, Program, String,
ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFrac,
)
def _scribe(arg, modules=("IASON",)):
return Program(
[ModuleCall(m) for m in modules],
[ExpressionStatement(BuiltIn("IASON_SCRIBE", [arg]))],
)
def _lege(arg, modules=("IASON",)):
return Program(
[ModuleCall(m) for m in modules],
[ExpressionStatement(BuiltIn("IASON_LEGE", [String(arg)]))],
)
def _src_lege(arg, extra_modules=()):
modules = ("IASON",) + tuple(extra_modules)
prefix = "\n".join(f"CVM {m}" for m in modules) + "\n"
return prefix + f"IASON_LEGE('{arg}')"
def _src_scribe(arg_text, extra_modules=()):
modules = ("IASON",) + tuple(extra_modules)
prefix = "\n".join(f"CVM {m}" for m in modules) + "\n"
return prefix + f"IASON_SCRIBE({arg_text})"
iason_tests = [
# ---- Parse: scalars ----
(_src_lege("null"), _lege("null"), ValNul()),
(_src_lege("true"), _lege("true"), ValBool(True)),
(_src_lege("false"), _lege("false"), ValBool(False)),
(_src_lege("42"), _lege("42"), ValInt(42)),
(_src_lege('"hello"'), _lege('"hello"'), ValStr("hello")),
# ---- Parse: empty containers ----
(_src_lege("[]"), _lege("[]"), ValList([])),
(_src_lege("{}"), _lege("{}"), ValDict({})),
# ---- Parse: array of mixed types ----
(_src_lege('[1, true, null, "x"]'),
_lege('[1, true, null, "x"]'),
ValList([ValInt(1), ValBool(True), ValNul(), ValStr("x")])),
# ---- Parse: nested ----
(_src_lege('{"a": [1, 2], "b": {"c": 3}}'),
_lege('{"a": [1, 2], "b": {"c": 3}}'),
ValDict({
"a": ValList([ValInt(1), ValInt(2)]),
"b": ValDict({"c": ValInt(3)}),
})),
# ---- Parse: numbers ----
(_src_lege("-7"), _lege("-7"), ValInt(-7)),
(_src_lege("0"), _lege("0"), ValInt(0)),
# ---- Parse: string escapes ----
# NB: single-quoted CENTVRION strings unescape \n / \" / \\ before the
# JSON parser sees them, so direct parse tests for those escapes would
# have ambiguous semantics. Serialize tests below cover the inverse, and
# this \u test exercises the JSON parser's escape path.
(_src_lege('"\\u00e9"'),
_lege('"\\u00e9"'),
ValStr("é")),
# ---- Parse: float without FRACTIO floors ----
(_src_lege("3.7"), _lege("3.7"), ValInt(3)),
(_src_lege("-2.5"), _lege("-2.5"), ValInt(-3)),
(_src_lege("1e2"), _lege("1e2"), ValInt(100)),
# ---- Parse: float with FRACTIO is exact ----
(_src_lege("0.5", extra_modules=("FRACTIO",)),
_lege("0.5", modules=("IASON", "FRACTIO")),
ValFrac(Fraction(1, 2))),
(_src_lege("0.1", extra_modules=("FRACTIO",)),
_lege("0.1", modules=("IASON", "FRACTIO")),
ValFrac(Fraction(1, 10))),
(_src_lege("-0.25", extra_modules=("FRACTIO",)),
_lege("-0.25", modules=("IASON", "FRACTIO")),
ValFrac(Fraction(-1, 4))),
(_src_lege("5", extra_modules=("FRACTIO",)),
_lege("5", modules=("IASON", "FRACTIO")),
ValInt(5)),
(_src_lege("3.0", extra_modules=("FRACTIO",)),
_lege("3.0", modules=("IASON", "FRACTIO")),
ValInt(3)),
# ---- Serialize: scalars ----
(_src_scribe("NVLLVS"), _scribe(Nullus()), ValStr("null")),
(_src_scribe("VERITAS"), _scribe(Bool(True)), ValStr("true")),
(_src_scribe("FALSITAS"), _scribe(Bool(False)), ValStr("false")),
(_src_scribe("XLII"), _scribe(Numeral("XLII")), ValStr("42")),
(_src_scribe('"hello"'), _scribe(String("hello")), ValStr('"hello"')),
(_src_scribe("[]"), _scribe(DataArray([])), ValStr("[]")),
(_src_scribe("TABVLA {}"), _scribe(DataDict([])), ValStr("{}")),
# ---- Serialize: nested ----
(_src_scribe("[I, II, III]"),
_scribe(DataArray([Numeral("I"), Numeral("II"), Numeral("III")])),
ValStr("[1, 2, 3]")),
(_src_scribe('TABVLA {"a" VT I, "b" VT VERITAS}'),
_scribe(DataDict([(String("a"), Numeral("I")), (String("b"), Bool(True))])),
ValStr('{"a": 1, "b": true}')),
# ---- Serialize: special chars ----
(_src_scribe('"a\\nb"'),
_scribe(String("a\nb")),
ValStr('"a\\nb"')),
(_src_scribe('"a\\"b"'),
_scribe(String('a"b')),
ValStr('"a\\"b"')),
(_src_scribe('"a\\\\b"'),
_scribe(String("a\\b")),
ValStr('"a\\\\b"')),
# ---- Round-trip ----
("CVM IASON\nDIC(IASON_LEGE('[1, 2, 3]'))",
Program([ModuleCall("IASON")], [ExpressionStatement(BuiltIn("DIC",
[BuiltIn("IASON_LEGE", [String("[1, 2, 3]")])]))]),
ValStr("[I II III]"), "[I II III]\n"),
("CVM IASON\nDIC(IASON_SCRIBE(IASON_LEGE('{\"a\": [1, true, null]}')))",
Program([ModuleCall("IASON")], [ExpressionStatement(BuiltIn("DIC",
[BuiltIn("IASON_SCRIBE",
[BuiltIn("IASON_LEGE", [String('{"a": [1, true, null]}')])])]))]),
ValStr('{"a": [1, true, null]}'),
'{"a": [1, true, null]}\n'),
("CVM IASON\nCVM FRACTIO\nDIC(IASON_SCRIBE(IASON_LEGE('0.5')))",
Program([ModuleCall("IASON"), ModuleCall("FRACTIO")],
[ExpressionStatement(BuiltIn("DIC",
[BuiltIn("IASON_SCRIBE",
[BuiltIn("IASON_LEGE", [String("0.5")])])]))]),
ValStr("0.5"), "0.5\n"),
("CVM IASON\nCVM FRACTIO\nDIC(IASON_SCRIBE(IASON_LEGE('0.1')))",
Program([ModuleCall("IASON"), ModuleCall("FRACTIO")],
[ExpressionStatement(BuiltIn("DIC",
[BuiltIn("IASON_SCRIBE",
[BuiltIn("IASON_LEGE", [String("0.1")])])]))]),
ValStr("0.1"), "0.1\n"),
# ---- Serialize: insertion order preserved ----
(_src_scribe('TABVLA {"b" VT II, "a" VT I, "c" VT III}'),
_scribe(DataDict([
(String("b"), Numeral("II")),
(String("a"), Numeral("I")),
(String("c"), Numeral("III")),
])),
ValStr('{"b": 2, "a": 1, "c": 3}')),
# ---- Whitespace-tolerant parse ----
(_src_lege(" [ 1 , 2 ] "),
_lege(" [ 1 , 2 ] "),
ValList([ValInt(1), ValInt(2)])),
# ---- Unicode passes through serialize (ensure_ascii=False) ----
('CVM IASON\nDIC(IASON_SCRIBE("café"))',
Program([ModuleCall("IASON")], [ExpressionStatement(BuiltIn("DIC",
[BuiltIn("IASON_SCRIBE", [String("café")])]))]),
ValStr('"café"'), '"café"\n'),
]
class TestIason(unittest.TestCase):
@parameterized.expand(iason_tests)
def test_iason(self, source, nodes, value, output="", input_lines=[]):
run_test(self, source, nodes, value, output, input_lines)

View File

@@ -24,10 +24,12 @@ from centvrion.lexer import Lexer
from centvrion.parser import Parser from centvrion.parser import Parser
from centvrion.values import ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac from centvrion.values import ValInt, ValStr, ValBool, ValList, ValDict, ValNul, ValFunc, ValFrac
_RUNTIME_C = os.path.join( _RUNTIME_DIR = os.path.join(
os.path.dirname(__file__), "..", os.path.dirname(__file__), "..",
"centvrion", "compiler", "runtime", "cent_runtime.c" "centvrion", "compiler", "runtime"
) )
_RUNTIME_C = os.path.join(_RUNTIME_DIR, "cent_runtime.c")
_IASON_C = os.path.join(_RUNTIME_DIR, "cent_iason.c")
def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]): def run_test(self, source, target_nodes, target_value, target_output="", input_lines=[]):
_cent_rng.seed(1) _cent_rng.seed(1)
@@ -92,7 +94,7 @@ def run_test(self, source, target_nodes, target_value, target_output="", input_l
tmp_bin_path = tmp_bin.name tmp_bin_path = tmp_bin.name
try: try:
subprocess.run( subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"], ["gcc", "-O2", tmp_c_path, _RUNTIME_C, _IASON_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd", "-lm"],
check=True, capture_output=True, check=True, capture_output=True,
) )
stdin_data = "".join(f"{l}\n" for l in input_lines) stdin_data = "".join(f"{l}\n" for l in input_lines)
@@ -124,7 +126,7 @@ def run_compiler_error_test(self, source):
tmp_bin_path = tmp_bin.name tmp_bin_path = tmp_bin.name
try: try:
subprocess.run( subprocess.run(
["gcc", "-O2", tmp_c_path, _RUNTIME_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd"], ["gcc", "-O2", tmp_c_path, _RUNTIME_C, _IASON_C, "-o", tmp_bin_path, "-lcurl", "-lmicrohttpd", "-lm"],
check=True, capture_output=True, check=True, capture_output=True,
) )
proc = subprocess.run([tmp_bin_path], capture_output=True, text=True) proc = subprocess.run([tmp_bin_path], capture_output=True, text=True)

View File

@@ -65,7 +65,7 @@
"patterns": [ "patterns": [
{ {
"name": "support.function.builtin.cent", "name": "support.function.builtin.cent",
"match": "\\b(ADDE|ADIVNGE|AVDI_NVMERVS|AVDI|AVSCVLTA|CLAVES|CONFLA|CRIBRA|DECIMATIO|DIC|DORMI|EVERRE|FORTVITVS_NVMERVS|FORTVITA_ELECTIO|INSERE|IVNGE|LEGE|LITTERA|LONGITVDO|MAIVSCVLA|MINVSCVLA|MVTA|NECTE|NVMERVS|ORDINA|PETE|PETITVR|QVAERE|SCINDE|SCRIBE|SEMEN|SENATVS|SVBSTITVE|TOLLE|TYPVS)\\b" "match": "\\b(ADDE|ADIVNGE|AVDI_NVMERVS|AVDI|AVSCVLTA|CLAVES|CONFLA|CRIBRA|DECIMATIO|DIC|DORMI|EVERRE|FORTVITVS_NVMERVS|FORTVITA_ELECTIO|IASON_LEGE|IASON_SCRIBE|INSERE|IVNGE|LEGE|LITTERA|LONGITVDO|MAIVSCVLA|MINVSCVLA|MVTA|NECTE|NVMERVS|ORDINA|PETE|PETITVR|QVAERE|SCINDE|SCRIBE|SEMEN|SENATVS|SVBSTITVE|TOLLE|TYPVS)\\b"
} }
] ]
}, },
@@ -73,7 +73,7 @@
"patterns": [ "patterns": [
{ {
"name": "support.class.module.cent", "name": "support.class.module.cent",
"match": "\\b(FORS|FRACTIO|MAGNVM|RETE|SCRIPTA|SVBNVLLA)\\b" "match": "\\b(FORS|FRACTIO|IASON|MAGNVM|RETE|SCRIPTA|SVBNVLLA)\\b"
} }
] ]
}, },