EXPERIMENT: resolve the crossing at execution, not at emission
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.
This commit is contained in:
@@ -3205,6 +3205,10 @@ fn fn_has_decorator(stmt: Map<String, Any>, name: String) -> Bool {
|
|||||||
// applies both); injection is topmost-wins, matching the VBD role convention,
|
// applies both); injection is topmost-wins, matching the VBD role convention,
|
||||||
// because a role is singular and a refusal is not.
|
// because a role is singular and a refusal is not.
|
||||||
fn cg_entry_seam(stmt: Map<String, Any>, fn_name: String) -> Void {
|
fn cg_entry_seam(stmt: Map<String, Any>, 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 gdl = stmt["decorators"]
|
||||||
let n_gdl: Int = native_list_len(gdl)
|
let n_gdl: Int = native_list_len(gdl)
|
||||||
let gi = 0
|
let gi = 0
|
||||||
|
|||||||
@@ -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
|
* dharma_emit generally — its payloads are hand-concatenated at 39 call sites
|
||||||
* with no escaping, and a value containing a quote silently corrupts the
|
* 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. */
|
* 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:
|
||||||
|
* <fn> <construct> entry|exit <target-symbol>
|
||||||
|
*
|
||||||
|
* 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) {
|
el_val_t engram_boundary_beat(el_val_t op_name, el_val_t construct) {
|
||||||
_eg_aff_boundary_ops++;
|
_eg_aff_boundary_ops++;
|
||||||
engram_chrono_tick();
|
engram_chrono_tick();
|
||||||
|
|||||||
@@ -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_age_field_catchup(void);
|
||||||
el_val_t engram_chrono_persist_tick(void);
|
el_val_t engram_chrono_persist_tick(void);
|
||||||
el_val_t engram_chrono_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_anchor_capture(void);
|
||||||
el_val_t engram_self_drift_json(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);
|
el_val_t engram_neighbors_json(el_val_t node_id, el_val_t max_depth, el_val_t direction);
|
||||||
|
|||||||
Reference in New Issue
Block a user