runtime: HTTP, in-process graph store, LLM, fs_list

Batches 2/3/4 of the runtime extension. The runtime grew from 1620
to 3112 lines (.c) and 247 to 286 lines (.h) — adding 27 new or
real-implementation builtins and replacing every batch-1 stub.

Batch 2 — HTTP / fs (8 builtins)
- http_get, http_post: replaced stubs with real libcurl client.
  Network errors return JSON {"error":"..."} so callers can detect.
- http_post_json: sets Content-Type: application/json.
- http_get_with_headers, http_post_with_headers: ElMap → headers.
- http_post_form_auth: form-urlencoded + Authorization header
  (Stripe-style API calls).
- http_serve: replaced stub with real POSIX-socket server, threaded,
  capped at 64 concurrent connections. Auto-detects content type
  (HTML / JSON / plain). Handler dispatch via named registry.
- fs_list: directory listing via opendir/readdir.

Batch 3 — In-process graph store (14 builtins)
- engram_node, engram_node_full: create node, returns UUID.
- engram_get_node, engram_forget, engram_node_count.
- engram_strengthen: Hebbian potentiation (+0.05, clamp 1.0,
  bumps last_activated).
- engram_search, engram_scan_nodes: text search, paginated scan.
- engram_connect, engram_edge_between, engram_neighbors,
  engram_neighbors_filtered, engram_edge_count.
- engram_activate: real spreading-activation algorithm.
  BFS to depth, max-activation merge across paths, decay 0.7/hop,
  multiplied by node confidence, filtered by epistemic_confidence
  ≥ 0.2 (refresh threshold), sorted desc.
- engram_save, engram_load: JSON snapshot persistence.

Batch 4 — LLM (5 builtins)
- llm_call, llm_call_system: Anthropic /v1/messages via libcurl.
  ANTHROPIC_API_KEY from env. Default model claude-sonnet-4-5.
- llm_vision: adds image content block. URL / base64 / file path
  detected by prefix.
- llm_models: returns the available model list.
- llm_call_agentic: stubbed with TODO (single-turn fallback to
  llm_call_system); full tool_use loop is the next iteration.

Codegen fix: emit Float literals as `el_from_float(<v>)`. Without
the wrapper, C implicit conversion truncates 0.8 to 0 when passed to
a builtin expecting el_val_t. Float helpers moved to el_runtime.h
so generated programs can call them.

Compile-time
- cc -std=c11 -Wall -Wextra -c el_runtime.c → no errors, no warnings.
- Link requires -lcurl -lpthread (documented in header comment).

Verified end-to-end
- engram_node × 2, engram_connect, engram_activate("Hebbian", 2)
  returns 2 activated nodes with correct epistemic confidence.
- http_get("https://httpbin.org/get") returns 259-byte JSON live.
- Self-host closure: stage1 vs stage2 byte-identical against the
  new runtime.
- engram_save → engram_load round-trip preserves graph.

