diff --git a/bootstrap.py b/bootstrap.py index a28aaa3..64b810c 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -1321,14 +1321,24 @@ class CodeGen: if has_toplevel_lets: self.blank() - # Function definitions + # Function definitions. Skip El's `fn main()` for the same reason we + # skip its forward decl above: a duplicate `el_val_t main(void)` would + # collide with the `int main(int argc, char**)` we emit below. The + # body of `fn main()` is instead folded into C's main() alongside + # any top-level statements. + el_main_body = None for s in stmts: if s.get('stmt') == 'FnDef': + if s.get('name') == 'main': + el_main_body = s.get('body', []) + continue self.cg_fn(s) - # main() - self.emit('int main(int argc, char** argv) {') - self.emit(' el_runtime_init_args(argc, argv);') + # main(). Use _argc/_argv as C parameter names so El programs are + # free to declare local `argv` / `argc` (and call args() / count_args()) + # without colliding with the C-side parameters. + self.emit('int main(int _argc, char** _argv) {') + self.emit(' el_runtime_init_args(_argc, _argv);') # cgi block init for s in stmts: @@ -1363,6 +1373,14 @@ class CodeGen: continue main_decl = self.cg_stmt(s, ' ', main_decl) + # If the source declared `fn main() -> Void { ... }`, fold its body + # in here. Mirrors codegen.el's behaviour and lets El programs + # written either way (top-level statements OR an explicit fn main) + # produce the same C main(). compiler.el itself uses this form. + if el_main_body: + for s in el_main_body: + main_decl = self.cg_stmt(s, ' ', main_decl) + self.emit(' return 0;') self.emit('}') self.blank() diff --git a/dist/platform/elc b/dist/platform/elc index 7b21bab..a4de585 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/dist/platform/elc.preselfhost b/dist/platform/elc.preselfhost new file mode 100755 index 0000000..68ee155 Binary files /dev/null and b/dist/platform/elc.preselfhost differ diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index 6d9252d..721bf30 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -165,7 +165,23 @@ fn cg_expr(expr: Map) -> String { if right_kind == "Str" { return "el_str_concat(" + left_c + ", " + right_c + ")" } - // If either side is an integer literal, this is arithmetic (not string concat) + // Type-driven dispatch via recursive is_int_expr: any expression + // whose value is provably Int (literal, typed Ident, known-Int + // builtin, or BinOp arithmetic over Ints) participates in + // arithmetic, not string concat. Recursion into BinOp lets + // `a + b + c` (chained Int adds) and `acc * 16 + d` route to + // arithmetic instead of falling to el_str_concat — both sides + // are Int so the outer `+` is too. + if is_int_expr(left) { + if is_int_expr(right) { + let op_c: String = binop_to_c(op) + return "(" + left_c + " " + op_c + " " + right_c + ")" + } + } + // Mixed cases: at least one side is provably Int but the other + // is not provably anything. Historical heuristic biases to + // arithmetic when a literal Int is present (preserves prior + // behaviour for `pos + 1` where `pos` is an untyped param). if left_kind == "Int" { let op_c: String = binop_to_c(op) return "(" + left_c + " " + op_c + " " + right_c + ")" @@ -174,57 +190,9 @@ fn cg_expr(expr: Map) -> String { let op_c: String = binop_to_c(op) return "(" + left_c + " " + op_c + " " + right_c + ")" } - // Type-driven dispatch: if both sides are Idents declared - // with type Int (parameters annotated `: Int` or let bindings - // annotated `: Int`), this is arithmetic, not concat. The - // current-function int-name set is maintained by cg_fn / - // cg_stmt via state_set("__int_names", csv). - if left_kind == "Ident" { - if right_kind == "Ident" { - let lname: String = left["name"] - let rname: String = right["name"] - if is_int_name(lname) { - if is_int_name(rname) { - let op_c: String = binop_to_c(op) - return "(" + left_c + " " + op_c + " " + right_c + ")" - } - } - } - } - // Same dispatch for Ident-Int + Call-to-known-Int-builtin (and the - // mirror). Without this, expressions like `pos + str_len(s)` get - // string-concatenated. is_int_call walks a known-builtin list. - if left_kind == "Ident" { - if right_kind == "Call" { - let lname: String = left["name"] - if is_int_name(lname) { - if is_int_call(right) { - let op_c: String = binop_to_c(op) - return "(" + left_c + " " + op_c + " " + right_c + ")" - } - } - } - } - if right_kind == "Ident" { - if left_kind == "Call" { - let rname: String = right["name"] - if is_int_name(rname) { - if is_int_call(left) { - let op_c: String = binop_to_c(op) - return "(" + left_c + " " + op_c + " " + right_c + ")" - } - } - } - } + // Otherwise: BinOp(+) with a Call/Ident side without int-typed + // evidence — fall back to string concat (the historical default). if left_kind == "Call" { - if right_kind == "Call" { - if is_int_call(left) { - if is_int_call(right) { - let op_c: String = binop_to_c(op) - return "(" + left_c + " " + op_c + " " + right_c + ")" - } - } - } return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Call" { @@ -242,8 +210,6 @@ fn cg_expr(expr: Map) -> String { return "el_str_concat(" + left_c + ", " + right_c + ")" } } - // Ident + Ident or Ident + unknown without int-typed evidence — - // fall back to string concat (the historical heuristic). if left_kind == "Ident" { return "el_str_concat(" + left_c + ", " + right_c + ")" } @@ -282,6 +248,16 @@ fn cg_expr(expr: Map) -> String { } } } + // Extend int-equality to mixed Ident/BinOp cases: `i == n - 1` + // where the left is an int-name Ident and the right is an + // arithmetic BinOp (or vice-versa). Without this check the + // fallthrough to str_eq produces str_eq(int_value, int_value) + // which reads the integer as a char* and segfaults. + if is_int_expr(left) { + if is_int_expr(right) { + return "(" + left_c + " == " + right_c + ")" + } + } if left_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" } @@ -326,6 +302,13 @@ fn cg_expr(expr: Map) -> String { } } } + // Same mixed Ident/BinOp fix as EqEq: use is_int_expr to detect + // integer-typed operands before falling through to !str_eq. + if is_int_expr(left) { + if is_int_expr(right) { + return "(" + left_c + " != " + right_c + ")" + } + } if left_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" } @@ -376,6 +359,12 @@ fn cg_expr(expr: Map) -> String { // violations to be emitted as #error directives at the // top of the generated C, so cc fails with a clear msg. cap_check_call(fn_name) + // Arity check against the builtin table — refuse, with a clear + // El-source message, when a known builtin gets the wrong arg + // count (e.g. `http_serve(port)` instead of `http_serve(port, + // handler)`). User-defined fns and variadic builtins pass + // through (builtin_arity returns -1). + arity_check_call(fn_name, arity) return fn_name + "(" + args_c + ")" } @@ -445,6 +434,11 @@ fn cg_expr(expr: Map) -> String { if kind == "Map" { let pairs = expr["pairs"] let n: Int = native_list_len(pairs) + // Empty literal: `el_map_new(0, )` is malformed C (trailing comma in + // a varargs call). Emit `el_map_new(0)` directly so empty-map + // 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 i = 0 while i < n { @@ -467,9 +461,7 @@ fn cg_expr(expr: Map) -> String { } if kind == "If" { - let cond = expr["cond"] - let cond_c: String = cg_expr(cond) - return "/* if-expr */ ((" + cond_c + ") ? (el_val_t)1 : (el_val_t)0)" + return cg_if_expr(expr) } if kind == "Match" { @@ -548,6 +540,89 @@ fn cg_match(expr: Map) -> String { out } +// ── If-as-expression codegen ───────────────────────────────────────────────── +// +// Lower `if cond { thenBody } else { elseBody }` used in expression position +// (e.g. `let x = if a { b } else { c }`) to a GCC/Clang statement-expression +// so the actual arm bodies are evaluated, not just `(cond ? 1 : 0)`. +// +// Each arm body is a list of statements; the result of the arm is the value +// of its final Expr statement (mirroring transform_implicit_return at function +// scope). Statements before the final Expr are emitted as expression-statements +// for their side effects. + +fn next_if_id() -> String { + let csv: String = state_get("__if_expr_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__if_expr_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +// Render a single arm of the if-as-expression: emit each statement-before-last +// as a side-effecting expression, then assign the final Expr's value to the +// result var. If the arm body is empty or its last stmt isn't an Expr, the +// 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 = "" + let i = 0 + while i < n { + let s = native_list_get(stmts, i) + let sk: String = s["stmt"] + let is_last: Bool = false + if i == n - 1 { let is_last = true } + if str_eq(sk, "Let") { + 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 + "; " + } else { + if str_eq(sk, "Return") { + let val = s["value"] + let val_c: String = cg_expr(val) + let out = out + 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 + "); " + } else { + let out = out + "(void)(" + val_c + "); " + } + } else { + // Non-trivial stmt kinds (While/For) shouldn't appear in + // expression-position arm bodies; emit nothing rather + // than malformed C. + } + } + } + let i = i + 1 + } + out +} + +fn cg_if_expr(expr: Map) -> String { + let cond = expr["cond"] + let then_stmts = expr["then"] + let else_stmts = expr["else"] + let has_else: Bool = expr["has_else"] + let cond_c: String = cg_expr(cond) + let id: String = next_if_id() + let result_var: String = "_if_result_" + id + let then_c: String = cg_if_expr_arm(then_stmts, result_var) + let else_c: String = "" + if has_else { + let else_c = cg_if_expr_arm(else_stmts, result_var) + } + let out: String = "({ el_val_t " + result_var + " = 0; if (" + cond_c + ") { " + then_c + "} else { " + else_c + "} " + result_var + "; })" + out +} + // ── Variable scope tracking ─────────────────────────────────────────────────── // // El allows `let x = expr` to both declare and reassign x in the same scope. @@ -859,6 +934,77 @@ fn is_int_call(call_expr: Map) -> Bool { return false } +// Recursive type-propagation: is `expr` known-Int at codegen time? +// This unifies the BinOp(+) dispatch so chained arithmetic over Int +// operands stays arithmetic. Without recursion, a wrapping `+` between +// `BinOp(+) of two Ints` and another Int falls to el_str_concat because +// the outer dispatch only checks the immediate kind, not the inner. +// +// Rules: +// Int literal → Int +// Ident in __int_names → Int +// Call to known-Int builtin → Int +// Neg of Int → Int +// BinOp arithmetic of two Ints → Int (Plus, Minus, Star, Slash, Percent) +// BinOp comparison/logical → Int (yields 0/1; safe to treat as Int) +// anything else → not provably Int +fn is_int_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "Int") { return true } + if str_eq(k, "Ident") { + let name: String = expr["name"] + return is_int_name(name) + } + if str_eq(k, "Call") { + return is_int_call(expr) + } + if str_eq(k, "Neg") { + return is_int_expr(expr["inner"]) + } + if str_eq(k, "Not") { + return true + } + if str_eq(k, "BinOp") { + let op: String = expr["op"] + // Comparisons and logicals always yield 0/1 — safe Int. + if str_eq(op, "EqEq") { return true } + if str_eq(op, "NotEq") { return true } + if str_eq(op, "Lt") { return true } + if str_eq(op, "Gt") { return true } + if str_eq(op, "LtEq") { return true } + if str_eq(op, "GtEq") { return true } + if str_eq(op, "And") { return true } + if str_eq(op, "Or") { return true } + // Arithmetic propagates: Int op Int → Int. + if str_eq(op, "Plus") { + if is_int_expr(expr["left"]) { + if is_int_expr(expr["right"]) { return true } + } + return false + } + if str_eq(op, "Minus") { + if is_int_expr(expr["left"]) { + if is_int_expr(expr["right"]) { return true } + } + return false + } + if str_eq(op, "Star") { + if is_int_expr(expr["left"]) { + if is_int_expr(expr["right"]) { return true } + } + return false + } + if str_eq(op, "Slash") { + if is_int_expr(expr["left"]) { + if is_int_expr(expr["right"]) { return true } + } + return false + } + return false + } + return false +} + // ── Capability-kind enforcement ────────────────────────────────────────────── // // A program's top-level block (cgi / service / none) determines which @@ -967,6 +1113,243 @@ fn emit_cap_violations() -> Void { } } +// ── Builtin arity table ─────────────────────────────────────────────────────── +// +// El programs sometimes call runtime builtins with the wrong number of +// arguments (e.g. `http_serve(port)` instead of `http_serve(port, handler)`). +// Without this check the generated C compiles to a call with too few / +// too many args and fails downstream cc with a generic "too few arguments" +// message that doesn't point to the El source line. +// +// Strategy: a small static table mirrors el_runtime.h. Variadic builtins +// (el_list_new, el_map_new, args) and unknown identifiers (user fns, +// dynamic dispatch) return -1 → no check. A mismatch records a violation +// in process state, which emit_arity_violations() turns into #error +// directives at the top of the generated C. +fn builtin_arity(name: String) -> Int { + // I/O + if str_eq(name, "println") { return 1 } + if str_eq(name, "print") { return 1 } + if str_eq(name, "readline") { return 0 } + // String + if str_eq(name, "el_str_concat") { return 2 } + if str_eq(name, "str_eq") { return 2 } + if str_eq(name, "str_starts_with") { return 2 } + if str_eq(name, "str_ends_with") { return 2 } + if str_eq(name, "str_len") { return 1 } + if str_eq(name, "str_concat") { return 2 } + if str_eq(name, "int_to_str") { return 1 } + if str_eq(name, "str_to_int") { return 1 } + if str_eq(name, "str_slice") { return 3 } + if str_eq(name, "str_contains") { return 2 } + if str_eq(name, "str_replace") { return 3 } + if str_eq(name, "str_to_upper") { return 1 } + if str_eq(name, "str_to_lower") { return 1 } + if str_eq(name, "str_trim") { return 1 } + if str_eq(name, "str_index_of") { return 2 } + if str_eq(name, "str_split") { return 2 } + if str_eq(name, "str_char_at") { return 2 } + if str_eq(name, "str_char_code") { return 2 } + if str_eq(name, "str_pad_left") { return 3 } + if str_eq(name, "str_pad_right") { return 3 } + if str_eq(name, "str_format") { return 2 } + if str_eq(name, "str_lower") { return 1 } + if str_eq(name, "str_upper") { return 1 } + // Math + if str_eq(name, "el_abs") { return 1 } + if str_eq(name, "el_max") { return 2 } + if str_eq(name, "el_min") { return 2 } + // List + if str_eq(name, "el_list_len") { return 1 } + if str_eq(name, "el_list_get") { return 2 } + if str_eq(name, "el_list_append") { return 2 } + if str_eq(name, "el_list_empty") { return 0 } + if str_eq(name, "el_list_clone") { return 1 } + if str_eq(name, "list_push") { return 2 } + if str_eq(name, "list_push_front") { return 2 } + if str_eq(name, "list_join") { return 2 } + if str_eq(name, "list_range") { return 2 } + // Map + if str_eq(name, "el_get_field") { return 2 } + if str_eq(name, "el_map_get") { return 2 } + if str_eq(name, "el_map_set") { return 3 } + // HTTP + if str_eq(name, "http_get") { return 1 } + if str_eq(name, "http_post") { return 2 } + if str_eq(name, "http_post_json") { return 2 } + if str_eq(name, "http_get_with_headers") { return 2 } + if str_eq(name, "http_post_with_headers") { return 3 } + if str_eq(name, "http_post_form_auth") { return 3 } + if str_eq(name, "http_serve") { return 2 } + if str_eq(name, "http_set_handler") { return 1 } + // Filesystem + if str_eq(name, "fs_read") { return 1 } + if str_eq(name, "fs_write") { return 2 } + if str_eq(name, "fs_list") { return 1 } + // JSON + if str_eq(name, "json_get") { return 2 } + if str_eq(name, "json_parse") { return 1 } + if str_eq(name, "json_stringify") { return 1 } + if str_eq(name, "json_get_string") { return 2 } + if str_eq(name, "json_get_int") { return 2 } + if str_eq(name, "json_get_float") { return 2 } + if str_eq(name, "json_get_bool") { return 2 } + if str_eq(name, "json_get_raw") { return 2 } + if str_eq(name, "json_set") { return 3 } + if str_eq(name, "json_array_len") { return 1 } + // Time + if str_eq(name, "time_now") { return 0 } + if str_eq(name, "time_now_utc") { return 0 } + if str_eq(name, "sleep_secs") { return 1 } + if str_eq(name, "sleep_ms") { return 1 } + if str_eq(name, "time_format") { return 2 } + if str_eq(name, "time_to_parts") { return 1 } + if str_eq(name, "time_from_parts") { return 3 } + if str_eq(name, "time_add") { return 3 } + if str_eq(name, "time_diff") { return 3 } + // UUID + if str_eq(name, "uuid_new") { return 0 } + if str_eq(name, "uuid_v4") { return 0 } + // Env / state + if str_eq(name, "env") { return 1 } + if str_eq(name, "state_set") { return 2 } + if str_eq(name, "state_get") { return 1 } + if str_eq(name, "state_del") { return 1 } + if str_eq(name, "state_keys") { return 0 } + // Float + if str_eq(name, "float_to_str") { return 1 } + if str_eq(name, "int_to_float") { return 1 } + if str_eq(name, "float_to_int") { return 1 } + if str_eq(name, "format_float") { return 2 } + if str_eq(name, "decimal_round") { return 2 } + if str_eq(name, "str_to_float") { return 1 } + // Math (Float) + if str_eq(name, "math_sqrt") { return 1 } + if str_eq(name, "math_log") { return 1 } + if str_eq(name, "math_ln") { return 1 } + if str_eq(name, "math_sin") { return 1 } + if str_eq(name, "math_cos") { return 1 } + if str_eq(name, "math_pi") { return 0 } + // Bool + if str_eq(name, "bool_to_str") { return 1 } + // Process + if str_eq(name, "exit_program") { return 1 } + // CGI / DHARMA + if str_eq(name, "dharma_connect") { return 1 } + if str_eq(name, "dharma_send") { return 2 } + if str_eq(name, "dharma_activate") { return 1 } + if str_eq(name, "dharma_emit") { return 2 } + if str_eq(name, "dharma_field") { return 1 } + if str_eq(name, "dharma_strengthen") { return 2 } + if str_eq(name, "dharma_relationship") { return 1 } + if str_eq(name, "dharma_peers") { return 0 } + // Engram + if str_eq(name, "engram_node") { return 3 } + if str_eq(name, "engram_node_full") { return 8 } + if str_eq(name, "engram_get_node") { return 1 } + if str_eq(name, "engram_strengthen") { return 1 } + if str_eq(name, "engram_forget") { return 1 } + if str_eq(name, "engram_node_count") { return 0 } + if str_eq(name, "engram_search") { return 2 } + if str_eq(name, "engram_scan_nodes") { return 2 } + if str_eq(name, "engram_connect") { return 4 } + if str_eq(name, "engram_edge_between") { return 2 } + if str_eq(name, "engram_neighbors") { return 1 } + if str_eq(name, "engram_neighbors_filtered") { return 3 } + if str_eq(name, "engram_edge_count") { return 0 } + if str_eq(name, "engram_activate") { return 2 } + if str_eq(name, "engram_save") { return 1 } + if str_eq(name, "engram_load") { return 1 } + if str_eq(name, "engram_get_node_json") { return 1 } + if str_eq(name, "engram_search_json") { return 2 } + if str_eq(name, "engram_scan_nodes_json") { return 2 } + if str_eq(name, "engram_neighbors_json") { return 3 } + if str_eq(name, "engram_activate_json") { return 2 } + if str_eq(name, "engram_stats_json") { return 0 } + // LLM + if str_eq(name, "llm_call") { return 2 } + if str_eq(name, "llm_call_system") { return 3 } + if str_eq(name, "llm_call_agentic") { return 4 } + if str_eq(name, "llm_vision") { return 4 } + if str_eq(name, "llm_models") { return 0 } + if str_eq(name, "llm_register_tool") { return 2 } + // Crypto + if str_eq(name, "sha256_hex") { return 1 } + if str_eq(name, "sha256_bytes") { return 1 } + if str_eq(name, "hmac_sha256_hex") { return 2 } + if str_eq(name, "hmac_sha256_bytes") { return 2 } + if str_eq(name, "base64_encode") { return 1 } + if str_eq(name, "base64_decode") { return 1 } + if str_eq(name, "base64url_encode") { return 1 } + if str_eq(name, "base64url_decode") { return 1 } + // Native VM aliases + if str_eq(name, "native_list_get") { return 2 } + if str_eq(name, "native_list_len") { return 1 } + if str_eq(name, "native_list_append") { return 2 } + if str_eq(name, "native_list_empty") { return 0 } + if str_eq(name, "native_list_clone") { return 1 } + if str_eq(name, "native_string_chars") { return 1 } + if str_eq(name, "native_int_to_str") { return 1 } + // Method-call aliases + if str_eq(name, "append") { return 2 } + if str_eq(name, "len") { return 1 } + if str_eq(name, "get") { return 2 } + if str_eq(name, "map_get") { return 2 } + if str_eq(name, "map_set") { return 3 } + // -1 sentinel: variadic / unknown / user-defined → no check. + return -1 +} + +fn arity_record_violation(fn_name: String, expected: Int, actual: Int) -> Bool { + let csv: String = state_get("__arity_violations") + if str_eq(csv, "") { let csv = "," } + // Encode as fn_name|expected|actual to recover all three at emit time. + let entry: String = fn_name + "|" + native_int_to_str(expected) + "|" + native_int_to_str(actual) + let key: String = "," + entry + "," + if str_contains(csv, key) { return true } + state_set("__arity_violations", csv + entry + ",") + return true +} + +// Validate the call's arity against the builtin table. Returns true (always) +// because cg_expr ignores the result; -1 from builtin_arity signals +// "no check possible" (variadic or user-defined). A mismatch is recorded +// and surfaced as an #error at the bottom of the generated C, so cc fails +// before it ever attempts to type-check the wrong call. +fn arity_check_call(fn_name: String, actual: Int) -> Bool { + let expected: Int = builtin_arity(fn_name) + if expected < 0 { return true } + if expected == actual { return true } + arity_record_violation(fn_name, expected, actual) + return true +} + +// Emit recorded arity violations as #error directives. +fn emit_arity_violations() -> Void { + let csv: String = state_get("__arity_violations") + if str_eq(csv, "") { return } + if str_eq(csv, ",") { return } + let n: Int = str_len(csv) + let i: Int = 1 + while i < n { + let next_comma: Int = str_index_of(str_slice(csv, i, n), ",") + if next_comma < 0 { return } + let entry: String = str_slice(csv, i, i + next_comma) + let p1: Int = str_index_of(entry, "|") + if p1 > 0 { + let fn_name: String = str_slice(entry, 0, p1) + let rest: String = str_slice(entry, p1 + 1, str_len(entry)) + let p2: Int = str_index_of(rest, "|") + if p2 > 0 { + let exp_s: String = str_slice(rest, 0, p2) + let act_s: String = str_slice(rest, p2 + 1, str_len(rest)) + emit_line("#error \"arity error: '" + fn_name + "' takes " + exp_s + " arguments, but called with " + act_s + "\"") + } + } + let i = i + next_comma + 1 + } +} + fn add_int_name(name: String) -> Bool { let csv: String = state_get("__int_names") if str_eq(csv, "") { csv = "," } @@ -1251,6 +1634,8 @@ fn codegen(stmts: [Map], source: String) -> String { state_set("__program_kind", kind) // Clear capability-violation accumulator from any prior compile. state_set("__cap_violations", "") + // Clear arity-violation accumulator from any prior compile. + state_set("__arity_violations", "") // Preamble emit_line("#include ") @@ -1276,6 +1661,40 @@ fn codegen(stmts: [Map], source: String) -> String { } emit_blank() + // Top-level `let` bindings → file-scope storage. El programs use + // top-level `let GREETING = "..."` as module constants that any + // function below should be able to read. Without this pass, a top- + // level Let only declares the name inside main()'s scope and any + // function referencing it compiles to an undefined-symbol use of + // the bare name (or, with non-static linkage, fails to link). + // + // We emit each top-level Let as `el_val_t NAME = VALUE;` at file + // scope and seed the int-name set when the binding is `: Int` so + // arithmetic/concat dispatch on the name works inside functions. + // Runtime-call initializers (e.g. `let m = el_map_new(...)`) cannot + // appear in C static initializers, so we emit a non-const slot and + // initialize it at the top of main() before any user statements run. + let has_toplevel_lets = false + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let kind: String = stmt["stmt"] + if str_eq(kind, "Let") { + let name: String = stmt["name"] + let ltype: String = stmt["type"] + if str_eq(ltype, "Int") { add_int_name(name) } + let val = stmt["value"] + let vk: String = val["expr"] + if str_eq(vk, "Int") { add_int_name(name) } + emit_line("el_val_t " + name + ";") + let has_toplevel_lets = true + } + let i = i + 1 + } + if has_toplevel_lets { + emit_blank() + } + // Function definitions let i = 0 while i < n { @@ -1286,9 +1705,11 @@ fn codegen(stmts: [Map], source: String) -> String { let i = i + 1 } - // main() - emit_line("int main(int argc, char** argv) {") - emit_line(" el_runtime_init_args(argc, argv);") + // 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. + emit_line("int main(int _argc, char** _argv) {") + emit_line(" el_runtime_init_args(_argc, _argv);") if cgi_count >= 1 { let cname: String = cgi_block["name"] let cdid: String = cgi_block["dharma_id"] @@ -1306,12 +1727,48 @@ fn codegen(stmts: [Map], source: String) -> String { let arg_eng: String = cgi_arg(ceng, has_eng) emit_line(" el_cgi_init(" + arg_name + ", " + arg_did + ", " + arg_prin + ", " + arg_net + ", " + arg_eng + ");") } + // Seed `declared` with the names of every top-level Let so that + // cg_stmt emits plain assignment (`X = ...;`) instead of a redundant + // `el_val_t X = ...;` shadowing the file-scope slot. let main_decl = native_list_empty() let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let kind: String = stmt["stmt"] + if str_eq(kind, "Let") { + let name: String = stmt["name"] + let main_decl = native_list_append(main_decl, name) + } + let i = i + 1 + } + // First pass: capture the body of `fn main()` if the source declared + // one. We've already skipped emitting it as a regular el_val_t + // function (see cg_fn early return); fold its body into C's main + // alongside top-level statements so the program actually runs. + let el_main_body = native_list_empty() + let i = 0 while i < n { let stmt = native_list_get(stmts, i) if is_fndef(stmt) { - // skip + let fn_name: String = stmt["name"] + if str_eq(fn_name, "main") { + let body = stmt["body"] + let bn: Int = native_list_len(body) + let bi: Int = 0 + while bi < bn { + let el_main_body = native_list_append(el_main_body, native_list_get(body, bi)) + let bi = bi + 1 + } + } + } + let i = i + 1 + } + + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + if is_fndef(stmt) { + // skip - fn defs already emitted above; fn main body folded later } else { if is_top_level_decl(stmt) { // skip @@ -1319,8 +1776,23 @@ fn codegen(stmts: [Map], source: String) -> String { let main_decl = cg_stmt(stmt, " ", main_decl) } } + // Release AST node after final use — each stmt is fully processed + // by this point (forward decls, fn defs, top-level lets, and now + // the main-body pass are all done). Releasing here prevents the + // accumulated AST from exhausting memory on large source files. + el_release(stmt) let i = i + 1 } + + // Fold fn main()'s body in here, after top-level statements. + let mn: Int = native_list_len(el_main_body) + let mi: Int = 0 + while mi < mn { + let mstmt = native_list_get(el_main_body, mi) + let main_decl = cg_stmt(mstmt, " ", main_decl) + let mi = mi + 1 + } + emit_line(" return 0;") emit_line("}") emit_blank() @@ -1330,6 +1802,10 @@ fn codegen(stmts: [Map], source: String) -> String { // the bottom is fine — preprocessor errors halt the build wherever // they appear. emit_cap_violations() + // Same for builtin-arity violations: cc halts on the first #error, + // so a misuse of a known builtin (wrong arg count) fails the build + // with a clear message naming the builtin and its expected arity. + emit_arity_violations() // Return empty string — output was streamed via println ""