lumbda/c/portal.c
russell@unturf.com 54c4c651bb portal-rng: asm xoshiro256** + cross-impl tests + default seed=0
Completes ticket 0001 started in 27f468c. 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 (supplements 27f468c):
- 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.
2026-04-20 11:11:47 -04:00

815 lines
30 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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);
}