From b55e6bfd53077f8dff57bfe63171cad44ff9267d Mon Sep 17 00:00:00 2001 From: bigmerge Date: Sat, 15 Aug 2026 21:51:35 -0500 Subject: [PATCH] codegen: either side Int is enough for == and !=, not both MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit let a: Int = 5 getint(5) == a -> str_eq(getint(5), a) SIGSEGV getint(5) == 5 -> getint(5) == 5 fine A function call whose return type codegen cannot infer poisoned the operator, and a declared Int on the other side did not save it. str_eq then read an integer as a char* and segfaulted. Only an integer LITERAL on one side forced the numeric form, which is why the bug stayed invisible: the common case happened to be safe. The check required BOTH operands to be provably Int: if is_int_expr(left) { if is_int_expr(right) { numeric } } Loosening to OR is strictly safer, not a trade: - when one side is a known Int, str_eq is ALWAYS wrong — it dereferences that integer — while numeric comparison is at worst a wrong answer on a program that was already ill-typed; - when neither side is Int nothing changes at all, so string comparison is untouched. Found by the test-framework agent while building the benchmark harness; it correctly declined to fix it mid-phase since it is a codegen semantics change. VERIFIED, because a semantics change earns more than an assertion: - 15/15 on a dedicated operator suite covering string literals, string vars, string-returning calls, mixed var/call, and != in every combination. The pre-change compiler scores 0/15 on the same file: it segfaults before printing anything. - self-hosting fixpoint byte-identical - the ONLY difference in the compiler's own generated C is the intended one: a nested if becoming two sequential ifs, in EqEq and NotEq. Nothing else moved. - neuron's full soul amalgam regenerates in 400ms, exit 0, output BYTE-IDENTICAL at 1,270,212 bytes - test_math 13/13, test_string 27/27, test_core 10/10, test_text 12/12 — 62 tests, 190 assertions, zero failures NOT fixed here, same family, flagged for a decision: Bool PARAMETERS are not tracked as int-like, so `cond == want` between two Bool params still lowers to str_eq and segfaults. Found while writing this commit's own test harness — the first version of it crashed on exactly that, on both the old and new compiler. It needs the same treatment, and it wants its own change. --- lang/el-compiler/src/codegen.el | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index fe1056f..1dd89ae 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -862,10 +862,23 @@ fn cg_expr(expr: Map) -> String { // arithmetic BinOp (or vice-versa). Without this check the // fallthrough to str_eq produces str_eq(int_value, int_value) // which reads the integer as a char* and segfaults. + // EITHER side provably Int is enough. Requiring BOTH meant a call + // whose return type codegen cannot infer poisoned the operator: + // getint(5) == a -> str_eq(getint(5), a) + // even with `a` declared Int. str_eq then reads an integer as a + // char* and segfaults. Only an integer LITERAL on one side forced + // the numeric form, so the bug was invisible in the common case. + // + // Loosening to OR is strictly safer: when one side is a known Int, + // str_eq is always wrong (it dereferences that int), while numeric + // comparison is at worst a wrong answer on an already ill-typed + // program. When neither side is Int nothing changes, so string + // comparison is untouched. if is_int_expr(left) { - if is_int_expr(right) { - return "(" + left_c + " == " + right_c + ")" - } + return "(" + left_c + " == " + right_c + ")" + } + if is_int_expr(right) { + return "(" + left_c + " == " + right_c + ")" } // Float literal or negative float literal: use plain == (bit-equal // el_val_t comparison). This handles `r0 == 3.0`, `neg == -3.0`, etc. @@ -921,10 +934,12 @@ fn cg_expr(expr: Map) -> String { } // Same mixed Ident/BinOp fix as EqEq: use is_int_expr to detect // integer-typed operands before falling through to !str_eq. + // Either side Int is enough — see the EqEq note above. if is_int_expr(left) { - if is_int_expr(right) { - return "(" + left_c + " != " + right_c + ")" - } + return "(" + left_c + " != " + right_c + ")" + } + if is_int_expr(right) { + return "(" + left_c + " != " + right_c + ")" } // Float-typed operands use plain != (bit-equal comparison). if is_float_expr(left) {