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).
3.4 KiB
0164 — if <optional> with no binding silently folds the has_value test to true
RESOLVED. Two colluding sites:
lowerIfExpremittedoptional_has_valueonly for the binding form, andemitCondBr's catch-all struct arm silently folded any non-i1 condition toi1 true("structs always truthy"). Fix: reduce a bindingless optional condition tooptional_has_valueinlowerIfExpr/lowerWhile, add a sharedlowerBoolConditionhelper forand/oroperands (the same defect affectedwhile/and/or), and add a lowering-time diagnostic (checkConditionType/isValidConditionTypeinlower/expr.zig) rejecting conditions whose type isn't bool/integer/pointer/optional — turning theemitCondBrsilent-true into a real type error and leaving the backend@panicas an unreachable tripwire. Regressions:examples/optionals/0908-if-optional-no-binding.sx,0909-optionals-while-no-binding.sx,0910-optionals-and-or-optional-operands.sx,examples/diagnostics/1194-diagnostics-condition-non-bool-type.sx(negative). Verified by 3 + 3 adversarial reviews. (Adjacent bugs found during review and filed separately: 0168 array-of-optionals element load, 0169 optional→bool coercion, 0170 closure-optional layout.)
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
#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 69–72) emitsoptional_has_valueonly whenie.binding_name != null. For a bindinglessif opt, it passescond = opt_val(the raw{T,i1}aggregate) straight tocondBr. Fix: emitoptional_has_valuewhenever the condition's resolved type is an optional, binding or not.src/backend/llvm/ops.zig(emitCondBr, ~lines 2378–2383) has a catch-allelse/struct arm that doescond = 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 silenttrue.
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.