Closes the known limitation from the self-host commit: `fn add(a:Int,
b:Int) { a + b }` now compiles to integer addition, not string concat.
Previously the codegen heuristic guessed string concat whenever both
operands were Idents with no literal anchor.
Mechanism
- parser captures the leading type identifier from `let x: T = ...`
bindings (new "type" field on Let) and from function parameter
annotations (new "type" field on each param).
- codegen maintains a per-function int-name set in process state via
state_set("__int_names", csv). cg_fn seeds it from typed parameters;
cg_stmt extends it from typed `let` bindings and from `let x = <Int
literal>` (literal inference).
- BinOp Plus: when both sides are Idents and both names are in the
int-name set, emit arithmetic; otherwise the existing literal-anchor
heuristic applies, with string concat as the fallback.
This is the first compiler change made entirely through the self-
hosting workflow — no Python bootstrap. Edit el source, run existing
elc on elc-combined.el, cc the output, test. Closure holds at the
new binary.
Tests
- add(40, 2) → 42
- count_to(10) → 45 (let i: Int / let total: Int rebinding)
- Regression suite (tiny/implret/whiletest/lextest) unchanged.
dist/platform/elc updated; .prev preserved.
Today's milestone: dist/platform/elc compiles itself byte-for-byte to
itself (stage1 == stage2 == stage3 verified). The compiler is now a
real binary in the world.
What landed
- Spec rewrite (language.md) to truth — every feature marked
implemented / planned / not-in-this-language with no fiction.
- C runtime extension: 51 new builtins. JSON parser + accessors,
time, UUID, env, in-process state K/V, float formatting + math,
string ops (index_of, split, char_at, char_code, pad_left/right,
format), list ops (push, push_front, join, range), bool_to_str.
Runtime grew 631 → 1611 lines, header 171 → 247.
- Codegen fix: transform_implicit_return lifts a function's bare
trailing expression into an explicit return. Without it, lex(),
parse(), and every other implicit-return function returned 0/nil
and the whole pipeline produced empty C output.
- Codegen fix: index expressions dispatch on AST kind. obj["literal"]
→ el_get_field (map), arr[i] → el_list_get (list). Same Index node
in the parser, two different runtime calls.
- Codegen fix: skip emitting fn main() (collides with C main()) and
honor parsed return-type annotations so Void functions don't get
return-wrapped (return println(x) is a C type error).
- Parser: capture return-type identifier from -> Ret annotations.
- Lexer: + vessel keyword, + % operator, + \r escape.
- Runtime fix: el_list_append now allocates a fresh list rather than
realloc'ing the input. Realloc moved blocks made caller pointers
dangle, which was inserting garbage values into declared lists and
causing strcmp segfaults. Persistent allocation eliminates the
whole class of use-after-free at modest memory cost.
Bootstrap path
- One-shot Python helper translated elc-combined.el to C and
produced stage1. Helper is disposable; not committed.
- stage1 compiles elc-combined.el → stage2.c which cc compiles to
stage2; stage2 compiles elc-combined.el → stage3.c. stage2.c and
stage3.c are byte-identical. Closure proven.
- New elc installed at dist/platform/elc; old broken binary
preserved as dist/platform/elc.legacy.
- dist/platform/elc.c is the canonical generated source.
- elvm and the bytecode pipeline are no longer on the critical path.
Known gap
- The `+` operator's heuristic dispatch still picks string concat
when both operands are Idents with no literal anchor. Self-hosting
works because the compiler source is careful, but `fn add(a:Int,
b:Int) { a + b }` will not do arithmetic until codegen reads the
parsed type annotations to dispatch. Fix is wiring; not done here.
Tested
- tiny / lextest / whiletest / map+field / array build all run.
- cgi-studio (1037 lines real El) compiles to C cleanly. Link fails
only because runtime is missing fs_list, json_encode, llm_*; those
are scheduled batches.
- Three-stage closure (stage1 vs stage2 vs stage3) byte-identical.