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

2.2 KiB
Raw Blame History

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

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