dist/platform/elc rebuilt against the new runtime (147 KB, up from
94 KB due to libcurl link). .prev2 preserves the prior binary.
This commit is contained in:
Will Anderson
2026-04-30 13:29:31 -05:00
parent 9fca4dc3ce
commit 951b8d574b
7 changed files with 1609 additions and 67 deletions
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1650,7 +1650,7 @@ el_val_t cg_expr(el_val_t expr) {
}
if (str_eq(kind, EL_STR("Float"))) {
el_val_t v = el_get_field(expr, EL_STR("value"));
return v;
return el_str_concat(el_str_concat(EL_STR("el_from_float("), v), EL_STR(")"));
}
if (str_eq(kind, EL_STR("Str"))) {
el_val_t v = el_get_field(expr, EL_STR("value"));
Vendored Executable
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
+57 -2
View File
@@ -22,6 +22,15 @@
* EL_STR(s) cast string literal to el_val_t
* EL_CSTR(v) cast el_val_t back to const char*
* EL_INT(v) identity el_val_t is already int64_t
*
* Link requirements:
* -lcurl required for the HTTP client (http_get, http_post, llm_*).
* -lpthread required for the HTTP server (one detached thread per
* connection, capped at 64 concurrent).
*
* Canonical compile command:
* cc -std=c11 -I el-compiler/runtime -lcurl -lpthread \
* -o <out> <prog>.c el-compiler/runtime/el_runtime.c
*/
#pragma once
@@ -36,6 +45,22 @@ typedef int64_t el_val_t;
#define EL_INT(v) (v)
#define EL_NULL ((el_val_t)0)
/* Float values share the el_val_t (int64) slot via a bit-cast.
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
* underlying bits represent the IEEE 754 double. Float-aware builtins
* (math, format, json) round-trip via these helpers. */
static inline double el_to_float(el_val_t v) {
union { int64_t i; double f; } u;
u.i = (int64_t)v;
return u.f;
}
static inline el_val_t el_from_float(double f) {
union { double f; int64_t i; } u;
u.f = f;
return (el_val_t)u.i;
}
#ifdef __cplusplus
extern "C" {
#endif
@@ -88,12 +113,18 @@ el_val_t el_map_set(el_val_t map, el_val_t key, el_val_t value);
el_val_t http_get(el_val_t url);
el_val_t http_post(el_val_t url, el_val_t body);
el_val_t http_post_json(el_val_t url, el_val_t json_body);
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
void http_serve(el_val_t port, el_val_t handler);
void http_set_handler(el_val_t name);
/* ── Filesystem ──────────────────────────────────────────────────────────── */
el_val_t fs_read(el_val_t path);
el_val_t fs_write(el_val_t path, el_val_t content);
el_val_t fs_list(el_val_t path);
/* ── JSON ────────────────────────────────────────────────────────────────── */
@@ -206,9 +237,33 @@ el_val_t dharma_peers(void);
* network-wide across all connected CGI graphs. */
el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience);
el_val_t engram_activate(el_val_t query, el_val_t depth);
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
el_val_t engram_node_full(el_val_t content, el_val_t node_type, el_val_t label,
el_val_t salience, el_val_t importance, el_val_t confidence,
el_val_t tier, el_val_t tags);
el_val_t engram_get_node(el_val_t id);
void engram_strengthen(el_val_t node_id);
void engram_forget(el_val_t node_id);
el_val_t engram_node_count(void);
el_val_t engram_search(el_val_t query, el_val_t limit);
el_val_t engram_scan_nodes(el_val_t limit, el_val_t offset);
void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation);
el_val_t engram_edge_between(el_val_t from_id, el_val_t to_id);
el_val_t engram_neighbors(el_val_t node_id);
el_val_t engram_neighbors_filtered(el_val_t node_id, el_val_t max_depth, el_val_t direction);
el_val_t engram_edge_count(void);
el_val_t engram_activate(el_val_t query, el_val_t depth);
el_val_t engram_save(el_val_t path);
el_val_t engram_load(el_val_t path);
/* ── LLM (Anthropic API client) ─────────────────────────────────────────────
* All functions call https://api.anthropic.com/v1/messages with the API key
* from env ANTHROPIC_API_KEY. Default model when empty: claude-sonnet-4-5. */
el_val_t llm_call(el_val_t model, el_val_t prompt);
el_val_t llm_call_system(el_val_t model, el_val_t system_prompt, el_val_t user_prompt);
el_val_t llm_call_agentic(el_val_t model, el_val_t system, el_val_t user, el_val_t tools);
el_val_t llm_vision(el_val_t model, el_val_t system, el_val_t prompt, el_val_t image_url_or_b64);
el_val_t llm_models(void);
/* ── args() ─────────────────────────────────────────────────────────────────
* Provides access to command-line arguments passed to the program.
+5 -1
View File
@@ -108,8 +108,12 @@ fn cg_expr(expr: Map<String, Any>) -> String {
}
if kind == "Float" {
// Wrap Float literals in el_from_float() so the bit pattern is
// preserved through the el_val_t (int64) slot. Without this,
// implicit doubleint64 conversion in C truncates `0.8` to `0`
// when passed to a builtin that expects el_val_t.
let v: String = expr["value"]
return v
return "el_from_float(" + v + ")"
}
if kind == "Str" {
+5 -5
View File
@@ -1,5 +1,4 @@
// elc-combined.el El self-hosting compiler, single-file bootstrap edition
// Inlines lexer + parser + codegen CLI entry at end.
// lexer.el el self-hosting lexer
//
@@ -536,7 +535,6 @@ fn lex(source: String) -> [Map<String, Any>] {
let tokens = native_list_append(tokens, make_tok("Eof", ""))
tokens
}
// parser.el el self-hosting recursive descent parser
//
// Consumes the token list produced by lexer.el and builds a list of AST
@@ -1320,7 +1318,6 @@ fn parse(tokens: [Map<String, Any>]) -> [Map<String, Any>] {
}
stmts
}
// codegen.el El compiler C source code generator
//
// Input: list of AST statement maps (from parser.el)
@@ -1431,8 +1428,12 @@ fn cg_expr(expr: Map<String, Any>) -> String {
}
if kind == "Float" {
// Wrap Float literals in el_from_float() so the bit pattern is
// preserved through the el_val_t (int64) slot. Without this,
// implicit doubleint64 conversion in C truncates `0.8` to `0`
// when passed to a builtin that expects el_val_t.
let v: String = expr["value"]
return v
return "el_from_float(" + v + ")"
}
if kind == "Str" {
@@ -2128,7 +2129,6 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
// Return empty string output was streamed via println
""
}
// compiler.el el self-hosting compiler pipeline
//
// Wires lexer -> parser -> codegen into a single compile() function.