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
This commit is contained in:
32
examples/0189-types-int-float-compare-promote.sx
Normal file
32
examples/0189-types-int-float-compare-promote.sx
Normal file
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
2
|
||||
1
|
||||
74
issues/0146-int-to-float-cast-dropped-in-comparison.md
Normal file
74
issues/0146-int-to-float-cast-dropped-in-comparison.md
Normal file
@@ -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.
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user