diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index f1de5d1..ebd9829 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -1517,6 +1517,16 @@ fn cg_stmt(stmt: Map, indent: String, declared: [String]) -> [Strin if str_eq(ltype, "Int") { add_int_name(name) } + // UNANNOTATED let: take the type from what the initialiser RETURNS. + // Without this, `let a = str_len(s)` loses the Int and a later `a + b` + // lowers to el_str_concat on two integers -- silently, with no error, + // producing a program that prints nothing where it should print 7. + // Verified present in the pre-change compiler too, so this is a repair + // rather than a regression. The return types were already needed for + // dispatch; they were just never consulted here. + if str_eq(ltype, "") { + if is_int_expr(val) { add_int_name(name) } + } // Same as params: Bool is an int in the value model. Without this a // `let ok: Bool = ...` compared to another Bool lowered to str_eq. if str_eq(ltype, "Bool") { @@ -1943,48 +1953,10 @@ fn is_duration_name(name: String) -> Bool { // string-concat on `+` when one side is a Call. New builtins must be added // here when they return Int and may participate in arithmetic. fn is_int_call(call_expr: Map) -> Bool { - let func = call_expr["func"] - let fk: String = func["expr"] - if !str_eq(fk, "Ident") { return false } - let name: String = func["name"] - if str_eq(name, "str_len") { return true } - if str_eq(name, "str_index_of") { return true } - if str_eq(name, "str_to_int") { return true } - if str_eq(name, "str_char_code") { return true } - if str_eq(name, "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 } - if str_eq(name, "json_get_int") { return true } - if str_eq(name, "json_array_len") { return true } - if str_eq(name, "engram_node_count") { return true } - if str_eq(name, "engram_edge_count") { return true } - if str_eq(name, "time_now") { return true } - if str_eq(name, "time_now_utc") { return true } - if str_eq(name, "time_diff") { return true } - if str_eq(name, "time_add") { return true } - if str_eq(name, "time_from_parts") { return true } - if str_eq(name, "el_abs") { return true } - if str_eq(name, "el_max") { return true } - if str_eq(name, "el_min") { return true } - 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 + call_returns(call_expr, "Int") } + // Known runtime builtins that return Float. Parallel to is_int_call — lets a // Call participate in float arithmetic (and get inferred into __float_names on // an unannotated `let`). New Float-returning builtins must be added here. diff --git a/lang/tests/native/test_compiler.el b/lang/tests/native/test_compiler.el index 47556fa..999e0c3 100644 --- a/lang/tests/native/test_compiler.el +++ b/lang/tests/native/test_compiler.el @@ -924,3 +924,22 @@ test "compiler-no-longer-emits-prohibition-errors" { let out: String = compile_capture(src) assert !str_contains(out, "boundary violation"), "the emitter does not adjudicate" } + +// ── Int return types drive + dispatch ──────────────────────────────────────── +// +// El has one type, so `a + b` must be dispatched from what the operands ARE. +// The 35 Int-returning builtins moved to signatures.rel; the dispatch stayed, +// because choosing between arithmetic and concatenation is emission. + +test "int-returning-builtin-drives-arithmetic-dispatch" { + let src: String = "fn main() { let a = str_len(\"hello\") let b = str_len(\"hi\") let c = a + b println(int_to_str(c)) }" + let out: String = compile_capture(src) + assert str_contains(out, "(a + b)"), "Int + Int is arithmetic" + assert !str_contains(out, "el_str_concat(a, b)"), "and NOT concatenation" +} + +test "string-plus-string-still-concatenates" { + let src: String = "fn main() { let s = \"a\" + \"b\" println(s) }" + let out: String = compile_capture(src) + assert str_contains(out, "el_str_concat"), "String + String still concatenates" +} diff --git a/lang/tools/check/signatures.rel b/lang/tools/check/signatures.rel index c0f502c..e7a7a01 100644 --- a/lang/tools/check/signatures.rel +++ b/lang/tools/check/signatures.rel @@ -31,3 +31,43 @@ el_duration_sub returns Duration el_duration_scale returns Duration el_duration_div returns Duration ttl_cache_age returns Duration + +# Int-returning builtins. Previously 35 hardcoded names in is_int_call(). +# These decide whether `a + b` is arithmetic or concatenation, so the +# compiler reads them at emission time -- dispatch, not adjudication. + +str_len returns Int +str_index_of returns Int +str_to_int returns Int +str_char_code returns Int +str_count returns Int +str_count_chars returns Int +str_count_bytes returns Int +str_count_lines returns Int +str_count_words returns Int +str_count_letters returns Int +str_count_digits returns Int +str_last_index_of returns Int +str_find_chars returns Int +native_list_len returns Int +el_list_len returns Int +len returns Int +json_get_int returns Int +json_array_len returns Int +engram_node_count returns Int +engram_edge_count returns Int +time_now returns Int +time_now_utc returns Int +time_diff returns Int +time_add returns Int +time_from_parts returns Int +el_abs returns Int +el_max returns Int +el_min returns Int +float_to_int returns Int +unix_timestamp returns Int +instant_to_unix_seconds returns Int +instant_to_unix_millis returns Int +duration_to_seconds returns Int +duration_to_millis returns Int +duration_to_nanos returns Int