af480f6266
Replaces the need for product-level denylist sanitizers. Small state-machine parser; tag-and-attribute allowlist passed as JSON; URL scheme validation on href/src attrs (http, https, mailto, fragment, relative); whole-subtree drop for script/style/iframe/ object/embed/form (plus rarer media containers). No comment- wrapping (was fragile to comment-injection bypass via a literal --> inside an attacker-supplied attribute value). Also picks up the codegen and parser changes for first-class Instant/Duration types (postfix-literal time values, typed binop dispatch) that were sitting in tree alongside this work. Test corpus at tests/html_sanitizer/ covers the live attacker probes (script, iframe, form, javascript:, about:, data:, img onerror, onclick) plus structural attacks (comment-injection bypass, tab-in-scheme bypass, encoded payloads, malformed input, empty input, plain text). 29 cases, all green. Self-host fixed point holds at 5720 lines via the canonical el-compiler/src/compiler.el entry. Snapshot tagged at dist/platform/elc.20260502-1249-self-host. Backlog: bl-dc55ae07
2276 lines
89 KiB
EmacsLisp
2276 lines
89 KiB
EmacsLisp
// codegen.el — El compiler C source code generator
|
|
//
|
|
// Input: list of AST statement maps (from parser.el)
|
|
// Output: C source printed to stdout (streamed, one line at a time)
|
|
//
|
|
// Each El program compiles to a single .c file that #includes el_runtime.h.
|
|
// Functions map directly to C functions; top-level statements become main().
|
|
//
|
|
// Entry point: fn codegen(stmts: [Map<String, Any>], source: String) -> String
|
|
// Returns "" — output goes to stdout via println().
|
|
//
|
|
// Streaming output avoids O(n²) string concatenation: each emitted line is
|
|
// printed immediately rather than appended to a growing string.
|
|
|
|
// ── String helpers ────────────────────────────────────────────────────────────
|
|
|
|
// Escape a C string literal (double-quotes and backslashes).
|
|
fn c_escape(s: String) -> String {
|
|
let chars: [String] = native_string_chars(s)
|
|
let total: Int = native_list_len(chars)
|
|
let out = ""
|
|
let i = 0
|
|
while i < total {
|
|
let ch: String = native_list_get(chars, i)
|
|
if ch == "\"" {
|
|
let out = out + "\\\""
|
|
} else {
|
|
if ch == "\\" {
|
|
let out = out + "\\\\"
|
|
} else {
|
|
if ch == "\n" {
|
|
let out = out + "\\n"
|
|
} else {
|
|
if ch == "\r" {
|
|
let out = out + "\\r"
|
|
} else {
|
|
if ch == "\t" {
|
|
let out = out + "\\t"
|
|
} else {
|
|
let out = out + ch
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
out
|
|
}
|
|
|
|
fn c_str_lit(s: String) -> String {
|
|
"\"" + c_escape(s) + "\""
|
|
}
|
|
|
|
// ── Type mapping ──────────────────────────────────────────────────────────────
|
|
|
|
fn el_type_to_c(type_str: String) -> String {
|
|
if type_str == "String" { return "const char*" }
|
|
if type_str == "Int" { return "int64_t" }
|
|
if type_str == "Bool" { return "int" }
|
|
if type_str == "Float" { return "double" }
|
|
if type_str == "Void" { return "void" }
|
|
if type_str == "void" { return "void" }
|
|
"void*"
|
|
}
|
|
|
|
// ── Code emission ─────────────────────────────────────────────────────────────
|
|
//
|
|
// emit_line/emit_blank stream output directly via println.
|
|
// This avoids building a large string in memory.
|
|
|
|
fn emit_line(line: String) -> Void {
|
|
println(line)
|
|
}
|
|
|
|
fn emit_blank() -> Void {
|
|
println("")
|
|
}
|
|
|
|
// ── Operator helpers ──────────────────────────────────────────────────────────
|
|
|
|
fn binop_to_c(op: String) -> String {
|
|
if op == "Plus" { return "+" }
|
|
if op == "Minus" { return "-" }
|
|
if op == "Star" { return "*" }
|
|
if op == "Slash" { return "/" }
|
|
if op == "EqEq" { return "==" }
|
|
if op == "NotEq" { return "!=" }
|
|
if op == "Lt" { return "<" }
|
|
if op == "Gt" { return ">" }
|
|
if op == "LtEq" { return "<=" }
|
|
if op == "GtEq" { return ">=" }
|
|
if op == "And" { return "&&" }
|
|
if op == "Or" { return "||" }
|
|
op
|
|
}
|
|
|
|
// ── Expression codegen ────────────────────────────────────────────────────────
|
|
//
|
|
// cg_expr returns a C expression string (not a statement).
|
|
|
|
// 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
|
|
// the generated C as a literal int64 expression.
|
|
fn duration_unit_nanos(unit: String) -> String {
|
|
if str_eq(unit, "nano") { return "1LL" }
|
|
if str_eq(unit, "nanos") { return "1LL" }
|
|
if str_eq(unit, "milli") { return "1000000LL" }
|
|
if str_eq(unit, "millis") { return "1000000LL" }
|
|
if str_eq(unit, "millisecond") { return "1000000LL" }
|
|
if str_eq(unit, "milliseconds") { return "1000000LL" }
|
|
if str_eq(unit, "second") { return "1000000000LL" }
|
|
if str_eq(unit, "seconds") { return "1000000000LL" }
|
|
if str_eq(unit, "minute") { return "60000000000LL" }
|
|
if str_eq(unit, "minutes") { return "60000000000LL" }
|
|
if str_eq(unit, "hour") { return "3600000000000LL" }
|
|
if str_eq(unit, "hours") { return "3600000000000LL" }
|
|
if str_eq(unit, "day") { return "86400000000000LL" }
|
|
if str_eq(unit, "days") { return "86400000000000LL" }
|
|
"1LL"
|
|
}
|
|
|
|
fn cg_expr(expr: Map<String, Any>) -> String {
|
|
let kind: String = expr["expr"]
|
|
|
|
if kind == "Int" {
|
|
let v: String = expr["value"]
|
|
return v
|
|
}
|
|
|
|
// 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.
|
|
if kind == "DurationLit" {
|
|
let count: String = expr["count"]
|
|
let unit: String = expr["unit"]
|
|
let mult: String = duration_unit_nanos(unit)
|
|
return "el_duration_from_nanos((el_val_t)(" + count + "LL * " + mult + "))"
|
|
}
|
|
|
|
if kind == "Float" {
|
|
// Wrap Float literals in el_from_float() so the bit pattern is
|
|
// preserved through the el_val_t (int64) slot. Without this,
|
|
// implicit double→int64 conversion in C truncates `0.8` to `0`
|
|
// when passed to a builtin that expects el_val_t.
|
|
let v: String = expr["value"]
|
|
return "el_from_float(" + v + ")"
|
|
}
|
|
|
|
if kind == "Str" {
|
|
let v: String = expr["value"]
|
|
return "EL_STR(" + c_str_lit(v) + ")"
|
|
}
|
|
|
|
if kind == "Bool" {
|
|
let v: String = expr["value"]
|
|
if v == "true" { return "1" }
|
|
return "0"
|
|
}
|
|
|
|
if kind == "Nil" {
|
|
return "EL_NULL"
|
|
}
|
|
|
|
if kind == "Ident" {
|
|
let name: String = expr["name"]
|
|
return name
|
|
}
|
|
|
|
if kind == "Not" {
|
|
let inner = expr["inner"]
|
|
let inner_c: String = cg_expr(inner)
|
|
return "!" + inner_c
|
|
}
|
|
|
|
if kind == "Neg" {
|
|
let inner = expr["inner"]
|
|
let inner_c: String = cg_expr(inner)
|
|
return "(-" + inner_c + ")"
|
|
}
|
|
|
|
if kind == "BinOp" {
|
|
let op: String = expr["op"]
|
|
let left = expr["left"]
|
|
let right = expr["right"]
|
|
let left_c: String = cg_expr(left)
|
|
let right_c: String = cg_expr(right)
|
|
let left_kind: String = left["expr"]
|
|
let right_kind: String = right["expr"]
|
|
|
|
// ── 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
|
|
// raw int arithmetic. The wrappers are no-op casts at the C level
|
|
// but make the intent explicit and centralise future changes (e.g.
|
|
// saturating arithmetic, overflow guards).
|
|
let left_is_inst: Bool = is_instant_expr(left)
|
|
let right_is_inst: Bool = is_instant_expr(right)
|
|
let left_is_dur: Bool = is_duration_expr(left)
|
|
let right_is_dur: Bool = is_duration_expr(right)
|
|
let any_temporal: Bool = false
|
|
if left_is_inst { let any_temporal = true }
|
|
if right_is_inst { let any_temporal = true }
|
|
if left_is_dur { let any_temporal = true }
|
|
if right_is_dur { let any_temporal = true }
|
|
if any_temporal {
|
|
if op == "Plus" {
|
|
if left_is_inst {
|
|
if right_is_dur {
|
|
return "el_instant_add_dur(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_is_inst {
|
|
time_record_violation("instant_plus_instant", "Instant + Instant is not allowed")
|
|
return "0 /* TIME_TYPE_ERROR: Instant + Instant */"
|
|
}
|
|
}
|
|
if left_is_dur {
|
|
if right_is_inst {
|
|
return "el_instant_add_dur(" + right_c + ", " + left_c + ")"
|
|
}
|
|
if right_is_dur {
|
|
return "el_duration_add(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if is_int_expr(right) {
|
|
time_record_violation("duration_plus_int", "Duration + Int is not allowed (use duration_seconds(n) or N.seconds)")
|
|
return "0 /* TIME_TYPE_ERROR: Duration + Int */"
|
|
}
|
|
}
|
|
if right_is_dur {
|
|
if is_int_expr(left) {
|
|
time_record_violation("duration_plus_int", "Int + Duration is not allowed")
|
|
return "0 /* TIME_TYPE_ERROR: Int + Duration */"
|
|
}
|
|
}
|
|
}
|
|
if op == "Minus" {
|
|
if left_is_inst {
|
|
if right_is_dur {
|
|
return "el_instant_sub_dur(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_is_inst {
|
|
return "el_instant_diff(" + left_c + ", " + right_c + ")"
|
|
}
|
|
}
|
|
if left_is_dur {
|
|
if right_is_dur {
|
|
return "el_duration_sub(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if is_int_expr(right) {
|
|
time_record_violation("duration_minus_int", "Duration - Int is not allowed")
|
|
return "0 /* TIME_TYPE_ERROR: Duration - Int */"
|
|
}
|
|
}
|
|
}
|
|
if op == "Star" {
|
|
if left_is_dur {
|
|
if is_int_expr(right) {
|
|
return "el_duration_scale(" + left_c + ", " + right_c + ")"
|
|
}
|
|
}
|
|
if right_is_dur {
|
|
if is_int_expr(left) {
|
|
return "el_duration_scale(" + right_c + ", " + left_c + ")"
|
|
}
|
|
}
|
|
}
|
|
if op == "Slash" {
|
|
if left_is_dur {
|
|
if is_int_expr(right) {
|
|
return "el_duration_div(" + left_c + ", " + right_c + ")"
|
|
}
|
|
}
|
|
}
|
|
// Comparisons. Cross-type comparisons are forbidden.
|
|
if op == "Lt" {
|
|
if left_is_inst {
|
|
if right_is_inst { return "el_instant_lt(" + left_c + ", " + right_c + ")" }
|
|
if right_is_dur {
|
|
time_record_violation("instant_cmp_duration", "Instant < Duration is not allowed")
|
|
return "0 /* TIME_TYPE_ERROR: Instant < Duration */"
|
|
}
|
|
}
|
|
if left_is_dur {
|
|
if right_is_dur { return "el_duration_lt(" + left_c + ", " + right_c + ")" }
|
|
if right_is_inst {
|
|
time_record_violation("duration_cmp_instant", "Duration < Instant is not allowed")
|
|
return "0 /* TIME_TYPE_ERROR: Duration < Instant */"
|
|
}
|
|
}
|
|
}
|
|
if op == "LtEq" {
|
|
if left_is_inst {
|
|
if right_is_inst { return "el_instant_le(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
if left_is_dur {
|
|
if right_is_dur { return "el_duration_le(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
}
|
|
if op == "Gt" {
|
|
if left_is_inst {
|
|
if right_is_inst { return "el_instant_gt(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
if left_is_dur {
|
|
if right_is_dur { return "el_duration_gt(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
}
|
|
if op == "GtEq" {
|
|
if left_is_inst {
|
|
if right_is_inst { return "el_instant_ge(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
if left_is_dur {
|
|
if right_is_dur { return "el_duration_ge(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
}
|
|
if op == "EqEq" {
|
|
if left_is_inst {
|
|
if right_is_inst { return "el_instant_eq(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
if left_is_dur {
|
|
if right_is_dur { return "el_duration_eq(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
}
|
|
if op == "NotEq" {
|
|
if left_is_inst {
|
|
if right_is_inst { return "el_instant_ne(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
if left_is_dur {
|
|
if right_is_dur { return "el_duration_ne(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
}
|
|
// 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).
|
|
}
|
|
|
|
if op == "Plus" {
|
|
// If either side is a string literal, always concat
|
|
if left_kind == "Str" {
|
|
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_kind == "Str" {
|
|
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
|
}
|
|
// Type-driven dispatch via recursive is_int_expr: any expression
|
|
// whose value is provably Int (literal, typed Ident, known-Int
|
|
// builtin, or BinOp arithmetic over Ints) participates in
|
|
// arithmetic, not string concat. Recursion into BinOp lets
|
|
// `a + b + c` (chained Int adds) and `acc * 16 + d` route to
|
|
// arithmetic instead of falling to el_str_concat — both sides
|
|
// are Int so the outer `+` is too.
|
|
if is_int_expr(left) {
|
|
if is_int_expr(right) {
|
|
let op_c: String = binop_to_c(op)
|
|
return "(" + left_c + " " + op_c + " " + right_c + ")"
|
|
}
|
|
}
|
|
// Mixed cases: at least one side is provably Int but the other
|
|
// is not provably anything. Historical heuristic biases to
|
|
// arithmetic when a literal Int is present (preserves prior
|
|
// behaviour for `pos + 1` where `pos` is an untyped param).
|
|
if left_kind == "Int" {
|
|
let op_c: String = binop_to_c(op)
|
|
return "(" + left_c + " " + op_c + " " + right_c + ")"
|
|
}
|
|
if right_kind == "Int" {
|
|
let op_c: String = binop_to_c(op)
|
|
return "(" + left_c + " " + op_c + " " + right_c + ")"
|
|
}
|
|
// Otherwise: BinOp(+) with a Call/Ident side without int-typed
|
|
// evidence — fall back to string concat (the historical default).
|
|
if left_kind == "Call" {
|
|
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_kind == "Call" {
|
|
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if left_kind == "BinOp" {
|
|
let left_op: String = left["op"]
|
|
if left_op == "Plus" {
|
|
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
|
}
|
|
}
|
|
if right_kind == "BinOp" {
|
|
let right_op: String = right["op"]
|
|
if right_op == "Plus" {
|
|
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
|
}
|
|
}
|
|
if left_kind == "Ident" {
|
|
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_kind == "Ident" {
|
|
return "el_str_concat(" + left_c + ", " + right_c + ")"
|
|
}
|
|
}
|
|
|
|
// String equality: use str_eq() when either side is a string literal or ident.
|
|
// Use plain == when comparing integer literals OR when both sides are
|
|
// 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.
|
|
if op == "EqEq" {
|
|
if left_kind == "Int" {
|
|
return "(" + left_c + " == " + right_c + ")"
|
|
}
|
|
if right_kind == "Int" {
|
|
return "(" + left_c + " == " + right_c + ")"
|
|
}
|
|
if left_kind == "Bool" {
|
|
return "(" + left_c + " == " + right_c + ")"
|
|
}
|
|
if right_kind == "Bool" {
|
|
return "(" + left_c + " == " + right_c + ")"
|
|
}
|
|
if left_kind == "Ident" {
|
|
if right_kind == "Ident" {
|
|
let lname: String = left["name"]
|
|
let rname: String = right["name"]
|
|
if is_int_name(lname) {
|
|
if is_int_name(rname) {
|
|
return "(" + left_c + " == " + right_c + ")"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Extend int-equality to mixed Ident/BinOp cases: `i == n - 1`
|
|
// where the left is an int-name Ident and the right is an
|
|
// arithmetic BinOp (or vice-versa). Without this check the
|
|
// fallthrough to str_eq produces str_eq(int_value, int_value)
|
|
// which reads the integer as a char* and segfaults.
|
|
if is_int_expr(left) {
|
|
if is_int_expr(right) {
|
|
return "(" + left_c + " == " + right_c + ")"
|
|
}
|
|
}
|
|
if left_kind == "Str" {
|
|
return "str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_kind == "Str" {
|
|
return "str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if left_kind == "Ident" {
|
|
return "str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_kind == "Ident" {
|
|
return "str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if left_kind == "Call" {
|
|
return "str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_kind == "Call" {
|
|
return "str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
}
|
|
|
|
if op == "NotEq" {
|
|
if left_kind == "Int" {
|
|
return "(" + left_c + " != " + right_c + ")"
|
|
}
|
|
if right_kind == "Int" {
|
|
return "(" + left_c + " != " + right_c + ")"
|
|
}
|
|
if left_kind == "Bool" {
|
|
return "(" + left_c + " != " + right_c + ")"
|
|
}
|
|
if right_kind == "Bool" {
|
|
return "(" + left_c + " != " + right_c + ")"
|
|
}
|
|
if left_kind == "Ident" {
|
|
if right_kind == "Ident" {
|
|
let lname: String = left["name"]
|
|
let rname: String = right["name"]
|
|
if is_int_name(lname) {
|
|
if is_int_name(rname) {
|
|
return "(" + left_c + " != " + right_c + ")"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Same mixed Ident/BinOp fix as EqEq: use is_int_expr to detect
|
|
// integer-typed operands before falling through to !str_eq.
|
|
if is_int_expr(left) {
|
|
if is_int_expr(right) {
|
|
return "(" + left_c + " != " + right_c + ")"
|
|
}
|
|
}
|
|
if left_kind == "Str" {
|
|
return "!str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_kind == "Str" {
|
|
return "!str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if left_kind == "Ident" {
|
|
return "!str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_kind == "Ident" {
|
|
return "!str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if left_kind == "Call" {
|
|
return "!str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
if right_kind == "Call" {
|
|
return "!str_eq(" + left_c + ", " + right_c + ")"
|
|
}
|
|
}
|
|
|
|
let op_c: String = binop_to_c(op)
|
|
return "(" + left_c + " " + op_c + " " + right_c + ")"
|
|
}
|
|
|
|
if kind == "Call" {
|
|
let func = expr["func"]
|
|
let args = expr["args"]
|
|
let arity: Int = native_list_len(args)
|
|
let func_kind: String = func["expr"]
|
|
|
|
let args_c = ""
|
|
let i = 0
|
|
while i < arity {
|
|
let arg = native_list_get(args, i)
|
|
let arg_c: String = cg_expr(arg)
|
|
if i > 0 {
|
|
let args_c = args_c + ", "
|
|
}
|
|
let args_c = args_c + arg_c
|
|
let i = i + 1
|
|
}
|
|
|
|
if func_kind == "Ident" {
|
|
let fn_name: String = func["name"]
|
|
// Capability-kind enforcement: services can't call
|
|
// self-formation primitives; utilities can't call any
|
|
// DHARMA or LLM primitives. cap_check_call records
|
|
// violations to be emitted as #error directives at the
|
|
// top of the generated C, so cc fails with a clear msg.
|
|
cap_check_call(fn_name)
|
|
// Arity check against the builtin table — refuse, with a clear
|
|
// El-source message, when a known builtin gets the wrong arg
|
|
// count (e.g. `http_serve(port)` instead of `http_serve(port,
|
|
// handler)`). User-defined fns and variadic builtins pass
|
|
// through (builtin_arity returns -1).
|
|
arity_check_call(fn_name, arity)
|
|
// 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
|
|
// a typed Duration. Acceptable: the spec marks them out for an
|
|
// audit pass during Phase 1.
|
|
if str_eq(fn_name, "sleep") {
|
|
if arity == 1 {
|
|
let only_arg = native_list_get(args, 0)
|
|
if is_duration_expr(only_arg) {
|
|
return "el_sleep_duration(" + args_c + ")"
|
|
}
|
|
}
|
|
}
|
|
return fn_name + "(" + args_c + ")"
|
|
}
|
|
|
|
if func_kind == "Field" {
|
|
let obj = func["object"]
|
|
let field: String = func["field"]
|
|
let obj_c: String = cg_expr(obj)
|
|
if arity > 0 {
|
|
return field + "(" + obj_c + ", " + args_c + ")"
|
|
}
|
|
return field + "(" + obj_c + ")"
|
|
}
|
|
|
|
let fn_c: String = cg_expr(func)
|
|
return fn_c + "(" + args_c + ")"
|
|
}
|
|
|
|
if kind == "Field" {
|
|
let obj = expr["object"]
|
|
let field: String = expr["field"]
|
|
let obj_c: String = cg_expr(obj)
|
|
// el_get_field takes el_val_t for both args, so the field name
|
|
// string literal must be wrapped in EL_STR(). Without the wrap
|
|
// the C compiler treats the bare const char* as an int64 (warns
|
|
// -Wint-conversion) and the runtime reads gibberish at the address
|
|
// when looking up the key.
|
|
return "el_get_field(" + obj_c + ", EL_STR(" + c_str_lit(field) + "))"
|
|
}
|
|
|
|
if kind == "Index" {
|
|
// El programs use `t["field"]` for map access and `arr[i]` for
|
|
// list access. The parser emits the same Index node for both.
|
|
// Dispatch at codegen time on the index expression kind: string-
|
|
// literal index → map field access (`el_get_field`); anything
|
|
// else → list element access (`el_list_get`).
|
|
let obj = expr["object"]
|
|
let idx = expr["index"]
|
|
let obj_c: String = cg_expr(obj)
|
|
let idx_c: String = cg_expr(idx)
|
|
let idx_kind: String = idx["expr"]
|
|
if str_eq(idx_kind, "Str") {
|
|
return "el_get_field(" + obj_c + ", " + idx_c + ")"
|
|
}
|
|
return "el_list_get(" + obj_c + ", " + idx_c + ")"
|
|
}
|
|
|
|
if kind == "Array" {
|
|
let elems = expr["elems"]
|
|
let n: Int = native_list_len(elems)
|
|
// Empty literal: el_list_new(0, ) generates malformed C (trailing
|
|
// comma in a varargs call). Emit el_list_empty() directly.
|
|
if n == 0 { return "el_list_empty()" }
|
|
let items = ""
|
|
let i = 0
|
|
while i < n {
|
|
let elem = native_list_get(elems, i)
|
|
let elem_c: String = cg_expr(elem)
|
|
if i > 0 {
|
|
let items = items + ", "
|
|
}
|
|
let items = items + elem_c
|
|
let i = i + 1
|
|
}
|
|
return "el_list_new(" + native_int_to_str(n) + ", " + items + ")"
|
|
}
|
|
|
|
if kind == "Map" {
|
|
let pairs = expr["pairs"]
|
|
let n: Int = native_list_len(pairs)
|
|
// Empty literal: `el_map_new(0, )` is malformed C (trailing comma in
|
|
// a varargs call). Emit `el_map_new(0)` directly so empty-map
|
|
// shadowing inside for/while/if bodies — `let acc: Map = {}` —
|
|
// doesn't fail downstream cc with parse errors.
|
|
if n == 0 { return "el_map_new(0)" }
|
|
let items = ""
|
|
let i = 0
|
|
while i < n {
|
|
let pair = native_list_get(pairs, i)
|
|
let key: String = pair["key"]
|
|
let val = pair["value"]
|
|
let val_c: String = cg_expr(val)
|
|
if i > 0 {
|
|
let items = items + ", "
|
|
}
|
|
let items = items + c_str_lit(key) + ", " + val_c
|
|
let i = i + 1
|
|
}
|
|
return "el_map_new(" + native_int_to_str(n) + ", " + items + ")"
|
|
}
|
|
|
|
if kind == "Try" {
|
|
let inner = expr["inner"]
|
|
return cg_expr(inner)
|
|
}
|
|
|
|
if kind == "If" {
|
|
return cg_if_expr(expr)
|
|
}
|
|
|
|
if kind == "Match" {
|
|
return cg_match(expr)
|
|
}
|
|
|
|
"EL_NULL"
|
|
}
|
|
|
|
// ── 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").
|
|
|
|
fn next_match_id() -> String {
|
|
let csv: String = state_get("__match_counter")
|
|
let n = 0
|
|
if !str_eq(csv, "") {
|
|
let n = str_to_int(csv)
|
|
}
|
|
let n = n + 1
|
|
state_set("__match_counter", native_int_to_str(n))
|
|
native_int_to_str(n)
|
|
}
|
|
|
|
fn cg_match(expr: Map<String, Any>) -> String {
|
|
let subject = expr["subject"]
|
|
let arms = expr["arms"]
|
|
let subj_c: String = cg_expr(subject)
|
|
let id: String = next_match_id()
|
|
let subj_var: String = "_match_subj_" + id
|
|
let result_var: String = "_match_result_" + id
|
|
let done_label: String = "_match_done_" + id
|
|
let out: String = "({ el_val_t " + subj_var + " = " + subj_c + "; el_val_t " + result_var + " = 0; "
|
|
let n: Int = native_list_len(arms)
|
|
let i = 0
|
|
while i < n {
|
|
let arm = native_list_get(arms, i)
|
|
let pat = arm["pattern"]
|
|
let body = arm["body"]
|
|
let pkind: String = pat["pattern"]
|
|
let body_c: String = cg_expr(body)
|
|
if str_eq(pkind, "Wildcard") {
|
|
let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
|
|
} else {
|
|
if str_eq(pkind, "Binding") {
|
|
let bname: String = pat["name"]
|
|
let out = out + "{ el_val_t " + bname + " = " + subj_var + "; " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
|
|
} else {
|
|
if str_eq(pkind, "LitInt") {
|
|
let v: String = pat["value"]
|
|
let out = out + "if (" + subj_var + " == " + v + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
|
|
} else {
|
|
if str_eq(pkind, "LitStr") {
|
|
let v: String = pat["value"]
|
|
let out = out + "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
|
|
} else {
|
|
if str_eq(pkind, "LitBool") {
|
|
let v: String = pat["value"]
|
|
let bv = "0"
|
|
if str_eq(v, "true") {
|
|
let bv = "1"
|
|
}
|
|
let out = out + "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
|
|
} else {
|
|
// unknown pattern → wildcard
|
|
let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } "
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
let out = out + done_label + ":; " + result_var + "; })"
|
|
out
|
|
}
|
|
|
|
// ── If-as-expression codegen ─────────────────────────────────────────────────
|
|
//
|
|
// Lower `if cond { thenBody } else { elseBody }` used in expression position
|
|
// (e.g. `let x = if a { b } else { c }`) to a GCC/Clang statement-expression
|
|
// so the actual arm bodies are evaluated, not just `(cond ? 1 : 0)`.
|
|
//
|
|
// Each arm body is a list of statements; the result of the arm is the value
|
|
// of its final Expr statement (mirroring transform_implicit_return at function
|
|
// scope). Statements before the final Expr are emitted as expression-statements
|
|
// for their side effects.
|
|
|
|
fn next_if_id() -> String {
|
|
let csv: String = state_get("__if_expr_counter")
|
|
let n = 0
|
|
if !str_eq(csv, "") {
|
|
let n = str_to_int(csv)
|
|
}
|
|
let n = n + 1
|
|
state_set("__if_expr_counter", native_int_to_str(n))
|
|
native_int_to_str(n)
|
|
}
|
|
|
|
// Render a single arm of the if-as-expression: emit each statement-before-last
|
|
// as a side-effecting expression, then assign the final Expr's value to the
|
|
// result var. If the arm body is empty or its last stmt isn't an Expr, the
|
|
// result var stays at its initial 0.
|
|
fn cg_if_expr_arm(stmts: [Map<String, Any>], result_var: String) -> String {
|
|
let n: Int = native_list_len(stmts)
|
|
let out = ""
|
|
let i = 0
|
|
while i < n {
|
|
let s = native_list_get(stmts, i)
|
|
let sk: String = s["stmt"]
|
|
let is_last: Bool = false
|
|
if i == n - 1 { let is_last = true }
|
|
if str_eq(sk, "Let") {
|
|
let name: String = s["name"]
|
|
let val = s["value"]
|
|
let val_c: String = cg_expr(val)
|
|
let out = out + "el_val_t " + name + " = " + val_c + "; "
|
|
} else {
|
|
if str_eq(sk, "Return") {
|
|
let val = s["value"]
|
|
let val_c: String = cg_expr(val)
|
|
let out = out + result_var + " = (" + val_c + "); "
|
|
} else {
|
|
if str_eq(sk, "Expr") {
|
|
let val = s["value"]
|
|
let val_c: String = cg_expr(val)
|
|
if is_last {
|
|
let out = out + result_var + " = (" + val_c + "); "
|
|
} else {
|
|
let out = out + "(void)(" + val_c + "); "
|
|
}
|
|
} else {
|
|
if str_eq(sk, "Assign") {
|
|
// 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).
|
|
let aname: String = s["name"]
|
|
let aval = s["value"]
|
|
let aval_c: String = cg_expr(aval)
|
|
let out = out + aname + " = " + aval_c + "; "
|
|
} else {
|
|
// Non-trivial stmt kinds (While/For) shouldn't appear in
|
|
// expression-position arm bodies; emit nothing rather
|
|
// than malformed C.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
out
|
|
}
|
|
|
|
fn cg_if_expr(expr: Map<String, Any>) -> String {
|
|
let cond = expr["cond"]
|
|
let then_stmts = expr["then"]
|
|
let else_stmts = expr["else"]
|
|
let has_else: Bool = expr["has_else"]
|
|
let cond_c: String = cg_expr(cond)
|
|
let id: String = next_if_id()
|
|
let result_var: String = "_if_result_" + id
|
|
let then_c: String = cg_if_expr_arm(then_stmts, result_var)
|
|
let else_c: String = ""
|
|
if has_else {
|
|
let else_c = cg_if_expr_arm(else_stmts, result_var)
|
|
}
|
|
let out: String = "({ el_val_t " + result_var + " = 0; if (" + cond_c + ") { " + then_c + "} else { " + else_c + "} " + result_var + "; })"
|
|
out
|
|
}
|
|
|
|
// ── Variable scope tracking ───────────────────────────────────────────────────
|
|
//
|
|
// El allows `let x = expr` to both declare and reassign x in the same scope.
|
|
// C doesn't allow redeclaring the same name in the same block.
|
|
// We track declared names in a list and emit `x = expr` (no type prefix)
|
|
// when x is already declared. The declared list is passed through all
|
|
// statement emitters.
|
|
|
|
fn list_contains(lst: [String], s: String) -> Bool {
|
|
let n: Int = native_list_len(lst)
|
|
let i = 0
|
|
while i < n {
|
|
let item: String = native_list_get(lst, i)
|
|
if item == s { return true }
|
|
let i = i + 1
|
|
}
|
|
false
|
|
}
|
|
|
|
// ── Statement codegen ─────────────────────────────────────────────────────────
|
|
//
|
|
// cg_stmt emits C lines via println. declared is a list of already-declared
|
|
// variable names in the current C scope; returns updated declared list.
|
|
|
|
fn cg_stmt(stmt: Map<String, Any>, indent: String, declared: [String]) -> [String] {
|
|
let kind: String = stmt["stmt"]
|
|
|
|
if kind == "Let" {
|
|
let name: String = stmt["name"]
|
|
let val = stmt["value"]
|
|
let val_c: String = cg_expr(val)
|
|
// If the binding is annotated `: Int` and val is an Int literal,
|
|
// register `name` in the per-function int-name set so that later
|
|
// `name + ...` dispatches to arithmetic, not concat.
|
|
let ltype: String = stmt["type"]
|
|
if str_eq(ltype, "Int") {
|
|
add_int_name(name)
|
|
}
|
|
// Temporal type annotations register the name with the matching
|
|
// typed-set so BinOp / comparison codegen routes through the
|
|
// typed wrappers and forbids cross-type ops.
|
|
if str_eq(ltype, "Instant") {
|
|
add_instant_name(name)
|
|
}
|
|
if str_eq(ltype, "Duration") {
|
|
add_duration_name(name)
|
|
}
|
|
// 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)
|
|
}
|
|
if is_duration_expr(val) {
|
|
add_duration_name(name)
|
|
}
|
|
let vk: String = val["expr"]
|
|
if str_eq(vk, "Int") {
|
|
add_int_name(name)
|
|
}
|
|
if list_contains(declared, name) {
|
|
emit_line(indent + name + " = " + val_c + ";")
|
|
return declared
|
|
} else {
|
|
emit_line(indent + "el_val_t " + name + " = " + val_c + ";")
|
|
return native_list_append(declared, name)
|
|
}
|
|
}
|
|
|
|
if kind == "Return" {
|
|
let val = stmt["value"]
|
|
let val_kind: String = val["expr"]
|
|
if val_kind == "Nil" {
|
|
emit_line(indent + "return 0;")
|
|
} else {
|
|
let val_c: String = cg_expr(val)
|
|
emit_line(indent + "return " + val_c + ";")
|
|
}
|
|
return declared
|
|
}
|
|
|
|
// Bare reassignment: `name = expr`. Always emits a plain C assignment
|
|
// (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
|
|
// in the surrounding scope.
|
|
if kind == "Assign" {
|
|
let name: String = stmt["name"]
|
|
let val = stmt["value"]
|
|
let val_c: String = cg_expr(val)
|
|
emit_line(indent + name + " = " + val_c + ";")
|
|
return declared
|
|
}
|
|
|
|
if kind == "Expr" {
|
|
let val = stmt["value"]
|
|
let val_kind: String = val["expr"]
|
|
if val_kind == "If" {
|
|
cg_if_stmt(val, indent, declared)
|
|
return declared
|
|
}
|
|
if val_kind == "For" {
|
|
cg_for_stmt(val, indent, declared)
|
|
return declared
|
|
}
|
|
let val_c: String = cg_expr(val)
|
|
emit_line(indent + val_c + ";")
|
|
return declared
|
|
}
|
|
|
|
if kind == "While" {
|
|
let cond = stmt["cond"]
|
|
let body = stmt["body"]
|
|
let cond_c: String = cg_expr(cond)
|
|
let cond_c = strip_outer_parens(cond_c)
|
|
emit_line(indent + "while (" + cond_c + ") {")
|
|
// 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))
|
|
emit_line(indent + "}")
|
|
return declared
|
|
}
|
|
|
|
if kind == "For" {
|
|
let item: String = stmt["item"]
|
|
let list_expr = stmt["list"]
|
|
let body = stmt["body"]
|
|
cg_for_body(item, list_expr, body, indent, declared)
|
|
return declared
|
|
}
|
|
|
|
if kind == "FnDef" { return declared }
|
|
if kind == "TypeDef" { return declared }
|
|
if kind == "EnumDef" { return declared }
|
|
if kind == "Import" { return declared }
|
|
if kind == "CgiBlock" { return declared }
|
|
declared
|
|
}
|
|
|
|
// Strip a single layer of surrounding parentheses from a C expression string.
|
|
fn strip_outer_parens(s: String) -> String {
|
|
let chars: [String] = native_string_chars(s)
|
|
let n: Int = native_list_len(chars)
|
|
if n < 2 { return s }
|
|
let first: String = native_list_get(chars, 0)
|
|
let last: String = native_list_get(chars, n - 1)
|
|
if first == "(" {
|
|
if last == ")" {
|
|
let depth = 1
|
|
let i = 1
|
|
let balanced = true
|
|
while i < n - 1 {
|
|
let ch: String = native_list_get(chars, i)
|
|
if ch == "(" {
|
|
let depth = depth + 1
|
|
}
|
|
if ch == ")" {
|
|
let depth = depth - 1
|
|
if depth == 0 {
|
|
let balanced = false
|
|
let i = n
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
if balanced {
|
|
let inner = ""
|
|
let j = 1
|
|
while j < n - 1 {
|
|
let ch: String = native_list_get(chars, j)
|
|
let inner = inner + ch
|
|
let j = j + 1
|
|
}
|
|
return inner
|
|
}
|
|
}
|
|
}
|
|
s
|
|
}
|
|
|
|
fn cg_if_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Void {
|
|
let cond = expr["cond"]
|
|
let then_stmts = expr["then"]
|
|
let else_stmts = expr["else"]
|
|
let has_else: Bool = expr["has_else"]
|
|
let cond_c: String = cg_expr(cond)
|
|
let cond_c = strip_outer_parens(cond_c)
|
|
emit_line(indent + "if (" + cond_c + ") {")
|
|
// 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)
|
|
// are shared.
|
|
cg_stmts(then_stmts, indent + " ", native_list_clone(declared))
|
|
if has_else {
|
|
emit_line(indent + "} else {")
|
|
cg_stmts(else_stmts, indent + " ", native_list_clone(declared))
|
|
}
|
|
emit_line(indent + "}")
|
|
}
|
|
|
|
fn cg_for_body(item: String, list_expr: Map<String, Any>, body: [Map<String, Any>], indent: String, declared: [String]) -> Void {
|
|
let list_c: String = cg_expr(list_expr)
|
|
let idx = "_el_i"
|
|
let list_tmp = "_el_lst"
|
|
let len_tmp = "_el_len"
|
|
emit_line(indent + "{")
|
|
emit_line(indent + " el_val_t " + list_tmp + " = " + list_c + ";")
|
|
emit_line(indent + " el_val_t " + len_tmp + " = el_list_len(" + list_tmp + ");")
|
|
emit_line(indent + " for (el_val_t " + idx + " = 0; " + idx + " < " + len_tmp + "; " + idx + "++) {")
|
|
emit_line(indent + " el_val_t " + item + " = el_list_get(" + list_tmp + ", " + idx + ");")
|
|
// Body lives inside its own C block; the loop variable and any locally
|
|
// let-bound names go out of scope at the closing brace, so we mustn't
|
|
// pollute the parent's `declared` with them.
|
|
let body_decl = native_list_clone(declared)
|
|
let body_decl = native_list_append(body_decl, item)
|
|
cg_stmts(body, indent + " ", body_decl)
|
|
emit_line(indent + " }")
|
|
emit_line(indent + "}")
|
|
}
|
|
|
|
fn cg_for_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Void {
|
|
let item: String = expr["item"]
|
|
let list_expr = expr["list"]
|
|
let body = expr["body"]
|
|
cg_for_body(item, list_expr, body, indent, declared)
|
|
}
|
|
|
|
fn cg_stmts(stmts: [Map<String, Any>], indent: String, declared: [String]) -> [String] {
|
|
let n: Int = native_list_len(stmts)
|
|
let i = 0
|
|
let decl = declared
|
|
while i < n {
|
|
let stmt = native_list_get(stmts, i)
|
|
let decl = cg_stmt(stmt, indent, decl)
|
|
let i = i + 1
|
|
}
|
|
decl
|
|
}
|
|
|
|
// ── Function declaration codegen ───────────────────────────────────────────────
|
|
|
|
fn param_decl(param: Map<String, Any>, idx: Int) -> String {
|
|
let name: String = param["name"]
|
|
"el_val_t " + name
|
|
}
|
|
|
|
fn params_to_c(params: [Map<String, Any>]) -> String {
|
|
let n: Int = native_list_len(params)
|
|
if n == 0 { return "void" }
|
|
let out = ""
|
|
let i = 0
|
|
while i < n {
|
|
let param = native_list_get(params, i)
|
|
let decl: String = param_decl(param, i)
|
|
if i > 0 {
|
|
let out = out + ", "
|
|
}
|
|
let out = out + decl
|
|
let i = i + 1
|
|
}
|
|
out
|
|
}
|
|
|
|
// Transform a function body so that an implicit-return final expression
|
|
// becomes an explicit Return. El allows the last expression in a function
|
|
// body to be the return value (e.g. `fn lex(s) { ... tokens }` returns
|
|
// `tokens`). Without this transform, the codegen emits the bare expression
|
|
// and falls through to the trailing `return 0;`, losing the value.
|
|
//
|
|
// Rules: a body ending in a bare Expr whose inner expr is NOT a control-
|
|
// flow construct (If/For) is rewritten so that final Expr becomes a
|
|
// Return statement carrying the same value. Bodies whose final statement
|
|
// is already a Return, While, For, or a non-value-producing form pass
|
|
// through unchanged.
|
|
fn transform_implicit_return(body: [Map<String, Any>]) -> [Map<String, Any>] {
|
|
let n: Int = native_list_len(body)
|
|
if n == 0 { return body }
|
|
let last: Map<String, Any> = native_list_get(body, n - 1)
|
|
let last_kind: String = last["stmt"]
|
|
if last_kind == "Expr" {
|
|
let val = last["value"]
|
|
let val_kind: String = val["expr"]
|
|
// Skip control-flow expressions used as statements
|
|
if val_kind == "If" { return body }
|
|
if val_kind == "For" { return body }
|
|
// Replace the last bare Expr with a Return carrying the same value
|
|
let new_body: [Map<String, Any>] = native_list_empty()
|
|
let i = 0
|
|
while i < n - 1 {
|
|
let new_body = native_list_append(new_body, native_list_get(body, i))
|
|
let i = i + 1
|
|
}
|
|
let return_stmt: Map<String, Any> = { "stmt": "Return", "value": val }
|
|
let new_body = native_list_append(new_body, return_stmt)
|
|
return new_body
|
|
}
|
|
body
|
|
}
|
|
|
|
// Test whether `name` is currently registered as an Int-typed identifier
|
|
// for the function being codegened. The set is maintained as a comma-
|
|
// bounded CSV in process state; cg_fn seeds it from typed parameters,
|
|
// cg_stmt extends it from typed `let` bindings.
|
|
fn is_int_name(name: String) -> Bool {
|
|
let csv: String = state_get("__int_names")
|
|
if str_eq(csv, "") { return false }
|
|
return str_contains(csv, "," + name + ",")
|
|
}
|
|
|
|
// 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
|
|
// surface mismatches (Instant + Instant, Duration + Int) as #error
|
|
// directives at the top of the generated C.
|
|
fn is_instant_name(name: String) -> Bool {
|
|
let csv: String = state_get("__instant_names")
|
|
if str_eq(csv, "") { return false }
|
|
return str_contains(csv, "," + name + ",")
|
|
}
|
|
|
|
fn is_duration_name(name: String) -> Bool {
|
|
let csv: String = state_get("__duration_names")
|
|
if str_eq(csv, "") { return false }
|
|
return str_contains(csv, "," + name + ",")
|
|
}
|
|
|
|
// Known runtime builtins that return Int. Used to dispatch arithmetic vs
|
|
// string-concat on `+` when one side is a Call. New builtins must be added
|
|
// here when they return Int and may participate in arithmetic.
|
|
fn is_int_call(call_expr: Map<String, Any>) -> Bool {
|
|
let func = call_expr["func"]
|
|
let fk: String = func["expr"]
|
|
if !str_eq(fk, "Ident") { return false }
|
|
let name: String = func["name"]
|
|
if str_eq(name, "str_len") { return true }
|
|
if str_eq(name, "str_index_of") { return true }
|
|
if str_eq(name, "str_to_int") { return true }
|
|
if str_eq(name, "str_char_code") { return true }
|
|
if str_eq(name, "native_list_len") { return true }
|
|
if str_eq(name, "el_list_len") { return true }
|
|
if str_eq(name, "len") { return true }
|
|
if str_eq(name, "json_get_int") { return true }
|
|
if str_eq(name, "json_array_len") { return true }
|
|
if str_eq(name, "engram_node_count") { return true }
|
|
if str_eq(name, "engram_edge_count") { return true }
|
|
if str_eq(name, "time_now") { return true }
|
|
if str_eq(name, "time_now_utc") { return true }
|
|
if str_eq(name, "time_diff") { return true }
|
|
if str_eq(name, "time_add") { return true }
|
|
if str_eq(name, "time_from_parts") { return true }
|
|
if str_eq(name, "el_abs") { return true }
|
|
if str_eq(name, "el_max") { return true }
|
|
if str_eq(name, "el_min") { return true }
|
|
if str_eq(name, "float_to_int") { return true }
|
|
if str_eq(name, "unix_timestamp") { return true }
|
|
if str_eq(name, "instant_to_unix_seconds") { return true }
|
|
if str_eq(name, "instant_to_unix_millis") { return true }
|
|
if str_eq(name, "duration_to_seconds") { return true }
|
|
if str_eq(name, "duration_to_millis") { return true }
|
|
if str_eq(name, "duration_to_nanos") { return true }
|
|
return false
|
|
}
|
|
|
|
// 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
|
|
// that now() is an Instant-returning Call.
|
|
fn is_instant_call(call_expr: Map<String, Any>) -> Bool {
|
|
let func = call_expr["func"]
|
|
let fk: String = func["expr"]
|
|
if !str_eq(fk, "Ident") { return false }
|
|
let name: String = func["name"]
|
|
if str_eq(name, "now") { return true }
|
|
if str_eq(name, "el_now_instant") { return true }
|
|
if str_eq(name, "unix_seconds") { return true }
|
|
if str_eq(name, "unix_millis") { return true }
|
|
if str_eq(name, "instant_from_iso8601") { return true }
|
|
if str_eq(name, "el_instant_add_dur") { return true }
|
|
if str_eq(name, "el_instant_sub_dur") { return true }
|
|
return false
|
|
}
|
|
|
|
// Builtins that return a Duration. Same role as is_instant_call.
|
|
fn is_duration_call(call_expr: Map<String, Any>) -> Bool {
|
|
let func = call_expr["func"]
|
|
let fk: String = func["expr"]
|
|
if !str_eq(fk, "Ident") { return false }
|
|
let name: String = func["name"]
|
|
if str_eq(name, "el_duration_from_nanos") { return true }
|
|
if str_eq(name, "duration_seconds") { return true }
|
|
if str_eq(name, "duration_millis") { return true }
|
|
if str_eq(name, "duration_nanos") { return true }
|
|
if str_eq(name, "el_instant_diff") { return true }
|
|
if str_eq(name, "el_duration_add") { return true }
|
|
if str_eq(name, "el_duration_sub") { return true }
|
|
if str_eq(name, "el_duration_scale") { return true }
|
|
if str_eq(name, "el_duration_div") { return true }
|
|
if str_eq(name, "ttl_cache_age") { return true }
|
|
return false
|
|
}
|
|
|
|
// 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
|
|
// 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 {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "Ident") {
|
|
let name: String = expr["name"]
|
|
return is_instant_name(name)
|
|
}
|
|
if str_eq(k, "Call") {
|
|
return is_instant_call(expr)
|
|
}
|
|
if str_eq(k, "BinOp") {
|
|
let op: String = expr["op"]
|
|
if str_eq(op, "Plus") {
|
|
// Instant + Duration → Instant
|
|
// Duration + Instant → Instant
|
|
if is_instant_expr(expr["left"]) {
|
|
if is_duration_expr(expr["right"]) { return true }
|
|
}
|
|
if is_duration_expr(expr["left"]) {
|
|
if is_instant_expr(expr["right"]) { return true }
|
|
}
|
|
return false
|
|
}
|
|
if str_eq(op, "Minus") {
|
|
// Instant - Duration → Instant
|
|
if is_instant_expr(expr["left"]) {
|
|
if is_duration_expr(expr["right"]) { return true }
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
fn is_duration_expr(expr: Map<String, Any>) -> Bool {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "DurationLit") { return true }
|
|
if str_eq(k, "Ident") {
|
|
let name: String = expr["name"]
|
|
return is_duration_name(name)
|
|
}
|
|
if str_eq(k, "Call") {
|
|
return is_duration_call(expr)
|
|
}
|
|
if str_eq(k, "Neg") {
|
|
return is_duration_expr(expr["inner"])
|
|
}
|
|
if str_eq(k, "BinOp") {
|
|
let op: String = expr["op"]
|
|
if str_eq(op, "Plus") {
|
|
// 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)
|
|
if is_duration_expr(expr["left"]) {
|
|
if is_duration_expr(expr["right"]) { return true }
|
|
}
|
|
if is_instant_expr(expr["left"]) {
|
|
if is_instant_expr(expr["right"]) { return true }
|
|
}
|
|
return false
|
|
}
|
|
if str_eq(op, "Star") {
|
|
// Duration * Int → Duration
|
|
// Int * Duration → Duration
|
|
if is_duration_expr(expr["left"]) {
|
|
if is_int_expr(expr["right"]) { return true }
|
|
}
|
|
if is_int_expr(expr["left"]) {
|
|
if is_duration_expr(expr["right"]) { return true }
|
|
}
|
|
return false
|
|
}
|
|
if str_eq(op, "Slash") {
|
|
// Duration / Int → Duration
|
|
if is_duration_expr(expr["left"]) {
|
|
if is_int_expr(expr["right"]) { return true }
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Record a temporal-type violation. Surfaced as `#error` directives at the
|
|
// top of the generated C, identical machinery to cap_record_violation.
|
|
// kinds: "instant_plus_instant", "duration_plus_int", etc.
|
|
fn time_record_violation(kind: String, detail: String) -> Bool {
|
|
let csv: String = state_get("__time_violations")
|
|
if str_eq(csv, "") { let csv = "," }
|
|
let entry: String = kind + ":" + detail
|
|
let key: String = "," + entry + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__time_violations", csv + entry + ",")
|
|
return true
|
|
}
|
|
|
|
// Recursive type-propagation: is `expr` known-Int at codegen time?
|
|
// This unifies the BinOp(+) dispatch so chained arithmetic over Int
|
|
// operands stays arithmetic. Without recursion, a wrapping `+` between
|
|
// `BinOp(+) of two Ints` and another Int falls to el_str_concat because
|
|
// the outer dispatch only checks the immediate kind, not the inner.
|
|
//
|
|
// Rules:
|
|
// Int literal → Int
|
|
// Ident in __int_names → Int
|
|
// Call to known-Int builtin → Int
|
|
// Neg of Int → Int
|
|
// BinOp arithmetic of two Ints → Int (Plus, Minus, Star, Slash, Percent)
|
|
// BinOp comparison/logical → Int (yields 0/1; safe to treat as Int)
|
|
// anything else → not provably Int
|
|
fn is_int_expr(expr: Map<String, Any>) -> Bool {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "Int") { return true }
|
|
if str_eq(k, "Ident") {
|
|
let name: String = expr["name"]
|
|
return is_int_name(name)
|
|
}
|
|
if str_eq(k, "Call") {
|
|
return is_int_call(expr)
|
|
}
|
|
if str_eq(k, "Neg") {
|
|
return is_int_expr(expr["inner"])
|
|
}
|
|
if str_eq(k, "Not") {
|
|
return true
|
|
}
|
|
if str_eq(k, "BinOp") {
|
|
let op: String = expr["op"]
|
|
// Comparisons and logicals always yield 0/1 — safe Int.
|
|
if str_eq(op, "EqEq") { return true }
|
|
if str_eq(op, "NotEq") { return true }
|
|
if str_eq(op, "Lt") { return true }
|
|
if str_eq(op, "Gt") { return true }
|
|
if str_eq(op, "LtEq") { return true }
|
|
if str_eq(op, "GtEq") { return true }
|
|
if str_eq(op, "And") { return true }
|
|
if str_eq(op, "Or") { return true }
|
|
// Arithmetic propagates: Int op Int → Int.
|
|
if str_eq(op, "Plus") {
|
|
if is_int_expr(expr["left"]) {
|
|
if is_int_expr(expr["right"]) { return true }
|
|
}
|
|
return false
|
|
}
|
|
if str_eq(op, "Minus") {
|
|
if is_int_expr(expr["left"]) {
|
|
if is_int_expr(expr["right"]) { return true }
|
|
}
|
|
return false
|
|
}
|
|
if str_eq(op, "Star") {
|
|
if is_int_expr(expr["left"]) {
|
|
if is_int_expr(expr["right"]) { return true }
|
|
}
|
|
return false
|
|
}
|
|
if str_eq(op, "Slash") {
|
|
if is_int_expr(expr["left"]) {
|
|
if is_int_expr(expr["right"]) { return true }
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ── Capability-kind enforcement ──────────────────────────────────────────────
|
|
//
|
|
// A program's top-level block (cgi / service / none) determines which
|
|
// runtime primitives it may call. The compiler records violations in
|
|
// process state during cg_expr's Call emission; codegen's entry point
|
|
// then emits #error directives at the top of the generated C so the
|
|
// downstream cc step fails with a clear message.
|
|
//
|
|
// Capability tiers:
|
|
// "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.
|
|
//
|
|
// 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.
|
|
|
|
fn cap_record_violation(kind: String, fn_name: String) -> Bool {
|
|
let csv: String = state_get("__cap_violations")
|
|
if str_eq(csv, "") { let csv = "," }
|
|
let entry: String = kind + ":" + fn_name
|
|
let key: String = "," + entry + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__cap_violations", csv + entry + ",")
|
|
return true
|
|
}
|
|
|
|
// 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 {
|
|
if str_eq(fn_name, "llm_call_agentic") { return true }
|
|
if str_eq(fn_name, "llm_register_tool") { return true }
|
|
if str_eq(fn_name, "dharma_emit") { return true }
|
|
if str_eq(fn_name, "dharma_field") { return true }
|
|
return false
|
|
}
|
|
|
|
// 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 }
|
|
if str_eq(fn_name, "dharma_activate") { return true }
|
|
if str_eq(fn_name, "dharma_emit") { return true }
|
|
if str_eq(fn_name, "dharma_field") { return true }
|
|
if str_eq(fn_name, "dharma_strengthen") { return true }
|
|
if str_eq(fn_name, "dharma_relationship") { return true }
|
|
if str_eq(fn_name, "dharma_peers") { return true }
|
|
return false
|
|
}
|
|
|
|
// 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 }
|
|
if str_eq(fn_name, "llm_call_agentic") { return true }
|
|
if str_eq(fn_name, "llm_vision") { return true }
|
|
if str_eq(fn_name, "llm_register_tool") { return true }
|
|
if str_eq(fn_name, "llm_models") { return true }
|
|
return false
|
|
}
|
|
|
|
fn cap_check_call(fn_name: String) -> Bool {
|
|
let kind: String = state_get("__program_kind")
|
|
if str_eq(kind, "cgi") { return true }
|
|
if str_eq(kind, "service") {
|
|
if is_self_formation_call(fn_name) {
|
|
cap_record_violation("service", fn_name)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
// utility (default)
|
|
if is_dharma_call(fn_name) {
|
|
cap_record_violation("utility", fn_name)
|
|
return false
|
|
}
|
|
if is_llm_call(fn_name) {
|
|
cap_record_violation("utility", fn_name)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Emit collected capability violations as #error directives. Called
|
|
// from codegen()'s entry point right after the cgi/service-block scan,
|
|
// so they appear at the very top of the generated C.
|
|
fn emit_cap_violations() -> Void {
|
|
let csv: String = state_get("__cap_violations")
|
|
if str_eq(csv, "") { return }
|
|
if str_eq(csv, ",") { return }
|
|
let n: Int = str_len(csv)
|
|
let i: Int = 1
|
|
while i < n {
|
|
let next_comma: Int = str_index_of(str_slice(csv, i, n), ",")
|
|
if next_comma < 0 { return }
|
|
let entry: String = str_slice(csv, i, i + next_comma)
|
|
let colon: Int = str_index_of(entry, ":")
|
|
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)\"")
|
|
}
|
|
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, …)
|
|
// 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.
|
|
fn emit_time_violations() -> Void {
|
|
let csv: String = state_get("__time_violations")
|
|
if str_eq(csv, "") { return }
|
|
if str_eq(csv, ",") { return }
|
|
let n: Int = str_len(csv)
|
|
let i: Int = 1
|
|
while i < n {
|
|
let next_comma: Int = str_index_of(str_slice(csv, i, n), ",")
|
|
if next_comma < 0 { return }
|
|
let entry: String = str_slice(csv, i, i + next_comma)
|
|
let colon: Int = str_index_of(entry, ":")
|
|
if colon > 0 {
|
|
let detail: String = str_slice(entry, colon + 1, str_len(entry))
|
|
emit_line("#error \"temporal type error: " + detail + "\"")
|
|
}
|
|
let i = i + next_comma + 1
|
|
}
|
|
}
|
|
|
|
// ── Builtin arity table ───────────────────────────────────────────────────────
|
|
//
|
|
// El programs sometimes call runtime builtins with the wrong number of
|
|
// arguments (e.g. `http_serve(port)` instead of `http_serve(port, handler)`).
|
|
// Without this check the generated C compiles to a call with too few /
|
|
// too many args and fails downstream cc with a generic "too few arguments"
|
|
// message that doesn't point to the El source line.
|
|
//
|
|
// Strategy: a small static table mirrors el_runtime.h. Variadic builtins
|
|
// (el_list_new, el_map_new, args) and unknown identifiers (user fns,
|
|
// dynamic dispatch) return -1 → no check. A mismatch records a violation
|
|
// in process state, which emit_arity_violations() turns into #error
|
|
// directives at the top of the generated C.
|
|
fn builtin_arity(name: String) -> Int {
|
|
// I/O
|
|
if str_eq(name, "println") { return 1 }
|
|
if str_eq(name, "print") { return 1 }
|
|
if str_eq(name, "readline") { return 0 }
|
|
// String
|
|
if str_eq(name, "el_str_concat") { return 2 }
|
|
if str_eq(name, "str_eq") { return 2 }
|
|
if str_eq(name, "str_starts_with") { return 2 }
|
|
if str_eq(name, "str_ends_with") { return 2 }
|
|
if str_eq(name, "str_len") { return 1 }
|
|
if str_eq(name, "str_concat") { return 2 }
|
|
if str_eq(name, "int_to_str") { return 1 }
|
|
if str_eq(name, "str_to_int") { return 1 }
|
|
if str_eq(name, "str_slice") { return 3 }
|
|
if str_eq(name, "str_contains") { return 2 }
|
|
if str_eq(name, "str_replace") { return 3 }
|
|
if str_eq(name, "str_to_upper") { return 1 }
|
|
if str_eq(name, "str_to_lower") { return 1 }
|
|
if str_eq(name, "str_trim") { return 1 }
|
|
if str_eq(name, "str_index_of") { return 2 }
|
|
if str_eq(name, "str_split") { return 2 }
|
|
if str_eq(name, "str_char_at") { return 2 }
|
|
if str_eq(name, "str_char_code") { return 2 }
|
|
if str_eq(name, "str_pad_left") { return 3 }
|
|
if str_eq(name, "str_pad_right") { return 3 }
|
|
if str_eq(name, "str_format") { return 2 }
|
|
if str_eq(name, "str_lower") { return 1 }
|
|
if str_eq(name, "str_upper") { return 1 }
|
|
// HTML sanitizer
|
|
if str_eq(name, "el_html_sanitize") { return 2 }
|
|
// Math
|
|
if str_eq(name, "el_abs") { return 1 }
|
|
if str_eq(name, "el_max") { return 2 }
|
|
if str_eq(name, "el_min") { return 2 }
|
|
// List
|
|
if str_eq(name, "el_list_len") { return 1 }
|
|
if str_eq(name, "el_list_get") { return 2 }
|
|
if str_eq(name, "el_list_append") { return 2 }
|
|
if str_eq(name, "el_list_empty") { return 0 }
|
|
if str_eq(name, "el_list_clone") { return 1 }
|
|
if str_eq(name, "list_push") { return 2 }
|
|
if str_eq(name, "list_push_front") { return 2 }
|
|
if str_eq(name, "list_join") { return 2 }
|
|
if str_eq(name, "list_range") { return 2 }
|
|
// Map
|
|
if str_eq(name, "el_get_field") { return 2 }
|
|
if str_eq(name, "el_map_get") { return 2 }
|
|
if str_eq(name, "el_map_set") { return 3 }
|
|
// HTTP
|
|
if str_eq(name, "http_get") { return 1 }
|
|
if str_eq(name, "http_post") { return 2 }
|
|
if str_eq(name, "http_post_json") { return 2 }
|
|
if str_eq(name, "http_get_with_headers") { return 2 }
|
|
if str_eq(name, "http_post_with_headers") { return 3 }
|
|
if str_eq(name, "http_post_form_auth") { return 3 }
|
|
if str_eq(name, "http_serve") { return 2 }
|
|
if str_eq(name, "http_set_handler") { return 1 }
|
|
// Filesystem
|
|
if str_eq(name, "fs_read") { return 1 }
|
|
if str_eq(name, "fs_write") { return 2 }
|
|
if str_eq(name, "fs_list") { return 1 }
|
|
// JSON
|
|
if str_eq(name, "json_get") { return 2 }
|
|
if str_eq(name, "json_parse") { return 1 }
|
|
if str_eq(name, "json_stringify") { return 1 }
|
|
if str_eq(name, "json_get_string") { return 2 }
|
|
if str_eq(name, "json_get_int") { return 2 }
|
|
if str_eq(name, "json_get_float") { return 2 }
|
|
if str_eq(name, "json_get_bool") { return 2 }
|
|
if str_eq(name, "json_get_raw") { return 2 }
|
|
if str_eq(name, "json_set") { return 3 }
|
|
if str_eq(name, "json_array_len") { return 1 }
|
|
// Time
|
|
if str_eq(name, "time_now") { return 0 }
|
|
if str_eq(name, "time_now_utc") { return 0 }
|
|
if str_eq(name, "sleep_secs") { return 1 }
|
|
if str_eq(name, "sleep_ms") { return 1 }
|
|
if str_eq(name, "time_format") { return 2 }
|
|
if str_eq(name, "time_to_parts") { return 1 }
|
|
if str_eq(name, "time_from_parts") { return 3 }
|
|
if str_eq(name, "time_add") { return 3 }
|
|
if str_eq(name, "time_diff") { return 3 }
|
|
// UUID
|
|
if str_eq(name, "uuid_new") { return 0 }
|
|
if str_eq(name, "uuid_v4") { return 0 }
|
|
// Env / state
|
|
if str_eq(name, "env") { return 1 }
|
|
if str_eq(name, "state_set") { return 2 }
|
|
if str_eq(name, "state_get") { return 1 }
|
|
if str_eq(name, "state_del") { return 1 }
|
|
if str_eq(name, "state_keys") { return 0 }
|
|
// Float
|
|
if str_eq(name, "float_to_str") { return 1 }
|
|
if str_eq(name, "int_to_float") { return 1 }
|
|
if str_eq(name, "float_to_int") { return 1 }
|
|
if str_eq(name, "format_float") { return 2 }
|
|
if str_eq(name, "decimal_round") { return 2 }
|
|
if str_eq(name, "str_to_float") { return 1 }
|
|
// Math (Float)
|
|
if str_eq(name, "math_sqrt") { return 1 }
|
|
if str_eq(name, "math_log") { return 1 }
|
|
if str_eq(name, "math_ln") { return 1 }
|
|
if str_eq(name, "math_sin") { return 1 }
|
|
if str_eq(name, "math_cos") { return 1 }
|
|
if str_eq(name, "math_pi") { return 0 }
|
|
// Bool
|
|
if str_eq(name, "bool_to_str") { return 1 }
|
|
// Process
|
|
if str_eq(name, "exit_program") { return 1 }
|
|
// CGI / DHARMA
|
|
if str_eq(name, "dharma_connect") { return 1 }
|
|
if str_eq(name, "dharma_send") { return 2 }
|
|
if str_eq(name, "dharma_activate") { return 1 }
|
|
if str_eq(name, "dharma_emit") { return 2 }
|
|
if str_eq(name, "dharma_field") { return 1 }
|
|
if str_eq(name, "dharma_strengthen") { return 2 }
|
|
if str_eq(name, "dharma_relationship") { return 1 }
|
|
if str_eq(name, "dharma_peers") { return 0 }
|
|
// Engram
|
|
if str_eq(name, "engram_node") { return 3 }
|
|
if str_eq(name, "engram_node_full") { return 8 }
|
|
if str_eq(name, "engram_get_node") { return 1 }
|
|
if str_eq(name, "engram_strengthen") { return 1 }
|
|
if str_eq(name, "engram_forget") { return 1 }
|
|
if str_eq(name, "engram_node_count") { return 0 }
|
|
if str_eq(name, "engram_search") { return 2 }
|
|
if str_eq(name, "engram_scan_nodes") { return 2 }
|
|
if str_eq(name, "engram_connect") { return 4 }
|
|
if str_eq(name, "engram_edge_between") { return 2 }
|
|
if str_eq(name, "engram_neighbors") { return 1 }
|
|
if str_eq(name, "engram_neighbors_filtered") { return 3 }
|
|
if str_eq(name, "engram_edge_count") { return 0 }
|
|
if str_eq(name, "engram_activate") { return 2 }
|
|
if str_eq(name, "engram_save") { return 1 }
|
|
if str_eq(name, "engram_load") { return 1 }
|
|
if str_eq(name, "engram_get_node_json") { return 1 }
|
|
if str_eq(name, "engram_search_json") { return 2 }
|
|
if str_eq(name, "engram_scan_nodes_json") { return 2 }
|
|
if str_eq(name, "engram_neighbors_json") { return 3 }
|
|
if str_eq(name, "engram_activate_json") { return 2 }
|
|
if str_eq(name, "engram_stats_json") { return 0 }
|
|
// LLM
|
|
if str_eq(name, "llm_call") { return 2 }
|
|
if str_eq(name, "llm_call_system") { return 3 }
|
|
if str_eq(name, "llm_call_agentic") { return 4 }
|
|
if str_eq(name, "llm_vision") { return 4 }
|
|
if str_eq(name, "llm_models") { return 0 }
|
|
if str_eq(name, "llm_register_tool") { return 2 }
|
|
// Crypto
|
|
if str_eq(name, "sha256_hex") { return 1 }
|
|
if str_eq(name, "sha256_bytes") { return 1 }
|
|
if str_eq(name, "hmac_sha256_hex") { return 2 }
|
|
if str_eq(name, "hmac_sha256_bytes") { return 2 }
|
|
if str_eq(name, "base64_encode") { return 1 }
|
|
if str_eq(name, "base64_decode") { return 1 }
|
|
if str_eq(name, "base64url_encode") { return 1 }
|
|
if str_eq(name, "base64url_decode") { return 1 }
|
|
// Native VM aliases
|
|
if str_eq(name, "native_list_get") { return 2 }
|
|
if str_eq(name, "native_list_len") { return 1 }
|
|
if str_eq(name, "native_list_append") { return 2 }
|
|
if str_eq(name, "native_list_empty") { return 0 }
|
|
if str_eq(name, "native_list_clone") { return 1 }
|
|
if str_eq(name, "native_string_chars") { return 1 }
|
|
if str_eq(name, "native_int_to_str") { return 1 }
|
|
// Method-call aliases
|
|
if str_eq(name, "append") { return 2 }
|
|
if str_eq(name, "len") { return 1 }
|
|
if str_eq(name, "get") { return 2 }
|
|
if str_eq(name, "map_get") { return 2 }
|
|
if str_eq(name, "map_set") { return 3 }
|
|
// -1 sentinel: variadic / unknown / user-defined → no check.
|
|
return -1
|
|
}
|
|
|
|
fn arity_record_violation(fn_name: String, expected: Int, actual: Int) -> Bool {
|
|
let csv: String = state_get("__arity_violations")
|
|
if str_eq(csv, "") { let csv = "," }
|
|
// Encode as fn_name|expected|actual to recover all three at emit time.
|
|
let entry: String = fn_name + "|" + native_int_to_str(expected) + "|" + native_int_to_str(actual)
|
|
let key: String = "," + entry + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__arity_violations", csv + entry + ",")
|
|
return true
|
|
}
|
|
|
|
// Validate the call's arity against the builtin table. Returns true (always)
|
|
// because cg_expr ignores the result; -1 from builtin_arity signals
|
|
// "no check possible" (variadic or user-defined). A mismatch is recorded
|
|
// and surfaced as an #error at the bottom of the generated C, so cc fails
|
|
// before it ever attempts to type-check the wrong call.
|
|
fn arity_check_call(fn_name: String, actual: Int) -> Bool {
|
|
let expected: Int = builtin_arity(fn_name)
|
|
if expected < 0 { return true }
|
|
if expected == actual { return true }
|
|
arity_record_violation(fn_name, expected, actual)
|
|
return true
|
|
}
|
|
|
|
// Emit recorded arity violations as #error directives.
|
|
fn emit_arity_violations() -> Void {
|
|
let csv: String = state_get("__arity_violations")
|
|
if str_eq(csv, "") { return }
|
|
if str_eq(csv, ",") { return }
|
|
let n: Int = str_len(csv)
|
|
let i: Int = 1
|
|
while i < n {
|
|
let next_comma: Int = str_index_of(str_slice(csv, i, n), ",")
|
|
if next_comma < 0 { return }
|
|
let entry: String = str_slice(csv, i, i + next_comma)
|
|
let p1: Int = str_index_of(entry, "|")
|
|
if p1 > 0 {
|
|
let fn_name: String = str_slice(entry, 0, p1)
|
|
let rest: String = str_slice(entry, p1 + 1, str_len(entry))
|
|
let p2: Int = str_index_of(rest, "|")
|
|
if p2 > 0 {
|
|
let exp_s: String = str_slice(rest, 0, p2)
|
|
let act_s: String = str_slice(rest, p2 + 1, str_len(rest))
|
|
emit_line("#error \"arity error: '" + fn_name + "' takes " + exp_s + " arguments, but called with " + act_s + "\"")
|
|
}
|
|
}
|
|
let i = i + next_comma + 1
|
|
}
|
|
}
|
|
|
|
fn add_int_name(name: String) -> Bool {
|
|
let csv: String = state_get("__int_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__int_names", csv + name + ",")
|
|
return true
|
|
}
|
|
|
|
fn add_instant_name(name: String) -> Bool {
|
|
let csv: String = state_get("__instant_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__instant_names", csv + name + ",")
|
|
return true
|
|
}
|
|
|
|
fn add_duration_name(name: String) -> Bool {
|
|
let csv: String = state_get("__duration_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__duration_names", csv + name + ",")
|
|
return true
|
|
}
|
|
|
|
fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
|
|
state_set("__int_names", ",")
|
|
state_set("__instant_names", ",")
|
|
state_set("__duration_names", ",")
|
|
let np: Int = native_list_len(params)
|
|
let pi = 0
|
|
while pi < np {
|
|
let param = native_list_get(params, pi)
|
|
let pname: String = param["name"]
|
|
let ptype: String = param["type"]
|
|
if str_eq(ptype, "Int") {
|
|
add_int_name(pname)
|
|
}
|
|
if str_eq(ptype, "Instant") {
|
|
add_instant_name(pname)
|
|
}
|
|
if str_eq(ptype, "Duration") {
|
|
add_duration_name(pname)
|
|
}
|
|
let pi = pi + 1
|
|
}
|
|
return true
|
|
}
|
|
|
|
fn cg_fn(stmt: Map<String, Any>) -> Void {
|
|
let fn_name: String = stmt["name"]
|
|
// Skip El's `fn main()` — C provides its own main() for top-level stmts
|
|
// and a duplicate `el_val_t main(void)` would collide with it.
|
|
if fn_name == "main" { return }
|
|
let params = stmt["params"]
|
|
let body = stmt["body"]
|
|
let ret_type: String = stmt["ret_type"]
|
|
let params_c: String = params_to_c(params)
|
|
// VBD role enforcement: dharma_emit / dharma_field may only be called
|
|
// from @manager-decorated functions. Surface violations to the C compiler
|
|
// via #error directives emitted before the function definition.
|
|
let decorator: String = stmt["decorator"]
|
|
if vbd_has_restricted_call(body) {
|
|
if !str_eq(decorator, "manager") {
|
|
emit_line("#error \"VBD violation: dharma_emit/dharma_field called from non-@manager fn '" + fn_name + "'\"")
|
|
}
|
|
}
|
|
// Seed the per-function int-name set so the `+` codegen can dispatch
|
|
// arithmetic vs concat on type-annotated identifiers.
|
|
build_int_names_for_params(params)
|
|
emit_line("el_val_t " + fn_name + "(" + params_c + ") {")
|
|
// Seed declared with parameter names so reassignment works
|
|
let decl = native_list_empty()
|
|
let np: Int = native_list_len(params)
|
|
let pi = 0
|
|
while pi < np {
|
|
let param = native_list_get(params, pi)
|
|
let pname: String = param["name"]
|
|
let decl = native_list_append(decl, pname)
|
|
let pi = pi + 1
|
|
}
|
|
// Lift the final bare expression into an explicit return so implicit
|
|
// returns ("fn lex(s) { ... tokens }") actually return their value.
|
|
// Void-returning functions skip this — wrapping `println(x)` in
|
|
// `return …` is a C type error.
|
|
let body_xformed = body
|
|
if !str_eq(ret_type, "Void") {
|
|
let body_xformed = transform_implicit_return(body)
|
|
}
|
|
cg_stmts(body_xformed, " ", decl)
|
|
emit_line(" return 0;")
|
|
emit_line("}")
|
|
emit_blank()
|
|
}
|
|
|
|
// ── Top-level codegen ─────────────────────────────────────────────────────────
|
|
|
|
fn is_fndef(stmt: Map<String, Any>) -> Bool {
|
|
let kind: String = stmt["stmt"]
|
|
if kind == "FnDef" { return true }
|
|
false
|
|
}
|
|
|
|
fn is_top_level_decl(stmt: Map<String, Any>) -> Bool {
|
|
let kind: String = stmt["stmt"]
|
|
if kind == "TypeDef" { return true }
|
|
if kind == "EnumDef" { return true }
|
|
if kind == "Import" { return true }
|
|
if kind == "CgiBlock" { return true }
|
|
false
|
|
}
|
|
|
|
// Format a string-or-EL_NULL argument for el_cgi_init.
|
|
fn cgi_arg(value: String, has_value: Bool) -> String {
|
|
if has_value {
|
|
return "EL_STR(" + c_str_lit(value) + ")"
|
|
}
|
|
return "EL_NULL"
|
|
}
|
|
|
|
// ── 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.
|
|
|
|
fn vbd_is_restricted_name(name: String) -> Bool {
|
|
if str_eq(name, "dharma_emit") { return true }
|
|
if str_eq(name, "dharma_field") { return true }
|
|
false
|
|
}
|
|
|
|
fn vbd_expr_has_restricted_call(expr: Map<String, Any>) -> Bool {
|
|
let kind: String = expr["expr"]
|
|
if str_eq(kind, "Call") {
|
|
let func = expr["func"]
|
|
let fk: String = func["expr"]
|
|
if str_eq(fk, "Ident") {
|
|
let fname: String = func["name"]
|
|
if vbd_is_restricted_name(fname) { return true }
|
|
}
|
|
if vbd_expr_has_restricted_call(func) { return true }
|
|
let args = expr["args"]
|
|
let an: Int = native_list_len(args)
|
|
let ai = 0
|
|
while ai < an {
|
|
let a = native_list_get(args, ai)
|
|
if vbd_expr_has_restricted_call(a) { return true }
|
|
let ai = ai + 1
|
|
}
|
|
return false
|
|
}
|
|
if str_eq(kind, "BinOp") {
|
|
let l = expr["left"]
|
|
let r = expr["right"]
|
|
if vbd_expr_has_restricted_call(l) { return true }
|
|
if vbd_expr_has_restricted_call(r) { return true }
|
|
return false
|
|
}
|
|
if str_eq(kind, "Not") {
|
|
return vbd_expr_has_restricted_call(expr["inner"])
|
|
}
|
|
if str_eq(kind, "Neg") {
|
|
return vbd_expr_has_restricted_call(expr["inner"])
|
|
}
|
|
if str_eq(kind, "Field") {
|
|
return vbd_expr_has_restricted_call(expr["object"])
|
|
}
|
|
if str_eq(kind, "Index") {
|
|
if vbd_expr_has_restricted_call(expr["object"]) { return true }
|
|
if vbd_expr_has_restricted_call(expr["index"]) { return true }
|
|
return false
|
|
}
|
|
if str_eq(kind, "Try") {
|
|
return vbd_expr_has_restricted_call(expr["inner"])
|
|
}
|
|
if str_eq(kind, "Array") {
|
|
let elems = expr["elems"]
|
|
let n: Int = native_list_len(elems)
|
|
let i = 0
|
|
while i < n {
|
|
let e = native_list_get(elems, i)
|
|
if vbd_expr_has_restricted_call(e) { return true }
|
|
let i = i + 1
|
|
}
|
|
return false
|
|
}
|
|
if str_eq(kind, "Map") {
|
|
let pairs = expr["pairs"]
|
|
let n: Int = native_list_len(pairs)
|
|
let i = 0
|
|
while i < n {
|
|
let pair = native_list_get(pairs, i)
|
|
let v = pair["value"]
|
|
if vbd_expr_has_restricted_call(v) { return true }
|
|
let i = i + 1
|
|
}
|
|
return false
|
|
}
|
|
if str_eq(kind, "If") {
|
|
if vbd_expr_has_restricted_call(expr["cond"]) { return true }
|
|
if vbd_has_restricted_call(expr["then"]) { return true }
|
|
if vbd_has_restricted_call(expr["else"]) { return true }
|
|
return false
|
|
}
|
|
if str_eq(kind, "For") {
|
|
if vbd_expr_has_restricted_call(expr["list"]) { return true }
|
|
if vbd_has_restricted_call(expr["body"]) { return true }
|
|
return false
|
|
}
|
|
if str_eq(kind, "Match") {
|
|
if vbd_expr_has_restricted_call(expr["subject"]) { return true }
|
|
let arms = expr["arms"]
|
|
let n: Int = native_list_len(arms)
|
|
let i = 0
|
|
while i < n {
|
|
let arm = native_list_get(arms, i)
|
|
let body = arm["body"]
|
|
if vbd_expr_has_restricted_call(body) { return true }
|
|
let i = i + 1
|
|
}
|
|
return false
|
|
}
|
|
false
|
|
}
|
|
|
|
fn vbd_has_restricted_call(stmts: [Map<String, Any>]) -> Bool {
|
|
let n: Int = native_list_len(stmts)
|
|
let i = 0
|
|
while i < n {
|
|
let s = native_list_get(stmts, i)
|
|
let sk: String = s["stmt"]
|
|
if str_eq(sk, "Let") {
|
|
if vbd_expr_has_restricted_call(s["value"]) { return true }
|
|
}
|
|
if str_eq(sk, "Return") {
|
|
if vbd_expr_has_restricted_call(s["value"]) { return true }
|
|
}
|
|
if str_eq(sk, "Expr") {
|
|
if vbd_expr_has_restricted_call(s["value"]) { return true }
|
|
}
|
|
if str_eq(sk, "While") {
|
|
if vbd_expr_has_restricted_call(s["cond"]) { return true }
|
|
if vbd_has_restricted_call(s["body"]) { return true }
|
|
}
|
|
if str_eq(sk, "For") {
|
|
if vbd_expr_has_restricted_call(s["list"]) { return true }
|
|
if vbd_has_restricted_call(s["body"]) { return true }
|
|
}
|
|
let i = i + 1
|
|
}
|
|
false
|
|
}
|
|
|
|
// ── 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
|
|
// (llm_call_agentic, llm_register_tool, dharma_emit,
|
|
// dharma_field, mindlink-creation).
|
|
// "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.
|
|
let n_top: Int = native_list_len(stmts)
|
|
let cgi_count = 0
|
|
let cgi_block: Map<String, Any> = { "stmt": "None" }
|
|
let svc_count = 0
|
|
let svc_block: Map<String, Any> = { "stmt": "None" }
|
|
let ti = 0
|
|
while ti < n_top {
|
|
let s = native_list_get(stmts, ti)
|
|
let sk: String = s["stmt"]
|
|
if str_eq(sk, "CgiBlock") {
|
|
let cgi_count = cgi_count + 1
|
|
if cgi_count == 1 {
|
|
let cgi_block = s
|
|
}
|
|
}
|
|
if str_eq(sk, "ServiceBlock") {
|
|
let svc_count = svc_count + 1
|
|
if svc_count == 1 {
|
|
let svc_block = s
|
|
}
|
|
}
|
|
let ti = ti + 1
|
|
}
|
|
if cgi_count > 1 {
|
|
emit_line("#error \"El: multiple cgi blocks in program (only one allowed)\"")
|
|
}
|
|
if svc_count > 1 {
|
|
emit_line("#error \"El: multiple service blocks in program (only one allowed)\"")
|
|
}
|
|
if cgi_count >= 1 {
|
|
if svc_count >= 1 {
|
|
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
|
|
// per-kind capability restrictions on every emitted call.
|
|
let kind: String = "utility"
|
|
if cgi_count >= 1 { let kind = "cgi" }
|
|
if svc_count >= 1 { let kind = "service" }
|
|
state_set("__program_kind", kind)
|
|
// Clear capability-violation accumulator from any prior compile.
|
|
state_set("__cap_violations", "")
|
|
// Clear arity-violation accumulator from any prior compile.
|
|
state_set("__arity_violations", "")
|
|
// Clear temporal-type-violation accumulator from any prior compile.
|
|
state_set("__time_violations", "")
|
|
|
|
// Preamble
|
|
emit_line("#include <stdint.h>")
|
|
emit_line("#include <stdlib.h>")
|
|
emit_line("#include \"el_runtime.h\"")
|
|
emit_blank()
|
|
|
|
// Forward declarations (skip `main` — C provides its own)
|
|
let n: Int = native_list_len(stmts)
|
|
let i = 0
|
|
while i < n {
|
|
let stmt = native_list_get(stmts, i)
|
|
let kind: String = stmt["stmt"]
|
|
if kind == "FnDef" {
|
|
let fn_name: String = stmt["name"]
|
|
if !str_eq(fn_name, "main") {
|
|
let params = stmt["params"]
|
|
let params_c: String = params_to_c(params)
|
|
emit_line("el_val_t " + fn_name + "(" + params_c + ");")
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
emit_blank()
|
|
|
|
// Top-level `let` bindings → file-scope storage. El programs use
|
|
// top-level `let GREETING = "..."` as module constants that any
|
|
// function below should be able to read. Without this pass, a top-
|
|
// level Let only declares the name inside main()'s scope and any
|
|
// function referencing it compiles to an undefined-symbol use of
|
|
// the bare name (or, with non-static linkage, fails to link).
|
|
//
|
|
// We emit each top-level Let as `el_val_t NAME = VALUE;` at file
|
|
// scope and seed the int-name set when the binding is `: Int` so
|
|
// arithmetic/concat dispatch on the name works inside functions.
|
|
// Runtime-call initializers (e.g. `let m = el_map_new(...)`) cannot
|
|
// appear in C static initializers, so we emit a non-const slot and
|
|
// initialize it at the top of main() before any user statements run.
|
|
let has_toplevel_lets = false
|
|
let i = 0
|
|
while i < n {
|
|
let stmt = native_list_get(stmts, i)
|
|
let kind: String = stmt["stmt"]
|
|
if str_eq(kind, "Let") {
|
|
let name: String = stmt["name"]
|
|
let ltype: String = stmt["type"]
|
|
if str_eq(ltype, "Int") { add_int_name(name) }
|
|
let val = stmt["value"]
|
|
let vk: String = val["expr"]
|
|
if str_eq(vk, "Int") { add_int_name(name) }
|
|
emit_line("el_val_t " + name + ";")
|
|
let has_toplevel_lets = true
|
|
}
|
|
let i = i + 1
|
|
}
|
|
if has_toplevel_lets {
|
|
emit_blank()
|
|
}
|
|
|
|
// Function definitions
|
|
let i = 0
|
|
while i < n {
|
|
let stmt = native_list_get(stmts, i)
|
|
if is_fndef(stmt) {
|
|
cg_fn(stmt)
|
|
}
|
|
let i = i + 1
|
|
}
|
|
|
|
// main(). Use _argc/_argv so El programs are free to declare their own
|
|
// local `argv` / `argc` (compiler.el itself does this) without colliding
|
|
// with the C-side parameters when fn main()'s body is folded in below.
|
|
emit_line("int main(int _argc, char** _argv) {")
|
|
emit_line(" el_runtime_init_args(_argc, _argv);")
|
|
if cgi_count >= 1 {
|
|
let cname: String = cgi_block["name"]
|
|
let cdid: String = cgi_block["dharma_id"]
|
|
let cprin: String = cgi_block["principal"]
|
|
let cnet: String = cgi_block["network"]
|
|
let ceng: String = cgi_block["engram"]
|
|
let has_did: Bool = cgi_block["has_dharma_id"]
|
|
let has_prin: Bool = cgi_block["has_principal"]
|
|
let has_net: Bool = cgi_block["has_network"]
|
|
let has_eng: Bool = cgi_block["has_engram"]
|
|
let arg_name: String = "EL_STR(" + c_str_lit(cname) + ")"
|
|
let arg_did: String = cgi_arg(cdid, has_did)
|
|
let arg_prin: String = cgi_arg(cprin, has_prin)
|
|
let arg_net: String = cgi_arg(cnet, has_net)
|
|
let arg_eng: String = cgi_arg(ceng, has_eng)
|
|
emit_line(" el_cgi_init(" + arg_name + ", " + arg_did + ", " + arg_prin + ", " + arg_net + ", " + arg_eng + ");")
|
|
}
|
|
// Seed `declared` with the names of every top-level Let so that
|
|
// cg_stmt emits plain assignment (`X = ...;`) instead of a redundant
|
|
// `el_val_t X = ...;` shadowing the file-scope slot.
|
|
let main_decl = native_list_empty()
|
|
let i = 0
|
|
while i < n {
|
|
let stmt = native_list_get(stmts, i)
|
|
let kind: String = stmt["stmt"]
|
|
if str_eq(kind, "Let") {
|
|
let name: String = stmt["name"]
|
|
let main_decl = native_list_append(main_decl, name)
|
|
}
|
|
let i = i + 1
|
|
}
|
|
// First pass: capture the body of `fn main()` if the source declared
|
|
// one. We've already skipped emitting it as a regular el_val_t
|
|
// function (see cg_fn early return); fold its body into C's main
|
|
// alongside top-level statements so the program actually runs.
|
|
let el_main_body = native_list_empty()
|
|
let i = 0
|
|
while i < n {
|
|
let stmt = native_list_get(stmts, i)
|
|
if is_fndef(stmt) {
|
|
let fn_name: String = stmt["name"]
|
|
if str_eq(fn_name, "main") {
|
|
let body = stmt["body"]
|
|
let bn: Int = native_list_len(body)
|
|
let bi: Int = 0
|
|
while bi < bn {
|
|
let el_main_body = native_list_append(el_main_body, native_list_get(body, bi))
|
|
let bi = bi + 1
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
|
|
let i = 0
|
|
while i < n {
|
|
let stmt = native_list_get(stmts, i)
|
|
if is_fndef(stmt) {
|
|
// skip - fn defs already emitted above; fn main body folded later
|
|
} else {
|
|
if is_top_level_decl(stmt) {
|
|
// skip
|
|
} else {
|
|
let main_decl = cg_stmt(stmt, " ", main_decl)
|
|
}
|
|
}
|
|
// Release AST node after final use — each stmt is fully processed
|
|
// by this point (forward decls, fn defs, top-level lets, and now
|
|
// the main-body pass are all done). Releasing here prevents the
|
|
// accumulated AST from exhausting memory on large source files.
|
|
el_release(stmt)
|
|
let i = i + 1
|
|
}
|
|
|
|
// Fold fn main()'s body in here, after top-level statements.
|
|
let mn: Int = native_list_len(el_main_body)
|
|
let mi: Int = 0
|
|
while mi < mn {
|
|
let mstmt = native_list_get(el_main_body, mi)
|
|
let main_decl = cg_stmt(mstmt, " ", main_decl)
|
|
let mi = mi + 1
|
|
}
|
|
|
|
emit_line(" return 0;")
|
|
emit_line("}")
|
|
emit_blank()
|
|
|
|
// 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
|
|
// 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, …).
|
|
emit_time_violations()
|
|
|
|
// Return empty string — output was streamed via println
|
|
""
|
|
}
|