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:
29
examples/optionals/0908-if-optional-no-binding.sx
Normal file
29
examples/optionals/0908-if-optional-no-binding.sx
Normal file
@@ -0,0 +1,29 @@
|
||||
// Bindingless `if <optional>` must test the optional's has_value flag, not
|
||||
// fold the `{T,i1}` aggregate truthy. Covers struct-form optionals (`?i64`,
|
||||
// `?P`) in both states, exercising BOTH branches — without any `|x|` binding.
|
||||
//
|
||||
// Regression (issue 0164): a bare `if opt { }` over a struct-repr optional
|
||||
// emitted `br i1 true`, so a null optional took the present branch.
|
||||
#import "modules/std.sx";
|
||||
|
||||
P :: struct { x: i64; }
|
||||
|
||||
check_i64 :: (n: ?i64) {
|
||||
if n { print("i64 present\n"); } else { print("i64 absent\n"); }
|
||||
}
|
||||
|
||||
check_p :: (p: ?P) {
|
||||
if p { print("P present\n"); } else { print("P absent\n"); }
|
||||
}
|
||||
|
||||
main :: () {
|
||||
a : ?i64 = null;
|
||||
b : ?i64 = 42;
|
||||
check_i64(a); // i64 absent
|
||||
check_i64(b); // i64 present
|
||||
|
||||
p_null : ?P = null;
|
||||
p_some : ?P = P.{ x = 9 };
|
||||
check_p(p_null); // P absent
|
||||
check_p(p_some); // P present
|
||||
}
|
||||
33
examples/optionals/0909-optionals-while-no-binding.sx
Normal file
33
examples/optionals/0909-optionals-while-no-binding.sx
Normal file
@@ -0,0 +1,33 @@
|
||||
// Bindingless `while <optional>` must test the optional's has_value flag,
|
||||
// exactly like `if <optional>` — not fold the `{T,i1}` aggregate truthy.
|
||||
//
|
||||
// Regression (issue 0164): a bare optional loop condition emitted `br i1 true`,
|
||||
// so the loop never observed `null` and either spun forever (present→drained)
|
||||
// or ran when it should have been skipped (already-null).
|
||||
#import "modules/std.sx";
|
||||
|
||||
main :: () {
|
||||
// Present `?i64` drained to null: the body runs each step until the
|
||||
// optional becomes null, then the loop stops.
|
||||
countdown : ?i64 = 3;
|
||||
runs := 0;
|
||||
while countdown {
|
||||
runs = runs + 1;
|
||||
v := countdown!; // unwrap the present value
|
||||
if v <= 1 {
|
||||
countdown = null; // drain → loop must STOP next header eval
|
||||
} else {
|
||||
countdown = v - 1;
|
||||
}
|
||||
}
|
||||
print("drain runs={}\n", runs); // 3
|
||||
|
||||
// Already-null `?i64`: the body must NOT run at all.
|
||||
empty : ?i64 = null;
|
||||
skipped := 0;
|
||||
while empty {
|
||||
skipped = skipped + 1;
|
||||
empty = null;
|
||||
}
|
||||
print("skip runs={}\n", skipped); // 0
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// `and` / `or` over bare optional operands reduce each operand to its
|
||||
// has_value flag (presence-as-truth), exactly like `if <optional>`. A bare
|
||||
// optional reaching the short-circuit merge as a `{T,i1}` aggregate used to
|
||||
// fold truthy (issue 0164); it must instead test presence.
|
||||
//
|
||||
// Truth table below: present `?i64` is "true", absent (null) is "false".
|
||||
#import "modules/std.sx";
|
||||
|
||||
yn :: (b: bool) -> string => if b then "T" else "F";
|
||||
|
||||
main :: () {
|
||||
p : ?i64 = 7; // present → truthy
|
||||
q : ?i64 = null; // absent → falsy
|
||||
|
||||
// and: true only when BOTH present.
|
||||
print("PP and = {}\n", yn(p and p)); // T
|
||||
print("PQ and = {}\n", yn(p and q)); // F (rhs absent)
|
||||
print("QP and = {}\n", yn(q and p)); // F (lhs absent, short-circuits)
|
||||
print("QQ and = {}\n", yn(q and q)); // F
|
||||
|
||||
// or: false only when BOTH absent.
|
||||
print("PP or = {}\n", yn(p or p)); // T
|
||||
print("PQ or = {}\n", yn(p or q)); // T (lhs present, short-circuits)
|
||||
print("QP or = {}\n", yn(q or p)); // T (rhs present)
|
||||
print("QQ or = {}\n", yn(q or q)); // F
|
||||
|
||||
// Mixed optional-and-bool: optional operand reduced to has_value,
|
||||
// bool operand used as-is.
|
||||
flag := true;
|
||||
print("Pb and = {}\n", yn(p and flag)); // T
|
||||
print("Qb and = {}\n", yn(q and flag)); // F (lhs absent)
|
||||
print("bQ or = {}\n", yn(flag or q)); // T (lhs true, short-circuits)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
i64 absent
|
||||
i64 present
|
||||
P absent
|
||||
P present
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
drain runs=3
|
||||
skip runs=0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
PP and = T
|
||||
PQ and = F
|
||||
QP and = F
|
||||
QQ and = F
|
||||
PP or = T
|
||||
PQ or = T
|
||||
QP or = T
|
||||
QQ or = F
|
||||
Pb and = T
|
||||
Qb and = F
|
||||
bQ or = T
|
||||
Reference in New Issue
Block a user