fix: literal element typing — typed-array null element, tuple coercion, positional var element (0173-0175)

0173: resolveArrayLiteralType gained no arm for [N]T/[]T heads, so a
([2]?i64).[...] head lost its ?i64 element type and a bare null reached
LLVM as const_null(.unresolved). Route structural heads through
resolveTypeWithBindings; validate an undefined element name in the head
via UnknownTypeChecker (semantic_diagnostics.zig) instead of a silent
empty-struct stub (no-silent-fallback).

0174: positional .{...} against a TUPLE target now coerces each element
to TupleInfo.fields[i] (was neither struct nor array, so uncoerced).

0175: a positional struct literal with a bare-variable element was
misclassified as a named shorthand (parser puns .{x} -> x=x), zeroing
the fields. has_names now consults the struct definition to reclassify a
punned non-field name as positional; positional coercion uses the
lowered value's real getRefType.

Regressions: optionals/0914, types/0199, types/0200, diagnostics/1196.
Verified by 4 adversarial reviews; suite 784/0. Filed adjacent bug 0176
(protocol-typed struct field method call aborts).
This commit is contained in:
agra
2026-06-23 00:25:28 +03:00
parent 5a436eddb1
commit 28bb101a4a
22 changed files with 369 additions and 11 deletions

View File

@@ -1,5 +1,18 @@
# 0173 — `(T).[ ... ]` typed array-literal with a `null` element panics (unresolved type at LLVM emission)
> **RESOLVED.** `resolveArrayLiteralType` had no arm for an `array_type_expr` /
> `slice_type_expr` head, so a `([2]?i64).[...]` head fell to `else =>
> .unresolved` — the `?i64` element type was lost and a bare `null` element
> reached LLVM as `const_null(.unresolved)`. Fix (`src/ir/lower/expr.zig`): route
> structural heads through `resolveTypeWithBindings` (recurses into the element).
> To honor the no-silent-fallback rule, an UNDEFINED element name in the head is
> now validated by `UnknownTypeChecker` (`src/ir/semantic_diagnostics.zig` —
> wired `al.type_expr` into `walkBodyTypes`), emitting `unknown type '<name>'`
> instead of a silent empty-struct stub. Regression:
> `examples/optionals/0914-optionals-typed-array-literal-null-element.sx` +
> `examples/diagnostics/1196-diagnostics-array-literal-head-unknown-type.sx`.
> Verified by 4 adversarial reviews.
## Symptom
An explicit-type array literal of the `(T).[ elems ]` form (the `.[...]`

View File

@@ -1,5 +1,16 @@
# 0174 — positional literal for a TUPLE target does not coerce elements (same corruption class as 0168)
> **RESOLVED.** `lowerStructLiteral`'s positional branch coerced struct fields
> and array/vector elements but not TUPLE targets (a tuple is neither — empty
> `struct_fields`, `.unresolved` `array_elem_ty`), so a bare element was stored
> raw into the field slot (a `{T,i1}` optional read back absent). Fix
> (`src/ir/lower/expr.zig`): compute `tuple_fields` from `TupleInfo.fields` and
> fold it into a unified `elem_target` (`struct_fields[i].ty` → `tuple_fields[i]`
> → `array_elem_ty`) that steers per-element `target_type` and drives
> `coerceToType`. Verified across optional/int→float/protocol/slice/enum/nested
> tuple elements + named tuples by 4 adversarial reviews. Regression:
> `examples/types/0199-types-tuple-positional-optional-element.sx`.
## Symptom
A positional literal `.{ a, b }` whose target is a TUPLE does not coerce its

View File

@@ -1,5 +1,17 @@
# 0175 — positional struct literal with a VARIABLE element silently zeroes the field
> **RESOLVED.** Root cause was named-vs-positional misclassification: the parser
> PUNS a bare-ident element `.{ x, … }` into a named field `x = x` (the legit
> `Vec4.{ w, z }` shorthand), so a positional-with-variable literal arrived as a
> spurious "named" literal and the named branch left every field at its default.
> Fix (`src/ir/lower/expr.zig`): `has_names` now consults the struct definition —
> a punned bare-ident whose name matches no declared field reclassifies the whole
> literal as positional; positional field coercion now uses the lowered value's
> actual `getRefType` (not a re-inferred `src_ty`) and steers per-field
> `target_type`. Legit shorthand, named, mixed, generic, forward-ref, and nested
> cases all verified unbroken by 4 adversarial reviews. Regression:
> `examples/types/0200-types-positional-struct-literal-variable-element.sx`.
## Symptom
A positional struct literal `S.{ x, ... }` whose element is a VARIABLE reference

View File

@@ -0,0 +1,47 @@
# 0176 — calling a method through a protocol-typed struct field aborts (exit 133, no diagnostic)
## Symptom
A struct field whose type is a PROTOCOL holds an erased value fine, but calling a
method THROUGH that field aborts the process (exit 133, SIGABRT) with no
diagnostic. Reading a non-protocol sibling field is fine; constructing the struct
is fine. The crash needs the method call-through. Reproduces with BOTH
struct-literal init and field assignment, so it is not a struct-literal bug — the
protocol field's method dispatch / vtable through a struct slot is the suspect.
Pre-existing (reproduces on clean master).
## Reproduction
```sx
#import "modules/std.sx";
Speaker :: protocol { speak :: (self: *Self) -> i64; }
Dog :: struct { n: i64 = 0; }
speak :: (self: *Dog) -> i64 { return self.n; }
Holder :: struct { s: Speaker; b: i64 = 0; }
main :: () {
d := Dog.{ n = 42 };
h : Holder = .{ s = d, b = 5 }; // or: h.s = d (field assign) — same crash
print("{}\n", h.s.speak()); // <-- aborts here, exit 133, no output
}
```
Expected: `42`. Observed: silent abort, exit 133. Reading `h.b` (the non-protocol
field) prints `5` fine; the crash is specifically the call through `h.s`.
## Investigation prompt
The erased protocol value stored in a struct field appears to lose its
method-table / self pointer, so dispatch through `h.s.speak()` reads a
null/garbage vtable. Compare against a protocol value in a LOCAL variable
(`s : Speaker = d; s.speak()` — does THAT work?) to isolate whether the bug is in
storing the erased value into a struct field, or in dispatching through a field
access. Suspect the protocol fat-value `{vtable/typeinfo, data-ptr}` layout when
embedded as a struct field: the field store (`emitStructInit` / field assign) may
truncate or mis-place the fat value, or the method-dispatch lowering for
`field.method()` may not load the full protocol header. Look at how a protocol
local dispatches vs how a protocol struct-field dispatches
(`src/ir/lower/expr.zig` method-call / field-access lowering + `src/backend/llvm`
protocol dispatch). Follow the no-silent-fallback rule. Verify: the repro prints
`42`; both struct-literal and field-assign init; a protocol field reassigned to a
different concrete type dispatches correctly. Add a
`examples/protocols/04xx-protocol-struct-field-dispatch.sx` regression.