Files
sx/issues/0169-optional-to-bool-coercion-silent-false.md
agra 3e8d003e3d fix: bindingless if/while/and/or over optional reads has_value (issue 0164)
lowerIfExpr emitted optional_has_value only for the binding form; a bare
'if opt' passed the raw {T,i1} aggregate to condBr, where emitCondBr's
catch-all struct arm silently folded it to 'i1 true' (structs always
truthy) — a silent miscompile that took the present-branch for null
optionals. while / and / or shared the same defect.

Reduce bindingless optional conditions to optional_has_value in
lowerIfExpr/lowerWhile and via a new lowerBoolCondition helper for and/or
operands. Replace the silent-true emitCondBr arm with a lowering-time
diagnostic (checkConditionType/isValidConditionType) rejecting conditions
whose type isn't bool/integer/pointer/optional; the backend @panic is now
an unreachable tripwire.

Regressions: examples/optionals/0908..0910 + diagnostics/1194 (negative).
Verified by 3+3 adversarial reviews.

Filed adjacent bugs found during review: 0168 (array-of-optionals element
load), 0169 (optional->bool coercion), 0170 (closure-optional layout).
2026-06-22 21:04:05 +03:00

49 lines
2.1 KiB
Markdown

# 0169 — optional passed where `bool` is expected silently coerces to `false` (always)
## Symptom
Passing an optional (`?T`) to a `bool` parameter (or any bool-typed position:
bool field initializer, `-> bool` return) compiles WITHOUT a diagnostic and
silently yields `false` for EVERY optional — present or null alike. Silent
miscompile.
This is inconsistent with `if opt` / `while opt`, which (after issue 0164)
correctly test the has_value flag. The argument/field-coercion path does not.
## Reproduction
```sx
#import "modules/std.sx";
takes_bool :: (b: bool) { if b { print("true\n"); } else { print("false\n"); } }
main :: () {
a : ?i64 = 42;
n : ?i64 = null;
takes_bool(a); // prints "false" — should be a type error, or "true" (present)
takes_bool(n); // prints "false" — (correct only by accident)
}
```
Expected: EITHER a compile-time type error (no implicit optional→bool
coercion), OR — if implicit coercion is intended — `true` for the present
optional and `false` for null, matching `if opt` semantics. Observed: always
`false`, no diagnostic.
## Investigation prompt
Decide the intended semantics first (check `specs.md` for whether optional→bool
is a legal implicit coercion):
- If NOT legal: the call-argument / assignment type-checker
(`src/ir/expr_typer.zig` / the coercion/check path) must REJECT an optional in
a bool-typed position with a located diagnostic — not silently produce a
zero/false. Find where the bool target type accepts the optional operand and
emit `self.diagnostics.addFmt(.err, span, ...)`.
- If legal (consistent with `if opt`): the coercion must lower to the
optional's has_value test (reuse `optional_has_value`), not a constant/garbage
`false`. The silent always-`false` is the rejected silent-fallback pattern.
Whichever is correct, the current always-`false` is wrong. Verify with the repro
(present → `true` or a type error; null → `false` or a type error). Add a
regression: an `examples/optionals/09xx-...` if coercion is legal, or a
`examples/diagnostics/11xx-...` negative test if it's rejected.