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).
This commit is contained in:
agra
2026-06-22 21:04:05 +03:00
parent 2637ae98a5
commit 3e8d003e3d
24 changed files with 418 additions and 13 deletions

View File

@@ -2375,11 +2375,19 @@ pub const Ops = struct {
cond = c.LLVMBuildICmp(self.e.builder, c.LLVMIntNE, cond, c.LLVMConstNull(cond_ty), "tobool");
} else if (cond_kind == c.LLVMIntegerTypeKind) {
cond = c.LLVMBuildICmp(self.e.builder, c.LLVMIntNE, cond, c.LLVMConstInt(cond_ty, 0, 0), "tobool");
} else if (cond_kind == c.LLVMStructTypeKind) {
// Struct values are always truthy
cond = c.LLVMConstInt(self.e.cached_i1, 1, 0);
} else {
cond = c.LLVMConstInt(self.e.cached_i1, 1, 0); // default truthy
// UNREACHABLE backend tripwire. A condBr condition must be i1,
// an integer, or a pointer. Anything else (a struct — e.g. an
// optional `{T,i1}` aggregate — or a float) is now rejected at
// lowering with a located type error: `checkConditionType` in
// src/ir/lower/expr.zig gates every condition site (`if` /
// `while` / `and` / `or`), and optionals are reduced to their
// has_value i1 before reaching here (issue 0164). Folding such a
// condition truthy was a silent miscompile (`if opt { }` always
// took the present branch); reaching this @panic now means a NEW
// condition site bypassed `checkConditionType` — add the check
// there, don't fold truthy.
@panic("emitCondBr: non-boolean condition reached condBr — should have been rejected at lowering as a type error (issue 0164; see checkConditionType in src/ir/lower/expr.zig)");
}
}
_ = c.LLVMBuildCondBr(self.e.builder, cond, then_bb, else_bb);