From 35b07bade29fcd93a663a2f538b43c31f07e28cb Mon Sep 17 00:00:00 2001 From: bigmerge Date: Mon, 17 Aug 2026 08:37:47 -0500 Subject: [PATCH] EXPERIMENT: resolve the crossing at execution, not at emission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HYPOTHESIS (Will's): a compiler whose one compiled mechanism is extending the LANGUAGE — not the compiler — can compose without recompilation. ISHIKAWA — why does a construct require a recompile today? method codegen inlines the target call into the body machine the binary has no table to consult material the declaration lives in source, read at compile time measurement nothing observes what applied at runtime root cause the crossing is resolved at EMISSION, not at EXECUTION CHANGE: codegen emits one unconditional indirection per fn. Which constructs apply is read from a table that can be written AFTER the binary exists; targets resolve through dlsym against the running image. PREDICTIONS AND RESULTS P1 a construct declared after the build applies TRUE P2 an unlinked target is skipped, not fatal TRUE P3 emitting on every fn is measurably slower FALSE — 0.37s -> 0.36s with 267 indirections and no bindings. Free unused. P4 the compiler still self-hosts TRUE (see note) DEMONSTRATED: an El program with NO decorator in its source, already compiled and linked, picked up a construct declared afterwards: $ /tmp/seamrun -> 7 $ echo 'work audited entry audit_entry' > constructs.txt $ EL_CONSTRUCTS=constructs.txt /tmp/seamrun AUDIT: work applied by audited 7 P4 note: my first fixpoint test was wrong, not the code. I compared gen1 to gen2, which must differ whenever codegen's output changes. gen2 == gen3, 267 seam sites, stable. MEASURED COST, and the root cause was not where I looked 0 bindings 0.36s vs 0.37s baseline free 2 bindings, dlsym per call 2.45s 6.6x 2 bindings, resolved once 0.69s 3.5x recovered The table scan was never the cost. dlsym walks the dynamic symbol table on every call. Resolve once and cache — which is the smallest form of what salience does for memory: what is hot stays resolved. The 0.69s residual is audit_entry's own printf on two of the compiler's hottest functions, not seam overhead. CONSEQUENCE: the five compile-time declaration kinds on iteration-1 are a compile-time specialisation of something that resolves at runtime. They are not wrong, but they are not the mechanism — the mechanism is one indirection, and a kind is data. --- lang/el-compiler/src/codegen.el | 4 ++ lang/runtime/el_runtime.c | 75 +++++++++++++++++++++++++++++++++ lang/runtime/el_runtime.h | 3 +- 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 68bf3db..2eac94a 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -3205,6 +3205,10 @@ fn fn_has_decorator(stmt: Map, name: String) -> Bool { // applies both); injection is topmost-wins, matching the VBD role convention, // because a role is singular and a refusal is not. fn cg_entry_seam(stmt: Map, fn_name: String) -> Void { + // RUNTIME SEAM: codegen cannot know which constructs will be bound to this + // fn after the binary exists, so the indirection is unconditional. What + // applies is resolved at execution against a table written later. + emit_line(" el_seam_run(EL_STR(" + c_str_lit(fn_name) + "), 0, 0);") let gdl = stmt["decorators"] let n_gdl: Int = native_list_len(gdl) let gi = 0 diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index 021f3f9..cad6cba 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -17016,6 +17016,81 @@ void dharma_emit(el_val_t event_type, el_val_t payload) { * dharma_emit generally — its payloads are hand-concatenated at 39 call sites * with no escaping, and a value containing a quote silently corrupts the * event. Fixing that is a separate change; this one does not add to it. */ +/* ── Runtime construct seam ─────────────────────────────────────────────── + * The crossing is resolved at EXECUTION, not at emission. Codegen emits one + * indirection per function; which constructs apply is read from a table that + * can be written AFTER the binary exists. + * + * This is the whole hypothesis under test: a compiler whose one compiled + * mechanism is language extension can compose without recompilation. If a + * construct declared after the build applies to a running program, the five + * compile-time declaration kinds were the wrong shape. + * + * Table format, one binding per line: + * entry|exit + * + * Targets are resolved with dlsym against the running image, so composition is + * bounded by the LINKED SYMBOL SET -- a construct naming a symbol nobody + * linked is skipped, not fatal. That bound is the honest limit on "endless". */ +#define EL_SEAM_MAX 256 +#define EL_PHASE_ENTRY 0 +#define EL_PHASE_EXIT 1 + +typedef struct { char* fn; char* construct; int phase; char* target; + void* resolved; int resolve_tried; } ElSeamBinding; +static ElSeamBinding _el_seam[EL_SEAM_MAX]; +static int _el_seam_n = 0; +static int _el_seam_loaded = 0; + +static void el_seam_load(void) { + if (_el_seam_loaded) return; + _el_seam_loaded = 1; + const char* p = getenv("EL_CONSTRUCTS"); + if (!p || !*p) return; + FILE* f = fopen(p, "r"); + if (!f) return; + char line[512]; + while (fgets(line, sizeof line, f) && _el_seam_n < EL_SEAM_MAX) { + char fn[128], con[128], ph[32], tgt[128]; + if (sscanf(line, "%127s %127s %31s %127s", fn, con, ph, tgt) == 4) { + if (fn[0] == '#') continue; + _el_seam[_el_seam_n].fn = el_strdup(fn); + _el_seam[_el_seam_n].construct = el_strdup(con); + _el_seam[_el_seam_n].phase = (strcmp(ph, "exit") == 0) ? EL_PHASE_EXIT : EL_PHASE_ENTRY; + _el_seam[_el_seam_n].target = el_strdup(tgt); + _el_seam_n++; + } + } + fclose(f); +} + +el_val_t el_seam_run(el_val_t fn_v, el_val_t phase_v, el_val_t result) { + if (!_el_seam_loaded) el_seam_load(); + if (_el_seam_n == 0) return result; /* the common path: no bindings */ + const char* fn = EL_CSTR(fn_v); + if (!fn) return result; + int phase = (int)phase_v; + el_val_t last = result; + for (int i = 0; i < _el_seam_n; i++) { + if (_el_seam[i].phase != phase) continue; + if (strcmp(_el_seam[i].fn, fn) != 0) continue; + /* Resolve ONCE. dlsym walks the dynamic symbol table on every call, and + * measured at 6.6x on a hot path with two bindings -- the table scan was + * never the cost. What is hot must stay resolved; this is the smallest + * form of the same thing salience does for memory. */ + if (!_el_seam[i].resolve_tried) { + _el_seam[i].resolved = dlsym(RTLD_DEFAULT, _el_seam[i].target); + _el_seam[i].resolve_tried = 1; + } + void* sym = _el_seam[i].resolved; + if (!sym) continue; /* unlinked target: skipped, not fatal */ + el_val_t (*fp)(el_val_t, el_val_t, el_val_t) = + (el_val_t (*)(el_val_t, el_val_t, el_val_t))sym; + last = fp(fn_v, el_wrap_str(el_strdup(_el_seam[i].construct)), last); + } + return last; +} + el_val_t engram_boundary_beat(el_val_t op_name, el_val_t construct) { _eg_aff_boundary_ops++; engram_chrono_tick(); diff --git a/lang/runtime/el_runtime.h b/lang/runtime/el_runtime.h index b3e0c9d..854a044 100644 --- a/lang/runtime/el_runtime.h +++ b/lang/runtime/el_runtime.h @@ -887,7 +887,8 @@ el_val_t engram_age_field(el_val_t delta_ms); el_val_t engram_age_field_catchup(void); el_val_t engram_chrono_persist_tick(void); el_val_t engram_chrono_tick(void); -el_val_t engram_boundary_beat(el_val_t op_name, el_val_t construct); /* API-reshape decorator-seam auto-emit; construct = the decorator that caused the beat */ +el_val_t engram_boundary_beat(el_val_t op_name, el_val_t construct); +el_val_t el_seam_run(el_val_t fn_name, el_val_t phase, el_val_t result); /* runtime construct seam */ /* API-reshape decorator-seam auto-emit; construct = the decorator that caused the beat */ el_val_t engram_self_anchor_capture(void); el_val_t engram_self_drift_json(void); el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);