lang: reject mismatched operand types in scalar arithmetic (issue 0055)
lowerBinaryOp derived the result type from the LHS alone and emitted add/sub/mul/div/mod without checking the RHS, so `s64 + string` lowered as `add : s64` and reinterpreted the string's bytes — printing garbage instead of erroring. Add isArithOperand (int / float / vector / pointer, plus custom int widths) and, for `+ - * / %`, diagnose `cannot apply '<op>' to operands of type '<lhs>' and '<rhs>'` and return a placeholder sentinel instead of the corrupting op. `.unresolved` operands pass through so a type we couldn't infer is never falsely rejected; the existing optional-unwrap and int×float promotion are accounted for before the check. Ordering (`< <= > >=`) and bitwise/shift (`& | ^ << >>`) ops share the same LHS-derived-type hole and are left as a noted follow-up in the issue. Regression: examples/214-arith-operand-type-check.sx (s64 + string, and non-numeric LHS string * s64).
This commit is contained in:
@@ -2732,6 +2732,32 @@ 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 => {
|
||||
const eff_rhs_ty = blk: {
|
||||
if (rhs_ty == .unresolved) break :blk self.builder.getRefType(rhs);
|
||||
if (!rhs_ty.isBuiltin()) {
|
||||
const ri = self.module.types.get(rhs_ty);
|
||||
if (ri == .optional) break :blk ri.optional.child;
|
||||
}
|
||||
break :blk rhs_ty;
|
||||
};
|
||||
if (!self.isArithOperand(ty) or !self.isArithOperand(eff_rhs_ty)) {
|
||||
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");
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
return switch (bop.op) {
|
||||
.add => self.builder.add(lhs, rhs, ty),
|
||||
.sub => self.builder.sub(lhs, rhs, ty),
|
||||
@@ -14619,6 +14645,45 @@ pub const Lowering = struct {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Operands valid for a scalar numeric op (`+ - * / %`): ints (incl.
|
||||
/// custom widths), floats, SIMD vectors, and pointers (pointer
|
||||
/// arithmetic). `.unresolved` returns true so a type we couldn't infer
|
||||
/// is never diagnosed — the check only fires on a concretely
|
||||
/// incompatible operand (e.g. `string`, a struct, an enum).
|
||||
fn isArithOperand(self: *Lowering, ty: TypeId) bool {
|
||||
if (ty == .unresolved) return true;
|
||||
if (isInt(ty) or isFloat(ty)) return true;
|
||||
if (ty.isBuiltin()) return false;
|
||||
return switch (self.module.types.get(ty)) {
|
||||
.signed, .unsigned, .vector, .pointer, .many_pointer => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 {
|
||||
return switch (op) {
|
||||
.add => "+",
|
||||
.sub => "-",
|
||||
.mul => "*",
|
||||
.div => "/",
|
||||
.mod => "%",
|
||||
.eq => "==",
|
||||
.neq => "!=",
|
||||
.lt => "<",
|
||||
.lte => "<=",
|
||||
.gt => ">",
|
||||
.gte => ">=",
|
||||
.and_op => "and",
|
||||
.or_op => "or",
|
||||
.bit_and => "&",
|
||||
.bit_or => "|",
|
||||
.bit_xor => "^",
|
||||
.shl => "<<",
|
||||
.shr => ">>",
|
||||
.in_op => "in",
|
||||
};
|
||||
}
|
||||
|
||||
fn typeBits(ty: TypeId) u32 {
|
||||
return switch (ty) {
|
||||
.bool => 1,
|
||||
|
||||
Reference in New Issue
Block a user