lang: extend operand-type check to ordering + bitwise/shift (issue 0055 follow-up)
The arithmetic-only check from the previous commit shared a hole with the comparison and bitwise/shift ops: lowerBinaryOp derives the result type from the LHS, so `s64 < string` fed mismatched types to `icmp` (LLVM verifier failure) and `s64 & string` reinterpreted the string's bytes. Add isOrderingOperand (numeric / enum / pointer / bool / vector) and isBitwiseOperand (integer / enum / bool / vector), and route `< <= > >=` and `& | ^ << >>` through them alongside the existing arithmetic check, all sharing one diagnostic + placeholder-sentinel path. Flags-enum bitwise (`.read | .write`, `perm & .read`), enum/pointer comparison, and int literals stay legal (50-smoke unaffected). Equality `== / !=` is deliberately left unchecked — its path is heavily special-cased (str_eq, Any unbox, optional == null); folding a check in without regressing those is a separate change, noted in the issue. Regression test renamed arith→binop and broadened to cover `+ * < & <<` against a string operand: examples/214-binop-operand-type-check.sx.
This commit is contained in:
@@ -2732,12 +2732,20 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Reject scalar arithmetic on incompatible operand types (e.g.
|
||||
// `s64 + string`). The result type `ty` is derived from the LHS,
|
||||
// so without this the op lowers as `add : <lhs>` and reinterprets
|
||||
// the RHS bytes as the LHS type, silently producing garbage.
|
||||
switch (bop.op) {
|
||||
.add, .sub, .mul, .div, .mod => {
|
||||
// Reject scalar ops on incompatible operand types (e.g.
|
||||
// `s64 + string`, `s64 < string`, `s64 & string`). The result type
|
||||
// `ty` is derived from the LHS, so without this the op lowers as
|
||||
// `<op> : <lhs>` and either reinterprets the RHS bytes (arithmetic
|
||||
// / bitwise → garbage) or feeds mismatched LLVM types to `icmp`
|
||||
// (ordering → verifier failure).
|
||||
{
|
||||
const group: enum { none, arith, ordering, bitwise } = switch (bop.op) {
|
||||
.add, .sub, .mul, .div, .mod => .arith,
|
||||
.lt, .lte, .gt, .gte => .ordering,
|
||||
.bit_and, .bit_or, .bit_xor, .shl, .shr => .bitwise,
|
||||
else => .none,
|
||||
};
|
||||
if (group != .none) {
|
||||
const eff_rhs_ty = blk: {
|
||||
if (rhs_ty == .unresolved) break :blk self.builder.getRefType(rhs);
|
||||
if (!rhs_ty.isBuiltin()) {
|
||||
@@ -2746,16 +2754,21 @@ pub const Lowering = struct {
|
||||
}
|
||||
break :blk rhs_ty;
|
||||
};
|
||||
if (!self.isArithOperand(ty) or !self.isArithOperand(eff_rhs_ty)) {
|
||||
const ok = switch (group) {
|
||||
.arith => self.isArithOperand(ty) and self.isArithOperand(eff_rhs_ty),
|
||||
.ordering => self.isOrderingOperand(ty) and self.isOrderingOperand(eff_rhs_ty),
|
||||
.bitwise => self.isBitwiseOperand(ty) and self.isBitwiseOperand(eff_rhs_ty),
|
||||
.none => true,
|
||||
};
|
||||
if (!ok) {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, bop.lhs.span, "cannot apply '{s}' to operands of type '{s}' and '{s}'", .{
|
||||
binOpSymbol(bop.op), self.formatTypeName(ty), self.formatTypeName(eff_rhs_ty),
|
||||
});
|
||||
}
|
||||
return self.emitPlaceholder("arith-type-mismatch");
|
||||
return self.emitPlaceholder("operand-type-mismatch");
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
return switch (bop.op) {
|
||||
@@ -14660,6 +14673,35 @@ pub const Lowering = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Operands valid for ordering comparisons (`< <= > >=`): numbers
|
||||
/// (incl. custom int widths), enums (ordinal), pointers (address
|
||||
/// order), bool, and SIMD vectors. NOT strings (no lexicographic `<`
|
||||
/// lowering exists) or any other aggregate. `.unresolved` passes so an
|
||||
/// un-inferable operand is never falsely diagnosed.
|
||||
fn isOrderingOperand(self: *Lowering, ty: TypeId) bool {
|
||||
if (ty == .unresolved) return true;
|
||||
if (isInt(ty) or isFloat(ty) or ty == .bool) return true;
|
||||
if (ty.isBuiltin()) return false;
|
||||
return switch (self.module.types.get(ty)) {
|
||||
.signed, .unsigned, .@"enum", .pointer, .many_pointer, .vector => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Operands valid for bitwise/shift ops (`& | ^ << >>`): integers
|
||||
/// (incl. custom widths), enums (flags are int-backed), bool, and SIMD
|
||||
/// vectors. NOT floats, strings, pointers, or aggregates. `.unresolved`
|
||||
/// passes (see `isOrderingOperand`).
|
||||
fn isBitwiseOperand(self: *Lowering, ty: TypeId) bool {
|
||||
if (ty == .unresolved) return true;
|
||||
if (isInt(ty) or ty == .bool) return true;
|
||||
if (ty.isBuiltin()) return false;
|
||||
return switch (self.module.types.get(ty)) {
|
||||
.signed, .unsigned, .@"enum", .vector => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 {
|
||||
return switch (op) {
|
||||
.add => "+",
|
||||
|
||||
Reference in New Issue
Block a user