// lexer.el — el self-hosting lexer // // Tokenises an el source string into a list of token maps. // Each token is a Map with keys: // "kind" -> String (e.g. "Int", "Ident", "Plus") // "value" -> String (the raw text of the token) // // Entry point: fn lex(source: String) -> [Map] // // Uses native_string_chars to split the source into a chars list, // then indexes it with native_list_get — avoids O(N²) string cloning. // ── Character helpers ───────────────────────────────────────────────────────── fn lex_is_digit(ch: String) -> Bool { if ch == "0" { return true } if ch == "1" { return true } if ch == "2" { return true } if ch == "3" { return true } if ch == "4" { return true } if ch == "5" { return true } if ch == "6" { return true } if ch == "7" { return true } if ch == "8" { return true } if ch == "9" { return true } false } fn lex_is_alpha(ch: String) -> Bool { if ch == "a" { return true } if ch == "b" { return true } if ch == "c" { return true } if ch == "d" { return true } if ch == "e" { return true } if ch == "f" { return true } if ch == "g" { return true } if ch == "h" { return true } if ch == "i" { return true } if ch == "j" { return true } if ch == "k" { return true } if ch == "l" { return true } if ch == "m" { return true } if ch == "n" { return true } if ch == "o" { return true } if ch == "p" { return true } if ch == "q" { return true } if ch == "r" { return true } if ch == "s" { return true } if ch == "t" { return true } if ch == "u" { return true } if ch == "v" { return true } if ch == "w" { return true } if ch == "x" { return true } if ch == "y" { return true } if ch == "z" { return true } if ch == "A" { return true } if ch == "B" { return true } if ch == "C" { return true } if ch == "D" { return true } if ch == "E" { return true } if ch == "F" { return true } if ch == "G" { return true } if ch == "H" { return true } if ch == "I" { return true } if ch == "J" { return true } if ch == "K" { return true } if ch == "L" { return true } if ch == "M" { return true } if ch == "N" { return true } if ch == "O" { return true } if ch == "P" { return true } if ch == "Q" { return true } if ch == "R" { return true } if ch == "S" { return true } if ch == "T" { return true } if ch == "U" { return true } if ch == "V" { return true } if ch == "W" { return true } if ch == "X" { return true } if ch == "Y" { return true } if ch == "Z" { return true } false } fn is_alnum_or_underscore(ch: String) -> Bool { if lex_is_digit(ch) { return true } if lex_is_alpha(ch) { return true } if ch == "_" { return true } false } fn lex_is_whitespace(ch: String) -> Bool { if ch == " " { return true } if ch == "\t" { return true } if ch == "\n" { return true } if ch == "\r" { return true } false } fn make_tok(kind: String, value: String) -> Map { let ln_s: String = state_get("__lex_line") let ln: Int = 1 if !str_eq(ln_s, "") { let ln = str_to_int(ln_s) } { "kind": kind, "value": value, "line": ln } } // ── Keyword lookup ──────────────────────────────────────────────────────────── fn keyword_kind(word: String) -> String { if word == "let" { return "Let" } if word == "fn" { return "Fn" } if word == "type" { return "Type" } if word == "enum" { return "Enum" } if word == "match" { return "Match" } if word == "return" { return "Return" } if word == "if" { return "If" } if word == "else" { return "Else" } if word == "for" { return "For" } if word == "in" { return "In" } if word == "while" { return "While" } if word == "import" { return "Import" } if word == "from" { return "From" } if word == "as" { return "As" } if word == "with" { return "With" } if word == "sealed" { return "Sealed" } if word == "activate" { return "Activate" } if word == "where" { return "Where" } if word == "test" { return "Test" } if word == "seed" { return "Seed" } if word == "assert" { return "Assert" } if word == "protocol" { return "Protocol" } if word == "impl" { return "Impl" } if word == "retry" { return "Retry" } if word == "times" { return "Times" } if word == "fallback" { return "Fallback" } if word == "reason" { return "Reason" } if word == "parallel" { return "Parallel" } if word == "trace" { return "Trace" } if word == "requires" { return "Requires" } if word == "deploy" { return "Deploy" } if word == "to" { return "To" } if word == "via" { return "Via" } if word == "target" { return "Target" } if word == "true" { return "Bool" } if word == "false" { return "Bool" } if word == "cgi" { return "Cgi" } if word == "service" { return "Service" } if word == "manager" { return "Manager" } if word == "engine" { return "Engine" } if word == "accessor" { return "Accessor" } if word == "vessel" { return "Vessel" } if word == "extern" { return "Extern" } if word == "try" { return "Try" } if word == "catch" { return "Catch" } "" } // ── Scan helpers ────────────────────────────────────────────────────────────── // All scan helpers receive the chars list and total length. // scan_digits — advance i while chars[i] is a digit // Returns { "text": ..., "pos": i } fn scan_digits(chars: [String], start: Int, total: Int) -> Map { let i = start let parts: [String] = native_list_empty() let running = true while running { if i >= total { let running = false } else { let ch: String = native_list_get(chars, i) if lex_is_digit(ch) { let parts = native_list_append(parts, ch) let i = i + 1 } else { let running = false } } } { "text": str_join(parts, ""), "pos": i } } // scan_ident — advance i while chars[i] is alphanumeric or underscore fn scan_ident(chars: [String], start: Int, total: Int) -> Map { let i = start let parts: [String] = native_list_empty() let running = true while running { if i >= total { let running = false } else { let ch: String = native_list_get(chars, i) if is_alnum_or_underscore(ch) { let parts = native_list_append(parts, ch) let i = i + 1 } else { let running = false } } } { "text": str_join(parts, ""), "pos": i } } // ── Code-bearing string detection + comment strip ──────────────────────────── // Inline JS/CSS literals embedded in El source (e.g. blobs // or stylesheet payloads inside string literals) carry their own line and // block comments. Those comments leak into the served HTML and reveal build // notes the visitor should never see. We strip them at the lexer so every // downstream consumer (codegen-c, codegen-js, parser) gets the cleaned form. // // looks_like_code — heuristic gate so we only strip strings that actually // embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim. fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool { let nchars: [String] = native_string_chars(needle) let nlen: Int = native_list_len(nchars) if start + nlen > total { return false } let i = 0 let matched = true while i < nlen { let a: String = native_list_get(chars, start + i) let b: String = native_list_get(nchars, i) if a == b { let i = i + 1 } else { let matched = false; let i = nlen } } matched } fn str_has(s: String, needle: String) -> Bool { let chars: [String] = native_string_chars(s) let total: Int = native_list_len(chars) let i = 0 let found = false while i < total { if substr_at(chars, i, total, needle) { let found = true let i = total } else { let i = i + 1 } } found } fn looks_like_code(s: String) -> Bool { if str_has(s, " String { let chars: [String] = native_string_chars(s) let total: Int = native_list_len(chars) let out_parts: [String] = native_list_empty() let i = 0 let in_squote = false let in_dquote = false let in_btick = false let prev = "" while i < total { let ch: String = native_list_get(chars, i) let in_js_string = false if in_squote { let in_js_string = true } if in_dquote { let in_js_string = true } if in_btick { let in_js_string = true } if in_js_string { // Backslash escape: consume next char verbatim regardless of which. if ch == "\\" { let out_parts = native_list_append(out_parts, ch) let next_i = i + 1 if next_i < total { let nc: String = native_list_get(chars, next_i) let out_parts = native_list_append(out_parts, nc) let prev = nc let i = next_i + 1 } else { let prev = ch let i = next_i } } else { if in_squote { if ch == "'" { let in_squote = false } } else { if in_dquote { if ch == "\"" { let in_dquote = false } } else { if in_btick { if ch == "`" { let in_btick = false } } } } let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } } else { // Not in a JS string. Check for comment openers. let next_i = i + 1 let next_ch = "" if next_i < total { let next_ch: String = native_list_get(chars, next_i) } if ch == "/" { if next_ch == "/" { // URL guard: prev char ':' means this is "://", not a comment. if prev == ":" { let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } else { // Skip until newline (newline itself is preserved so // surrounding line counts/structure stay sane). let i = i + 2 let scanning = true while scanning { if i >= total { let scanning = false } else { let lc: String = native_list_get(chars, i) if lc == "\n" { let scanning = false } else { let i = i + 1 } } } let prev = "" } } else { if next_ch == "*" { // Skip until matching "*/". let i = i + 2 let scanning2 = true while scanning2 { if i >= total { let scanning2 = false } else { let bc: String = native_list_get(chars, i) if bc == "*" { let after = i + 1 if after < total { let nc2: String = native_list_get(chars, after) if nc2 == "/" { let i = after + 1 let scanning2 = false } else { let i = i + 1 } } else { let i = i + 1 } } else { let i = i + 1 } } } let prev = "" } else { let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } } } else { // Open a JS string? if ch == "'" { let in_squote = true let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } else { if ch == "\"" { let in_dquote = true let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } else { if ch == "`" { let in_btick = true let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } else { let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } } } } } } str_join(out_parts, "") } // scan_string — scan a quoted string literal, handling \" escapes. // Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close } fn scan_string(chars: [String], start: Int, total: Int) -> Map { let i = start let parts: [String] = native_list_empty() let running = true while running { if i >= total { let running = false } else { let ch: String = native_list_get(chars, i) if ch == "\\" { // escape: peek next char let next_i = i + 1 if next_i < total { let next_ch: String = native_list_get(chars, next_i) if next_ch == "\"" { let parts = native_list_append(parts, "\"") let i = next_i + 1 } else { if next_ch == "n" { let parts = native_list_append(parts, "\n") let i = next_i + 1 } else { if next_ch == "t" { let parts = native_list_append(parts, "\t") let i = next_i + 1 } else { if next_ch == "r" { let parts = native_list_append(parts, "\r") let i = next_i + 1 } else { if next_ch == "\\" { let parts = native_list_append(parts, "\\") let i = next_i + 1 } else { let parts = native_list_append(parts, next_ch) let i = next_i + 1 } } } } } } else { let i = i + 1 } } else { if ch == "\"" { let i = i + 1 let running = false } else { let parts = native_list_append(parts, ch) let i = i + 1 } } } } { "text": str_join(parts, ""), "pos": i } } // ── Main lexer ──────────────────────────────────────────────────────────────── fn lex(source: String) -> [Map] { let chars: [String] = native_string_chars(source) let total: Int = native_list_len(chars) let tokens: [Map] = native_list_empty() let i: Int = 0 let line_num: Int = 1 state_set("__lex_line", "1") while i < total { let ch: String = native_list_get(chars, i) // Skip whitespace; track newlines for line-number reporting if lex_is_whitespace(ch) { if ch == "\n" { let line_num = line_num + 1 state_set("__lex_line", native_int_to_str(line_num)) } let i = i + 1 } else { // Line comments: // if ch == "/" { let next_i = i + 1 if next_i < total { let next_ch: String = native_list_get(chars, next_i) if next_ch == "/" { // skip to end of line let i = i + 2 let running2 = true while running2 { if i >= total { let running2 = false } else { let lch: String = native_list_get(chars, i) if lch == "\n" { let running2 = false } else { let i = i + 1 } } } } else { let tokens = native_list_append(tokens, make_tok("Slash", "/")) let i = i + 1 } } else { let tokens = native_list_append(tokens, make_tok("Slash", "/")) let i = i + 1 } } else { // String literal if ch == "\"" { let result = scan_string(chars, i + 1, total) let str_text: String = result["text"] let new_pos: Int = result["pos"] // Compile-time scrub: strings that embed JS or CSS get // their // line comments and /* block comments stripped // before the token reaches the parser. Plain prose passes // through untouched. let clean_text = str_text if looks_like_code(str_text) { let clean_text = strip_code_comments(str_text) } let tokens = native_list_append(tokens, make_tok("Str", clean_text)) let i = new_pos } else { // Number literal if lex_is_digit(ch) { let result = scan_digits(chars, i, total) let num_text: String = result["text"] let new_pos: Int = result["pos"] // check for float (dot followed by digit) if new_pos < total { let dot_ch: String = native_list_get(chars, new_pos) if dot_ch == "." { let after_dot = new_pos + 1 if after_dot < total { let after_dot_ch: String = native_list_get(chars, after_dot) if lex_is_digit(after_dot_ch) { let frac_result = scan_digits(chars, after_dot, total) let frac_text: String = frac_result["text"] let frac_pos: Int = frac_result["pos"] let tokens = native_list_append(tokens, make_tok("Float", num_text + "." + frac_text)) let i = frac_pos } else { let tokens = native_list_append(tokens, make_tok("Int", num_text)) let i = new_pos } } else { let tokens = native_list_append(tokens, make_tok("Int", num_text)) let i = new_pos } } else { let tokens = native_list_append(tokens, make_tok("Int", num_text)) let i = new_pos } } else { let tokens = native_list_append(tokens, make_tok("Int", num_text)) let i = new_pos } } else { // Identifier or keyword if lex_is_alpha(ch) || ch == "_" { let result = scan_ident(chars, i, total) let word: String = result["text"] let new_pos: Int = result["pos"] let kw = keyword_kind(word) if kw == "" { let tokens = native_list_append(tokens, make_tok("Ident", word)) } else { let tokens = native_list_append(tokens, make_tok(kw, word)) } let i = new_pos } else { // Multi-char and single-char operators/delimiters let peek_i = i + 1 let peek_ch = "" if peek_i < total { let peek_ch: String = native_list_get(chars, peek_i) } if ch == "=" { if peek_ch == "=" { let tokens = native_list_append(tokens, make_tok("EqEq", "==")) let i = i + 2 } else { if peek_ch == ">" { let tokens = native_list_append(tokens, make_tok("FatArrow", "=>")) let i = i + 2 } else { let tokens = native_list_append(tokens, make_tok("Eq", "=")) let i = i + 1 } } } else { if ch == "!" { if peek_ch == "=" { let tokens = native_list_append(tokens, make_tok("NotEq", "!=")) let i = i + 2 } else { let tokens = native_list_append(tokens, make_tok("Not", "!")) let i = i + 1 } } else { if ch == "<" { if peek_ch == "=" { let tokens = native_list_append(tokens, make_tok("LtEq", "<=")) let i = i + 2 } else { let tokens = native_list_append(tokens, make_tok("Lt", "<")) let i = i + 1 } } else { if ch == ">" { if peek_ch == "=" { let tokens = native_list_append(tokens, make_tok("GtEq", ">=")) let i = i + 2 } else { let tokens = native_list_append(tokens, make_tok("Gt", ">")) let i = i + 1 } } else { if ch == "&" { if peek_ch == "&" { let tokens = native_list_append(tokens, make_tok("And", "&&")) let i = i + 2 } else { let i = i + 1 } } else { if ch == "|" { if peek_ch == "|" { let tokens = native_list_append(tokens, make_tok("Or", "||")) let i = i + 2 } else { if peek_ch == ">" { let tokens = native_list_append(tokens, make_tok("PipeOp", "|>")) let i = i + 2 } else { let tokens = native_list_append(tokens, make_tok("Pipe", "|")) let i = i + 1 } } } else { if ch == "-" { if peek_ch == ">" { let tokens = native_list_append(tokens, make_tok("Arrow", "->")) let i = i + 2 } else { let tokens = native_list_append(tokens, make_tok("Minus", "-")) let i = i + 1 } } else { if ch == ":" { if peek_ch == ":" { let tokens = native_list_append(tokens, make_tok("ColonColon", "::")) let i = i + 2 } else { let tokens = native_list_append(tokens, make_tok("Colon", ":")) let i = i + 1 } } else { if ch == "+" { let tokens = native_list_append(tokens, make_tok("Plus", "+")) let i = i + 1 } else { if ch == "*" { let tokens = native_list_append(tokens, make_tok("Star", "*")) let i = i + 1 } else { if ch == "%" { let tokens = native_list_append(tokens, make_tok("Percent", "%")) let i = i + 1 } else { if ch == "(" { let tokens = native_list_append(tokens, make_tok("LParen", "(")) let i = i + 1 } else { if ch == ")" { let tokens = native_list_append(tokens, make_tok("RParen", ")")) let i = i + 1 } else { if ch == "{" { let tokens = native_list_append(tokens, make_tok("LBrace", "{")) let i = i + 1 } else { if ch == "}" { let tokens = native_list_append(tokens, make_tok("RBrace", "}")) let i = i + 1 } else { if ch == "[" { let tokens = native_list_append(tokens, make_tok("LBracket", "[")) let i = i + 1 } else { if ch == "]" { let tokens = native_list_append(tokens, make_tok("RBracket", "]")) let i = i + 1 } else { if ch == "," { let tokens = native_list_append(tokens, make_tok("Comma", ",")) let i = i + 1 } else { if ch == "." { let tokens = native_list_append(tokens, make_tok("Dot", ".")) let i = i + 1 } else { if ch == ";" { let tokens = native_list_append(tokens, make_tok("Semicolon", ";")) let i = i + 1 } else { if ch == "@" { let tokens = native_list_append(tokens, make_tok("At", "@")) let i = i + 1 } else { if ch == "?" { let tokens = native_list_append(tokens, make_tok("QuestionMark", "?")) let i = i + 1 } else { if ch == "#" { let tokens = native_list_append(tokens, make_tok("Hash", "#")) let i = i + 1 } else { // unknown char — skip let i = i + 1 } } } } } } } } } } } } } } } } } } } } } } } } } } } } } let tokens = native_list_append(tokens, make_tok("Eof", "")) tokens } // parser.el — el self-hosting recursive descent parser // // Consumes the token list produced by lexer.el and builds a list of AST // statement maps. Each statement and expression is a Map. // // The cursor (integer position into the token list) is threaded through every // parse function. Functions return { "node": , "pos": }. // // The token list is passed as a parameter to all parse functions. // native_list_get is used to index into it without cloning. // // Entry point: fn parse(tokens: [Map]) -> [Map] // ── Token access helpers ────────────────────────────────────────────────────── fn tok_at(tokens: [Map], pos: Int) -> Map { native_list_get(tokens, pos) } fn tok_kind(tokens: [Map], pos: Int) -> String { let t = native_list_get(tokens, pos) t["kind"] } fn tok_value(tokens: [Map], pos: Int) -> String { let t = native_list_get(tokens, pos) t["value"] } // tok_line — return the source line number of the token at pos (1-indexed). // Returns 1 if the token has no "line" field or line is 0. fn tok_line(tokens: [Map], pos: Int) -> Int { let t = native_list_get(tokens, pos) let ln: Int = t["line"] if ln <= 0 { return 1 } ln } fn expect(tokens: [Map], pos: Int, kind: String) -> Int { let k = tok_kind(tokens, pos) if k == kind { return pos + 1 } // On mismatch just advance; error recovery is best-effort pos + 1 } // ── Result helpers ──────────────────────────────────────────────────────────── fn make_result(node: Map, pos: Int) -> Map { { "node": node, "pos": pos } } // ── Type annotation parser ──────────────────────────────────────────────────── // Skips over a type annotation, returning the new position. // Types can be: Ident, [Type], Map, Type?, Type fn skip_type(tokens: [Map], pos: Int) -> Int { let k = tok_kind(tokens, pos) // Array type: [Type] if k == "LBracket" { let p = pos + 1 let p = skip_type(tokens, p) let p = expect(tokens, p, "RBracket") return p } // Named type (possibly generic) if k == "Ident" { let p = pos + 1 let k2 = tok_kind(tokens, p) if k2 == "Lt" { // Generic params: skip until matching > let p = p + 1 let depth = 1 let running = true while running { let kk = tok_kind(tokens, p) if kk == "Eof" { let running = false } else { if kk == "Lt" { let depth = depth + 1 let p = p + 1 } else { if kk == "Gt" { let depth = depth - 1 let p = p + 1 if depth <= 0 { let running = false } } else { let p = p + 1 } } } } let k3 = tok_kind(tokens, p) if k3 == "QuestionMark" { let p = p + 1 } return p } // Optional marker if k2 == "QuestionMark" { return p + 1 } return p } pos + 1 } // ── Parameter list ──────────────────────────────────────────────────────────── // Parses (name: Type, name: Type, ...) — returns { "params": [...], "pos": ... } fn parse_params(tokens: [Map], pos: Int) -> Map { let p = expect(tokens, pos, "LParen") let params: [Map] = native_list_empty() let running = true while running { let k = tok_kind(tokens, p) if k == "RParen" { let running = false } else { if k == "Eof" { let running = false } else { // param name let pname = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "Colon") // Capture the leading type identifier so codegen can dispatch // arithmetic vs string-concat on `+` based on declared types. let ptype = "" let kt = tok_kind(tokens, p) if kt == "Ident" { let ptype = tok_value(tokens, p) } let p = skip_type(tokens, p) let param = { "name": pname, "type": ptype } let params = native_list_append(params, param) let k2 = tok_kind(tokens, p) if k2 == "Comma" { let p = p + 1 } } } } let p = expect(tokens, p, "RParen") { "params": params, "pos": p } } // ── Expression parsing ──────────────────────────────────────────────────────── // ── HTML template parser ────────────────────────────────────────────────────── // // HTML templates are written as unquoted HTML in expression position: // return

