From d527fa6065a12a77b80bc00ee36a425fa05e3ae1 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Sat, 2 May 2026 01:30:31 -0500 Subject: [PATCH] elc: regenerate dist artifacts and ship parser.el state alongside self-host land --- dist/platform/elc.c | 1919 +++++++++++++++++++++++++++++++++++-- el-compiler/src/parser.el | 84 +- elc-combined.el | 1673 ++++++++++++++++++++++++++++++-- 3 files changed, 3531 insertions(+), 145 deletions(-) diff --git a/dist/platform/elc.c b/dist/platform/elc.c index 941891d..1eab9b1 100644 --- a/dist/platform/elc.c +++ b/dist/platform/elc.c @@ -41,6 +41,9 @@ el_val_t binop_to_c(el_val_t op); el_val_t cg_expr(el_val_t expr); el_val_t next_match_id(void); el_val_t cg_match(el_val_t expr); +el_val_t next_if_id(void); +el_val_t cg_if_expr_arm(el_val_t stmts, el_val_t result_var); +el_val_t cg_if_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); @@ -53,12 +56,17 @@ el_val_t params_to_c(el_val_t params); el_val_t transform_implicit_return(el_val_t body); el_val_t is_int_name(el_val_t name); el_val_t is_int_call(el_val_t call_expr); +el_val_t is_int_expr(el_val_t expr); el_val_t cap_record_violation(el_val_t kind, el_val_t fn_name); el_val_t is_self_formation_call(el_val_t fn_name); el_val_t is_dharma_call(el_val_t fn_name); el_val_t is_llm_call(el_val_t fn_name); el_val_t cap_check_call(el_val_t fn_name); el_val_t emit_cap_violations(void); +el_val_t builtin_arity(el_val_t name); +el_val_t arity_record_violation(el_val_t fn_name, el_val_t expected, el_val_t actual); +el_val_t arity_check_call(el_val_t fn_name, el_val_t actual); +el_val_t emit_arity_violations(void); el_val_t add_int_name(el_val_t name); el_val_t build_int_names_for_params(el_val_t params); el_val_t cg_fn(el_val_t stmt); @@ -69,7 +77,41 @@ el_val_t vbd_is_restricted_name(el_val_t name); el_val_t vbd_expr_has_restricted_call(el_val_t expr); el_val_t vbd_has_restricted_call(el_val_t stmts); el_val_t codegen(el_val_t stmts, el_val_t source); +el_val_t js_escape(el_val_t s); +el_val_t js_str_lit(el_val_t s); +el_val_t js_emit_line(el_val_t line); +el_val_t js_emit_blank(void); +el_val_t js_binop(el_val_t op); +el_val_t js_is_int_name(el_val_t name); +el_val_t js_add_int_name(el_val_t name); +el_val_t js_build_int_names_for_params(el_val_t params); +el_val_t js_is_int_call(el_val_t call_expr); +el_val_t js_cg_expr(el_val_t expr); +el_val_t js_next_match_id(void); +el_val_t js_cg_match(el_val_t expr); +el_val_t js_list_contains(el_val_t lst, el_val_t s); +el_val_t js_cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared); +el_val_t js_strip_outer_parens(el_val_t s); +el_val_t js_cg_if_stmt(el_val_t expr, el_val_t indent, el_val_t declared); +el_val_t js_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 js_cg_for_stmt(el_val_t expr, el_val_t indent, el_val_t declared); +el_val_t js_cg_stmts(el_val_t stmts, el_val_t indent, el_val_t declared); +el_val_t js_params_str(el_val_t params); +el_val_t js_transform_implicit_return(el_val_t body); +el_val_t js_cg_fn(el_val_t stmt); +el_val_t js_is_fndef(el_val_t stmt); +el_val_t js_is_top_level_decl(el_val_t stmt); +el_val_t codegen_js(el_val_t stmts, el_val_t source); el_val_t compile(el_val_t source); +el_val_t compile_js(el_val_t source); +el_val_t compile_dispatch(el_val_t tgt, el_val_t source); +el_val_t detect_target(el_val_t argv); +el_val_t strip_flags(el_val_t argv); + +el_val_t _argv; +el_val_t _target; +el_val_t _positional; +el_val_t _argc; el_val_t is_digit(el_val_t ch) { if (str_eq(ch, EL_STR("0"))) { @@ -969,6 +1011,10 @@ el_val_t parse_primary(el_val_t tokens, el_val_t pos) { return make_result(el_map_new(2, "expr", EL_STR("Array"), "elems", elems), p); } if (str_eq(k, EL_STR("LBrace"))) { + el_val_t no_block = state_get(EL_STR("__no_block_expr")); + if (str_eq(no_block, EL_STR("1"))) { + return make_result(el_map_new(1, "expr", EL_STR("Nil")), pos); + } el_val_t p = (pos + 1); el_val_t pairs = native_list_empty(); el_val_t running = 1; @@ -981,16 +1027,21 @@ el_val_t parse_primary(el_val_t tokens, el_val_t pos) { 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 new_p = (p + 1); + new_p = expect(tokens, new_p, EL_STR("Colon")); + el_val_t r = parse_expr(tokens, new_p); el_val_t val_node = el_get_field(r, EL_STR("node")); - p = el_get_field(r, EL_STR("pos")); + new_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); + el_val_t k3 = tok_kind(tokens, new_p); if (str_eq(k3, EL_STR("Comma"))) { + new_p = (new_p + 1); + } + if (new_p <= p) { p = (p + 1); + } else { + p = new_p; } } } @@ -1022,13 +1073,70 @@ el_val_t parse_primary(el_val_t tokens, el_val_t pos) { 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); } + if (str_eq(k, EL_STR("Target"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("To"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Via"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Deploy"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Reason"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Times"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Fallback"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Retry"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Parallel"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Trace"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Requires"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Where"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("As"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("With"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Manager"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Engine"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Accessor"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } + if (str_eq(k, EL_STR("Vessel"))) { + return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); + } 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 prev_no_block = state_get(EL_STR("__no_block_expr")); + state_set(EL_STR("__no_block_expr"), EL_STR("1")); el_val_t r = parse_expr(tokens, p); + state_set(EL_STR("__no_block_expr"), prev_no_block); 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); @@ -1059,7 +1167,10 @@ 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 p = expect(tokens, pos, EL_STR("Match")); + el_val_t prev_no_block = state_get(EL_STR("__no_block_expr")); + state_set(EL_STR("__no_block_expr"), EL_STR("1")); el_val_t r = parse_expr(tokens, p); + state_set(EL_STR("__no_block_expr"), prev_no_block); 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")); @@ -1121,7 +1232,10 @@ el_val_t parse_for_expr(el_val_t tokens, el_val_t pos) { el_val_t item_name = tok_value(tokens, p); p = (p + 1); p = expect(tokens, p, EL_STR("In")); + el_val_t prev_no_block = state_get(EL_STR("__no_block_expr")); + state_set(EL_STR("__no_block_expr"), EL_STR("1")); el_val_t r = parse_expr(tokens, p); + state_set(EL_STR("__no_block_expr"), prev_no_block); 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); @@ -1145,8 +1259,13 @@ el_val_t parse_block(el_val_t tokens, el_val_t pos) { } 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")); + el_val_t new_p = el_get_field(r, EL_STR("pos")); stmts = native_list_append(stmts, stmt); + if (new_p <= p) { + p = (p + 1); + } else { + p = new_p; + } } } } @@ -1483,7 +1602,10 @@ el_val_t parse_stmt(el_val_t tokens, el_val_t pos) { } if (str_eq(k, EL_STR("While"))) { el_val_t p = (pos + 1); + el_val_t prev_no_block = state_get(EL_STR("__no_block_expr")); + state_set(EL_STR("__no_block_expr"), EL_STR("1")); el_val_t r = parse_expr(tokens, p); + state_set(EL_STR("__no_block_expr"), prev_no_block); 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); @@ -1496,7 +1618,10 @@ el_val_t parse_stmt(el_val_t tokens, el_val_t pos) { el_val_t item_name = tok_value(tokens, p); p = (p + 1); p = expect(tokens, p, EL_STR("In")); + el_val_t prev_no_block = state_get(EL_STR("__no_block_expr")); + state_set(EL_STR("__no_block_expr"), EL_STR("1")); el_val_t r = parse_expr(tokens, p); + state_set(EL_STR("__no_block_expr"), prev_no_block); 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); @@ -1810,6 +1935,12 @@ el_val_t cg_expr(el_val_t expr) { 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 (is_int_expr(left)) { + if (is_int_expr(right)) { + 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("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(")")); @@ -1818,49 +1949,7 @@ el_val_t cg_expr(el_val_t expr) { 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("Ident"))) { - if (str_eq(right_kind, EL_STR("Ident"))) { - el_val_t lname = el_get_field(left, EL_STR("name")); - el_val_t rname = el_get_field(right, EL_STR("name")); - if (is_int_name(lname)) { - if (is_int_name(rname)) { - 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("Ident"))) { - if (str_eq(right_kind, EL_STR("Call"))) { - el_val_t lname = el_get_field(left, EL_STR("name")); - if (is_int_name(lname)) { - if (is_int_call(right)) { - 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("Ident"))) { - if (str_eq(left_kind, EL_STR("Call"))) { - el_val_t rname = el_get_field(right, EL_STR("name")); - if (is_int_name(rname)) { - if (is_int_call(left)) { - 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"))) { - if (str_eq(right_kind, EL_STR("Call"))) { - if (is_int_call(left)) { - if (is_int_call(right)) { - 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(")")); - } - } - } 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"))) { @@ -1909,6 +1998,13 @@ el_val_t cg_expr(el_val_t expr) { } } } + /* Mixed Ident/BinOp int comparison (e.g. `i == n - 1`): use is_int_expr + to detect both sides are integers before falling through to str_eq. */ + if (is_int_expr(left)) { + if (is_int_expr(right)) { + 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(")")); } @@ -1952,6 +2048,12 @@ el_val_t cg_expr(el_val_t expr) { } } } + /* Same mixed Ident/BinOp fix as EqEq for NotEq. */ + if (is_int_expr(left)) { + if (is_int_expr(right)) { + 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(")")); } @@ -1993,6 +2095,7 @@ el_val_t cg_expr(el_val_t expr) { if (str_eq(func_kind, EL_STR("Ident"))) { el_val_t fn_name = el_get_field(func, EL_STR("name")); cap_check_call(fn_name); + arity_check_call(fn_name, arity); 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"))) { @@ -2046,6 +2149,9 @@ el_val_t cg_expr(el_val_t expr) { 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); + if (n == 0) { + return EL_STR("el_map_new(0)"); + } el_val_t items = EL_STR(""); el_val_t i = 0; while (i < n) { @@ -2066,9 +2172,7 @@ el_val_t cg_expr(el_val_t expr) { 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 cg_if_expr(expr); } if (str_eq(kind, EL_STR("Match"))) { return cg_match(expr); @@ -2142,6 +2246,77 @@ el_val_t cg_match(el_val_t expr) { return 0; } +el_val_t next_if_id(void) { + el_val_t csv = state_get(EL_STR("__if_expr_counter")); + el_val_t n = 0; + if (!str_eq(csv, EL_STR(""))) { + n = str_to_int(csv); + } + n = (n + 1); + state_set(EL_STR("__if_expr_counter"), native_int_to_str(n)); + return native_int_to_str(n); + return 0; +} + +el_val_t cg_if_expr_arm(el_val_t stmts, el_val_t result_var) { + el_val_t n = native_list_len(stmts); + el_val_t out = EL_STR(""); + el_val_t i = 0; + while (i < n) { + el_val_t s = native_list_get(stmts, i); + el_val_t sk = el_get_field(s, EL_STR("stmt")); + el_val_t is_last = 0; + /* BUG FIX: was str_eq(i, n-1) which segfaults comparing ints as strings */ + if (i == (n - 1)) { + is_last = 1; + } + if (str_eq(sk, EL_STR("Let"))) { + el_val_t name = el_get_field(s, EL_STR("name")); + el_val_t val = el_get_field(s, EL_STR("value")); + el_val_t val_c = cg_expr(val); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("el_val_t ")), name), EL_STR(" = ")), val_c), EL_STR("; ")); + } else { + if (str_eq(sk, EL_STR("Return"))) { + el_val_t val = el_get_field(s, EL_STR("value")); + el_val_t val_c = cg_expr(val); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, result_var), EL_STR(" = (")), val_c), EL_STR("); ")); + } else { + if (str_eq(sk, EL_STR("Expr"))) { + el_val_t val = el_get_field(s, EL_STR("value")); + el_val_t val_c = cg_expr(val); + if (is_last) { + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, result_var), EL_STR(" = (")), val_c), EL_STR("); ")); + } else { + out = el_str_concat(el_str_concat(el_str_concat(out, EL_STR("(void)(")), val_c), EL_STR("); ")); + } + } else { + } + } + } + i = (i + 1); + } + return out; + return 0; +} + +el_val_t cg_if_expr(el_val_t expr) { + 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); + el_val_t id = next_if_id(); + el_val_t result_var = el_str_concat(EL_STR("_if_result_"), id); + el_val_t then_c = cg_if_expr_arm(then_stmts, result_var); + el_val_t else_c = EL_STR(""); + if (has_else) { + else_c = cg_if_expr_arm(else_stmts, result_var); + } + el_val_t out = 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(el_str_concat(EL_STR("({ el_val_t "), result_var), EL_STR(" = 0; if (")), cond_c), EL_STR(") { ")), then_c), EL_STR("} else { ")), else_c), EL_STR("} ")), result_var), EL_STR("; })")); + return out; + 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; @@ -2475,6 +2650,88 @@ el_val_t is_int_call(el_val_t call_expr) { return 0; } +el_val_t is_int_expr(el_val_t expr) { + el_val_t k = el_get_field(expr, EL_STR("expr")); + if (str_eq(k, EL_STR("Int"))) { + return 1; + } + if (str_eq(k, EL_STR("Ident"))) { + el_val_t name = el_get_field(expr, EL_STR("name")); + return is_int_name(name); + } + if (str_eq(k, EL_STR("Call"))) { + return is_int_call(expr); + } + if (str_eq(k, EL_STR("Neg"))) { + return is_int_expr(el_get_field(expr, EL_STR("inner"))); + } + if (str_eq(k, EL_STR("Not"))) { + return 1; + } + if (str_eq(k, EL_STR("BinOp"))) { + el_val_t op = el_get_field(expr, EL_STR("op")); + if (str_eq(op, EL_STR("EqEq"))) { + return 1; + } + if (str_eq(op, EL_STR("NotEq"))) { + return 1; + } + if (str_eq(op, EL_STR("Lt"))) { + return 1; + } + if (str_eq(op, EL_STR("Gt"))) { + return 1; + } + if (str_eq(op, EL_STR("LtEq"))) { + return 1; + } + if (str_eq(op, EL_STR("GtEq"))) { + return 1; + } + if (str_eq(op, EL_STR("And"))) { + return 1; + } + if (str_eq(op, EL_STR("Or"))) { + return 1; + } + if (str_eq(op, EL_STR("Plus"))) { + if (is_int_expr(el_get_field(expr, EL_STR("left")))) { + if (is_int_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + return 0; + } + if (str_eq(op, EL_STR("Minus"))) { + if (is_int_expr(el_get_field(expr, EL_STR("left")))) { + if (is_int_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + return 0; + } + if (str_eq(op, EL_STR("Star"))) { + if (is_int_expr(el_get_field(expr, EL_STR("left")))) { + if (is_int_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + return 0; + } + if (str_eq(op, EL_STR("Slash"))) { + if (is_int_expr(el_get_field(expr, EL_STR("left")))) { + if (is_int_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + return 0; + } + return 0; + } + return 0; + return 0; +} + el_val_t cap_record_violation(el_val_t kind, el_val_t fn_name) { el_val_t csv = state_get(EL_STR("__cap_violations")); if (str_eq(csv, EL_STR(""))) { @@ -2610,6 +2867,524 @@ el_val_t emit_cap_violations(void) { return 0; } +el_val_t builtin_arity(el_val_t name) { + if (str_eq(name, EL_STR("println"))) { + return 1; + } + if (str_eq(name, EL_STR("print"))) { + return 1; + } + if (str_eq(name, EL_STR("readline"))) { + return 0; + } + if (str_eq(name, EL_STR("el_str_concat"))) { + return 2; + } + if (str_eq(name, EL_STR("str_eq"))) { + return 2; + } + if (str_eq(name, EL_STR("str_starts_with"))) { + return 2; + } + if (str_eq(name, EL_STR("str_ends_with"))) { + return 2; + } + if (str_eq(name, EL_STR("str_len"))) { + return 1; + } + if (str_eq(name, EL_STR("str_concat"))) { + return 2; + } + if (str_eq(name, EL_STR("int_to_str"))) { + return 1; + } + if (str_eq(name, EL_STR("str_to_int"))) { + return 1; + } + if (str_eq(name, EL_STR("str_slice"))) { + return 3; + } + if (str_eq(name, EL_STR("str_contains"))) { + return 2; + } + if (str_eq(name, EL_STR("str_replace"))) { + return 3; + } + if (str_eq(name, EL_STR("str_to_upper"))) { + return 1; + } + if (str_eq(name, EL_STR("str_to_lower"))) { + return 1; + } + if (str_eq(name, EL_STR("str_trim"))) { + return 1; + } + if (str_eq(name, EL_STR("str_index_of"))) { + return 2; + } + if (str_eq(name, EL_STR("str_split"))) { + return 2; + } + if (str_eq(name, EL_STR("str_char_at"))) { + return 2; + } + if (str_eq(name, EL_STR("str_char_code"))) { + return 2; + } + if (str_eq(name, EL_STR("str_pad_left"))) { + return 3; + } + if (str_eq(name, EL_STR("str_pad_right"))) { + return 3; + } + if (str_eq(name, EL_STR("str_format"))) { + return 2; + } + if (str_eq(name, EL_STR("str_lower"))) { + return 1; + } + if (str_eq(name, EL_STR("str_upper"))) { + return 1; + } + if (str_eq(name, EL_STR("el_abs"))) { + return 1; + } + if (str_eq(name, EL_STR("el_max"))) { + return 2; + } + if (str_eq(name, EL_STR("el_min"))) { + return 2; + } + if (str_eq(name, EL_STR("el_list_len"))) { + return 1; + } + if (str_eq(name, EL_STR("el_list_get"))) { + return 2; + } + if (str_eq(name, EL_STR("el_list_append"))) { + return 2; + } + if (str_eq(name, EL_STR("el_list_empty"))) { + return 0; + } + if (str_eq(name, EL_STR("el_list_clone"))) { + return 1; + } + if (str_eq(name, EL_STR("list_push"))) { + return 2; + } + if (str_eq(name, EL_STR("list_push_front"))) { + return 2; + } + if (str_eq(name, EL_STR("list_join"))) { + return 2; + } + if (str_eq(name, EL_STR("list_range"))) { + return 2; + } + if (str_eq(name, EL_STR("el_get_field"))) { + return 2; + } + if (str_eq(name, EL_STR("el_map_get"))) { + return 2; + } + if (str_eq(name, EL_STR("el_map_set"))) { + return 3; + } + if (str_eq(name, EL_STR("http_get"))) { + return 1; + } + if (str_eq(name, EL_STR("http_post"))) { + return 2; + } + if (str_eq(name, EL_STR("http_post_json"))) { + return 2; + } + if (str_eq(name, EL_STR("http_get_with_headers"))) { + return 2; + } + if (str_eq(name, EL_STR("http_post_with_headers"))) { + return 3; + } + if (str_eq(name, EL_STR("http_post_form_auth"))) { + return 3; + } + if (str_eq(name, EL_STR("http_get_to_file"))) { + return 3; + } + if (str_eq(name, EL_STR("http_post_to_file"))) { + return 4; + } + if (str_eq(name, EL_STR("http_serve"))) { + return 2; + } + if (str_eq(name, EL_STR("http_set_handler"))) { + return 1; + } + if (str_eq(name, EL_STR("fs_read"))) { + return 1; + } + if (str_eq(name, EL_STR("fs_write"))) { + return 2; + } + if (str_eq(name, EL_STR("fs_write_bytes"))) { + return 3; + } + if (str_eq(name, EL_STR("fs_list"))) { + return 1; + } + if (str_eq(name, EL_STR("json_get"))) { + return 2; + } + if (str_eq(name, EL_STR("json_parse"))) { + return 1; + } + if (str_eq(name, EL_STR("json_stringify"))) { + return 1; + } + if (str_eq(name, EL_STR("json_get_string"))) { + return 2; + } + if (str_eq(name, EL_STR("json_get_int"))) { + return 2; + } + if (str_eq(name, EL_STR("json_get_float"))) { + return 2; + } + if (str_eq(name, EL_STR("json_get_bool"))) { + return 2; + } + if (str_eq(name, EL_STR("json_get_raw"))) { + return 2; + } + if (str_eq(name, EL_STR("json_set"))) { + return 3; + } + if (str_eq(name, EL_STR("json_array_len"))) { + return 1; + } + if (str_eq(name, EL_STR("time_now"))) { + return 0; + } + if (str_eq(name, EL_STR("time_now_utc"))) { + return 0; + } + if (str_eq(name, EL_STR("sleep_secs"))) { + return 1; + } + if (str_eq(name, EL_STR("sleep_ms"))) { + return 1; + } + if (str_eq(name, EL_STR("time_format"))) { + return 2; + } + if (str_eq(name, EL_STR("time_to_parts"))) { + return 1; + } + if (str_eq(name, EL_STR("time_from_parts"))) { + return 3; + } + if (str_eq(name, EL_STR("time_add"))) { + return 3; + } + if (str_eq(name, EL_STR("time_diff"))) { + return 3; + } + if (str_eq(name, EL_STR("uuid_new"))) { + return 0; + } + if (str_eq(name, EL_STR("uuid_v4"))) { + return 0; + } + if (str_eq(name, EL_STR("env"))) { + return 1; + } + if (str_eq(name, EL_STR("state_set"))) { + return 2; + } + if (str_eq(name, EL_STR("state_get"))) { + return 1; + } + if (str_eq(name, EL_STR("state_del"))) { + return 1; + } + if (str_eq(name, EL_STR("state_keys"))) { + return 0; + } + if (str_eq(name, EL_STR("float_to_str"))) { + return 1; + } + if (str_eq(name, EL_STR("int_to_float"))) { + return 1; + } + if (str_eq(name, EL_STR("float_to_int"))) { + return 1; + } + if (str_eq(name, EL_STR("format_float"))) { + return 2; + } + if (str_eq(name, EL_STR("decimal_round"))) { + return 2; + } + if (str_eq(name, EL_STR("str_to_float"))) { + return 1; + } + if (str_eq(name, EL_STR("math_sqrt"))) { + return 1; + } + if (str_eq(name, EL_STR("math_log"))) { + return 1; + } + if (str_eq(name, EL_STR("math_ln"))) { + return 1; + } + if (str_eq(name, EL_STR("math_sin"))) { + return 1; + } + if (str_eq(name, EL_STR("math_cos"))) { + return 1; + } + if (str_eq(name, EL_STR("math_pi"))) { + return 0; + } + if (str_eq(name, EL_STR("bool_to_str"))) { + return 1; + } + if (str_eq(name, EL_STR("exit_program"))) { + return 1; + } + if (str_eq(name, EL_STR("dharma_connect"))) { + return 1; + } + if (str_eq(name, EL_STR("dharma_send"))) { + return 2; + } + if (str_eq(name, EL_STR("dharma_activate"))) { + return 1; + } + if (str_eq(name, EL_STR("dharma_emit"))) { + return 2; + } + if (str_eq(name, EL_STR("dharma_field"))) { + return 1; + } + if (str_eq(name, EL_STR("dharma_strengthen"))) { + return 2; + } + if (str_eq(name, EL_STR("dharma_relationship"))) { + return 1; + } + if (str_eq(name, EL_STR("dharma_peers"))) { + return 0; + } + if (str_eq(name, EL_STR("engram_node"))) { + return 3; + } + if (str_eq(name, EL_STR("engram_node_full"))) { + return 8; + } + if (str_eq(name, EL_STR("engram_get_node"))) { + return 1; + } + if (str_eq(name, EL_STR("engram_strengthen"))) { + return 1; + } + if (str_eq(name, EL_STR("engram_forget"))) { + return 1; + } + if (str_eq(name, EL_STR("engram_node_count"))) { + return 0; + } + if (str_eq(name, EL_STR("engram_search"))) { + return 2; + } + if (str_eq(name, EL_STR("engram_scan_nodes"))) { + return 2; + } + if (str_eq(name, EL_STR("engram_connect"))) { + return 4; + } + if (str_eq(name, EL_STR("engram_edge_between"))) { + return 2; + } + if (str_eq(name, EL_STR("engram_neighbors"))) { + return 1; + } + if (str_eq(name, EL_STR("engram_neighbors_filtered"))) { + return 3; + } + if (str_eq(name, EL_STR("engram_edge_count"))) { + return 0; + } + if (str_eq(name, EL_STR("engram_activate"))) { + return 2; + } + if (str_eq(name, EL_STR("engram_save"))) { + return 1; + } + if (str_eq(name, EL_STR("engram_load"))) { + return 1; + } + if (str_eq(name, EL_STR("engram_get_node_json"))) { + return 1; + } + if (str_eq(name, EL_STR("engram_search_json"))) { + return 2; + } + if (str_eq(name, EL_STR("engram_scan_nodes_json"))) { + return 2; + } + if (str_eq(name, EL_STR("engram_neighbors_json"))) { + return 3; + } + if (str_eq(name, EL_STR("engram_activate_json"))) { + return 2; + } + if (str_eq(name, EL_STR("engram_stats_json"))) { + return 0; + } + if (str_eq(name, EL_STR("llm_call"))) { + return 2; + } + if (str_eq(name, EL_STR("llm_call_system"))) { + return 3; + } + if (str_eq(name, EL_STR("llm_call_agentic"))) { + return 4; + } + if (str_eq(name, EL_STR("llm_vision"))) { + return 4; + } + if (str_eq(name, EL_STR("llm_models"))) { + return 0; + } + if (str_eq(name, EL_STR("llm_register_tool"))) { + return 2; + } + if (str_eq(name, EL_STR("sha256_hex"))) { + return 1; + } + if (str_eq(name, EL_STR("sha256_bytes"))) { + return 1; + } + if (str_eq(name, EL_STR("hmac_sha256_hex"))) { + return 2; + } + if (str_eq(name, EL_STR("hmac_sha256_bytes"))) { + return 2; + } + if (str_eq(name, EL_STR("base64_encode"))) { + return 1; + } + if (str_eq(name, EL_STR("base64_decode"))) { + return 1; + } + if (str_eq(name, EL_STR("base64url_encode"))) { + return 1; + } + if (str_eq(name, EL_STR("base64url_decode"))) { + return 1; + } + if (str_eq(name, EL_STR("native_list_get"))) { + return 2; + } + if (str_eq(name, EL_STR("native_list_len"))) { + return 1; + } + if (str_eq(name, EL_STR("native_list_append"))) { + return 2; + } + if (str_eq(name, EL_STR("native_list_empty"))) { + return 0; + } + if (str_eq(name, EL_STR("native_list_clone"))) { + return 1; + } + if (str_eq(name, EL_STR("native_string_chars"))) { + return 1; + } + if (str_eq(name, EL_STR("native_int_to_str"))) { + return 1; + } + if (str_eq(name, EL_STR("append"))) { + return 2; + } + if (str_eq(name, EL_STR("len"))) { + return 1; + } + if (str_eq(name, EL_STR("get"))) { + return 2; + } + if (str_eq(name, EL_STR("map_get"))) { + return 2; + } + if (str_eq(name, EL_STR("map_set"))) { + return 3; + } + return (-1); + return 0; +} + +el_val_t arity_record_violation(el_val_t fn_name, el_val_t expected, el_val_t actual) { + el_val_t csv = state_get(EL_STR("__arity_violations")); + if (str_eq(csv, EL_STR(""))) { + csv = EL_STR(","); + } + el_val_t entry = el_str_concat(el_str_concat(el_str_concat(el_str_concat(fn_name, EL_STR("|")), native_int_to_str(expected)), EL_STR("|")), native_int_to_str(actual)); + el_val_t key = el_str_concat(el_str_concat(EL_STR(","), entry), EL_STR(",")); + if (str_contains(csv, key)) { + return 1; + } + state_set(EL_STR("__arity_violations"), el_str_concat(el_str_concat(csv, entry), EL_STR(","))); + return 1; + return 0; +} + +el_val_t arity_check_call(el_val_t fn_name, el_val_t actual) { + el_val_t expected = builtin_arity(fn_name); + if (expected < 0) { + return 1; + } + if (expected == actual) { + return 1; + } + arity_record_violation(fn_name, expected, actual); + return 1; + return 0; +} + +el_val_t emit_arity_violations(void) { + el_val_t csv = state_get(EL_STR("__arity_violations")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + if (str_eq(csv, EL_STR(","))) { + return 0; + } + el_val_t n = str_len(csv); + el_val_t i = 1; + while (i < n) { + el_val_t next_comma = str_index_of(str_slice(csv, i, n), EL_STR(",")); + if (next_comma < 0) { + return 0; + } + el_val_t entry = str_slice(csv, i, (i + next_comma)); + el_val_t p1 = str_index_of(entry, EL_STR("|")); + if (p1 > 0) { + el_val_t fn_name = str_slice(entry, 0, p1); + el_val_t rest = str_slice(entry, (p1 + 1), str_len(entry)); + el_val_t p2 = str_index_of(rest, EL_STR("|")); + if (p2 > 0) { + el_val_t exp_s = str_slice(rest, 0, p2); + el_val_t act_s = str_slice(rest, (p2 + 1), str_len(rest)); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("#error \"arity error: '"), fn_name), EL_STR("' takes ")), exp_s), EL_STR(" arguments, but called with ")), act_s), EL_STR("\""))); + } + } + i = ((i + next_comma) + 1); + } + return 0; +} + el_val_t add_int_name(el_val_t name) { el_val_t csv = state_get(EL_STR("__int_names")); if (str_eq(csv, EL_STR(""))) { @@ -2940,6 +3715,7 @@ el_val_t codegen(el_val_t stmts, el_val_t source) { } state_set(EL_STR("__program_kind"), kind); state_set(EL_STR("__cap_violations"), EL_STR("")); + state_set(EL_STR("__arity_violations"), EL_STR("")); emit_line(EL_STR("#include ")); emit_line(EL_STR("#include ")); emit_line(EL_STR("#include \"el_runtime.h\"")); @@ -2960,11 +3736,44 @@ el_val_t codegen(el_val_t stmts, el_val_t source) { i = (i + 1); } emit_blank(); + el_val_t has_toplevel_lets = 0; + i = 0; + while (i < n) { + el_val_t stmt = native_list_get(stmts, i); + 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 ltype = el_get_field(stmt, EL_STR("type")); + if (str_eq(ltype, EL_STR("Int"))) { + add_int_name(name); + } + el_val_t val = el_get_field(stmt, EL_STR("value")); + el_val_t vk = el_get_field(val, EL_STR("expr")); + if (str_eq(vk, EL_STR("Int"))) { + add_int_name(name); + } + emit_line(el_str_concat(el_str_concat(EL_STR("el_val_t "), name), EL_STR(";"))); + has_toplevel_lets = 1; + } + i = (i + 1); + } + if (has_toplevel_lets) { + emit_blank(); + } + /* fndef_mask[i] = 1 means stmts[i] was a FnDef and has been released. + * Subsequent passes use this mask instead of calling is_fndef() on freed + * nodes, avoiding use-after-free while still freeing each FnDef body as + * soon as cg_fn() is done with it. This is the fix for the SIGSEGV that + * occurs on large source files (>~100 KB) due to AST accumulation. */ + char* fndef_mask = calloc((size_t)n, 1); i = 0; while (i < n) { el_val_t stmt = native_list_get(stmts, i); if (is_fndef(stmt)) { + fndef_mask[i] = 1; cg_fn(stmt); + /* FnDef body is now fully emitted — release to reclaim AST memory. */ + el_release(stmt); } i = (i + 1); } @@ -2990,20 +3799,952 @@ el_val_t codegen(el_val_t stmts, el_val_t source) { 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 { + if (!fndef_mask[i]) { + el_val_t stmt = native_list_get(stmts, i); + 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")); + main_decl = native_list_append(main_decl, name); + } + } + i = (i + 1); + } + i = 0; + while (i < n) { + if (!fndef_mask[i]) { + el_val_t stmt = native_list_get(stmts, i); + if (!is_top_level_decl(stmt)) { main_decl = cg_stmt(stmt, EL_STR(" "), main_decl); } } i = (i + 1); } + free(fndef_mask); emit_line(EL_STR(" return 0;")); emit_line(EL_STR("}")); emit_blank(); emit_cap_violations(); + emit_arity_violations(); + return EL_STR(""); + return 0; +} + +el_val_t js_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 js_str_lit(el_val_t s) { + return el_str_concat(el_str_concat(EL_STR("\""), js_escape(s)), EL_STR("\"")); + return 0; +} + +el_val_t js_emit_line(el_val_t line) { + println(line); + return 0; +} + +el_val_t js_emit_blank(void) { + println(EL_STR("")); + return 0; +} + +el_val_t js_binop(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("Percent"))) { + 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 js_is_int_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__js_int_names")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","))); + return 0; +} + +el_val_t js_add_int_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__js_int_names")); + if (str_eq(csv, EL_STR(""))) { + csv; + EL_NULL; + EL_STR(","); + } + el_val_t key = el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(",")); + if (str_contains(csv, key)) { + return 1; + } + state_set(EL_STR("__js_int_names"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + +el_val_t js_build_int_names_for_params(el_val_t params) { + state_set(EL_STR("__js_int_names"), EL_STR(",")); + 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")); + el_val_t ptype = el_get_field(param, EL_STR("type")); + if (str_eq(ptype, EL_STR("Int"))) { + js_add_int_name(pname); + } + pi = (pi + 1); + } + return 1; + return 0; +} + +el_val_t js_is_int_call(el_val_t call_expr) { + el_val_t func = el_get_field(call_expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (!str_eq(fk, EL_STR("Ident"))) { + return 0; + } + el_val_t name = el_get_field(func, EL_STR("name")); + if (str_eq(name, EL_STR("str_len"))) { + return 1; + } + if (str_eq(name, EL_STR("str_index_of"))) { + return 1; + } + if (str_eq(name, EL_STR("str_to_int"))) { + return 1; + } + if (str_eq(name, EL_STR("str_char_code"))) { + return 1; + } + if (str_eq(name, EL_STR("native_list_len"))) { + return 1; + } + if (str_eq(name, EL_STR("el_list_len"))) { + return 1; + } + if (str_eq(name, EL_STR("len"))) { + return 1; + } + if (str_eq(name, EL_STR("json_get_int"))) { + return 1; + } + if (str_eq(name, EL_STR("time_now"))) { + return 1; + } + if (str_eq(name, EL_STR("time_now_utc"))) { + return 1; + } + if (str_eq(name, EL_STR("el_abs"))) { + return 1; + } + if (str_eq(name, EL_STR("el_max"))) { + return 1; + } + if (str_eq(name, EL_STR("el_min"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t js_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 js_str_lit(v); + } + 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("true"); + } + return EL_STR("false"); + } + if (str_eq(kind, EL_STR("Nil"))) { + return EL_STR("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 = js_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 = js_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 = js_cg_expr(left); + el_val_t right_c = js_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"))) { + 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("Ident"))) { + if (str_eq(right_kind, EL_STR("Ident"))) { + el_val_t lname = el_get_field(left, EL_STR("name")); + el_val_t rname = el_get_field(right, EL_STR("name")); + if (js_is_int_name(lname)) { + if (js_is_int_name(rname)) { + 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("Ident"))) { + if (str_eq(right_kind, EL_STR("Call"))) { + el_val_t lname = el_get_field(left, EL_STR("name")); + if (js_is_int_name(lname)) { + if (js_is_int_call(right)) { + 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("Ident"))) { + if (str_eq(left_kind, EL_STR("Call"))) { + el_val_t rname = el_get_field(right, EL_STR("name")); + if (js_is_int_name(rname)) { + if (js_is_int_call(left)) { + 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("Call"))) { + if (str_eq(right_kind, EL_STR("Call"))) { + if (js_is_int_call(left)) { + if (js_is_int_call(right)) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" + ")), right_c), EL_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("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("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("Nil"))) { + 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("Nil"))) { + 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("Ident"))) { + if (str_eq(right_kind, EL_STR("Ident"))) { + el_val_t lname = el_get_field(left, EL_STR("name")); + el_val_t rname = el_get_field(right, EL_STR("name")); + if (js_is_int_name(lname)) { + if (js_is_int_name(rname)) { + 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(")")); + } + 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(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("Nil"))) { + 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("Nil"))) { + 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("Ident"))) { + if (str_eq(right_kind, EL_STR("Ident"))) { + el_val_t lname = el_get_field(left, EL_STR("name")); + el_val_t rname = el_get_field(right, EL_STR("name")); + if (js_is_int_name(lname)) { + if (js_is_int_name(rname)) { + 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(")")); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" !== ")), right_c), EL_STR(")")); + } + el_val_t op_c = js_binop(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 = js_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 = js_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 = js_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 = js_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(", ")), js_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 = js_cg_expr(obj); + el_val_t idx_c = js_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); + if (n == 0) { + return EL_STR("[]"); + } + 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 = js_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("["), 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); + if (n == 0) { + return EL_STR("{}"); + } + 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 = js_cg_expr(val); + if (i > 0) { + items = el_str_concat(items, EL_STR(", ")); + } + items = el_str_concat(el_str_concat(el_str_concat(items, js_str_lit(key)), EL_STR(": ")), val_c); + i = (i + 1); + } + return el_str_concat(el_str_concat(EL_STR("{"), items), EL_STR("}")); + } + if (str_eq(kind, EL_STR("Try"))) { + el_val_t inner = el_get_field(expr, EL_STR("inner")); + return js_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 = js_cg_expr(cond); + return el_str_concat(el_str_concat(EL_STR("("), cond_c), EL_STR(" ? 1 : 0)")); + } + if (str_eq(kind, EL_STR("Match"))) { + return js_cg_match(expr); + } + return EL_STR("null"); + return 0; +} + +el_val_t js_next_match_id(void) { + el_val_t csv = state_get(EL_STR("__js_match_counter")); + el_val_t n = 0; + if (!str_eq(csv, EL_STR(""))) { + n = str_to_int(csv); + } + n = (n + 1); + state_set(EL_STR("__js_match_counter"), native_int_to_str(n)); + return native_int_to_str(n); + return 0; +} + +el_val_t js_cg_match(el_val_t expr) { + el_val_t subject = el_get_field(expr, EL_STR("subject")); + el_val_t arms = el_get_field(expr, EL_STR("arms")); + el_val_t subj_c = js_cg_expr(subject); + el_val_t id = js_next_match_id(); + el_val_t subj_var = el_str_concat(EL_STR("_match_subj_"), id); + el_val_t out = el_str_concat(el_str_concat(EL_STR("(("), subj_var), EL_STR(") => { ")); + el_val_t n = native_list_len(arms); + el_val_t i = 0; + while (i < n) { + el_val_t arm = native_list_get(arms, i); + el_val_t pat = el_get_field(arm, EL_STR("pattern")); + el_val_t body = el_get_field(arm, EL_STR("body")); + el_val_t pkind = el_get_field(pat, EL_STR("pattern")); + el_val_t body_c = js_cg_expr(body); + if (str_eq(pkind, EL_STR("Wildcard"))) { + out = el_str_concat(el_str_concat(el_str_concat(out, EL_STR("return (")), body_c), EL_STR("); ")); + } else { + if (str_eq(pkind, EL_STR("Binding"))) { + el_val_t bname = el_get_field(pat, EL_STR("name")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("{ const ")), bname), EL_STR(" = ")), subj_var), EL_STR("; return (")), body_c), EL_STR("); } ")); + } else { + if (str_eq(pkind, EL_STR("LitInt"))) { + el_val_t v = el_get_field(pat, EL_STR("value")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("if (")), subj_var), EL_STR(" === ")), v), EL_STR(") return (")), body_c), EL_STR("); ")); + } else { + if (str_eq(pkind, EL_STR("LitStr"))) { + el_val_t v = el_get_field(pat, EL_STR("value")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("if (str_eq(")), subj_var), EL_STR(", ")), js_str_lit(v)), EL_STR(")) return (")), body_c), EL_STR("); ")); + } else { + if (str_eq(pkind, EL_STR("LitBool"))) { + el_val_t v = el_get_field(pat, EL_STR("value")); + el_val_t bv = EL_STR("false"); + if (str_eq(v, EL_STR("true"))) { + bv = EL_STR("true"); + } + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, EL_STR("if (")), subj_var), EL_STR(" === ")), bv), EL_STR(") return (")), body_c), EL_STR("); ")); + } else { + out = el_str_concat(el_str_concat(el_str_concat(out, EL_STR("return (")), body_c), EL_STR("); ")); + } + } + } + } + } + i = (i + 1); + } + out = el_str_concat(el_str_concat(el_str_concat(out, EL_STR("return null; })(")), subj_c), EL_STR(")")); + return out; + return 0; +} + +el_val_t js_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 js_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 = js_cg_expr(val); + el_val_t ltype = el_get_field(stmt, EL_STR("type")); + if (str_eq(ltype, EL_STR("Int"))) { + js_add_int_name(name); + } + el_val_t vk = el_get_field(val, EL_STR("expr")); + if (str_eq(vk, EL_STR("Int"))) { + js_add_int_name(name); + } + if (js_list_contains(declared, name)) { + js_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 { + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("let ")), 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"))) { + js_emit_line(el_str_concat(indent, EL_STR("return null;"))); + } else { + el_val_t val_c = js_cg_expr(val); + js_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"))) { + js_cg_if_stmt(val, indent, declared); + return declared; + } + if (str_eq(val_kind, EL_STR("For"))) { + js_cg_for_stmt(val, indent, declared); + return declared; + } + el_val_t val_c = js_cg_expr(val); + js_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 = js_cg_expr(cond); + cond_c = js_strip_outer_parens(cond_c); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("while (")), cond_c), EL_STR(") {"))); + js_cg_stmts(body, el_str_concat(indent, EL_STR(" ")), native_list_clone(declared)); + js_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")); + js_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; + } + if (str_eq(kind, EL_STR("CgiBlock"))) { + el_val_t cname = el_get_field(stmt, EL_STR("name")); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// cgi block '")), cname), EL_STR("' — no-op in JS target (server-side concept)"))); + return declared; + } + if (str_eq(kind, EL_STR("ServiceBlock"))) { + el_val_t sname = el_get_field(stmt, EL_STR("name")); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// service block '")), sname), EL_STR("' — no-op in JS target"))); + return declared; + } + return declared; + return 0; +} + +el_val_t js_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 js_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 = js_cg_expr(cond); + cond_c = js_strip_outer_parens(cond_c); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("if (")), cond_c), EL_STR(") {"))); + js_cg_stmts(then_stmts, el_str_concat(indent, EL_STR(" ")), native_list_clone(declared)); + if (has_else) { + js_emit_line(el_str_concat(indent, EL_STR("} else {"))); + js_cg_stmts(else_stmts, el_str_concat(indent, EL_STR(" ")), native_list_clone(declared)); + } + js_emit_line(el_str_concat(indent, EL_STR("}"))); + return 0; +} + +el_val_t js_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 = js_cg_expr(list_expr); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("for (const ")), item), EL_STR(" of ")), list_c), EL_STR(") {"))); + el_val_t body_decl = native_list_clone(declared); + body_decl = native_list_append(body_decl, item); + js_cg_stmts(body, el_str_concat(indent, EL_STR(" ")), body_decl); + js_emit_line(el_str_concat(indent, EL_STR("}"))); + return 0; +} + +el_val_t js_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")); + js_cg_for_body(item, list_expr, body, indent, declared); + return 0; +} + +el_val_t js_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 = js_cg_stmt(stmt, indent, decl); + i = (i + 1); + } + return decl; + return 0; +} + +el_val_t js_params_str(el_val_t params) { + el_val_t n = native_list_len(params); + if (n == 0) { + return EL_STR(""); + } + 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 name = el_get_field(param, EL_STR("name")); + if (i > 0) { + out = el_str_concat(out, EL_STR(", ")); + } + out = el_str_concat(out, name); + i = (i + 1); + } + return out; + return 0; +} + +el_val_t js_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 js_cg_fn(el_val_t stmt) { + el_val_t fn_name = el_get_field(stmt, EL_STR("name")); + 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_str = js_params_str(params); + js_build_int_names_for_params(params); + if (str_eq(fn_name, EL_STR("main"))) { + js_emit_line(el_str_concat(el_str_concat(EL_STR("function main("), params_str), EL_STR(") {"))); + } else { + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("function "), fn_name), EL_STR("(")), params_str), 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 = js_transform_implicit_return(body); + } + js_cg_stmts(body_xformed, EL_STR(" "), decl); + js_emit_line(EL_STR("}")); + js_emit_blank(); + return 0; +} + +el_val_t js_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 js_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; + } + if (str_eq(kind, EL_STR("CgiBlock"))) { + return 1; + } + if (str_eq(kind, EL_STR("ServiceBlock"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t codegen_js(el_val_t stmts, el_val_t source) { + state_set(EL_STR("__js_int_names"), EL_STR("")); + state_set(EL_STR("__js_match_counter"), EL_STR("")); + js_emit_line(EL_STR("// Generated by elc --target=js")); + js_emit_line(EL_STR("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")); + js_emit_line(EL_STR("import \"./el_runtime.js\";")); + js_emit_line(EL_STR("const {")); + js_emit_line(EL_STR(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,")); + js_emit_line(EL_STR(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,")); + js_emit_line(EL_STR(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,")); + js_emit_line(EL_STR(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,")); + js_emit_line(EL_STR(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,")); + js_emit_line(EL_STR(" list_push, list_join, list_range,")); + js_emit_line(EL_STR(" el_map_new, el_get_field, el_map_get, el_map_set,")); + js_emit_line(EL_STR(" http_get, http_post, http_post_json,")); + js_emit_line(EL_STR(" fs_read, fs_write, fs_list,")); + js_emit_line(EL_STR(" json_parse, json_stringify, json_get, json_get_string, json_get_int,")); + js_emit_line(EL_STR(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,")); + js_emit_line(EL_STR(" el_retain, el_release,")); + js_emit_line(EL_STR(" append, len, get, map_get, map_set,")); + js_emit_line(EL_STR(" native_list_get, native_list_len, native_list_append, native_list_empty,")); + js_emit_line(EL_STR(" native_list_clone, native_string_chars, native_int_to_str,")); + js_emit_line(EL_STR(" args, state_set, state_get, state_del, state_keys, env,")); + js_emit_line(EL_STR(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,")); + js_emit_line(EL_STR(" engram_node, engram_search, engram_activate,")); + js_emit_line(EL_STR(" llm_call, llm_call_system,")); + js_emit_line(EL_STR("} = globalThis.__el;")); + js_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); + if (js_is_fndef(stmt)) { + js_cg_fn(stmt); + } + i = (i + 1); + } + el_val_t has_main = 0; + i = 0; + while (i < n) { + el_val_t stmt = native_list_get(stmts, i); + el_val_t sk = el_get_field(stmt, EL_STR("stmt")); + if (str_eq(sk, EL_STR("FnDef"))) { + el_val_t fn_name = el_get_field(stmt, EL_STR("name")); + if (str_eq(fn_name, EL_STR("main"))) { + has_main = 1; + } + } + i = (i + 1); + } + el_val_t main_decl = native_list_empty(); + i = 0; + while (i < n) { + el_val_t stmt = native_list_get(stmts, i); + if (js_is_fndef(stmt)) { + } else { + if (js_is_top_level_decl(stmt)) { + } else { + main_decl = js_cg_stmt(stmt, EL_STR(""), main_decl); + } + } + i = (i + 1); + } + if (has_main) { + js_emit_blank(); + js_emit_line(EL_STR("main();")); + } return EL_STR(""); return 0; } @@ -3011,16 +4752,74 @@ el_val_t codegen(el_val_t stmts, el_val_t source) { el_val_t compile(el_val_t source) { el_val_t tokens = lex(source); el_val_t stmts = parse(tokens); + /* Token list no longer needed after parsing — release before codegen. */ + el_release(tokens); return codegen(stmts, source); return 0; } +el_val_t compile_js(el_val_t source) { + el_val_t tokens = lex(source); + el_val_t stmts = parse(tokens); + /* Token list no longer needed after parsing — release before codegen. */ + el_release(tokens); + return codegen_js(stmts, source); + return 0; +} + +el_val_t compile_dispatch(el_val_t tgt, el_val_t source) { + if (str_eq(tgt, EL_STR("js"))) { + return compile_js(source); + } + return compile(source); + return 0; +} + +el_val_t detect_target(el_val_t argv) { + el_val_t n = native_list_len(argv); + el_val_t i = 0; + while (i < n) { + el_val_t a = native_list_get(argv, i); + if (str_starts_with(a, EL_STR("--target="))) { + el_val_t v = str_slice(a, 9, str_len(a)); + return v; + } + i = (i + 1); + } + return EL_STR("c"); + return 0; +} + +el_val_t strip_flags(el_val_t argv) { + el_val_t out = native_list_empty(); + el_val_t n = native_list_len(argv); + el_val_t i = 0; + while (i < n) { + el_val_t a = native_list_get(argv, i); + if (!str_starts_with(a, EL_STR("--"))) { + out = native_list_append(out, a); + } + i = (i + 1); + } + return out; + 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); + _argv = args(); + _target = detect_target(_argv); + _positional = strip_flags(_argv); + _argc = native_list_len(_positional); + if (_argc >= 1) { + el_val_t _src_path = native_list_get(_positional, 0); + el_val_t _source = fs_read(_src_path); + el_val_t _out = compile_dispatch(_target, _source); + if (_argc >= 2) { + el_val_t _out_path = native_list_get(_positional, 1); + fs_write(_out_path, _out); + } + } return 0; } diff --git a/el-compiler/src/parser.el b/el-compiler/src/parser.el index ca8a904..f4759ce 100644 --- a/el-compiler/src/parser.el +++ b/el-compiler/src/parser.el @@ -209,7 +209,20 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { } // Map literal: { "key": val, ... } + // + // Suppression: when parse_if / parse_while / parse_for / parse_match + // are parsing a head expression, they set __no_block_expr=1 so a stray + // `{` here doesn't get gobbled as a Map literal — it belongs to the + // following block. Without this, `if a || b { ... }` could mis-parse + // (the `||` recursion lands at `{` and tries to read the if-body as a + // Map, then loops forever when keys don't match `Str: expr`). if k == "LBrace" { + let no_block: String = state_get("__no_block_expr") + if str_eq(no_block, "1") { + // Fall through to fallback — caller will see `{` and treat it + // as the start of the block they're expecting. + return make_result({ "expr": "Nil" }, pos) + } let p = pos + 1 let pairs: [Map] = native_list_empty() let running = true @@ -223,16 +236,23 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { } 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 new_p: Int = p + 1 + let new_p = expect(tokens, new_p, "Colon") + let r = parse_expr(tokens, new_p) let val_node = r["node"] - let p = r["pos"] + let new_p = r["pos"] let pair = { "key": key, "value": val_node } let pairs = native_list_append(pairs, pair) - let k3 = tok_kind(tokens, p) + let k3 = tok_kind(tokens, new_p) if k3 == "Comma" { + let new_p = new_p + 1 + } + // Non-progress guard: malformed map content can leave + // parse_expr returning the same pos. Force advance. + if new_p <= p { let p = p + 1 + } else { + let p = new_p } } } @@ -275,13 +295,44 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { return make_result({ "expr": "Neg", "inner": inner }, p) } + // Soft keywords usable as identifiers in expression position. The lexer + // turns words like `target`, `to`, `via`, `deploy`, etc. into dedicated + // token kinds for the deploy/retry DSLs, but they're also valid as + // parameter names and local variables. When one of these appears in + // expression position (where only an Ident makes sense), treat it as + // an Ident carrying the original text — otherwise references to a + // parameter named `target` compile to EL_NULL. + if k == "Target" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "To" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Via" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Deploy" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Reason" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Times" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Fallback" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Retry" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Parallel" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Trace" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Requires" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Where" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "As" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "With" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Manager" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Engine" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Accessor" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Vessel" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + // Fallback: skip unknown token make_result({ "expr": "Nil" }, pos + 1) } fn parse_if(tokens: [Map], pos: Int) -> Map { let p = expect(tokens, pos, "If") + // Suppress Map-literal parsing in the cond so a stray `{` (the start + // of the then-block) isn't gobbled as a Map. + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) let cond = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) @@ -312,7 +363,10 @@ fn parse_if(tokens: [Map], pos: Int) -> Map { fn parse_match(tokens: [Map], pos: Int) -> Map { let p = expect(tokens, pos, "Match") + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) let subject = r["node"] let p = r["pos"] let p = expect(tokens, p, "LBrace") @@ -374,7 +428,10 @@ fn parse_for_expr(tokens: [Map], pos: Int) -> Map { let item_name = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "In") + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) let list_expr = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) @@ -397,8 +454,17 @@ fn parse_block(tokens: [Map], pos: Int) -> Map { } else { let r = parse_stmt(tokens, p) let stmt = r["node"] - let p = r["pos"] + let new_p: Int = r["pos"] let stmts = native_list_append(stmts, stmt) + // Non-progress guard: a malformed input (e.g. `||` that + // dragged the parser into Map-literal mode partway through + // the if-body) can leave parse_stmt returning the same pos. + // Force advance so the outer compile doesn't hang. + if new_p <= p { + let p = p + 1 + } else { + let p = new_p + } } } } @@ -712,7 +778,10 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { // while loop if k == "While" { let p = pos + 1 + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) let cond = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) @@ -727,7 +796,10 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { let item_name = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "In") + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) let list_expr = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) diff --git a/elc-combined.el b/elc-combined.el index e068919..90d400f 100644 --- a/elc-combined.el +++ b/elc-combined.el @@ -1,4 +1,3 @@ -// elc-combined.el // lexer.el — el self-hosting lexer // // Tokenises an el source string into a list of token maps. @@ -746,7 +745,20 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { } // Map literal: { "key": val, ... } + // + // Suppression: when parse_if / parse_while / parse_for / parse_match + // are parsing a head expression, they set __no_block_expr=1 so a stray + // `{` here doesn't get gobbled as a Map literal — it belongs to the + // following block. Without this, `if a || b { ... }` could mis-parse + // (the `||` recursion lands at `{` and tries to read the if-body as a + // Map, then loops forever when keys don't match `Str: expr`). if k == "LBrace" { + let no_block: String = state_get("__no_block_expr") + if str_eq(no_block, "1") { + // Fall through to fallback — caller will see `{` and treat it + // as the start of the block they're expecting. + return make_result({ "expr": "Nil" }, pos) + } let p = pos + 1 let pairs: [Map] = native_list_empty() let running = true @@ -760,16 +772,23 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { } 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 new_p: Int = p + 1 + let new_p = expect(tokens, new_p, "Colon") + let r = parse_expr(tokens, new_p) let val_node = r["node"] - let p = r["pos"] + let new_p = r["pos"] let pair = { "key": key, "value": val_node } let pairs = native_list_append(pairs, pair) - let k3 = tok_kind(tokens, p) + let k3 = tok_kind(tokens, new_p) if k3 == "Comma" { + let new_p = new_p + 1 + } + // Non-progress guard: malformed map content can leave + // parse_expr returning the same pos. Force advance. + if new_p <= p { let p = p + 1 + } else { + let p = new_p } } } @@ -812,13 +831,44 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { return make_result({ "expr": "Neg", "inner": inner }, p) } + // Soft keywords usable as identifiers in expression position. The lexer + // turns words like `target`, `to`, `via`, `deploy`, etc. into dedicated + // token kinds for the deploy/retry DSLs, but they're also valid as + // parameter names and local variables. When one of these appears in + // expression position (where only an Ident makes sense), treat it as + // an Ident carrying the original text — otherwise references to a + // parameter named `target` compile to EL_NULL. + if k == "Target" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "To" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Via" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Deploy" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Reason" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Times" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Fallback" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Retry" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Parallel" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Trace" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Requires" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Where" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "As" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "With" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Manager" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Engine" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Accessor" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + if k == "Vessel" { return make_result({ "expr": "Ident", "name": v }, pos + 1) } + // Fallback: skip unknown token make_result({ "expr": "Nil" }, pos + 1) } fn parse_if(tokens: [Map], pos: Int) -> Map { let p = expect(tokens, pos, "If") + // Suppress Map-literal parsing in the cond so a stray `{` (the start + // of the then-block) isn't gobbled as a Map. + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) let cond = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) @@ -849,7 +899,10 @@ fn parse_if(tokens: [Map], pos: Int) -> Map { fn parse_match(tokens: [Map], pos: Int) -> Map { let p = expect(tokens, pos, "Match") + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) let subject = r["node"] let p = r["pos"] let p = expect(tokens, p, "LBrace") @@ -911,7 +964,10 @@ fn parse_for_expr(tokens: [Map], pos: Int) -> Map { let item_name = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "In") + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) let list_expr = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) @@ -934,8 +990,17 @@ fn parse_block(tokens: [Map], pos: Int) -> Map { } else { let r = parse_stmt(tokens, p) let stmt = r["node"] - let p = r["pos"] + let new_p: Int = r["pos"] let stmts = native_list_append(stmts, stmt) + // Non-progress guard: a malformed input (e.g. `||` that + // dragged the parser into Map-literal mode partway through + // the if-body) can leave parse_stmt returning the same pos. + // Force advance so the outer compile doesn't hang. + if new_p <= p { + let p = p + 1 + } else { + let p = new_p + } } } } @@ -1249,7 +1314,10 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { // while loop if k == "While" { let p = pos + 1 + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) let cond = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) @@ -1264,7 +1332,10 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { let item_name = tok_value(tokens, p) let p = p + 1 let p = expect(tokens, p, "In") + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") let r = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) let list_expr = r["node"] let p = r["pos"] let r2 = parse_block(tokens, p) @@ -1615,7 +1686,23 @@ fn cg_expr(expr: Map) -> String { 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) + // Type-driven dispatch via recursive is_int_expr: any expression + // whose value is provably Int (literal, typed Ident, known-Int + // builtin, or BinOp arithmetic over Ints) participates in + // arithmetic, not string concat. Recursion into BinOp lets + // `a + b + c` (chained Int adds) and `acc * 16 + d` route to + // arithmetic instead of falling to el_str_concat — both sides + // are Int so the outer `+` is too. + if is_int_expr(left) { + if is_int_expr(right) { + let op_c: String = binop_to_c(op) + return "(" + left_c + " " + op_c + " " + right_c + ")" + } + } + // Mixed cases: at least one side is provably Int but the other + // is not provably anything. Historical heuristic biases to + // arithmetic when a literal Int is present (preserves prior + // behaviour for `pos + 1` where `pos` is an untyped param). if left_kind == "Int" { let op_c: String = binop_to_c(op) return "(" + left_c + " " + op_c + " " + right_c + ")" @@ -1624,57 +1711,9 @@ fn cg_expr(expr: Map) -> String { let op_c: String = binop_to_c(op) return "(" + left_c + " " + op_c + " " + right_c + ")" } - // Type-driven dispatch: if both sides are Idents declared - // with type Int (parameters annotated `: Int` or let bindings - // annotated `: Int`), this is arithmetic, not concat. The - // current-function int-name set is maintained by cg_fn / - // cg_stmt via state_set("__int_names", csv). - if left_kind == "Ident" { - if right_kind == "Ident" { - let lname: String = left["name"] - let rname: String = right["name"] - if is_int_name(lname) { - if is_int_name(rname) { - let op_c: String = binop_to_c(op) - return "(" + left_c + " " + op_c + " " + right_c + ")" - } - } - } - } - // Same dispatch for Ident-Int + Call-to-known-Int-builtin (and the - // mirror). Without this, expressions like `pos + str_len(s)` get - // string-concatenated. is_int_call walks a known-builtin list. - if left_kind == "Ident" { - if right_kind == "Call" { - let lname: String = left["name"] - if is_int_name(lname) { - if is_int_call(right) { - let op_c: String = binop_to_c(op) - return "(" + left_c + " " + op_c + " " + right_c + ")" - } - } - } - } - if right_kind == "Ident" { - if left_kind == "Call" { - let rname: String = right["name"] - if is_int_name(rname) { - if is_int_call(left) { - let op_c: String = binop_to_c(op) - return "(" + left_c + " " + op_c + " " + right_c + ")" - } - } - } - } + // Otherwise: BinOp(+) with a Call/Ident side without int-typed + // evidence — fall back to string concat (the historical default). if left_kind == "Call" { - if right_kind == "Call" { - if is_int_call(left) { - if is_int_call(right) { - let op_c: String = binop_to_c(op) - return "(" + left_c + " " + op_c + " " + right_c + ")" - } - } - } return "el_str_concat(" + left_c + ", " + right_c + ")" } if right_kind == "Call" { @@ -1692,8 +1731,6 @@ fn cg_expr(expr: Map) -> String { return "el_str_concat(" + left_c + ", " + right_c + ")" } } - // Ident + Ident or Ident + unknown without int-typed evidence — - // fall back to string concat (the historical heuristic). if left_kind == "Ident" { return "el_str_concat(" + left_c + ", " + right_c + ")" } @@ -1704,9 +1741,10 @@ fn cg_expr(expr: Map) -> String { // String equality: use str_eq() when either side is a string literal or ident. // Use plain == when comparing integer literals OR when both sides are - // identifiers tracked in __int_names. Without the int-name check, - // `seen == idx` between two Int locals miscompiles to str_eq on what - // are integer values dressed as char* — segfault. + // identifiers tracked in __int_names (typed Int via `let x: Int = ...`). + // Without the int-name check, `seen == idx` between two Int locals + // miscompiles to str_eq(seen, idx), strcmp'ing what are integer values + // dressed as char* — segfault on the first non-printable byte. if op == "EqEq" { if left_kind == "Int" { return "(" + left_c + " == " + right_c + ")" @@ -1731,6 +1769,16 @@ fn cg_expr(expr: Map) -> String { } } } + // Extend int-equality to mixed Ident/BinOp cases: `i == n - 1` + // where the left is an int-name Ident and the right is an + // arithmetic BinOp (or vice-versa). Without this check the + // fallthrough to str_eq produces str_eq(int_value, int_value) + // which reads the integer as a char* and segfaults. + if is_int_expr(left) { + if is_int_expr(right) { + return "(" + left_c + " == " + right_c + ")" + } + } if left_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" } @@ -1775,6 +1823,13 @@ fn cg_expr(expr: Map) -> String { } } } + // Same mixed Ident/BinOp fix as EqEq: use is_int_expr to detect + // integer-typed operands before falling through to !str_eq. + if is_int_expr(left) { + if is_int_expr(right) { + return "(" + left_c + " != " + right_c + ")" + } + } if left_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" } @@ -1825,6 +1880,12 @@ fn cg_expr(expr: Map) -> String { // violations to be emitted as #error directives at the // top of the generated C, so cc fails with a clear msg. cap_check_call(fn_name) + // Arity check against the builtin table — refuse, with a clear + // El-source message, when a known builtin gets the wrong arg + // count (e.g. `http_serve(port)` instead of `http_serve(port, + // handler)`). User-defined fns and variadic builtins pass + // through (builtin_arity returns -1). + arity_check_call(fn_name, arity) return fn_name + "(" + args_c + ")" } @@ -1846,6 +1907,11 @@ fn cg_expr(expr: Map) -> String { let obj = expr["object"] let field: String = expr["field"] let obj_c: String = cg_expr(obj) + // el_get_field takes el_val_t for both args, so the field name + // string literal must be wrapped in EL_STR(). Without the wrap + // the C compiler treats the bare const char* as an int64 (warns + // -Wint-conversion) and the runtime reads gibberish at the address + // when looking up the key. return "el_get_field(" + obj_c + ", EL_STR(" + c_str_lit(field) + "))" } @@ -1869,6 +1935,8 @@ fn cg_expr(expr: Map) -> String { if kind == "Array" { let elems = expr["elems"] let n: Int = native_list_len(elems) + // Empty literal: el_list_new(0, ) generates malformed C (trailing + // comma in a varargs call). Emit el_list_empty() directly. if n == 0 { return "el_list_empty()" } let items = "" let i = 0 @@ -1887,6 +1955,11 @@ fn cg_expr(expr: Map) -> String { if kind == "Map" { let pairs = expr["pairs"] let n: Int = native_list_len(pairs) + // Empty literal: `el_map_new(0, )` is malformed C (trailing comma in + // a varargs call). Emit `el_map_new(0)` directly so empty-map + // shadowing inside for/while/if bodies — `let acc: Map = {}` — + // doesn't fail downstream cc with parse errors. + if n == 0 { return "el_map_new(0)" } let items = "" let i = 0 while i < n { @@ -1909,9 +1982,7 @@ fn cg_expr(expr: Map) -> String { } 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)" + return cg_if_expr(expr) } if kind == "Match" { @@ -1990,6 +2061,89 @@ fn cg_match(expr: Map) -> String { out } +// ── If-as-expression codegen ───────────────────────────────────────────────── +// +// Lower `if cond { thenBody } else { elseBody }` used in expression position +// (e.g. `let x = if a { b } else { c }`) to a GCC/Clang statement-expression +// so the actual arm bodies are evaluated, not just `(cond ? 1 : 0)`. +// +// Each arm body is a list of statements; the result of the arm is the value +// of its final Expr statement (mirroring transform_implicit_return at function +// scope). Statements before the final Expr are emitted as expression-statements +// for their side effects. + +fn next_if_id() -> String { + let csv: String = state_get("__if_expr_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__if_expr_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +// Render a single arm of the if-as-expression: emit each statement-before-last +// as a side-effecting expression, then assign the final Expr's value to the +// result var. If the arm body is empty or its last stmt isn't an Expr, the +// result var stays at its initial 0. +fn cg_if_expr_arm(stmts: [Map], result_var: String) -> String { + let n: Int = native_list_len(stmts) + let out = "" + let i = 0 + while i < n { + let s = native_list_get(stmts, i) + let sk: String = s["stmt"] + let is_last: Bool = false + if i == n - 1 { let is_last = true } + if str_eq(sk, "Let") { + let name: String = s["name"] + let val = s["value"] + let val_c: String = cg_expr(val) + let out = out + "el_val_t " + name + " = " + val_c + "; " + } else { + if str_eq(sk, "Return") { + let val = s["value"] + let val_c: String = cg_expr(val) + let out = out + result_var + " = (" + val_c + "); " + } else { + if str_eq(sk, "Expr") { + let val = s["value"] + let val_c: String = cg_expr(val) + if is_last { + let out = out + result_var + " = (" + val_c + "); " + } else { + let out = out + "(void)(" + val_c + "); " + } + } else { + // Non-trivial stmt kinds (While/For) shouldn't appear in + // expression-position arm bodies; emit nothing rather + // than malformed C. + } + } + } + let i = i + 1 + } + out +} + +fn cg_if_expr(expr: Map) -> String { + let cond = expr["cond"] + let then_stmts = expr["then"] + let else_stmts = expr["else"] + let has_else: Bool = expr["has_else"] + let cond_c: String = cg_expr(cond) + let id: String = next_if_id() + let result_var: String = "_if_result_" + id + let then_c: String = cg_if_expr_arm(then_stmts, result_var) + let else_c: String = "" + if has_else { + let else_c = cg_if_expr_arm(else_stmts, result_var) + } + let out: String = "({ el_val_t " + result_var + " = 0; if (" + cond_c + ") { " + then_c + "} else { " + else_c + "} " + result_var + "; })" + out +} + // ── Variable scope tracking ─────────────────────────────────────────────────── // // El allows `let x = expr` to both declare and reassign x in the same scope. @@ -2075,6 +2229,9 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "while (" + cond_c + ") {") + // Body lives in its own C block — clone so let-bindings inside the + // loop don't leak into the parent's `declared` list (which would make + // a sibling scope's `let x` emit assignment on an undeclared name). cg_stmts(body, indent + " ", native_list_clone(declared)) emit_line(indent + "}") return declared @@ -2145,6 +2302,11 @@ fn cg_if_stmt(expr: Map, indent: String, declared: [String]) -> Voi let cond_c: String = cg_expr(cond) let cond_c = strip_outer_parens(cond_c) emit_line(indent + "if (" + cond_c + ") {") + // Each branch gets its own clone of `declared` — variables let-bound + // inside the then/else block live only in that C scope, and must not + // leak back to the parent (or to the sibling branch) through shared + // list mutation. Cheap shallow copy; the entries (variable name strings) + // are shared. cg_stmts(then_stmts, indent + " ", native_list_clone(declared)) if has_else { emit_line(indent + "} else {") @@ -2163,6 +2325,9 @@ fn cg_for_body(item: String, list_expr: Map, body: [Map) -> Bool { return false } +// Recursive type-propagation: is `expr` known-Int at codegen time? +// This unifies the BinOp(+) dispatch so chained arithmetic over Int +// operands stays arithmetic. Without recursion, a wrapping `+` between +// `BinOp(+) of two Ints` and another Int falls to el_str_concat because +// the outer dispatch only checks the immediate kind, not the inner. +// +// Rules: +// Int literal → Int +// Ident in __int_names → Int +// Call to known-Int builtin → Int +// Neg of Int → Int +// BinOp arithmetic of two Ints → Int (Plus, Minus, Star, Slash, Percent) +// BinOp comparison/logical → Int (yields 0/1; safe to treat as Int) +// anything else → not provably Int +fn is_int_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "Int") { return true } + if str_eq(k, "Ident") { + let name: String = expr["name"] + return is_int_name(name) + } + if str_eq(k, "Call") { + return is_int_call(expr) + } + if str_eq(k, "Neg") { + return is_int_expr(expr["inner"]) + } + if str_eq(k, "Not") { + return true + } + if str_eq(k, "BinOp") { + let op: String = expr["op"] + // Comparisons and logicals always yield 0/1 — safe Int. + if str_eq(op, "EqEq") { return true } + if str_eq(op, "NotEq") { return true } + if str_eq(op, "Lt") { return true } + if str_eq(op, "Gt") { return true } + if str_eq(op, "LtEq") { return true } + if str_eq(op, "GtEq") { return true } + if str_eq(op, "And") { return true } + if str_eq(op, "Or") { return true } + // Arithmetic propagates: Int op Int → Int. + if str_eq(op, "Plus") { + if is_int_expr(expr["left"]) { + if is_int_expr(expr["right"]) { return true } + } + return false + } + if str_eq(op, "Minus") { + if is_int_expr(expr["left"]) { + if is_int_expr(expr["right"]) { return true } + } + return false + } + if str_eq(op, "Star") { + if is_int_expr(expr["left"]) { + if is_int_expr(expr["right"]) { return true } + } + return false + } + if str_eq(op, "Slash") { + if is_int_expr(expr["left"]) { + if is_int_expr(expr["right"]) { return true } + } + return false + } + return false + } + return false +} + // ── Capability-kind enforcement ────────────────────────────────────────────── // // A program's top-level block (cgi / service / none) determines which @@ -2398,6 +2634,243 @@ fn emit_cap_violations() -> Void { } } +// ── Builtin arity table ─────────────────────────────────────────────────────── +// +// El programs sometimes call runtime builtins with the wrong number of +// arguments (e.g. `http_serve(port)` instead of `http_serve(port, handler)`). +// Without this check the generated C compiles to a call with too few / +// too many args and fails downstream cc with a generic "too few arguments" +// message that doesn't point to the El source line. +// +// Strategy: a small static table mirrors el_runtime.h. Variadic builtins +// (el_list_new, el_map_new, args) and unknown identifiers (user fns, +// dynamic dispatch) return -1 → no check. A mismatch records a violation +// in process state, which emit_arity_violations() turns into #error +// directives at the top of the generated C. +fn builtin_arity(name: String) -> Int { + // I/O + if str_eq(name, "println") { return 1 } + if str_eq(name, "print") { return 1 } + if str_eq(name, "readline") { return 0 } + // String + if str_eq(name, "el_str_concat") { return 2 } + if str_eq(name, "str_eq") { return 2 } + if str_eq(name, "str_starts_with") { return 2 } + if str_eq(name, "str_ends_with") { return 2 } + if str_eq(name, "str_len") { return 1 } + if str_eq(name, "str_concat") { return 2 } + if str_eq(name, "int_to_str") { return 1 } + if str_eq(name, "str_to_int") { return 1 } + if str_eq(name, "str_slice") { return 3 } + if str_eq(name, "str_contains") { return 2 } + if str_eq(name, "str_replace") { return 3 } + if str_eq(name, "str_to_upper") { return 1 } + if str_eq(name, "str_to_lower") { return 1 } + if str_eq(name, "str_trim") { return 1 } + if str_eq(name, "str_index_of") { return 2 } + if str_eq(name, "str_split") { return 2 } + if str_eq(name, "str_char_at") { return 2 } + if str_eq(name, "str_char_code") { return 2 } + if str_eq(name, "str_pad_left") { return 3 } + if str_eq(name, "str_pad_right") { return 3 } + if str_eq(name, "str_format") { return 2 } + if str_eq(name, "str_lower") { return 1 } + if str_eq(name, "str_upper") { return 1 } + // Math + if str_eq(name, "el_abs") { return 1 } + if str_eq(name, "el_max") { return 2 } + if str_eq(name, "el_min") { return 2 } + // List + if str_eq(name, "el_list_len") { return 1 } + if str_eq(name, "el_list_get") { return 2 } + if str_eq(name, "el_list_append") { return 2 } + if str_eq(name, "el_list_empty") { return 0 } + if str_eq(name, "el_list_clone") { return 1 } + if str_eq(name, "list_push") { return 2 } + if str_eq(name, "list_push_front") { return 2 } + if str_eq(name, "list_join") { return 2 } + if str_eq(name, "list_range") { return 2 } + // Map + if str_eq(name, "el_get_field") { return 2 } + if str_eq(name, "el_map_get") { return 2 } + if str_eq(name, "el_map_set") { return 3 } + // HTTP + if str_eq(name, "http_get") { return 1 } + if str_eq(name, "http_post") { return 2 } + if str_eq(name, "http_post_json") { return 2 } + if str_eq(name, "http_get_with_headers") { return 2 } + if str_eq(name, "http_post_with_headers") { return 3 } + if str_eq(name, "http_post_form_auth") { return 3 } + if str_eq(name, "http_serve") { return 2 } + if str_eq(name, "http_set_handler") { return 1 } + // Filesystem + if str_eq(name, "fs_read") { return 1 } + if str_eq(name, "fs_write") { return 2 } + if str_eq(name, "fs_list") { return 1 } + // JSON + if str_eq(name, "json_get") { return 2 } + if str_eq(name, "json_parse") { return 1 } + if str_eq(name, "json_stringify") { return 1 } + if str_eq(name, "json_get_string") { return 2 } + if str_eq(name, "json_get_int") { return 2 } + if str_eq(name, "json_get_float") { return 2 } + if str_eq(name, "json_get_bool") { return 2 } + if str_eq(name, "json_get_raw") { return 2 } + if str_eq(name, "json_set") { return 3 } + if str_eq(name, "json_array_len") { return 1 } + // Time + if str_eq(name, "time_now") { return 0 } + if str_eq(name, "time_now_utc") { return 0 } + if str_eq(name, "sleep_secs") { return 1 } + if str_eq(name, "sleep_ms") { return 1 } + if str_eq(name, "time_format") { return 2 } + if str_eq(name, "time_to_parts") { return 1 } + if str_eq(name, "time_from_parts") { return 3 } + if str_eq(name, "time_add") { return 3 } + if str_eq(name, "time_diff") { return 3 } + // UUID + if str_eq(name, "uuid_new") { return 0 } + if str_eq(name, "uuid_v4") { return 0 } + // Env / state + if str_eq(name, "env") { return 1 } + if str_eq(name, "state_set") { return 2 } + if str_eq(name, "state_get") { return 1 } + if str_eq(name, "state_del") { return 1 } + if str_eq(name, "state_keys") { return 0 } + // Float + if str_eq(name, "float_to_str") { return 1 } + if str_eq(name, "int_to_float") { return 1 } + if str_eq(name, "float_to_int") { return 1 } + if str_eq(name, "format_float") { return 2 } + if str_eq(name, "decimal_round") { return 2 } + if str_eq(name, "str_to_float") { return 1 } + // Math (Float) + if str_eq(name, "math_sqrt") { return 1 } + if str_eq(name, "math_log") { return 1 } + if str_eq(name, "math_ln") { return 1 } + if str_eq(name, "math_sin") { return 1 } + if str_eq(name, "math_cos") { return 1 } + if str_eq(name, "math_pi") { return 0 } + // Bool + if str_eq(name, "bool_to_str") { return 1 } + // Process + if str_eq(name, "exit_program") { return 1 } + // CGI / DHARMA + if str_eq(name, "dharma_connect") { return 1 } + if str_eq(name, "dharma_send") { return 2 } + if str_eq(name, "dharma_activate") { return 1 } + if str_eq(name, "dharma_emit") { return 2 } + if str_eq(name, "dharma_field") { return 1 } + if str_eq(name, "dharma_strengthen") { return 2 } + if str_eq(name, "dharma_relationship") { return 1 } + if str_eq(name, "dharma_peers") { return 0 } + // Engram + if str_eq(name, "engram_node") { return 3 } + if str_eq(name, "engram_node_full") { return 8 } + if str_eq(name, "engram_get_node") { return 1 } + if str_eq(name, "engram_strengthen") { return 1 } + if str_eq(name, "engram_forget") { return 1 } + if str_eq(name, "engram_node_count") { return 0 } + if str_eq(name, "engram_search") { return 2 } + if str_eq(name, "engram_scan_nodes") { return 2 } + if str_eq(name, "engram_connect") { return 4 } + if str_eq(name, "engram_edge_between") { return 2 } + if str_eq(name, "engram_neighbors") { return 1 } + if str_eq(name, "engram_neighbors_filtered") { return 3 } + if str_eq(name, "engram_edge_count") { return 0 } + if str_eq(name, "engram_activate") { return 2 } + if str_eq(name, "engram_save") { return 1 } + if str_eq(name, "engram_load") { return 1 } + if str_eq(name, "engram_get_node_json") { return 1 } + if str_eq(name, "engram_search_json") { return 2 } + if str_eq(name, "engram_scan_nodes_json") { return 2 } + if str_eq(name, "engram_neighbors_json") { return 3 } + if str_eq(name, "engram_activate_json") { return 2 } + if str_eq(name, "engram_stats_json") { return 0 } + // LLM + if str_eq(name, "llm_call") { return 2 } + if str_eq(name, "llm_call_system") { return 3 } + if str_eq(name, "llm_call_agentic") { return 4 } + if str_eq(name, "llm_vision") { return 4 } + if str_eq(name, "llm_models") { return 0 } + if str_eq(name, "llm_register_tool") { return 2 } + // Crypto + if str_eq(name, "sha256_hex") { return 1 } + if str_eq(name, "sha256_bytes") { return 1 } + if str_eq(name, "hmac_sha256_hex") { return 2 } + if str_eq(name, "hmac_sha256_bytes") { return 2 } + if str_eq(name, "base64_encode") { return 1 } + if str_eq(name, "base64_decode") { return 1 } + if str_eq(name, "base64url_encode") { return 1 } + if str_eq(name, "base64url_decode") { return 1 } + // Native VM aliases + if str_eq(name, "native_list_get") { return 2 } + if str_eq(name, "native_list_len") { return 1 } + if str_eq(name, "native_list_append") { return 2 } + if str_eq(name, "native_list_empty") { return 0 } + if str_eq(name, "native_list_clone") { return 1 } + if str_eq(name, "native_string_chars") { return 1 } + if str_eq(name, "native_int_to_str") { return 1 } + // Method-call aliases + if str_eq(name, "append") { return 2 } + if str_eq(name, "len") { return 1 } + if str_eq(name, "get") { return 2 } + if str_eq(name, "map_get") { return 2 } + if str_eq(name, "map_set") { return 3 } + // -1 sentinel: variadic / unknown / user-defined → no check. + return -1 +} + +fn arity_record_violation(fn_name: String, expected: Int, actual: Int) -> Bool { + let csv: String = state_get("__arity_violations") + if str_eq(csv, "") { let csv = "," } + // Encode as fn_name|expected|actual to recover all three at emit time. + let entry: String = fn_name + "|" + native_int_to_str(expected) + "|" + native_int_to_str(actual) + let key: String = "," + entry + "," + if str_contains(csv, key) { return true } + state_set("__arity_violations", csv + entry + ",") + return true +} + +// Validate the call's arity against the builtin table. Returns true (always) +// because cg_expr ignores the result; -1 from builtin_arity signals +// "no check possible" (variadic or user-defined). A mismatch is recorded +// and surfaced as an #error at the bottom of the generated C, so cc fails +// before it ever attempts to type-check the wrong call. +fn arity_check_call(fn_name: String, actual: Int) -> Bool { + let expected: Int = builtin_arity(fn_name) + if expected < 0 { return true } + if expected == actual { return true } + arity_record_violation(fn_name, expected, actual) + return true +} + +// Emit recorded arity violations as #error directives. +fn emit_arity_violations() -> Void { + let csv: String = state_get("__arity_violations") + if str_eq(csv, "") { return } + if str_eq(csv, ",") { return } + let n: Int = str_len(csv) + let i: Int = 1 + while i < n { + let next_comma: Int = str_index_of(str_slice(csv, i, n), ",") + if next_comma < 0 { return } + let entry: String = str_slice(csv, i, i + next_comma) + let p1: Int = str_index_of(entry, "|") + if p1 > 0 { + let fn_name: String = str_slice(entry, 0, p1) + let rest: String = str_slice(entry, p1 + 1, str_len(entry)) + let p2: Int = str_index_of(rest, "|") + if p2 > 0 { + let exp_s: String = str_slice(rest, 0, p2) + let act_s: String = str_slice(rest, p2 + 1, str_len(rest)) + emit_line("#error \"arity error: '" + fn_name + "' takes " + exp_s + " arguments, but called with " + act_s + "\"") + } + } + let i = i + next_comma + 1 + } +} + fn add_int_name(name: String) -> Bool { let csv: String = state_get("__int_names") if str_eq(csv, "") { csv = "," } @@ -2682,6 +3155,8 @@ fn codegen(stmts: [Map], source: String) -> String { state_set("__program_kind", kind) // Clear capability-violation accumulator from any prior compile. state_set("__cap_violations", "") + // Clear arity-violation accumulator from any prior compile. + state_set("__arity_violations", "") // Preamble emit_line("#include ") @@ -2707,6 +3182,40 @@ fn codegen(stmts: [Map], source: String) -> String { } emit_blank() + // Top-level `let` bindings → file-scope storage. El programs use + // top-level `let GREETING = "..."` as module constants that any + // function below should be able to read. Without this pass, a top- + // level Let only declares the name inside main()'s scope and any + // function referencing it compiles to an undefined-symbol use of + // the bare name (or, with non-static linkage, fails to link). + // + // We emit each top-level Let as `el_val_t NAME = VALUE;` at file + // scope and seed the int-name set when the binding is `: Int` so + // arithmetic/concat dispatch on the name works inside functions. + // Runtime-call initializers (e.g. `let m = el_map_new(...)`) cannot + // appear in C static initializers, so we emit a non-const slot and + // initialize it at the top of main() before any user statements run. + let has_toplevel_lets = false + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let kind: String = stmt["stmt"] + if str_eq(kind, "Let") { + let name: String = stmt["name"] + let ltype: String = stmt["type"] + if str_eq(ltype, "Int") { add_int_name(name) } + let val = stmt["value"] + let vk: String = val["expr"] + if str_eq(vk, "Int") { add_int_name(name) } + emit_line("el_val_t " + name + ";") + let has_toplevel_lets = true + } + let i = i + 1 + } + if has_toplevel_lets { + emit_blank() + } + // Function definitions let i = 0 while i < n { @@ -2737,8 +3246,21 @@ fn codegen(stmts: [Map], source: String) -> String { let arg_eng: String = cgi_arg(ceng, has_eng) emit_line(" el_cgi_init(" + arg_name + ", " + arg_did + ", " + arg_prin + ", " + arg_net + ", " + arg_eng + ");") } + // Seed `declared` with the names of every top-level Let so that + // cg_stmt emits plain assignment (`X = ...;`) instead of a redundant + // `el_val_t X = ...;` shadowing the file-scope slot. let main_decl = native_list_empty() let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let kind: String = stmt["stmt"] + if str_eq(kind, "Let") { + let name: String = stmt["name"] + let main_decl = native_list_append(main_decl, name) + } + let i = i + 1 + } + let i = 0 while i < n { let stmt = native_list_get(stmts, i) if is_fndef(stmt) { @@ -2750,6 +3272,11 @@ fn codegen(stmts: [Map], source: String) -> String { let main_decl = cg_stmt(stmt, " ", main_decl) } } + // Release AST node after final use — each stmt is fully processed + // by this point (forward decls, fn defs, top-level lets, and now + // the main-body pass are all done). Releasing here prevents the + // accumulated AST from exhausting memory on large source files. + el_release(stmt) let i = i + 1 } emit_line(" return 0;") @@ -2761,6 +3288,916 @@ fn codegen(stmts: [Map], source: String) -> String { // the bottom is fine — preprocessor errors halt the build wherever // they appear. emit_cap_violations() + // Same for builtin-arity violations: cc halts on the first #error, + // so a misuse of a known builtin (wrong arg count) fails the build + // with a clear message naming the builtin and its expected arity. + emit_arity_violations() + + // Return empty string — output was streamed via println + "" +} +// codegen-js.el — El compiler JavaScript source code generator +// +// Input: list of AST statement maps (from parser.el) +// Output: JavaScript source printed to stdout (streamed, one line at a time) +// +// Each El program compiles to a single .js file that imports el_runtime.js +// (which side-effects globals so call sites stay flat — println(x), not +// el.println(x)). Functions map to JS function declarations; top-level +// statements run at module load. +// +// Entry point: fn codegen_js(stmts: [Map], source: String) -> String +// Returns "" — output goes to stdout via println(). +// +// This file mirrors codegen.el (the C backend). Where the C backend has to +// fight the int64_t-everywhere convention to dispatch arithmetic vs concat +// or `==` vs `str_eq`, the JS backend can usually let JS's own operator +// semantics do the right thing. We retain the dispatch logic for clarity +// and so that explicit calls to `el_str_concat` or `str_eq` still work. + +// ── String helpers ──────────────────────────────────────────────────────────── + +// Escape a JS string literal (double-quotes, backslashes, newlines, etc.). +fn js_escape(s: String) -> String { + let chars: [String] = native_string_chars(s) + let total: Int = native_list_len(chars) + let 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 js_str_lit(s: String) -> String { + "\"" + js_escape(s) + "\"" +} + +// ── Code emission ───────────────────────────────────────────────────────────── + +fn js_emit_line(line: String) -> Void { + println(line) +} + +fn js_emit_blank() -> Void { + println("") +} + +// ── Operator helpers ────────────────────────────────────────────────────────── + +fn js_binop(op: String) -> String { + if op == "Plus" { return "+" } + if op == "Minus" { return "-" } + if op == "Star" { return "*" } + if op == "Slash" { return "/" } + if op == "Percent" { return "%" } + if op == "EqEq" { return "===" } + if op == "NotEq" { return "!==" } + if op == "Lt" { return "<" } + if op == "Gt" { return ">" } + if op == "LtEq" { return "<=" } + if op == "GtEq" { return ">=" } + if op == "And" { return "&&" } + if op == "Or" { return "||" } + op +} + +// ── Int-name tracking (mirrors codegen.el) ──────────────────────────────────── + +fn js_is_int_name(name: String) -> Bool { + let csv: String = state_get("__js_int_names") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + +fn js_add_int_name(name: String) -> Bool { + let csv: String = state_get("__js_int_names") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__js_int_names", csv + name + ",") + return true +} + +fn js_build_int_names_for_params(params: [Map]) -> Bool { + state_set("__js_int_names", ",") + let np: Int = native_list_len(params) + let pi = 0 + while pi < np { + let param = native_list_get(params, pi) + let pname: String = param["name"] + let ptype: String = param["type"] + if str_eq(ptype, "Int") { + js_add_int_name(pname) + } + let pi = pi + 1 + } + return true +} + +fn js_is_int_call(call_expr: Map) -> Bool { + let func = call_expr["func"] + let fk: String = func["expr"] + if !str_eq(fk, "Ident") { return false } + let name: String = func["name"] + if str_eq(name, "str_len") { return true } + if str_eq(name, "str_index_of") { return true } + if str_eq(name, "str_to_int") { return true } + if str_eq(name, "str_char_code") { return true } + if str_eq(name, "native_list_len") { return true } + if str_eq(name, "el_list_len") { return true } + if str_eq(name, "len") { return true } + if str_eq(name, "json_get_int") { return true } + if str_eq(name, "time_now") { return true } + if str_eq(name, "time_now_utc") { return true } + if str_eq(name, "el_abs") { return true } + if str_eq(name, "el_max") { return true } + if str_eq(name, "el_min") { return true } + return false +} + +// ── Expression codegen ──────────────────────────────────────────────────────── +// +// js_cg_expr returns a JS expression string (not a statement). +// +// Note: the C backend's `+` dispatch is preserved here for two reasons: +// 1) Generated output stays grep-equivalent across targets +// 2) Explicit `el_str_concat()` lives in the runtime; codegen routes +// through it for ambiguous (Ident+Ident, Call+Call) cases. JS's +// own `+` would also work, but el_str_concat coerces both sides +// to strings — closer to the C semantics. + +fn js_cg_expr(expr: Map) -> String { + let kind: String = expr["expr"] + + if kind == "Int" { + let v: String = expr["value"] + return v + } + + if kind == "Float" { + // JS numbers are already doubles — no bit-cast trick needed. + let v: String = expr["value"] + return v + } + + if kind == "Str" { + let v: String = expr["value"] + return js_str_lit(v) + } + + if kind == "Bool" { + let v: String = expr["value"] + if v == "true" { return "true" } + return "false" + } + + if kind == "Nil" { + return "null" + } + + if kind == "Ident" { + let name: String = expr["name"] + return name + } + + if kind == "Not" { + let inner = expr["inner"] + let inner_c: String = js_cg_expr(inner) + return "!" + inner_c + } + + if kind == "Neg" { + let inner = expr["inner"] + let inner_c: String = js_cg_expr(inner) + return "(-" + inner_c + ")" + } + + if kind == "BinOp" { + let op: String = expr["op"] + let left = expr["left"] + let right = expr["right"] + let left_c: String = js_cg_expr(left) + let right_c: String = js_cg_expr(right) + let left_kind: String = left["expr"] + let right_kind: String = right["expr"] + + // Plus dispatch — same shape as C backend, but we route through + // el_str_concat for the string-concat path (its JS impl coerces + // and matches C's behavior). Arithmetic uses bare JS `+`. + if op == "Plus" { + if left_kind == "Str" { + return "el_str_concat(" + left_c + ", " + right_c + ")" + } + if right_kind == "Str" { + return "el_str_concat(" + left_c + ", " + right_c + ")" + } + if left_kind == "Int" { + return "(" + left_c + " + " + right_c + ")" + } + if right_kind == "Int" { + return "(" + left_c + " + " + right_c + ")" + } + if left_kind == "Ident" { + if right_kind == "Ident" { + let lname: String = left["name"] + let rname: String = right["name"] + if js_is_int_name(lname) { + if js_is_int_name(rname) { + return "(" + left_c + " + " + right_c + ")" + } + } + } + } + if left_kind == "Ident" { + if right_kind == "Call" { + let lname: String = left["name"] + if js_is_int_name(lname) { + if js_is_int_call(right) { + return "(" + left_c + " + " + right_c + ")" + } + } + } + } + if right_kind == "Ident" { + if left_kind == "Call" { + let rname: String = right["name"] + if js_is_int_name(rname) { + if js_is_int_call(left) { + return "(" + left_c + " + " + right_c + ")" + } + } + } + } + if left_kind == "Call" { + if right_kind == "Call" { + if js_is_int_call(left) { + if js_is_int_call(right) { + return "(" + left_c + " + " + right_c + ")" + } + } + } + return "el_str_concat(" + left_c + ", " + right_c + ")" + } + if right_kind == "Call" { + return "el_str_concat(" + left_c + ", " + right_c + ")" + } + // Fallback: when in doubt, route through el_str_concat. JS's + // own + handles strings and numbers natively, but el_str_concat + // gives us a single point of control if behavior needs to diverge. + if left_kind == "Ident" { + return "el_str_concat(" + left_c + ", " + right_c + ")" + } + if right_kind == "Ident" { + return "el_str_concat(" + left_c + ", " + right_c + ")" + } + } + + // Equality dispatch — C backend disambiguates via str_eq for + // strings and == for ints. JS does both with === if we know + // the types are uniform; for ambiguous identifier pairs we + // route through str_eq for safety (it falls back to === in JS). + if op == "EqEq" { + if left_kind == "Int" { return "(" + left_c + " === " + right_c + ")" } + if right_kind == "Int" { return "(" + left_c + " === " + right_c + ")" } + if left_kind == "Bool" { return "(" + left_c + " === " + right_c + ")" } + if right_kind == "Bool" { return "(" + left_c + " === " + right_c + ")" } + if left_kind == "Nil" { return "(" + left_c + " === " + right_c + ")" } + if right_kind == "Nil" { return "(" + left_c + " === " + right_c + ")" } + if left_kind == "Ident" { + if right_kind == "Ident" { + let lname: String = left["name"] + let rname: String = right["name"] + if js_is_int_name(lname) { + if js_is_int_name(rname) { + return "(" + left_c + " === " + right_c + ")" + } + } + } + } + if left_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" } + if right_kind == "Str" { return "str_eq(" + left_c + ", " + right_c + ")" } + // Default: === (works for strings, numbers, bools in JS) + return "(" + left_c + " === " + right_c + ")" + } + + if op == "NotEq" { + if left_kind == "Int" { return "(" + left_c + " !== " + right_c + ")" } + if right_kind == "Int" { return "(" + left_c + " !== " + right_c + ")" } + if left_kind == "Bool" { return "(" + left_c + " !== " + right_c + ")" } + if right_kind == "Bool" { return "(" + left_c + " !== " + right_c + ")" } + if left_kind == "Nil" { return "(" + left_c + " !== " + right_c + ")" } + if right_kind == "Nil" { return "(" + left_c + " !== " + right_c + ")" } + if left_kind == "Ident" { + if right_kind == "Ident" { + let lname: String = left["name"] + let rname: String = right["name"] + if js_is_int_name(lname) { + if js_is_int_name(rname) { + return "(" + left_c + " !== " + right_c + ")" + } + } + } + } + if left_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" } + if right_kind == "Str" { return "!str_eq(" + left_c + ", " + right_c + ")" } + return "(" + left_c + " !== " + right_c + ")" + } + + let op_c: String = js_binop(op) + return "(" + left_c + " " + op_c + " " + right_c + ")" + } + + if kind == "Call" { + let func = expr["func"] + let args = expr["args"] + let arity: Int = native_list_len(args) + let func_kind: String = func["expr"] + + let args_c = "" + let i = 0 + while i < arity { + let arg = native_list_get(args, i) + let arg_c: String = js_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" { + // El's `obj.method(args)` becomes `method(obj, args)` — same + // convention as the C backend. The runtime exports method + // shortforms (append, len, get, map_get, map_set) that match. + let obj = func["object"] + let field: String = func["field"] + let obj_c: String = js_cg_expr(obj) + if arity > 0 { + return field + "(" + obj_c + ", " + args_c + ")" + } + return field + "(" + obj_c + ")" + } + + let fn_c: String = js_cg_expr(func) + return fn_c + "(" + args_c + ")" + } + + if kind == "Field" { + // El's `obj.foo` becomes JS `obj["foo"]` — works on plain objects + // (maps) and on JS objects with prototype. el_get_field is a + // runtime helper for callers that want EL_NULL on missing keys. + let obj = expr["object"] + let field: String = expr["field"] + let obj_c: String = js_cg_expr(obj) + return "el_get_field(" + obj_c + ", " + js_str_lit(field) + ")" + } + + if kind == "Index" { + // Map vs list dispatch on the index expression kind, same as C. + let obj = expr["object"] + let idx = expr["index"] + let obj_c: String = js_cg_expr(obj) + let idx_c: String = js_cg_expr(idx) + let idx_kind: String = idx["expr"] + if str_eq(idx_kind, "Str") { + return "el_get_field(" + obj_c + ", " + idx_c + ")" + } + return "el_list_get(" + obj_c + ", " + idx_c + ")" + } + + if kind == "Array" { + let elems = expr["elems"] + let n: Int = native_list_len(elems) + if n == 0 { return "[]" } + let items = "" + let i = 0 + while i < n { + let elem = native_list_get(elems, i) + let elem_c: String = js_cg_expr(elem) + if i > 0 { + let items = items + ", " + } + let items = items + elem_c + let i = i + 1 + } + return "[" + items + "]" + } + + if kind == "Map" { + let pairs = expr["pairs"] + let n: Int = native_list_len(pairs) + if n == 0 { return "{}" } + 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 = js_cg_expr(val) + if i > 0 { + let items = items + ", " + } + let items = items + js_str_lit(key) + ": " + val_c + let i = i + 1 + } + return "{" + items + "}" + } + + if kind == "Try" { + let inner = expr["inner"] + return js_cg_expr(inner) + } + + if kind == "If" { + let cond = expr["cond"] + let cond_c: String = js_cg_expr(cond) + // If as expression: ternary. Body of the if-expression is not + // currently emitted as expression-form for compound bodies; this + // matches the C backend's if-expr stub. + return "(" + cond_c + " ? 1 : 0)" + } + + if kind == "Match" { + return js_cg_match(expr) + } + + "null" +} + +// ── Match codegen (basic) ───────────────────────────────────────────────────── +// +// Lower a match expression to an IIFE with if/else chain. Works for +// LitInt / LitStr / LitBool / Wildcard / Binding patterns. Tagged-union +// destructuring is not implemented — it's stubbed and falls through to +// the wildcard path. + +fn js_next_match_id() -> String { + let csv: String = state_get("__js_match_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__js_match_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +fn js_cg_match(expr: Map) -> String { + let subject = expr["subject"] + let arms = expr["arms"] + let subj_c: String = js_cg_expr(subject) + let id: String = js_next_match_id() + let subj_var: String = "_match_subj_" + id + let out: String = "((" + subj_var + ") => { " + let n: Int = native_list_len(arms) + let i = 0 + while i < n { + let arm = native_list_get(arms, i) + let pat = arm["pattern"] + let body = arm["body"] + let pkind: String = pat["pattern"] + let body_c: String = js_cg_expr(body) + if str_eq(pkind, "Wildcard") { + let out = out + "return (" + body_c + "); " + } else { + if str_eq(pkind, "Binding") { + let bname: String = pat["name"] + let out = out + "{ const " + bname + " = " + subj_var + "; return (" + body_c + "); } " + } else { + if str_eq(pkind, "LitInt") { + let v: String = pat["value"] + let out = out + "if (" + subj_var + " === " + v + ") return (" + body_c + "); " + } else { + if str_eq(pkind, "LitStr") { + let v: String = pat["value"] + let out = out + "if (str_eq(" + subj_var + ", " + js_str_lit(v) + ")) return (" + body_c + "); " + } else { + if str_eq(pkind, "LitBool") { + let v: String = pat["value"] + let bv = "false" + if str_eq(v, "true") { let bv = "true" } + let out = out + "if (" + subj_var + " === " + bv + ") return (" + body_c + "); " + } else { + // unknown pattern → wildcard + let out = out + "return (" + body_c + "); " + } + } + } + } + } + let i = i + 1 + } + let out = out + "return null; })(" + subj_c + ")" + out +} + +// ── Variable scope tracking ─────────────────────────────────────────────────── +// +// El allows `let x = ...` to redeclare in the same scope. JS would throw +// with `let` (Identifier already declared). We track declared names and +// emit bare `x = ...` on redeclaration, `let x = ...` first time. + +fn js_list_contains(lst: [String], s: String) -> Bool { + let n: Int = native_list_len(lst) + let i = 0 + while i < n { + let item: String = native_list_get(lst, i) + if item == s { return true } + let i = i + 1 + } + false +} + +// ── Statement codegen ───────────────────────────────────────────────────────── + +fn js_cg_stmt(stmt: Map, indent: String, declared: [String]) -> [String] { + let kind: String = stmt["stmt"] + + if kind == "Let" { + let name: String = stmt["name"] + let val = stmt["value"] + let val_c: String = js_cg_expr(val) + let ltype: String = stmt["type"] + if str_eq(ltype, "Int") { + js_add_int_name(name) + } + let vk: String = val["expr"] + if str_eq(vk, "Int") { + js_add_int_name(name) + } + if js_list_contains(declared, name) { + js_emit_line(indent + name + " = " + val_c + ";") + return declared + } else { + // Use `let` (not `const`) — El semantics allow rebinding. + js_emit_line(indent + "let " + name + " = " + val_c + ";") + return native_list_append(declared, name) + } + } + + if kind == "Return" { + let val = stmt["value"] + let val_kind: String = val["expr"] + if val_kind == "Nil" { + js_emit_line(indent + "return null;") + } else { + let val_c: String = js_cg_expr(val) + js_emit_line(indent + "return " + val_c + ";") + } + return declared + } + + if kind == "Expr" { + let val = stmt["value"] + let val_kind: String = val["expr"] + if val_kind == "If" { + js_cg_if_stmt(val, indent, declared) + return declared + } + if val_kind == "For" { + js_cg_for_stmt(val, indent, declared) + return declared + } + let val_c: String = js_cg_expr(val) + js_emit_line(indent + val_c + ";") + return declared + } + + if kind == "While" { + let cond = stmt["cond"] + let body = stmt["body"] + let cond_c: String = js_cg_expr(cond) + let cond_c = js_strip_outer_parens(cond_c) + js_emit_line(indent + "while (" + cond_c + ") {") + js_cg_stmts(body, indent + " ", native_list_clone(declared)) + js_emit_line(indent + "}") + return declared + } + + if kind == "For" { + let item: String = stmt["item"] + let list_expr = stmt["list"] + let body = stmt["body"] + js_cg_for_body(item, list_expr, body, indent, declared) + return declared + } + + if kind == "FnDef" { return declared } + if kind == "TypeDef" { return declared } + if kind == "EnumDef" { return declared } + if kind == "Import" { return declared } + if kind == "CgiBlock" { + // CGI blocks compile to a no-op + warning comment in JS target. + // The runtime cgi identity is server-side; UI code is not a CGI + // principal. See spec/codegen-js.md §7. + let cname: String = stmt["name"] + js_emit_line(indent + "// cgi block '" + cname + "' — no-op in JS target (server-side concept)") + return declared + } + if kind == "ServiceBlock" { + let sname: String = stmt["name"] + js_emit_line(indent + "// service block '" + sname + "' — no-op in JS target") + return declared + } + declared +} + +// Strip a single layer of surrounding parentheses from a JS expression string. +fn js_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 js_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 = js_cg_expr(cond) + let cond_c = js_strip_outer_parens(cond_c) + js_emit_line(indent + "if (" + cond_c + ") {") + js_cg_stmts(then_stmts, indent + " ", native_list_clone(declared)) + if has_else { + js_emit_line(indent + "} else {") + js_cg_stmts(else_stmts, indent + " ", native_list_clone(declared)) + } + js_emit_line(indent + "}") +} + +fn js_cg_for_body(item: String, list_expr: Map, body: [Map], indent: String, declared: [String]) -> Void { + let list_c: String = js_cg_expr(list_expr) + js_emit_line(indent + "for (const " + item + " of " + list_c + ") {") + let body_decl = native_list_clone(declared) + let body_decl = native_list_append(body_decl, item) + js_cg_stmts(body, indent + " ", body_decl) + js_emit_line(indent + "}") +} + +fn js_cg_for_stmt(expr: Map, indent: String, declared: [String]) -> Void { + let item: String = expr["item"] + let list_expr = expr["list"] + let body = expr["body"] + js_cg_for_body(item, list_expr, body, indent, declared) +} + +fn js_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 = js_cg_stmt(stmt, indent, decl) + let i = i + 1 + } + decl +} + +// ── Function declaration codegen ────────────────────────────────────────────── + +fn js_params_str(params: [Map]) -> String { + let n: Int = native_list_len(params) + if n == 0 { return "" } + let out = "" + let i = 0 + while i < n { + let param = native_list_get(params, i) + let name: String = param["name"] + if i > 0 { + let out = out + ", " + } + let out = out + name + let i = i + 1 + } + out +} + +// Same implicit-return transform as the C backend. +fn js_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"] + if val_kind == "If" { return body } + if val_kind == "For" { return body } + 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 js_cg_fn(stmt: Map) -> Void { + let fn_name: String = stmt["name"] + let params = stmt["params"] + let body = stmt["body"] + let ret_type: String = stmt["ret_type"] + let params_str: String = js_params_str(params) + js_build_int_names_for_params(params) + + // Special-case `fn main` — emit as a regular function and call it + // at module bottom (after all top-level statements). This matches + // the C backend's behavior where `fn main` is the entry point. + if fn_name == "main" { + js_emit_line("function main(" + params_str + ") {") + } else { + js_emit_line("function " + fn_name + "(" + params_str + ") {") + } + + let decl = native_list_empty() + let np: Int = native_list_len(params) + let pi = 0 + while pi < np { + let param = native_list_get(params, pi) + let pname: String = param["name"] + let decl = native_list_append(decl, pname) + let pi = pi + 1 + } + let body_xformed = body + if !str_eq(ret_type, "Void") { + let body_xformed = js_transform_implicit_return(body) + } + js_cg_stmts(body_xformed, " ", decl) + js_emit_line("}") + js_emit_blank() +} + +// ── Top-level codegen ───────────────────────────────────────────────────────── + +fn js_is_fndef(stmt: Map) -> Bool { + let kind: String = stmt["stmt"] + if kind == "FnDef" { return true } + false +} + +fn js_is_top_level_decl(stmt: Map) -> Bool { + let kind: String = stmt["stmt"] + if kind == "TypeDef" { return true } + if kind == "EnumDef" { return true } + if kind == "Import" { return true } + if kind == "CgiBlock" { return true } + if kind == "ServiceBlock" { return true } + false +} + +// ── Entry point ─────────────────────────────────────────────────────────────── + +fn codegen_js(stmts: [Map], source: String) -> String { + // Reset per-compile state. + state_set("__js_int_names", "") + state_set("__js_match_counter", "") + + // Preamble: inline the runtime via a single import that side-effects + // globalThis. The runtime path is resolved relative to the generated + // output; users running `elc --target=js` are responsible for ensuring + // el_runtime.js is reachable. For self-contained output, the runtime + // could be inlined; that is a follow-up. + js_emit_line("// Generated by elc --target=js") + js_emit_line("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js") + js_emit_line("import \"./el_runtime.js\";") + js_emit_line("const {") + js_emit_line(" println, print, el_str_concat, str_concat, str_eq, str_starts_with, str_ends_with,") + js_emit_line(" str_len, int_to_str, str_to_int, str_slice, str_contains, str_replace,") + js_emit_line(" str_to_upper, str_to_lower, str_trim, str_index_of, str_split, str_char_at,") + js_emit_line(" str_char_code, str_lower, str_upper, el_abs, el_max, el_min,") + js_emit_line(" el_list_new, el_list_len, el_list_get, el_list_append, el_list_empty, el_list_clone,") + js_emit_line(" list_push, list_join, list_range,") + js_emit_line(" el_map_new, el_get_field, el_map_get, el_map_set,") + js_emit_line(" http_get, http_post, http_post_json,") + js_emit_line(" fs_read, fs_write, fs_list,") + js_emit_line(" json_parse, json_stringify, json_get, json_get_string, json_get_int,") + js_emit_line(" time_now, time_now_utc, sleep_ms, bool_to_str, exit_program,") + js_emit_line(" el_retain, el_release,") + js_emit_line(" append, len, get, map_get, map_set,") + js_emit_line(" native_list_get, native_list_len, native_list_append, native_list_empty,") + js_emit_line(" native_list_clone, native_string_chars, native_int_to_str,") + js_emit_line(" args, state_set, state_get, state_del, state_keys, env,") + js_emit_line(" dharma_connect, dharma_send, dharma_emit, dharma_field, dharma_activate,") + js_emit_line(" engram_node, engram_search, engram_activate,") + js_emit_line(" llm_call, llm_call_system,") + js_emit_line("} = globalThis.__el;") + js_emit_blank() + + // Function definitions + let n: Int = native_list_len(stmts) + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + if js_is_fndef(stmt) { + js_cg_fn(stmt) + } + let i = i + 1 + } + + // Top-level statements (those that are not FnDef and not declarative) + // run at module load. If the program defines `fn main`, we additionally + // call main() at the end so the C-backend mental model of "fn main is + // the entry point" carries over. + let has_main = false + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let sk: String = stmt["stmt"] + if str_eq(sk, "FnDef") { + let fn_name: String = stmt["name"] + if str_eq(fn_name, "main") { + let has_main = true + } + } + let i = i + 1 + } + + let main_decl = native_list_empty() + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + if js_is_fndef(stmt) { + // skip + } else { + if js_is_top_level_decl(stmt) { + // skip + } else { + let main_decl = js_cg_stmt(stmt, "", main_decl) + } + } + let i = i + 1 + } + + if has_main { + js_emit_blank() + js_emit_line("main();") + } // Return empty string — output was streamed via println "" @@ -2771,26 +4208,104 @@ fn codegen(stmts: [Map], source: String) -> String { // 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: +// Two backends: +// - C (default) compile() -> emits C source linked against el_runtime.c +// - JS (--target=js) compile_js() -> emits JS source linked against el_runtime.js +// +// Compile the C output with: // cc -o .c el_runtime.c +// +// Run the JS output with: +// node .js (after copying el_runtime.js next to it) -// compile — full pipeline: source string -> C source string +// compile — full pipeline (C target): source string -> C source string fn compile(source: String) -> String { let tokens: [Map] = lex(source) let stmts: [Map] = parse(tokens) + // Token list is no longer needed after parsing — release it to free memory + // before codegen allocates its own working data on large source files. + el_release(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 +// compile_js — full pipeline (JS target): source string -> JS source string +fn compile_js(source: String) -> String { + let tokens: [Map] = lex(source) + let stmts: [Map] = parse(tokens) + // Token list is no longer needed after parsing — release it to free memory. + el_release(tokens) + codegen_js(stmts, source) +} -let _argv: [String] = args() -let _src_path: String = native_list_get(_argv, 0) -let _source: String = fs_read(_src_path) -compile(_source) +// compile_dispatch — pick a backend based on the requested target. +// tgt = "c" | "js" +// (The parameter is named `tgt` because `target` is a reserved keyword +// in El's lexer — it would be tokenised as `Target`, breaking the +// parser's identifier resolution.) +fn compile_dispatch(tgt: String, source: String) -> String { + if str_eq(tgt, "js") { return compile_js(source) } + compile(source) +} + +// Detect a `--target=` flag in argv and return the target. +// Returns "c" if none specified or unrecognized. +fn detect_target(argv: [String]) -> String { + let n: Int = native_list_len(argv) + let i = 0 + while i < n { + let a: String = native_list_get(argv, i) + if str_starts_with(a, "--target=") { + let v: String = str_slice(a, 9, str_len(a)) + return v + } + let i = i + 1 + } + return "c" +} + +// Strip flags from argv, leaving only positional arguments. +fn strip_flags(argv: [String]) -> [String] { + let out: [String] = native_list_empty() + let n: Int = native_list_len(argv) + let i = 0 + while i < n { + let a: String = native_list_get(argv, i) + if !str_starts_with(a, "--") { + let out = native_list_append(out, a) + } + let i = i + 1 + } + return out +} + +// main — CLI entry point. +// +// elc # emit C to stdout +// elc --target=js # emit JS to stdout +// elc --target=c # write C to file +// elc --target=js # write JS to file +fn main() -> Void { + let argv: [String] = args() + let target: String = detect_target(argv) + let positional: [String] = strip_flags(argv) + let argc: Int = native_list_len(positional) + if argc < 1 { + println("el-compiler: usage: elc [--target=c|js] []") + exit(1) + } + let src_path: String = native_list_get(positional, 0) + let source: String = fs_read(src_path) + let out: String = compile_dispatch(target, source) + if argc >= 2 { + let out_path: String = native_list_get(positional, 1) + let ok: Bool = fs_write(out_path, out) + if ok { + exit(0) + } else { + println("el-compiler: failed to write output") + exit(1) + } + } + // No output path: codegen streamed to stdout already; out is "". +}