// compiler.el — el self-hosting compiler pipeline // // Wires lexer -> parser -> codegen into a single compile() function. // This is the bootstrap entry point: compiled once by the Rust el-compiler, // then self-hosted from that point forward. // // Two backends: // - C (default) compile() -> emits C source linked against el_runtime.c // - JS (--target=js) compile_js() -> emits JS source linked against el_runtime.js // // Compile the C output with: // cc -o .c el_runtime.c // // Run the JS output with: // node .js (after copying el_runtime.js next to it) import "lexer.el" import "parser.el" import "codegen.el" import "codegen-js.el" // compile — full pipeline (C target): source string -> C source string fn compile(source: String) -> String { let tokens: [Map] = lex(source) let stmts: [Map] = parse(tokens) // Token list is no longer needed after parsing — release it to free memory // before codegen allocates its own working data on large source files. el_release(tokens) codegen(stmts, source) } // compile_js — full pipeline (JS target, module mode): source string -> JS source string fn compile_js(source: String) -> String { let tokens: [Map] = lex(source) let stmts: [Map] = parse(tokens) // Token list is no longer needed after parsing — release it to free memory. el_release(tokens) codegen_js(stmts, source) } // compile_js_with_bundle — JS target in bundle mode. // Reads el_runtime.js from runtime_path and inlines it inside an IIFE. fn compile_js_with_bundle(source: String, runtime_path: String) -> String { let tokens: [Map] = lex(source) let stmts: [Map] = parse(tokens) el_release(tokens) let runtime_content: String = fs_read(runtime_path) if str_eq(runtime_content, "") { println("el-compiler: warning: --bundle: could not read runtime at " + runtime_path) println("el-compiler: warning: bundle output will be incomplete") } codegen_js_bundle(stmts, source, runtime_content) } // compile_dispatch — pick a backend based on the requested target. // tgt = "c" | "js" // (The parameter is named `tgt` because `target` is a reserved keyword // in El's lexer — it would be tokenised as `Target`, breaking the // parser's identifier resolution.) fn compile_dispatch(tgt: String, source: String) -> String { if str_eq(tgt, "js") { return compile_js(source) } compile(source) } // compile_dispatch_bundle — like compile_dispatch but bundle mode for JS. fn compile_dispatch_bundle(tgt: String, source: String, runtime_path: String) -> String { if str_eq(tgt, "js") { return compile_js_with_bundle(source, runtime_path) } compile(source) } // Detect a `--target=` flag in argv and return the target. // Returns "c" if none specified or unrecognized. fn detect_target(argv: [String]) -> String { let n: Int = native_list_len(argv) let i = 0 while i < n { let a: String = native_list_get(argv, i) if str_starts_with(a, "--target=") { let v: String = str_slice(a, 9, str_len(a)) return v } let i = i + 1 } return "c" } // Strip flags from argv, leaving only positional arguments. fn strip_flags(argv: [String]) -> [String] { let out: [String] = native_list_empty() let n: Int = native_list_len(argv) let i = 0 while i < n { let a: String = native_list_get(argv, i) if !str_starts_with(a, "--") { let out = native_list_append(out, a) } let i = i + 1 } return out } // Detect --emit-header flag in argv. fn detect_emit_header(argv: [String]) -> Bool { let n: Int = native_list_len(argv) let i = 0 while i < n { let a: String = native_list_get(argv, i) if str_eq(a, "--emit-header") { return true } let i = i + 1 } return false } // Detect --bundle flag in argv. fn detect_bundle(argv: [String]) -> Bool { let n: Int = native_list_len(argv) let i = 0 while i < n { let a: String = native_list_get(argv, i) if str_eq(a, "--bundle") { return true } let i = i + 1 } return false } // Resolve the runtime path for --bundle mode. // Looks for el_runtime.js next to the source file first; // if not found there, looks next to the elc binary itself. // Returns "" if not found anywhere (caller emits a warning). fn resolve_runtime_path(src_path: String) -> String { let src_dir: String = dirname_of(src_path) let candidate: String = src_dir + "/el_runtime.js" let existing: String = fs_read(candidate) if !str_eq(existing, "") { return candidate } return "" } // Reconstruct an El type annotation string from a parsed type node. fn type_node_to_el(t: Map) -> String { let k: String = t["kind"] if str_eq(k, "Simple") { return t["name"] } if str_eq(k, "List") { let inner: String = type_node_to_el(t["inner"]) return "[" + inner + "]" } if str_eq(k, "Map") { let kt: String = type_node_to_el(t["key"]) let vt: String = type_node_to_el(t["val"]) return "Map<" + kt + ", " + vt + ">" } "Any" } // emit_header — write a .elh file from parsed statements. // Scans for FnDef nodes and emits 'extern fn' declarations. fn emit_header(stmts: [Map], hdr_path: String) -> Void { let n: Int = native_list_len(stmts) let i = 0 let parts: [String] = native_list_empty() let parts = native_list_append(parts, "// auto-generated by elc --emit-header — do not edit\n") while i < n { let stmt = native_list_get(stmts, i) let kind: String = stmt["stmt"] if str_eq(kind, "FnDef") { let name: String = stmt["name"] if !str_eq(name, "main") { let params = stmt["params"] let ret_type: String = stmt["ret_type"] // build param list let np: Int = native_list_len(params) let pi = 0 let param_parts: [String] = native_list_empty() while pi < np { let param = native_list_get(params, pi) let pname: String = param["name"] let ptype: String = param["type"] if str_eq(ptype, "") { let ptype = "Any" } let param_parts = native_list_append(param_parts, pname + ": " + ptype) let pi = pi + 1 } let params_str: String = str_join(param_parts, ", ") let ret_str: String = ret_type if str_eq(ret_str, "") { let ret_str = "Any" } let sig: String = "extern fn " + name + "(" + params_str + ") -> " + ret_str let parts = native_list_append(parts, sig + "\n") } } let i = i + 1 } let content: String = str_join(parts, "") let ok: Bool = fs_write(hdr_path, content) } // ── Import resolution ──────────────────────────────────────────────────────── // // elc supports two forms of import: // import "path/to/file.el" — quoted relative path // from module import { Name } — bare module name resolves to module.el // in the entry source's directory // // Codegen treats Import statements as no-ops (declarations only), so to // actually link bodies across files we textually concatenate every imported // source ahead of the entry source before lex/parse. resolve_imports does a // depth-first traversal with deduplication so any module that gets pulled in // transitively is included exactly once. fn dirname_of(path: String) -> String { let n: Int = str_len(path) let i: Int = n - 1 while i >= 0 { let c: String = str_slice(path, i, i + 1) if str_eq(c, "/") { return str_slice(path, 0, i) } let i = i - 1 } return "." } // Extract the resolved file path from a single trimmed source line. Returns // "" if the line is not an import. fn parse_import_line(trimmed: String, dir: String) -> String { if str_starts_with(trimmed, "import \"") { let after: String = str_slice(trimmed, 8, str_len(trimmed)) let q: Int = str_index_of(after, "\"") if q > 0 { let mod: String = str_slice(after, 0, q) return dir + "/" + mod } } if str_starts_with(trimmed, "from ") { let after: String = str_slice(trimmed, 5, str_len(trimmed)) // module name is the first whitespace-delimited token let sp: Int = str_index_of(after, " ") if sp > 0 { let mod_raw: String = str_slice(after, 0, sp) let mod: String = str_trim(mod_raw) if !str_eq(mod, "") { return dir + "/" + mod + ".el" } } } return "" } // Recursively resolve imports starting from src_path. Returns the combined // source text with every imported module's body inlined ahead of the entry // source, deduplicated by absolute path. Uses state_set to track which paths // have already been pulled in for this run. // // Accumulates chunks into lists and joins once at the end to avoid the O(n²) // memory growth caused by repeated `prefix = prefix + chunk` concatenation. fn resolve_imports(src_path: String) -> String { let seen_key: String = "__elc_imp__:" + src_path let already: String = state_get(seen_key) if !str_eq(already, "") { return "" } state_set(seen_key, "1") let source: String = fs_read(src_path) let dir: String = dirname_of(src_path) let lines: [String] = str_split(source, "\n") let n: Int = native_list_len(lines) // Collect chunks into lists — O(1) amortized per append. // Join once at the end — O(n) single pass. let prefix_chunks: [String] = native_list_empty() let body_chunks: [String] = native_list_empty() let i: Int = 0 while i < n { let line: String = native_list_get(lines, i) let trimmed: String = str_trim(line) let imp_path: String = parse_import_line(trimmed, dir) if !str_eq(imp_path, "") { // Use pre-compiled header if available (separate compilation). // Only check .elh for imported files — never for the entry file itself. let imp_elh_path: String = str_slice(imp_path, 0, str_len(imp_path) - 3) + ".elh" let imp_elh: String = fs_read(imp_elh_path) if !str_eq(imp_elh, "") { // Header exists: mark the .el as seen (so it won't be re-inlined // if something else also imports it) and use the header text. let seen_imp_key: String = "__elc_imp__:" + imp_path state_set(seen_imp_key, "1") let prefix_chunks = native_list_append(prefix_chunks, imp_elh) } else { let imp_body: String = resolve_imports(imp_path) let prefix_chunks = native_list_append(prefix_chunks, imp_body) } } else { let body_chunks = native_list_append(body_chunks, line + "\n") } let i = i + 1 } return str_join(prefix_chunks, "") + str_join(body_chunks, "") } // main — CLI entry point. // // elc # emit C to stdout // elc --target=js # emit JS (module) to stdout // elc --target=js --bundle # emit self-contained JS (IIFE) to stdout // elc --target=c # write C to file // elc --target=js # write JS to file // elc --target=js --bundle # write bundled JS to file fn main() -> Void { let argv: [String] = args() // Use `tgt` not `target`: `target` is a reserved keyword in the lexer // (Section 1.5 of the language spec). detect_target itself is fine // because the function-name position has no token-class restriction. let tgt: String = detect_target(argv) let do_emit_header: Bool = detect_emit_header(argv) let do_bundle: Bool = detect_bundle(argv) let positional: [String] = strip_flags(argv) let argc: Int = native_list_len(positional) if argc < 1 { println("el-compiler: usage: elc [--target=c|js] [--bundle] [--emit-header] []") exit(1) } let src_path: String = native_list_get(positional, 0) // When --emit-header is requested, parse the source file directly // (without inlining imports) and write out a .elh file alongside the .c. if do_emit_header { let raw_source: String = fs_read(src_path) let hdr_tokens: [Map] = lex(raw_source) let hdr_stmts: [Map] = parse(hdr_tokens) el_release(hdr_tokens) let hdr_path: String = str_slice(src_path, 0, str_len(src_path) - 3) + ".elh" emit_header(hdr_stmts, hdr_path) el_release(hdr_stmts) } let source: String = resolve_imports(src_path) let out: String = "" if do_bundle { let runtime_path: String = resolve_runtime_path(src_path) let out = compile_dispatch_bundle(tgt, source, runtime_path) } else { let out = compile_dispatch(tgt, source) } if argc >= 2 { let out_path: String = native_list_get(positional, 1) let ok: Bool = fs_write(out_path, out) if ok { exit(0) } else { println("el-compiler: failed to write output") exit(1) } } // No output path: codegen streamed to stdout already; out is "". }