diff --git a/dist/platform/elc b/dist/platform/elc index 7992a4e..04c36ef 100755 Binary files a/dist/platform/elc and b/dist/platform/elc differ diff --git a/dist/platform/elc.c b/dist/platform/elc.c index 1eab9b1..52ef58a 100644 --- a/dist/platform/elc.c +++ b/dist/platform/elc.c @@ -2,14 +2,18 @@ #include #include "el_runtime.h" -el_val_t is_digit(el_val_t ch); -el_val_t is_alpha(el_val_t ch); +el_val_t lex_is_digit(el_val_t ch); +el_val_t lex_is_alpha(el_val_t ch); el_val_t is_alnum_or_underscore(el_val_t ch); -el_val_t is_whitespace(el_val_t ch); +el_val_t lex_is_whitespace(el_val_t ch); el_val_t make_tok(el_val_t kind, el_val_t value); el_val_t keyword_kind(el_val_t word); el_val_t scan_digits(el_val_t chars, el_val_t start, el_val_t total); el_val_t scan_ident(el_val_t chars, el_val_t start, el_val_t total); +el_val_t substr_at(el_val_t chars, el_val_t start, el_val_t total, el_val_t needle); +el_val_t str_has(el_val_t s, el_val_t needle); +el_val_t looks_like_code(el_val_t s); +el_val_t strip_code_comments(el_val_t s); el_val_t scan_string(el_val_t chars, el_val_t start, el_val_t total); el_val_t lex(el_val_t source); el_val_t tok_at(el_val_t tokens, el_val_t pos); @@ -25,6 +29,7 @@ el_val_t parse_match(el_val_t tokens, el_val_t pos); el_val_t parse_pattern(el_val_t tokens, el_val_t pos); el_val_t parse_for_expr(el_val_t tokens, el_val_t pos); el_val_t parse_block(el_val_t tokens, el_val_t pos); +el_val_t is_duration_unit(el_val_t name); el_val_t parse_postfix(el_val_t tokens, el_val_t pos); el_val_t op_precedence(el_val_t kind); el_val_t is_binop(el_val_t kind); @@ -38,6 +43,7 @@ el_val_t el_type_to_c(el_val_t type_str); el_val_t emit_line(el_val_t line); el_val_t emit_blank(void); el_val_t binop_to_c(el_val_t op); +el_val_t duration_unit_nanos(el_val_t unit); 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); @@ -55,7 +61,35 @@ el_val_t param_decl(el_val_t param, el_val_t idx); el_val_t params_to_c(el_val_t params); el_val_t transform_implicit_return(el_val_t body); el_val_t is_int_name(el_val_t name); +el_val_t is_instant_name(el_val_t name); +el_val_t is_duration_name(el_val_t name); el_val_t is_int_call(el_val_t call_expr); +el_val_t is_instant_call(el_val_t call_expr); +el_val_t is_duration_call(el_val_t call_expr); +el_val_t is_calendar_name(el_val_t name); +el_val_t is_caltime_name(el_val_t name); +el_val_t is_rhythm_name(el_val_t name); +el_val_t is_localdate_name(el_val_t name); +el_val_t is_localtime_name(el_val_t name); +el_val_t is_localdt_name(el_val_t name); +el_val_t is_zone_name(el_val_t name); +el_val_t is_calendar_call(el_val_t call_expr); +el_val_t is_caltime_call(el_val_t call_expr); +el_val_t is_rhythm_call(el_val_t call_expr); +el_val_t is_localdate_call(el_val_t call_expr); +el_val_t is_localtime_call(el_val_t call_expr); +el_val_t is_localdt_call(el_val_t call_expr); +el_val_t is_zone_call(el_val_t call_expr); +el_val_t is_calendar_expr(el_val_t expr); +el_val_t is_caltime_expr(el_val_t expr); +el_val_t is_rhythm_expr(el_val_t expr); +el_val_t is_localdate_expr(el_val_t expr); +el_val_t is_localtime_expr(el_val_t expr); +el_val_t is_localdt_expr(el_val_t expr); +el_val_t is_zone_expr(el_val_t expr); +el_val_t is_instant_expr(el_val_t expr); +el_val_t is_duration_expr(el_val_t expr); +el_val_t time_record_violation(el_val_t kind, el_val_t detail); el_val_t is_int_expr(el_val_t expr); el_val_t cap_record_violation(el_val_t kind, el_val_t fn_name); el_val_t is_self_formation_call(el_val_t fn_name); @@ -63,11 +97,21 @@ el_val_t is_dharma_call(el_val_t fn_name); el_val_t is_llm_call(el_val_t fn_name); el_val_t cap_check_call(el_val_t fn_name); el_val_t emit_cap_violations(void); +el_val_t emit_time_violations(void); el_val_t builtin_arity(el_val_t name); el_val_t arity_record_violation(el_val_t fn_name, el_val_t expected, el_val_t actual); el_val_t arity_check_call(el_val_t fn_name, el_val_t actual); el_val_t emit_arity_violations(void); el_val_t add_int_name(el_val_t name); +el_val_t add_instant_name(el_val_t name); +el_val_t add_duration_name(el_val_t name); +el_val_t add_calendar_name(el_val_t name); +el_val_t add_caltime_name(el_val_t name); +el_val_t add_rhythm_name(el_val_t name); +el_val_t add_localdate_name(el_val_t name); +el_val_t add_localtime_name(el_val_t name); +el_val_t add_localdt_name(el_val_t name); +el_val_t add_zone_name(el_val_t name); el_val_t build_int_names_for_params(el_val_t params); el_val_t cg_fn(el_val_t stmt); el_val_t is_fndef(el_val_t stmt); @@ -107,13 +151,11 @@ el_val_t compile_js(el_val_t source); el_val_t compile_dispatch(el_val_t tgt, el_val_t source); el_val_t detect_target(el_val_t argv); el_val_t strip_flags(el_val_t argv); +el_val_t 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 _argv; -el_val_t _target; -el_val_t _positional; -el_val_t _argc; - -el_val_t is_digit(el_val_t ch) { +el_val_t lex_is_digit(el_val_t ch) { if (str_eq(ch, EL_STR("0"))) { return 1; } @@ -148,7 +190,7 @@ el_val_t is_digit(el_val_t ch) { return 0; } -el_val_t is_alpha(el_val_t ch) { +el_val_t lex_is_alpha(el_val_t ch) { if (str_eq(ch, EL_STR("a"))) { return 1; } @@ -310,10 +352,10 @@ el_val_t is_alpha(el_val_t ch) { } el_val_t is_alnum_or_underscore(el_val_t ch) { - if (is_digit(ch)) { + if (lex_is_digit(ch)) { return 1; } - if (is_alpha(ch)) { + if (lex_is_alpha(ch)) { return 1; } if (str_eq(ch, EL_STR("_"))) { @@ -323,7 +365,7 @@ el_val_t is_alnum_or_underscore(el_val_t ch) { return 0; } -el_val_t is_whitespace(el_val_t ch) { +el_val_t lex_is_whitespace(el_val_t ch) { if (str_eq(ch, EL_STR(" "))) { return 1; } @@ -485,7 +527,7 @@ el_val_t scan_digits(el_val_t chars, el_val_t start, el_val_t total) { running = 0; } else { el_val_t ch = native_list_get(chars, i); - if (is_digit(ch)) { + if (lex_is_digit(ch)) { text = el_str_concat(text, ch); i = (i + 1); } else { @@ -518,6 +560,213 @@ el_val_t scan_ident(el_val_t chars, el_val_t start, el_val_t total) { return 0; } +el_val_t substr_at(el_val_t chars, el_val_t start, el_val_t total, el_val_t needle) { + el_val_t nchars = native_string_chars(needle); + el_val_t nlen = native_list_len(nchars); + if ((start + nlen) > total) { + return 0; + } + el_val_t i = 0; + el_val_t matched = 1; + while (i < nlen) { + el_val_t a = native_list_get(chars, (start + i)); + el_val_t b = native_list_get(nchars, i); + if (str_eq(a, b)) { + i = (i + 1); + } else { + matched = 0; + EL_NULL; + i = nlen; + } + } + return matched; + return 0; +} + +el_val_t str_has(el_val_t s, el_val_t needle) { + el_val_t chars = native_string_chars(s); + el_val_t total = native_list_len(chars); + el_val_t i = 0; + el_val_t found = 0; + while (i < total) { + if (substr_at(chars, i, total, needle)) { + found = 1; + i = total; + } else { + i = (i + 1); + } + } + return found; + return 0; +} + +el_val_t looks_like_code(el_val_t s) { + if (str_has(s, EL_STR("= total) { + scanning = 0; + } else { + el_val_t lc = native_list_get(chars, i); + if (str_eq(lc, EL_STR("\n"))) { + scanning = 0; + } else { + i = (i + 1); + } + } + } + prev = EL_STR(""); + } + } else { + if (str_eq(next_ch, EL_STR("*"))) { + i = (i + 2); + el_val_t scanning2 = 1; + while (scanning2) { + if (i >= total) { + scanning2 = 0; + } else { + el_val_t bc = native_list_get(chars, i); + if (str_eq(bc, EL_STR("*"))) { + el_val_t after = (i + 1); + if (after < total) { + el_val_t nc2 = native_list_get(chars, after); + if (str_eq(nc2, EL_STR("/"))) { + i = (after + 1); + scanning2 = 0; + } else { + i = (i + 1); + } + } else { + i = (i + 1); + } + } else { + i = (i + 1); + } + } + } + prev = EL_STR(""); + } else { + out = el_str_concat(out, ch); + prev = ch; + i = (i + 1); + } + } + } else { + if (str_eq(ch, EL_STR("'"))) { + in_squote = 1; + out = el_str_concat(out, ch); + prev = ch; + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("\""))) { + in_dquote = 1; + out = el_str_concat(out, ch); + prev = ch; + i = (i + 1); + } else { + if (str_eq(ch, EL_STR("`"))) { + in_btick = 1; + out = el_str_concat(out, ch); + prev = ch; + i = (i + 1); + } else { + out = el_str_concat(out, ch); + prev = ch; + i = (i + 1); + } + } + } + } + } + } + return out; + return 0; +} + el_val_t scan_string(el_val_t chars, el_val_t start, el_val_t total) { el_val_t i = start; el_val_t text = EL_STR(""); @@ -583,7 +832,7 @@ el_val_t lex(el_val_t source) { el_val_t i = 0; while (i < total) { el_val_t ch = native_list_get(chars, i); - if (is_whitespace(ch)) { + if (lex_is_whitespace(ch)) { i = (i + 1); } else { if (str_eq(ch, EL_STR("/"))) { @@ -618,10 +867,14 @@ el_val_t lex(el_val_t source) { el_val_t result = scan_string(chars, (i + 1), total); el_val_t str_text = el_get_field(result, EL_STR("text")); el_val_t new_pos = el_get_field(result, EL_STR("pos")); - tokens = native_list_append(tokens, make_tok(EL_STR("Str"), str_text)); + el_val_t clean_text = str_text; + if (looks_like_code(str_text)) { + clean_text = strip_code_comments(str_text); + } + tokens = native_list_append(tokens, make_tok(EL_STR("Str"), clean_text)); i = new_pos; } else { - if (is_digit(ch)) { + if (lex_is_digit(ch)) { el_val_t result = scan_digits(chars, i, total); el_val_t num_text = el_get_field(result, EL_STR("text")); el_val_t new_pos = el_get_field(result, EL_STR("pos")); @@ -631,7 +884,7 @@ el_val_t lex(el_val_t source) { el_val_t after_dot = (new_pos + 1); if (after_dot < total) { el_val_t after_dot_ch = native_list_get(chars, after_dot); - if (is_digit(after_dot_ch)) { + if (lex_is_digit(after_dot_ch)) { el_val_t frac_result = scan_digits(chars, after_dot, total); el_val_t frac_text = el_get_field(frac_result, EL_STR("text")); el_val_t frac_pos = el_get_field(frac_result, EL_STR("pos")); @@ -654,7 +907,7 @@ el_val_t lex(el_val_t source) { i = new_pos; } } else { - if (is_alpha(ch) || str_eq(ch, EL_STR("_"))) { + if (lex_is_alpha(ch) || str_eq(ch, EL_STR("_"))) { el_val_t result = scan_ident(chars, i, total); el_val_t word = el_get_field(result, EL_STR("text")); el_val_t new_pos = el_get_field(result, EL_STR("pos")); @@ -1274,10 +1527,72 @@ el_val_t parse_block(el_val_t tokens, el_val_t pos) { return 0; } +el_val_t is_duration_unit(el_val_t name) { + if (str_eq(name, EL_STR("nanos"))) { + return 1; + } + if (str_eq(name, EL_STR("nano"))) { + return 1; + } + if (str_eq(name, EL_STR("millis"))) { + return 1; + } + if (str_eq(name, EL_STR("milli"))) { + return 1; + } + if (str_eq(name, EL_STR("millisecond"))) { + return 1; + } + if (str_eq(name, EL_STR("milliseconds"))) { + return 1; + } + if (str_eq(name, EL_STR("second"))) { + return 1; + } + if (str_eq(name, EL_STR("seconds"))) { + return 1; + } + if (str_eq(name, EL_STR("minute"))) { + return 1; + } + if (str_eq(name, EL_STR("minutes"))) { + return 1; + } + if (str_eq(name, EL_STR("hour"))) { + return 1; + } + if (str_eq(name, EL_STR("hours"))) { + return 1; + } + if (str_eq(name, EL_STR("day"))) { + return 1; + } + if (str_eq(name, EL_STR("days"))) { + return 1; + } + return 0; + return 0; +} + el_val_t parse_postfix(el_val_t tokens, el_val_t pos) { el_val_t r = parse_primary(tokens, pos); el_val_t node = el_get_field(r, EL_STR("node")); el_val_t p = el_get_field(r, EL_STR("pos")); + el_val_t primary_kind = el_get_field(node, EL_STR("expr")); + if (str_eq(primary_kind, EL_STR("Int"))) { + el_val_t dot_kind = tok_kind(tokens, p); + if (str_eq(dot_kind, EL_STR("Dot"))) { + el_val_t unit_kind = tok_kind(tokens, (p + 1)); + if (str_eq(unit_kind, EL_STR("Ident"))) { + el_val_t unit_name = tok_value(tokens, (p + 1)); + if (is_duration_unit(unit_name)) { + el_val_t count_str = el_get_field(node, EL_STR("value")); + node = el_map_new(3, "expr", EL_STR("DurationLit"), "count", count_str, "unit", unit_name); + p = (p + 2); + } + } + } + } el_val_t running = 1; while (running) { el_val_t k = tok_kind(tokens, p); @@ -1733,6 +2048,17 @@ 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("Ident"))) { + el_val_t k2 = tok_kind(tokens, (pos + 1)); + if (str_eq(k2, EL_STR("Eq"))) { + el_val_t name = tok_value(tokens, pos); + el_val_t p = (pos + 2); + el_val_t r = parse_expr(tokens, p); + el_val_t val = el_get_field(r, EL_STR("node")); + p = el_get_field(r, EL_STR("pos")); + return make_result(el_map_new(3, "stmt", EL_STR("Assign"), "name", name, "value", val), p); + } + } el_val_t r = parse_expr(tokens, pos); el_val_t val = el_get_field(r, EL_STR("node")); el_val_t p = el_get_field(r, EL_STR("pos")); @@ -1882,12 +2208,65 @@ el_val_t binop_to_c(el_val_t op) { return 0; } +el_val_t duration_unit_nanos(el_val_t unit) { + if (str_eq(unit, EL_STR("nano"))) { + return EL_STR("1LL"); + } + if (str_eq(unit, EL_STR("nanos"))) { + return EL_STR("1LL"); + } + if (str_eq(unit, EL_STR("milli"))) { + return EL_STR("1000000LL"); + } + if (str_eq(unit, EL_STR("millis"))) { + return EL_STR("1000000LL"); + } + if (str_eq(unit, EL_STR("millisecond"))) { + return EL_STR("1000000LL"); + } + if (str_eq(unit, EL_STR("milliseconds"))) { + return EL_STR("1000000LL"); + } + if (str_eq(unit, EL_STR("second"))) { + return EL_STR("1000000000LL"); + } + if (str_eq(unit, EL_STR("seconds"))) { + return EL_STR("1000000000LL"); + } + if (str_eq(unit, EL_STR("minute"))) { + return EL_STR("60000000000LL"); + } + if (str_eq(unit, EL_STR("minutes"))) { + return EL_STR("60000000000LL"); + } + if (str_eq(unit, EL_STR("hour"))) { + return EL_STR("3600000000000LL"); + } + if (str_eq(unit, EL_STR("hours"))) { + return EL_STR("3600000000000LL"); + } + if (str_eq(unit, EL_STR("day"))) { + return EL_STR("86400000000000LL"); + } + if (str_eq(unit, EL_STR("days"))) { + return EL_STR("86400000000000LL"); + } + return EL_STR("1LL"); + return 0; +} + el_val_t cg_expr(el_val_t expr) { el_val_t kind = el_get_field(expr, EL_STR("expr")); if (str_eq(kind, EL_STR("Int"))) { el_val_t v = el_get_field(expr, EL_STR("value")); return v; } + if (str_eq(kind, EL_STR("DurationLit"))) { + el_val_t count = el_get_field(expr, EL_STR("count")); + el_val_t unit = el_get_field(expr, EL_STR("unit")); + el_val_t mult = duration_unit_nanos(unit); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_from_nanos((el_val_t)("), count), EL_STR("LL * ")), mult), EL_STR("))")); + } if (str_eq(kind, EL_STR("Float"))) { el_val_t v = el_get_field(expr, EL_STR("value")); return el_str_concat(el_str_concat(EL_STR("el_from_float("), v), EL_STR(")")); @@ -1928,6 +2307,210 @@ el_val_t cg_expr(el_val_t expr) { el_val_t right_c = cg_expr(right); el_val_t left_kind = el_get_field(left, EL_STR("expr")); el_val_t right_kind = el_get_field(right, EL_STR("expr")); + el_val_t left_is_inst = is_instant_expr(left); + el_val_t right_is_inst = is_instant_expr(right); + el_val_t left_is_dur = is_duration_expr(left); + el_val_t right_is_dur = is_duration_expr(right); + el_val_t left_is_ld = is_localdate_expr(left); + el_val_t right_is_ld = is_localdate_expr(right); + el_val_t left_is_lt = is_localtime_expr(left); + el_val_t right_is_lt = is_localtime_expr(right); + el_val_t left_is_ct = is_caltime_expr(left); + el_val_t right_is_ct = is_caltime_expr(right); + if (left_is_ld) { + if (str_eq(op, EL_STR("Plus"))) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_local_date_add_dur("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (str_eq(op, EL_STR("Lt"))) { + if (right_is_ld) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_local_date_lt("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (str_eq(op, EL_STR("EqEq"))) { + if (right_is_ld) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_local_date_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + } + if (left_is_lt) { + if (str_eq(op, EL_STR("Plus"))) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_local_time_add_dur("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + } + if (left_is_ct) { + if (str_eq(op, EL_STR("Plus"))) { + if (right_is_ct) { + time_record_violation(EL_STR("caltime_plus_caltime"), EL_STR("CalendarTime + CalendarTime is not allowed (use cal_to_instant + Duration)")); + return EL_STR("0 /* TIME_TYPE_ERROR: CalendarTime + CalendarTime */"); + } + } + } + el_val_t any_temporal = 0; + if (left_is_inst) { + any_temporal = 1; + } + if (right_is_inst) { + any_temporal = 1; + } + if (left_is_dur) { + any_temporal = 1; + } + if (right_is_dur) { + any_temporal = 1; + } + if (any_temporal) { + if (str_eq(op, EL_STR("Plus"))) { + if (left_is_inst) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_instant_add_dur("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (right_is_inst) { + time_record_violation(EL_STR("instant_plus_instant"), EL_STR("Instant + Instant is not allowed")); + return EL_STR("0 /* TIME_TYPE_ERROR: Instant + Instant */"); + } + } + if (left_is_dur) { + if (right_is_inst) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_instant_add_dur("), right_c), EL_STR(", ")), left_c), EL_STR(")")); + } + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_add("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (is_int_expr(right)) { + time_record_violation(EL_STR("duration_plus_int"), EL_STR("Duration + Int is not allowed (use duration_seconds(n) or N.seconds)")); + return EL_STR("0 /* TIME_TYPE_ERROR: Duration + Int */"); + } + } + if (right_is_dur) { + if (is_int_expr(left)) { + time_record_violation(EL_STR("duration_plus_int"), EL_STR("Int + Duration is not allowed")); + return EL_STR("0 /* TIME_TYPE_ERROR: Int + Duration */"); + } + } + } + if (str_eq(op, EL_STR("Minus"))) { + if (left_is_inst) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_instant_sub_dur("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (right_is_inst) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_instant_diff("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (left_is_dur) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_sub("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (is_int_expr(right)) { + time_record_violation(EL_STR("duration_minus_int"), EL_STR("Duration - Int is not allowed")); + return EL_STR("0 /* TIME_TYPE_ERROR: Duration - Int */"); + } + } + } + if (str_eq(op, EL_STR("Star"))) { + if (left_is_dur) { + if (is_int_expr(right)) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_scale("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (right_is_dur) { + if (is_int_expr(left)) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_scale("), right_c), EL_STR(", ")), left_c), EL_STR(")")); + } + } + } + if (str_eq(op, EL_STR("Slash"))) { + if (left_is_dur) { + if (is_int_expr(right)) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_div("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + } + if (str_eq(op, EL_STR("Lt"))) { + if (left_is_inst) { + if (right_is_inst) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_instant_lt("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (right_is_dur) { + time_record_violation(EL_STR("instant_cmp_duration"), EL_STR("Instant < Duration is not allowed")); + return EL_STR("0 /* TIME_TYPE_ERROR: Instant < Duration */"); + } + } + if (left_is_dur) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_lt("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + if (right_is_inst) { + time_record_violation(EL_STR("duration_cmp_instant"), EL_STR("Duration < Instant is not allowed")); + return EL_STR("0 /* TIME_TYPE_ERROR: Duration < Instant */"); + } + } + } + if (str_eq(op, EL_STR("LtEq"))) { + if (left_is_inst) { + if (right_is_inst) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_instant_le("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (left_is_dur) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_le("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + } + if (str_eq(op, EL_STR("Gt"))) { + if (left_is_inst) { + if (right_is_inst) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_instant_gt("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (left_is_dur) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_gt("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + } + if (str_eq(op, EL_STR("GtEq"))) { + if (left_is_inst) { + if (right_is_inst) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_instant_ge("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (left_is_dur) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_ge("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + } + if (str_eq(op, EL_STR("EqEq"))) { + if (left_is_inst) { + if (right_is_inst) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_instant_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (left_is_dur) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_eq("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + } + if (str_eq(op, EL_STR("NotEq"))) { + if (left_is_inst) { + if (right_is_inst) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_instant_ne("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + if (left_is_dur) { + if (right_is_dur) { + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_duration_ne("), left_c), EL_STR(", ")), right_c), EL_STR(")")); + } + } + } + } if (str_eq(op, EL_STR("Plus"))) { if (str_eq(left_kind, EL_STR("Str"))) { return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("el_str_concat("), left_c), EL_STR(", ")), right_c), EL_STR(")")); @@ -1998,8 +2581,6 @@ el_val_t cg_expr(el_val_t expr) { } } } - /* Mixed Ident/BinOp int comparison (e.g. `i == n - 1`): use is_int_expr - to detect both sides are integers before falling through to str_eq. */ if (is_int_expr(left)) { if (is_int_expr(right)) { return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" == ")), right_c), EL_STR(")")); @@ -2048,7 +2629,6 @@ el_val_t cg_expr(el_val_t expr) { } } } - /* Same mixed Ident/BinOp fix as EqEq for NotEq. */ if (is_int_expr(left)) { if (is_int_expr(right)) { return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), left_c), EL_STR(" != ")), right_c), EL_STR(")")); @@ -2096,6 +2676,14 @@ el_val_t cg_expr(el_val_t expr) { el_val_t fn_name = el_get_field(func, EL_STR("name")); cap_check_call(fn_name); arity_check_call(fn_name, arity); + if (str_eq(fn_name, EL_STR("sleep"))) { + if (arity == 1) { + el_val_t only_arg = native_list_get(args, 0); + if (is_duration_expr(only_arg)) { + return el_str_concat(el_str_concat(EL_STR("el_sleep_duration("), args_c), EL_STR(")")); + } + } + } return el_str_concat(el_str_concat(el_str_concat(fn_name, EL_STR("(")), args_c), EL_STR(")")); } if (str_eq(func_kind, EL_STR("Field"))) { @@ -2266,7 +2854,6 @@ el_val_t cg_if_expr_arm(el_val_t stmts, el_val_t result_var) { el_val_t s = native_list_get(stmts, i); el_val_t sk = el_get_field(s, EL_STR("stmt")); el_val_t is_last = 0; - /* BUG FIX: was str_eq(i, n-1) which segfaults comparing ints as strings */ if (i == (n - 1)) { is_last = 1; } @@ -2290,6 +2877,13 @@ el_val_t cg_if_expr_arm(el_val_t stmts, el_val_t result_var) { out = el_str_concat(el_str_concat(el_str_concat(out, EL_STR("(void)(")), val_c), EL_STR("); ")); } } else { + if (str_eq(sk, EL_STR("Assign"))) { + el_val_t aname = el_get_field(s, EL_STR("name")); + el_val_t aval = el_get_field(s, EL_STR("value")); + el_val_t aval_c = cg_expr(aval); + out = el_str_concat(el_str_concat(el_str_concat(el_str_concat(out, aname), EL_STR(" = ")), aval_c), EL_STR("; ")); + } else { + } } } } @@ -2341,6 +2935,60 @@ el_val_t cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared) { if (str_eq(ltype, EL_STR("Int"))) { add_int_name(name); } + if (str_eq(ltype, EL_STR("Instant"))) { + add_instant_name(name); + } + if (str_eq(ltype, EL_STR("Duration"))) { + add_duration_name(name); + } + if (str_eq(ltype, EL_STR("Calendar"))) { + add_calendar_name(name); + } + if (str_eq(ltype, EL_STR("CalendarTime"))) { + add_caltime_name(name); + } + if (str_eq(ltype, EL_STR("Rhythm"))) { + add_rhythm_name(name); + } + if (str_eq(ltype, EL_STR("LocalDate"))) { + add_localdate_name(name); + } + if (str_eq(ltype, EL_STR("LocalTime"))) { + add_localtime_name(name); + } + if (str_eq(ltype, EL_STR("LocalDateTime"))) { + add_localdt_name(name); + } + if (str_eq(ltype, EL_STR("Zone"))) { + add_zone_name(name); + } + if (is_instant_expr(val)) { + add_instant_name(name); + } + if (is_duration_expr(val)) { + add_duration_name(name); + } + if (is_calendar_expr(val)) { + add_calendar_name(name); + } + if (is_caltime_expr(val)) { + add_caltime_name(name); + } + if (is_rhythm_expr(val)) { + add_rhythm_name(name); + } + if (is_localdate_expr(val)) { + add_localdate_name(name); + } + if (is_localtime_expr(val)) { + add_localtime_name(name); + } + if (is_localdt_expr(val)) { + add_localdt_name(name); + } + if (is_zone_expr(val)) { + add_zone_name(name); + } el_val_t vk = el_get_field(val, EL_STR("expr")); if (str_eq(vk, EL_STR("Int"))) { add_int_name(name); @@ -2364,6 +3012,13 @@ el_val_t cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared) { } return declared; } + if (str_eq(kind, EL_STR("Assign"))) { + el_val_t name = el_get_field(stmt, EL_STR("name")); + el_val_t val = el_get_field(stmt, EL_STR("value")); + el_val_t val_c = cg_expr(val); + emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, name), EL_STR(" = ")), val_c), EL_STR(";"))); + return declared; + } if (str_eq(kind, EL_STR("Expr"))) { el_val_t val = el_get_field(stmt, EL_STR("value")); el_val_t val_kind = el_get_field(val, EL_STR("expr")); @@ -2579,6 +3234,24 @@ el_val_t is_int_name(el_val_t name) { return 0; } +el_val_t is_instant_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__instant_names")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","))); + return 0; +} + +el_val_t is_duration_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__duration_names")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","))); + return 0; +} + el_val_t is_int_call(el_val_t call_expr) { el_val_t func = el_get_field(call_expr, EL_STR("func")); el_val_t fk = el_get_field(func, EL_STR("expr")); @@ -2598,6 +3271,33 @@ el_val_t is_int_call(el_val_t call_expr) { if (str_eq(name, EL_STR("str_char_code"))) { return 1; } + if (str_eq(name, EL_STR("str_count"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_chars"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_bytes"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_lines"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_words"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_letters"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_digits"))) { + return 1; + } + if (str_eq(name, EL_STR("str_last_index_of"))) { + return 1; + } + if (str_eq(name, EL_STR("str_find_chars"))) { + return 1; + } if (str_eq(name, EL_STR("native_list_len"))) { return 1; } @@ -2646,10 +3346,527 @@ el_val_t is_int_call(el_val_t call_expr) { if (str_eq(name, EL_STR("float_to_int"))) { return 1; } + if (str_eq(name, EL_STR("unix_timestamp"))) { + return 1; + } + if (str_eq(name, EL_STR("instant_to_unix_seconds"))) { + return 1; + } + if (str_eq(name, EL_STR("instant_to_unix_millis"))) { + return 1; + } + if (str_eq(name, EL_STR("duration_to_seconds"))) { + return 1; + } + if (str_eq(name, EL_STR("duration_to_millis"))) { + return 1; + } + if (str_eq(name, EL_STR("duration_to_nanos"))) { + return 1; + } return 0; return 0; } +el_val_t is_instant_call(el_val_t call_expr) { + el_val_t func = el_get_field(call_expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (!str_eq(fk, EL_STR("Ident"))) { + return 0; + } + el_val_t name = el_get_field(func, EL_STR("name")); + if (str_eq(name, EL_STR("now"))) { + return 1; + } + if (str_eq(name, EL_STR("el_now_instant"))) { + return 1; + } + if (str_eq(name, EL_STR("unix_seconds"))) { + return 1; + } + if (str_eq(name, EL_STR("unix_millis"))) { + return 1; + } + if (str_eq(name, EL_STR("instant_from_iso8601"))) { + return 1; + } + if (str_eq(name, EL_STR("el_instant_add_dur"))) { + return 1; + } + if (str_eq(name, EL_STR("el_instant_sub_dur"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_duration_call(el_val_t call_expr) { + el_val_t func = el_get_field(call_expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (!str_eq(fk, EL_STR("Ident"))) { + return 0; + } + el_val_t name = el_get_field(func, EL_STR("name")); + if (str_eq(name, EL_STR("el_duration_from_nanos"))) { + return 1; + } + if (str_eq(name, EL_STR("duration_seconds"))) { + return 1; + } + if (str_eq(name, EL_STR("duration_millis"))) { + return 1; + } + if (str_eq(name, EL_STR("duration_nanos"))) { + return 1; + } + if (str_eq(name, EL_STR("el_instant_diff"))) { + return 1; + } + if (str_eq(name, EL_STR("el_duration_add"))) { + return 1; + } + if (str_eq(name, EL_STR("el_duration_sub"))) { + return 1; + } + if (str_eq(name, EL_STR("el_duration_scale"))) { + return 1; + } + if (str_eq(name, EL_STR("el_duration_div"))) { + return 1; + } + if (str_eq(name, EL_STR("ttl_cache_age"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_calendar_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__calendar_names")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","))); + return 0; +} + +el_val_t is_caltime_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__caltime_names")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","))); + return 0; +} + +el_val_t is_rhythm_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__rhythm_names")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","))); + return 0; +} + +el_val_t is_localdate_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__localdate_names")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","))); + return 0; +} + +el_val_t is_localtime_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__localtime_names")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","))); + return 0; +} + +el_val_t is_localdt_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__localdt_names")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","))); + return 0; +} + +el_val_t is_zone_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__zone_names")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + return str_contains(csv, el_str_concat(el_str_concat(EL_STR(","), name), EL_STR(","))); + return 0; +} + +el_val_t is_calendar_call(el_val_t call_expr) { + el_val_t func = el_get_field(call_expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (!str_eq(fk, EL_STR("Ident"))) { + return 0; + } + el_val_t name = el_get_field(func, EL_STR("name")); + if (str_eq(name, EL_STR("earth_calendar"))) { + return 1; + } + if (str_eq(name, EL_STR("earth_calendar_default"))) { + return 1; + } + if (str_eq(name, EL_STR("mars_calendar"))) { + return 1; + } + if (str_eq(name, EL_STR("cycle_calendar"))) { + return 1; + } + if (str_eq(name, EL_STR("no_cycle_calendar"))) { + return 1; + } + if (str_eq(name, EL_STR("relative_calendar"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_caltime_call(el_val_t call_expr) { + el_val_t func = el_get_field(call_expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (!str_eq(fk, EL_STR("Ident"))) { + return 0; + } + el_val_t name = el_get_field(func, EL_STR("name")); + if (str_eq(name, EL_STR("now_in"))) { + return 1; + } + if (str_eq(name, EL_STR("in_calendar"))) { + return 1; + } + if (str_eq(name, EL_STR("cal_in"))) { + return 1; + } + if (str_eq(name, EL_STR("zoned"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_rhythm_call(el_val_t call_expr) { + el_val_t func = el_get_field(call_expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (!str_eq(fk, EL_STR("Ident"))) { + return 0; + } + el_val_t name = el_get_field(func, EL_STR("name")); + if (str_eq(name, EL_STR("rhythm_cycle_start"))) { + return 1; + } + if (str_eq(name, EL_STR("rhythm_cycle_phase"))) { + return 1; + } + if (str_eq(name, EL_STR("rhythm_duration"))) { + return 1; + } + if (str_eq(name, EL_STR("rhythm_session_start"))) { + return 1; + } + if (str_eq(name, EL_STR("rhythm_event"))) { + return 1; + } + if (str_eq(name, EL_STR("rhythm_and"))) { + return 1; + } + if (str_eq(name, EL_STR("rhythm_or"))) { + return 1; + } + if (str_eq(name, EL_STR("rhythm_weekday"))) { + return 1; + } + if (str_eq(name, EL_STR("rhythm_weekly_at"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_localdate_call(el_val_t call_expr) { + el_val_t func = el_get_field(call_expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (!str_eq(fk, EL_STR("Ident"))) { + return 0; + } + el_val_t name = el_get_field(func, EL_STR("name")); + if (str_eq(name, EL_STR("local_date"))) { + return 1; + } + if (str_eq(name, EL_STR("el_local_date_add_dur"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_localtime_call(el_val_t call_expr) { + el_val_t func = el_get_field(call_expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (!str_eq(fk, EL_STR("Ident"))) { + return 0; + } + el_val_t name = el_get_field(func, EL_STR("name")); + if (str_eq(name, EL_STR("local_time"))) { + return 1; + } + if (str_eq(name, EL_STR("el_local_time_add_dur"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_localdt_call(el_val_t call_expr) { + el_val_t func = el_get_field(call_expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (!str_eq(fk, EL_STR("Ident"))) { + return 0; + } + el_val_t name = el_get_field(func, EL_STR("name")); + if (str_eq(name, EL_STR("local_datetime"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_zone_call(el_val_t call_expr) { + el_val_t func = el_get_field(call_expr, EL_STR("func")); + el_val_t fk = el_get_field(func, EL_STR("expr")); + if (!str_eq(fk, EL_STR("Ident"))) { + return 0; + } + el_val_t name = el_get_field(func, EL_STR("name")); + if (str_eq(name, EL_STR("zone"))) { + return 1; + } + if (str_eq(name, EL_STR("zone_utc"))) { + return 1; + } + if (str_eq(name, EL_STR("zone_local"))) { + return 1; + } + if (str_eq(name, EL_STR("zone_offset"))) { + return 1; + } + return 0; + return 0; +} + +el_val_t is_calendar_expr(el_val_t expr) { + el_val_t k = el_get_field(expr, EL_STR("expr")); + if (str_eq(k, EL_STR("Ident"))) { + return is_calendar_name(el_get_field(expr, EL_STR("name"))); + } + if (str_eq(k, EL_STR("Call"))) { + return is_calendar_call(expr); + } + return 0; + return 0; +} + +el_val_t is_caltime_expr(el_val_t expr) { + el_val_t k = el_get_field(expr, EL_STR("expr")); + if (str_eq(k, EL_STR("Ident"))) { + return is_caltime_name(el_get_field(expr, EL_STR("name"))); + } + if (str_eq(k, EL_STR("Call"))) { + return is_caltime_call(expr); + } + return 0; + return 0; +} + +el_val_t is_rhythm_expr(el_val_t expr) { + el_val_t k = el_get_field(expr, EL_STR("expr")); + if (str_eq(k, EL_STR("Ident"))) { + return is_rhythm_name(el_get_field(expr, EL_STR("name"))); + } + if (str_eq(k, EL_STR("Call"))) { + return is_rhythm_call(expr); + } + return 0; + return 0; +} + +el_val_t is_localdate_expr(el_val_t expr) { + el_val_t k = el_get_field(expr, EL_STR("expr")); + if (str_eq(k, EL_STR("Ident"))) { + return is_localdate_name(el_get_field(expr, EL_STR("name"))); + } + if (str_eq(k, EL_STR("Call"))) { + return is_localdate_call(expr); + } + return 0; + return 0; +} + +el_val_t is_localtime_expr(el_val_t expr) { + el_val_t k = el_get_field(expr, EL_STR("expr")); + if (str_eq(k, EL_STR("Ident"))) { + return is_localtime_name(el_get_field(expr, EL_STR("name"))); + } + if (str_eq(k, EL_STR("Call"))) { + return is_localtime_call(expr); + } + return 0; + return 0; +} + +el_val_t is_localdt_expr(el_val_t expr) { + el_val_t k = el_get_field(expr, EL_STR("expr")); + if (str_eq(k, EL_STR("Ident"))) { + return is_localdt_name(el_get_field(expr, EL_STR("name"))); + } + if (str_eq(k, EL_STR("Call"))) { + return is_localdt_call(expr); + } + return 0; + return 0; +} + +el_val_t is_zone_expr(el_val_t expr) { + el_val_t k = el_get_field(expr, EL_STR("expr")); + if (str_eq(k, EL_STR("Ident"))) { + return is_zone_name(el_get_field(expr, EL_STR("name"))); + } + if (str_eq(k, EL_STR("Call"))) { + return is_zone_call(expr); + } + return 0; + return 0; +} + +el_val_t is_instant_expr(el_val_t expr) { + el_val_t k = el_get_field(expr, EL_STR("expr")); + if (str_eq(k, EL_STR("Ident"))) { + el_val_t name = el_get_field(expr, EL_STR("name")); + return is_instant_name(name); + } + if (str_eq(k, EL_STR("Call"))) { + return is_instant_call(expr); + } + if (str_eq(k, EL_STR("BinOp"))) { + el_val_t op = el_get_field(expr, EL_STR("op")); + if (str_eq(op, EL_STR("Plus"))) { + if (is_instant_expr(el_get_field(expr, EL_STR("left")))) { + if (is_duration_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + if (is_duration_expr(el_get_field(expr, EL_STR("left")))) { + if (is_instant_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + return 0; + } + if (str_eq(op, EL_STR("Minus"))) { + if (is_instant_expr(el_get_field(expr, EL_STR("left")))) { + if (is_duration_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + return 0; + } + return 0; + } + return 0; + return 0; +} + +el_val_t is_duration_expr(el_val_t expr) { + el_val_t k = el_get_field(expr, EL_STR("expr")); + if (str_eq(k, EL_STR("DurationLit"))) { + return 1; + } + if (str_eq(k, EL_STR("Ident"))) { + el_val_t name = el_get_field(expr, EL_STR("name")); + return is_duration_name(name); + } + if (str_eq(k, EL_STR("Call"))) { + return is_duration_call(expr); + } + if (str_eq(k, EL_STR("Neg"))) { + return is_duration_expr(el_get_field(expr, EL_STR("inner"))); + } + if (str_eq(k, EL_STR("BinOp"))) { + el_val_t op = el_get_field(expr, EL_STR("op")); + if (str_eq(op, EL_STR("Plus"))) { + if (is_duration_expr(el_get_field(expr, EL_STR("left")))) { + if (is_duration_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + return 0; + } + if (str_eq(op, EL_STR("Minus"))) { + if (is_duration_expr(el_get_field(expr, EL_STR("left")))) { + if (is_duration_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + if (is_instant_expr(el_get_field(expr, EL_STR("left")))) { + if (is_instant_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + return 0; + } + if (str_eq(op, EL_STR("Star"))) { + if (is_duration_expr(el_get_field(expr, EL_STR("left")))) { + if (is_int_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + if (is_int_expr(el_get_field(expr, EL_STR("left")))) { + if (is_duration_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + return 0; + } + if (str_eq(op, EL_STR("Slash"))) { + if (is_duration_expr(el_get_field(expr, EL_STR("left")))) { + if (is_int_expr(el_get_field(expr, EL_STR("right")))) { + return 1; + } + } + return 0; + } + return 0; + } + return 0; + return 0; +} + +el_val_t time_record_violation(el_val_t kind, el_val_t detail) { + el_val_t csv = state_get(EL_STR("__time_violations")); + if (str_eq(csv, EL_STR(""))) { + csv = EL_STR(","); + } + el_val_t entry = el_str_concat(el_str_concat(kind, EL_STR(":")), detail); + el_val_t key = el_str_concat(el_str_concat(EL_STR(","), entry), EL_STR(",")); + if (str_contains(csv, key)) { + return 1; + } + state_set(EL_STR("__time_violations"), el_str_concat(el_str_concat(csv, entry), EL_STR(","))); + return 1; + return 0; +} + el_val_t is_int_expr(el_val_t expr) { el_val_t k = el_get_field(expr, EL_STR("expr")); if (str_eq(k, EL_STR("Int"))) { @@ -2867,6 +4084,32 @@ el_val_t emit_cap_violations(void) { return 0; } +el_val_t emit_time_violations(void) { + el_val_t csv = state_get(EL_STR("__time_violations")); + if (str_eq(csv, EL_STR(""))) { + return 0; + } + if (str_eq(csv, EL_STR(","))) { + return 0; + } + el_val_t n = str_len(csv); + el_val_t i = 1; + while (i < n) { + el_val_t next_comma = str_index_of(str_slice(csv, i, n), EL_STR(",")); + if (next_comma < 0) { + return 0; + } + el_val_t entry = str_slice(csv, i, (i + next_comma)); + el_val_t colon = str_index_of(entry, EL_STR(":")); + if (colon > 0) { + el_val_t detail = str_slice(entry, (colon + 1), str_len(entry)); + emit_line(el_str_concat(el_str_concat(EL_STR("#error \"temporal type error: "), detail), EL_STR("\""))); + } + i = ((i + next_comma) + 1); + } + return 0; +} + el_val_t builtin_arity(el_val_t name) { if (str_eq(name, EL_STR("println"))) { return 1; @@ -2946,6 +4189,93 @@ el_val_t builtin_arity(el_val_t name) { if (str_eq(name, EL_STR("str_upper"))) { return 1; } + if (str_eq(name, EL_STR("str_count"))) { + return 2; + } + if (str_eq(name, EL_STR("str_count_chars"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_bytes"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_lines"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_words"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_letters"))) { + return 1; + } + if (str_eq(name, EL_STR("str_count_digits"))) { + return 1; + } + if (str_eq(name, EL_STR("str_index_of_all"))) { + return 2; + } + if (str_eq(name, EL_STR("str_last_index_of"))) { + return 2; + } + if (str_eq(name, EL_STR("str_find_chars"))) { + return 2; + } + if (str_eq(name, EL_STR("str_repeat"))) { + return 2; + } + if (str_eq(name, EL_STR("str_reverse"))) { + return 1; + } + if (str_eq(name, EL_STR("str_strip_prefix"))) { + return 2; + } + if (str_eq(name, EL_STR("str_strip_suffix"))) { + return 2; + } + if (str_eq(name, EL_STR("str_strip_chars"))) { + return 2; + } + if (str_eq(name, EL_STR("str_lstrip"))) { + return 1; + } + if (str_eq(name, EL_STR("str_rstrip"))) { + return 1; + } + if (str_eq(name, EL_STR("is_letter"))) { + return 1; + } + if (str_eq(name, EL_STR("is_digit"))) { + return 1; + } + if (str_eq(name, EL_STR("is_alphanumeric"))) { + return 1; + } + if (str_eq(name, EL_STR("is_whitespace"))) { + return 1; + } + if (str_eq(name, EL_STR("is_punctuation"))) { + return 1; + } + if (str_eq(name, EL_STR("is_uppercase"))) { + return 1; + } + if (str_eq(name, EL_STR("is_lowercase"))) { + return 1; + } + if (str_eq(name, EL_STR("str_split_lines"))) { + return 1; + } + if (str_eq(name, EL_STR("str_split_chars"))) { + return 1; + } + if (str_eq(name, EL_STR("str_split_n"))) { + return 3; + } + if (str_eq(name, EL_STR("str_join"))) { + return 2; + } + if (str_eq(name, EL_STR("el_html_sanitize"))) { + return 2; + } if (str_eq(name, EL_STR("el_abs"))) { return 1; } @@ -3009,12 +4339,6 @@ el_val_t builtin_arity(el_val_t name) { if (str_eq(name, EL_STR("http_post_form_auth"))) { return 3; } - if (str_eq(name, EL_STR("http_get_to_file"))) { - return 3; - } - if (str_eq(name, EL_STR("http_post_to_file"))) { - return 4; - } if (str_eq(name, EL_STR("http_serve"))) { return 2; } @@ -3027,9 +4351,6 @@ el_val_t builtin_arity(el_val_t name) { if (str_eq(name, EL_STR("fs_write"))) { return 2; } - if (str_eq(name, EL_STR("fs_write_bytes"))) { - return 3; - } if (str_eq(name, EL_STR("fs_list"))) { return 1; } @@ -3388,9 +4709,7 @@ el_val_t emit_arity_violations(void) { el_val_t add_int_name(el_val_t name) { el_val_t csv = state_get(EL_STR("__int_names")); if (str_eq(csv, EL_STR(""))) { - csv; - EL_NULL; - 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)) { @@ -3401,8 +4720,143 @@ el_val_t add_int_name(el_val_t name) { return 0; } +el_val_t add_instant_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__instant_names")); + 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("__instant_names"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + +el_val_t add_duration_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__duration_names")); + 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("__duration_names"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + +el_val_t add_calendar_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__calendar_names")); + 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("__calendar_names"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + +el_val_t add_caltime_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__caltime_names")); + 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("__caltime_names"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + +el_val_t add_rhythm_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__rhythm_names")); + 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("__rhythm_names"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + +el_val_t add_localdate_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__localdate_names")); + 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("__localdate_names"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + +el_val_t add_localtime_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__localtime_names")); + 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("__localtime_names"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + +el_val_t add_localdt_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__localdt_names")); + 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("__localdt_names"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + +el_val_t add_zone_name(el_val_t name) { + el_val_t csv = state_get(EL_STR("__zone_names")); + 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("__zone_names"), el_str_concat(el_str_concat(csv, name), EL_STR(","))); + return 1; + return 0; +} + el_val_t build_int_names_for_params(el_val_t params) { state_set(EL_STR("__int_names"), EL_STR(",")); + state_set(EL_STR("__instant_names"), EL_STR(",")); + state_set(EL_STR("__duration_names"), EL_STR(",")); + state_set(EL_STR("__calendar_names"), EL_STR(",")); + state_set(EL_STR("__caltime_names"), EL_STR(",")); + state_set(EL_STR("__rhythm_names"), EL_STR(",")); + state_set(EL_STR("__localdate_names"), EL_STR(",")); + state_set(EL_STR("__localtime_names"), EL_STR(",")); + state_set(EL_STR("__localdt_names"), EL_STR(",")); + state_set(EL_STR("__zone_names"), EL_STR(",")); el_val_t np = native_list_len(params); el_val_t pi = 0; while (pi < np) { @@ -3412,6 +4866,33 @@ el_val_t build_int_names_for_params(el_val_t params) { if (str_eq(ptype, EL_STR("Int"))) { add_int_name(pname); } + if (str_eq(ptype, EL_STR("Instant"))) { + add_instant_name(pname); + } + if (str_eq(ptype, EL_STR("Duration"))) { + add_duration_name(pname); + } + if (str_eq(ptype, EL_STR("Calendar"))) { + add_calendar_name(pname); + } + if (str_eq(ptype, EL_STR("CalendarTime"))) { + add_caltime_name(pname); + } + if (str_eq(ptype, EL_STR("Rhythm"))) { + add_rhythm_name(pname); + } + if (str_eq(ptype, EL_STR("LocalDate"))) { + add_localdate_name(pname); + } + if (str_eq(ptype, EL_STR("LocalTime"))) { + add_localtime_name(pname); + } + if (str_eq(ptype, EL_STR("LocalDateTime"))) { + add_localdt_name(pname); + } + if (str_eq(ptype, EL_STR("Zone"))) { + add_zone_name(pname); + } pi = (pi + 1); } return 1; @@ -3716,6 +5197,7 @@ el_val_t codegen(el_val_t stmts, el_val_t source) { state_set(EL_STR("__program_kind"), kind); state_set(EL_STR("__cap_violations"), EL_STR("")); state_set(EL_STR("__arity_violations"), EL_STR("")); + state_set(EL_STR("__time_violations"), EL_STR("")); emit_line(EL_STR("#include ")); emit_line(EL_STR("#include ")); emit_line(EL_STR("#include \"el_runtime.h\"")); @@ -3760,25 +5242,16 @@ el_val_t codegen(el_val_t stmts, el_val_t source) { if (has_toplevel_lets) { emit_blank(); } - /* fndef_mask[i] = 1 means stmts[i] was a FnDef and has been released. - * Subsequent passes use this mask instead of calling is_fndef() on freed - * nodes, avoiding use-after-free while still freeing each FnDef body as - * soon as cg_fn() is done with it. This is the fix for the SIGSEGV that - * occurs on large source files (>~100 KB) due to AST accumulation. */ - char* fndef_mask = calloc((size_t)n, 1); i = 0; while (i < n) { el_val_t stmt = native_list_get(stmts, i); if (is_fndef(stmt)) { - fndef_mask[i] = 1; cg_fn(stmt); - /* FnDef body is now fully emitted — release to reclaim AST memory. */ - el_release(stmt); } i = (i + 1); } - emit_line(EL_STR("int main(int argc, char** argv) {")); - emit_line(EL_STR(" el_runtime_init_args(argc, argv);")); + emit_line(EL_STR("int main(int _argc, char** _argv) {")); + emit_line(EL_STR(" el_runtime_init_args(_argc, _argv);")); if (cgi_count >= 1) { el_val_t cname = el_get_field(cgi_block, EL_STR("name")); el_val_t cdid = el_get_field(cgi_block, EL_STR("dharma_id")); @@ -3799,32 +5272,58 @@ el_val_t codegen(el_val_t stmts, el_val_t source) { el_val_t main_decl = native_list_empty(); i = 0; while (i < n) { - if (!fndef_mask[i]) { - el_val_t stmt = native_list_get(stmts, i); - kind = el_get_field(stmt, EL_STR("stmt")); - if (str_eq(kind, EL_STR("Let"))) { - el_val_t name = el_get_field(stmt, EL_STR("name")); - main_decl = native_list_append(main_decl, name); + el_val_t stmt = native_list_get(stmts, i); + kind = el_get_field(stmt, EL_STR("stmt")); + if (str_eq(kind, EL_STR("Let"))) { + el_val_t name = el_get_field(stmt, EL_STR("name")); + main_decl = native_list_append(main_decl, name); + } + i = (i + 1); + } + el_val_t el_main_body = native_list_empty(); + 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"))) { + el_val_t body = el_get_field(stmt, EL_STR("body")); + el_val_t bn = native_list_len(body); + el_val_t bi = 0; + while (bi < bn) { + el_main_body = native_list_append(el_main_body, native_list_get(body, bi)); + bi = (bi + 1); + } } } i = (i + 1); } i = 0; while (i < n) { - if (!fndef_mask[i]) { - el_val_t stmt = native_list_get(stmts, i); - if (!is_top_level_decl(stmt)) { + el_val_t stmt = native_list_get(stmts, i); + if (is_fndef(stmt)) { + } else { + if (is_top_level_decl(stmt)) { + } else { main_decl = cg_stmt(stmt, EL_STR(" "), main_decl); } } + el_release(stmt); i = (i + 1); } - free(fndef_mask); + el_val_t mn = native_list_len(el_main_body); + el_val_t mi = 0; + while (mi < mn) { + el_val_t mstmt = native_list_get(el_main_body, mi); + main_decl = cg_stmt(mstmt, EL_STR(" "), main_decl); + mi = (mi + 1); + } emit_line(EL_STR(" return 0;")); emit_line(EL_STR("}")); emit_blank(); emit_cap_violations(); emit_arity_violations(); + emit_time_violations(); return EL_STR(""); return 0; } @@ -3934,9 +5433,7 @@ 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 csv = state_get(EL_STR("__js_int_names")); if (str_eq(csv, EL_STR(""))) { - csv; - EL_NULL; - 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)) { @@ -4020,6 +5517,54 @@ el_val_t js_cg_expr(el_val_t expr) { el_val_t v = el_get_field(expr, EL_STR("value")); return v; } + if (str_eq(kind, EL_STR("DurationLit"))) { + el_val_t count = el_get_field(expr, EL_STR("count")); + el_val_t unit = el_get_field(expr, EL_STR("unit")); + el_val_t mult_ns = EL_STR("1"); + if (str_eq(unit, EL_STR("nano"))) { + mult_ns = EL_STR("1"); + } + if (str_eq(unit, EL_STR("nanos"))) { + mult_ns = EL_STR("1"); + } + if (str_eq(unit, EL_STR("milli"))) { + mult_ns = EL_STR("1000000"); + } + if (str_eq(unit, EL_STR("millis"))) { + mult_ns = EL_STR("1000000"); + } + if (str_eq(unit, EL_STR("millisecond"))) { + mult_ns = EL_STR("1000000"); + } + if (str_eq(unit, EL_STR("milliseconds"))) { + mult_ns = EL_STR("1000000"); + } + if (str_eq(unit, EL_STR("second"))) { + mult_ns = EL_STR("1000000000"); + } + if (str_eq(unit, EL_STR("seconds"))) { + mult_ns = EL_STR("1000000000"); + } + if (str_eq(unit, EL_STR("minute"))) { + mult_ns = EL_STR("60000000000"); + } + if (str_eq(unit, EL_STR("minutes"))) { + mult_ns = EL_STR("60000000000"); + } + if (str_eq(unit, EL_STR("hour"))) { + mult_ns = EL_STR("3600000000000"); + } + if (str_eq(unit, EL_STR("hours"))) { + mult_ns = EL_STR("3600000000000"); + } + if (str_eq(unit, EL_STR("day"))) { + mult_ns = EL_STR("86400000000000"); + } + if (str_eq(unit, EL_STR("days"))) { + mult_ns = EL_STR("86400000000000"); + } + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("("), count), EL_STR(" * ")), mult_ns), EL_STR(")")); + } if (str_eq(kind, EL_STR("Float"))) { el_val_t v = el_get_field(expr, EL_STR("value")); return v; @@ -4418,6 +5963,13 @@ el_val_t js_cg_stmt(el_val_t stmt, el_val_t indent, el_val_t declared) { } return declared; } + if (str_eq(kind, EL_STR("Assign"))) { + el_val_t name = el_get_field(stmt, EL_STR("name")); + el_val_t val = el_get_field(stmt, EL_STR("value")); + el_val_t val_c = js_cg_expr(val); + js_emit_line(el_str_concat(el_str_concat(el_str_concat(el_str_concat(indent, name), EL_STR(" = ")), val_c), EL_STR(";"))); + return declared; + } if (str_eq(kind, EL_STR("Expr"))) { el_val_t val = el_get_field(stmt, EL_STR("value")); el_val_t val_kind = el_get_field(val, EL_STR("expr")); @@ -4752,7 +6304,6 @@ el_val_t codegen_js(el_val_t stmts, el_val_t source) { el_val_t compile(el_val_t source) { el_val_t tokens = lex(source); el_val_t stmts = parse(tokens); - /* Token list no longer needed after parsing — release before codegen. */ el_release(tokens); return codegen(stmts, source); return 0; @@ -4761,7 +6312,6 @@ el_val_t compile(el_val_t source) { el_val_t compile_js(el_val_t source) { el_val_t tokens = lex(source); el_val_t stmts = parse(tokens); - /* Token list no longer needed after parsing — release before codegen. */ el_release(tokens); return codegen_js(stmts, source); return 0; @@ -4805,19 +6355,94 @@ el_val_t strip_flags(el_val_t argv) { return 0; } -int main(int argc, char** argv) { - el_runtime_init_args(argc, argv); - _argv = args(); - _target = detect_target(_argv); - _positional = strip_flags(_argv); - _argc = native_list_len(_positional); - if (_argc >= 1) { - el_val_t _src_path = native_list_get(_positional, 0); - el_val_t _source = fs_read(_src_path); - el_val_t _out = compile_dispatch(_target, _source); - if (_argc >= 2) { - el_val_t _out_path = native_list_get(_positional, 1); - fs_write(_out_path, _out); +el_val_t dirname_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, 0, i); + } + i = (i - 1); + } + return EL_STR("."); + return 0; +} + +el_val_t parse_import_line(el_val_t trimmed, el_val_t dir) { + if (str_starts_with(trimmed, EL_STR("import \""))) { + el_val_t after = str_slice(trimmed, 8, str_len(trimmed)); + el_val_t q = str_index_of(after, EL_STR("\"")); + if (q > 0) { + el_val_t mod = str_slice(after, 0, q); + return el_str_concat(el_str_concat(dir, EL_STR("/")), mod); + } + } + if (str_starts_with(trimmed, EL_STR("from "))) { + el_val_t after = str_slice(trimmed, 5, str_len(trimmed)); + el_val_t sp = str_index_of(after, EL_STR(" ")); + if (sp > 0) { + el_val_t mod_raw = str_slice(after, 0, sp); + el_val_t mod = str_trim(mod_raw); + if (!str_eq(mod, EL_STR(""))) { + return el_str_concat(el_str_concat(el_str_concat(dir, EL_STR("/")), mod), EL_STR(".el")); + } + } + } + return EL_STR(""); + return 0; +} + +el_val_t resolve_imports(el_val_t src_path) { + el_val_t seen_key = el_str_concat(EL_STR("__elc_imp__:"), src_path); + el_val_t already = state_get(seen_key); + if (!str_eq(already, EL_STR(""))) { + return EL_STR(""); + } + state_set(seen_key, EL_STR("1")); + el_val_t source = fs_read(src_path); + el_val_t dir = dirname_of(src_path); + el_val_t lines = str_split(source, EL_STR("\n")); + el_val_t n = native_list_len(lines); + el_val_t prefix = EL_STR(""); + el_val_t body = EL_STR(""); + el_val_t i = 0; + while (i < n) { + el_val_t line = native_list_get(lines, i); + el_val_t trimmed = str_trim(line); + el_val_t imp_path = parse_import_line(trimmed, dir); + if (!str_eq(imp_path, EL_STR(""))) { + prefix = el_str_concat(prefix, resolve_imports(imp_path)); + } else { + body = el_str_concat(el_str_concat(body, line), EL_STR("\n")); + } + i = (i + 1); + } + return el_str_concat(prefix, body); + 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 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] []")); + exit(1); + } + el_val_t src_path = native_list_get(positional, 0); + el_val_t source = resolve_imports(src_path); + el_val_t 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); + if (ok) { + exit(0); + } else { + println(EL_STR("el-compiler: failed to write output")); + exit(1); } } return 0; diff --git a/el-compiler/runtime/el_runtime.c b/el-compiler/runtime/el_runtime.c index 2a3beaf..c54ddc6 100644 --- a/el-compiler/runtime/el_runtime.c +++ b/el-compiler/runtime/el_runtime.c @@ -1469,22 +1469,25 @@ void http_serve(el_val_t port, el_val_t handler) { } int p = (int)port; if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return; } - int sock = socket(AF_INET, SOCK_STREAM, 0); + /* Dual-stack: AF_INET6 with IPV6_V6ONLY=0 accepts both IPv4 and IPv6. + * This makes `localhost` work in browsers that resolve it to ::1 first. */ + int sock = socket(AF_INET6, SOCK_STREAM, 0); if (sock < 0) { perror("socket"); return; } - int yes = 1; + int yes = 1; int no = 0; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); - struct sockaddr_in addr; + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_ANY); - addr.sin_port = htons((uint16_t)p); + addr.sin6_family = AF_INET6; + addr.sin6_addr = in6addr_any; + addr.sin6_port = htons((uint16_t)p); if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); close(sock); return; } if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; } - fprintf(stderr, "[http] listening on 0.0.0.0:%d\n", p); + fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p); while (1) { - struct sockaddr_in cli; + struct sockaddr_in6 cli; socklen_t clen = sizeof(cli); int cfd = accept(sock, (struct sockaddr*)&cli, &clen); if (cfd < 0) { @@ -1715,22 +1718,24 @@ void http_serve_v2(el_val_t port, el_val_t handler) { fprintf(stderr, "http_serve_v2: invalid port %d\n", p); return; } - int sock = socket(AF_INET, SOCK_STREAM, 0); + /* Dual-stack: same as http_serve - AF_INET6 + IPV6_V6ONLY=0. */ + int sock = socket(AF_INET6, SOCK_STREAM, 0); if (sock < 0) { perror("socket"); return; } - int yes = 1; + int yes = 1; int no = 0; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); - struct sockaddr_in addr; + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_ANY); - addr.sin_port = htons((uint16_t)p); + addr.sin6_family = AF_INET6; + addr.sin6_addr = in6addr_any; + addr.sin6_port = htons((uint16_t)p); if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); close(sock); return; } if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; } - fprintf(stderr, "[http v2] listening on 0.0.0.0:%d\n", p); + fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p); while (1) { - struct sockaddr_in cli; + struct sockaddr_in6 cli; socklen_t clen = sizeof(cli); int cfd = accept(sock, (struct sockaddr*)&cli, &clen); if (cfd < 0) { diff --git a/elc-combined.el b/elc-combined.el index 5eb8c09..3216641 100644 --- a/elc-combined.el +++ b/elc-combined.el @@ -195,6 +195,208 @@ fn scan_ident(chars: [String], start: Int, total: Int) -> Map { { "text": text, "pos": i } } +// ── Code-bearing string detection + comment strip ──────────────────────────── +// Inline JS/CSS literals embedded in El source (e.g. blobs +// or stylesheet payloads inside string literals) carry their own line and +// block comments. Those comments leak into the served HTML and reveal build +// notes the visitor should never see. We strip them at the lexer so every +// downstream consumer (codegen-c, codegen-js, parser) gets the cleaned form. +// +// looks_like_code — heuristic gate so we only strip strings that actually +// embed JS or CSS. Plain prose, hex blobs, JSON, etc. pass through verbatim. + +fn substr_at(chars: [String], start: Int, total: Int, needle: String) -> Bool { + let nchars: [String] = native_string_chars(needle) + let nlen: Int = native_list_len(nchars) + if start + nlen > total { return false } + let i = 0 + let matched = true + while i < nlen { + let a: String = native_list_get(chars, start + i) + let b: String = native_list_get(nchars, i) + if a == b { let i = i + 1 } else { let matched = false; let i = nlen } + } + matched +} + +fn str_has(s: String, needle: String) -> Bool { + let chars: [String] = native_string_chars(s) + let total: Int = native_list_len(chars) + let i = 0 + let found = false + while i < total { + if substr_at(chars, i, total, needle) { + let found = true + let i = total + } else { + let i = i + 1 + } + } + found +} + +fn looks_like_code(s: String) -> Bool { + if str_has(s, " String { + let chars: [String] = native_string_chars(s) + let total: Int = native_list_len(chars) + let out = "" + let i = 0 + let in_squote = false + let in_dquote = false + let in_btick = false + let prev = "" + while i < total { + let ch: String = native_list_get(chars, i) + let in_js_string = false + if in_squote { let in_js_string = true } + if in_dquote { let in_js_string = true } + if in_btick { let in_js_string = true } + + if in_js_string { + // Backslash escape: consume next char verbatim regardless of which. + if ch == "\\" { + let out = out + ch + let next_i = i + 1 + if next_i < total { + let nc: String = native_list_get(chars, next_i) + let out = out + nc + let prev = nc + let i = next_i + 1 + } else { + let prev = ch + let i = next_i + } + } else { + if in_squote { + if ch == "'" { let in_squote = false } + } else { + if in_dquote { + if ch == "\"" { let in_dquote = false } + } else { + if in_btick { + if ch == "`" { let in_btick = false } + } + } + } + let out = out + ch + let prev = ch + let i = i + 1 + } + } else { + // Not in a JS string. Check for comment openers. + let next_i = i + 1 + let next_ch = "" + if next_i < total { + let next_ch: String = native_list_get(chars, next_i) + } + + if ch == "/" { + if next_ch == "/" { + // URL guard: prev char ':' means this is "://", not a comment. + if prev == ":" { + let out = out + ch + let prev = ch + let i = i + 1 + } else { + // Skip until newline (newline itself is preserved so + // surrounding line counts/structure stay sane). + let i = i + 2 + let scanning = true + while scanning { + if i >= total { + let scanning = false + } else { + let lc: String = native_list_get(chars, i) + if lc == "\n" { + let scanning = false + } else { + let i = i + 1 + } + } + } + let prev = "" + } + } else { + if next_ch == "*" { + // Skip until matching "*/". + let i = i + 2 + let scanning2 = true + while scanning2 { + if i >= total { + let scanning2 = false + } else { + let bc: String = native_list_get(chars, i) + if bc == "*" { + let after = i + 1 + if after < total { + let nc2: String = native_list_get(chars, after) + if nc2 == "/" { + let i = after + 1 + let scanning2 = false + } else { + let i = i + 1 + } + } else { + let i = i + 1 + } + } else { + let i = i + 1 + } + } + } + let prev = "" + } else { + let out = out + ch + let prev = ch + let i = i + 1 + } + } + } else { + // Open a JS string? + if ch == "'" { + let in_squote = true + let out = out + ch + let prev = ch + let i = i + 1 + } else { + if ch == "\"" { + let in_dquote = true + let out = out + ch + let prev = ch + let i = i + 1 + } else { + if ch == "`" { + let in_btick = true + let out = out + ch + let prev = ch + let i = i + 1 + } else { + let out = out + ch + let prev = ch + let i = i + 1 + } + } + } + } + } + } + out +} + // 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 { @@ -305,7 +507,15 @@ fn lex(source: String) -> [Map] { let result = scan_string(chars, i + 1, total) let str_text: String = result["text"] let new_pos: Int = result["pos"] - let tokens = native_list_append(tokens, make_tok("Str", str_text)) + // Compile-time scrub: strings that embed JS or CSS get + // their // line comments and /* block comments stripped + // before the token reaches the parser. Plain prose passes + // through untouched. + let clean_text = str_text + if looks_like_code(str_text) { + let clean_text = strip_code_comments(str_text) + } + let tokens = native_list_append(tokens, make_tok("Str", clean_text)) let i = new_pos } else { // Number literal @@ -1010,10 +1220,55 @@ fn parse_block(tokens: [Map], pos: Int) -> Map { // ── Postfix expressions (calls, field access, index) ───────────────────────── +// is_duration_unit — recognise the postfix unit suffix on a numeric literal. +// Used by parse_postfix to detect `30.seconds`-shape time literals before +// falling back to the generic `obj.field` field-access lowering. Singular +// and plural forms map to the same nanosecond multiplier; codegen does the +// arithmetic at compile time. +fn is_duration_unit(name: String) -> Bool { + if name == "nanos" { return true } + if name == "nano" { return true } + if name == "millis" { return true } + if name == "milli" { return true } + if name == "millisecond" { return true } + if name == "milliseconds" { return true } + if name == "second" { return true } + if name == "seconds" { return true } + if name == "minute" { return true } + if name == "minutes" { return true } + if name == "hour" { return true } + if name == "hours" { return true } + if name == "day" { return true } + if name == "days" { return true } + false +} + fn parse_postfix(tokens: [Map], pos: Int) -> Map { let r = parse_primary(tokens, pos) let node = r["node"] let p = r["pos"] + + // Postfix duration literal: `.` where is one of + // nanos | millis | seconds | minutes | hours | days (each with an + // optional plural). We recognise this before the generic Dot-as-field + // path so `30.seconds` lowers to a DurationLit AST node carrying the + // count and the unit, not a field access on an Int. + let primary_kind: String = node["expr"] + if primary_kind == "Int" { + let dot_kind = tok_kind(tokens, p) + if dot_kind == "Dot" { + let unit_kind = tok_kind(tokens, p + 1) + if unit_kind == "Ident" { + let unit_name = tok_value(tokens, p + 1) + if is_duration_unit(unit_name) { + let count_str: String = node["value"] + let node = { "expr": "DurationLit", "count": count_str, "unit": unit_name } + let p = p + 2 + } + } + } + } + let running = true while running { let k = tok_kind(tokens, p) @@ -1482,6 +1737,24 @@ fn parse_stmt(tokens: [Map], pos: Int) -> Map { }, p) } + // Bare reassignment: `name = expr`. Handled BEFORE the expression + // fallback so we don't drop the assign on the floor and emit three + // orphan expressions (the original silent-miscompile bug). El's `let` + // already permits redeclaration, so this only applies when the parser + // sees an Ident followed directly by `=`. `==` is a separate kind + // (EqEq) so there's no ambiguity. + if k == "Ident" { + let k2 = tok_kind(tokens, pos + 1) + if k2 == "Eq" { + let name = tok_value(tokens, pos) + let p = pos + 2 + let r = parse_expr(tokens, p) + let val = r["node"] + let p = r["pos"] + return make_result({ "stmt": "Assign", "name": name, "value": val }, p) + } + } + // bare expression or if/match statement let r = parse_expr(tokens, pos) let val = r["node"] @@ -1620,6 +1893,29 @@ fn binop_to_c(op: String) -> String { // // cg_expr returns a C expression string (not a statement). +// duration_unit_nanos — multiplier from a postfix-literal unit name to +// nanoseconds. Singular and plural forms collapse to the same multiplier; +// the parser already restricted `unit` to the set is_duration_unit accepts. +// Returns the multiplier as a decimal string suitable for splicing into +// the generated C as a literal int64 expression. +fn duration_unit_nanos(unit: String) -> String { + if str_eq(unit, "nano") { return "1LL" } + if str_eq(unit, "nanos") { return "1LL" } + if str_eq(unit, "milli") { return "1000000LL" } + if str_eq(unit, "millis") { return "1000000LL" } + if str_eq(unit, "millisecond") { return "1000000LL" } + if str_eq(unit, "milliseconds") { return "1000000LL" } + if str_eq(unit, "second") { return "1000000000LL" } + if str_eq(unit, "seconds") { return "1000000000LL" } + if str_eq(unit, "minute") { return "60000000000LL" } + if str_eq(unit, "minutes") { return "60000000000LL" } + if str_eq(unit, "hour") { return "3600000000000LL" } + if str_eq(unit, "hours") { return "3600000000000LL" } + if str_eq(unit, "day") { return "86400000000000LL" } + if str_eq(unit, "days") { return "86400000000000LL" } + "1LL" +} + fn cg_expr(expr: Map) -> String { let kind: String = expr["expr"] @@ -1628,6 +1924,17 @@ fn cg_expr(expr: Map) -> String { return v } + // DurationLit — postfix-literal time value (e.g. 30.seconds, 1.hour). + // Lowered to a literal int64 nanosecond count, wrapped in the runtime + // entry point so the intent is explicit at the C level. The arithmetic + // is fully constant-folded by any optimising C compiler. + if kind == "DurationLit" { + let count: String = expr["count"] + let unit: String = expr["unit"] + let mult: String = duration_unit_nanos(unit) + return "el_duration_from_nanos((el_val_t)(" + count + "LL * " + mult + "))" + } + if kind == "Float" { // Wrap Float literals in el_from_float() so the bit pattern is // preserved through the el_val_t (int64) slot. Without this, @@ -1678,6 +1985,198 @@ 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) ──────── + // Run BEFORE the int / string / generic paths so typed temporal + // operands route through the runtime wrappers and invalid combos + // become #error directives rather than silently falling through to + // raw int arithmetic. The wrappers are no-op casts at the C level + // but make the intent explicit and centralise future changes (e.g. + // saturating arithmetic, overflow guards). + let left_is_inst: Bool = is_instant_expr(left) + let right_is_inst: Bool = is_instant_expr(right) + let left_is_dur: Bool = is_duration_expr(left) + let right_is_dur: Bool = is_duration_expr(right) + + // Phase 1.5 LocalDate / LocalTime / CalendarTime dispatch. These + // route through their typed runtime wrappers (el_local_date_add_dur, + // el_local_time_add_dur, el_local_date_lt, el_local_date_eq) and + // forbid mismatched ops at codegen time. Cross-calendar arithmetic + // (CalendarTime + CalendarTime, CalendarTime - CalendarTime under + // mismatched calendars) is structurally meaningless: a CalendarTime + // already projects an Instant under a Calendar, so subtraction + // between two of them only makes sense in instant-space (use + // cal_to_instant first). + let left_is_ld: Bool = is_localdate_expr(left) + let right_is_ld: Bool = is_localdate_expr(right) + let left_is_lt: Bool = is_localtime_expr(left) + let right_is_lt: Bool = is_localtime_expr(right) + let left_is_ct: Bool = is_caltime_expr(left) + let right_is_ct: Bool = is_caltime_expr(right) + if left_is_ld { + if op == "Plus" { + if right_is_dur { + return "el_local_date_add_dur(" + left_c + ", " + right_c + ")" + } + } + if op == "Lt" { + if right_is_ld { return "el_local_date_lt(" + left_c + ", " + right_c + ")" } + } + if op == "EqEq" { + if right_is_ld { return "el_local_date_eq(" + left_c + ", " + right_c + ")" } + } + } + if left_is_lt { + if op == "Plus" { + if right_is_dur { + return "el_local_time_add_dur(" + left_c + ", " + right_c + ")" + } + } + } + if left_is_ct { + if op == "Plus" { + if right_is_ct { + time_record_violation("caltime_plus_caltime", "CalendarTime + CalendarTime is not allowed (use cal_to_instant + Duration)") + return "0 /* TIME_TYPE_ERROR: CalendarTime + CalendarTime */" + } + } + } + + let any_temporal: Bool = false + if left_is_inst { let any_temporal = true } + if right_is_inst { let any_temporal = true } + if left_is_dur { let any_temporal = true } + if right_is_dur { let any_temporal = true } + if any_temporal { + if op == "Plus" { + if left_is_inst { + if right_is_dur { + return "el_instant_add_dur(" + left_c + ", " + right_c + ")" + } + if right_is_inst { + time_record_violation("instant_plus_instant", "Instant + Instant is not allowed") + return "0 /* TIME_TYPE_ERROR: Instant + Instant */" + } + } + if left_is_dur { + if right_is_inst { + return "el_instant_add_dur(" + right_c + ", " + left_c + ")" + } + if right_is_dur { + return "el_duration_add(" + left_c + ", " + right_c + ")" + } + if is_int_expr(right) { + time_record_violation("duration_plus_int", "Duration + Int is not allowed (use duration_seconds(n) or N.seconds)") + return "0 /* TIME_TYPE_ERROR: Duration + Int */" + } + } + if right_is_dur { + if is_int_expr(left) { + time_record_violation("duration_plus_int", "Int + Duration is not allowed") + return "0 /* TIME_TYPE_ERROR: Int + Duration */" + } + } + } + if op == "Minus" { + if left_is_inst { + if right_is_dur { + return "el_instant_sub_dur(" + left_c + ", " + right_c + ")" + } + if right_is_inst { + return "el_instant_diff(" + left_c + ", " + right_c + ")" + } + } + if left_is_dur { + if right_is_dur { + return "el_duration_sub(" + left_c + ", " + right_c + ")" + } + if is_int_expr(right) { + time_record_violation("duration_minus_int", "Duration - Int is not allowed") + return "0 /* TIME_TYPE_ERROR: Duration - Int */" + } + } + } + if op == "Star" { + if left_is_dur { + if is_int_expr(right) { + return "el_duration_scale(" + left_c + ", " + right_c + ")" + } + } + if right_is_dur { + if is_int_expr(left) { + return "el_duration_scale(" + right_c + ", " + left_c + ")" + } + } + } + if op == "Slash" { + if left_is_dur { + if is_int_expr(right) { + return "el_duration_div(" + left_c + ", " + right_c + ")" + } + } + } + // Comparisons. Cross-type comparisons are forbidden. + if op == "Lt" { + if left_is_inst { + if right_is_inst { return "el_instant_lt(" + left_c + ", " + right_c + ")" } + if right_is_dur { + time_record_violation("instant_cmp_duration", "Instant < Duration is not allowed") + return "0 /* TIME_TYPE_ERROR: Instant < Duration */" + } + } + if left_is_dur { + if right_is_dur { return "el_duration_lt(" + left_c + ", " + right_c + ")" } + if right_is_inst { + time_record_violation("duration_cmp_instant", "Duration < Instant is not allowed") + return "0 /* TIME_TYPE_ERROR: Duration < Instant */" + } + } + } + if op == "LtEq" { + if left_is_inst { + if right_is_inst { return "el_instant_le(" + left_c + ", " + right_c + ")" } + } + if left_is_dur { + if right_is_dur { return "el_duration_le(" + left_c + ", " + right_c + ")" } + } + } + if op == "Gt" { + if left_is_inst { + if right_is_inst { return "el_instant_gt(" + left_c + ", " + right_c + ")" } + } + if left_is_dur { + if right_is_dur { return "el_duration_gt(" + left_c + ", " + right_c + ")" } + } + } + if op == "GtEq" { + if left_is_inst { + if right_is_inst { return "el_instant_ge(" + left_c + ", " + right_c + ")" } + } + if left_is_dur { + if right_is_dur { return "el_duration_ge(" + left_c + ", " + right_c + ")" } + } + } + if op == "EqEq" { + if left_is_inst { + if right_is_inst { return "el_instant_eq(" + left_c + ", " + right_c + ")" } + } + if left_is_dur { + if right_is_dur { return "el_duration_eq(" + left_c + ", " + right_c + ")" } + } + } + if op == "NotEq" { + if left_is_inst { + if right_is_inst { return "el_instant_ne(" + left_c + ", " + right_c + ")" } + } + if left_is_dur { + if right_is_dur { return "el_duration_ne(" + left_c + ", " + right_c + ")" } + } + } + // Fall through — let the existing path handle anything we + // didn't explicitly cover (typically string-concat with a + // typed temporal value, e.g. for debug prints, which works + // because both share the int64 slot). + } + if op == "Plus" { // If either side is a string literal, always concat if left_kind == "Str" { @@ -1886,6 +2385,21 @@ fn cg_expr(expr: Map) -> String { // handler)`). User-defined fns and variadic builtins pass // through (builtin_arity returns -1). arity_check_call(fn_name, arity) + // sleep(Duration) — Phase 1 of the typed-time work. When the + // single arg is provably a Duration we lower to el_sleep_duration + // so the runtime sees nanos directly. Existing sleep() callers + // that pass an Int still emit `sleep()`, which falls through + // to the no-such-symbol path — those call sites must migrate to + // a typed Duration. Acceptable: the spec marks them out for an + // audit pass during Phase 1. + if str_eq(fn_name, "sleep") { + if arity == 1 { + let only_arg = native_list_get(args, 0) + if is_duration_expr(only_arg) { + return "el_sleep_duration(" + args_c + ")" + } + } + } return fn_name + "(" + args_c + ")" } @@ -2116,9 +2630,20 @@ fn cg_if_expr_arm(stmts: [Map], result_var: String) -> String { let out = out + "(void)(" + val_c + "); " } } else { - // Non-trivial stmt kinds (While/For) shouldn't appear in - // expression-position arm bodies; emit nothing rather - // than malformed C. + if str_eq(sk, "Assign") { + // Real reassignment in an expression-position arm — + // emit the store; the arm's "value" stays whatever + // result_var was last set to, which is the El + // semantics (assignment is a statement, not a value). + let aname: String = s["name"] + let aval = s["value"] + let aval_c: String = cg_expr(aval) + let out = out + aname + " = " + aval_c + "; " + } else { + // Non-trivial stmt kinds (While/For) shouldn't appear in + // expression-position arm bodies; emit nothing rather + // than malformed C. + } } } } @@ -2182,6 +2707,65 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin if str_eq(ltype, "Int") { add_int_name(name) } + // Temporal type annotations register the name with the matching + // typed-set so BinOp / comparison codegen routes through the + // typed wrappers and forbids cross-type ops. + if str_eq(ltype, "Instant") { + add_instant_name(name) + } + if str_eq(ltype, "Duration") { + add_duration_name(name) + } + if str_eq(ltype, "Calendar") { + add_calendar_name(name) + } + if str_eq(ltype, "CalendarTime") { + add_caltime_name(name) + } + if str_eq(ltype, "Rhythm") { + add_rhythm_name(name) + } + if str_eq(ltype, "LocalDate") { + add_localdate_name(name) + } + if str_eq(ltype, "LocalTime") { + add_localtime_name(name) + } + if str_eq(ltype, "LocalDateTime") { + add_localdt_name(name) + } + if str_eq(ltype, "Zone") { + add_zone_name(name) + } + // Inference from RHS — duration literals and known-typed calls + // propagate even when the let is unannotated. + if is_instant_expr(val) { + add_instant_name(name) + } + if is_duration_expr(val) { + add_duration_name(name) + } + if is_calendar_expr(val) { + add_calendar_name(name) + } + if is_caltime_expr(val) { + add_caltime_name(name) + } + if is_rhythm_expr(val) { + add_rhythm_name(name) + } + if is_localdate_expr(val) { + add_localdate_name(name) + } + if is_localtime_expr(val) { + add_localtime_name(name) + } + if is_localdt_expr(val) { + add_localdt_name(name) + } + if is_zone_expr(val) { + add_zone_name(name) + } let vk: String = val["expr"] if str_eq(vk, "Int") { add_int_name(name) @@ -2207,6 +2791,20 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin return declared } + // Bare reassignment: `name = expr`. Always emits a plain C assignment + // (no `el_val_t` prefix) — by construction the parser only produces + // Assign for an existing identifier. If the name happens NOT to be in + // `declared` for the current C scope (it was let-bound by an enclosing + // block) the emit still resolves at C level because the variable lives + // in the surrounding scope. + if kind == "Assign" { + let name: String = stmt["name"] + let val = stmt["value"] + let val_c: String = cg_expr(val) + emit_line(indent + name + " = " + val_c + ";") + return declared + } + if kind == "Expr" { let val = stmt["value"] let val_kind: String = val["expr"] @@ -2424,6 +3022,23 @@ fn is_int_name(name: String) -> Bool { return str_contains(csv, "," + name + ",") } +// Same shape as is_int_name, for Instant- and Duration-typed bindings. +// Used by the BinOp/comparison codegen to dispatch arithmetic through the +// typed runtime wrappers (el_instant_add_dur, el_duration_lt, …) and to +// surface mismatches (Instant + Instant, Duration + Int) as #error +// directives at the top of the generated C. +fn is_instant_name(name: String) -> Bool { + let csv: String = state_get("__instant_names") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + +fn is_duration_name(name: String) -> Bool { + let csv: String = state_get("__duration_names") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + // Known runtime builtins that return Int. Used to dispatch arithmetic vs // string-concat on `+` when one side is a Call. New builtins must be added // here when they return Int and may participate in arithmetic. @@ -2461,9 +3076,346 @@ fn is_int_call(call_expr: Map) -> Bool { if str_eq(name, "el_max") { return true } if str_eq(name, "el_min") { return true } if str_eq(name, "float_to_int") { return true } + if str_eq(name, "unix_timestamp") { return true } + if str_eq(name, "instant_to_unix_seconds") { return true } + if str_eq(name, "instant_to_unix_millis") { return true } + if str_eq(name, "duration_to_seconds") { return true } + if str_eq(name, "duration_to_millis") { return true } + if str_eq(name, "duration_to_nanos") { return true } return false } +// Builtins that return an Instant. Used by is_instant_expr and the BinOp +// dispatch — `now() + 5.seconds` types as Instant only because we can see +// that now() is an Instant-returning Call. +fn is_instant_call(call_expr: Map) -> Bool { + let func = call_expr["func"] + let fk: String = func["expr"] + if !str_eq(fk, "Ident") { return false } + let name: String = func["name"] + if str_eq(name, "now") { return true } + if str_eq(name, "el_now_instant") { return true } + if str_eq(name, "unix_seconds") { return true } + if str_eq(name, "unix_millis") { return true } + if str_eq(name, "instant_from_iso8601") { return true } + if str_eq(name, "el_instant_add_dur") { return true } + if str_eq(name, "el_instant_sub_dur") { return true } + return false +} + +// Builtins that return a Duration. Same role as is_instant_call. +fn is_duration_call(call_expr: Map) -> Bool { + let func = call_expr["func"] + let fk: String = func["expr"] + if !str_eq(fk, "Ident") { return false } + let name: String = func["name"] + if str_eq(name, "el_duration_from_nanos") { return true } + if str_eq(name, "duration_seconds") { return true } + if str_eq(name, "duration_millis") { return true } + if str_eq(name, "duration_nanos") { return true } + if str_eq(name, "el_instant_diff") { return true } + if str_eq(name, "el_duration_add") { return true } + if str_eq(name, "el_duration_sub") { return true } + if str_eq(name, "el_duration_scale") { return true } + if str_eq(name, "el_duration_div") { return true } + if str_eq(name, "ttl_cache_age") { return true } + return false +} + +// Phase 1.5 — Calendar / CalendarTime / Rhythm / LocalDate / LocalTime / +// LocalDateTime / Zone are first-class boxed types. Each has its own name +// set in process state, populated from typed `let` bindings and parameter +// annotations. The BinOp dispatcher consults these to forbid mismatched +// arithmetic (e.g. CalendarTime + CalendarTime, LocalDate < CalendarTime). +fn is_calendar_name(name: String) -> Bool { + let csv: String = state_get("__calendar_names") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + +fn is_caltime_name(name: String) -> Bool { + let csv: String = state_get("__caltime_names") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + +fn is_rhythm_name(name: String) -> Bool { + let csv: String = state_get("__rhythm_names") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + +fn is_localdate_name(name: String) -> Bool { + let csv: String = state_get("__localdate_names") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + +fn is_localtime_name(name: String) -> Bool { + let csv: String = state_get("__localtime_names") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + +fn is_localdt_name(name: String) -> Bool { + let csv: String = state_get("__localdt_names") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + +fn is_zone_name(name: String) -> Bool { + let csv: String = state_get("__zone_names") + if str_eq(csv, "") { return false } + return str_contains(csv, "," + name + ",") +} + +// Calendar-returning builtins. earth_calendar / mars_calendar / cycle_calendar +// / no_cycle_calendar / relative_calendar all box a calendar struct. +fn is_calendar_call(call_expr: Map) -> Bool { + let func = call_expr["func"] + let fk: String = func["expr"] + if !str_eq(fk, "Ident") { return false } + let name: String = func["name"] + if str_eq(name, "earth_calendar") { return true } + if str_eq(name, "earth_calendar_default") { return true } + if str_eq(name, "mars_calendar") { return true } + if str_eq(name, "cycle_calendar") { return true } + if str_eq(name, "no_cycle_calendar") { return true } + if str_eq(name, "relative_calendar") { return true } + return false +} + +// CalendarTime-returning builtins. +fn is_caltime_call(call_expr: Map) -> Bool { + let func = call_expr["func"] + let fk: String = func["expr"] + if !str_eq(fk, "Ident") { return false } + let name: String = func["name"] + if str_eq(name, "now_in") { return true } + if str_eq(name, "in_calendar") { return true } + if str_eq(name, "cal_in") { return true } + if str_eq(name, "zoned") { return true } + return false +} + +// Rhythm-returning builtins. +fn is_rhythm_call(call_expr: Map) -> Bool { + let func = call_expr["func"] + let fk: String = func["expr"] + if !str_eq(fk, "Ident") { return false } + let name: String = func["name"] + if str_eq(name, "rhythm_cycle_start") { return true } + if str_eq(name, "rhythm_cycle_phase") { return true } + if str_eq(name, "rhythm_duration") { return true } + if str_eq(name, "rhythm_session_start") { return true } + if str_eq(name, "rhythm_event") { return true } + if str_eq(name, "rhythm_and") { return true } + if str_eq(name, "rhythm_or") { return true } + if str_eq(name, "rhythm_weekday") { return true } + if str_eq(name, "rhythm_weekly_at") { return true } + return false +} + +// LocalDate-returning builtins. +fn is_localdate_call(call_expr: Map) -> Bool { + let func = call_expr["func"] + let fk: String = func["expr"] + if !str_eq(fk, "Ident") { return false } + let name: String = func["name"] + if str_eq(name, "local_date") { return true } + if str_eq(name, "el_local_date_add_dur") { return true } + return false +} + +fn is_localtime_call(call_expr: Map) -> Bool { + let func = call_expr["func"] + let fk: String = func["expr"] + if !str_eq(fk, "Ident") { return false } + let name: String = func["name"] + if str_eq(name, "local_time") { return true } + if str_eq(name, "el_local_time_add_dur") { return true } + return false +} + +fn is_localdt_call(call_expr: Map) -> Bool { + let func = call_expr["func"] + let fk: String = func["expr"] + if !str_eq(fk, "Ident") { return false } + let name: String = func["name"] + if str_eq(name, "local_datetime") { return true } + return false +} + +fn is_zone_call(call_expr: Map) -> Bool { + let func = call_expr["func"] + let fk: String = func["expr"] + if !str_eq(fk, "Ident") { return false } + let name: String = func["name"] + if str_eq(name, "zone") { return true } + if str_eq(name, "zone_utc") { return true } + if str_eq(name, "zone_local") { return true } + if str_eq(name, "zone_offset") { return true } + return false +} + +fn is_calendar_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "Ident") { return is_calendar_name(expr["name"]) } + if str_eq(k, "Call") { return is_calendar_call(expr) } + return false +} + +fn is_caltime_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "Ident") { return is_caltime_name(expr["name"]) } + if str_eq(k, "Call") { return is_caltime_call(expr) } + return false +} + +fn is_rhythm_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "Ident") { return is_rhythm_name(expr["name"]) } + if str_eq(k, "Call") { return is_rhythm_call(expr) } + return false +} + +fn is_localdate_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "Ident") { return is_localdate_name(expr["name"]) } + if str_eq(k, "Call") { return is_localdate_call(expr) } + return false +} + +fn is_localtime_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "Ident") { return is_localtime_name(expr["name"]) } + if str_eq(k, "Call") { return is_localtime_call(expr) } + return false +} + +fn is_localdt_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "Ident") { return is_localdt_name(expr["name"]) } + if str_eq(k, "Call") { return is_localdt_call(expr) } + return false +} + +fn is_zone_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "Ident") { return is_zone_name(expr["name"]) } + if str_eq(k, "Call") { return is_zone_call(expr) } + return false +} + +// Recursive type predicates for Instant / Duration. Mirror is_int_expr. +// is_instant_expr / is_duration_expr return true only when the expression +// is provably of that type at codegen time. Anything ambiguous returns +// false — the BinOp dispatcher then leaves the expression on the +// untyped-int path, which is the safest fallback because at the runtime +// level all three types share the int64 slot. +fn is_instant_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "Ident") { + let name: String = expr["name"] + return is_instant_name(name) + } + if str_eq(k, "Call") { + return is_instant_call(expr) + } + if str_eq(k, "BinOp") { + let op: String = expr["op"] + if str_eq(op, "Plus") { + // Instant + Duration → Instant + // Duration + Instant → Instant + if is_instant_expr(expr["left"]) { + if is_duration_expr(expr["right"]) { return true } + } + if is_duration_expr(expr["left"]) { + if is_instant_expr(expr["right"]) { return true } + } + return false + } + if str_eq(op, "Minus") { + // Instant - Duration → Instant + if is_instant_expr(expr["left"]) { + if is_duration_expr(expr["right"]) { return true } + } + return false + } + return false + } + return false +} + +fn is_duration_expr(expr: Map) -> Bool { + let k: String = expr["expr"] + if str_eq(k, "DurationLit") { return true } + if str_eq(k, "Ident") { + let name: String = expr["name"] + return is_duration_name(name) + } + if str_eq(k, "Call") { + return is_duration_call(expr) + } + if str_eq(k, "Neg") { + return is_duration_expr(expr["inner"]) + } + if str_eq(k, "BinOp") { + let op: String = expr["op"] + if str_eq(op, "Plus") { + // Duration + Duration → Duration + if is_duration_expr(expr["left"]) { + if is_duration_expr(expr["right"]) { return true } + } + return false + } + if str_eq(op, "Minus") { + // Duration - Duration → Duration + // Instant - Instant → Duration (caught here, not in is_instant_expr) + if is_duration_expr(expr["left"]) { + if is_duration_expr(expr["right"]) { return true } + } + if is_instant_expr(expr["left"]) { + if is_instant_expr(expr["right"]) { return true } + } + return false + } + if str_eq(op, "Star") { + // Duration * Int → Duration + // Int * Duration → Duration + if is_duration_expr(expr["left"]) { + if is_int_expr(expr["right"]) { return true } + } + if is_int_expr(expr["left"]) { + if is_duration_expr(expr["right"]) { return true } + } + return false + } + if str_eq(op, "Slash") { + // Duration / Int → Duration + if is_duration_expr(expr["left"]) { + if is_int_expr(expr["right"]) { return true } + } + return false + } + return false + } + return false +} + +// Record a temporal-type violation. Surfaced as `#error` directives at the +// top of the generated C, identical machinery to cap_record_violation. +// kinds: "instant_plus_instant", "duration_plus_int", etc. +fn time_record_violation(kind: String, detail: String) -> Bool { + let csv: String = state_get("__time_violations") + if str_eq(csv, "") { let csv = "," } + let entry: String = kind + ":" + detail + let key: String = "," + entry + "," + if str_contains(csv, key) { return true } + state_set("__time_violations", csv + entry + ",") + return true +} + // Recursive type-propagation: is `expr` known-Int at codegen time? // This unifies the BinOp(+) dispatch so chained arithmetic over Int // operands stays arithmetic. Without recursion, a wrapping `+` between @@ -2643,6 +3595,30 @@ fn emit_cap_violations() -> Void { } } +// Surface temporal-type violations as #error directives. The cg_expr BinOp +// dispatcher records each violation (Instant + Instant, Duration + Int, …) +// as a CSV entry "kind:detail" via time_record_violation. Each entry maps +// to a single #error so downstream cc fails the build with a clear El- +// source-level message before the bogus C even links. +fn emit_time_violations() -> Void { + let csv: String = state_get("__time_violations") + if str_eq(csv, "") { return } + if str_eq(csv, ",") { return } + let n: Int = str_len(csv) + let i: Int = 1 + while i < n { + let next_comma: Int = str_index_of(str_slice(csv, i, n), ",") + if next_comma < 0 { return } + let entry: String = str_slice(csv, i, i + next_comma) + let colon: Int = str_index_of(entry, ":") + if colon > 0 { + let detail: String = str_slice(entry, colon + 1, str_len(entry)) + emit_line("#error \"temporal type error: " + detail + "\"") + } + let i = i + next_comma + 1 + } +} + // ── Builtin arity table ─────────────────────────────────────────────────────── // // El programs sometimes call runtime builtins with the wrong number of @@ -2714,6 +3690,8 @@ fn builtin_arity(name: String) -> Int { if str_eq(name, "str_split_chars") { return 1 } if str_eq(name, "str_split_n") { return 3 } if str_eq(name, "str_join") { return 2 } + // HTML sanitizer + if str_eq(name, "el_html_sanitize") { return 2 } // Math if str_eq(name, "el_abs") { return 1 } if str_eq(name, "el_max") { return 2 } @@ -2918,8 +3896,98 @@ fn add_int_name(name: String) -> Bool { return true } +fn add_instant_name(name: String) -> Bool { + let csv: String = state_get("__instant_names") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__instant_names", csv + name + ",") + return true +} + +fn add_duration_name(name: String) -> Bool { + let csv: String = state_get("__duration_names") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__duration_names", csv + name + ",") + return true +} + +fn add_calendar_name(name: String) -> Bool { + let csv: String = state_get("__calendar_names") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__calendar_names", csv + name + ",") + return true +} + +fn add_caltime_name(name: String) -> Bool { + let csv: String = state_get("__caltime_names") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__caltime_names", csv + name + ",") + return true +} + +fn add_rhythm_name(name: String) -> Bool { + let csv: String = state_get("__rhythm_names") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__rhythm_names", csv + name + ",") + return true +} + +fn add_localdate_name(name: String) -> Bool { + let csv: String = state_get("__localdate_names") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__localdate_names", csv + name + ",") + return true +} + +fn add_localtime_name(name: String) -> Bool { + let csv: String = state_get("__localtime_names") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__localtime_names", csv + name + ",") + return true +} + +fn add_localdt_name(name: String) -> Bool { + let csv: String = state_get("__localdt_names") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__localdt_names", csv + name + ",") + return true +} + +fn add_zone_name(name: String) -> Bool { + let csv: String = state_get("__zone_names") + if str_eq(csv, "") { csv = "," } + let key: String = "," + name + "," + if str_contains(csv, key) { return true } + state_set("__zone_names", csv + name + ",") + return true +} + fn build_int_names_for_params(params: [Map]) -> Bool { state_set("__int_names", ",") + state_set("__instant_names", ",") + state_set("__duration_names", ",") + state_set("__calendar_names", ",") + state_set("__caltime_names", ",") + state_set("__rhythm_names", ",") + state_set("__localdate_names", ",") + state_set("__localtime_names", ",") + state_set("__localdt_names", ",") + state_set("__zone_names", ",") let np: Int = native_list_len(params) let pi = 0 while pi < np { @@ -2929,6 +3997,33 @@ fn build_int_names_for_params(params: [Map]) -> Bool { if str_eq(ptype, "Int") { add_int_name(pname) } + if str_eq(ptype, "Instant") { + add_instant_name(pname) + } + if str_eq(ptype, "Duration") { + add_duration_name(pname) + } + if str_eq(ptype, "Calendar") { + add_calendar_name(pname) + } + if str_eq(ptype, "CalendarTime") { + add_caltime_name(pname) + } + if str_eq(ptype, "Rhythm") { + add_rhythm_name(pname) + } + if str_eq(ptype, "LocalDate") { + add_localdate_name(pname) + } + if str_eq(ptype, "LocalTime") { + add_localtime_name(pname) + } + if str_eq(ptype, "LocalDateTime") { + add_localdt_name(pname) + } + if str_eq(ptype, "Zone") { + add_zone_name(pname) + } let pi = pi + 1 } return true @@ -3195,6 +4290,8 @@ fn codegen(stmts: [Map], source: String) -> String { state_set("__cap_violations", "") // Clear arity-violation accumulator from any prior compile. state_set("__arity_violations", "") + // Clear temporal-type-violation accumulator from any prior compile. + state_set("__time_violations", "") // Preamble emit_line("#include ") @@ -3264,9 +4361,11 @@ fn codegen(stmts: [Map], source: String) -> String { let i = i + 1 } - // main() - emit_line("int main(int argc, char** argv) {") - emit_line(" el_runtime_init_args(argc, argv);") + // main(). Use _argc/_argv so El programs are free to declare their own + // local `argv` / `argc` (compiler.el itself does this) without colliding + // with the C-side parameters when fn main()'s body is folded in below. + emit_line("int main(int _argc, char** _argv) {") + emit_line(" el_runtime_init_args(_argc, _argv);") if cgi_count >= 1 { let cname: String = cgi_block["name"] let cdid: String = cgi_block["dharma_id"] @@ -3298,11 +4397,34 @@ fn codegen(stmts: [Map], source: String) -> String { } let i = i + 1 } + // First pass: capture the body of `fn main()` if the source declared + // one. We've already skipped emitting it as a regular el_val_t + // function (see cg_fn early return); fold its body into C's main + // alongside top-level statements so the program actually runs. + let el_main_body = native_list_empty() let i = 0 while i < n { let stmt = native_list_get(stmts, i) if is_fndef(stmt) { - // skip + let fn_name: String = stmt["name"] + if str_eq(fn_name, "main") { + let body = stmt["body"] + let bn: Int = native_list_len(body) + let bi: Int = 0 + while bi < bn { + let el_main_body = native_list_append(el_main_body, native_list_get(body, bi)) + let bi = bi + 1 + } + } + } + let i = i + 1 + } + + let i = 0 + while i < n { + let stmt = native_list_get(stmts, i) + if is_fndef(stmt) { + // skip - fn defs already emitted above; fn main body folded later } else { if is_top_level_decl(stmt) { // skip @@ -3317,6 +4439,16 @@ fn codegen(stmts: [Map], source: String) -> String { el_release(stmt) let i = i + 1 } + + // Fold fn main()'s body in here, after top-level statements. + let mn: Int = native_list_len(el_main_body) + let mi: Int = 0 + while mi < mn { + let mstmt = native_list_get(el_main_body, mi) + let main_decl = cg_stmt(mstmt, " ", main_decl) + let mi = mi + 1 + } + emit_line(" return 0;") emit_line("}") emit_blank() @@ -3330,6 +4462,8 @@ fn codegen(stmts: [Map], source: String) -> String { // so a misuse of a known builtin (wrong arg count) fails the build // with a clear message naming the builtin and its expected arity. emit_arity_violations() + // Temporal-type violations (Instant + Instant, Duration + Int, …). + emit_time_violations() // Return empty string — output was streamed via println "" @@ -3464,15 +4598,6 @@ fn js_is_int_call(call_expr: Map) -> Bool { if str_eq(name, "str_index_of") { return true } if str_eq(name, "str_to_int") { return true } if str_eq(name, "str_char_code") { return true } - if str_eq(name, "str_count") { return true } - if str_eq(name, "str_count_chars") { return true } - if str_eq(name, "str_count_bytes") { return true } - if str_eq(name, "str_count_lines") { return true } - if str_eq(name, "str_count_words") { return true } - if str_eq(name, "str_count_letters") { return true } - if str_eq(name, "str_count_digits") { return true } - if str_eq(name, "str_last_index_of") { return true } - if str_eq(name, "str_find_chars") { return true } if str_eq(name, "native_list_len") { return true } if str_eq(name, "el_list_len") { return true } if str_eq(name, "len") { return true } @@ -3504,6 +4629,33 @@ fn js_cg_expr(expr: Map) -> String { return v } + // DurationLit — postfix-literal time value (e.g. 30.seconds, 1.hour). + // The JS backend lowers to a literal integer nanosecond count. The C + // backend uses the typed wrapper el_duration_from_nanos to make intent + // explicit at the runtime boundary; JS has no equivalent shim yet, so + // we lower directly. A future Phase 2 JS time runtime can route through + // a wrapper once added. + if kind == "DurationLit" { + let count: String = expr["count"] + let unit: String = expr["unit"] + let mult_ns = "1" + if str_eq(unit, "nano") { let mult_ns = "1" } + if str_eq(unit, "nanos") { let mult_ns = "1" } + if str_eq(unit, "milli") { let mult_ns = "1000000" } + if str_eq(unit, "millis") { let mult_ns = "1000000" } + if str_eq(unit, "millisecond") { let mult_ns = "1000000" } + if str_eq(unit, "milliseconds") { let mult_ns = "1000000" } + if str_eq(unit, "second") { let mult_ns = "1000000000" } + if str_eq(unit, "seconds") { let mult_ns = "1000000000" } + if str_eq(unit, "minute") { let mult_ns = "60000000000" } + if str_eq(unit, "minutes") { let mult_ns = "60000000000" } + if str_eq(unit, "hour") { let mult_ns = "3600000000000" } + if str_eq(unit, "hours") { let mult_ns = "3600000000000" } + if str_eq(unit, "day") { let mult_ns = "86400000000000" } + if str_eq(unit, "days") { let mult_ns = "86400000000000" } + return "(" + count + " * " + mult_ns + ")" + } + if kind == "Float" { // JS numbers are already doubles — no bit-cast trick needed. let v: String = expr["value"] @@ -3922,6 +5074,16 @@ fn js_cg_stmt(stmt: Map, indent: String, declared: [String]) -> [St return declared } + // Bare reassignment: `name = expr`. Mirrors the C backend — emits a + // plain JS assignment without `let` so we don't shadow an outer binding. + if kind == "Assign" { + let name: String = stmt["name"] + let val = stmt["value"] + let val_c: String = js_cg_expr(val) + js_emit_line(indent + name + " = " + val_c + ";") + return declared + } + if kind == "Expr" { let val = stmt["value"] let val_kind: String = val["expr"] @@ -4326,6 +5488,91 @@ fn strip_flags(argv: [String]) -> [String] { return out } +// ── Import resolution ──────────────────────────────────────────────────────── +// +// elc supports two forms of import: +// import "path/to/file.el" — quoted relative path +// from module import { Name } — bare module name resolves to module.el +// in the entry source's directory +// +// Codegen treats Import statements as no-ops (declarations only), so to +// actually link bodies across files we textually concatenate every imported +// source ahead of the entry source before lex/parse. resolve_imports does a +// depth-first traversal with deduplication so any module that gets pulled in +// transitively is included exactly once. + +fn dirname_of(path: String) -> String { + let n: Int = str_len(path) + let i: Int = n - 1 + while i >= 0 { + let c: String = str_slice(path, i, i + 1) + if str_eq(c, "/") { + return str_slice(path, 0, i) + } + let i = i - 1 + } + return "." +} + +// Extract the resolved file path from a single trimmed source line. Returns +// "" if the line is not an import. +fn parse_import_line(trimmed: String, dir: String) -> String { + if str_starts_with(trimmed, "import \"") { + let after: String = str_slice(trimmed, 8, str_len(trimmed)) + let q: Int = str_index_of(after, "\"") + if q > 0 { + let mod: String = str_slice(after, 0, q) + return dir + "/" + mod + } + } + if str_starts_with(trimmed, "from ") { + let after: String = str_slice(trimmed, 5, str_len(trimmed)) + // module name is the first whitespace-delimited token + let sp: Int = str_index_of(after, " ") + if sp > 0 { + let mod_raw: String = str_slice(after, 0, sp) + let mod: String = str_trim(mod_raw) + if !str_eq(mod, "") { + return dir + "/" + mod + ".el" + } + } + } + return "" +} + +// Recursively resolve imports starting from src_path. Returns the combined +// source text with every imported module's body inlined ahead of the entry +// source, deduplicated by absolute path. Uses state_set to track which paths +// have already been pulled in for this run. +fn resolve_imports(src_path: String) -> String { + let seen_key: String = "__elc_imp__:" + src_path + let already: String = state_get(seen_key) + if !str_eq(already, "") { return "" } + state_set(seen_key, "1") + + let source: String = fs_read(src_path) + let dir: String = dirname_of(src_path) + let lines: [String] = str_split(source, "\n") + let n: Int = native_list_len(lines) + + // First pass: pull in every import body ahead of this file's body. + let prefix: String = "" + let body: String = "" + let i: Int = 0 + while i < n { + let line: String = native_list_get(lines, i) + let trimmed: String = str_trim(line) + let imp_path: String = parse_import_line(trimmed, dir) + if !str_eq(imp_path, "") { + let prefix = prefix + resolve_imports(imp_path) + } else { + let body = body + line + "\n" + } + let i = i + 1 + } + return prefix + body +} + // main — CLI entry point. // // elc # emit C to stdout @@ -4334,7 +5581,10 @@ fn strip_flags(argv: [String]) -> [String] { // elc --target=js # write JS to file fn main() -> Void { let argv: [String] = args() - let target: String = detect_target(argv) + // Use `tgt` not `target`: `target` is a reserved keyword in the lexer + // (Section 1.5 of the language spec). detect_target itself is fine + // because the function-name position has no token-class restriction. + let tgt: String = detect_target(argv) let positional: [String] = strip_flags(argv) let argc: Int = native_list_len(positional) if argc < 1 { @@ -4342,8 +5592,8 @@ fn main() -> Void { exit(1) } let src_path: String = native_list_get(positional, 0) - let source: String = fs_read(src_path) - let out: String = compile_dispatch(target, source) + let source: String = resolve_imports(src_path) + let out: String = compile_dispatch(tgt, source) if argc >= 2 { let out_path: String = native_list_get(positional, 1) let ok: Bool = fs_write(out_path, out)