diff --git a/dist/elc-new.c b/dist/elc-new.c index d7ab52c..9375c79 100644 --- a/dist/elc-new.c +++ b/dist/elc-new.c @@ -1,5 +1,6 @@ #include #include +#include #include "el_runtime.h" el_val_t lex_is_digit(el_val_t ch); @@ -19,10 +20,19 @@ el_val_t lex(el_val_t source); el_val_t tok_at(el_val_t tokens, el_val_t pos); el_val_t tok_kind(el_val_t tokens, el_val_t pos); el_val_t tok_value(el_val_t tokens, el_val_t pos); +el_val_t tok_line(el_val_t tokens, el_val_t pos); el_val_t expect(el_val_t tokens, el_val_t pos, el_val_t kind); el_val_t make_result(el_val_t node, el_val_t pos); el_val_t skip_type(el_val_t tokens, el_val_t pos); el_val_t parse_params(el_val_t tokens, el_val_t pos); +el_val_t is_html_tag_name(el_val_t name); +el_val_t is_void_element(el_val_t name); +el_val_t parse_html_text_tokens(el_val_t tokens, el_val_t pos); +el_val_t parse_html_attrs(el_val_t tokens, el_val_t pos); +el_val_t parse_html_children(el_val_t tokens, el_val_t pos, el_val_t parent_tag); +el_val_t parse_html_each_body(el_val_t tokens, el_val_t pos); +el_val_t parse_html_element(el_val_t tokens, el_val_t pos); +el_val_t parse_html_template(el_val_t tokens, el_val_t pos); el_val_t parse_primary(el_val_t tokens, el_val_t pos); el_val_t parse_if(el_val_t tokens, el_val_t pos); el_val_t parse_match(el_val_t tokens, el_val_t pos); @@ -47,6 +57,12 @@ el_val_t emit_line(el_val_t line); el_val_t emit_blank(void); el_val_t binop_to_c(el_val_t op); el_val_t duration_unit_nanos(el_val_t unit); +el_val_t next_html_id(void); +el_val_t cg_html_parts(el_val_t children, el_val_t acc_var); +el_val_t cg_html_attrs_str(el_val_t attrs, el_val_t acc_var); +el_val_t cg_html_element_str(el_val_t elem, el_val_t acc_var); +el_val_t cg_html_each(el_val_t node, el_val_t acc_var); +el_val_t cg_html_template(el_val_t expr); 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); @@ -123,19 +139,38 @@ el_val_t cgi_arg(el_val_t value, el_val_t has_value); 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 cg_stmt_assert_text(el_val_t stmt, el_val_t test_name); +el_val_t cg_stmt_assert_json(el_val_t stmt, el_val_t test_name, el_val_t file_name, el_val_t test_line); +el_val_t cg_stmts_in_test(el_val_t stmts, el_val_t indent, el_val_t declared, el_val_t test_name, el_val_t reporter, el_val_t file_name, el_val_t test_line); +el_val_t cg_test_fn(el_val_t test_def, el_val_t idx, el_val_t reporter, el_val_t file_name); +el_val_t codegen_test(el_val_t stmts, el_val_t reporter, el_val_t file_name); el_val_t codegen(el_val_t stmts, el_val_t source); +el_val_t codegen_with_tests(el_val_t stmts, el_val_t source, el_val_t reporter, el_val_t file_name); +el_val_t codegen_inner(el_val_t stmts, el_val_t source, el_val_t test_mode, el_val_t reporter, el_val_t file_name); 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_el_method(el_val_t name); +el_val_t js_is_async_builtin(el_val_t name); +el_val_t js_register_async_fn(el_val_t name); +el_val_t js_is_async_fn(el_val_t name); 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_next_html_id(void); +el_val_t js_cg_html_parts(el_val_t children, el_val_t acc_var); +el_val_t js_cg_html_attrs_str(el_val_t attrs, el_val_t acc_var); +el_val_t js_cg_html_element_str(el_val_t elem, el_val_t acc_var); +el_val_t js_cg_html_each(el_val_t node, el_val_t acc_var); +el_val_t js_cg_html_template(el_val_t 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_next_lambda_id(void); +el_val_t js_cg_lambda(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); @@ -148,18 +183,45 @@ 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 js_cg_stmt_assert_text(el_val_t stmt, el_val_t test_name); +el_val_t js_cg_stmt_assert_json(el_val_t stmt, el_val_t test_name, el_val_t file_name, el_val_t test_line); +el_val_t js_cg_stmts_in_test(el_val_t stmts, el_val_t indent, el_val_t declared, el_val_t test_name, el_val_t reporter, el_val_t file_name, el_val_t test_line); +el_val_t js_cg_test_fn(el_val_t test_def, el_val_t idx, el_val_t reporter, el_val_t file_name); +el_val_t js_codegen_test(el_val_t stmts, el_val_t reporter, el_val_t file_name); el_val_t codegen_js(el_val_t stmts, el_val_t source); +el_val_t codegen_js_test(el_val_t stmts, el_val_t source, el_val_t reporter, el_val_t file_name); +el_val_t codegen_js_bundle(el_val_t stmts, el_val_t source, el_val_t runtime_content); +el_val_t codegen_js_inner(el_val_t stmts, el_val_t source, el_val_t bundle_mode, el_val_t runtime_content, el_val_t test_mode, el_val_t reporter, el_val_t file_name); +el_val_t js_strip_es_exports(el_val_t content); el_val_t compile(el_val_t source); el_val_t compile_js(el_val_t source); +el_val_t compile_js_with_bundle(el_val_t source, el_val_t runtime_path); +el_val_t compile_test(el_val_t source, el_val_t reporter, el_val_t file_name); +el_val_t compile_js_test(el_val_t source, el_val_t reporter, el_val_t file_name); el_val_t compile_dispatch(el_val_t tgt, el_val_t source); +el_val_t compile_dispatch_test(el_val_t tgt, el_val_t source, el_val_t reporter, el_val_t file_name); +el_val_t compile_dispatch_bundle(el_val_t tgt, el_val_t source, el_val_t runtime_path); el_val_t detect_target(el_val_t argv); el_val_t strip_flags(el_val_t argv); el_val_t detect_emit_header(el_val_t argv); +el_val_t detect_bundle(el_val_t argv); +el_val_t detect_minify(el_val_t argv); +el_val_t detect_obfuscate(el_val_t argv); +el_val_t detect_test(el_val_t argv); +el_val_t detect_reporter(el_val_t argv); +el_val_t basename_of(el_val_t path); +el_val_t make_temp_path(el_val_t suffix); +el_val_t js_reserved_names(void); +el_val_t find_node_tool(el_val_t tool_name, el_val_t src_dir); +el_val_t apply_minify(el_val_t js_path, el_val_t out_path, el_val_t src_dir); +el_val_t apply_obfuscate(el_val_t js_path, el_val_t out_path, el_val_t src_dir); +el_val_t resolve_runtime_path(el_val_t src_path); el_val_t type_node_to_el(el_val_t t); el_val_t emit_header(el_val_t stmts, el_val_t hdr_path); el_val_t dirname_of(el_val_t path); el_val_t parse_import_line(el_val_t trimmed, el_val_t dir); el_val_t resolve_imports(el_val_t src_path); +el_val_t run_with_postprocess(el_val_t tgt, el_val_t source, el_val_t src_path, el_val_t do_bundle, el_val_t do_obfuscate, el_val_t argc, el_val_t positional); el_val_t lex_is_digit(el_val_t ch) { if (str_eq(ch, EL_STR("0"))) { @@ -389,7 +451,12 @@ el_val_t lex_is_whitespace(el_val_t ch) { } el_val_t make_tok(el_val_t kind, el_val_t value) { - return el_map_new(2, "kind", kind, "value", value); + el_val_t ln_s = state_get(EL_STR("__lex_line")); + el_val_t ln = 1; + if (!str_eq(ln_s, EL_STR(""))) { + ln = str_to_int(ln_s); + } + return el_map_new(3, "kind", kind, "value", value, "line", ln); return 0; } @@ -523,6 +590,12 @@ el_val_t keyword_kind(el_val_t word) { if (str_eq(word, EL_STR("extern"))) { return EL_STR("Extern"); } + if (str_eq(word, EL_STR("try"))) { + return EL_STR("Try"); + } + if (str_eq(word, EL_STR("catch"))) { + return EL_STR("Catch"); + } return EL_STR(""); return 0; } @@ -839,9 +912,15 @@ el_val_t lex(el_val_t source) { el_val_t total = native_list_len(chars); el_val_t tokens = native_list_empty(); el_val_t i = 0; + el_val_t line_num = 1; + state_set(EL_STR("__lex_line"), EL_STR("1")); while (i < total) { el_val_t ch = native_list_get(chars, i); if (lex_is_whitespace(ch)) { + if (str_eq(ch, EL_STR("\n"))) { + line_num = (line_num + 1); + state_set(EL_STR("__lex_line"), native_int_to_str(line_num)); + } i = (i + 1); } else { if (str_eq(ch, EL_STR("/"))) { @@ -1070,7 +1149,12 @@ el_val_t lex(el_val_t source) { tokens = native_list_append(tokens, make_tok(EL_STR("QuestionMark"), EL_STR("?"))); i = (i + 1); } else { - i = (i + 1); + if (str_eq(ch, EL_STR("#"))) { + tokens = native_list_append(tokens, make_tok(EL_STR("Hash"), EL_STR("#"))); + i = (i + 1); + } else { + i = (i + 1); + } } } } @@ -1121,6 +1205,16 @@ el_val_t tok_value(el_val_t tokens, el_val_t pos) { return 0; } +el_val_t tok_line(el_val_t tokens, el_val_t pos) { + el_val_t t = native_list_get(tokens, pos); + el_val_t ln = el_get_field(t, EL_STR("line")); + if (ln <= 0) { + return 1; + } + return ln; + return 0; +} + el_val_t expect(el_val_t tokens, el_val_t pos, el_val_t kind) { el_val_t k = tok_kind(tokens, pos); if (str_eq(k, kind)) { @@ -1221,6 +1315,643 @@ el_val_t parse_params(el_val_t tokens, el_val_t pos) { return 0; } +el_val_t is_html_tag_name(el_val_t name) { + if (str_eq(name, EL_STR("a"))) { + return 1; + } + if (str_eq(name, EL_STR("abbr"))) { + return 1; + } + if (str_eq(name, EL_STR("address"))) { + return 1; + } + if (str_eq(name, EL_STR("area"))) { + return 1; + } + if (str_eq(name, EL_STR("article"))) { + return 1; + } + if (str_eq(name, EL_STR("aside"))) { + return 1; + } + if (str_eq(name, EL_STR("audio"))) { + return 1; + } + if (str_eq(name, EL_STR("b"))) { + return 1; + } + if (str_eq(name, EL_STR("base"))) { + return 1; + } + if (str_eq(name, EL_STR("blockquote"))) { + return 1; + } + if (str_eq(name, EL_STR("body"))) { + return 1; + } + if (str_eq(name, EL_STR("br"))) { + return 1; + } + if (str_eq(name, EL_STR("button"))) { + return 1; + } + if (str_eq(name, EL_STR("canvas"))) { + return 1; + } + if (str_eq(name, EL_STR("caption"))) { + return 1; + } + if (str_eq(name, EL_STR("cite"))) { + return 1; + } + if (str_eq(name, EL_STR("code"))) { + return 1; + } + if (str_eq(name, EL_STR("col"))) { + return 1; + } + if (str_eq(name, EL_STR("colgroup"))) { + return 1; + } + if (str_eq(name, EL_STR("data"))) { + return 1; + } + if (str_eq(name, EL_STR("datalist"))) { + return 1; + } + if (str_eq(name, EL_STR("dd"))) { + return 1; + } + if (str_eq(name, EL_STR("del"))) { + return 1; + } + if (str_eq(name, EL_STR("details"))) { + return 1; + } + if (str_eq(name, EL_STR("dfn"))) { + return 1; + } + if (str_eq(name, EL_STR("dialog"))) { + return 1; + } + if (str_eq(name, EL_STR("div"))) { + return 1; + } + if (str_eq(name, EL_STR("dl"))) { + return 1; + } + if (str_eq(name, EL_STR("dt"))) { + return 1; + } + if (str_eq(name, EL_STR("em"))) { + return 1; + } + if (str_eq(name, EL_STR("embed"))) { + return 1; + } + if (str_eq(name, EL_STR("fieldset"))) { + return 1; + } + if (str_eq(name, EL_STR("figcaption"))) { + return 1; + } + if (str_eq(name, EL_STR("figure"))) { + return 1; + } + if (str_eq(name, EL_STR("footer"))) { + return 1; + } + if (str_eq(name, EL_STR("form"))) { + return 1; + } + if (str_eq(name, EL_STR("h1"))) { + return 1; + } + if (str_eq(name, EL_STR("h2"))) { + return 1; + } + if (str_eq(name, EL_STR("h3"))) { + return 1; + } + if (str_eq(name, EL_STR("h4"))) { + return 1; + } + if (str_eq(name, EL_STR("h5"))) { + return 1; + } + if (str_eq(name, EL_STR("h6"))) { + return 1; + } + if (str_eq(name, EL_STR("head"))) { + return 1; + } + if (str_eq(name, EL_STR("header"))) { + return 1; + } + if (str_eq(name, EL_STR("hr"))) { + return 1; + } + if (str_eq(name, EL_STR("html"))) { + return 1; + } + if (str_eq(name, EL_STR("i"))) { + return 1; + } + if (str_eq(name, EL_STR("iframe"))) { + return 1; + } + if (str_eq(name, EL_STR("img"))) { + return 1; + } + if (str_eq(name, EL_STR("input"))) { + return 1; + } + if (str_eq(name, EL_STR("ins"))) { + return 1; + } + if (str_eq(name, EL_STR("kbd"))) { + return 1; + } + if (str_eq(name, EL_STR("label"))) { + return 1; + } + if (str_eq(name, EL_STR("legend"))) { + return 1; + } + if (str_eq(name, EL_STR("li"))) { + return 1; + } + if (str_eq(name, EL_STR("link"))) { + return 1; + } + if (str_eq(name, EL_STR("main"))) { + return 1; + } + if (str_eq(name, EL_STR("map"))) { + return 1; + } + if (str_eq(name, EL_STR("mark"))) { + return 1; + } + if (str_eq(name, EL_STR("menu"))) { + return 1; + } + if (str_eq(name, EL_STR("meta"))) { + return 1; + } + if (str_eq(name, EL_STR("meter"))) { + return 1; + } + if (str_eq(name, EL_STR("nav"))) { + return 1; + } + if (str_eq(name, EL_STR("noscript"))) { + return 1; + } + if (str_eq(name, EL_STR("object"))) { + return 1; + } + if (str_eq(name, EL_STR("ol"))) { + return 1; + } + if (str_eq(name, EL_STR("optgroup"))) { + return 1; + } + if (str_eq(name, EL_STR("option"))) { + return 1; + } + if (str_eq(name, EL_STR("output"))) { + return 1; + } + if (str_eq(name, EL_STR("p"))) { + return 1; + } + if (str_eq(name, EL_STR("param"))) { + return 1; + } + if (str_eq(name, EL_STR("picture"))) { + return 1; + } + if (str_eq(name, EL_STR("pre"))) { + return 1; + } + if (str_eq(name, EL_STR("progress"))) { + return 1; + } + if (str_eq(name, EL_STR("q"))) { + return 1; + } + if (str_eq(name, EL_STR("rp"))) { + return 1; + } + if (str_eq(name, EL_STR("rt"))) { + return 1; + } + if (str_eq(name, EL_STR("ruby"))) { + return 1; + } + if (str_eq(name, EL_STR("s"))) { + return 1; + } + if (str_eq(name, EL_STR("samp"))) { + return 1; + } + if (str_eq(name, EL_STR("script"))) { + return 1; + } + if (str_eq(name, EL_STR("section"))) { + return 1; + } + if (str_eq(name, EL_STR("select"))) { + return 1; + } + if (str_eq(name, EL_STR("small"))) { + return 1; + } + if (str_eq(name, EL_STR("source"))) { + return 1; + } + if (str_eq(name, EL_STR("span"))) { + return 1; + } + if (str_eq(name, EL_STR("strong"))) { + return 1; + } + if (str_eq(name, EL_STR("style"))) { + return 1; + } + if (str_eq(name, EL_STR("sub"))) { + return 1; + } + if (str_eq(name, EL_STR("summary"))) { + return 1; + } + if (str_eq(name, EL_STR("sup"))) { + return 1; + } + if (str_eq(name, EL_STR("table"))) { + return 1; + } + if (str_eq(name, EL_STR("tbody"))) { + return 1; + } + if (str_eq(name, EL_STR("td"))) { + return 1; + } + if (str_eq(name, EL_STR("template"))) { + return 1; + } + if (str_eq(name, EL_STR("textarea"))) { + return 1; + } + if (str_eq(name, EL_STR("tfoot"))) { + return 1; + } + if (str_eq(name, EL_STR("th"))) { + return 1; + } + if (str_eq(name, EL_STR("thead"))) { + return 1; + } + if (str_eq(name, EL_STR("time"))) { + return 1; + } + if (str_eq(name, EL_STR("title"))) { + return 1; + } + if (str_eq(name, EL_STR("tr"))) { + return 1; + } + if (str_eq(name, EL_STR("track"))) { + return 1; + } + if (str_eq(name, EL_STR("u"))) { + return 1; + } + if (str_eq(name, EL_STR("ul"))) { + return 1; + } + if (str_eq(name, EL_STR("var"))) { + return 1; + } + if (str_eq(name, EL_STR("video"))) { + return 1; + } + if (str_eq(name, EL_STR("wbr"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_void_element(el_val_t name) { + if (str_eq(name, EL_STR("area"))) { + return 1; + } + if (str_eq(name, EL_STR("base"))) { + return 1; + } + if (str_eq(name, EL_STR("br"))) { + return 1; + } + if (str_eq(name, EL_STR("col"))) { + return 1; + } + if (str_eq(name, EL_STR("embed"))) { + return 1; + } + if (str_eq(name, EL_STR("hr"))) { + return 1; + } + if (str_eq(name, EL_STR("img"))) { + return 1; + } + if (str_eq(name, EL_STR("input"))) { + return 1; + } + if (str_eq(name, EL_STR("link"))) { + return 1; + } + if (str_eq(name, EL_STR("meta"))) { + return 1; + } + if (str_eq(name, EL_STR("param"))) { + return 1; + } + if (str_eq(name, EL_STR("source"))) { + return 1; + } + if (str_eq(name, EL_STR("track"))) { + return 1; + } + if (str_eq(name, EL_STR("wbr"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t parse_html_text_tokens(el_val_t tokens, el_val_t pos) { + el_val_t parts = native_list_empty(); + el_val_t p = pos; + el_val_t running = 1; + while (running) { + el_val_t k = tok_kind(tokens, p); + if (str_eq(k, EL_STR("Eof"))) { + running = 0; + } else { + if (str_eq(k, EL_STR("Lt"))) { + running = 0; + } else { + if (str_eq(k, EL_STR("LBrace"))) { + running = 0; + } else { + el_val_t v = tok_value(tokens, p); + parts = native_list_append(parts, v); + p = (p + 1); + } + } + } + } + return el_map_new(2, "text", str_join(parts, EL_STR(" ")), "pos", p); + return 0; +} + +el_val_t parse_html_attrs(el_val_t tokens, el_val_t pos) { + el_val_t attrs = native_list_empty(); + el_val_t p = pos; + el_val_t running = 1; + while (running) { + el_val_t k = tok_kind(tokens, p); + if (str_eq(k, EL_STR("Gt"))) { + running = 0; + } else { + if (str_eq(k, EL_STR("Slash"))) { + running = 0; + } else { + if (str_eq(k, EL_STR("Eof"))) { + running = 0; + } else { + el_val_t attr_name = tok_value(tokens, p); + p = (p + 1); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Eq"))) { + p = (p + 1); + el_val_t k3 = tok_kind(tokens, p); + if (str_eq(k3, EL_STR("Str"))) { + el_val_t attr_val = tok_value(tokens, p); + p = (p + 1); + attrs = native_list_append(attrs, el_map_new(3, "name", attr_name, "kind", EL_STR("static"), "value", attr_val)); + } else { + if (str_eq(k3, EL_STR("LBrace"))) { + el_val_t r = parse_expr(tokens, (p + 1)); + el_val_t val_node = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + p = expect(tokens, p, EL_STR("RBrace")); + attrs = native_list_append(attrs, el_map_new(3, "name", attr_name, "kind", EL_STR("dynamic"), "value", val_node)); + } else { + } + } + } else { + attrs = native_list_append(attrs, el_map_new(2, "name", attr_name, "kind", EL_STR("bool"))); + } + } + } + } + } + return el_map_new(2, "attrs", attrs, "pos", p); + return 0; +} + +el_val_t parse_html_children(el_val_t tokens, el_val_t pos, el_val_t parent_tag) { + el_val_t children = native_list_empty(); + el_val_t p = pos; + el_val_t running = 1; + while (running) { + el_val_t k = tok_kind(tokens, p); + if (str_eq(k, EL_STR("Eof"))) { + running = 0; + } else { + if (str_eq(k, EL_STR("Lt"))) { + el_val_t k2 = tok_kind(tokens, (p + 1)); + if (str_eq(k2, EL_STR("Slash"))) { + p = (p + 2); + el_val_t close_name = tok_value(tokens, p); + p = (p + 1); + p = expect(tokens, p, EL_STR("Gt")); + running = 0; + } else { + if (str_eq(k2, EL_STR("Not"))) { + el_val_t k3_v = tok_value(tokens, (p + 2)); + if (str_eq(k3_v, EL_STR("doctype"))) { + p = (p + 2); + el_val_t scanning = 1; + while (scanning) { + el_val_t ck = tok_kind(tokens, p); + if (str_eq(ck, EL_STR("Eof"))) { + scanning = 0; + } + if (str_eq(ck, EL_STR("Gt"))) { + p = (p + 1); + scanning = 0; + } else { + p = (p + 1); + } + } + children = native_list_append(children, el_map_new(1, "html", EL_STR("Doctype"))); + } else { + p = (p + 1); + } + } else { + el_val_t r = parse_html_element(tokens, p); + el_val_t child = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + children = native_list_append(children, child); + } + } + } else { + if (str_eq(k, EL_STR("LBrace"))) { + el_val_t k2 = tok_kind(tokens, (p + 1)); + if (str_eq(k2, EL_STR("Hash"))) { + el_val_t k3_v = tok_value(tokens, (p + 2)); + if (str_eq(k3_v, EL_STR("each"))) { + p = (p + 3); + 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_list = parse_expr(tokens, p); + state_set(EL_STR("__no_block_expr"), prev_no_block); + el_val_t list_expr = el_get_field(r_list, EL_STR("node")); + p = el_get_field(r_list, EL_STR("pos")); + p = expect(tokens, p, EL_STR("As")); + el_val_t item_name = tok_value(tokens, p); + p = (p + 1); + p = expect(tokens, p, EL_STR("RBrace")); + el_val_t r_body = parse_html_each_body(tokens, p); + el_val_t body_children = el_get_field(r_body, EL_STR("children")); + p = el_get_field(r_body, EL_STR("pos")); + el_val_t each_node = el_map_new(4, "html", EL_STR("Each"), "list", list_expr, "item", item_name, "body", body_children); + children = native_list_append(children, each_node); + } else { + p = (p + 1); + } + } else { + if (str_eq(k2, EL_STR("Slash"))) { + p = (p + 2); + p = (p + 1); + p = expect(tokens, p, EL_STR("RBrace")); + running = 0; + } else { + el_val_t r = parse_expr(tokens, (p + 1)); + el_val_t interp_val = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + p = expect(tokens, p, EL_STR("RBrace")); + el_val_t is_raw_call = 0; + el_val_t interp_kind = el_get_field(interp_val, EL_STR("expr")); + if (str_eq(interp_kind, EL_STR("Call"))) { + el_val_t fn_node = el_get_field(interp_val, EL_STR("func")); + el_val_t fn_kind = el_get_field(fn_node, EL_STR("expr")); + if (str_eq(fn_kind, EL_STR("Ident"))) { + el_val_t fn_name_v = el_get_field(fn_node, EL_STR("name")); + if (str_eq(fn_name_v, EL_STR("raw"))) { + is_raw_call = 1; + } + } + } + if (is_raw_call) { + el_val_t raw_args = el_get_field(interp_val, EL_STR("args")); + el_val_t raw_inner = native_list_get(raw_args, 0); + children = native_list_append(children, el_map_new(2, "html", EL_STR("Raw"), "value", raw_inner)); + } else { + children = native_list_append(children, el_map_new(2, "html", EL_STR("Interp"), "value", interp_val)); + } + } + } + } else { + el_val_t r_text = parse_html_text_tokens(tokens, p); + el_val_t text_str = el_get_field(r_text, EL_STR("text")); + p = el_get_field(r_text, EL_STR("pos")); + el_val_t text_trimmed = str_trim(text_str); + if (!str_eq(text_trimmed, EL_STR(""))) { + children = native_list_append(children, el_map_new(2, "html", EL_STR("Text"), "text", text_trimmed)); + } + } + } + } + } + return el_map_new(2, "children", children, "pos", p); + return 0; +} + +el_val_t parse_html_each_body(el_val_t tokens, el_val_t pos) { + return parse_html_children(tokens, pos, EL_STR("__each__")); + return 0; +} + +el_val_t parse_html_element(el_val_t tokens, el_val_t pos) { + el_val_t p = pos; + p = expect(tokens, p, EL_STR("Lt")); + el_val_t tag_name = tok_value(tokens, p); + p = (p + 1); + el_val_t r_attrs = parse_html_attrs(tokens, p); + el_val_t attrs = el_get_field(r_attrs, EL_STR("attrs")); + p = el_get_field(r_attrs, EL_STR("pos")); + el_val_t k = tok_kind(tokens, p); + el_val_t self_closing = 0; + if (str_eq(k, EL_STR("Slash"))) { + p = (p + 1); + p = expect(tokens, p, EL_STR("Gt")); + self_closing = 1; + return make_result(el_map_new(5, "html", EL_STR("Element"), "tag", tag_name, "attrs", attrs, "children", native_list_empty(), "self_closing", 1), p); + } + p = expect(tokens, p, EL_STR("Gt")); + if (is_void_element(tag_name)) { + return make_result(el_map_new(5, "html", EL_STR("Element"), "tag", tag_name, "attrs", attrs, "children", native_list_empty(), "self_closing", 1), p); + } + el_val_t r_children = parse_html_children(tokens, p, tag_name); + el_val_t children = el_get_field(r_children, EL_STR("children")); + p = el_get_field(r_children, EL_STR("pos")); + return make_result(el_map_new(5, "html", EL_STR("Element"), "tag", tag_name, "attrs", attrs, "children", children, "self_closing", 0), p); + return 0; +} + +el_val_t parse_html_template(el_val_t tokens, el_val_t pos) { + el_val_t p = pos; + el_val_t doctype = 0; + el_val_t k = tok_kind(tokens, p); + el_val_t k2 = tok_kind(tokens, (p + 1)); + if (str_eq(k, EL_STR("Lt"))) { + if (str_eq(k2, EL_STR("Not"))) { + el_val_t k3_v = tok_value(tokens, (p + 2)); + if (str_eq(k3_v, EL_STR("doctype"))) { + doctype = 1; + p = (p + 2); + el_val_t scanning = 1; + while (scanning) { + el_val_t ck = tok_kind(tokens, p); + if (str_eq(ck, EL_STR("Eof"))) { + scanning = 0; + } + if (str_eq(ck, EL_STR("Gt"))) { + p = (p + 1); + scanning = 0; + } else { + p = (p + 1); + } + } + } + } + } + el_val_t r = parse_html_element(tokens, p); + el_val_t root = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + el_val_t root_with_doctype = root; + if (doctype) { + root_with_doctype = el_map_new(6, "html", el_get_field(root, EL_STR("html")), "tag", el_get_field(root, EL_STR("tag")), "attrs", el_get_field(root, EL_STR("attrs")), "children", el_get_field(root, EL_STR("children")), "self_closing", el_get_field(root, EL_STR("self_closing")), "doctype", 1); + } + return make_result(el_map_new(2, "expr", EL_STR("HtmlTemplate"), "root", root_with_doctype), p); + return 0; +} + el_val_t parse_primary(el_val_t tokens, el_val_t pos) { el_val_t k = tok_kind(tokens, pos); el_val_t v = tok_value(tokens, pos); @@ -1236,6 +1967,18 @@ el_val_t parse_primary(el_val_t tokens, el_val_t pos) { if (str_eq(k, EL_STR("Bool"))) { return make_result(el_map_new(2, "expr", EL_STR("Bool"), "value", v), (pos + 1)); } + if (str_eq(k, EL_STR("Lt"))) { + el_val_t k2 = tok_kind(tokens, (pos + 1)); + if (str_eq(k2, EL_STR("Not"))) { + return parse_html_template(tokens, pos); + } + if (str_eq(k2, EL_STR("Ident"))) { + el_val_t tag_candidate = tok_value(tokens, (pos + 1)); + if (is_html_tag_name(tag_candidate)) { + return parse_html_template(tokens, pos); + } + } + } if (str_eq(k, EL_STR("Ident"))) { return make_result(el_map_new(2, "expr", EL_STR("Ident"), "name", v), (pos + 1)); } @@ -1323,6 +2066,26 @@ el_val_t parse_primary(el_val_t tokens, el_val_t pos) { el_val_t r = parse_for_expr(tokens, pos); return r; } + if (str_eq(k, EL_STR("Fn"))) { + el_val_t p = (pos + 1); + el_val_t r = parse_params(tokens, p); + el_val_t params = el_get_field(r, EL_STR("params")); + p = el_get_field(r, EL_STR("pos")); + el_val_t ret_type = EL_STR(""); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Arrow"))) { + p = (p + 1); + el_val_t kt = tok_kind(tokens, p); + if (str_eq(kt, EL_STR("Ident"))) { + ret_type = tok_value(tokens, p); + } + p = skip_type(tokens, p); + } + el_val_t r2 = parse_block(tokens, p); + el_val_t body = el_get_field(r2, EL_STR("stmts")); + p = el_get_field(r2, EL_STR("pos")); + return make_result(el_map_new(4, "expr", EL_STR("Lambda"), "params", params, "body", body, "ret_type", ret_type), p); + } if (str_eq(k, EL_STR("Not"))) { el_val_t r = parse_primary(tokens, (pos + 1)); el_val_t inner = el_get_field(r, EL_STR("node")); @@ -1474,6 +2237,11 @@ el_val_t parse_pattern(el_val_t tokens, el_val_t pos) { if (str_eq(v, EL_STR("_"))) { return make_result(el_map_new(1, "pattern", EL_STR("Wildcard")), (pos + 1)); } + el_val_t next_k = tok_kind(tokens, (pos + 1)); + if (str_eq(next_k, EL_STR("ColonColon"))) { + el_val_t variant_name = tok_value(tokens, (pos + 2)); + return make_result(el_map_new(3, "pattern", EL_STR("Variant"), "enum_name", v, "variant", variant_name), (pos + 3)); + } return make_result(el_map_new(2, "pattern", EL_STR("Binding"), "name", v), (pos + 1)); } if (str_eq(k, EL_STR("Int"))) { @@ -1855,6 +2623,10 @@ el_val_t parse_stmt(el_val_t tokens, el_val_t pos) { el_val_t p = (pos + 1); el_val_t name = tok_value(tokens, p); p = (p + 1); + el_val_t pk = tok_kind(tokens, p); + if (str_eq(pk, EL_STR("Eq"))) { + p = (p + 1); + } p = expect(tokens, p, EL_STR("LBrace")); el_val_t fields = native_list_empty(); el_val_t running = 1; @@ -1976,6 +2748,34 @@ el_val_t parse_stmt(el_val_t tokens, el_val_t pos) { p = el_get_field(r2, EL_STR("pos")); return make_result(el_map_new(4, "stmt", EL_STR("For"), "item", item_name, "list", list_expr, "body", body), p); } + if (str_eq(k, EL_STR("Try"))) { + el_val_t p = (pos + 1); + el_val_t r_try = parse_block(tokens, p); + el_val_t try_body = el_get_field(r_try, EL_STR("stmts")); + p = el_get_field(r_try, EL_STR("pos")); + el_val_t catch_name = EL_STR("err"); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Catch"))) { + p = (p + 1); + p = expect(tokens, p, EL_STR("LParen")); + el_val_t kn = tok_kind(tokens, p); + if (str_eq(kn, EL_STR("Ident"))) { + catch_name = tok_value(tokens, p); + p = (p + 1); + } + el_val_t k3 = tok_kind(tokens, p); + if (str_eq(k3, EL_STR("Colon"))) { + p = (p + 1); + p = skip_type(tokens, p); + } + p = expect(tokens, p, EL_STR("RParen")); + el_val_t r_catch = parse_block(tokens, p); + el_val_t catch_body = el_get_field(r_catch, EL_STR("stmts")); + p = el_get_field(r_catch, EL_STR("pos")); + return make_result(el_map_new(4, "stmt", EL_STR("TryCatch"), "try_body", try_body, "catch_name", catch_name, "catch_body", catch_body), p); + } + return make_result(el_map_new(4, "stmt", EL_STR("TryCatch"), "try_body", try_body, "catch_name", catch_name, "catch_body", native_list_empty()), p); + } if (str_eq(k, EL_STR("At"))) { el_val_t p = (pos + 1); el_val_t dec_name = tok_value(tokens, p); @@ -2080,6 +2880,31 @@ el_val_t parse_stmt(el_val_t tokens, el_val_t pos) { p = expect(tokens, p, EL_STR("RBrace")); return make_result(el_map_new(4, "stmt", EL_STR("ServiceBlock"), "name", name, "sponsor", sponsor, "domain", domain), p); } + if (str_eq(k, EL_STR("Assert"))) { + el_val_t assert_line = tok_line(tokens, pos); + el_val_t p = (pos + 1); + el_val_t r = parse_expr(tokens, p); + el_val_t expr_node = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + el_val_t msg = EL_STR(""); + el_val_t k2 = tok_kind(tokens, p); + if (str_eq(k2, EL_STR("Comma"))) { + p = (p + 1); + msg = tok_value(tokens, p); + p = (p + 1); + } + return make_result(el_map_new(4, "stmt", EL_STR("Assert"), "expr", expr_node, "msg", msg, "line", assert_line), p); + } + if (str_eq(k, EL_STR("Test"))) { + el_val_t test_line = tok_line(tokens, pos); + el_val_t p = (pos + 1); + el_val_t name = tok_value(tokens, p); + p = (p + 1); + el_val_t r2 = parse_block(tokens, p); + el_val_t body = el_get_field(r2, EL_STR("stmts")); + p = el_get_field(r2, EL_STR("pos")); + return make_result(el_map_new(4, "stmt", EL_STR("TestDef"), "name", name, "body", body, "line", test_line), p); + } if (str_eq(k, EL_STR("Ident"))) { el_val_t k2 = tok_kind(tokens, (pos + 1)); if (str_eq(k2, EL_STR("Eq"))) { @@ -2330,6 +3155,136 @@ el_val_t duration_unit_nanos(el_val_t unit) { return 0; } +el_val_t next_html_id(void) { + el_val_t csv = state_get(EL_STR("__html_counter")); + el_val_t n = 0; + if (!str_eq(csv, EL_STR(""))) { + n = str_to_int(csv); + } + n = (n + 1); + state_set(EL_STR("__html_counter"), native_int_to_str(n)); + return native_int_to_str(n); + return 0; +} + +el_val_t cg_html_parts(el_val_t children, el_val_t acc_var) { + el_val_t n = native_list_len(children); + el_val_t i = 0; + el_val_t out = EL_STR(""); + while (i < n) { + el_val_t child = native_list_get(children, i); + el_val_t html_kind = el_get_field(child, EL_STR("html")); + if (str_eq(html_kind, EL_STR("Text"))) { + el_val_t text = el_get_field(child, EL_STR("text")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", EL_STR(")), c_str_lit(text)), EL_STR(")); ")); + } + if (str_eq(html_kind, EL_STR("Doctype"))) { + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", EL_STR(\"\")); ")); + } + if (str_eq(html_kind, EL_STR("Interp"))) { + el_val_t val_node = el_get_field(child, EL_STR("value")); + el_val_t val_c = cg_expr(val_node); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", html_escape(")), val_c), EL_STR(")); ")); + } + if (str_eq(html_kind, EL_STR("Raw"))) { + el_val_t val_node = el_get_field(child, EL_STR("value")); + el_val_t val_c = cg_expr(val_node); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", html_raw(")), val_c), EL_STR(")); ")); + } + if (str_eq(html_kind, EL_STR("Element"))) { + el_val_t elem_c = cg_html_element_str(child, acc_var); + out = el_str_concat(out, elem_c); + } + if (str_eq(html_kind, EL_STR("Each"))) { + el_val_t each_c = cg_html_each(child, acc_var); + out = el_str_concat(out, each_c); + } + i = (i + 1); + } + return out; + return 0; +} + +el_val_t cg_html_attrs_str(el_val_t attrs, el_val_t acc_var) { + el_val_t n = native_list_len(attrs); + el_val_t i = 0; + el_val_t out = EL_STR(""); + el_val_t close_q = el_str_concat(el_str_concat(EL_STR("EL_STR("), c_str_lit(EL_STR("\""))), EL_STR(")")); + while (i < n) { + el_val_t attr = native_list_get(attrs, i); + el_val_t attr_name = el_get_field(attr, EL_STR("name")); + el_val_t kind = el_get_field(attr, EL_STR("kind")); + el_val_t open_val = el_str_concat(el_str_concat(EL_STR(" "), attr_name), EL_STR("=\"")); + el_val_t open_attr = el_str_concat(el_str_concat(EL_STR("EL_STR("), c_str_lit(open_val)), EL_STR(")")); + if (str_eq(kind, EL_STR("static"))) { + el_val_t sv = el_get_field(attr, EL_STR("value")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", ")), open_attr), EL_STR("); ")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", EL_STR(")), c_str_lit(sv)), EL_STR(")); ")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", ")), close_q), EL_STR("); ")); + } else { + if (str_eq(kind, EL_STR("dynamic"))) { + el_val_t val_node = el_get_field(attr, EL_STR("value")); + el_val_t val_c = cg_expr(val_node); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", ")), open_attr), EL_STR("); ")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", html_escape(")), val_c), EL_STR(")); ")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", ")), close_q), EL_STR("); ")); + } else { + el_val_t bool_attr = el_str_concat(el_str_concat(EL_STR("EL_STR("), c_str_lit(el_str_concat(EL_STR(" "), attr_name))), EL_STR(")")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", ")), bool_attr), EL_STR("); ")); + } + } + i = (i + 1); + } + return out; + return 0; +} + +el_val_t cg_html_element_str(el_val_t elem, el_val_t acc_var) { + el_val_t tag = el_get_field(elem, EL_STR("tag")); + el_val_t attrs = el_get_field(elem, EL_STR("attrs")); + el_val_t children = el_get_field(elem, EL_STR("children")); + el_val_t self_closing = el_get_field(elem, EL_STR("self_closing")); + el_val_t out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(acc_var, EL_STR(" = el_str_concat(")), acc_var), EL_STR(", EL_STR(\"<")), tag), EL_STR("\")); ")); + out = el_str_concat(out, cg_html_attrs_str(attrs, acc_var)); + if (self_closing) { + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", EL_STR(\"/>\")); ")); + } else { + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", EL_STR(\">\")); ")); + out = el_str_concat(out, cg_html_parts(children, acc_var)); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" = el_str_concat(")), acc_var), EL_STR(", EL_STR(\"\")); ")); + } + return out; + return 0; +} + +el_val_t cg_html_each(el_val_t node, el_val_t acc_var) { + el_val_t list_expr = el_get_field(node, EL_STR("list")); + el_val_t item_name = el_get_field(node, EL_STR("item")); + el_val_t body_children = el_get_field(node, EL_STR("body")); + el_val_t id = next_html_id(); + el_val_t list_var = el_str_concat(EL_STR("_html_list_"), id); + el_val_t len_var = el_str_concat(EL_STR("_html_len_"), id); + el_val_t idx_var = el_str_concat(EL_STR("_html_i_"), id); + el_val_t list_c = cg_expr(list_expr); + el_val_t inner_c = cg_html_parts(body_children, acc_var); + return 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_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_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{ el_val_t "), list_var), EL_STR(" = (")), list_c), EL_STR("); el_val_t ")), len_var), EL_STR(" = el_list_len(")), list_var), EL_STR("); for (el_val_t ")), idx_var), EL_STR(" = 0; ")), idx_var), EL_STR(" < ")), len_var), EL_STR("; ")), idx_var), EL_STR("++) { el_val_t ")), item_name), EL_STR(" = el_list_get(")), list_var), EL_STR(", ")), idx_var), EL_STR("); ")), inner_c), EL_STR("} } ")); + return 0; +} + +el_val_t cg_html_template(el_val_t expr) { + el_val_t root = el_get_field(expr, EL_STR("root")); + el_val_t id = next_html_id(); + el_val_t acc = el_str_concat(EL_STR("_html_"), id); + el_val_t doctype_flag = el_get_field(root, EL_STR("doctype")); + el_val_t doctype_prefix = EL_STR(""); + if (doctype_flag) { + doctype_prefix = el_str_concat(el_str_concat(el_str_concat(acc, EL_STR(" = el_str_concat(")), acc), EL_STR(", EL_STR(\"\")); ")); + } + el_val_t body = cg_html_element_str(root, acc); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("({ el_val_t "), acc), EL_STR(" = EL_STR(\"\"); ")), doctype_prefix), body), acc), EL_STR("; })")); + return 0; +} + el_val_t cg_expr(el_val_t expr) { el_val_t kind = el_get_field(expr, EL_STR("expr")); if (str_eq(kind, EL_STR("Int"))) { @@ -2866,6 +3821,9 @@ el_val_t cg_expr(el_val_t expr) { if (str_eq(kind, EL_STR("Match"))) { return cg_match(expr); } + if (str_eq(kind, EL_STR("HtmlTemplate"))) { + return cg_html_template(expr); + } return EL_STR("EL_NULL"); return 0; } @@ -2923,7 +3881,12 @@ el_val_t cg_match(el_val_t expr) { } parts = native_list_append(parts, 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("if ("), subj_var), EL_STR(" == ")), bv), EL_STR(") { ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } "))); } else { - parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{ "), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } "))); + if (str_eq(pkind, EL_STR("Variant"))) { + el_val_t variant = el_get_field(pat, EL_STR("variant")); + parts = native_list_append(parts, 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("if (str_eq("), subj_var), EL_STR(", EL_STR(")), c_str_lit(variant)), EL_STR("))) { ")), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } "))); + } else { + parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{ "), result_var), EL_STR(" = (")), body_c), EL_STR("); goto ")), done_label), EL_STR("; } "))); + } } } } @@ -3171,6 +4134,21 @@ el_val_t cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared) { if (str_eq(kind, EL_STR("CgiBlock"))) { return declared; } + if (str_eq(kind, EL_STR("ServiceBlock"))) { + return declared; + } + if (str_eq(kind, EL_STR("TestDef"))) { + return declared; + } + if (str_eq(kind, EL_STR("Assert"))) { + return declared; + } + if (str_eq(kind, EL_STR("TryCatch"))) { + el_val_t try_body = el_get_field(stmt, EL_STR("try_body")); + emit_line(el_str_concat(indent, EL_STR("/* try (C target: exception handling not supported) */"))); + cg_stmts(try_body, indent, native_list_clone(declared)); + return declared; + } return declared; return 0; } @@ -4569,6 +5547,15 @@ el_val_t builtin_arity(el_val_t name) { if (str_eq(name, EL_STR("exit_program"))) { return 1; } + if (str_eq(name, EL_STR("getpid_now"))) { + return 0; + } + if (str_eq(name, EL_STR("stdout_to_file"))) { + return 1; + } + if (str_eq(name, EL_STR("stdout_restore"))) { + return 0; + } if (str_eq(name, EL_STR("exec_command"))) { return 1; } @@ -5069,6 +6056,9 @@ el_val_t is_top_level_decl(el_val_t stmt) { if (str_eq(kind, EL_STR("ExternFn"))) { return 1; } + if (str_eq(kind, EL_STR("TestDef"))) { + return 1; + } return 0; return 0; } @@ -5262,7 +6252,169 @@ el_val_t vbd_has_restricted_call(el_val_t stmts) { return 0; } +el_val_t cg_stmt_assert_text(el_val_t stmt, el_val_t test_name) { + el_val_t expr_node = el_get_field(stmt, EL_STR("expr")); + el_val_t msg = el_get_field(stmt, EL_STR("msg")); + el_val_t expr_c = cg_expr(expr_node); + expr_c = strip_outer_parens(expr_c); + el_val_t disp_msg = EL_STR("assert failed"); + if (!str_eq(msg, EL_STR(""))) { + disp_msg = msg; + } + emit_line(el_str_concat(el_str_concat(EL_STR(" if (!("), expr_c), EL_STR(")) {"))); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" fprintf(stderr, \" FAIL "), c_escape(test_name)), EL_STR(" \\xe2\\x80\\x94 ")), c_escape(disp_msg)), EL_STR("\\n\");"))); + emit_line(EL_STR(" return 1;")); + emit_line(EL_STR(" }")); + return 0; +} + +el_val_t cg_stmt_assert_json(el_val_t stmt, el_val_t test_name, el_val_t file_name, el_val_t test_line) { + el_val_t expr_node = el_get_field(stmt, EL_STR("expr")); + el_val_t msg = el_get_field(stmt, EL_STR("msg")); + el_val_t assert_line = el_get_field(stmt, EL_STR("line")); + el_val_t expr_c = cg_expr(expr_node); + expr_c = strip_outer_parens(expr_c); + el_val_t disp_msg = EL_STR("assert failed"); + if (!str_eq(msg, EL_STR(""))) { + disp_msg = msg; + } + el_val_t json_line = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"type\":\"test_fail\",\"name\":\""), c_escape(test_name)), EL_STR("\",\"file\":\"")), c_escape(file_name)), EL_STR("\",\"line\":")), native_int_to_str(test_line)), EL_STR(",\"assert_line\":")), native_int_to_str(assert_line)), EL_STR(",\"message\":\"")), c_escape(disp_msg)), EL_STR("\"}")); + emit_line(el_str_concat(el_str_concat(EL_STR(" if (!("), expr_c), EL_STR(")) {"))); + emit_line(el_str_concat(el_str_concat(EL_STR(" puts("), c_str_lit(json_line)), EL_STR(");"))); + emit_line(EL_STR(" return 1;")); + emit_line(EL_STR(" }")); + return 0; +} + +el_val_t cg_stmts_in_test(el_val_t stmts, el_val_t indent, el_val_t declared, el_val_t test_name, el_val_t reporter, el_val_t file_name, el_val_t test_line) { + 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); + el_val_t sk = el_get_field(stmt, EL_STR("stmt")); + if (str_eq(sk, EL_STR("Assert"))) { + if (str_eq(reporter, EL_STR("json"))) { + cg_stmt_assert_json(stmt, test_name, file_name, test_line); + } else { + cg_stmt_assert_text(stmt, test_name); + } + } else { + decl = cg_stmt(stmt, indent, decl); + } + i = (i + 1); + } + return decl; + return 0; +} + +el_val_t cg_test_fn(el_val_t test_def, el_val_t idx, el_val_t reporter, el_val_t file_name) { + el_val_t fn_name = el_str_concat(EL_STR("el_test_"), native_int_to_str(idx)); + el_val_t test_name = el_get_field(test_def, EL_STR("name")); + el_val_t test_line = el_get_field(test_def, EL_STR("line")); + el_val_t body = el_get_field(test_def, EL_STR("body")); + emit_line(el_str_concat(el_str_concat(EL_STR("static int "), fn_name), EL_STR("(void) {"))); + cg_stmts_in_test(body, EL_STR(" "), native_list_empty(), test_name, reporter, file_name, test_line); + emit_line(EL_STR(" return 0;")); + emit_line(EL_STR("}")); + emit_blank(); + return fn_name; + return 0; +} + +el_val_t codegen_test(el_val_t stmts, el_val_t reporter, el_val_t file_name) { + el_val_t n = native_list_len(stmts); + el_val_t test_defs = native_list_empty(); + el_val_t 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("TestDef"))) { + test_defs = native_list_append(test_defs, stmt); + } + i = (i + 1); + } + el_val_t n_tests = native_list_len(test_defs); + el_val_t ti = 0; + while (ti < n_tests) { + el_val_t fn_name = el_str_concat(EL_STR("el_test_"), native_int_to_str(ti)); + emit_line(el_str_concat(el_str_concat(EL_STR("static int "), fn_name), EL_STR("(void);"))); + ti = (ti + 1); + } + emit_blank(); + i = 0; + while (i < n) { + el_val_t stmt = native_list_get(stmts, i); + if (is_fndef(stmt)) { + el_val_t fn_name = el_get_field(stmt, EL_STR("name")); + if (!str_eq(fn_name, EL_STR("main"))) { + cg_fn(stmt); + } + } + i = (i + 1); + } + ti = 0; + while (ti < n_tests) { + el_val_t test_def = native_list_get(test_defs, ti); + cg_test_fn(test_def, ti, reporter, file_name); + ti = (ti + 1); + } + el_val_t test_word = EL_STR("tests"); + if (n_tests == 1) { + test_word = EL_STR("test"); + } + emit_line(EL_STR("int main(void) {")); + emit_line(EL_STR(" int pass = 0; int fail = 0;")); + if (str_eq(reporter, EL_STR("json"))) { + el_val_t suite_start_json = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"type\":\"suite_start\",\"file\":\""), c_escape(file_name)), EL_STR("\",\"total\":")), native_int_to_str(n_tests)), EL_STR("}")); + emit_line(el_str_concat(el_str_concat(EL_STR(" puts("), c_str_lit(suite_start_json)), EL_STR(");"))); + ti = 0; + while (ti < n_tests) { + el_val_t test_def = native_list_get(test_defs, ti); + el_val_t test_name = el_get_field(test_def, EL_STR("name")); + el_val_t test_line = el_get_field(test_def, EL_STR("line")); + el_val_t fn_name = el_str_concat(EL_STR("el_test_"), native_int_to_str(ti)); + el_val_t start_json = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"type\":\"test_start\",\"name\":\""), c_escape(test_name)), EL_STR("\",\"file\":\"")), c_escape(file_name)), EL_STR("\",\"line\":")), native_int_to_str(test_line)), EL_STR("}")); + el_val_t pass_json = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"type\":\"test_pass\",\"name\":\""), c_escape(test_name)), EL_STR("\",\"file\":\"")), c_escape(file_name)), EL_STR("\",\"line\":")), native_int_to_str(test_line)), EL_STR(",\"duration_ms\":0}")); + emit_line(el_str_concat(el_str_concat(EL_STR(" puts("), c_str_lit(start_json)), EL_STR(");"))); + emit_line(el_str_concat(el_str_concat(EL_STR(" if ("), fn_name), EL_STR("() == 0) {"))); + emit_line(EL_STR(" pass++;")); + emit_line(el_str_concat(el_str_concat(EL_STR(" puts("), c_str_lit(pass_json)), EL_STR(");"))); + emit_line(EL_STR(" } else { fail++; }")); + ti = (ti + 1); + } + emit_line(EL_STR(" printf(\"{\\\"type\\\":\\\"suite_end\\\",\\\"passed\\\":%d,\\\"failed\\\":%d}\\n\", pass, fail);")); + } else { + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" fprintf(stderr, \"==> running "), native_int_to_str(n_tests)), EL_STR(" ")), test_word), EL_STR("\\n\\n\");"))); + ti = 0; + while (ti < n_tests) { + el_val_t test_def = native_list_get(test_defs, ti); + el_val_t test_name = el_get_field(test_def, EL_STR("name")); + el_val_t fn_name = el_str_concat(EL_STR("el_test_"), native_int_to_str(ti)); + emit_line(el_str_concat(el_str_concat(EL_STR(" fprintf(stderr, \" RUN "), c_escape(test_name)), EL_STR("\\n\");"))); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" if ("), fn_name), EL_STR("() == 0) { pass++; fprintf(stderr, \" PASS ")), c_escape(test_name)), EL_STR("\\n\"); }"))); + emit_line(EL_STR(" else { fail++; }")); + ti = (ti + 1); + } + emit_line(EL_STR(" fprintf(stderr, \"\\n%d passed, %d failed\\n\", pass, fail);")); + } + emit_line(EL_STR(" return fail > 0 ? 1 : 0;")); + emit_line(EL_STR("}")); + emit_blank(); + return 0; +} + el_val_t codegen(el_val_t stmts, el_val_t source) { + return codegen_inner(stmts, source, 0, EL_STR("text"), EL_STR("")); + return 0; +} + +el_val_t codegen_with_tests(el_val_t stmts, el_val_t source, el_val_t reporter, el_val_t file_name) { + return codegen_inner(stmts, source, 1, reporter, file_name); + return 0; +} + +el_val_t codegen_inner(el_val_t stmts, el_val_t source, el_val_t test_mode, el_val_t reporter, el_val_t file_name) { el_val_t n_top = native_list_len(stmts); el_val_t cgi_count = 0; el_val_t cgi_block = el_map_new(1, "stmt", EL_STR("None")); @@ -5310,6 +6462,7 @@ el_val_t codegen(el_val_t stmts, el_val_t source) { state_set(EL_STR("__time_violations"), EL_STR("")); emit_line(EL_STR("#include ")); emit_line(EL_STR("#include ")); + emit_line(EL_STR("#include ")); emit_line(EL_STR("#include \"el_runtime.h\"")); el_val_t imp_n = native_list_len(stmts); el_val_t imp_i = 0; @@ -5385,6 +6538,13 @@ el_val_t codegen(el_val_t stmts, el_val_t source) { if (has_toplevel_lets) { emit_blank(); } + if (test_mode) { + codegen_test(stmts, reporter, file_name); + emit_cap_violations(); + emit_arity_violations(); + emit_time_violations(); + return EL_STR(""); + } el_val_t has_el_main = 0; el_val_t has_toplevel_stmts = 0; i = 0; @@ -5594,6 +6754,69 @@ el_val_t js_binop(el_val_t op) { return 0; } +el_val_t js_is_el_method(el_val_t name) { + if (str_eq(name, EL_STR("append"))) { + return 1; + } + if (str_eq(name, EL_STR("len"))) { + return 1; + } + if (str_eq(name, EL_STR("get"))) { + return 1; + } + if (str_eq(name, EL_STR("map_get"))) { + return 1; + } + if (str_eq(name, EL_STR("map_set"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t js_is_async_builtin(el_val_t name) { + if (str_eq(name, EL_STR("http_get"))) { + return 1; + } + if (str_eq(name, EL_STR("http_post"))) { + return 1; + } + if (str_eq(name, EL_STR("http_post_json"))) { + return 1; + } + if (str_eq(name, EL_STR("http_get_with_headers"))) { + return 1; + } + if (str_eq(name, EL_STR("http_post_with_headers"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t js_register_async_fn(el_val_t name) { + el_val_t csv = state_get(EL_STR("__js_async_fns")); + if (str_eq(csv, EL_STR(""))) { + csv = 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_async_fns"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + +el_val_t js_is_async_fn(el_val_t name) { + el_val_t csv = state_get(EL_STR("__js_async_fns")); + 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_is_int_name(el_val_t name) { el_val_t csv = state_get(EL_STR("__js_int_names")); if (str_eq(csv, EL_STR(""))) { @@ -5684,6 +6907,133 @@ el_val_t js_is_int_call(el_val_t call_expr) { return 0; } +el_val_t js_next_html_id(void) { + el_val_t csv = state_get(EL_STR("__js_html_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_html_counter"), native_int_to_str(n)); + return native_int_to_str(n); + return 0; +} + +el_val_t js_cg_html_parts(el_val_t children, el_val_t acc_var) { + el_val_t n = native_list_len(children); + el_val_t i = 0; + el_val_t out = EL_STR(""); + while (i < n) { + el_val_t child = native_list_get(children, i); + el_val_t html_kind = el_get_field(child, EL_STR("html")); + if (str_eq(html_kind, EL_STR("Text"))) { + el_val_t text = el_get_field(child, EL_STR("text")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += ")), js_str_lit(text)), EL_STR("; ")); + } + if (str_eq(html_kind, EL_STR("Doctype"))) { + out = el_str_concat(el_str_concat(out, acc_var), EL_STR(" += \"\"; ")); + } + if (str_eq(html_kind, EL_STR("Interp"))) { + el_val_t val_node = el_get_field(child, EL_STR("value")); + el_val_t val_c = js_cg_expr(val_node); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += html_escape(")), val_c), EL_STR("); ")); + } + if (str_eq(html_kind, EL_STR("Raw"))) { + el_val_t val_node = el_get_field(child, EL_STR("value")); + el_val_t val_c = js_cg_expr(val_node); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += html_raw(")), val_c), EL_STR("); ")); + } + if (str_eq(html_kind, EL_STR("Element"))) { + el_val_t elem_c = js_cg_html_element_str(child, acc_var); + out = el_str_concat(out, elem_c); + } + if (str_eq(html_kind, EL_STR("Each"))) { + el_val_t each_c = js_cg_html_each(child, acc_var); + out = el_str_concat(out, each_c); + } + i = (i + 1); + } + return out; + return 0; +} + +el_val_t js_cg_html_attrs_str(el_val_t attrs, el_val_t acc_var) { + el_val_t n = native_list_len(attrs); + el_val_t i = 0; + el_val_t out = EL_STR(""); + while (i < n) { + el_val_t attr = native_list_get(attrs, i); + el_val_t attr_name = el_get_field(attr, EL_STR("name")); + el_val_t kind = el_get_field(attr, EL_STR("kind")); + el_val_t open_val = el_str_concat(el_str_concat(EL_STR(" "), attr_name), EL_STR("=\"")); + if (str_eq(kind, EL_STR("static"))) { + el_val_t sv = el_get_field(attr, EL_STR("value")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += ")), js_str_lit(open_val)), EL_STR("; ")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += ")), js_str_lit(sv)), EL_STR("; ")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += ")), js_str_lit(EL_STR("\""))), EL_STR("; ")); + } else { + if (str_eq(kind, EL_STR("dynamic"))) { + el_val_t val_node = el_get_field(attr, EL_STR("value")); + el_val_t val_c = js_cg_expr(val_node); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += ")), js_str_lit(open_val)), EL_STR("; ")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += html_escape(")), val_c), EL_STR("); ")); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += ")), js_str_lit(EL_STR("\""))), EL_STR("; ")); + } else { + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += ")), js_str_lit(el_str_concat(EL_STR(" "), attr_name))), EL_STR("; ")); + } + } + i = (i + 1); + } + return out; + return 0; +} + +el_val_t js_cg_html_element_str(el_val_t elem, el_val_t acc_var) { + el_val_t tag = el_get_field(elem, EL_STR("tag")); + el_val_t attrs = el_get_field(elem, EL_STR("attrs")); + el_val_t children = el_get_field(elem, EL_STR("children")); + el_val_t self_closing = el_get_field(elem, EL_STR("self_closing")); + el_val_t out = el_str_concat(el_str_concat(el_str_concat(acc_var, EL_STR(" += ")), js_str_lit(el_str_concat(EL_STR("<"), tag))), EL_STR("; ")); + out = el_str_concat(out, js_cg_html_attrs_str(attrs, acc_var)); + if (self_closing) { + out = el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += \"/>\"")), EL_STR("; ")); + } else { + out = el_str_concat(el_str_concat(out, acc_var), EL_STR(" += \">\"; ")); + out = el_str_concat(out, js_cg_html_parts(children, acc_var)); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, acc_var), EL_STR(" += ")), js_str_lit(el_str_concat(el_str_concat(EL_STR("")))), EL_STR("; ")); + } + return out; + return 0; +} + +el_val_t js_cg_html_each(el_val_t node, el_val_t acc_var) { + el_val_t list_expr = el_get_field(node, EL_STR("list")); + el_val_t item_name = el_get_field(node, EL_STR("item")); + el_val_t body_children = el_get_field(node, EL_STR("body")); + el_val_t id = js_next_html_id(); + el_val_t list_var = el_str_concat(EL_STR("_html_list_"), id); + el_val_t len_var = el_str_concat(EL_STR("_html_len_"), id); + el_val_t idx_var = el_str_concat(EL_STR("_html_i_"), id); + el_val_t list_c = js_cg_expr(list_expr); + el_val_t inner_c = js_cg_html_parts(body_children, acc_var); + return 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_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_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{ const "), list_var), EL_STR(" = ")), list_c), EL_STR("; const ")), len_var), EL_STR(" = el_list_len(")), list_var), EL_STR("); for (let ")), idx_var), EL_STR(" = 0; ")), idx_var), EL_STR(" < ")), len_var), EL_STR("; ")), idx_var), EL_STR("++) { const ")), item_name), EL_STR(" = el_list_get(")), list_var), EL_STR(", ")), idx_var), EL_STR("); ")), inner_c), EL_STR("} } ")); + return 0; +} + +el_val_t js_cg_html_template(el_val_t expr) { + el_val_t root = el_get_field(expr, EL_STR("root")); + el_val_t id = js_next_html_id(); + el_val_t acc = el_str_concat(EL_STR("_html_"), id); + el_val_t doctype_flag = el_get_field(root, EL_STR("doctype")); + el_val_t doctype_prefix = EL_STR(""); + if (doctype_flag) { + doctype_prefix = el_str_concat(acc, EL_STR(" += \"\"; ")); + } + el_val_t body = js_cg_html_element_str(root, acc); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("(() => { let "), acc), EL_STR(" = \"\"; ")), doctype_prefix), body), EL_STR("return ")), acc), EL_STR("; })()")); + 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"))) { @@ -5937,16 +7287,29 @@ el_val_t js_cg_expr(el_val_t expr) { el_val_t args_c = str_join(args_parts, EL_STR(", ")); 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(")")); + el_val_t call_expr = el_str_concat(el_str_concat(el_str_concat(fn_name, EL_STR("(")), args_c), EL_STR(")")); + if (js_is_async_builtin(fn_name)) { + return el_str_concat(EL_STR("await "), call_expr); + } + if (js_is_async_fn(fn_name)) { + return el_str_concat(EL_STR("await "), call_expr); + } + return call_expr; } 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(")")); + if (js_is_el_method(field)) { + 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(")")); } - return el_str_concat(el_str_concat(el_str_concat(field, EL_STR("(")), obj_c), EL_STR(")")); + if (arity > 0) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(obj_c, EL_STR(".")), field), EL_STR("(")), args_c), EL_STR(")")); + } + return el_str_concat(el_str_concat(el_str_concat(obj_c, EL_STR(".")), field), 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(")")); @@ -5954,8 +7317,14 @@ el_val_t js_cg_expr(el_val_t expr) { 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_kind = el_get_field(obj, EL_STR("expr")); + if (str_eq(obj_kind, EL_STR("Try"))) { + el_val_t inner = el_get_field(obj, EL_STR("inner")); + el_val_t inner_c = js_cg_expr(inner); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), inner_c), EL_STR(")?.[")), js_str_lit(field)), EL_STR("] ?? null")); + } 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(")")); + return el_str_concat(el_str_concat(el_str_concat(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")); @@ -5963,6 +7332,12 @@ el_val_t js_cg_expr(el_val_t expr) { 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")); + el_val_t obj_kind = el_get_field(obj, EL_STR("expr")); + if (str_eq(obj_kind, EL_STR("Try"))) { + el_val_t inner = el_get_field(obj, EL_STR("inner")); + el_val_t inner_c = js_cg_expr(inner); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), inner_c), EL_STR(")?.[")), idx_c), EL_STR("] ?? null")); + } 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(")")); } @@ -6014,6 +7389,12 @@ el_val_t js_cg_expr(el_val_t expr) { if (str_eq(kind, EL_STR("Match"))) { return js_cg_match(expr); } + if (str_eq(kind, EL_STR("Lambda"))) { + return js_cg_lambda(expr); + } + if (str_eq(kind, EL_STR("HtmlTemplate"))) { + return js_cg_html_template(expr); + } return EL_STR("null"); return 0; } @@ -6069,7 +7450,12 @@ el_val_t js_cg_match(el_val_t expr) { } parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("if ("), subj_var), EL_STR(" === ")), bv), EL_STR(") return (")), body_c), EL_STR("); "))); } else { - parts = native_list_append(parts, el_str_concat(el_str_concat(EL_STR("return ("), body_c), EL_STR("); "))); + if (str_eq(pkind, EL_STR("Variant"))) { + el_val_t variant = el_get_field(pat, EL_STR("variant")); + parts = native_list_append(parts, el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("if (str_eq("), subj_var), EL_STR(", ")), js_str_lit(variant)), EL_STR(")) return (")), body_c), EL_STR("); "))); + } else { + parts = native_list_append(parts, el_str_concat(el_str_concat(EL_STR("return ("), body_c), EL_STR("); "))); + } } } } @@ -6082,6 +7468,47 @@ el_val_t js_cg_match(el_val_t expr) { return 0; } +el_val_t js_next_lambda_id(void) { + el_val_t csv = state_get(EL_STR("__js_lambda_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_lambda_counter"), native_int_to_str(n)); + return native_int_to_str(n); + return 0; +} + +el_val_t js_cg_lambda(el_val_t expr) { + el_val_t params = el_get_field(expr, EL_STR("params")); + el_val_t body = el_get_field(expr, EL_STR("body")); + el_val_t ret_type = el_get_field(expr, EL_STR("ret_type")); + el_val_t id = js_next_lambda_id(); + el_val_t lambda_name = el_str_concat(EL_STR("__lambda_"), id); + el_val_t params_str = js_params_str(params); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("function "), lambda_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_build_int_names_for_params(params); + js_cg_stmts(body_xformed, EL_STR(" "), decl); + js_emit_line(EL_STR("}")); + js_emit_blank(); + return lambda_name; + 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; @@ -6180,14 +7607,36 @@ el_val_t js_cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared) { if (str_eq(kind, EL_STR("Import"))) { return declared; } + if (str_eq(kind, EL_STR("TestDef"))) { + return declared; + } + if (str_eq(kind, EL_STR("Assert"))) { + return declared; + } + if (str_eq(kind, EL_STR("TryCatch"))) { + el_val_t try_body = el_get_field(stmt, EL_STR("try_body")); + el_val_t catch_name = el_get_field(stmt, EL_STR("catch_name")); + el_val_t catch_body = el_get_field(stmt, EL_STR("catch_body")); + js_emit_line(el_str_concat(indent, EL_STR("try {"))); + js_cg_stmts(try_body, el_str_concat(indent, EL_STR(" ")), native_list_clone(declared)); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("} catch (")), catch_name), EL_STR(") {"))); + js_cg_stmts(catch_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("ExternFn"))) { + el_val_t ename = el_get_field(stmt, EL_STR("name")); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// extern fn ")), ename), EL_STR(" \xe2\x80\x94 provided by the JS environment"))); + 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)"))); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// cgi block '")), cname), EL_STR("' \xe2\x80\x94 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"))); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(indent, EL_STR("// service block '")), sname), EL_STR("' \xe2\x80\x94 no-op in JS target"))); return declared; } return declared; @@ -6330,12 +7779,22 @@ el_val_t js_cg_fn(el_val_t stmt) { 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 decorator = el_get_field(stmt, EL_STR("decorator")); 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(") {"))); + if (str_eq(decorator, EL_STR("async"))) { + js_register_async_fn(fn_name); + if (str_eq(fn_name, EL_STR("main"))) { + js_emit_line(el_str_concat(el_str_concat(EL_STR("async function main("), params_str), EL_STR(") {"))); + } else { + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("async function "), fn_name), EL_STR("(")), 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(") {"))); + 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); @@ -6382,40 +7841,264 @@ el_val_t js_is_top_level_decl(el_val_t stmt) { if (str_eq(kind, EL_STR("ServiceBlock"))) { return 1; } + if (str_eq(kind, EL_STR("ExternFn"))) { + return 1; + } + if (str_eq(kind, EL_STR("TestDef"))) { + 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 js_cg_stmt_assert_text(el_val_t stmt, el_val_t test_name) { + el_val_t expr_node = el_get_field(stmt, EL_STR("expr")); + el_val_t msg = el_get_field(stmt, EL_STR("msg")); + el_val_t expr_c = js_cg_expr(expr_node); + el_val_t disp_msg = EL_STR("assert failed"); + if (!str_eq(msg, EL_STR(""))) { + disp_msg = msg; + } + js_emit_line(el_str_concat(el_str_concat(EL_STR(" if (!("), expr_c), EL_STR(")) {"))); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" process.stderr.write(\" FAIL "), js_escape(test_name)), EL_STR(" \xe2\x80\x94 ")), js_escape(disp_msg)), EL_STR("\\n\");"))); + js_emit_line(EL_STR(" return false;")); + js_emit_line(EL_STR(" }")); + return 0; +} + +el_val_t js_cg_stmt_assert_json(el_val_t stmt, el_val_t test_name, el_val_t file_name, el_val_t test_line) { + el_val_t expr_node = el_get_field(stmt, EL_STR("expr")); + el_val_t msg = el_get_field(stmt, EL_STR("msg")); + el_val_t assert_line = el_get_field(stmt, EL_STR("line")); + el_val_t expr_c = js_cg_expr(expr_node); + el_val_t disp_msg = EL_STR("assert failed"); + if (!str_eq(msg, EL_STR(""))) { + disp_msg = msg; + } + js_emit_line(el_str_concat(el_str_concat(EL_STR(" if (!("), expr_c), EL_STR(")) {"))); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" process.stdout.write(JSON.stringify({type:\"test_fail\",name:"), js_str_lit(test_name)), EL_STR(",file:")), js_str_lit(file_name)), EL_STR(",line:")), native_int_to_str(test_line)), EL_STR(",assert_line:")), native_int_to_str(assert_line)), EL_STR(",message:")), js_str_lit(disp_msg)), EL_STR("}) + \"\\n\");"))); + js_emit_line(EL_STR(" return false;")); + js_emit_line(EL_STR(" }")); + return 0; +} + +el_val_t js_cg_stmts_in_test(el_val_t stmts, el_val_t indent, el_val_t declared, el_val_t test_name, el_val_t reporter, el_val_t file_name, el_val_t test_line) { 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); + el_val_t sk = el_get_field(stmt, EL_STR("stmt")); + if (str_eq(sk, EL_STR("Assert"))) { + if (str_eq(reporter, EL_STR("json"))) { + js_cg_stmt_assert_json(stmt, test_name, file_name, test_line); + } else { + js_cg_stmt_assert_text(stmt, test_name); + } + } else { + decl = js_cg_stmt(stmt, indent, decl); + } + i = (i + 1); + } + return decl; + return 0; +} + +el_val_t js_cg_test_fn(el_val_t test_def, el_val_t idx, el_val_t reporter, el_val_t file_name) { + el_val_t fn_name = el_str_concat(EL_STR("el_test_"), native_int_to_str(idx)); + el_val_t test_name = el_get_field(test_def, EL_STR("name")); + el_val_t test_line = el_get_field(test_def, EL_STR("line")); + el_val_t body = el_get_field(test_def, EL_STR("body")); + js_emit_line(el_str_concat(el_str_concat(EL_STR("async function "), fn_name), EL_STR("() {"))); + js_cg_stmts_in_test(body, EL_STR(" "), native_list_empty(), test_name, reporter, file_name, test_line); + js_emit_line(EL_STR(" return true;")); + js_emit_line(EL_STR("}")); + js_emit_blank(); + return fn_name; + return 0; +} + +el_val_t js_codegen_test(el_val_t stmts, el_val_t reporter, el_val_t file_name) { + el_val_t n = native_list_len(stmts); + el_val_t test_defs = native_list_empty(); + el_val_t 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("TestDef"))) { + test_defs = native_list_append(test_defs, stmt); + } + i = (i + 1); + } + el_val_t n_tests = native_list_len(test_defs); + i = 0; + while (i < n) { + el_val_t stmt = native_list_get(stmts, i); + if (js_is_fndef(stmt)) { + el_val_t fn_name = el_get_field(stmt, EL_STR("name")); + if (!str_eq(fn_name, EL_STR("main"))) { + js_cg_fn(stmt); + } + } + i = (i + 1); + } + el_val_t ti = 0; + while (ti < n_tests) { + el_val_t test_def = native_list_get(test_defs, ti); + js_cg_test_fn(test_def, ti, reporter, file_name); + ti = (ti + 1); + } + el_val_t test_word = EL_STR("tests"); + if (n_tests == 1) { + test_word = EL_STR("test"); + } + js_emit_line(EL_STR("(async () => {")); + js_emit_line(EL_STR(" let pass = 0; let fail = 0;")); + if (str_eq(reporter, EL_STR("json"))) { + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" process.stdout.write(JSON.stringify({type:\"suite_start\",file:"), js_str_lit(file_name)), EL_STR(",total:")), native_int_to_str(n_tests)), EL_STR("}) + \"\\n\");"))); + ti = 0; + while (ti < n_tests) { + el_val_t test_def = native_list_get(test_defs, ti); + el_val_t test_name = el_get_field(test_def, EL_STR("name")); + el_val_t test_line = el_get_field(test_def, EL_STR("line")); + el_val_t fn_name = el_str_concat(EL_STR("el_test_"), native_int_to_str(ti)); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" process.stdout.write(JSON.stringify({type:\"test_start\",name:"), js_str_lit(test_name)), EL_STR(",file:")), js_str_lit(file_name)), EL_STR(",line:")), native_int_to_str(test_line)), EL_STR("}) + \"\\n\");"))); + js_emit_line(el_str_concat(el_str_concat(EL_STR(" if (await "), fn_name), EL_STR("()) {"))); + js_emit_line(EL_STR(" pass++;")); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" process.stdout.write(JSON.stringify({type:\"test_pass\",name:"), js_str_lit(test_name)), EL_STR(",file:")), js_str_lit(file_name)), EL_STR(",line:")), native_int_to_str(test_line)), EL_STR(",duration_ms:0}) + \"\\n\");"))); + js_emit_line(EL_STR(" } else { fail++; }")); + ti = (ti + 1); + } + js_emit_line(EL_STR(" process.stdout.write(JSON.stringify({type:\"suite_end\",passed:pass,failed:fail}) + \"\\n\");")); + } else { + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" process.stderr.write(\"==> running "), native_int_to_str(n_tests)), EL_STR(" ")), test_word), EL_STR("\\n\\n\");"))); + ti = 0; + while (ti < n_tests) { + el_val_t test_def = native_list_get(test_defs, ti); + el_val_t test_name = el_get_field(test_def, EL_STR("name")); + el_val_t fn_name = el_str_concat(EL_STR("el_test_"), native_int_to_str(ti)); + js_emit_line(el_str_concat(el_str_concat(EL_STR(" process.stderr.write(\" RUN "), js_escape(test_name)), EL_STR("\\n\");"))); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(" if (await "), fn_name), EL_STR("()) { pass++; process.stderr.write(\" PASS ")), js_escape(test_name)), EL_STR("\\n\"); }"))); + js_emit_line(EL_STR(" else { fail++; }")); + ti = (ti + 1); + } + js_emit_line(EL_STR(" process.stderr.write(\"\\n\" + pass + \" passed, \" + fail + \" failed\\n\");")); + } + js_emit_line(EL_STR(" process.exit(fail > 0 ? 1 : 0);")); + js_emit_line(EL_STR("})();")); + return 0; +} + +el_val_t codegen_js(el_val_t stmts, el_val_t source) { + return codegen_js_inner(stmts, source, 0, EL_STR(""), 0, EL_STR("text"), EL_STR("")); + return 0; +} + +el_val_t codegen_js_test(el_val_t stmts, el_val_t source, el_val_t reporter, el_val_t file_name) { + return codegen_js_inner(stmts, source, 0, EL_STR(""), 1, reporter, file_name); + return 0; +} + +el_val_t codegen_js_bundle(el_val_t stmts, el_val_t source, el_val_t runtime_content) { + return codegen_js_inner(stmts, source, 1, runtime_content, 0, EL_STR("text"), EL_STR("")); + return 0; +} + +el_val_t codegen_js_inner(el_val_t stmts, el_val_t source, el_val_t bundle_mode, el_val_t runtime_content, el_val_t test_mode, el_val_t reporter, el_val_t file_name) { + state_set(EL_STR("__js_int_names"), EL_STR("")); + state_set(EL_STR("__js_match_counter"), EL_STR("")); + state_set(EL_STR("__js_async_fns"), EL_STR("")); + state_set(EL_STR("__js_lambda_counter"), EL_STR("")); + js_emit_line(EL_STR("// Generated by elc --target=js")); + if (bundle_mode) { + js_emit_line(EL_STR("// Bundle mode: runtime inlined, no import statement needed.")); + js_emit_line(EL_STR("")); + js_emit_line(EL_STR(";(function() {")); + js_emit_line(EL_STR("\"use strict\";")); + js_emit_line(js_strip_es_exports(runtime_content)); + js_emit_line(EL_STR("")); + } else { + js_emit_line(EL_STR("// Runtime: foundation/el/el-compiler/runtime/el_runtime.js")); + js_emit_line(EL_STR("import \"./el_runtime.js\";")); + } + if (!bundle_mode) { + 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(" dom_get_element, dom_get_value, dom_set_value, dom_get_text, dom_set_text,")); + js_emit_line(EL_STR(" dom_set_prop, dom_get_prop, dom_set_style, dom_add_class, dom_remove_class,")); + js_emit_line(EL_STR(" dom_show, dom_hide, dom_listen, dom_query, dom_query_all, dom_create,")); + js_emit_line(EL_STR(" dom_append, dom_remove, dom_is_null,")); + js_emit_line(EL_STR(" dom_set_attr, dom_get_attr, dom_remove_attr, dom_set_html, dom_get_html,")); + js_emit_line(EL_STR(" dom_get_parent, dom_contains_class, dom_get_checked, dom_set_checked,")); + js_emit_line(EL_STR(" set_timeout, set_interval, clear_interval,")); + js_emit_line(EL_STR(" local_storage_get, local_storage_set, local_storage_remove,")); + js_emit_line(EL_STR(" window_location, window_redirect, window_on_load,")); + js_emit_line(EL_STR(" console_log,")); + js_emit_line(EL_STR(" window_set, window_get, native_js, native_js_call,")); + js_emit_line(EL_STR(" promise_then, promise_catch, promise_resolve, promise_reject,")); + js_emit_line(EL_STR(" object_assign, object_keys, object_values, json_deep_clone,")); + js_emit_line(EL_STR(" array_from, type_of, instanceof_check,")); + 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); + el_val_t sk = el_get_field(stmt, EL_STR("stmt")); + if (str_eq(sk, EL_STR("Import"))) { + el_val_t ipath = el_get_field(stmt, EL_STR("path")); + el_val_t is_url = str_starts_with(ipath, EL_STR("http://")); + is_url = (is_url || str_starts_with(ipath, EL_STR("https://"))); + el_val_t is_js = !str_ends_with(ipath, EL_STR(".el")); + if (is_url || is_js) { + if (bundle_mode) { + js_emit_line(el_str_concat(EL_STR("// external: "), ipath)); + } else { + js_emit_line(el_str_concat(el_str_concat(EL_STR("import "), js_str_lit(ipath)), EL_STR(";"))); + } + } + } + i = (i + 1); + } + js_emit_blank(); + n = native_list_len(stmts); + 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 dec = el_get_field(stmt, EL_STR("decorator")); + if (str_eq(dec, EL_STR("async"))) { + el_val_t aname = el_get_field(stmt, EL_STR("name")); + js_register_async_fn(aname); + } + } + i = (i + 1); + } + if (test_mode) { + js_codegen_test(stmts, reporter, file_name); + return EL_STR(""); + } + i = 0; while (i < n) { el_val_t stmt = native_list_get(stmts, i); if (js_is_fndef(stmt)) { @@ -6453,10 +8136,37 @@ el_val_t codegen_js(el_val_t stmts, el_val_t source) { js_emit_blank(); js_emit_line(EL_STR("main();")); } + if (bundle_mode) { + js_emit_line(EL_STR("")); + js_emit_line(EL_STR("})();")); + } return EL_STR(""); return 0; } +el_val_t js_strip_es_exports(el_val_t content) { + el_val_t lines = str_split(content, EL_STR("\n")); + el_val_t n = native_list_len(lines); + el_val_t out = native_list_empty(); + el_val_t i = 0; + while (i < n) { + el_val_t line = native_list_get(lines, i); + el_val_t trimmed = str_trim(line); + if (str_starts_with(trimmed, EL_STR("export {"))) { + i = n; + } else { + if (str_starts_with(trimmed, EL_STR("export default"))) { + i = n; + } else { + out = native_list_append(out, line); + } + } + i = (i + 1); + } + return str_join(out, EL_STR("\n")); + return 0; +} + el_val_t compile(el_val_t source) { el_val_t tokens = lex(source); el_val_t stmts = parse(tokens); @@ -6473,6 +8183,35 @@ el_val_t compile_js(el_val_t source) { return 0; } +el_val_t compile_js_with_bundle(el_val_t source, el_val_t runtime_path) { + el_val_t tokens = lex(source); + el_val_t stmts = parse(tokens); + el_release(tokens); + el_val_t runtime_content = fs_read(runtime_path); + if (str_eq(runtime_content, EL_STR(""))) { + println(el_str_concat(EL_STR("el-compiler: warning: --bundle: could not read runtime at "), runtime_path)); + println(EL_STR("el-compiler: warning: bundle output will be incomplete")); + } + return codegen_js_bundle(stmts, source, runtime_content); + return 0; +} + +el_val_t compile_test(el_val_t source, el_val_t reporter, el_val_t file_name) { + el_val_t tokens = lex(source); + el_val_t stmts = parse(tokens); + el_release(tokens); + return codegen_with_tests(stmts, source, reporter, file_name); + return 0; +} + +el_val_t compile_js_test(el_val_t source, el_val_t reporter, el_val_t file_name) { + el_val_t tokens = lex(source); + el_val_t stmts = parse(tokens); + el_release(tokens); + return codegen_js_test(stmts, source, reporter, file_name); + 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); @@ -6481,6 +8220,22 @@ el_val_t compile_dispatch(el_val_t tgt, el_val_t source) { return 0; } +el_val_t compile_dispatch_test(el_val_t tgt, el_val_t source, el_val_t reporter, el_val_t file_name) { + if (str_eq(tgt, EL_STR("js"))) { + return compile_js_test(source, reporter, file_name); + } + return compile_test(source, reporter, file_name); + return 0; +} + +el_val_t compile_dispatch_bundle(el_val_t tgt, el_val_t source, el_val_t runtime_path) { + if (str_eq(tgt, EL_STR("js"))) { + return compile_js_with_bundle(source, runtime_path); + } + 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; @@ -6525,6 +8280,170 @@ el_val_t detect_emit_header(el_val_t argv) { return 0; } +el_val_t detect_bundle(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_eq(a, EL_STR("--bundle"))) { + return 1; + } + i = (i + 1); + } + return 0; + return 0; +} + +el_val_t detect_minify(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_eq(a, EL_STR("--minify"))) { + return 1; + } + i = (i + 1); + } + return 0; + return 0; +} + +el_val_t detect_obfuscate(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_eq(a, EL_STR("--obfuscate"))) { + return 1; + } + i = (i + 1); + } + return 0; + return 0; +} + +el_val_t detect_test(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_eq(a, EL_STR("--test"))) { + return 1; + } + i = (i + 1); + } + return 0; + return 0; +} + +el_val_t detect_reporter(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("--reporter="))) { + el_val_t v = str_slice(a, 11, str_len(a)); + return v; + } + i = (i + 1); + } + return EL_STR("text"); + return 0; +} + +el_val_t basename_of(el_val_t path) { + el_val_t n = str_len(path); + el_val_t i = (n - 1); + while (i >= 0) { + el_val_t c = str_slice(path, i, (i + 1)); + if (str_eq(c, EL_STR("/"))) { + return str_slice(path, (i + 1), n); + } + i = (i - 1); + } + return path; + return 0; +} + +el_val_t make_temp_path(el_val_t suffix) { + el_val_t pid = getpid_now(); + el_val_t ts = time_now(); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("/tmp/elc-"), native_int_to_str(pid)), EL_STR("-")), native_int_to_str(ts)), EL_STR(".")), suffix); + return 0; +} + +el_val_t js_reserved_names(void) { + return EL_STR("neuronDemoToggle,neuronDemoSend,neuronDemoReset,signInWith,signInWithEmail,signUpWithEmail,sendMagicLink,signOut,resetPassword,sendResetEmail,updatePassword,showSignIn,showSignUp,hideReset,setSort,addFamilyMember,removeFamilyMember,copyForPlatform,entHeadcountChange,NEURON_CFG"); + return 0; +} + +el_val_t find_node_tool(el_val_t tool_name, el_val_t src_dir) { + el_val_t cand1 = el_str_concat(el_str_concat(src_dir, EL_STR("/node_modules/.bin/")), tool_name); + el_val_t check1 = str_trim(exec_capture(el_str_concat(el_str_concat(EL_STR("test -x "), cand1), EL_STR(" && echo yes 2>/dev/null")))); + if (str_eq(check1, EL_STR("yes"))) { + return cand1; + } + el_val_t parent_dir = dirname_of(src_dir); + el_val_t cand2 = el_str_concat(el_str_concat(parent_dir, EL_STR("/node_modules/.bin/")), tool_name); + el_val_t check2 = str_trim(exec_capture(el_str_concat(el_str_concat(EL_STR("test -x "), cand2), EL_STR(" && echo yes 2>/dev/null")))); + if (str_eq(check2, EL_STR("yes"))) { + return cand2; + } + el_val_t npx_path = str_trim(exec_capture(EL_STR("which npx 2>/dev/null"))); + if (!str_eq(npx_path, EL_STR(""))) { + return el_str_concat(EL_STR("npx --yes "), tool_name); + } + return EL_STR(""); + return 0; +} + +el_val_t apply_minify(el_val_t js_path, el_val_t out_path, el_val_t src_dir) { + el_val_t terser = find_node_tool(EL_STR("terser"), src_dir); + if (str_eq(terser, EL_STR(""))) { + println(EL_STR("el-compiler: error: terser not found. Run 'npm install terser' in your project directory.")); + return 0; + } + el_val_t names = js_reserved_names(); + el_val_t compress_opts = EL_STR("passes=2,drop_console=false,drop_debugger=true"); + el_val_t mangle_reserved = el_str_concat(el_str_concat(EL_STR("'reserved=["), names), EL_STR("]'")); + el_val_t cmd = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(terser, EL_STR(" ")), js_path), EL_STR(" --compress ")), compress_opts), EL_STR(" --mangle ")), mangle_reserved), EL_STR(" --output ")), out_path); + el_val_t ret = exec_command(cmd); + if (ret == 0) { + return 1; + } + println(el_str_concat(el_str_concat(EL_STR("el-compiler: error: terser failed (exit "), native_int_to_str(ret)), EL_STR(")"))); + return 0; + return 0; +} + +el_val_t apply_obfuscate(el_val_t js_path, el_val_t out_path, el_val_t src_dir) { + el_val_t obfuscator = find_node_tool(EL_STR("javascript-obfuscator"), src_dir); + if (str_eq(obfuscator, EL_STR(""))) { + println(EL_STR("el-compiler: error: javascript-obfuscator not found. Run 'npm install javascript-obfuscator' in your project directory.")); + return 0; + } + el_val_t names = js_reserved_names(); + el_val_t cmd = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(obfuscator, EL_STR(" ")), js_path), EL_STR(" --output ")), out_path), EL_STR(" --compact true --simplify true --string-array true --string-array-encoding base64 --string-array-threshold 0.75 --identifier-names-generator hexadecimal --rename-globals false --self-defending false --reserved-names ")), names); + el_val_t ret = exec_command(cmd); + if (ret == 0) { + return 1; + } + println(el_str_concat(el_str_concat(EL_STR("el-compiler: error: javascript-obfuscator failed (exit "), native_int_to_str(ret)), EL_STR(")"))); + return 0; + return 0; +} + +el_val_t resolve_runtime_path(el_val_t src_path) { + el_val_t src_dir = dirname_of(src_path); + el_val_t candidate = el_str_concat(src_dir, EL_STR("/el_runtime.js")); + el_val_t existing = fs_read(candidate); + if (!str_eq(existing, EL_STR(""))) { + return candidate; + } + return EL_STR(""); + return 0; +} + el_val_t type_node_to_el(el_val_t t) { el_val_t k = el_get_field(t, EL_STR("kind")); if (str_eq(k, EL_STR("Simple"))) { @@ -6547,7 +8466,7 @@ el_val_t emit_header(el_val_t stmts, el_val_t hdr_path) { el_val_t n = native_list_len(stmts); el_val_t i = 0; el_val_t parts = native_list_empty(); - parts = native_list_append(parts, EL_STR("// auto-generated by elc --emit-header - do not edit\n")); + parts = native_list_append(parts, EL_STR("// auto-generated by elc --emit-header \xe2\x80\x94 do not edit\n")); while (i < n) { el_val_t stmt = native_list_get(stmts, i); el_val_t kind = el_get_field(stmt, EL_STR("stmt")); @@ -6661,17 +8580,78 @@ el_val_t resolve_imports(el_val_t src_path) { return 0; } +el_val_t run_with_postprocess(el_val_t tgt, el_val_t source, el_val_t src_path, el_val_t do_bundle, el_val_t do_obfuscate, el_val_t argc, el_val_t positional) { + el_val_t src_dir = dirname_of(src_path); + el_val_t tmp_gen = make_temp_path(EL_STR("js")); + el_val_t tmp_min = make_temp_path(EL_STR("min.js")); + stdout_to_file(tmp_gen); + if (do_bundle) { + el_val_t runtime_path = resolve_runtime_path(src_path); + compile_dispatch_bundle(tgt, source, runtime_path); + } else { + compile_dispatch(tgt, source); + } + stdout_restore(); + el_val_t ok_min = apply_minify(tmp_gen, tmp_min, src_dir); + if (!ok_min) { + exec_command(el_str_concat(el_str_concat(el_str_concat(EL_STR("rm -f "), tmp_gen), EL_STR(" ")), tmp_min)); + exit(1); + } + state_set(EL_STR("__elc_final_js"), tmp_min); + if (do_obfuscate) { + el_val_t tmp_obf = make_temp_path(EL_STR("obf.js")); + el_val_t ok_obf = apply_obfuscate(tmp_min, tmp_obf, src_dir); + if (!ok_obf) { + exec_command(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("rm -f "), tmp_gen), EL_STR(" ")), tmp_min), EL_STR(" ")), tmp_obf)); + exit(1); + } + state_set(EL_STR("__elc_final_js"), tmp_obf); + } + el_val_t final_path = state_get(EL_STR("__elc_final_js")); + el_val_t final_js = fs_read(final_path); + exec_command(el_str_concat(el_str_concat(el_str_concat(EL_STR("rm -f "), tmp_gen), EL_STR(" ")), tmp_min)); + if (do_obfuscate) { + exec_command(el_str_concat(EL_STR("rm -f "), final_path)); + } + if (argc >= 2) { + el_val_t out_path = native_list_get(positional, 1); + el_val_t ok = fs_write(out_path, final_js); + if (ok) { + return 0; + } else { + println(EL_STR("el-compiler: failed to write output")); + exit(1); + } + } + print(final_js); + return 0; +} + int main(int _argc, char** _argv) { el_runtime_init_args(_argc, _argv); el_val_t argv = args(); el_val_t tgt = detect_target(argv); el_val_t do_emit_header = detect_emit_header(argv); + el_val_t do_bundle = detect_bundle(argv); + el_val_t do_minify = detect_minify(argv); + el_val_t do_obfuscate = detect_obfuscate(argv); + el_val_t do_test = detect_test(argv); + el_val_t reporter = detect_reporter(argv); + if (do_obfuscate) { + do_minify = 1; + } el_val_t positional = strip_flags(argv); el_val_t argc = native_list_len(positional); if (argc < 1) { - println(EL_STR("el-compiler: usage: elc [--target=c|js] [--emit-header] []")); + println(EL_STR("el-compiler: usage: elc [--target=c|js] [--test] [--reporter=text|json] [--bundle] [--minify] [--obfuscate] [--emit-header] []")); exit(1); } + if (do_minify) { + if (!str_eq(tgt, EL_STR("js"))) { + println(EL_STR("el-compiler: error: --minify and --obfuscate require --target=js")); + exit(1); + } + } el_val_t src_path = native_list_get(positional, 0); if (do_emit_header) { el_val_t raw_source = fs_read(src_path); @@ -6683,7 +8663,31 @@ int main(int _argc, char** _argv) { el_release(hdr_stmts); } el_val_t source = resolve_imports(src_path); - el_val_t out = compile_dispatch(tgt, source); + el_val_t file_name = basename_of(src_path); + if (do_test) { + el_val_t out = compile_dispatch_test(tgt, source, reporter, file_name); + if (argc >= 2) { + el_val_t out_path = native_list_get(positional, 1); + el_val_t ok = fs_write(out_path, out); + if (ok) { + exit(0); + } + println(EL_STR("el-compiler: failed to write output")); + exit(1); + } + exit(0); + } + if (do_minify) { + run_with_postprocess(tgt, source, src_path, do_bundle, do_obfuscate, argc, positional); + exit(0); + } + el_val_t out = EL_STR(""); + if (do_bundle) { + el_val_t runtime_path = resolve_runtime_path(src_path); + out = compile_dispatch_bundle(tgt, source, runtime_path); + } else { + out = compile_dispatch(tgt, source); + } if (argc >= 2) { el_val_t out_path = native_list_get(positional, 1); el_val_t ok = fs_write(out_path, out); diff --git a/dist/platform/elc b/dist/platform/elc index 4d31bd5..3c2861e 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/elc-combined.el b/elc-combined.el index 3216641..653a092 100644 --- a/elc-combined.el +++ b/elc-combined.el @@ -98,7 +98,10 @@ fn lex_is_whitespace(ch: String) -> Bool { } fn make_tok(kind: String, value: String) -> Map { - { "kind": kind, "value": value } + let ln_s: String = state_get("__lex_line") + let ln: Int = 1 + if !str_eq(ln_s, "") { let ln = str_to_int(ln_s) } + { "kind": kind, "value": value, "line": ln } } // ── Keyword lookup ──────────────────────────────────────────────────────────── @@ -146,6 +149,9 @@ fn keyword_kind(word: String) -> String { if word == "engine" { return "Engine" } if word == "accessor" { return "Accessor" } if word == "vessel" { return "Vessel" } + if word == "extern" { return "Extern" } + if word == "try" { return "Try" } + if word == "catch" { return "Catch" } "" } @@ -156,7 +162,7 @@ fn keyword_kind(word: String) -> String { // Returns { "text": ..., "pos": i } fn scan_digits(chars: [String], start: Int, total: Int) -> Map { let i = start - let text = "" + let parts: [String] = native_list_empty() let running = true while running { if i >= total { @@ -164,20 +170,20 @@ fn scan_digits(chars: [String], start: Int, total: Int) -> Map { } else { let ch: String = native_list_get(chars, i) if lex_is_digit(ch) { - let text = text + ch + let parts = native_list_append(parts, ch) let i = i + 1 } else { let running = false } } } - { "text": text, "pos": i } + { "text": str_join(parts, ""), "pos": i } } // scan_ident — advance i while chars[i] is alphanumeric or underscore fn scan_ident(chars: [String], start: Int, total: Int) -> Map { let i = start - let text = "" + let parts: [String] = native_list_empty() let running = true while running { if i >= total { @@ -185,14 +191,14 @@ fn scan_ident(chars: [String], start: Int, total: Int) -> Map { } else { let ch: String = native_list_get(chars, i) if is_alnum_or_underscore(ch) { - let text = text + ch + let parts = native_list_append(parts, ch) let i = i + 1 } else { let running = false } } } - { "text": text, "pos": i } + { "text": str_join(parts, ""), "pos": i } } // ── Code-bearing string detection + comment strip ──────────────────────────── @@ -253,7 +259,7 @@ fn looks_like_code(s: String) -> Bool { fn strip_code_comments(s: String) -> String { let chars: [String] = native_string_chars(s) let total: Int = native_list_len(chars) - let out = "" + let out_parts: [String] = native_list_empty() let i = 0 let in_squote = false let in_dquote = false @@ -269,11 +275,11 @@ fn strip_code_comments(s: String) -> String { if in_js_string { // Backslash escape: consume next char verbatim regardless of which. if ch == "\\" { - let out = out + ch + let out_parts = native_list_append(out_parts, ch) let next_i = i + 1 if next_i < total { let nc: String = native_list_get(chars, next_i) - let out = out + nc + let out_parts = native_list_append(out_parts, nc) let prev = nc let i = next_i + 1 } else { @@ -292,7 +298,7 @@ fn strip_code_comments(s: String) -> String { } } } - let out = out + ch + let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } @@ -308,7 +314,7 @@ fn strip_code_comments(s: String) -> String { if next_ch == "/" { // URL guard: prev char ':' means this is "://", not a comment. if prev == ":" { - let out = out + ch + let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } else { @@ -360,7 +366,7 @@ fn strip_code_comments(s: String) -> String { } let prev = "" } else { - let out = out + ch + let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } @@ -369,23 +375,23 @@ fn strip_code_comments(s: String) -> String { // Open a JS string? if ch == "'" { let in_squote = true - let out = out + ch + let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } else { if ch == "\"" { let in_dquote = true - let out = out + ch + let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } else { if ch == "`" { let in_btick = true - let out = out + ch + let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } else { - let out = out + ch + let out_parts = native_list_append(out_parts, ch) let prev = ch let i = i + 1 } @@ -394,14 +400,14 @@ fn strip_code_comments(s: String) -> String { } } } - out + str_join(out_parts, "") } // scan_string — scan a quoted string literal, handling \" escapes. // Starts AFTER the opening quote. Returns { "text": content, "pos": i_after_close } fn scan_string(chars: [String], start: Int, total: Int) -> Map { let i = start - let text = "" + let parts: [String] = native_list_empty() let running = true while running { if i >= total { @@ -414,26 +420,26 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map { if next_i < total { let next_ch: String = native_list_get(chars, next_i) if next_ch == "\"" { - let text = text + "\"" + let parts = native_list_append(parts, "\"") let i = next_i + 1 } else { if next_ch == "n" { - let text = text + "\n" + let parts = native_list_append(parts, "\n") let i = next_i + 1 } else { if next_ch == "t" { - let text = text + "\t" + let parts = native_list_append(parts, "\t") let i = next_i + 1 } else { if next_ch == "r" { - let text = text + "\r" + let parts = native_list_append(parts, "\r") let i = next_i + 1 } else { if next_ch == "\\" { - let text = text + "\\" + let parts = native_list_append(parts, "\\") let i = next_i + 1 } else { - let text = text + next_ch + let parts = native_list_append(parts, next_ch) let i = next_i + 1 } } @@ -448,13 +454,13 @@ fn scan_string(chars: [String], start: Int, total: Int) -> Map { let i = i + 1 let running = false } else { - let text = text + ch + let parts = native_list_append(parts, ch) let i = i + 1 } } } } - { "text": text, "pos": i } + { "text": str_join(parts, ""), "pos": i } } // ── Main lexer ──────────────────────────────────────────────────────────────── @@ -464,12 +470,18 @@ fn lex(source: String) -> [Map] { let total: Int = native_list_len(chars) let tokens: [Map] = native_list_empty() let i: Int = 0 + let line_num: Int = 1 + state_set("__lex_line", "1") while i < total { let ch: String = native_list_get(chars, i) - // Skip whitespace + // Skip whitespace; track newlines for line-number reporting if lex_is_whitespace(ch) { + if ch == "\n" { + let line_num = line_num + 1 + state_set("__lex_line", native_int_to_str(line_num)) + } let i = i + 1 } else { // Line comments: // @@ -710,8 +722,13 @@ fn lex(source: String) -> [Map] { let tokens = native_list_append(tokens, make_tok("QuestionMark", "?")) let i = i + 1 } else { - // unknown char — skip - let i = i + 1 + if ch == "#" { + let tokens = native_list_append(tokens, make_tok("Hash", "#")) + let i = i + 1 + } else { + // unknown char — skip + let i = i + 1 + } } } } @@ -773,6 +790,15 @@ fn tok_value(tokens: [Map], pos: Int) -> String { t["value"] } +// tok_line — return the source line number of the token at pos (1-indexed). +// Returns 1 if the token has no "line" field or line is 0. +fn tok_line(tokens: [Map], pos: Int) -> Int { + let t = native_list_get(tokens, pos) + let ln: Int = t["line"] + if ln <= 0 { return 1 } + ln +} + fn expect(tokens: [Map], pos: Int, kind: String) -> Int { let k = tok_kind(tokens, pos) if k == kind { @@ -888,6 +914,460 @@ fn parse_params(tokens: [Map], pos: Int) -> Map { // ── Expression parsing ──────────────────────────────────────────────────────── +// ── HTML template parser ────────────────────────────────────────────────────── +// +// HTML templates are written as unquoted HTML in expression position: +// return

