self-host: fold fn main() body into C int main(); rename C params

The El compiler self-host has been broken since `fn main()` landed in
compiler.el. Both bootstrap.py and codegen.el skipped emitting an
`el_val_t main()` (correct - it would collide with C's int main),
but neither folded the body anywhere. The C int main() got just
runtime init + return, so any El program that put its work inside
`fn main()` produced a binary that did nothing.

Fix in two places (bootstrap.py and codegen.el, kept symmetric):

  1. Capture the body of `fn main()` during the FnDef pass.
  2. Emit `int main(int _argc, char** _argv)` so El programs can
     declare their own local `argv` / `argc` (compiler.el itself
     does this) without colliding.
  3. After top-level statements, fold the captured fn main body
     into C main alongside them, then return 0.

Self-host fixed point reached: gen 2 and gen 3 of compiler.el's
output are byte-identical (md5 5b4eca2a...). The new elc compiles
products/web/src/main.el natively now - 24 imports resolved, 1,173
lines of C, every imported function (page_open, nav, pricing,
checkout_page, account_page, founding_badge…) emits its forward
decl + body without a concat preprocessor in sight.

Backup of the prior self-hosted binary is at
dist/platform/elc.preselfhost in case we need to fall back.
This commit is contained in:
Will Anderson
2026-05-02 01:30:04 -05:00
parent 276c0e5997
commit 13948f57a6
4 changed files with 558 additions and 64 deletions
+22 -4
View File
@@ -1321,14 +1321,24 @@ class CodeGen:
if has_toplevel_lets:
self.blank()
# Function definitions
# Function definitions. Skip El's `fn main()` for the same reason we
# skip its forward decl above: a duplicate `el_val_t main(void)` would
# collide with the `int main(int argc, char**)` we emit below. The
# body of `fn main()` is instead folded into C's main() alongside
# any top-level statements.
el_main_body = None
for s in stmts:
if s.get('stmt') == 'FnDef':
if s.get('name') == 'main':
el_main_body = s.get('body', [])
continue
self.cg_fn(s)
# main()
self.emit('int main(int argc, char** argv) {')
self.emit(' el_runtime_init_args(argc, argv);')
# main(). Use _argc/_argv as C parameter names so El programs are
# free to declare local `argv` / `argc` (and call args() / count_args())
# without colliding with the C-side parameters.
self.emit('int main(int _argc, char** _argv) {')
self.emit(' el_runtime_init_args(_argc, _argv);')
# cgi block init
for s in stmts:
@@ -1363,6 +1373,14 @@ class CodeGen:
continue
main_decl = self.cg_stmt(s, ' ', main_decl)
# If the source declared `fn main() -> Void { ... }`, fold its body
# in here. Mirrors codegen.el's behaviour and lets El programs
# written either way (top-level statements OR an explicit fn main)
# produce the same C main(). compiler.el itself uses this form.
if el_main_body:
for s in el_main_body:
main_decl = self.cg_stmt(s, ' ', main_decl)
self.emit(' return 0;')
self.emit('}')
self.blank()