{title}

// // The parser detects an HTML template when parse_primary sees Lt followed // by a lowercase ident (a known or assumed HTML element name) or `!` (for // ). It then recursively parses the HTML into an AST. // // AST nodes produced: // { "expr": "HtmlTemplate", "root": child_node } // { "html": "Element", "tag": "div", "attrs": [...], "children": [...], "self_closing": bool } // { "html": "Text", "text": "..." } // { "html": "Interp", "value": expr_node } // { "html": "Each", "list": expr_node, "item": "name", "body": [...] } // { "html": "Doctype" } // { "html": "Raw", "value": expr_node } fn is_html_tag_name(name: String) -> Bool { if str_eq(name, "a") { return true } if str_eq(name, "abbr") { return true } if str_eq(name, "address") { return true } if str_eq(name, "area") { return true } if str_eq(name, "article") { return true } if str_eq(name, "aside") { return true } if str_eq(name, "audio") { return true } if str_eq(name, "b") { return true } if str_eq(name, "base") { return true } if str_eq(name, "blockquote") { return true } if str_eq(name, "body") { return true } if str_eq(name, "br") { return true } if str_eq(name, "button") { return true } if str_eq(name, "canvas") { return true } if str_eq(name, "caption") { return true } if str_eq(name, "cite") { return true } if str_eq(name, "code") { return true } if str_eq(name, "col") { return true } if str_eq(name, "colgroup") { return true } if str_eq(name, "data") { return true } if str_eq(name, "datalist") { return true } if str_eq(name, "dd") { return true } if str_eq(name, "del") { return true } if str_eq(name, "details") { return true } if str_eq(name, "dfn") { return true } if str_eq(name, "dialog") { return true } if str_eq(name, "div") { return true } if str_eq(name, "dl") { return true } if str_eq(name, "dt") { return true } if str_eq(name, "em") { return true } if str_eq(name, "embed") { return true } if str_eq(name, "fieldset") { return true } if str_eq(name, "figcaption") { return true } if str_eq(name, "figure") { return true } if str_eq(name, "footer") { return true } if str_eq(name, "form") { return true } if str_eq(name, "h1") { return true } if str_eq(name, "h2") { return true } if str_eq(name, "h3") { return true } if str_eq(name, "h4") { return true } if str_eq(name, "h5") { return true } if str_eq(name, "h6") { return true } if str_eq(name, "head") { return true } if str_eq(name, "header") { return true } if str_eq(name, "hr") { return true } if str_eq(name, "html") { return true } if str_eq(name, "i") { return true } if str_eq(name, "iframe") { return true } if str_eq(name, "img") { return true } if str_eq(name, "input") { return true } if str_eq(name, "ins") { return true } if str_eq(name, "kbd") { return true } if str_eq(name, "label") { return true } if str_eq(name, "legend") { return true } if str_eq(name, "li") { return true } if str_eq(name, "link") { return true } if str_eq(name, "main") { return true } if str_eq(name, "map") { return true } if str_eq(name, "mark") { return true } if str_eq(name, "menu") { return true } if str_eq(name, "meta") { return true } if str_eq(name, "meter") { return true } if str_eq(name, "nav") { return true } if str_eq(name, "noscript") { return true } if str_eq(name, "object") { return true } if str_eq(name, "ol") { return true } if str_eq(name, "optgroup") { return true } if str_eq(name, "option") { return true } if str_eq(name, "output") { return true } if str_eq(name, "p") { return true } if str_eq(name, "param") { return true } if str_eq(name, "picture") { return true } if str_eq(name, "pre") { return true } if str_eq(name, "progress") { return true } if str_eq(name, "q") { return true } if str_eq(name, "rp") { return true } if str_eq(name, "rt") { return true } if str_eq(name, "ruby") { return true } if str_eq(name, "s") { return true } if str_eq(name, "samp") { return true } if str_eq(name, "script") { return true } if str_eq(name, "section") { return true } if str_eq(name, "select") { return true } if str_eq(name, "small") { return true } if str_eq(name, "source") { return true } if str_eq(name, "span") { return true } if str_eq(name, "strong") { return true } if str_eq(name, "style") { return true } if str_eq(name, "sub") { return true } if str_eq(name, "summary") { return true } if str_eq(name, "sup") { return true } if str_eq(name, "table") { return true } if str_eq(name, "tbody") { return true } if str_eq(name, "td") { return true } if str_eq(name, "template") { return true } if str_eq(name, "textarea") { return true } if str_eq(name, "tfoot") { return true } if str_eq(name, "th") { return true } if str_eq(name, "thead") { return true } if str_eq(name, "time") { return true } if str_eq(name, "title") { return true } if str_eq(name, "tr") { return true } if str_eq(name, "track") { return true } if str_eq(name, "u") { return true } if str_eq(name, "ul") { return true } if str_eq(name, "var") { return true } if str_eq(name, "video") { return true } if str_eq(name, "wbr") { return true } false } fn is_void_element(name: String) -> Bool { if str_eq(name, "area") { return true } if str_eq(name, "base") { return true } if str_eq(name, "br") { return true } if str_eq(name, "col") { return true } if str_eq(name, "embed") { return true } if str_eq(name, "hr") { return true } if str_eq(name, "img") { return true } if str_eq(name, "input") { return true } if str_eq(name, "link") { return true } if str_eq(name, "meta") { return true } if str_eq(name, "param") { return true } if str_eq(name, "source") { return true } if str_eq(name, "track") { return true } if str_eq(name, "wbr") { return true } false } // Collect tokens as text content until we hit Lt, LBrace, Eof, or a // closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p } fn parse_html_text_tokens(tokens: [Map], pos: Int) -> Map { let parts: [String] = native_list_empty() let p = pos let running = true while running { let k = tok_kind(tokens, p) if str_eq(k, "Eof") { let running = false } else { if str_eq(k, "Lt") { let running = false } else { if str_eq(k, "LBrace") { let running = false } else { // Check for ). fn parse_html_attrs(tokens: [Map], pos: Int) -> Map { let attrs: [Map] = native_list_empty() let p = pos let running = true while running { let k = tok_kind(tokens, p) if str_eq(k, "Gt") { let running = false } else { if str_eq(k, "Slash") { let running = false } else { if str_eq(k, "Eof") { let running = false } else { // Attribute name: could be Ident or keyword used as attr name let attr_name = tok_value(tokens, p) let p = p + 1 let k2 = tok_kind(tokens, p) if str_eq(k2, "Eq") { let p = p + 1 let k3 = tok_kind(tokens, p) if str_eq(k3, "Str") { // static: attr="value" let attr_val = tok_value(tokens, p) let p = p + 1 let attrs = native_list_append(attrs, { "name": attr_name, "kind": "static", "value": attr_val }) } else { if str_eq(k3, "LBrace") { // dynamic: attr={expr} let r = parse_expr(tokens, p + 1) let val_node = r["node"] let p = r["pos"] let p = expect(tokens, p, "RBrace") let attrs = native_list_append(attrs, { "name": attr_name, "kind": "dynamic", "value": val_node }) } else { // malformed, skip } } } else { // boolean attribute let attrs = native_list_append(attrs, { "name": attr_name, "kind": "bool" }) } } } } } { "attrs": attrs, "pos": p } } // Parse the children of an HTML element until we see the closing tag // or EOF. Returns { "children": [...], "pos": p_after_closing_tag } fn parse_html_children(tokens: [Map], pos: Int, parent_tag: String) -> Map { let children: [Map] = native_list_empty() let p = pos let running = true while running { let k = tok_kind(tokens, p) if str_eq(k, "Eof") { let running = false } else { if str_eq(k, "Lt") { // Check for closing tag: — consume and stop let p = p + 2 // skip tag name let close_name = tok_value(tokens, p) let p = p + 1 // consume > let p = expect(tokens, p, "Gt") let running = false } else { if str_eq(k2, "Not") { // Possible let k3_v = tok_value(tokens, p + 2) if str_eq(k3_v, "doctype") { // consume let p = p + 2 // skip until > let scanning = true while scanning { let ck = tok_kind(tokens, p) if str_eq(ck, "Eof") { let scanning = false } if str_eq(ck, "Gt") { let p = p + 1 let scanning = false } else { let p = p + 1 } } let children = native_list_append(children, { "html": "Doctype" }) } else { let p = p + 1 } } else { // nested element let r = parse_html_element(tokens, p) let child = r["node"] let p = r["pos"] let children = native_list_append(children, child) } } } else { if str_eq(k, "LBrace") { // Interpolation: {expr} or {#each ...} or {/each} let k2 = tok_kind(tokens, p + 1) if str_eq(k2, "Hash") { // {#each list as item} let k3_v = tok_value(tokens, p + 2) if str_eq(k3_v, "each") { let p = p + 3 // parse list expr up to "as" keyword let prev_no_block: String = state_get("__no_block_expr") state_set("__no_block_expr", "1") let r_list = parse_expr(tokens, p) state_set("__no_block_expr", prev_no_block) let list_expr = r_list["node"] let p = r_list["pos"] // expect "as" let p = expect(tokens, p, "As") // item variable name let item_name = tok_value(tokens, p) let p = p + 1 // consume closing } let p = expect(tokens, p, "RBrace") // parse body until {/each} let r_body = parse_html_each_body(tokens, p) let body_children = r_body["children"] let p = r_body["pos"] let each_node: Map = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children } let children = native_list_append(children, each_node) } else { let p = p + 1 } } else { if str_eq(k2, "Slash") { // {/each} — end of each block, stop // skip {/each} let p = p + 2 // skip "each" let p = p + 1 // skip } let p = expect(tokens, p, "RBrace") let running = false } else { // regular {expr} let r = parse_expr(tokens, p + 1) let interp_val = r["node"] let p = r["pos"] let p = expect(tokens, p, "RBrace") // Check if the expr is a call to raw() let is_raw_call = false let interp_kind: String = interp_val["expr"] if str_eq(interp_kind, "Call") { let fn_node = interp_val["func"] let fn_kind: String = fn_node["expr"] if str_eq(fn_kind, "Ident") { let fn_name_v: String = fn_node["name"] if str_eq(fn_name_v, "raw") { let is_raw_call = true } } } if is_raw_call { let raw_args = interp_val["args"] let raw_inner = native_list_get(raw_args, 0) let children = native_list_append(children, { "html": "Raw", "value": raw_inner }) } else { let children = native_list_append(children, { "html": "Interp", "value": interp_val }) } } } } else { // Text tokens — collect run of non-special tokens let r_text = parse_html_text_tokens(tokens, p) let text_str: String = r_text["text"] let p = r_text["pos"] let text_trimmed: String = str_trim(text_str) if !str_eq(text_trimmed, "") { let children = native_list_append(children, { "html": "Text", "text": text_trimmed }) } } } } } { "children": children, "pos": p } } // Parse body of {#each} until {/each}. Mirrors parse_html_children but // stops at the {/each} sentinel rather than a closing element tag. fn parse_html_each_body(tokens: [Map], pos: Int) -> Map { parse_html_children(tokens, pos, "__each__") } // Parse a single HTML element: children // or self-closing: // Pos points to the Lt token. fn parse_html_element(tokens: [Map], pos: Int) -> Map { let p = pos // consume < let p = expect(tokens, p, "Lt") // tag name let tag_name = tok_value(tokens, p) let p = p + 1 // parse attributes let r_attrs = parse_html_attrs(tokens, p) let attrs = r_attrs["attrs"] let p = r_attrs["pos"] // check for self-closing /> or void element let k = tok_kind(tokens, p) let self_closing = false if str_eq(k, "Slash") { // /> let p = p + 1 let p = expect(tokens, p, "Gt") let self_closing = true return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p) } // consume > let p = expect(tokens, p, "Gt") // void elements have no children, no closing tag if is_void_element(tag_name) { return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p) } // parse children let r_children = parse_html_children(tokens, p, tag_name) let children = r_children["children"] let p = r_children["pos"] make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": children, "self_closing": false }, p) } // Entry point for HTML template parsing. // Pos points to Lt (or Lt Not for ). // May parse an optional prefix followed by the root element. fn parse_html_template(tokens: [Map], pos: Int) -> Map { let p = pos // Check for let doctype = false let k = tok_kind(tokens, p) let k2 = tok_kind(tokens, p + 1) if str_eq(k, "Lt") { if str_eq(k2, "Not") { let k3_v = tok_value(tokens, p + 2) if str_eq(k3_v, "doctype") { let doctype = true // consume let p = p + 2 let scanning = true while scanning { let ck = tok_kind(tokens, p) if str_eq(ck, "Eof") { let scanning = false } if str_eq(ck, "Gt") { let p = p + 1 let scanning = false } else { let p = p + 1 } } } } } // Parse root element let r = parse_html_element(tokens, p) let root = r["node"] let p = r["pos"] let root_with_doctype = root if doctype { let root_with_doctype = { "html": root["html"], "tag": root["tag"], "attrs": root["attrs"], "children": root["children"], "self_closing": root["self_closing"], "doctype": true } } make_result({ "expr": "HtmlTemplate", "root": root_with_doctype }, p) } fn parse_primary(tokens: [Map], pos: Int) -> Map { let k = tok_kind(tokens, pos) let v = tok_value(tokens, pos) // Integer literal if k == "Int" { return make_result({ "expr": "Int", "value": v }, pos + 1) } // Float literal if k == "Float" { return make_result({ "expr": "Float", "value": v }, pos + 1) } // String literal if k == "Str" { return make_result({ "expr": "Str", "value": v }, pos + 1) } // Bool literal if k == "Bool" { return make_result({ "expr": "Bool", "value": v }, pos + 1) } // HTML template literal: ... or ... // Detected in value position only; `<` in comparison position is already // consumed by parse_binop before parse_primary is reached. if k == "Lt" { let k2 = tok_kind(tokens, pos + 1) if str_eq(k2, "Not") { return parse_html_template(tokens, pos) } if str_eq(k2, "Ident") { let tag_candidate = tok_value(tokens, pos + 1) if is_html_tag_name(tag_candidate) { return parse_html_template(tokens, pos) } } } // Identifier if k == "Ident" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } // Grouped expression if k == "LParen" { let r = parse_expr(tokens, pos + 1) let node = r["node"] let p = r["pos"] let p = expect(tokens, p, "RParen") return make_result(node, p) } // Array literal: [e1, e2, ...] if k == "LBracket" { let p = pos + 1 let elems: [Map] = native_list_empty() let running = true while running { let k2 = tok_kind(tokens, p) if k2 == "RBracket" { let running = false } else { if k2 == "Eof" { let running = false } else { let r = parse_expr(tokens, p) let elem = r["node"] let p = r["pos"] let elems = native_list_append(elems, elem) let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } let p = expect(tokens, p, "RBracket") return make_result({ "expr": "Array", "elems": elems }, p) } // Map literal: { "key": val, ... } // // Suppression: when parse_if / parse_while / parse_for / parse_match // are parsing a head expression, they set __no_block_expr=1 so a stray // `{` here doesn't get gobbled as a Map literal — it belongs to the // following block. Without this, `if a || b { ... }` could mis-parse // (the `||` recursion lands at `{` and tries to read the if-body as a // Map, then loops forever when keys don't match `Str: expr`). if k == "LBrace" { let no_block: String = state_get("__no_block_expr") if str_eq(no_block, "1") { // Fall through to fallback — caller will see `{` and treat it // as the start of the block they're expecting. return make_result({ "expr": "Nil" }, pos) } let p = pos + 1 let pairs: [Map] = native_list_empty() let running = true while running { let k2 = tok_kind(tokens, p) if k2 == "RBrace" { let running = false } else { if k2 == "Eof" { let running = false } else { // key: Str token let key = tok_value(tokens, p) let new_p: Int = p + 1 let new_p = expect(tokens, new_p, "Colon") let r = parse_expr(tokens, new_p) let val_node = r["node"] let new_p = r["pos"] let pair = { "key": key, "value": val_node } let pairs = native_list_append(pairs, pair) let k3 = tok_kind(tokens, new_p) if k3 == "Comma" { let new_p = new_p + 1 } // Non-progress guard: malformed map content can leave // parse_expr returning the same pos. Force advance. if new_p <= p { let p = p + 1 } else { let p = new_p } } } } let p = expect(tokens, p, "RBrace") return make_result({ "expr": "Map", "pairs": pairs }, p) } // if expression if k == "If" { let r = parse_if(tokens, pos) return r } // match expression if k == "Match" { let r = parse_match(tokens, pos) return r } // for expression (used as statement) if k == "For" { let r = parse_for_expr(tokens, pos) return r } // Anonymous function literal (lambda): fn(params) -> RetType { body } // Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... }) // Produces a Lambda expression node (distinct from a named FnDef statement). if k == "Fn" { let p = pos + 1 let r = parse_params(tokens, p) let params = r["params"] let p = r["pos"] let ret_type = "" let k2 = tok_kind(tokens, p) if k2 == "Arrow" { let p = p + 1 let kt = tok_kind(tokens, p) if kt == "Ident" { let ret_type = tok_value(tokens, p) } let p = skip_type(tokens, p) } let r2 = parse_block(tokens, p) let body = r2["stmts"] let p = r2["pos"] return make_result({ "expr": "Lambda", "params": params, "body": body, "ret_type": ret_type }, p) } // Unary not if k == "Not" { let r = parse_primary(tokens, pos + 1) let inner = r["node"] let p = r["pos"] return make_result({ "expr": "Not", "inner": inner }, p) } // Unary minus if k == "Minus" { let r = parse_primary(tokens, pos + 1) let inner = r["node"] let p = r["pos"] return make_result({ "expr": "Neg", "inner": inner }, p) } // Soft keywords usable as identifiers in expression position. The lexer // turns words like `target`, `to`, `via`, `deploy`, etc. into dedicated // token kinds for the deploy/retry DSLs, but they're also valid as // parameter names and local variables. When one of these appears in // expression position (where only an Ident makes sense), treat it as // an Ident carrying the original text — otherwise references to a // parameter named `target` compile to EL_NULL. if k == "Target" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "To" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Via" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Deploy" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Reason" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Times" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Fallback" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Retry" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Parallel" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Trace" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Requires" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Where" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "As" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "With" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Manager" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Engine" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Accessor" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } if k == "Vessel" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } // Fallback: skip unknown token make_result({ "expr": "Nil" }, pos + 1) } fn parse_if(tokens: [Map], pos: Int) -> Map { let p = expect(tokens, pos, "If") // Suppress Map-literal parsing in the cond so a stray `{` (the start // of the then-block) isn't gobbled as a Map. let prev_no_block: String = state_get("__no_block_expr") state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) state_set("__no_block_expr", prev_no_block) let cond = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) let then_stmts = r2["stmts"] let p = r2["pos"] let has_else = false let else_stmts: [Map] = native_list_empty() let k2 = tok_kind(tokens, p) if k2 == "Else" { let p = p + 1 let k3 = tok_kind(tokens, p) if k3 == "If" { // else-if chain: parse as nested if let r3 = parse_if(tokens, p) let nested = r3["node"] let p = r3["pos"] let else_stmts = native_list_append(else_stmts, { "stmt": "Expr", "value": nested }) let has_else = true } else { let r3 = parse_block(tokens, p) let else_stmts = r3["stmts"] let p = r3["pos"] let has_else = true } } make_result({ "expr": "If", "cond": cond, "then": then_stmts, "else": else_stmts, "has_else": has_else }, p) } fn parse_match(tokens: [Map], pos: Int) -> Map { let p = expect(tokens, pos, "Match") let prev_no_block: String = state_get("__no_block_expr") state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) state_set("__no_block_expr", prev_no_block) let subject = r["node"] let p = r["pos"] let p = expect(tokens, p, "LBrace") let arms: [Map] = native_list_empty() let running = true while running { let k = tok_kind(tokens, p) if k == "RBrace" { let running = false } else { if k == "Eof" { let running = false } else { // parse pattern => body let r2 = parse_pattern(tokens, p) let pattern = r2["node"] let p = r2["pos"] let p = expect(tokens, p, "FatArrow") let r3 = parse_expr(tokens, p) let body = r3["node"] let p = r3["pos"] let arm = { "pattern": pattern, "body": body } let arms = native_list_append(arms, arm) let k2 = tok_kind(tokens, p) if k2 == "Comma" { let p = p + 1 } } } } let p = expect(tokens, p, "RBrace") make_result({ "expr": "Match", "subject": subject, "arms": arms }, p) } fn parse_pattern(tokens: [Map], pos: Int) -> Map { let k = tok_kind(tokens, pos) if k == "Ident" { let v = tok_value(tokens, pos) if v == "_" { return make_result({ "pattern": "Wildcard" }, pos + 1) } // Check for Enum::Variant pattern (Color::Red, Status::Ok, etc.) // Lexed as: Ident ColonColon Ident let next_k = tok_kind(tokens, pos + 1) if next_k == "ColonColon" { let variant_name = tok_value(tokens, pos + 2) return make_result({ "pattern": "Variant", "enum_name": v, "variant": variant_name }, pos + 3) } return make_result({ "pattern": "Binding", "name": v }, pos + 1) } if k == "Int" { return make_result({ "pattern": "LitInt", "value": tok_value(tokens, pos) }, pos + 1) } if k == "Str" { return make_result({ "pattern": "LitStr", "value": tok_value(tokens, pos) }, pos + 1) } if k == "Bool" { return make_result({ "pattern": "LitBool", "value": tok_value(tokens, pos) }, pos + 1) } // Wildcard _ make_result({ "pattern": "Wildcard" }, pos + 1) } fn parse_for_expr(tokens: [Map], pos: Int) -> Map { let p = expect(tokens, pos, "For") let item_name = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "In") let prev_no_block: String = state_get("__no_block_expr") state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) state_set("__no_block_expr", prev_no_block) let list_expr = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) let body = r2["stmts"] let p = r2["pos"] make_result({ "expr": "For", "item": item_name, "list": list_expr, "body": body }, p) } fn parse_block(tokens: [Map], pos: Int) -> Map { let p = expect(tokens, pos, "LBrace") let stmts: [Map] = native_list_empty() let running = true while running { let k = tok_kind(tokens, p) if k == "RBrace" { let running = false } else { if k == "Eof" { let running = false } else { let r = parse_stmt(tokens, p) let stmt = r["node"] let new_p: Int = r["pos"] let stmts = native_list_append(stmts, stmt) // Non-progress guard: a malformed input (e.g. `||` that // dragged the parser into Map-literal mode partway through // the if-body) can leave parse_stmt returning the same pos. // Force advance so the outer compile doesn't hang. if new_p <= p { let p = p + 1 } else { let p = new_p } } } } let p = expect(tokens, p, "RBrace") { "stmts": stmts, "pos": p } } // ── Postfix expressions (calls, field access, index) ───────────────────────── // is_duration_unit — recognise the postfix unit suffix on a numeric literal. // Used by parse_postfix to detect `30.seconds`-shape time literals before // falling back to the generic `obj.field` field-access lowering. Singular // and plural forms map to the same nanosecond multiplier; codegen does the // arithmetic at compile time. fn is_duration_unit(name: String) -> Bool { if name == "nanos" { return true } if name == "nano" { return true } if name == "millis" { return true } if name == "milli" { return true } if name == "millisecond" { return true } if name == "milliseconds" { return true } if name == "second" { return true } if name == "seconds" { return true } if name == "minute" { return true } if name == "minutes" { return true } if name == "hour" { return true } if name == "hours" { return true } if name == "day" { return true } if name == "days" { return true } false } fn parse_postfix(tokens: [Map], pos: Int) -> Map { let r = parse_primary(tokens, pos) let node = r["node"] let p = r["pos"] // Postfix duration literal: `.` where is one of // nanos | millis | seconds | minutes | hours | days (each with an // optional plural). We recognise this before the generic Dot-as-field // path so `30.seconds` lowers to a DurationLit AST node carrying the // count and the unit, not a field access on an Int. let primary_kind: String = node["expr"] if primary_kind == "Int" { let dot_kind = tok_kind(tokens, p) if dot_kind == "Dot" { let unit_kind = tok_kind(tokens, p + 1) if unit_kind == "Ident" { let unit_name = tok_value(tokens, p + 1) if is_duration_unit(unit_name) { let count_str: String = node["value"] let node = { "expr": "DurationLit", "count": count_str, "unit": unit_name } let p = p + 2 } } } } let running = true while running { let k = tok_kind(tokens, p) if k == "LParen" { // function call let p = p + 1 let args: [Map] = native_list_empty() let run2 = true while run2 { let k2 = tok_kind(tokens, p) if k2 == "RParen" { let run2 = false } else { if k2 == "Eof" { let run2 = false } else { let r2 = parse_expr(tokens, p) let arg = r2["node"] let p = r2["pos"] let args = native_list_append(args, arg) let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } let p = expect(tokens, p, "RParen") let node = { "expr": "Call", "func": node, "args": args } } else { if k == "Dot" { let field = tok_value(tokens, p + 1) let p = p + 2 let node = { "expr": "Field", "object": node, "field": field } } else { if k == "LBracket" { let r2 = parse_expr(tokens, p + 1) let idx = r2["node"] let p = r2["pos"] let p = expect(tokens, p, "RBracket") let node = { "expr": "Index", "object": node, "index": idx } } else { if k == "QuestionMark" { let p = p + 1 let node = { "expr": "Try", "inner": node } } else { let running = false } } } } } make_result(node, p) } // ── Binary expression precedence climbing ──────────────────────────────────── fn op_precedence(kind: String) -> Int { if kind == "Or" { return 1 } if kind == "And" { return 2 } if kind == "EqEq" { return 3 } if kind == "NotEq" { return 3 } if kind == "Lt" { return 4 } if kind == "Gt" { return 4 } if kind == "LtEq" { return 4 } if kind == "GtEq" { return 4 } if kind == "Plus" { return 5 } if kind == "Minus" { return 5 } if kind == "Star" { return 6 } if kind == "Slash" { return 6 } 0 } fn is_binop(kind: String) -> Bool { if kind == "Or" { return true } if kind == "And" { return true } if kind == "EqEq" { return true } if kind == "NotEq" { return true } if kind == "Lt" { return true } if kind == "Gt" { return true } if kind == "LtEq" { return true } if kind == "GtEq" { return true } if kind == "Plus" { return true } if kind == "Minus" { return true } if kind == "Star" { return true } if kind == "Slash" { return true } false } fn parse_binop(tokens: [Map], pos: Int, min_prec: Int) -> Map { let r = parse_postfix(tokens, pos) let left = r["node"] let p = r["pos"] let running = true while running { let k = tok_kind(tokens, p) let prec = op_precedence(k) if is_binop(k) { if prec >= min_prec { let op = k let r2 = parse_binop(tokens, p + 1, prec + 1) let right = r2["node"] let p = r2["pos"] let left = { "expr": "BinOp", "op": op, "left": left, "right": right } } else { let running = false } } else { let running = false } } make_result(left, p) } fn parse_expr(tokens: [Map], pos: Int) -> Map { parse_binop(tokens, pos, 1) } // ── Statement parsing ───────────────────────────────────────────────────────── fn parse_stmt(tokens: [Map], pos: Int) -> Map { let k = tok_kind(tokens, pos) // let binding if k == "Let" { let p = pos + 1 let name = tok_value(tokens, p) let p = p + 1 let ltype = "" let k2 = tok_kind(tokens, p) // optional type annotation: name: Type — capture the leading // identifier so codegen can dispatch arithmetic vs concat on // `+` between two typed Idents. if k2 == "Colon" { let p = p + 1 let kt = tok_kind(tokens, p) if kt == "Ident" { let ltype = tok_value(tokens, p) } let p = skip_type(tokens, p) } let p = expect(tokens, p, "Eq") let r = parse_expr(tokens, p) let val = r["node"] let p = r["pos"] return make_result({ "stmt": "Let", "name": name, "value": val, "type": ltype }, p) } // return statement if k == "Return" { let p = pos + 1 let k2 = tok_kind(tokens, p) if k2 == "RBrace" { return make_result({ "stmt": "Return", "value": { "expr": "Nil" } }, p) } if k2 == "Eof" { return make_result({ "stmt": "Return", "value": { "expr": "Nil" } }, p) } let r = parse_expr(tokens, p) let val = r["node"] let p = r["pos"] return make_result({ "stmt": "Return", "value": val }, p) } // extern fn declaration (no body — forward declaration for separate compilation) if k == "Extern" { let p = pos + 1 let k2: String = tok_kind(tokens, p) if str_eq(k2, "Fn") { let p = p + 1 let name: String = tok_value(tokens, p) let p = p + 1 let r = parse_params(tokens, p) let params = r["params"] let p = r["pos"] let ret_type = "" let k3: String = tok_kind(tokens, p) if str_eq(k3, "Arrow") { let p = p + 1 let kt: String = tok_kind(tokens, p) if str_eq(kt, "Ident") { let ret_type = tok_value(tokens, p) } let p = skip_type(tokens, p) } return make_result({ "stmt": "ExternFn", "name": name, "params": params, "ret_type": ret_type }, p) } } // fn definition if k == "Fn" { let p = pos + 1 let name = tok_value(tokens, p) let p = p + 1 let r = parse_params(tokens, p) let params = r["params"] let p = r["pos"] // return type annotation: -> Type. Capture the leading identifier // so codegen can distinguish Void-returning functions from value- // returning ones. Anything not "Void" is treated as a value type. let ret_type = "" let k2 = tok_kind(tokens, p) if k2 == "Arrow" { let p = p + 1 let kt = tok_kind(tokens, p) if kt == "Ident" { let ret_type = tok_value(tokens, p) } let p = skip_type(tokens, p) } let r2 = parse_block(tokens, p) let body = r2["stmts"] let p = r2["pos"] return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p) } // type definition: `type Name = { field: Type, ... }` // The `=` between the name and the brace is optional in the spec but // present in practice. Skip it if present before consuming the LBrace. if k == "Type" { let p = pos + 1 let name = tok_value(tokens, p) let p = p + 1 // Consume optional `=` before the opening brace let pk = tok_kind(tokens, p) if pk == "Eq" { let p = p + 1 } let p = expect(tokens, p, "LBrace") let fields: [Map] = native_list_empty() let running = true while running { let k2 = tok_kind(tokens, p) if k2 == "RBrace" { let running = false } else { if k2 == "Eof" { let running = false } else { let fname = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "Colon") let p = skip_type(tokens, p) let fields = native_list_append(fields, { "name": fname }) let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } let p = expect(tokens, p, "RBrace") return make_result({ "stmt": "TypeDef", "name": name, "fields": fields }, p) } // enum definition if k == "Enum" { let p = pos + 1 let name = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "LBrace") let variants: [Map] = native_list_empty() let running = true while running { let k2 = tok_kind(tokens, p) if k2 == "RBrace" { let running = false } else { if k2 == "Eof" { let running = false } else { let vname = tok_value(tokens, p) let p = p + 1 let variants = native_list_append(variants, { "name": vname }) let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } let p = expect(tokens, p, "RBrace") return make_result({ "stmt": "EnumDef", "name": name, "variants": variants }, p) } // import statement if k == "Import" { let p = pos + 1 let path = tok_value(tokens, p) let p = p + 1 return make_result({ "stmt": "Import", "path": path }, p) } // from ... import { ... } if k == "From" { let p = pos + 1 let module_name = tok_value(tokens, p) let p = p + 1 // skip "import" keyword let k2 = tok_kind(tokens, p) if k2 == "Import" { let p = p + 1 } // skip { Name, ... } let k3 = tok_kind(tokens, p) if k3 == "LBrace" { let p = p + 1 let running = true while running { let k4 = tok_kind(tokens, p) if k4 == "RBrace" { let running = false } else { if k4 == "Eof" { let running = false } else { let p = p + 1 let k5 = tok_kind(tokens, p) if k5 == "Comma" { let p = p + 1 } } } } let p = expect(tokens, p, "RBrace") } return make_result({ "stmt": "Import", "path": module_name }, p) } // while loop if k == "While" { let p = pos + 1 let prev_no_block: String = state_get("__no_block_expr") state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) state_set("__no_block_expr", prev_no_block) let cond = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) let body = r2["stmts"] let p = r2["pos"] return make_result({ "stmt": "While", "cond": cond, "body": body }, p) } // for loop if k == "For" { let p = pos + 1 let item_name = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "In") let prev_no_block: String = state_get("__no_block_expr") state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) state_set("__no_block_expr", prev_no_block) let list_expr = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) let body = r2["stmts"] let p = r2["pos"] return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p) } // try/catch statement // try { body } catch (name: Type) { handler } // The catch variable name and type are both captured; type is skipped. if k == "Try" { let p = pos + 1 let r_try = parse_block(tokens, p) let try_body = r_try["stmts"] let p = r_try["pos"] let catch_name = "err" let k2 = tok_kind(tokens, p) if str_eq(k2, "Catch") { let p = p + 1 let p = expect(tokens, p, "LParen") // catch variable name let kn = tok_kind(tokens, p) if str_eq(kn, "Ident") { let catch_name = tok_value(tokens, p) let p = p + 1 } // optional type annotation: : Type let k3 = tok_kind(tokens, p) if str_eq(k3, "Colon") { let p = p + 1 let p = skip_type(tokens, p) } let p = expect(tokens, p, "RParen") let r_catch = parse_block(tokens, p) let catch_body = r_catch["stmts"] let p = r_catch["pos"] return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": catch_body }, p) } return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": native_list_empty() }, p) } // @decorator — capture decorator name and attach to following stmt if k == "At" { let p = pos + 1 let dec_name = tok_value(tokens, p) let p = p + 1 let r = parse_stmt(tokens, p) let inner = r["node"] let p2 = r["pos"] let inner_kind: String = inner["stmt"] if str_eq(inner_kind, "FnDef") { let with_dec = { "stmt": "FnDef", "name": inner["name"], "params": inner["params"], "body": inner["body"], "ret_type": inner["ret_type"], "decorator": dec_name } return make_result(with_dec, p2) } return r } // cgi block: cgi "name" { field: "val", ... } if k == "Cgi" { let p = pos + 1 let name = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "LBrace") let dharma_id = "" let principal = "" let network = "" let engram = "" let has_dharma_id = false let has_principal = false let has_network = false let has_engram = false let running = true while running { let k2 = tok_kind(tokens, p) if k2 == "RBrace" { let running = false } else { if k2 == "Eof" { let running = false } else { let fname = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "Colon") let fval = tok_value(tokens, p) let p = p + 1 if str_eq(fname, "dharma_id") { let dharma_id = fval let has_dharma_id = true } if str_eq(fname, "principal") { let principal = fval let has_principal = true } if str_eq(fname, "network") { let network = fval let has_network = true } if str_eq(fname, "engram") { let engram = fval let has_engram = true } let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } let p = expect(tokens, p, "RBrace") return make_result({ "stmt": "CgiBlock", "name": name, "dharma_id": dharma_id, "principal": principal, "network": network, "engram": engram, "has_dharma_id": has_dharma_id, "has_principal": has_principal, "has_network": has_network, "has_engram": has_engram }, p) } // service block: service "name" { sponsor: "...", domain: "...", ... } // // A `service` declaration restricts the program's capabilities at // compile time: services CANNOT call self-formation primitives // (llm_call_agentic, llm_register_tool, dharma_emit, dharma_field, // mindlink-creation). Codegen enforces this with #error directives. if k == "Service" { let p = pos + 1 let name = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "LBrace") let sponsor = "" let domain = "" let running = true while running { let k2 = tok_kind(tokens, p) if k2 == "RBrace" { let running = false } else { if k2 == "Eof" { let running = false } else { let fname = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "Colon") let fval = tok_value(tokens, p) let p = p + 1 if str_eq(fname, "sponsor") { let sponsor = fval } if str_eq(fname, "domain") { let domain = fval } let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } let p = expect(tokens, p, "RBrace") return make_result({ "stmt": "ServiceBlock", "name": name, "sponsor": sponsor, "domain": domain }, p) } // assert statement: assert or assert , "msg" if k == "Assert" { let assert_line: Int = tok_line(tokens, pos) let p = pos + 1 let r = parse_expr(tokens, p) let expr_node = r["node"] let p = r["pos"] let msg = "" let k2 = tok_kind(tokens, p) if k2 == "Comma" { let p = p + 1 let msg = tok_value(tokens, p) let p = p + 1 } return make_result({ "stmt": "Assert", "expr": expr_node, "msg": msg, "line": assert_line }, p) } // test block: test "name" { stmts } if k == "Test" { let test_line: Int = tok_line(tokens, pos) let p = pos + 1 let name = tok_value(tokens, p) let p = p + 1 let r2 = parse_block(tokens, p) let body = r2["stmts"] let p = r2["pos"] return make_result({ "stmt": "TestDef", "name": name, "body": body, "line": test_line }, p) } // Bare reassignment: `name = expr`. Handled BEFORE the expression // fallback so we don't drop the assign on the floor and emit three // orphan expressions (the original silent-miscompile bug). El's `let` // already permits redeclaration, so this only applies when the parser // sees an Ident followed directly by `=`. `==` is a separate kind // (EqEq) so there's no ambiguity. if k == "Ident" { let k2 = tok_kind(tokens, pos + 1) if k2 == "Eq" { let name = tok_value(tokens, pos) let p = pos + 2 let r = parse_expr(tokens, p) let val = r["node"] let p = r["pos"] return make_result({ "stmt": "Assign", "name": name, "value": val }, p) } } // bare expression or if/match statement let r = parse_expr(tokens, pos) let val = r["node"] let p = r["pos"] make_result({ "stmt": "Expr", "value": val }, p) } // ── Top-level parse ──────────────────────────────────────────────────────────── fn parse(tokens: [Map]) -> [Map] { let total: Int = native_list_len(tokens) let stmts: [Map] = native_list_empty() let pos: Int = 0 let running = true while running { if pos >= total { let running = false } else { let k = tok_kind(tokens, pos) if k == "Eof" { let running = false } else { let r = parse_stmt(tokens, pos) let stmt = r["node"] let new_pos: Int = r["pos"] let stmts = native_list_append(stmts, stmt) // Guard against infinite loops — if pos didn't advance, force it if new_pos <= pos { let pos = pos + 1 } else { let pos = new_pos } } } } stmts } // 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], 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 { // Use index-based byte scanning via str_char_code(s, i) and str_char_at(s, i). // This avoids native_string_chars + str_join, which corrupts high-byte (>= 0x80) // characters because list_join's looks_like_string heuristic rejects strings // whose first byte is >= 0x7F and emits them as decimal pointer values instead. // // IMPORTANT: after a \xNN hex escape, if the next byte is a hex digit // (0-9, a-f, A-F), we emit `""` to split the C string literal so the C // compiler does not greedily read extra hex digits as part of the escape. // E.g. "\xad" followed by "bamos" must become "\xad" "bamos" because 'b' // is a hex digit and C would otherwise read "\xadb" (= 0xADB, out of range). let total: Int = str_len(s) let parts: [String] = native_list_empty() let i: Int = 0 let prev_was_hex_escape: Bool = false while i < total { let bval: Int = str_char_code(s, i) // If the previous token was a \xNN escape and the current byte is a // hex digit, insert an empty string literal ("") to break the escape. if prev_was_hex_escape { if is_hex_digit_byte(bval) { let parts = native_list_append(parts, "\"\"") } } let prev_was_hex_escape = false if bval == 34 { // 34 = '"' let parts = native_list_append(parts, "\\\"") } else { if bval == 92 { // 92 = '\\' let parts = native_list_append(parts, "\\\\") } else { if bval == 10 { // 10 = '\n' let parts = native_list_append(parts, "\\n") } else { if bval == 13 { // 13 = '\r' let parts = native_list_append(parts, "\\r") } else { if bval == 9 { // 9 = '\t' let parts = native_list_append(parts, "\\t") } else { if bval >= 128 { // Escape non-ASCII bytes (>= 0x80) as \xNN so // Clang does not misinterpret multi-byte UTF-8 // sequences in C string literals. let parts = native_list_append(parts, "\\x" + byte_to_hex2(bval)) let prev_was_hex_escape = true } else { let parts = native_list_append(parts, str_char_at(s, i)) } } } } } } let i = i + 1 } str_join(parts, "") } fn c_str_lit(s: String) -> String { "\"" + c_escape(s) + "\"" } // -- Type mapping -------------------------------------------------------------- fn el_type_to_c(type_str: String) -> String { if type_str == "String" { return "const char*" } if type_str == "Int" { return "int64_t" } if type_str == "Bool" { return "int" } if type_str == "Float" { return "double" } if type_str == "Void" { return "void" } if type_str == "void" { return "void" } "void*" } // -- Code emission ------------------------------------------------------------- // // emit_line/emit_blank stream output directly via println. // This avoids building a large string in memory. fn emit_line(line: String) -> Void { println(line) } fn emit_blank() -> Void { println("") } // -- Operator helpers ---------------------------------------------------------- fn binop_to_c(op: String) -> String { if op == "Plus" { return "+" } if op == "Minus" { return "-" } if op == "Star" { return "*" } if op == "Slash" { return "/" } if op == "EqEq" { return "==" } if op == "NotEq" { return "!=" } if op == "Lt" { return "<" } if op == "Gt" { return ">" } if op == "LtEq" { return "<=" } if op == "GtEq" { return ">=" } if op == "And" { return "&&" } if op == "Or" { return "||" } op } // -- Expression codegen -------------------------------------------------------- // // cg_expr returns a C expression string (not a statement). // duration_unit_nanos - multiplier from a postfix-literal unit name to // nanoseconds. Singular and plural forms collapse to the same multiplier; // the parser already restricted `unit` to the set is_duration_unit accepts. // Returns the multiplier as a decimal string suitable for splicing into // the generated C as a literal int64 expression. fn duration_unit_nanos(unit: String) -> String { if str_eq(unit, "nano") { return "1LL" } if str_eq(unit, "nanos") { return "1LL" } if str_eq(unit, "milli") { return "1000000LL" } if str_eq(unit, "millis") { return "1000000LL" } if str_eq(unit, "millisecond") { return "1000000LL" } if str_eq(unit, "milliseconds") { return "1000000LL" } if str_eq(unit, "second") { return "1000000000LL" } if str_eq(unit, "seconds") { return "1000000000LL" } if str_eq(unit, "minute") { return "60000000000LL" } if str_eq(unit, "minutes") { return "60000000000LL" } if str_eq(unit, "hour") { return "3600000000000LL" } if str_eq(unit, "hours") { return "3600000000000LL" } if str_eq(unit, "day") { return "86400000000000LL" } if str_eq(unit, "days") { return "86400000000000LL" } "1LL" } // ── 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], acc_var: String) -> String { let n: Int = native_list_len(children) let i = 0 let out = "" while i < n { let child: Map = native_list_get(children, i) let html_kind: String = child["html"] if str_eq(html_kind, "Text") { let text: String = child["text"] let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(text) + ")); " } if str_eq(html_kind, "Doctype") { let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"\")); " } if str_eq(html_kind, "Interp") { let val_node = child["value"] let val_c: String = cg_expr(val_node) let out = out + 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 out = out + 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 out = out + elem_c } if str_eq(html_kind, "Each") { let each_c: String = cg_html_each(child, acc_var) let out = out + each_c } let i = i + 1 } out } // 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], acc_var: String) -> String { let n: Int = native_list_len(attrs) let i = 0 let out = "" // Closing-quote snippet: EL_STR("\"") in C text. let close_q: String = "EL_STR(" + c_str_lit("\"") + ")" while i < n { let attr: Map = 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 out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); " let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(sv) + ")); " let out = out + 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 out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); " let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); " let out = out + 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 out = out + acc_var + " = el_str_concat(" + acc_var + ", " + bool_attr + "); " } } let i = i + 1 } out } // Generate code for a single element, appending into acc_var. fn cg_html_element_str(elem: Map, acc_var: String) -> String { let tag: String = elem["tag"] let attrs: [Map] = elem["attrs"] let children: [Map] = elem["children"] let self_closing: Bool = elem["self_closing"] // Open tag: let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"/>\")); " } else { // Close open tag: > let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\">\")); " let out = out + cg_html_parts(children, acc_var) let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"\")); " } out } // Generate code for {#each list as item} ... {/each}. fn cg_html_each(node: Map, acc_var: String) -> String { let list_expr = node["list"] let item_name: String = node["item"] let body_children: [Map] = 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 + "} } " } // Top-level HTML template codegen — returns a C statement-expression string. fn cg_html_template(expr: Map) -> 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 let doctype_flag: Bool = root["doctype"] let doctype_prefix: String = "" if doctype_flag { let doctype_prefix = acc + " = el_str_concat(" + acc + ", EL_STR(\"\")); " } 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 { let kind: String = expr["expr"] if kind == "Int" { let v: String = expr["value"] return v } // DurationLit - postfix-literal time value (e.g. 30.seconds, 1.hour). // Lowered to a literal int64 nanosecond count, wrapped in the runtime // entry point so the intent is explicit at the C level. The arithmetic // is fully constant-folded by any optimising C compiler. if kind == "DurationLit" { let count: String = expr["count"] let unit: String = expr["unit"] let mult: String = duration_unit_nanos(unit) return "el_duration_from_nanos((el_val_t)(" + count + "LL * " + mult + "))" } if kind == "Float" { // Wrap Float literals in el_from_float() so the bit pattern is // preserved through the el_val_t (int64) slot. Without this, // implicit double->int64 conversion in C truncates `0.8` to `0` // when passed to a builtin that expects el_val_t. let v: String = expr["value"] return "el_from_float(" + v + ")" } if kind == "Str" { let v: String = expr["value"] return "EL_STR(" + c_str_lit(v) + ")" } if kind == "Bool" { let v: String = expr["value"] if v == "true" { return "1" } return "0" } if kind == "Nil" { return "EL_NULL" } if kind == "Ident" { let name: String = expr["name"] return name } if kind == "Not" { let inner = expr["inner"] let inner_c: String = cg_expr(inner) return "!" + inner_c } if kind == "Neg" { let inner = expr["inner"] let inner_c: String = cg_expr(inner) return "(-" + inner_c + ")" } if kind == "BinOp" { let op: String = expr["op"] let left = expr["left"] let right = expr["right"] let left_c: String = cg_expr(left) let right_c: String = cg_expr(right) let left_kind: String = left["expr"] let right_kind: String = right["expr"] // -- 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). } if op == "Plus" { // If either side is a string literal, always concat if left_kind == "Str" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Str" { return "el_str_concat(" + left_c + ", " + right_c + ")" } // Type-driven dispatch via recursive is_int_expr: any expression // whose value is provably Int (literal, typed Ident, known-Int // builtin, or BinOp arithmetic over Ints) participates in // arithmetic, not string concat. Recursion into BinOp lets // `a + b + c` (chained Int adds) and `acc * 16 + d` route to // arithmetic instead of falling to el_str_concat - both sides // are Int so the outer `+` is too. if is_int_expr(left) { if is_int_expr(right) { let op_c: String = binop_to_c(op) return "(" + left_c + " " + op_c + " " + right_c + ")" } } // Mixed cases: at least one side is provably Int but the other // is not provably anything. Historical heuristic biases to // arithmetic when a literal Int is present (preserves prior // behaviour for `pos + 1` where `pos` is an untyped param). if left_kind == "Int" { let op_c: String = binop_to_c(op) return "(" + left_c + " " + op_c + " " + right_c + ")" } if right_kind == "Int" { let op_c: String = binop_to_c(op) return "(" + left_c + " " + op_c + " " + right_c + ")" } // Otherwise: BinOp(+) with a Call/Ident side without int-typed // evidence - fall back to string concat (the historical default). if left_kind == "Call" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Call" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if left_kind == "BinOp" { let left_op: String = left["op"] if left_op == "Plus" { return "el_str_concat(" + left_c + ", " + right_c + ")" } } if right_kind == "BinOp" { let right_op: String = right["op"] if right_op == "Plus" { return "el_str_concat(" + left_c + ", " + right_c + ")" } } if left_kind == "Ident" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Ident" { return "el_str_concat(" + left_c + ", " + right_c + ")" } } // String equality: use str_eq() when either side is a string literal or ident. // Use plain == when comparing integer literals OR when both sides are // identifiers tracked in __int_names (typed Int via `let x: Int = ...`). // Without the int-name check, `seen == idx` between two Int locals // miscompiles to str_eq(seen, idx), strcmp'ing what are integer values // dressed as char* - segfault on the first non-printable byte. if op == "EqEq" { if left_kind == "Int" { return "(" + left_c + " == " + right_c + ")" } if right_kind == "Int" { return "(" + left_c + " == " + right_c + ")" } if left_kind == "Bool" { return "(" + left_c + " == " + right_c + ")" } if right_kind == "Bool" { return "(" + left_c + " == " + right_c + ")" } if left_kind == "Ident" { if right_kind == "Ident" { let lname: String = left["name"] let rname: String = right["name"] if is_int_name(lname) { if is_int_name(rname) { return "(" + left_c + " == " + right_c + ")" } } } } // Extend int-equality to mixed Ident/BinOp cases: `i == n - 1` // where the left is an int-name Ident and the right is an // arithmetic BinOp (or vice-versa). Without this check the // fallthrough to str_eq produces str_eq(int_value, int_value) // which reads the integer as a char* and segfaults. if is_int_expr(left) { if is_int_expr(right) { return "(" + left_c + " == " + right_c + ")" } } if left_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" } if left_kind == "Ident" { return "str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Ident" { return "str_eq(" + left_c + ", " + right_c + ")" } if left_kind == "Call" { return "str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Call" { return "str_eq(" + left_c + ", " + right_c + ")" } } if op == "NotEq" { if left_kind == "Int" { return "(" + left_c + " != " + right_c + ")" } if right_kind == "Int" { return "(" + left_c + " != " + right_c + ")" } if left_kind == "Bool" { return "(" + left_c + " != " + right_c + ")" } if right_kind == "Bool" { return "(" + left_c + " != " + right_c + ")" } if left_kind == "Ident" { if right_kind == "Ident" { let lname: String = left["name"] let rname: String = right["name"] if is_int_name(lname) { if is_int_name(rname) { return "(" + left_c + " != " + right_c + ")" } } } } // Same mixed Ident/BinOp fix as EqEq: use is_int_expr to detect // integer-typed operands before falling through to !str_eq. if is_int_expr(left) { if is_int_expr(right) { return "(" + left_c + " != " + right_c + ")" } } if left_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" } if left_kind == "Ident" { return "!str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Ident" { return "!str_eq(" + left_c + ", " + right_c + ")" } if left_kind == "Call" { return "!str_eq(" + left_c + ", " + right_c + ")" } if right_kind == "Call" { return "!str_eq(" + left_c + ", " + right_c + ")" } } let op_c: String = binop_to_c(op) return "(" + left_c + " " + op_c + " " + right_c + ")" } if kind == "Call" { let func = expr["func"] let args = expr["args"] let arity: Int = native_list_len(args) let func_kind: String = func["expr"] let args_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, ", ") 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()`, 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 } return "el_list_new(" + native_int_to_str(n) + ", " + str_join(items_parts, ", ") + ")" } 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 } return "el_map_new(" + native_int_to_str(n) + ", " + str_join(items_parts, ", ") + ")" } 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) } "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 { 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 + "; })") str_join(parts, "") } // -- If-as-expression codegen ------------------------------------------------- // // Lower `if cond { thenBody } else { elseBody }` used in expression position // (e.g. `let x = if a { b } else { c }`) to a GCC/Clang statement-expression // so the actual arm bodies are evaluated, not just `(cond ? 1 : 0)`. // // Each arm body is a list of statements; the result of the arm is the value // of its final Expr statement (mirroring transform_implicit_return at function // scope). Statements before the final Expr are emitted as expression-statements // for their side effects. fn next_if_id() -> String { let csv: String = state_get("__if_expr_counter") let n = 0 if !str_eq(csv, "") { let n = str_to_int(csv) } let n = n + 1 state_set("__if_expr_counter", native_int_to_str(n)) native_int_to_str(n) } // Render a single arm of the if-as-expression: emit each statement-before-last // as a side-effecting expression, then assign the final Expr's value to the // result var. If the arm body is empty or its last stmt isn't an Expr, the // result var stays at its initial 0. fn cg_if_expr_arm(stmts: [Map], 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() let i = 0 while i < n { let s = native_list_get(stmts, i) let sk: String = s["stmt"] let is_last: Bool = false if i == n - 1 { let is_last = true } if str_eq(sk, "Let") { let name: String = s["name"] let val = s["value"] let val_c: String = cg_expr(val) let 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) 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 { 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 } str_join(parts, "") } fn cg_if_expr(expr: Map) -> 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, indent: String, declared: [String]) -> [String] { let kind: String = stmt["stmt"] if kind == "Let" { let name: String = stmt["name"] let val = stmt["value"] let val_c: String = cg_expr(val) // If the binding is annotated `: Int` and val is an Int literal, // register `name` in the per-function int-name set so that later // `name + ...` dispatches to arithmetic, not concat. let ltype: String = stmt["type"] if str_eq(ltype, "Int") { add_int_name(name) } // Temporal type annotations register the name with the matching // typed-set so BinOp / comparison codegen routes through the // typed wrappers and forbids cross-type ops. if str_eq(ltype, "Instant") { add_instant_name(name) } if str_eq(ltype, "Duration") { add_duration_name(name) } 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) } let vk: String = val["expr"] if str_eq(vk, "Int") { add_int_name(name) } if list_contains(declared, name) { emit_line(indent + name + " = " + val_c + ";") return declared } else { emit_line(indent + "el_val_t " + name + " = " + val_c + ";") return native_list_append(declared, name) } } if kind == "Return" { let val = stmt["value"] let val_kind: String = val["expr"] if val_kind == "Nil" { emit_line(indent + "return 0;") } else { let val_c: String = cg_expr(val) emit_line(indent + "return " + val_c + ";") } return declared } // Bare reassignment: `name = expr`. Always emits a plain C assignment // (no `el_val_t` prefix) - by construction the parser only produces // Assign for an existing identifier. If the name happens NOT to be in // `declared` for the current C scope (it was let-bound by an enclosing // block) the emit still resolves at C level because the variable lives // in the surrounding scope. if kind == "Assign" { let name: String = stmt["name"] let val = stmt["value"] let val_c: String = cg_expr(val) emit_line(indent + name + " = " + val_c + ";") return declared } if kind == "Expr" { let val = stmt["value"] let val_kind: String = val["expr"] if val_kind == "If" { cg_if_stmt(val, indent, declared) return declared } if val_kind == "For" { cg_for_stmt(val, indent, declared) return declared } let val_c: String = cg_expr(val) emit_line(indent + val_c + ";") return declared } if kind == "While" { let cond = stmt["cond"] let body = stmt["body"] let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "while (" + cond_c + ") {") // Body lives in its own C block - clone so let-bindings inside the // loop don't leak into the parent's `declared` list (which would make // a sibling scope's `let x` emit assignment on an undeclared name). cg_stmts(body, indent + " ", native_list_clone(declared)) emit_line(indent + "}") return declared } if kind == "For" { let item: String = stmt["item"] let list_expr = stmt["list"] let body = stmt["body"] cg_for_body(item, list_expr, body, indent, declared) return declared } if kind == "FnDef" { return declared } if kind == "TypeDef" { return declared } if kind == "EnumDef" { return declared } if kind == "Import" { return declared } if kind == "ExternFn" { return declared } if kind == "CgiBlock" { return declared } if kind == "ServiceBlock" { return declared } // TestDef: skip in normal (non-test) mode. // In test mode the body is emitted by cg_test_fn, not here. if kind == "TestDef" { return declared } // Assert: no-op in normal mode. In test mode, cg_stmt_assert is used // directly when emitting the per-test function body. if kind == "Assert" { 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 } 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, 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, body: [Map], 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, 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], indent: String, declared: [String]) -> [String] { let n: Int = native_list_len(stmts) let i = 0 let decl = declared while i < n { let stmt = native_list_get(stmts, i) let decl = cg_stmt(stmt, indent, decl) let i = i + 1 } decl } // -- Function declaration codegen ----------------------------------------------- fn param_decl(param: Map, idx: Int) -> String { let name: String = param["name"] "el_val_t " + name } fn params_to_c(params: [Map]) -> 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 } str_join(parts, ", ") } // 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]) -> [Map] { let n: Int = native_list_len(body) if n == 0 { return body } let last: Map = 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] = 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 = { "stmt": "Return", "value": val } let new_body = native_list_append(new_body, return_stmt) return new_body } body } // Test whether `name` is currently registered as an Int-typed identifier // for the function being codegened. The set is maintained as a comma- // bounded CSV in process state; cg_fn seeds it from typed parameters, // cg_stmt extends it from typed `let` bindings. fn is_int_name(name: String) -> Bool { let csv: String = state_get("__int_names") if str_eq(csv, "") { return false } return str_contains(csv, "," + name + ",") } // Same shape as is_int_name, for Instant- and Duration-typed bindings. // Used by the BinOp/comparison codegen to dispatch arithmetic through the // typed runtime wrappers (el_instant_add_dur, el_duration_lt, -) and to // surface mismatches (Instant + Instant, Duration + Int) as #error // directives at the top of the generated C. fn is_instant_name(name: String) -> Bool { let csv: String = state_get("__instant_names") if str_eq(csv, "") { return false } return str_contains(csv, "," + name + ",") } fn is_duration_name(name: String) -> Bool { let csv: String = state_get("__duration_names") if str_eq(csv, "") { return false } return str_contains(csv, "," + name + ",") } // Known runtime builtins that return Int. Used to dispatch arithmetic vs // string-concat on `+` when one side is a Call. New builtins must be added // here when they return Int and may participate in arithmetic. fn is_int_call(call_expr: Map) -> 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 } // 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> Bool { let k: String = expr["expr"] if str_eq(k, "Int") { return true } if str_eq(k, "Ident") { let name: String = expr["name"] return is_int_name(name) } if str_eq(k, "Call") { return is_int_call(expr) } if str_eq(k, "Neg") { return is_int_expr(expr["inner"]) } if str_eq(k, "Not") { return true } if str_eq(k, "BinOp") { let op: String = expr["op"] // Comparisons and logicals always yield 0/1 - safe Int. if str_eq(op, "EqEq") { return true } if str_eq(op, "NotEq") { return true } if str_eq(op, "Lt") { return true } if str_eq(op, "Gt") { return true } if str_eq(op, "LtEq") { return true } if str_eq(op, "GtEq") { return true } if str_eq(op, "And") { return true } if str_eq(op, "Or") { return true } // Arithmetic propagates: Int op Int -> Int. if str_eq(op, "Plus") { if is_int_expr(expr["left"]) { if is_int_expr(expr["right"]) { return true } } return false } if str_eq(op, "Minus") { if is_int_expr(expr["left"]) { if is_int_expr(expr["right"]) { return true } } return false } if str_eq(op, "Star") { if is_int_expr(expr["left"]) { if is_int_expr(expr["right"]) { return true } } return false } if str_eq(op, "Slash") { if is_int_expr(expr["left"]) { if is_int_expr(expr["right"]) { return true } } return false } return false } return false } // -- Capability-kind enforcement ---------------------------------------------- // // A program's top-level block (cgi / service / none) determines which // runtime primitives it may call. The compiler records violations in // process state during cg_expr's Call emission; codegen's entry point // then emits #error directives at the top of the generated C so the // downstream cc step fails with a clear message. // // Capability tiers: // "cgi" - full self-formation. All primitives. // "service" - bounded. Cannot call self-formation primitives: // llm_call_agentic, llm_register_tool, dharma_emit, // dharma_field. Single-turn LLM calls are allowed. // "utility" - default. No DHARMA, no LLM. Pure compute + I/O. // // The compiler-level rule is structural: the binary either CAN or CANNOT // emit the call. There is no runtime check, no opt-in, no override. fn cap_record_violation(kind: String, fn_name: String) -> Bool { let csv: String = state_get("__cap_violations") if str_eq(csv, "") { let csv = "," } let entry: String = kind + ":" + fn_name let key: String = "," + entry + "," if str_contains(csv, key) { return true } state_set("__cap_violations", csv + entry + ",") return true } // Self-formation primitives - the cut between CGI and service. A program // that emits these calls IS structurally a CGI; we forbid them everywhere // else. fn is_self_formation_call(fn_name: String) -> Bool { if str_eq(fn_name, "llm_call_agentic") { return true } if str_eq(fn_name, "llm_register_tool") { return true } if str_eq(fn_name, "dharma_emit") { return true } if str_eq(fn_name, "dharma_field") { return true } return false } // Any DHARMA primitive - utilities have zero network presence. fn is_dharma_call(fn_name: String) -> Bool { if str_eq(fn_name, "dharma_connect") { return true } if str_eq(fn_name, "dharma_send") { return true } if str_eq(fn_name, "dharma_activate") { return true } if str_eq(fn_name, "dharma_emit") { return true } if str_eq(fn_name, "dharma_field") { return true } if str_eq(fn_name, "dharma_strengthen") { return true } if str_eq(fn_name, "dharma_relationship") { return true } if str_eq(fn_name, "dharma_peers") { return true } return false } // Any LLM primitive - utilities have no LLM access at all. fn is_llm_call(fn_name: String) -> Bool { if str_eq(fn_name, "llm_call") { return true } if str_eq(fn_name, "llm_call_system") { return true } if str_eq(fn_name, "llm_call_agentic") { return true } if str_eq(fn_name, "llm_vision") { return true } if str_eq(fn_name, "llm_register_tool") { return true } if str_eq(fn_name, "llm_models") { return true } return false } fn cap_check_call(fn_name: String) -> Bool { let kind: String = state_get("__program_kind") if str_eq(kind, "cgi") { return true } if str_eq(kind, "service") { if is_self_formation_call(fn_name) { cap_record_violation("service", fn_name) return false } return true } // utility (default) if is_dharma_call(fn_name) { cap_record_violation("utility", fn_name) return false } if is_llm_call(fn_name) { cap_record_violation("utility", fn_name) return false } return true } // Emit collected capability violations as #error directives. Called // from codegen()'s entry point right after the cgi/service-block scan, // so they appear at the very top of the generated C. fn emit_cap_violations() -> Void { let csv: String = state_get("__cap_violations") if str_eq(csv, "") { return } if str_eq(csv, ",") { return } let n: Int = str_len(csv) let i: Int = 1 while i < n { let next_comma: Int = str_index_of(str_slice(csv, i, n), ",") if next_comma < 0 { return } let entry: String = str_slice(csv, i, i + next_comma) let colon: Int = str_index_of(entry, ":") if colon > 0 { let kind: String = str_slice(entry, 0, colon) let fn_name: String = str_slice(entry, colon + 1, str_len(entry)) emit_line("#error \"capability violation: '" + kind + "' programs may not call '" + fn_name + "' (self-formation primitive - only 'cgi' programs may use it)\"") } let i = i + next_comma + 1 } } // Surface temporal-type violations as #error directives. The cg_expr BinOp // dispatcher records each violation (Instant + Instant, Duration + Int, -) // as a CSV entry "kind:detail" via time_record_violation. Each entry maps // to a single #error so downstream cc fails the build with a clear El- // source-level message before the bogus C even links. fn emit_time_violations() -> Void { let csv: String = state_get("__time_violations") if str_eq(csv, "") { return } if str_eq(csv, ",") { return } let n: Int = str_len(csv) let i: Int = 1 while i < n { let next_comma: Int = str_index_of(str_slice(csv, i, n), ",") if next_comma < 0 { return } let entry: String = str_slice(csv, i, i + next_comma) let colon: Int = str_index_of(entry, ":") if colon > 0 { let detail: String = str_slice(entry, colon + 1, str_len(entry)) emit_line("#error \"temporal type error: " + detail + "\"") } let i = i + next_comma + 1 } } // -- Builtin arity table ------------------------------------------------------- // // El programs sometimes call runtime builtins with the wrong number of // arguments (e.g. `http_serve(port)` instead of `http_serve(port, handler)`). // Without this check the generated C compiles to a call with too few / // too many args and fails downstream cc with a generic "too few arguments" // message that doesn't point to the El source line. // // Strategy: a small static table mirrors el_runtime.h. Variadic builtins // (el_list_new, el_map_new, args) and unknown identifiers (user fns, // dynamic dispatch) return -1 -> no check. A mismatch records a violation // in process state, which emit_arity_violations() turns into #error // directives at the top of the generated C. fn builtin_arity(name: String) -> Int { // I/O if str_eq(name, "println") { return 1 } if str_eq(name, "print") { return 1 } if str_eq(name, "readline") { return 0 } // String if str_eq(name, "el_str_concat") { return 2 } if str_eq(name, "str_eq") { return 2 } if str_eq(name, "str_starts_with") { return 2 } if str_eq(name, "str_ends_with") { return 2 } if str_eq(name, "str_len") { return 1 } if str_eq(name, "str_concat") { return 2 } if str_eq(name, "int_to_str") { return 1 } if str_eq(name, "str_to_int") { return 1 } if str_eq(name, "str_slice") { return 3 } if str_eq(name, "str_contains") { return 2 } if str_eq(name, "str_replace") { return 3 } if str_eq(name, "str_to_upper") { return 1 } if str_eq(name, "str_to_lower") { return 1 } if str_eq(name, "str_trim") { return 1 } if str_eq(name, "str_index_of") { return 2 } if str_eq(name, "str_split") { return 2 } if str_eq(name, "str_char_at") { return 2 } if str_eq(name, "str_char_code") { return 2 } if str_eq(name, "str_pad_left") { return 3 } if str_eq(name, "str_pad_right") { return 3 } if str_eq(name, "str_format") { return 2 } if str_eq(name, "str_lower") { return 1 } if str_eq(name, "str_upper") { return 1 } // 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 } // Filesystem if str_eq(name, "fs_read") { return 1 } if str_eq(name, "fs_write") { return 2 } if str_eq(name, "fs_list") { return 1 } // JSON if str_eq(name, "json_get") { return 2 } if str_eq(name, "json_parse") { return 1 } if str_eq(name, "json_stringify") { return 1 } if str_eq(name, "json_get_string") { return 2 } if str_eq(name, "json_get_int") { return 2 } if str_eq(name, "json_get_float") { return 2 } if str_eq(name, "json_get_bool") { return 2 } if str_eq(name, "json_get_raw") { return 2 } if str_eq(name, "json_set") { return 3 } if str_eq(name, "json_array_len") { return 1 } // Time if str_eq(name, "time_now") { return 0 } if str_eq(name, "time_now_utc") { return 0 } if str_eq(name, "sleep_secs") { return 1 } if str_eq(name, "sleep_ms") { return 1 } if str_eq(name, "time_format") { return 2 } if str_eq(name, "time_to_parts") { return 1 } if str_eq(name, "time_from_parts") { return 3 } if str_eq(name, "time_add") { return 3 } if str_eq(name, "time_diff") { return 3 } // UUID if str_eq(name, "uuid_new") { return 0 } if str_eq(name, "uuid_v4") { return 0 } // Env / state if str_eq(name, "env") { return 1 } if str_eq(name, "state_set") { return 2 } if str_eq(name, "state_get") { return 1 } if str_eq(name, "state_del") { return 1 } if str_eq(name, "state_keys") { return 0 } // Float if str_eq(name, "float_to_str") { return 1 } if str_eq(name, "int_to_float") { return 1 } if str_eq(name, "float_to_int") { return 1 } if str_eq(name, "format_float") { return 2 } if str_eq(name, "decimal_round") { return 2 } if str_eq(name, "str_to_float") { return 1 } // Math (Float) if str_eq(name, "math_sqrt") { return 1 } if str_eq(name, "math_log") { return 1 } if str_eq(name, "math_ln") { return 1 } if str_eq(name, "math_sin") { return 1 } if str_eq(name, "math_cos") { return 1 } if str_eq(name, "math_pi") { return 0 } // Bool if str_eq(name, "bool_to_str") { return 1 } // Process if str_eq(name, "exit_program") { return 1 } // 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_get_node_json") { return 1 } if str_eq(name, "engram_search_json") { return 2 } if str_eq(name, "engram_scan_nodes_json") { return 2 } if str_eq(name, "engram_neighbors_json") { return 3 } if str_eq(name, "engram_activate_json") { return 2 } if str_eq(name, "engram_stats_json") { return 0 } // LLM if str_eq(name, "llm_call") { return 2 } if str_eq(name, "llm_call_system") { return 3 } if str_eq(name, "llm_call_agentic") { return 4 } if str_eq(name, "llm_vision") { return 4 } if str_eq(name, "llm_models") { return 0 } if str_eq(name, "llm_register_tool") { return 2 } // Crypto if str_eq(name, "sha256_hex") { return 1 } if str_eq(name, "sha256_bytes") { return 1 } if str_eq(name, "hmac_sha256_hex") { return 2 } if str_eq(name, "hmac_sha256_bytes") { return 2 } if str_eq(name, "base64_encode") { return 1 } if str_eq(name, "base64_decode") { return 1 } if str_eq(name, "base64url_encode") { return 1 } if str_eq(name, "base64url_decode") { return 1 } // Native VM aliases if str_eq(name, "native_list_get") { return 2 } if str_eq(name, "native_list_len") { return 1 } if str_eq(name, "native_list_append") { return 2 } if str_eq(name, "native_list_empty") { return 0 } if str_eq(name, "native_list_clone") { return 1 } if str_eq(name, "native_string_chars") { return 1 } if str_eq(name, "native_int_to_str") { return 1 } // Method-call aliases if str_eq(name, "append") { return 2 } if str_eq(name, "len") { return 1 } if str_eq(name, "get") { return 2 } if str_eq(name, "map_get") { return 2 } if str_eq(name, "map_set") { return 3 } // -1 sentinel: variadic / unknown / user-defined -> no check. return -1 } fn arity_record_violation(fn_name: String, expected: Int, actual: Int) -> Bool { let csv: String = state_get("__arity_violations") if str_eq(csv, "") { let csv = "," } // Encode as fn_name|expected|actual to recover all three at emit time. let entry: String = fn_name + "|" + native_int_to_str(expected) + "|" + native_int_to_str(actual) let key: String = "," + entry + "," if str_contains(csv, key) { return true } state_set("__arity_violations", csv + entry + ",") return true } // Validate the call's arity against the builtin table. Returns true (always) // because cg_expr ignores the result; -1 from builtin_arity signals // "no check possible" (variadic or user-defined). A mismatch is recorded // and surfaced as an #error at the bottom of the generated C, so cc fails // before it ever attempts to type-check the wrong call. fn arity_check_call(fn_name: String, actual: Int) -> Bool { let expected: Int = builtin_arity(fn_name) if expected < 0 { return true } if expected == actual { return true } arity_record_violation(fn_name, expected, actual) return true } // Emit recorded arity violations as #error directives. fn emit_arity_violations() -> Void { let csv: String = state_get("__arity_violations") if str_eq(csv, "") { return } if str_eq(csv, ",") { return } let n: Int = str_len(csv) let i: Int = 1 while i < n { let next_comma: Int = str_index_of(str_slice(csv, i, n), ",") if next_comma < 0 { return } let entry: String = str_slice(csv, i, i + next_comma) let p1: Int = str_index_of(entry, "|") if p1 > 0 { let fn_name: String = str_slice(entry, 0, p1) let rest: String = str_slice(entry, p1 + 1, str_len(entry)) let p2: Int = str_index_of(rest, "|") if p2 > 0 { let exp_s: String = str_slice(rest, 0, p2) let act_s: String = str_slice(rest, p2 + 1, str_len(rest)) emit_line("#error \"arity error: '" + fn_name + "' takes " + exp_s + " arguments, but called with " + act_s + "\"") } } let i = i + next_comma + 1 } } fn add_int_name(name: String) -> Bool { let csv: String = state_get("__int_names") if str_eq(csv, "") { csv = "," } let key: String = "," + name + "," if str_contains(csv, key) { return true } state_set("__int_names", csv + name + ",") return true } fn add_instant_name(name: String) -> Bool { let csv: String = state_get("__instant_names") if str_eq(csv, "") { csv = "," } let key: String = "," + name + "," if str_contains(csv, key) { return true } state_set("__instant_names", csv + name + ",") return true } fn add_duration_name(name: String) -> Bool { let csv: String = state_get("__duration_names") if str_eq(csv, "") { csv = "," } let key: String = "," + name + "," if str_contains(csv, key) { return true } state_set("__duration_names", csv + name + ",") return true } fn 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]) -> Bool { state_set("__int_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, "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 cg_fn(stmt: Map) -> Void { let fn_name: String = stmt["name"] // Skip El's `fn main()` - C provides its own main() for top-level stmts // and a duplicate `el_val_t main(void)` would collide with it. if fn_name == "main" { return } let params = stmt["params"] let body = stmt["body"] let ret_type: String = stmt["ret_type"] let params_c: String = params_to_c(params) // VBD role enforcement: dharma_emit / dharma_field may only be called // from @manager-decorated functions. Surface violations to the C compiler // via #error directives emitted before the function definition. let decorator: String = stmt["decorator"] if vbd_has_restricted_call(body) { if !str_eq(decorator, "manager") { emit_line("#error \"VBD violation: dharma_emit/dharma_field called from non-@manager fn '" + fn_name + "'\"") } } // Seed the per-function int-name set so the `+` codegen can dispatch // arithmetic vs concat on type-annotated identifiers. build_int_names_for_params(params) emit_line("el_val_t " + fn_name + "(" + params_c + ") {") // Seed declared with parameter names so reassignment works let decl = native_list_empty() let np: Int = native_list_len(params) let pi = 0 while pi < np { let param = native_list_get(params, pi) let pname: String = param["name"] let decl = native_list_append(decl, pname) let pi = pi + 1 } // Lift the final bare expression into an explicit return so implicit // returns ("fn lex(s) { ... tokens }") actually return their value. // Void-returning functions skip this - wrapping `println(x)` in // `return -` is a C type error. let body_xformed = body if !str_eq(ret_type, "Void") { let body_xformed = transform_implicit_return(body) } cg_stmts(body_xformed, " ", decl) emit_line(" return 0;") emit_line("}") emit_blank() } // -- Top-level codegen --------------------------------------------------------- fn is_fndef(stmt: Map) -> Bool { let kind: String = stmt["stmt"] if kind == "FnDef" { return true } false } fn is_top_level_decl(stmt: Map) -> 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 } if kind == "TestDef" { 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) -> 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]) -> 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 } // -- Test mode codegen ---------------------------------------------------------- // // reporter = "text" → human-readable output to stderr // reporter = "json" → newline-delimited JSON to stdout // // Each test function signature: static int el_test_N(void) // Returns 0 = pass, non-zero = fail. // // Text assert: if (!expr) { fprintf(stderr, " FAIL \n"); return 1; } // JSON assert: stores the assert_line for the runner's suite_end emission. // Emits {"type":"test_fail",...} to stdout then returns 1. fn cg_stmt_assert_text(stmt: Map, test_name: String) -> Void { let expr_node = stmt["expr"] let msg: String = stmt["msg"] let expr_c: String = cg_expr(expr_node) let expr_c = strip_outer_parens(expr_c) let disp_msg = "assert failed" if !str_eq(msg, "") { let disp_msg = msg } emit_line(" if (!(" + expr_c + ")) {") emit_line(" fprintf(stderr, \" FAIL " + c_escape(test_name) + " \\xe2\\x80\\x94 " + c_escape(disp_msg) + "\\n\");") emit_line(" return 1;") emit_line(" }") } fn cg_stmt_assert_json(stmt: Map, test_name: String, file_name: String, test_line: Int) -> Void { let expr_node = stmt["expr"] let msg: String = stmt["msg"] let assert_line: Int = stmt["line"] let expr_c: String = cg_expr(expr_node) let expr_c = strip_outer_parens(expr_c) let disp_msg = "assert failed" if !str_eq(msg, "") { let disp_msg = msg } // Embed all compile-time-known strings as C string literals (no %s runtime formatting). let json_line: String = "{\"type\":\"test_fail\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + ",\"assert_line\":" + native_int_to_str(assert_line) + ",\"message\":\"" + c_escape(disp_msg) + "\"}" emit_line(" if (!(" + expr_c + ")) {") emit_line(" puts(" + c_str_lit(json_line) + ");") emit_line(" return 1;") emit_line(" }") } // cg_stmts_in_test: emit test body statements, routing Assert to the // appropriate handler based on reporter mode. fn cg_stmts_in_test(stmts: [Map], indent: String, declared: [String], test_name: String, reporter: String, file_name: String, test_line: Int) -> [String] { let n: Int = native_list_len(stmts) let i = 0 let decl = declared while i < n { let stmt = native_list_get(stmts, i) let sk: String = stmt["stmt"] if str_eq(sk, "Assert") { if str_eq(reporter, "json") { cg_stmt_assert_json(stmt, test_name, file_name, test_line) } else { cg_stmt_assert_text(stmt, test_name) } } else { let decl = cg_stmt(stmt, indent, decl) } let i = i + 1 } decl } // cg_test_fn: emit a single test function. fn cg_test_fn(test_def: Map, idx: Int, reporter: String, file_name: String) -> String { let fn_name: String = "el_test_" + native_int_to_str(idx) let test_name: String = test_def["name"] let test_line: Int = test_def["line"] let body = test_def["body"] emit_line("static int " + fn_name + "(void) {") cg_stmts_in_test(body, " ", native_list_empty(), test_name, reporter, file_name, test_line) emit_line(" return 0;") emit_line("}") emit_blank() fn_name } // codegen_test: emit a complete test binary. // reporter: "text" (stderr human-readable) or "json" (stdout ndjson) // file_name: basename of the source file (used in JSON output) fn codegen_test(stmts: [Map], reporter: String, file_name: String) -> Void { // Collect all TestDef nodes in order. let n: Int = native_list_len(stmts) let test_defs: [Map] = native_list_empty() let i = 0 while i < n { let stmt = native_list_get(stmts, i) let sk: String = stmt["stmt"] if str_eq(sk, "TestDef") { let test_defs = native_list_append(test_defs, stmt) } let i = i + 1 } let n_tests: Int = native_list_len(test_defs) // Emit forward declarations for test functions. let ti = 0 while ti < n_tests { let fn_name: String = "el_test_" + native_int_to_str(ti) emit_line("static int " + fn_name + "(void);") let ti = ti + 1 } emit_blank() // Emit all non-test, non-main function definitions. 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") { cg_fn(stmt) } } let i = i + 1 } // Emit each test function. let ti = 0 while ti < n_tests { let test_def = native_list_get(test_defs, ti) cg_test_fn(test_def, ti, reporter, file_name) let ti = ti + 1 } // Emit the test runner main(). let test_word = "tests" if n_tests == 1 { let test_word = "test" } emit_line("int main(void) {") emit_line(" int pass = 0; int fail = 0;") if str_eq(reporter, "json") { // JSON reporter: all strings are compile-time constants; use puts. // Only suite_end needs runtime pass/fail counts (printf). let suite_start_json: String = "{\"type\":\"suite_start\",\"file\":\"" + c_escape(file_name) + "\",\"total\":" + native_int_to_str(n_tests) + "}" emit_line(" puts(" + c_str_lit(suite_start_json) + ");") let ti = 0 while ti < n_tests { let test_def = native_list_get(test_defs, ti) let test_name: String = test_def["name"] let test_line: Int = test_def["line"] let fn_name: String = "el_test_" + native_int_to_str(ti) let start_json: String = "{\"type\":\"test_start\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + "}" let pass_json: String = "{\"type\":\"test_pass\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + ",\"duration_ms\":0}" emit_line(" puts(" + c_str_lit(start_json) + ");") emit_line(" if (" + fn_name + "() == 0) {") emit_line(" pass++;") emit_line(" puts(" + c_str_lit(pass_json) + ");") emit_line(" } else { fail++; }") let ti = ti + 1 } // suite_end needs runtime pass/fail counts emit_line(" printf(\"{\\\"type\\\":\\\"suite_end\\\",\\\"passed\\\":%d,\\\"failed\\\":%d}\\n\", pass, fail);") } else { // Text reporter: human-readable to stderr emit_line(" fprintf(stderr, \"==> running " + native_int_to_str(n_tests) + " " + test_word + "\\n\\n\");") let ti = 0 while ti < n_tests { let test_def = native_list_get(test_defs, ti) let test_name: String = test_def["name"] let fn_name: String = "el_test_" + native_int_to_str(ti) emit_line(" fprintf(stderr, \" RUN " + c_escape(test_name) + "\\n\");") emit_line(" if (" + fn_name + "() == 0) { pass++; fprintf(stderr, \" PASS " + c_escape(test_name) + "\\n\"); }") emit_line(" else { fail++; }") let ti = ti + 1 } emit_line(" fprintf(stderr, \"\\n%d passed, %d failed\\n\", pass, fail);") } emit_line(" return fail > 0 ? 1 : 0;") emit_line("}") emit_blank() } // -- Entry point ---------------------------------------------------------------- fn codegen(stmts: [Map], source: String) -> String { codegen_inner(stmts, source, false, "text", "") } // codegen_with_tests: emit a test binary. // reporter: "text" or "json" // file_name: basename of the source file (used in JSON output) fn codegen_with_tests(stmts: [Map], source: String, reporter: String, file_name: String) -> String { codegen_inner(stmts, source, true, reporter, file_name) } fn codegen_inner(stmts: [Map], source: String, test_mode: Bool, reporter: String, file_name: 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 = { "stmt": "None" } let svc_count = 0 let svc_block: Map = { "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", "") // In test mode, delegate to the test-specific path which emits test // functions and a test runner main() instead of the normal program. // Test mode still needs the standard preamble (#includes, forward // decls) so we emit that before branching. // // Preamble emit_line("#include ") emit_line("#include ") emit_line("#include ") emit_line("#include \"el_runtime.h\"") // Cross-module forward declarations: for each imported module, emit // #include "module.elh" so Clang sees the function signatures from // that module without needing the full source inlined. The .elh files // are generated by `elc --emit-header` and live in the same dist/ // directory as the generated .c files. We use basename only (strip // the directory prefix and .el extension) so the include resolves // correctly regardless of the source tree layout. let imp_n: Int = native_list_len(stmts) let imp_i = 0 while imp_i < imp_n { let imp_stmt = native_list_get(stmts, imp_i) let imp_kind: String = imp_stmt["stmt"] if str_eq(imp_kind, "Import") { let imp_path: String = imp_stmt["path"] // Extract basename: find last '/' and strip from there. let imp_path_len: Int = str_len(imp_path) let imp_last_slash: Int = -1 let imp_j: Int = 0 while imp_j < imp_path_len { let imp_c: String = str_slice(imp_path, imp_j, imp_j + 1) if str_eq(imp_c, "/") { let imp_last_slash = imp_j } let imp_j = imp_j + 1 } let imp_base: String = str_slice(imp_path, imp_last_slash + 1, imp_path_len) // Strip .el extension if present. let imp_base_len: Int = str_len(imp_base) let imp_bname: String = imp_base if str_ends_with(imp_base, ".el") { let imp_bname = str_slice(imp_base, 0, imp_base_len - 3) } emit_line("#include \"" + imp_bname + ".elh\"") } let imp_i = imp_i + 1 } emit_blank() // Forward declarations (skip `main` - C provides its own) 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() } // Test mode: emit test functions and test runner instead of normal program. if test_mode { codegen_test(stmts, reporter, file_name) emit_cap_violations() emit_arity_violations() emit_time_violations() return "" } // 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 "" } // codegen-js.el — El compiler JavaScript source code generator // // Input: list of AST statement maps (from parser.el) // Output: JavaScript source printed to stdout (streamed, one line at a time) // // Each El program compiles to a single .js file that imports el_runtime.js // (which side-effects globals so call sites stay flat — println(x), not // el.println(x)). Functions map to JS function declarations; top-level // statements run at module load. // // Entry point: fn codegen_js(stmts: [Map], source: String) -> String // Returns "" — output goes to stdout via println(). // // This file mirrors codegen.el (the C backend). Where the C backend has to // fight the int64_t-everywhere convention to dispatch arithmetic vs concat // or `==` vs `str_eq`, the JS backend can usually let JS's own operator // semantics do the right thing. We retain the dispatch logic for clarity // and so that explicit calls to `el_str_concat` or `str_eq` still work. // ── String helpers ──────────────────────────────────────────────────────────── // Escape a JS string literal (double-quotes, backslashes, newlines, etc.). fn js_escape(s: String) -> String { let chars: [String] = native_string_chars(s) let total: Int = native_list_len(chars) let parts: [String] = native_list_empty() let i = 0 while i < total { let ch: String = native_list_get(chars, i) if ch == "\"" { let parts = native_list_append(parts, "\\\"") } else { if ch == "\\" { let parts = native_list_append(parts, "\\\\") } else { if ch == "\n" { let parts = native_list_append(parts, "\\n") } else { if ch == "\r" { let parts = native_list_append(parts, "\\r") } else { if ch == "\t" { let parts = native_list_append(parts, "\\t") } else { let parts = native_list_append(parts, ch) } } } } } let i = i + 1 } str_join(parts, "") } fn js_str_lit(s: String) -> String { "\"" + js_escape(s) + "\"" } // ── Code emission ───────────────────────────────────────────────────────────── fn js_emit_line(line: String) -> Void { println(line) } fn js_emit_blank() -> Void { println("") } // ── Operator helpers ────────────────────────────────────────────────────────── fn js_binop(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 } // ── Known El runtime method names ───────────────────────────────────────────── // // These are the method shortforms exported by el_runtime.js and used by the // El C-backend convention of `obj.method(args)` -> `method(obj, args)`. // Any method name NOT in this set is treated as a native JS method call on the // receiver object, emitting `obj.method(args)` directly. // // This is the mechanism that makes `client.auth.signInWithOtp(payload)` work // without `native_js_call`: the receiver is Any-typed, the method is unknown // to El, so codegen emits the JS call directly. fn js_is_el_method(name: String) -> Bool { if str_eq(name, "append") { return true } if str_eq(name, "len") { return true } if str_eq(name, "get") { return true } if str_eq(name, "map_get") { return true } if str_eq(name, "map_set") { return true } false } // ── Async function tracking ─────────────────────────────────────────────────── // // Functions decorated with @async are recorded here. Any call to a known-async // builtin (http_get, http_post, http_post_json) or to a user-declared @async // function gets an `await` prefix in generated JS. // // Known-async builtins — these return Promise in el_runtime.js. fn js_is_async_builtin(name: String) -> Bool { if str_eq(name, "http_get") { return true } if str_eq(name, "http_post") { return true } if str_eq(name, "http_post_json") { return true } if str_eq(name, "http_get_with_headers") { return true } if str_eq(name, "http_post_with_headers") { return true } false } fn js_register_async_fn(name: String) -> Bool { let csv: String = state_get("__js_async_fns") if str_eq(csv, "") { csv = "," } let key: String = "," + name + "," if str_contains(csv, key) { return true } state_set("__js_async_fns", csv + name + ",") return true } fn js_is_async_fn(name: String) -> Bool { let csv: String = state_get("__js_async_fns") if str_eq(csv, "") { return false } return str_contains(csv, "," + name + ",") } // ── Int-name tracking (mirrors codegen.el) ──────────────────────────────────── fn js_is_int_name(name: String) -> Bool { let csv: String = state_get("__js_int_names") if str_eq(csv, "") { return false } return str_contains(csv, "," + name + ",") } fn js_add_int_name(name: String) -> Bool { let csv: String = state_get("__js_int_names") if str_eq(csv, "") { csv = "," } let key: String = "," + name + "," if str_contains(csv, key) { return true } state_set("__js_int_names", csv + name + ",") return true } fn js_build_int_names_for_params(params: [Map]) -> Bool { state_set("__js_int_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") { js_add_int_name(pname) } let pi = pi + 1 } return true } fn js_is_int_call(call_expr: Map) -> Bool { let func = call_expr["func"] let fk: String = func["expr"] if !str_eq(fk, "Ident") { return false } let name: String = func["name"] if str_eq(name, "str_len") { return true } if str_eq(name, "str_index_of") { return true } if str_eq(name, "str_to_int") { return true } if str_eq(name, "str_char_code") { return true } if str_eq(name, "native_list_len") { return true } if str_eq(name, "el_list_len") { return true } if str_eq(name, "len") { return true } if str_eq(name, "json_get_int") { return true } if str_eq(name, "time_now") { return true } if str_eq(name, "time_now_utc") { 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 } return false } // ── HTML template codegen (JS) ──────────────────────────────────────────────── // // HTML template expressions compile to a JS IIFE that builds the HTML string // using string concatenation. Interpolated values go through html_escape(); // raw() bypasses escaping. {#each} blocks compile to Array.forEach or a // for-loop that pushes fragments into a parts array. // // Entry point: js_cg_html_template(expr) → JS expression string. fn js_next_html_id() -> String { let csv: String = state_get("__js_html_counter") let n = 0 if !str_eq(csv, "") { let n = str_to_int(csv) } let n = n + 1 state_set("__js_html_counter", native_int_to_str(n)) native_int_to_str(n) } fn js_cg_html_parts(children: [Map], acc_var: String) -> String { let n: Int = native_list_len(children) let i = 0 let out = "" while i < n { let child: Map = native_list_get(children, i) let html_kind: String = child["html"] if str_eq(html_kind, "Text") { let text: String = child["text"] let out = out + acc_var + " += " + js_str_lit(text) + "; " } if str_eq(html_kind, "Doctype") { let out = out + acc_var + " += \"\"; " } if str_eq(html_kind, "Interp") { let val_node = child["value"] let val_c: String = js_cg_expr(val_node) let out = out + acc_var + " += html_escape(" + val_c + "); " } if str_eq(html_kind, "Raw") { let val_node = child["value"] let val_c: String = js_cg_expr(val_node) let out = out + acc_var + " += html_raw(" + val_c + "); " } if str_eq(html_kind, "Element") { let elem_c: String = js_cg_html_element_str(child, acc_var) let out = out + elem_c } if str_eq(html_kind, "Each") { let each_c: String = js_cg_html_each(child, acc_var) let out = out + each_c } let i = i + 1 } out } fn js_cg_html_attrs_str(attrs: [Map], acc_var: String) -> String { let n: Int = native_list_len(attrs) let i = 0 let out = "" while i < n { let attr: Map = native_list_get(attrs, i) let attr_name: String = attr["name"] let kind: String = attr["kind"] // open-attr snippet: " name=\"" let open_val: String = " " + attr_name + "=\"" if str_eq(kind, "static") { let sv: String = attr["value"] let out = out + acc_var + " += " + js_str_lit(open_val) + "; " let out = out + acc_var + " += " + js_str_lit(sv) + "; " let out = out + acc_var + " += " + js_str_lit("\"") + "; " } else { if str_eq(kind, "dynamic") { let val_node = attr["value"] let val_c: String = js_cg_expr(val_node) let out = out + acc_var + " += " + js_str_lit(open_val) + "; " let out = out + acc_var + " += html_escape(" + val_c + "); " let out = out + acc_var + " += " + js_str_lit("\"") + "; " } else { // Boolean attribute let out = out + acc_var + " += " + js_str_lit(" " + attr_name) + "; " } } let i = i + 1 } out } fn js_cg_html_element_str(elem: Map, acc_var: String) -> String { let tag: String = elem["tag"] let attrs: [Map] = elem["attrs"] let children: [Map] = elem["children"] let self_closing: Bool = elem["self_closing"] let out = acc_var + " += " + js_str_lit("<" + tag) + "; " let out = out + js_cg_html_attrs_str(attrs, acc_var) if self_closing { let out = out + acc_var + " += \"/>\"" + "; " } else { let out = out + acc_var + " += \">\"; " let out = out + js_cg_html_parts(children, acc_var) let out = out + acc_var + " += " + js_str_lit("") + "; " } out } fn js_cg_html_each(node: Map, acc_var: String) -> String { let list_expr = node["list"] let item_name: String = node["item"] let body_children: [Map] = node["body"] let id: String = js_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 = js_cg_expr(list_expr) let inner_c: String = js_cg_html_parts(body_children, acc_var) "{ const " + list_var + " = " + list_c + "; const " + len_var + " = el_list_len(" + list_var + "); for (let " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { const " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } " } fn js_cg_html_template(expr: Map) -> String { let root = expr["root"] let id: String = js_next_html_id() let acc: String = "_html_" + id let doctype_flag: Bool = root["doctype"] let doctype_prefix: String = "" if doctype_flag { let doctype_prefix = acc + " += \"\"; " } let body: String = js_cg_html_element_str(root, acc) "(() => { let " + acc + " = \"\"; " + doctype_prefix + body + "return " + acc + "; })()" } // ── Expression codegen ──────────────────────────────────────────────────────── // // js_cg_expr returns a JS expression string (not a statement). // // Note: the C backend's `+` dispatch is preserved here for two reasons: // 1) Generated output stays grep-equivalent across targets // 2) Explicit `el_str_concat()` lives in the runtime; codegen routes // through it for ambiguous (Ident+Ident, Call+Call) cases. JS's // own `+` would also work, but el_str_concat coerces both sides // to strings — closer to the C semantics. fn js_cg_expr(expr: Map) -> 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). // The JS backend lowers to a literal integer nanosecond count. The C // backend uses the typed wrapper el_duration_from_nanos to make intent // explicit at the runtime boundary; JS has no equivalent shim yet, so // we lower directly. A future Phase 2 JS time runtime can route through // a wrapper once added. if kind == "DurationLit" { let count: String = expr["count"] let unit: String = expr["unit"] let mult_ns = "1" if str_eq(unit, "nano") { let mult_ns = "1" } if str_eq(unit, "nanos") { let mult_ns = "1" } if str_eq(unit, "milli") { let mult_ns = "1000000" } if str_eq(unit, "millis") { let mult_ns = "1000000" } if str_eq(unit, "millisecond") { let mult_ns = "1000000" } if str_eq(unit, "milliseconds") { let mult_ns = "1000000" } if str_eq(unit, "second") { let mult_ns = "1000000000" } if str_eq(unit, "seconds") { let mult_ns = "1000000000" } if str_eq(unit, "minute") { let mult_ns = "60000000000" } if str_eq(unit, "minutes") { let mult_ns = "60000000000" } if str_eq(unit, "hour") { let mult_ns = "3600000000000" } if str_eq(unit, "hours") { let mult_ns = "3600000000000" } if str_eq(unit, "day") { let mult_ns = "86400000000000" } if str_eq(unit, "days") { let mult_ns = "86400000000000" } return "(" + count + " * " + mult_ns + ")" } if kind == "Float" { // JS numbers are already doubles — no bit-cast trick needed. let v: String = expr["value"] return v } if kind == "Str" { let v: String = expr["value"] return js_str_lit(v) } if kind == "Bool" { let v: String = expr["value"] if v == "true" { return "true" } return "false" } if kind == "Nil" { return "null" } if kind == "Ident" { let name: String = expr["name"] return name } if kind == "Not" { let inner = expr["inner"] let inner_c: String = js_cg_expr(inner) return "!" + inner_c } if kind == "Neg" { let inner = expr["inner"] let inner_c: String = js_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 = js_cg_expr(left) let right_c: String = js_cg_expr(right) let left_kind: String = left["expr"] let right_kind: String = right["expr"] // Plus dispatch — same shape as C backend, but we route through // el_str_concat for the string-concat path (its JS impl coerces // and matches C's behavior). Arithmetic uses bare JS `+`. if op == "Plus" { if left_kind == "Str" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Str" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if left_kind == "Int" { return "(" + left_c + " + " + right_c + ")" } if right_kind == "Int" { return "(" + left_c + " + " + right_c + ")" } if left_kind == "Ident" { if right_kind == "Ident" { let lname: String = left["name"] let rname: String = right["name"] if js_is_int_name(lname) { if js_is_int_name(rname) { return "(" + left_c + " + " + right_c + ")" } } } } if left_kind == "Ident" { if right_kind == "Call" { let lname: String = left["name"] if js_is_int_name(lname) { if js_is_int_call(right) { return "(" + left_c + " + " + right_c + ")" } } } } if right_kind == "Ident" { if left_kind == "Call" { let rname: String = right["name"] if js_is_int_name(rname) { if js_is_int_call(left) { return "(" + left_c + " + " + right_c + ")" } } } } if left_kind == "Call" { if right_kind == "Call" { if js_is_int_call(left) { if js_is_int_call(right) { return "(" + left_c + " + " + right_c + ")" } } } return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Call" { return "el_str_concat(" + left_c + ", " + right_c + ")" } // Fallback: when in doubt, route through el_str_concat. JS's // own + handles strings and numbers natively, but el_str_concat // gives us a single point of control if behavior needs to diverge. if left_kind == "Ident" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Ident" { return "el_str_concat(" + left_c + ", " + right_c + ")" } } // Equality dispatch — C backend disambiguates via str_eq for // strings and == for ints. JS does both with === if we know // the types are uniform; for ambiguous identifier pairs we // route through str_eq for safety (it falls back to === in JS). 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 == "Nil" { return "(" + left_c + " === " + right_c + ")" } if right_kind == "Nil" { return "(" + left_c + " === " + right_c + ")" } if left_kind == "Ident" { if right_kind == "Ident" { let lname: String = left["name"] let rname: String = right["name"] if js_is_int_name(lname) { if js_is_int_name(rname) { 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 + ")" } // Default: === (works for strings, numbers, bools in JS) return "(" + 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 == "Nil" { return "(" + left_c + " !== " + right_c + ")" } if right_kind == "Nil" { return "(" + left_c + " !== " + right_c + ")" } if left_kind == "Ident" { if right_kind == "Ident" { let lname: String = left["name"] let rname: String = right["name"] if js_is_int_name(lname) { if js_is_int_name(rname) { 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 + ")" } return "(" + left_c + " !== " + right_c + ")" } let op_c: String = js_binop(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 = js_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, ", ") if func_kind == "Ident" { let fn_name: String = func["name"] let call_expr: String = fn_name + "(" + args_c + ")" if js_is_async_builtin(fn_name) { return "await " + call_expr } if js_is_async_fn(fn_name) { return "await " + call_expr } return call_expr } if func_kind == "Field" { let obj = func["object"] let field: String = func["field"] let obj_c: String = js_cg_expr(obj) // If the method is a known El runtime shortform, keep the El // convention: `method(obj, args)`. This preserves backward // compatibility with list.append(x), map.map_get(k), etc. if js_is_el_method(field) { if arity > 0 { return field + "(" + obj_c + ", " + args_c + ")" } return field + "(" + obj_c + ")" } // Unknown method — emit as a native JS method call on the // receiver. This handles Any-typed values (third-party library // objects, DOM elements, Promises, etc.) without requiring // native_js_call. Example: `client.auth.signInWithOtp(payload)` // emits `client["auth"].signInWithOtp(args_c)`. if arity > 0 { return obj_c + "." + field + "(" + args_c + ")" } return obj_c + "." + field + "()" } let fn_c: String = js_cg_expr(func) return fn_c + "(" + args_c + ")" } if kind == "Field" { // El's `obj.foo` becomes JS `obj["foo"]` — direct bracket access. // This works for plain El map objects AND for real JS objects with // prototype-inherited properties (DOM elements, third-party library // objects, Promises, etc.). el_get_field used hasOwnProperty which // silently returned null for inherited props, breaking e.g. client.auth. // // Nil-propagation: `obj?.foo` emits `(obj)?.["foo"] ?? null`. let obj = expr["object"] let field: String = expr["field"] let obj_kind: String = obj["expr"] if str_eq(obj_kind, "Try") { let inner = obj["inner"] let inner_c: String = js_cg_expr(inner) return "(" + inner_c + ")?.[" + js_str_lit(field) + "] ?? null" } let obj_c: String = js_cg_expr(obj) return obj_c + "[" + js_str_lit(field) + "]" } if kind == "Index" { // Map vs list dispatch on the index expression kind, same as C. // If the object is a Try (nil-propagation), use JS optional indexing. let obj = expr["object"] let idx = expr["index"] let obj_c: String = js_cg_expr(obj) let idx_c: String = js_cg_expr(idx) let idx_kind: String = idx["expr"] let obj_kind: String = obj["expr"] if str_eq(obj_kind, "Try") { let inner = obj["inner"] let inner_c: String = js_cg_expr(inner) return "(" + inner_c + ")?.[" + idx_c + "] ?? null" } 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) if n == 0 { return "[]" } let items_parts: [String] = native_list_empty() let i = 0 while i < n { let elem = native_list_get(elems, i) let elem_c: String = js_cg_expr(elem) let items_parts = native_list_append(items_parts, elem_c) let i = i + 1 } return "[" + str_join(items_parts, ", ") + "]" } if kind == "Map" { let pairs = expr["pairs"] let n: Int = native_list_len(pairs) if n == 0 { return "{}" } 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 = js_cg_expr(val) let items_parts = native_list_append(items_parts, js_str_lit(key) + ": " + val_c) let i = i + 1 } return "{" + str_join(items_parts, ", ") + "}" } if kind == "Try" { // Postfix `?` — nil-propagation guard. // When used as `expr?.field` the Field handler above intercepts and // emits `(expr)?.["field"]`. Here, a bare `expr?` (not followed by // field/index access) passes through to the inner expression unchanged // (it acts as an identity but marks the value as "nil-propagating" for // its caller). This matches the C backend's current behavior. let inner = expr["inner"] return js_cg_expr(inner) } if kind == "If" { let cond = expr["cond"] let cond_c: String = js_cg_expr(cond) // If as expression: ternary. Body of the if-expression is not // currently emitted as expression-form for compound bodies; this // matches the C backend's if-expr stub. return "(" + cond_c + " ? 1 : 0)" } if kind == "Match" { return js_cg_match(expr) } // Lambda (anonymous function literal): fn(params) -> RetType { body } // Emitted as a JS arrow function expression: (params) => { body }. // Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... }) if kind == "Lambda" { return js_cg_lambda(expr) } if kind == "HtmlTemplate" { return js_cg_html_template(expr) } "null" } // ── Match codegen (basic) ───────────────────────────────────────────────────── // // Lower a match expression to an IIFE with if/else chain. Works for // LitInt / LitStr / LitBool / Wildcard / Binding patterns. Tagged-union // destructuring is not implemented — it's stubbed and falls through to // the wildcard path. fn js_next_match_id() -> String { let csv: String = state_get("__js_match_counter") let n = 0 if !str_eq(csv, "") { let n = str_to_int(csv) } let n = n + 1 state_set("__js_match_counter", native_int_to_str(n)) native_int_to_str(n) } fn js_cg_match(expr: Map) -> String { let subject = expr["subject"] let arms = expr["arms"] let subj_c: String = js_cg_expr(subject) let id: String = js_next_match_id() let subj_var: String = "_match_subj_" + id let parts: [String] = native_list_empty() let parts = native_list_append(parts, "((" + subj_var + ") => { ") 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 = js_cg_expr(body) if str_eq(pkind, "Wildcard") { let parts = native_list_append(parts, "return (" + body_c + "); ") } else { if str_eq(pkind, "Binding") { let bname: String = pat["name"] let parts = native_list_append(parts, "{ const " + bname + " = " + subj_var + "; return (" + body_c + "); } ") } else { if str_eq(pkind, "LitInt") { let v: String = pat["value"] let parts = native_list_append(parts, "if (" + subj_var + " === " + v + ") return (" + body_c + "); ") } else { if str_eq(pkind, "LitStr") { let v: String = pat["value"] let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", " + js_str_lit(v) + ")) return (" + body_c + "); ") } else { if str_eq(pkind, "LitBool") { let v: String = pat["value"] let bv = "false" if str_eq(v, "true") { let bv = "true" } let parts = native_list_append(parts, "if (" + subj_var + " === " + bv + ") return (" + body_c + "); ") } else { if str_eq(pkind, "Variant") { // Enum::Variant patterns — El enums compile to plain // strings (the variant name) or ints. Match the subject // against the variant name string. let variant: String = pat["variant"] let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", " + js_str_lit(variant) + ")) return (" + body_c + "); ") } else { // unknown pattern → wildcard let parts = native_list_append(parts, "return (" + body_c + "); ") } } } } } } let i = i + 1 } let parts = native_list_append(parts, "return null; })(" + subj_c + ")") str_join(parts, "") } // ── Lambda codegen ──────────────────────────────────────────────────────────── // // Anonymous function literals: fn(params) -> RetType { body } // // Strategy: emit the lambda as a hoisted JS function declaration with a // generated name (__lambda_N), then return the name as the expression value. // This works because JS function declarations are hoisted within their scope, // so the generated name is valid at any use site within the same function or // module. The emitted code looks like: // // function __lambda_1(event) { dom_hide(spinner); } // ... // dom_listen(btn, "click", __lambda_1); // // This approach is clean, debuggable, and avoids any need for a string-buffer // mode in the codegen. fn js_next_lambda_id() -> String { let csv: String = state_get("__js_lambda_counter") let n = 0 if !str_eq(csv, "") { let n = str_to_int(csv) } let n = n + 1 state_set("__js_lambda_counter", native_int_to_str(n)) native_int_to_str(n) } fn js_cg_lambda(expr: Map) -> String { let params = expr["params"] let body = expr["body"] let ret_type: String = expr["ret_type"] let id: String = js_next_lambda_id() let lambda_name: String = "__lambda_" + id let params_str: String = js_params_str(params) // Emit the function definition immediately into the output stream. // It will appear before the statement containing this expression. js_emit_line("function " + lambda_name + "(" + params_str + ") {") 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 } let body_xformed = body if !str_eq(ret_type, "Void") { let body_xformed = js_transform_implicit_return(body) } js_build_int_names_for_params(params) js_cg_stmts(body_xformed, " ", decl) js_emit_line("}") js_emit_blank() // Return the function name as the expression value. lambda_name } // ── Variable scope tracking ─────────────────────────────────────────────────── // // El allows `let x = ...` to redeclare in the same scope. JS would throw // with `let` (Identifier already declared). We track declared names and // emit bare `x = ...` on redeclaration, `let x = ...` first time. fn js_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 ───────────────────────────────────────────────────────── fn js_cg_stmt(stmt: Map, 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 = js_cg_expr(val) let ltype: String = stmt["type"] if str_eq(ltype, "Int") { js_add_int_name(name) } let vk: String = val["expr"] if str_eq(vk, "Int") { js_add_int_name(name) } if js_list_contains(declared, name) { js_emit_line(indent + name + " = " + val_c + ";") return declared } else { // Use `let` (not `const`) — El semantics allow rebinding. js_emit_line(indent + "let " + 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" { js_emit_line(indent + "return null;") } else { let val_c: String = js_cg_expr(val) js_emit_line(indent + "return " + val_c + ";") } return declared } // Bare reassignment: `name = expr`. Mirrors the C backend — emits a // plain JS assignment without `let` so we don't shadow an outer binding. if kind == "Assign" { let name: String = stmt["name"] let val = stmt["value"] let val_c: String = js_cg_expr(val) js_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" { js_cg_if_stmt(val, indent, declared) return declared } if val_kind == "For" { js_cg_for_stmt(val, indent, declared) return declared } let val_c: String = js_cg_expr(val) js_emit_line(indent + val_c + ";") return declared } if kind == "While" { let cond = stmt["cond"] let body = stmt["body"] let cond_c: String = js_cg_expr(cond) let cond_c = js_strip_outer_parens(cond_c) js_emit_line(indent + "while (" + cond_c + ") {") js_cg_stmts(body, indent + " ", native_list_clone(declared)) js_emit_line(indent + "}") return declared } if kind == "For" { let item: String = stmt["item"] let list_expr = stmt["list"] let body = stmt["body"] js_cg_for_body(item, list_expr, body, indent, declared) return declared } if kind == "FnDef" { return declared } if kind == "TypeDef" { return declared } if kind == "EnumDef" { return declared } if kind == "Import" { return declared } // TestDef: skip in normal mode; handled by js_codegen_test in test mode. if kind == "TestDef" { return declared } // Assert: no-op in normal mode; handled by js_cg_stmt_assert in test mode. if kind == "Assert" { return declared } if kind == "TryCatch" { let try_body = stmt["try_body"] let catch_name: String = stmt["catch_name"] let catch_body = stmt["catch_body"] js_emit_line(indent + "try {") js_cg_stmts(try_body, indent + " ", native_list_clone(declared)) js_emit_line(indent + "} catch (" + catch_name + ") {") js_cg_stmts(catch_body, indent + " ", native_list_clone(declared)) js_emit_line(indent + "}") return declared } // ExternFn: the function exists in the JS environment (loaded via