engram tiered storage: engram-service wiring + elc fold-hang fix + prune-store mirror

- Wire paged store into the ENGRAM SERVICE (server.el, the authoritative durable
  owner): boot->engram_store_boot, persist_canonical->engram_store_checkpoint,
  gated by ENGRAM_STORE.
- elc (lang/elc.c + src/parser.el + codegen.el + elc-combined.el): OOB guard in
  tok_kind/tok_value + parse_block progress backstop — fixes the pre-existing
  unbounded-memory fold hang on sessions.el.
- engram_prune_telemetry mirrors ISE prune to the store (store_forget) so store
  live-count tracks resident and stale telemetry stays bounded.
- Deployed live 2026-08-12: engram :8742 on neuron.egm+WAL, count reconciled 11552.
This commit is contained in:
2026-08-12 14:14:20 -05:00
parent 9a0266cbf9
commit bb64a236ed
7 changed files with 263 additions and 26 deletions
+105 -2
View File
@@ -1423,15 +1423,53 @@ el_val_t tok_at(el_val_t tokens, el_val_t pos) {
}
el_val_t tok_kind(el_val_t tokens, el_val_t pos) {
/* Out-of-range reads MUST report the Eof sentinel so every `== "Eof"`
termination guard in the parser fires. Without this, reading past the
trailing Eof token returns runtime null (native_list_get OOB -> 0), which
matches no delimiter, letting inner parse loops (parse_block, parse_binop)
append AST nodes forever on malformed input -> unbounded allocation -> OOM. */
el_val_t n = (native_list_len(tokens) / 2);
if (pos < 0) {
return EL_STR("Eof");
}
if (pos >= n) {
return EL_STR("Eof");
}
return native_list_get(tokens, (pos * 2));
return 0;
}
el_val_t tok_value(el_val_t tokens, el_val_t pos) {
el_val_t n = (native_list_len(tokens) / 2);
if (pos < 0) {
return EL_STR("");
}
if (pos >= n) {
return EL_STR("");
}
return native_list_get(tokens, ((pos * 2) + 1));
return 0;
}
/* parse_progress_fatal — robustness backstop. Called by the token-consuming
driver loops when they detect they have iterated more times than there are
tokens (an impossibility for a well-formed program, where every iteration
consumes at least one token). Names the offending token and exits non-zero
instead of looping forever / exhausting memory. */
el_val_t parse_progress_fatal(el_val_t where, el_val_t tokens, el_val_t pos) {
el_val_t k = tok_kind(tokens, pos);
el_val_t v = tok_value(tokens, pos);
println(el_str_concat(el_str_concat(el_str_concat(el_str_concat(
EL_STR("elc: FATAL: parser made no forward progress in "), where),
EL_STR(" at token index ")), native_int_to_str(pos)),
el_str_concat(EL_STR(" (kind="), el_str_concat(k, EL_STR(")")))));
println(el_str_concat(el_str_concat(
EL_STR("elc: likely a malformed construct near '"), v),
EL_STR("' — e.g. an unterminated string or an unescaped double-quote inside a string literal (use \\\" ).")));
exit(1);
return 0;
}
el_val_t expect(el_val_t tokens, el_val_t pos, el_val_t kind) {
el_val_t k = tok_kind(tokens, pos);
if (str_eq(k, kind)) {
@@ -2689,7 +2727,16 @@ el_val_t parse_block(el_val_t tokens, el_val_t pos) {
el_val_t p = expect(tokens, pos, EL_STR("LBrace"));
el_val_t stmts = native_list_empty();
el_val_t running = 1;
/* Runaway backstop: a block can hold at most (token count) statements, since
every iteration consumes >= 1 token. If we exceed that, the cursor has run
off the end without terminating (malformed input) -> fail fast, don't hang. */
el_val_t __blk_total = (native_list_len(tokens) / 2);
el_val_t __blk_iters = 0;
while (running) {
__blk_iters = (__blk_iters + 1);
if (__blk_iters > (__blk_total + 8)) {
parse_progress_fatal(EL_STR("parse_block"), tokens, p);
}
el_val_t k = tok_kind(tokens, p);
if (str_eq(k, EL_STR("RBrace"))) {
running = 0;
@@ -4838,9 +4885,51 @@ el_val_t next_if_id(void) {
return 0;
}
/* is_void_builtin — true for runtime builtins declared `void` in el_runtime.h.
User `-> Void` functions are emitted as el_val_t (return 0) so they are safe
to assign; only these C-level void builtins are not. */
el_val_t is_void_builtin(el_val_t name) {
if (str_eq(name, EL_STR("println"))) { return 1; }
if (str_eq(name, EL_STR("print"))) { return 1; }
if (str_eq(name, EL_STR("engram_strengthen"))) { return 1; }
if (str_eq(name, EL_STR("engram_forget"))) { return 1; }
if (str_eq(name, EL_STR("engram_connect"))) { return 1; }
if (str_eq(name, EL_STR("dharma_emit"))) { return 1; }
if (str_eq(name, EL_STR("dharma_strengthen"))) { return 1; }
if (str_eq(name, EL_STR("llm_register_tool"))) { return 1; }
if (str_eq(name, EL_STR("exit_program"))) { return 1; }
if (str_eq(name, EL_STR("http_serve"))) { return 1; }
if (str_eq(name, EL_STR("http_set_handler"))) { return 1; }
if (str_eq(name, EL_STR("http_serve_async"))) { return 1; }
if (str_eq(name, EL_STR("el_cgi_init"))) { return 1; }
if (str_eq(name, EL_STR("el_retain"))) { return 1; }
if (str_eq(name, EL_STR("el_release"))) { return 1; }
return 0;
}
/* cg_expr_is_void — true if `val` is a direct call to a void builtin, so the
if-expression arm must emit it as a bare statement rather than assigning its
(nonexistent) value to the result var. */
el_val_t cg_expr_is_void(el_val_t val) {
el_val_t vk = el_get_field(val, EL_STR("expr"));
if (str_eq(vk, EL_STR("Call"))) {
el_val_t f = el_get_field(val, EL_STR("func"));
el_val_t fk = el_get_field(f, EL_STR("expr"));
if (str_eq(fk, EL_STR("Ident"))) {
return is_void_builtin(el_get_field(f, EL_STR("name")));
}
}
return 0;
}
el_val_t cg_if_expr_arm(el_val_t stmts, el_val_t result_var) {
el_val_t n = native_list_len(stmts);
el_val_t parts = native_list_empty();
/* Track names already declared in this arm's C block. El permits `let x`
to redeclare/rebind x in the same scope, but C forbids redeclaring the
same name in one block. Emit `el_val_t x = ...` the first time and a
plain `x = ...` reassignment thereafter (mirrors cg_stmt's `declared`). */
el_val_t declared = native_list_empty();
el_val_t i = 0;
while (i < n) {
el_val_t s = native_list_get(stmts, i);
@@ -4853,18 +4942,31 @@ el_val_t cg_if_expr_arm(el_val_t stmts, el_val_t result_var) {
el_val_t name = el_get_field(s, EL_STR("name"));
el_val_t val = el_get_field(s, EL_STR("value"));
el_val_t val_c = cg_expr(val);
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_val_t "), name), EL_STR(" = ")), val_c), EL_STR("; ")));
if (list_contains(declared, name)) {
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(name, EL_STR(" = ")), val_c), EL_STR("; ")));
} else {
declared = native_list_append(declared, name);
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_val_t "), name), EL_STR(" = ")), val_c), EL_STR("; ")));
}
} else {
if (str_eq(sk, EL_STR("Return"))) {
el_val_t val = el_get_field(s, EL_STR("value"));
el_val_t val_c = cg_expr(val);
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(result_var, EL_STR(" = (")), val_c), EL_STR("); ")));
if (cg_expr_is_void(val)) {
parts = native_list_append(parts, el_str_concat(val_c, EL_STR("; ")));
} else {
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(result_var, EL_STR(" = (")), val_c), EL_STR("); ")));
}
} else {
if (str_eq(sk, EL_STR("Expr"))) {
el_val_t val = el_get_field(s, EL_STR("value"));
el_val_t val_c = cg_expr(val);
if (is_last) {
if (cg_expr_is_void(val)) {
parts = native_list_append(parts, el_str_concat(val_c, EL_STR("; ")));
} else {
parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(result_var, EL_STR(" = (")), val_c), EL_STR("); ")));
}
} else {
parts = native_list_append(parts, el_str_concat(el_str_concat(EL_STR("(void)("), val_c), EL_STR("); ")));
}
@@ -4883,6 +4985,7 @@ el_val_t cg_if_expr_arm(el_val_t stmts, el_val_t result_var) {
}
el_val_t result = str_join(parts, EL_STR(""));
el_release(parts);
el_release(declared);
return result;
return 0;
}