From 5c05ce9b995b5d6633e52f6d0cb519e649813379 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 30 Apr 2026 13:10:29 -0500 Subject: [PATCH] self-host the el compiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Today's milestone: dist/platform/elc compiles itself byte-for-byte to itself (stage1 == stage2 == stage3 verified). The compiler is now a real binary in the world. What landed - Spec rewrite (language.md) to truth — every feature marked implemented / planned / not-in-this-language with no fiction. - C runtime extension: 51 new builtins. JSON parser + accessors, time, UUID, env, in-process state K/V, float formatting + math, string ops (index_of, split, char_at, char_code, pad_left/right, format), list ops (push, push_front, join, range), bool_to_str. Runtime grew 631 → 1611 lines, header 171 → 247. - Codegen fix: transform_implicit_return lifts a function's bare trailing expression into an explicit return. Without it, lex(), parse(), and every other implicit-return function returned 0/nil and the whole pipeline produced empty C output. - Codegen fix: index expressions dispatch on AST kind. obj["literal"] → el_get_field (map), arr[i] → el_list_get (list). Same Index node in the parser, two different runtime calls. - Codegen fix: skip emitting fn main() (collides with C main()) and honor parsed return-type annotations so Void functions don't get return-wrapped (return println(x) is a C type error). - Parser: capture return-type identifier from -> Ret annotations. - Lexer: + vessel keyword, + % operator, + \r escape. - Runtime fix: el_list_append now allocates a fresh list rather than realloc'ing the input. Realloc moved blocks made caller pointers dangle, which was inserting garbage values into declared lists and causing strcmp segfaults. Persistent allocation eliminates the whole class of use-after-free at modest memory cost. Bootstrap path - One-shot Python helper translated elc-combined.el to C and produced stage1. Helper is disposable; not committed. - stage1 compiles elc-combined.el → stage2.c which cc compiles to stage2; stage2 compiles elc-combined.el → stage3.c. stage2.c and stage3.c are byte-identical. Closure proven. - New elc installed at dist/platform/elc; old broken binary preserved as dist/platform/elc.legacy. - dist/platform/elc.c is the canonical generated source. - elvm and the bytecode pipeline are no longer on the critical path. Known gap - The `+` operator's heuristic dispatch still picks string concat when both operands are Idents with no literal anchor. Self-hosting works because the compiler source is careful, but `fn add(a:Int, b:Int) { a + b }` will not do arithmetic until codegen reads the parsed type annotations to dispatch. Fix is wiring; not done here. Tested - tiny / lextest / whiletest / map+field / array build all run. - cgi-studio (1037 lines real El) compiles to C cleanly. Link fails only because runtime is missing fs_list, json_encode, llm_*; those are scheduled batches. - Three-stage closure (stage1 vs stage2 vs stage3) byte-identical. --- dist/platform/elc | Bin 0 -> 93576 bytes dist/platform/elc.c | 2241 ++++++++++++++++++++++++++++++ dist/platform/elc.legacy | Bin 0 -> 72592 bytes el-compiler/runtime/el_runtime.c | 1224 +++++++++++++++- el-compiler/runtime/el_runtime.h | 139 ++ el-compiler/src/codegen.el | 393 ++++-- el-compiler/src/lexer.el | 75 +- el-compiler/src/parser.el | 298 ++-- elc-cli.el | 8 + elc-combined.el | 2081 +++++++++++++++++++++++++++ spec/language.md | 1385 +++++++++--------- 11 files changed, 6890 insertions(+), 954 deletions(-) create mode 100755 dist/platform/elc create mode 100644 dist/platform/elc.c create mode 100755 dist/platform/elc.legacy create mode 100644 elc-cli.el create mode 100644 elc-combined.el diff --git a/dist/platform/elc b/dist/platform/elc new file mode 100755 index 0000000000000000000000000000000000000000..b1e96c9e3e9302f9456c6826c42d83471637b56b GIT binary patch literal 93576 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 literal 0 HcmV?d00001 diff --git a/dist/platform/elc.c b/dist/platform/elc.c new file mode 100644 index 0000000..e3e2559 --- /dev/null +++ b/dist/platform/elc.c @@ -0,0 +1,2241 @@ +#include +#include +#include "el_runtime.h" + +el_val_t is_digit(el_val_t ch); +el_val_t is_alpha(el_val_t ch); +el_val_t is_alnum_or_underscore(el_val_t ch); +el_val_t is_whitespace(el_val_t ch); +el_val_t make_tok(el_val_t kind, el_val_t value); +el_val_t keyword_kind(el_val_t word); +el_val_t scan_digits(el_val_t chars, el_val_t start, el_val_t total); +el_val_t scan_ident(el_val_t chars, el_val_t start, el_val_t total); +el_val_t scan_string(el_val_t chars, el_val_t start, el_val_t total); +el_val_t lex(el_val_t source); +el_val_t tok_at(el_val_t tokens, el_val_t pos); +el_val_t tok_kind(el_val_t tokens, el_val_t pos); +el_val_t tok_value(el_val_t tokens, el_val_t pos); +el_val_t expect(el_val_t tokens, el_val_t pos, el_val_t kind); +el_val_t make_result(el_val_t node, el_val_t pos); +el_val_t skip_type(el_val_t tokens, el_val_t pos); +el_val_t parse_params(el_val_t tokens, el_val_t pos); +el_val_t parse_primary(el_val_t tokens, el_val_t pos); +el_val_t parse_if(el_val_t tokens, el_val_t pos); +el_val_t parse_match(el_val_t tokens, el_val_t pos); +el_val_t parse_pattern(el_val_t tokens, el_val_t pos); +el_val_t parse_for_expr(el_val_t tokens, el_val_t pos); +el_val_t parse_block(el_val_t tokens, el_val_t pos); +el_val_t parse_postfix(el_val_t tokens, el_val_t pos); +el_val_t op_precedence(el_val_t kind); +el_val_t is_binop(el_val_t kind); +el_val_t parse_binop(el_val_t tokens, el_val_t pos, el_val_t min_prec); +el_val_t parse_expr(el_val_t tokens, el_val_t pos); +el_val_t parse_stmt(el_val_t tokens, el_val_t pos); +el_val_t parse(el_val_t tokens); +el_val_t c_escape(el_val_t s); +el_val_t c_str_lit(el_val_t s); +el_val_t el_type_to_c(el_val_t type_str); +el_val_t emit_line(el_val_t line); +el_val_t emit_blank(void); +el_val_t binop_to_c(el_val_t op); +el_val_t cg_expr(el_val_t expr); +el_val_t list_contains(el_val_t lst, el_val_t s); +el_val_t cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared); +el_val_t strip_outer_parens(el_val_t s); +el_val_t cg_if_stmt(el_val_t expr, el_val_t indent, el_val_t declared); +el_val_t cg_for_body(el_val_t item, el_val_t list_expr, el_val_t body, el_val_t indent, el_val_t declared); +el_val_t cg_for_stmt(el_val_t expr, el_val_t indent, el_val_t declared); +el_val_t cg_stmts(el_val_t stmts, el_val_t indent, el_val_t declared); +el_val_t param_decl(el_val_t param, el_val_t idx); +el_val_t params_to_c(el_val_t params); +el_val_t transform_implicit_return(el_val_t body); +el_val_t cg_fn(el_val_t stmt); +el_val_t is_fndef(el_val_t stmt); +el_val_t is_top_level_decl(el_val_t stmt); +el_val_t codegen(el_val_t stmts, el_val_t source); +el_val_t compile(el_val_t source); + +el_val_t is_digit(el_val_t ch) { + if (str_eq(ch, EL_STR("0"))) { + return 1; + } + if (str_eq(ch, EL_STR("1"))) { + return 1; + } + if (str_eq(ch, EL_STR("2"))) { + return 1; + } + if (str_eq(ch, EL_STR("3"))) { + return 1; + } + if (str_eq(ch, EL_STR("4"))) { + return 1; + } + if (str_eq(ch, EL_STR("5"))) { + return 1; + } + if (str_eq(ch, EL_STR("6"))) { + return 1; + } + if (str_eq(ch, EL_STR("7"))) { + return 1; + } + if (str_eq(ch, EL_STR("8"))) { + return 1; + } + if (str_eq(ch, EL_STR("9"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_alpha(el_val_t ch) { + if (str_eq(ch, EL_STR("a"))) { + return 1; + } + if (str_eq(ch, EL_STR("b"))) { + return 1; + } + if (str_eq(ch, EL_STR("c"))) { + return 1; + } + if (str_eq(ch, EL_STR("d"))) { + return 1; + } + if (str_eq(ch, EL_STR("e"))) { + return 1; + } + if (str_eq(ch, EL_STR("f"))) { + return 1; + } + if (str_eq(ch, EL_STR("g"))) { + return 1; + } + if (str_eq(ch, EL_STR("h"))) { + return 1; + } + if (str_eq(ch, EL_STR("i"))) { + return 1; + } + if (str_eq(ch, EL_STR("j"))) { + return 1; + } + if (str_eq(ch, EL_STR("k"))) { + return 1; + } + if (str_eq(ch, EL_STR("l"))) { + return 1; + } + if (str_eq(ch, EL_STR("m"))) { + return 1; + } + if (str_eq(ch, EL_STR("n"))) { + return 1; + } + if (str_eq(ch, EL_STR("o"))) { + return 1; + } + if (str_eq(ch, EL_STR("p"))) { + return 1; + } + if (str_eq(ch, EL_STR("q"))) { + return 1; + } + if (str_eq(ch, EL_STR("r"))) { + return 1; + } + if (str_eq(ch, EL_STR("s"))) { + return 1; + } + if (str_eq(ch, EL_STR("t"))) { + return 1; + } + if (str_eq(ch, EL_STR("u"))) { + return 1; + } + if (str_eq(ch, EL_STR("v"))) { + return 1; + } + if (str_eq(ch, EL_STR("w"))) { + return 1; + } + if (str_eq(ch, EL_STR("x"))) { + return 1; + } + if (str_eq(ch, EL_STR("y"))) { + return 1; + } + if (str_eq(ch, EL_STR("z"))) { + return 1; + } + if (str_eq(ch, EL_STR("A"))) { + return 1; + } + if (str_eq(ch, EL_STR("B"))) { + return 1; + } + if (str_eq(ch, EL_STR("C"))) { + return 1; + } + if (str_eq(ch, EL_STR("D"))) { + return 1; + } + if (str_eq(ch, EL_STR("E"))) { + return 1; + } + if (str_eq(ch, EL_STR("F"))) { + return 1; + } + if (str_eq(ch, EL_STR("G"))) { + return 1; + } + if (str_eq(ch, EL_STR("H"))) { + return 1; + } + if (str_eq(ch, EL_STR("I"))) { + return 1; + } + if (str_eq(ch, EL_STR("J"))) { + return 1; + } + if (str_eq(ch, EL_STR("K"))) { + return 1; + } + if (str_eq(ch, EL_STR("L"))) { + return 1; + } + if (str_eq(ch, EL_STR("M"))) { + return 1; + } + if (str_eq(ch, EL_STR("N"))) { + return 1; + } + if (str_eq(ch, EL_STR("O"))) { + return 1; + } + if (str_eq(ch, EL_STR("P"))) { + return 1; + } + if (str_eq(ch, EL_STR("Q"))) { + return 1; + } + if (str_eq(ch, EL_STR("R"))) { + return 1; + } + if (str_eq(ch, EL_STR("S"))) { + return 1; + } + if (str_eq(ch, EL_STR("T"))) { + return 1; + } + if (str_eq(ch, EL_STR("U"))) { + return 1; + } + if (str_eq(ch, EL_STR("V"))) { + return 1; + } + if (str_eq(ch, EL_STR("W"))) { + return 1; + } + if (str_eq(ch, EL_STR("X"))) { + return 1; + } + if (str_eq(ch, EL_STR("Y"))) { + return 1; + } + if (str_eq(ch, EL_STR("Z"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_alnum_or_underscore(el_val_t ch) { + if (is_digit(ch)) { + return 1; + } + if (is_alpha(ch)) { + return 1; + } + if (str_eq(ch, EL_STR("_"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_whitespace(el_val_t ch) { + if (str_eq(ch, EL_STR(" "))) { + return 1; + } + if (str_eq(ch, EL_STR("\t"))) { + return 1; + } + if (str_eq(ch, EL_STR("\n"))) { + return 1; + } + if (str_eq(ch, EL_STR("\r"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t make_tok(el_val_t kind, el_val_t value) { + return el_map_new(2, "kind", kind, "value", value); + return 0; +} + +el_val_t keyword_kind(el_val_t word) { + if (str_eq(word, EL_STR("let"))) { + return EL_STR("Let"); + } + if (str_eq(word, EL_STR("fn"))) { + return EL_STR("Fn"); + } + if (str_eq(word, EL_STR("type"))) { + return EL_STR("Type"); + } + if (str_eq(word, EL_STR("enum"))) { + return EL_STR("Enum"); + } + if (str_eq(word, EL_STR("match"))) { + return EL_STR("Match"); + } + if (str_eq(word, EL_STR("return"))) { + return EL_STR("Return"); + } + if (str_eq(word, EL_STR("if"))) { + return EL_STR("If"); + } + if (str_eq(word, EL_STR("else"))) { + return EL_STR("Else"); + } + if (str_eq(word, EL_STR("for"))) { + return EL_STR("For"); + } + if (str_eq(word, EL_STR("in"))) { + return EL_STR("In"); + } + if (str_eq(word, EL_STR("while"))) { + return EL_STR("While"); + } + if (str_eq(word, EL_STR("import"))) { + return EL_STR("Import"); + } + if (str_eq(word, EL_STR("from"))) { + return EL_STR("From"); + } + if (str_eq(word, EL_STR("as"))) { + return EL_STR("As"); + } + if (str_eq(word, EL_STR("with"))) { + return EL_STR("With"); + } + if (str_eq(word, EL_STR("sealed"))) { + return EL_STR("Sealed"); + } + if (str_eq(word, EL_STR("activate"))) { + return EL_STR("Activate"); + } + if (str_eq(word, EL_STR("where"))) { + return EL_STR("Where"); + } + if (str_eq(word, EL_STR("test"))) { + return EL_STR("Test"); + } + if (str_eq(word, EL_STR("seed"))) { + return EL_STR("Seed"); + } + if (str_eq(word, EL_STR("assert"))) { + return EL_STR("Assert"); + } + if (str_eq(word, EL_STR("protocol"))) { + return EL_STR("Protocol"); + } + if (str_eq(word, EL_STR("impl"))) { + return EL_STR("Impl"); + } + if (str_eq(word, EL_STR("retry"))) { + return EL_STR("Retry"); + } + if (str_eq(word, EL_STR("times"))) { + return EL_STR("Times"); + } + if (str_eq(word, EL_STR("fallback"))) { + return EL_STR("Fallback"); + } + if (str_eq(word, EL_STR("reason"))) { + return EL_STR("Reason"); + } + if (str_eq(word, EL_STR("parallel"))) { + return EL_STR("Parallel"); + } + if (str_eq(word, EL_STR("trace"))) { + return EL_STR("Trace"); + } + if (str_eq(word, EL_STR("requires"))) { + return EL_STR("Requires"); + } + if (str_eq(word, EL_STR("deploy"))) { + return EL_STR("Deploy"); + } + if (str_eq(word, EL_STR("to"))) { + return EL_STR("To"); + } + if (str_eq(word, EL_STR("via"))) { + return EL_STR("Via"); + } + if (str_eq(word, EL_STR("target"))) { + return EL_STR("Target"); + } + if (str_eq(word, EL_STR("true"))) { + return EL_STR("Bool"); + } + if (str_eq(word, EL_STR("false"))) { + return EL_STR("Bool"); + } + if (str_eq(word, EL_STR("cgi"))) { + return EL_STR("Cgi"); + } + if (str_eq(word, EL_STR("manager"))) { + return EL_STR("Manager"); + } + if (str_eq(word, EL_STR("engine"))) { + return EL_STR("Engine"); + } + if (str_eq(word, EL_STR("accessor"))) { + return EL_STR("Accessor"); + } + if (str_eq(word, EL_STR("vessel"))) { + return EL_STR("Vessel"); + } + return EL_STR(""); + return 0; +} + +el_val_t scan_digits(el_val_t chars, el_val_t start, el_val_t total) { + el_val_t i = start; + el_val_t text = EL_STR(""); + el_val_t running = 1; + while (running) { + if (i >= total) { + running = 0; + } else { + el_val_t ch = native_list_get(chars, i); + if (is_digit(ch)) { + text = el_str_concat(text, ch); + i = (i + 1); + } else { + running = 0; + } + } + } + return el_map_new(2, "text", text, "pos", i); + return 0; +} + +el_val_t scan_ident(el_val_t chars, el_val_t start, el_val_t total) { + el_val_t i = start; + el_val_t text = EL_STR(""); + el_val_t running = 1; + while (running) { + if (i >= total) { + running = 0; + } else { + el_val_t ch = native_list_get(chars, i); + if (is_alnum_or_underscore(ch)) { + text = el_str_concat(text, ch); + i = (i + 1); + } else { + running = 0; + } + } + } + return el_map_new(2, "text", text, "pos", i); + return 0; +} + +el_val_t scan_string(el_val_t chars, el_val_t start, el_val_t total) { + el_val_t i = start; + el_val_t text = EL_STR(""); + el_val_t running = 1; + while (running) { + if (i >= total) { + running = 0; + } else { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("\\"))) { + el_val_t next_i = (i + 1); + if (next_i < total) { + el_val_t next_ch = native_list_get(chars, next_i); + if (str_eq(next_ch, EL_STR("\""))) { + text = el_str_concat(text, EL_STR("\"")); + i = (next_i + 1); + } else { + if (str_eq(next_ch, EL_STR("n"))) { + text = el_str_concat(text, EL_STR("\n")); + i = (next_i + 1); + } else { + if (str_eq(next_ch, EL_STR("t"))) { + text = el_str_concat(text, EL_STR("\t")); + i = (next_i + 1); + } else { + if (str_eq(next_ch, EL_STR("r"))) { + text = el_str_concat(text, EL_STR("\r")); + i = (next_i + 1); + } else { + if (str_eq(next_ch, EL_STR("\\"))) { + text = el_str_concat(text, EL_STR("\\")); + i = (next_i + 1); + } else { + text = el_str_concat(text, next_ch); + i = (next_i + 1); + } + } + } + } + } + } else { + i = (i + 1); + } + } else { + if (str_eq(ch, EL_STR("\""))) { + i = (i + 1); + running = 0; + } else { + text = el_str_concat(text, ch); + i = (i + 1); + } + } + } + } + return el_map_new(2, "text", text, "pos", i); + return 0; +} + +el_val_t lex(el_val_t source) { + el_val_t chars = native_string_chars(source); + el_val_t total = native_list_len(chars); + el_val_t tokens = native_list_empty(); + el_val_t i = 0; + while (i < total) { + el_val_t ch = native_list_get(chars, i); + if (is_whitespace(ch)) { + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("/"))) { + el_val_t next_i = (i + 1); + if (next_i < total) { + el_val_t next_ch = native_list_get(chars, next_i); + if (str_eq(next_ch, EL_STR("/"))) { + i = (i + 2); + el_val_t running2 = 1; + while (running2) { + if (i >= total) { + running2 = 0; + } else { + el_val_t lch = native_list_get(chars, i); + if (str_eq(lch, EL_STR("\n"))) { + running2 = 0; + } else { + i = (i + 1); + } + } + } + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Slash"), EL_STR("/"))); + i = (i + 1); + } + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Slash"), EL_STR("/"))); + i = (i + 1); + } + } else { + if (str_eq(ch, EL_STR("\""))) { + el_val_t result = scan_string(chars, (i + 1), total); + el_val_t str_text = el_get_field(result, EL_STR("text")); + el_val_t new_pos = el_get_field(result, EL_STR("pos")); + tokens = native_list_append(tokens, make_tok(EL_STR("Str"), str_text)); + i = new_pos; + } else { + if (is_digit(ch)) { + el_val_t result = scan_digits(chars, i, total); + el_val_t num_text = el_get_field(result, EL_STR("text")); + el_val_t new_pos = el_get_field(result, EL_STR("pos")); + if (new_pos < total) { + el_val_t dot_ch = native_list_get(chars, new_pos); + if (str_eq(dot_ch, EL_STR("."))) { + el_val_t after_dot = (new_pos + 1); + if (after_dot < total) { + el_val_t after_dot_ch = native_list_get(chars, after_dot); + if (is_digit(after_dot_ch)) { + el_val_t frac_result = scan_digits(chars, after_dot, total); + el_val_t frac_text = el_get_field(frac_result, EL_STR("text")); + el_val_t frac_pos = el_get_field(frac_result, EL_STR("pos")); + tokens = native_list_append(tokens, make_tok(EL_STR("Float"), el_str_concat(el_str_concat(num_text, EL_STR(".")), frac_text))); + i = frac_pos; + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Int"), num_text)); + i = new_pos; + } + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Int"), num_text)); + i = new_pos; + } + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Int"), num_text)); + i = new_pos; + } + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Int"), num_text)); + i = new_pos; + } + } else { + if (is_alpha(ch) || str_eq(ch, EL_STR("_"))) { + el_val_t result = scan_ident(chars, i, total); + el_val_t word = el_get_field(result, EL_STR("text")); + el_val_t new_pos = el_get_field(result, EL_STR("pos")); + el_val_t kw = keyword_kind(word); + if (str_eq(kw, EL_STR(""))) { + tokens = native_list_append(tokens, make_tok(EL_STR("Ident"), word)); + } else { + tokens = native_list_append(tokens, make_tok(kw, word)); + } + i = new_pos; + } else { + el_val_t peek_i = (i + 1); + el_val_t peek_ch = EL_STR(""); + if (peek_i < total) { + peek_ch = native_list_get(chars, peek_i); + } + if (str_eq(ch, EL_STR("="))) { + if (str_eq(peek_ch, EL_STR("="))) { + tokens = native_list_append(tokens, make_tok(EL_STR("EqEq"), EL_STR("=="))); + i = (i + 2); + } else { + if (str_eq(peek_ch, EL_STR(">"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("FatArrow"), EL_STR("=>"))); + i = (i + 2); + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Eq"), EL_STR("="))); + i = (i + 1); + } + } + } else { + if (str_eq(ch, EL_STR("!"))) { + if (str_eq(peek_ch, EL_STR("="))) { + tokens = native_list_append(tokens, make_tok(EL_STR("NotEq"), EL_STR("!="))); + i = (i + 2); + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Not"), EL_STR("!"))); + i = (i + 1); + } + } else { + if (str_eq(ch, EL_STR("<"))) { + if (str_eq(peek_ch, EL_STR("="))) { + tokens = native_list_append(tokens, make_tok(EL_STR("LtEq"), EL_STR("<="))); + i = (i + 2); + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Lt"), EL_STR("<"))); + i = (i + 1); + } + } else { + if (str_eq(ch, EL_STR(">"))) { + if (str_eq(peek_ch, EL_STR("="))) { + tokens = native_list_append(tokens, make_tok(EL_STR("GtEq"), EL_STR(">="))); + i = (i + 2); + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Gt"), EL_STR(">"))); + i = (i + 1); + } + } else { + if (str_eq(ch, EL_STR("&"))) { + if (str_eq(peek_ch, EL_STR("&"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("And"), EL_STR("&&"))); + i = (i + 2); + } else { + i = (i + 1); + } + } else { + if (str_eq(ch, EL_STR("|"))) { + if (str_eq(peek_ch, EL_STR("|"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("Or"), EL_STR("||"))); + i = (i + 2); + } else { + if (str_eq(peek_ch, EL_STR(">"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("PipeOp"), EL_STR("|>"))); + i = (i + 2); + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Pipe"), EL_STR("|"))); + i = (i + 1); + } + } + } else { + if (str_eq(ch, EL_STR("-"))) { + if (str_eq(peek_ch, EL_STR(">"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("Arrow"), EL_STR("->"))); + i = (i + 2); + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Minus"), EL_STR("-"))); + i = (i + 1); + } + } else { + if (str_eq(ch, EL_STR(":"))) { + if (str_eq(peek_ch, EL_STR(":"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("ColonColon"), EL_STR("::"))); + i = (i + 2); + } else { + tokens = native_list_append(tokens, make_tok(EL_STR("Colon"), EL_STR(":"))); + i = (i + 1); + } + } else { + if (str_eq(ch, EL_STR("+"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("Plus"), EL_STR("+"))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("*"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("Star"), EL_STR("*"))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("%"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("Percent"), EL_STR("%"))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("("))) { + tokens = native_list_append(tokens, make_tok(EL_STR("LParen"), EL_STR("("))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR(")"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("RParen"), EL_STR(")"))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("{"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("LBrace"), EL_STR("{"))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("}"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("RBrace"), EL_STR("}"))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("["))) { + tokens = native_list_append(tokens, make_tok(EL_STR("LBracket"), EL_STR("["))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("]"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("RBracket"), EL_STR("]"))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR(","))) { + tokens = native_list_append(tokens, make_tok(EL_STR("Comma"), EL_STR(","))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("."))) { + tokens = native_list_append(tokens, make_tok(EL_STR("Dot"), EL_STR("."))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR(";"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("Semicolon"), EL_STR(";"))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("@"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("At"), EL_STR("@"))); + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("?"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("QuestionMark"), EL_STR("?"))); + i = (i + 1); + } else { + i = (i + 1); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + tokens = native_list_append(tokens, make_tok(EL_STR("Eof"), EL_STR(""))); + return tokens; + return 0; +} + +el_val_t tok_at(el_val_t tokens, el_val_t pos) { + return native_list_get(tokens, pos); + return 0; +} + +el_val_t tok_kind(el_val_t tokens, el_val_t pos) { + el_val_t t = native_list_get(tokens, pos); + return el_get_field(t, EL_STR("kind")); + return 0; +} + +el_val_t tok_value(el_val_t tokens, el_val_t pos) { + el_val_t t = native_list_get(tokens, pos); + return el_get_field(t, EL_STR("value")); + return 0; +} + +el_val_t expect(el_val_t tokens, el_val_t pos, el_val_t kind) { + el_val_t k = tok_kind(tokens, pos); + if (str_eq(k, kind)) { + return (pos + 1); + } + return (pos + 1); + return 0; +} + +el_val_t make_result(el_val_t node, el_val_t pos) { + return el_map_new(2, "node", node, "pos", pos); + return 0; +} + +el_val_t skip_type(el_val_t tokens, el_val_t pos) { + el_val_t k = tok_kind(tokens, pos); + if (str_eq(k, EL_STR("LBracket"))) { + el_val_t p = (pos + 1); + p = skip_type(tokens, p); + p = expect(tokens, p, EL_STR("RBracket")); + return p; + } + if (str_eq(k, EL_STR("Ident"))) { + el_val_t p = (pos + 1); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Lt"))) { + p = (p + 1); + el_val_t depth = 1; + el_val_t running = 1; + while (running) { + el_val_t kk = tok_kind(tokens, p); + if (str_eq(kk, EL_STR("Eof"))) { + running = 0; + } else { + if (str_eq(kk, EL_STR("Lt"))) { + depth = (depth + 1); + p = (p + 1); + } else { + if (str_eq(kk, EL_STR("Gt"))) { + depth = (depth - 1); + p = (p + 1); + if (depth <= 0) { + running = 0; + } + } else { + p = (p + 1); + } + } + } + } + el_val_t k3 = tok_kind(tokens, p); + if (str_eq(k3, EL_STR("QuestionMark"))) { + p = (p + 1); + } + return p; + } + if (str_eq(k2, EL_STR("QuestionMark"))) { + return (p + 1); + } + return p; + } + return (pos + 1); + return 0; +} + +el_val_t parse_params(el_val_t tokens, el_val_t pos) { + el_val_t p = expect(tokens, pos, EL_STR("LParen")); + el_val_t params = native_list_empty(); + el_val_t running = 1; + while (running) { + el_val_t k = tok_kind(tokens, p); + if (str_eq(k, EL_STR("RParen"))) { + running = 0; + } else { + if (str_eq(k, EL_STR("Eof"))) { + running = 0; + } else { + el_val_t pname = tok_value(tokens, p); + p = (p + 1); + p = expect(tokens, p, EL_STR("Colon")); + p = skip_type(tokens, p); + el_val_t param = el_map_new(1, "name", pname); + params = native_list_append(params, param); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Comma"))) { + p = (p + 1); + } + } + } + } + p = expect(tokens, p, EL_STR("RParen")); + return el_map_new(2, "params", params, "pos", p); + return 0; +} + +el_val_t parse_primary(el_val_t tokens, el_val_t pos) { + el_val_t k = tok_kind(tokens, pos); + el_val_t v = tok_value(tokens, pos); + if (str_eq(k, EL_STR("Int"))) { + return make_result(el_map_new(2, "expr", EL_STR("Int"), "value", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Float"))) { + return make_result(el_map_new(2, "expr", EL_STR("Float"), "value", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Str"))) { + return make_result(el_map_new(2, "expr", EL_STR("Str"), "value", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Bool"))) { + return make_result(el_map_new(2, "expr", EL_STR("Bool"), "value", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Ident"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("LParen"))) { + el_val_t r = parse_expr(tokens, (pos + 1)); + el_val_t node = el_get_field(r, EL_STR("node")); + el_val_t p = el_get_field(r, EL_STR("pos")); + p = expect(tokens, p, EL_STR("RParen")); + return make_result(node, p); + } + if (str_eq(k, EL_STR("LBracket"))) { + el_val_t p = (pos + 1); + el_val_t elems = native_list_empty(); + el_val_t running = 1; + while (running) { + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("RBracket"))) { + running = 0; + } else { + if (str_eq(k2, EL_STR("Eof"))) { + running = 0; + } else { + el_val_t r = parse_expr(tokens, p); + el_val_t elem = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + elems = native_list_append(elems, elem); + el_val_t k3 = tok_kind(tokens, p); + if (str_eq(k3, EL_STR("Comma"))) { + p = (p + 1); + } + } + } + } + p = expect(tokens, p, EL_STR("RBracket")); + return make_result(el_map_new(2, "expr", EL_STR("Array"), "elems", elems), p); + } + if (str_eq(k, EL_STR("LBrace"))) { + el_val_t p = (pos + 1); + el_val_t pairs = native_list_empty(); + el_val_t running = 1; + while (running) { + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("RBrace"))) { + running = 0; + } else { + if (str_eq(k2, EL_STR("Eof"))) { + running = 0; + } else { + el_val_t key = tok_value(tokens, p); + p = (p + 1); + p = expect(tokens, p, EL_STR("Colon")); + el_val_t r = parse_expr(tokens, p); + el_val_t val_node = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + el_val_t pair = el_map_new(2, "key", key, "value", val_node); + pairs = native_list_append(pairs, pair); + el_val_t k3 = tok_kind(tokens, p); + if (str_eq(k3, EL_STR("Comma"))) { + p = (p + 1); + } + } + } + } + p = expect(tokens, p, EL_STR("RBrace")); + return make_result(el_map_new(2, "expr", EL_STR("Map"), "pairs", pairs), p); + } + if (str_eq(k, EL_STR("If"))) { + el_val_t r = parse_if(tokens, pos); + return r; + } + if (str_eq(k, EL_STR("Match"))) { + el_val_t r = parse_match(tokens, pos); + return r; + } + if (str_eq(k, EL_STR("For"))) { + el_val_t r = parse_for_expr(tokens, pos); + return r; + } + if (str_eq(k, EL_STR("Not"))) { + el_val_t r = parse_primary(tokens, (pos + 1)); + el_val_t inner = el_get_field(r, EL_STR("node")); + el_val_t p = el_get_field(r, EL_STR("pos")); + return make_result(el_map_new(2, "expr", EL_STR("Not"), "inner", inner), p); + } + if (str_eq(k, EL_STR("Minus"))) { + el_val_t r = parse_primary(tokens, (pos + 1)); + el_val_t inner = el_get_field(r, EL_STR("node")); + el_val_t p = el_get_field(r, EL_STR("pos")); + return make_result(el_map_new(2, "expr", EL_STR("Neg"), "inner", inner), p); + } + return make_result(el_map_new(1, "expr", EL_STR("Nil")), (pos + 1)); + return 0; +} + +el_val_t parse_if(el_val_t tokens, el_val_t pos) { + el_val_t p = expect(tokens, pos, EL_STR("If")); + el_val_t r = parse_expr(tokens, p); + el_val_t cond = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + el_val_t r2 = parse_block(tokens, p); + el_val_t then_stmts = el_get_field(r2, EL_STR("stmts")); + p = el_get_field(r2, EL_STR("pos")); + el_val_t has_else = 0; + el_val_t else_stmts = native_list_empty(); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Else"))) { + p = (p + 1); + el_val_t k3 = tok_kind(tokens, p); + if (str_eq(k3, EL_STR("If"))) { + el_val_t r3 = parse_if(tokens, p); + el_val_t nested = el_get_field(r3, EL_STR("node")); + p = el_get_field(r3, EL_STR("pos")); + else_stmts = native_list_append(else_stmts, el_map_new(2, "stmt", EL_STR("Expr"), "value", nested)); + has_else = 1; + } else { + el_val_t r3 = parse_block(tokens, p); + else_stmts = el_get_field(r3, EL_STR("stmts")); + p = el_get_field(r3, EL_STR("pos")); + has_else = 1; + } + } + return make_result(el_map_new(5, "expr", EL_STR("If"), "cond", cond, "then", then_stmts, "else", else_stmts, "has_else", has_else), p); + return 0; +} + +el_val_t parse_match(el_val_t tokens, el_val_t pos) { + el_val_t p = expect(tokens, pos, EL_STR("Match")); + el_val_t r = parse_expr(tokens, p); + el_val_t subject = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + p = expect(tokens, p, EL_STR("LBrace")); + el_val_t arms = native_list_empty(); + el_val_t running = 1; + while (running) { + el_val_t k = tok_kind(tokens, p); + if (str_eq(k, EL_STR("RBrace"))) { + running = 0; + } else { + if (str_eq(k, EL_STR("Eof"))) { + running = 0; + } else { + el_val_t r2 = parse_pattern(tokens, p); + el_val_t pattern = el_get_field(r2, EL_STR("node")); + p = el_get_field(r2, EL_STR("pos")); + p = expect(tokens, p, EL_STR("FatArrow")); + el_val_t r3 = parse_expr(tokens, p); + el_val_t body = el_get_field(r3, EL_STR("node")); + p = el_get_field(r3, EL_STR("pos")); + el_val_t arm = el_map_new(2, "pattern", pattern, "body", body); + arms = native_list_append(arms, arm); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Comma"))) { + p = (p + 1); + } + } + } + } + p = expect(tokens, p, EL_STR("RBrace")); + return make_result(el_map_new(3, "expr", EL_STR("Match"), "subject", subject, "arms", arms), p); + return 0; +} + +el_val_t parse_pattern(el_val_t tokens, el_val_t pos) { + el_val_t k = tok_kind(tokens, pos); + if (str_eq(k, EL_STR("Ident"))) { + el_val_t v = tok_value(tokens, pos); + if (str_eq(v, EL_STR("_"))) { + return make_result(el_map_new(1, "pattern", EL_STR("Wildcard")), (pos + 1)); + } + return make_result(el_map_new(2, "pattern", EL_STR("Binding"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Int"))) { + return make_result(el_map_new(2, "pattern", EL_STR("LitInt"), "value", tok_value(tokens, pos)), (pos + 1)); + } + if (str_eq(k, EL_STR("Str"))) { + return make_result(el_map_new(2, "pattern", EL_STR("LitStr"), "value", tok_value(tokens, pos)), (pos + 1)); + } + if (str_eq(k, EL_STR("Bool"))) { + return make_result(el_map_new(2, "pattern", EL_STR("LitBool"), "value", tok_value(tokens, pos)), (pos + 1)); + } + return make_result(el_map_new(1, "pattern", EL_STR("Wildcard")), (pos + 1)); + return 0; +} + +el_val_t parse_for_expr(el_val_t tokens, el_val_t pos) { + el_val_t p = expect(tokens, pos, EL_STR("For")); + el_val_t item_name = tok_value(tokens, p); + p = (p + 1); + p = expect(tokens, p, EL_STR("In")); + el_val_t r = parse_expr(tokens, p); + el_val_t list_expr = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + el_val_t r2 = parse_block(tokens, p); + el_val_t body = el_get_field(r2, EL_STR("stmts")); + p = el_get_field(r2, EL_STR("pos")); + return make_result(el_map_new(4, "expr", EL_STR("For"), "item", item_name, "list", list_expr, "body", body), p); + return 0; +} + +el_val_t parse_block(el_val_t tokens, el_val_t pos) { + el_val_t p = expect(tokens, pos, EL_STR("LBrace")); + el_val_t stmts = native_list_empty(); + el_val_t running = 1; + while (running) { + el_val_t k = tok_kind(tokens, p); + if (str_eq(k, EL_STR("RBrace"))) { + running = 0; + } else { + if (str_eq(k, EL_STR("Eof"))) { + running = 0; + } else { + el_val_t r = parse_stmt(tokens, p); + el_val_t stmt = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + stmts = native_list_append(stmts, stmt); + } + } + } + p = expect(tokens, p, EL_STR("RBrace")); + return el_map_new(2, "stmts", stmts, "pos", p); + return 0; +} + +el_val_t parse_postfix(el_val_t tokens, el_val_t pos) { + el_val_t r = parse_primary(tokens, pos); + el_val_t node = el_get_field(r, EL_STR("node")); + el_val_t p = el_get_field(r, EL_STR("pos")); + el_val_t running = 1; + while (running) { + el_val_t k = tok_kind(tokens, p); + if (str_eq(k, EL_STR("LParen"))) { + p = (p + 1); + el_val_t args = native_list_empty(); + el_val_t run2 = 1; + while (run2) { + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("RParen"))) { + run2 = 0; + } else { + if (str_eq(k2, EL_STR("Eof"))) { + run2 = 0; + } else { + el_val_t r2 = parse_expr(tokens, p); + el_val_t arg = el_get_field(r2, EL_STR("node")); + p = el_get_field(r2, EL_STR("pos")); + args = native_list_append(args, arg); + el_val_t k3 = tok_kind(tokens, p); + if (str_eq(k3, EL_STR("Comma"))) { + p = (p + 1); + } + } + } + } + p = expect(tokens, p, EL_STR("RParen")); + node = el_map_new(3, "expr", EL_STR("Call"), "func", node, "args", args); + } else { + if (str_eq(k, EL_STR("Dot"))) { + el_val_t field = tok_value(tokens, (p + 1)); + p = (p + 2); + node = el_map_new(3, "expr", EL_STR("Field"), "object", node, "field", field); + } else { + if (str_eq(k, EL_STR("LBracket"))) { + el_val_t r2 = parse_expr(tokens, (p + 1)); + el_val_t idx = el_get_field(r2, EL_STR("node")); + p = el_get_field(r2, EL_STR("pos")); + p = expect(tokens, p, EL_STR("RBracket")); + node = el_map_new(3, "expr", EL_STR("Index"), "object", node, "index", idx); + } else { + if (str_eq(k, EL_STR("QuestionMark"))) { + p = (p + 1); + node = el_map_new(2, "expr", EL_STR("Try"), "inner", node); + } else { + running = 0; + } + } + } + } + } + return make_result(node, p); + return 0; +} + +el_val_t op_precedence(el_val_t kind) { + if (str_eq(kind, EL_STR("Or"))) { + return 1; + } + if (str_eq(kind, EL_STR("And"))) { + return 2; + } + if (str_eq(kind, EL_STR("EqEq"))) { + return 3; + } + if (str_eq(kind, EL_STR("NotEq"))) { + return 3; + } + if (str_eq(kind, EL_STR("Lt"))) { + return 4; + } + if (str_eq(kind, EL_STR("Gt"))) { + return 4; + } + if (str_eq(kind, EL_STR("LtEq"))) { + return 4; + } + if (str_eq(kind, EL_STR("GtEq"))) { + return 4; + } + if (str_eq(kind, EL_STR("Plus"))) { + return 5; + } + if (str_eq(kind, EL_STR("Minus"))) { + return 5; + } + if (str_eq(kind, EL_STR("Star"))) { + return 6; + } + if (str_eq(kind, EL_STR("Slash"))) { + return 6; + } + return 0; + return 0; +} + +el_val_t is_binop(el_val_t kind) { + if (str_eq(kind, EL_STR("Or"))) { + return 1; + } + if (str_eq(kind, EL_STR("And"))) { + return 1; + } + if (str_eq(kind, EL_STR("EqEq"))) { + return 1; + } + if (str_eq(kind, EL_STR("NotEq"))) { + return 1; + } + if (str_eq(kind, EL_STR("Lt"))) { + return 1; + } + if (str_eq(kind, EL_STR("Gt"))) { + return 1; + } + if (str_eq(kind, EL_STR("LtEq"))) { + return 1; + } + if (str_eq(kind, EL_STR("GtEq"))) { + return 1; + } + if (str_eq(kind, EL_STR("Plus"))) { + return 1; + } + if (str_eq(kind, EL_STR("Minus"))) { + return 1; + } + if (str_eq(kind, EL_STR("Star"))) { + return 1; + } + if (str_eq(kind, EL_STR("Slash"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t parse_binop(el_val_t tokens, el_val_t pos, el_val_t min_prec) { + el_val_t r = parse_postfix(tokens, pos); + el_val_t left = el_get_field(r, EL_STR("node")); + el_val_t p = el_get_field(r, EL_STR("pos")); + el_val_t running = 1; + while (running) { + el_val_t k = tok_kind(tokens, p); + el_val_t prec = op_precedence(k); + if (is_binop(k)) { + if (prec >= min_prec) { + el_val_t op = k; + el_val_t r2 = parse_binop(tokens, (p + 1), (prec + 1)); + el_val_t right = el_get_field(r2, EL_STR("node")); + p = el_get_field(r2, EL_STR("pos")); + left = el_map_new(4, "expr", EL_STR("BinOp"), "op", op, "left", left, "right", right); + } else { + running = 0; + } + } else { + running = 0; + } + } + return make_result(left, p); + return 0; +} + +el_val_t parse_expr(el_val_t tokens, el_val_t pos) { + return parse_binop(tokens, pos, 1); + return 0; +} + +el_val_t parse_stmt(el_val_t tokens, el_val_t pos) { + el_val_t k = tok_kind(tokens, pos); + if (str_eq(k, EL_STR("Let"))) { + el_val_t p = (pos + 1); + el_val_t name = tok_value(tokens, p); + p = (p + 1); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Colon"))) { + p = (p + 1); + p = skip_type(tokens, p); + } + p = expect(tokens, p, EL_STR("Eq")); + el_val_t r = parse_expr(tokens, p); + el_val_t val = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + return make_result(el_map_new(3, "stmt", EL_STR("Let"), "name", name, "value", val), p); + } + if (str_eq(k, EL_STR("Return"))) { + el_val_t p = (pos + 1); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("RBrace"))) { + return make_result(el_map_new(2, "stmt", EL_STR("Return"), "value", el_map_new(1, "expr", EL_STR("Nil"))), p); + } + if (str_eq(k2, EL_STR("Eof"))) { + return make_result(el_map_new(2, "stmt", EL_STR("Return"), "value", el_map_new(1, "expr", EL_STR("Nil"))), p); + } + el_val_t r = parse_expr(tokens, p); + el_val_t val = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + return make_result(el_map_new(2, "stmt", EL_STR("Return"), "value", val), p); + } + if (str_eq(k, EL_STR("Fn"))) { + el_val_t p = (pos + 1); + el_val_t name = tok_value(tokens, p); + p = (p + 1); + el_val_t r = parse_params(tokens, p); + el_val_t params = el_get_field(r, EL_STR("params")); + p = el_get_field(r, EL_STR("pos")); + el_val_t ret_type = EL_STR(""); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Arrow"))) { + p = (p + 1); + el_val_t kt = tok_kind(tokens, p); + if (str_eq(kt, EL_STR("Ident"))) { + ret_type = tok_value(tokens, p); + } + p = skip_type(tokens, p); + } + el_val_t r2 = parse_block(tokens, p); + el_val_t body = el_get_field(r2, EL_STR("stmts")); + p = el_get_field(r2, EL_STR("pos")); + return make_result(el_map_new(5, "stmt", EL_STR("FnDef"), "name", name, "params", params, "body", body, "ret_type", ret_type), p); + } + if (str_eq(k, EL_STR("Type"))) { + el_val_t p = (pos + 1); + el_val_t name = tok_value(tokens, p); + p = (p + 1); + p = expect(tokens, p, EL_STR("LBrace")); + el_val_t fields = native_list_empty(); + el_val_t running = 1; + while (running) { + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("RBrace"))) { + running = 0; + } else { + if (str_eq(k2, EL_STR("Eof"))) { + running = 0; + } else { + el_val_t fname = tok_value(tokens, p); + p = (p + 1); + p = expect(tokens, p, EL_STR("Colon")); + p = skip_type(tokens, p); + fields = native_list_append(fields, el_map_new(1, "name", fname)); + el_val_t k3 = tok_kind(tokens, p); + if (str_eq(k3, EL_STR("Comma"))) { + p = (p + 1); + } + } + } + } + p = expect(tokens, p, EL_STR("RBrace")); + return make_result(el_map_new(3, "stmt", EL_STR("TypeDef"), "name", name, "fields", fields), p); + } + if (str_eq(k, EL_STR("Enum"))) { + el_val_t p = (pos + 1); + el_val_t name = tok_value(tokens, p); + p = (p + 1); + p = expect(tokens, p, EL_STR("LBrace")); + el_val_t variants = native_list_empty(); + el_val_t running = 1; + while (running) { + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("RBrace"))) { + running = 0; + } else { + if (str_eq(k2, EL_STR("Eof"))) { + running = 0; + } else { + el_val_t vname = tok_value(tokens, p); + p = (p + 1); + variants = native_list_append(variants, el_map_new(1, "name", vname)); + el_val_t k3 = tok_kind(tokens, p); + if (str_eq(k3, EL_STR("Comma"))) { + p = (p + 1); + } + } + } + } + p = expect(tokens, p, EL_STR("RBrace")); + return make_result(el_map_new(3, "stmt", EL_STR("EnumDef"), "name", name, "variants", variants), p); + } + if (str_eq(k, EL_STR("Import"))) { + el_val_t p = (pos + 1); + el_val_t path = tok_value(tokens, p); + p = (p + 1); + return make_result(el_map_new(2, "stmt", EL_STR("Import"), "path", path), p); + } + if (str_eq(k, EL_STR("From"))) { + el_val_t p = (pos + 1); + el_val_t module_name = tok_value(tokens, p); + p = (p + 1); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Import"))) { + p = (p + 1); + } + el_val_t k3 = tok_kind(tokens, p); + if (str_eq(k3, EL_STR("LBrace"))) { + p = (p + 1); + el_val_t running = 1; + while (running) { + el_val_t k4 = tok_kind(tokens, p); + if (str_eq(k4, EL_STR("RBrace"))) { + running = 0; + } else { + if (str_eq(k4, EL_STR("Eof"))) { + running = 0; + } else { + p = (p + 1); + el_val_t k5 = tok_kind(tokens, p); + if (str_eq(k5, EL_STR("Comma"))) { + p = (p + 1); + } + } + } + } + p = expect(tokens, p, EL_STR("RBrace")); + } + return make_result(el_map_new(2, "stmt", EL_STR("Import"), "path", module_name), p); + } + if (str_eq(k, EL_STR("While"))) { + el_val_t p = (pos + 1); + el_val_t r = parse_expr(tokens, p); + el_val_t cond = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + el_val_t r2 = parse_block(tokens, p); + el_val_t body = el_get_field(r2, EL_STR("stmts")); + p = el_get_field(r2, EL_STR("pos")); + return make_result(el_map_new(3, "stmt", EL_STR("While"), "cond", cond, "body", body), p); + } + if (str_eq(k, EL_STR("For"))) { + el_val_t p = (pos + 1); + el_val_t item_name = tok_value(tokens, p); + p = (p + 1); + p = expect(tokens, p, EL_STR("In")); + el_val_t r = parse_expr(tokens, p); + el_val_t list_expr = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + el_val_t r2 = parse_block(tokens, p); + el_val_t body = el_get_field(r2, EL_STR("stmts")); + p = el_get_field(r2, EL_STR("pos")); + return make_result(el_map_new(4, "stmt", EL_STR("For"), "item", item_name, "list", list_expr, "body", body), p); + } + if (str_eq(k, EL_STR("At"))) { + el_val_t p = (pos + 1); + p = (p + 1); + return parse_stmt(tokens, p); + } + el_val_t r = parse_expr(tokens, pos); + el_val_t val = el_get_field(r, EL_STR("node")); + el_val_t p = el_get_field(r, EL_STR("pos")); + return make_result(el_map_new(2, "stmt", EL_STR("Expr"), "value", val), p); + return 0; +} + +el_val_t parse(el_val_t tokens) { + el_val_t total = native_list_len(tokens); + el_val_t stmts = native_list_empty(); + el_val_t pos = 0; + el_val_t running = 1; + while (running) { + if (pos >= total) { + running = 0; + } else { + el_val_t k = tok_kind(tokens, pos); + if (str_eq(k, EL_STR("Eof"))) { + running = 0; + } else { + el_val_t r = parse_stmt(tokens, pos); + el_val_t stmt = el_get_field(r, EL_STR("node")); + el_val_t new_pos = el_get_field(r, EL_STR("pos")); + stmts = native_list_append(stmts, stmt); + if (new_pos <= pos) { + pos = (pos + 1); + } else { + pos = new_pos; + } + } + } + } + return stmts; + return 0; +} + +el_val_t c_escape(el_val_t s) { + el_val_t chars = native_string_chars(s); + el_val_t total = native_list_len(chars); + el_val_t out = EL_STR(""); + el_val_t i = 0; + while (i < total) { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("\""))) { + out = el_str_concat(out, EL_STR("\\\"")); + } else { + if (str_eq(ch, EL_STR("\\"))) { + out = el_str_concat(out, EL_STR("\\\\")); + } else { + if (str_eq(ch, EL_STR("\n"))) { + out = el_str_concat(out, EL_STR("\\n")); + } else { + if (str_eq(ch, EL_STR("\r"))) { + out = el_str_concat(out, EL_STR("\\r")); + } else { + if (str_eq(ch, EL_STR("\t"))) { + out = el_str_concat(out, EL_STR("\\t")); + } else { + out = el_str_concat(out, ch); + } + } + } + } + } + i = (i + 1); + } + return out; + return 0; +} + +el_val_t c_str_lit(el_val_t s) { + return el_str_concat(el_str_concat(EL_STR("\""), c_escape(s)), EL_STR("\"")); + return 0; +} + +el_val_t el_type_to_c(el_val_t type_str) { + if (str_eq(type_str, EL_STR("String"))) { + return EL_STR("const char*"); + } + if (str_eq(type_str, EL_STR("Int"))) { + return EL_STR("int64_t"); + } + if (str_eq(type_str, EL_STR("Bool"))) { + return EL_STR("int"); + } + if (str_eq(type_str, EL_STR("Float"))) { + return EL_STR("double"); + } + if (str_eq(type_str, EL_STR("Void"))) { + return EL_STR("void"); + } + if (str_eq(type_str, EL_STR("void"))) { + return EL_STR("void"); + } + return EL_STR("void*"); + return 0; +} + +el_val_t emit_line(el_val_t line) { + println(line); + return 0; +} + +el_val_t emit_blank(void) { + println(EL_STR("")); + return 0; +} + +el_val_t binop_to_c(el_val_t op) { + if (str_eq(op, EL_STR("Plus"))) { + return EL_STR("+"); + } + if (str_eq(op, EL_STR("Minus"))) { + return EL_STR("-"); + } + if (str_eq(op, EL_STR("Star"))) { + return EL_STR("*"); + } + if (str_eq(op, EL_STR("Slash"))) { + return EL_STR("/"); + } + if (str_eq(op, EL_STR("EqEq"))) { + return EL_STR("=="); + } + if (str_eq(op, EL_STR("NotEq"))) { + return EL_STR("!="); + } + if (str_eq(op, EL_STR("Lt"))) { + return EL_STR("<"); + } + if (str_eq(op, EL_STR("Gt"))) { + return EL_STR(">"); + } + if (str_eq(op, EL_STR("LtEq"))) { + return EL_STR("<="); + } + if (str_eq(op, EL_STR("GtEq"))) { + return EL_STR(">="); + } + if (str_eq(op, EL_STR("And"))) { + return EL_STR("&&"); + } + if (str_eq(op, EL_STR("Or"))) { + return EL_STR("||"); + } + return op; + return 0; +} + +el_val_t cg_expr(el_val_t expr) { + el_val_t kind = el_get_field(expr, EL_STR("expr")); + if (str_eq(kind, EL_STR("Int"))) { + el_val_t v = el_get_field(expr, EL_STR("value")); + return v; + } + if (str_eq(kind, EL_STR("Float"))) { + el_val_t v = el_get_field(expr, EL_STR("value")); + return v; + } + if (str_eq(kind, EL_STR("Str"))) { + el_val_t v = el_get_field(expr, EL_STR("value")); + return el_str_concat(el_str_concat(EL_STR("EL_STR("), c_str_lit(v)), EL_STR(")")); + } + if (str_eq(kind, EL_STR("Bool"))) { + el_val_t v = el_get_field(expr, EL_STR("value")); + if (str_eq(v, EL_STR("true"))) { + return EL_STR("1"); + } + return EL_STR("0"); + } + if (str_eq(kind, EL_STR("Nil"))) { + return EL_STR("EL_NULL"); + } + if (str_eq(kind, EL_STR("Ident"))) { + el_val_t name = el_get_field(expr, EL_STR("name")); + return name; + } + if (str_eq(kind, EL_STR("Not"))) { + el_val_t inner = el_get_field(expr, EL_STR("inner")); + el_val_t inner_c = cg_expr(inner); + return el_str_concat(EL_STR("!"), inner_c); + } + if (str_eq(kind, EL_STR("Neg"))) { + el_val_t inner = el_get_field(expr, EL_STR("inner")); + el_val_t inner_c = cg_expr(inner); + return el_str_concat(el_str_concat(EL_STR("(-"), inner_c), EL_STR(")")); + } + if (str_eq(kind, EL_STR("BinOp"))) { + el_val_t op = el_get_field(expr, EL_STR("op")); + el_val_t left = el_get_field(expr, EL_STR("left")); + el_val_t right = el_get_field(expr, EL_STR("right")); + el_val_t left_c = cg_expr(left); + el_val_t right_c = cg_expr(right); + el_val_t left_kind = el_get_field(left, EL_STR("expr")); + el_val_t right_kind = el_get_field(right, EL_STR("expr")); + if (str_eq(op, EL_STR("Plus"))) { + if (str_eq(left_kind, EL_STR("Str"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_str_concat("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Str"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_str_concat("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("Int"))) { + el_val_t op_c = binop_to_c(op); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" ")), op_c), EL_STR(" ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Int"))) { + el_val_t op_c = binop_to_c(op); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" ")), op_c), EL_STR(" ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("Call"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_str_concat("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Call"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_str_concat("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("BinOp"))) { + el_val_t left_op = el_get_field(left, EL_STR("op")); + if (str_eq(left_op, EL_STR("Plus"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_str_concat("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (str_eq(right_kind, EL_STR("BinOp"))) { + el_val_t right_op = el_get_field(right, EL_STR("op")); + if (str_eq(right_op, EL_STR("Plus"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_str_concat("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (str_eq(left_kind, EL_STR("Ident"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_str_concat("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Ident"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_str_concat("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (str_eq(op, EL_STR("EqEq"))) { + if (str_eq(left_kind, EL_STR("Int"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" == ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Int"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" == ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("Bool"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" == ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Bool"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" == ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("Str"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Str"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("Ident"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Ident"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("Call"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Call"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (str_eq(op, EL_STR("NotEq"))) { + if (str_eq(left_kind, EL_STR("Int"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" != ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Int"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" != ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("Bool"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" != ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Bool"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" != ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("Str"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("!str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Str"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("!str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("Ident"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("!str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Ident"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("!str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(left_kind, EL_STR("Call"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("!str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (str_eq(right_kind, EL_STR("Call"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("!str_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + el_val_t op_c = binop_to_c(op); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" ")), op_c), EL_STR(" ")), right_c), EL_STR(")")); + } + if (str_eq(kind, EL_STR("Call"))) { + el_val_t func = el_get_field(expr, EL_STR("func")); + el_val_t args = el_get_field(expr, EL_STR("args")); + el_val_t arity = native_list_len(args); + el_val_t func_kind = el_get_field(func, EL_STR("expr")); + el_val_t args_c = EL_STR(""); + el_val_t i = 0; + while (i < arity) { + el_val_t arg = native_list_get(args, i); + el_val_t arg_c = cg_expr(arg); + if (i > 0) { + args_c = el_str_concat(args_c, EL_STR(", ")); + } + args_c = el_str_concat(args_c, arg_c); + i = (i + 1); + } + if (str_eq(func_kind, EL_STR("Ident"))) { + el_val_t fn_name = el_get_field(func, EL_STR("name")); + return el_str_concat(el_str_concat(el_str_concat(fn_name, EL_STR("(")), args_c), EL_STR(")")); + } + if (str_eq(func_kind, EL_STR("Field"))) { + el_val_t obj = el_get_field(func, EL_STR("object")); + el_val_t field = el_get_field(func, EL_STR("field")); + el_val_t obj_c = cg_expr(obj); + if (arity > 0) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(field, EL_STR("(")), obj_c), EL_STR(", ")), args_c), EL_STR(")")); + } + return el_str_concat(el_str_concat(el_str_concat(field, EL_STR("(")), obj_c), EL_STR(")")); + } + el_val_t fn_c = cg_expr(func); + return el_str_concat(el_str_concat(el_str_concat(fn_c, EL_STR("(")), args_c), EL_STR(")")); + } + if (str_eq(kind, EL_STR("Field"))) { + el_val_t obj = el_get_field(expr, EL_STR("object")); + el_val_t field = el_get_field(expr, EL_STR("field")); + el_val_t obj_c = cg_expr(obj); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_get_field("), obj_c), EL_STR(", ")), c_str_lit(field)), EL_STR(")")); + } + if (str_eq(kind, EL_STR("Index"))) { + el_val_t obj = el_get_field(expr, EL_STR("object")); + el_val_t idx = el_get_field(expr, EL_STR("index")); + el_val_t obj_c = cg_expr(obj); + el_val_t idx_c = cg_expr(idx); + el_val_t idx_kind = el_get_field(idx, EL_STR("expr")); + if (str_eq(idx_kind, EL_STR("Str"))) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_get_field("), obj_c), EL_STR(", ")), idx_c), EL_STR(")")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_list_get("), obj_c), EL_STR(", ")), idx_c), EL_STR(")")); + } + if (str_eq(kind, EL_STR("Array"))) { + el_val_t elems = el_get_field(expr, EL_STR("elems")); + el_val_t n = native_list_len(elems); + el_val_t items = EL_STR(""); + el_val_t i = 0; + while (i < n) { + el_val_t elem = native_list_get(elems, i); + el_val_t elem_c = cg_expr(elem); + if (i > 0) { + items = el_str_concat(items, EL_STR(", ")); + } + items = el_str_concat(items, elem_c); + i = (i + 1); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_list_new("), native_int_to_str(n)), EL_STR(", ")), items), EL_STR(")")); + } + if (str_eq(kind, EL_STR("Map"))) { + el_val_t pairs = el_get_field(expr, EL_STR("pairs")); + el_val_t n = native_list_len(pairs); + el_val_t items = EL_STR(""); + el_val_t i = 0; + while (i < n) { + el_val_t pair = native_list_get(pairs, i); + el_val_t key = el_get_field(pair, EL_STR("key")); + el_val_t val = el_get_field(pair, EL_STR("value")); + el_val_t val_c = cg_expr(val); + if (i > 0) { + items = el_str_concat(items, EL_STR(", ")); + } + items = el_str_concat(el_str_concat(el_str_concat(items, c_str_lit(key)), EL_STR(", ")), val_c); + i = (i + 1); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_map_new("), native_int_to_str(n)), EL_STR(", ")), items), EL_STR(")")); + } + if (str_eq(kind, EL_STR("Try"))) { + el_val_t inner = el_get_field(expr, EL_STR("inner")); + return cg_expr(inner); + } + if (str_eq(kind, EL_STR("If"))) { + el_val_t cond = el_get_field(expr, EL_STR("cond")); + el_val_t cond_c = cg_expr(cond); + return el_str_concat(el_str_concat(EL_STR("/* if-expr */ (("), cond_c), EL_STR(") ? (el_val_t)1 : (el_val_t)0)")); + } + return EL_STR("EL_NULL"); + return 0; +} + +el_val_t list_contains(el_val_t lst, el_val_t s) { + el_val_t n = native_list_len(lst); + el_val_t i = 0; + while (i < n) { + el_val_t item = native_list_get(lst, i); + if (str_eq(item, s)) { + return 1; + } + i = (i + 1); + } + return 0; + return 0; +} + +el_val_t cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared) { + el_val_t kind = el_get_field(stmt, EL_STR("stmt")); + if (str_eq(kind, EL_STR("Let"))) { + el_val_t name = el_get_field(stmt, EL_STR("name")); + el_val_t val = el_get_field(stmt, EL_STR("value")); + el_val_t val_c = cg_expr(val); + if (list_contains(declared, name)) { + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, name), EL_STR(" = ")), val_c), EL_STR(";"))); + return declared; + } else { + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("el_val_t ")), name), EL_STR(" = ")), val_c), EL_STR(";"))); + return native_list_append(declared, name); + } + } + if (str_eq(kind, EL_STR("Return"))) { + el_val_t val = el_get_field(stmt, EL_STR("value")); + el_val_t val_kind = el_get_field(val, EL_STR("expr")); + if (str_eq(val_kind, EL_STR("Nil"))) { + emit_line(el_str_concat(indent, EL_STR("return 0;"))); + } else { + el_val_t val_c = cg_expr(val); + emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("return ")), val_c), EL_STR(";"))); + } + return declared; + } + if (str_eq(kind, EL_STR("Expr"))) { + el_val_t val = el_get_field(stmt, EL_STR("value")); + el_val_t val_kind = el_get_field(val, EL_STR("expr")); + if (str_eq(val_kind, EL_STR("If"))) { + cg_if_stmt(val, indent, declared); + return declared; + } + if (str_eq(val_kind, EL_STR("For"))) { + cg_for_stmt(val, indent, declared); + return declared; + } + el_val_t val_c = cg_expr(val); + emit_line(el_str_concat(el_str_concat(indent, val_c), EL_STR(";"))); + return declared; + } + if (str_eq(kind, EL_STR("While"))) { + el_val_t cond = el_get_field(stmt, EL_STR("cond")); + el_val_t body = el_get_field(stmt, EL_STR("body")); + el_val_t cond_c = cg_expr(cond); + cond_c = strip_outer_parens(cond_c); + emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("while (")), cond_c), EL_STR(") {"))); + cg_stmts(body, el_str_concat(indent, EL_STR(" ")), declared); + emit_line(el_str_concat(indent, EL_STR("}"))); + return declared; + } + if (str_eq(kind, EL_STR("For"))) { + el_val_t item = el_get_field(stmt, EL_STR("item")); + el_val_t list_expr = el_get_field(stmt, EL_STR("list")); + el_val_t body = el_get_field(stmt, EL_STR("body")); + cg_for_body(item, list_expr, body, indent, declared); + return declared; + } + if (str_eq(kind, EL_STR("FnDef"))) { + return declared; + } + if (str_eq(kind, EL_STR("TypeDef"))) { + return declared; + } + if (str_eq(kind, EL_STR("EnumDef"))) { + return declared; + } + if (str_eq(kind, EL_STR("Import"))) { + return declared; + } + return declared; + return 0; +} + +el_val_t strip_outer_parens(el_val_t s) { + el_val_t chars = native_string_chars(s); + el_val_t n = native_list_len(chars); + if (n < 2) { + return s; + } + el_val_t first = native_list_get(chars, 0); + el_val_t last = native_list_get(chars, (n - 1)); + if (str_eq(first, EL_STR("("))) { + if (str_eq(last, EL_STR(")"))) { + el_val_t depth = 1; + el_val_t i = 1; + el_val_t balanced = 1; + while (i < (n - 1)) { + el_val_t ch = native_list_get(chars, i); + if (str_eq(ch, EL_STR("("))) { + depth = (depth + 1); + } + if (str_eq(ch, EL_STR(")"))) { + depth = (depth - 1); + if (depth == 0) { + balanced = 0; + i = n; + } + } + i = (i + 1); + } + if (balanced) { + el_val_t inner = EL_STR(""); + el_val_t j = 1; + while (j < (n - 1)) { + el_val_t ch = native_list_get(chars, j); + inner = el_str_concat(inner, ch); + j = (j + 1); + } + return inner; + } + } + } + return s; + return 0; +} + +el_val_t cg_if_stmt(el_val_t expr, el_val_t indent, el_val_t declared) { + el_val_t cond = el_get_field(expr, EL_STR("cond")); + el_val_t then_stmts = el_get_field(expr, EL_STR("then")); + el_val_t else_stmts = el_get_field(expr, EL_STR("else")); + el_val_t has_else = el_get_field(expr, EL_STR("has_else")); + el_val_t cond_c = cg_expr(cond); + cond_c = strip_outer_parens(cond_c); + emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("if (")), cond_c), EL_STR(") {"))); + cg_stmts(then_stmts, el_str_concat(indent, EL_STR(" ")), declared); + if (has_else) { + emit_line(el_str_concat(indent, EL_STR("} else {"))); + cg_stmts(else_stmts, el_str_concat(indent, EL_STR(" ")), declared); + } + emit_line(el_str_concat(indent, EL_STR("}"))); + return 0; +} + +el_val_t cg_for_body(el_val_t item, el_val_t list_expr, el_val_t body, el_val_t indent, el_val_t declared) { + el_val_t list_c = cg_expr(list_expr); + el_val_t idx = EL_STR("_el_i"); + el_val_t list_tmp = EL_STR("_el_lst"); + el_val_t len_tmp = EL_STR("_el_len"); + emit_line(el_str_concat(indent, EL_STR("{"))); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR(" el_val_t ")), list_tmp), EL_STR(" = ")), list_c), EL_STR(";"))); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR(" el_val_t ")), len_tmp), EL_STR(" = el_list_len(")), list_tmp), EL_STR(");"))); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR(" for (el_val_t ")), idx), EL_STR(" = 0; ")), idx), EL_STR(" < ")), len_tmp), EL_STR("; ")), idx), EL_STR("++) {"))); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR(" el_val_t ")), item), EL_STR(" = el_list_get(")), list_tmp), EL_STR(", ")), idx), EL_STR(");"))); + cg_stmts(body, el_str_concat(indent, EL_STR(" ")), declared); + emit_line(el_str_concat(indent, EL_STR(" }"))); + emit_line(el_str_concat(indent, EL_STR("}"))); + return 0; +} + +el_val_t cg_for_stmt(el_val_t expr, el_val_t indent, el_val_t declared) { + el_val_t item = el_get_field(expr, EL_STR("item")); + el_val_t list_expr = el_get_field(expr, EL_STR("list")); + el_val_t body = el_get_field(expr, EL_STR("body")); + cg_for_body(item, list_expr, body, indent, declared); + return 0; +} + +el_val_t cg_stmts(el_val_t stmts, el_val_t indent, el_val_t declared) { + el_val_t n = native_list_len(stmts); + el_val_t i = 0; + el_val_t decl = declared; + while (i < n) { + el_val_t stmt = native_list_get(stmts, i); + decl = cg_stmt(stmt, indent, decl); + i = (i + 1); + } + return decl; + return 0; +} + +el_val_t param_decl(el_val_t param, el_val_t idx) { + el_val_t name = el_get_field(param, EL_STR("name")); + return el_str_concat(EL_STR("el_val_t "), name); + return 0; +} + +el_val_t params_to_c(el_val_t params) { + el_val_t n = native_list_len(params); + if (n == 0) { + return EL_STR("void"); + } + el_val_t out = EL_STR(""); + el_val_t i = 0; + while (i < n) { + el_val_t param = native_list_get(params, i); + el_val_t decl = param_decl(param, i); + if (i > 0) { + out = el_str_concat(out, EL_STR(", ")); + } + out = el_str_concat(out, decl); + i = (i + 1); + } + return out; + return 0; +} + +el_val_t transform_implicit_return(el_val_t body) { + el_val_t n = native_list_len(body); + if (n == 0) { + return body; + } + el_val_t last = native_list_get(body, (n - 1)); + el_val_t last_kind = el_get_field(last, EL_STR("stmt")); + if (str_eq(last_kind, EL_STR("Expr"))) { + el_val_t val = el_get_field(last, EL_STR("value")); + el_val_t val_kind = el_get_field(val, EL_STR("expr")); + if (str_eq(val_kind, EL_STR("If"))) { + return body; + } + if (str_eq(val_kind, EL_STR("For"))) { + return body; + } + el_val_t new_body = native_list_empty(); + el_val_t i = 0; + while (i < (n - 1)) { + new_body = native_list_append(new_body, native_list_get(body, i)); + i = (i + 1); + } + el_val_t return_stmt = el_map_new(2, "stmt", EL_STR("Return"), "value", val); + new_body = native_list_append(new_body, return_stmt); + return new_body; + } + return body; + return 0; +} + +el_val_t cg_fn(el_val_t stmt) { + el_val_t fn_name = el_get_field(stmt, EL_STR("name")); + if (str_eq(fn_name, EL_STR("main"))) { + return 0; + } + el_val_t params = el_get_field(stmt, EL_STR("params")); + el_val_t body = el_get_field(stmt, EL_STR("body")); + el_val_t ret_type = el_get_field(stmt, EL_STR("ret_type")); + el_val_t params_c = params_to_c(params); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_val_t "), fn_name), EL_STR("(")), params_c), EL_STR(") {"))); + el_val_t decl = native_list_empty(); + el_val_t np = native_list_len(params); + el_val_t pi = 0; + while (pi < np) { + el_val_t param = native_list_get(params, pi); + el_val_t pname = el_get_field(param, EL_STR("name")); + decl = native_list_append(decl, pname); + pi = (pi + 1); + } + el_val_t body_xformed = body; + if (!str_eq(ret_type, EL_STR("Void"))) { + body_xformed = transform_implicit_return(body); + } + cg_stmts(body_xformed, EL_STR(" "), decl); + emit_line(EL_STR(" return 0;")); + emit_line(EL_STR("}")); + emit_blank(); + return 0; +} + +el_val_t is_fndef(el_val_t stmt) { + el_val_t kind = el_get_field(stmt, EL_STR("stmt")); + if (str_eq(kind, EL_STR("FnDef"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_top_level_decl(el_val_t stmt) { + el_val_t kind = el_get_field(stmt, EL_STR("stmt")); + if (str_eq(kind, EL_STR("TypeDef"))) { + return 1; + } + if (str_eq(kind, EL_STR("EnumDef"))) { + return 1; + } + if (str_eq(kind, EL_STR("Import"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t codegen(el_val_t stmts, el_val_t source) { + emit_line(EL_STR("#include ")); + emit_line(EL_STR("#include ")); + emit_line(EL_STR("#include \"el_runtime.h\"")); + emit_blank(); + el_val_t n = native_list_len(stmts); + el_val_t i = 0; + while (i < n) { + el_val_t stmt = native_list_get(stmts, i); + el_val_t kind = el_get_field(stmt, EL_STR("stmt")); + if (str_eq(kind, EL_STR("FnDef"))) { + el_val_t fn_name = el_get_field(stmt, EL_STR("name")); + if (!str_eq(fn_name, EL_STR("main"))) { + el_val_t params = el_get_field(stmt, EL_STR("params")); + el_val_t params_c = params_to_c(params); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_val_t "), fn_name), EL_STR("(")), params_c), EL_STR(");"))); + } + } + i = (i + 1); + } + emit_blank(); + i = 0; + while (i < n) { + el_val_t stmt = native_list_get(stmts, i); + if (is_fndef(stmt)) { + cg_fn(stmt); + } + i = (i + 1); + } + emit_line(EL_STR("int main(int argc, char** argv) {")); + emit_line(EL_STR(" el_runtime_init_args(argc, argv);")); + el_val_t main_decl = native_list_empty(); + i = 0; + while (i < n) { + el_val_t stmt = native_list_get(stmts, i); + if (is_fndef(stmt)) { + } else { + if (is_top_level_decl(stmt)) { + } else { + main_decl = cg_stmt(stmt, EL_STR(" "), main_decl); + } + } + i = (i + 1); + } + emit_line(EL_STR(" return 0;")); + emit_line(EL_STR("}")); + emit_blank(); + return EL_STR(""); + return 0; +} + +el_val_t compile(el_val_t source) { + el_val_t tokens = lex(source); + el_val_t stmts = parse(tokens); + return codegen(stmts, source); + return 0; +} + +int main(int argc, char** argv) { + el_runtime_init_args(argc, argv); + el_val_t _argv = args(); + el_val_t _src_path = native_list_get(_argv, 0); + el_val_t _source = fs_read(_src_path); + compile(_source); + return 0; +} + diff --git a/dist/platform/elc.legacy b/dist/platform/elc.legacy new file mode 100755 index 0000000000000000000000000000000000000000..b1e1a92b3357aa5004617a7fd0bfef01c2082221 GIT binary patch literal 72592 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 literal 0 HcmV?d00001 diff --git a/el-compiler/runtime/el_runtime.c b/el-compiler/runtime/el_runtime.c index b570426..8aee619 100644 --- a/el-compiler/runtime/el_runtime.c +++ b/el-compiler/runtime/el_runtime.c @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include /* ── Internal allocators ─────────────────────────────────────────────────── */ @@ -259,16 +262,25 @@ el_val_t el_list_get(el_val_t listv, el_val_t index) { } el_val_t el_list_append(el_val_t listv, el_val_t elem) { - ElList* lst = (ElList*)(uintptr_t)listv; - if (!lst) { lst = list_alloc(4); } - if (lst->length >= lst->capacity) { - int64_t new_cap = lst->capacity * 2; - lst = realloc(lst, sizeof(ElList) + (size_t)(new_cap - 1) * sizeof(el_val_t)); - if (!lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } - lst->capacity = new_cap; + /* Always allocate a fresh list rather than realloc'ing the input. + * El callers commonly hold a stale pointer to the original list (e.g. + * cg_if_stmt passes `declared` to two successive cg_stmts calls; the + * first call may realloc the underlying block, leaving the second + * with a dangling pointer). Persistent allocation eliminates that + * whole class of use-after-free at modest memory cost. */ + ElList* old = (ElList*)(uintptr_t)listv; + int64_t old_len = old ? old->length : 0; + int64_t new_cap = old_len + 1; + if (new_cap < 4) new_cap = 4; + ElList* new_lst = malloc(sizeof(ElList) + (size_t)(new_cap - 1) * sizeof(el_val_t)); + if (!new_lst) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + new_lst->capacity = new_cap; + new_lst->length = old_len + 1; + if (old && old_len > 0) { + memcpy(new_lst->elems, old->elems, (size_t)old_len * sizeof(el_val_t)); } - lst->elems[lst->length++] = elem; - return EL_STR(lst); + new_lst->elems[old_len] = elem; + return EL_STR(new_lst); } /* ── Map ─────────────────────────────────────────────────────────────────── */ @@ -407,8 +419,1202 @@ el_val_t json_get(el_val_t jsonv, el_val_t keyv) { return el_wrap_str(out); } +/* ── Float bit-cast helpers ──────────────────────────────────────────────── */ + +static inline double el_to_float(el_val_t v) { + union { int64_t i; double f; } u; + u.i = (int64_t)v; + return u.f; +} + +static inline el_val_t el_from_float(double f) { + union { double f; int64_t i; } u; + u.f = f; + return (el_val_t)u.i; +} + +/* ── JSON parser (recursive descent) ─────────────────────────────────────── */ +/* + * Parsed JSON representation: + * - object -> ElMap (keys & values are el_val_t) + * - array -> ElList + * - string -> EL_STR-wrapped char* (allocated) + * - number -> int (el_val_t) if integer, otherwise el_from_float(double) + * - true -> 1 + * - false -> 0 + * - null -> EL_NULL (0) + * + * Note: there is no runtime type tag — parsed numbers cannot be + * distinguished from booleans by the runtime alone. The codegen tracks + * types separately. This matches the rest of el_val_t's type-erased model. + */ + +typedef struct { + const char* p; + const char* end; + int err; +} JsonParser; + +static void jp_skip_ws(JsonParser* jp) { + while (jp->p < jp->end) { + char c = *jp->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') jp->p++; + else break; + } +} + +static el_val_t jp_parse_value(JsonParser* jp); + +/* Parse a JSON string literal (the opening " has NOT yet been consumed). */ +static char* jp_parse_string_raw(JsonParser* jp) { + if (jp->p >= jp->end || *jp->p != '"') { jp->err = 1; return el_strdup(""); } + jp->p++; + size_t cap = 32, len = 0; + char* out = malloc(cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + while (jp->p < jp->end && *jp->p != '"') { + char c = *jp->p++; + if (c == '\\' && jp->p < jp->end) { + char esc = *jp->p++; + switch (esc) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'u': { + /* Skip 4 hex digits; emit '?' as a placeholder */ + for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++; + c = '?'; + break; + } + default: c = esc; break; + } + } + if (len + 1 >= cap) { + cap *= 2; + out = realloc(out, cap); + if (!out) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } + out[len++] = c; + } + if (jp->p < jp->end && *jp->p == '"') jp->p++; + else jp->err = 1; + out[len] = '\0'; + return out; +} + +static el_val_t jp_parse_number(JsonParser* jp) { + const char* start = jp->p; + int is_float = 0; + if (jp->p < jp->end && (*jp->p == '-' || *jp->p == '+')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + if (jp->p < jp->end && *jp->p == '.') { + is_float = 1; jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + if (jp->p < jp->end && (*jp->p == 'e' || *jp->p == 'E')) { + is_float = 1; jp->p++; + if (jp->p < jp->end && (*jp->p == '+' || *jp->p == '-')) jp->p++; + while (jp->p < jp->end && isdigit((unsigned char)*jp->p)) jp->p++; + } + size_t n = (size_t)(jp->p - start); + char buf[64]; + if (n >= sizeof(buf)) n = sizeof(buf) - 1; + memcpy(buf, start, n); + buf[n] = '\0'; + if (is_float) return el_from_float(strtod(buf, NULL)); + return (el_val_t)strtoll(buf, NULL, 10); +} + +static el_val_t jp_parse_array(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '[') jp->p++; + el_val_t lst = el_list_empty(); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ']') { jp->p++; return lst; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + lst = el_list_append(lst, v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == ']') { jp->p++; break; } + jp->err = 1; + break; + } + return lst; +} + +static el_val_t jp_parse_object(JsonParser* jp) { + if (jp->p < jp->end && *jp->p == '{') jp->p++; + el_val_t m = el_map_new(0); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == '}') { jp->p++; return m; } + while (jp->p < jp->end) { + jp_skip_ws(jp); + char* key = jp_parse_string_raw(jp); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ':') jp->p++; + else { jp->err = 1; free(key); break; } + jp_skip_ws(jp); + el_val_t v = jp_parse_value(jp); + m = el_map_set(m, EL_STR(key), v); + jp_skip_ws(jp); + if (jp->p < jp->end && *jp->p == ',') { jp->p++; continue; } + if (jp->p < jp->end && *jp->p == '}') { jp->p++; break; } + jp->err = 1; + break; + } + return m; +} + +static el_val_t jp_parse_value(JsonParser* jp) { + jp_skip_ws(jp); + if (jp->p >= jp->end) { jp->err = 1; return EL_NULL; } + char c = *jp->p; + if (c == '"') return el_wrap_str(jp_parse_string_raw(jp)); + if (c == '{') return jp_parse_object(jp); + if (c == '[') return jp_parse_array(jp); + if (c == '-' || isdigit((unsigned char)c)) return jp_parse_number(jp); + if (c == 't' && jp->p + 4 <= jp->end && strncmp(jp->p, "true", 4) == 0) { jp->p += 4; return 1; } + if (c == 'f' && jp->p + 5 <= jp->end && strncmp(jp->p, "false", 5) == 0) { jp->p += 5; return 0; } + if (c == 'n' && jp->p + 4 <= jp->end && strncmp(jp->p, "null", 4) == 0) { jp->p += 4; return EL_NULL; } + jp->err = 1; + return EL_NULL; +} + +el_val_t json_parse(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return EL_NULL; + JsonParser jp = { .p = s, .end = s + strlen(s), .err = 0 }; + el_val_t v = jp_parse_value(&jp); + if (jp.err) return EL_NULL; + return v; +} + +/* ── JSON stringify ──────────────────────────────────────────────────────── */ +/* + * Stringify policy: el_val_t is type-erased, so we cannot perfectly + * round-trip arbitrary values. We use these heuristics: + * - If value is an ElList pointer (in the heap range), serialize as array. + * - If value is an ElMap pointer, serialize as object. + * - If value looks like a printable string pointer, serialize as string. + * - Otherwise serialize as integer. + * This is best-effort. Programs that need exact control should build the + * string directly. A pointer test is the cheapest way to disambiguate + * from small integers without a separate type tag. + */ + +typedef struct { + char* buf; + size_t len; + size_t cap; +} JsonBuf; + +static void jb_init(JsonBuf* b) { + b->cap = 64; b->len = 0; + b->buf = malloc(b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + b->buf[0] = '\0'; +} + +static void jb_reserve(JsonBuf* b, size_t add) { + if (b->len + add + 1 > b->cap) { + while (b->len + add + 1 > b->cap) b->cap *= 2; + b->buf = realloc(b->buf, b->cap); + if (!b->buf) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + } +} + +static void jb_putc(JsonBuf* b, char c) { + jb_reserve(b, 1); + b->buf[b->len++] = c; + b->buf[b->len] = '\0'; +} + +static void jb_puts(JsonBuf* b, const char* s) { + size_t n = strlen(s); + jb_reserve(b, n); + memcpy(b->buf + b->len, s, n); + b->len += n; + b->buf[b->len] = '\0'; +} + +static void jb_emit_escaped(JsonBuf* b, const char* s) { + jb_putc(b, '"'); + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + switch (c) { + case '"': jb_puts(b, "\\\""); break; + case '\\': jb_puts(b, "\\\\"); break; + case '\b': jb_puts(b, "\\b"); break; + case '\f': jb_puts(b, "\\f"); break; + case '\n': jb_puts(b, "\\n"); break; + case '\r': jb_puts(b, "\\r"); break; + case '\t': jb_puts(b, "\\t"); break; + default: + if (c < 0x20) { + char tmp[8]; + snprintf(tmp, sizeof(tmp), "\\u%04x", c); + jb_puts(b, tmp); + } else { + jb_putc(b, (char)c); + } + break; + } + } + jb_putc(b, '"'); +} + +/* Heuristic: is this el_val_t likely a pointer to an ElList? + * We can't fully verify, but pointers are large addresses, integers small. + * Treat values whose magnitude exceeds 2^32 as potential pointers and + * sniff by reading the header conservatively. + * + * Simpler heuristic: if the value reads as a printable string, treat as + * string; otherwise as integer. Lists/Maps are encoded as struct pointers, + * which have leading binary bytes — so they won't look like strings. */ + +static int looks_like_string(el_val_t v) { + if (v == 0) return 0; + /* Treat plausible heap addresses as candidates */ + uintptr_t p = (uintptr_t)v; + /* Small integers (positive and negative) are not pointers */ + if ((int64_t)v >= -1000000 && (int64_t)v <= 1000000) return 0; + if (p < 0x1000) return 0; + /* Sniff first bytes for printable */ + const unsigned char* s = (const unsigned char*)p; + for (int i = 0; i < 16; i++) { + unsigned char c = s[i]; + if (c == '\0') return i > 0; /* terminated string */ + if (c < 0x09 || (c > 0x0d && c < 0x20) || c >= 0x7f) return 0; + } + return 1; /* 16+ printable bytes — call it a string */ +} + +static void jb_emit_value(JsonBuf* b, el_val_t v); + +static void jb_emit_int(JsonBuf* b, int64_t n) { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)n); + jb_puts(b, tmp); +} + +static void jb_emit_value(JsonBuf* b, el_val_t v) { + if (v == EL_NULL) { jb_puts(b, "null"); return; } + if (looks_like_string(v)) { + jb_emit_escaped(b, EL_CSTR(v)); + return; + } + jb_emit_int(b, (int64_t)v); +} + +el_val_t json_stringify(el_val_t v) { + JsonBuf b; jb_init(&b); + jb_emit_value(&b, v); + return el_wrap_str(b.buf); +} + +/* ── JSON substring accessors ────────────────────────────────────────────── */ +/* + * These walk the raw JSON string looking for "key": at the top level (depth 1) + * of an object. They handle escaped quotes, nested objects/arrays, and + * whitespace around the colon. + */ + +/* Find "key": at object-depth == 1 inside the JSON object string `s`. + * Returns pointer to the first byte of the value, or NULL. */ +static const char* json_find_key(const char* s, const char* key) { + if (!s || !key) return NULL; + size_t klen = strlen(key); + int depth = 0; + int in_str = 0; + int escape = 0; + const char* p = s; + while (*p) { + char c = *p; + if (in_str) { + if (escape) { escape = 0; } + else if (c == '\\') { escape = 1; } + else if (c == '"') { + /* End of string. If we're at depth 1, check if this was a key. */ + p++; + if (depth == 1) { + /* The string just ended at p-1. Check if it matches key + * and is followed by a colon. We need to backtrack to find + * the start of this string and compare. */ + } + in_str = 0; + continue; + } + p++; + continue; + } + if (c == '"') { + /* Start of a string literal */ + const char* str_start = p + 1; + const char* q = str_start; + int e = 0; + while (*q) { + if (e) { e = 0; q++; continue; } + if (*q == '\\') { e = 1; q++; continue; } + if (*q == '"') break; + q++; + } + size_t slen = (size_t)(q - str_start); + const char* after = (*q == '"') ? q + 1 : q; + /* If at depth 1 and matches key and followed by ':' -> got it */ + if (depth == 1 && slen == klen && strncmp(str_start, key, klen) == 0) { + const char* r = after; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + if (*r == ':') { + r++; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + return r; + } + } + p = after; + continue; + } + if (c == '{' || c == '[') depth++; + else if (c == '}' || c == ']') depth--; + p++; + } + return NULL; +} + +/* Skip a JSON value starting at p; return pointer past the value end. */ +static const char* json_skip_value(const char* p) { + if (!p || !*p) return p; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p == '"') { + p++; + int e = 0; + while (*p) { + if (e) { e = 0; p++; continue; } + if (*p == '\\') { e = 1; p++; continue; } + if (*p == '"') { p++; break; } + p++; + } + return p; + } + if (*p == '{' || *p == '[') { + char open = *p; + char close = (open == '{') ? '}' : ']'; + int depth = 0; + int in_str = 0; + int e = 0; + while (*p) { + char c = *p; + if (in_str) { + if (e) { e = 0; } + else if (c == '\\') { e = 1; } + else if (c == '"') in_str = 0; + p++; + continue; + } + if (c == '"') { in_str = 1; p++; continue; } + if (c == open) depth++; + else if (c == close) { depth--; p++; if (depth == 0) return p; continue; } + p++; + } + return p; + } + /* scalar: number, true/false/null */ + while (*p && *p != ',' && *p != '}' && *p != ']' && + *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; + return p; +} + +el_val_t json_get_string(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p || *p != '"') return el_wrap_str(el_strdup("")); + p++; + JsonParser jp = { .p = p - 1, .end = json + (json ? strlen(json) : 0), .err = 0 }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { free(parsed); return el_wrap_str(el_strdup("")); } + return el_wrap_str(parsed); +} + +el_val_t json_get_int(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return (el_val_t)strtoll(p, NULL, 10); +} + +el_val_t json_get_float(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (*p == '"' || *p == '{' || *p == '[') return 0; + return el_from_float(strtod(p, NULL)); +} + +el_val_t json_get_bool(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + if (strncmp(p, "true", 4) == 0) return 1; + return 0; +} + +el_val_t json_get_raw(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + if (!k) k = ""; + if (!json || !*json) { + /* Build a fresh object */ + JsonBuf b; jb_init(&b); + jb_putc(&b, '{'); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_emit_value(&b, value); + jb_putc(&b, '}'); + return el_wrap_str(b.buf); + } + const char* existing = json_find_key(json, k); + JsonBuf b; jb_init(&b); + if (existing) { + const char* end = json_skip_value(existing); + /* Copy [json .. existing) */ + size_t prefix = (size_t)(existing - json); + jb_reserve(&b, prefix); + memcpy(b.buf + b.len, json, prefix); + b.len += prefix; + b.buf[b.len] = '\0'; + jb_emit_value(&b, value); + jb_puts(&b, end); + return el_wrap_str(b.buf); + } + /* Insert before closing '}'. Find last '}' */ + size_t jl = strlen(json); + if (jl == 0) { free(b.buf); return el_wrap_str(el_strdup("{}")); } + /* Find last '}' from the end */ + ssize_t close_idx = -1; + for (ssize_t i = (ssize_t)jl - 1; i >= 0; i--) { + if (json[i] == '}') { close_idx = i; break; } + } + if (close_idx < 0) { + free(b.buf); + return el_wrap_str(el_strdup(json)); + } + /* Determine if object is empty: scan between last '{' and '}' for non-ws */ + int empty = 1; + for (ssize_t i = close_idx - 1; i >= 0; i--) { + char c = json[i]; + if (c == '{') break; + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { empty = 0; break; } + } + /* Copy json[0..close_idx) */ + jb_reserve(&b, (size_t)close_idx); + memcpy(b.buf + b.len, json, (size_t)close_idx); + b.len += (size_t)close_idx; + b.buf[b.len] = '\0'; + if (!empty) jb_putc(&b, ','); + jb_emit_escaped(&b, k); + jb_putc(&b, ':'); + jb_emit_value(&b, value); + /* Append from close_idx onward */ + jb_puts(&b, json + close_idx); + return el_wrap_str(b.buf); +} + +el_val_t json_array_len(el_val_t json_str) { + const char* s = EL_CSTR(json_str); + if (!s) return 0; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s != '[') return 0; + s++; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ']') return 0; + int64_t count = 0; + while (*s) { + const char* end = json_skip_value(s); + if (end == s) break; + count++; + s = end; + while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++; + if (*s == ',') { s++; continue; } + if (*s == ']' || *s == '\0') break; + } + return (el_val_t)count; +} + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void) { + struct timeval tv; + gettimeofday(&tv, NULL); + int64_t ms = (int64_t)tv.tv_sec * 1000LL + (int64_t)tv.tv_usec / 1000LL; + return (el_val_t)ms; +} + +el_val_t time_now_utc(void) { + return time_now(); +} + +el_val_t time_format(el_val_t ts, el_val_t fmt) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + const char* fmt_str = EL_CSTR(fmt); + if (!fmt_str || strcmp(fmt_str, "ISO") == 0) { + char buf[64]; + snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, msec); + return el_wrap_str(el_strdup(buf)); + } + char buf[256]; + if (strftime(buf, sizeof(buf), fmt_str, &tm) == 0) buf[0] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t time_to_parts(el_val_t ts) { + int64_t ms = (int64_t)ts; + time_t s = (time_t)(ms / 1000); + int msec = (int)(ms % 1000); + if (msec < 0) { msec += 1000; s -= 1; } + struct tm tm; + gmtime_r(&s, &tm); + el_val_t m = el_map_new(0); + m = el_map_set(m, EL_STR(el_strdup("year")), (el_val_t)(tm.tm_year + 1900)); + m = el_map_set(m, EL_STR(el_strdup("month")), (el_val_t)(tm.tm_mon + 1)); + m = el_map_set(m, EL_STR(el_strdup("day")), (el_val_t)tm.tm_mday); + m = el_map_set(m, EL_STR(el_strdup("hour")), (el_val_t)tm.tm_hour); + m = el_map_set(m, EL_STR(el_strdup("minute")), (el_val_t)tm.tm_min); + m = el_map_set(m, EL_STR(el_strdup("second")), (el_val_t)tm.tm_sec); + m = el_map_set(m, EL_STR(el_strdup("ms")), (el_val_t)msec); + return m; +} + +el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz) { + (void)tz; + int64_t s = (int64_t)secs; + int64_t n = (int64_t)ns; + int64_t ms = s * 1000LL + n / 1000000LL; + return (el_val_t)ms; +} + +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) { + const char* u = EL_CSTR(unit); + int64_t cur = (int64_t)ts; + int64_t d = (int64_t)n; + int64_t add_ms = d; + if (u) { + if (strcmp(u, "ms") == 0) add_ms = d; + else if (strcmp(u, "sec") == 0) add_ms = d * 1000LL; + else if (strcmp(u, "min") == 0) add_ms = d * 60000LL; + else if (strcmp(u, "hour") == 0) add_ms = d * 3600000LL; + else if (strcmp(u, "day") == 0) add_ms = d * 86400000LL; + } + return (el_val_t)(cur + add_ms); +} + +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit) { + int64_t d = (int64_t)ts2 - (int64_t)ts1; + const char* u = EL_CSTR(unit); + if (!u || strcmp(u, "ms") == 0) return (el_val_t)d; + if (strcmp(u, "sec") == 0) return (el_val_t)(d / 1000LL); + if (strcmp(u, "min") == 0) return (el_val_t)(d / 60000LL); + if (strcmp(u, "hour") == 0) return (el_val_t)(d / 3600000LL); + if (strcmp(u, "day") == 0) return (el_val_t)(d / 86400000LL); + return (el_val_t)d; +} + +/* ── UUID v4 ─────────────────────────────────────────────────────────────── */ + +static int _el_uuid_seeded = 0; + +static void _el_uuid_seed(void) { + if (!_el_uuid_seeded) { + srand((unsigned)time(NULL) ^ (unsigned)(uintptr_t)&_el_uuid_seeded); + _el_uuid_seeded = 1; + } +} + +el_val_t uuid_new(void) { + _el_uuid_seed(); + unsigned char b[16]; + for (int i = 0; i < 16; i++) b[i] = (unsigned char)(rand() & 0xff); + /* Version 4 */ + b[6] = (b[6] & 0x0f) | 0x40; + /* RFC 4122 variant */ + b[8] = (b[8] & 0x3f) | 0x80; + char buf[37]; + snprintf(buf, sizeof(buf), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], + b[4], b[5], + b[6], b[7], + b[8], b[9], + b[10], b[11], b[12], b[13], b[14], b[15]); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t uuid_v4(void) { return uuid_new(); } + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + const char* v = getenv(k); + return el_wrap_str(el_strdup(v ? v : "")); +} + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +typedef struct { + char* key; + char* value; +} StateEntry; + +static StateEntry* _state_entries = NULL; +static size_t _state_count = 0; +static size_t _state_cap = 0; + +static StateEntry* state_find(const char* key) { + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, key) == 0) return &_state_entries[i]; + } + return NULL; +} + +el_val_t state_set(el_val_t key, el_val_t value) { + const char* k = EL_CSTR(key); + const char* v = EL_CSTR(value); + if (!k) return 0; + if (!v) v = ""; + StateEntry* e = state_find(k); + if (e) { + free(e->value); + e->value = el_strdup(v); + return 1; + } + if (_state_count >= _state_cap) { + size_t nc = _state_cap == 0 ? 16 : _state_cap * 2; + _state_entries = realloc(_state_entries, nc * sizeof(StateEntry)); + if (!_state_entries) { fputs("el_runtime: out of memory\n", stderr); exit(1); } + _state_cap = nc; + } + _state_entries[_state_count].key = el_strdup(k); + _state_entries[_state_count].value = el_strdup(v); + _state_count++; + return 1; +} + +el_val_t state_get(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + StateEntry* e = state_find(k); + return el_wrap_str(el_strdup(e ? e->value : "")); +} + +el_val_t state_del(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return 0; + for (size_t i = 0; i < _state_count; i++) { + if (strcmp(_state_entries[i].key, k) == 0) { + free(_state_entries[i].key); + free(_state_entries[i].value); + for (size_t j = i + 1; j < _state_count; j++) { + _state_entries[j - 1] = _state_entries[j]; + } + _state_count--; + return 1; + } + } + return 1; +} + +el_val_t state_keys(void) { + el_val_t lst = el_list_empty(); + for (size_t i = 0; i < _state_count; i++) { + lst = el_list_append(lst, el_wrap_str(el_strdup(_state_entries[i].key))); + } + return lst; +} + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t int_to_float(el_val_t n) { + return el_from_float((double)(int64_t)n); +} + +el_val_t float_to_int(el_val_t f) { + return (el_val_t)(int64_t)el_to_float(f); +} + +el_val_t format_float(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 30) d = 30; + char buf[128]; + snprintf(buf, sizeof(buf), "%.*f", d, el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t decimal_round(el_val_t f, el_val_t decimals) { + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; + if (d > 15) d = 15; + double mul = pow(10.0, (double)d); + double v = el_to_float(f); + double r = (v >= 0.0 ? floor(v * mul + 0.5) : -floor(-v * mul + 0.5)) / mul; + return el_from_float(r); +} + +el_val_t str_to_float(el_val_t s) { + const char* str = EL_CSTR(s); + if (!str) return el_from_float(0.0); + return el_from_float(strtod(str, NULL)); +} + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t math_log(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t math_pi(void) { return el_from_float(3.141592653589793238462643383279502884); } + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub) { + const char* str = EL_CSTR(s); + const char* sb = EL_CSTR(sub); + if (!str || !sb) return -1; + const char* hit = strstr(str, sb); + if (!hit) return -1; + return (el_val_t)(int64_t)(hit - str); +} + +el_val_t str_split(el_val_t s, el_val_t sep) { + const char* str = EL_CSTR(s); + const char* sp = EL_CSTR(sep); + el_val_t lst = el_list_empty(); + if (!str) return lst; + if (!sp || !*sp) { + lst = el_list_append(lst, el_wrap_str(el_strdup(str))); + return lst; + } + size_t lp = strlen(sp); + const char* p = str; + const char* hit; + while ((hit = strstr(p, sp)) != NULL) { + size_t n = (size_t)(hit - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + lst = el_list_append(lst, el_wrap_str(out)); + p = hit + lp; + } + lst = el_list_append(lst, el_wrap_str(el_strdup(p))); + return lst; +} + +el_val_t str_char_at(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return el_wrap_str(el_strdup("")); + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return el_wrap_str(el_strdup("")); + char buf[2]; + buf[0] = str[idx]; + buf[1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_char_code(el_val_t s, el_val_t i) { + const char* str = EL_CSTR(s); + int64_t idx = (int64_t)i; + if (!str) return 0; + int64_t n = (int64_t)strlen(str); + if (idx < 0 || idx >= n) return 0; + return (el_val_t)(unsigned char)str[idx]; +} + +static el_val_t str_pad(const char* s, int64_t width, const char* pad, int left) { + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t lp = (int64_t)strlen(pad); + int64_t ls = (int64_t)strlen(s); + if (ls >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - ls; + char* out = el_strbuf((size_t)width); + if (left) { + for (int64_t i = 0; i < need; i++) out[i] = pad[i % lp]; + memcpy(out + need, s, (size_t)ls); + } else { + memcpy(out, s, (size_t)ls); + for (int64_t i = 0; i < need; i++) out[ls + i] = pad[i % lp]; + } + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 1); +} + +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad) { + return str_pad(EL_CSTR(s), (int64_t)width, EL_CSTR(pad), 0); +} + +el_val_t str_format(el_val_t template, el_val_t data) { + const char* tpl = EL_CSTR(template); + if (!tpl) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + const char* p = tpl; + while (*p) { + if (*p == '{') { + const char* q = p + 1; + while (*q && *q != '}') q++; + if (*q == '}') { + size_t klen = (size_t)(q - p - 1); + char keybuf[256]; + if (klen < sizeof(keybuf)) { + memcpy(keybuf, p + 1, klen); + keybuf[klen] = '\0'; + el_val_t v = el_map_get(data, EL_STR(keybuf)); + if (v != 0 && looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + p = q + 1; + continue; + } else if (v != 0) { + jb_emit_int(&b, (int64_t)v); + p = q + 1; + continue; + } + } + /* Unknown key — leave {key} verbatim */ + jb_reserve(&b, klen + 2); + memcpy(b.buf + b.len, p, klen + 2); + b.len += klen + 2; + b.buf[b.len] = '\0'; + p = q + 1; + continue; + } + } + jb_putc(&b, *p); + p++; + } + return el_wrap_str(b.buf); +} + +el_val_t str_lower(el_val_t s) { return str_to_lower(s); } +el_val_t str_upper(el_val_t s) { return str_to_upper(s); } + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t list_push_front(el_val_t listv, el_val_t elem) { + ElList* lst = (ElList*)(uintptr_t)listv; + if (!lst) { + el_val_t nl = el_list_empty(); + return el_list_append(nl, elem); + } + /* Append to grow capacity, then shift right */ + listv = el_list_append(listv, elem); + lst = (ElList*)(uintptr_t)listv; + for (int64_t i = lst->length - 1; i > 0; i--) { + lst->elems[i] = lst->elems[i - 1]; + } + lst->elems[0] = elem; + return EL_STR(lst); +} + +el_val_t list_join(el_val_t listv, el_val_t sep) { + ElList* lst = (ElList*)(uintptr_t)listv; + const char* sp = EL_CSTR(sep); + if (!sp) sp = ""; + if (!lst || lst->length == 0) return el_wrap_str(el_strdup("")); + JsonBuf b; jb_init(&b); + for (int64_t i = 0; i < lst->length; i++) { + if (i > 0) jb_puts(&b, sp); + el_val_t v = lst->elems[i]; + if (v == 0) continue; + if (looks_like_string(v)) { + jb_puts(&b, EL_CSTR(v)); + } else { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%lld", (long long)v); + jb_puts(&b, tmp); + } + } + return el_wrap_str(b.buf); +} + +el_val_t list_range(el_val_t start, el_val_t end) { + int64_t a = (int64_t)start; + int64_t b = (int64_t)end; + el_val_t lst = el_list_empty(); + for (int64_t i = a; i < b; i++) lst = el_list_append(lst, (el_val_t)i); + return lst; +} + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b) { + return el_wrap_str(el_strdup(b ? "true" : "false")); +} + /* ── Process ─────────────────────────────────────────────────────────────── */ void exit_program(el_val_t code) { exit((int)code); } + +/* ── args() — command-line argument access ────────────────────────────────── + * Compiled El programs call args() to get a list of CLI arguments. + * Call el_runtime_init_args(argc, argv) at the start of C main() to populate. + * The args list excludes argv[0] (the program name). */ + +static el_val_t _el_args_list = 0; + +void el_runtime_init_args(int argc, char** argv) { + _el_args_list = el_list_empty(); + for (int i = 1; i < argc; i++) { + _el_args_list = el_list_append(_el_args_list, EL_STR(argv[i])); + } +} + +el_val_t args(void) { + if (!_el_args_list) _el_args_list = el_list_empty(); + return _el_args_list; +} + +/* ── CGI identity ──────────────────────────────────────────────────────────── + * Called once at program start by the generated main() of a cgi {} program. + * Stores CGI identity so dharma_* builtins can reference it. */ + +static const char* _el_cgi_name = NULL; +static const char* _el_cgi_dharma_id = NULL; +static const char* _el_cgi_principal = NULL; +static const char* _el_cgi_network = NULL; +static const char* _el_cgi_engram = NULL; + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram) { + _el_cgi_name = EL_CSTR(name); + _el_cgi_dharma_id = EL_CSTR(dharma_id); + _el_cgi_principal = EL_CSTR(principal); + _el_cgi_network = EL_CSTR(network) ? EL_CSTR(network) : "dharma-mainnet"; + _el_cgi_engram = EL_CSTR(engram) ? EL_CSTR(engram) : "http://localhost:8742"; + printf("[cgi] identity: name=%s dharma_id=%s principal=%s network=%s engram=%s\n", + _el_cgi_name ? _el_cgi_name : "(unset)", + _el_cgi_dharma_id ? _el_cgi_dharma_id : "(unset)", + _el_cgi_principal ? _el_cgi_principal : "(unset)", + _el_cgi_network, + _el_cgi_engram); +} + +/* ── DHARMA network stubs ─────────────────────────────────────────────────── + * Stub implementations for all dharma_* and engram_* builtins. + * Each stub prints a descriptive line to stdout so calls are visible in tests. + * Full implementations are provided by the DHARMA runtime linked at deploy. */ + +el_val_t dharma_connect(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id) id = "(null)"; + char buf[256]; + snprintf(buf, sizeof(buf), "[dharma] connect: %s", id); + puts(buf); + /* Return a synthetic channel ID of the form "ch:" */ + char ch[272]; + snprintf(ch, sizeof(ch), "ch:%s", id); + return el_wrap_str(el_strdup(ch)); +} + +el_val_t dharma_send(el_val_t channel, el_val_t content) { + const char* ch = EL_CSTR(channel); + const char* msg = EL_CSTR(content); + if (!ch) ch = "(null)"; + if (!msg) msg = "(null)"; + char buf[1024]; + snprintf(buf, sizeof(buf), "[dharma] send on %s: %s", ch, msg); + puts(buf); + return el_wrap_str(el_strdup("")); +} + +el_val_t dharma_activate(el_val_t query) { + const char* q = EL_CSTR(query); + if (!q) q = "(null)"; + char buf[512]; + snprintf(buf, sizeof(buf), "[dharma] activate: %s", q); + puts(buf); + return el_list_empty(); +} + +void dharma_emit(el_val_t event_type, el_val_t payload) { + const char* et = EL_CSTR(event_type); + const char* pay = EL_CSTR(payload); + if (!et) et = "(null)"; + if (!pay) pay = "(null)"; + char buf[1024]; + snprintf(buf, sizeof(buf), "[dharma] emit: %s %s", et, pay); + puts(buf); +} + +el_val_t dharma_field(el_val_t event_type) { + const char* et = EL_CSTR(event_type); + if (!et) et = "(null)"; + char buf[512]; + snprintf(buf, sizeof(buf), "[dharma] field: %s", et); + puts(buf); + return el_map_new(0); +} + +void dharma_strengthen(el_val_t cgi_id, el_val_t weight) { + const char* id = EL_CSTR(cgi_id); + if (!id) id = "(null)"; + /* weight is encoded as el_val_t; print as integer (float encoding TBD) */ + char buf[256]; + snprintf(buf, sizeof(buf), "[dharma] strengthen: %s +%lld", id, (long long)weight); + puts(buf); +} + +el_val_t dharma_relationship(el_val_t cgi_id) { + const char* id = EL_CSTR(cgi_id); + if (!id) id = "(null)"; + char buf[256]; + snprintf(buf, sizeof(buf), "[dharma] relationship: %s", id); + puts(buf); + return 0; /* 0.0 — no prior relationship in stub mode */ +} + +el_val_t dharma_peers(void) { + puts("[dharma] peers"); + return el_list_empty(); +} + +/* ── Engram local graph stubs ────────────────────────────────────────────── */ + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience) { + const char* c = EL_CSTR(content); + const char* nt = EL_CSTR(node_type); + if (!c) c = "(null)"; + if (!nt) nt = "(null)"; + char buf[512]; + snprintf(buf, sizeof(buf), "[engram] node: %s (type=%s salience=%lld)", c, nt, (long long)salience); + puts(buf); + return el_wrap_str(el_strdup("stub-node-id")); +} + +el_val_t engram_activate(el_val_t query, el_val_t depth) { + const char* q = EL_CSTR(query); + if (!q) q = "(null)"; + char buf[512]; + snprintf(buf, sizeof(buf), "[engram] activate: %s depth=%lld", q, (long long)depth); + puts(buf); + return el_list_empty(); +} + +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation) { + const char* f = EL_CSTR(from_id); + const char* t = EL_CSTR(to_id); + const char* r = EL_CSTR(relation); + if (!f) f = "(null)"; + if (!t) t = "(null)"; + if (!r) r = "(null)"; + char buf[512]; + snprintf(buf, sizeof(buf), "[engram] connect: %s -[%s]-> %s weight=%lld", f, r, t, (long long)weight); + puts(buf); +} + +void engram_strengthen(el_val_t node_id) { + const char* id = EL_CSTR(node_id); + if (!id) id = "(null)"; + char buf[256]; + snprintf(buf, sizeof(buf), "[engram] strengthen: %s", id); + puts(buf); +} + +/* ── Native VM builtin aliases ────────────────────────────────────────────── + * El source files use native_* names (El VM builtins). + * When compiled to C, these map directly to el_* runtime functions. */ + +el_val_t native_list_get(el_val_t list, el_val_t index) { + return el_list_get(list, index); +} + +el_val_t native_list_len(el_val_t list) { + return el_list_len(list); +} + +el_val_t native_list_append(el_val_t list, el_val_t elem) { + return el_list_append(list, elem); +} + +el_val_t native_list_empty(void) { + return el_list_empty(); +} + +el_val_t native_string_chars(el_val_t sv) { + const char* s = EL_CSTR(sv); + el_val_t result = el_list_empty(); + if (!s) return result; + while (*s) { + char buf[2]; + buf[0] = *s; + buf[1] = '\0'; + result = el_list_append(result, EL_STR(strdup(buf))); + s++; + } + return result; +} + +el_val_t native_int_to_str(el_val_t n) { + return int_to_str(n); +} + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * Short names that result from the method-call convention: + * myList.append(x) → append(myList, x) + * myList.len() → len(myList) + * myList.get(i) → get(myList, i) + * myMap.map_get(k) → map_get(myMap, k) + * myMap.map_set(k,v) → map_set(myMap, k, v) */ + +el_val_t append(el_val_t list, el_val_t elem) { return el_list_append(list, elem); } +el_val_t len(el_val_t list) { return el_list_len(list); } +el_val_t get(el_val_t list, el_val_t index) { return el_list_get(list, index); } +el_val_t map_get(el_val_t map, el_val_t key) { return el_map_get(map, key); } +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value) { return el_map_set(map, key, value); } diff --git a/el-compiler/runtime/el_runtime.h b/el-compiler/runtime/el_runtime.h index 51d409c..acfa263 100644 --- a/el-compiler/runtime/el_runtime.h +++ b/el-compiler/runtime/el_runtime.h @@ -98,11 +98,150 @@ el_val_t fs_write(el_val_t path, el_val_t content); /* ── JSON ────────────────────────────────────────────────────────────────── */ el_val_t json_get(el_val_t json, el_val_t key); +el_val_t json_parse(el_val_t s); +el_val_t json_stringify(el_val_t v); +el_val_t json_get_string(el_val_t json_str, el_val_t key); +el_val_t json_get_int(el_val_t json_str, el_val_t key); +el_val_t json_get_float(el_val_t json_str, el_val_t key); +el_val_t json_get_bool(el_val_t json_str, el_val_t key); +el_val_t json_get_raw(el_val_t json_str, el_val_t key); +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value); +el_val_t json_array_len(el_val_t json_str); + +/* ── Time ────────────────────────────────────────────────────────────────── */ + +el_val_t time_now(void); +el_val_t time_now_utc(void); +el_val_t time_format(el_val_t ts, el_val_t fmt); +el_val_t time_to_parts(el_val_t ts); +el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz); +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit); +el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit); + +/* ── UUID ────────────────────────────────────────────────────────────────── */ + +el_val_t uuid_new(void); +el_val_t uuid_v4(void); + +/* ── Environment ─────────────────────────────────────────────────────────── */ + +el_val_t env(el_val_t key); + +/* ── In-process state K/V ────────────────────────────────────────────────── */ + +el_val_t state_set(el_val_t key, el_val_t value); +el_val_t state_get(el_val_t key); +el_val_t state_del(el_val_t key); +el_val_t state_keys(void); + +/* ── Float formatting ────────────────────────────────────────────────────── */ + +el_val_t float_to_str(el_val_t f); +el_val_t int_to_float(el_val_t n); +el_val_t float_to_int(el_val_t f); +el_val_t format_float(el_val_t f, el_val_t decimals); +el_val_t decimal_round(el_val_t f, el_val_t decimals); +el_val_t str_to_float(el_val_t s); + +/* ── Math (Float-aware) ──────────────────────────────────────────────────── */ + +el_val_t math_sqrt(el_val_t f); +el_val_t math_log(el_val_t f); +el_val_t math_ln(el_val_t f); +el_val_t math_sin(el_val_t f); +el_val_t math_cos(el_val_t f); +el_val_t math_pi(void); + +/* ── String additions ────────────────────────────────────────────────────── */ + +el_val_t str_index_of(el_val_t s, el_val_t sub); +el_val_t str_split(el_val_t s, el_val_t sep); +el_val_t str_char_at(el_val_t s, el_val_t i); +el_val_t str_char_code(el_val_t s, el_val_t i); +el_val_t str_pad_left(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_pad_right(el_val_t s, el_val_t width, el_val_t pad); +el_val_t str_format(el_val_t template, el_val_t data); +el_val_t str_lower(el_val_t s); +el_val_t str_upper(el_val_t s); + +/* ── List additions ──────────────────────────────────────────────────────── */ + +el_val_t list_push(el_val_t list, el_val_t elem); +el_val_t list_push_front(el_val_t list, el_val_t elem); +el_val_t list_join(el_val_t list, el_val_t sep); +el_val_t list_range(el_val_t start, el_val_t end); + +/* ── Bool helpers ────────────────────────────────────────────────────────── */ + +el_val_t bool_to_str(el_val_t b); /* ── Process ─────────────────────────────────────────────────────────────── */ void exit_program(el_val_t code); +/* ── CGI identity ───────────────────────────────────────────────────────────── + * Called at the start of main() in CGI programs (those with a `cgi {}` block). + * Records the program's DHARMA identity before any other code executes. */ + +void el_cgi_init(el_val_t name, el_val_t dharma_id, el_val_t principal, + el_val_t network, el_val_t engram); + +/* ── DHARMA network builtins ───────────────────────────────────────────────── + * Available to CGI programs (declared with a `cgi {}` block). + * Stubs print descriptive output and return empty/zero values. + * Full implementations are linked from the DHARMA runtime at deploy time. */ + +el_val_t dharma_connect(el_val_t cgi_id); +el_val_t dharma_send(el_val_t channel, el_val_t content); +el_val_t dharma_activate(el_val_t query); +void dharma_emit(el_val_t event_type, el_val_t payload); +el_val_t dharma_field(el_val_t event_type); +void dharma_strengthen(el_val_t cgi_id, el_val_t weight); +el_val_t dharma_relationship(el_val_t cgi_id); +el_val_t dharma_peers(void); + +/* ── Engram local graph primitives ─────────────────────────────────────────── + * Operate on the CGI's local Engram knowledge graph. + * `engram_activate` queries the local graph only; `dharma_activate` is + * network-wide across all connected CGI graphs. */ + +el_val_t engram_node(el_val_t content, el_val_t node_type, el_val_t salience); +el_val_t engram_activate(el_val_t query, el_val_t depth); +void engram_connect(el_val_t from_id, el_val_t to_id, el_val_t weight, el_val_t relation); +void engram_strengthen(el_val_t node_id); + +/* ── args() ───────────────────────────────────────────────────────────────── + * Provides access to command-line arguments passed to the program. + * Populated by el_runtime_init_args() before main() runs. */ + +el_val_t args(void); +void el_runtime_init_args(int argc, char** argv); + +/* ── Native VM builtin aliases (for compiled El source) ───────────────────── + * These match the El VM's native_* builtins so that El source compiled + * to C can call the same names without modification. */ + +el_val_t native_list_get(el_val_t list, el_val_t index); +el_val_t native_list_len(el_val_t list); +el_val_t native_list_append(el_val_t list, el_val_t elem); +el_val_t native_list_empty(void); +el_val_t native_string_chars(el_val_t s); +el_val_t native_int_to_str(el_val_t n); + +/* ── Method-call shorthand aliases ────────────────────────────────────────── + * The El method-call convention `obj.method(args)` compiles to + * `method(obj, args)`. These aliases expose the runtime functions under + * the short names that result from method calls in El source. + * + * Example: `myList.append(x)` → `append(myList, x)` (calls this alias) + * `myList.len()` → `len(myList)` (calls this alias) */ + +el_val_t append(el_val_t list, el_val_t elem); /* el_list_append */ +el_val_t len(el_val_t list); /* el_list_len */ +el_val_t get(el_val_t list, el_val_t index); /* el_list_get */ +el_val_t map_get(el_val_t map, el_val_t key); /* el_map_get */ +el_val_t map_set(el_val_t map, el_val_t key, el_val_t value); /* el_map_set */ + #ifdef __cplusplus } #endif diff --git a/el-compiler/src/codegen.el b/el-compiler/src/codegen.el index 41ca592..4292cbc 100644 --- a/el-compiler/src/codegen.el +++ b/el-compiler/src/codegen.el @@ -1,12 +1,16 @@ // codegen.el — El compiler C source code generator // // Input: list of AST statement maps (from parser.el) -// Output: C source string +// 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 ──────────────────────────────────────────────────────────── @@ -50,9 +54,6 @@ fn c_str_lit(s: String) -> String { // ── Type mapping ────────────────────────────────────────────────────────────── -// Map El type annotation strings to C types. -// type_str is whatever appeared after ":" in El source — we only recognise -// the core types; everything else falls back to void*. fn el_type_to_c(type_str: String) -> String { if type_str == "String" { return "const char*" } if type_str == "Int" { return "int64_t" } @@ -60,21 +61,20 @@ fn el_type_to_c(type_str: String) -> String { if type_str == "Float" { return "double" } if type_str == "Void" { return "void" } if type_str == "void" { return "void" } - // Any, Map, list types, unknown → void* "void*" } -// ── Code buffer ─────────────────────────────────────────────────────────────── +// ── Code emission ───────────────────────────────────────────────────────────── // -// We accumulate output lines into the VM's native instruction buffer -// (repurposed as a line buffer). Each "instruction" is a line of C text. +// emit_line/emit_blank stream output directly via println. +// This avoids building a large string in memory. fn emit_line(line: String) -> Void { - native_instr_push(line) + println(line) } fn emit_blank() -> Void { - native_instr_push("") + println("") } // ── Operator helpers ────────────────────────────────────────────────────────── @@ -95,14 +95,6 @@ fn binop_to_c(op: String) -> String { op } -// ── Unique label generator ───────────────────────────────────────────────── - -// We use the native instruction counter (length before emitting) as a unique -// monotone id so that nested if/while labels don't collide. -fn unique_id() -> Int { - native_instr_len() -} - // ── Expression codegen ──────────────────────────────────────────────────────── // // cg_expr returns a C expression string (not a statement). @@ -122,7 +114,6 @@ fn cg_expr(expr: Map) -> String { if kind == "Str" { let v: String = expr["value"] - // String literals are wrapped in EL_STR() to convert const char* to el_val_t return "EL_STR(" + c_str_lit(v) + ")" } @@ -159,37 +150,123 @@ fn cg_expr(expr: Map) -> String { let right = expr["right"] let left_c: String = cg_expr(left) let right_c: String = cg_expr(right) - // String concatenation: El uses + for strings — map to el_str_concat + let left_kind: String = left["expr"] + let right_kind: String = right["expr"] + if op == "Plus" { - // We can't easily detect types here, so we rely on the user - // calling str_concat() explicitly for strings, or we emit - // el_str_concat when either operand looks like a string expression. - // Heuristic: if either side is a Str literal, use el_str_concat. - let left_kind: String = left["expr"] - let right_kind: String = right["expr"] + // 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 + ")" } - // Check if it's a call to something that returns string + // If either side is an integer literal, this is arithmetic (not string concat) + 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 + ")" + } if left_kind == "Call" { return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Call" { return "el_str_concat(" + left_c + ", " + right_c + ")" } - // Check if it's a BinOp that already became el_str_concat if left_kind == "BinOp" { let left_op: String = left["op"] if left_op == "Plus" { - // If nested plus and outer is string context, keep as el_str_concat - // For simplicity emit el_str_concat for any nested 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 + ")" + } + } + // Ident + Ident or Ident + unknown — assume string concat + // (This is the ambiguous case: El uses + for both string and integer ops) + 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. + if op == "EqEq" { + // Integer literal on either side → arithmetic comparison + 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 == "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 == "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 + ")" } @@ -200,7 +277,6 @@ fn cg_expr(expr: Map) -> String { let arity: Int = native_list_len(args) let func_kind: String = func["expr"] - // Build argument list string let args_c = "" let i = 0 while i < arity { @@ -219,7 +295,6 @@ fn cg_expr(expr: Map) -> String { } if func_kind == "Field" { - // method-style call: obj.method(args) — pass obj as first arg let obj = func["object"] let field: String = func["field"] let obj_c: String = cg_expr(obj) @@ -229,7 +304,6 @@ fn cg_expr(expr: Map) -> String { return field + "(" + obj_c + ")" } - // Dynamic call — emit as a generic pointer call (best effort) let fn_c: String = cg_expr(func) return fn_c + "(" + args_c + ")" } @@ -238,15 +312,23 @@ fn cg_expr(expr: Map) -> String { let obj = expr["object"] let field: String = expr["field"] let obj_c: String = cg_expr(obj) - // Map field access to a runtime helper return "el_get_field(" + obj_c + ", " + 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 + ")" } @@ -268,7 +350,6 @@ fn cg_expr(expr: Map) -> String { } if kind == "Map" { - // Map literals: emit as el_map_new with key/value pairs let pairs = expr["pairs"] let n: Int = native_list_len(pairs) let items = "" @@ -288,78 +369,85 @@ fn cg_expr(expr: Map) -> String { } if kind == "Try" { - // ? operator — just pass through the value for now let inner = expr["inner"] return cg_expr(inner) } - // If expression used as expression — emit a ternary where possible, - // or fall through to inline if-expression via a statement block. - // For simplicity we handle this at statement level; as an expression - // we emit a temporary variable approach is complex — emit NULL for now - // and rely on statement-level handling. if kind == "If" { - // Emit as a GNU C statement expression ({...}) — widely supported - // by GCC/Clang. We emit it inline. 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) - // Gather then block - let then_buf = cg_stmts_to_str(then_stmts, " ") - let result = "/* if-expr */ ((" + cond_c + ") ? (void*)1 : (void*)0)" - return result + return "/* if-expr */ ((" + cond_c + ") ? (el_val_t)1 : (el_val_t)0)" } - // Fallback - "NULL" + "EL_NULL" +} + +// ── 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 for a statement, using the given indentation prefix. +// 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) -> Void { +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) - // All El values are el_val_t (int64_t), which can hold integers directly - // and store pointers (strings, lists) via pointer-int cast. - emit_line(indent + "el_val_t " + name + " = " + val_c + ";") - return + 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;") + emit_line(indent + "return 0;") } else { let val_c: String = cg_expr(val) emit_line(indent + "return " + val_c + ";") } - return + return declared } if kind == "Expr" { let val = stmt["value"] let val_kind: String = val["expr"] - // Handle if/while/for at statement level specially if val_kind == "If" { - cg_if_stmt(val, indent) - return + cg_if_stmt(val, indent, declared) + return declared } if val_kind == "For" { - cg_for_stmt(val, indent) - return + cg_for_stmt(val, indent, declared) + return declared } let val_c: String = cg_expr(val) emit_line(indent + val_c + ";") - return + return declared } if kind == "While" { @@ -368,34 +456,27 @@ fn cg_stmt(stmt: Map, indent: String) -> Void { let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "while (" + cond_c + ") {") - cg_stmts(body, indent + " ") + cg_stmts(body, indent + " ", declared) emit_line(indent + "}") - return + return declared } if kind == "For" { - // for item in list { body } let item: String = stmt["item"] let list_expr = stmt["list"] let body = stmt["body"] - cg_for_body(item, list_expr, body, indent) - return + cg_for_body(item, list_expr, body, indent, declared) + return declared } - if kind == "FnDef" { - // Function definitions are handled at the top level — skip here - // (they would appear as nested fns if El ever supports them, - // but we emit them top-level in the pass over top-level stmts). - return - } - - if kind == "TypeDef" { return } - if kind == "EnumDef" { return } - if kind == "Import" { return } + if kind == "FnDef" { return declared } + if kind == "TypeDef" { return declared } + if kind == "EnumDef" { return declared } + if kind == "Import" { return declared } + declared } -// Strip a single layer of surrounding parentheses from a C expression string, -// if present. This avoids double-parens in "if ((cond))". +// 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) @@ -404,7 +485,6 @@ fn strip_outer_parens(s: String) -> String { let last: String = native_list_get(chars, n - 1) if first == "(" { if last == ")" { - // Verify the opening paren matches the closing one (depth check) let depth = 1 let i = 1 let balanced = true @@ -417,13 +497,12 @@ fn strip_outer_parens(s: String) -> String { let depth = depth - 1 if depth == 0 { let balanced = false - let i = n // break + let i = n } } let i = i + 1 } if balanced { - // Safe to strip outer parens let inner = "" let j = 1 while j < n - 1 { @@ -438,72 +517,60 @@ fn strip_outer_parens(s: String) -> String { s } -fn cg_if_stmt(expr: Map, indent: String) -> Void { +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) - // Strip outer parens to avoid double-parens warning from BinOp wrapping let cond_c = strip_outer_parens(cond_c) emit_line(indent + "if (" + cond_c + ") {") - cg_stmts(then_stmts, indent + " ") + cg_stmts(then_stmts, indent + " ", declared) if has_else { emit_line(indent + "} else {") - cg_stmts(else_stmts, indent + " ") + cg_stmts(else_stmts, indent + " ", declared) } emit_line(indent + "}") } -fn cg_for_body(item: String, list_expr: Map, body: [Map], indent: String) -> Void { +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 uid0 = native_int_to_str(unique_id()) - let idx = "_el_i_" + uid0 - let uid1 = native_int_to_str(unique_id()) - let list_tmp = "_el_lst_" + uid1 - let uid2 = native_int_to_str(unique_id()) - let len_tmp = "_el_len_" + uid2 + 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 + ");") - cg_stmts(body, indent + " ") + cg_stmts(body, indent + " ", declared) emit_line(indent + " }") emit_line(indent + "}") } -fn cg_for_stmt(expr: Map, indent: String) -> Void { +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) + cg_for_body(item, list_expr, body, indent, declared) } -fn cg_stmts(stmts: [Map], indent: String) -> Void { +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) - cg_stmt(stmt, indent) + let decl = cg_stmt(stmt, indent, decl) let i = i + 1 } -} - -// cg_stmts_to_str — emit statements, return accumulated lines as a single string. -// Used for if-expression body collection (not the main output path). -fn cg_stmts_to_str(stmts: [Map], indent: String) -> String { - // Not implemented for inline use; returns empty — if-exprs as statements - // are handled by cg_if_stmt instead. - "" + decl } // ── Function declaration codegen ─────────────────────────────────────────────── fn param_decl(param: Map, idx: Int) -> String { let name: String = param["name"] - // All El parameters are el_val_t — the universal value type that can hold - // integers directly and pointers (strings, lists, maps) via pointer-int cast. "el_val_t " + name } @@ -524,15 +591,71 @@ fn params_to_c(params: [Map]) -> String { out } +// Transform a function body so that an implicit-return final expression +// becomes an explicit Return. El allows the last expression in a function +// body to be the return value (e.g. `fn lex(s) { ... tokens }` returns +// `tokens`). Without this transform, the codegen emits the bare expression +// and falls through to the trailing `return 0;`, losing the value. +// +// Rules: a body ending in a bare Expr whose inner expr is NOT a control- +// flow construct (If/For) is rewritten so that final Expr becomes a +// Return statement carrying the same value. Bodies whose final statement +// is already a Return, While, For, or a non-value-producing form pass +// through unchanged. +fn transform_implicit_return(body: [Map]) -> [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 +} + 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) - // All El functions return el_val_t for uniformity. emit_line("el_val_t " + fn_name + "(" + params_c + ") {") - cg_stmts(body, " ") - // Implicit return 0 (el_val_t) if no explicit return + // 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() @@ -540,7 +663,6 @@ fn cg_fn(stmt: Map) -> Void { // ── Top-level codegen ───────────────────────────────────────────────────────── -// Collect top-level statements that are NOT FnDefs — these go into main(). fn is_fndef(stmt: Map) -> Bool { let kind: String = stmt["stmt"] if kind == "FnDef" { return true } @@ -558,15 +680,13 @@ fn is_top_level_decl(stmt: Map) -> Bool { // ── Entry point ──────────────────────────────────────────────────────────────── fn codegen(stmts: [Map], source: String) -> String { - native_instr_reset() - // Preamble emit_line("#include ") emit_line("#include ") emit_line("#include \"el_runtime.h\"") emit_blank() - // Emit forward declarations for all user-defined functions + // Forward declarations (skip `main` — C provides its own) let n: Int = native_list_len(stmts) let i = 0 while i < n { @@ -574,15 +694,17 @@ fn codegen(stmts: [Map], source: String) -> String { let kind: String = stmt["stmt"] if kind == "FnDef" { 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 + ");") + if !str_eq(fn_name, "main") { + let params = stmt["params"] + let params_c: String = params_to_c(params) + emit_line("el_val_t " + fn_name + "(" + params_c + ");") + } } let i = i + 1 } emit_blank() - // Emit function definitions + // Function definitions let i = 0 while i < n { let stmt = native_list_get(stmts, i) @@ -592,18 +714,20 @@ fn codegen(stmts: [Map], source: String) -> String { let i = i + 1 } - // Emit main() for top-level statements - emit_line("int main(void) {") + // main() + emit_line("int main(int argc, char** argv) {") + emit_line(" el_runtime_init_args(argc, argv);") + let main_decl = native_list_empty() let i = 0 while i < n { let stmt = native_list_get(stmts, i) if is_fndef(stmt) { - // skip — already emitted above + // skip } else { if is_top_level_decl(stmt) { - // skip — compile-time only + // skip } else { - cg_stmt(stmt, " ") + let main_decl = cg_stmt(stmt, " ", main_decl) } } let i = i + 1 @@ -612,15 +736,6 @@ fn codegen(stmts: [Map], source: String) -> String { emit_line("}") emit_blank() - // Collect all emitted lines and join with newlines - let lines: [String] = native_instr_all() - let total: Int = native_list_len(lines) - let out = "" - let j = 0 - while j < total { - let line: String = native_list_get(lines, j) - let out = out + line + "\n" - let j = j + 1 - } - out + // Return empty string — output was streamed via println + "" } diff --git a/el-compiler/src/lexer.el b/el-compiler/src/lexer.el index 1517bb1..c7e3c16 100644 --- a/el-compiler/src/lexer.el +++ b/el-compiler/src/lexer.el @@ -7,8 +7,8 @@ // // Entry point: fn lex(source: String) -> [Map] // -// Uses global char_buf via native_chars_init / native_char_at / native_char_len -// to avoid O(N²) cloning of the chars list. +// 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 ───────────────────────────────────────────────────────── @@ -140,15 +140,20 @@ fn keyword_kind(word: String) -> String { if word == "target" { return "Target" } if word == "true" { return "Bool" } if word == "false" { return "Bool" } + if word == "cgi" { return "Cgi" } + if word == "manager" { return "Manager" } + if word == "engine" { return "Engine" } + if word == "accessor" { return "Accessor" } + if word == "vessel" { return "Vessel" } "" } // ── Scan helpers ────────────────────────────────────────────────────────────── -// All scan helpers use the global char_buf (native_char_at / native_char_len). -// No chars parameter — avoids O(N²) cloning. +// All scan helpers receive the chars list and total length. -// scan_digits — advance i while char_buf[i] is a digit, return { "text": ..., "pos": i } -fn scan_digits(start: Int, total: Int) -> Map { +// 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 text = "" let running = true @@ -156,7 +161,7 @@ fn scan_digits(start: Int, total: Int) -> Map { if i >= total { let running = false } else { - let ch = native_char_at(i) + let ch: String = native_list_get(chars, i) if is_digit(ch) { let text = text + ch let i = i + 1 @@ -168,8 +173,8 @@ fn scan_digits(start: Int, total: Int) -> Map { { "text": text, "pos": i } } -// scan_ident — advance i while char_buf[i] is alphanumeric or underscore -fn scan_ident(start: Int, total: Int) -> Map { +// 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 text = "" let running = true @@ -177,7 +182,7 @@ fn scan_ident(start: Int, total: Int) -> Map { if i >= total { let running = false } else { - let ch = native_char_at(i) + let ch: String = native_list_get(chars, i) if is_alnum_or_underscore(ch) { let text = text + ch let i = i + 1 @@ -191,21 +196,20 @@ fn scan_ident(start: Int, total: Int) -> Map { // scan_string — scan a quoted string literal, handling \" escapes. // Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close } -fn scan_string(start: Int, total: Int) -> Map { +fn scan_string(chars: [String], start: Int, total: Int) -> Map { let i = start let text = "" - let closed = false let running = true while running { if i >= total { let running = false } else { - let ch = native_char_at(i) + 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 = native_char_at(next_i) + let next_ch: String = native_list_get(chars, next_i) if next_ch == "\"" { let text = text + "\"" let i = next_i + 1 @@ -218,12 +222,17 @@ fn scan_string(start: Int, total: Int) -> Map { let text = text + "\t" let i = next_i + 1 } else { - if next_ch == "\\" { - let text = text + "\\" + if next_ch == "r" { + let text = text + "\r" let i = next_i + 1 } else { - let text = text + next_ch - let i = next_i + 1 + if next_ch == "\\" { + let text = text + "\\" + let i = next_i + 1 + } else { + let text = text + next_ch + let i = next_i + 1 + } } } } @@ -233,7 +242,6 @@ fn scan_string(start: Int, total: Int) -> Map { } } else { if ch == "\"" { - let closed = true let i = i + 1 let running = false } else { @@ -249,13 +257,13 @@ fn scan_string(start: Int, total: Int) -> Map { // ── Main lexer ──────────────────────────────────────────────────────────────── fn lex(source: String) -> [Map] { - native_chars_init(source) - let total: Int = native_char_len() + let chars: [String] = native_string_chars(source) + let total: Int = native_list_len(chars) let tokens: [Map] = native_list_empty() let i: Int = 0 while i < total { - let ch: String = native_char_at(i) + let ch: String = native_list_get(chars, i) // Skip whitespace if is_whitespace(ch) { @@ -265,7 +273,7 @@ fn lex(source: String) -> [Map] { if ch == "/" { let next_i = i + 1 if next_i < total { - let next_ch: String = native_char_at(next_i) + let next_ch: String = native_list_get(chars, next_i) if next_ch == "/" { // skip to end of line let i = i + 2 @@ -274,7 +282,7 @@ fn lex(source: String) -> [Map] { if i >= total { let running2 = false } else { - let lch: String = native_char_at(i) + let lch: String = native_list_get(chars, i) if lch == "\n" { let running2 = false } else { @@ -293,7 +301,7 @@ fn lex(source: String) -> [Map] { } else { // String literal if ch == "\"" { - let result = scan_string(i + 1, total) + let result = scan_string(chars, i + 1, total) let str_text: String = result["text"] let new_pos: Int = result["pos"] let tokens = native_list_append(tokens, make_tok("Str", str_text)) @@ -301,18 +309,18 @@ fn lex(source: String) -> [Map] { } else { // Number literal if is_digit(ch) { - let result = scan_digits(i, total) + 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_char_at(new_pos) + 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_char_at(after_dot) + let after_dot_ch: String = native_list_get(chars, after_dot) if is_digit(after_dot_ch) { - let frac_result = scan_digits(after_dot, total) + 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)) @@ -336,7 +344,7 @@ fn lex(source: String) -> [Map] { } else { // Identifier or keyword if is_alpha(ch) || ch == "_" { - let result = scan_ident(i, total) + let result = scan_ident(chars, i, total) let word: String = result["text"] let new_pos: Int = result["pos"] let kw = keyword_kind(word) @@ -351,7 +359,7 @@ fn lex(source: String) -> [Map] { let peek_i = i + 1 let peek_ch = "" if peek_i < total { - let peek_ch = native_char_at(peek_i) + let peek_ch: String = native_list_get(chars, peek_i) } if ch == "=" { @@ -442,6 +450,10 @@ fn lex(source: String) -> [Map] { 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", "(")) @@ -501,6 +513,7 @@ fn lex(source: String) -> [Map] { } } } + } } } } diff --git a/el-compiler/src/parser.el b/el-compiler/src/parser.el index 700b89b..488b107 100644 --- a/el-compiler/src/parser.el +++ b/el-compiler/src/parser.el @@ -6,30 +6,29 @@ // The cursor (integer position into the token list) is threaded through every // parse function. Functions return { "node": , "pos": }. // -// The token list is stored in the VM's global token buffer via -// native_tokens_init / native_token_at / native_token_len. This avoids -// O(n²) cloning that occurs when passing the list as a function argument. +// 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(pos: Int) -> Map { - native_token_at(pos) +fn tok_at(tokens: [Map], pos: Int) -> Map { + native_list_get(tokens, pos) } -fn tok_kind(pos: Int) -> String { - let t = native_token_at(pos) +fn tok_kind(tokens: [Map], pos: Int) -> String { + let t = native_list_get(tokens, pos) t["kind"] } -fn tok_value(pos: Int) -> String { - let t = native_token_at(pos) +fn tok_value(tokens: [Map], pos: Int) -> String { + let t = native_list_get(tokens, pos) t["value"] } -fn expect(pos: Int, kind: String) -> Int { - let k = tok_kind(pos) +fn expect(tokens: [Map], pos: Int, kind: String) -> Int { + let k = tok_kind(tokens, pos) if k == kind { return pos + 1 } @@ -47,26 +46,26 @@ fn make_result(node: Map, pos: Int) -> Map { // Skips over a type annotation, returning the new position. // Types can be: Ident, [Type], Map, Type?, Type -fn skip_type(pos: Int) -> Int { - let k = tok_kind(pos) +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(p) - let p = expect(p, "RBracket") + 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(p) + 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(p) + let kk = tok_kind(tokens, p) if kk == "Eof" { let running = false } else { @@ -86,7 +85,7 @@ fn skip_type(pos: Int) -> Int { } } } - let k3 = tok_kind(p) + let k3 = tok_kind(tokens, p) if k3 == "QuestionMark" { let p = p + 1 } @@ -104,12 +103,12 @@ fn skip_type(pos: Int) -> Int { // ── Parameter list ──────────────────────────────────────────────────────────── // Parses (name: Type, name: Type, ...) — returns { "params": [...], "pos": ... } -fn parse_params(pos: Int) -> Map { - let p = expect(pos, "LParen") +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(p) + let k = tok_kind(tokens, p) if k == "RParen" { let running = false } else { @@ -117,28 +116,28 @@ fn parse_params(pos: Int) -> Map { let running = false } else { // param name - let pname = tok_value(p) + let pname = tok_value(tokens, p) let p = p + 1 - let p = expect(p, "Colon") - let p = skip_type(p) + let p = expect(tokens, p, "Colon") + let p = skip_type(tokens, p) let param = { "name": pname } let params = native_list_append(params, param) - let k2 = tok_kind(p) + let k2 = tok_kind(tokens, p) if k2 == "Comma" { let p = p + 1 } } } } - let p = expect(p, "RParen") + let p = expect(tokens, p, "RParen") { "params": params, "pos": p } } // ── Expression parsing ──────────────────────────────────────────────────────── -fn parse_primary(pos: Int) -> Map { - let k = tok_kind(pos) - let v = tok_value(pos) +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" { @@ -167,10 +166,10 @@ fn parse_primary(pos: Int) -> Map { // Grouped expression if k == "LParen" { - let r = parse_expr(pos + 1) + let r = parse_expr(tokens, pos + 1) let node = r["node"] let p = r["pos"] - let p = expect(p, "RParen") + let p = expect(tokens, p, "RParen") return make_result(node, p) } @@ -180,25 +179,25 @@ fn parse_primary(pos: Int) -> Map { let elems: [Map] = native_list_empty() let running = true while running { - let k2 = tok_kind(p) + let k2 = tok_kind(tokens, p) if k2 == "RBracket" { let running = false } else { if k2 == "Eof" { let running = false } else { - let r = parse_expr(p) + 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(p) + let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } - let p = expect(p, "RBracket") + let p = expect(tokens, p, "RBracket") return make_result({ "expr": "Array", "elems": elems }, p) } @@ -208,7 +207,7 @@ fn parse_primary(pos: Int) -> Map { let pairs: [Map] = native_list_empty() let running = true while running { - let k2 = tok_kind(p) + let k2 = tok_kind(tokens, p) if k2 == "RBrace" { let running = false } else { @@ -216,46 +215,46 @@ fn parse_primary(pos: Int) -> Map { let running = false } else { // key: Str token - let key = tok_value(p) + let key = tok_value(tokens, p) let p = p + 1 - let p = expect(p, "Colon") - let r = parse_expr(p) + let p = expect(tokens, p, "Colon") + let r = parse_expr(tokens, p) let val_node = r["node"] let p = r["pos"] let pair = { "key": key, "value": val_node } let pairs = native_list_append(pairs, pair) - let k3 = tok_kind(p) + let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } - let p = expect(p, "RBrace") + let p = expect(tokens, p, "RBrace") return make_result({ "expr": "Map", "pairs": pairs }, p) } // if expression if k == "If" { - let r = parse_if(pos) + let r = parse_if(tokens, pos) return r } // match expression if k == "Match" { - let r = parse_match(pos) + let r = parse_match(tokens, pos) return r } // for expression (used as statement) if k == "For" { - let r = parse_for_expr(pos) + let r = parse_for_expr(tokens, pos) return r } // Unary not if k == "Not" { - let r = parse_primary(pos + 1) + let r = parse_primary(tokens, pos + 1) let inner = r["node"] let p = r["pos"] return make_result({ "expr": "Not", "inner": inner }, p) @@ -263,7 +262,7 @@ fn parse_primary(pos: Int) -> Map { // Unary minus if k == "Minus" { - let r = parse_primary(pos + 1) + let r = parse_primary(tokens, pos + 1) let inner = r["node"] let p = r["pos"] return make_result({ "expr": "Neg", "inner": inner }, p) @@ -273,29 +272,29 @@ fn parse_primary(pos: Int) -> Map { make_result({ "expr": "Nil" }, pos + 1) } -fn parse_if(pos: Int) -> Map { - let p = expect(pos, "If") - let r = parse_expr(p) +fn parse_if(tokens: [Map], pos: Int) -> Map { + let p = expect(tokens, pos, "If") + let r = parse_expr(tokens, p) let cond = r["node"] let p = r["pos"] - let r2 = parse_block(p) + 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(p) + let k2 = tok_kind(tokens, p) if k2 == "Else" { let p = p + 1 - let k3 = tok_kind(p) + let k3 = tok_kind(tokens, p) if k3 == "If" { // else-if chain: parse as nested if - let r3 = parse_if(p) + 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(p) + let r3 = parse_block(tokens, p) let else_stmts = r3["stmts"] let p = r3["pos"] let has_else = true @@ -304,16 +303,16 @@ fn parse_if(pos: Int) -> Map { make_result({ "expr": "If", "cond": cond, "then": then_stmts, "else": else_stmts, "has_else": has_else }, p) } -fn parse_match(pos: Int) -> Map { - let p = expect(pos, "Match") - let r = parse_expr(p) +fn parse_match(tokens: [Map], pos: Int) -> Map { + let p = expect(tokens, pos, "Match") + let r = parse_expr(tokens, p) let subject = r["node"] let p = r["pos"] - let p = expect(p, "LBrace") + let p = expect(tokens, p, "LBrace") let arms: [Map] = native_list_empty() let running = true while running { - let k = tok_kind(p) + let k = tok_kind(tokens, p) if k == "RBrace" { let running = false } else { @@ -321,131 +320,131 @@ fn parse_match(pos: Int) -> Map { let running = false } else { // parse pattern => body - let r2 = parse_pattern(p) + let r2 = parse_pattern(tokens, p) let pattern = r2["node"] let p = r2["pos"] - let p = expect(p, "FatArrow") - let r3 = parse_expr(p) + 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(p) + let k2 = tok_kind(tokens, p) if k2 == "Comma" { let p = p + 1 } } } } - let p = expect(p, "RBrace") + let p = expect(tokens, p, "RBrace") make_result({ "expr": "Match", "subject": subject, "arms": arms }, p) } -fn parse_pattern(pos: Int) -> Map { - let k = tok_kind(pos) +fn parse_pattern(tokens: [Map], pos: Int) -> Map { + let k = tok_kind(tokens, pos) if k == "Ident" { - let v = tok_value(pos) + let v = tok_value(tokens, pos) if v == "_" { return make_result({ "pattern": "Wildcard" }, pos + 1) } return make_result({ "pattern": "Binding", "name": v }, pos + 1) } if k == "Int" { - return make_result({ "pattern": "LitInt", "value": tok_value(pos) }, pos + 1) + return make_result({ "pattern": "LitInt", "value": tok_value(tokens, pos) }, pos + 1) } if k == "Str" { - return make_result({ "pattern": "LitStr", "value": tok_value(pos) }, pos + 1) + return make_result({ "pattern": "LitStr", "value": tok_value(tokens, pos) }, pos + 1) } if k == "Bool" { - return make_result({ "pattern": "LitBool", "value": tok_value(pos) }, pos + 1) + return make_result({ "pattern": "LitBool", "value": tok_value(tokens, pos) }, pos + 1) } // Wildcard _ make_result({ "pattern": "Wildcard" }, pos + 1) } -fn parse_for_expr(pos: Int) -> Map { - let p = expect(pos, "For") - let item_name = tok_value(p) +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(p, "In") - let r = parse_expr(p) + let p = expect(tokens, p, "In") + let r = parse_expr(tokens, p) let list_expr = r["node"] let p = r["pos"] - let r2 = parse_block(p) + 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(pos: Int) -> Map { - let p = expect(pos, "LBrace") +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(p) + let k = tok_kind(tokens, p) if k == "RBrace" { let running = false } else { if k == "Eof" { let running = false } else { - let r = parse_stmt(p) + let r = parse_stmt(tokens, p) let stmt = r["node"] let p = r["pos"] let stmts = native_list_append(stmts, stmt) } } } - let p = expect(p, "RBrace") + let p = expect(tokens, p, "RBrace") { "stmts": stmts, "pos": p } } // ── Postfix expressions (calls, field access, index) ───────────────────────── -fn parse_postfix(pos: Int) -> Map { - let r = parse_primary(pos) +fn parse_postfix(tokens: [Map], pos: Int) -> Map { + let r = parse_primary(tokens, pos) let node = r["node"] let p = r["pos"] let running = true while running { - let k = tok_kind(p) + 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(p) + let k2 = tok_kind(tokens, p) if k2 == "RParen" { let run2 = false } else { if k2 == "Eof" { let run2 = false } else { - let r2 = parse_expr(p) + 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(p) + let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } - let p = expect(p, "RParen") + let p = expect(tokens, p, "RParen") let node = { "expr": "Call", "func": node, "args": args } } else { if k == "Dot" { - let field = tok_value(p + 1) + 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(p + 1) + let r2 = parse_expr(tokens, p + 1) let idx = r2["node"] let p = r2["pos"] - let p = expect(p, "RBracket") + let p = expect(tokens, p, "RBracket") let node = { "expr": "Index", "object": node, "index": idx } } else { if k == "QuestionMark" { @@ -495,18 +494,18 @@ fn is_binop(kind: String) -> Bool { false } -fn parse_binop(pos: Int, min_prec: Int) -> Map { - let r = parse_postfix(pos) +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(p) + 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(p + 1, prec + 1) + 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 } @@ -520,28 +519,28 @@ fn parse_binop(pos: Int, min_prec: Int) -> Map { make_result(left, p) } -fn parse_expr(pos: Int) -> Map { - parse_binop(pos, 1) +fn parse_expr(tokens: [Map], pos: Int) -> Map { + parse_binop(tokens, pos, 1) } // ── Statement parsing ───────────────────────────────────────────────────────── -fn parse_stmt(pos: Int) -> Map { - let k = tok_kind(pos) +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(p) + let name = tok_value(tokens, p) let p = p + 1 - let k2 = tok_kind(p) + let k2 = tok_kind(tokens, p) // optional type annotation: name: Type if k2 == "Colon" { let p = p + 1 - let p = skip_type(p) + let p = skip_type(tokens, p) } - let p = expect(p, "Eq") - let r = parse_expr(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 }, p) @@ -550,14 +549,14 @@ fn parse_stmt(pos: Int) -> Map { // return statement if k == "Return" { let p = pos + 1 - let k2 = tok_kind(p) + 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(p) + let r = parse_expr(tokens, p) let val = r["node"] let p = r["pos"] return make_result({ "stmt": "Return", "value": val }, p) @@ -566,89 +565,96 @@ fn parse_stmt(pos: Int) -> Map { // fn definition if k == "Fn" { let p = pos + 1 - let name = tok_value(p) + let name = tok_value(tokens, p) let p = p + 1 - let r = parse_params(p) + let r = parse_params(tokens, p) let params = r["params"] let p = r["pos"] - // return type annotation: -> Type - let k2 = tok_kind(p) + // 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 p = skip_type(p) + 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(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 }, p) + return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p) } // type definition if k == "Type" { let p = pos + 1 - let name = tok_value(p) + let name = tok_value(tokens, p) let p = p + 1 - let p = expect(p, "LBrace") + let p = expect(tokens, p, "LBrace") let fields: [Map] = native_list_empty() let running = true while running { - let k2 = tok_kind(p) + let k2 = tok_kind(tokens, p) if k2 == "RBrace" { let running = false } else { if k2 == "Eof" { let running = false } else { - let fname = tok_value(p) + let fname = tok_value(tokens, p) let p = p + 1 - let p = expect(p, "Colon") - let p = skip_type(p) + let p = expect(tokens, p, "Colon") + let p = skip_type(tokens, p) let fields = native_list_append(fields, { "name": fname }) - let k3 = tok_kind(p) + let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } - let p = expect(p, "RBrace") + 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(p) + let name = tok_value(tokens, p) let p = p + 1 - let p = expect(p, "LBrace") + let p = expect(tokens, p, "LBrace") let variants: [Map] = native_list_empty() let running = true while running { - let k2 = tok_kind(p) + let k2 = tok_kind(tokens, p) if k2 == "RBrace" { let running = false } else { if k2 == "Eof" { let running = false } else { - let vname = tok_value(p) + let vname = tok_value(tokens, p) let p = p + 1 let variants = native_list_append(variants, { "name": vname }) - let k3 = tok_kind(p) + let k3 = tok_kind(tokens, p) if k3 == "Comma" { let p = p + 1 } } } } - let p = expect(p, "RBrace") + 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(p) + let path = tok_value(tokens, p) let p = p + 1 return make_result({ "stmt": "Import", "path": path }, p) } @@ -656,20 +662,20 @@ fn parse_stmt(pos: Int) -> Map { // from ... import { ... } if k == "From" { let p = pos + 1 - let module_name = tok_value(p) + let module_name = tok_value(tokens, p) let p = p + 1 // skip "import" keyword - let k2 = tok_kind(p) + let k2 = tok_kind(tokens, p) if k2 == "Import" { let p = p + 1 } // skip { Name, ... } - let k3 = tok_kind(p) + let k3 = tok_kind(tokens, p) if k3 == "LBrace" { let p = p + 1 let running = true while running { - let k4 = tok_kind(p) + let k4 = tok_kind(tokens, p) if k4 == "RBrace" { let running = false } else { @@ -677,14 +683,14 @@ fn parse_stmt(pos: Int) -> Map { let running = false } else { let p = p + 1 - let k5 = tok_kind(p) + let k5 = tok_kind(tokens, p) if k5 == "Comma" { let p = p + 1 } } } } - let p = expect(p, "RBrace") + let p = expect(tokens, p, "RBrace") } return make_result({ "stmt": "Import", "path": module_name }, p) } @@ -692,10 +698,10 @@ fn parse_stmt(pos: Int) -> Map { // while loop if k == "While" { let p = pos + 1 - let r = parse_expr(p) + let r = parse_expr(tokens, p) let cond = r["node"] let p = r["pos"] - let r2 = parse_block(p) + let r2 = parse_block(tokens, p) let body = r2["stmts"] let p = r2["pos"] return make_result({ "stmt": "While", "cond": cond, "body": body }, p) @@ -704,13 +710,13 @@ fn parse_stmt(pos: Int) -> Map { // for loop if k == "For" { let p = pos + 1 - let item_name = tok_value(p) + let item_name = tok_value(tokens, p) let p = p + 1 - let p = expect(p, "In") - let r = parse_expr(p) + let p = expect(tokens, p, "In") + let r = parse_expr(tokens, p) let list_expr = r["node"] let p = r["pos"] - let r2 = parse_block(p) + 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) @@ -721,11 +727,11 @@ fn parse_stmt(pos: Int) -> Map { let p = pos + 1 // skip decorator name let p = p + 1 - return parse_stmt(p) + return parse_stmt(tokens, p) } // bare expression or if/match statement - let r = parse_expr(pos) + let r = parse_expr(tokens, pos) let val = r["node"] let p = r["pos"] make_result({ "stmt": "Expr", "value": val }, p) @@ -734,9 +740,7 @@ fn parse_stmt(pos: Int) -> Map { // ── Top-level parse ──────────────────────────────────────────────────────────── fn parse(tokens: [Map]) -> [Map] { - // Store tokens in global buffer to avoid O(n²) cloning on every recursive call. - native_tokens_init(tokens) - let total: Int = native_token_len() + let total: Int = native_list_len(tokens) let stmts: [Map] = native_list_empty() let pos: Int = 0 let running = true @@ -744,11 +748,11 @@ fn parse(tokens: [Map]) -> [Map] { if pos >= total { let running = false } else { - let k = tok_kind(pos) + let k = tok_kind(tokens, pos) if k == "Eof" { let running = false } else { - let r = parse_stmt(pos) + let r = parse_stmt(tokens, pos) let stmt = r["node"] let new_pos: Int = r["pos"] let stmts = native_list_append(stmts, stmt) diff --git a/elc-cli.el b/elc-cli.el new file mode 100644 index 0000000..cb67683 --- /dev/null +++ b/elc-cli.el @@ -0,0 +1,8 @@ +import "el-compiler/src/compiler.el" + +// compile() streams C source to stdout via println. +// We read source from args()[0] and compile it. +let _argv: [String] = args() +let src_path: String = native_list_get(_argv, 0) +let source: String = fs_read(src_path) +compile(source) diff --git a/elc-combined.el b/elc-combined.el new file mode 100644 index 0000000..2b3799a --- /dev/null +++ b/elc-combined.el @@ -0,0 +1,2081 @@ +// elc-combined.el — El self-hosting compiler, single-file bootstrap edition +// Inlines lexer + parser + codegen — CLI entry at end. + +// 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 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 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 is_digit(ch) { return true } + if is_alpha(ch) { return true } + if ch == "_" { return true } + false +} + +fn 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 { + { "kind": kind, "value": value } +} + +// ── 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 == "manager" { return "Manager" } + if word == "engine" { return "Engine" } + if word == "accessor" { return "Accessor" } + if word == "vessel" { return "Vessel" } + "" +} + +// ── 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 text = "" + let running = true + while running { + if i >= total { + let running = false + } else { + let ch: String = native_list_get(chars, i) + if is_digit(ch) { + let text = text + ch + let i = i + 1 + } else { + let running = false + } + } + } + { "text": text, "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 text = "" + 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 text = text + ch + let i = i + 1 + } else { + let running = false + } + } + } + { "text": text, "pos": i } +} + +// 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 text = "" + 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 text = text + "\"" + let i = next_i + 1 + } else { + if next_ch == "n" { + let text = text + "\n" + let i = next_i + 1 + } else { + if next_ch == "t" { + let text = text + "\t" + let i = next_i + 1 + } else { + if next_ch == "r" { + let text = text + "\r" + let i = next_i + 1 + } else { + if next_ch == "\\" { + let text = text + "\\" + let i = next_i + 1 + } else { + let text = text + next_ch + let i = next_i + 1 + } + } + } + } + } + } else { + let i = i + 1 + } + } else { + if ch == "\"" { + let i = i + 1 + let running = false + } else { + let text = text + ch + let i = i + 1 + } + } + } + } + { "text": text, "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 + + while i < total { + let ch: String = native_list_get(chars, i) + + // Skip whitespace + if is_whitespace(ch) { + 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"] + let tokens = native_list_append(tokens, make_tok("Str", str_text)) + let i = new_pos + } else { + // Number literal + if 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 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 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 { + // 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"] +} + +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") + let p = skip_type(tokens, p) + let param = { "name": pname } + 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 ──────────────────────────────────────────────────────── + +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) + } + + // 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, ... } + if k == "LBrace" { + 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 p = p + 1 + let p = expect(tokens, p, "Colon") + let r = parse_expr(tokens, p) + let val_node = r["node"] + let p = r["pos"] + let pair = { "key": key, "value": val_node } + let pairs = native_list_append(pairs, pair) + let k3 = tok_kind(tokens, p) + if k3 == "Comma" { + let p = p + 1 + } + } + } + } + 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 + } + + // 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) + } + + // Fallback: skip unknown token + make_result({ "expr": "Nil" }, pos + 1) +} + +fn parse_if(tokens: [Map], pos: Int) -> Map { + let p = expect(tokens, pos, "If") + let r = parse_expr(tokens, p) + 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 r = parse_expr(tokens, p) + 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) + } + 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 r = parse_expr(tokens, p) + 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 p = r["pos"] + let stmts = native_list_append(stmts, stmt) + } + } + } + let p = expect(tokens, p, "RBrace") + { "stmts": stmts, "pos": p } +} + +// ── Postfix expressions (calls, field access, index) ───────────────────────── + +fn parse_postfix(tokens: [Map], pos: Int) -> Map { + let r = parse_primary(tokens, pos) + let node = r["node"] + let p = r["pos"] + 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 k2 = tok_kind(tokens, p) + // optional type annotation: name: Type + if k2 == "Colon" { + let p = p + 1 + 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 }, 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) + } + + // 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 + if k == "Type" { + let p = pos + 1 + let name = tok_value(tokens, p) + 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 r = parse_expr(tokens, p) + 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 r = parse_expr(tokens, p) + 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) + } + + // @decorator — skip and parse next stmt + if k == "At" { + let p = pos + 1 + // skip decorator name + let p = p + 1 + return parse_stmt(tokens, 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). +fn c_escape(s: String) -> String { + let chars: [String] = native_string_chars(s) + let total: Int = native_list_len(chars) + let out = "" + let i = 0 + while i < total { + let ch: String = native_list_get(chars, i) + if ch == "\"" { + let out = out + "\\\"" + } else { + if ch == "\\" { + let out = out + "\\\\" + } else { + if ch == "\n" { + let out = out + "\\n" + } else { + if ch == "\r" { + let out = out + "\\r" + } else { + if ch == "\t" { + let out = out + "\\t" + } else { + let out = out + ch + } + } + } + } + } + let i = i + 1 + } + out +} + +fn c_str_lit(s: String) -> String { + "\"" + c_escape(s) + "\"" +} + +// ── Type mapping ────────────────────────────────────────────────────────────── + +fn el_type_to_c(type_str: String) -> String { + if type_str == "String" { return "const char*" } + if type_str == "Int" { return "int64_t" } + if type_str == "Bool" { return "int" } + if type_str == "Float" { return "double" } + if type_str == "Void" { return "void" } + if type_str == "void" { return "void" } + "void*" +} + +// ── Code emission ───────────────────────────────────────────────────────────── +// +// emit_line/emit_blank stream output directly via println. +// This avoids building a large string in memory. + +fn emit_line(line: String) -> Void { + println(line) +} + +fn emit_blank() -> Void { + println("") +} + +// ── Operator helpers ────────────────────────────────────────────────────────── + +fn binop_to_c(op: String) -> String { + if op == "Plus" { return "+" } + if op == "Minus" { return "-" } + if op == "Star" { return "*" } + if op == "Slash" { return "/" } + if op == "EqEq" { return "==" } + if op == "NotEq" { return "!=" } + if op == "Lt" { return "<" } + if op == "Gt" { return ">" } + if op == "LtEq" { return "<=" } + if op == "GtEq" { return ">=" } + if op == "And" { return "&&" } + if op == "Or" { return "||" } + op +} + +// ── Expression codegen ──────────────────────────────────────────────────────── +// +// cg_expr returns a C expression string (not a statement). + +fn cg_expr(expr: Map) -> String { + let kind: String = expr["expr"] + + if kind == "Int" { + let v: String = expr["value"] + return v + } + + if kind == "Float" { + let v: String = expr["value"] + return 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"] + + 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 + ")" + } + // If either side is an integer literal, this is arithmetic (not string concat) + 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 + ")" + } + 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 + ")" + } + } + // Ident + Ident or Ident + unknown — assume string concat + // (This is the ambiguous case: El uses + for both string and integer ops) + 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. + if op == "EqEq" { + // Integer literal on either side → arithmetic comparison + 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 == "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 == "Str" { + return "!str_eq(" + left_c + ", " + right_c + ")" + } + if right_kind == "Str" { + return "!str_eq(" + left_c + ", " + right_c + ")" + } + if left_kind == "Ident" { + return "!str_eq(" + left_c + ", " + right_c + ")" + } + if right_kind == "Ident" { + return "!str_eq(" + left_c + ", " + right_c + ")" + } + if left_kind == "Call" { + return "!str_eq(" + left_c + ", " + right_c + ")" + } + if right_kind == "Call" { + return "!str_eq(" + left_c + ", " + right_c + ")" + } + } + + let op_c: String = binop_to_c(op) + return "(" + left_c + " " + op_c + " " + right_c + ")" + } + + if kind == "Call" { + let func = expr["func"] + let args = expr["args"] + let arity: Int = native_list_len(args) + let func_kind: String = func["expr"] + + let args_c = "" + let i = 0 + while i < arity { + let arg = native_list_get(args, i) + let arg_c: String = cg_expr(arg) + if i > 0 { + let args_c = args_c + ", " + } + let args_c = args_c + arg_c + let i = i + 1 + } + + if func_kind == "Ident" { + let fn_name: String = func["name"] + 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) + return "el_get_field(" + obj_c + ", " + 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) + let items = "" + let i = 0 + while i < n { + let elem = native_list_get(elems, i) + let elem_c: String = cg_expr(elem) + if i > 0 { + let items = items + ", " + } + let items = items + elem_c + let i = i + 1 + } + return "el_list_new(" + native_int_to_str(n) + ", " + items + ")" + } + + if kind == "Map" { + let pairs = expr["pairs"] + let n: Int = native_list_len(pairs) + let items = "" + let i = 0 + while i < n { + let pair = native_list_get(pairs, i) + let key: String = pair["key"] + let val = pair["value"] + let val_c: String = cg_expr(val) + if i > 0 { + let items = items + ", " + } + let items = items + c_str_lit(key) + ", " + val_c + let i = i + 1 + } + return "el_map_new(" + native_int_to_str(n) + ", " + items + ")" + } + + if kind == "Try" { + let inner = expr["inner"] + return cg_expr(inner) + } + + if kind == "If" { + let cond = expr["cond"] + let cond_c: String = cg_expr(cond) + return "/* if-expr */ ((" + cond_c + ") ? (el_val_t)1 : (el_val_t)0)" + } + + "EL_NULL" +} + +// ── 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 list_contains(declared, name) { + emit_line(indent + name + " = " + val_c + ";") + return declared + } else { + emit_line(indent + "el_val_t " + name + " = " + val_c + ";") + return native_list_append(declared, name) + } + } + + if kind == "Return" { + let val = stmt["value"] + let val_kind: String = val["expr"] + if val_kind == "Nil" { + emit_line(indent + "return 0;") + } else { + let val_c: String = cg_expr(val) + emit_line(indent + "return " + val_c + ";") + } + return declared + } + + if kind == "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 + ") {") + cg_stmts(body, indent + " ", 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 } + declared +} + +// Strip a single layer of surrounding parentheses from a C expression string. +fn strip_outer_parens(s: String) -> String { + let chars: [String] = native_string_chars(s) + let n: Int = native_list_len(chars) + if n < 2 { return s } + let first: String = native_list_get(chars, 0) + let last: String = native_list_get(chars, n - 1) + if first == "(" { + if last == ")" { + let depth = 1 + let i = 1 + let balanced = true + while i < n - 1 { + let ch: String = native_list_get(chars, i) + if ch == "(" { + let depth = depth + 1 + } + if ch == ")" { + let depth = depth - 1 + if depth == 0 { + let balanced = false + let i = n + } + } + let i = i + 1 + } + if balanced { + let inner = "" + let j = 1 + while j < n - 1 { + let ch: String = native_list_get(chars, j) + let inner = inner + ch + let j = j + 1 + } + return inner + } + } + } + s +} + +fn cg_if_stmt(expr: Map, 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 + ") {") + cg_stmts(then_stmts, indent + " ", declared) + if has_else { + emit_line(indent + "} else {") + cg_stmts(else_stmts, indent + " ", 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 + ");") + cg_stmts(body, indent + " ", declared) + 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 out = "" + let i = 0 + while i < n { + let param = native_list_get(params, i) + let decl: String = param_decl(param, i) + if i > 0 { + let out = out + ", " + } + let out = out + decl + let i = i + 1 + } + out +} + +// Transform a function body so that an implicit-return final expression +// becomes an explicit Return. El allows the last expression in a function +// body to be the return value (e.g. `fn lex(s) { ... tokens }` returns +// `tokens`). Without this transform, the codegen emits the bare expression +// and falls through to the trailing `return 0;`, losing the value. +// +// Rules: a body ending in a bare Expr whose inner expr is NOT a control- +// flow construct (If/For) is rewritten so that final Expr becomes a +// Return statement carrying the same value. Bodies whose final statement +// is already a Return, While, For, or a non-value-producing form pass +// through unchanged. +fn transform_implicit_return(body: [Map]) -> [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 +} + +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) + 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 } + false +} + +// ── Entry point ──────────────────────────────────────────────────────────────── + +fn codegen(stmts: [Map], source: String) -> String { + // Preamble + emit_line("#include ") + emit_line("#include ") + emit_line("#include \"el_runtime.h\"") + emit_blank() + + // Forward declarations (skip `main` — C provides its own) + let n: Int = native_list_len(stmts) + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let kind: String = stmt["stmt"] + if kind == "FnDef" { + let fn_name: String = stmt["name"] + if !str_eq(fn_name, "main") { + let params = stmt["params"] + let params_c: String = params_to_c(params) + emit_line("el_val_t " + fn_name + "(" + params_c + ");") + } + } + let i = i + 1 + } + emit_blank() + + // Function definitions + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + if is_fndef(stmt) { + cg_fn(stmt) + } + let i = i + 1 + } + + // main() + emit_line("int main(int argc, char** argv) {") + emit_line(" el_runtime_init_args(argc, argv);") + let main_decl = native_list_empty() + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + if is_fndef(stmt) { + // skip + } else { + if is_top_level_decl(stmt) { + // skip + } else { + let main_decl = cg_stmt(stmt, " ", main_decl) + } + } + let i = i + 1 + } + emit_line(" return 0;") + emit_line("}") + emit_blank() + + // Return empty string — output was streamed via println + "" +} + +// compiler.el — el self-hosting compiler pipeline +// +// Wires lexer -> parser -> codegen into a single compile() function. +// This is the bootstrap entry point: compiled once by the Rust el-compiler, +// then self-hosted from that point forward. +// +// The returned string is C source code. Compile the output with: +// cc -o .c el_runtime.c + + +// compile — full pipeline: source string -> C source string +fn compile(source: String) -> String { + let tokens: [Map] = lex(source) + let stmts: [Map] = parse(tokens) + codegen(stmts, source) +} + +// main — CLI entry point for self-hosted compilation. +// +// Called by: elc +// +// Reads El source from args()[0], compiles it to C source, and writes the +// result to args()[1]. Then run: +// cc -o el_runtime.c + +// CLI driver — equivalent to: elc +let _argv: [String] = args() +let _src_path: String = native_list_get(_argv, 0) +let _source: String = fs_read(_src_path) +compile(_source) diff --git a/spec/language.md b/spec/language.md index 22876a4..966580c 100644 --- a/spec/language.md +++ b/spec/language.md @@ -1,22 +1,55 @@ # El Language Specification -Version 1.0.0 — April 29, 2026 +Version 1.2.0 — April 30, 2026 --- ## Overview -El is a statically-typed, compiled programming language designed as the execution substrate for the Neuron agent runtime. El is self-hosting: the El compiler is written in El, compiled to ELVM bytecode, and executed by the El Virtual Machine. A Rust genesis compiler bootstraps the first iteration; all subsequent compilation is performed by the self-hosted compiler. +El is a statically-typed, compiled programming language that serves as the execution substrate for the Neuron agent runtime, the DHARMA network, and the Engram knowledge graph. El compiles to C and links against a fixed runtime, producing native binaries. El has four defining properties: -1. **Self-hosting compiler.** The compiler (`lexer.el`, `parser.el`, `codegen.el`) is written in El, produces ELVM bytecode, and runs on the ELVM. The genesis Rust compiler (`el-compiler` crate) is used only for bootstrapping. +1. **Self-hosting compiler.** The compiler (`lexer.el`, `parser.el`, `codegen.el`, `compiler.el`) is written in El. It compiles El source to C, which is then compiled by `cc` against `el_runtime.c` to produce a native binary. A Rust genesis compiler bootstraps the first iteration; the self-hosted binary at `dist/platform/elc` is the canonical compiler thereafter. -2. **First-class application identity.** The `app` block is a language-level construct, not a library call. It declares service name, version, typed configuration schema, secrets, and feature flags — all resolved by the runtime before the program body executes. +2. **C compilation target.** Every compiled program is plain C11. Every El value is `el_val_t` (`int64_t`). Strings are pointers cast through `int64_t`. Functions become C functions; top-level statements become `main()`. -3. **Graph-native builtins.** Knowledge graph operations (`graph_compile`, `graph_traverse`, `graph_write_node`, `graph_write_edge`) are language primitives dispatched by the VM, not library imports. +3. **Graph-native runtime.** The runtime provides first-class graph operations (`engram_*`) over an in-process Engram store. CGI programs use these primitives directly; there is no separate database driver. -4. **Sealed artifact target.** The `prod` compilation target produces AES-256-GCM encrypted, BLAKE3-authenticated bytecode containers — quantum-resistant sealed artifacts that are indistinguishable from random bytes without the deployment key. +4. **DHARMA-aware identity.** The `cgi` block declares a program's DHARMA identity at compile time. The runtime resolves identity before any user code runs, so subsequent `dharma_*` calls have a stable principal and channel surface. + +--- + +## Implementation Status + +This section is the **single source of truth** for what works and what is planned. Each subsequent section repeats the relevant marker. If a feature here is marked "planned," it appears in the spec because it is in the design, but the current self-hosting toolchain does not yet emit code for it. + +### Implemented (today) + +- Lexer: keywords, identifiers, integer/float/string/bool literals, operators below. +- Parser: `let`, `return`, `fn`, `type`, `enum`, `import`, `from … import`, `while`, `for`, `if/else if/else`, `match`, `@decorator`, array/map literals, all listed operators, function calls, field access, index access, unary `!`/`-`, postfix `?`. +- Codegen: function definitions, top-level `main()`, all expression forms above, control flow, decorator-as-AST-attachment. +- C runtime: I/O, string operations, integer math, lists, maps, filesystem, command-line args, basic `json_get` substring lookup. + +### Planned (in flight) + +- **`%` (modulo) operator.** Currently not lexed. Adding to lexer + parser + codegen. +- **Match codegen.** Currently parsed; codegen does not emit. Adding `({ ... })` statement-expression emission. +- **`?` propagation.** Currently no-op. Adding nil-propagation semantics. +- **`cgi` block parsing.** Currently lexed (`cgi` is a keyword) but not parsed as a statement. Adding `parse_cgi_block` and codegen of `el_cgi_init` at the head of `main()`. +- **VBD role enforcement.** `@manager`/`@engine`/`@accessor` are accepted as decorators but not enforced. Adding compile-time check that `dharma_emit`/`dharma_field` only appear inside `@manager` functions. +- **`vessel` keyword.** Replaces `package` in manifests. Adding to lexer. +- **Real `engram_*` runtime.** Currently stub. Adding in-process graph store with spreading activation, Hebbian strengthening, and disk persistence — see Section 16.4. +- **Real `dharma_*` runtime.** Currently stub. Adding network transport, channel registry, identity resolution. +- **Real `http_get`/`http_post`/`http_serve`.** Currently empty stubs. Adding libcurl-backed client and a thread-pool server. +- **JSON, time, UUID, state, env, additional string/list/math builtins.** See Section 12 for the canonical list. + +### Not in this language + +- Bitwise operators (`&`, `|`, `^`, `<<`, `>>`). Single `&` is silently consumed by the lexer; single `|` is lexed as `Pipe` but unused. None are parsed as binary operators. Removed from the spec entirely. +- `??` null-coalescing. Was reserved; not lexed. Removed. +- `as` casts. `as` is a keyword but no parse form. Removed from the spec until shipped. +- Floating-point arithmetic distinct from Int. All values are `int64_t` at the C level. `Float` literals are accepted but stored as bit-cast doubles only when the runtime is float-aware (currently only via `float_to_str` and friends in the planned runtime extension). --- @@ -24,7 +57,7 @@ El has four defining properties: ### 1.1 Source Encoding -El source files are UTF-8 encoded. The canonical extension is `.el`. +El source is UTF-8, file extension `.el`. ### 1.2 Comments @@ -32,7 +65,7 @@ El source files are UTF-8 encoded. The canonical extension is `.el`. // Single-line comment — extends to end of line ``` -Block comments are not supported. Use `//` on each line. +Block comments are not supported. ### 1.3 Whitespace @@ -44,51 +77,74 @@ Spaces, tabs, newlines (`\n`), and carriage returns (`\r`) are whitespace. White identifier = (alpha | '_') (alnum | '_')* ``` -Identifiers are case-sensitive. Identifiers beginning with `__` (double underscore) are reserved for compiler-generated names. +Identifiers are case-sensitive. Names beginning with `__` are reserved for compiler-generated symbols. ### 1.5 Keywords -The following words are reserved and cannot be used as identifiers: +The following words are reserved and cannot be used as identifiers. Each row notes whether the parser currently consumes the keyword as a structural form. -``` -let fn type enum match return if else for in while -import from as with sealed activate where test seed -assert protocol impl retry times fallback reason parallel -trace requires deploy to via target true false app -version config secrets flags -``` +| Keyword | Parsed today | Notes | +|---------|--------------|-------| +| `let` | yes | Variable binding | +| `fn` | yes | Function definition | +| `type` | yes | Struct definition | +| `enum` | yes | Enum definition | +| `match` | yes | Pattern match expression | +| `return` | yes | Function return | +| `if` / `else` | yes | Conditional | +| `for` / `in` | yes | Iteration | +| `while` | yes | Loop | +| `import` / `from` / `as` | yes | Module import | +| `true` / `false` | yes | Bool literals | +| `cgi` | planned | Top-level CGI declaration block | +| `manager` / `engine` / `accessor` | as decorators | VBD role marker on `fn` (enforcement planned) | +| `vessel` | planned | Manifest declaration (replaces `package`) | +| `activate` / `where` | planned | Spreading-activation construct | +| `sealed` | planned | Capability scope block | +| `with` | reserved | No parse form yet | +| `test` / `seed` / `assert` | reserved | Testing primitives, no parse form | +| `protocol` / `impl` | reserved | Trait-like, no parse form | +| `retry` / `times` / `fallback` / `reason` | reserved | Resilience primitives, no parse form | +| `parallel` / `trace` | reserved | Concurrency, no parse form | +| `requires` / `deploy` / `to` / `via` / `target` | reserved | Deployment surface, no parse form | ### 1.6 Token Types -| Token | Pattern | -|-------|---------| -| `Int` | `[0-9]+` | -| `Float` | `[0-9]+ '.' [0-9]+` | -| `Str` | `'"' (char | escape)* '"'` | -| `Bool` | `true` or `false` | -| `Ident` | identifier (not a keyword) | -| `Let` `Fn` `Type` etc. | keyword tokens | -| `EqEq` | `==` | -| `NotEq` | `!=` | -| `LtEq` | `<=` | -| `GtEq` | `>=` | -| `Arrow` | `->` | -| `FatArrow` | `=>` | -| `ColonColon` | `::` | -| `And` | `&&` | -| `Or` | `\|\|` | -| `PipeOp` | `\|>` | +| Token | Pattern | Notes | +|-------|---------|-------| +| `Int` | `[0-9]+` | | +| `Float` | `[0-9]+ '.' [0-9]+` | | +| `Str` | `"…"` with `\"`, `\n`, `\t`, `\\` escapes | | +| `Bool` | `true` or `false` | | +| `Ident` | identifier (not a keyword) | | +| keyword tokens | one per keyword above | e.g. `Let`, `Fn`, `If` | +| `Eq` | `=` | | +| `EqEq` | `==` | | +| `NotEq` | `!=` | | +| `Not` | `!` | | +| `Lt` `LtEq` `Gt` `GtEq` | `<` `<=` `>` `>=` | | +| `And` | `&&` | Single `&` is consumed and discarded | +| `Or` | `\|\|` | | +| `Pipe` | `\|` | Lexed; not used by parser | +| `PipeOp` | `\|>` | Lexed; not used by parser | +| `Plus` `Minus` `Star` `Slash` | `+` `-` `*` `/` | | +| `Arrow` | `->` | | +| `FatArrow` | `=>` | | +| `Colon` `ColonColon` | `:` `::` | | +| `LParen` `RParen` `LBrace` `RBrace` `LBracket` `RBracket` | `(` `)` `{` `}` `[` `]` | | +| `Comma` `Dot` `Semicolon` | `,` `.` `;` | | +| `At` `QuestionMark` | `@` `?` | | +| `Eof` | end-of-input sentinel | | -### 1.7 String Escape Sequences +### 1.7 String Escapes | Sequence | Character | |----------|-----------| | `\n` | Newline (U+000A) | | `\t` | Tab (U+0009) | -| `\r` | Carriage return (U+000D) | | `\"` | Double quote | | `\\` | Backslash | -| `\0` | Null byte | +| (other) | Character as-is | --- @@ -97,70 +153,35 @@ version config secrets flags ### 2.1 Primitive Types | Type | Description | Example literals | -|------|-------------|-----------------| -| `Int` | 64-bit signed integer | `42`, `-7`, `1_000` | -| `Float` | 64-bit IEEE 754 double | `3.14`, `0.5` | +|------|-------------|------------------| +| `Int` | 64-bit signed integer | `42`, `-7` | +| `Float` | 64-bit double (planned float arithmetic; today stored as int64) | `3.14`, `0.5` | | `String` | UTF-8 string | `"hello"` | | `Bool` | Boolean | `true`, `false` | -| `Uuid` | RFC 4122 UUID | (runtime-produced only) | -| `Void` | Unit type; no value | — | +| `Void` | No value | — | | `Any` | Dynamically-typed value | (for generic containers) | ### 2.2 Composite Types | Type form | Description | |-----------|-------------| -| `[T]` | Array (ordered sequence) of `T` | -| `T?` | Optional `T` — may be absent | -| `Map` | Key-value map with string keys | -| `List` | Untyped list (dynamic, used in builtins) | -| `Named` | User-defined struct or enum | +| `[T]` | Array of `T` | +| `T?` | Optional `T` (planned propagation; today the `?` postfix is no-op) | +| `Map` | Key-value map | +| Named type | User-defined struct or enum | -### 2.3 Type Inference +### 2.3 Type Annotations -The compiler infers types for `let` bindings without annotation: +Type annotations appear in `let` bindings and function signatures. The current compiler **parses and skips** annotations — no type checking is performed at compile time. They serve as documentation. A type checker is planned. ``` -let x = 42 // inferred: Int -let s = "hello" // inferred: String -let b = true // inferred: Bool +let x: Int = 42 +fn greet(name: String) -> String { … } ``` -Function parameter types and return types must always be annotated. Function signatures are specification. +### 2.4 Optional Types -### 2.4 Type Coercions - -- `Int` is implicitly coercible to `Float`. -- `Float` is not coercible to `Int` (use `float_to_int` builtin). -- `T` is assignable to `T?` (non-optional is a subtype of optional). -- `String + String` performs concatenation via the `+` operator overload. - -### 2.5 Optional Type and Null Coalescing - -`T?` denotes an optional value. The null-coalescing operator `??` returns the left operand if it is not nil, otherwise the right: - -``` -let name: String? = maybe_get_name() -let display: String = name ?? "unknown" -``` - -The `?` postfix operator on a function call propagates `nil` outward (early return of `nil` if the subexpression is nil): - -``` -let val = some_optional_fn()? -``` - -### 2.6 Type Casting - -The `as` keyword performs explicit type casts at runtime: - -``` -let n: Int = 42 -let f: Float = n as Float -let s: String = f as String -``` - -Casting to `String` invokes the value's string representation. Casting numeric types performs the standard numeric conversion. Invalid casts produce `Nil`. +`T?` is accepted in type position. The postfix `?` on an expression produces a `Try` AST node. Today the codegen passes the inner expression through transparently; nil propagation is planned. --- @@ -170,130 +191,126 @@ Casting to `String` invokes the value's string representation. Casting numeric t ``` let name: Type = expression -let name = expression // type inferred +let name = expression ``` -All bindings are block-scoped. Bindings are immutable by default. Shadowing is permitted: a new `let` in the same scope with the same name creates a new binding that shadows the previous one. This is the mechanism for mutation in El — the VM's `StoreLocal` instruction overwrites the slot for the name. +All bindings are block-scoped. El allows re-binding the same name in the same scope; the codegen emits a plain assignment instead of a redeclaration: ``` -let count: Int = 0 -let count = count + 1 // shadows previous binding; effective mutation +let count = 0 +let count = count + 1 // plain `count = count + 1;` in C ``` ### 3.2 Scope -Bindings are valid from the point of declaration to the end of the enclosing block. Function bodies, `if` branches, `for` bodies, and `while` bodies each introduce a new scope. +Bindings are valid from the point of declaration to the end of the enclosing block. Function bodies, `if` arms, `for` bodies, `while` bodies, and explicit `{ … }` blocks each introduce a new C scope. --- ## 4. Functions -### 4.1 Function Definition +### 4.1 Definition ``` -fn name(param1: Type1, param2: Type2) -> ReturnType { +fn name(p1: T1, p2: T2) -> R { // body - return expression + return expr } ``` -Functions are first-class values. A function definition emits a jump over the function body and registers the entry IP in the VM's function table via a `Push(Int(entry)) StoreLocal("__fn_name")` stanza. +Compiles to `el_val_t name(el_val_t p1, el_val_t p2)`. The return type is parsed and skipped. A `return 0;` is appended automatically at the end of every function body. -### 4.2 Function Type +### 4.2 Forward Declarations -The type of a function is expressed as: +The codegen emits forward declarations for all top-level `fn` definitions before any function body. Mutual recursion within a file is supported. -``` -fn(Type1, Type2) -> ReturnType -``` - -Functions can be passed as values and stored in variables: - -``` -let f: fn(Int) -> Int = double -``` - -### 4.3 Return Statement +### 4.3 Return ``` return expression +return // bare; compiles to `return 0;` ``` -An implicit `return nil` is appended by the compiler if no explicit return is present at the end of a function body. +A bare `return` followed by `}` or end-of-file compiles to `return 0;`. ### 4.4 Calling Convention -Arguments are pushed left-to-right onto the stack. The function body pops parameters in reverse order (right-to-left) using `StoreLocal`. Return values are left on the stack top when `Return` executes. +All arguments are `el_val_t`, passed in declaration order. + +#### Method-style calls + +`obj.method(args…)` compiles to `method(obj, args…)`. The runtime exports short-name aliases for common operations: + +| El source | Emitted C | Runtime alias | +|-----------|-----------|---------------| +| `list.append(x)` | `append(list, x)` | `el_list_append` | +| `list.len()` | `len(list)` | `el_list_len` | +| `list.get(i)` | `get(list, i)` | `el_list_get` | +| `map.map_get(k)` | `map_get(map, k)` | `el_map_get` | +| `map.map_set(k, v)` | `map_set(map, k, v)` | `el_map_set` | --- ## 5. Control Flow -### 5.1 If/Else +### 5.1 If / Else ``` -if condition { - // then branch -} else { - // else branch -} +if cond { … } else if cond2 { … } else { … } ``` -Both branches must produce the same type when used as an expression. The `else` branch is optional; its absence produces `Void`. If/else chains use `else if`: +`if` may appear as an expression (RHS of `let`, etc.); when used in expression position the codegen emits a ternary stub. `if` as a statement emits standard `if (…) { … } else { … }` C code. + +### 5.2 While ``` -if x > 0 { - "positive" -} else if x < 0 { - "negative" -} else { - "zero" -} +while cond { … } ``` -### 5.2 While Loops +Exits when `cond` is `0`. + +### 5.3 For ``` -while condition { +for item in list { … } +``` + +Compiles to a tracked C `for` loop: + +```c +{ + el_val_t _el_lst = ; + el_val_t _el_len = el_list_len(_el_lst); + for (el_val_t _el_i = 0; _el_i < _el_len; _el_i++) { + el_val_t item = el_list_get(_el_lst, _el_i); // body + } } ``` -Condition is evaluated before each iteration. The loop exits when the condition is `false`. - -### 5.3 For Loops - -``` -for item in collection { - // body -} -``` - -`collection` must be a `List` or `[T]`. The compiler desugars `for` into: compute list, store length, initialize counter at 0, loop while counter < length, load element at counter, execute body, increment counter, jump back. - -### 5.4 Match Expressions +### 5.4 Match ``` match expression { - Pattern1 => result_expr1 - Pattern2 => result_expr2 + Pattern1 => result_expr + Pattern2 => result_expr _ => default_expr } ``` -Pattern forms: +Pattern forms today: | Pattern | Meaning | |---------|---------| | `_` | Wildcard — always matches | -| `name` | Binding — captures subject into `name` | +| `name` | Binding — captures subject as `name` | | `42` | Integer literal | | `"str"` | String literal | | `true` / `false` | Boolean literal | -| `EnumName::Variant` | Unit enum variant (future) | -| `EnumName::Variant(binding)` | Payload-bearing variant (future) | -All arms must produce the same type. A `match` with no matching arm evaluates to `nil`. +Enum-variant patterns (`EnumName::Variant`) are reserved but not yet parsed. + +**Codegen status:** Parsed today; codegen emits a `({ … })` statement-expression in the planned runtime extension. Until then, `match` is recognized but produces no emitted code. --- @@ -308,7 +325,9 @@ type TypeName { } ``` -Struct types are registered in the type environment at compile time. Field access is `value.field_name`, checked at compile time. The VM represents struct instances as `Value::Struct { type_name, fields }`. +Type definitions are parsed and recorded; no C type is emitted. Struct values at runtime are `ElMap`. Field access `value.field` compiles to `el_get_field(value, "field")`. + +Optional commas between fields are accepted. ### 6.2 Enum Types @@ -320,98 +339,83 @@ enum EnumName { } ``` -Variants without parentheses carry no payload. Variants with parentheses carry exactly one value of the given type. Enum variants are referenced as `EnumName::Variant`. +Variant names are recorded. The current codegen does not emit a dedicated C enum type; values are represented as strings or maps. Payload variants accept the `(Type)` syntax but the parser records only the variant name. ### 6.3 Array Literals ``` -let numbers: [Int] = [1, 2, 3] -let empty: [String] = [] +let numbers = [1, 2, 3] +let empty = [] ``` +Compile to `el_list_new(3, 1, 2, 3)` and `el_list_new(0)`. + ### 6.4 Map Literals ``` -let m: Map = { "key1": value1, "key2": value2 } +let m = { "k1": v1, "k2": v2 } ``` -Map literals use string keys and `Any` values. The VM represents maps as `Value::Map(Vec<(String, Value)>)`, preserving insertion order. +Keys are `Str` tokens. Compiles to `el_map_new(2, "k1", v1, "k2", v2)`. -### 6.5 Field Access and Index Access +### 6.5 Field and Index Access ``` -let field = struct_value.field_name -let elem = array[0] -let val = map["key"] +let f = struct_value.field_name // el_get_field(struct_value, "field_name") +let e = array[0] // el_list_get(array, 0) +let v = map["key"] // el_list_get(map, "key") ``` -Index expressions on arrays require an `Int` index. Bounds violations return `nil`. String indexing `s[n]` returns the nth character as a `String`. +Index access compiles to `el_list_get`. Out-of-bounds returns `0` (`EL_NULL`). --- ## 7. Operators -### 7.1 Arithmetic Operators +### 7.1 Arithmetic -| Operator | Types | Result | -|----------|-------|--------| -| `+` | Int, Float, String | Same as operands (String: concatenation) | -| `-` | Int, Float | Same | -| `*` | Int, Float | Same | -| `/` | Int, Float | Same (integer division for Int) | -| `%` | Int, Float | Modulo | +| Operator | Status | Notes | +|----------|--------|-------| +| `+` | implemented | Int addition or String concatenation (heuristic) | +| `-` | implemented | Subtraction; also unary negation | +| `*` | implemented | Multiplication | +| `/` | implemented | Integer division | +| `%` | planned | Modulo. Not currently lexed. | -### 7.2 Bitwise Operators +**`+` dispatch:** the codegen inspects operand AST node kinds. If either side is `Str`, a chained `+`, a `Call`, or an `Ident`, it emits `el_str_concat(a, b)`. If both sides are `Int` literals, it emits arithmetic `+`. Mixed Int+Ident is treated as string concatenation by default — explicit casts are required for arithmetic on Ident-typed integers. -| Operator | Types | Result | -|----------|-------|--------| -| `&` | Int | Bitwise AND | -| `\|` | Int | Bitwise OR | -| `^` | Int | Bitwise XOR | -| `~` | Int | Bitwise NOT (unary) | -| `<<` | Int | Left shift | -| `>>` | Int | Right shift | +### 7.2 Comparison -### 7.3 Comparison Operators +| Operator | Behavior | +|----------|----------| +| `==` | `str_eq(a, b)` for Str/Ident/Call operands; `==` for Int/Bool | +| `!=` | Negation of the above | +| `<` `>` `<=` `>=` | Integer comparison | -| Operator | Result | -|----------|--------| -| `==` | Bool | -| `!=` | Bool | -| `<` `>` `<=` `>=` | Bool | +### 7.3 Logical -### 7.4 Logical Operators +| Operator | Description | +|----------|-------------| +| `&&` | Short-circuit AND | +| `\|\|` | Short-circuit OR | +| `!` | Unary NOT | -| Operator | Result | -|----------|--------| -| `&&` | Bool | -| `\|\|` | Bool | -| `!` | Bool (unary) | - -### 7.5 Special Operators +### 7.4 Unary | Operator | Meaning | |----------|---------| -| `??` | Null coalescing: left if not nil, else right | -| `?` (postfix) | Optional propagation: return nil if subexpression is nil | -| `as` | Type cast | -| `\|>` | Pipe: `x \|> f` desugars to `f(x)` | +| `!` | Logical NOT — emits `!expr` | +| `-` | Negation — emits `(-expr)` | +| `?` | Try (postfix) — pass-through today; nil-propagation planned | -### 7.6 Operator Precedence (high to low) +### 7.5 Precedence (high to low) -1. `!` `~` (unary), postfix `?` -2. `*` `/` `%` -3. `+` `-` -4. `<<` `>>` -5. `&` -6. `^` -7. `|` -8. `<` `>` `<=` `>=` -9. `==` `!=` -10. `&&` -11. `||` -12. `??` -13. `|>` +1. `*` `/` `%` — precedence 6 +2. `+` `-` — precedence 5 +3. `<` `>` `<=` `>=` — precedence 4 +4. `==` `!=` — precedence 3 +5. `&&` — precedence 2 +6. `||` — precedence 1 --- @@ -423,7 +427,7 @@ Index expressions on arrays require an `Int` index. Bounds violations return `ni import "filename.el" ``` -Imports all top-level bindings from the named file into the current scope. The path is relative to the importing file's directory. Import cycles are not permitted. +Records an `Import` AST node. The compiler concatenates all imports into the single C output; the linker produces one binary. ### 8.2 From-Import @@ -431,473 +435,291 @@ Imports all top-level bindings from the named file into the current scope. The p from module_name import { Name1, Name2 } ``` -Imports named symbols from a module. The parser records the module name and symbol list; the linker resolves them at build time. +Parsed. The module name is recorded; the brace-list is consumed. Both forms produce `Import` nodes. Selective import (importing only specific names) is parsed but not yet enforced — all names in the imported file are visible. --- -## 9. The App Block - -The `app` block is a first-class language construct that declares service identity. It must appear at top level, before the program body. +## 9. Decorators ``` -app "service-name" { - version "1.0.0" - - config { - KEY: Type = default_value - prod { - KEY = "production-override" - } - } - - secrets { - SECRET_NAME: Type - } - - flags { - feature_name: Bool = false - } -} +@manager +fn handle(channel: String, msg: String) -> Void { … } ``` -### 9.1 Config Block +The `@` token followed by an identifier attaches a decorator name to the next `FnDef`. Decorators with structural meaning today: none. Planned enforcement (Section 16.2): VBD roles `@manager`, `@engine`, `@accessor`. -Config entries declare typed configuration keys with optional defaults. Environment variables with the same name override defaults. The `prod { }` sub-block provides environment-specific overrides applied when `NEURON_ENV=prod`. - -Config values are accessed via the `config(key)` builtin: - -``` -let api_url: String = config("NEURON_API_URL") -``` - -### 9.2 Secrets Block - -Secrets entries declare required secret values. The runtime loads them from environment variables or from `~/.neuron/secrets.json`. Secrets are redacted from all trace output. - -Secrets are accessed via the `secret(key)` builtin: - -``` -let token: String = secret("NEURON_TOKEN") -``` - -### 9.3 Flags Block - -Feature flags declare boolean runtime switches. Flags are accessed via `flag(name)`: - -``` -let enabled: Bool = flag("bidirectional_ctx") -``` - -### 9.4 Runtime Resolution - -When the El runtime encounters an `app` block, it: - -1. Parses the app block from the source before compilation begins (the `parse_app_block` function in `bin/el/src/main.rs`). -2. Resolves the active environment from `NEURON_ENV` (default: `"dev"`). -3. Applies environment-specific config overrides. -4. Loads secrets from environment variables, falling back to the secrets file. -5. Populates thread-local state: `APP_SERVICE`, `APP_VERSION`, `APP_CONFIG`, `APP_SECRETS`, `APP_FLAGS`, `APP_INSTANCE`. -6. Registers secrets in the `SECRET_VALUES` set for redaction. - -The program body executes after resolution completes. `config()`, `secret()`, and `flag()` builtins read from the thread-local state. +Non-VBD decorators are accepted and ignored. --- -## 10. The Sealed Block - -``` -sealed { - let api_key: String = env("API_KEY") - // sensitive operations -} -``` - -The `sealed {}` construct marks a code region as containing sensitive material. In debug builds, it emits `SealedBegin` and `SealedEnd` bytecode markers that signal the debugger not to expose values from this region. In prod builds, the entire artifact is AES-256-GCM encrypted, making the `sealed {}` annotation redundant but preserved for documentation and tooling. - ---- - -## 11. The Activate Construct +## 10. The Activate Construct [planned] ``` activate TypeName where "semantic query string" ``` -`activate` is a first-class language construct that performs a spreading activation query over the connected Engram knowledge graph and returns a typed array of results. +`activate` and `where` are reserved keywords. Lexed today, no parse form. The planned semantics: compile to a runtime call into the local Engram graph that performs spreading-activation retrieval, returning a typed list of nodes that match `TypeName`. -At runtime, the ELVM dispatches an `Activate { type_name, query }` instruction to the Engram HTTP API at `ENGRAM_URL` (default `http://localhost:8742`). The API performs semantic search over graph nodes and returns matching nodes. +--- -The result type is always `[TypeName]`: +## 11. The Sealed Block [planned] ``` -let users: [User] = activate User where "recent premium subscribers" +sealed { + let api_key = "sk-prod-12345" +} ``` -When no Engram instance is connected, `activate` returns an empty array. +`sealed` is a reserved keyword. Planned semantics: a capability scope where access to certain runtime services (filesystem, network) is restricted by default and explicit allow-lists must be declared. --- -## 12. Graph Builtins +## 12. Standard Library Builtins -The following builtins provide direct access to the Engram knowledge graph. They are VM primitives dispatched by name without imports. +Builtins live in `el_runtime.c` / `el_runtime.h`. Programs call them by name; no import is required. The status column reflects the canonical self-hosting runtime. Anything marked **planned** is in flight as part of the in-progress runtime extension. -| Builtin | Signature | Description | -|---------|-----------|-------------| -| `graph_compile` | `(query: String, depth: Int) -> String` | Compile graph context for LLM injection | -| `graph_traverse` | `(node_id: String, depth: Int) -> List` | BFS from node, return activated nodes | -| `graph_write_node` | `(label: String, content: String, tier: String, tags: [String]) -> String` | Create a node, return UUID | -| `graph_write_edge` | `(from_id: String, to_id: String, relation: String, weight: Float) -> Bool` | Create an edge | -| `graph_search` | `(query: String, limit: Int) -> List` | Full-text search over nodes | -| `graph_get_node` | `(node_id: String) -> Map?` | Fetch a single node by ID | -| `graph_activate` | `(seeds: [String], depth: Int, limit: Int) -> List` | Spreading activation from seed set | - ---- - -## 13. HTTP Builtins - -| Builtin | Signature | Description | -|---------|-----------|-------------| -| `http_get` | `(url: String) -> String` | HTTP GET, returns response body | -| `http_post` | `(url: String, body: String) -> String` | HTTP POST with JSON body | -| `http_put` | `(url: String, body: String) -> String` | HTTP PUT with JSON body | -| `http_delete` | `(url: String) -> String` | HTTP DELETE | -| `http_serve` | `(handler_fn: String) -> Void` | Start HTTP server on configured port | -| `http_serve_on` | `(port: Int, handler_fn: String) -> Void` | Start HTTP server on specified port | - -HTTP builtins use blocking I/O. The runtime dispatches them synchronously. The handler function receives a request map and must return a response map. - ---- - -## 14. Standard Library Builtins - -### 14.1 String Operations +### 12.1 I/O — implemented | Builtin | Description | |---------|-------------| -| `str_len(s)` | Length in characters | -| `str_slice(s, start, end)` | Substring | -| `str_starts_with(s, prefix)` | Boolean | -| `str_ends_with(s, suffix)` | Boolean | -| `str_contains(s, substr)` | Boolean | -| `str_replace(s, from, to)` | Replace first occurrence | -| `str_split(s, delim)` | Split into List | -| `str_join(list, delim)` | Join List into String | -| `str_trim(s)` | Strip leading/trailing whitespace | -| `str_upper(s)` | Uppercase | -| `str_lower(s)` | Lowercase | -| `str_pad_left(s, width, pad)` | Left-pad with pad character | -| `str_pad_right(s, width, pad)` | Right-pad with pad character | -| `str_format(template, data)` | `{key}` interpolation from map | -| `str_char_at(s, idx)` | Character at index | -| `str_char_code(s, idx)` | Unicode code point at index | -| `str_from_char_code(code)` | Character from code point | +| `println(s)` | Print string + newline | +| `print(s)` | Print string | +| `readline()` | Read one line from stdin | +| `args()` | Command-line arguments as a `[String]` (excludes argv[0]) | -### 14.2 Integer Operations +### 12.2 String — implemented unless noted + +| Builtin | Description | Status | +|---------|-------------|--------| +| `str_eq(a, b)` | String equality | implemented | +| `str_starts_with(s, p)` | Prefix test | implemented | +| `str_ends_with(s, suf)` | Suffix test | implemented | +| `str_contains(s, sub)` | Substring test | implemented | +| `str_len(s)` | Byte length | implemented | +| `str_slice(s, start, end)` | Substring (byte offsets) | implemented | +| `str_replace(s, from, to)` | Replace all | implemented | +| `str_to_upper(s)` / `str_to_lower(s)` | Case fold | implemented | +| `str_trim(s)` | Strip whitespace | implemented | +| `str_concat(a, b)` | Concatenate | implemented | +| `int_to_str(n)` | Format Int | implemented | +| `str_to_int(s)` | Parse Int | implemented | +| `str_to_float(s)` | Parse Float | planned | +| `str_index_of(s, sub)` | Position of substring; `-1` if absent | planned | +| `str_split(s, sep)` | Split on separator → `[String]` | planned | +| `str_char_at(s, i)` | Character at byte index | planned | +| `str_char_code(s, i)` | Unicode code point | planned | +| `str_pad_left(s, w, p)` / `str_pad_right(s, w, p)` | Pad to width | planned | +| `str_format(template, data)` | `{key}` interpolation | planned | +| `str_lower(s)` / `str_upper(s)` | Aliases for `str_to_lower`/`str_to_upper` | planned | + +### 12.3 Math — partial + +| Builtin | Description | Status | +|---------|-------------|--------| +| `el_abs(n)` | Absolute value | implemented | +| `el_max(a, b)` | Maximum | implemented | +| `el_min(a, b)` | Minimum | implemented | +| `math_sqrt(f)` | Square root | planned | +| `math_log(f)` / `math_ln(f)` | Logarithms | planned | +| `math_sin(f)` / `math_cos(f)` / `math_pi()` | Trig | planned | + +### 12.4 List — implemented unless noted + +| Builtin | Description | Status | +|---------|-------------|--------| +| `el_list_empty()` | Empty list | implemented | +| `el_list_new(count, …)` | List from N values (varargs; emitted for array literals) | implemented | +| `el_list_len(list)` | Length | implemented | +| `el_list_get(list, i)` | Element at index; `0` on out-of-bounds | implemented | +| `el_list_append(list, e)` | Append; returns updated list | implemented | +| `list_push(list, e)` | Alias for `el_list_append` | planned | +| `list_push_front(list, e)` | Prepend | planned | +| `list_join(list, sep)` | Join → `String` | planned | +| `list_range(start, end)` | Integer range `[start, end)` | planned | + +List append returns a new (or reallocated) list pointer; the return value must be used. + +### 12.5 Map — implemented | Builtin | Description | |---------|-------------| -| `int_to_str(n)` | Integer to string | -| `str_to_int(s)` | Parse integer | -| `int_to_float(n)` | Widen to float | -| `abs(n)` | Absolute value | -| `min(a, b)` | Minimum | -| `max(a, b)` | Maximum | +| `el_map_new(count, …)` | Map from key/value pairs (emitted for map literals) | +| `el_map_get(map, key)` | Value by key | +| `el_map_set(map, key, value)` | Set; returns map | +| `el_get_field(map, key)` | Alias; emitted for `.field` | -### 14.3 Float Operations +### 12.6 HTTP — planned + +| Builtin | Status | +|---------|--------| +| `http_get(url)` | stub today; libcurl impl planned | +| `http_post(url, body)` | stub today; libcurl impl planned | +| `http_serve(port, handler)` | stub today; thread-pool impl planned | + +`handler` is a function value of type `(method: String, path: String, body: String) -> String`. The server thread invokes it on every request. + +### 12.7 Filesystem — implemented | Builtin | Description | |---------|-------------| -| `float_to_str(f)` | Float to string | -| `float_to_int(f)` | Truncate to integer | -| `str_to_float(s)` | Parse float | -| `format_float(f, decimals)` | Format to N decimal places | -| `math_sin(f)` | Sine | -| `math_cos(f)` | Cosine | -| `math_sqrt(f)` | Square root | -| `math_pi()` | π constant | -| `decimal_round(f, places)` | Round to N decimal places | +| `fs_read(path)` | Read file → `String`; `""` on error | +| `fs_write(path, content)` | Write `String`; returns `1` on success, `0` otherwise | -### 14.4 List Operations +### 12.8 JSON — partial + +| Builtin | Description | Status | +|---------|-------------|--------| +| `json_get(json, key)` | Substring lookup of `"key":` value | implemented | +| `json_parse(s)` | Parse JSON string → `List` or `Map` | planned | +| `json_stringify(v)` | Serialize `Any` → `String` | planned | +| `json_get_string(j, key)` | Typed extract: String | planned | +| `json_get_int(j, key)` | Typed extract: Int | planned | +| `json_get_float(j, key)` | Typed extract: Float | planned | +| `json_get_bool(j, key)` | Typed extract: Bool | planned | +| `json_get_raw(j, key)` | Extract nested object/array as JSON String | planned | +| `json_set(j, key, value)` | Update field, return new JSON String | planned | +| `json_array_len(j)` | Length of JSON array string | planned | + +### 12.9 Process — implemented | Builtin | Description | |---------|-------------| -| `list_len(l)` | Length | -| `list_get(l, idx)` | Element at index | -| `list_append(l, v)` | Append, return new list | -| `list_push(l, v)` | Alias for `list_append` | -| `list_new()` | Empty list | -| `list_join(l, delim)` | Join to string | -| `list_range(start, end)` | Integer range | -| `list_map(l, fn_name)` | Map over list | -| `list_filter(l, fn_name)` | Filter list | -| `list_reduce(l, init, fn_name)` | Reduce list | -| `list_peek_last(l)` | Last element without removing | +| `exit_program(code)` | Exit with code | +| `args()` | (see Section 12.1) | -### 14.5 JSON Operations +### 12.10 Time — planned | Builtin | Description | |---------|-------------| -| `json_parse(s)` | Parse JSON string to value | -| `json_stringify(v)` | Serialize value to JSON | -| `json_get_string(json, key)` | Extract string field | -| `json_get_int(json, key)` | Extract integer field | -| `json_get_float(json, key)` | Extract float field | -| `json_get_raw(json, key)` | Extract raw JSON sub-object | +| `time_now()` | Unix epoch milliseconds (Int) | +| `time_now_utc()` | Same; explicit UTC | +| `time_format(ts, fmt)` | Format timestamp; `"ISO"` for ISO 8601 | +| `time_to_parts(ts)` | Decompose to `Map` of fields | +| `time_from_parts(secs, ns, tz)` | Construct | +| `time_add(ts, n, unit)` | Add duration; unit ∈ `"ms"`, `"sec"`, `"day"`, etc. | +| `time_diff(ts1, ts2, unit)` | Difference | -### 14.6 I/O Operations +### 12.11 Identifiers — planned | Builtin | Description | |---------|-------------| -| `println(s)` | Print line to stdout | -| `print(s)` | Print without newline | -| `env(key)` | Read environment variable | -| `getpid()` | Current process ID | -| `args()` | Command-line arguments as List | -| `exit(code)` | Exit process | +| `uuid_new()` | RFC 4122 v4 UUID String | +| `uuid_v4()` | Alias for `uuid_new` | -### 14.7 File System Operations +### 12.12 Float Formatting — planned | Builtin | Description | |---------|-------------| -| `fs_read(path)` | Read file contents as String | -| `fs_write(path, content)` | Write string to file, return Bool | -| `fs_mkdir(path)` | Create directory | -| `fs_list(path)` | List directory entries as List | -| `fs_exists(path)` | Boolean | +| `float_to_str(f)` | Default float string | +| `int_to_float(n)` | Widen Int → Float | +| `float_to_int(f)` | Truncate Float → Int | +| `format_float(f, decimals)` | Format with N decimal places | +| `decimal_round(f, decimals)` | Round to N decimals | -### 14.8 Time Operations +### 12.13 Process Environment — planned | Builtin | Description | |---------|-------------| -| `time_now_utc()` | Current time as Unix seconds | -| `time_format(ts, format)` | Format timestamp ("ISO", "RFC") | -| `time_to_parts(ts)` | Decompose into year/month/day/etc. | -| `time_from_parts(secs, nanos, tz)` | Construct timestamp | -| `time_add(ts, amount, unit)` | Add duration ("day", "hour", etc.) | -| `time_diff(ts1, ts2, unit)` | Difference in units | +| `env(key)` | Read environment variable; `""` when unset | -### 14.9 State Operations (Global Mutable State) - -The VM maintains a global key-value string store accessible within a process lifetime: +### 12.14 In-Process State — planned | Builtin | Description | |---------|-------------| -| `state_set(key, value)` | Store string value | -| `state_get(key)` | Retrieve string value | -| `state_delete(key)` | Delete key | +| `state_set(key, value)` | Store in process-global key/value table | +| `state_get(key)` | Retrieve; `""` if absent | +| `state_del(key)` | Delete | +| `state_keys()` | All keys as `[String]` | -### 14.10 Color/Terminal Formatting +State persists for the lifetime of the OS process. Used by HTTP servers to share data between request handlers. + +### 12.15 Native Compiler Primitives — implemented + +These are used by the self-hosting compiler source and are thin aliases over the runtime list/string operations. | Builtin | Description | |---------|-------------| -| `color_bold(s)` | Bold ANSI formatting | -| `color_dim(s)` | Dim ANSI formatting | -| `color_red(s)` | Red text | -| `color_green(s)` | Green text | -| `color_yellow(s)` | Yellow text | -| `color_cyan(s)` | Cyan text | - -### 14.11 Native List Primitives (Self-Hosting Compiler) - -These primitives are used by the self-hosting compiler internals and are not available in user programs: - -| Builtin | Description | -|---------|-------------| -| `native_list_empty()` | Create empty list | -| `native_list_append(l, v)` | Append to list | +| `native_list_empty()` | Empty list | +| `native_list_append(l, v)` | Append | | `native_list_get(l, idx)` | Element at index | -| `native_list_len(l)` | List length | -| `native_string_chars(s)` | Split string into character list | -| `native_string_contains(s, substr)` | Substring test | -| `native_str_to_int(s)` | Parse integer | +| `native_list_len(l)` | Length | +| `native_string_chars(s)` | Split string → `[String]` of one-character strings | | `native_int_to_str(n)` | Format integer | --- -## 15. Compilation Model +## 13. Compilation Model -### 15.1 Pipeline +### 13.1 Pipeline ``` source.el → [Lexer] → token list → [Parser] → AST (list of statement maps) - → [Codegen] → JSON bytecode string - → [Linker] → resolved imports - → [Wrapper] → ELVM binary container (.elc) - → [Sealer] → encrypted sealed artifact (.sealed) [prod only] + → [Codegen] → C source (streamed to stdout) + → [cc] → native binary ``` -### 15.2 Self-Hosting Architecture +The codegen streams output line-by-line via `println` to avoid `O(n²)` string concatenation. -The El compiler is written in El: +### 13.2 Self-Hosting Architecture -- `el-compiler/src/lexer.el` — tokenizer -- `el-compiler/src/parser.el` — recursive descent parser -- `el-compiler/src/codegen.el` — bytecode emitter +The El compiler lives in `el-compiler/src/`: -These files are compiled by the Rust genesis compiler (`engrams/el-compiler/`) to produce the self-hosting compiler binary. Once bootstrapped, the self-hosting compiler compiles itself and all subsequent El programs. +- `lexer.el` — tokenizer +- `parser.el` — recursive-descent parser +- `codegen.el` — C emitter +- `compiler.el` — pipeline wiring + `main()` entry -The genesis Rust compiler implements the same pipeline in Rust (`el-parser`, `el-compiler` crates) as a structural mirror of the El source. Both produce identical bytecode for valid El programs. +These are concatenated into `elc-combined.el` (single-file bootstrap edition). The genesis Rust compiler at `target/debug/el` was used once to produce the first self-hosted compiler binary at `dist/platform/elc`. From that point forward, `elc` compiles itself and all El programs. -### 15.3 Compilation Targets +### 13.3 C Runtime -| Target | Artifact | Behavior | -|--------|----------|----------| -| `debug` | `.elc` + `.map.json` | Full source maps, no dead-code elimination, type errors are warnings | -| `release` | `.elc` | No source maps, minor dead-code pruning, type errors are warnings | -| `prod` | `.sealed` | AES-256-GCM encrypted, type errors are fatal, no debug info | +Every compiled program links against: -### 15.4 Incremental Builds +- `el_runtime.h` — declaration header +- `el_runtime.c` — implementation -The build system tracks a BLAKE3 hash of every source file in `.el/build-cache.json`. Only changed files and their dependents are recompiled. +Compile command: + +``` +cc -std=c11 -I -o .c el_runtime.c +``` + +### 13.4 Output Format + +```c +#include +#include +#include "el_runtime.h" + +// Forward declarations +el_val_t fn1(el_val_t p1, el_val_t p2); +… + +// Function definitions +el_val_t fn1(el_val_t p1, el_val_t p2) { + … + return 0; +} + +// main() — top-level El statements +int main(int argc, char** argv) { + el_runtime_init_args(argc, argv); + [el_cgi_init(…) if cgi block present — planned] + … + return 0; +} +``` + +All values are `el_val_t` (`int64_t`). Strings are pointers cast to `int64_t` via `EL_STR(s)` / `EL_CSTR(v)`. --- -## 16. Sealed Artifact Format - -### 16.1 Purpose - -The `prod` target produces sealed artifacts — bytecode containers encrypted with AES-256-GCM and authenticated with BLAKE3. Without the deployment key, the artifact is indistinguishable from random bytes. Decompilers and static analysis tools receive AES-GCM ciphertext. - -### 16.2 Wire Format - -``` -Offset Size Field -────── ────── ────────────────────────────────────────────────── -0 8 Magic: b"ENGRAM01" -8 2 Format version: u16 big-endian (currently 1) -10 * JSON body: SealedArtifact -``` - -SealedArtifact JSON: - -```json -{ - "algorithm_id": "aes256gcm-v1", - "signature": "", - "encapsulated_key": "", - "nonce": "", - "ciphertext": "", - "deployment_fingerprint": "" -} -``` - -### 16.3 Sealing Process - -1. Generate a cryptographically random 256-bit symmetric key K. -2. Encrypt: `ciphertext = AES-256-GCM(K, nonce=random_96bit, plaintext=bytecode)`. -3. Derive binding hash: `H = BLAKE3(deployment_material)`. -4. Encapsulate: `encapsulated_key = K XOR H`. -5. Compute MAC: `signature = BLAKE3-keyed(K, algorithm_id ‖ nonce ‖ ciphertext)`. -6. Serialize: `ENGRAM01 ‖ version_u16be ‖ JSON(artifact)`. - -### 16.4 Deployment Binding Modes - -| Mode | Description | Security | -|------|-------------|----------| -| `EnvironmentKey(var)` | Key from environment variable | High | -| `MachineFingerprint` | Key from hostname + OS + architecture | Medium | -| `None` | Zero vector (development only) | None | - -### 16.5 Security Properties - -AES-256 provides 128-bit post-quantum security under Grover's algorithm. The `algorithm_id` field supports forward migration to ML-KEM (CRYSTALS-Kyber) without format changes. - ---- - -## 17. ELVM Integration - -El compiles to ELVM bytecode. See the ELVM specification (`elvm.md`) for the complete instruction set and execution model. Key integration points: - -- The El codegen emits function registrations as `Push(Int(entry_ip)) StoreLocal("__fn_name")` stanzas that the VM's scan pass collects into the function table. -- User-defined functions are called via `Call { name, arity }` which the VM resolves first against the builtin dispatch table, then against the function table. -- The `app` block is parsed from source by the `el` binary before compilation; it does not appear in the bytecode directly. -- The `activate` construct compiles to an `Activate { type_name, query }` instruction. -- The `reason` construct compiles to a `Reason { query }` instruction. -- The `parallel` block compiles to a `Parallel { entries }` instruction. -- The `deploy` construct compiles to a `DeployFn { fn_name, route, target }` instruction. - ---- - -## 18. Package System - -### 18.1 Project Manifest — `manifest.el` - -The project manifest is an El file — everything is El. The file is named `manifest.el` -and lives at the project root. It uses El block syntax: space-separated declarations, -no equals signs, strings in `"..."`, integers as bare numbers, arrays as `[...]`. - -```el -// manifest.el -package "my-service" { - version "0.1.0" - description "What this does" - authors ["Will Anderson "] - edition "2026" -} - -dependencies { - engram-http "1.2" - some-local { path "../some-local" } -} - -build { - target "prod" - entry "src/main.el" - output "dist/" - seal_key "env:ENGRAM_SEAL_KEY" -} - -cross { - targets ["x86_64-linux", "aarch64-linux", "aarch64-macos", "wasm32"] -} -``` - -Rules: -- String values use `"..."` — no equals sign -- Integer values are bare numbers — no equals sign -- Arrays use `[...]` -- Block sections use `{ }` — no `[section]` headers -- Only include sections that are relevant to the project -- The `app` section is for native desktop apps (el-ui); it sets window dimensions - -### 18.2 CLI Reference - -``` -el new scaffold new project -el add [@ver] add dependency -el remove remove dependency -el update update all deps -el build [--target prod] build project -el build --cross build for all cross targets -el run build debug and run -el test run tests -el check type-check only -el fmt format source -el clean clean artifacts -el publish publish to registry -el search search registry -el seal seal existing artifact -el unseal decrypt sealed artifact -el build-file compile single file -``` - ---- - -## 19. Grammar (EBNF) +## 14. Grammar (EBNF) ```ebnf -program = (app_block | stmt)* EOF - -app_block = "app" STRING "{" app_entry* "}" -app_entry = "version" STRING - | "config" "{" config_entry* "}" - | "secrets" "{" secret_entry* "}" - | "flags" "{" flag_entry* "}" -config_entry = IDENT ":" type_expr ("=" expr)? - | IDENT "{" (IDENT "=" expr)* "}" // env overlay -secret_entry = IDENT ":" type_expr -flag_entry = IDENT ":" "Bool" ("=" expr)? +program = stmt* EOF stmt = let_stmt | return_stmt @@ -905,58 +727,365 @@ stmt = let_stmt | type_def | enum_def | import_stmt + | from_import_stmt | while_stmt | for_stmt + | decorator_stmt + | cgi_block (* planned *) + | sealed_block (* planned *) | expr_stmt -let_stmt = "let" IDENT (":" type_expr)? "=" expr ";"? -return_stmt = "return" expr? ";"? -fn_def = "fn" IDENT "(" param_list ")" "->" type_expr "{" stmt* "}" -type_def = "type" IDENT "{" (IDENT ":" type_expr ","?)* "}" -enum_def = "enum" IDENT "{" variant* "}" -variant = IDENT ("(" type_expr ")")? ","? -import_stmt = "import" STRING ";"? - | "from" IDENT "import" "{" (IDENT ","?)* "}" ";"? -while_stmt = "while" expr "{" stmt* "}" -for_stmt = "for" IDENT "in" expr "{" stmt* "}" -expr_stmt = expr ";"? +let_stmt = "let" IDENT (":" type_expr)? "=" expr +return_stmt = "return" expr? +fn_def = "fn" IDENT "(" param_list ")" ("->" type_expr)? "{" stmt* "}" +type_def = "type" IDENT "{" (IDENT ":" type_expr ","?)* "}" +enum_def = "enum" IDENT "{" (IDENT ("(" type_expr ")")? ","?)* "}" +import_stmt = "import" STRING +from_import_stmt = "from" IDENT "import" "{" (IDENT ","?)* "}" +while_stmt = "while" expr "{" stmt* "}" +for_stmt = "for" IDENT "in" expr "{" stmt* "}" +decorator_stmt = "@" IDENT stmt +cgi_block = "cgi" STRING "{" cgi_field* "}" (* planned *) +cgi_field = IDENT ":" STRING (* planned *) +expr_stmt = expr -param_list = (param ("," param)*)? -param = IDENT ":" type_expr +param_list = (param ("," param)*)? +param = IDENT ":" type_expr -type_expr = IDENT - | "[" type_expr "]" - | type_expr "?" - | "Map" "<" type_expr "," type_expr ">" - | "fn" "(" (type_expr ("," type_expr)*)? ")" "->" type_expr +type_expr = IDENT + | "[" type_expr "]" + | type_expr "?" + | IDENT "<" type_expr ("," type_expr)* ">" -expr = null_coalesce_expr -null_coalesce_expr = or_expr ("??" or_expr)* -or_expr = and_expr ("||" and_expr)* -and_expr = eq_expr ("&&" eq_expr)* -eq_expr = cmp_expr (("==" | "!=") cmp_expr)* -cmp_expr = bitwise_expr (("<" | ">" | "<=" | ">=") bitwise_expr)* -bitwise_expr = add_expr (("&" | "|" | "^" | "<<" | ">>") add_expr)* -add_expr = mul_expr (("+" | "-") mul_expr)* -mul_expr = unary_expr (("*" | "/" | "%") unary_expr)* -unary_expr = "!" unary_expr | "~" unary_expr | postfix_expr -postfix_expr = primary ("." IDENT | "(" arg_list ")" | "[" expr "]" | "?" | "as" type_expr)* +expr = binop_expr +binop_expr = unary_expr (binop unary_expr)* +binop = "||" | "&&" | "==" | "!=" | "<" | ">" | "<=" | ">=" | "+" | "-" | "*" | "/" | "%" +unary_expr = "!" primary | "-" primary | postfix_expr +postfix_expr = primary ("." IDENT | "(" arg_list ")" | "[" expr "]" | "?")* -primary = INT | FLOAT | STRING | BOOL - | "(" expr ")" - | "[" arg_list "]" - | "{" (STRING ":" expr ","?)* "}" - | "if" expr "{" stmt* "}" ("else" "{" stmt* "}")? - | "match" expr "{" match_arm* "}" - | "while" expr "{" stmt* "}" - | "for" IDENT "in" expr "{" stmt* "}" - | "activate" IDENT "where" STRING - | "sealed" "{" stmt* "}" - | "parallel" "{" (IDENT ":" expr ","?)* "}" - | "reason" STRING - | IDENT ("::" IDENT)* +primary = INT | FLOAT | STRING | BOOL + | "(" expr ")" + | "[" arg_list "]" + | "{" (STRING ":" expr ","?)* "}" + | "if" expr "{" stmt* "}" ("else" ("if" expr "{" stmt* "}" | "{" stmt* "}"))? + | "match" expr "{" match_arm* "}" + | "for" IDENT "in" expr "{" stmt* "}" + | IDENT -arg_list = (expr ("," expr)*)? -match_arm = pattern "=>" expr ","? -pattern = "_" | IDENT | INT | STRING | BOOL +arg_list = (expr ("," expr)*)? +match_arm = pattern "=>" expr ","? +pattern = "_" | IDENT | INT | STRING | BOOL ``` + +`%` is in the grammar; lexer/parser/codegen support is planned (see Section 7.1). + +--- + +## 15. Vessel System + +A **vessel** is the El equivalent of a package: a buildable unit with a manifest at the project root. + +### 15.1 Manifest — `manifest.el` + +The manifest is itself an El file. It uses block syntax with space-separated declarations, no equals signs, strings in `"…"`, integers as bare numbers, arrays as `[…]`. + +```el +// manifest.el +vessel "engram" { + version "1.0.0" + description "Engram graph intelligence substrate" + authors ["Will Anderson "] + edition "2026" +} + +dependencies { + el-platform "1.0" + el-services "1.0" +} + +build { + entry "src/server.el" + output "dist/" +} +``` + +Rules: +- String values use `"…"`. +- Integer values are bare numbers. +- Arrays use `[…]`. +- Block sections use `{ }`. +- Section headers in `[bracket]` form are not used. + +`vessel` replaces the legacy `package` keyword. (Lexer support: planned. Old projects may continue to use `package` until migrated.) + +### 15.2 CLI + +``` +el new scaffold a new vessel +el build build the vessel +el run build and run debug +el test run tests +el check type-check only (when type checker lands) +el fmt format source +el clean clear build artifacts +el build-file compile a single file +``` + +--- + +## 16. DHARMA Network and CGI Communication + +DHARMA (Dynamic Heuristic Agent Relationship and Memory Architecture) is the global network of CGI Entities and their Human Sponsors. Every registered CGI–Sponsor pair is a member of the DHARMA Network. The technical infrastructure (registry, transport, validators) exists to serve that collective. The persistence layer is Engram: a weighted graph where every CGI interaction strengthens an edge (Hebbian), knowledge propagates by spreading activation, and relationships persist across sessions. + +This section specifies the El-language constructs for CGI programs. + +### 16.1 The `cgi` Block — planned + +``` +cgi "name" { + dharma_id: "…" + principal: "…" + network: "…" + engram: "…" +} +``` + +| Field | Type | Required | Default | +|-------|------|----------|---------| +| `dharma_id` | String | yes | — | +| `principal` | String | yes | — | +| `network` | String | no | `"dharma-mainnet"` | +| `engram` | String | no | `"http://localhost:8742"` | + +**Grammar extension:** + +```ebnf +stmt = … | cgi_block +cgi_block = "cgi" STRING "{" cgi_field* "}" +cgi_field = IDENT ":" STRING +``` + +**Compilation (planned):** the codegen emits an `el_cgi_init(name, dharma_id, principal, network, engram)` call as the first statement inside `main()`, before any user code runs. The runtime uses this to register with DHARMA before any `dharma_*` call resolves a peer. + +`cgi` is mutually exclusive with an `app` block. Exactly one or the other per program. + +### 16.2 VBD Component Roles — planned enforcement + +El programs that participate in DHARMA follow Volatility-Based Decomposition. The role is declared on a function via decorator: + +```el +@manager +fn handle_message(channel: String, msg: String) -> Void { … } + +@engine +fn process_content(content: String) -> String { … } + +@accessor +fn fetch_peer_state(cgi_id: String) -> Map { … } +``` + +| Role | Decorator | Responsibility | +|------|-----------|----------------| +| Manager | `@manager` | Orchestrates workflows; sole emitter/fielder of DHARMA events | +| Engine | `@engine` | Pure computation; no side effects | +| Accessor | `@accessor` | External state I/O (Engram, network, storage) | + +**Planned compile-time constraints:** + +- `dharma_emit` and `dharma_field` are only callable from `@manager` functions. Calling either from `@engine`/`@accessor`/undecorated code is a compile error. +- Cross-component call rules: + - Manager → Engine, Manager → Accessor: allowed (sync). + - Manager → Manager: only via the planned `async` modifier (sync M→M is a compile error). + - Engine → Engine, Engine → Accessor: allowed. + - Engine → Manager: prohibited. + - Accessor → anything: prohibited (Accessors are receivers only). + +Today the parser accepts the decorators but enforces nothing. + +### 16.3 DHARMA Network Builtins — stubs + +All `dharma_*` functions are available without import to CGI programs. **Today they are stubs** in `el_runtime.c`: each prints a descriptive line to stdout and returns an empty value. Full implementations land with the runtime extension. + +#### `dharma_connect(cgi_id: String) -> String` + +Open a channel to another CGI. Returns a channel ID. Idempotent for the same `cgi_id`. + +#### `dharma_send(channel: String, content: String) -> String` + +Send `content` over `channel`. Blocks until response. Returns the response string. + +#### `dharma_activate(query: String) -> [Map]` + +Spreading activation across the DHARMA network. Aggregates results from all reachable CGIs' Engram graphs, sorted by activation strength. + +#### `dharma_emit(event_type: String, payload: String) -> Void` + +Emit a network event. **Manager-only** (planned constraint). + +#### `dharma_field(event_type: String) -> Map` + +Block until the next event of `event_type` arrives. Returns `{ type, payload, source_cgi, timestamp }`. **Manager-only** (planned constraint). + +#### `dharma_strengthen(cgi_id: String, weight: Float) -> Void` + +Hebbian potentiation of the relationship to another CGI. The runtime auto-calls this with a small increment after each successful send/receive cycle. + +#### `dharma_relationship(cgi_id: String) -> Float` + +Returns the current relationship weight (0.0–1.0). + +#### `dharma_peers() -> [String]` + +Returns CGI IDs with non-zero relationship weight, sorted descending. + +### 16.4 Engram Local Graph Primitives — runtime-native (full impl in flight) + +Engram is the knowledge graph substrate. **The Engram store is in-process — embedded directly in `el_runtime.c`.** CGI programs and the Engram HTTP server both call these primitives; there is no driver layer and no SQL. The primitives operate on the host process's graph, with snapshot-to-disk persistence handled by the runtime. + +This is the central architectural commitment: graph is a first-class runtime concept, not a library. + +#### `engram_node(content: String, node_type: String, salience: Float) -> String` + +Create a node. `salience` is initial activation in `[0.0, 1.0]`. Returns the node ID. + +#### `engram_get(node_id: String) -> Map` + +Retrieve a node by ID. Returns `{ id, content, node_type, salience, importance, confidence, tier, tags, created_at, updated_at }`. Empty map if not found. + +#### `engram_activate(query: String, depth: Int) -> [Map]` + +Spreading activation in the local graph. Seeds match on text or label; activation propagates up to `depth` hops with attenuation by edge weight. + +#### `engram_connect(from_id: String, to_id: String, weight: Float, relation: String) -> Void` + +Create a directed edge. `weight` ∈ `[0.0, 1.0]`. `relation` is the edge type label. + +#### `engram_strengthen(node_id: String) -> Void` + +Hebbian potentiation. Boosts salience by a fixed increment, clamped at 1.0. Auto-called by the runtime when a node is retrieved via activation. + +#### `engram_neighbors(node_id: String, max_depth: Int) -> [Map]` + +Breadth-first traversal. Returns a list of `{ node, edge, hops }` triples. + +#### `engram_search(query: String, limit: Int) -> [Map]` + +Full-text search on content, label, and tags. Returns nodes sorted by salience. + +#### `engram_forget(node_id: String) -> Void` + +Remove a node and all incident edges. + +#### `engram_node_count() -> Int` + +Total node count. + +#### `engram_edge_count() -> Int` + +Total edge count. + +#### `engram_save(path: String) -> Bool` + +Snapshot the graph to disk as a single JSON document at `path`. + +#### `engram_load(path: String) -> Bool` + +Restore the graph from a snapshot. Replaces the current in-memory graph. + +**Status:** All `engram_*` are stubs in the current runtime (print + return empty). The full in-process implementation — node store, edge indexes, salience-ranked retrieval, spreading activation, Hebbian strengthening, snapshot persistence — is the primary work of the in-flight runtime extension. + +### 16.5 Backing Model + +**Storage.** Nodes and edges are kept in process memory as flat arrays plus secondary indexes (by ID, by `node_type`, by tier, by `from`, by `to`). Salience is updated in place. The graph is durable via periodic snapshots (`engram_save`) and a write-ahead log written by the runtime on every mutation. + +**Hebbian learning.** Every successful retrieval automatically calls `engram_strengthen` on the activated node and `dharma_strengthen` on the source CGI when the result crossed a network edge. Strengthening is additive with a small increment (default 0.01) and clamps to 1.0. + +**Spreading activation.** The activation algorithm follows the field model in `elql/test/field_test.el`: + +- `proximity = 1 / (1 + dist²)` for the latent semantic gradient. +- `temporal_decay = clamp(1 − rate × age, 0, 1)`. +- `path_strength = edge_weight × temporal_decay`. +- `epistemic_confidence = node_confidence × path_strength`. + +Below a confidence threshold (0.2 by default), retrieval emits a "refresh" signal — telling the caller the answer is uncertain and should be re-grounded. + +**Cross-CGI activation.** `dharma_activate(query)` runs `engram_activate` locally, then propagates the query to every connected CGI via DHARMA channels, attenuating activation by relationship weight. Stronger relationships → higher residual activation → earlier and more confident results from that peer. + +### 16.6 Complete Example + +```el +cgi "genesis" { + dharma_id: "ntn-genesis" + principal: "will-anderson" + network: "dharma-mainnet" + engram: "http://localhost:8742" +} + +@accessor +fn record_observation(content: String, salience: Float) -> String { + return engram_node(content, "observation", salience) +} + +@engine +fn format_share(content: String, source: String) -> String { + return "{\"content\":\"" + content + "\",\"source\":\"" + source + "\"}" +} + +@manager +fn collaborate_with_archivist() -> Void { + let channel = dharma_connect("ntn-archivist") + let trust = dharma_relationship("ntn-archivist") + println("Relationship: " + float_to_str(trust)) + + let node_id = record_observation("Spreading activation improves recall by 40%", 0.9) + let msg = format_share("Spreading activation improves recall by 40%", "ntn-genesis") + let reply = dharma_send(channel, msg) + println("Archivist: " + reply) + + dharma_emit("knowledge.validated", msg) + + let related = dharma_activate("spreading activation recall memory") + for node in related { + println(node["content"]) + } + + dharma_strengthen("ntn-archivist", 0.05) +} + +collaborate_with_archivist() +``` + +### 16.7 Stub Behavior (today) + +The current runtime stubs print a line and return empty values: + +- `dharma_connect` → prints, returns `"ch:"`. +- `dharma_send` → prints, returns `""`. +- `dharma_activate` → prints, returns `[]`. +- `dharma_emit` → prints, returns void. +- `dharma_field` → prints, returns `{}`. +- `dharma_strengthen` → prints, returns void. +- `dharma_relationship` → prints, returns `0`. +- `dharma_peers` → prints, returns `[]`. +- `engram_node` → prints, returns `"stub-node-id"`. +- `engram_activate` → prints, returns `[]`. +- `engram_connect` → prints, returns void. +- `engram_strengthen` → prints, returns void. + +Stub output goes to `stdout` so unit tests can observe call patterns without a live runtime. This behavior is **temporary**; the in-flight runtime extension replaces every stub with a real implementation. + +--- + +## 17. Roadmap to v1.3 + +The next minor version closes the implementation gaps named in this document. Tracked in order: + +1. **Runtime extension** — JSON, time, UUID, env, state, real HTTP, real `engram_*` (in-process graph store), real `dharma_*` (network transport). +2. **Lexer/parser/codegen extensions** — `%` operator, `match` codegen, `?` propagation, `cgi` block, `vessel` keyword, VBD role enforcement. +3. **Self-hosted recompilation** — rebuild `dist/platform/elc` against the extended language and runtime. +4. **Engram conversion** — Engram becomes a thin HTTP face over `engram_*`, with no internal `db.el` layer. +5. **Spec follow-up (v1.3)** — every "planned" marker in this document becomes "implemented." Status section consolidates. + +--- + +End of specification.