From cbef1c1ebb77c34f40ac1fec84f7daf952f6d29e Mon Sep 17 00:00:00 2001 From: bigmerge Date: Mon, 17 Aug 2026 09:32:13 -0500 Subject: [PATCH] =?UTF-8?q?EXPERIMENT:=20Int=20return=20types=20as=20data?= =?UTF-8?q?=20=E2=80=94=20and=20the=20bug=20that=20fell=20out?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PREDICTIONS AND RESULTS P1 is_int_call's 35 hardcoded names move to data TRUE P2 is_int_name stays -- it is annotation propagation TRUE P3 the dispatch stays -- it is emission TRUE P4 codegen shrinks ~40 lines TRUE 4507 -> 4469 P5 the design doc's characterisation is WRONG TRUE P6 the moved data also fixes the bug it exposed TRUE P5 CORRECTS THE RECORD. el-language-design.md and geometry-vs-code.md both cite "== lowering to str_eq unless both operand names are in a hardcoded int-name set -- a literal list of variable names treated as integers" as the paradigm defect. It is not one. __int_names is populated from TYPE ANNOTATIONS (param["type"] == "Int"), which is primitive but legitimate type propagation. The actual defect was is_int_call: 35 hardcoded builtin return types, the same shape as the temporal 19. P6 IS A LIVE CORRECTNESS BUG, PRE-EXISTING, NOW FIXED let a = str_len("hello") // no annotation let b = str_len("hi") let c = a + b // -> el_str_concat(a, b) on two integers Verified identical on the pre-change compiler, so not a regression. It compiled clean, ran, and printed NOTHING where it should print 7. No error at any layer. The repair is three lines: an unannotated let takes its type from what the initialiser returns. The return types were already required for dispatch and were simply never consulted at the binding site. Moving them into data is what made the gap visible -- reading the code for eight hours did not. 98/98 native + 2 new, 31/31 integration, fixpoint ok. --- lang/el-compiler/src/codegen.el | 52 +++++++----------------------- lang/tests/native/test_compiler.el | 19 +++++++++++ lang/tools/check/signatures.rel | 40 +++++++++++++++++++++++ 3 files changed, 71 insertions(+), 40 deletions(-) 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