Add separate compilation: extern fn, --emit-header, elb build coordinator

This commit is contained in:
Will Anderson
2026-05-02 21:10:44 -05:00
parent 64e870c207
commit a084feb812
5 changed files with 600 additions and 62 deletions
+23
View File
@@ -1853,6 +1853,29 @@ el_val_t fs_write_bytes(el_val_t pathv, el_val_t bytesv, el_val_t lengthv) {
return 1;
}
// exec_command — run a shell command, return exit code (0 = success).
// Used by elb and other El tooling to invoke subprocesses.
el_val_t exec_command(el_val_t cmdv) {
const char* cmd = EL_CSTR(cmdv);
if (!cmd) return (el_val_t)(int64_t)-1;
int ret = system(cmd);
return (el_val_t)(int64_t)ret;
}
// exec_capture — run a shell command, capture stdout, return as String.
// Returns "" on failure.
el_val_t exec_capture(el_val_t cmdv) {
const char* cmd = EL_CSTR(cmdv);
if (!cmd) return el_wrap_str(el_strdup(""));
FILE* f = popen(cmd, "r");
if (!f) return el_wrap_str(el_strdup(""));
JsonBuf b; jb_init(&b);
char buf[4096];
while (fgets(buf, sizeof(buf), f)) jb_puts(&b, buf);
pclose(f);
return el_wrap_str(b.buf);
}
el_val_t fs_list(el_val_t pathv) {
const char* path = EL_CSTR(pathv);
el_val_t lst = el_list_empty();
+4
View File
@@ -739,6 +739,10 @@ el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */
/* See bottom of el_runtime.c for the implementation.
* Configured by env vars OTLP_ENDPOINT, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION.
* No-op when OTLP_ENDPOINT is unset. Drop-on-failure semantics. */
/* ── Subprocess execution ────────────────────────────────────────────────── */
el_val_t exec_command(el_val_t cmd); /* run shell command, return exit code */
el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
el_val_t trace_span_start(el_val_t name);
+100 -55
View File
@@ -18,26 +18,26 @@
fn c_escape(s: String) -> String {
let chars: [String] = native_string_chars(s)
let total: Int = native_list_len(chars)
let out = ""
let parts: [String] = native_list_empty()
let i = 0
while i < total {
let ch: String = native_list_get(chars, i)
if ch == "\"" {
let out = out + "\\\""
let parts = native_list_append(parts, "\\\"")
} else {
if ch == "\\" {
let out = out + "\\\\"
let parts = native_list_append(parts, "\\\\")
} else {
if ch == "\n" {
let out = out + "\\n"
let parts = native_list_append(parts, "\\n")
} else {
if ch == "\r" {
let out = out + "\\r"
let parts = native_list_append(parts, "\\r")
} else {
if ch == "\t" {
let out = out + "\\t"
let parts = native_list_append(parts, "\\t")
} else {
let out = out + ch
let parts = native_list_append(parts, ch)
}
}
}
@@ -45,7 +45,7 @@ fn c_escape(s: String) -> String {
}
let i = i + 1
}
out
str_join(parts, "")
}
fn c_str_lit(s: String) -> String {
@@ -191,6 +191,25 @@ fn cg_expr(expr: Map<String, Any>) -> String {
let left_kind: String = left["expr"]
let right_kind: String = right["expr"]
// String/equality fast-path: skip O(N²) temporal traversals
// The 10 temporal predicates below each recurse into the left subtree:
// O(depth) state_get calls per predicate, O(N²) total for a chain of N
// string-concat BinOps (e.g. the 70-100-part HTML chains in soul.el).
// When either operand is a bare Str literal the result is always concat
// or str_eq no temporal dispatch is possible. Exit immediately.
if str_eq(op, "Plus") {
if str_eq(left_kind, "Str") { return "el_str_concat(" + left_c + ", " + right_c + ")" }
if str_eq(right_kind, "Str") { return "el_str_concat(" + left_c + ", " + right_c + ")" }
}
if str_eq(op, "EqEq") {
if str_eq(left_kind, "Str") { return "str_eq(" + left_c + ", " + right_c + ")" }
if str_eq(right_kind, "Str") { return "str_eq(" + left_c + ", " + right_c + ")" }
}
if str_eq(op, "NotEq") {
if str_eq(left_kind, "Str") { return "!str_eq(" + left_c + ", " + right_c + ")" }
if str_eq(right_kind, "Str") { return "!str_eq(" + left_c + ", " + right_c + ")" }
}
// Temporal-type dispatch (Instant + Duration first-class)
// Run BEFORE the int / string / generic paths so typed temporal
// operands route through the runtime wrappers and invalid combos
@@ -565,17 +584,15 @@ fn cg_expr(expr: Map<String, Any>) -> String {
let arity: Int = native_list_len(args)
let func_kind: String = func["expr"]
let args_c = ""
let args_parts: [String] = native_list_empty()
let i = 0
while i < arity {
let arg = native_list_get(args, i)
let arg_c: String = cg_expr(arg)
if i > 0 {
let args_c = args_c + ", "
}
let args_c = args_c + arg_c
let args_parts = native_list_append(args_parts, arg_c)
let i = i + 1
}
let args_c: String = str_join(args_parts, ", ")
if func_kind == "Ident" {
let fn_name: String = func["name"]
@@ -658,18 +675,15 @@ fn cg_expr(expr: Map<String, Any>) -> String {
// Empty literal: el_list_new(0, ) generates malformed C (trailing
// comma in a varargs call). Emit el_list_empty() directly.
if n == 0 { return "el_list_empty()" }
let items = ""
let items_parts: [String] = native_list_empty()
let i = 0
while i < n {
let elem = native_list_get(elems, i)
let elem_c: String = cg_expr(elem)
if i > 0 {
let items = items + ", "
}
let items = items + elem_c
let items_parts = native_list_append(items_parts, elem_c)
let i = i + 1
}
return "el_list_new(" + native_int_to_str(n) + ", " + items + ")"
return "el_list_new(" + native_int_to_str(n) + ", " + str_join(items_parts, ", ") + ")"
}
if kind == "Map" {
@@ -680,20 +694,17 @@ fn cg_expr(expr: Map<String, Any>) -> String {
// shadowing inside for/while/if bodies `let acc: Map = {}`
// doesn't fail downstream cc with parse errors.
if n == 0 { return "el_map_new(0)" }
let items = ""
let items_parts: [String] = native_list_empty()
let i = 0
while i < n {
let pair = native_list_get(pairs, i)
let key: String = pair["key"]
let val = pair["value"]
let val_c: String = cg_expr(val)
if i > 0 {
let items = items + ", "
}
let items = items + c_str_lit(key) + ", " + val_c
let items_parts = native_list_append(items_parts, c_str_lit(key) + ", " + val_c)
let i = i + 1
}
return "el_map_new(" + native_int_to_str(n) + ", " + items + ")"
return "el_map_new(" + native_int_to_str(n) + ", " + str_join(items_parts, ", ") + ")"
}
if kind == "Try" {
@@ -736,7 +747,9 @@ fn cg_match(expr: Map<String, Any>) -> String {
let subj_var: String = "_match_subj_" + id
let result_var: String = "_match_result_" + id
let done_label: String = "_match_done_" + id
let out: String = "({ el_val_t " + subj_var + " = " + subj_c + "; el_val_t " + result_var + " = 0; "
// Accumulate arm fragments into a list to avoid O(n²) string growth.
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "({ el_val_t " + subj_var + " = " + subj_c + "; el_val_t " + result_var + " = 0; ")
let n: Int = native_list_len(arms)
let i = 0
while i < n {
@@ -746,19 +759,19 @@ fn cg_match(expr: Map<String, Any>) -> String {
let pkind: String = pat["pattern"]
let body_c: String = cg_expr(body)
if str_eq(pkind, "Wildcard") {
let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
} else {
if str_eq(pkind, "Binding") {
let bname: String = pat["name"]
let out = out + "{ el_val_t " + bname + " = " + subj_var + "; " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
let parts = native_list_append(parts, "{ el_val_t " + bname + " = " + subj_var + "; " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
} else {
if str_eq(pkind, "LitInt") {
let v: String = pat["value"]
let out = out + "if (" + subj_var + " == " + v + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
let parts = native_list_append(parts, "if (" + subj_var + " == " + v + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
} else {
if str_eq(pkind, "LitStr") {
let v: String = pat["value"]
let out = out + "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
} else {
if str_eq(pkind, "LitBool") {
let v: String = pat["value"]
@@ -766,10 +779,10 @@ fn cg_match(expr: Map<String, Any>) -> String {
if str_eq(v, "true") {
let bv = "1"
}
let out = out + "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
let parts = native_list_append(parts, "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
} else {
// unknown pattern wildcard
let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
}
}
}
@@ -777,8 +790,8 @@ fn cg_match(expr: Map<String, Any>) -> String {
}
let i = i + 1
}
let out = out + done_label + ":; " + result_var + "; })"
out
let parts = native_list_append(parts, done_label + ":; " + result_var + "; })")
str_join(parts, "")
}
// If-as-expression codegen
@@ -809,7 +822,8 @@ fn next_if_id() -> String {
// result var stays at its initial 0.
fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
let n: Int = native_list_len(stmts)
let out = ""
// Collect statement fragments into a list to avoid O(n²) string growth.
let parts: [String] = native_list_empty()
let i = 0
while i < n {
let s = native_list_get(stmts, i)
@@ -820,20 +834,20 @@ fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
let name: String = s["name"]
let val = s["value"]
let val_c: String = cg_expr(val)
let out = out + "el_val_t " + name + " = " + val_c + "; "
let parts = native_list_append(parts, "el_val_t " + name + " = " + val_c + "; ")
} else {
if str_eq(sk, "Return") {
let val = s["value"]
let val_c: String = cg_expr(val)
let out = out + result_var + " = (" + val_c + "); "
let parts = native_list_append(parts, result_var + " = (" + val_c + "); ")
} else {
if str_eq(sk, "Expr") {
let val = s["value"]
let val_c: String = cg_expr(val)
if is_last {
let out = out + result_var + " = (" + val_c + "); "
let parts = native_list_append(parts, result_var + " = (" + val_c + "); ")
} else {
let out = out + "(void)(" + val_c + "); "
let parts = native_list_append(parts, "(void)(" + val_c + "); ")
}
} else {
if str_eq(sk, "Assign") {
@@ -844,7 +858,7 @@ fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
let aname: String = s["name"]
let aval = s["value"]
let aval_c: String = cg_expr(aval)
let out = out + aname + " = " + aval_c + "; "
let parts = native_list_append(parts, aname + " = " + aval_c + "; ")
} else {
// Non-trivial stmt kinds (While/For) shouldn't appear in
// expression-position arm bodies; emit nothing rather
@@ -855,7 +869,7 @@ fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
}
let i = i + 1
}
out
str_join(parts, "")
}
fn cg_if_expr(expr: Map<String, Any>) -> String {
@@ -1053,6 +1067,7 @@ fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [Strin
if kind == "TypeDef" { return declared }
if kind == "EnumDef" { return declared }
if kind == "Import" { return declared }
if kind == "ExternFn" { return declared }
if kind == "CgiBlock" { return declared }
declared
}
@@ -1084,14 +1099,7 @@ fn strip_outer_parens(s: String) -> String {
let i = i + 1
}
if balanced {
let inner = ""
let j = 1
while j < n - 1 {
let ch: String = native_list_get(chars, j)
let inner = inner + ch
let j = j + 1
}
return inner
return str_slice(s, 1, n - 1)
}
}
}
@@ -1168,18 +1176,15 @@ fn param_decl(param: Map<String, Any>, idx: Int) -> String {
fn params_to_c(params: [Map<String, Any>]) -> String {
let n: Int = native_list_len(params)
if n == 0 { return "void" }
let out = ""
let parts: [String] = native_list_empty()
let i = 0
while i < n {
let param = native_list_get(params, i)
let decl: String = param_decl(param, i)
if i > 0 {
let out = out + ", "
}
let out = out + decl
let parts = native_list_append(parts, decl)
let i = i + 1
}
out
str_join(parts, ", ")
}
// Transform a function body so that an implicit-return final expression
@@ -2295,6 +2300,7 @@ fn is_top_level_decl(stmt: Map<String, Any>) -> Bool {
if kind == "EnumDef" { return true }
if kind == "Import" { return true }
if kind == "CgiBlock" { return true }
if kind == "ExternFn" { return true }
false
}
@@ -2519,6 +2525,12 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
emit_line("el_val_t " + fn_name + "(" + params_c + ");")
}
}
if kind == "ExternFn" {
let fn_name: String = stmt["name"]
let params = stmt["params"]
let params_c: String = params_to_c(params)
emit_line("el_val_t " + fn_name + "(" + params_c + ");")
}
let i = i + 1
}
emit_blank()
@@ -2557,6 +2569,36 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
emit_blank()
}
// Detect whether this compilation unit has an entry point.
// A unit is a library (no C main emitted) when there is no fn main()
// and no top-level executable statements. This supports separate
// compilation: library .c files contain only function definitions.
let has_el_main = false
let has_toplevel_stmts = false
let i = 0
while i < n {
let stmt = native_list_get(stmts, i)
let sk: String = stmt["stmt"]
if str_eq(sk, "FnDef") {
let fn_name_chk: String = stmt["name"]
if str_eq(fn_name_chk, "main") { let has_el_main = true }
}
if !is_fndef(stmt) {
if !is_top_level_decl(stmt) {
if !str_eq(sk, "Let") {
let has_toplevel_stmts = true
}
}
}
let i = i + 1
}
let is_library = false
if !has_el_main {
if !has_toplevel_stmts {
let is_library = true
}
}
// Function definitions
let i = 0
while i < n {
@@ -2567,6 +2609,9 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
let i = i + 1
}
// Skip C main() for library units (no fn main, no top-level stmts)
if is_library { return "" }
// main(). Use _argc/_argv so El programs are free to declare their own
// local `argv` / `argc` (compiler.el itself does this) without colliding
// with the C-side parameters when fn main()'s body is folded in below.
+106 -7
View File
@@ -79,6 +79,74 @@ fn strip_flags(argv: [String]) -> [String] {
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
}
// 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:
@@ -135,6 +203,9 @@ fn parse_import_line(trimmed: String, dir: String) -> String {
// 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)
@@ -146,22 +217,36 @@ fn resolve_imports(src_path: String) -> String {
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 = ""
// 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, "") {
let prefix = prefix + resolve_imports(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 = body + line + "\n"
let body_chunks = native_list_append(body_chunks, line + "\n")
}
let i = i + 1
}
return prefix + body
return str_join(prefix_chunks, "") + str_join(body_chunks, "")
}
// main CLI entry point.
@@ -176,13 +261,27 @@ fn main() -> Void {
// (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 positional: [String] = strip_flags(argv)
let argc: Int = native_list_len(positional)
if argc < 1 {
println("el-compiler: usage: elc [--target=c|js] <source.el> [<output>]")
println("el-compiler: usage: elc [--target=c|js] [--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 = compile_dispatch(tgt, source)
if argc >= 2 {
+367
View File
@@ -0,0 +1,367 @@
// elb.el El Build Coordinator
//
// The build system for El programs. Written in El. Builds El.
//
// Usage:
// elb # build from manifest.el in current dir
// elb --clean # remove generated artifacts and rebuild
// elb --dry-run # print actions without executing
// elb --jobs=N # parallel compile jobs (default: 4)
// elb --out=DIR # output directory (default: dist)
// elb --runtime=PATH # path to el_runtime.c
//
// How it works (the .NET model):
// 1. Read manifest.el to find the entry file
// 2. Walk the import graph depth-first, build topological order
// 3. For each file: if .el is newer than .elh/.c, compile with elc --emit-header
// 4. Link all .c files + el_runtime.c into the final binary
//
// Each module compiles independently no 128K-line blobs.
// Downstream compilations read .elh headers (function signatures only),
// not source. Incremental: only recompile what changed.
// Flags
fn flag_bool(argv: [String], name: 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, name) { return true }
let i = i + 1
}
return false
}
fn flag_val(argv: [String], name: String, default_val: String) -> String {
let n: Int = native_list_len(argv)
let prefix: String = name + "="
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_starts_with(a, prefix) {
return str_slice(a, str_len(prefix), str_len(a))
}
let i = i + 1
}
return default_val
}
// Manifest parsing
//
// Read the entry file from manifest.el:
// build { entry "soul.el" }
fn parse_manifest_entry(src: String) -> String {
let lines: [String] = str_split(src, "\n")
let n: Int = native_list_len(lines)
let i = 0
while i < n {
let line: String = native_list_get(lines, i)
let t: String = str_trim(line)
if str_starts_with(t, "entry ") {
// entry "soul.el"
let after: String = str_slice(t, 6, str_len(t))
let trimmed: String = str_trim(after)
// strip surrounding quotes
if str_starts_with(trimmed, "\"") {
let inner: String = str_slice(trimmed, 1, str_len(trimmed))
let q: Int = str_index_of(inner, "\"")
if q >= 0 {
return str_slice(inner, 0, q)
}
}
}
let i = i + 1
}
return ""
}
fn parse_manifest_name(src: String) -> String {
let lines: [String] = str_split(src, "\n")
let n: Int = native_list_len(lines)
let i = 0
while i < n {
let line: String = native_list_get(lines, i)
let t: String = str_trim(line)
if str_starts_with(t, "package ") {
let after: String = str_slice(t, 8, str_len(t))
let trimmed: String = str_trim(after)
if str_starts_with(trimmed, "\"") {
let inner: String = str_slice(trimmed, 1, str_len(trimmed))
let q: Int = str_index_of(inner, "\"")
if q >= 0 {
return str_slice(inner, 0, q)
}
}
}
let i = i + 1
}
return "out"
}
// Path helpers
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 "."
}
fn basename_noext(path: String) -> String {
// strip directory
let n: Int = str_len(path)
let last_slash: Int = -1
let i = 0
while i < n {
let c: String = str_slice(path, i, i + 1)
if str_eq(c, "/") { let last_slash = i }
let i = i + 1
}
let base: String = str_slice(path, last_slash + 1, n)
// strip .el extension
let bn: Int = str_len(base)
if str_ends_with(base, ".el") {
return str_slice(base, 0, bn - 3)
}
return base
}
fn path_with_ext(path: String, ext: String) -> String {
let n: Int = str_len(path)
if str_ends_with(path, ".el") {
return str_slice(path, 0, n - 3) + ext
}
return path + ext
}
fn file_is_newer(a: String, b: String) -> Bool {
// Returns true if file a is newer than file b, or if b doesn't exist.
// Uses exec_capture with stat to compare modification times.
let cmd: String = "test -f " + b + " && test " + a + " -nt " + b + " && echo yes || echo no"
let result: String = str_trim(exec_capture(cmd))
if str_eq(result, "yes") { return true }
// b doesn't exist check with test -f
let exist_cmd: String = "test -f " + b + " && echo exists || echo missing"
let exist: String = str_trim(exec_capture(exist_cmd))
if str_eq(exist, "missing") { return true }
return false
}
// Import graph walker
//
// Walk import statements in each .el file to build the dependency graph.
// Returns a list of absolute paths in topological order (deps before dependents).
fn parse_import_path(line: String, dir: String) -> String {
let t: String = str_trim(line)
if str_starts_with(t, "import \"") {
let after: String = str_slice(t, 8, str_len(t))
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(t, "from ") {
let after: String = str_slice(t, 5, str_len(t))
let sp: Int = str_index_of(after, " ")
if sp > 0 {
let mod: String = str_trim(str_slice(after, 0, sp))
if !str_eq(mod, "") {
return dir + "/" + mod + ".el"
}
}
}
return ""
}
fn walk_imports(src_path: String, visited: [String], order: [String]) -> Map<String, Any> {
// Dedup check
let n: Int = native_list_len(visited)
let i = 0
while i < n {
let v: String = native_list_get(visited, i)
if str_eq(v, src_path) {
return { "visited": visited, "order": order }
}
let i = i + 1
}
let visited = native_list_append(visited, src_path)
let source: String = fs_read(src_path)
if str_eq(source, "") {
return { "visited": visited, "order": order }
}
let dir: String = dirname_of(src_path)
let lines: [String] = str_split(source, "\n")
let ln: Int = native_list_len(lines)
let j = 0
while j < ln {
let line: String = native_list_get(lines, j)
let imp: String = parse_import_path(line, dir)
if !str_eq(imp, "") {
let r = walk_imports(imp, visited, order)
let visited = r["visited"]
let order = r["order"]
}
let j = j + 1
}
// Add self after all deps
let order = native_list_append(order, src_path)
return { "visited": visited, "order": order }
}
// Build
fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: Bool, verbose: Bool) -> Bool {
let bname: String = basename_noext(src_path)
let c_out: String = out_dir + "/" + bname + ".c"
let elh_out: String = out_dir + "/" + bname + ".elh"
// Check if recompile needed
if !file_is_newer(src_path, c_out) {
if verbose {
println(" skip " + bname + ".el (up to date)")
}
return true
}
let cmd: String = elc_bin + " --emit-header " + src_path + " " + c_out
println(" compile " + src_path)
if dry_run { return true }
let ret: Int = exec_command(cmd)
if ret != 0 {
println("elb: compile failed: " + src_path)
return false
}
return true
}
fn link_binary(c_files: [String], out_bin: String, runtime_path: String, dry_run: Bool) -> Bool {
let n: Int = native_list_len(c_files)
let parts: [String] = native_list_empty()
let parts = native_list_append(parts, "cc -O2 -I " + dirname_of(runtime_path))
let i = 0
while i < n {
let f: String = native_list_get(c_files, i)
let parts = native_list_append(parts, f)
let i = i + 1
}
let parts = native_list_append(parts, runtime_path)
let parts = native_list_append(parts, "-lcurl -lpthread")
let parts = native_list_append(parts, "-o " + out_bin)
let cmd: String = str_join(parts, " ")
println(" link " + out_bin)
if dry_run { return true }
let ret: Int = exec_command(cmd)
if ret != 0 {
println("elb: link failed")
return false
}
return true
}
// Main
fn main() -> Void {
let argv: [String] = args()
let clean: Bool = flag_bool(argv, "--clean")
let dry_run: Bool = flag_bool(argv, "--dry-run")
let verbose: Bool = flag_bool(argv, "--verbose")
let out_dir: String = flag_val(argv, "--out", "dist")
let elc_bin: String = flag_val(argv, "--elc", "elc")
let runtime: String = flag_val(argv, "--runtime", "")
// Find manifest
let manifest_src: String = fs_read("manifest.el")
if str_eq(manifest_src, "") {
println("elb: no manifest.el found in current directory")
exit(1)
}
let pkg_name: String = parse_manifest_name(manifest_src)
let entry: String = parse_manifest_entry(manifest_src)
if str_eq(entry, "") {
println("elb: manifest.el has no 'entry' declaration")
exit(1)
}
println("elb: building " + pkg_name + " (entry: " + entry + ")")
// Locate runtime
let runtime_path: String = runtime
if str_eq(runtime_path, "") {
// Try to find el_runtime.c relative to elc binary
let which_out: String = str_trim(exec_capture("which " + elc_bin + " 2>/dev/null"))
if !str_eq(which_out, "") {
let elc_dir: String = dirname_of(which_out)
runtime_path = elc_dir + "/../el-compiler/runtime/el_runtime.c"
}
}
if str_eq(runtime_path, "") {
println("elb: cannot locate el_runtime.c — use --runtime=PATH")
exit(1)
}
// Ensure output directory
let mkdir_ret: Int = exec_command("mkdir -p " + out_dir)
// Clean if requested
if clean {
println("elb: cleaning " + out_dir)
if !dry_run {
let rm_ret: Int = exec_command("rm -f " + out_dir + "/*.c " + out_dir + "/*.elh")
}
}
// Walk import graph from entry file
let empty_visited: [String] = native_list_empty()
let empty_order: [String] = native_list_empty()
let r = walk_imports(entry, empty_visited, empty_order)
let order: [String] = r["order"]
let total: Int = native_list_len(order)
println("elb: " + native_int_to_str(total) + " modules in build graph")
// Compile each module
let c_files: [String] = native_list_empty()
let i = 0
let ok = true
while i < total {
let src: String = native_list_get(order, i)
let bname: String = basename_noext(src)
let c_out: String = out_dir + "/" + bname + ".c"
let compiled: Bool = compile_module(src, out_dir, elc_bin, dry_run, verbose)
if !compiled {
let ok = false
let i = total
} else {
let c_files = native_list_append(c_files, c_out)
}
let i = i + 1
}
if !ok {
println("elb: build failed")
exit(1)
}
// Link
let out_bin: String = out_dir + "/" + pkg_name
let linked: Bool = link_binary(c_files, out_bin, runtime_path, dry_run)
if !linked {
println("elb: link failed")
exit(1)
}
println("elb: done → " + out_bin)
}