fix looks_like_string for empty strings and UTF-8, add cross-module includes in codegen

This commit is contained in:
Will Anderson
2026-05-03 00:27:20 -05:00
parent 3d71db4958
commit e180baf776
4 changed files with 207 additions and 100 deletions
Vendored Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+7 -2
View File
@@ -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 */
}
+200 -98
View File
@@ -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<String, Any>], 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, Any>) -> 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, Any>) -> 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 doubleint64 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, 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
// 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(<int>)`, 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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, Any>) -> 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<String, Any>], 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<String, Any>], 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, Any>) -> 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<String, Any>, 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<String, Any>, 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<String, Any>, 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<String, Any>, 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<String, Any>], indent: String, declared: [String]) -> [S
decl
}
// Function declaration codegen
// -- Function declaration codegen -----------------------------------------------
fn param_decl(param: Map<String, Any>, 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<String, Any>) -> 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<String, Any>) -> Bool {
let func = call_expr["func"]
@@ -1333,7 +1400,7 @@ fn is_duration_call(call_expr: Map<String, Any>) -> 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<String, Any>) -> 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<String, Any>) -> Bool {
@@ -1536,8 +1603,8 @@ fn is_instant_expr(expr: Map<String, Any>) -> 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<String, Any>) -> 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<String, Any>) -> 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<String, Any>) -> 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<String, Any>) -> 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<String, Any>) -> Bool {
let k: String = expr["expr"]
if str_eq(k, "Int") { return true }
@@ -1659,7 +1726,7 @@ fn is_int_expr(expr: Map<String, Any>) -> 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<String, Any>) -> 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<String, Any>) -> 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<String, Any>) -> 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<String, Any>]) -> Bool {
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
// 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<String, Any>) -> 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<String, Any>) -> Void {
emit_blank()
}
// Top-level codegen
// -- Top-level codegen ---------------------------------------------------------
fn is_fndef(stmt: Map<String, Any>) -> 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<String, Any>]) -> Bool {
false
}
// Entry point
// -- Entry point ----------------------------------------------------------------
fn codegen(stmts: [Map<String, Any>], 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<String, Any>], 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<String, Any>], source: String) -> String {
emit_line("#include <stdint.h>")
emit_line("#include <stdlib.h>")
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<String, Any>], 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<String, Any>], 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<String, Any>], 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
""
}