lumbda/c/portal.c
russell@unturf.com 4960381c67 portal-rng: add (random-seed-from-os!) across all three tiers
Ticket 0002 — reads 8 bytes from /dev/urandom (little-endian u64) and
seeds xoshiro256**. Opt-in kernel entropy for stochastic runs; the
default stays deterministic (k=0 at startup), so ticket 0001's
portal-reproducibility contract is unchanged.

Real-world flow now one call away:
  Machine A: (random-seed-from-os!) + run simulation + portal-save
  Machine B: portal-resume — same stream, bit-for-bit

All three impls fail loud on /dev/urandom trouble (LispErr in Python
and C, stderr + exit(1) in asm) — no silent fallback to a weak seed.

Tests:
- tests/functional.lsp: 2 new shared asserts (entropic + replay)
- asm/test.sh: 2 new asm-local checks (149 total, was 147)
- make test-all green across Python (205), C (205), asm (149)

Whitepaper §7.5 gains one sentence noting the OS-seed path.
unmoad: zero new findings in added code.
2026-04-20 15:50:26 -04:00

832 lines
31 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"
#include <fcntl.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;
}
/* Opt-in kernel entropy seed. See docs/tickets/0002-os-entropy-seed.md. */
static Value builtin_random_seed_from_os(Value *args, int nargs, Env *env) {
(void)args; (void)env;
if (nargs != 0) lisp_error("random-seed-from-os!: expected 0 args");
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) lisp_error("random-seed-from-os!: cannot open /dev/urandom");
uint64_t k = 0;
ssize_t n = read(fd, &k, sizeof k);
close(fd);
if (n != (ssize_t)sizeof k)
lisp_error("random-seed-from-os!: short read from /dev/urandom");
rng_seed(k);
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-seed-from-os!"), VAL_BUILTIN(builtin_random_seed_from_os));
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);
}