From 7057175fb6e206bf9b0a3e05c075b2202724430c Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 21 Jun 2026 09:11:52 +0300 Subject: [PATCH] fix: promote mismatched comparison operands before emitting cmp (issue 0146) A comparison with int-vs-float (or two float widths) operands emitted cmp on the raw operands with no promotion, unlike the arithmetic arms -- producing a mixed-type compare the LLVM verifier rejects / mis-evaluates. lowerBinaryOp now coerces each operand to the promoted common type (from arithResultType) via coerceToType (SIToFP / FPExt) for the ordering/equality arms when the promoted type is a float, so LLVM gets a well-typed fcmp. Regression: examples/0189-types-int-float-compare-promote.sx --- .../0189-types-int-float-compare-promote.sx | 32 ++++++++ .../0189-types-int-float-compare-promote.exit | 1 + ...189-types-int-float-compare-promote.stderr | 1 + ...189-types-int-float-compare-promote.stdout | 2 + ...int-to-float-cast-dropped-in-comparison.md | 74 +++++++++++++++++++ src/ir/lower/expr.zig | 28 +++++++ 6 files changed, 138 insertions(+) create mode 100644 examples/0189-types-int-float-compare-promote.sx create mode 100644 examples/expected/0189-types-int-float-compare-promote.exit create mode 100644 examples/expected/0189-types-int-float-compare-promote.stderr create mode 100644 examples/expected/0189-types-int-float-compare-promote.stdout create mode 100644 issues/0146-int-to-float-cast-dropped-in-comparison.md diff --git a/examples/0189-types-int-float-compare-promote.sx b/examples/0189-types-int-float-compare-promote.sx new file mode 100644 index 00000000..2f59bc85 --- /dev/null +++ b/examples/0189-types-int-float-compare-promote.sx @@ -0,0 +1,32 @@ +// A comparison with mismatched scalar operands (int vs float, or two float +// widths) must promote both operands to the common type before emitting the +// compare — `lowerBinaryOp` now coerces each operand to the promoted float +// type for the ordering/equality arms (as the arithmetic arms already do), +// so the cast materializes and LLVM gets a well-typed fcmp instead of a +// mixed-type compare that the verifier rejects / silently mis-evaluates. +// +// Regression (issue 0146). +#import "modules/std.sx"; + +// i32 < f32: the `xx i` cast must materialize as an i32->f32 SIToFP. +ceil_half :: (v: f32) -> i32 { + t := v - 0.5; + i : i32 = xx t; + if xx i < t { // 1.0 < 1.8 -> true + i += 1; + } + i +} + +// f64 vs f32 sibling: `xx y + 0.5` infers f64; comparing to an f32 must FPExt. +ge :: (y: i32, lo: f32) -> i32 { + sy := xx y + 0.5; // 3.5 (f64) + if sy >= lo { return 1; } + 0 +} + +main :: () -> i32 { + print("{}\n", ceil_half(2.3)); // 2 + print("{}\n", ge(3, 2.0)); // 1 + 0 +} diff --git a/examples/expected/0189-types-int-float-compare-promote.exit b/examples/expected/0189-types-int-float-compare-promote.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0189-types-int-float-compare-promote.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0189-types-int-float-compare-promote.stderr b/examples/expected/0189-types-int-float-compare-promote.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0189-types-int-float-compare-promote.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0189-types-int-float-compare-promote.stdout b/examples/expected/0189-types-int-float-compare-promote.stdout new file mode 100644 index 00000000..5f1d0ece --- /dev/null +++ b/examples/expected/0189-types-int-float-compare-promote.stdout @@ -0,0 +1,2 @@ +2 +1 diff --git a/issues/0146-int-to-float-cast-dropped-in-comparison.md b/issues/0146-int-to-float-cast-dropped-in-comparison.md new file mode 100644 index 00000000..679f2369 --- /dev/null +++ b/issues/0146-int-to-float-cast-dropped-in-comparison.md @@ -0,0 +1,74 @@ +# 0146 — `xx i < t` drops the int→float cast, emits a mixed-type compare + +> **RESOLVED.** `lowerBinaryOp` (src/ir/lower/expr.zig) now promotes the +> comparison operands to the common type before emitting the compare: for the +> ordering/equality arms, when the promoted type `ty` (from `arithResultType`) +> is a float, each operand whose IR type differs is coerced via `coerceToType` +> (SIToFP / FPExt). LLVM then receives same-typed operands and a well-formed +> `fcmp`, instead of a mixed-type compare the verifier rejects. Regression +> test: `examples/0189-types-int-float-compare-promote.sx`. + +## Summary + +A comparison whose left operand is an *inline* int→float cast against a float +right operand (`if xx i < t { ... }`, with `i : i32` and `t : f32`) does NOT +coerce the integer to float. The backend emits the compare with mismatched +operand types and LLVM module verification fails the whole build: + +``` +LLVM verification failed: Both operands to ICmp instruction are not of the same type! + %icmp = icmp slt i32 %load4, float %load5 +error: default build pipeline failed: ComptimeVmBail: comptime emit_object: object emission failed +``` + +The same shape with `==` and `>` produces the matching `icmp eq`/`icmp sgt` +variants — it is the cast in the comparison operand that is lost, not a single +operator. + +## Minimal repro + +```sx +#import "modules/std.sx"; +ceil_half :: (v: f32) -> i32 { + t := v - 0.5; + i : i32 = xx t; + if xx i < t { // <-- icmp slt i32, float — cast dropped + i += 1; + } + i +} +main :: () -> i32 { + print("{}\n", ceil_half(2.3)); + 0 +} +``` + +`bash sx build` on this fails at object emission with the verifier error above. + +## Expected + +`xx i` is an i32→f32 cast; the comparison should be a float compare +(`fcmp`), i.e. the cast must materialize before the compare. The literal value +is computed correctly at comptime — only the emitted compare is malformed. + +## Workaround (documented, in use) + +Bind the cast to a typed float local first, then compare the local: + +```sx +i : i32 = xx t; +fi : f32 = xx i; // materialize the cast into a typed local +if fi < t { i += 1; } // float compare, both operands f32 — OK +``` + +This compiles and runs correctly. Applied in `doc/selection.sx` +(`ceil_half` / `floor_half_excl`). + +## Related + +A sibling symptom in the same function family: `sy := xx y + 0.5` (with +`y : i32`, `0.5` a comptime float) infers `sy` as **f64/double**, so a later +`sy >= lo` against an `f32 lo` emits `fcmp oge double, float`. Annotating the +local explicitly (`sy : f32 = xx y + 0.5`) pins it to f32 and the compare +matches. Likely the same missing-coercion root cause around mixed +int/comptime-float expressions feeding a float local or a compare. diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index 05cc64dd..7008f6dd 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -2761,6 +2761,34 @@ pub fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref { } } + // Comparison operand promotion. Arithmetic arms below carry the promoted + // common type `ty` on the result op, so the LLVM emitter re-matches the + // operands against it (`matchBinOpTypes`). Comparisons carry `.bool` + // instead, so `emitCmp`/`emitCmpOrdered` only see the raw operand LLVM + // types — and those only reconcile int↔int width (SExt/ZExt). A mixed + // int-vs-float compare (`xx i < t`, i:i32 t:f32) or a two-float-width + // compare (`f64 >= f32`) reaches the emitter with mismatched operands and + // fails LLVM verification (issue 0146). Coerce each operand up to the + // promoted common type HERE — `coerceToType` emits the SIToFP / FPExt / + // width-ext — so the operands are already type-equal when the cmp is built. + // Restricted to float `ty`: an int↔int compare is handled by the emitter, + // and a non-numeric `ty` (struct/string/enum) has its own cmp path. + switch (bop.op) { + .eq, .neq, .lt, .lte, .gt, .gte => { + if (Lowering.isFloat(ty)) { + const lhs_ir = self.builder.getRefType(lhs); + if (lhs_ir != ty and (Lowering.isFloat(lhs_ir) or self.isIntEx(lhs_ir))) { + lhs = self.coerceToType(lhs, lhs_ir, ty); + } + const rhs_ir = self.builder.getRefType(rhs); + if (rhs_ir != ty and (Lowering.isFloat(rhs_ir) or self.isIntEx(rhs_ir))) { + rhs = self.coerceToType(rhs, rhs_ir, ty); + } + } + }, + else => {}, + } + return switch (bop.op) { .add => self.builder.add(lhs, rhs, ty), .sub => self.builder.sub(lhs, rhs, ty),