fix: optional-chain getter/field correctness from 0160 adversarial review

Five adversarial reviews of the issue-0160 fix surfaced three more bugs in the
touched optional-chain / optional-coercion code; all fixed here:

1. A COLD generic-instance getter through `?.` (`?*Vec(i64)` `.getter`, never
   called directly first) panicked with "unresolved type reached LLVM emission":
   a cold instance method is absent from resolveFuncByName, so the getter's
   return type resolved to .unresolved → a ?unresolved merge type. lowerOptionalChain
   and getterReturnTypeOnDeref now warm the monomorph (ensureGenericInstanceMethodLowered)
   before querying its return type. (The 0907 test passed only by luck — List(i64)
   is warmed by stdlib use; 0907 now also exercises a cold user generic.)

2. A real-field read through a `?*T` chain (`op?.field`, op: ?*T) reinterpreted
   the pointer bits as the field (silent garbage) — the some-branch real-field
   path didn't load through the pointer. It now derefs `?*T` before the field
   access. (Pre-existing — the else-branch predates 0160 — but it's the same
   function and a silent miscompile, so fixed here.)

3. `?[]T = array` skipped the array→slice promotion (corrupt .len/.ptr): the
   lowerVarDecl optional arm wrapped the raw array. It now coerces the value to
   the optional's child type (array→slice) before wrapping.

Regression examples 0906/0907 extended to cover all three. Distinct PRE-EXISTING
bugs the reviews surfaced in untouched subsystems are filed as issues 0161
(struct-literal vs scalar), 0162 (#run returning an optional aggregate), 0163
(untagged-union payload-binding match).
This commit is contained in:
agra
2026-06-22 18:55:41 +03:00
parent 1b0c857b91
commit ff9e448f8c
10 changed files with 214 additions and 4 deletions

View File

@@ -17,6 +17,19 @@
> unsupported for all fields) and was never part of this bug. Regression tests:
> `examples/optionals/0906-optionals-struct-literal-into-optional.sx` and
> `examples/optionals/0907-optionals-accessor-through-chain.sx`.
>
> **Review follow-ups (5 adversarial reviews on the fix):** three further
> optional bugs in the touched code were fixed in the same pass — (1) a COLD
> generic-instance getter through `?.` panicked (`?unresolved`) because the
> monomorph wasn't lowered before its return type was queried; `lowerOptionalChain`
> + `getterReturnTypeOnDeref` now warm it. (2) a real-field read through a `?*T`
> chain reinterpreted the pointer bits as the field (silent garbage); the
> some-branch now loads through the pointer. (3) `?[]T = array` skipped
> array→slice promotion (corrupt `.len`); `lowerVarDecl`'s optional arm now
> coerces to the child before wrapping. Both regression examples were extended.
> Separately-filed PRE-EXISTING bugs the reviews surfaced (distinct subsystems,
> untouched here): [[0161]] struct-literal vs scalar, [[0162]] `#run` returning an
> optional aggregate, [[0163]] untagged-union payload-binding match.
## Symptom

View File

@@ -0,0 +1,43 @@
# 0161 — struct literal against a non-aggregate (scalar) type crashes instead of diagnosing
## Symptom
A struct literal `.{ field = ... }` whose resolved target type is a scalar (or
any non-struct) reaches LLVM emission and fails verification, instead of emitting
a clean "struct literal against non-struct type" diagnostic.
- Observed: `LLVM verification failed: Invalid InsertValueInst operands! %si = insertvalue i64 undef, i64 1, 0` (exit 1).
- Expected: a diagnostic like "cannot build a struct literal for non-struct type 'i64'".
`.{}` (empty) against a scalar is worse — it silently produces garbage with no
diagnostic.
This surfaced while reviewing issue 0160: `?i64 = .{...}` routes through the
struct-literal→optional path (which recurses with the child type `i64` as
target) into this same crash. But it is NOT optional-specific — a plain
`i64 = .{...}` crashes identically, so the root cause is the general
struct-literal path, not the 0160 optional handling.
## Reproduction
```sx
#import "modules/std.sx";
main :: () {
x : i64 = .{ a = 1 }; // struct literal targeting a scalar
print("{}\n", x); // actual: LLVM verification failure
}
```
Also: `y : i64 = .{}` → silent garbage; `o : ?i64 = .{ a = 1 }` → same crash.
## Investigation prompt
`src/ir/lower/expr.zig` `lowerStructLiteral`: after the resolved literal type
`ty` is computed (and the optional/union special-cases), the named/positional
field path calls `getStructFields(ty)` and emits `structInit`/`insertvalue`
without first checking that `ty` is actually a struct. Add an early guard: if
`ty.isBuiltin()` or `module.types.get(ty)` is not `.@"struct"` (after the
existing tagged-union / union / optional intercepts), emit a diagnostic via
`self.diagnostics.addFmt(.err, span, "...", .{...})` and return a placeholder,
rather than building `insertvalue` against a scalar LLVM type. Verify with the
repro (expect a clean error, exit 1, no LLVM panic). Add
`examples/diagnostics/11xx-...` for the negative case.

