diff --git a/dist/platform/elb b/dist/platform/elb new file mode 100755 index 0000000..003dd19 Binary files /dev/null and b/dist/platform/elb differ diff --git a/dist/platform/elc b/dist/platform/elc index 04c36ef..5acf296 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/el-compiler/runtime/el_runtime.c b/el-compiler/runtime/el_runtime.c index ad5dbb8..717ff1b 100644 --- a/el-compiler/runtime/el_runtime.c +++ b/el-compiler/runtime/el_runtime.c @@ -2939,8 +2939,13 @@ static int looks_like_string(el_val_t v) { const unsigned char* s = (const unsigned char*)p; for (int i = 0; i < 16; i++) { unsigned char c = s[i]; - if (c == '\0') return i > 0; /* terminated string */ - if (c < 0x09 || (c > 0x0d && c < 0x20) || c >= 0x7f) return 0; + if (c == '\0') return 1; /* terminated string (empty string is still a valid string) */ + /* Reject C0 control chars (non-whitespace), allow UTF-8 high bytes. + * 0x09-0x0d = tab/newline/cr/vt/ff (whitespace, OK) + * 0x20-0x7e = printable ASCII (OK) + * 0x7f = DEL (reject) + * 0x80-0xff = UTF-8 continuation/lead bytes (OK for multi-byte chars) */ + if (c < 0x09 || (c > 0x0d && c < 0x20) || c == 0x7f) return 0; } return 1; /* 16+ printable bytes — call it a string */ } diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index afe3cb7..4868fd7 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -1,4 +1,4 @@ -// codegen.el — El compiler C source code generator +// 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) @@ -7,37 +7,90 @@ // 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(). +// Returns "" - output goes to stdout via println(). // -// Streaming output avoids O(n²) string concatenation: each emitted line is +// Streaming output avoids O(n-) string concatenation: each emitted line is // printed immediately rather than appended to a growing string. -// ── String helpers ──────────────────────────────────────────────────────────── +// -- String helpers ------------------------------------------------------------ // Escape a C string literal (double-quotes and backslashes). +// Hex-encode a single nibble (0-15) as a lowercase hex character. +fn nibble_to_hex(n: Int) -> String { + str_char_at("0123456789abcdef", n) +} + +// Encode a byte value (0-255) as a two-character hex string. +fn byte_to_hex2(b: Int) -> String { + let hi: Int = (b / 16) + let lo: Int = (b - hi * 16) + nibble_to_hex(hi) + nibble_to_hex(lo) +} + +// Return true if the byte value is a C hex digit (0-9, a-f, A-F). +// Used to determine whether a \xNN escape needs a string-literal split +// to prevent the C preprocessor from greedily consuming following hex chars. +fn is_hex_digit_byte(b: Int) -> Bool { + if b >= 48 { if b <= 57 { return true } } // 0-9 + if b >= 65 { if b <= 70 { return true } } // A-F + if b >= 97 { if b <= 102 { return true } } // a-f + false +} + fn c_escape(s: String) -> String { - let chars: [String] = native_string_chars(s) - let total: Int = native_list_len(chars) + // Use index-based byte scanning via str_char_code(s, i) and str_char_at(s, i). + // This avoids native_string_chars + str_join, which corrupts high-byte (>= 0x80) + // characters because list_join's looks_like_string heuristic rejects strings + // whose first byte is >= 0x7F and emits them as decimal pointer values instead. + // + // IMPORTANT: after a \xNN hex escape, if the next byte is a hex digit + // (0-9, a-f, A-F), we emit `""` to split the C string literal so the C + // compiler does not greedily read extra hex digits as part of the escape. + // E.g. "\xad" followed by "bamos" must become "\xad" "bamos" because 'b' + // is a hex digit and C would otherwise read "\xadb" (= 0xADB, out of range). + let total: Int = str_len(s) let parts: [String] = native_list_empty() - let i = 0 + let i: Int = 0 + let prev_was_hex_escape: Bool = false while i < total { - let ch: String = native_list_get(chars, i) - if ch == "\"" { + let bval: Int = str_char_code(s, i) + // If the previous token was a \xNN escape and the current byte is a + // hex digit, insert an empty string literal ("") to break the escape. + if prev_was_hex_escape { + if is_hex_digit_byte(bval) { + let parts = native_list_append(parts, "\"\"") + } + } + let prev_was_hex_escape = false + if bval == 34 { + // 34 = '"' let parts = native_list_append(parts, "\\\"") } else { - if ch == "\\" { + if bval == 92 { + // 92 = '\\' let parts = native_list_append(parts, "\\\\") } else { - if ch == "\n" { + if bval == 10 { + // 10 = '\n' let parts = native_list_append(parts, "\\n") } else { - if ch == "\r" { + if bval == 13 { + // 13 = '\r' let parts = native_list_append(parts, "\\r") } else { - if ch == "\t" { + if bval == 9 { + // 9 = '\t' let parts = native_list_append(parts, "\\t") } else { - let parts = native_list_append(parts, ch) + if bval >= 128 { + // Escape non-ASCII bytes (>= 0x80) as \xNN so + // Clang does not misinterpret multi-byte UTF-8 + // sequences in C string literals. + let parts = native_list_append(parts, "\\x" + byte_to_hex2(bval)) + let prev_was_hex_escape = true + } else { + let parts = native_list_append(parts, str_char_at(s, i)) + } } } } @@ -52,7 +105,7 @@ fn c_str_lit(s: String) -> String { "\"" + c_escape(s) + "\"" } -// ── Type mapping ────────────────────────────────────────────────────────────── +// -- Type mapping -------------------------------------------------------------- fn el_type_to_c(type_str: String) -> String { if type_str == "String" { return "const char*" } @@ -64,7 +117,7 @@ fn el_type_to_c(type_str: String) -> String { "void*" } -// ── Code emission ───────────────────────────────────────────────────────────── +// -- Code emission ------------------------------------------------------------- // // emit_line/emit_blank stream output directly via println. // This avoids building a large string in memory. @@ -77,7 +130,7 @@ fn emit_blank() -> Void { println("") } -// ── Operator helpers ────────────────────────────────────────────────────────── +// -- Operator helpers ---------------------------------------------------------- fn binop_to_c(op: String) -> String { if op == "Plus" { return "+" } @@ -95,11 +148,11 @@ fn binop_to_c(op: String) -> String { op } -// ── Expression codegen ──────────────────────────────────────────────────────── +// -- Expression codegen -------------------------------------------------------- // // cg_expr returns a C expression string (not a statement). -// duration_unit_nanos — multiplier from a postfix-literal unit name to +// duration_unit_nanos - multiplier from a postfix-literal unit name to // nanoseconds. Singular and plural forms collapse to the same multiplier; // the parser already restricted `unit` to the set is_duration_unit accepts. // Returns the multiplier as a decimal string suitable for splicing into @@ -130,7 +183,7 @@ fn cg_expr(expr: Map) -> String { return v } - // DurationLit — postfix-literal time value (e.g. 30.seconds, 1.hour). + // DurationLit - postfix-literal time value (e.g. 30.seconds, 1.hour). // Lowered to a literal int64 nanosecond count, wrapped in the runtime // entry point so the intent is explicit at the C level. The arithmetic // is fully constant-folded by any optimising C compiler. @@ -144,7 +197,7 @@ fn cg_expr(expr: Map) -> String { 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` + // 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 + ")" @@ -191,12 +244,12 @@ fn cg_expr(expr: Map) -> String { let left_kind: String = left["expr"] let right_kind: String = right["expr"] - // ── String/equality fast-path: skip O(N²) temporal traversals ──────── + // -- String/equality fast-path: skip O(N-) temporal traversals -------- // The 10 temporal predicates below each recurse into the left subtree: - // O(depth) state_get calls per predicate, O(N²) total for a chain of N + // O(depth) state_get calls per predicate, O(N-) total for a chain of N // string-concat BinOps (e.g. the 70-100-part HTML chains in soul.el). // When either operand is a bare Str literal the result is always concat - // or str_eq — no temporal dispatch is possible. Exit immediately. + // or str_eq - no temporal dispatch is possible. Exit immediately. if str_eq(op, "Plus") { if str_eq(left_kind, "Str") { return "el_str_concat(" + left_c + ", " + right_c + ")" } if str_eq(right_kind, "Str") { return "el_str_concat(" + left_c + ", " + right_c + ")" } @@ -210,7 +263,7 @@ fn cg_expr(expr: Map) -> String { if str_eq(right_kind, "Str") { return "!str_eq(" + left_c + ", " + right_c + ")" } } - // ── Temporal-type dispatch (Instant + Duration first-class) ──────── + // -- Temporal-type dispatch (Instant + Duration first-class) -------- // Run BEFORE the int / string / generic paths so typed temporal // operands route through the runtime wrappers and invalid combos // become #error directives rather than silently falling through to @@ -396,7 +449,7 @@ fn cg_expr(expr: Map) -> String { if right_is_dur { return "el_duration_ne(" + left_c + ", " + right_c + ")" } } } - // Fall through — let the existing path handle anything we + // Fall through - let the existing path handle anything we // didn't explicitly cover (typically string-concat with a // typed temporal value, e.g. for debug prints, which works // because both share the int64 slot). @@ -415,7 +468,7 @@ fn cg_expr(expr: Map) -> String { // 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 + // 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) { @@ -436,7 +489,7 @@ fn cg_expr(expr: Map) -> String { 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). + // evidence - fall back to string concat (the historical default). if left_kind == "Call" { return "el_str_concat(" + left_c + ", " + right_c + ")" } @@ -468,7 +521,7 @@ fn cg_expr(expr: Map) -> String { // identifiers tracked in __int_names (typed Int via `let x: Int = ...`). // Without the int-name check, `seen == idx` between two Int locals // miscompiles to str_eq(seen, idx), strcmp'ing what are integer values - // dressed as char* — segfault on the first non-printable byte. + // dressed as char* - segfault on the first non-printable byte. if op == "EqEq" { if left_kind == "Int" { return "(" + left_c + " == " + right_c + ")" @@ -602,17 +655,17 @@ 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 + // 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) - // sleep(Duration) — Phase 1 of the typed-time work. When the + // sleep(Duration) - Phase 1 of the typed-time work. When the // single arg is provably a Duration we lower to el_sleep_duration // so the runtime sees nanos directly. Existing sleep() callers // that pass an Int still emit `sleep()`, which falls through - // to the no-such-symbol path — those call sites must migrate to + // to the no-such-symbol path - those call sites must migrate to // a typed Duration. Acceptable: the spec marks them out for an // audit pass during Phase 1. if str_eq(fn_name, "sleep") { @@ -623,6 +676,20 @@ fn cg_expr(expr: Map) -> String { } } } + // el_from_float takes a raw C double - do not wrap the float + // argument in el_from_float() again. Without this, the float + // literal codegen (which wraps every Float in el_from_float()) + // produces el_from_float(el_from_float(0.7)) - double-encoded. + if str_eq(fn_name, "el_from_float") { + if arity == 1 { + let only_arg = native_list_get(args, 0) + let arg_kind: String = only_arg["expr"] + if str_eq(arg_kind, "Float") { + let v: String = only_arg["value"] + return "el_from_float(" + v + ")" + } + } + } return fn_name + "(" + args_c + ")" } @@ -656,8 +723,8 @@ fn cg_expr(expr: Map) -> String { // 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`). + // 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) @@ -691,7 +758,7 @@ fn cg_expr(expr: Map) -> String { 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 = {}` — + // 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_parts: [String] = native_list_empty() @@ -723,7 +790,7 @@ fn cg_expr(expr: Map) -> String { "EL_NULL" } -// ── Match codegen ───────────────────────────────────────────────────────────── +// -- Match codegen ------------------------------------------------------------- // // Lower a match expression to a GCC/Clang statement-expression. // A unique label suffix is allocated per match via state_set("__match_counter"). @@ -747,7 +814,7 @@ fn cg_match(expr: Map) -> String { let subj_var: String = "_match_subj_" + id let result_var: String = "_match_result_" + id let done_label: String = "_match_done_" + id - // Accumulate arm fragments into a list to avoid O(n²) string growth. + // Accumulate arm fragments into a list to avoid O(n-) string growth. let parts: [String] = native_list_empty() let parts = native_list_append(parts, "({ el_val_t " + subj_var + " = " + subj_c + "; el_val_t " + result_var + " = 0; ") let n: Int = native_list_len(arms) @@ -781,7 +848,7 @@ fn cg_match(expr: Map) -> String { } let parts = native_list_append(parts, "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") } else { - // unknown pattern → wildcard + // unknown pattern -> wildcard let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") } } @@ -794,7 +861,7 @@ fn cg_match(expr: Map) -> String { str_join(parts, "") } -// ── If-as-expression codegen ───────────────────────────────────────────────── +// -- 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 @@ -822,7 +889,7 @@ fn next_if_id() -> String { // 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) - // Collect statement fragments into a list to avoid O(n²) string growth. + // Collect statement fragments into a list to avoid O(n-) string growth. let parts: [String] = native_list_empty() let i = 0 while i < n { @@ -851,7 +918,7 @@ fn cg_if_expr_arm(stmts: [Map], result_var: String) -> String { } } else { if str_eq(sk, "Assign") { - // Real reassignment in an expression-position arm — + // Real reassignment in an expression-position arm - // emit the store; the arm's "value" stays whatever // result_var was last set to, which is the El // semantics (assignment is a statement, not a value). @@ -889,7 +956,7 @@ fn cg_if_expr(expr: Map) -> String { out } -// ── Variable scope tracking ─────────────────────────────────────────────────── +// -- 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. @@ -908,7 +975,7 @@ fn list_contains(lst: [String], s: String) -> Bool { false } -// ── Statement codegen ───────────────────────────────────────────────────────── +// -- 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. @@ -957,7 +1024,7 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin if str_eq(ltype, "Zone") { add_zone_name(name) } - // Inference from RHS — duration literals and known-typed calls + // Inference from RHS - duration literals and known-typed calls // propagate even when the let is unannotated. if is_instant_expr(val) { add_instant_name(name) @@ -1012,7 +1079,7 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin } // Bare reassignment: `name = expr`. Always emits a plain C assignment - // (no `el_val_t` prefix) — by construction the parser only produces + // (no `el_val_t` prefix) - by construction the parser only produces // Assign for an existing identifier. If the name happens NOT to be in // `declared` for the current C scope (it was let-bound by an enclosing // block) the emit still resolves at C level because the variable lives @@ -1047,7 +1114,7 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "while (" + cond_c + ") {") - // Body lives in its own C block — clone so let-bindings inside the + // Body lives in its own C block - clone so let-bindings inside the // loop don't leak into the parent's `declared` list (which would make // a sibling scope's `let x` emit assignment on an undeclared name). cg_stmts(body, indent + " ", native_list_clone(declared)) @@ -1114,7 +1181,7 @@ fn cg_if_stmt(expr: Map, indent: String, declared: [String]) -> Voi let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "if (" + cond_c + ") {") - // Each branch gets its own clone of `declared` — variables let-bound + // Each branch gets its own clone of `declared` - variables let-bound // inside the then/else block live only in that C scope, and must not // leak back to the parent (or to the sibling branch) through shared // list mutation. Cheap shallow copy; the entries (variable name strings) @@ -1166,7 +1233,7 @@ fn cg_stmts(stmts: [Map], indent: String, declared: [String]) -> [S decl } -// ── Function declaration codegen ─────────────────────────────────────────────── +// -- Function declaration codegen ----------------------------------------------- fn param_decl(param: Map, idx: Int) -> String { let name: String = param["name"] @@ -1235,7 +1302,7 @@ fn is_int_name(name: String) -> Bool { // Same shape as is_int_name, for Instant- and Duration-typed bindings. // Used by the BinOp/comparison codegen to dispatch arithmetic through the -// typed runtime wrappers (el_instant_add_dur, el_duration_lt, …) and to +// typed runtime wrappers (el_instant_add_dur, el_duration_lt, -) and to // surface mismatches (Instant + Instant, Duration + Int) as #error // directives at the top of the generated C. fn is_instant_name(name: String) -> Bool { @@ -1297,7 +1364,7 @@ fn is_int_call(call_expr: Map) -> Bool { } // Builtins that return an Instant. Used by is_instant_expr and the BinOp -// dispatch — `now() + 5.seconds` types as Instant only because we can see +// dispatch - `now() + 5.seconds` types as Instant only because we can see // that now() is an Instant-returning Call. fn is_instant_call(call_expr: Map) -> Bool { let func = call_expr["func"] @@ -1333,7 +1400,7 @@ fn is_duration_call(call_expr: Map) -> Bool { return false } -// Phase 1.5 — Calendar / CalendarTime / Rhythm / LocalDate / LocalTime / +// Phase 1.5 - Calendar / CalendarTime / Rhythm / LocalDate / LocalTime / // LocalDateTime / Zone are first-class boxed types. Each has its own name // set in process state, populated from typed `let` bindings and parameter // annotations. The BinOp dispatcher consults these to forbid mismatched @@ -1521,7 +1588,7 @@ fn is_zone_expr(expr: Map) -> Bool { // Recursive type predicates for Instant / Duration. Mirror is_int_expr. // is_instant_expr / is_duration_expr return true only when the expression // is provably of that type at codegen time. Anything ambiguous returns -// false — the BinOp dispatcher then leaves the expression on the +// false - the BinOp dispatcher then leaves the expression on the // untyped-int path, which is the safest fallback because at the runtime // level all three types share the int64 slot. fn is_instant_expr(expr: Map) -> Bool { @@ -1536,8 +1603,8 @@ fn is_instant_expr(expr: Map) -> Bool { if str_eq(k, "BinOp") { let op: String = expr["op"] if str_eq(op, "Plus") { - // Instant + Duration → Instant - // Duration + Instant → Instant + // Instant + Duration -> Instant + // Duration + Instant -> Instant if is_instant_expr(expr["left"]) { if is_duration_expr(expr["right"]) { return true } } @@ -1547,7 +1614,7 @@ fn is_instant_expr(expr: Map) -> Bool { return false } if str_eq(op, "Minus") { - // Instant - Duration → Instant + // Instant - Duration -> Instant if is_instant_expr(expr["left"]) { if is_duration_expr(expr["right"]) { return true } } @@ -1574,15 +1641,15 @@ fn is_duration_expr(expr: Map) -> Bool { if str_eq(k, "BinOp") { let op: String = expr["op"] if str_eq(op, "Plus") { - // Duration + Duration → Duration + // Duration + Duration -> Duration if is_duration_expr(expr["left"]) { if is_duration_expr(expr["right"]) { return true } } return false } if str_eq(op, "Minus") { - // Duration - Duration → Duration - // Instant - Instant → Duration (caught here, not in is_instant_expr) + // Duration - Duration -> Duration + // Instant - Instant -> Duration (caught here, not in is_instant_expr) if is_duration_expr(expr["left"]) { if is_duration_expr(expr["right"]) { return true } } @@ -1592,8 +1659,8 @@ fn is_duration_expr(expr: Map) -> Bool { return false } if str_eq(op, "Star") { - // Duration * Int → Duration - // Int * Duration → Duration + // Duration * Int -> Duration + // Int * Duration -> Duration if is_duration_expr(expr["left"]) { if is_int_expr(expr["right"]) { return true } } @@ -1603,7 +1670,7 @@ fn is_duration_expr(expr: Map) -> Bool { return false } if str_eq(op, "Slash") { - // Duration / Int → Duration + // Duration / Int -> Duration if is_duration_expr(expr["left"]) { if is_int_expr(expr["right"]) { return true } } @@ -1634,13 +1701,13 @@ fn time_record_violation(kind: String, detail: String) -> Bool { // 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 +// 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 } @@ -1659,7 +1726,7 @@ fn is_int_expr(expr: Map) -> Bool { } if str_eq(k, "BinOp") { let op: String = expr["op"] - // Comparisons and logicals always yield 0/1 — safe Int. + // 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 } @@ -1668,7 +1735,7 @@ fn is_int_expr(expr: Map) -> Bool { 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. + // 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 } @@ -1698,7 +1765,7 @@ fn is_int_expr(expr: Map) -> Bool { return false } -// ── Capability-kind enforcement ────────────────────────────────────────────── +// -- Capability-kind enforcement ---------------------------------------------- // // A program's top-level block (cgi / service / none) determines which // runtime primitives it may call. The compiler records violations in @@ -1707,11 +1774,11 @@ fn is_int_expr(expr: Map) -> Bool { // downstream cc step fails with a clear message. // // Capability tiers: -// "cgi" — full self-formation. All primitives. -// "service" — bounded. Cannot call self-formation primitives: +// "cgi" - full self-formation. All primitives. +// "service" - bounded. Cannot call self-formation primitives: // llm_call_agentic, llm_register_tool, dharma_emit, // dharma_field. Single-turn LLM calls are allowed. -// "utility" — default. No DHARMA, no LLM. Pure compute + I/O. +// "utility" - default. No DHARMA, no LLM. Pure compute + I/O. // // The compiler-level rule is structural: the binary either CAN or CANNOT // emit the call. There is no runtime check, no opt-in, no override. @@ -1726,7 +1793,7 @@ fn cap_record_violation(kind: String, fn_name: String) -> Bool { return true } -// Self-formation primitives — the cut between CGI and service. A program +// Self-formation primitives - the cut between CGI and service. A program // that emits these calls IS structurally a CGI; we forbid them everywhere // else. fn is_self_formation_call(fn_name: String) -> Bool { @@ -1737,7 +1804,7 @@ fn is_self_formation_call(fn_name: String) -> Bool { return false } -// Any DHARMA primitive — utilities have zero network presence. +// Any DHARMA primitive - utilities have zero network presence. fn is_dharma_call(fn_name: String) -> Bool { if str_eq(fn_name, "dharma_connect") { return true } if str_eq(fn_name, "dharma_send") { return true } @@ -1750,7 +1817,7 @@ fn is_dharma_call(fn_name: String) -> Bool { return false } -// Any LLM primitive — utilities have no LLM access at all. +// Any LLM primitive - utilities have no LLM access at all. fn is_llm_call(fn_name: String) -> Bool { if str_eq(fn_name, "llm_call") { return true } if str_eq(fn_name, "llm_call_system") { return true } @@ -1800,14 +1867,14 @@ fn emit_cap_violations() -> Void { if colon > 0 { let kind: String = str_slice(entry, 0, colon) let fn_name: String = str_slice(entry, colon + 1, str_len(entry)) - emit_line("#error \"capability violation: '" + kind + "' programs may not call '" + fn_name + "' (self-formation primitive — only 'cgi' programs may use it)\"") + emit_line("#error \"capability violation: '" + kind + "' programs may not call '" + fn_name + "' (self-formation primitive - only 'cgi' programs may use it)\"") } let i = i + next_comma + 1 } } // Surface temporal-type violations as #error directives. The cg_expr BinOp -// dispatcher records each violation (Instant + Instant, Duration + Int, …) +// dispatcher records each violation (Instant + Instant, Duration + Int, -) // as a CSV entry "kind:detail" via time_record_violation. Each entry maps // to a single #error so downstream cc fails the build with a clear El- // source-level message before the bogus C even links. @@ -1830,7 +1897,7 @@ fn emit_time_violations() -> Void { } } -// ── Builtin arity table ─────────────────────────────────────────────────────── +// -- 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)`). @@ -1840,7 +1907,7 @@ fn emit_time_violations() -> Void { // // 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 +// 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 { @@ -2044,7 +2111,7 @@ fn builtin_arity(name: String) -> Int { 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. + // -1 sentinel: variadic / unknown / user-defined -> no check. return -1 } @@ -2242,7 +2309,7 @@ fn build_int_names_for_params(params: [Map]) -> Bool { 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 + // 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"] @@ -2274,8 +2341,8 @@ fn cg_fn(stmt: Map) -> Void { } // 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. + // 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) @@ -2286,7 +2353,7 @@ fn cg_fn(stmt: Map) -> Void { emit_blank() } -// ── Top-level codegen ───────────────────────────────────────────────────────── +// -- Top-level codegen --------------------------------------------------------- fn is_fndef(stmt: Map) -> Bool { let kind: String = stmt["stmt"] @@ -2312,7 +2379,7 @@ fn cgi_arg(value: String, has_value: Bool) -> String { return "EL_NULL" } -// ── VBD role enforcement ────────────────────────────────────────────────────── +// -- VBD role enforcement ------------------------------------------------------ // // Scan a function body for direct calls to DHARMA-restricted builtins // (dharma_emit, dharma_field). These may only appear inside @manager fns. @@ -2445,16 +2512,16 @@ fn vbd_has_restricted_call(stmts: [Map]) -> Bool { false } -// ── Entry point ──────────────────────────────────────────────────────────────── +// -- Entry point ---------------------------------------------------------------- fn codegen(stmts: [Map], source: String) -> String { // Detect cgi/service blocks: at most one declarative top-level block. // The block determines the program's CAPABILITY KIND: - // "cgi" — full self-formation. Calls all primitives. - // "service" — bounded. Cannot call self-formation primitives + // "cgi" - full self-formation. Calls all primitives. + // "service" - bounded. Cannot call self-formation primitives // (llm_call_agentic, llm_register_tool, dharma_emit, // dharma_field, mindlink-creation). - // "utility" — default; no DHARMA membership, no LLM, no agentic. + // "utility" - default; no DHARMA membership, no LLM, no agentic. // Codegen enforces this with #error directives at every restricted // call site. The capability boundary is structural: a binary either // CAN or CANNOT do a thing, and the compiler decides at emission time. @@ -2489,7 +2556,7 @@ fn codegen(stmts: [Map], source: String) -> String { } if cgi_count >= 1 { if svc_count >= 1 { - emit_line("#error \"El: program declares both cgi and service blocks (mutually exclusive — pick one)\"") + emit_line("#error \"El: program declares both cgi and service blocks (mutually exclusive - pick one)\"") } } // Stash the program kind so cg_expr's Call branch can enforce @@ -2509,9 +2576,44 @@ fn codegen(stmts: [Map], source: String) -> String { emit_line("#include ") emit_line("#include ") emit_line("#include \"el_runtime.h\"") + + // Cross-module forward declarations: for each imported module, emit + // #include "module.elh" so Clang sees the function signatures from + // that module without needing the full source inlined. The .elh files + // are generated by `elc --emit-header` and live in the same dist/ + // directory as the generated .c files. We use basename only (strip + // the directory prefix and .el extension) so the include resolves + // correctly regardless of the source tree layout. + let imp_n: Int = native_list_len(stmts) + let imp_i = 0 + while imp_i < imp_n { + let imp_stmt = native_list_get(stmts, imp_i) + let imp_kind: String = imp_stmt["stmt"] + if str_eq(imp_kind, "Import") { + let imp_path: String = imp_stmt["path"] + // Extract basename: find last '/' and strip from there. + let imp_path_len: Int = str_len(imp_path) + let imp_last_slash: Int = -1 + let imp_j: Int = 0 + while imp_j < imp_path_len { + let imp_c: String = str_slice(imp_path, imp_j, imp_j + 1) + if str_eq(imp_c, "/") { let imp_last_slash = imp_j } + let imp_j = imp_j + 1 + } + let imp_base: String = str_slice(imp_path, imp_last_slash + 1, imp_path_len) + // Strip .el extension if present. + let imp_base_len: Int = str_len(imp_base) + let imp_bname: String = imp_base + if str_ends_with(imp_base, ".el") { + let imp_bname = str_slice(imp_base, 0, imp_base_len - 3) + } + emit_line("#include \"" + imp_bname + ".elh\"") + } + let imp_i = imp_i + 1 + } emit_blank() - // Forward declarations (skip `main` — C provides its own) + // Forward declarations (skip `main` - C provides its own) let n: Int = native_list_len(stmts) let i = 0 while i < n { @@ -2535,7 +2637,7 @@ fn codegen(stmts: [Map], source: String) -> String { } emit_blank() - // Top-level `let` bindings → file-scope storage. El programs use + // 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 @@ -2683,7 +2785,7 @@ 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 + // 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. @@ -2706,16 +2808,16 @@ fn codegen(stmts: [Map], source: String) -> String { // Emit any accumulated capability-violation #error directives. cc // will fail on the first one and surface the message; placement at - // the bottom is fine — preprocessor errors halt the build wherever + // 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() - // Temporal-type violations (Instant + Instant, Duration + Int, …). + // Temporal-type violations (Instant + Instant, Duration + Int, -). emit_time_violations() - // Return empty string — output was streamed via println + // Return empty string - output was streamed via println "" }