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:
agra
2026-05-30 09:56:32 +03:00
parent 8e74e4acb2
commit 6016b08712
5 changed files with 197 additions and 0 deletions

View File

@@ -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,