diff --git a/el-compiler/runtime/el_runtime.c b/el-compiler/runtime/el_runtime.c index 8f8a602..ad5dbb8 100644 --- a/el-compiler/runtime/el_runtime.c +++ b/el-compiler/runtime/el_runtime.c @@ -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(); diff --git a/el-compiler/runtime/el_runtime.h b/el-compiler/runtime/el_runtime.h index 68bae8b..90da132 100644 --- a/el-compiler/runtime/el_runtime.h +++ b/el-compiler/runtime/el_runtime.h @@ -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); diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index d5530a1..afe3cb7 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -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 { 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 { 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 { // 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 { // 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 { 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 { 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 { 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 { } 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], 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], 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], 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], result_var: String) -> String { } let i = i + 1 } - out + str_join(parts, "") } fn cg_if_expr(expr: Map) -> String { @@ -1053,6 +1067,7 @@ fn cg_stmt(stmt: Map, 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, idx: Int) -> String { fn params_to_c(params: [Map]) -> 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) -> 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], 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], 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], 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. diff --git a/el-compiler/src/compiler.el b/el-compiler/src/compiler.el index b7c5db3..fef934b 100644 --- a/el-compiler/src/compiler.el +++ b/el-compiler/src/compiler.el @@ -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 { + 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: @@ -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] []") + println("el-compiler: usage: elc [--target=c|js] [--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 = compile_dispatch(tgt, source) if argc >= 2 { diff --git a/elb.el b/elb.el new file mode 100644 index 0000000..40642d5 --- /dev/null +++ b/elb.el @@ -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 { + // 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) +}