b55e6bfd53
El SDK CI - dev / build-and-test (pull_request) Failing after 12m20s
let a: Int = 5
getint(5) == a -> str_eq(getint(5), a) SIGSEGV
getint(5) == 5 -> getint(5) == 5 fine
A function call whose return type codegen cannot infer poisoned the operator,
and a declared Int on the other side did not save it. str_eq then read an
integer as a char* and segfaulted. Only an integer LITERAL on one side forced
the numeric form, which is why the bug stayed invisible: the common case
happened to be safe.
The check required BOTH operands to be provably Int:
if is_int_expr(left) { if is_int_expr(right) { numeric } }
Loosening to OR is strictly safer, not a trade:
- when one side is a known Int, str_eq is ALWAYS wrong — it dereferences
that integer — while numeric comparison is at worst a wrong answer on a
program that was already ill-typed;
- when neither side is Int nothing changes at all, so string comparison is
untouched.
Found by the test-framework agent while building the benchmark harness; it
correctly declined to fix it mid-phase since it is a codegen semantics change.
VERIFIED, because a semantics change earns more than an assertion:
- 15/15 on a dedicated operator suite covering string literals, string vars,
string-returning calls, mixed var/call, and != in every combination. The
pre-change compiler scores 0/15 on the same file: it segfaults before
printing anything.
- self-hosting fixpoint byte-identical
- the ONLY difference in the compiler's own generated C is the intended one:
a nested if becoming two sequential ifs, in EqEq and NotEq. Nothing else
moved.
- neuron's full soul amalgam regenerates in 400ms, exit 0, output
BYTE-IDENTICAL at 1,270,212 bytes
- test_math 13/13, test_string 27/27, test_core 10/10, test_text 12/12 —
62 tests, 190 assertions, zero failures
NOT fixed here, same family, flagged for a decision: Bool PARAMETERS are not
tracked as int-like, so `cond == want` between two Bool params still lowers to
str_eq and segfaults. Found while writing this commit's own test harness — the
first version of it crashed on exactly that, on both the old and new compiler.
It needs the same treatment, and it wants its own change.
4549 lines
187 KiB
EmacsLisp
4549 lines
187 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).
|
|
// 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 {
|
|
// Batch ASCII chars using str_slice instead of str_char_at per byte.
|
|
// Track clean_start: the beginning of the current run of bytes that need
|
|
// no escaping. On each special byte, flush the accumulated clean run via
|
|
// str_slice, then append the escape. This reduces parts-list appends from
|
|
// O(N) to O(K) where K = number of special bytes << N for normal strings.
|
|
//
|
|
// Special bytes: '"'=34, '\\'=92, '\n'=10, '\r'=13, '\t'=9, any byte>=128.
|
|
//
|
|
// 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: Int = 0
|
|
let clean_start: Int = 0
|
|
let prev_was_hex_escape: Bool = false
|
|
while i < total {
|
|
let bval: Int = str_char_code(s, i)
|
|
// Handle the hex-escape split case first: if prev was \xNN and this
|
|
// byte is a hex digit, we must flush the clean run and insert "".
|
|
// (At this point clean_start == i since the previous special byte
|
|
// already reset it, so flush is a no-op unless something is pending.)
|
|
if prev_was_hex_escape {
|
|
if is_hex_digit_byte(bval) {
|
|
// Flush any accumulated clean bytes before the split marker.
|
|
if clean_start < i {
|
|
let parts = native_list_append(parts, str_slice(s, clean_start, i))
|
|
}
|
|
let parts = native_list_append(parts, "\"\"")
|
|
let clean_start = i
|
|
}
|
|
}
|
|
let prev_was_hex_escape = false
|
|
if bval == 34 {
|
|
// 34 = '"' — flush clean run, then escape
|
|
if clean_start < i {
|
|
let parts = native_list_append(parts, str_slice(s, clean_start, i))
|
|
}
|
|
let parts = native_list_append(parts, "\\\"")
|
|
let clean_start = i + 1
|
|
} else {
|
|
if bval == 92 {
|
|
// 92 = '\\'
|
|
if clean_start < i {
|
|
let parts = native_list_append(parts, str_slice(s, clean_start, i))
|
|
}
|
|
let parts = native_list_append(parts, "\\\\")
|
|
let clean_start = i + 1
|
|
} else {
|
|
if bval == 10 {
|
|
// 10 = '\n'
|
|
if clean_start < i {
|
|
let parts = native_list_append(parts, str_slice(s, clean_start, i))
|
|
}
|
|
let parts = native_list_append(parts, "\\n")
|
|
let clean_start = i + 1
|
|
} else {
|
|
if bval == 13 {
|
|
// 13 = '\r'
|
|
if clean_start < i {
|
|
let parts = native_list_append(parts, str_slice(s, clean_start, i))
|
|
}
|
|
let parts = native_list_append(parts, "\\r")
|
|
let clean_start = i + 1
|
|
} else {
|
|
if bval == 9 {
|
|
// 9 = '\t'
|
|
if clean_start < i {
|
|
let parts = native_list_append(parts, str_slice(s, clean_start, i))
|
|
}
|
|
let parts = native_list_append(parts, "\\t")
|
|
let clean_start = i + 1
|
|
} else {
|
|
if bval >= 128 {
|
|
// Non-ASCII: flush, then \xNN
|
|
if clean_start < i {
|
|
let parts = native_list_append(parts, str_slice(s, clean_start, i))
|
|
}
|
|
let parts = native_list_append(parts, "\\x" + byte_to_hex2(bval))
|
|
let prev_was_hex_escape = true
|
|
let clean_start = i + 1
|
|
}
|
|
// else: plain ASCII — extends the current clean run (no append)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
// Flush the final clean run if any
|
|
if clean_start < total {
|
|
let parts = native_list_append(parts, str_slice(s, clean_start, total))
|
|
}
|
|
let result: String = str_join(parts, "")
|
|
// parts list fully consumed — release to free peak heap.
|
|
el_release(parts)
|
|
result
|
|
}
|
|
|
|
fn c_str_lit(s: String) -> String {
|
|
"\"" + c_escape(s) + "\""
|
|
}
|
|
|
|
// sanitize_test_name — convert a test name string to a valid C identifier fragment.
|
|
// "int-to-str" -> "int_to_str", "lex empty" -> "lex_empty"
|
|
fn sanitize_test_name(name: String) -> String {
|
|
let n: Int = str_len(name)
|
|
let i: Int = 0
|
|
let out: String = ""
|
|
while i < n {
|
|
let code: Int = str_char_code(name, i)
|
|
// a-z: 97-122, A-Z: 65-90, 0-9: 48-57 — keep; everything else -> '_'
|
|
if code >= 97 {
|
|
if code <= 122 {
|
|
let out = out + str_char_at(name, i)
|
|
} else {
|
|
let out = out + "_"
|
|
}
|
|
} else {
|
|
if code >= 65 {
|
|
if code <= 90 {
|
|
let out = out + str_char_at(name, i)
|
|
} else {
|
|
if code >= 48 {
|
|
if code <= 57 {
|
|
let out = out + str_char_at(name, i)
|
|
} else {
|
|
let out = out + "_"
|
|
}
|
|
} else {
|
|
let out = out + "_"
|
|
}
|
|
}
|
|
} else {
|
|
if code >= 48 {
|
|
if code <= 57 {
|
|
let out = out + str_char_at(name, i)
|
|
} else {
|
|
let out = out + "_"
|
|
}
|
|
} else {
|
|
let out = out + "_"
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
out
|
|
}
|
|
|
|
// -- 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 == "Percent" { 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"
|
|
}
|
|
|
|
// ── HTML template codegen ─────────────────────────────────────────────────
|
|
//
|
|
// cg_html_template(expr) emits a C statement-expression `({ ... })` that
|
|
// builds the HTML string by chaining el_str_concat calls.
|
|
//
|
|
// Interpolated values are passed through html_escape(); the raw() form
|
|
// bypasses escaping. {#each} blocks compile to C for-loops that index into
|
|
// the list with el_list_get / el_list_len.
|
|
//
|
|
// A per-template accumulator variable `_html_N` holds the growing string.
|
|
// A global counter stored in state keeps names unique.
|
|
|
|
fn next_html_id() -> String {
|
|
let csv: String = state_get("__html_counter")
|
|
let n = 0
|
|
if !str_eq(csv, "") {
|
|
let n = str_to_int(csv)
|
|
}
|
|
let n = n + 1
|
|
state_set("__html_counter", native_int_to_str(n))
|
|
native_int_to_str(n)
|
|
}
|
|
|
|
// Emit children nodes into a flat list of C fragment strings (parts).
|
|
// Each part is either a static string fragment (already C-literal form) or
|
|
// a dynamic expression that produces an el_val_t string.
|
|
// We build them all into parts, then the caller wraps with concat chain.
|
|
|
|
fn cg_html_parts(children: [Map<String, Any>], acc_var: String) -> String {
|
|
// Accumulate fragments into a list to avoid O(n²) string growth.
|
|
// Each append is O(1); the single str_join at the end is O(total_size).
|
|
let n: Int = native_list_len(children)
|
|
let i = 0
|
|
let parts: [String] = native_list_empty()
|
|
while i < n {
|
|
let child: Map<String, Any> = native_list_get(children, i)
|
|
let html_kind: String = child["html"]
|
|
if str_eq(html_kind, "Text") {
|
|
let text: String = child["text"]
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(text) + ")); ")
|
|
}
|
|
if str_eq(html_kind, "Doctype") {
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<!doctype html>\")); ")
|
|
}
|
|
if str_eq(html_kind, "Interp") {
|
|
let val_node = child["value"]
|
|
let val_c: String = cg_expr(val_node)
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); ")
|
|
}
|
|
if str_eq(html_kind, "Raw") {
|
|
let val_node = child["value"]
|
|
let val_c: String = cg_expr(val_node)
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", html_raw(" + val_c + ")); ")
|
|
}
|
|
if str_eq(html_kind, "Element") {
|
|
let elem_c: String = cg_html_element_str(child, acc_var)
|
|
let parts = native_list_append(parts, elem_c)
|
|
}
|
|
if str_eq(html_kind, "Each") {
|
|
let each_c: String = cg_html_each(child, acc_var)
|
|
let parts = native_list_append(parts, each_c)
|
|
}
|
|
if str_eq(html_kind, "HtmlIf") {
|
|
let if_c: String = cg_html_if(child, acc_var)
|
|
let parts = native_list_append(parts, if_c)
|
|
}
|
|
let i = i + 1
|
|
}
|
|
str_join(parts, "")
|
|
}
|
|
|
|
// Generate open-tag attribute fragments inline.
|
|
// Parser stores attrs with "kind": "static" | "dynamic" | "bool".
|
|
// Static: "value" is the raw string value (not an expr node).
|
|
// Dynamic: "value" is an expr node.
|
|
// Bool: no "value" field.
|
|
fn cg_html_attrs_str(attrs: [Map<String, Any>], acc_var: String) -> String {
|
|
// Accumulate fragments into a list to avoid O(n²) string growth.
|
|
let n: Int = native_list_len(attrs)
|
|
let i = 0
|
|
let parts: [String] = native_list_empty()
|
|
// Closing-quote snippet: EL_STR("\"") in C text.
|
|
let close_q: String = "EL_STR(" + c_str_lit("\"") + ")"
|
|
while i < n {
|
|
let attr: Map<String, Any> = native_list_get(attrs, i)
|
|
let attr_name: String = attr["name"]
|
|
let kind: String = attr["kind"]
|
|
// Build: EL_STR(" name=\"")
|
|
let open_val: String = " " + attr_name + "=\""
|
|
let open_attr: String = "EL_STR(" + c_str_lit(open_val) + ")"
|
|
if str_eq(kind, "static") {
|
|
// Static attribute: value is a raw string.
|
|
let sv: String = attr["value"]
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); ")
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(sv) + ")); ")
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); ")
|
|
} else {
|
|
if str_eq(kind, "dynamic") {
|
|
// Dynamic attribute: value is an expr node — html_escape it.
|
|
let val_node = attr["value"]
|
|
let val_c: String = cg_expr(val_node)
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); ")
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); ")
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); ")
|
|
} else {
|
|
// Boolean attribute (no value): emit " name"
|
|
let bool_attr: String = "EL_STR(" + c_str_lit(" " + attr_name) + ")"
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", " + bool_attr + "); ")
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
str_join(parts, "")
|
|
}
|
|
|
|
// Generate code for a single element, appending into acc_var.
|
|
fn cg_html_element_str(elem: Map<String, Any>, acc_var: String) -> String {
|
|
let tag: String = elem["tag"]
|
|
let attrs: [Map<String, Any>] = elem["attrs"]
|
|
let children: [Map<String, Any>] = elem["children"]
|
|
let self_closing: Bool = elem["self_closing"]
|
|
// Accumulate into a list to avoid O(n²) string growth for deeply nested trees.
|
|
let parts: [String] = native_list_empty()
|
|
// Open tag: <tagname
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"<" + tag + "\")); ")
|
|
let parts = native_list_append(parts, cg_html_attrs_str(attrs, acc_var))
|
|
if self_closing {
|
|
// Self-closing void element: />
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"/>\")); ")
|
|
} else {
|
|
// Close open tag: >
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\">\")); ")
|
|
let parts = native_list_append(parts, cg_html_parts(children, acc_var))
|
|
let parts = native_list_append(parts, acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"</" + tag + ">\")); ")
|
|
}
|
|
str_join(parts, "")
|
|
}
|
|
|
|
// Generate code for {#each list as item} ... {/each}.
|
|
fn cg_html_each(node: Map<String, Any>, acc_var: String) -> String {
|
|
let list_expr = node["list"]
|
|
let item_name: String = node["item"]
|
|
let body_children: [Map<String, Any>] = node["body"]
|
|
let id: String = next_html_id()
|
|
let list_var: String = "_html_list_" + id
|
|
let len_var: String = "_html_len_" + id
|
|
let idx_var: String = "_html_i_" + id
|
|
let list_c: String = cg_expr(list_expr)
|
|
let inner_c: String = cg_html_parts(body_children, acc_var)
|
|
// Emit: { el_val_t _list = expr; int _len = el_list_len(_list);
|
|
// for (int _i = 0; _i < _len; _i++) {
|
|
// el_val_t item = el_list_get(_list, _i); inner_c } }
|
|
"{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } "
|
|
}
|
|
|
|
// Generate code for {#if cond} ... {/if} (with optional {#else}).
|
|
fn cg_html_if(node: Map<String, Any>, acc_var: String) -> String {
|
|
let cond_expr = node["cond"]
|
|
let then_children: [Map<String, Any>] = node["then"]
|
|
let else_children: [Map<String, Any>] = node["else"]
|
|
let cond_c: String = cg_expr(cond_expr)
|
|
let then_c: String = cg_html_parts(then_children, acc_var)
|
|
let else_c: String = cg_html_parts(else_children, acc_var)
|
|
"if (" + cond_c + ") { " + then_c + " } else { " + else_c + " } "
|
|
}
|
|
|
|
// Top-level HTML template codegen — returns a C statement-expression string.
|
|
fn cg_html_template(expr: Map<String, Any>) -> String {
|
|
let root = expr["root"]
|
|
let id: String = next_html_id()
|
|
let acc: String = "_html_" + id
|
|
// If the root element has doctype:true the parser tagged it from <!doctype html>
|
|
let doctype_flag: Bool = root["doctype"]
|
|
let doctype_prefix: String = ""
|
|
if doctype_flag {
|
|
let doctype_prefix = acc + " = el_str_concat(" + acc + ", EL_STR(\"<!doctype html>\")); "
|
|
}
|
|
let body: String = cg_html_element_str(root, acc)
|
|
"({ el_val_t " + acc + " = EL_STR(\"\"); " + doctype_prefix + body + acc + "; })"
|
|
}
|
|
|
|
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_kind: String = inner["expr"]
|
|
// Float literal negation: emit el_from_float(-n) so the IEEE 754 sign
|
|
// bit is set correctly. Arithmetic negation of the int64 bit pattern
|
|
// (the el_val_t representation) produces garbage, not -f.
|
|
if str_eq(inner_kind, "Float") {
|
|
let fval: String = inner["value"]
|
|
return "el_from_float(-" + fval + ")"
|
|
}
|
|
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"]
|
|
|
|
// -- 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
|
|
// 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.
|
|
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 + ")" }
|
|
}
|
|
if str_eq(op, "EqEq") {
|
|
if str_eq(left_kind, "Str") { return "str_eq(" + left_c + ", " + right_c + ")" }
|
|
if str_eq(right_kind, "Str") { return "str_eq(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
if str_eq(op, "NotEq") {
|
|
if str_eq(left_kind, "Str") { return "!str_eq(" + left_c + ", " + right_c + ")" }
|
|
if str_eq(right_kind, "Str") { return "!str_eq(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
|
|
// -- 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)
|
|
|
|
// Phase 1.5 LocalDate / LocalTime / CalendarTime dispatch. These
|
|
// route through their typed runtime wrappers (el_local_date_add_dur,
|
|
// el_local_time_add_dur, el_local_date_lt, el_local_date_eq) and
|
|
// forbid mismatched ops at codegen time. Cross-calendar arithmetic
|
|
// (CalendarTime + CalendarTime, CalendarTime - CalendarTime under
|
|
// mismatched calendars) is structurally meaningless: a CalendarTime
|
|
// already projects an Instant under a Calendar, so subtraction
|
|
// between two of them only makes sense in instant-space (use
|
|
// cal_to_instant first).
|
|
let left_is_ld: Bool = is_localdate_expr(left)
|
|
let right_is_ld: Bool = is_localdate_expr(right)
|
|
let left_is_lt: Bool = is_localtime_expr(left)
|
|
let right_is_lt: Bool = is_localtime_expr(right)
|
|
let left_is_ct: Bool = is_caltime_expr(left)
|
|
let right_is_ct: Bool = is_caltime_expr(right)
|
|
if left_is_ld {
|
|
if op == "Plus" {
|
|
if right_is_dur {
|
|
return "el_local_date_add_dur(" + left_c + ", " + right_c + ")"
|
|
}
|
|
}
|
|
if op == "Lt" {
|
|
if right_is_ld { return "el_local_date_lt(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
if op == "EqEq" {
|
|
if right_is_ld { return "el_local_date_eq(" + left_c + ", " + right_c + ")" }
|
|
}
|
|
}
|
|
if left_is_lt {
|
|
if op == "Plus" {
|
|
if right_is_dur {
|
|
return "el_local_time_add_dur(" + left_c + ", " + right_c + ")"
|
|
}
|
|
}
|
|
}
|
|
if left_is_ct {
|
|
if op == "Plus" {
|
|
if right_is_ct {
|
|
time_record_violation("caltime_plus_caltime", "CalendarTime + CalendarTime is not allowed (use cal_to_instant + Duration)")
|
|
return "0 /* TIME_TYPE_ERROR: CalendarTime + CalendarTime */"
|
|
}
|
|
}
|
|
}
|
|
|
|
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).
|
|
}
|
|
|
|
// -- Float arithmetic / comparison dispatch -------------------------
|
|
// When either operand is provably Float, promote BOTH operands to a C
|
|
// double and emit real double arithmetic. Arithmetic results are
|
|
// re-wrapped into the el_val_t float slot via el_from_float(); the
|
|
// integer % becomes fmod(); comparisons yield a bare 0/1. Without this
|
|
// path, float `+` fell to el_str_concat (segfault) and `- * / %` fell
|
|
// to integer ops on the raw IEEE-754 bit pattern (garbage). Mixed
|
|
// Int/Float promotes the Int side with a (double) cast. This runs
|
|
// AFTER temporal dispatch (a Float is never temporal) and BEFORE the
|
|
// Int/String paths, and is skipped when either side is a bare Str so
|
|
// string concat/equality is never disturbed.
|
|
if !str_eq(left_kind, "Str") {
|
|
if !str_eq(right_kind, "Str") {
|
|
let any_float: Bool = false
|
|
if is_float_expr(left) { let any_float = true }
|
|
if is_float_expr(right) { let any_float = true }
|
|
if any_float {
|
|
let l_d: String = float_operand_c(left, left_c)
|
|
let r_d: String = float_operand_c(right, right_c)
|
|
if str_eq(op, "Plus") { return "el_from_float(" + l_d + " + " + r_d + ")" }
|
|
if str_eq(op, "Minus") { return "el_from_float(" + l_d + " - " + r_d + ")" }
|
|
if str_eq(op, "Star") { return "el_from_float(" + l_d + " * " + r_d + ")" }
|
|
if str_eq(op, "Slash") { return "el_from_float(" + l_d + " / " + r_d + ")" }
|
|
if str_eq(op, "Percent") { return "el_from_float(fmod(" + l_d + ", " + r_d + "))" }
|
|
if str_eq(op, "Lt") { return "(" + l_d + " < " + r_d + ")" }
|
|
if str_eq(op, "Gt") { return "(" + l_d + " > " + r_d + ")" }
|
|
if str_eq(op, "LtEq") { return "(" + l_d + " <= " + r_d + ")" }
|
|
if str_eq(op, "GtEq") { return "(" + l_d + " >= " + r_d + ")" }
|
|
if str_eq(op, "EqEq") { return "(" + l_d + " == " + r_d + ")" }
|
|
if str_eq(op, "NotEq") { return "(" + l_d + " != " + r_d + ")" }
|
|
// And/Or with a float operand is ill-typed — fall through.
|
|
}
|
|
}
|
|
}
|
|
|
|
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.
|
|
// EITHER side provably Int is enough. Requiring BOTH meant a call
|
|
// whose return type codegen cannot infer poisoned the operator:
|
|
// getint(5) == a -> str_eq(getint(5), a)
|
|
// even with `a` declared Int. str_eq then reads an integer as a
|
|
// char* and segfaults. Only an integer LITERAL on one side forced
|
|
// the numeric form, so the bug was invisible in the common case.
|
|
//
|
|
// Loosening to OR is strictly safer: when one side is a known Int,
|
|
// str_eq is always wrong (it dereferences that int), while numeric
|
|
// comparison is at worst a wrong answer on an already ill-typed
|
|
// program. When neither side is Int nothing changes, so string
|
|
// comparison is untouched.
|
|
if is_int_expr(left) {
|
|
return "(" + left_c + " == " + right_c + ")"
|
|
}
|
|
if is_int_expr(right) {
|
|
return "(" + left_c + " == " + right_c + ")"
|
|
}
|
|
// Float literal or negative float literal: use plain == (bit-equal
|
|
// el_val_t comparison). This handles `r0 == 3.0`, `neg == -3.0`, etc.
|
|
if is_float_expr(left) {
|
|
return "(" + left_c + " == " + right_c + ")"
|
|
}
|
|
if is_float_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.
|
|
// Either side Int is enough — see the EqEq note above.
|
|
if is_int_expr(left) {
|
|
return "(" + left_c + " != " + right_c + ")"
|
|
}
|
|
if is_int_expr(right) {
|
|
return "(" + left_c + " != " + right_c + ")"
|
|
}
|
|
// Float-typed operands use plain != (bit-equal comparison).
|
|
if is_float_expr(left) {
|
|
return "(" + left_c + " != " + right_c + ")"
|
|
}
|
|
if is_float_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_parts: [String] = native_list_empty()
|
|
let i = 0
|
|
while i < arity {
|
|
let arg = native_list_get(args, i)
|
|
let arg_c: String = cg_expr(arg)
|
|
let args_parts = native_list_append(args_parts, arg_c)
|
|
let i = i + 1
|
|
}
|
|
let args_c: String = str_join(args_parts, ", ")
|
|
// args_parts list fully consumed — release to free peak heap.
|
|
el_release(args_parts)
|
|
|
|
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 + ")"
|
|
}
|
|
}
|
|
}
|
|
// 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 + ")"
|
|
}
|
|
|
|
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_parts: [String] = native_list_empty()
|
|
let i = 0
|
|
while i < n {
|
|
let elem = native_list_get(elems, i)
|
|
let elem_c: String = cg_expr(elem)
|
|
let items_parts = native_list_append(items_parts, elem_c)
|
|
let i = i + 1
|
|
}
|
|
let items_joined: String = str_join(items_parts, ", ")
|
|
// items_parts fully consumed — release to free peak heap.
|
|
el_release(items_parts)
|
|
return "el_list_new(" + native_int_to_str(n) + ", " + items_joined + ")"
|
|
}
|
|
|
|
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_parts: [String] = native_list_empty()
|
|
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)
|
|
let items_parts = native_list_append(items_parts, c_str_lit(key) + ", " + val_c)
|
|
let i = i + 1
|
|
}
|
|
let items_joined: String = str_join(items_parts, ", ")
|
|
// items_parts fully consumed — release to free peak heap.
|
|
el_release(items_parts)
|
|
return "el_map_new(" + native_int_to_str(n) + ", " + items_joined + ")"
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if kind == "HtmlTemplate" {
|
|
return cg_html_template(expr)
|
|
}
|
|
|
|
if kind == "Lambda" {
|
|
// Lambda expressions are JS-target only. In the C target, emit EL_NULL.
|
|
"EL_NULL"
|
|
} else {
|
|
"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
|
|
// 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)
|
|
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 parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
|
} else {
|
|
if str_eq(pkind, "Binding") {
|
|
let bname: String = pat["name"]
|
|
let parts = native_list_append(parts, "{ 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 parts = native_list_append(parts, "if (" + subj_var + " == " + v + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
|
} else {
|
|
if str_eq(pkind, "LitStr") {
|
|
let v: String = pat["value"]
|
|
let parts = native_list_append(parts, "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 parts = native_list_append(parts, "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
|
} else {
|
|
if str_eq(pkind, "Variant") {
|
|
// Enum::Variant pattern — match against the variant name
|
|
// string (El enums compile to plain strings).
|
|
let variant: String = pat["variant"]
|
|
let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(variant) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
|
} else {
|
|
// unknown pattern -> wildcard
|
|
let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
let parts = native_list_append(parts, done_label + ":; " + result_var + "; })")
|
|
let result: String = str_join(parts, "")
|
|
// parts list fully consumed — release to free peak heap.
|
|
el_release(parts)
|
|
result
|
|
}
|
|
|
|
// Lower a match statement (used for side effects, not as an expression) to a
|
|
// chain of C if/else if/else blocks. The subject is evaluated once into a
|
|
// scoped temporary; each arm generates a condition and a braced body; the
|
|
// wildcard/binding arm becomes the final `else` branch.
|
|
//
|
|
// Pattern dispatch:
|
|
// LitStr -> str_eq(subj, EL_STR("..."))
|
|
// LitInt -> subj == N
|
|
// LitBool -> subj == 1 / subj == 0
|
|
// Binding -> else { el_val_t name = subj; <body> }
|
|
// Wildcard -> else { <body> }
|
|
fn cg_match_stmt(expr: Map<String, Any>, indent: String, declared: [String]) -> Void {
|
|
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 inner: String = indent + " "
|
|
emit_line(indent + "{")
|
|
emit_line(inner + "el_val_t " + subj_var + " = " + subj_c + ";")
|
|
let n: Int = native_list_len(arms)
|
|
let i = 0
|
|
let first_cond: Bool = true
|
|
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, "LitStr") {
|
|
let v: String = pat["value"]
|
|
let cond_str = "str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))"
|
|
if first_cond {
|
|
emit_line(inner + "if (" + cond_str + ") {")
|
|
let first_cond = false
|
|
} else {
|
|
emit_line(inner + "} else if (" + cond_str + ") {")
|
|
}
|
|
emit_line(inner + " " + body_c + ";")
|
|
} else {
|
|
if str_eq(pkind, "LitInt") {
|
|
let v: String = pat["value"]
|
|
let cond_str = subj_var + " == " + v
|
|
if first_cond {
|
|
emit_line(inner + "if (" + cond_str + ") {")
|
|
let first_cond = false
|
|
} else {
|
|
emit_line(inner + "} else if (" + cond_str + ") {")
|
|
}
|
|
emit_line(inner + " " + body_c + ";")
|
|
} else {
|
|
if str_eq(pkind, "LitBool") {
|
|
let v: String = pat["value"]
|
|
let bv = "0"
|
|
if str_eq(v, "true") {
|
|
let bv = "1"
|
|
}
|
|
let cond_str = subj_var + " == " + bv
|
|
if first_cond {
|
|
emit_line(inner + "if (" + cond_str + ") {")
|
|
let first_cond = false
|
|
} else {
|
|
emit_line(inner + "} else if (" + cond_str + ") {")
|
|
}
|
|
emit_line(inner + " " + body_c + ";")
|
|
} else {
|
|
// Wildcard or Binding - becomes the else branch
|
|
if first_cond {
|
|
emit_line(inner + "{")
|
|
} else {
|
|
emit_line(inner + "} else {")
|
|
}
|
|
if str_eq(pkind, "Binding") {
|
|
let bname: String = pat["name"]
|
|
emit_line(inner + " el_val_t " + bname + " = " + subj_var + ";")
|
|
}
|
|
emit_line(inner + " " + body_c + ";")
|
|
emit_line(inner + "}")
|
|
let first_cond = true
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
// Close any open if/else-if chain (only reached when last arm was a literal pattern)
|
|
if !first_cond {
|
|
emit_line(inner + "}")
|
|
}
|
|
emit_line(indent + "}")
|
|
}
|
|
|
|
// -- 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)
|
|
}
|
|
|
|
// is_void_builtin — true for runtime builtins declared `void` in el_runtime.h.
|
|
// User `-> Void` functions are emitted as el_val_t (return 0) so they are safe
|
|
// to assign; only these C-level void builtins are not.
|
|
fn is_void_builtin(name: String) -> Bool {
|
|
if str_eq(name, "println") { return true }
|
|
if str_eq(name, "print") { return true }
|
|
if str_eq(name, "engram_strengthen") { return true }
|
|
if str_eq(name, "engram_forget") { return true }
|
|
if str_eq(name, "engram_connect") { return true }
|
|
if str_eq(name, "dharma_emit") { return true }
|
|
if str_eq(name, "dharma_strengthen") { return true }
|
|
if str_eq(name, "llm_register_tool") { return true }
|
|
if str_eq(name, "exit_program") { return true }
|
|
if str_eq(name, "http_serve") { return true }
|
|
if str_eq(name, "http_set_handler") { return true }
|
|
if str_eq(name, "http_serve_async") { return true }
|
|
if str_eq(name, "el_cgi_init") { return true }
|
|
if str_eq(name, "el_retain") { return true }
|
|
if str_eq(name, "el_release") { return true }
|
|
false
|
|
}
|
|
|
|
// cg_expr_is_void — true if `val` is a direct call to a void builtin, so the
|
|
// if-expression arm must emit it as a bare statement rather than assigning its
|
|
// (nonexistent) value to the result var.
|
|
fn cg_expr_is_void(val: Map<String, Any>) -> Bool {
|
|
let vk: String = val["expr"]
|
|
if str_eq(vk, "Call") {
|
|
let f = val["func"]
|
|
let fk: String = f["expr"]
|
|
if str_eq(fk, "Ident") {
|
|
return is_void_builtin(f["name"])
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
// 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)
|
|
// Collect statement fragments into a list to avoid O(n-) string growth.
|
|
let parts: [String] = native_list_empty()
|
|
// Track names already declared in this arm's C block. El permits `let x`
|
|
// to redeclare/rebind x in the same scope, but C forbids redeclaring the
|
|
// same name in one block: emit `el_val_t x = ...` first, `x = ...` after.
|
|
let declared: [String] = native_list_empty()
|
|
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)
|
|
if list_contains(declared, name) {
|
|
let parts = native_list_append(parts, name + " = " + val_c + "; ")
|
|
} else {
|
|
let declared = native_list_append(declared, name)
|
|
let parts = native_list_append(parts, "el_val_t " + name + " = " + val_c + "; ")
|
|
}
|
|
} else {
|
|
if str_eq(sk, "Return") {
|
|
let val = s["value"]
|
|
let val_c: String = cg_expr(val)
|
|
if cg_expr_is_void(val) {
|
|
let parts = native_list_append(parts, val_c + "; ")
|
|
} else {
|
|
let parts = native_list_append(parts, result_var + " = (" + val_c + "); ")
|
|
}
|
|
} else {
|
|
if str_eq(sk, "Expr") {
|
|
let val = s["value"]
|
|
let val_c: String = cg_expr(val)
|
|
if is_last {
|
|
if cg_expr_is_void(val) {
|
|
let parts = native_list_append(parts, val_c + "; ")
|
|
} else {
|
|
let parts = native_list_append(parts, result_var + " = (" + val_c + "); ")
|
|
}
|
|
} else {
|
|
let parts = native_list_append(parts, "(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 parts = native_list_append(parts, 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
|
|
}
|
|
let result: String = str_join(parts, "")
|
|
// parts list fully consumed — release to free peak heap.
|
|
el_release(parts)
|
|
result
|
|
}
|
|
|
|
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)
|
|
}
|
|
if str_eq(ltype, "Float") {
|
|
add_float_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)
|
|
}
|
|
if str_eq(ltype, "Calendar") {
|
|
add_calendar_name(name)
|
|
}
|
|
if str_eq(ltype, "CalendarTime") {
|
|
add_caltime_name(name)
|
|
}
|
|
if str_eq(ltype, "Rhythm") {
|
|
add_rhythm_name(name)
|
|
}
|
|
if str_eq(ltype, "LocalDate") {
|
|
add_localdate_name(name)
|
|
}
|
|
if str_eq(ltype, "LocalTime") {
|
|
add_localtime_name(name)
|
|
}
|
|
if str_eq(ltype, "LocalDateTime") {
|
|
add_localdt_name(name)
|
|
}
|
|
if str_eq(ltype, "Zone") {
|
|
add_zone_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)
|
|
}
|
|
if is_calendar_expr(val) {
|
|
add_calendar_name(name)
|
|
}
|
|
if is_caltime_expr(val) {
|
|
add_caltime_name(name)
|
|
}
|
|
if is_rhythm_expr(val) {
|
|
add_rhythm_name(name)
|
|
}
|
|
if is_localdate_expr(val) {
|
|
add_localdate_name(name)
|
|
}
|
|
if is_localtime_expr(val) {
|
|
add_localtime_name(name)
|
|
}
|
|
if is_localdt_expr(val) {
|
|
add_localdt_name(name)
|
|
}
|
|
if is_zone_expr(val) {
|
|
add_zone_name(name)
|
|
}
|
|
// Float inference: an unannotated `let` whose RHS is provably Float
|
|
// (literal, math builtin, or float arithmetic) registers the name so
|
|
// later `name <op> ...` routes through the double path.
|
|
if is_float_expr(val) {
|
|
add_float_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
|
|
}
|
|
|
|
if kind == "Break" {
|
|
emit_line(indent + "break;")
|
|
return declared
|
|
}
|
|
|
|
if kind == "Continue" {
|
|
emit_line(indent + "continue;")
|
|
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
|
|
}
|
|
if val_kind == "Match" {
|
|
cg_match_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 == "ForRange" {
|
|
let var_name: String = stmt["var"]
|
|
let start_expr = stmt["start"]
|
|
let end_expr = stmt["end"]
|
|
let inclusive: Bool = stmt["inclusive"]
|
|
let body = stmt["body"]
|
|
let start_c: String = cg_expr(start_expr)
|
|
let end_c: String = cg_expr(end_expr)
|
|
// Loop variable introduced as a C local scoped to the for statement.
|
|
// Body gets its own declared clone so let-bindings don't leak out.
|
|
let body_decl = native_list_clone(declared)
|
|
let body_decl = native_list_append(body_decl, var_name)
|
|
if inclusive {
|
|
emit_line(indent + "for (el_val_t " + var_name + " = " + start_c + "; " + var_name + " <= " + end_c + "; " + var_name + "++) {")
|
|
} else {
|
|
emit_line(indent + "for (el_val_t " + var_name + " = " + start_c + "; " + var_name + " < " + end_c + "; " + var_name + "++) {")
|
|
}
|
|
cg_stmts(body, indent + " ", body_decl)
|
|
emit_line(indent + "}")
|
|
return declared
|
|
}
|
|
|
|
if kind == "FnDef" { return declared }
|
|
if kind == "TypeDef" { return declared }
|
|
if kind == "EnumDef" { return declared }
|
|
if kind == "Import" { return declared }
|
|
if kind == "ExternFn" { return declared }
|
|
if kind == "CgiBlock" { return declared }
|
|
// TryCatch: browser-only control flow. In the C target, emit a comment
|
|
// noting that the try body runs unconditionally; error handling is a no-op.
|
|
// Programs that rely on catching JS exceptions should compile with --target=js.
|
|
if kind == "TryCatch" {
|
|
let try_body = stmt["try_body"]
|
|
emit_line(indent + "/* try (C target: exception handling not supported) */")
|
|
cg_stmts(try_body, indent, native_list_clone(declared))
|
|
return declared
|
|
}
|
|
|
|
// assert <cond> , <msg> — test harness assertion
|
|
if kind == "Assert" {
|
|
let cond_node = stmt["cond"]
|
|
let msg_node = stmt["msg"]
|
|
let c_cond: String = cg_expr(cond_node)
|
|
let c_msg: String = ""
|
|
let msg_kind: String = msg_node["expr"]
|
|
if str_eq(msg_kind, "Str") {
|
|
let raw_msg: String = msg_node["value"]
|
|
let c_msg = "\"" + c_escape(raw_msg) + "\""
|
|
} else {
|
|
let c_msg = "EL_STR_PTR(" + cg_expr(msg_node) + ")"
|
|
}
|
|
// Assertions record into PER-TEST state, not global counters. The test
|
|
// is the unit of result; a global pass/fail tally cannot say which test
|
|
// failed or whether a test ran at all. Reporting is the runner's job —
|
|
// nothing is printed here.
|
|
emit_line(indent + "if (!(" + c_cond + ")) {")
|
|
emit_line(indent + " __el_test_fail(" + c_msg + ");")
|
|
emit_line(indent + "} else { __el_cur_asserts++; }")
|
|
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 {
|
|
return str_slice(s, 1, n - 1)
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
// Per-statement arena scope: free intermediate strings (str_concat
|
|
// fragments, cg_expr results) after each statement is emitted.
|
|
let s_mark: Any = el_arena_push()
|
|
let decl = cg_stmt(stmt, indent, decl)
|
|
el_arena_pop(s_mark)
|
|
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 parts: [String] = native_list_empty()
|
|
let i = 0
|
|
while i < n {
|
|
let param = native_list_get(params, i)
|
|
let decl: String = param_decl(param, i)
|
|
let parts = native_list_append(parts, decl)
|
|
let i = i + 1
|
|
}
|
|
let result: String = str_join(parts, ", ")
|
|
// parts list fully consumed — release to free peak heap.
|
|
el_release(parts)
|
|
result
|
|
}
|
|
|
|
// 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 + ",")
|
|
}
|
|
|
|
// Float-typed binding set — parallel to is_int_name. Populated from `let x:
|
|
// Float = ...` annotations, `: Float` parameters, and inference from a Float
|
|
// RHS. Consulted by is_float_expr to route arithmetic through the double path.
|
|
fn is_float_name(name: String) -> Bool {
|
|
let csv: String = state_get("__float_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, "str_count") { return true }
|
|
if str_eq(name, "str_count_chars") { return true }
|
|
if str_eq(name, "str_count_bytes") { return true }
|
|
if str_eq(name, "str_count_lines") { return true }
|
|
if str_eq(name, "str_count_words") { return true }
|
|
if str_eq(name, "str_count_letters") { return true }
|
|
if str_eq(name, "str_count_digits") { return true }
|
|
if str_eq(name, "str_last_index_of") { return true }
|
|
if str_eq(name, "str_find_chars") { 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
|
|
}
|
|
|
|
// Known runtime builtins that return Float. Parallel to is_int_call — lets a
|
|
// Call participate in float arithmetic (and get inferred into __float_names on
|
|
// an unannotated `let`). New Float-returning builtins must be added here.
|
|
fn is_float_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, "int_to_float") { return true }
|
|
if str_eq(name, "str_to_float") { return true }
|
|
if str_eq(name, "json_get_float") { return true }
|
|
if str_eq(name, "decimal_round") { return true }
|
|
if str_eq(name, "math_pi") { return true }
|
|
if str_eq(name, "math_sin") { return true }
|
|
if str_eq(name, "math_cos") { return true }
|
|
if str_eq(name, "math_sqrt") { return true }
|
|
if str_eq(name, "math_log") { return true }
|
|
if str_eq(name, "math_ln") { return true }
|
|
if str_eq(name, "el_from_float") { 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
|
|
}
|
|
|
|
// 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
|
|
// arithmetic (e.g. CalendarTime + CalendarTime, LocalDate < CalendarTime).
|
|
fn is_calendar_name(name: String) -> Bool {
|
|
let csv: String = state_get("__calendar_names")
|
|
if str_eq(csv, "") { return false }
|
|
return str_contains(csv, "," + name + ",")
|
|
}
|
|
|
|
fn is_caltime_name(name: String) -> Bool {
|
|
let csv: String = state_get("__caltime_names")
|
|
if str_eq(csv, "") { return false }
|
|
return str_contains(csv, "," + name + ",")
|
|
}
|
|
|
|
fn is_rhythm_name(name: String) -> Bool {
|
|
let csv: String = state_get("__rhythm_names")
|
|
if str_eq(csv, "") { return false }
|
|
return str_contains(csv, "," + name + ",")
|
|
}
|
|
|
|
fn is_localdate_name(name: String) -> Bool {
|
|
let csv: String = state_get("__localdate_names")
|
|
if str_eq(csv, "") { return false }
|
|
return str_contains(csv, "," + name + ",")
|
|
}
|
|
|
|
fn is_localtime_name(name: String) -> Bool {
|
|
let csv: String = state_get("__localtime_names")
|
|
if str_eq(csv, "") { return false }
|
|
return str_contains(csv, "," + name + ",")
|
|
}
|
|
|
|
fn is_localdt_name(name: String) -> Bool {
|
|
let csv: String = state_get("__localdt_names")
|
|
if str_eq(csv, "") { return false }
|
|
return str_contains(csv, "," + name + ",")
|
|
}
|
|
|
|
fn is_zone_name(name: String) -> Bool {
|
|
let csv: String = state_get("__zone_names")
|
|
if str_eq(csv, "") { return false }
|
|
return str_contains(csv, "," + name + ",")
|
|
}
|
|
|
|
// Calendar-returning builtins. earth_calendar / mars_calendar / cycle_calendar
|
|
// / no_cycle_calendar / relative_calendar all box a calendar struct.
|
|
fn is_calendar_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, "earth_calendar") { return true }
|
|
if str_eq(name, "earth_calendar_default") { return true }
|
|
if str_eq(name, "mars_calendar") { return true }
|
|
if str_eq(name, "cycle_calendar") { return true }
|
|
if str_eq(name, "no_cycle_calendar") { return true }
|
|
if str_eq(name, "relative_calendar") { return true }
|
|
return false
|
|
}
|
|
|
|
// CalendarTime-returning builtins.
|
|
fn is_caltime_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_in") { return true }
|
|
if str_eq(name, "in_calendar") { return true }
|
|
if str_eq(name, "cal_in") { return true }
|
|
if str_eq(name, "zoned") { return true }
|
|
return false
|
|
}
|
|
|
|
// Rhythm-returning builtins.
|
|
fn is_rhythm_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, "rhythm_cycle_start") { return true }
|
|
if str_eq(name, "rhythm_cycle_phase") { return true }
|
|
if str_eq(name, "rhythm_duration") { return true }
|
|
if str_eq(name, "rhythm_session_start") { return true }
|
|
if str_eq(name, "rhythm_event") { return true }
|
|
if str_eq(name, "rhythm_and") { return true }
|
|
if str_eq(name, "rhythm_or") { return true }
|
|
if str_eq(name, "rhythm_weekday") { return true }
|
|
if str_eq(name, "rhythm_weekly_at") { return true }
|
|
return false
|
|
}
|
|
|
|
// LocalDate-returning builtins.
|
|
fn is_localdate_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, "local_date") { return true }
|
|
if str_eq(name, "el_local_date_add_dur") { return true }
|
|
return false
|
|
}
|
|
|
|
fn is_localtime_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, "local_time") { return true }
|
|
if str_eq(name, "el_local_time_add_dur") { return true }
|
|
return false
|
|
}
|
|
|
|
fn is_localdt_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, "local_datetime") { return true }
|
|
return false
|
|
}
|
|
|
|
fn is_zone_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, "zone") { return true }
|
|
if str_eq(name, "zone_utc") { return true }
|
|
if str_eq(name, "zone_local") { return true }
|
|
if str_eq(name, "zone_offset") { return true }
|
|
return false
|
|
}
|
|
|
|
fn is_calendar_expr(expr: Map<String, Any>) -> Bool {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "Ident") { return is_calendar_name(expr["name"]) }
|
|
if str_eq(k, "Call") { return is_calendar_call(expr) }
|
|
return false
|
|
}
|
|
|
|
fn is_caltime_expr(expr: Map<String, Any>) -> Bool {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "Ident") { return is_caltime_name(expr["name"]) }
|
|
if str_eq(k, "Call") { return is_caltime_call(expr) }
|
|
return false
|
|
}
|
|
|
|
fn is_rhythm_expr(expr: Map<String, Any>) -> Bool {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "Ident") { return is_rhythm_name(expr["name"]) }
|
|
if str_eq(k, "Call") { return is_rhythm_call(expr) }
|
|
return false
|
|
}
|
|
|
|
fn is_localdate_expr(expr: Map<String, Any>) -> Bool {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "Ident") { return is_localdate_name(expr["name"]) }
|
|
if str_eq(k, "Call") { return is_localdate_call(expr) }
|
|
return false
|
|
}
|
|
|
|
fn is_localtime_expr(expr: Map<String, Any>) -> Bool {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "Ident") { return is_localtime_name(expr["name"]) }
|
|
if str_eq(k, "Call") { return is_localtime_call(expr) }
|
|
return false
|
|
}
|
|
|
|
fn is_localdt_expr(expr: Map<String, Any>) -> Bool {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "Ident") { return is_localdt_name(expr["name"]) }
|
|
if str_eq(k, "Call") { return is_localdt_call(expr) }
|
|
return false
|
|
}
|
|
|
|
fn is_zone_expr(expr: Map<String, Any>) -> Bool {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "Ident") { return is_zone_name(expr["name"]) }
|
|
if str_eq(k, "Call") { return is_zone_call(expr) }
|
|
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
|
|
}
|
|
if str_eq(op, "Percent") {
|
|
if is_int_expr(expr["left"]) {
|
|
if is_int_expr(expr["right"]) { return true }
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
// is_float_expr — true when expr is (or provably evaluates to) a Float value.
|
|
// Mirrors is_int_expr: Float literal, Neg of a Float, a Float-typed Ident
|
|
// (registered in __float_names), a Float-returning builtin Call, or a Float
|
|
// arithmetic BinOp (float propagates: if either operand is Float the result
|
|
// is Float). Drives the float-arithmetic dispatch in cg_expr and the EqEq/
|
|
// NotEq float comparison. Kept conservative (only provably-Float) so it never
|
|
// intercepts a value the Int paths must own — the two predicates are mutually
|
|
// exclusive on well-typed source.
|
|
fn is_float_expr(expr: Map<String, Any>) -> Bool {
|
|
let k: String = expr["expr"]
|
|
if str_eq(k, "Float") { return true }
|
|
if str_eq(k, "Neg") {
|
|
return is_float_expr(expr["inner"])
|
|
}
|
|
if str_eq(k, "Ident") {
|
|
let name: String = expr["name"]
|
|
return is_float_name(name)
|
|
}
|
|
if str_eq(k, "Call") {
|
|
return is_float_call(expr)
|
|
}
|
|
if str_eq(k, "BinOp") {
|
|
let op: String = expr["op"]
|
|
if str_eq(op, "Plus") {
|
|
if is_float_expr(expr["left"]) { return true }
|
|
if is_float_expr(expr["right"]) { return true }
|
|
return false
|
|
}
|
|
if str_eq(op, "Minus") {
|
|
if is_float_expr(expr["left"]) { return true }
|
|
if is_float_expr(expr["right"]) { return true }
|
|
return false
|
|
}
|
|
if str_eq(op, "Star") {
|
|
if is_float_expr(expr["left"]) { return true }
|
|
if is_float_expr(expr["right"]) { return true }
|
|
return false
|
|
}
|
|
if str_eq(op, "Slash") {
|
|
if is_float_expr(expr["left"]) { return true }
|
|
if is_float_expr(expr["right"]) { return true }
|
|
return false
|
|
}
|
|
if str_eq(op, "Percent") {
|
|
if is_float_expr(expr["left"]) { return true }
|
|
if is_float_expr(expr["right"]) { return true }
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
false
|
|
}
|
|
|
|
// float_operand_c — render a BinOp operand as a C double for float arithmetic.
|
|
// A provably-Float operand round-trips through el_to_float() (recovering the
|
|
// double from the int64 bit-slot); anything else is treated as Int-like and
|
|
// numerically converted with (double). This is why Float locals/params must
|
|
// be annotated `: Float` — an unannotated var holding float bits would be
|
|
// mis-converted by the (double) cast, exactly as the Int system requires
|
|
// annotations to dispatch arithmetic vs concat.
|
|
fn float_operand_c(expr: Map<String, Any>, expr_c: String) -> String {
|
|
if is_float_expr(expr) { return "el_to_float(" + expr_c + ")" }
|
|
return "(double)(" + expr_c + ")"
|
|
}
|
|
|
|
// -- 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 }
|
|
// LSP seed primitives
|
|
if str_eq(name, "__read_n") { return 1 }
|
|
if str_eq(name, "__print_raw") { return 1 }
|
|
// Test-registry accessors. These are not runtime builtins — they are
|
|
// GENERATED into the same translation unit by the --test path below, one
|
|
// set per test binary. They are declared here so the El-side runner in
|
|
// runtime/eltest.el can call them with a known arity.
|
|
if str_eq(name, "__el_reg_count") { return 0 }
|
|
if str_eq(name, "__el_reg_name") { return 1 }
|
|
if str_eq(name, "__el_reg_invoke") { return 1 }
|
|
if str_eq(name, "__el_reg_last_ns") { return 0 }
|
|
if str_eq(name, "__el_reg_msg") { return 0 }
|
|
if str_eq(name, "__el_reg_asserts") { return 0 }
|
|
if str_eq(name, "__el_opt_json") { 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 }
|
|
// Text-processing primitives (Phase 1)
|
|
if str_eq(name, "str_count") { return 2 }
|
|
if str_eq(name, "str_count_chars") { return 1 }
|
|
if str_eq(name, "str_count_bytes") { return 1 }
|
|
if str_eq(name, "str_count_lines") { return 1 }
|
|
if str_eq(name, "str_count_words") { return 1 }
|
|
if str_eq(name, "str_count_letters") { return 1 }
|
|
if str_eq(name, "str_count_digits") { return 1 }
|
|
if str_eq(name, "str_index_of_all") { return 2 }
|
|
if str_eq(name, "str_last_index_of") { return 2 }
|
|
if str_eq(name, "str_find_chars") { return 2 }
|
|
if str_eq(name, "str_repeat") { return 2 }
|
|
if str_eq(name, "str_reverse") { return 1 }
|
|
if str_eq(name, "str_strip_prefix") { return 2 }
|
|
if str_eq(name, "str_strip_suffix") { return 2 }
|
|
if str_eq(name, "str_strip_chars") { return 2 }
|
|
if str_eq(name, "str_lstrip") { return 1 }
|
|
if str_eq(name, "str_rstrip") { return 1 }
|
|
if str_eq(name, "is_letter") { return 1 }
|
|
if str_eq(name, "is_digit") { return 1 }
|
|
if str_eq(name, "is_alphanumeric") { return 1 }
|
|
if str_eq(name, "is_whitespace") { return 1 }
|
|
if str_eq(name, "is_punctuation") { return 1 }
|
|
if str_eq(name, "is_uppercase") { return 1 }
|
|
if str_eq(name, "is_lowercase") { return 1 }
|
|
if str_eq(name, "str_split_lines") { return 1 }
|
|
if str_eq(name, "str_split_chars") { return 1 }
|
|
if str_eq(name, "str_split_n") { return 3 }
|
|
if str_eq(name, "str_join") { return 2 }
|
|
// 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 }
|
|
// Seed primitives (__-prefix) — runtime/el_seed.c
|
|
if str_eq(name, "__str_len") { return 1 }
|
|
if str_eq(name, "__str_char_at") { return 2 }
|
|
if str_eq(name, "__str_alloc") { return 1 }
|
|
if str_eq(name, "__str_set_char") { return 3 }
|
|
if str_eq(name, "__str_cmp") { return 2 }
|
|
if str_eq(name, "__str_ncmp") { return 3 }
|
|
if str_eq(name, "__str_concat_raw") { return 2 }
|
|
if str_eq(name, "__str_slice_raw") { return 3 }
|
|
if str_eq(name, "__int_to_str") { return 1 }
|
|
if str_eq(name, "__str_to_int") { return 1 }
|
|
if str_eq(name, "__float_to_str") { return 1 }
|
|
if str_eq(name, "__str_to_float") { return 1 }
|
|
if str_eq(name, "__println") { return 1 }
|
|
if str_eq(name, "__print") { return 1 }
|
|
if str_eq(name, "__readline") { return 0 }
|
|
if str_eq(name, "__fs_read") { return 1 }
|
|
if str_eq(name, "__fs_write") { return 2 }
|
|
if str_eq(name, "__fs_exists") { return 1 }
|
|
if str_eq(name, "__fs_list_raw") { return 1 }
|
|
if str_eq(name, "__fs_mkdir") { return 1 }
|
|
if str_eq(name, "__fs_write_bytes") { return 3 }
|
|
if str_eq(name, "__http_do") { return 5 }
|
|
if str_eq(name, "__http_do_map") { return 5 }
|
|
if str_eq(name, "__http_do_to_file") { return 5 }
|
|
if str_eq(name, "__http_serve") { return 2 }
|
|
if str_eq(name, "__http_serve_v2") { return 2 }
|
|
if str_eq(name, "__http_response") { return 3 }
|
|
if str_eq(name, "__thread_create") { return 2 }
|
|
if str_eq(name, "__thread_join") { return 1 }
|
|
if str_eq(name, "__mutex_new") { return 0 }
|
|
if str_eq(name, "__mutex_lock") { return 1 }
|
|
if str_eq(name, "__mutex_unlock") { return 1 }
|
|
if str_eq(name, "__exec") { return 1 }
|
|
if str_eq(name, "__exec_bg") { return 1 }
|
|
if str_eq(name, "__env_get") { return 1 }
|
|
if str_eq(name, "__args_json") { return 0 }
|
|
if str_eq(name, "__exit_program") { return 1 }
|
|
if str_eq(name, "__time_now_ns") { return 0 }
|
|
if str_eq(name, "__sleep_ms") { return 1 }
|
|
if str_eq(name, "__uuid_v4") { return 0 }
|
|
if str_eq(name, "__sqrt_f") { return 1 }
|
|
if str_eq(name, "__log_f") { return 1 }
|
|
if str_eq(name, "__ln_f") { return 1 }
|
|
if str_eq(name, "__sin_f") { return 1 }
|
|
if str_eq(name, "__cos_f") { return 1 }
|
|
if str_eq(name, "__pi_f") { return 0 }
|
|
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 }
|
|
if str_eq(name, "__html_sanitize") { return 2 }
|
|
if str_eq(name, "__url_encode") { return 1 }
|
|
if str_eq(name, "__url_decode") { return 1 }
|
|
if str_eq(name, "__json_get") { return 2 }
|
|
if str_eq(name, "__json_get_raw") { return 2 }
|
|
if str_eq(name, "__json_parse_map") { return 1 }
|
|
if str_eq(name, "__json_stringify_val") { return 1 }
|
|
if str_eq(name, "__json_array_len") { return 1 }
|
|
if str_eq(name, "__json_array_get") { return 2 }
|
|
if str_eq(name, "__json_array_get_string") { return 2 }
|
|
if str_eq(name, "__json_set") { return 3 }
|
|
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_activate") { return 2 }
|
|
if str_eq(name, "__engram_activate_json") { return 2 }
|
|
if str_eq(name, "__engram_op_assert_json") { return 2 }
|
|
if str_eq(name, "__engram_node_full_in") { return 9 }
|
|
if str_eq(name, "__engram_connect_in") { return 5 }
|
|
if str_eq(name, "__engram_scan_nodes_json") { return 2 }
|
|
if str_eq(name, "__engram_edges_json") { return 2 }
|
|
if str_eq(name, "__engram_pool_stats_json") { return 0 }
|
|
if str_eq(name, "__el_alloc_count") { return 0 }
|
|
if str_eq(name, "__el_alloc_bytes") { return 0 }
|
|
if str_eq(name, "__el_peak_rss") { return 0 }
|
|
if str_eq(name, "__generate") { 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 }
|
|
if str_eq(name, "fs_size") { return 1 }
|
|
if str_eq(name, "fs_read_b64_chunk") { return 3 }
|
|
// 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 }
|
|
// Process info
|
|
if str_eq(name, "getpid_now") { return 0 }
|
|
// stdout redirect (used by elc post-processing)
|
|
if str_eq(name, "stdout_to_file") { return 1 }
|
|
if str_eq(name, "stdout_restore") { return 0 }
|
|
// Subprocess execution
|
|
if str_eq(name, "exec_command") { return 1 }
|
|
if str_eq(name, "exec_capture") { return 1 }
|
|
if str_eq(name, "exec") { return 1 }
|
|
if str_eq(name, "exec_bg") { 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_store_boot") { return 1 }
|
|
if str_eq(name, "engram_store_checkpoint") { return 0 }
|
|
if str_eq(name, "engram_store_close") { return 0 }
|
|
if str_eq(name, "engram_get_node_json") { return 1 }
|
|
if str_eq(name, "engram_get_node_by_label") { 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_edges_json") { return 2 }
|
|
if str_eq(name, "engram_pool_stats_json") { return 0 }
|
|
if str_eq(name, "el_alloc_count") { return 0 }
|
|
if str_eq(name, "el_alloc_bytes") { return 0 }
|
|
if str_eq(name, "el_peak_rss") { return 0 }
|
|
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 }
|
|
if str_eq(name, "engram_op_assert_json") { return 2 }
|
|
if str_eq(name, "engram_node_full_in") { return 9 }
|
|
if str_eq(name, "engram_connect_in") { return 5 }
|
|
// 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 }
|
|
// Threading seed primitives
|
|
if str_eq(name, "__thread_create") { return 2 }
|
|
if str_eq(name, "__thread_join") { return 1 }
|
|
if str_eq(name, "__mutex_new") { return 0 }
|
|
if str_eq(name, "__mutex_lock") { return 1 }
|
|
if str_eq(name, "__mutex_unlock") { return 1 }
|
|
// Channel seed primitives
|
|
if str_eq(name, "__channel_new") { return 1 }
|
|
if str_eq(name, "__channel_send") { return 2 }
|
|
if str_eq(name, "__channel_recv") { return 1 }
|
|
if str_eq(name, "__channel_try_recv") { return 1 }
|
|
if str_eq(name, "__channel_close") { return 1 }
|
|
// Arena mark/restore builtins
|
|
if str_eq(name, "el_arena_push") { return 0 }
|
|
if str_eq(name, "el_arena_pop") { return 1 }
|
|
// -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_float_name(name: String) -> Bool {
|
|
let csv: String = state_get("__float_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__float_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 add_calendar_name(name: String) -> Bool {
|
|
let csv: String = state_get("__calendar_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__calendar_names", csv + name + ",")
|
|
return true
|
|
}
|
|
|
|
fn add_caltime_name(name: String) -> Bool {
|
|
let csv: String = state_get("__caltime_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__caltime_names", csv + name + ",")
|
|
return true
|
|
}
|
|
|
|
fn add_rhythm_name(name: String) -> Bool {
|
|
let csv: String = state_get("__rhythm_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__rhythm_names", csv + name + ",")
|
|
return true
|
|
}
|
|
|
|
fn add_localdate_name(name: String) -> Bool {
|
|
let csv: String = state_get("__localdate_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__localdate_names", csv + name + ",")
|
|
return true
|
|
}
|
|
|
|
fn add_localtime_name(name: String) -> Bool {
|
|
let csv: String = state_get("__localtime_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__localtime_names", csv + name + ",")
|
|
return true
|
|
}
|
|
|
|
fn add_localdt_name(name: String) -> Bool {
|
|
let csv: String = state_get("__localdt_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__localdt_names", csv + name + ",")
|
|
return true
|
|
}
|
|
|
|
fn add_zone_name(name: String) -> Bool {
|
|
let csv: String = state_get("__zone_names")
|
|
if str_eq(csv, "") { csv = "," }
|
|
let key: String = "," + name + ","
|
|
if str_contains(csv, key) { return true }
|
|
state_set("__zone_names", csv + name + ",")
|
|
return true
|
|
}
|
|
|
|
fn build_int_names_for_params(params: [Map<String, Any>]) -> Bool {
|
|
state_set("__int_names", ",")
|
|
state_set("__float_names", ",")
|
|
state_set("__instant_names", ",")
|
|
state_set("__duration_names", ",")
|
|
state_set("__calendar_names", ",")
|
|
state_set("__caltime_names", ",")
|
|
state_set("__rhythm_names", ",")
|
|
state_set("__localdate_names", ",")
|
|
state_set("__localtime_names", ",")
|
|
state_set("__localdt_names", ",")
|
|
state_set("__zone_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, "Float") {
|
|
add_float_name(pname)
|
|
}
|
|
if str_eq(ptype, "Instant") {
|
|
add_instant_name(pname)
|
|
}
|
|
if str_eq(ptype, "Duration") {
|
|
add_duration_name(pname)
|
|
}
|
|
if str_eq(ptype, "Calendar") {
|
|
add_calendar_name(pname)
|
|
}
|
|
if str_eq(ptype, "CalendarTime") {
|
|
add_caltime_name(pname)
|
|
}
|
|
if str_eq(ptype, "Rhythm") {
|
|
add_rhythm_name(pname)
|
|
}
|
|
if str_eq(ptype, "LocalDate") {
|
|
add_localdate_name(pname)
|
|
}
|
|
if str_eq(ptype, "LocalTime") {
|
|
add_localtime_name(pname)
|
|
}
|
|
if str_eq(ptype, "LocalDateTime") {
|
|
add_localdt_name(pname)
|
|
}
|
|
if str_eq(ptype, "Zone") {
|
|
add_zone_name(pname)
|
|
}
|
|
let pi = pi + 1
|
|
}
|
|
return true
|
|
}
|
|
|
|
// fn_has_decorator — does this FnDef carry a decorator named `name`?
|
|
// Reads the `decorators` list [{name, args}] attached by the parser. Absent
|
|
// key -> native_list_len returns 0 -> false. This is the multi-decorator-aware
|
|
// replacement for the old single `decorator` string check, so a fn may stack
|
|
// roles with other decorators (e.g. `@route(...) @manager fn ...`).
|
|
fn fn_has_decorator(stmt: Map<String, Any>, name: String) -> Bool {
|
|
let dl = stmt["decorators"]
|
|
let n: Int = native_list_len(dl)
|
|
let i = 0
|
|
while i < n {
|
|
let d = native_list_get(dl, i)
|
|
let dn: String = d["name"]
|
|
if str_eq(dn, name) { return true }
|
|
let i = i + 1
|
|
}
|
|
false
|
|
}
|
|
|
|
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. Read the
|
|
// decorator LIST so the role may be stacked with other decorators.
|
|
if vbd_has_restricted_call(body) {
|
|
if !fn_has_decorator(stmt, "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 + ") {")
|
|
// ── API-reshape decorator-seam: auto-emit at the decorated-fn boundary ──
|
|
// Every @manager/@accessor fn gets ONE injected call to engram_boundary_beat
|
|
// at entry — interoception (chrono tick) + telemetry (afferent counter) +
|
|
// strengthen (self-activity) + a dharma bus event — so a decorated op
|
|
// self-reports with ZERO hand-written instrumentation in its body. (VBD role
|
|
// = the topmost decorator; write it topmost when stacking with @route.)
|
|
if fn_has_decorator(stmt, "manager") || fn_has_decorator(stmt, "accessor") {
|
|
emit_line(" engram_boundary_beat(EL_STR(" + c_str_lit(fn_name) + "));")
|
|
}
|
|
// 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)
|
|
}
|
|
let final_decl = cg_stmts(body_xformed, " ", decl)
|
|
el_release(final_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 }
|
|
if kind == "ExternFn" { 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\"")
|
|
|
|
// 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)
|
|
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 + ");")
|
|
}
|
|
}
|
|
if kind == "ExternFn" {
|
|
let fn_name: String = stmt["name"]
|
|
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()
|
|
}
|
|
|
|
// Detect whether this compilation unit has an entry point.
|
|
// A unit is a library (no C main emitted) when there is no fn main()
|
|
// and no top-level executable statements. This supports separate
|
|
// compilation: library .c files contain only function definitions.
|
|
let has_el_main = false
|
|
let has_toplevel_stmts = false
|
|
let i = 0
|
|
while i < n {
|
|
let stmt = native_list_get(stmts, i)
|
|
let sk: String = stmt["stmt"]
|
|
if str_eq(sk, "FnDef") {
|
|
let fn_name_chk: String = stmt["name"]
|
|
if str_eq(fn_name_chk, "main") { let has_el_main = true }
|
|
}
|
|
if !is_fndef(stmt) {
|
|
if !is_top_level_decl(stmt) {
|
|
if !str_eq(sk, "Let") {
|
|
let has_toplevel_stmts = true
|
|
}
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
let is_library = false
|
|
if !has_el_main {
|
|
if !has_toplevel_stmts {
|
|
let is_library = true
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Skip C main() for library units (no fn main, no top-level stmts)
|
|
if is_library { return "" }
|
|
|
|
// 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
|
|
""
|
|
}
|
|
|
|
// ── Streaming codegen (JIT function-at-a-time) ─────────────────────────────
|
|
//
|
|
// codegen_streaming is a memory-efficient alternative to codegen().
|
|
// Instead of receiving the full parsed AST, it receives the raw token list
|
|
// and a pre-scanned signature list (from scan_fn_sigs in parser.el).
|
|
//
|
|
// Pipeline:
|
|
// 1. Scan phase (already done by caller): scan_fn_sigs(tokens) -> sigs
|
|
// 2. Emit preamble using sigs (no full AST needed)
|
|
// 3. For each top-level statement:
|
|
// parse_one(tokens, pos) -> { node, pos }
|
|
// cg_decl_streaming(node) <- emit C for this one decl
|
|
// el_release(node) <- discard AST immediately
|
|
//
|
|
// Peak memory: O(one function's AST) instead of O(whole program AST).
|
|
//
|
|
// Entry point: codegen_streaming(tokens, sigs, source) -> String
|
|
|
|
// cg_decl_streaming — emit C for a single top-level declaration.
|
|
// Handles FnDef, ExternFn, TypeDef, EnumDef, Import, CgiBlock, ServiceBlock.
|
|
// Top-level Let statements go into the main() body, not here.
|
|
// Top-level executable statements (non-fn, non-let, non-decl) are
|
|
// accumulated into state and emitted later in main().
|
|
fn cg_decl_streaming(stmt: Map<String, Any>) -> Void {
|
|
let sk: String = stmt["stmt"]
|
|
if str_eq(sk, "FnDef") {
|
|
cg_fn(stmt)
|
|
return
|
|
}
|
|
// All other top-level decl kinds are either no-ops (Import, TypeDef,
|
|
// EnumDef, ExternFn forward decl already emitted) or capability markers
|
|
// (CgiBlock, ServiceBlock already handled in preamble).
|
|
// Top-level Lets are also no-ops here (file-scope slots already emitted).
|
|
// Executable top-level stmts (Expr, Return, etc.) are accumulated in state.
|
|
if !str_eq(sk, "FnDef") {
|
|
if !is_top_level_decl(stmt) {
|
|
if !str_eq(sk, "Let") {
|
|
// This is an executable top-level statement.
|
|
// We can't emit it into main() yet because we haven't started
|
|
// emitting main(). Accumulate in state as a list index.
|
|
// We'll collect these into a list and emit after all fns.
|
|
state_set("__streaming_has_toplevel_stmts", "1")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── @route dispatcher generation ──────────────────────────────────────────────
|
|
//
|
|
// Scan the token stream for @route-decorated fns and synthesize a generic HTTP
|
|
// dispatcher `el_route_dispatch(method, clean, path, body)`. A decorated handler
|
|
// must have the uniform signature (method, path, body) -> String. The dispatcher
|
|
// matches `clean` (the query-stripped path, supplied by the caller) against each
|
|
// route and calls the handler with the ORIGINAL `path` so query strings survive.
|
|
// Returns the sentinel "__EL_NO_ROUTE__" when nothing matches, so the caller may
|
|
// fall through to any remaining hand-written branches (mixed mode).
|
|
//
|
|
// Decorator grammar: @route(path, method, kind, suffix)
|
|
// path — the match string (or the prefix, for compound)
|
|
// method — "GET" | "POST" | ... ; a '|'-list like "GET|POST"; "ANY"/"" = no guard
|
|
// kind — "exact" (default) | "prefix" | "suffix" | "compound"
|
|
// suffix — for "compound": the required str_ends_with suffix
|
|
//
|
|
// The dispatch table is emitted SPECIFICITY-SORTED (most-specific first), NOT in
|
|
// source order, so overlapping prefixes (e.g. /api/x/search vs /api/x) never
|
|
// shadow each other regardless of how the handlers are written.
|
|
|
|
// split_pipe — split "GET|POST" on '|' into ["GET","POST"]. Self-contained
|
|
// (no dependency on str_split runtime semantics).
|
|
fn split_pipe(s: String) -> [String] {
|
|
let out: [String] = native_list_empty()
|
|
let cur: String = ""
|
|
let n: Int = str_len(s)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let ch: String = str_slice(s, i, i + 1)
|
|
if str_eq(ch, "|") {
|
|
let out = native_list_append(out, cur)
|
|
let cur = ""
|
|
} else {
|
|
let cur = cur + ch
|
|
}
|
|
let i = i + 1
|
|
}
|
|
let out = native_list_append(out, cur)
|
|
out
|
|
}
|
|
|
|
// route_make_record — build a route record map from the @route decorator args.
|
|
fn route_make_record(fn_name: String, args: [String]) -> Map<String, Any> {
|
|
let na: Int = native_list_len(args)
|
|
let rpath: String = ""
|
|
if na >= 1 { let rpath = native_list_get(args, 0) }
|
|
let rmethod: String = "GET"
|
|
if na >= 2 { let rmethod = native_list_get(args, 1) }
|
|
let rkind: String = "exact"
|
|
if na >= 3 { let rkind = native_list_get(args, 2) }
|
|
let rsuffix: String = ""
|
|
if na >= 4 { let rsuffix = native_list_get(args, 3) }
|
|
{ "name": fn_name, "path": rpath, "method": rmethod, "kind": rkind, "suffix": rsuffix }
|
|
}
|
|
|
|
// route_spec_score — higher = more specific = emitted earlier. Ordering:
|
|
// exact > compound > suffix > prefix; within a class, a longer path/suffix
|
|
// wins (so /api/x/search sorts before /api/x). Guarantees correct dispatch
|
|
// independent of source order.
|
|
fn route_spec_score(rec: Map<String, Any>) -> Int {
|
|
let kind: String = rec["kind"]
|
|
let path: String = rec["path"]
|
|
let suffix: String = rec["suffix"]
|
|
let plen: Int = str_len(path)
|
|
let slen: Int = str_len(suffix)
|
|
if str_eq(kind, "exact") { return 4000000 + plen }
|
|
if str_eq(kind, "compound") { return 3000000 + plen * 100 + slen }
|
|
if str_eq(kind, "suffix") { return 2000000 + slen }
|
|
return 1000000 + plen
|
|
}
|
|
|
|
// route_sort_desc — selection sort of route records by descending specificity.
|
|
// N is small (routes per module), so O(n^2) is fine and keeps codegen simple.
|
|
fn route_sort_desc(recs: [Map<String, Any>]) -> [Map<String, Any>] {
|
|
let n: Int = native_list_len(recs)
|
|
let out: [Map<String, Any>] = native_list_empty()
|
|
let used: [Bool] = native_list_empty()
|
|
let u: Int = 0
|
|
while u < n {
|
|
let used = native_list_append(used, false)
|
|
let u = u + 1
|
|
}
|
|
let picked: Int = 0
|
|
while picked < n {
|
|
let best_i: Int = 0 - 1
|
|
let best_score: Int = 0 - 1
|
|
let i: Int = 0
|
|
while i < n {
|
|
let is_used: Bool = native_list_get(used, i)
|
|
if !is_used {
|
|
let sc: Int = route_spec_score(native_list_get(recs, i))
|
|
if sc > best_score {
|
|
let best_score = sc
|
|
let best_i = i
|
|
}
|
|
}
|
|
let i = i + 1
|
|
}
|
|
let out = native_list_append(out, native_list_get(recs, best_i))
|
|
// Rebuild `used` with best_i marked (runtime has no native_list_set).
|
|
let new_used: [Bool] = native_list_empty()
|
|
let j: Int = 0
|
|
while j < n {
|
|
if j == best_i {
|
|
let new_used = native_list_append(new_used, true)
|
|
} else {
|
|
let new_used = native_list_append(new_used, native_list_get(used, j))
|
|
}
|
|
let j = j + 1
|
|
}
|
|
let used = new_used
|
|
let picked = picked + 1
|
|
}
|
|
out
|
|
}
|
|
|
|
// scan_routes — token-level scan collecting every @route-decorated fn as a
|
|
// route record. Runs once per module (like scan_fn_sigs) so the dispatcher can
|
|
// be synthesized in the streaming backend, which discards per-fn ASTs. Handles
|
|
// decorator STACKING: `@route(...) @manager fn` still records the route.
|
|
fn scan_routes(tokens: [Any]) -> [Map<String, Any>] {
|
|
let total: Int = native_list_len(tokens) / 2
|
|
let recs: [Map<String, Any>] = native_list_empty()
|
|
let has_pending: Bool = false
|
|
let pending_args: [String] = native_list_empty()
|
|
let pos: Int = 0
|
|
let going: Bool = true
|
|
while going {
|
|
if pos >= total {
|
|
let going = false
|
|
} else {
|
|
let k: String = tok_kind(tokens, pos)
|
|
if str_eq(k, "Eof") {
|
|
let going = false
|
|
} else {
|
|
if str_eq(k, "At") {
|
|
let dname: String = tok_value(tokens, pos + 1)
|
|
let p: Int = pos + 2
|
|
let args: [String] = native_list_empty()
|
|
let ka: String = tok_kind(tokens, p)
|
|
if str_eq(ka, "LParen") {
|
|
let p = p + 1
|
|
let running: Bool = true
|
|
while running {
|
|
let kd: String = tok_kind(tokens, p)
|
|
if str_eq(kd, "RParen") {
|
|
let running = false
|
|
} else {
|
|
if str_eq(kd, "Eof") {
|
|
let running = false
|
|
} else {
|
|
if str_eq(kd, "Str") {
|
|
let args = native_list_append(args, tok_value(tokens, p))
|
|
}
|
|
let p = p + 1
|
|
}
|
|
}
|
|
}
|
|
if str_eq(tok_kind(tokens, p), "RParen") { let p = p + 1 }
|
|
}
|
|
if str_eq(dname, "route") {
|
|
let has_pending = true
|
|
let pending_args = args
|
|
}
|
|
let pos = p
|
|
} else {
|
|
if str_eq(k, "Fn") {
|
|
let fname: String = tok_value(tokens, pos + 1)
|
|
if has_pending {
|
|
let recs = native_list_append(recs, route_make_record(fname, pending_args))
|
|
let has_pending = false
|
|
}
|
|
let pos = pos + 2
|
|
} else {
|
|
let pos = pos + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
recs
|
|
}
|
|
|
|
// program_has_routes — did scan_routes find any @route fn?
|
|
fn program_has_routes(recs: [Map<String, Any>]) -> Bool {
|
|
native_list_len(recs) > 0
|
|
}
|
|
|
|
// route_method_guard — C boolean prefix guarding on HTTP method, or "" for none.
|
|
fn route_method_guard(method: String) -> String {
|
|
if str_eq(method, "") { return "" }
|
|
if str_eq(method, "ANY") { return "" }
|
|
if str_contains(method, "|") {
|
|
let parts: [String] = split_pipe(method)
|
|
let np: Int = native_list_len(parts)
|
|
let expr: String = ""
|
|
let i: Int = 0
|
|
while i < np {
|
|
let m: String = native_list_get(parts, i)
|
|
if str_eq(m, "") {
|
|
let i = i + 1
|
|
} else {
|
|
let piece: String = "str_eq(method, EL_STR(" + c_str_lit(m) + "))"
|
|
if str_eq(expr, "") {
|
|
let expr = piece
|
|
} else {
|
|
let expr = expr + " || " + piece
|
|
}
|
|
let i = i + 1
|
|
}
|
|
}
|
|
if str_eq(expr, "") { return "" }
|
|
return "(" + expr + ") && "
|
|
}
|
|
"str_eq(method, EL_STR(" + c_str_lit(method) + ")) && "
|
|
}
|
|
|
|
// route_match_expr — C boolean matching `clean` against the route path/kind.
|
|
fn route_match_expr(kind: String, path: String, suffix: String) -> String {
|
|
if str_eq(kind, "prefix") {
|
|
return "str_starts_with(clean, EL_STR(" + c_str_lit(path) + "))"
|
|
}
|
|
if str_eq(kind, "suffix") {
|
|
return "str_ends_with(clean, EL_STR(" + c_str_lit(path) + "))"
|
|
}
|
|
if str_eq(kind, "compound") {
|
|
return "str_starts_with(clean, EL_STR(" + c_str_lit(path) + ")) && str_ends_with(clean, EL_STR(" + c_str_lit(suffix) + "))"
|
|
}
|
|
"str_eq(clean, EL_STR(" + c_str_lit(path) + "))"
|
|
}
|
|
|
|
// emit_route_dispatch — emit the generated el_route_dispatch definition from the
|
|
// specificity-sorted route records. No-op if there are no routes.
|
|
fn emit_route_dispatch(recs: [Map<String, Any>]) -> Void {
|
|
if !program_has_routes(recs) { return }
|
|
let sorted: [Map<String, Any>] = route_sort_desc(recs)
|
|
emit_line("// ── generated @route dispatcher (specificity-sorted) ──")
|
|
emit_line("el_val_t el_route_dispatch(el_val_t method, el_val_t clean, el_val_t path, el_val_t body) {")
|
|
let n: Int = native_list_len(sorted)
|
|
let i: Int = 0
|
|
while i < n {
|
|
let rec = native_list_get(sorted, i)
|
|
let guard: String = route_method_guard(rec["method"])
|
|
let match_e: String = route_match_expr(rec["kind"], rec["path"], rec["suffix"])
|
|
let fn_name: String = rec["name"]
|
|
emit_line(" if (" + guard + match_e + ") { return " + fn_name + "(method, path, body); }")
|
|
let i = i + 1
|
|
}
|
|
emit_line(" return EL_STR(\"__EL_NO_ROUTE__\");")
|
|
emit_line("}")
|
|
emit_blank()
|
|
}
|
|
|
|
// emit_streaming_preamble — emit #includes, forward decls, and file-scope lets
|
|
// using the pre-scanned signature data (no full AST).
|
|
fn emit_streaming_preamble(sigs: [Map<String, Any>], source: String) -> Void {
|
|
let n: Int = native_list_len(sigs)
|
|
|
|
// Detect program kind from sigs
|
|
let cgi_count: Int = 0
|
|
let svc_count: Int = 0
|
|
let i: Int = 0
|
|
while i < n {
|
|
let sig = native_list_get(sigs, i)
|
|
let sk: String = sig["kind"]
|
|
if str_eq(sk, "cgi_block") { let cgi_count = cgi_count + 1 }
|
|
if str_eq(sk, "service_block") { let svc_count = svc_count + 1 }
|
|
let i = i + 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)\"")
|
|
}
|
|
}
|
|
let kind: String = "utility"
|
|
if cgi_count >= 1 { let kind = "cgi" }
|
|
if svc_count >= 1 { let kind = "service" }
|
|
state_set("__program_kind", kind)
|
|
state_set("__cap_violations", "")
|
|
state_set("__arity_violations", "")
|
|
state_set("__time_violations", "")
|
|
|
|
emit_line("#include <stdint.h>")
|
|
emit_line("#include <stdlib.h>")
|
|
emit_line("#include \"el_runtime.h\"")
|
|
emit_blank()
|
|
|
|
// Forward declarations — use pre-computed params_c strings from scan.
|
|
let i = 0
|
|
while i < n {
|
|
let sig = native_list_get(sigs, i)
|
|
let sk: String = sig["kind"]
|
|
if str_eq(sk, "fn") {
|
|
let fn_name: String = sig["name"]
|
|
if !str_eq(fn_name, "main") {
|
|
let params_c: String = sig["params_c"]
|
|
emit_line("el_val_t " + fn_name + "(" + params_c + ");")
|
|
}
|
|
}
|
|
if str_eq(sk, "extern_fn") {
|
|
let fn_name: String = sig["name"]
|
|
let params_c: String = sig["params_c"]
|
|
emit_line("el_val_t " + fn_name + "(" + params_c + ");")
|
|
}
|
|
let i = i + 1
|
|
}
|
|
emit_blank()
|
|
|
|
// File-scope let slots
|
|
let has_toplevel_lets: Bool = false
|
|
let i = 0
|
|
while i < n {
|
|
let sig = native_list_get(sigs, i)
|
|
let sk: String = sig["kind"]
|
|
if str_eq(sk, "toplevel_let") {
|
|
let name: String = sig["name"]
|
|
let ltype: String = sig["ltype"]
|
|
if str_eq(ltype, "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() }
|
|
}
|
|
|
|
// codegen_streaming — JIT function-at-a-time compiler backend.
|
|
// tokens: flat token list from lex()
|
|
// sigs: pre-scanned signature list from scan_fn_sigs(tokens)
|
|
// source: original source string (for string literal lookup)
|
|
fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) -> String {
|
|
let total_tokens: Int = native_list_len(tokens) / 2
|
|
|
|
// Emit preamble (forward decls, file-scope lets, #includes)
|
|
// Arena scope: free intermediate strings built during preamble emission.
|
|
let preamble_mark: Any = el_arena_push()
|
|
emit_streaming_preamble(sigs, source)
|
|
el_arena_pop(preamble_mark)
|
|
|
|
// @route: scan the token stream once for @route-decorated fns. Kept in
|
|
// codegen_streaming scope (survives the per-fn arena pops and el_release of
|
|
// tokens below via refcount, like `sigs`). If any exist, forward-declare the
|
|
// generated dispatcher NOW so hand-written fns (e.g. handle_request) may call
|
|
// it before its definition is emitted after the fn-emit loop.
|
|
let route_records: [Map<String, Any>] = scan_routes(tokens)
|
|
if program_has_routes(route_records) {
|
|
emit_line("el_val_t el_route_dispatch(el_val_t method, el_val_t clean, el_val_t path, el_val_t body);")
|
|
emit_blank()
|
|
}
|
|
|
|
// Detect whether there is a fn main() and whether there are top-level
|
|
// executable stmts (for library detection) from sigs.
|
|
let has_el_main: Bool = false
|
|
let ns: Int = native_list_len(sigs)
|
|
let si: Int = 0
|
|
while si < ns {
|
|
let sig = native_list_get(sigs, si)
|
|
let sk2: String = sig["kind"]
|
|
if str_eq(sk2, "fn") {
|
|
let fn_name_chk: String = sig["name"]
|
|
if str_eq(fn_name_chk, "main") { let has_el_main = true }
|
|
}
|
|
let si = si + 1
|
|
}
|
|
|
|
// Collect top-level let names for seeding main()'s declared set.
|
|
let toplevel_let_names: [String] = native_list_empty()
|
|
let si = 0
|
|
while si < ns {
|
|
let sig = native_list_get(sigs, si)
|
|
let sk2: String = sig["kind"]
|
|
if str_eq(sk2, "toplevel_let") {
|
|
let tname: String = sig["name"]
|
|
let toplevel_let_names = native_list_append(toplevel_let_names, tname)
|
|
}
|
|
let si = si + 1
|
|
}
|
|
|
|
// In test mode: collect test function names for harness main().
|
|
let test_is_mode: Bool = false
|
|
let tmode_str: String = state_get("__test_mode")
|
|
if str_eq(tmode_str, "1") { let test_is_mode = true }
|
|
let test_names: [String] = native_list_empty()
|
|
let test_c_names: [String] = native_list_empty()
|
|
|
|
// Emit test harness preamble (counters, fail printer) when in test mode.
|
|
if test_is_mode {
|
|
emit_line("#include <stdio.h>")
|
|
emit_line("#include <string.h>")
|
|
emit_line("#include <time.h>")
|
|
emit_blank()
|
|
// Per-test result state. Reset by __el_reg_invoke before each test, so
|
|
// every test gets its own record rather than contributing to a global
|
|
// tally. The first failure message is retained; later ones only bump
|
|
// the count, which keeps the common case allocation-free.
|
|
emit_line("static int __el_cur_fails = 0;")
|
|
emit_line("static int __el_cur_asserts = 0;")
|
|
emit_line("static char __el_cur_msg[512] = \"\";")
|
|
emit_line("static const char *__el_cur_test = \"(none)\";")
|
|
emit_line("static void __el_test_fail(const char *msg) {")
|
|
emit_line(" if (__el_cur_fails == 0 && msg) {")
|
|
emit_line(" snprintf(__el_cur_msg, sizeof __el_cur_msg, \"%s\", msg);")
|
|
emit_line(" }")
|
|
emit_line(" __el_cur_fails++; __el_cur_asserts++;")
|
|
emit_line("}")
|
|
emit_blank()
|
|
// Forward declarations for the registry accessors. The definitions are
|
|
// emitted at the END of the unit (they reference the test functions,
|
|
// which do not exist yet at this point), but the El-side runner is
|
|
// compiled in between and calls them — so it needs the prototypes here.
|
|
emit_line("el_val_t __el_reg_count(void);")
|
|
emit_line("el_val_t __el_reg_name(el_val_t i);")
|
|
emit_line("el_val_t __el_reg_invoke(el_val_t i);")
|
|
emit_line("el_val_t __el_reg_last_ns(void);")
|
|
emit_line("el_val_t __el_reg_msg(void);")
|
|
emit_line("el_val_t __el_reg_asserts(void);")
|
|
emit_line("el_val_t __el_opt_json(void);")
|
|
emit_blank()
|
|
}
|
|
|
|
// Streaming parse-emit loop.
|
|
// For each parsed stmt:
|
|
// - FnDef (not main): emit immediately via cg_fn, release AST
|
|
// - Others: accumulate only fn-main body and top-level executable stmts
|
|
// (these are small in count relative to fn bodies)
|
|
let pos: Int = 0
|
|
let el_main_body: [Map<String, Any>] = native_list_empty()
|
|
let toplevel_exec_stmts: [Map<String, Any>] = native_list_empty()
|
|
// CGI IDENTITY CAPTURE (2026-08-09). A cgi block is a top-level DECLARATION, so
|
|
// the classifier below correctly excludes it from toplevel_exec_stmts and calls
|
|
// el_release on it. The identity emission further down then searched
|
|
// toplevel_exec_stmts for it — a list that structurally can never contain it —
|
|
// found nothing, and emitted nothing, silently. Measured: that search sees only
|
|
// [Let, Expr] for a program whose first statement is a cgi block.
|
|
// Fix: copy the values out BEFORE the release (strings, so no dangling reference)
|
|
// and emit from these. No search, so the failure mode is removed rather than moved.
|
|
let cgi_have: Bool = false
|
|
let cgi_name_v: String = ""
|
|
let cgi_did_v: String = ""
|
|
let cgi_prin_v: String = ""
|
|
let cgi_net_v: String = ""
|
|
let cgi_eng_v: String = ""
|
|
let cgi_has_did: Bool = false
|
|
let cgi_has_prin: Bool = false
|
|
let cgi_has_net: Bool = false
|
|
let cgi_has_eng: Bool = false
|
|
let has_toplevel_exec: Bool = false
|
|
|
|
let stream_running: Bool = true
|
|
while stream_running {
|
|
if pos >= total_tokens {
|
|
let stream_running = false
|
|
} else {
|
|
let k: String = tok_kind(tokens, pos)
|
|
if str_eq(k, "Eof") {
|
|
let stream_running = false
|
|
} else {
|
|
if str_eq(k, "Test") {
|
|
if test_is_mode {
|
|
// Compile test "name" { ... } block into a static void __el_test_NAME() function.
|
|
let p: Int = pos + 1
|
|
let test_name: String = "unnamed"
|
|
if str_eq(tok_kind(tokens, p), "Str") {
|
|
let test_name = tok_value(tokens, p)
|
|
let p = p + 1
|
|
}
|
|
let fn_c_name: String = "__el_test_" + sanitize_test_name(test_name)
|
|
let test_names = native_list_append(test_names, test_name)
|
|
let test_c_names = native_list_append(test_c_names, fn_c_name)
|
|
// Emit the test function header.
|
|
emit_line("static void " + fn_c_name + "(void) {")
|
|
emit_line(" __el_cur_test = \"" + c_escape(test_name) + "\";")
|
|
// Skip the opening LBrace and parse body statements.
|
|
if str_eq(tok_kind(tokens, p), "LBrace") { let p = p + 1 }
|
|
let body_decl: [String] = native_list_empty()
|
|
let body_done: Bool = false
|
|
while !body_done {
|
|
let bk: String = tok_kind(tokens, p)
|
|
if str_eq(bk, "RBrace") {
|
|
let body_done = true
|
|
} else {
|
|
if str_eq(bk, "Eof") {
|
|
let body_done = true
|
|
} else {
|
|
let br = parse_one(tokens, p)
|
|
let bstmt = br["node"]
|
|
let np: Int = br["pos"]
|
|
el_release(br)
|
|
if np > p {
|
|
let body_arena: Any = el_arena_push()
|
|
let body_decl = cg_stmt(bstmt, " ", body_decl)
|
|
el_arena_pop(body_arena)
|
|
el_release(bstmt)
|
|
let p = np
|
|
} else {
|
|
let p = p + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Skip past closing RBrace.
|
|
if str_eq(tok_kind(tokens, p), "RBrace") { let p = p + 1 }
|
|
el_release(body_decl)
|
|
emit_line("}")
|
|
emit_blank()
|
|
let pos = p
|
|
} else {
|
|
// Non-test mode: skip test blocks entirely to avoid OOM.
|
|
// Without this skip, the body `{ ... }` would be parsed as a Map
|
|
// literal, building a huge AST with O(n²) string allocation.
|
|
let p: Int = pos + 1
|
|
let k_name: String = tok_kind(tokens, p)
|
|
if str_eq(k_name, "Str") { let p = p + 1 }
|
|
let k_body: String = tok_kind(tokens, p)
|
|
if str_eq(k_body, "LBrace") { let p = skip_to_rbrace(tokens, p) }
|
|
let pos = p
|
|
}
|
|
} else {
|
|
let r = parse_one(tokens, pos)
|
|
let stmt = r["node"]
|
|
let new_pos: Int = r["pos"]
|
|
el_release(r)
|
|
|
|
// Guard against infinite loops
|
|
if new_pos <= pos {
|
|
el_release(stmt)
|
|
let pos = pos + 1
|
|
} else {
|
|
let sk: String = stmt["stmt"]
|
|
|
|
if str_eq(sk, "FnDef") {
|
|
let fn_name2: String = stmt["name"]
|
|
if str_eq(fn_name2, "main") {
|
|
// Capture main() body for later
|
|
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
|
|
}
|
|
el_release(stmt)
|
|
} else {
|
|
// Emit immediately — this is the JIT core
|
|
// Arena scope: free all intermediate strings (str_concat,
|
|
// int_to_str, cg_expr fragments) after each function.
|
|
let fn_arena_mark: Any = el_arena_push()
|
|
cg_fn(stmt)
|
|
el_release(stmt)
|
|
el_arena_pop(fn_arena_mark)
|
|
el_mem_check()
|
|
}
|
|
} else {
|
|
if is_top_level_decl(stmt) {
|
|
// Import, TypeDef, EnumDef, CgiBlock, ServiceBlock, ExternFn
|
|
// These are no-ops in codegen (forward decls already emitted)
|
|
// — except a CgiBlock, whose declared identity must survive
|
|
// this release to be emitted as a compiled constant.
|
|
if str_eq(sk, "CgiBlock") {
|
|
let cgi_have = true
|
|
let cgi_name_v = stmt["name"]
|
|
let cgi_did_v = stmt["dharma_id"]
|
|
let cgi_prin_v = stmt["principal"]
|
|
let cgi_net_v = stmt["network"]
|
|
let cgi_eng_v = stmt["engram"]
|
|
let cgi_has_did = stmt["has_dharma_id"]
|
|
let cgi_has_prin = stmt["has_principal"]
|
|
let cgi_has_net = stmt["has_network"]
|
|
let cgi_has_eng = stmt["has_engram"]
|
|
}
|
|
el_release(stmt)
|
|
} else {
|
|
if str_eq(sk, "Let") {
|
|
// Top-level let: file-scope slot already declared.
|
|
// Keep for main() init — these are few and small.
|
|
let toplevel_exec_stmts = native_list_append(toplevel_exec_stmts, stmt)
|
|
let has_toplevel_exec = true
|
|
} else {
|
|
// Executable top-level stmt (rare)
|
|
let toplevel_exec_stmts = native_list_append(toplevel_exec_stmts, stmt)
|
|
let has_toplevel_exec = true
|
|
}
|
|
}
|
|
}
|
|
|
|
let pos = new_pos
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// @route: emit the generated dispatcher definition now — after every handler
|
|
// fn has been emitted, but before `tokens` is released (route_records holds
|
|
// its own refs to the extracted strings). No-op unless the module declared
|
|
// at least one @route fn. Emitted before the test/library early-returns so it
|
|
// is present in library modules (e.g. neuron's routes.el) too.
|
|
let route_arena_mark: Any = el_arena_push()
|
|
emit_route_dispatch(route_records)
|
|
el_arena_pop(route_arena_mark)
|
|
|
|
// Tokens fully consumed by the streaming loop — release now to free peak heap.
|
|
el_release(tokens)
|
|
|
|
if test_is_mode {
|
|
// Test mode: emit test harness main() that calls each collected test function.
|
|
// Discard El's main body and top-level exec stmts (not needed in test harness).
|
|
el_release(el_main_body)
|
|
el_release(toplevel_exec_stmts)
|
|
el_release(toplevel_let_names)
|
|
el_release(sigs)
|
|
|
|
let test_arena_mark: Any = el_arena_push()
|
|
let tn: Int = native_list_len(test_c_names)
|
|
|
|
// ── Generated test registry ──────────────────────────────────────────
|
|
// Discovery happens HERE, at compile time. The runner never searches
|
|
// for tests; it walks this table. That ordering — discovery strictly
|
|
// before execution — is what makes --list, filtering, sharding and
|
|
// per-test reporting possible later, and it is why the old harness
|
|
// (which inlined direct calls into main) could not have any of them.
|
|
emit_line("typedef void (*__el_test_fp)(void);")
|
|
emit_line("typedef struct { const char *name; __el_test_fp fn; } __el_test_entry;")
|
|
emit_line("static const __el_test_entry __el_registry[] = {")
|
|
let ri: Int = 0
|
|
while ri < tn {
|
|
let r_name: String = native_list_get(test_names, ri)
|
|
let r_cfn: String = native_list_get(test_c_names, ri)
|
|
emit_line(" { \"" + c_escape(r_name) + "\", " + r_cfn + " },")
|
|
let ri = ri + 1
|
|
}
|
|
// Trailing sentinel keeps the array non-empty when a file declares no
|
|
// tests (a zero-length array is not valid C).
|
|
emit_line(" { 0, 0 }")
|
|
emit_line("};")
|
|
emit_line("static const int __el_registry_n = " + int_to_str(tn) + ";")
|
|
emit_blank()
|
|
emit_line("static long long __el_last_ns = 0;")
|
|
emit_line("static int __el_opt_json_v = 0;")
|
|
emit_blank()
|
|
|
|
// ── Index-based accessors ────────────────────────────────────────────
|
|
// El has no function pointers, so the runner works purely in indices.
|
|
// This is the whole seam between generated C and the El-side runner.
|
|
emit_line("el_val_t __el_reg_count(void) { return (el_val_t)(int64_t)__el_registry_n; }")
|
|
emit_line("el_val_t __el_reg_name(el_val_t i) {")
|
|
emit_line(" int64_t k = (int64_t)i;")
|
|
emit_line(" if (k < 0 || k >= __el_registry_n) return EL_STR(\"\");")
|
|
emit_line(" return EL_STR(__el_registry[k].name);")
|
|
emit_line("}")
|
|
// Timing is taken immediately around the call, in C, on the MONOTONIC
|
|
// clock — never the wall clock, which can step backwards under NTP.
|
|
emit_line("el_val_t __el_reg_invoke(el_val_t i) {")
|
|
emit_line(" int64_t k = (int64_t)i;")
|
|
emit_line(" if (k < 0 || k >= __el_registry_n) return 0;")
|
|
emit_line(" __el_cur_fails = 0; __el_cur_asserts = 0; __el_cur_msg[0] = '\\0';")
|
|
emit_line(" __el_cur_test = __el_registry[k].name;")
|
|
emit_line(" struct timespec _t0, _t1;")
|
|
emit_line(" clock_gettime(CLOCK_MONOTONIC, &_t0);")
|
|
emit_line(" __el_registry[k].fn();")
|
|
emit_line(" clock_gettime(CLOCK_MONOTONIC, &_t1);")
|
|
emit_line(" __el_last_ns = (long long)(_t1.tv_sec - _t0.tv_sec) * 1000000000LL")
|
|
emit_line(" + (long long)(_t1.tv_nsec - _t0.tv_nsec);")
|
|
emit_line(" return (el_val_t)(int64_t)__el_cur_fails;")
|
|
emit_line("}")
|
|
emit_line("el_val_t __el_reg_last_ns(void) { return (el_val_t)(int64_t)__el_last_ns; }")
|
|
emit_line("el_val_t __el_reg_msg(void) { return EL_STR(__el_cur_msg); }")
|
|
emit_line("el_val_t __el_reg_asserts(void) { return (el_val_t)(int64_t)__el_cur_asserts; }")
|
|
emit_line("el_val_t __el_opt_json(void) { return (el_val_t)(int64_t)__el_opt_json_v; }")
|
|
emit_blank()
|
|
|
|
// main() delegates to the El-side runner. Everything above this line is
|
|
// generated glue; all reporting logic lives in runtime/eltest.el.
|
|
emit_line("int main(int _argc, char **_argv) {")
|
|
emit_line(" el_runtime_init_args(_argc, _argv);")
|
|
emit_line(" for (int _i = 1; _i < _argc; _i++) {")
|
|
emit_line(" if (strcmp(_argv[_i], \"--json\") == 0) __el_opt_json_v = 1;")
|
|
emit_line(" }")
|
|
emit_line(" return (int)(int64_t)el_test_main();")
|
|
emit_line("}")
|
|
el_arena_pop(test_arena_mark)
|
|
el_release(test_names)
|
|
el_release(test_c_names)
|
|
return ""
|
|
}
|
|
|
|
// Release test tracking lists (empty in non-test mode).
|
|
el_release(test_names)
|
|
el_release(test_c_names)
|
|
|
|
// Library detection: no fn main and no top-level executable stmts
|
|
let is_library: Bool = false
|
|
if !has_el_main {
|
|
if !has_toplevel_exec {
|
|
let is_library = true
|
|
}
|
|
}
|
|
if is_library { return "" }
|
|
|
|
// Emit main() — wrap in arena scope to free intermediate strings.
|
|
let main_arena_mark: Any = el_arena_push()
|
|
let kind2: String = state_get("__program_kind")
|
|
emit_line("int main(int _argc, char** _argv) {")
|
|
emit_line(" el_runtime_init_args(_argc, _argv);")
|
|
|
|
// cgi init if needed
|
|
let ns2: Int = native_list_len(sigs)
|
|
let si2: Int = 0
|
|
while si2 < ns2 {
|
|
let sig2 = native_list_get(sigs, si2)
|
|
let sk3: String = sig2["kind"]
|
|
if str_eq(sk3, "cgi_block") {
|
|
// Emit from the values captured before the declaration was released.
|
|
// The previous implementation searched toplevel_exec_stmts, which by
|
|
// construction never contains a declaration — so it emitted nothing and
|
|
// said nothing. See the capture block near toplevel_exec_stmts init.
|
|
if cgi_have {
|
|
let arg_name2: String = "EL_STR(" + c_str_lit(cgi_name_v) + ")"
|
|
let arg_did2: String = cgi_arg(cgi_did_v, cgi_has_did)
|
|
let arg_prin2: String = cgi_arg(cgi_prin_v, cgi_has_prin)
|
|
let arg_net2: String = cgi_arg(cgi_net_v, cgi_has_net)
|
|
let arg_eng2: String = cgi_arg(cgi_eng_v, cgi_has_eng)
|
|
emit_line(" el_cgi_init(" + arg_name2 + ", " + arg_did2 + ", " + arg_prin2 + ", " + arg_net2 + ", " + arg_eng2 + ");")
|
|
}
|
|
}
|
|
let si2 = si2 + 1
|
|
}
|
|
|
|
// sigs fully consumed — release to free peak heap.
|
|
el_release(sigs)
|
|
|
|
// Seed declared set with top-level let names
|
|
let main_decl2: [String] = native_list_empty()
|
|
let tln: Int = native_list_len(toplevel_let_names)
|
|
let tli: Int = 0
|
|
while tli < tln {
|
|
let main_decl2 = native_list_append(main_decl2, native_list_get(toplevel_let_names, tli))
|
|
let tli = tli + 1
|
|
}
|
|
// toplevel_let_names fully consumed — release to free peak heap.
|
|
el_release(toplevel_let_names)
|
|
|
|
// Emit top-level executable stmts (lets and others) into main()
|
|
// Per-statement arena scope mirrors el_main_body: frees intermediate strings
|
|
// (str_concat fragments from cg_expr) after each statement, preventing O(n²)
|
|
// accumulation when many stmts are present (e.g. from unrecognized constructs).
|
|
let tes_n2: Int = native_list_len(toplevel_exec_stmts)
|
|
let tes_i2: Int = 0
|
|
while tes_i2 < tes_n2 {
|
|
let tes2 = native_list_get(toplevel_exec_stmts, tes_i2)
|
|
let tes_k2: String = tes2["stmt"]
|
|
if !str_eq(tes_k2, "CgiBlock") {
|
|
if !str_eq(tes_k2, "ServiceBlock") {
|
|
let tes_mark: Any = el_arena_push()
|
|
let main_decl2 = cg_stmt(tes2, " ", main_decl2)
|
|
el_arena_pop(tes_mark)
|
|
}
|
|
}
|
|
let tes_i2 = tes_i2 + 1
|
|
}
|
|
// toplevel_exec_stmts fully consumed — release to free peak heap.
|
|
el_release(toplevel_exec_stmts)
|
|
|
|
// Emit fn main() body — per-statement arena scope frees intermediate strings.
|
|
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 stmt_mark: Any = el_arena_push()
|
|
let main_decl2 = cg_stmt(mstmt, " ", main_decl2)
|
|
el_arena_pop(stmt_mark)
|
|
let mi = mi + 1
|
|
}
|
|
// el_main_body and main_decl2 fully consumed — release to free peak heap.
|
|
el_release(el_main_body)
|
|
el_release(main_decl2)
|
|
|
|
emit_line(" return 0;")
|
|
emit_line("}")
|
|
emit_blank()
|
|
|
|
emit_cap_violations()
|
|
emit_arity_violations()
|
|
emit_time_violations()
|
|
el_arena_pop(main_arena_mark)
|
|
|
|
""
|
|
}
|