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).
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 emitself.diagnostics.addFmt(.err, span, ...). - If legal (consistent with
if opt): the coercion must lower to the optional's has_value test (reuseoptional_has_value), not a constant/garbagefalse. The silent always-falseis 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.