diff --git a/dist/platform/elc b/dist/platform/elc new file mode 100755 index 0000000..b1e96c9 Binary files /dev/null and b/dist/platform/elc differ 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 0000000..b1e1a92 Binary files /dev/null and b/dist/platform/elc.legacy differ 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.