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