fix(ir): infer mixed int+float arithmetic as the promoted float [F0.7]
`ExprTyper.inferType`'s binary-op arm inferred every non-comparison op
from the LHS alone, so `M + 0.5` (s64 + f64) statically typed as s64
while `0.5 + M` typed as f64 — operand-order-dependent. The value path
(`lowerBinaryOp`) already promoted int×float → float, so static
inference disagreed with the value: `M + 0.5` formatted as a truncated
int and a typed const `BAD : s64 : M + 0.5` was accepted+truncated
(issue 0088 mixed-numeric escape).
Extract the value path's inline promotion into a shared
`Lowering.arithResultType(lhs, rhs)` and reuse it at both sites, so
arithmetic / bitwise / shift inference reports exactly the type the
lowered value carries — int LHS × float RHS → the float, order-
independent. The value-path behavior is unchanged (the block is moved
verbatim into the helper), so no IR shifts; the suite stays green. The
typed-const validation reuses `inferExprType`, so this auto-closes the
escape with no change to the validation logic.
- examples/1143: BAD/BAD2 (`s64 : M + 0.5`, `s64 : 0.5 + M`) rejected
in both operand orders.
- examples/0162: MF/MFR (`f64 : M + 0.5`, `f64 : 0.5 + M`) fold to 2.5.
- examples/0163 (new): pins the inference fix in a value context
(`print("{}", n + 0.5)` formats the float, both orders, +-*/, f32).
- expr_typer.test.zig: arithResultType + mixed-arithmetic inference.
- specs.md / readme.md: document the numeric-promotion rule.
- issues/0088: RESOLVED banner notes the inferExprType root fix.
This commit is contained in:
@@ -34,7 +34,7 @@ test "expr_typer: literal shapes" {
|
||||
try std.testing.expectEqual(TypeId.string, l.inferExprType(&str_n));
|
||||
}
|
||||
|
||||
test "expr_typer: binary comparison is bool, arithmetic takes lhs type" {
|
||||
test "expr_typer: binary comparison is bool, int arithmetic stays int" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
@@ -50,6 +50,46 @@ test "expr_typer: binary comparison is bool, arithmetic takes lhs type" {
|
||||
try std.testing.expectEqual(TypeId.s64, l.inferExprType(&add));
|
||||
}
|
||||
|
||||
// issue 0088 (attempt 3): a non-comparison binary op infers the PROMOTED result
|
||||
// of (lhs, rhs), not the LHS alone — so a mixed int+float op types as the float
|
||||
// in EITHER operand order (was LHS-biased: `int + float` → s64 while
|
||||
// `float + int` → f64). This is what feeds the typed-const validation that
|
||||
// rejected `s64 : 0.5 + M` but not `s64 : M + 0.5`.
|
||||
test "expr_typer: mixed int+float arithmetic promotes to float, order-independent" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var l = Lowering.init(&module);
|
||||
|
||||
var int_n = node(.{ .int_literal = .{ .value = 2 } });
|
||||
var float_n = node(.{ .float_literal = .{ .value = 0.5 } });
|
||||
|
||||
// int LHS, float RHS → f64 (was s64 before the fix).
|
||||
var add_if = node(.{ .binary_op = .{ .op = .add, .lhs = &int_n, .rhs = &float_n } });
|
||||
try std.testing.expectEqual(TypeId.f64, l.inferExprType(&add_if));
|
||||
|
||||
// float LHS, int RHS → f64 (already correct; confirms order-independence).
|
||||
var add_fi = node(.{ .binary_op = .{ .op = .add, .lhs = &float_n, .rhs = &int_n } });
|
||||
try std.testing.expectEqual(TypeId.f64, l.inferExprType(&add_fi));
|
||||
|
||||
// Multiplication promotes the same way.
|
||||
var mul_if = node(.{ .binary_op = .{ .op = .mul, .lhs = &int_n, .rhs = &float_n } });
|
||||
try std.testing.expectEqual(TypeId.f64, l.inferExprType(&mul_if));
|
||||
}
|
||||
|
||||
// The shared promotion helper itself (single source of truth for both
|
||||
// `lowerBinaryOp`'s value type and `inferExprType`): an integer LHS with a
|
||||
// floating-point RHS promotes to the float; every other pairing keeps the LHS.
|
||||
test "arithResultType: int×float promotes to float, else takes lhs" {
|
||||
try std.testing.expectEqual(TypeId.f64, Lowering.arithResultType(.s64, .f64));
|
||||
try std.testing.expectEqual(TypeId.f32, Lowering.arithResultType(.u32, .f32));
|
||||
try std.testing.expectEqual(TypeId.f32, Lowering.arithResultType(.s64, .f32));
|
||||
// Non-promoting pairings keep the LHS type.
|
||||
try std.testing.expectEqual(TypeId.s64, Lowering.arithResultType(.s64, .s64));
|
||||
try std.testing.expectEqual(TypeId.f64, Lowering.arithResultType(.f64, .s64));
|
||||
try std.testing.expectEqual(TypeId.f32, Lowering.arithResultType(.f32, .f64));
|
||||
}
|
||||
|
||||
test "expr_typer: unary not is bool, negate preserves operand type" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
|
||||
@@ -49,7 +49,12 @@ pub const ExprTyper = struct {
|
||||
break :blk .bool;
|
||||
},
|
||||
.eq, .neq, .lt, .lte, .gt, .gte, .and_op, .in_op => .bool,
|
||||
else => self.l.inferExprType(bop.lhs),
|
||||
// Arithmetic / bitwise / shift ops: infer the PROMOTED result
|
||||
// of (lhs, rhs), not the LHS alone — `Lowering.arithResultType`
|
||||
// is the same rule `lowerBinaryOp` applies, so `M + 0.5` types
|
||||
// as `f64` regardless of operand order (was LHS-biased: `M + 0.5`
|
||||
// → s64 while `0.5 + M` → f64).
|
||||
else => Lowering.arithResultType(self.l.inferExprType(bop.lhs), self.l.inferExprType(bop.rhs)),
|
||||
},
|
||||
.unary_op => |uop| switch (uop.op) {
|
||||
.not => .bool,
|
||||
|
||||
@@ -3263,19 +3263,12 @@ pub const Lowering = struct {
|
||||
const rhs_ref_pointee = self.refCapturePointee(bop.rhs);
|
||||
if (rhs_ref_pointee) |p| rhs = self.builder.load(rhs, p);
|
||||
self.target_type = saved_tt;
|
||||
// Infer result type from LHS operand (covers float, bool, etc.)
|
||||
var ty = lhs_ty;
|
||||
|
||||
// Promote int×float → float (e.g., s64 * f32 → f32)
|
||||
// Only for scalar int LHS — don't affect vectors or structs.
|
||||
{
|
||||
const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs);
|
||||
const l_int = isInt(ty);
|
||||
const r_float = (rhs_inferred == .f32 or rhs_inferred == .f64);
|
||||
if (l_int and r_float) {
|
||||
ty = rhs_inferred;
|
||||
}
|
||||
}
|
||||
// Result type follows the shared promotion rule: an int LHS with a
|
||||
// float RHS promotes to the float (`s64 * f32` → `f32`); vectors /
|
||||
// structs keep the LHS type. `inferExprType` reuses the same helper
|
||||
// so static typing agrees with the value produced here.
|
||||
const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs);
|
||||
var ty = arithResultType(lhs_ty, rhs_inferred);
|
||||
|
||||
// Auto-unwrap optional operands for arithmetic/comparison
|
||||
if (!ty.isBuiltin()) {
|
||||
@@ -14528,6 +14521,20 @@ pub const Lowering = struct {
|
||||
return ty == .f32 or ty == .f64;
|
||||
}
|
||||
|
||||
/// Result type of an arithmetic / bitwise / shift binary op over two
|
||||
/// scalar operand types. This is the single promotion rule shared by the
|
||||
/// value path (`lowerBinaryOp`) and AST-level inference
|
||||
/// (`ExprTyper.inferType`'s binary-op arm), so static typing reports
|
||||
/// exactly the type the lowered value carries. An integer LHS with a
|
||||
/// floating-point RHS promotes to the float (`s64 + f64` → `f64`); every
|
||||
/// other pairing — including vectors / structs, whose `isInt` is false —
|
||||
/// takes the LHS type. Comparison / logical ops never reach here (they
|
||||
/// are `.bool` at both sites).
|
||||
pub fn arithResultType(lhs_ty: TypeId, rhs_ty: TypeId) TypeId {
|
||||
if (isInt(lhs_ty) and isFloat(rhs_ty)) return rhs_ty;
|
||||
return lhs_ty;
|
||||
}
|
||||
|
||||
fn isInt(ty: TypeId) bool {
|
||||
return switch (ty) {
|
||||
.s8, .s16, .s32, .s64, .u8, .u16, .u32, .u64, .usize, .isize => true,
|
||||
|
||||
Reference in New Issue
Block a user