{title}

+// +// The parser detects an HTML template when parse_primary sees Lt followed +// by a lowercase ident (a known or assumed HTML element name) or `!` (for +// ). It then recursively parses the HTML into an AST. +// +// AST nodes produced: +// { "expr": "HtmlTemplate", "root": child_node } +// { "html": "Element", "tag": "div", "attrs": [...], "children": [...], "self_closing": bool } +// { "html": "Text", "text": "..." } +// { "html": "Interp", "value": expr_node } +// { "html": "Each", "list": expr_node, "item": "name", "body": [...] } +// { "html": "Doctype" } +// { "html": "Raw", "value": expr_node } + +fn is_html_tag_name(name: String) -> Bool { + if str_eq(name, "a") { return true } + if str_eq(name, "abbr") { return true } + if str_eq(name, "address") { return true } + if str_eq(name, "area") { return true } + if str_eq(name, "article") { return true } + if str_eq(name, "aside") { return true } + if str_eq(name, "audio") { return true } + if str_eq(name, "b") { return true } + if str_eq(name, "base") { return true } + if str_eq(name, "blockquote") { return true } + if str_eq(name, "body") { return true } + if str_eq(name, "br") { return true } + if str_eq(name, "button") { return true } + if str_eq(name, "canvas") { return true } + if str_eq(name, "caption") { return true } + if str_eq(name, "cite") { return true } + if str_eq(name, "code") { return true } + if str_eq(name, "col") { return true } + if str_eq(name, "colgroup") { return true } + if str_eq(name, "data") { return true } + if str_eq(name, "datalist") { return true } + if str_eq(name, "dd") { return true } + if str_eq(name, "del") { return true } + if str_eq(name, "details") { return true } + if str_eq(name, "dfn") { return true } + if str_eq(name, "dialog") { return true } + if str_eq(name, "div") { return true } + if str_eq(name, "dl") { return true } + if str_eq(name, "dt") { return true } + if str_eq(name, "em") { return true } + if str_eq(name, "embed") { return true } + if str_eq(name, "fieldset") { return true } + if str_eq(name, "figcaption") { return true } + if str_eq(name, "figure") { return true } + if str_eq(name, "footer") { return true } + if str_eq(name, "form") { return true } + if str_eq(name, "h1") { return true } + if str_eq(name, "h2") { return true } + if str_eq(name, "h3") { return true } + if str_eq(name, "h4") { return true } + if str_eq(name, "h5") { return true } + if str_eq(name, "h6") { return true } + if str_eq(name, "head") { return true } + if str_eq(name, "header") { return true } + if str_eq(name, "hr") { return true } + if str_eq(name, "html") { return true } + if str_eq(name, "i") { return true } + if str_eq(name, "iframe") { return true } + if str_eq(name, "img") { return true } + if str_eq(name, "input") { return true } + if str_eq(name, "ins") { return true } + if str_eq(name, "kbd") { return true } + if str_eq(name, "label") { return true } + if str_eq(name, "legend") { return true } + if str_eq(name, "li") { return true } + if str_eq(name, "link") { return true } + if str_eq(name, "main") { return true } + if str_eq(name, "map") { return true } + if str_eq(name, "mark") { return true } + if str_eq(name, "menu") { return true } + if str_eq(name, "meta") { return true } + if str_eq(name, "meter") { return true } + if str_eq(name, "nav") { return true } + if str_eq(name, "noscript") { return true } + if str_eq(name, "object") { return true } + if str_eq(name, "ol") { return true } + if str_eq(name, "optgroup") { return true } + if str_eq(name, "option") { return true } + if str_eq(name, "output") { return true } + if str_eq(name, "p") { return true } + if str_eq(name, "param") { return true } + if str_eq(name, "picture") { return true } + if str_eq(name, "pre") { return true } + if str_eq(name, "progress") { return true } + if str_eq(name, "q") { return true } + if str_eq(name, "rp") { return true } + if str_eq(name, "rt") { return true } + if str_eq(name, "ruby") { return true } + if str_eq(name, "s") { return true } + if str_eq(name, "samp") { return true } + if str_eq(name, "script") { return true } + if str_eq(name, "section") { return true } + if str_eq(name, "select") { return true } + if str_eq(name, "small") { return true } + if str_eq(name, "source") { return true } + if str_eq(name, "span") { return true } + if str_eq(name, "strong") { return true } + if str_eq(name, "style") { return true } + if str_eq(name, "sub") { return true } + if str_eq(name, "summary") { return true } + if str_eq(name, "sup") { return true } + if str_eq(name, "table") { return true } + if str_eq(name, "tbody") { return true } + if str_eq(name, "td") { return true } + if str_eq(name, "template") { return true } + if str_eq(name, "textarea") { return true } + if str_eq(name, "tfoot") { return true } + if str_eq(name, "th") { return true } + if str_eq(name, "thead") { return true } + if str_eq(name, "time") { return true } + if str_eq(name, "title") { return true } + if str_eq(name, "tr") { return true } + if str_eq(name, "track") { return true } + if str_eq(name, "u") { return true } + if str_eq(name, "ul") { return true } + if str_eq(name, "var") { return true } + if str_eq(name, "video") { return true } + if str_eq(name, "wbr") { return true } + false +} + +fn is_void_element(name: String) -> Bool { + if str_eq(name, "area") { return true } + if str_eq(name, "base") { return true } + if str_eq(name, "br") { return true } + if str_eq(name, "col") { return true } + if str_eq(name, "embed") { return true } + if str_eq(name, "hr") { return true } + if str_eq(name, "img") { return true } + if str_eq(name, "input") { return true } + if str_eq(name, "link") { return true } + if str_eq(name, "meta") { return true } + if str_eq(name, "param") { return true } + if str_eq(name, "source") { return true } + if str_eq(name, "track") { return true } + if str_eq(name, "wbr") { return true } + false +} + +// Collect tokens as text content until we hit Lt, LBrace, Eof, or a +// closing-tag marker (Lt Slash). Returns { "text": "...", "pos": p } +fn parse_html_text_tokens(tokens: [Map], pos: Int) -> Map { + let parts: [String] = native_list_empty() + let p = pos + let running = true + while running { + let k = tok_kind(tokens, p) + if str_eq(k, "Eof") { + let running = false + } else { + if str_eq(k, "Lt") { + let running = false + } else { + if str_eq(k, "LBrace") { + let running = false + } else { + // Check for ). +fn parse_html_attrs(tokens: [Map], pos: Int) -> Map { + let attrs: [Map] = native_list_empty() + let p = pos + let running = true + while running { + let k = tok_kind(tokens, p) + if str_eq(k, "Gt") { + let running = false + } else { + if str_eq(k, "Slash") { + let running = false + } else { + if str_eq(k, "Eof") { + let running = false + } else { + // Attribute name: could be Ident or keyword used as attr name + let attr_name = tok_value(tokens, p) + let p = p + 1 + let k2 = tok_kind(tokens, p) + if str_eq(k2, "Eq") { + let p = p + 1 + let k3 = tok_kind(tokens, p) + if str_eq(k3, "Str") { + // static: attr="value" + let attr_val = tok_value(tokens, p) + let p = p + 1 + let attrs = native_list_append(attrs, { "name": attr_name, "kind": "static", "value": attr_val }) + } else { + if str_eq(k3, "LBrace") { + // dynamic: attr={expr} + let r = parse_expr(tokens, p + 1) + let val_node = r["node"] + let p = r["pos"] + let p = expect(tokens, p, "RBrace") + let attrs = native_list_append(attrs, { "name": attr_name, "kind": "dynamic", "value": val_node }) + } else { + // malformed, skip + } + } + } else { + // boolean attribute + let attrs = native_list_append(attrs, { "name": attr_name, "kind": "bool" }) + } + } + } + } + } + { "attrs": attrs, "pos": p } +} + +// Parse the children of an HTML element until we see the closing tag +// or EOF. Returns { "children": [...], "pos": p_after_closing_tag } +fn parse_html_children(tokens: [Map], pos: Int, parent_tag: String) -> Map { + let children: [Map] = native_list_empty() + let p = pos + let running = true + while running { + let k = tok_kind(tokens, p) + if str_eq(k, "Eof") { + let running = false + } else { + if str_eq(k, "Lt") { + // Check for closing tag: — consume and stop + let p = p + 2 + // skip tag name + let close_name = tok_value(tokens, p) + let p = p + 1 + // consume > + let p = expect(tokens, p, "Gt") + let running = false + } else { + if str_eq(k2, "Not") { + // Possible + let k3_v = tok_value(tokens, p + 2) + if str_eq(k3_v, "doctype") { + // consume + let p = p + 2 + // skip until > + let scanning = true + while scanning { + let ck = tok_kind(tokens, p) + if str_eq(ck, "Eof") { let scanning = false } + if str_eq(ck, "Gt") { + let p = p + 1 + let scanning = false + } else { + let p = p + 1 + } + } + let children = native_list_append(children, { "html": "Doctype" }) + } else { + let p = p + 1 + } + } else { + // nested element + let r = parse_html_element(tokens, p) + let child = r["node"] + let p = r["pos"] + let children = native_list_append(children, child) + } + } + } else { + if str_eq(k, "LBrace") { + // Interpolation: {expr} or {#each ...} or {/each} + let k2 = tok_kind(tokens, p + 1) + if str_eq(k2, "Hash") { + // {#each list as item} + let k3_v = tok_value(tokens, p + 2) + if str_eq(k3_v, "each") { + let p = p + 3 + // parse list expr up to "as" keyword + let prev_no_block: String = state_get("__no_block_expr") + state_set("__no_block_expr", "1") + let r_list = parse_expr(tokens, p) + state_set("__no_block_expr", prev_no_block) + let list_expr = r_list["node"] + let p = r_list["pos"] + // expect "as" + let p = expect(tokens, p, "As") + // item variable name + let item_name = tok_value(tokens, p) + let p = p + 1 + // consume closing } + let p = expect(tokens, p, "RBrace") + // parse body until {/each} + let r_body = parse_html_each_body(tokens, p) + let body_children = r_body["children"] + let p = r_body["pos"] + let each_node: Map = { "html": "Each", "list": list_expr, "item": item_name, "body": body_children } + let children = native_list_append(children, each_node) + } else { + let p = p + 1 + } + } else { + if str_eq(k2, "Slash") { + // {/each} — end of each block, stop + // skip {/each} + let p = p + 2 + // skip "each" + let p = p + 1 + // skip } + let p = expect(tokens, p, "RBrace") + let running = false + } else { + // regular {expr} + let r = parse_expr(tokens, p + 1) + let interp_val = r["node"] + let p = r["pos"] + let p = expect(tokens, p, "RBrace") + // Check if the expr is a call to raw() + let is_raw_call = false + let interp_kind: String = interp_val["expr"] + if str_eq(interp_kind, "Call") { + let fn_node = interp_val["func"] + let fn_kind: String = fn_node["expr"] + if str_eq(fn_kind, "Ident") { + let fn_name_v: String = fn_node["name"] + if str_eq(fn_name_v, "raw") { + let is_raw_call = true + } + } + } + if is_raw_call { + let raw_args = interp_val["args"] + let raw_inner = native_list_get(raw_args, 0) + let children = native_list_append(children, { "html": "Raw", "value": raw_inner }) + } else { + let children = native_list_append(children, { "html": "Interp", "value": interp_val }) + } + } + } + } else { + // Text tokens — collect run of non-special tokens + let r_text = parse_html_text_tokens(tokens, p) + let text_str: String = r_text["text"] + let p = r_text["pos"] + let text_trimmed: String = str_trim(text_str) + if !str_eq(text_trimmed, "") { + let children = native_list_append(children, { "html": "Text", "text": text_trimmed }) + } + } + } + } + } + { "children": children, "pos": p } +} + +// Parse body of {#each} until {/each}. Mirrors parse_html_children but +// stops at the {/each} sentinel rather than a closing element tag. +fn parse_html_each_body(tokens: [Map], pos: Int) -> Map { + parse_html_children(tokens, pos, "__each__") +} + +// Parse a single HTML element: children +// or self-closing: +// Pos points to the Lt token. +fn parse_html_element(tokens: [Map], pos: Int) -> Map { + let p = pos + // consume < + let p = expect(tokens, p, "Lt") + // tag name + let tag_name = tok_value(tokens, p) + let p = p + 1 + // parse attributes + let r_attrs = parse_html_attrs(tokens, p) + let attrs = r_attrs["attrs"] + let p = r_attrs["pos"] + // check for self-closing /> or void element + let k = tok_kind(tokens, p) + let self_closing = false + if str_eq(k, "Slash") { + // /> + let p = p + 1 + let p = expect(tokens, p, "Gt") + let self_closing = true + return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p) + } + // consume > + let p = expect(tokens, p, "Gt") + // void elements have no children, no closing tag + if is_void_element(tag_name) { + return make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": native_list_empty(), "self_closing": true }, p) + } + // parse children + let r_children = parse_html_children(tokens, p, tag_name) + let children = r_children["children"] + let p = r_children["pos"] + make_result({ "html": "Element", "tag": tag_name, "attrs": attrs, "children": children, "self_closing": false }, p) +} + +// Entry point for HTML template parsing. +// Pos points to Lt (or Lt Not for ). +// May parse an optional prefix followed by the root element. +fn parse_html_template(tokens: [Map], pos: Int) -> Map { + let p = pos + // Check for + let doctype = false + let k = tok_kind(tokens, p) + let k2 = tok_kind(tokens, p + 1) + if str_eq(k, "Lt") { + if str_eq(k2, "Not") { + let k3_v = tok_value(tokens, p + 2) + if str_eq(k3_v, "doctype") { + let doctype = true + // consume + let p = p + 2 + let scanning = true + while scanning { + let ck = tok_kind(tokens, p) + if str_eq(ck, "Eof") { let scanning = false } + if str_eq(ck, "Gt") { + let p = p + 1 + let scanning = false + } else { + let p = p + 1 + } + } + } + } + } + // Parse root element + let r = parse_html_element(tokens, p) + let root = r["node"] + let p = r["pos"] + let root_with_doctype = root + if doctype { + let root_with_doctype = { "html": root["html"], "tag": root["tag"], "attrs": root["attrs"], "children": root["children"], "self_closing": root["self_closing"], "doctype": true } + } + make_result({ "expr": "HtmlTemplate", "root": root_with_doctype }, p) +} + fn parse_primary(tokens: [Map], pos: Int) -> Map { let k = tok_kind(tokens, pos) let v = tok_value(tokens, pos) @@ -912,6 +1392,22 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { return make_result({ "expr": "Bool", "value": v }, pos + 1) } + // HTML template literal: ... or ... + // Detected in value position only; `<` in comparison position is already + // consumed by parse_binop before parse_primary is reached. + if k == "Lt" { + let k2 = tok_kind(tokens, pos + 1) + if str_eq(k2, "Not") { + return parse_html_template(tokens, pos) + } + if str_eq(k2, "Ident") { + let tag_candidate = tok_value(tokens, pos + 1) + if is_html_tag_name(tag_candidate) { + return parse_html_template(tokens, pos) + } + } + } + // Identifier if k == "Ident" { return make_result({ "expr": "Ident", "name": v }, pos + 1) @@ -1025,6 +1521,30 @@ fn parse_primary(tokens: [Map], pos: Int) -> Map { return r } + // Anonymous function literal (lambda): fn(params) -> RetType { body } + // Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... }) + // Produces a Lambda expression node (distinct from a named FnDef statement). + if k == "Fn" { + let p = pos + 1 + let r = parse_params(tokens, p) + let params = r["params"] + let p = r["pos"] + let ret_type = "" + let k2 = tok_kind(tokens, p) + if k2 == "Arrow" { + let p = p + 1 + let kt = tok_kind(tokens, p) + if kt == "Ident" { + let ret_type = tok_value(tokens, p) + } + let p = skip_type(tokens, p) + } + let r2 = parse_block(tokens, p) + let body = r2["stmts"] + let p = r2["pos"] + return make_result({ "expr": "Lambda", "params": params, "body": body, "ret_type": ret_type }, p) + } + // Unary not if k == "Not" { let r = parse_primary(tokens, pos + 1) @@ -1154,6 +1674,13 @@ fn parse_pattern(tokens: [Map], pos: Int) -> Map { if v == "_" { return make_result({ "pattern": "Wildcard" }, pos + 1) } + // Check for Enum::Variant pattern (Color::Red, Status::Ok, etc.) + // Lexed as: Ident ColonColon Ident + let next_k = tok_kind(tokens, pos + 1) + if next_k == "ColonColon" { + let variant_name = tok_value(tokens, pos + 2) + return make_result({ "pattern": "Variant", "enum_name": v, "variant": variant_name }, pos + 3) + } return make_result({ "pattern": "Binding", "name": v }, pos + 1) } if k == "Int" { @@ -1433,6 +1960,29 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { return make_result({ "stmt": "Return", "value": val }, p) } + // extern fn declaration (no body — forward declaration for separate compilation) + if k == "Extern" { + let p = pos + 1 + let k2: String = tok_kind(tokens, p) + if str_eq(k2, "Fn") { + let p = p + 1 + let name: String = tok_value(tokens, p) + let p = p + 1 + let r = parse_params(tokens, p) + let params = r["params"] + let p = r["pos"] + let ret_type = "" + let k3: String = tok_kind(tokens, p) + if str_eq(k3, "Arrow") { + let p = p + 1 + let kt: String = tok_kind(tokens, p) + if str_eq(kt, "Ident") { let ret_type = tok_value(tokens, p) } + let p = skip_type(tokens, p) + } + return make_result({ "stmt": "ExternFn", "name": name, "params": params, "ret_type": ret_type }, p) + } + } + // fn definition if k == "Fn" { let p = pos + 1 @@ -1460,11 +2010,16 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { return make_result({ "stmt": "FnDef", "name": name, "params": params, "body": body, "ret_type": ret_type }, p) } - // type definition + // type definition: `type Name = { field: Type, ... }` + // The `=` between the name and the brace is optional in the spec but + // present in practice. Skip it if present before consuming the LBrace. if k == "Type" { let p = pos + 1 let name = tok_value(tokens, p) let p = p + 1 + // Consume optional `=` before the opening brace + let pk = tok_kind(tokens, p) + if pk == "Eq" { let p = p + 1 } let p = expect(tokens, p, "LBrace") let fields: [Map] = native_list_empty() let running = true @@ -1599,6 +2154,40 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { return make_result({ "stmt": "For", "item": item_name, "list": list_expr, "body": body }, p) } + // try/catch statement + // try { body } catch (name: Type) { handler } + // The catch variable name and type are both captured; type is skipped. + if k == "Try" { + let p = pos + 1 + let r_try = parse_block(tokens, p) + let try_body = r_try["stmts"] + let p = r_try["pos"] + let catch_name = "err" + let k2 = tok_kind(tokens, p) + if str_eq(k2, "Catch") { + let p = p + 1 + let p = expect(tokens, p, "LParen") + // catch variable name + let kn = tok_kind(tokens, p) + if str_eq(kn, "Ident") { + let catch_name = tok_value(tokens, p) + let p = p + 1 + } + // optional type annotation: : Type + let k3 = tok_kind(tokens, p) + if str_eq(k3, "Colon") { + let p = p + 1 + let p = skip_type(tokens, p) + } + let p = expect(tokens, p, "RParen") + let r_catch = parse_block(tokens, p) + let catch_body = r_catch["stmts"] + let p = r_catch["pos"] + return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": catch_body }, p) + } + return make_result({ "stmt": "TryCatch", "try_body": try_body, "catch_name": catch_name, "catch_body": native_list_empty() }, p) + } + // @decorator — capture decorator name and attach to following stmt if k == "At" { let p = pos + 1 @@ -1737,6 +2326,35 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { }, p) } + // assert statement: assert or assert , "msg" + if k == "Assert" { + let assert_line: Int = tok_line(tokens, pos) + let p = pos + 1 + let r = parse_expr(tokens, p) + let expr_node = r["node"] + let p = r["pos"] + let msg = "" + let k2 = tok_kind(tokens, p) + if k2 == "Comma" { + let p = p + 1 + let msg = tok_value(tokens, p) + let p = p + 1 + } + return make_result({ "stmt": "Assert", "expr": expr_node, "msg": msg, "line": assert_line }, p) + } + + // test block: test "name" { stmts } + if k == "Test" { + let test_line: Int = tok_line(tokens, pos) + let p = pos + 1 + let name = tok_value(tokens, p) + let p = p + 1 + let r2 = parse_block(tokens, p) + let body = r2["stmts"] + let p = r2["pos"] + return make_result({ "stmt": "TestDef", "name": name, "body": body, "line": test_line }, p) + } + // Bare reassignment: `name = expr`. Handled BEFORE the expression // fallback so we don't drop the assign on the floor and emit three // orphan expressions (the original silent-miscompile bug). El's `let` @@ -1792,7 +2410,7 @@ fn parse(tokens: [Map]) -> [Map] { } stmts } -// codegen.el — El compiler C source code generator +// codegen.el - El compiler C source code generator // // Input: list of AST statement maps (from parser.el) // Output: C source printed to stdout (streamed, one line at a time) @@ -1801,37 +2419,90 @@ fn parse(tokens: [Map]) -> [Map] { // Functions map directly to C functions; top-level statements become main(). // // Entry point: fn codegen(stmts: [Map], source: String) -> String -// Returns "" — output goes to stdout via println(). +// Returns "" - output goes to stdout via println(). // -// Streaming output avoids O(n²) string concatenation: each emitted line is +// Streaming output avoids O(n-) string concatenation: each emitted line is // printed immediately rather than appended to a growing string. -// ── String helpers ──────────────────────────────────────────────────────────── +// -- String helpers ------------------------------------------------------------ // Escape a C string literal (double-quotes and backslashes). +// Hex-encode a single nibble (0-15) as a lowercase hex character. +fn nibble_to_hex(n: Int) -> String { + str_char_at("0123456789abcdef", n) +} + +// Encode a byte value (0-255) as a two-character hex string. +fn byte_to_hex2(b: Int) -> String { + let hi: Int = (b / 16) + let lo: Int = (b - hi * 16) + nibble_to_hex(hi) + nibble_to_hex(lo) +} + +// Return true if the byte value is a C hex digit (0-9, a-f, A-F). +// Used to determine whether a \xNN escape needs a string-literal split +// to prevent the C preprocessor from greedily consuming following hex chars. +fn is_hex_digit_byte(b: Int) -> Bool { + if b >= 48 { if b <= 57 { return true } } // 0-9 + if b >= 65 { if b <= 70 { return true } } // A-F + if b >= 97 { if b <= 102 { return true } } // a-f + false +} + fn c_escape(s: String) -> String { - let chars: [String] = native_string_chars(s) - let total: Int = native_list_len(chars) - let out = "" - let i = 0 + // Use index-based byte scanning via str_char_code(s, i) and str_char_at(s, i). + // This avoids native_string_chars + str_join, which corrupts high-byte (>= 0x80) + // characters because list_join's looks_like_string heuristic rejects strings + // whose first byte is >= 0x7F and emits them as decimal pointer values instead. + // + // IMPORTANT: after a \xNN hex escape, if the next byte is a hex digit + // (0-9, a-f, A-F), we emit `""` to split the C string literal so the C + // compiler does not greedily read extra hex digits as part of the escape. + // E.g. "\xad" followed by "bamos" must become "\xad" "bamos" because 'b' + // is a hex digit and C would otherwise read "\xadb" (= 0xADB, out of range). + let total: Int = str_len(s) + let parts: [String] = native_list_empty() + let i: Int = 0 + let prev_was_hex_escape: Bool = false while i < total { - let ch: String = native_list_get(chars, i) - if ch == "\"" { - let out = out + "\\\"" + let bval: Int = str_char_code(s, i) + // If the previous token was a \xNN escape and the current byte is a + // hex digit, insert an empty string literal ("") to break the escape. + if prev_was_hex_escape { + if is_hex_digit_byte(bval) { + let parts = native_list_append(parts, "\"\"") + } + } + let prev_was_hex_escape = false + if bval == 34 { + // 34 = '"' + let parts = native_list_append(parts, "\\\"") } else { - if ch == "\\" { - let out = out + "\\\\" + if bval == 92 { + // 92 = '\\' + let parts = native_list_append(parts, "\\\\") } else { - if ch == "\n" { - let out = out + "\\n" + if bval == 10 { + // 10 = '\n' + let parts = native_list_append(parts, "\\n") } else { - if ch == "\r" { - let out = out + "\\r" + if bval == 13 { + // 13 = '\r' + let parts = native_list_append(parts, "\\r") } else { - if ch == "\t" { - let out = out + "\\t" + if bval == 9 { + // 9 = '\t' + let parts = native_list_append(parts, "\\t") } else { - let out = out + ch + if bval >= 128 { + // Escape non-ASCII bytes (>= 0x80) as \xNN so + // Clang does not misinterpret multi-byte UTF-8 + // sequences in C string literals. + let parts = native_list_append(parts, "\\x" + byte_to_hex2(bval)) + let prev_was_hex_escape = true + } else { + let parts = native_list_append(parts, str_char_at(s, i)) + } } } } @@ -1839,14 +2510,14 @@ fn c_escape(s: String) -> String { } let i = i + 1 } - out + str_join(parts, "") } fn c_str_lit(s: String) -> String { "\"" + c_escape(s) + "\"" } -// ── Type mapping ────────────────────────────────────────────────────────────── +// -- Type mapping -------------------------------------------------------------- fn el_type_to_c(type_str: String) -> String { if type_str == "String" { return "const char*" } @@ -1858,7 +2529,7 @@ fn el_type_to_c(type_str: String) -> String { "void*" } -// ── Code emission ───────────────────────────────────────────────────────────── +// -- Code emission ------------------------------------------------------------- // // emit_line/emit_blank stream output directly via println. // This avoids building a large string in memory. @@ -1871,7 +2542,7 @@ fn emit_blank() -> Void { println("") } -// ── Operator helpers ────────────────────────────────────────────────────────── +// -- Operator helpers ---------------------------------------------------------- fn binop_to_c(op: String) -> String { if op == "Plus" { return "+" } @@ -1889,11 +2560,11 @@ fn binop_to_c(op: String) -> String { op } -// ── Expression codegen ──────────────────────────────────────────────────────── +// -- Expression codegen -------------------------------------------------------- // // cg_expr returns a C expression string (not a statement). -// duration_unit_nanos — multiplier from a postfix-literal unit name to +// duration_unit_nanos - multiplier from a postfix-literal unit name to // nanoseconds. Singular and plural forms collapse to the same multiplier; // the parser already restricted `unit` to the set is_duration_unit accepts. // Returns the multiplier as a decimal string suitable for splicing into @@ -1916,6 +2587,167 @@ fn duration_unit_nanos(unit: String) -> String { "1LL" } +// ── HTML template codegen ───────────────────────────────────────────────── +// +// cg_html_template(expr) emits a C statement-expression `({ ... })` that +// builds the HTML string by chaining el_str_concat calls. +// +// Interpolated values are passed through html_escape(); the raw() form +// bypasses escaping. {#each} blocks compile to C for-loops that index into +// the list with el_list_get / el_list_len. +// +// A per-template accumulator variable `_html_N` holds the growing string. +// A global counter stored in state keeps names unique. + +fn next_html_id() -> String { + let csv: String = state_get("__html_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__html_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +// Emit children nodes into a flat list of C fragment strings (parts). +// Each part is either a static string fragment (already C-literal form) or +// a dynamic expression that produces an el_val_t string. +// We build them all into parts, then the caller wraps with concat chain. + +fn cg_html_parts(children: [Map], acc_var: String) -> String { + let n: Int = native_list_len(children) + let i = 0 + let out = "" + while i < n { + let child: Map = native_list_get(children, i) + let html_kind: String = child["html"] + if str_eq(html_kind, "Text") { + let text: String = child["text"] + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(text) + ")); " + } + if str_eq(html_kind, "Doctype") { + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"\")); " + } + if str_eq(html_kind, "Interp") { + let val_node = child["value"] + let val_c: String = cg_expr(val_node) + let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); " + } + if str_eq(html_kind, "Raw") { + let val_node = child["value"] + let val_c: String = cg_expr(val_node) + let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_raw(" + val_c + ")); " + } + if str_eq(html_kind, "Element") { + let elem_c: String = cg_html_element_str(child, acc_var) + let out = out + elem_c + } + if str_eq(html_kind, "Each") { + let each_c: String = cg_html_each(child, acc_var) + let out = out + each_c + } + let i = i + 1 + } + out +} + +// Generate open-tag attribute fragments inline. +// Parser stores attrs with "kind": "static" | "dynamic" | "bool". +// Static: "value" is the raw string value (not an expr node). +// Dynamic: "value" is an expr node. +// Bool: no "value" field. +fn cg_html_attrs_str(attrs: [Map], acc_var: String) -> String { + let n: Int = native_list_len(attrs) + let i = 0 + let out = "" + // Closing-quote snippet: EL_STR("\"") in C text. + let close_q: String = "EL_STR(" + c_str_lit("\"") + ")" + while i < n { + let attr: Map = native_list_get(attrs, i) + let attr_name: String = attr["name"] + let kind: String = attr["kind"] + // Build: EL_STR(" name=\"") + let open_val: String = " " + attr_name + "=\"" + let open_attr: String = "EL_STR(" + c_str_lit(open_val) + ")" + if str_eq(kind, "static") { + // Static attribute: value is a raw string. + let sv: String = attr["value"] + let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); " + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(" + c_str_lit(sv) + ")); " + let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); " + } else { + if str_eq(kind, "dynamic") { + // Dynamic attribute: value is an expr node — html_escape it. + let val_node = attr["value"] + let val_c: String = cg_expr(val_node) + let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + open_attr + "); " + let out = out + acc_var + " = el_str_concat(" + acc_var + ", html_escape(" + val_c + ")); " + let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + close_q + "); " + } else { + // Boolean attribute (no value): emit " name" + let bool_attr: String = "EL_STR(" + c_str_lit(" " + attr_name) + ")" + let out = out + acc_var + " = el_str_concat(" + acc_var + ", " + bool_attr + "); " + } + } + let i = i + 1 + } + out +} + +// Generate code for a single element, appending into acc_var. +fn cg_html_element_str(elem: Map, acc_var: String) -> String { + let tag: String = elem["tag"] + let attrs: [Map] = elem["attrs"] + let children: [Map] = elem["children"] + let self_closing: Bool = elem["self_closing"] + // Open tag: + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"/>\")); " + } else { + // Close open tag: > + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\">\")); " + let out = out + cg_html_parts(children, acc_var) + let out = out + acc_var + " = el_str_concat(" + acc_var + ", EL_STR(\"\")); " + } + out +} + +// Generate code for {#each list as item} ... {/each}. +fn cg_html_each(node: Map, acc_var: String) -> String { + let list_expr = node["list"] + let item_name: String = node["item"] + let body_children: [Map] = node["body"] + let id: String = next_html_id() + let list_var: String = "_html_list_" + id + let len_var: String = "_html_len_" + id + let idx_var: String = "_html_i_" + id + let list_c: String = cg_expr(list_expr) + let inner_c: String = cg_html_parts(body_children, acc_var) + // Emit: { el_val_t _list = expr; int _len = el_list_len(_list); + // for (int _i = 0; _i < _len; _i++) { + // el_val_t item = el_list_get(_list, _i); inner_c } } + "{ el_val_t " + list_var + " = (" + list_c + "); el_val_t " + len_var + " = el_list_len(" + list_var + "); for (el_val_t " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { el_val_t " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } " +} + +// Top-level HTML template codegen — returns a C statement-expression string. +fn cg_html_template(expr: Map) -> String { + let root = expr["root"] + let id: String = next_html_id() + let acc: String = "_html_" + id + // If the root element has doctype:true the parser tagged it from + let doctype_flag: Bool = root["doctype"] + let doctype_prefix: String = "" + if doctype_flag { + let doctype_prefix = acc + " = el_str_concat(" + acc + ", EL_STR(\"\")); " + } + let body: String = cg_html_element_str(root, acc) + "({ el_val_t " + acc + " = EL_STR(\"\"); " + doctype_prefix + body + acc + "; })" +} + fn cg_expr(expr: Map) -> String { let kind: String = expr["expr"] @@ -1924,7 +2756,7 @@ fn cg_expr(expr: Map) -> String { return v } - // DurationLit — postfix-literal time value (e.g. 30.seconds, 1.hour). + // DurationLit - postfix-literal time value (e.g. 30.seconds, 1.hour). // Lowered to a literal int64 nanosecond count, wrapped in the runtime // entry point so the intent is explicit at the C level. The arithmetic // is fully constant-folded by any optimising C compiler. @@ -1938,7 +2770,7 @@ fn cg_expr(expr: Map) -> String { if kind == "Float" { // Wrap Float literals in el_from_float() so the bit pattern is // preserved through the el_val_t (int64) slot. Without this, - // implicit double→int64 conversion in C truncates `0.8` to `0` + // implicit double->int64 conversion in C truncates `0.8` to `0` // when passed to a builtin that expects el_val_t. let v: String = expr["value"] return "el_from_float(" + v + ")" @@ -1985,7 +2817,26 @@ fn cg_expr(expr: Map) -> String { let left_kind: String = left["expr"] let right_kind: String = right["expr"] - // ── Temporal-type dispatch (Instant + Duration first-class) ──────── + // -- String/equality fast-path: skip O(N-) temporal traversals -------- + // The 10 temporal predicates below each recurse into the left subtree: + // O(depth) state_get calls per predicate, O(N-) total for a chain of N + // string-concat BinOps (e.g. the 70-100-part HTML chains in soul.el). + // When either operand is a bare Str literal the result is always concat + // or str_eq - no temporal dispatch is possible. Exit immediately. + if str_eq(op, "Plus") { + if str_eq(left_kind, "Str") { return "el_str_concat(" + left_c + ", " + right_c + ")" } + if str_eq(right_kind, "Str") { return "el_str_concat(" + left_c + ", " + right_c + ")" } + } + if str_eq(op, "EqEq") { + if str_eq(left_kind, "Str") { return "str_eq(" + left_c + ", " + right_c + ")" } + if str_eq(right_kind, "Str") { return "str_eq(" + left_c + ", " + right_c + ")" } + } + if str_eq(op, "NotEq") { + if str_eq(left_kind, "Str") { return "!str_eq(" + left_c + ", " + right_c + ")" } + if str_eq(right_kind, "Str") { return "!str_eq(" + left_c + ", " + right_c + ")" } + } + + // -- Temporal-type dispatch (Instant + Duration first-class) -------- // Run BEFORE the int / string / generic paths so typed temporal // operands route through the runtime wrappers and invalid combos // become #error directives rather than silently falling through to @@ -2171,7 +3022,7 @@ fn cg_expr(expr: Map) -> String { if right_is_dur { return "el_duration_ne(" + left_c + ", " + right_c + ")" } } } - // Fall through — let the existing path handle anything we + // Fall through - let the existing path handle anything we // didn't explicitly cover (typically string-concat with a // typed temporal value, e.g. for debug prints, which works // because both share the int64 slot). @@ -2190,7 +3041,7 @@ fn cg_expr(expr: Map) -> String { // 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 + // 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) { @@ -2211,7 +3062,7 @@ fn cg_expr(expr: Map) -> String { 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). + // evidence - fall back to string concat (the historical default). if left_kind == "Call" { return "el_str_concat(" + left_c + ", " + right_c + ")" } @@ -2243,7 +3094,7 @@ fn cg_expr(expr: Map) -> String { // 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. + // dressed as char* - segfault on the first non-printable byte. if op == "EqEq" { if left_kind == "Int" { return "(" + left_c + " == " + right_c + ")" @@ -2359,17 +3210,15 @@ fn cg_expr(expr: Map) -> String { let arity: Int = native_list_len(args) let func_kind: String = func["expr"] - let args_c = "" + let args_parts: [String] = native_list_empty() let i = 0 while i < arity { let arg = native_list_get(args, i) let arg_c: String = cg_expr(arg) - if i > 0 { - let args_c = args_c + ", " - } - let args_c = args_c + arg_c + let args_parts = native_list_append(args_parts, arg_c) let i = i + 1 } + let args_c: String = str_join(args_parts, ", ") if func_kind == "Ident" { let fn_name: String = func["name"] @@ -2379,17 +3228,17 @@ 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 + // Arity check against the builtin table - refuse, with a clear // El-source message, when a known builtin gets the wrong arg // count (e.g. `http_serve(port)` instead of `http_serve(port, // handler)`). User-defined fns and variadic builtins pass // through (builtin_arity returns -1). arity_check_call(fn_name, arity) - // sleep(Duration) — Phase 1 of the typed-time work. When the + // sleep(Duration) - Phase 1 of the typed-time work. When the // single arg is provably a Duration we lower to el_sleep_duration // so the runtime sees nanos directly. Existing sleep() callers // that pass an Int still emit `sleep()`, which falls through - // to the no-such-symbol path — those call sites must migrate to + // to the no-such-symbol path - those call sites must migrate to // a typed Duration. Acceptable: the spec marks them out for an // audit pass during Phase 1. if str_eq(fn_name, "sleep") { @@ -2400,6 +3249,20 @@ fn cg_expr(expr: Map) -> String { } } } + // el_from_float takes a raw C double - do not wrap the float + // argument in el_from_float() again. Without this, the float + // literal codegen (which wraps every Float in el_from_float()) + // produces el_from_float(el_from_float(0.7)) - double-encoded. + if str_eq(fn_name, "el_from_float") { + if arity == 1 { + let only_arg = native_list_get(args, 0) + let arg_kind: String = only_arg["expr"] + if str_eq(arg_kind, "Float") { + let v: String = only_arg["value"] + return "el_from_float(" + v + ")" + } + } + } return fn_name + "(" + args_c + ")" } @@ -2433,8 +3296,8 @@ fn cg_expr(expr: Map) -> String { // El programs use `t["field"]` for map access and `arr[i]` for // list access. The parser emits the same Index node for both. // Dispatch at codegen time on the index expression kind: string- - // literal index → map field access (`el_get_field`); anything - // else → list element access (`el_list_get`). + // literal index -> map field access (`el_get_field`); anything + // else -> list element access (`el_list_get`). let obj = expr["object"] let idx = expr["index"] let obj_c: String = cg_expr(obj) @@ -2452,18 +3315,15 @@ fn cg_expr(expr: Map) -> String { // 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 items_parts: [String] = native_list_empty() let i = 0 while i < n { let elem = native_list_get(elems, i) let elem_c: String = cg_expr(elem) - if i > 0 { - let items = items + ", " - } - let items = items + elem_c + let items_parts = native_list_append(items_parts, elem_c) let i = i + 1 } - return "el_list_new(" + native_int_to_str(n) + ", " + items + ")" + return "el_list_new(" + native_int_to_str(n) + ", " + str_join(items_parts, ", ") + ")" } if kind == "Map" { @@ -2471,23 +3331,20 @@ fn cg_expr(expr: Map) -> String { 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 = {}` — + // 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 items_parts: [String] = native_list_empty() let i = 0 while i < n { let pair = native_list_get(pairs, i) let key: String = pair["key"] let val = pair["value"] let val_c: String = cg_expr(val) - if i > 0 { - let items = items + ", " - } - let items = items + c_str_lit(key) + ", " + val_c + let items_parts = native_list_append(items_parts, c_str_lit(key) + ", " + val_c) let i = i + 1 } - return "el_map_new(" + native_int_to_str(n) + ", " + items + ")" + return "el_map_new(" + native_int_to_str(n) + ", " + str_join(items_parts, ", ") + ")" } if kind == "Try" { @@ -2503,10 +3360,14 @@ fn cg_expr(expr: Map) -> String { return cg_match(expr) } + if kind == "HtmlTemplate" { + return cg_html_template(expr) + } + "EL_NULL" } -// ── Match codegen ───────────────────────────────────────────────────────────── +// -- Match codegen ------------------------------------------------------------- // // Lower a match expression to a GCC/Clang statement-expression. // A unique label suffix is allocated per match via state_set("__match_counter"). @@ -2530,7 +3391,9 @@ fn cg_match(expr: Map) -> String { let subj_var: String = "_match_subj_" + id let result_var: String = "_match_result_" + id let done_label: String = "_match_done_" + id - let out: String = "({ el_val_t " + subj_var + " = " + subj_c + "; el_val_t " + result_var + " = 0; " + // Accumulate arm fragments into a list to avoid O(n-) string growth. + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, "({ el_val_t " + subj_var + " = " + subj_c + "; el_val_t " + result_var + " = 0; ") let n: Int = native_list_len(arms) let i = 0 while i < n { @@ -2540,19 +3403,19 @@ fn cg_match(expr: Map) -> String { let pkind: String = pat["pattern"] let body_c: String = cg_expr(body) if str_eq(pkind, "Wildcard") { - let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") } else { if str_eq(pkind, "Binding") { let bname: String = pat["name"] - let out = out + "{ el_val_t " + bname + " = " + subj_var + "; " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + let parts = native_list_append(parts, "{ el_val_t " + bname + " = " + subj_var + "; " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") } else { if str_eq(pkind, "LitInt") { let v: String = pat["value"] - let out = out + "if (" + subj_var + " == " + v + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + let parts = native_list_append(parts, "if (" + subj_var + " == " + v + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") } else { if str_eq(pkind, "LitStr") { let v: String = pat["value"] - let out = out + "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(v) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") } else { if str_eq(pkind, "LitBool") { let v: String = pat["value"] @@ -2560,10 +3423,17 @@ fn cg_match(expr: Map) -> String { if str_eq(v, "true") { let bv = "1" } - let out = out + "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + let parts = native_list_append(parts, "if (" + subj_var + " == " + bv + ") { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") } else { - // unknown pattern → wildcard - let out = out + "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } " + if str_eq(pkind, "Variant") { + // Enum::Variant pattern — match against the variant name + // string (El enums compile to plain strings). + let variant: String = pat["variant"] + let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", EL_STR(" + c_str_lit(variant) + "))) { " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") + } else { + // unknown pattern -> wildcard + let parts = native_list_append(parts, "{ " + result_var + " = (" + body_c + "); goto " + done_label + "; } ") + } } } } @@ -2571,11 +3441,11 @@ fn cg_match(expr: Map) -> String { } let i = i + 1 } - let out = out + done_label + ":; " + result_var + "; })" - out + let parts = native_list_append(parts, done_label + ":; " + result_var + "; })") + str_join(parts, "") } -// ── If-as-expression codegen ───────────────────────────────────────────────── +// -- 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 @@ -2603,7 +3473,8 @@ fn next_if_id() -> String { // 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 = "" + // Collect statement fragments into a list to avoid O(n-) string growth. + let parts: [String] = native_list_empty() let i = 0 while i < n { let s = native_list_get(stmts, i) @@ -2614,31 +3485,31 @@ fn cg_if_expr_arm(stmts: [Map], result_var: String) -> String { 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 + "; " + let parts = native_list_append(parts, "el_val_t " + name + " = " + val_c + "; ") } else { if str_eq(sk, "Return") { let val = s["value"] let val_c: String = cg_expr(val) - let out = out + result_var + " = (" + val_c + "); " + let parts = native_list_append(parts, result_var + " = (" + val_c + "); ") } else { if str_eq(sk, "Expr") { let val = s["value"] let val_c: String = cg_expr(val) if is_last { - let out = out + result_var + " = (" + val_c + "); " + let parts = native_list_append(parts, result_var + " = (" + val_c + "); ") } else { - let out = out + "(void)(" + val_c + "); " + let parts = native_list_append(parts, "(void)(" + val_c + "); ") } } else { if str_eq(sk, "Assign") { - // Real reassignment in an expression-position arm — + // Real reassignment in an expression-position arm - // emit the store; the arm's "value" stays whatever // result_var was last set to, which is the El // semantics (assignment is a statement, not a value). let aname: String = s["name"] let aval = s["value"] let aval_c: String = cg_expr(aval) - let out = out + aname + " = " + aval_c + "; " + let parts = native_list_append(parts, aname + " = " + aval_c + "; ") } else { // Non-trivial stmt kinds (While/For) shouldn't appear in // expression-position arm bodies; emit nothing rather @@ -2649,7 +3520,7 @@ fn cg_if_expr_arm(stmts: [Map], result_var: String) -> String { } let i = i + 1 } - out + str_join(parts, "") } fn cg_if_expr(expr: Map) -> String { @@ -2669,7 +3540,7 @@ fn cg_if_expr(expr: Map) -> String { out } -// ── Variable scope tracking ─────────────────────────────────────────────────── +// -- Variable scope tracking --------------------------------------------------- // // El allows `let x = expr` to both declare and reassign x in the same scope. // C doesn't allow redeclaring the same name in the same block. @@ -2688,7 +3559,7 @@ fn list_contains(lst: [String], s: String) -> Bool { false } -// ── Statement codegen ───────────────────────────────────────────────────────── +// -- Statement codegen --------------------------------------------------------- // // cg_stmt emits C lines via println. declared is a list of already-declared // variable names in the current C scope; returns updated declared list. @@ -2737,7 +3608,7 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin if str_eq(ltype, "Zone") { add_zone_name(name) } - // Inference from RHS — duration literals and known-typed calls + // Inference from RHS - duration literals and known-typed calls // propagate even when the let is unannotated. if is_instant_expr(val) { add_instant_name(name) @@ -2792,7 +3663,7 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin } // Bare reassignment: `name = expr`. Always emits a plain C assignment - // (no `el_val_t` prefix) — by construction the parser only produces + // (no `el_val_t` prefix) - by construction the parser only produces // Assign for an existing identifier. If the name happens NOT to be in // `declared` for the current C scope (it was let-bound by an enclosing // block) the emit still resolves at C level because the variable lives @@ -2827,7 +3698,7 @@ 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 + // 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)) @@ -2847,7 +3718,24 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin if kind == "TypeDef" { return declared } if kind == "EnumDef" { return declared } if kind == "Import" { return declared } + if kind == "ExternFn" { return declared } if kind == "CgiBlock" { return declared } + if kind == "ServiceBlock" { return declared } + // TestDef: skip in normal (non-test) mode. + // In test mode the body is emitted by cg_test_fn, not here. + if kind == "TestDef" { return declared } + // Assert: no-op in normal mode. In test mode, cg_stmt_assert is used + // directly when emitting the per-test function body. + if kind == "Assert" { return declared } + // TryCatch: browser-only control flow. In the C target, emit a comment + // noting that the try body runs unconditionally; error handling is a no-op. + // Programs that rely on catching JS exceptions should compile with --target=js. + if kind == "TryCatch" { + let try_body = stmt["try_body"] + emit_line(indent + "/* try (C target: exception handling not supported) */") + cg_stmts(try_body, indent, native_list_clone(declared)) + return declared + } declared } @@ -2878,14 +3766,7 @@ fn strip_outer_parens(s: String) -> String { 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 + return str_slice(s, 1, n - 1) } } } @@ -2900,7 +3781,7 @@ 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 + // 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) @@ -2952,7 +3833,7 @@ fn cg_stmts(stmts: [Map], indent: String, declared: [String]) -> [S decl } -// ── Function declaration codegen ─────────────────────────────────────────────── +// -- Function declaration codegen ----------------------------------------------- fn param_decl(param: Map, idx: Int) -> String { let name: String = param["name"] @@ -2962,18 +3843,15 @@ fn param_decl(param: Map, idx: Int) -> String { fn params_to_c(params: [Map]) -> String { let n: Int = native_list_len(params) if n == 0 { return "void" } - let out = "" + let parts: [String] = native_list_empty() let i = 0 while i < n { let param = native_list_get(params, i) let decl: String = param_decl(param, i) - if i > 0 { - let out = out + ", " - } - let out = out + decl + let parts = native_list_append(parts, decl) let i = i + 1 } - out + str_join(parts, ", ") } // Transform a function body so that an implicit-return final expression @@ -3024,7 +3902,7 @@ fn is_int_name(name: String) -> Bool { // Same shape as is_int_name, for Instant- and Duration-typed bindings. // Used by the BinOp/comparison codegen to dispatch arithmetic through the -// typed runtime wrappers (el_instant_add_dur, el_duration_lt, …) and to +// typed runtime wrappers (el_instant_add_dur, el_duration_lt, -) and to // surface mismatches (Instant + Instant, Duration + Int) as #error // directives at the top of the generated C. fn is_instant_name(name: String) -> Bool { @@ -3086,7 +3964,7 @@ fn is_int_call(call_expr: Map) -> Bool { } // Builtins that return an Instant. Used by is_instant_expr and the BinOp -// dispatch — `now() + 5.seconds` types as Instant only because we can see +// dispatch - `now() + 5.seconds` types as Instant only because we can see // that now() is an Instant-returning Call. fn is_instant_call(call_expr: Map) -> Bool { let func = call_expr["func"] @@ -3122,7 +4000,7 @@ fn is_duration_call(call_expr: Map) -> Bool { return false } -// Phase 1.5 — Calendar / CalendarTime / Rhythm / LocalDate / LocalTime / +// Phase 1.5 - Calendar / CalendarTime / Rhythm / LocalDate / LocalTime / // LocalDateTime / Zone are first-class boxed types. Each has its own name // set in process state, populated from typed `let` bindings and parameter // annotations. The BinOp dispatcher consults these to forbid mismatched @@ -3310,7 +4188,7 @@ fn is_zone_expr(expr: Map) -> Bool { // Recursive type predicates for Instant / Duration. Mirror is_int_expr. // is_instant_expr / is_duration_expr return true only when the expression // is provably of that type at codegen time. Anything ambiguous returns -// false — the BinOp dispatcher then leaves the expression on the +// false - the BinOp dispatcher then leaves the expression on the // untyped-int path, which is the safest fallback because at the runtime // level all three types share the int64 slot. fn is_instant_expr(expr: Map) -> Bool { @@ -3325,8 +4203,8 @@ fn is_instant_expr(expr: Map) -> Bool { if str_eq(k, "BinOp") { let op: String = expr["op"] if str_eq(op, "Plus") { - // Instant + Duration → Instant - // Duration + Instant → Instant + // Instant + Duration -> Instant + // Duration + Instant -> Instant if is_instant_expr(expr["left"]) { if is_duration_expr(expr["right"]) { return true } } @@ -3336,7 +4214,7 @@ fn is_instant_expr(expr: Map) -> Bool { return false } if str_eq(op, "Minus") { - // Instant - Duration → Instant + // Instant - Duration -> Instant if is_instant_expr(expr["left"]) { if is_duration_expr(expr["right"]) { return true } } @@ -3363,15 +4241,15 @@ fn is_duration_expr(expr: Map) -> Bool { if str_eq(k, "BinOp") { let op: String = expr["op"] if str_eq(op, "Plus") { - // Duration + Duration → Duration + // Duration + Duration -> Duration if is_duration_expr(expr["left"]) { if is_duration_expr(expr["right"]) { return true } } return false } if str_eq(op, "Minus") { - // Duration - Duration → Duration - // Instant - Instant → Duration (caught here, not in is_instant_expr) + // Duration - Duration -> Duration + // Instant - Instant -> Duration (caught here, not in is_instant_expr) if is_duration_expr(expr["left"]) { if is_duration_expr(expr["right"]) { return true } } @@ -3381,8 +4259,8 @@ fn is_duration_expr(expr: Map) -> Bool { return false } if str_eq(op, "Star") { - // Duration * Int → Duration - // Int * Duration → Duration + // Duration * Int -> Duration + // Int * Duration -> Duration if is_duration_expr(expr["left"]) { if is_int_expr(expr["right"]) { return true } } @@ -3392,7 +4270,7 @@ fn is_duration_expr(expr: Map) -> Bool { return false } if str_eq(op, "Slash") { - // Duration / Int → Duration + // Duration / Int -> Duration if is_duration_expr(expr["left"]) { if is_int_expr(expr["right"]) { return true } } @@ -3423,13 +4301,13 @@ fn time_record_violation(kind: String, detail: String) -> Bool { // 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 +// 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 } @@ -3448,7 +4326,7 @@ fn is_int_expr(expr: Map) -> Bool { } if str_eq(k, "BinOp") { let op: String = expr["op"] - // Comparisons and logicals always yield 0/1 — safe Int. + // 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 } @@ -3457,7 +4335,7 @@ fn is_int_expr(expr: Map) -> Bool { 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. + // 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 } @@ -3487,7 +4365,7 @@ fn is_int_expr(expr: Map) -> Bool { return false } -// ── Capability-kind enforcement ────────────────────────────────────────────── +// -- Capability-kind enforcement ---------------------------------------------- // // A program's top-level block (cgi / service / none) determines which // runtime primitives it may call. The compiler records violations in @@ -3496,11 +4374,11 @@ fn is_int_expr(expr: Map) -> Bool { // downstream cc step fails with a clear message. // // Capability tiers: -// "cgi" — full self-formation. All primitives. -// "service" — bounded. Cannot call self-formation primitives: +// "cgi" - full self-formation. All primitives. +// "service" - bounded. Cannot call self-formation primitives: // llm_call_agentic, llm_register_tool, dharma_emit, // dharma_field. Single-turn LLM calls are allowed. -// "utility" — default. No DHARMA, no LLM. Pure compute + I/O. +// "utility" - default. No DHARMA, no LLM. Pure compute + I/O. // // The compiler-level rule is structural: the binary either CAN or CANNOT // emit the call. There is no runtime check, no opt-in, no override. @@ -3515,7 +4393,7 @@ fn cap_record_violation(kind: String, fn_name: String) -> Bool { return true } -// Self-formation primitives — the cut between CGI and service. A program +// Self-formation primitives - the cut between CGI and service. A program // that emits these calls IS structurally a CGI; we forbid them everywhere // else. fn is_self_formation_call(fn_name: String) -> Bool { @@ -3526,7 +4404,7 @@ fn is_self_formation_call(fn_name: String) -> Bool { return false } -// Any DHARMA primitive — utilities have zero network presence. +// Any DHARMA primitive - utilities have zero network presence. fn is_dharma_call(fn_name: String) -> Bool { if str_eq(fn_name, "dharma_connect") { return true } if str_eq(fn_name, "dharma_send") { return true } @@ -3539,7 +4417,7 @@ fn is_dharma_call(fn_name: String) -> Bool { return false } -// Any LLM primitive — utilities have no LLM access at all. +// Any LLM primitive - utilities have no LLM access at all. fn is_llm_call(fn_name: String) -> Bool { if str_eq(fn_name, "llm_call") { return true } if str_eq(fn_name, "llm_call_system") { return true } @@ -3589,14 +4467,14 @@ fn emit_cap_violations() -> Void { if colon > 0 { let kind: String = str_slice(entry, 0, colon) let fn_name: String = str_slice(entry, colon + 1, str_len(entry)) - emit_line("#error \"capability violation: '" + kind + "' programs may not call '" + fn_name + "' (self-formation primitive — only 'cgi' programs may use it)\"") + emit_line("#error \"capability violation: '" + kind + "' programs may not call '" + fn_name + "' (self-formation primitive - only 'cgi' programs may use it)\"") } let i = i + next_comma + 1 } } // Surface temporal-type violations as #error directives. The cg_expr BinOp -// dispatcher records each violation (Instant + Instant, Duration + Int, …) +// dispatcher records each violation (Instant + Instant, Duration + Int, -) // as a CSV entry "kind:detail" via time_record_violation. Each entry maps // to a single #error so downstream cc fails the build with a clear El- // source-level message before the bogus C even links. @@ -3619,7 +4497,7 @@ fn emit_time_violations() -> Void { } } -// ── Builtin arity table ─────────────────────────────────────────────────────── +// -- 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)`). @@ -3629,7 +4507,7 @@ fn emit_time_violations() -> Void { // // 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 +// 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 { @@ -3771,6 +4649,16 @@ fn builtin_arity(name: String) -> Int { if str_eq(name, "bool_to_str") { return 1 } // Process if str_eq(name, "exit_program") { return 1 } + // Process info + if str_eq(name, "getpid_now") { return 0 } + // stdout redirect (used by elc post-processing) + if str_eq(name, "stdout_to_file") { return 1 } + if str_eq(name, "stdout_restore") { return 0 } + // Subprocess execution + if str_eq(name, "exec_command") { return 1 } + if str_eq(name, "exec_capture") { return 1 } + if str_eq(name, "exec") { return 1 } + if str_eq(name, "exec_bg") { return 1 } // CGI / DHARMA if str_eq(name, "dharma_connect") { return 1 } if str_eq(name, "dharma_send") { return 2 } @@ -3833,7 +4721,7 @@ fn builtin_arity(name: String) -> Int { 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. + // -1 sentinel: variadic / unknown / user-defined -> no check. return -1 } @@ -4031,7 +4919,7 @@ fn build_int_names_for_params(params: [Map]) -> Bool { fn cg_fn(stmt: Map) -> Void { let fn_name: String = stmt["name"] - // Skip El's `fn main()` — C provides its own main() for top-level stmts + // Skip El's `fn main()` - C provides its own main() for top-level stmts // and a duplicate `el_val_t main(void)` would collide with it. if fn_name == "main" { return } let params = stmt["params"] @@ -4063,8 +4951,8 @@ fn cg_fn(stmt: Map) -> Void { } // Lift the final bare expression into an explicit return so implicit // returns ("fn lex(s) { ... tokens }") actually return their value. - // Void-returning functions skip this — wrapping `println(x)` in - // `return …` is a C type error. + // Void-returning functions skip this - wrapping `println(x)` in + // `return -` is a C type error. let body_xformed = body if !str_eq(ret_type, "Void") { let body_xformed = transform_implicit_return(body) @@ -4075,7 +4963,7 @@ fn cg_fn(stmt: Map) -> Void { emit_blank() } -// ── Top-level codegen ───────────────────────────────────────────────────────── +// -- Top-level codegen --------------------------------------------------------- fn is_fndef(stmt: Map) -> Bool { let kind: String = stmt["stmt"] @@ -4089,6 +4977,8 @@ fn is_top_level_decl(stmt: Map) -> Bool { if kind == "EnumDef" { return true } if kind == "Import" { return true } if kind == "CgiBlock" { return true } + if kind == "ExternFn" { return true } + if kind == "TestDef" { return true } false } @@ -4100,7 +4990,7 @@ fn cgi_arg(value: String, has_value: Bool) -> String { return "EL_NULL" } -// ── VBD role enforcement ────────────────────────────────────────────────────── +// -- VBD role enforcement ------------------------------------------------------ // // Scan a function body for direct calls to DHARMA-restricted builtins // (dharma_emit, dharma_field). These may only appear inside @manager fns. @@ -4233,16 +5123,202 @@ fn vbd_has_restricted_call(stmts: [Map]) -> Bool { false } -// ── Entry point ──────────────────────────────────────────────────────────────── +// -- Test mode codegen ---------------------------------------------------------- +// +// reporter = "text" → human-readable output to stderr +// reporter = "json" → newline-delimited JSON to stdout +// +// Each test function signature: static int el_test_N(void) +// Returns 0 = pass, non-zero = fail. +// +// Text assert: if (!expr) { fprintf(stderr, " FAIL \n"); return 1; } +// JSON assert: stores the assert_line for the runner's suite_end emission. +// Emits {"type":"test_fail",...} to stdout then returns 1. + +fn cg_stmt_assert_text(stmt: Map, test_name: String) -> Void { + let expr_node = stmt["expr"] + let msg: String = stmt["msg"] + let expr_c: String = cg_expr(expr_node) + let expr_c = strip_outer_parens(expr_c) + let disp_msg = "assert failed" + if !str_eq(msg, "") { let disp_msg = msg } + emit_line(" if (!(" + expr_c + ")) {") + emit_line(" fprintf(stderr, \" FAIL " + c_escape(test_name) + " \\xe2\\x80\\x94 " + c_escape(disp_msg) + "\\n\");") + emit_line(" return 1;") + emit_line(" }") +} + +fn cg_stmt_assert_json(stmt: Map, test_name: String, file_name: String, test_line: Int) -> Void { + let expr_node = stmt["expr"] + let msg: String = stmt["msg"] + let assert_line: Int = stmt["line"] + let expr_c: String = cg_expr(expr_node) + let expr_c = strip_outer_parens(expr_c) + let disp_msg = "assert failed" + if !str_eq(msg, "") { let disp_msg = msg } + // Embed all compile-time-known strings as C string literals (no %s runtime formatting). + let json_line: String = "{\"type\":\"test_fail\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + ",\"assert_line\":" + native_int_to_str(assert_line) + ",\"message\":\"" + c_escape(disp_msg) + "\"}" + emit_line(" if (!(" + expr_c + ")) {") + emit_line(" puts(" + c_str_lit(json_line) + ");") + emit_line(" return 1;") + emit_line(" }") +} + +// cg_stmts_in_test: emit test body statements, routing Assert to the +// appropriate handler based on reporter mode. +fn cg_stmts_in_test(stmts: [Map], indent: String, declared: [String], test_name: String, reporter: String, file_name: String, test_line: Int) -> [String] { + let n: Int = native_list_len(stmts) + let i = 0 + let decl = declared + while i < n { + let stmt = native_list_get(stmts, i) + let sk: String = stmt["stmt"] + if str_eq(sk, "Assert") { + if str_eq(reporter, "json") { + cg_stmt_assert_json(stmt, test_name, file_name, test_line) + } else { + cg_stmt_assert_text(stmt, test_name) + } + } else { + let decl = cg_stmt(stmt, indent, decl) + } + let i = i + 1 + } + decl +} + +// cg_test_fn: emit a single test function. +fn cg_test_fn(test_def: Map, idx: Int, reporter: String, file_name: String) -> String { + let fn_name: String = "el_test_" + native_int_to_str(idx) + let test_name: String = test_def["name"] + let test_line: Int = test_def["line"] + let body = test_def["body"] + emit_line("static int " + fn_name + "(void) {") + cg_stmts_in_test(body, " ", native_list_empty(), test_name, reporter, file_name, test_line) + emit_line(" return 0;") + emit_line("}") + emit_blank() + fn_name +} + +// codegen_test: emit a complete test binary. +// reporter: "text" (stderr human-readable) or "json" (stdout ndjson) +// file_name: basename of the source file (used in JSON output) +fn codegen_test(stmts: [Map], reporter: String, file_name: String) -> Void { + // Collect all TestDef nodes in order. + let n: Int = native_list_len(stmts) + let test_defs: [Map] = native_list_empty() + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let sk: String = stmt["stmt"] + if str_eq(sk, "TestDef") { + let test_defs = native_list_append(test_defs, stmt) + } + let i = i + 1 + } + let n_tests: Int = native_list_len(test_defs) + + // Emit forward declarations for test functions. + let ti = 0 + while ti < n_tests { + let fn_name: String = "el_test_" + native_int_to_str(ti) + emit_line("static int " + fn_name + "(void);") + let ti = ti + 1 + } + emit_blank() + + // Emit all non-test, non-main function definitions. + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + if is_fndef(stmt) { + let fn_name: String = stmt["name"] + if !str_eq(fn_name, "main") { + cg_fn(stmt) + } + } + let i = i + 1 + } + + // Emit each test function. + let ti = 0 + while ti < n_tests { + let test_def = native_list_get(test_defs, ti) + cg_test_fn(test_def, ti, reporter, file_name) + let ti = ti + 1 + } + + // Emit the test runner main(). + let test_word = "tests" + if n_tests == 1 { let test_word = "test" } + emit_line("int main(void) {") + emit_line(" int pass = 0; int fail = 0;") + + if str_eq(reporter, "json") { + // JSON reporter: all strings are compile-time constants; use puts. + // Only suite_end needs runtime pass/fail counts (printf). + let suite_start_json: String = "{\"type\":\"suite_start\",\"file\":\"" + c_escape(file_name) + "\",\"total\":" + native_int_to_str(n_tests) + "}" + emit_line(" puts(" + c_str_lit(suite_start_json) + ");") + let ti = 0 + while ti < n_tests { + let test_def = native_list_get(test_defs, ti) + let test_name: String = test_def["name"] + let test_line: Int = test_def["line"] + let fn_name: String = "el_test_" + native_int_to_str(ti) + let start_json: String = "{\"type\":\"test_start\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + "}" + let pass_json: String = "{\"type\":\"test_pass\",\"name\":\"" + c_escape(test_name) + "\",\"file\":\"" + c_escape(file_name) + "\",\"line\":" + native_int_to_str(test_line) + ",\"duration_ms\":0}" + emit_line(" puts(" + c_str_lit(start_json) + ");") + emit_line(" if (" + fn_name + "() == 0) {") + emit_line(" pass++;") + emit_line(" puts(" + c_str_lit(pass_json) + ");") + emit_line(" } else { fail++; }") + let ti = ti + 1 + } + // suite_end needs runtime pass/fail counts + emit_line(" printf(\"{\\\"type\\\":\\\"suite_end\\\",\\\"passed\\\":%d,\\\"failed\\\":%d}\\n\", pass, fail);") + } else { + // Text reporter: human-readable to stderr + emit_line(" fprintf(stderr, \"==> running " + native_int_to_str(n_tests) + " " + test_word + "\\n\\n\");") + let ti = 0 + while ti < n_tests { + let test_def = native_list_get(test_defs, ti) + let test_name: String = test_def["name"] + let fn_name: String = "el_test_" + native_int_to_str(ti) + emit_line(" fprintf(stderr, \" RUN " + c_escape(test_name) + "\\n\");") + emit_line(" if (" + fn_name + "() == 0) { pass++; fprintf(stderr, \" PASS " + c_escape(test_name) + "\\n\"); }") + emit_line(" else { fail++; }") + let ti = ti + 1 + } + emit_line(" fprintf(stderr, \"\\n%d passed, %d failed\\n\", pass, fail);") + } + + emit_line(" return fail > 0 ? 1 : 0;") + emit_line("}") + emit_blank() +} + +// -- Entry point ---------------------------------------------------------------- fn codegen(stmts: [Map], source: String) -> String { + codegen_inner(stmts, source, false, "text", "") +} + +// codegen_with_tests: emit a test binary. +// reporter: "text" or "json" +// file_name: basename of the source file (used in JSON output) +fn codegen_with_tests(stmts: [Map], source: String, reporter: String, file_name: String) -> String { + codegen_inner(stmts, source, true, reporter, file_name) +} + +fn codegen_inner(stmts: [Map], source: String, test_mode: Bool, reporter: String, file_name: String) -> String { // Detect cgi/service blocks: at most one declarative top-level block. // The block determines the program's CAPABILITY KIND: - // "cgi" — full self-formation. Calls all primitives. - // "service" — bounded. Cannot call self-formation primitives + // "cgi" - full self-formation. Calls all primitives. + // "service" - bounded. Cannot call self-formation primitives // (llm_call_agentic, llm_register_tool, dharma_emit, // dharma_field, mindlink-creation). - // "utility" — default; no DHARMA membership, no LLM, no agentic. + // "utility" - default; no DHARMA membership, no LLM, no agentic. // Codegen enforces this with #error directives at every restricted // call site. The capability boundary is structural: a binary either // CAN or CANNOT do a thing, and the compiler decides at emission time. @@ -4277,7 +5353,7 @@ fn codegen(stmts: [Map], source: String) -> String { } if cgi_count >= 1 { if svc_count >= 1 { - emit_line("#error \"El: program declares both cgi and service blocks (mutually exclusive — pick one)\"") + emit_line("#error \"El: program declares both cgi and service blocks (mutually exclusive - pick one)\"") } } // Stash the program kind so cg_expr's Call branch can enforce @@ -4293,13 +5369,54 @@ fn codegen(stmts: [Map], source: String) -> String { // Clear temporal-type-violation accumulator from any prior compile. state_set("__time_violations", "") + // In test mode, delegate to the test-specific path which emits test + // functions and a test runner main() instead of the normal program. + // Test mode still needs the standard preamble (#includes, forward + // decls) so we emit that before branching. + // // Preamble emit_line("#include ") emit_line("#include ") + emit_line("#include ") emit_line("#include \"el_runtime.h\"") + + // Cross-module forward declarations: for each imported module, emit + // #include "module.elh" so Clang sees the function signatures from + // that module without needing the full source inlined. The .elh files + // are generated by `elc --emit-header` and live in the same dist/ + // directory as the generated .c files. We use basename only (strip + // the directory prefix and .el extension) so the include resolves + // correctly regardless of the source tree layout. + let imp_n: Int = native_list_len(stmts) + let imp_i = 0 + while imp_i < imp_n { + let imp_stmt = native_list_get(stmts, imp_i) + let imp_kind: String = imp_stmt["stmt"] + if str_eq(imp_kind, "Import") { + let imp_path: String = imp_stmt["path"] + // Extract basename: find last '/' and strip from there. + let imp_path_len: Int = str_len(imp_path) + let imp_last_slash: Int = -1 + let imp_j: Int = 0 + while imp_j < imp_path_len { + let imp_c: String = str_slice(imp_path, imp_j, imp_j + 1) + if str_eq(imp_c, "/") { let imp_last_slash = imp_j } + let imp_j = imp_j + 1 + } + let imp_base: String = str_slice(imp_path, imp_last_slash + 1, imp_path_len) + // Strip .el extension if present. + let imp_base_len: Int = str_len(imp_base) + let imp_bname: String = imp_base + if str_ends_with(imp_base, ".el") { + let imp_bname = str_slice(imp_base, 0, imp_base_len - 3) + } + emit_line("#include \"" + imp_bname + ".elh\"") + } + let imp_i = imp_i + 1 + } emit_blank() - // Forward declarations (skip `main` — C provides its own) + // Forward declarations (skip `main` - C provides its own) let n: Int = native_list_len(stmts) let i = 0 while i < n { @@ -4313,11 +5430,17 @@ fn codegen(stmts: [Map], source: String) -> String { emit_line("el_val_t " + fn_name + "(" + params_c + ");") } } + if kind == "ExternFn" { + let fn_name: String = stmt["name"] + let params = stmt["params"] + let params_c: String = params_to_c(params) + emit_line("el_val_t " + fn_name + "(" + params_c + ");") + } let i = i + 1 } emit_blank() - // Top-level `let` bindings → file-scope storage. El programs use + // Top-level `let` 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 @@ -4351,6 +5474,45 @@ fn codegen(stmts: [Map], source: String) -> String { emit_blank() } + // Test mode: emit test functions and test runner instead of normal program. + if test_mode { + codegen_test(stmts, reporter, file_name) + emit_cap_violations() + emit_arity_violations() + emit_time_violations() + return "" + } + + // Detect whether this compilation unit has an entry point. + // A unit is a library (no C main emitted) when there is no fn main() + // and no top-level executable statements. This supports separate + // compilation: library .c files contain only function definitions. + let has_el_main = false + let has_toplevel_stmts = false + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + let sk: String = stmt["stmt"] + if str_eq(sk, "FnDef") { + let fn_name_chk: String = stmt["name"] + if str_eq(fn_name_chk, "main") { let has_el_main = true } + } + if !is_fndef(stmt) { + if !is_top_level_decl(stmt) { + if !str_eq(sk, "Let") { + let has_toplevel_stmts = true + } + } + } + let i = i + 1 + } + let is_library = false + if !has_el_main { + if !has_toplevel_stmts { + let is_library = true + } + } + // Function definitions let i = 0 while i < n { @@ -4361,6 +5523,9 @@ fn codegen(stmts: [Map], source: String) -> String { let i = i + 1 } + // Skip C main() for library units (no fn main, no top-level stmts) + if is_library { return "" } + // main(). Use _argc/_argv so El programs are free to declare their own // local `argv` / `argc` (compiler.el itself does this) without colliding // with the C-side parameters when fn main()'s body is folded in below. @@ -4432,7 +5597,7 @@ 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 + // 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. @@ -4455,17 +5620,17 @@ fn codegen(stmts: [Map], source: String) -> String { // Emit any accumulated capability-violation #error directives. cc // will fail on the first one and surface the message; placement at - // the bottom is fine — preprocessor errors halt the build wherever + // the bottom is fine - preprocessor errors halt the build wherever // they appear. emit_cap_violations() // Same for builtin-arity violations: cc halts on the first #error, // so a misuse of a known builtin (wrong arg count) fails the build // with a clear message naming the builtin and its expected arity. emit_arity_violations() - // Temporal-type violations (Instant + Instant, Duration + Int, …). + // Temporal-type violations (Instant + Instant, Duration + Int, -). emit_time_violations() - // Return empty string — output was streamed via println + // Return empty string - output was streamed via println "" } // codegen-js.el — El compiler JavaScript source code generator @@ -4493,26 +5658,26 @@ fn codegen(stmts: [Map], source: String) -> String { fn js_escape(s: String) -> String { let chars: [String] = native_string_chars(s) let total: Int = native_list_len(chars) - let out = "" + let parts: [String] = native_list_empty() let i = 0 while i < total { let ch: String = native_list_get(chars, i) if ch == "\"" { - let out = out + "\\\"" + let parts = native_list_append(parts, "\\\"") } else { if ch == "\\" { - let out = out + "\\\\" + let parts = native_list_append(parts, "\\\\") } else { if ch == "\n" { - let out = out + "\\n" + let parts = native_list_append(parts, "\\n") } else { if ch == "\r" { - let out = out + "\\r" + let parts = native_list_append(parts, "\\r") } else { if ch == "\t" { - let out = out + "\\t" + let parts = native_list_append(parts, "\\t") } else { - let out = out + ch + let parts = native_list_append(parts, ch) } } } @@ -4520,7 +5685,7 @@ fn js_escape(s: String) -> String { } let i = i + 1 } - out + str_join(parts, "") } fn js_str_lit(s: String) -> String { @@ -4556,6 +5721,57 @@ fn js_binop(op: String) -> String { op } +// ── Known El runtime method names ───────────────────────────────────────────── +// +// These are the method shortforms exported by el_runtime.js and used by the +// El C-backend convention of `obj.method(args)` -> `method(obj, args)`. +// Any method name NOT in this set is treated as a native JS method call on the +// receiver object, emitting `obj.method(args)` directly. +// +// This is the mechanism that makes `client.auth.signInWithOtp(payload)` work +// without `native_js_call`: the receiver is Any-typed, the method is unknown +// to El, so codegen emits the JS call directly. + +fn js_is_el_method(name: String) -> Bool { + if str_eq(name, "append") { return true } + if str_eq(name, "len") { return true } + if str_eq(name, "get") { return true } + if str_eq(name, "map_get") { return true } + if str_eq(name, "map_set") { return true } + false +} + +// ── Async function tracking ─────────────────────────────────────────────────── +// +// Functions decorated with @async are recorded here. Any call to a known-async +// builtin (http_get, http_post, http_post_json) or to a user-declared @async +// function gets an `await` prefix in generated JS. +// +// Known-async builtins — these return Promise in el_runtime.js. +fn js_is_async_builtin(name: String) -> Bool { + if str_eq(name, "http_get") { return true } + if str_eq(name, "http_post") { return true } + if str_eq(name, "http_post_json") { return true } + if str_eq(name, "http_get_with_headers") { return true } + if str_eq(name, "http_post_with_headers") { return true } + false +} + +fn js_register_async_fn(name: String) -> Bool { + let csv: String = state_get("__js_async_fns") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__js_async_fns", csv + name + ",") + return true +} + +fn js_is_async_fn(name: String) -> Bool { + let csv: String = state_get("__js_async_fns") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + // ── Int-name tracking (mirrors codegen.el) ──────────────────────────────────── fn js_is_int_name(name: String) -> Bool { @@ -4610,6 +5826,138 @@ fn js_is_int_call(call_expr: Map) -> Bool { return false } +// ── HTML template codegen (JS) ──────────────────────────────────────────────── +// +// HTML template expressions compile to a JS IIFE that builds the HTML string +// using string concatenation. Interpolated values go through html_escape(); +// raw() bypasses escaping. {#each} blocks compile to Array.forEach or a +// for-loop that pushes fragments into a parts array. +// +// Entry point: js_cg_html_template(expr) → JS expression string. + +fn js_next_html_id() -> String { + let csv: String = state_get("__js_html_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__js_html_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +fn js_cg_html_parts(children: [Map], acc_var: String) -> String { + let n: Int = native_list_len(children) + let i = 0 + let out = "" + while i < n { + let child: Map = native_list_get(children, i) + let html_kind: String = child["html"] + if str_eq(html_kind, "Text") { + let text: String = child["text"] + let out = out + acc_var + " += " + js_str_lit(text) + "; " + } + if str_eq(html_kind, "Doctype") { + let out = out + acc_var + " += \"\"; " + } + if str_eq(html_kind, "Interp") { + let val_node = child["value"] + let val_c: String = js_cg_expr(val_node) + let out = out + acc_var + " += html_escape(" + val_c + "); " + } + if str_eq(html_kind, "Raw") { + let val_node = child["value"] + let val_c: String = js_cg_expr(val_node) + let out = out + acc_var + " += html_raw(" + val_c + "); " + } + if str_eq(html_kind, "Element") { + let elem_c: String = js_cg_html_element_str(child, acc_var) + let out = out + elem_c + } + if str_eq(html_kind, "Each") { + let each_c: String = js_cg_html_each(child, acc_var) + let out = out + each_c + } + let i = i + 1 + } + out +} + +fn js_cg_html_attrs_str(attrs: [Map], acc_var: String) -> String { + let n: Int = native_list_len(attrs) + let i = 0 + let out = "" + while i < n { + let attr: Map = native_list_get(attrs, i) + let attr_name: String = attr["name"] + let kind: String = attr["kind"] + // open-attr snippet: " name=\"" + let open_val: String = " " + attr_name + "=\"" + if str_eq(kind, "static") { + let sv: String = attr["value"] + let out = out + acc_var + " += " + js_str_lit(open_val) + "; " + let out = out + acc_var + " += " + js_str_lit(sv) + "; " + let out = out + acc_var + " += " + js_str_lit("\"") + "; " + } else { + if str_eq(kind, "dynamic") { + let val_node = attr["value"] + let val_c: String = js_cg_expr(val_node) + let out = out + acc_var + " += " + js_str_lit(open_val) + "; " + let out = out + acc_var + " += html_escape(" + val_c + "); " + let out = out + acc_var + " += " + js_str_lit("\"") + "; " + } else { + // Boolean attribute + let out = out + acc_var + " += " + js_str_lit(" " + attr_name) + "; " + } + } + let i = i + 1 + } + out +} + +fn js_cg_html_element_str(elem: Map, acc_var: String) -> String { + let tag: String = elem["tag"] + let attrs: [Map] = elem["attrs"] + let children: [Map] = elem["children"] + let self_closing: Bool = elem["self_closing"] + let out = acc_var + " += " + js_str_lit("<" + tag) + "; " + let out = out + js_cg_html_attrs_str(attrs, acc_var) + if self_closing { + let out = out + acc_var + " += \"/>\"" + "; " + } else { + let out = out + acc_var + " += \">\"; " + let out = out + js_cg_html_parts(children, acc_var) + let out = out + acc_var + " += " + js_str_lit("") + "; " + } + out +} + +fn js_cg_html_each(node: Map, acc_var: String) -> String { + let list_expr = node["list"] + let item_name: String = node["item"] + let body_children: [Map] = node["body"] + let id: String = js_next_html_id() + let list_var: String = "_html_list_" + id + let len_var: String = "_html_len_" + id + let idx_var: String = "_html_i_" + id + let list_c: String = js_cg_expr(list_expr) + let inner_c: String = js_cg_html_parts(body_children, acc_var) + "{ const " + list_var + " = " + list_c + "; const " + len_var + " = el_list_len(" + list_var + "); for (let " + idx_var + " = 0; " + idx_var + " < " + len_var + "; " + idx_var + "++) { const " + item_name + " = el_list_get(" + list_var + ", " + idx_var + "); " + inner_c + "} } " +} + +fn js_cg_html_template(expr: Map) -> String { + let root = expr["root"] + let id: String = js_next_html_id() + let acc: String = "_html_" + id + let doctype_flag: Bool = root["doctype"] + let doctype_prefix: String = "" + if doctype_flag { + let doctype_prefix = acc + " += \"\"; " + } + let body: String = js_cg_html_element_str(root, acc) + "(() => { let " + acc + " = \"\"; " + doctype_prefix + body + "return " + acc + "; })()" +} + // ── Expression codegen ──────────────────────────────────────────────────────── // // js_cg_expr returns a JS expression string (not a statement). @@ -4835,34 +6183,50 @@ fn js_cg_expr(expr: Map) -> String { let arity: Int = native_list_len(args) let func_kind: String = func["expr"] - let args_c = "" + let args_parts: [String] = native_list_empty() let i = 0 while i < arity { let arg = native_list_get(args, i) let arg_c: String = js_cg_expr(arg) - if i > 0 { - let args_c = args_c + ", " - } - let args_c = args_c + arg_c + let args_parts = native_list_append(args_parts, arg_c) let i = i + 1 } + let args_c: String = str_join(args_parts, ", ") if func_kind == "Ident" { let fn_name: String = func["name"] - return fn_name + "(" + args_c + ")" + let call_expr: String = fn_name + "(" + args_c + ")" + if js_is_async_builtin(fn_name) { + return "await " + call_expr + } + if js_is_async_fn(fn_name) { + return "await " + call_expr + } + return call_expr } if func_kind == "Field" { - // 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 + ")" + // If the method is a known El runtime shortform, keep the El + // convention: `method(obj, args)`. This preserves backward + // compatibility with list.append(x), map.map_get(k), etc. + if js_is_el_method(field) { + if arity > 0 { + return field + "(" + obj_c + ", " + args_c + ")" + } + return field + "(" + obj_c + ")" } - return field + "(" + obj_c + ")" + // Unknown method — emit as a native JS method call on the + // receiver. This handles Any-typed values (third-party library + // objects, DOM elements, Promises, etc.) without requiring + // native_js_call. Example: `client.auth.signInWithOtp(payload)` + // emits `client["auth"].signInWithOtp(args_c)`. + if arity > 0 { + return obj_c + "." + field + "(" + args_c + ")" + } + return obj_c + "." + field + "()" } let fn_c: String = js_cg_expr(func) @@ -4870,22 +6234,39 @@ fn js_cg_expr(expr: Map) -> String { } 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. + // El's `obj.foo` becomes JS `obj["foo"]` — direct bracket access. + // This works for plain El map objects AND for real JS objects with + // prototype-inherited properties (DOM elements, third-party library + // objects, Promises, etc.). el_get_field used hasOwnProperty which + // silently returned null for inherited props, breaking e.g. client.auth. + // + // Nil-propagation: `obj?.foo` emits `(obj)?.["foo"] ?? null`. let obj = expr["object"] let field: String = expr["field"] + let obj_kind: String = obj["expr"] + if str_eq(obj_kind, "Try") { + let inner = obj["inner"] + let inner_c: String = js_cg_expr(inner) + return "(" + inner_c + ")?.[" + js_str_lit(field) + "] ?? null" + } let obj_c: String = js_cg_expr(obj) - return "el_get_field(" + obj_c + ", " + js_str_lit(field) + ")" + return obj_c + "[" + js_str_lit(field) + "]" } if kind == "Index" { // Map vs list dispatch on the index expression kind, same as C. + // If the object is a Try (nil-propagation), use JS optional indexing. let obj = expr["object"] let idx = expr["index"] let obj_c: String = js_cg_expr(obj) let idx_c: String = js_cg_expr(idx) let idx_kind: String = idx["expr"] + let obj_kind: String = obj["expr"] + if str_eq(obj_kind, "Try") { + let inner = obj["inner"] + let inner_c: String = js_cg_expr(inner) + return "(" + inner_c + ")?.[" + idx_c + "] ?? null" + } if str_eq(idx_kind, "Str") { return "el_get_field(" + obj_c + ", " + idx_c + ")" } @@ -4896,41 +6277,41 @@ fn js_cg_expr(expr: Map) -> String { let elems = expr["elems"] let n: Int = native_list_len(elems) if n == 0 { return "[]" } - let items = "" + let items_parts: [String] = native_list_empty() let i = 0 while i < n { let elem = native_list_get(elems, i) let elem_c: String = js_cg_expr(elem) - if i > 0 { - let items = items + ", " - } - let items = items + elem_c + let items_parts = native_list_append(items_parts, elem_c) let i = i + 1 } - return "[" + items + "]" + return "[" + str_join(items_parts, ", ") + "]" } if kind == "Map" { let pairs = expr["pairs"] let n: Int = native_list_len(pairs) if n == 0 { return "{}" } - let items = "" + let items_parts: [String] = native_list_empty() let i = 0 while i < n { let pair = native_list_get(pairs, i) let key: String = pair["key"] let val = pair["value"] let val_c: String = js_cg_expr(val) - if i > 0 { - let items = items + ", " - } - let items = items + js_str_lit(key) + ": " + val_c + let items_parts = native_list_append(items_parts, js_str_lit(key) + ": " + val_c) let i = i + 1 } - return "{" + items + "}" + return "{" + str_join(items_parts, ", ") + "}" } if kind == "Try" { + // Postfix `?` — nil-propagation guard. + // When used as `expr?.field` the Field handler above intercepts and + // emits `(expr)?.["field"]`. Here, a bare `expr?` (not followed by + // field/index access) passes through to the inner expression unchanged + // (it acts as an identity but marks the value as "nil-propagating" for + // its caller). This matches the C backend's current behavior. let inner = expr["inner"] return js_cg_expr(inner) } @@ -4948,6 +6329,17 @@ fn js_cg_expr(expr: Map) -> String { return js_cg_match(expr) } + // Lambda (anonymous function literal): fn(params) -> RetType { body } + // Emitted as a JS arrow function expression: (params) => { body }. + // Used for inline callbacks: dom_listen(el, "click", fn(e: Any) -> Void { ... }) + if kind == "Lambda" { + return js_cg_lambda(expr) + } + + if kind == "HtmlTemplate" { + return js_cg_html_template(expr) + } + "null" } @@ -4975,7 +6367,8 @@ fn js_cg_match(expr: Map) -> String { 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 parts: [String] = native_list_empty() + let parts = native_list_append(parts, "((" + subj_var + ") => { ") let n: Int = native_list_len(arms) let i = 0 while i < n { @@ -4985,28 +6378,36 @@ fn js_cg_match(expr: Map) -> String { let pkind: String = pat["pattern"] let body_c: String = js_cg_expr(body) if str_eq(pkind, "Wildcard") { - let out = out + "return (" + body_c + "); " + let parts = native_list_append(parts, "return (" + body_c + "); ") } else { if str_eq(pkind, "Binding") { let bname: String = pat["name"] - let out = out + "{ const " + bname + " = " + subj_var + "; return (" + body_c + "); } " + let parts = native_list_append(parts, "{ const " + bname + " = " + subj_var + "; return (" + body_c + "); } ") } else { if str_eq(pkind, "LitInt") { let v: String = pat["value"] - let out = out + "if (" + subj_var + " === " + v + ") return (" + body_c + "); " + let parts = native_list_append(parts, "if (" + subj_var + " === " + v + ") return (" + body_c + "); ") } else { if str_eq(pkind, "LitStr") { let v: String = pat["value"] - let out = out + "if (str_eq(" + subj_var + ", " + js_str_lit(v) + ")) return (" + body_c + "); " + let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", " + js_str_lit(v) + ")) return (" + body_c + "); ") } else { if str_eq(pkind, "LitBool") { let v: String = pat["value"] let bv = "false" if str_eq(v, "true") { let bv = "true" } - let out = out + "if (" + subj_var + " === " + bv + ") return (" + body_c + "); " + let parts = native_list_append(parts, "if (" + subj_var + " === " + bv + ") return (" + body_c + "); ") } else { - // unknown pattern → wildcard - let out = out + "return (" + body_c + "); " + if str_eq(pkind, "Variant") { + // Enum::Variant patterns — El enums compile to plain + // strings (the variant name) or ints. Match the subject + // against the variant name string. + let variant: String = pat["variant"] + let parts = native_list_append(parts, "if (str_eq(" + subj_var + ", " + js_str_lit(variant) + ")) return (" + body_c + "); ") + } else { + // unknown pattern → wildcard + let parts = native_list_append(parts, "return (" + body_c + "); ") + } } } } @@ -5014,8 +6415,67 @@ fn js_cg_match(expr: Map) -> String { } let i = i + 1 } - let out = out + "return null; })(" + subj_c + ")" - out + let parts = native_list_append(parts, "return null; })(" + subj_c + ")") + str_join(parts, "") +} + +// ── Lambda codegen ──────────────────────────────────────────────────────────── +// +// Anonymous function literals: fn(params) -> RetType { body } +// +// Strategy: emit the lambda as a hoisted JS function declaration with a +// generated name (__lambda_N), then return the name as the expression value. +// This works because JS function declarations are hoisted within their scope, +// so the generated name is valid at any use site within the same function or +// module. The emitted code looks like: +// +// function __lambda_1(event) { dom_hide(spinner); } +// ... +// dom_listen(btn, "click", __lambda_1); +// +// This approach is clean, debuggable, and avoids any need for a string-buffer +// mode in the codegen. + +fn js_next_lambda_id() -> String { + let csv: String = state_get("__js_lambda_counter") + let n = 0 + if !str_eq(csv, "") { + let n = str_to_int(csv) + } + let n = n + 1 + state_set("__js_lambda_counter", native_int_to_str(n)) + native_int_to_str(n) +} + +fn js_cg_lambda(expr: Map) -> String { + let params = expr["params"] + let body = expr["body"] + let ret_type: String = expr["ret_type"] + let id: String = js_next_lambda_id() + let lambda_name: String = "__lambda_" + id + let params_str: String = js_params_str(params) + // Emit the function definition immediately into the output stream. + // It will appear before the statement containing this expression. + js_emit_line("function " + lambda_name + "(" + params_str + ") {") + let decl = native_list_empty() + let np: Int = native_list_len(params) + let pi = 0 + while pi < np { + let param = native_list_get(params, pi) + let pname: String = param["name"] + let decl = native_list_append(decl, pname) + let pi = pi + 1 + } + let body_xformed = body + if !str_eq(ret_type, "Void") { + let body_xformed = js_transform_implicit_return(body) + } + js_build_int_names_for_params(params) + js_cg_stmts(body_xformed, " ", decl) + js_emit_line("}") + js_emit_blank() + // Return the function name as the expression value. + lambda_name } // ── Variable scope tracking ─────────────────────────────────────────────────── @@ -5123,6 +6583,31 @@ fn js_cg_stmt(stmt: Map, indent: String, declared: [String]) -> [St if kind == "TypeDef" { return declared } if kind == "EnumDef" { return declared } if kind == "Import" { return declared } + // TestDef: skip in normal mode; handled by js_codegen_test in test mode. + if kind == "TestDef" { return declared } + // Assert: no-op in normal mode; handled by js_cg_stmt_assert in test mode. + if kind == "Assert" { return declared } + + if kind == "TryCatch" { + let try_body = stmt["try_body"] + let catch_name: String = stmt["catch_name"] + let catch_body = stmt["catch_body"] + js_emit_line(indent + "try {") + js_cg_stmts(try_body, indent + " ", native_list_clone(declared)) + js_emit_line(indent + "} catch (" + catch_name + ") {") + js_cg_stmts(catch_body, indent + " ", native_list_clone(declared)) + js_emit_line(indent + "}") + return declared + } + + // ExternFn: the function exists in the JS environment (loaded via