// 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): 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_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) } // 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 } // ── 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. 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) // First pass: pull in every import body ahead of this file's body. let prefix: String = "" let body: String = "" 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, "") { let prefix = prefix + resolve_imports(imp_path) } else { let body = body + line + "\n" } let i = i + 1 } return prefix + body } // main — CLI entry point. // // elc # emit C to stdout // elc --target=js # emit JS to stdout // elc --target=c # write C to file // elc --target=js # write 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 positional: [String] = strip_flags(argv) let argc: Int = native_list_len(positional) if argc < 1 { println("el-compiler: usage: elc [--target=c|js] []") exit(1) } let src_path: String = native_list_get(positional, 0) let source: String = resolve_imports(src_path) let out: String = 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 "". }