Completes ticket 0001 started in27f468c. All three impls now carry bit-identical xoshiro256**; portal state round-trips across process boundaries in every producer x consumer cell (Python <-> C <-> asm). asm impl: - 4 new builtins: random-seed!, random-int, random-state, random-state! - g_rng_state in BSS (4 x u64); rng_splitmix64_step, rng_seed, rng_next - Binary portal header bumped LUMBDAB1/48 -> LUMBDAB2/80; carries 32 bytes of rng state at offsets 40..64, reserved moved to 72 - No float support in asm, so (random) intentionally omitted there - _start seeds with 0 so the stream is deterministic from startup Python + C (supplements27f468c): - rng_seed(0) auto-invoked at module load / register_portal_builtins so (random) without explicit (random-seed!) returns a real value instead of the all-zero xoshiro fixed point Tests: - tests/functional.lsp: 7 new shared assertions (Python + C) - asm/test.sh: 5 new asm-local assertions (142 -> 147) - tests/portal-rng-save.lsp / portal-rng-load.lsp: portable S-expression portal that captures both state AND next-5 baseline so loader self- verifies without a separate harness - tests/portal-cross-test.sh: 9 new producer x consumer RNG cells; all 18 cells pass end-to-end Verified: seed=42, (random-int 1000000) draws 1..10 = 558742 543102 559009 124193 317476 750584 200754 814407 344958 929085 identical in Python, C, and asm. unmoad scan: zero new findings in added code.
815 lines
30 KiB
C
815 lines
30 KiB
C
/*
|
||
* portal.c — Save/resume machine state to JSON files
|
||
*
|
||
* Port of the Python portal_save/portal_resume system.
|
||
* Serializes environment bindings and full continuations.
|
||
*/
|
||
#include "lumbda.h"
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
* Portal checkpoint — thread-local signal for mid-execution save
|
||
*
|
||
* MOAD-0002: Module-level global — intentional coupling. Checked in the VM
|
||
* hot loop (OP_JUMP, OP_TAIL_CALL) so passing it as a parameter would add
|
||
* overhead to every iteration.
|
||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||
|
||
__thread const char *g_portal_checkpoint_path = NULL;
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
* xoshiro256** — deterministic, portable PRNG shared with Python and asm.
|
||
* Portal serializes these 4 words so simulations continue across processes
|
||
* with a bit-identical random stream. Reference: Blackman & Vigna 2018.
|
||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||
|
||
static uint64_t g_rng_state[4] = {0, 0, 0, 0};
|
||
|
||
static uint64_t rng_rotl(uint64_t x, int k) {
|
||
return (x << k) | (x >> (64 - k));
|
||
}
|
||
|
||
static uint64_t rng_splitmix64_step(uint64_t *z) {
|
||
*z += 0x9e3779b97f4a7c15ULL;
|
||
uint64_t r = *z;
|
||
r = (r ^ (r >> 30)) * 0xbf58476d1ce4e5b9ULL;
|
||
r = (r ^ (r >> 27)) * 0x94d049bb133111ebULL;
|
||
return r ^ (r >> 31);
|
||
}
|
||
|
||
static void rng_seed(uint64_t k) {
|
||
uint64_t z = k;
|
||
for (int i = 0; i < 4; i++) g_rng_state[i] = rng_splitmix64_step(&z);
|
||
}
|
||
|
||
static uint64_t rng_next(void) {
|
||
uint64_t result = rng_rotl(g_rng_state[1] * 5, 7) * 9;
|
||
uint64_t t = g_rng_state[1] << 17;
|
||
g_rng_state[2] ^= g_rng_state[0];
|
||
g_rng_state[3] ^= g_rng_state[1];
|
||
g_rng_state[1] ^= g_rng_state[2];
|
||
g_rng_state[0] ^= g_rng_state[3];
|
||
g_rng_state[2] ^= t;
|
||
g_rng_state[3] = rng_rotl(g_rng_state[3], 45);
|
||
return result;
|
||
}
|
||
|
||
void rng_get_halves(uint32_t out[8]) {
|
||
for (int i = 0; i < 4; i++) {
|
||
out[2 * i] = (uint32_t)(g_rng_state[i] & 0xffffffffULL);
|
||
out[2 * i + 1] = (uint32_t)(g_rng_state[i] >> 32);
|
||
}
|
||
}
|
||
|
||
void rng_set_halves(const uint32_t in[8]) {
|
||
for (int i = 0; i < 4; i++) {
|
||
g_rng_state[i] = ((uint64_t)in[2 * i + 1] << 32) | (uint64_t)in[2 * i];
|
||
}
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
* Minimal JSON writer — writes directly to FILE*
|
||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||
|
||
static void json_write_string(FILE *fp, const char *s) {
|
||
fputc('"', fp);
|
||
for (const char *p = s; *p; p++) {
|
||
switch (*p) {
|
||
case '"': fputs("\\\"", fp); break;
|
||
case '\\': fputs("\\\\", fp); break;
|
||
case '\n': fputs("\\n", fp); break;
|
||
case '\r': fputs("\\r", fp); break;
|
||
case '\t': fputs("\\t", fp); break;
|
||
default:
|
||
if ((unsigned char)*p < 0x20)
|
||
fprintf(fp, "\\u%04x", (unsigned)*p);
|
||
else
|
||
fputc(*p, fp);
|
||
}
|
||
}
|
||
fputc('"', fp);
|
||
}
|
||
|
||
static void json_write_value(FILE *fp, Value v);
|
||
|
||
static void json_write_env_bindings(FILE *fp, Env *env, bool global_only) {
|
||
/* Write user-defined bindings as a JSON object */
|
||
fputc('{', fp);
|
||
bool first = true;
|
||
for (size_t i = 0; i < env->nbuckets; i++) {
|
||
EnvBinding *b = env->buckets[i];
|
||
while (b) {
|
||
if (global_only) {
|
||
/* Skip builtins for global env — only save user values */
|
||
if (IS_BUILTIN(b->val)) { b = b->next; continue; }
|
||
if (IS_MACRO(b->val)) { b = b->next; continue; }
|
||
}
|
||
if (!first) fputc(',', fp);
|
||
first = false;
|
||
json_write_string(fp, sym_name(b->sym));
|
||
fputc(':', fp);
|
||
json_write_value(fp, b->val);
|
||
b = b->next;
|
||
}
|
||
}
|
||
fputc('}', fp);
|
||
}
|
||
|
||
static void json_write_value(FILE *fp, Value v) {
|
||
if (IS_NIL(v)) {
|
||
fputs("{\"t\":\"nil\"}", fp);
|
||
} else if (IS_VOID(v)) {
|
||
fputs("{\"t\":\"void\"}", fp);
|
||
} else if (IS_TRUE(v)) {
|
||
fputs("{\"t\":\"bool\",\"v\":true}", fp);
|
||
} else if (IS_FALSE(v)) {
|
||
fputs("{\"t\":\"bool\",\"v\":false}", fp);
|
||
} else if (IS_EOF(v)) {
|
||
fputs("{\"t\":\"eof\"}", fp);
|
||
} else if (IS_INT(v)) {
|
||
fprintf(fp, "%lld", (long long)as_int(v));
|
||
} else if (IS_DOUBLE(v)) {
|
||
double d = as_double(v);
|
||
if (isinf(d)) fprintf(fp, "{\"t\":\"float\",\"v\":\"%s\"}", d > 0 ? "+inf" : "-inf");
|
||
else if (isnan(d)) fputs("{\"t\":\"float\",\"v\":\"nan\"}", fp);
|
||
else fprintf(fp, "{\"t\":\"float\",\"v\":%.17g}", d);
|
||
} else if (IS_RATIONAL(v)) {
|
||
Rational *r = AS_RATIONAL(v);
|
||
fprintf(fp, "{\"t\":\"frac\",\"n\":%lld,\"d\":%lld}",
|
||
(long long)r->num, (long long)r->den);
|
||
} else if (IS_SYM(v)) {
|
||
fputs("{\"t\":\"sym\",\"v\":", fp);
|
||
json_write_string(fp, sym_name(v));
|
||
fputc('}', fp);
|
||
} else if (IS_STRING(v)) {
|
||
ULString *s = AS_STRING(v);
|
||
fputs(s->mutable ? "{\"t\":\"mstr\",\"v\":" : "{\"t\":\"str\",\"v\":", fp);
|
||
json_write_string(fp, s->data);
|
||
fputc('}', fp);
|
||
} else if (IS_CHAR(v)) {
|
||
fprintf(fp, "{\"t\":\"char\",\"v\":%d}", AS_CHAR(v));
|
||
} else if (IS_PAIR(v)) {
|
||
fputs("{\"t\":\"pair\",\"car\":", fp);
|
||
json_write_value(fp, CAR(v));
|
||
fputs(",\"cdr\":", fp);
|
||
json_write_value(fp, CDR(v));
|
||
fputc('}', fp);
|
||
} else if (IS_VECTOR(v)) {
|
||
ULVector *vec = AS_VECTOR(v);
|
||
fputs("{\"t\":\"vec\",\"v\":[", fp);
|
||
for (size_t i = 0; i < vec->len; i++) {
|
||
if (i > 0) fputc(',', fp);
|
||
json_write_value(fp, vec->data[i]);
|
||
}
|
||
fputs("]}", fp);
|
||
} else if (IS_HASHTABLE(v)) {
|
||
ULHashTable *ht = AS_HASHTABLE(v);
|
||
fputs("{\"t\":\"hash\",\"entries\":[", fp);
|
||
bool first = true;
|
||
for (size_t i = 0; i < ht->nbuckets; i++) {
|
||
HTEntry *e = ht->buckets[i];
|
||
while (e) {
|
||
if (!first) fputc(',', fp);
|
||
first = false;
|
||
fputc('[', fp);
|
||
json_write_value(fp, e->key);
|
||
fputc(',', fp);
|
||
json_write_value(fp, e->value);
|
||
fputc(']', fp);
|
||
e = e->next;
|
||
}
|
||
}
|
||
fputs("]}", fp);
|
||
} else if (IS_PROC(v)) {
|
||
/* Serialize procedure body as source text */
|
||
Proc *p = AS_PROC(v);
|
||
fputs("{\"t\":\"proc\"", fp);
|
||
if (p->name) { fputs(",\"name\":", fp); json_write_string(fp, p->name); }
|
||
fputs(",\"params\":[", fp);
|
||
for (int i = 0; i < p->nparams; i++) {
|
||
if (i > 0) fputc(',', fp);
|
||
json_write_string(fp, sym_name(p->params[i]));
|
||
}
|
||
fputs("]", fp);
|
||
if (!IS_NIL(p->rest)) {
|
||
fputs(",\"rest\":", fp);
|
||
json_write_string(fp, sym_name(p->rest));
|
||
}
|
||
fputs(",\"body\":[", fp);
|
||
for (int i = 0; i < p->body.count; i++) {
|
||
if (i > 0) fputc(',', fp);
|
||
char *s = show(p->body.exprs[i], false);
|
||
json_write_string(fp, s);
|
||
ul_free(s);
|
||
}
|
||
fputs("]}", fp);
|
||
} else if (IS_COMPILED_PROC(v)) {
|
||
/* For compiled procs, we save minimal info */
|
||
CompiledProc *cp = AS_COMPILED_PROC(v);
|
||
fputs("{\"t\":\"cproc\"", fp);
|
||
if (cp->name) { fputs(",\"name\":", fp); json_write_string(fp, cp->name); }
|
||
fputs(",\"params\":[", fp);
|
||
for (int i = 0; i < cp->nparams; i++) {
|
||
if (i > 0) fputc(',', fp);
|
||
json_write_string(fp, sym_name(cp->params[i]));
|
||
}
|
||
fputs("]", fp);
|
||
if (!IS_NIL(cp->rest)) {
|
||
fputs(",\"rest\":", fp);
|
||
json_write_string(fp, sym_name(cp->rest));
|
||
}
|
||
fputs("}", fp);
|
||
} else if (IS_CONTINUATION(v)) {
|
||
fputs("{\"t\":\"cont_marker\"}", fp);
|
||
} else if (IS_BUILTIN(v)) {
|
||
fputs("{\"t\":\"builtin\"}", fp);
|
||
} else {
|
||
fputs("{\"t\":\"opaque\"}", fp);
|
||
}
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
* portal_save — write env + optional continuation to JSON
|
||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||
|
||
void portal_save(Env *env, const char *path, FullCont *continuation) {
|
||
FILE *fp = fopen(path, "w");
|
||
if (!fp) {
|
||
lisp_error("portal_save: cannot open %s", path);
|
||
return;
|
||
}
|
||
|
||
fputs("{\"format\":\"lumbda-portal-v1\",\n", fp);
|
||
|
||
/* RNG state — xoshiro256** as 8 × u32 halves (low, high, low, high, ...) */
|
||
{
|
||
uint32_t halves[8];
|
||
rng_get_halves(halves);
|
||
fputs("\"rng\":{\"algo\":\"xoshiro256**\",\"state\":[", fp);
|
||
for (int i = 0; i < 8; i++) {
|
||
if (i > 0) fputc(',', fp);
|
||
fprintf(fp, "%u", halves[i]);
|
||
}
|
||
fputs("]},\n", fp);
|
||
}
|
||
|
||
/* Environment bindings (user-defined only from global) */
|
||
fputs("\"env\":", fp);
|
||
json_write_env_bindings(fp, env, (env->global == env));
|
||
fputs(",\n", fp);
|
||
|
||
/* Continuation */
|
||
if (continuation) {
|
||
fputs("\"continuation\":{\"t\":\"cont\",\n", fp);
|
||
fprintf(fp, "\"ip\":%d,\n", continuation->ip);
|
||
fprintf(fp, "\"n_instrs\":%d,\n", continuation->n_instrs);
|
||
|
||
/* Stack */
|
||
fputs("\"stack\":[", fp);
|
||
for (int i = 0; i < continuation->stack_len; i++) {
|
||
if (i > 0) fputc(',', fp);
|
||
json_write_value(fp, continuation->stack[i]);
|
||
}
|
||
fputs("],\n", fp);
|
||
|
||
/* Environment */
|
||
fputs("\"env\":", fp);
|
||
json_write_env_bindings(fp, continuation->env, false);
|
||
fputs(",\n", fp);
|
||
|
||
/* Frames */
|
||
fprintf(fp, "\"nframes\":%d,\n", continuation->nframes);
|
||
fputs("\"frames\":[", fp);
|
||
for (int i = 0; i < continuation->nframes; i++) {
|
||
if (i > 0) fputc(',', fp);
|
||
VMFrame *f = &continuation->frames[i];
|
||
fputs("{", fp);
|
||
fprintf(fp, "\"ip\":%d,\"n_instrs\":%d,", f->ip, f->n_instrs);
|
||
fputs("\"stack\":[", fp);
|
||
for (int j = 0; j < f->stack_len; j++) {
|
||
if (j > 0) fputc(',', fp);
|
||
json_write_value(fp, f->stack[j]);
|
||
}
|
||
fputs("],\"env\":", fp);
|
||
json_write_env_bindings(fp, f->env, false);
|
||
fputc('}', fp);
|
||
}
|
||
fputs("]\n", fp);
|
||
|
||
fputc('}', fp);
|
||
} else {
|
||
fputs("\"continuation\":null", fp);
|
||
}
|
||
|
||
fputs("\n}\n", fp);
|
||
fclose(fp);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
* Minimal JSON reader — for portal_resume
|
||
*
|
||
* Supports: objects, arrays, strings, numbers, true, false, null
|
||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||
|
||
typedef enum {
|
||
JT_NULL, JT_BOOL, JT_INT, JT_FLOAT, JT_STRING, JT_ARRAY, JT_OBJECT
|
||
} JsonType;
|
||
|
||
typedef struct JsonNode JsonNode;
|
||
|
||
typedef struct JsonKV {
|
||
char *key;
|
||
JsonNode *value;
|
||
struct JsonKV *next;
|
||
} JsonKV;
|
||
|
||
struct JsonNode {
|
||
JsonType type;
|
||
union {
|
||
bool bval;
|
||
int64_t ival;
|
||
double fval;
|
||
char *sval;
|
||
struct { JsonNode **items; int count; } array;
|
||
JsonKV *object; /* linked list of key-value pairs */
|
||
};
|
||
};
|
||
|
||
static void skip_ws(const char **p) {
|
||
while (**p == ' ' || **p == '\t' || **p == '\n' || **p == '\r') (*p)++;
|
||
}
|
||
|
||
static JsonNode *json_parse(const char **p);
|
||
|
||
static char *json_parse_string(const char **p) {
|
||
if (**p != '"') return NULL;
|
||
(*p)++; /* skip opening " */
|
||
int cap = 256;
|
||
char *buf = (char *)ul_malloc(cap);
|
||
int len = 0;
|
||
while (**p && **p != '"') {
|
||
if (len + 8 >= cap) { cap *= 2; buf = (char *)ul_realloc(buf, cap); }
|
||
if (**p == '\\') {
|
||
(*p)++;
|
||
switch (**p) {
|
||
case '"': buf[len++] = '"'; break;
|
||
case '\\': buf[len++] = '\\'; break;
|
||
case 'n': buf[len++] = '\n'; break;
|
||
case 'r': buf[len++] = '\r'; break;
|
||
case 't': buf[len++] = '\t'; break;
|
||
case '/': buf[len++] = '/'; break;
|
||
case 'u': {
|
||
(*p)++;
|
||
char hex[5] = {0};
|
||
for (int i = 0; i < 4 && **p; i++) hex[i] = *(*p)++;
|
||
int code = (int)strtol(hex, NULL, 16);
|
||
if (code < 0x80) buf[len++] = (char)code;
|
||
else { buf[len++] = '?'; }
|
||
continue; /* skip the (*p)++ below */
|
||
}
|
||
default: buf[len++] = **p;
|
||
}
|
||
} else {
|
||
buf[len++] = **p;
|
||
}
|
||
(*p)++;
|
||
}
|
||
if (**p == '"') (*p)++; /* skip closing " */
|
||
buf[len] = '\0';
|
||
return buf;
|
||
}
|
||
|
||
static JsonNode *json_make_node(JsonType type) {
|
||
JsonNode *n = (JsonNode *)ul_malloc(sizeof(JsonNode));
|
||
memset(n, 0, sizeof(JsonNode));
|
||
n->type = type;
|
||
return n;
|
||
}
|
||
|
||
static JsonNode *json_parse(const char **p) {
|
||
skip_ws(p);
|
||
if (**p == '\0') return NULL;
|
||
|
||
if (**p == '"') {
|
||
JsonNode *n = json_make_node(JT_STRING);
|
||
n->sval = json_parse_string(p);
|
||
return n;
|
||
}
|
||
if (**p == '{') {
|
||
(*p)++;
|
||
JsonNode *n = json_make_node(JT_OBJECT);
|
||
n->object = NULL;
|
||
skip_ws(p);
|
||
if (**p == '}') { (*p)++; return n; }
|
||
JsonKV *tail = NULL;
|
||
while (1) {
|
||
skip_ws(p);
|
||
char *key = json_parse_string(p);
|
||
skip_ws(p);
|
||
if (**p == ':') (*p)++;
|
||
JsonNode *val = json_parse(p);
|
||
JsonKV *kv = (JsonKV *)ul_malloc(sizeof(JsonKV));
|
||
kv->key = key;
|
||
kv->value = val;
|
||
kv->next = NULL;
|
||
if (!tail) n->object = kv;
|
||
else tail->next = kv;
|
||
tail = kv;
|
||
skip_ws(p);
|
||
if (**p == ',') (*p)++;
|
||
else break;
|
||
}
|
||
skip_ws(p);
|
||
if (**p == '}') (*p)++;
|
||
return n;
|
||
}
|
||
if (**p == '[') {
|
||
(*p)++;
|
||
JsonNode *n = json_make_node(JT_ARRAY);
|
||
int cap = 16;
|
||
n->array.items = (JsonNode **)ul_malloc(sizeof(JsonNode *) * cap);
|
||
n->array.count = 0;
|
||
skip_ws(p);
|
||
if (**p == ']') { (*p)++; return n; }
|
||
while (1) {
|
||
if (n->array.count >= cap) {
|
||
cap *= 2;
|
||
n->array.items = (JsonNode **)ul_realloc(n->array.items, sizeof(JsonNode *) * cap);
|
||
}
|
||
n->array.items[n->array.count++] = json_parse(p);
|
||
skip_ws(p);
|
||
if (**p == ',') (*p)++;
|
||
else break;
|
||
}
|
||
skip_ws(p);
|
||
if (**p == ']') (*p)++;
|
||
return n;
|
||
}
|
||
if (strncmp(*p, "true", 4) == 0) {
|
||
*p += 4;
|
||
JsonNode *n = json_make_node(JT_BOOL);
|
||
n->bval = true;
|
||
return n;
|
||
}
|
||
if (strncmp(*p, "false", 5) == 0) {
|
||
*p += 5;
|
||
JsonNode *n = json_make_node(JT_BOOL);
|
||
n->bval = false;
|
||
return n;
|
||
}
|
||
if (strncmp(*p, "null", 4) == 0) {
|
||
*p += 4;
|
||
return json_make_node(JT_NULL);
|
||
}
|
||
/* Number */
|
||
{
|
||
char *end;
|
||
double d = strtod(*p, &end);
|
||
if (end != *p) {
|
||
/* Check if it's actually an integer */
|
||
bool is_int = true;
|
||
for (const char *c = *p; c < end; c++) {
|
||
if (*c == '.' || *c == 'e' || *c == 'E') { is_int = false; break; }
|
||
}
|
||
*p = end;
|
||
if (is_int && d >= -140737488355328LL && d <= 140737488355327LL) {
|
||
JsonNode *n = json_make_node(JT_INT);
|
||
n->ival = (int64_t)d;
|
||
return n;
|
||
}
|
||
JsonNode *n = json_make_node(JT_FLOAT);
|
||
n->fval = d;
|
||
return n;
|
||
}
|
||
}
|
||
/* Skip unknown */
|
||
(*p)++;
|
||
return json_make_node(JT_NULL);
|
||
}
|
||
|
||
static JsonNode *json_obj_get(JsonNode *obj, const char *key) {
|
||
if (!obj || obj->type != JT_OBJECT) return NULL;
|
||
for (JsonKV *kv = obj->object; kv; kv = kv->next) {
|
||
if (kv->key && strcmp(kv->key, key) == 0) return kv->value;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
static const char *json_str(JsonNode *n) {
|
||
if (!n || n->type != JT_STRING) return NULL;
|
||
return n->sval;
|
||
}
|
||
|
||
static int64_t json_int(JsonNode *n) {
|
||
if (!n) return 0;
|
||
if (n->type == JT_INT) return n->ival;
|
||
if (n->type == JT_FLOAT) return (int64_t)n->fval;
|
||
return 0;
|
||
}
|
||
|
||
static void json_free(JsonNode *n) {
|
||
if (!n) return;
|
||
switch (n->type) {
|
||
case JT_STRING: ul_free(n->sval); break;
|
||
case JT_ARRAY:
|
||
for (int i = 0; i < n->array.count; i++) json_free(n->array.items[i]);
|
||
ul_free(n->array.items);
|
||
break;
|
||
case JT_OBJECT: {
|
||
JsonKV *kv = n->object;
|
||
while (kv) {
|
||
JsonKV *next = kv->next;
|
||
ul_free(kv->key);
|
||
json_free(kv->value);
|
||
ul_free(kv);
|
||
kv = next;
|
||
}
|
||
break;
|
||
}
|
||
default: break;
|
||
}
|
||
ul_free(n);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
* Deserialize a JSON value node into a Lisp Value
|
||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||
|
||
static Value json_to_value(JsonNode *n, Env *base_env) {
|
||
if (!n || n->type == JT_NULL) return VAL_NIL;
|
||
if (n->type == JT_INT) return VAL_INT(n->ival);
|
||
if (n->type == JT_FLOAT) return make_double(n->fval);
|
||
|
||
if (n->type != JT_OBJECT) return VAL_NIL;
|
||
|
||
const char *t = json_str(json_obj_get(n, "t"));
|
||
if (!t) return VAL_NIL;
|
||
|
||
if (strcmp(t, "nil") == 0) return VAL_NIL;
|
||
if (strcmp(t, "void") == 0) return VAL_VOID;
|
||
if (strcmp(t, "eof") == 0) return VAL_EOF;
|
||
if (strcmp(t, "bool") == 0) {
|
||
JsonNode *v = json_obj_get(n, "v");
|
||
return (v && v->type == JT_BOOL && v->bval) ? VAL_TRUE : VAL_FALSE;
|
||
}
|
||
if (strcmp(t, "float") == 0) {
|
||
JsonNode *v = json_obj_get(n, "v");
|
||
if (v && v->type == JT_STRING) {
|
||
if (strcmp(v->sval, "+inf") == 0) return make_double(INFINITY);
|
||
if (strcmp(v->sval, "-inf") == 0) return make_double(-INFINITY);
|
||
if (strcmp(v->sval, "nan") == 0) return make_double(NAN);
|
||
}
|
||
if (v && v->type == JT_FLOAT) return make_double(v->fval);
|
||
if (v && v->type == JT_INT) return make_double((double)v->ival);
|
||
return make_double(0.0);
|
||
}
|
||
if (strcmp(t, "frac") == 0) {
|
||
int64_t num = json_int(json_obj_get(n, "n"));
|
||
int64_t den = json_int(json_obj_get(n, "d"));
|
||
return rational_normalize(num, den);
|
||
}
|
||
if (strcmp(t, "sym") == 0) {
|
||
const char *v = json_str(json_obj_get(n, "v"));
|
||
return v ? intern(v) : VAL_NIL;
|
||
}
|
||
if (strcmp(t, "str") == 0) {
|
||
const char *v = json_str(json_obj_get(n, "v"));
|
||
return v ? make_string_from_cstr(v) : make_string_from_cstr("");
|
||
}
|
||
if (strcmp(t, "mstr") == 0) {
|
||
const char *v = json_str(json_obj_get(n, "v"));
|
||
return v ? make_string(v, strlen(v), true) : make_string("", 0, true);
|
||
}
|
||
if (strcmp(t, "char") == 0) {
|
||
return VAL_CHAR((int)json_int(json_obj_get(n, "v")));
|
||
}
|
||
if (strcmp(t, "pair") == 0) {
|
||
Value car = json_to_value(json_obj_get(n, "car"), base_env);
|
||
Value cdr = json_to_value(json_obj_get(n, "cdr"), base_env);
|
||
return cons(car, cdr);
|
||
}
|
||
if (strcmp(t, "vec") == 0) {
|
||
JsonNode *arr = json_obj_get(n, "v");
|
||
if (!arr || arr->type != JT_ARRAY) return make_vector(0, VAL_NIL);
|
||
Value vec = make_vector(arr->array.count, VAL_NIL);
|
||
for (int i = 0; i < arr->array.count; i++) {
|
||
AS_VECTOR(vec)->data[i] = json_to_value(arr->array.items[i], base_env);
|
||
}
|
||
return vec;
|
||
}
|
||
if (strcmp(t, "hash") == 0) {
|
||
Value ht = make_hashtable();
|
||
JsonNode *entries = json_obj_get(n, "entries");
|
||
if (entries && entries->type == JT_ARRAY) {
|
||
for (int i = 0; i < entries->array.count; i++) {
|
||
JsonNode *pair = entries->array.items[i];
|
||
if (pair && pair->type == JT_ARRAY && pair->array.count >= 2) {
|
||
Value k = json_to_value(pair->array.items[0], base_env);
|
||
Value v = json_to_value(pair->array.items[1], base_env);
|
||
ht_set(AS_HASHTABLE(ht), k, v);
|
||
}
|
||
}
|
||
}
|
||
return ht;
|
||
}
|
||
if (strcmp(t, "proc") == 0) {
|
||
/* Reconstruct a Proc from serialized body source */
|
||
const char *name = json_str(json_obj_get(n, "name"));
|
||
JsonNode *params_arr = json_obj_get(n, "params");
|
||
const char *rest_str = json_str(json_obj_get(n, "rest"));
|
||
JsonNode *body_arr = json_obj_get(n, "body");
|
||
|
||
int nparams = (params_arr && params_arr->type == JT_ARRAY) ? params_arr->array.count : 0;
|
||
Value *params = (Value *)ul_malloc(sizeof(Value) * (nparams > 0 ? nparams : 1));
|
||
for (int i = 0; i < nparams; i++) {
|
||
params[i] = intern(json_str(params_arr->array.items[i]));
|
||
}
|
||
Value rest = rest_str ? intern(rest_str) : VAL_NIL;
|
||
|
||
int nbody = (body_arr && body_arr->type == JT_ARRAY) ? body_arr->array.count : 0;
|
||
ExprList body;
|
||
body.count = nbody;
|
||
body.exprs = (Value *)ul_malloc(sizeof(Value) * (nbody > 0 ? nbody : 1));
|
||
for (int i = 0; i < nbody; i++) {
|
||
const char *src = json_str(body_arr->array.items[i]);
|
||
if (src) {
|
||
int count;
|
||
Value *parsed = read_all(src, &count, false);
|
||
body.exprs[i] = count > 0 ? parsed[0] : VAL_VOID;
|
||
ul_free(parsed);
|
||
} else {
|
||
body.exprs[i] = VAL_VOID;
|
||
}
|
||
}
|
||
|
||
Proc *p = make_proc(params, nparams, rest, body, base_env, name);
|
||
ul_free(params);
|
||
return VAL_PTR(p);
|
||
}
|
||
if (strcmp(t, "builtin") == 0 || strcmp(t, "opaque") == 0 ||
|
||
strcmp(t, "cproc") == 0 || strcmp(t, "cont_marker") == 0) {
|
||
/* These can't be fully reconstructed — return void */
|
||
return VAL_VOID;
|
||
}
|
||
|
||
return VAL_NIL;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
* portal_resume — load machine state from JSON file
|
||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||
|
||
bool portal_resume(const char *path, Env *base_env, Env **out_env, FullCont **out_cont) {
|
||
FILE *fp = fopen(path, "r");
|
||
if (!fp) return false;
|
||
|
||
fseek(fp, 0, SEEK_END);
|
||
long sz = ftell(fp);
|
||
fseek(fp, 0, SEEK_SET);
|
||
|
||
char *src = (char *)ul_malloc(sz + 1);
|
||
size_t nread = fread(src, 1, sz, fp);
|
||
src[nread] = '\0';
|
||
fclose(fp);
|
||
|
||
const char *p = src;
|
||
JsonNode *root = json_parse(&p);
|
||
ul_free(src);
|
||
|
||
if (!root || root->type != JT_OBJECT) {
|
||
json_free(root);
|
||
return false;
|
||
}
|
||
|
||
/* Check format */
|
||
const char *fmt = json_str(json_obj_get(root, "format"));
|
||
if (!fmt || strcmp(fmt, "lumbda-portal-v1") != 0) {
|
||
json_free(root);
|
||
return false;
|
||
}
|
||
|
||
/* Restore RNG state if present (absent = pre-RNG portal file, skip) */
|
||
JsonNode *rng_node = json_obj_get(root, "rng");
|
||
if (rng_node && rng_node->type == JT_OBJECT) {
|
||
JsonNode *state_arr = json_obj_get(rng_node, "state");
|
||
if (state_arr && state_arr->type == JT_ARRAY && state_arr->array.count == 8) {
|
||
uint32_t halves[8];
|
||
for (int i = 0; i < 8; i++) {
|
||
halves[i] = (uint32_t)json_int(state_arr->array.items[i]);
|
||
}
|
||
rng_set_halves(halves);
|
||
}
|
||
}
|
||
|
||
/* Merge environment bindings into base_env */
|
||
JsonNode *env_node = json_obj_get(root, "env");
|
||
if (env_node && env_node->type == JT_OBJECT) {
|
||
for (JsonKV *kv = env_node->object; kv; kv = kv->next) {
|
||
if (!kv->key) continue;
|
||
Value sym = intern(kv->key);
|
||
Value val = json_to_value(kv->value, base_env);
|
||
/* Only set if non-void (skip builtins that couldn't be serialized) */
|
||
if (!IS_VOID(val)) {
|
||
env_define(base_env, sym, val);
|
||
}
|
||
}
|
||
}
|
||
|
||
*out_env = base_env;
|
||
|
||
/* Continuation — for now, we mark it present but don't fully
|
||
* reconstruct bytecode (that requires code serialization).
|
||
* The continuation is stored so callers know one was saved. */
|
||
JsonNode *cont_node = json_obj_get(root, "continuation");
|
||
if (cont_node && cont_node->type == JT_OBJECT) {
|
||
/* A continuation was saved. We can't fully restore it without
|
||
* reconstructing the bytecode instructions, but we signal its presence. */
|
||
*out_cont = (FullCont *)ul_malloc(sizeof(FullCont));
|
||
(*out_cont)->hdr.type = OBJ_CONTINUATION;
|
||
(*out_cont)->nframes = 0;
|
||
(*out_cont)->frames = NULL;
|
||
(*out_cont)->stack = NULL;
|
||
(*out_cont)->stack_len = 0;
|
||
(*out_cont)->ip = (int)json_int(json_obj_get(cont_node, "ip"));
|
||
(*out_cont)->instrs = NULL;
|
||
(*out_cont)->n_instrs = 0;
|
||
(*out_cont)->env = base_env;
|
||
(*out_cont)->vm_id = NULL;
|
||
} else {
|
||
*out_cont = NULL;
|
||
}
|
||
|
||
json_free(root);
|
||
return true;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
* portal-checkpoint! builtin — signals VM to save at next safe point
|
||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||
|
||
static Value builtin_portal_checkpoint(Value *args, int nargs, Env *env) {
|
||
(void)env;
|
||
if (nargs < 1 || !IS_STRING(args[0]))
|
||
lisp_error("portal-checkpoint!: expected string path");
|
||
g_portal_checkpoint_path = ul_strdup(AS_STRING(args[0])->data);
|
||
return VAL_VOID;
|
||
}
|
||
|
||
static Value builtin_random_seed_bang(Value *args, int nargs, Env *env) {
|
||
(void)env;
|
||
if (nargs != 1) lisp_error("random-seed!: expected 1 arg");
|
||
rng_seed((uint64_t)as_number_int(args[0]));
|
||
return VAL_VOID;
|
||
}
|
||
|
||
static Value builtin_random(Value *args, int nargs, Env *env) {
|
||
(void)args; (void)env;
|
||
if (nargs != 0) lisp_error("random: expected 0 args");
|
||
return make_double((double)(rng_next() >> 11) / (double)(1ULL << 53));
|
||
}
|
||
|
||
static Value builtin_random_int(Value *args, int nargs, Env *env) {
|
||
(void)env;
|
||
if (nargs != 1) lisp_error("random-int: expected 1 arg");
|
||
int64_t n = as_number_int(args[0]);
|
||
if (n <= 0) lisp_error("random-int: n must be positive, got %lld", (long long)n);
|
||
return VAL_INT((int64_t)(rng_next() % (uint64_t)n));
|
||
}
|
||
|
||
static Value builtin_random_state(Value *args, int nargs, Env *env) {
|
||
(void)args; (void)env;
|
||
if (nargs != 0) lisp_error("random-state: expected 0 args");
|
||
uint32_t halves[8];
|
||
rng_get_halves(halves);
|
||
Value list = VAL_NIL;
|
||
for (int i = 7; i >= 0; i--) {
|
||
list = cons(VAL_INT((int64_t)halves[i]), list);
|
||
}
|
||
return list;
|
||
}
|
||
|
||
static Value builtin_random_state_bang(Value *args, int nargs, Env *env) {
|
||
(void)env;
|
||
if (nargs != 1) lisp_error("random-state!: expected 1 arg (list of 8 ints)");
|
||
uint32_t halves[8];
|
||
Value lst = args[0];
|
||
for (int i = 0; i < 8; i++) {
|
||
if (!IS_PAIR(lst)) lisp_error("random-state!: list too short");
|
||
halves[i] = (uint32_t)as_number_int(CAR(lst));
|
||
lst = CDR(lst);
|
||
}
|
||
rng_set_halves(halves);
|
||
return VAL_VOID;
|
||
}
|
||
|
||
void register_portal_builtins(Env *env) {
|
||
env_define(env, intern("portal-checkpoint!"), VAL_BUILTIN(builtin_portal_checkpoint));
|
||
env_define(env, intern("portal-save!"), VAL_BUILTIN(builtin_portal_checkpoint));
|
||
env_define(env, intern("random-seed!"), VAL_BUILTIN(builtin_random_seed_bang));
|
||
env_define(env, intern("random"), VAL_BUILTIN(builtin_random));
|
||
env_define(env, intern("random-int"), VAL_BUILTIN(builtin_random_int));
|
||
env_define(env, intern("random-state"), VAL_BUILTIN(builtin_random_state));
|
||
env_define(env, intern("random-state!"), VAL_BUILTIN(builtin_random_state_bang));
|
||
/* Default seed = 0 so (random) without (random-seed!) is deterministic
|
||
* and non-zero. All three impls agree on this startup state. */
|
||
rng_seed(0);
|
||
}
|