log v1 experiments: nineteen cycles, organised by the method that produced them
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.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Cycles
|
||||
|
||||
Each is one `Ishikawa → scientific method → Six Sigma` loop, run in an isolated
|
||||
worktree so a wrong answer cost nothing. Named for the **defect**, not the fix.
|
||||
|
||||
| # | Cycle | Root cause | Predictions | Landed |
|
||||
|---|---|---|---|---|
|
||||
| 01 | [constructs-have-nowhere-to-be](01-constructs-have-nowhere-to-be.md) | a construct had nothing to BE, so its meaning lived in the emitter | 3/3 | yes |
|
||||
| 02 | [a-construct-cannot-refuse](02-a-construct-cannot-refuse.md) | injection discards the target's result; no form said no | 4/4 | yes |
|
||||
| 03 | [the-wrapper-was-conditional](03-the-wrapper-was-conditional.md) | exit injection needed compile-time knowledge only because the wrapper was conditional | 3/4 | yes |
|
||||
| 04 | [c-has-no-closure-syntax](04-c-has-no-closure-syntax.md) | "C has no closures" taken as a fact about what is possible | 5/7 | yes |
|
||||
| 05 | [the-emitter-discards-what-it-knows](05-the-emitter-discards-what-it-knows.md) | codegen sees every construct relation and throws it away | 5/5 | branch |
|
||||
| 06 | [the-crossing-resolves-at-emission](06-the-crossing-resolves-at-emission.md) | the binary has no table to consult | 3/4 | yes |
|
||||
| 07 | [invocation-is-not-composable](07-invocation-is-not-composable.md) | the wrapper called the target directly | 5/5 | yes |
|
||||
| 08 | [the-emitter-adjudicates](08-the-emitter-adjudicates.md) | a prohibition had nowhere to live but a `#error` | 4/5 | yes |
|
||||
| 09 | [policy-inside-the-compiler](09-policy-inside-the-compiler.md) | a program cannot declare its own restrictions, so the tier policy was compiled in | 4/5 | yes |
|
||||
| 10 | [a-second-copy-of-the-header](10-a-second-copy-of-the-header.md) | builtin arity hand-maintained beside `el_runtime.h` | 4/5 | yes |
|
||||
| 11 | [one-type-erases-the-return](11-one-type-erases-the-return.md) | `el_val_t` means the header cannot say `now()` returns an Instant | 4/5 | yes |
|
||||
| 12 | [judgment-lives-with-knowledge](12-judgment-lives-with-knowledge.md) | the emitter knows the types, so it also judged them | 5/5 | yes |
|
||||
| 13 | [thirty-five-return-types](13-thirty-five-return-types.md) | `is_int_call` hardcoded what drives `+` dispatch | 6/6 | yes |
|
||||
| 14 | [keywords-that-reserve-nothing](14-keywords-that-reserve-nothing.md) | 5 of 46 keywords consumed by no path | 6/6 | yes |
|
||||
| 15 | [no-namespacing-at-all](15-no-namespacing-at-all.md) | `import` is textual inlining; every name is global | 4/4 | yes |
|
||||
| 16 | [tokens-carry-no-position](16-tokens-carry-no-position.md) | a token was `(kind, value)`, so no diagnostic could name a place | 6/6 | yes |
|
||||
| 17 | [annotations-are-never-checked](17-annotations-are-never-checked.md) | the annotation feeds dispatch and is never verified | 6/6 | branch |
|
||||
| 18 | [async-half-expressible](18-async-half-expressible.md) | **first attempt was DOGMA** — no predictions, rigged test | 4/4 (2nd) | branch |
|
||||
| 19 | [a-convention-is-not-a-gate](19-a-convention-is-not-a-gate.md) | `looks_like_heap_obj` is static, so every type re-derives it | 6/7 | yes |
|
||||
@@ -0,0 +1,42 @@
|
||||
# constructs have nowhere to be
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `5718943`
|
||||
|
||||
```
|
||||
let a construct declare its own meaning instead of the emitter knowing it
|
||||
|
||||
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.
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# a construct cannot refuse
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `60737b0`
|
||||
|
||||
```
|
||||
let a construct refuse, not only observe
|
||||
|
||||
@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.
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
# the wrapper was conditional
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `4f7568b`
|
||||
|
||||
```
|
||||
give a construct its after-crossing face, and let constructs compose
|
||||
|
||||
§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.
|
||||
```
|
||||
|
||||
## Record — `285166c`
|
||||
|
||||
```
|
||||
EXPERIMENT: emit the wrapper unconditionally, so exit binds at runtime too
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `b40754f`
|
||||
|
||||
```
|
||||
land unconditional wrapper: exit crossings resolve at runtime
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
# c has no closure syntax
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `2bed848`
|
||||
|
||||
```
|
||||
EXPERIMENT: hand the construct the body as a real closure
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `7d01608`
|
||||
|
||||
```
|
||||
land wraps_body: a construct controls invocation
|
||||
|
||||
Proven on experiment/wraps-body (2bed848): base(5) wrapped by a target that
|
||||
invokes the body twice returns 10; a target that never invokes it returns 999.
|
||||
Neither is expressible by deciding whether to repeat.
|
||||
|
||||
Root cause it corrected: 'C has no closures' is a fact about one grammar, not
|
||||
about what can be emitted. And El's single type (el_val_t = int64_t) cannot
|
||||
describe a callable, so codegen emits the calling convention rather than asking
|
||||
El's type system for something it structurally cannot say.
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# the emitter discards what it knows
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `a5af871`
|
||||
|
||||
```
|
||||
EXPERIMENT: let the compiler write down what it already knows
|
||||
|
||||
HYPOTHESIS: attribution is redundant for static structure. Codegen sees every
|
||||
construct-to-function relation at emission time and discards it, so the only
|
||||
way to learn the structure back is to run the program and read what it
|
||||
reported. That is instrumentation compensating for erasure.
|
||||
|
||||
PREDICTIONS, committed before running:
|
||||
1 derivable at compile time with no runtime call expected TRUE
|
||||
2 complete for guards and exits (invisible today) expected TRUE
|
||||
3 answers it for code that has never executed expected TRUE
|
||||
4 deterministic expected TRUE
|
||||
5 makes the entry beat redundant expected FALSE
|
||||
|
||||
RESULT: 5/5 as predicted. From a program that was never executed:
|
||||
|
||||
authenticate guards_at_entry login my_auth
|
||||
durable injects_at_exit save persist_now
|
||||
authenticate guards_at_entry critical my_auth
|
||||
durable injects_at_exit critical persist_now
|
||||
manager injects_at_entry critical engram_boundary_beat
|
||||
|
||||
Prediction 5 held: the relation records that a boundary COULD be crossed, the
|
||||
beat records that it WAS. They are different facts and neither replaces the
|
||||
other.
|
||||
|
||||
CONSEQUENCE, and it undercuts the first pass on iteration-1: construct identity
|
||||
was available at compile time all along. With relations recorded at build, the
|
||||
runtime needs only the function name and attribution becomes a join rather than
|
||||
a payload. The counter-argument is that the payload is self-describing while
|
||||
the file must be pinned to the artifact or the two drift and attribution is
|
||||
silently lost — which is the same conclusion as "compile against a manifold
|
||||
revision and record the revision in the artifact", reached from the other side.
|
||||
|
||||
Written to a file rather than the engram on purpose: a compile that consults a
|
||||
manifold produces different output from identical source at different times.
|
||||
The file is content-addressed; the engram ingests it. Determinism preserved,
|
||||
mechanism proven.
|
||||
```
|
||||
@@ -0,0 +1,170 @@
|
||||
# the crossing resolves at emission
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `35b07ba`
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `886626a`
|
||||
|
||||
```
|
||||
seam refusal + control tests: a runtime binding can short-circuit
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `28d19da`
|
||||
|
||||
```
|
||||
strip the compile-time machinery the seam replaces
|
||||
|
||||
PREDICTION: codegen.el drops below 4661, its size before any of these passes.
|
||||
RESULT: FALSE. 5157 -> 5096. Still +435 over baseline.
|
||||
|
||||
injects_at_entry collapsed into the seam removed
|
||||
guards_at_entry collapsed into the seam removed
|
||||
injects_at_exit needs the body-helper wrapper STRUCTURAL
|
||||
wraps_body needs the closure + wrapper structural
|
||||
prohibits_outside a #error cannot be emitted at runtime
|
||||
|
||||
The wrapper is not a consequence of compile-time resolution. Early returns must
|
||||
be routed through something no matter when the target is resolved, so exit
|
||||
injection was never going to collapse. I predicted it would because I had
|
||||
conflated "resolved late" with "emitted less".
|
||||
|
||||
What did collapse is entry injection and refusal -- 61 lines of compiler
|
||||
replaced by one refusable indirection, with the capability now bindable after
|
||||
the binary exists.
|
||||
|
||||
8 tests fail, and they are exactly the 8 controls for compile-time entry
|
||||
injection and guards. No unrelated breakage: the controls reported precisely
|
||||
what moved. They assert emission of something that now happens at runtime, so
|
||||
they need rewriting as integration tests -- which the framework does not
|
||||
currently support, because runtime binding needs a built binary and an
|
||||
environment, not compile_capture.
|
||||
|
||||
Verified after the strip: fixpoint gen2==gen3, observation and refusal both
|
||||
work through the seam with the compiler knowing nothing about either.
|
||||
```
|
||||
|
||||
## Record — `8bbb750`
|
||||
|
||||
```
|
||||
control the claim that cannot be unit tested
|
||||
|
||||
The seam's whole claim is that a construct declared AFTER a binary exists
|
||||
applies to that already-built program. compile_capture only sees emitted text,
|
||||
so it structurally cannot check this: it needs a built binary, a linked target,
|
||||
and an environment. Verified by hand until now, which is the standing problem
|
||||
this session has been about.
|
||||
|
||||
tests/integration/seam_binding.sh builds a probe from El source containing no
|
||||
construct at all, links a target that El never references, and asserts:
|
||||
|
||||
ok unbound program is unaffected
|
||||
ok a construct declared AFTER the build applies
|
||||
ok a construct declared after the build can REFUSE
|
||||
ok an unlinked target is skipped, not fatal
|
||||
ok a binding for a different fn does not fire
|
||||
ok two constructs compose on one crossing
|
||||
|
||||
6 assertions, 6 passed, 0 failed
|
||||
|
||||
The eight controls that failed after the strip were replaced, not repaired.
|
||||
They asserted compile-time emission of capability that moved to runtime;
|
||||
contorting them would have kept an assertion whose subject no longer exists.
|
||||
Three took their place, asserting the emitted shape, and the behaviour they
|
||||
used to cover is now the integration harness's job -- which is the honest
|
||||
division, since the shape and the behaviour are no longer the same fact.
|
||||
|
||||
99/99 native compiler tests pass. Fixpoint holds.
|
||||
```
|
||||
|
||||
## Record — `24f7fb5`
|
||||
|
||||
```
|
||||
land the runtime seam: resolve the crossing at execution
|
||||
|
||||
Five compile-time passes added 491 lines to the thing that was supposed to stop
|
||||
growing. The seam is ~55 lines of C and one line of emission, and it does at
|
||||
runtime what three of those five kinds did at compile time -- for programs that
|
||||
are already built.
|
||||
|
||||
a construct declared AFTER the binary exists applies to it
|
||||
free when unused: 0.36s vs 0.37s baseline across 267 indirections
|
||||
dlsym was the cost, not the table scan; resolve-once recovered 3.5x
|
||||
refusal works, composition works, unlinked targets are skipped not fatal
|
||||
|
||||
injects_at_exit and wraps_body do NOT collapse: early returns must route
|
||||
through the body-helper wrapper regardless of when the target is resolved. The
|
||||
wrapper is structural, which I had wrong. prohibits_outside cannot move at all
|
||||
-- a #error has no runtime.
|
||||
|
||||
Controls: 99/99 native compiler tests, plus tests/integration/seam_binding.sh
|
||||
(6/6) for the claim compile_capture structurally cannot see.
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
# invocation is not composable
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `bc2f26d`
|
||||
|
||||
```
|
||||
EXPERIMENT: invocation control resolves at runtime
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `c04d68f`
|
||||
|
||||
```
|
||||
land runtime invocation control: only prohibition remains compiled
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# the emitter adjudicates
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `c741cfe`
|
||||
|
||||
```
|
||||
EXPERIMENT: prohibition becomes a query over emitted relations
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `60c07ad`
|
||||
|
||||
```
|
||||
land prohibition-as-query: the emitter records, it no longer adjudicates
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# policy inside the compiler
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `c2d9596`
|
||||
|
||||
```
|
||||
EXPERIMENT: the capability tier becomes shipped policy plus a query
|
||||
|
||||
Capability differs from prohibits_outside in one way that matters: a utility
|
||||
program cannot be trusted to declare its own restrictions, because it would
|
||||
declare none. So the policy comes from OUTSIDE the program -- it ships with the
|
||||
language as data, editable without a compiler release.
|
||||
|
||||
tools/check/capabilities.rel 18 names that were string literals in codegen
|
||||
tools/check/capabilities.sh the query that decides
|
||||
|
||||
PREDICTIONS AND RESULTS
|
||||
P1 codegen emits kind + call graph, drops the 4 name tests TRUE zero #errors
|
||||
P2 the 18 literals become a data file TRUE
|
||||
P3 the checker catches capability violations TRUE exit=1
|
||||
P4 codegen drops ~76 lines TRUE 4963 -> 4881
|
||||
P5 below the 4661 baseline FALSE ~+230
|
||||
|
||||
TWO DEFECTS THE HARNESS FOUND THAT READING WOULD NOT HAVE
|
||||
|
||||
1. Calls inside main became invisible. cg_fn returns early for main -- C
|
||||
provides its own -- so hooking the recording there left every call in main
|
||||
unrecorded: a blind spot exactly where a program does its work. The old
|
||||
cap_check_call ran from cg_expr and did see main. Moved the recording to
|
||||
cg_expr.
|
||||
|
||||
2. Caller attribution was stale. __cg_current_fn kept whatever cg_fn set last,
|
||||
so a violation in main was reported against the previously emitted function.
|
||||
The test still PASSED, because the violation was detected -- only the name
|
||||
was wrong, and a diagnostic naming the wrong fn is worse than none. Fixed at
|
||||
all three main-emission sites; the first patch missed two because the live
|
||||
path is codegen_streaming.
|
||||
|
||||
98/98 native, 7/7 + 4/4 + 5/5 integration, fixpoint ok.
|
||||
```
|
||||
|
||||
## Record — `29f78f9`
|
||||
|
||||
```
|
||||
land capability-as-policy: eighteen literals become a data file
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
# a second copy of the header
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `9cc6040`
|
||||
|
||||
```
|
||||
EXPERIMENT: derive arity from the runtime's own declarations
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `d9e301b`
|
||||
|
||||
```
|
||||
land arity-from-header: the runtime declares its own surface
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
# one type erases the return
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `d2d89fc`
|
||||
|
||||
```
|
||||
EXPERIMENT: temporal types as data — and the pass that GREW the compiler
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `e01e079`
|
||||
|
||||
```
|
||||
land temporal signatures as data: the type table leaves, the dispatch stays
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# judgment lives with knowledge
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `e8e25a0`
|
||||
|
||||
```
|
||||
EXPERIMENT: temporal adjudication moves out; the placeholder stays
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `50425f3`
|
||||
|
||||
```
|
||||
land temporal adjudication as a query: the emitter records, the rules are data
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# thirty five return types
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `cbef1c1`
|
||||
|
||||
```
|
||||
EXPERIMENT: Int return types as data — and the bug that fell out
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `505e5e7`
|
||||
|
||||
```
|
||||
land int signatures, and repair a silent miscompilation they exposed
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
# keywords that reserve nothing
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `0143cc4`
|
||||
|
||||
```
|
||||
ANSWER: is a grammar a convention, or a region?
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `067dd40`
|
||||
|
||||
```
|
||||
answer the parsing question: a grammar is a basis, and five keywords reserved nothing
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# no namespacing at all
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `79f6cb7`
|
||||
|
||||
```
|
||||
ANSWER: if the partition is a neighbourhood, does linking survive?
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `f23cb2b`
|
||||
|
||||
```
|
||||
answer the module question: the partition is a path, and there is no namespacing
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
# tokens carry no position
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `6c975b1`
|
||||
|
||||
```
|
||||
thread provenance through resolve_imports
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
## Record — `cb7289f`
|
||||
|
||||
```
|
||||
thread provenance end to end: a diagnostic can finally name a place
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# annotations are never checked
|
||||
|
||||
**Status: verified on `experiment/annotation-checking`, not merged.**
|
||||
|
||||
## Ishikawa — why does El silently miscompile?
|
||||
|
||||
Three bugs 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 but no type CHECKING. The annotation
|
||||
feeds dispatch and is never itself verified.
|
||||
```
|
||||
|
||||
## Predictions
|
||||
|
||||
```
|
||||
P1 let x: Int = "hello" compiles clean expect TRUE
|
||||
P2 let s: String = 42 compiles clean expect TRUE
|
||||
P3 the annotation drives dispatch, unverified expect TRUE
|
||||
P4 same root cause as all three bugs found today expect TRUE
|
||||
P5 checking literal-vs-annotation catches both expect TRUE
|
||||
P6 zero false positives across the compiler's source expect TRUE
|
||||
```
|
||||
|
||||
## Results — 6/6, and worse than a wrong answer
|
||||
|
||||
```
|
||||
let x: Int = "hello"; x + 1 → 4343631981 a string POINTER used as an integer
|
||||
let s: String = 42; println(s) → nothing address 42 dereferenced as a string
|
||||
```
|
||||
|
||||
The first **leaks a raw memory address into program output**. The second is an
|
||||
**arbitrary-read primitive** if that integer is ever attacker-influenced.
|
||||
|
||||
Verified: 6/6, zero false positives across the compiler's own source, fixpoint
|
||||
ok, 105/105 native.
|
||||
|
||||
## Six Sigma
|
||||
|
||||
The emitter only **records** the mismatch; `tools/check/annotations.sh` decides —
|
||||
consistent with every other check. Literals are checked because they are
|
||||
unambiguous.
|
||||
|
||||
**Incomplete, stated not hidden:** only literals. `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`.
|
||||
@@ -0,0 +1,73 @@
|
||||
# async — half expressible, and the cycle that was dogma
|
||||
|
||||
**Status: measured on a branch, not merged. Two runs — the first was invalid.**
|
||||
|
||||
## The first attempt was DOGMA, not science
|
||||
|
||||
I had just finished arguing that `@async` was expressible, then ran something to
|
||||
confirm it. **No prediction was committed.** The test was rigged in a way that
|
||||
should have been visible while writing it:
|
||||
|
||||
```c
|
||||
pthread_create(&t,NULL,runner,NULL); pthread_join(t,NULL);
|
||||
```
|
||||
|
||||
`join` immediately after `create` — the caller blocks until the body finishes.
|
||||
That is a thread round-trip, not deferral. And the test printed the word
|
||||
`DEFERRED` itself: I wrote the conclusion into the output and read it back.
|
||||
|
||||
```
|
||||
Ishikawa on the rigged test
|
||||
method ran after concluding, not to decide
|
||||
machine nothing forces a prediction before execution
|
||||
material the assertion was written into the output string
|
||||
measurement no falsification criterion existed, so nothing could fail
|
||||
root cause the test was authored by the party holding the conclusion,
|
||||
with no commitment made before it ran
|
||||
```
|
||||
|
||||
Discarded and re-run properly.
|
||||
|
||||
## Second run — predictions committed first
|
||||
|
||||
```
|
||||
P1 the caller proceeds while the body runs expect TRUE
|
||||
P2 interleaving is observable in timestamps expect TRUE
|
||||
P3 the result cannot be retrieved — one 64-bit slot, no
|
||||
future type, so the wrap either blocks or returns
|
||||
something that is not the result expect TRUE
|
||||
P4 therefore HALF expressible: fire-and-forget yes, await no expect TRUE
|
||||
```
|
||||
|
||||
## Results — 4/4
|
||||
|
||||
```
|
||||
[ 18 us] wrap RETURNS to caller
|
||||
[ 29 us] body START
|
||||
caller continues, got 0
|
||||
[ 50176 us] body END (computed 42)
|
||||
caller done
|
||||
```
|
||||
|
||||
The caller got **0, not 42**. Both of my earlier claims were wrong in opposite
|
||||
directions: "not expressible" was too strong — fire-and-forget works today,
|
||||
bound after the build, no compiler change. "Expressible" was too strong the
|
||||
other way.
|
||||
|
||||
## Follow-on cycle — a future is one more tagged object
|
||||
|
||||
```
|
||||
P1 el_val_t already carries tagged heap objects TRUE 5 magic tags exist
|
||||
P2 a future is one more TRUE
|
||||
P3 the caller awaits and gets 42 TRUE
|
||||
P4 ZERO compiler changes TRUE runtime C + one binding
|
||||
P5 the unbound path still works FALSE SIGSEGV
|
||||
```
|
||||
|
||||
**P4 is the result.** `@async` — called unexpressible for hours — needs no
|
||||
compiler change. A future is one more magic-tagged heap object; `defer` returns
|
||||
the handle, `el_await` blocks.
|
||||
|
||||
**P5 is the failure that mattered.** Sixty seconds after diagnosing
|
||||
`let s: String = 42` as an arbitrary read, I wrote the identical defect into
|
||||
`el_await`: reading `->magic` off an unvalidated slot. That opened cycle 19.
|
||||
@@ -0,0 +1,61 @@
|
||||
# a convention is not a gate
|
||||
|
||||
One `Ishikawa → scientific method → Six Sigma` loop. The record below is the
|
||||
commit message as written at the time, before the outcome was known to anyone
|
||||
reading this file.
|
||||
|
||||
## Record — `9a6c161`
|
||||
|
||||
```
|
||||
a slot must be validated before it is dereferenced
|
||||
|
||||
ISHIKAWA: el_val_t carries integers AND tagged heap pointers, so "is this a
|
||||
pointer" is undecidable without checking first. That check was a CONVENTION
|
||||
every author had to know rather than a GATE they had to pass through, and
|
||||
looks_like_heap_obj was static -- so every sibling translation unit re-derived
|
||||
it.
|
||||
|
||||
MEASURED, across the five existing tags
|
||||
geom_of looks_like_heap_obj full guard correct
|
||||
mfld_of looks_like_heap_obj full guard correct
|
||||
el_bin_lookup (uintptr_t)p < 4096 floor only reads 8 bytes BACKWARD
|
||||
el_input_len s ? ... : 0 NULL only strlen's an integer
|
||||
|
||||
sha256_hex(50000) -> exit 139, SIGSEGV, compiled clean
|
||||
|
||||
PREDICTIONS AND RESULTS
|
||||
P1 looks_like_heap_obj is static, not exported TRUE
|
||||
P2 each tagged type re-derives the check TRUE
|
||||
P3 at least one is missing guard components TRUE (two are)
|
||||
P6 sha256_hex(<int>) reads out of bounds TRUE
|
||||
P8 routing el_bin_lookup through the gate fixes it FALSE
|
||||
P9 the legitimate hash is unchanged TRUE
|
||||
P11 fixpoint and suites hold TRUE
|
||||
|
||||
P8 IS THE USEFUL FAILURE. Guarding the tagged lookup changed nothing --
|
||||
looks_like_heap_obj(49992) correctly returns 0, el_bin_lookup bails, and then
|
||||
el_input_len falls through to strlen() on address 50000. The FALLBACK was the
|
||||
hazard, not the tagged path. A NULL check does not establish that a slot is a
|
||||
pointer. I would have shipped the wrong fix and called it verified.
|
||||
|
||||
A MEASUREMENT DEFECT, fourth today: my first run of the crash reported exit=0,
|
||||
because $? read head's exit through a pipe rather than the program's. I nearly
|
||||
recorded a segfault as a clean run. Same shape as grepping only parser.el and
|
||||
searching by variable name instead of by operation.
|
||||
|
||||
AND I PROVED THE HAZARD FROM THE INSIDE. Sixty seconds after diagnosing
|
||||
`let s: String = 42` as an arbitrary-read primitive, I wrote the identical
|
||||
defect into el_await -- dereferencing ->magic off an unvalidated slot -- and
|
||||
only then found the runtime had already made it twice.
|
||||
|
||||
el_tagged() is now exported in el_runtime.h. Anything that dereferences a slot
|
||||
without passing through it is the defect.
|
||||
|
||||
105/105 native, 42/42 integration across eight harnesses, fixpoint ok.
|
||||
```
|
||||
|
||||
## Record — `3049a70`
|
||||
|
||||
```
|
||||
make the guard a gate: sha256_hex(50000) no longer segfaults
|
||||
```
|
||||
Reference in New Issue
Block a user