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);