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:
@@ -0,0 +1,16 @@
|
|||||||
|
// A branch condition (`if` / `while` / `and` / `or`) must reduce to an i1:
|
||||||
|
// its type must be a bool, integer, pointer, or optional. A struct (or float,
|
||||||
|
// etc.) has no truthiness — it used to be silently folded truthy at lowering
|
||||||
|
// then `@panic` in the LLVM backend (issue 0164). It must instead be a clean,
|
||||||
|
// located compile-time TYPE error.
|
||||||
|
//
|
||||||
|
// Negative test: locks the new diagnostic. `if <struct>` is rejected.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
S :: struct { x: i64; }
|
||||||
|
|
||||||
|
main :: () -> i64 {
|
||||||
|
s : S = .{ x = 1 };
|
||||||
|
if s { return 1; } // ERROR: condition must be a bool, integer, pointer, or optional
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: condition must be a bool, integer, pointer, or optional, but has type 'S'
|
||||||
|
--> examples/diagnostics/1194-diagnostics-condition-non-bool-type.sx:14:8
|
||||||
|
|
|
||||||
|
14 | if s { return 1; } // ERROR: condition must be a bool, integer, pointer, or optional
|
||||||
|
| ^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
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
|
||||||
@@ -1,5 +1,23 @@
|
|||||||
# 0164 — `if <optional>` with no binding silently folds the has_value test to `true`
|
# 0164 — `if <optional>` with no binding silently folds the has_value test to `true`
|
||||||
|
|
||||||
|
> **RESOLVED.** Two colluding sites: `lowerIfExpr` emitted `optional_has_value`
|
||||||
|
> only for the binding form, and `emitCondBr`'s catch-all struct arm silently
|
||||||
|
> folded any non-i1 condition to `i1 true` ("structs always truthy"). Fix: reduce
|
||||||
|
> a bindingless optional condition to `optional_has_value` in `lowerIfExpr`/
|
||||||
|
> `lowerWhile`, add a shared `lowerBoolCondition` helper for `and`/`or` operands
|
||||||
|
> (the same defect affected `while`/`and`/`or`), and add a lowering-time
|
||||||
|
> diagnostic (`checkConditionType`/`isValidConditionType` in `lower/expr.zig`)
|
||||||
|
> rejecting conditions whose type isn't bool/integer/pointer/optional — turning
|
||||||
|
> the `emitCondBr` silent-true into a real type error and leaving the backend
|
||||||
|
> `@panic` as 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
|
## Symptom
|
||||||
|
|
||||||
Branching on an optional **without a binding** (`if opt { ... }`) takes the
|
Branching on an optional **without a binding** (`if opt { ... }`) takes the
|
||||||
|
|||||||
45
issues/0168-array-of-optionals-element-load-broken.md
Normal file
45
issues/0168-array-of-optionals-element-load-broken.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 0168 — indexing an array of optionals `[N]?T` produces a wrong/garbage element (segfault or wrong value)
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Reading an element of an array whose element type is an optional (`[N]?T`) is
|
||||||
|
broken: depending on how the result is used it either SEGFAULTS or yields the
|
||||||
|
WRONG value (reads a present element as absent). Independent of issue 0164 (the
|
||||||
|
`if`-on-optional fix) — reproduces with a plain `??` and with a copy-to-local.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
main :: () {
|
||||||
|
arr : [2]?i64 = .{ null, 7 };
|
||||||
|
|
||||||
|
// (1) index result used directly → SEGFAULT (exit 134)
|
||||||
|
x := arr[1];
|
||||||
|
print("{}\n", x ?? -1); // expected: 7
|
||||||
|
|
||||||
|
// (2) copy element to a local then test → WRONG VALUE
|
||||||
|
e := arr[1]; // element 1 is present (7)
|
||||||
|
if e { print("present\n"); } else { print("absent\n"); } // prints "absent" — WRONG
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: element 1 is the present optional `7`. Observed: segfault in case
|
||||||
|
(1); `absent` (a wrong/absent optional) in case (2). The original surfacing form
|
||||||
|
`if arr[0] { ... }` also segfaults.
|
||||||
|
|
||||||
|
## Investigation prompt
|
||||||
|
|
||||||
|
The element load / addressing for an array of optionals appears to compute the
|
||||||
|
wrong element stride or mis-materialize the loaded `{T,i1}` optional aggregate.
|
||||||
|
Suspect the index/element-load lowering (`src/ir/lower/expr.zig` index-get path,
|
||||||
|
and `src/backend/llvm/ops.zig` `emitIndexGet`) when the element type is an
|
||||||
|
optional aggregate `{T,i1}` — check that the element size/alignment used for the
|
||||||
|
GEP matches the optional's real size (cf. the `size_of` vs `typeSizeBytes`
|
||||||
|
nuance for optionals), and that the loaded value is the full aggregate, not a
|
||||||
|
truncated/garbage read. Compare against a working `[N]T` (non-optional) array
|
||||||
|
load to isolate whether it's stride math or aggregate materialization.
|
||||||
|
|
||||||
|
Verify: case (1) prints `7`, case (2) prints `present`; also test `[N]?T` with a
|
||||||
|
struct payload (`[2]?Pt`) and writing elements then reading them back. Add an
|
||||||
|
`examples/optionals/09xx-array-of-optionals.sx` regression.
|
||||||
48
issues/0169-optional-to-bool-coercion-silent-false.md
Normal file
48
issues/0169-optional-to-bool-coercion-silent-false.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#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
|
||||||
|
emit `self.diagnostics.addFmt(.err, span, ...)`.
|
||||||
|
- If legal (consistent with `if opt`): the coercion must lower to the
|
||||||
|
optional's has_value test (reuse `optional_has_value`), not a constant/garbage
|
||||||
|
`false`. The silent always-`false` is 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.
|
||||||
47
issues/0170-closure-optional-layout-truncated.md
Normal file
47
issues/0170-closure-optional-layout-truncated.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# 0170 — closure optional `?(() -> ...)` alloca is sized one word, truncating the `{fn,env}` closure value
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
An optional of a closure (`?(() -> void)`, `?Fn`) is mis-laid-out: the optional
|
||||||
|
alloca is typed `{ {ptr}, i1 }` (one pointer word + flag) but a closure value is
|
||||||
|
two words `{ {ptr, ptr}, i1 }` = `{ {fn, env}, has }`. Storing the two-word
|
||||||
|
closure constant into the one-word-typed alloca truncates it; reading the
|
||||||
|
has_value flag (`extractvalue …, 1`) then returns the closure's `env` word
|
||||||
|
(commonly null → 0 → `i1 false`) instead of the real flag. A PRESENT closure
|
||||||
|
optional therefore tests as ABSENT. Silent miscompile.
|
||||||
|
|
||||||
|
Independent of issue 0164 (the condition-reduction fix): `g != null` is also
|
||||||
|
wrong, so it's a layout/representation bug, not a truthiness bug.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
main :: () {
|
||||||
|
g : ?(() -> void) = () { print("called\n"); };
|
||||||
|
if g { print("present\n"); } else { print("absent\n"); } // prints "absent" — WRONG
|
||||||
|
if g != null { print("nn-present\n"); } else { print("nn-absent\n"); } // "nn-absent" — WRONG
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `present` / `nn-present` (a freshly-assigned closure is present).
|
||||||
|
Observed: `absent` / `nn-absent`, exit 0.
|
||||||
|
|
||||||
|
## Investigation prompt
|
||||||
|
|
||||||
|
The optional-of-closure type lowering uses the wrong child layout — it sizes the
|
||||||
|
optional payload as a single pointer rather than the closure's `{fn, env}` fat
|
||||||
|
value. Suspect the optional type lowering / `toLLVMType` for `?Closure`
|
||||||
|
(`src/ir/types.zig` optional lowering + `src/backend/llvm/types.zig`), and the
|
||||||
|
`optional_wrap` / has_value codegen (`src/backend/llvm/ops.zig`
|
||||||
|
`emitOptionalWrap` / `emitOptionalHasValue`) — the payload offset/size and the
|
||||||
|
has_value flag offset must use the closure's full 2-word size. Compare against
|
||||||
|
the `?Closure` handling the comptime VM already documents (issue 0162's fix
|
||||||
|
notes a `?Closure` sentinel/`{fn,env}` layout). Decide the canonical runtime
|
||||||
|
repr (sentinel fn-ptr-null vs discriminated `{ {fn,env}, i1 }`) and make alloca
|
||||||
|
size, store, and has_value read all agree.
|
||||||
|
|
||||||
|
Verify: the repro prints `present` / `nn-present`; calling through the unwrapped
|
||||||
|
closure (`g!()`) prints `called`; a null `?Fn` tests absent. Add an
|
||||||
|
`examples/optionals/09xx-closure-optional.sx` regression (present + null +
|
||||||
|
call-through).
|
||||||
@@ -2375,11 +2375,19 @@ pub const Ops = struct {
|
|||||||
cond = c.LLVMBuildICmp(self.e.builder, c.LLVMIntNE, cond, c.LLVMConstNull(cond_ty), "tobool");
|
cond = c.LLVMBuildICmp(self.e.builder, c.LLVMIntNE, cond, c.LLVMConstNull(cond_ty), "tobool");
|
||||||
} else if (cond_kind == c.LLVMIntegerTypeKind) {
|
} else if (cond_kind == c.LLVMIntegerTypeKind) {
|
||||||
cond = c.LLVMBuildICmp(self.e.builder, c.LLVMIntNE, cond, c.LLVMConstInt(cond_ty, 0, 0), "tobool");
|
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 {
|
} 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);
|
_ = c.LLVMBuildCondBr(self.e.builder, cond, then_bb, else_bb);
|
||||||
|
|||||||
@@ -2008,6 +2008,8 @@ pub const Lowering = struct {
|
|||||||
pub const asmResultType = lower_expr.asmResultType;
|
pub const asmResultType = lower_expr.asmResultType;
|
||||||
pub const refCapturePointee = lower_expr.refCapturePointee;
|
pub const refCapturePointee = lower_expr.refCapturePointee;
|
||||||
pub const lowerBinaryOp = lower_expr.lowerBinaryOp;
|
pub const lowerBinaryOp = lower_expr.lowerBinaryOp;
|
||||||
|
pub const lowerBoolCondition = lower_expr.lowerBoolCondition;
|
||||||
|
pub const checkConditionType = lower_expr.checkConditionType;
|
||||||
pub const lowerTupleOp = lower_expr.lowerTupleOp;
|
pub const lowerTupleOp = lower_expr.lowerTupleOp;
|
||||||
pub const lowerTupleLexCompare = lower_expr.lowerTupleLexCompare;
|
pub const lowerTupleLexCompare = lower_expr.lowerTupleLexCompare;
|
||||||
pub const lowerTupleMembership = lower_expr.lowerTupleMembership;
|
pub const lowerTupleMembership = lower_expr.lowerTupleMembership;
|
||||||
|
|||||||
@@ -66,10 +66,30 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
|
|||||||
self.target_type = null;
|
self.target_type = null;
|
||||||
const opt_val = self.lowerExpr(ie.condition);
|
const opt_val = self.lowerExpr(ie.condition);
|
||||||
self.target_type = saved_cond_target;
|
self.target_type = saved_cond_target;
|
||||||
const cond = if (ie.binding_name != null) blk: {
|
// Whenever the condition is an optional we must test its has_value flag,
|
||||||
// The condition is an optional — emit has_value check
|
// not the optional aggregate itself. This holds with OR without a binding:
|
||||||
break :blk self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool);
|
// a bare `if opt { }` must read has_value too (else the `{T,i1}` struct
|
||||||
} else opt_val;
|
// reaches condBr and gets folded truthy — issue 0164). `optional_has_value`
|
||||||
|
// handles every optional repr (struct `{T,i1}`, `?Closure` {fn,env},
|
||||||
|
// pointer-sentinel `?*T`/`?cstring`), so this is uniform across all of them.
|
||||||
|
const cond_ty = self.inferExprType(ie.condition);
|
||||||
|
const cond_is_optional = blk: {
|
||||||
|
if (ie.binding_name != null) break :blk true;
|
||||||
|
if (cond_ty.isBuiltin()) break :blk false;
|
||||||
|
break :blk self.module.types.get(cond_ty) == .optional;
|
||||||
|
};
|
||||||
|
// A bare `if <expr> { }` (no binding) must have a condition type that can
|
||||||
|
// be tested as an i1 (bool/integer/pointer/optional). Anything else — a
|
||||||
|
// struct, float, etc. — used to be folded truthy then `@panic` in the
|
||||||
|
// backend (issue 0164); reject it here with a located type error. With a
|
||||||
|
// binding (`if v := opt`) the condition is required to be an optional, so
|
||||||
|
// the optional reduction below applies and we skip the bare-cond check.
|
||||||
|
const cond = if (cond_is_optional)
|
||||||
|
self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool)
|
||||||
|
else if (self.checkConditionType(cond_ty, ie.condition.span))
|
||||||
|
opt_val
|
||||||
|
else
|
||||||
|
self.builder.constBool(false);
|
||||||
const has_else = ie.else_branch != null;
|
const has_else = ie.else_branch != null;
|
||||||
// If-else produces a value when inline OR when in value position (force_block_value)
|
// If-else produces a value when inline OR when in value position (force_block_value)
|
||||||
var is_value = (ie.is_inline or self.force_block_value) and has_else;
|
var is_value = (ie.is_inline or self.force_block_value) and has_else;
|
||||||
@@ -220,7 +240,23 @@ pub fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref {
|
|||||||
|
|
||||||
// Header: evaluate condition
|
// Header: evaluate condition
|
||||||
self.builder.switchToBlock(header_bb);
|
self.builder.switchToBlock(header_bb);
|
||||||
const cond = self.lowerExpr(we.condition);
|
const cond_val = self.lowerExpr(we.condition);
|
||||||
|
// A bare optional loop condition (`while opt { }`) must test has_value,
|
||||||
|
// exactly like `if opt { }` — otherwise the `{T,i1}` aggregate reaches
|
||||||
|
// condBr and folds truthy (issue 0164). `optional_has_value` covers every
|
||||||
|
// optional repr (struct / `?Closure` / pointer-sentinel). A non-condition
|
||||||
|
// type (struct/float/...) is a located type error (same as `if`), not a
|
||||||
|
// backend `@panic`.
|
||||||
|
const cond = blk: {
|
||||||
|
const cond_ty = self.inferExprType(we.condition);
|
||||||
|
if (!cond_ty.isBuiltin() and self.module.types.get(cond_ty) == .optional) {
|
||||||
|
break :blk self.builder.emit(.{ .optional_has_value = .{ .operand = cond_val } }, .bool);
|
||||||
|
}
|
||||||
|
if (!self.checkConditionType(cond_ty, we.condition.span)) {
|
||||||
|
break :blk self.builder.constBool(false);
|
||||||
|
}
|
||||||
|
break :blk cond_val;
|
||||||
|
};
|
||||||
self.builder.condBr(cond, body_bb, &.{}, exit_bb, &.{});
|
self.builder.condBr(cond, body_bb, &.{}, exit_bb, &.{});
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
|
|||||||
@@ -2745,16 +2745,76 @@ pub fn refCapturePointee(self: *Lowering, node: *const Node) ?TypeId {
|
|||||||
return if (info == .pointer) info.pointer.pointee else null;
|
return if (info == .pointer) info.pointer.pointee else null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is `ty` a type that may be used directly as a runtime branch condition?
|
||||||
|
/// A condBr (and the short-circuit `and`/`or` merges) ultimately tests an
|
||||||
|
/// i1: lowering must therefore reduce the condition to something the backend
|
||||||
|
/// can compare against zero/null. The acceptable categories all lower to an
|
||||||
|
/// LLVM integer or pointer (or, for `.optional`, are reduced to their
|
||||||
|
/// has_value i1 by the caller):
|
||||||
|
/// • bool / integers (signed/unsigned/usize/isize)
|
||||||
|
/// • integer-backed nominals: `enum` (incl. `enum flags`, e.g. `if p & .read`)
|
||||||
|
/// and `error_set` (a u32 tag) — both reach condBr as a plain integer
|
||||||
|
/// • pointers: `*T` / `[*]T` / `cstring` (compared against null)
|
||||||
|
/// • `optional` — the caller emits `optional_has_value`
|
||||||
|
/// Everything else (float, void, string, any, type_value, struct/union/tuple/
|
||||||
|
/// array/slice/vector/function/closure/protocol/pack) reaches condBr as a
|
||||||
|
/// non-comparable aggregate or a value with no truthiness, and previously got
|
||||||
|
/// silently folded truthy then `@panic`d in the backend (issue 0164). Such a
|
||||||
|
/// condition is a type error — see `checkConditionType`.
|
||||||
|
fn isValidConditionType(self: *Lowering, ty: TypeId) bool {
|
||||||
|
if (ty == .unresolved) return true; // already-diagnosed elsewhere; don't double-report
|
||||||
|
return switch (self.module.types.get(ty)) {
|
||||||
|
.bool, .signed, .unsigned, .usize, .isize => true,
|
||||||
|
.pointer, .many_pointer, .cstring => true,
|
||||||
|
.@"enum", .error_set => true,
|
||||||
|
.optional => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit a located type error when `ty` cannot be used as a branch condition
|
||||||
|
/// (see `isValidConditionType`). Returns `true` if the type is valid (caller
|
||||||
|
/// proceeds normally), `false` if a diagnostic was emitted (caller should
|
||||||
|
/// recover with a placeholder bool so lowering doesn't crash before the
|
||||||
|
/// diagnostic surfaces). This is the lowering-time replacement for the
|
||||||
|
/// backend `@panic` in `emitCondBr` (issue 0164): the type and span are both
|
||||||
|
/// available here, so we report a clean compile-time error instead.
|
||||||
|
pub fn checkConditionType(self: *Lowering, ty: TypeId, span: ast.Span) bool {
|
||||||
|
if (isValidConditionType(self, ty)) return true;
|
||||||
|
if (self.diagnostics) |d| d.addFmt(.err, span, "condition must be a bool, integer, pointer, or optional, but has type '{s}'", .{self.formatTypeName(ty)});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lower `node` as a boolean condition. If its type is an optional, reduce
|
||||||
|
/// it to its has_value flag (presence-as-truth) — same rule as `if opt`/
|
||||||
|
/// `while opt`. Without this, a bare optional operand reaches a condBr/phi as
|
||||||
|
/// a `{T,i1}` aggregate and folds truthy (issue 0164). Returns an i1/bool Ref.
|
||||||
|
/// A non-condition-typed operand (struct/float/...) is rejected with a located
|
||||||
|
/// type error via `checkConditionType`; on rejection a placeholder `false` is
|
||||||
|
/// returned so lowering can continue to surface the diagnostic.
|
||||||
|
pub fn lowerBoolCondition(self: *Lowering, node: *const Node) Ref {
|
||||||
|
const ty = self.inferExprType(node);
|
||||||
|
if (!self.checkConditionType(ty, node.span)) {
|
||||||
|
_ = self.lowerExpr(node); // still lower for side effects / further diagnostics
|
||||||
|
return self.builder.constBool(false);
|
||||||
|
}
|
||||||
|
const v = self.lowerExpr(node);
|
||||||
|
if (!ty.isBuiltin() and self.module.types.get(ty) == .optional) {
|
||||||
|
return self.builder.emit(.{ .optional_has_value = .{ .operand = v } }, .bool);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref {
|
pub fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref {
|
||||||
// Short-circuit: `a and b` → if a then b else false
|
// Short-circuit: `a and b` → if a then b else false
|
||||||
if (bop.op == .and_op) {
|
if (bop.op == .and_op) {
|
||||||
const lhs = self.lowerExpr(bop.lhs);
|
const lhs = self.lowerBoolCondition(bop.lhs);
|
||||||
const rhs_bb = self.freshBlock("and.rhs");
|
const rhs_bb = self.freshBlock("and.rhs");
|
||||||
const merge_bb = self.freshBlockWithParams("and.merge", &.{.bool});
|
const merge_bb = self.freshBlockWithParams("and.merge", &.{.bool});
|
||||||
const false_val = self.builder.constBool(false);
|
const false_val = self.builder.constBool(false);
|
||||||
self.builder.condBr(lhs, rhs_bb, &.{}, merge_bb, &.{false_val});
|
self.builder.condBr(lhs, rhs_bb, &.{}, merge_bb, &.{false_val});
|
||||||
self.builder.switchToBlock(rhs_bb);
|
self.builder.switchToBlock(rhs_bb);
|
||||||
const rhs = self.lowerExpr(bop.rhs);
|
const rhs = self.lowerBoolCondition(bop.rhs);
|
||||||
self.builder.br(merge_bb, &.{rhs});
|
self.builder.br(merge_bb, &.{rhs});
|
||||||
self.builder.switchToBlock(merge_bb);
|
self.builder.switchToBlock(merge_bb);
|
||||||
return self.builder.blockParam(merge_bb, 0, .bool);
|
return self.builder.blockParam(merge_bb, 0, .bool);
|
||||||
@@ -2768,13 +2828,13 @@ pub fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref {
|
|||||||
if (self.orIsFailableChain(bop)) {
|
if (self.orIsFailableChain(bop)) {
|
||||||
return self.lowerFailableOr(bop);
|
return self.lowerFailableOr(bop);
|
||||||
}
|
}
|
||||||
const lhs = self.lowerExpr(bop.lhs);
|
const lhs = self.lowerBoolCondition(bop.lhs);
|
||||||
const rhs_bb = self.freshBlock("or.rhs");
|
const rhs_bb = self.freshBlock("or.rhs");
|
||||||
const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool});
|
const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool});
|
||||||
const true_val = self.builder.constBool(true);
|
const true_val = self.builder.constBool(true);
|
||||||
self.builder.condBr(lhs, merge_bb, &.{true_val}, rhs_bb, &.{});
|
self.builder.condBr(lhs, merge_bb, &.{true_val}, rhs_bb, &.{});
|
||||||
self.builder.switchToBlock(rhs_bb);
|
self.builder.switchToBlock(rhs_bb);
|
||||||
const rhs = self.lowerExpr(bop.rhs);
|
const rhs = self.lowerBoolCondition(bop.rhs);
|
||||||
self.builder.br(merge_bb, &.{rhs});
|
self.builder.br(merge_bb, &.{rhs});
|
||||||
self.builder.switchToBlock(merge_bb);
|
self.builder.switchToBlock(merge_bb);
|
||||||
return self.builder.blockParam(merge_bb, 0, .bool);
|
return self.builder.blockParam(merge_bb, 0, .bool);
|
||||||
|
|||||||
Reference in New Issue
Block a user