0fa9e749e1
Three changes that turned the runtime into something Engram-the-server
can actually run on top of.
1. engram_*_json accessors. The runtime's engram_get_node/search/scan/
neighbors/activate return ElList/ElMap; passing those through
json_stringify hit the type-erasure wall (an ElList* has no header
that distinguishes it from a string pointer). Added pre-serialized
sibling builtins:
engram_get_node_json(id) -> JSON object
engram_search_json(query, limit) -> JSON array of node objects
engram_scan_nodes_json(limit, offset)
engram_neighbors_json(node_id, max_depth, direction)
engram_activate_json(query, depth)
engram_stats_json()
Each walks the typed C structures and serializes directly, reusing
the existing engram_emit_node_json / engram_emit_edge_json helpers
from the snapshot path.
2. http_set_handler now falls back to dlsym(RTLD_DEFAULT, name) when
the named handler isn't already in the C-level registry. El programs
that define `fn handle_request(method, path, body) -> String` can
register themselves just by calling http_set_handler("handle_request").
No C glue required. Verified live on a real El server.
3. Codegen: extended int-typed dispatch on `+` to handle Calls. New
helper is_int_call recognizes a known-int-returning builtin set:
str_len, str_index_of, str_to_int, str_char_code, native_list_len,
el_list_len, len, json_get_int, json_array_len, engram_node_count,
engram_edge_count, time_now, time_now_utc, time_diff, time_add,
time_from_parts, el_abs/max/min, float_to_int. With this,
`pos + str_len(needle)` compiles to integer arithmetic instead of
string concat. The earlier limitation noted in the previous commit
(Ident + Call returning Int) is now closed.
Also: el_to_float / el_from_float moved to el_runtime.h as static
inlines so generated programs can use them. Eliminates the unused
inline definitions that were duplicating in the .c file.
Closure verified: stage1 vs stage2 byte-identical against the new
runtime. dist/platform/elc rebuilt; .prev4 preserved.
Engram server (engram/src/server.el) end-to-end:
POST /api/nodes ×3 → 3 UUIDs returned
POST /api/edges ×2 → linkage made
GET /api/stats → {"node_count":3,"edge_count":2}
GET /api/search?q=spreading&limit=5 → 1 hit, full node JSON
POST /api/activate {"query":"Hebbian","depth":3}
→ seed node @ hop 0, strength 0.8
→ 1-hop neighbor @ strength 0.392 (= 0.8 × 0.7 weight × 0.7 decay)
GET /api/neighbors/<id>?depth=2 → {node, edge, hops} triple
POST /api/save → {"ok":true,"path":"..."}
Server stays alive across all routes.
Snapshot save/load on restart still TODO — server starts with 0 nodes
even when a snapshot exists; investigation pending.
876 lines
31 KiB
EmacsLisp
876 lines
31 KiB
EmacsLisp
// 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<String, Any>], 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, Any>) -> 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<String, Any>, 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<String, Any>, 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<String, Any>, body: [Map<String, Any>], 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<String, Any>, 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<String, Any>], 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<String, Any>, idx: Int) -> String {
|
|
let name: String = param["name"]
|
|
"el_val_t " + name
|
|
}
|
|
|
|
fn params_to_c(params: [Map<String, Any>]) -> 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<String, Any>]) -> [Map<String, Any>] {
|
|
let n: Int = native_list_len(body)
|
|
if n == 0 { return body }
|
|
let last: Map<String, Any> = 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<String, Any>] = 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<String, Any> = { "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<String, Any>) -> 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<String, Any>]) -> 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<String, Any>) -> 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<String, Any>) -> Bool {
|
|
let kind: String = stmt["stmt"]
|
|
if kind == "FnDef" { return true }
|
|
false
|
|
}
|
|
|
|
fn is_top_level_decl(stmt: Map<String, Any>) -> 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<String, Any>], source: String) -> String {
|
|
// Preamble
|
|
emit_line("#include <stdint.h>")
|
|
emit_line("#include <stdlib.h>")
|
|
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
|
|
""
|
|
}
|