self-host the el compiler
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.
This commit is contained in:
+44
-31
@@ -7,8 +7,8 @@
|
||||
//
|
||||
// Entry point: fn lex(source: String) -> [Map<String, Any>]
|
||||
//
|
||||
// Uses global char_buf via native_chars_init / native_char_at / native_char_len
|
||||
// to avoid O(N²) cloning of the chars list.
|
||||
// Uses native_string_chars to split the source into a chars list,
|
||||
// then indexes it with native_list_get — avoids O(N²) string cloning.
|
||||
|
||||
// ── Character helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -140,15 +140,20 @@ fn keyword_kind(word: String) -> String {
|
||||
if word == "target" { return "Target" }
|
||||
if word == "true" { return "Bool" }
|
||||
if word == "false" { return "Bool" }
|
||||
if word == "cgi" { return "Cgi" }
|
||||
if word == "manager" { return "Manager" }
|
||||
if word == "engine" { return "Engine" }
|
||||
if word == "accessor" { return "Accessor" }
|
||||
if word == "vessel" { return "Vessel" }
|
||||
""
|
||||
}
|
||||
|
||||
// ── Scan helpers ──────────────────────────────────────────────────────────────
|
||||
// All scan helpers use the global char_buf (native_char_at / native_char_len).
|
||||
// No chars parameter — avoids O(N²) cloning.
|
||||
// All scan helpers receive the chars list and total length.
|
||||
|
||||
// scan_digits — advance i while char_buf[i] is a digit, return { "text": ..., "pos": i }
|
||||
fn scan_digits(start: Int, total: Int) -> Map<String, Any> {
|
||||
// scan_digits — advance i while chars[i] is a digit
|
||||
// Returns { "text": ..., "pos": i }
|
||||
fn scan_digits(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let text = ""
|
||||
let running = true
|
||||
@@ -156,7 +161,7 @@ fn scan_digits(start: Int, total: Int) -> Map<String, Any> {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch = native_char_at(i)
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if is_digit(ch) {
|
||||
let text = text + ch
|
||||
let i = i + 1
|
||||
@@ -168,8 +173,8 @@ fn scan_digits(start: Int, total: Int) -> Map<String, Any> {
|
||||
{ "text": text, "pos": i }
|
||||
}
|
||||
|
||||
// scan_ident — advance i while char_buf[i] is alphanumeric or underscore
|
||||
fn scan_ident(start: Int, total: Int) -> Map<String, Any> {
|
||||
// scan_ident — advance i while chars[i] is alphanumeric or underscore
|
||||
fn scan_ident(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let text = ""
|
||||
let running = true
|
||||
@@ -177,7 +182,7 @@ fn scan_ident(start: Int, total: Int) -> Map<String, Any> {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch = native_char_at(i)
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if is_alnum_or_underscore(ch) {
|
||||
let text = text + ch
|
||||
let i = i + 1
|
||||
@@ -191,21 +196,20 @@ fn scan_ident(start: Int, total: Int) -> Map<String, Any> {
|
||||
|
||||
// scan_string — scan a quoted string literal, handling \" escapes.
|
||||
// Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close }
|
||||
fn scan_string(start: Int, total: Int) -> Map<String, Any> {
|
||||
fn scan_string(chars: [String], start: Int, total: Int) -> Map<String, Any> {
|
||||
let i = start
|
||||
let text = ""
|
||||
let closed = false
|
||||
let running = true
|
||||
while running {
|
||||
if i >= total {
|
||||
let running = false
|
||||
} else {
|
||||
let ch = native_char_at(i)
|
||||
let ch: String = native_list_get(chars, i)
|
||||
if ch == "\\" {
|
||||
// escape: peek next char
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch = native_char_at(next_i)
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "\"" {
|
||||
let text = text + "\""
|
||||
let i = next_i + 1
|
||||
@@ -218,12 +222,17 @@ fn scan_string(start: Int, total: Int) -> Map<String, Any> {
|
||||
let text = text + "\t"
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
if next_ch == "\\" {
|
||||
let text = text + "\\"
|
||||
if next_ch == "r" {
|
||||
let text = text + "\r"
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let text = text + next_ch
|
||||
let i = next_i + 1
|
||||
if next_ch == "\\" {
|
||||
let text = text + "\\"
|
||||
let i = next_i + 1
|
||||
} else {
|
||||
let text = text + next_ch
|
||||
let i = next_i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +242,6 @@ fn scan_string(start: Int, total: Int) -> Map<String, Any> {
|
||||
}
|
||||
} else {
|
||||
if ch == "\"" {
|
||||
let closed = true
|
||||
let i = i + 1
|
||||
let running = false
|
||||
} else {
|
||||
@@ -249,13 +257,13 @@ fn scan_string(start: Int, total: Int) -> Map<String, Any> {
|
||||
// ── Main lexer ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn lex(source: String) -> [Map<String, Any>] {
|
||||
native_chars_init(source)
|
||||
let total: Int = native_char_len()
|
||||
let chars: [String] = native_string_chars(source)
|
||||
let total: Int = native_list_len(chars)
|
||||
let tokens: [Map<String, Any>] = native_list_empty()
|
||||
let i: Int = 0
|
||||
|
||||
while i < total {
|
||||
let ch: String = native_char_at(i)
|
||||
let ch: String = native_list_get(chars, i)
|
||||
|
||||
// Skip whitespace
|
||||
if is_whitespace(ch) {
|
||||
@@ -265,7 +273,7 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
if ch == "/" {
|
||||
let next_i = i + 1
|
||||
if next_i < total {
|
||||
let next_ch: String = native_char_at(next_i)
|
||||
let next_ch: String = native_list_get(chars, next_i)
|
||||
if next_ch == "/" {
|
||||
// skip to end of line
|
||||
let i = i + 2
|
||||
@@ -274,7 +282,7 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
if i >= total {
|
||||
let running2 = false
|
||||
} else {
|
||||
let lch: String = native_char_at(i)
|
||||
let lch: String = native_list_get(chars, i)
|
||||
if lch == "\n" {
|
||||
let running2 = false
|
||||
} else {
|
||||
@@ -293,7 +301,7 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
} else {
|
||||
// String literal
|
||||
if ch == "\"" {
|
||||
let result = scan_string(i + 1, total)
|
||||
let result = scan_string(chars, i + 1, total)
|
||||
let str_text: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
let tokens = native_list_append(tokens, make_tok("Str", str_text))
|
||||
@@ -301,18 +309,18 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
} else {
|
||||
// Number literal
|
||||
if is_digit(ch) {
|
||||
let result = scan_digits(i, total)
|
||||
let result = scan_digits(chars, i, total)
|
||||
let num_text: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
// check for float (dot followed by digit)
|
||||
if new_pos < total {
|
||||
let dot_ch: String = native_char_at(new_pos)
|
||||
let dot_ch: String = native_list_get(chars, new_pos)
|
||||
if dot_ch == "." {
|
||||
let after_dot = new_pos + 1
|
||||
if after_dot < total {
|
||||
let after_dot_ch: String = native_char_at(after_dot)
|
||||
let after_dot_ch: String = native_list_get(chars, after_dot)
|
||||
if is_digit(after_dot_ch) {
|
||||
let frac_result = scan_digits(after_dot, total)
|
||||
let frac_result = scan_digits(chars, after_dot, total)
|
||||
let frac_text: String = frac_result["text"]
|
||||
let frac_pos: Int = frac_result["pos"]
|
||||
let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text))
|
||||
@@ -336,7 +344,7 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
} else {
|
||||
// Identifier or keyword
|
||||
if is_alpha(ch) || ch == "_" {
|
||||
let result = scan_ident(i, total)
|
||||
let result = scan_ident(chars, i, total)
|
||||
let word: String = result["text"]
|
||||
let new_pos: Int = result["pos"]
|
||||
let kw = keyword_kind(word)
|
||||
@@ -351,7 +359,7 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
let peek_i = i + 1
|
||||
let peek_ch = ""
|
||||
if peek_i < total {
|
||||
let peek_ch = native_char_at(peek_i)
|
||||
let peek_ch: String = native_list_get(chars, peek_i)
|
||||
}
|
||||
|
||||
if ch == "=" {
|
||||
@@ -442,6 +450,10 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
if ch == "*" {
|
||||
let tokens = native_list_append(tokens, make_tok("Star", "*"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "%" {
|
||||
let tokens = native_list_append(tokens, make_tok("Percent", "%"))
|
||||
let i = i + 1
|
||||
} else {
|
||||
if ch == "(" {
|
||||
let tokens = native_list_append(tokens, make_tok("LParen", "("))
|
||||
@@ -501,6 +513,7 @@ fn lex(source: String) -> [Map<String, Any>] {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user