Archived
422442b14e
elc --target=js --bundle source.el > output.js produces a single file
with no import statement that can drop directly into a <script> tag.
How it works:
- detect_bundle() reads the --bundle flag from argv
- resolve_runtime_path() looks for el_runtime.js next to the source file
- compile_js_with_bundle() reads the runtime, calls codegen_js_bundle()
- codegen_js_inner(bundle_mode=true):
- emits ;(function() { "use strict"; at the top
- inlines the runtime content (stripping ES export statements which
are invalid inside an IIFE via js_strip_es_exports())
- skips the const {...} = globalThis.__el destructure -- the inlined
function declarations are already in scope within the IIFE
- closes with })(); after main()
Usage: elc --target=js --bundle app.el > app.js
Place el_runtime.js in the same directory as app.el.
354 lines
14 KiB
EmacsLisp
354 lines
14 KiB
EmacsLisp
// 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 <prog> <prog>.c el_runtime.c
|
|
//
|
|
// Run the JS output with:
|
|
// node <prog>.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<String, Any>] = lex(source)
|
|
let stmts: [Map<String, Any>] = 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<String, Any>] = lex(source)
|
|
let stmts: [Map<String, Any>] = 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<String, Any>] = lex(source)
|
|
let stmts: [Map<String, Any>] = 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=<lang>` 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, Any>) -> 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<String, Any>], 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 <source.el> # emit C to stdout
|
|
// elc --target=js <source.el> # emit JS (module) to stdout
|
|
// elc --target=js --bundle <source.el> # emit self-contained JS (IIFE) to stdout
|
|
// elc --target=c <source.el> <out.c> # write C to file
|
|
// elc --target=js <source.el> <out.js> # write JS to file
|
|
// elc --target=js --bundle <source.el> <out.js> # 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] <source.el> [<output>]")
|
|
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<String, Any>] = lex(raw_source)
|
|
let hdr_stmts: [Map<String, Any>] = 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 "".
|
|
}
|