iteration-1: the compiler stops adjudicating #164
Reference in New Issue
Block a user
Delete Branch "iteration-1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Nineteen
Ishikawa → scientific method → Six Sigmacycles, each run in anisolated worktree with predictions committed before execution. Full log in
docs/v1/experiments/.What came out of the compiler
The compiler no longer adjudicates anything. It records what it saw and emits C;
nine files under
lang/tools/check/decide, at build time, and every one iseditable without rebuilding.
A construct declared after a binary exists applies to that already-built
program — observed, refused, composed. Free when unbound: 0.36s vs 0.37s
baseline across 267 indirections.
Live defects found and fixed
Each compiled clean, ran, and produced a wrong result or a crash with no
diagnostic at any layer. All pre-existing.
let a = str_len(s); a + bel_str_concaton two integers. Printed nothing, should print 7.let seed = 42seed/impl/sealed/protocol/activatewere keywords consumed by nothing. Printed 0 instead of 44.let t: Instant = now(); t + 3Duration + Intwas refused;Instant + Intnever was.let x: Int = "hello"; x + 14343631981— a string pointer used as an integer.let s: String = 42; println(s)sha256_hex(50000)el_input_lenfell through tostrlen()on an integer.el_tagged()is now exported inel_runtime.h: validating a slot beforedereferencing it was a convention every author had to know —
geom_ofandmfld_ofwere correct,el_bin_lookupandel_input_lenwere not.Also
duplicate-definition check names both files with file-local line numbers.
error handling, parsing, numeric literals, modules. See
docs/v1/experiments/findings/answers.md.Verification
Every cycle had to show all three before landing, and this branch shows them now:
Honest gaps
codegen.elis 4493 vs a 4661 baseline — only 168 below, after peaking at 5152.let x: Int = some_string_fn()still passes: a data gap in
signatures.rel, not a capability limit.@asyncis half expressible — fire-and-forget works with no compilerchange;
awaitneeds a future type, demonstrated on a branch and not landed.The beat reported which function crossed a boundary, never which decorator put the beat there. So the graph accumulated boundary events with no attribution, and no construct could be measured — "is this decorator earning its keep" stayed an argument instead of a traversal. engram_boundary_beat now takes the construct and carries it on the bus as {"construct":"..."}. The injection point, the beat, and the accumulation already existed; only the attribution was missing. Also pins a known defect as a test: codegen calls fn_has_decorator for exactly three names (manager, accessor, route). Twelve others parse, attach, and compile to nothing — including @authenticate (6 uses), @authorize (3), @rate_limit (3) and @validate (2), which look like protection and are not. decorator-authenticate-compiles-to-nothing asserts that @authenticate emits byte-identical C to no decorator at all, so fixing it will be a visible flip. Verified: compiler self-hosts byte-identically, 86/86 native compiler tests pass, emitted C carries the construct for both @manager and @accessor.codegen called fn_has_decorator for exactly three names — manager, accessor, route. Twelve others parsed, attached as {name,args}, and compiled to nothing, including four that look like protection: @authenticate (6 uses), @authorize (3), @rate_limit (3), @validate (2). The cause was not that the branches were untidy. A construct had nothing to BE, so its meaning had nowhere to live except the emitter, and every construct was therefore a compiler edit. A name -> injection table would have moved the enumeration twenty lines up without removing it. So the construct now carries its own meaning: @decorator("injects_at_entry", "engram_boundary_beat") fn audited() {} @audited fn risky_op() -> Int { ... } // gets the beat, attributed to "audited" scan_declared_decorators is a token-level pre-pass beside scan_routes, forced by streaming codegen having no whole-program AST. manager and accessor are seeded as the compiled-in core — the fixedSelf shape from substrate.go: a complete fallback exists, declaration is enrichment. This is the injection half of the seam only. The prohibition half (@manager's #error on dharma_emit) stays hardcoded, because "which calls may appear inside this boundary" is a query over program structure and there is nothing yet to ask. Verified three ways: emitted C for existing @manager/@accessor code is byte-identical to the hardcoded path; a construct with a name the compiler has never heard of injects correctly; the compiler self-hosts byte-identically. 90/90 native compiler tests pass.@authenticate (6 uses), @authorize (3), @rate_limit (3) and @validate (2) parsed, attached, and compiled to nothing. Fourteen applications that read as protection and emitted no instruction — a function decorated @authenticate compiled byte-identically to an undecorated one. The missing capability was not authentication. It was that a construct could observe a boundary but never refuse one. injects_at_entry discards the target's result; there was no form in which a construct could say no. @decorator("guards_at_entry", "my_auth") fn authenticate() {} @authenticate @authorize fn handler() -> String { ... } emits, at entry: { el_val_t __g = my_auth(EL_STR("handler"), EL_STR("authenticate")); if (__g) return __g; } { el_val_t __g = my_roles(EL_STR("handler"), EL_STR("authorize")); if (__g) return __g; } Guards precede injections because a refused call must not report a crossing, and every guard runs where the topmost injecting construct wins — refusal is not a role, so it does not follow the role convention. The compiler still knows nothing about auth. The program points the construct at its own function, which is where that decision belongs. Verified: existing @manager/@accessor output byte-identical, compiler self-hosts byte-identically, guards stack in declaration order and emit before the beat. 94/94 native compiler tests pass.§6 records 62 persist-after-mutate sites, 10 auth-per-route, and index-after-append that failed at 9 of 9 — every one an obligation at a crossing that decayed into "remember to do this afterwards." An obligation a human must remember is not an obligation, and the 9-of-9 figure is what that costs. @decorator("injects_at_exit", "persist_now") fn durable() {} The body moves into a static helper and the visible fn becomes a wrapper, so EARLY RETURNS pass through the exit injection. Emitting it only before the fall-through return would have silently missed every early return — the exact failure class this seam exists to remove. Fns with no exit construct emit byte-identically to before. Three independent constructs now compose on one fn, none known to the compiler: el_val_t mutate(el_val_t k) { { el_val_t __g = my_auth(EL_STR("mutate"), EL_STR("authenticate")); if (__g) return __g; } engram_boundary_beat(EL_STR("mutate"), EL_STR("manager")); el_val_t __r = __el_body_mutate(k); persist_now(EL_STR("mutate"), EL_STR("durable"), __r); return __r; } Guard, then entry, then body, then exit. §5.2 asked whether `hold` is one construct or two; the implementation answers one construct with two faces, selected by declared kind rather than by two mechanisms. Verified: existing output byte-identical, compiler self-hosts byte-identically, early returns pass through the exit, ordering holds under composition. 98/98 native compiler tests pass.ROOT CAUSE of the weaker design: "C has no closures" was taken as a fact about what is possible. It is a fact about one grammar. Every C++ lambda, every Go closure, every Rust closure compiles to a struct of captured values plus a function pointer -- which is what is emitted here. Codegen emits C; it is not written in C's syntax, and the distinction is the whole difference between a construct that can only decide whether to repeat and one that controls invocation. It would also have crippled the JS backend, which has closures natively, for a limit that applies only to the C one. PREDICTIONS AND RESULTS 1 env struct + thunk taking void* TRUE 2 fails to compile: struct redefinition FALSE -- C allows the inner declaration to shadow. Prediction wrong; C is more permissive than assumed. A different real defect surfaced instead: a wrap with no exit construct emitted `(EL_STR("f"), EL_STR(""), __r);` -- a call to an empty target -- because has_exit was reused as "needs a wrapper" and the exit line was emitted unconditionally. Fixed. 3 compiles when the target is declared in El FALSE -- and this is the root cause worth keeping: El has ONE type, el_val_t = int64_t. El's type system cannot describe a callable, so `extern fn` and the real signature cannot be made to agree in El's own vocabulary. The fix is not a cast: codegen DEFINES the wrap calling convention, so codegen emits the extern declaration. The convention is not El-expressible; it is emitted. 4 target controls invocation, 0..N times TRUE 5 existing @manager output byte-identical TRUE 6 compiler fixpoint holds TRUE 7 emitting the convention makes it compile TRUE MEASURED base(5) wrapped by a target that invokes the body twice and sums -> 10 never_runs(5) wrapped by a target that never invokes it -> 999 Neither is expressible by "decide whether to repeat". This supersedes the repeats_body experiment on experiment/repeats-body, which was built around the mistaken limit.The other half of a boundary: not what runs when something crosses, but what may not cross at all. It was two string literals in vbd_is_restricted_name and one #error in cg_fn — one prohibition, uneditable without a compiler release. @decorator("prohibits_outside", "raw_sql") fn repository() {} fn sneaky() -> Int { raw_sql("DROP") } // #error "boundary violation: raw_sql may only be called from an // @repository fn, but 'sneaky' is not one" The recursive matcher is parameterised through a state key rather than by threading an argument through every branch of the walk — the mechanism codegen already uses for __match_counter and __if_expr_counter. Each prohibition is checked in its own turn, so the owning construct is known by construction and the diagnostic names it instead of hardcoding one rule's wording. PREDICTIONS AND RESULTS 1 the 3 duplicated uniqueness rules are textually identical TRUE 2 a declared prohibition reproduces @manager's #error TRUE 3 existing output byte-identical TRUE 4 a program can declare its own prohibition TRUE 5 fixpoint holds TRUE I misread result 2 on first pass: a @manager fn calling dharma_emit still emitted one #error, which looked like a failure. It is the CAPABILITY-tier rule at codegen.el:2578, a separate prohibition system, and it fires identically on the pre-change compiler. MEASURED DEFECTS STILL OPEN - two independent prohibition systems (VBD constructs, capability tiers); only the first is declarable - 3 uniqueness rules written 6 times, once per codegen path, kept in sync by hand and identical today 102/102 native compiler tests pass, compiler self-hosts byte-identically.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.Prediction 3 was FALSE. I expected refusal to be impossible through the seam because the entry indirection discarded its return. One line: { el_val_t __s = el_seam_run(EL_STR(f), 0, 0); if (__s) return __s; } work() returns 7; bound to a refusing construct AFTER the build it returns 42. So three of the five compile-time kinds are runtime-bindable: entry injection, exit injection, and refusal. wraps_body needs invocation control and prohibits_outside is compile-time by nature. 104/104 native compiler tests pass.ISHIKAWA: why did exit injection still need compile-time knowledge? Because the body-helper wrapper was only emitted when codegen already knew an exit construct existed. The wrapper being conditional was the cause, not the wrapper being necessary. PREDICTIONS AND RESULTS P1 exit becomes runtime-bindable TRUE returns 14, bound after the build P2 codegen shrinks TRUE 5094 -> 5044 P3 cost 5-15% from a call frame on every fn FALSE 0.37s -> 0.38s, ~3% P4 fixpoint holds TRUE Every fn now gets a body helper and a wrapper. It has to be unconditional: early returns must route through something for an exit construct to observe them, and codegen cannot know which fns will be bound after the binary exists. Removed with the machinery: declare_exit, decorator_exit, cg_exit_target, cg_exit_construct, and the injects_at_exit scanner branch. Two controls failed and were rewritten rather than repaired -- no-exit-construct-emits-no-wrapper asserted the optimisation this removes, so it is now inverted. The integration harness gained a seventh assertion: an exit construct declared after the build replaces the result. 99/99 native, 7/7 integration, fixpoint gen2==gen3.ISHIKAWA: why did wraps_body need compile-time knowledge? Because the wrapper called the target directly. If the wrapper calls through the seam instead, the seam can call the body itself, and a construct bound after the build decides how and whether to invoke it. PREDICTIONS AND RESULTS P1 wrap becomes runtime-bindable TRUE body x3 -> 21, never invoked -> 111 P2 codegen shrinks TRUE 5042 -> 4977 P3 cost 5-10% from an indirect call on every fn TRUE 0.36s -> 0.39s, ~8% P4 zero-param fns break on the empty struct TRUE empty struct is a GNU extension, empty init is C23. Fixed with a char field. P5 fixpoint holds TRUE PROCESS FAILURE worth recording: my first patch silently did not apply because I dropped the assert on the string replacement. The build then failed with "undeclared identifier __thunk_noargs", which I nearly attributed to the empty-struct prediction. The guard that would have caught it existed and I removed it -- the same shape as every other defect found tonight. Removed: declare_wrap, decorator_wrap, cg_wrap_target, cg_wrap_construct, params_to_call_args, and the wraps_body scanner branch. prohibits_outside is now the ONLY construct kind left at compile time, and it cannot move: a #error has no runtime.I said prohibition could not move because "a #error has no runtime". That conflated two separable things: WHEN a violation is detected (build time -- correct, and unchanged) and WHERE the rule and the checker live (the compiler -- assumed). A prohibition is a containment relation over the call graph. So codegen now records what it saw: sneaky calls raw_sql allowed calls raw_sql allowed calls @repository repository calls prohibits:raw_sql and tools/check/prohibitions.sh decides, at build time, outside the compiler. PREDICTIONS AND RESULTS P1 codegen can emit the call graph it already walks TRUE P2 the check becomes a query outside the compiler TRUE P3 all prohibition decisions leave codegen TRUE zero #errors now P4 violations still caught at build time TRUE exit=1 P5 codegen drops below the 4661 baseline FALSE 4962, +301 P5 is the finding. The TRAVERSAL is irreducible -- you must walk the AST to find calls, and those ~120 lines do not move no matter who decides. What is not irreducible is the rule (which names) or the decision (#error). Those left. I predicted the whole 223 lines would go because I had not separated walking from adjudicating. Still compiled, and measured rather than assumed: the capability-tier system (cap_check_call, is_self_formation_call, is_dharma_call, is_llm_call, cap_record_violation, emit_cap_violations) is 76 lines of the same shape -- prohibits_WITHIN rather than prohibits_outside, so the checker needs the opposite polarity to absorb it. 98/98 native, 4/4 prohibition_query.sh, 7/7 seam_binding.sh, fixpoint ok.codegen.el carried builtin_arity(): 344 lines, 300 entries, a hand-maintained second copy of el_runtime.h. PREDICTIONS AND RESULTS P1 the table duplicates the header TRUE 243 shared names P2 they have already drifted FALSE ZERO drift. The duplicate had been maintained correctly. P3 codegen can emit call-arity relations TRUE P4 the check becomes a query against the header TRUE P5 codegen drops to roughly baseline TRUE 4903 -> 4512, 149 BELOW the 4661 it started at P2 being false is the better result: the table was not WRONG, it was INCOMPLETE. 110 functions the runtime declares had no entry, so calling them with the wrong argument count produced no El-level diagnostic at all. Measured: the old compiler reports 0 arity errors for __http_do_map_to_file(1); the query reports "takes 5 arguments, called with 1". Deriving from the header fixes coverage AND makes drift impossible by construction. 503 signatures, versus 300 entries maintained by hand. THREE DEFECTS IN MY OWN CHECKER, each found by running it rather than reading it 1. El names and C names differ -- `println` is `__println`. 60 of 500 decls carry the prefix and codegen owns the mapping; the old table carried both keys. One rule covers all 60. 2. Multi-line declarations parsed as zero params, so the checker reported "takes 0" for a function taking 5. A diagnostic with the wrong number in it is worse than none -- the same shape as the stale caller attribution in the previous pass. 3. Fixing (2) by joining lines dropped 500 signatures to 334, because a declaration preceded by a comment no longer started its record. Comments are stripped first now. 98/98 native, 5/5 arity_query.sh, fixpoint ok.This block is structurally unlike the previous four. It does not only adjudicate, it DISPATCHES: Instant + Duration must become el_instant_add_dur, LocalDate + Duration must become el_local_date_add_dur. The emitted C depends on the type answer, so it cannot move to a post-hoc query. Selecting which call to emit is an emitter's actual job. PREDICTIONS AND RESULTS P1 the block conflates dispatch with adjudication TRUE P2 adjudication can move, dispatch cannot TRUE P3 this pass shrinks codegen far less than the last TRUE, and worse: 4513 -> 4537, it GREW by 24 lines P4 the rules are affine algebra, closed by construction TRUE P5 no type propagation -- name tracking plus a hardcoded list of which builtins return which type TRUE, 19 names P3 is the honest result and it is not spun: moving 19 names into a data file cost more lines than it saved, because a generic loader is larger than the enumeration it replaces. The win is not line count. It is that adding a 20th temporal builtin is now a one-line edit to signatures.rel instead of a compiler change, and that the data is inspectable. WHY THE HEADER CANNOT SUPPLY THIS, unlike arity: el_runtime.h declares every builtin as returning el_val_t, because El has ONE type. That single type is why the whole seam is cheap and it is exactly why the C boundary cannot say that now() returns an Instant while unix_seconds() returns an Int. The El-level type is real and the boundary erases it. INCOMPLETE, and stated rather than hidden: P2 said adjudication could move to a query. It has NOT. Violations still emit TIME_TYPE_ERROR inline from the emitter. Only the type DATA moved. Moving the adjudication needs the operand types recorded as relations, which is a further pass. 98/98 native, 4/4 temporal_signatures.sh, fixpoint ok.The previous pass moved the type DATA and left the judgment inline, which I stated rather than hid. This finishes it. PREDICTIONS AND RESULTS P1 codegen can emit operand-type relations TRUE "main calls temporal:instant_plus_instant" P2 the affine rules are a small closed set as data TRUE 6 rules P3 violations still caught at build time TRUE exit=1 P4 the reporter leaves codegen TRUE 4538 -> 4507 P5 the TIME_TYPE_ERROR placeholder must STAY TRUE P5 is the boundary of this whole approach. The emitter has to emit SOMETHING for an illegal expression -- it cannot emit nothing and it cannot decide what the program meant. So the placeholder is irreducible in the same way the AST traversal was: what moved is the judgment and the wording, not the fact that something must be written. The rules are affine algebra and the set is closed because there are only two kinds of thing. An Instant is a POINT, a Duration is a DISPLACEMENT: add a displacement to a point, subtract two points for a displacement, combine displacements. Nothing else is meaningful, which is why the enumeration in temporal.rel cannot grow the way an allowlist does. A defect in my own checker, found by running it: the .rel file uses aligned columns and my awk assumed a single space, so the message came out with the rule key still prefixed. Same class as the multi-line header parse in the arity pass -- formatting assumptions that only fail when you look at the output. 98/98 native, 6/6 temporal_query.sh, fixpoint ok.PREDICTIONS AND RESULTS P1 is_int_call's 35 hardcoded names move to data TRUE P2 is_int_name stays -- it is annotation propagation TRUE P3 the dispatch stays -- it is emission TRUE P4 codegen shrinks ~40 lines TRUE 4507 -> 4469 P5 the design doc's characterisation is WRONG TRUE P6 the moved data also fixes the bug it exposed TRUE P5 CORRECTS THE RECORD. el-language-design.md and geometry-vs-code.md both cite "== lowering to str_eq unless both operand names are in a hardcoded int-name set -- a literal list of variable names treated as integers" as the paradigm defect. It is not one. __int_names is populated from TYPE ANNOTATIONS (param["type"] == "Int"), which is primitive but legitimate type propagation. The actual defect was is_int_call: 35 hardcoded builtin return types, the same shape as the temporal 19. P6 IS A LIVE CORRECTNESS BUG, PRE-EXISTING, NOW FIXED let a = str_len("hello") // no annotation let b = str_len("hi") let c = a + b // -> el_str_concat(a, b) on two integers Verified identical on the pre-change compiler, so not a regression. It compiled clean, ran, and printed NOTHING where it should print 7. No error at any layer. The repair is three lines: an unannotated let takes its type from what the initialiser returns. The return types were already required for dispatch and were simply never consulted at the binding site. Moving them into data is what made the gap visible -- reading the code for eight hours did not. 98/98 native + 2 new, 31/31 integration, fixpoint ok.Both, at different layers -- and it is the same split as serialization: the convention is the BASIS, never the ACT. lexeme -> token `fn` means function-start because someone said so CONVENTION shape recognition given tokens, which construct is this REGION source -> structure parsing is transduction onto that basis GEOMETRY byte traversal something must read them in order IRREDUCIBLE Three things push the ACT toward region rather than convention: ambiguity (a * b needs context; a grammar resolves it with the lexer hack, a region by neighbourhood), error recovery (nearest-region is free), and precedence, which is ordering along an axis with a conventional parameter. AND THE SHOULD GATE SAYS NO TO THE OBVIOUS MOVE Every other table this session moved to data. This one stays code. The keyword set is CLOSED by the language definition -- it does not leak the way an allowlist does -- and the lexer runs before the program is understood, so a program can never declare its own keywords. Externalising it costs file I/O on every compile and buys nothing. Same verdict as is_digit in ASCII. WHAT WAS ACTUALLY WRONG: five of 46 keywords were consumed by no parser or codegen path. sealed, activate, seed, protocol, impl. Each stole an identifier from users for nothing. SECOND SILENT MISCOMPILATION OF THE DAY. Using one did not fail to parse: let seed = 42 let impl = seed + 1 compiled CLEAN -- zero cc errors -- and printed 0 instead of 44. No diagnostic at any layer. Fixed by removing the five. A DEFECT IN MY OWN MEASUREMENT, caught before it did damage: my first pass checked only parser.el and reported `test` as inert too. codegen consumes it at 4135 for --test mode, and the tree has 408 uses. Removing it would have broken every test in the suite. The measurement was re-run across all four consumers. 100/100 native + 2 new, 31/31 integration, fixpoint ok.3a position, or a convention we agreed on? c48db6c2a8Both, at different layers, and the split is the same as everywhere else. The NUMERAL is convention -- int_to_str was already form 1, because no position determines that twelve is written 1 then 2 in base ten. The NUMBER is a position: three things are three things regardless of notation. But the sharper answer follows from `love = 0`. A bare `3` is a MAGNITUDE WITH NO AXIS. It is not a position until something gives it a direction, which is exactly why 3.days needs a calendar and why time_add(t, n, "min") had to carry its axis as a string. PREDICTIONS AND RESULTS P1 numeral = convention, number = position TRUE P2 a bare literal is dimensionless until context types it TRUE P3 there is a measurable place where El guesses TRUE P4 Instant + Int is not caught though Duration + Int is TRUE P5 the rule catches it TRUE P6 nothing legitimate in the tree relies on it TRUE P3/P4 IS THE DEFECT, and it was found by reasoning from the philosophy and then measured. Duration + Int was refused -- "an Int carries no unit" -- while let t: Instant = now() let u: Instant = t + 3 compiled to raw (t + 3) and reported CLEAN. Adding a dimensionless number to a point is worse than adding it to a displacement: it silently moves the instant by an unspecified amount. 3 of what? Whatever the representation happens to be, which is the leak itself. The asymmetry had no justification; the rule was simply never written. P6 MATTERED. Two calendar tests looked like Instant + Int: let later: Instant = i + 1.hour let later: Instant = base + 15.hours They are not. `1.hour` lexes to a Duration -- el_duration_from_nanos(1LL * 3600000000000LL) -- and both stay clean. That is the whole answer demonstrated in one line: t + 3 is refused because 3 has no axis; t + 1.hour is accepted because .hour supplies one. 104/104 native + 2 new, integration green, fixpoint ok.The question is premature, and measuring says why. El's partition is a FILESYSTEM PATH, not a neighbourhood, and there is no namespacing at all. MEASURED import is textual inlining (resolve_imports), guarded against double inclusion by a __elc_imp__:<path> state key when a .elh header exists the header is inlined instead and the .el is marked seen, so symbols resolve at C link time -- so linking IS real, delegated to C two modules defining `helper` emit two C functions into one translation unit So linking barely survives the PATH partition. Whether it survives a neighbourhood partition cannot be asked yet. A DIAGNOSTIC REGRESSION I CAUSED, found by asking this question. cc does catch the collision, but reports: error: redefinition of '__el_body_helper' error: redefinition of '__env_helper' error: redefinition of '__thunk_helper' error: redefinition of 'helper' The user's own function is FOURTH. The first three are generated symbols introduced by the unconditional-wrapper pass earlier today -- before it, there was one clear message. Repaired by catching the collision at El level instead: duplicate definition: 'helper' is defined 2 times — El has no namespacing, so imported modules share one global scope LIMIT, stated rather than hidden: textual inlining destroys file provenance. By the time codegen runs there is one source string, so the message can say WHICH name collides but not which files. Naming a.el and b.el needs provenance threaded through resolve_imports. 104/104 native, 4/4 definitions_query.sh, the compiler itself reports clean, fixpoint ok.The module question ended with a limit: textual inlining destroys file provenance, so a duplicate-definition message could name the symbol but not the files. Threading it exposed a bigger absence first. TOKENS HAD NO POSITION AT ALL. A token was a flat (kind, value) pair, so NO diagnostic in El could name a place -- every error named a symbol and never a line. That is the prerequisite the module question was resting on. THE CHAIN, end to end lexer counts newlines; tok_append mints (kind, value, line) parser stride 2 -> 3; tok_line added; FnDef carries its line codegen records <fn> defines_at:<line> resolve_imports publishes <file> spans <start> <end> for the combined source checker maps a combined line back to file:line-within-that-file duplicate definition: 'helper' is defined 2 times — El has no namespacing, so imported modules share one global scope /tmp/modtest/a.el:1 /tmp/modtest/b.el:1 PREDICTIONS AND RESULTS P1 15 stride sites, encapsulated in tok_kind/tok_value TRUE, but see below P2 adding a line field is mechanical TRUE P3 the lexer must count newlines TRUE P4 resolve_imports can record per-file line ranges TRUE P5 the message can then name both files TRUE P6 token memory grows TRUE, 25.0 -> 33.9 MB (+36%) FOUR DEFECTS, EACH FOUND BY RUNNING AND NOT BY READING 1. interp_tokens_append_all walks the token list DIRECTLY with its own copy of the stride. Gen1 built fine and gen2 emitted corrupt C, because the compiler's own source uses string interpolation. My search missed it because I grepped for the variable name `tokens`; it is called `dst`/`result`. Searching by name instead of by shape -- third time today. 2. tok_count in test_compiler.el carried the stride too. I had scoped the search to compiler sources and it had escaped into the tests. 3. Nested resolve_imports calls accumulated spans into shared state, so each republished meaningless line ranges under the parent's name. Making the buffer local fixed it; guarding the WRITE did not, which is what I tried first. 4. The first working version reported b.el:3 -- the COMBINED line against a filename that has no line 3. A file:line that does not match the file is worse than no line at all. 105/105 native, 37/37 integration, fixpoint ok, compiler self-checks clean.cycles/ one file per Ishikawa -> scientific method -> Six Sigma loop, named for the DEFECT not the fix, carrying the commit record as written at the time findings/ what the cycles produced, cross-cut: live bugs, architecture answers, and defects in my own measurement The organising finding is that predictions which came back FALSE produced every significant result. Eleven of sixty-one failed, and those eleven found: that the arity table was not drifted but 40% incomplete; that the AST traversal is irreducible and only rules and judgments move; that guards could refuse through the seam after all; and that routing el_bin_lookup through the gate did NOT fix the SIGSEGV, because the fallback strlen was the hazard -- a wrong fix I would otherwise have shipped as verified. One cycle was run without committing predictions first and had to be discarded as rigged. It is kept, in full, as 18-async-half-expressible.md.ISHIKAWA: three silent miscompilations found the same day shared one shape. method type tracked by per-function name sets, fed from annotations machine el_val_t erases everything at the C boundary material no propagation through expressions measurement nothing verifies an annotation against what it annotates root cause El has type ANNOTATIONS and no type CHECKING. The annotation feeds dispatch and is never itself verified. MEASURED, and it is not merely a wrong answer let x: Int = "hello" ; x + 1 -> printed 4343631981, a string POINTER interpreted as an integer let s: String = 42 ; println -> dereferenced address 42 The first leaks a raw memory address into program output. The second is an arbitrary-read primitive if the integer is ever attacker-influenced. PREDICTIONS AND RESULTS P1 let x: Int = "hello" compiles clean TRUE P2 let s: String = 42 compiles clean TRUE P3 the annotation drives dispatch, unverified TRUE P4 same root cause as all three bugs found today TRUE P5 checking literal-vs-annotation catches both TRUE P6 zero false positives across the compiler's source TRUE The emitter only RECORDS the mismatch; tools/check/annotations.sh decides, consistent with every other check landed today. INCOMPLETE, stated rather than hidden: only literals are checked. let x: Int = some_string_fn() still passes, because signatures.rel carries Int/Instant/Duration and no String entries. That is a DATA gap, not a capability limit -- every El function declares its return type in source and codegen already holds ret_type on every FnDef. 105/105 native, 5/5 annotation_query.sh, fixpoint ok.