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:
@@ -1,5 +1,23 @@
|
||||
# 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
|
||||
|
||||
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).
|
||||
Reference in New Issue
Block a user