fix looks_like_string for empty strings and UTF-8, add cross-module includes in codegen
This commit is contained in:
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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
@@ -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 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, 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
|
||||
""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user