# 0055 — binary arithmetic accepts mismatched operand types (`i64 + string`)

**FIXED** (`examples/214-binop-operand-type-check.sx`). `lowerBinaryOp` in
[src/ir/lower.zig](../src/ir/lower.zig) now checks operand-type
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:

- **arithmetic** `+ - * / %` → `isArithOperand` (numeric / vector /
  pointer). Without it `i64 + string` lowered as `add : i64` and
  reinterpreted the string's bytes — garbage.
- **ordering** `< <= > >=` → `isOrderingOperand` (numeric / enum / pointer
  / bool / vector). Without it `i64 < string` fed mismatched LLVM types to
  `icmp` and tripped the verifier.
- **bitwise / shift** `& | ^ << >>` → `isBitwiseOperand` (integer / enum /
  bool / vector). Without it `i64 & 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`).
`i64 == 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

Binary arithmetic operators (`+`, `-`, `*`, `/`, `%`) perform **no
operand-type compatibility check**. `i64 + string` compiles cleanly and
runs, reinterpreting the `string` operand's bytes (pointer/len) as an
integer.

- **Observed:** `a + c` where `a: i64`, `c: string` compiles and prints a
  garbage number (e.g. `4346102832` — `40 + <string data pointer>`).
- **Expected:** a type-error diagnostic, e.g.
  `cannot apply '+' to operands of type 'i64' and 'string'`.

Surfaced in `examples/213-canonical-map.sx`: a third source `v3: StrCell`
(`VL(string)`) was added so the mapper became `(a, b, c) => a + b + c`
with `a, b: i64` and `c: string`. The canonical `map` infers the closure
params from the projected pack element types, so `a + b + c` is
`i64 + i64 + string` — which should reject. Instead `r.get()` prints
garbage (`4312977714`).

> Note: the working-tree copy of `examples/213` also contains a *separate*
> typo — `Closure(..sources.Target)` instead of `..sources.T`. `.Target`
> is an unknown projection name and produces "cannot infer type of lambda
> parameter" errors that mask this bug. Restoring `.T` exposes the real
> hole. The two are independent; this issue is about the missing arithmetic
> operand-type check, reproducible standalone with no pack machinery.

## Reproduction

Minimal, standalone (only `modules/std.sx`):

```sx
#import "modules/std.sx";

main :: () -> i32 {
    a : i64 = 40;
    c : string = "it should error";
    r := a + c;          // expected: type error (i64 + string)
    print("{}\n", r);    // actual: prints garbage, e.g. 4346102832
    0;
}
```

```
$ ./zig-out/bin/sx run repro.sx
4346102832          # should be a compile error, not a number
```

## Investigation prompt

> Binary arithmetic in the sx compiler does no operand-type compatibility
> check. `lowerBinaryOp` in [src/ir/lower.zig](src/ir/lower.zig) (starts
> ~L2545) computes the result type `ty` purely from the **LHS** operand
> (`var ty = lhs_ty;` ~L2680), then at the final `switch (bop.op)` (~L2735)
> emits `.add => self.builder.add(lhs, rhs, ty)` (and `.sub/.mul/.div/.mod`)
> with that LHS-derived `ty` — never verifying `rhs_ty` is compatible. For
> `i64 + string`, `ty = .i64` and the `string` rhs Ref is fed to an
> `add : i64`, reinterpreting its bytes as an integer.
>
> The fix: before the arithmetic `switch` arms, for the arithmetic ops
> (`add/sub/mul/div/mod`) check that `lhs_ty` and `rhs_ty` are
> arithmetic-compatible (both numeric — int/float — modulo the existing
> int×float promotion at ~L2682, or otherwise an exact match). When they
> are not, emit a diagnostic via the existing
> `if (self.diagnostics) |diags| diags.addFmt(.err, <span>, "cannot apply
> '{s}' to operands of type '{...}' and '{...}'", .{...})` pattern (see the
> diagnostics calls already in this file, e.g. ~L2175, ~L2193) and return a
> non-corrupting sentinel rather than a silently-wrong `add`.
>
> Watch the legitimate non-numeric `+` paths so the check doesn't
> false-positive: tuple ops (`lowerTupleOp`, handled earlier ~L2718),
> string `==`/`!=` (handled ~L2710), optional auto-unwrap (~L2693), and the
> int×float promotion (~L2682). Decide whether string concatenation via `+`
> is intended to be supported at all — if not, `string + string` should
> also reject (currently untested here).
>
> Span: confirm how to get the operator/expression span for the diagnostic
> — `lowerBinaryOp` takes `bop: *const ast.BinaryOp`; check whether
> `ast.BinaryOp` carries a span or whether the enclosing node's span must
> be threaded in (the other `addFmt` sites use `node.span`).
>
> Verify: re-run the repro above — expect a type-error diagnostic and a
> non-zero exit instead of a printed integer. Then restore `examples/213`'s
> `.Target` → `.T` and confirm `(a, b, c) => a + b + c` with a `string`
> third source now errors at the `+` rather than printing garbage. Run
> `bash tests/run_examples.sh` to confirm no legitimate arithmetic
> regressed.
