Adds two post-processing flags that produce production-ready browser JS in a
single elc invocation, replacing extract-js.py in the web product pipeline:
elc --target=js --bundle --minify source.el > output.min.js
elc --target=js --bundle --obfuscate source.el > output.obf.js
--minify shells out to terser (passes=2, no drop_console, drop_debugger).
--obfuscate shells out to javascript-obfuscator with the same options as the
old extract-js.py script. --obfuscate implies --minify.
Tool discovery: checks ./node_modules/.bin/, ../node_modules/.bin/ (monorepo),
then falls back to npx. Both flags require --target=js; passing either without
it exits 1 with a clear error.
Both tools receive a reserved-names list of globals referenced from HTML
onclick= attributes (neuronDemoToggle, signInWith, NEURON_CFG, etc.) so they
are not mangled.
Implementation adds stdout_to_file(path)/stdout_restore() builtins to the C
runtime so codegen's println-streamed output can be captured to a temp file
before being piped through the external tools. Temp files use
/tmp/elc-<pid>-<timestamp>.js naming and are cleaned up on success and failure.
Rebuilds dist/platform/elc and dist/platform/elc.c. Self-hosting verified.
Iteration 5:
? nil-propagation: Field and Index handlers in js_cg_expr now detect when
the object expression is a Try node (the AST node for postfix `?`).
When detected, emit JS optional chaining: `(expr)?.["field"] ?? null`.
The `?? null` normalizes JS undefined to El's null. A bare `expr?` not
followed by field/index still passes through unchanged.
browser-auth.el: a realistic 130-line example demonstrating:
- @async function with Supabase via native_js_call
- DOM bridge: get/set value/text/attr, add/remove class, show/hide
- local_storage_get/set for session hints
- window_on_load for initialization
- window_set to expose functions to the browser global scope
- set_timeout for transient state, is_valid_email for input validation
Compiles cleanly with elc --target=js --bundle
Spec updated: status promoted to Phase 4 / ~80% coverage, nil-prop
status updated, new example referenced.
Iteration 3: closes the browser API gap needed for real web pages.
New builtins in el_runtime.js:
Extended DOM: dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html,
dom_get_html, dom_get_parent, dom_contains_class, dom_get_checked,
dom_set_checked
Timers: set_timeout, set_interval, clear_interval
Local storage: local_storage_get, local_storage_set, local_storage_remove
Window: window_location, window_redirect, window_on_load
Debug: console_log
All browser-only functions use _ensureBrowser guard. Timer functions
work in both Node and browser. All new names added to __el export
object, ES named exports, and codegen-js.el destructure preamble.
Spec table updated to document new categories.
- el_runtime.js: add 19 dom_* builtins (browser-only, throw in Node),
window_set/window_get for exposing El functions to the browser global
scope, and native_js/native_js_call escape hatches for third-party libs
- codegen-js.el: destructure all new builtins in generated preamble; add
@async decorator support that emits async function + await at call sites
for known-async HTTP builtins and user-declared @async functions; pre-
registration pass ensures forward calls to @async functions get await
- spec/codegen-js.md: mark Phase 3 (DOM bridge) implemented, document
@async approach and its limitations, update builtin table and status
- examples/browser-counter.el: canonical example showing dom_get_element,
dom_set_text, dom_is_null, window_set, and state_set/get
- .gitea/workflows/sdk-release.yaml: build elc from bootstrap, run tests,
publish latest release, dispatch el-sdk-updated to downstream repos
- install.sh: one-command El SDK install from Gitea release
The Rust bootstrap was archived in 4f3543b and removed from the working
tree in e7a49eb. The bytecode tier was retired in 9fca4dc. What remained
on disk was leftover platform binaries (dist/platform/el-macos-universal,
el-windows-x86_64.exe) that nothing should be invoking, the elvm.md spec
for the retired bytecode tier, and the 8.7GB target/ build cache that
was tracked despite being in .gitignore.
Untracks target/, removes the platform binaries and elvm.md, and updates
spec/language.md so its self-hosting section no longer references the
genesis Rust path. The canonical toolchain is dist/platform/elc against
el-compiler/runtime/el_runtime.{h,c} — one compiler, one runtime, one
language.
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.