Files
sx/issues/0164-if-optional-no-binding-folds-true.md
agra 2637ae98a5 docs: file issues 0164-0167 (optional/comptime bugs found during 0162 review)
0164 if <optional> no-binding folds has_value to true (silent miscompile)
0165 parenthesized nested optional ?(?T) malformed double-wrap (crash)
0166 ?? .{ } struct-literal default unresolved type (crash)
0167 comptime regToValue array-in-aggregate gap + unclean recovery
2026-06-22 19:43:55 +03:00

54 lines
2.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 0164 — `if <optional>` with no binding silently folds the has_value test to `true`
## Symptom
Branching on an optional **without a binding** (`if opt { ... }`) takes the
present-branch unconditionally for any optional whose LLVM representation is a
struct (`?i64`, `?T`, `?f64`, …). The has_value flag is never read — the IR
emits `br i1 true`. SILENT MISCOMPILE (no diagnostic, wrong runtime result).
Pointer-sentinel optionals (`?cstring`, `?*T`, `?Closure`) are unaffected — they
lower to a bare `ptr` and hit the correct `icmp` path. The `if opt |x| { ... }`
*binding* form is also correct (it emits `optional_has_value`).
Observed vs expected: the repro prints `present` for a null optional; expected
`absent`.
## Reproduction
```sx
#import "modules/std.sx";
check :: (n: ?i64) { if n { print("present\n"); } else { print("absent\n"); } }
main :: () {
a : ?i64 = null;
b : ?i64 = 42;
check(a); // prints "present" — WRONG, expected "absent"
check(b); // prints "present" — correct
}
```
Reproduces identically for function-return init, literal-`null` init,
literal-value init, and param-passed optionals — universal, not init-path
specific.
## Investigation prompt
Two sites collude:
- `src/ir/lower/control_flow.zig` (`lowerIfExpr`, ~lines 6972) emits
`optional_has_value` **only when `ie.binding_name != null`**. For a bindingless
`if opt`, it passes `cond = opt_val` (the raw `{T,i1}` aggregate) straight to
`condBr`. Fix: emit `optional_has_value` whenever the condition's resolved type
is an optional, binding or not.
- `src/backend/llvm/ops.zig` (`emitCondBr`, ~lines 23782383) has a catch-all
`else`/struct arm that does `cond = LLVMConstInt(i1, 1, 0)` with the comment
"Struct values are always truthy". This is exactly the REJECTED
silent-fallback pattern (see CLAUDE.md). After the lowering fix, make this arm
a LOUD bail (a non-i1, non-pointer condition reaching condBr is a compiler
bug) rather than a silent `true`.
Verify: the repro prints `absent` / `present`; check the IR no longer contains
`br i1 true` for the optional condition. Add an
`examples/optionals/09xx-if-optional-no-binding.sx` regression covering null
and present `?i64`/`?T` without a binding, both branches.