Files
sx/issues/0199-any-eq-concrete-llvm-verify-fail.md
agra b322dcfe61 fix: type-safe stores + Any unbox/eq; finish multi-return deferrals
Type-checking gaps (segfault/corruption → compile errors):

- 0197: reject a store into an annotated slot whose value has no modeled
  coercion AND a different byte width (a 16-byte string into a 4-byte i32
  overran the slot and segfaulted). New checkAssignable / noneReinterpretIsUnsafe
  (coerce.zig, width via the LLVM-accurate typeSizeBytes) wired into every store
  site: var/const-decl, single + multi assignment (identifier/field/index/
  element/deref), named-return defaults. Same-width reinterpretations (*T→[*]T,
  i64→isize, fn-ref) and explicit xx/cast stay allowed; cascades suppressed via
  externalErrorsExist. Examples 1205, 1206.
- 0198: an implicit `Any → T` unbox is now a compile error (it blindly
  reinterpreted the boxed payload — silent garbage for a wrong scalar, a segfault
  for an aggregate). xx and compiler-generated match/pack unboxes are unaffected.
  Example 1207.
- 0199: `Any == <concrete>` (one operand Any) aborted the LLVM verifier — the
  comparison arm now fires when either operand is Any, boxing the concrete side
  first. Example 0654.

Multi-return deferrals (PLAN-MULTIRET #6 + named-order + D3 + generic):

- Reorder named return elements by name instead of requiring slot order; error on
  unknown/duplicate/missing (value-only AND full-failable-tuple forms). Examples
  0210, 0214.
- Reject a bare-paren (A, B) multi-return signature in generic-arg position
  (return-position-only). Example 0215.
- Multi-return closure types / lambda literals work via the reused tuple
  machinery (destructure, single-bind+field, lambda arg). Example 0216.
- Generic multi-return: positional works (0217); 0200: the named-slot
  implicit-return form now works for generic free fns + struct methods —
  monomorphizeFunction now calls bindNamedReturnSlots. Example 0218.

readme.md documents the annotated-store coercion rule; CHECKPOINT-MULTIRET.md
updated. Full corpus green (850/0).
2026-06-27 17:28:27 +03:00

62 lines
3.1 KiB
Markdown

> **RESOLVED** (2026-06-27). Fix: the `Any`-shaped `==`/`!=` arm in
> `src/ir/lower/expr.zig` now fires when EITHER operand is `.any` (was both). A
> concrete operand is boxed to `Any` (`builder.boxAny`) first, so both sides are
> 16-byte boxes; then both unbox to their `.i64` value words and compare — the
> same value-identity the both-`Any` path uses (tags not compared). An
> already-errored `.unresolved` / `.void` operand falls through (no cascade).
> Verified: `x == 5`, `x == 6`, `x != 6`, `5 == x` (reversed), bool `Any`, and the
> both-`Any` form all work; no verifier abort. Regression test:
> `examples/comptime/0654-comptime-any-eq-concrete.sx`. (Aggregate-`Any`
> comparison still uses value-word identity — the same limitation the both-`Any`
> path always had; orthogonal to this verifier fix.)
# 0199 — `Any == <concrete>` (one operand `Any`) fails LLVM verification
**Symptom** — An equality / inequality comparison where exactly ONE operand is
`Any` and the other is a concrete type is not handled: it falls through to a
plain `icmp` on a 16-byte `{tag, value}` aggregate vs a scalar and aborts the
LLVM verifier.
- Observed: `x : Any = 5; if x == 5 { ... }`
`error: Both operands to ICmp are not of the same type! {i64,i64} vs i64`,
`LLVM verification failed`, exit 1 (loud — not a segfault / silent miscompile).
- Expected: either box the concrete operand to `Any` (then compare as `Any ==
Any`, the path that already works) consulting the tag, OR a clean located
compile diagnostic (e.g. "compare an 'Any' against a value of its boxed type,
or `xx` the Any first"). Not an LLVM verifier abort.
Distinct from issue 0198 (the implicit `Any → T` unbox). Surfaced by the
adversarial review of the 0198 fix. `Any == Any` works correctly.
## Reproduction
```sx
#import "modules/std.sx";
main :: () -> i64 {
x : Any = 5;
if x == 5 { return 1; } // error: ICmp operand type mismatch {i64,i64} vs i64
return 0;
}
```
`./zig-out/bin/sx run repro.sx` → `LLVM verification failed`, exit 1.
## Investigation prompt
The `Any` equality path is in `src/ir/lower/expr.zig` (~3201-3215), gated on
`lhs_ty == .any and rhs_ty == .any` — it `unbox_any`s both sides to `.i64` and
`cmp_eq`s the value words. When only ONE side is `.any`, that guard is false and
the comparison falls through to the generic numeric/`icmp` path, which emits an
`icmp` between the 16-byte `Any` aggregate and the scalar → verifier abort.
The fix likely adds a mixed-operand arm: when exactly one operand is `.any` and
the other is a concrete type `T`, box the concrete operand to `Any`
(`self.builder.boxAny(concrete, T)`) and reuse the existing `Any == Any`
value-word comparison — OR, if comparing only the payload word is unsound across
types (a `5:i64` and a `5.0:f64` would compare equal by bits), gate on the tag
too / emit a diagnostic. Decide whether `Any == concrete` should compare by
(tag AND value) or be disallowed; mirror whatever `Any == Any` semantics are
documented. Verify: the repro compiles and `x == 5` is true, OR a clean
diagnostic is emitted — never an LLVM verifier abort.