Add separate compilation: extern fn, --emit-header, elb build coordinator
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user