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
This commit is contained in:
53
issues/0164-if-optional-no-binding-folds-true.md
Normal file
53
issues/0164-if-optional-no-binding-folds-true.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#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 69–72) 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 2378–2383) 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.
|
||||||
50
issues/0165-parenthesized-nested-optional-malformed.md
Normal file
50
issues/0165-parenthesized-nested-optional-malformed.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# 0165 — parenthesized nested optional `?(?T)` resolves to a malformed double-wrapped type
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
A nested optional written `?(?i64)` resolves to a spurious extra struct wrapper:
|
||||||
|
the destination type lowers to `{ { {i64,i1} }, i1 }` (triple-wrapped) instead of
|
||||||
|
the correct `{ {i64,i1}, i1 }`. Assigning an inner `?i64` then fails the LLVM
|
||||||
|
verifier:
|
||||||
|
|
||||||
|
```
|
||||||
|
LLVM verification failed: Invalid InsertValueInst operands!
|
||||||
|
%ow.val = insertvalue { { { i64, i1 } }, i1 } undef, { i64, i1 } %load, 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Crash (non-zero exit). `??i64` (unparenthesized) is a separate parse error
|
||||||
|
(`expected type name`) and is NOT this bug — only the parenthesized `?(?T)` form
|
||||||
|
reaches type resolution and produces the malformed layout.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
main :: () {
|
||||||
|
inner : ?i64 = 5;
|
||||||
|
outer : ?(?i64) = inner;
|
||||||
|
print("ok\n");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `ok` (a well-formed `{ {i64,i1}, i1 }` outer optional wrapping the
|
||||||
|
inner `?i64`). Observed: LLVM verifier abort.
|
||||||
|
|
||||||
|
## Investigation prompt
|
||||||
|
|
||||||
|
The type-table interning (`src/ir/types.zig` `optionalOf` / the optional
|
||||||
|
lowering near `types.zig:87`) produces the CORRECT `{ {i64,i1}, i1 }` for a
|
||||||
|
real `optional(optional(i64))`. So the malformed layout comes from
|
||||||
|
`instruction.ty` itself: the parenthesized `(?i64)` inner type expression is
|
||||||
|
resolved as a single-field STRUCT wrapping `?i64`, not as the optional type
|
||||||
|
directly. Suspected area: resolution of a parenthesized type expression
|
||||||
|
(`src/ir/type_resolver.zig` and/or the parser's handling of `(?T)` as a type) —
|
||||||
|
a parenthesized type should resolve to the inner type unchanged, not introduce a
|
||||||
|
tuple/struct wrapper. `src/backend/llvm/ops.zig` `emitOptionalWrap` is the
|
||||||
|
faithful victim (it uses `toLLVMType(instruction.ty)`), not the cause.
|
||||||
|
|
||||||
|
Verify: the repro prints `ok`; `?(?i64)` round-trips (unwrap inner, read value);
|
||||||
|
confirm the IR type is `{ {i64,i1}, i1 }`. Add an
|
||||||
|
`examples/optionals/09xx-nested-parenthesized-optional.sx` regression. (The
|
||||||
|
`unresolved type` panic seen when an `!` unwrap is added is downstream recovery
|
||||||
|
fallout of the same root cause — should disappear once the layout is correct.)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 0166 — `?? .{ ... }` struct-literal default panics with "unresolved type reached LLVM emission"
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Using a struct literal as the default of a `??` (null-coalesce) operator panics:
|
||||||
|
|
||||||
|
```
|
||||||
|
panic: unresolved type reached LLVM emission
|
||||||
|
```
|
||||||
|
|
||||||
|
in `emitStructInit` (exit 134 / SIGABRT). The coalesce result type is inferred
|
||||||
|
correctly (the optional's child `T`), but that target type is NOT threaded into
|
||||||
|
the RHS struct-literal lowering, so the `struct_init` instruction's `.ty` stays
|
||||||
|
`.unresolved` and reaches codegen.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
T :: struct { a: i64 = 0; }
|
||||||
|
mk :: () -> ?T { return null; }
|
||||||
|
main :: () { t := mk() ?? .{ a = 9 }; print("{}\n", t.a); }
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `9` (null lhs → take the struct-literal default, typed as `T`).
|
||||||
|
Observed: `panic: unresolved type reached LLVM emission`, exit 134.
|
||||||
|
|
||||||
|
## Investigation prompt
|
||||||
|
|
||||||
|
The result type IS inferred correctly — `src/ir/expr_typer.zig` (~lines 413–425)
|
||||||
|
returns the optional's child `T` for the coalesce expression. The gap is in
|
||||||
|
lowering: the `??` RHS struct literal is lowered without a target type. Suspected
|
||||||
|
area: `src/ir/lower/expr.zig` `lowerNullCoalesce` (dispatched ~expr.zig:2417)
|
||||||
|
must set the lowering target type to the lhs optional's child (`T`) before
|
||||||
|
lowering `nc.rhs`, so an untyped `.{ ... }` literal on the RHS resolves to `T`
|
||||||
|
the same way an assignment/return target would. Mirror however other contexts
|
||||||
|
push an expected type into an untyped struct-literal lowering.
|
||||||
|
|
||||||
|
Verify: the repro prints `9`; also test a present lhs (`mk` returns a value →
|
||||||
|
prints the value's field, default not taken) and a nested-field struct literal
|
||||||
|
default. Add an `examples/optionals/09xx-null-coalesce-struct-literal.sx`
|
||||||
|
regression.
|
||||||
66
issues/0167-comptime-regtovalue-array-in-aggregate.md
Normal file
66
issues/0167-comptime-regtovalue-array-in-aggregate.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# 0167 — comptime `#run` returning an aggregate that contains an array fails the reg→value bridge (+ unclean recovery)
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Two related defects:
|
||||||
|
|
||||||
|
**(C)** A `#run` (comptime const init) whose function returns a struct/aggregate
|
||||||
|
**containing an array field** fails comptime evaluation with a LOUD bail:
|
||||||
|
|
||||||
|
`error: comptime init of 'G' failed: reg→value: aggregate shape not bridged yet`
|
||||||
|
|
||||||
|
This is the general array-in-aggregate gap in the comptime VM's `regToValue`
|
||||||
|
bridge — it handles scalar/struct/slice/tuple/optional payloads but not an array
|
||||||
|
nested inside the aggregate. (The `?Arr` form noted while fixing issue 0162 is
|
||||||
|
the SAME root cause, not optional-specific.) This is a loud limitation, not a
|
||||||
|
silent miscompile.
|
||||||
|
|
||||||
|
**(E)** When such a comptime init has already failed, downstream codegen does not
|
||||||
|
hard-abort: a later use of the now-`unresolved`-typed const (e.g.
|
||||||
|
`A?.xs[0]`) panics `unresolved type reached LLVM emission` (exit 134) instead of
|
||||||
|
the clean exit-1 the compiler should produce after a comptime-init failure.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
C (loud bail — the primary feature to implement):
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
Arr3 :: struct { xs: [3]i64; }
|
||||||
|
mk :: () -> Arr3 { r : Arr3 = ---; r.xs[0]=1; r.xs[1]=2; r.xs[2]=3; return r; }
|
||||||
|
G :: #run mk();
|
||||||
|
main :: () { print("{} {} {}\n", G.xs[0], G.xs[1], G.xs[2]); }
|
||||||
|
```
|
||||||
|
Expected: `1 2 3`. Observed: `error: ... reg→value: aggregate shape not bridged yet`, exit 1.
|
||||||
|
|
||||||
|
E (recovery should be clean, not a panic):
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
Arr3 :: struct { xs: [3]i64; }
|
||||||
|
mk :: () -> ?Arr3 { r : Arr3 = ---; r.xs[0]=1; r.xs[1]=2; r.xs[2]=3; return r; }
|
||||||
|
A :: #run mk();
|
||||||
|
main :: () { print("{}\n", A?.xs[0]); }
|
||||||
|
```
|
||||||
|
Observed: `panic: unresolved type reached LLVM emission`, exit 134. Expected:
|
||||||
|
once (C) is implemented this evaluates; independently, a failed comptime init
|
||||||
|
must abort cleanly (exit 1) rather than reach LLVM emission.
|
||||||
|
|
||||||
|
## Investigation prompt
|
||||||
|
|
||||||
|
**C:** `src/ir/comptime_vm.zig` `regToValue` (the `failMsg("reg→value: aggregate
|
||||||
|
shape not bridged yet")` bail, ~line 2270). Add an array arm: read `len` elements
|
||||||
|
of the element type from the aggregate memory at the array field's offset,
|
||||||
|
bridging each via `regToValue(elem_ty)` recursively, producing a `Value` array.
|
||||||
|
This must compose with the struct-field walk so an array nested inside a struct
|
||||||
|
(and the `?Arr` optional payload from 0162) both work. Follow the no-silent-
|
||||||
|
fallback rule — any element type you can't bridge bails loudly with a specific
|
||||||
|
message.
|
||||||
|
|
||||||
|
**E:** after a `#run`/comptime-init failure sets `comptime_failed = true` (see
|
||||||
|
the `#run` call sites in `src/ir/emit_llvm.zig`), the pipeline should stop before
|
||||||
|
LLVM emission rather than proceeding with an `unresolved` const type. Find where
|
||||||
|
`comptime_failed` is checked (or should be) before codegen in `src/main.zig` /
|
||||||
|
the emit driver, and make a failed comptime init a hard, clean abort.
|
||||||
|
|
||||||
|
Verify: repro C prints `1 2 3`; an array-of-struct and struct-with-array both
|
||||||
|
bridge; repro E either evaluates (after C) or exits 1 cleanly with the comptime
|
||||||
|
error and no panic. Add `examples/comptime/06xx-comptime-run-array-aggregate.sx`.
|
||||||
Reference in New Issue
Block a user