// codegen.el — El compiler C source code generator // // Input: list of AST statement maps (from parser.el) // Output: C source printed to stdout (streamed, one line at a time) // // Each El program compiles to a single .c file that #includes el_runtime.h. // Functions map directly to C functions; top-level statements become main(). // // Entry point: fn codegen(stmts: [Map], source: String) -> String // Returns "" — output goes to stdout via println(). // // Streaming output avoids O(n²) string concatenation: each emitted line is // printed immediately rather than appended to a growing string. // ── String helpers ──────────────────────────────────────────────────────────── // Escape a C string literal (double-quotes and backslashes). fn c_escape(s: String) -> String { let chars: [String] = native_string_chars(s) let total: Int = native_list_len(chars) let out = "" let i = 0 while i < total { let ch: String = native_list_get(chars, i) if ch == "\"" { let out = out + "\\\"" } else { if ch == "\\" { let out = out + "\\\\" } else { if ch == "\n" { let out = out + "\\n" } else { if ch == "\r" { let out = out + "\\r" } else { if ch == "\t" { let out = out + "\\t" } else { let out = out + ch } } } } } let i = i + 1 } out } fn c_str_lit(s: String) -> String { "\"" + c_escape(s) + "\"" } // ── Type mapping ────────────────────────────────────────────────────────────── fn el_type_to_c(type_str: String) -> String { if type_str == "String" { return "const char*" } if type_str == "Int" { return "int64_t" } if type_str == "Bool" { return "int" } if type_str == "Float" { return "double" } if type_str == "Void" { return "void" } if type_str == "void" { return "void" } "void*" } // ── Code emission ───────────────────────────────────────────────────────────── // // emit_line/emit_blank stream output directly via println. // This avoids building a large string in memory. fn emit_line(line: String) -> Void { println(line) } fn emit_blank() -> Void { println("") } // ── Operator helpers ────────────────────────────────────────────────────────── fn binop_to_c(op: String) -> String { if op == "Plus" { return "+" } if op == "Minus" { return "-" } if op == "Star" { return "*" } if op == "Slash" { return "/" } if op == "EqEq" { return "==" } if op == "NotEq" { return "!=" } if op == "Lt" { return "<" } if op == "Gt" { return ">" } if op == "LtEq" { return "<=" } if op == "GtEq" { return ">=" } if op == "And" { return "&&" } if op == "Or" { return "||" } op } // ── Expression codegen ──────────────────────────────────────────────────────── // // cg_expr returns a C expression string (not a statement). fn cg_expr(expr: Map) -> String { let kind: String = expr["expr"] if kind == "Int" { let v: String = expr["value"] return v } if kind == "Float" { // Wrap Float literals in el_from_float() so the bit pattern is // preserved through the el_val_t (int64) slot. Without this, // implicit double→int64 conversion in C truncates `0.8` to `0` // when passed to a builtin that expects el_val_t. let v: String = expr["value"] return "el_from_float(" + v + ")" } if kind == "Str" { let v: String = expr["value"] return "EL_STR(" + c_str_lit(v) + ")" } if kind == "Bool" { let v: String = expr["value"] if v == "true" { return "1" } return "0" } if kind == "Nil" { return "EL_NULL" } if kind == "Ident" { let name: String = expr["name"] return name } if kind == "Not" { let inner = expr["inner"] let inner_c: String = cg_expr(inner) return "!" + inner_c } if kind == "Neg" { let inner = expr["inner"] let inner_c: String = cg_expr(inner) return "(-" + inner_c + ")" } if kind == "BinOp" { let op: String = expr["op"] let left = expr["left"] let right = expr["right"] let left_c: String = cg_expr(left) let right_c: String = cg_expr(right) let left_kind: String = left["expr"] let right_kind: String = right["expr"] if op == "Plus" { // If either side is a string literal, always concat if left_kind == "Str" { return "el_str_concat(" + left_c + ", " + right_c + ")" } 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) if left_kind == "Int" { let op_c: String = binop_to_c(op) return "(" + left_c + " " + op_c + " " + right_c + ")" } if right_kind == "Int" { 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 + ")" } } } } 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" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if left_kind == "BinOp" { let left_op: String = left["op"] if left_op == "Plus" { return "el_str_concat(" + left_c + ", " + right_c + ")" } } if right_kind == "BinOp" { let right_op: String = right["op"] if right_op == "Plus" { 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 + ")" } if right_kind == "Ident" { return "el_str_concat(" + left_c + ", " + right_c + ")" } } // String equality: use str_eq() when either side is a string literal or ident. // Use plain == when comparing integer literals. if op == "EqEq" { // Integer literal on either side → arithmetic comparison if left_kind == "Int" { return "(" + left_c + " == " + right_c + ")" } if right_kind == "Int" { return "(" + left_c + " == " + right_c + ")" } if left_kind == "Bool" { return "(" + left_c + " == " + right_c + ")" } if right_kind == "Bool" { return "(" + left_c + " == " + right_c + ")" } if left_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" } if left_kind == "Ident" { return "str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Ident" { return "str_eq(" + left_c + ", " + right_c + ")" } if left_kind == "Call" { return "str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Call" { return "str_eq(" + left_c + ", " + right_c + ")" } } if op == "NotEq" { if left_kind == "Int" { return "(" + left_c + " != " + right_c + ")" } if right_kind == "Int" { return "(" + left_c + " != " + right_c + ")" } if left_kind == "Bool" { return "(" + left_c + " != " + right_c + ")" } if right_kind == "Bool" { return "(" + left_c + " != " + right_c + ")" } if left_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" } if left_kind == "Ident" { return "!str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Ident" { return "!str_eq(" + left_c + ", " + right_c + ")" } if left_kind == "Call" { return "!str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Call" { return "!str_eq(" + left_c + ", " + right_c + ")" } } let op_c: String = binop_to_c(op) return "(" + left_c + " " + op_c + " " + right_c + ")" } if kind == "Call" { let func = expr["func"] let args = expr["args"] let arity: Int = native_list_len(args) let func_kind: String = func["expr"] let args_c = "" 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 i = i + 1 } if func_kind == "Ident" { let fn_name: String = func["name"] return fn_name + "(" + args_c + ")" } if func_kind == "Field" { let obj = func["object"] let field: String = func["field"] let obj_c: String = cg_expr(obj) if arity > 0 { return field + "(" + obj_c + ", " + args_c + ")" } return field + "(" + obj_c + ")" } let fn_c: String = cg_expr(func) return fn_c + "(" + args_c + ")" } if kind == "Field" { let obj = expr["object"] let field: String = expr["field"] let obj_c: String = cg_expr(obj) return "el_get_field(" + obj_c + ", " + c_str_lit(field) + ")" } if kind == "Index" { // El programs use `t["field"]` for map access and `arr[i]` for // list access. The parser emits the same Index node for both. // Dispatch at codegen time on the index expression kind: string- // literal index → map field access (`el_get_field`); anything // else → list element access (`el_list_get`). let obj = expr["object"] let idx = expr["index"] let obj_c: String = cg_expr(obj) let idx_c: String = cg_expr(idx) let idx_kind: String = idx["expr"] if str_eq(idx_kind, "Str") { return "el_get_field(" + obj_c + ", " + idx_c + ")" } return "el_list_get(" + obj_c + ", " + idx_c + ")" } if kind == "Array" { let elems = expr["elems"] let n: Int = native_list_len(elems) let items = "" 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 i = i + 1 } return "el_list_new(" + native_int_to_str(n) + ", " + items + ")" } if kind == "Map" { let pairs = expr["pairs"] let n: Int = native_list_len(pairs) let items = "" 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 i = i + 1 } return "el_map_new(" + native_int_to_str(n) + ", " + items + ")" } if kind == "Try" { let inner = expr["inner"] return cg_expr(inner) } 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)" } "EL_NULL" } // ── Variable scope tracking ─────────────────────────────────────────────────── // // El allows `let x = expr` to both declare and reassign x in the same scope. // C doesn't allow redeclaring the same name in the same block. // We track declared names in a list and emit `x = expr` (no type prefix) // when x is already declared. The declared list is passed through all // statement emitters. fn list_contains(lst: [String], s: String) -> Bool { let n: Int = native_list_len(lst) let i = 0 while i < n { let item: String = native_list_get(lst, i) if item == s { return true } let i = i + 1 } false } // ── Statement codegen ───────────────────────────────────────────────────────── // // cg_stmt emits C lines via println. declared is a list of already-declared // variable names in the current C scope; returns updated declared list. fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [String] { let kind: String = stmt["stmt"] if kind == "Let" { let name: String = stmt["name"] let val = stmt["value"] let val_c: String = cg_expr(val) // If the binding is annotated `: Int` and val is an Int literal, // register `name` in the per-function int-name set so that later // `name + ...` dispatches to arithmetic, not concat. let ltype: String = stmt["type"] if str_eq(ltype, "Int") { add_int_name(name) } let vk: String = val["expr"] if str_eq(vk, "Int") { add_int_name(name) } if list_contains(declared, name) { emit_line(indent + name + " = " + val_c + ";") return declared } else { emit_line(indent + "el_val_t " + name + " = " + val_c + ";") return native_list_append(declared, name) } } if kind == "Return" { let val = stmt["value"] let val_kind: String = val["expr"] if val_kind == "Nil" { emit_line(indent + "return 0;") } else { let val_c: String = cg_expr(val) emit_line(indent + "return " + val_c + ";") } return declared } if kind == "Expr" { let val = stmt["value"] let val_kind: String = val["expr"] if val_kind == "If" { cg_if_stmt(val, indent, declared) return declared } if val_kind == "For" { cg_for_stmt(val, indent, declared) return declared } let val_c: String = cg_expr(val) emit_line(indent + val_c + ";") return declared } if kind == "While" { let cond = stmt["cond"] let body = stmt["body"] let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "while (" + cond_c + ") {") cg_stmts(body, indent + " ", declared) emit_line(indent + "}") return declared } if kind == "For" { let item: String = stmt["item"] let list_expr = stmt["list"] let body = stmt["body"] cg_for_body(item, list_expr, body, indent, declared) return declared } if kind == "FnDef" { return declared } if kind == "TypeDef" { return declared } if kind == "EnumDef" { return declared } if kind == "Import" { return declared } declared } // Strip a single layer of surrounding parentheses from a C expression string. fn strip_outer_parens(s: String) -> String { let chars: [String] = native_string_chars(s) let n: Int = native_list_len(chars) if n < 2 { return s } let first: String = native_list_get(chars, 0) let last: String = native_list_get(chars, n - 1) if first == "(" { if last == ")" { let depth = 1 let i = 1 let balanced = true while i < n - 1 { let ch: String = native_list_get(chars, i) if ch == "(" { let depth = depth + 1 } if ch == ")" { let depth = depth - 1 if depth == 0 { let balanced = false let i = n } } 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 } } } s } fn cg_if_stmt(expr: Map, indent: String, declared: [String]) -> Void { 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 cond_c = strip_outer_parens(cond_c) emit_line(indent + "if (" + cond_c + ") {") cg_stmts(then_stmts, indent + " ", declared) if has_else { emit_line(indent + "} else {") cg_stmts(else_stmts, indent + " ", declared) } emit_line(indent + "}") } fn cg_for_body(item: String, list_expr: Map, body: [Map], indent: String, declared: [String]) -> Void { let list_c: String = cg_expr(list_expr) let idx = "_el_i" let list_tmp = "_el_lst" let len_tmp = "_el_len" emit_line(indent + "{") emit_line(indent + " el_val_t " + list_tmp + " = " + list_c + ";") emit_line(indent + " el_val_t " + len_tmp + " = el_list_len(" + list_tmp + ");") emit_line(indent + " for (el_val_t " + idx + " = 0; " + idx + " < " + len_tmp + "; " + idx + "++) {") emit_line(indent + " el_val_t " + item + " = el_list_get(" + list_tmp + ", " + idx + ");") cg_stmts(body, indent + " ", declared) emit_line(indent + " }") emit_line(indent + "}") } fn cg_for_stmt(expr: Map, indent: String, declared: [String]) -> Void { let item: String = expr["item"] let list_expr = expr["list"] let body = expr["body"] cg_for_body(item, list_expr, body, indent, declared) } fn cg_stmts(stmts: [Map], indent: String, declared: [String]) -> [String] { let n: Int = native_list_len(stmts) let i = 0 let decl = declared while i < n { let stmt = native_list_get(stmts, i) let decl = cg_stmt(stmt, indent, decl) let i = i + 1 } decl } // ── Function declaration codegen ─────────────────────────────────────────────── fn param_decl(param: Map, idx: Int) -> String { let name: String = param["name"] "el_val_t " + name } fn params_to_c(params: [Map]) -> String { let n: Int = native_list_len(params) if n == 0 { return "void" } let out = "" 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 i = i + 1 } out } // Transform a function body so that an implicit-return final expression // becomes an explicit Return. El allows the last expression in a function // body to be the return value (e.g. `fn lex(s) { ... tokens }` returns // `tokens`). Without this transform, the codegen emits the bare expression // and falls through to the trailing `return 0;`, losing the value. // // Rules: a body ending in a bare Expr whose inner expr is NOT a control- // flow construct (If/For) is rewritten so that final Expr becomes a // Return statement carrying the same value. Bodies whose final statement // is already a Return, While, For, or a non-value-producing form pass // through unchanged. fn transform_implicit_return(body: [Map]) -> [Map] { let n: Int = native_list_len(body) if n == 0 { return body } let last: Map = native_list_get(body, n - 1) let last_kind: String = last["stmt"] if last_kind == "Expr" { let val = last["value"] let val_kind: String = val["expr"] // Skip control-flow expressions used as statements if val_kind == "If" { return body } if val_kind == "For" { return body } // Replace the last bare Expr with a Return carrying the same value let new_body: [Map] = native_list_empty() let i = 0 while i < n - 1 { let new_body = native_list_append(new_body, native_list_get(body, i)) let i = i + 1 } let return_stmt: Map = { "stmt": "Return", "value": val } let new_body = native_list_append(new_body, return_stmt) return new_body } body } // Test whether `name` is currently registered as an Int-typed identifier // for the function being codegened. The set is maintained as a comma- // bounded CSV in process state; cg_fn seeds it from typed parameters, // cg_stmt extends it from typed `let` bindings. fn is_int_name(name: String) -> Bool { let csv: String = state_get("__int_names") if str_eq(csv, "") { return false } return str_contains(csv, "," + name + ",") } // Known runtime builtins that return Int. Used to dispatch arithmetic vs // string-concat on `+` when one side is a Call. New builtins must be added // here when they return Int and may participate in arithmetic. fn is_int_call(call_expr: Map) -> Bool { let func = call_expr["func"] let fk: String = func["expr"] if !str_eq(fk, "Ident") { return false } let name: String = func["name"] if str_eq(name, "str_len") { return true } if str_eq(name, "str_index_of") { return true } if str_eq(name, "str_to_int") { return true } if str_eq(name, "str_char_code") { return true } if str_eq(name, "native_list_len") { return true } if str_eq(name, "el_list_len") { return true } if str_eq(name, "len") { return true } if str_eq(name, "json_get_int") { return true } if str_eq(name, "json_array_len") { return true } if str_eq(name, "engram_node_count") { return true } if str_eq(name, "engram_edge_count") { return true } if str_eq(name, "time_now") { return true } if str_eq(name, "time_now_utc") { return true } if str_eq(name, "time_diff") { return true } if str_eq(name, "time_add") { return true } if str_eq(name, "time_from_parts") { return true } if str_eq(name, "el_abs") { return true } if str_eq(name, "el_max") { return true } if str_eq(name, "el_min") { return true } if str_eq(name, "float_to_int") { return true } return false } fn add_int_name(name: String) -> Bool { let csv: String = state_get("__int_names") if str_eq(csv, "") { csv = "," } let key: String = "," + name + "," if str_contains(csv, key) { return true } state_set("__int_names", csv + name + ",") return true } fn build_int_names_for_params(params: [Map]) -> Bool { state_set("__int_names", ",") let np: Int = native_list_len(params) let pi = 0 while pi < np { let param = native_list_get(params, pi) let pname: String = param["name"] let ptype: String = param["type"] if str_eq(ptype, "Int") { add_int_name(pname) } let pi = pi + 1 } return true } fn cg_fn(stmt: Map) -> Void { let fn_name: String = stmt["name"] // Skip El's `fn main()` — C provides its own main() for top-level stmts // and a duplicate `el_val_t main(void)` would collide with it. if fn_name == "main" { return } let params = stmt["params"] let body = stmt["body"] let ret_type: String = stmt["ret_type"] let params_c: String = params_to_c(params) // Seed the per-function int-name set so the `+` codegen can dispatch // arithmetic vs concat on type-annotated identifiers. build_int_names_for_params(params) emit_line("el_val_t " + fn_name + "(" + params_c + ") {") // Seed declared with parameter names so reassignment works let decl = native_list_empty() let np: Int = native_list_len(params) let pi = 0 while pi < np { let param = native_list_get(params, pi) let pname: String = param["name"] let decl = native_list_append(decl, pname) let pi = pi + 1 } // Lift the final bare expression into an explicit return so implicit // returns ("fn lex(s) { ... tokens }") actually return their value. // Void-returning functions skip this — wrapping `println(x)` in // `return …` is a C type error. let body_xformed = body if !str_eq(ret_type, "Void") { let body_xformed = transform_implicit_return(body) } cg_stmts(body_xformed, " ", decl) emit_line(" return 0;") emit_line("}") emit_blank() } // ── Top-level codegen ───────────────────────────────────────────────────────── fn is_fndef(stmt: Map) -> Bool { let kind: String = stmt["stmt"] if kind == "FnDef" { return true } false } fn is_top_level_decl(stmt: Map) -> Bool { let kind: String = stmt["stmt"] if kind == "TypeDef" { return true } if kind == "EnumDef" { return true } if kind == "Import" { return true } false } // ── Entry point ──────────────────────────────────────────────────────────────── fn codegen(stmts: [Map], source: String) -> String { // Preamble emit_line("#include ") emit_line("#include ") emit_line("#include \"el_runtime.h\"") emit_blank() // Forward declarations (skip `main` — C provides its own) let n: Int = native_list_len(stmts) let i = 0 while i < n { let stmt = native_list_get(stmts, i) let kind: String = stmt["stmt"] if kind == "FnDef" { let fn_name: String = stmt["name"] if !str_eq(fn_name, "main") { 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() // Function definitions let i = 0 while i < n { let stmt = native_list_get(stmts, i) if is_fndef(stmt) { cg_fn(stmt) } let i = i + 1 } // main() emit_line("int main(int argc, char** argv) {") emit_line(" el_runtime_init_args(argc, argv);") let main_decl = native_list_empty() let i = 0 while i < n { let stmt = native_list_get(stmts, i) if is_fndef(stmt) { // skip } else { if is_top_level_decl(stmt) { // skip } else { let main_decl = cg_stmt(stmt, " ", main_decl) } } let i = i + 1 } emit_line(" return 0;") emit_line("}") emit_blank() // Return empty string — output was streamed via println "" }