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

2.1 KiB

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

#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.