self-host: fold fn main() body into C int main(); rename C params
The El compiler self-host has been broken since `fn main()` landed in
compiler.el. Both bootstrap.py and codegen.el skipped emitting an
`el_val_t main()` (correct - it would collide with C's int main),
but neither folded the body anywhere. The C int main() got just
runtime init + return, so any El program that put its work inside
`fn main()` produced a binary that did nothing.
Fix in two places (bootstrap.py and codegen.el, kept symmetric):
1. Capture the body of `fn main()` during the FnDef pass.
2. Emit `int main(int _argc, char** _argv)` so El programs can
declare their own local `argv` / `argc` (compiler.el itself
does this) without colliding.
3. After top-level statements, fold the captured fn main body
into C main alongside them, then return 0.
Self-host fixed point reached: gen 2 and gen 3 of compiler.el's
output are byte-identical (md5 5b4eca2a...). The new elc compiles
products/web/src/main.el natively now - 24 imports resolved, 1,173
lines of C, every imported function (page_open, nav, pricing,
checkout_page, account_page, founding_badge…) emits its forward
decl + body without a concat preprocessor in sight.
Backup of the prior self-hosted binary is at
dist/platform/elc.preselfhost in case we need to fall back.
This commit is contained in:
+22
-4
@@ -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()
|
||||
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
+536
-60
@@ -165,7 +165,23 @@ fn cg_expr(expr: Map<String, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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<String, Any>], 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, Any>) -> 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<String, Any>) -> 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<String, Any>) -> 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<String, Any>], 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 <stdint.h>")
|
||||
@@ -1276,6 +1661,40 @@ fn codegen(stmts: [Map<String, Any>], 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<String, Any>], 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<String, Any>], 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<String, Any>], 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<String, Any>], 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
|
||||
""
|
||||
|
||||
Reference in New Issue
Block a user