runtime: engram_*_json accessors, http_set_handler dlsym, codegen int-call

Three changes that turned the runtime into something Engram-the-server
can actually run on top of.

1. engram_*_json accessors. The runtime's engram_get_node/search/scan/
   neighbors/activate return ElList/ElMap; passing those through
   json_stringify hit the type-erasure wall (an ElList* has no header
   that distinguishes it from a string pointer). Added pre-serialized
   sibling builtins:

     engram_get_node_json(id)         -> JSON object
     engram_search_json(query, limit) -> JSON array of node objects
     engram_scan_nodes_json(limit, offset)
     engram_neighbors_json(node_id, max_depth, direction)
     engram_activate_json(query, depth)
     engram_stats_json()

   Each walks the typed C structures and serializes directly, reusing
   the existing engram_emit_node_json / engram_emit_edge_json helpers
   from the snapshot path.

2. http_set_handler now falls back to dlsym(RTLD_DEFAULT, name) when
   the named handler isn't already in the C-level registry. El programs
   that define `fn handle_request(method, path, body) -> String` can
   register themselves just by calling http_set_handler("handle_request").
   No C glue required. Verified live on a real El server.

3. Codegen: extended int-typed dispatch on `+` to handle Calls. New
   helper is_int_call recognizes a known-int-returning builtin set:
   str_len, str_index_of, str_to_int, str_char_code, native_list_len,
   el_list_len, len, json_get_int, json_array_len, engram_node_count,
   engram_edge_count, time_now, time_now_utc, time_diff, time_add,
   time_from_parts, el_abs/max/min, float_to_int. With this,
   `pos + str_len(needle)` compiles to integer arithmetic instead of
   string concat. The earlier limitation noted in the previous commit
   (Ident + Call returning Int) is now closed.

Also: el_to_float / el_from_float moved to el_runtime.h as static
inlines so generated programs can use them. Eliminates the unused
inline definitions that were duplicating in the .c file.

Closure verified: stage1 vs stage2 byte-identical against the new
runtime. dist/platform/elc rebuilt; .prev4 preserved.

Engram server (engram/src/server.el) end-to-end:
  POST /api/nodes ×3 → 3 UUIDs returned
  POST /api/edges ×2 → linkage made
  GET /api/stats → {"node_count":3,"edge_count":2}
  GET /api/search?q=spreading&limit=5 → 1 hit, full node JSON
  POST /api/activate {"query":"Hebbian","depth":3}
    → seed node @ hop 0, strength 0.8
    → 1-hop neighbor @ strength 0.392 (= 0.8 × 0.7 weight × 0.7 decay)
  GET /api/neighbors/<id>?depth=2 → {node, edge, hops} triple
  POST /api/save → {"ok":true,"path":"..."}
  Server stays alive across all routes.

Snapshot save/load on restart still TODO — server starts with 0 nodes
even when a snapshot exists; investigation pending.
This commit is contained in:
Will Anderson
2026-04-30 13:44:41 -05:00
parent 6bdd4a4ba9
commit 0fa9e749e1
7 changed files with 428 additions and 3 deletions
BIN
View File
Binary file not shown.
+102
View File
@@ -50,6 +50,7 @@ el_val_t param_decl(el_val_t param, el_val_t idx);
el_val_t params_to_c(el_val_t params);
el_val_t transform_implicit_return(el_val_t body);
el_val_t is_int_name(el_val_t name);
el_val_t is_int_call(el_val_t call_expr);
el_val_t add_int_name(el_val_t name);
el_val_t build_int_names_for_params(el_val_t params);
el_val_t cg_fn(el_val_t stmt);
@@ -1715,7 +1716,37 @@ el_val_t cg_expr(el_val_t expr) {
}
}
}
if (str_eq(left_kind, EL_STR("Ident"))) {
if (str_eq(right_kind, EL_STR("Call"))) {
el_val_t lname = el_get_field(left, EL_STR("name"));
if (is_int_name(lname)) {
if (is_int_call(right)) {
el_val_t op_c = binop_to_c(op);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" ")), op_c), EL_STR(" ")), right_c), EL_STR(")"));
}
}
}
}
if (str_eq(right_kind, EL_STR("Ident"))) {
if (str_eq(left_kind, EL_STR("Call"))) {
el_val_t rname = el_get_field(right, EL_STR("name"));
if (is_int_name(rname)) {
if (is_int_call(left)) {
el_val_t op_c = binop_to_c(op);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" ")), op_c), EL_STR(" ")), right_c), EL_STR(")"));
}
}
}
}
if (str_eq(left_kind, EL_STR("Call"))) {
if (str_eq(right_kind, EL_STR("Call"))) {
if (is_int_call(left)) {
if (is_int_call(right)) {
el_val_t op_c = binop_to_c(op);
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" ")), op_c), EL_STR(" ")), right_c), EL_STR(")"));
}
}
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_str_concat("), left_c), EL_STR(", ")), right_c), EL_STR(")"));
}
if (str_eq(right_kind, EL_STR("Call"))) {
@@ -2160,6 +2191,77 @@ el_val_t is_int_name(el_val_t name) {
return 0;
}
el_val_t is_int_call(el_val_t call_expr) {
el_val_t func = el_get_field(call_expr, EL_STR("func"));
el_val_t fk = el_get_field(func, EL_STR("expr"));
if (!str_eq(fk, EL_STR("Ident"))) {
return 0;
}
el_val_t name = el_get_field(func, EL_STR("name"));
if (str_eq(name, EL_STR("str_len"))) {
return 1;
}
if (str_eq(name, EL_STR("str_index_of"))) {
return 1;
}
if (str_eq(name, EL_STR("str_to_int"))) {
return 1;
}
if (str_eq(name, EL_STR("str_char_code"))) {
return 1;
}
if (str_eq(name, EL_STR("native_list_len"))) {
return 1;
}
if (str_eq(name, EL_STR("el_list_len"))) {
return 1;
}
if (str_eq(name, EL_STR("len"))) {
return 1;
}
if (str_eq(name, EL_STR("json_get_int"))) {
return 1;
}
if (str_eq(name, EL_STR("json_array_len"))) {
return 1;
}
if (str_eq(name, EL_STR("engram_node_count"))) {
return 1;
}
if (str_eq(name, EL_STR("engram_edge_count"))) {
return 1;
}
if (str_eq(name, EL_STR("time_now"))) {
return 1;
}
if (str_eq(name, EL_STR("time_now_utc"))) {
return 1;
}
if (str_eq(name, EL_STR("time_diff"))) {
return 1;
}
if (str_eq(name, EL_STR("time_add"))) {
return 1;
}
if (str_eq(name, EL_STR("time_from_parts"))) {
return 1;
}
if (str_eq(name, EL_STR("el_abs"))) {
return 1;
}
if (str_eq(name, EL_STR("el_max"))) {
return 1;
}
if (str_eq(name, EL_STR("el_min"))) {
return 1;
}
if (str_eq(name, EL_STR("float_to_int"))) {
return 1;
}
return 0;
return 0;
}
el_val_t add_int_name(el_val_t name) {
el_val_t csv = state_get(EL_STR("__int_names"));
if (str_eq(csv, EL_STR(""))) {
Vendored Executable
BIN
View File
Binary file not shown.