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:
agra
2026-05-30 10:30:57 +03:00
parent 6016b08712
commit ac7f1d10e5
7 changed files with 132 additions and 50 deletions

View File

@@ -1,21 +1,33 @@
# 0055 — binary arithmetic accepts mismatched operand types (`s64 + string`)
**FIXED** (`examples/214-arith-operand-type-check.sx`). `lowerBinaryOp` in
**FIXED** (`examples/214-binop-operand-type-check.sx`). `lowerBinaryOp` in
[src/ir/lower.zig](../src/ir/lower.zig) now checks operand-type
compatibility for the scalar arithmetic ops (`+ - * / %`) before emitting:
if either operand (after the existing optional-unwrap / int×float
promotion) is not a numeric / vector / pointer type via the new
`isArithOperand`, it emits `cannot apply '<op>' to operands of type '<lhs>'
and '<rhs>'` and returns a placeholder sentinel instead of an `add` that
reinterprets the RHS bytes. `.unresolved` operands are passed through so a
type we couldn't infer is never falsely diagnosed. Regression test:
`examples/214` (covers `s64 + string` and non-numeric LHS `string * s64`).
compatibility for every scalar binary-op group before emitting, via three
predicates that pass `.unresolved` through (so a type we couldn't infer is
never falsely diagnosed) but reject a concretely incompatible operand:
Not covered by this fix (same LHS-derived-type shape, separate ops): the
ordering comparisons (`< <= > >=`) and bitwise/shift ops (`& | ^ << >>`)
have the same hole — `s64 < string` / `s64 & string` still lower without an
operand check. Left out to avoid touching enum/flags comparison + bitwise
paths in the same change; flag as a follow-up if it bites.
- **arithmetic** `+ - * / %``isArithOperand` (numeric / vector /
pointer). Without it `s64 + string` lowered as `add : s64` and
reinterpreted the string's bytes — garbage.
- **ordering** `< <= > >=``isOrderingOperand` (numeric / enum / pointer
/ bool / vector). Without it `s64 < string` fed mismatched LLVM types to
`icmp` and tripped the verifier.
- **bitwise / shift** `& | ^ << >>``isBitwiseOperand` (integer / enum /
bool / vector). Without it `s64 & string` reinterpreted the bytes.
On mismatch it emits `cannot apply '<op>' to operands of type '<lhs>' and
'<rhs>'` and returns a placeholder sentinel instead of the corrupting op.
The existing optional-unwrap and int×float promotion are applied before the
check. Legitimate mixes — flags-enum bitwise (`.read | .write`,
`perm & .read`), enum/pointer comparison, int literals — are unaffected
(covered by `examples/50-smoke.sx`). Regression test: `examples/214`
(rejects `+ * < & <<` against a `string` operand).
Still NOT covered (left deliberately): equality `==` / `!=`, whose path is
heavily special-cased (string `str_eq`, `Any` unbox, `optional == null`).
`s64 == string` still slips through. Folding a compatibility check into
that path without regressing the special cases is a separate change — open
a fresh issue if it bites.
## Symptom