View File

@@ -0,0 +1,40 @@
# 0162 — `#run` returning an optional aggregate fails the comptime VM reg→value bridge
## Symptom
A `#run` (or comptime const init) whose function returns an OPTIONAL value
(`?T`, `?i64`, any optional) fails comptime evaluation with:
`error: comptime init of 'X' failed: reg→value: aggregate shape not bridged yet`
A non-optional return of the same type works. This is a pre-existing limitation
in the comptime VM's register→value bridge for optional-typed results; it is
orthogonal to issue 0160 (it reproduces for a value-init optional with no struct
literal anywhere, and for a scalar optional `?i64`).
## Reproduction
```sx
#import "modules/std.sx";
T :: struct { a: i64 = 0; }
mk :: () -> ?T { t : T = .{ a = 7 }; return t; }
mk2 :: () -> ?i64 { return 5; }
X :: #run mk(); // error: reg→value: aggregate shape not bridged yet
Y :: #run mk2(); // same class of failure
main :: () { print("ok\n"); }
```
Baseline that WORKS: `Z :: #run (() -> T { return .{ a = 7 }; })();` (non-optional).
## Investigation prompt
`src/ir/comptime_vm.zig` — the reg→value bridge (search "aggregate shape not
bridged" / `regToValue`) handles scalars/structs/slices but bails on an
OPTIONAL-typed result. An optional is `{payload, has_value}` (or a pointer for
`?*T` / a sentinel for `?Closure`); the bridge needs to read the has_value flag
and, when set, bridge the payload as its child type (recursively), producing a
`Value` optional — and a null optional when clear. Add the `.optional` arm to
the reg→value bridge (mirror the value→reg direction, which already builds
optionals — see `makeStringList`/`writeField` optional handling). Verify with
the repro (expect `X`/`Y` to evaluate, `main` prints ok). Add a
`examples/comptime/06xx-...` regression.

View File

@@ -0,0 +1,43 @@
# 0163 — payload-binding match on a plain untagged `union` panics instead of diagnosing
## Symptom
A `match`-style `if x == { case .variant: (v) { ... } }` with a PAYLOAD BINDING
`(v)` on a value of a plain UNTAGGED `union` type panics in the LLVM backend
instead of producing a diagnostic. An untagged union has no discriminant, so a
case-payload binding is not a valid construct and should be rejected at
typecheck.
- Observed: `thread panic: unresolved type reached LLVM emission` at
`src/backend/llvm/types.zig:196`, reached via `emit_llvm.zig:1289`
`declareFunction``toLLVMType(param.ty)` (exit 134).
- Expected: a clean diagnostic (e.g. "cannot bind a payload from an untagged
union — use a tagged enum/union with a discriminant").
Surfaced during the issue-0160 review (blast-radius probing). NOT caused by 0160
— the panic path is union-match lowering → `declareFunction`, none of which the
0160 fix touches. Removing the `(v)` binding, or using a tagged `enum` instead,
both work.
## Reproduction
```sx
#import "modules/std.sx";
Shape :: union { circle: i64; rect: i64; } // plain untagged union (no discriminant)
main :: () {
s : Shape = .{ circle = 5 };
r := if s == { case .circle: (v) { v } case .rect: (v) { v * 2 } }; // panic
print("{}\n", r);
}
```
## Investigation prompt
The match/case lowering binds a case payload `(v)` whose type it resolves
against the union variant — but a plain `.@"union"` (untagged) has no per-variant
discriminant, so the binding's type leaks out as `.unresolved` and reaches
`declareFunction`. In the match-arm lowering (grep the case/`match_arm` path in
`src/ir/lower/`), reject a payload-binding case when the scrutinee type is an
untagged `.@"union"` (only `.tagged_union` / `.@"enum"` payloads are bindable):
emit a diagnostic and bail, before any `.unresolved` type is produced. Verify
with the repro (expect a clean error, not a panic). Add a diagnostics example.