From 28bb101a4a25bd6cbc2b2c63f496e9d4c79a6e3a Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 23 Jun 2026 00:25:28 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20literal=20element=20typing=20=E2=80=94?= =?UTF-8?q?=20typed-array=20null=20element,=20tuple=20coercion,=20position?= =?UTF-8?q?al=20var=20element=20(0173-0175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- ...nostics-array-literal-head-unknown-type.sx | 21 ++++ ...stics-array-literal-head-unknown-type.exit | 1 + ...ics-array-literal-head-unknown-type.stderr | 5 + ...ics-array-literal-head-unknown-type.stdout | 1 + ...ionals-typed-array-literal-null-element.sx | 32 ++++++ ...nals-typed-array-literal-null-element.exit | 1 + ...ls-typed-array-literal-null-element.stderr | 1 + ...ls-typed-array-literal-null-element.stdout | 5 + ...types-tuple-positional-optional-element.sx | 34 +++++++ ...itional-struct-literal-variable-element.sx | 63 ++++++++++++ ...pes-tuple-positional-optional-element.exit | 1 + ...s-tuple-positional-optional-element.stderr | 1 + ...s-tuple-positional-optional-element.stdout | 5 + ...ional-struct-literal-variable-element.exit | 1 + ...nal-struct-literal-variable-element.stderr | 1 + ...nal-struct-literal-variable-element.stdout | 9 ++ ...d-array-literal-null-element-unresolved.md | 13 +++ ...-positional-literal-element-not-coerced.md | 11 +++ ...-struct-literal-variable-element-zeroed.md | 12 +++ ...l-typed-struct-field-method-call-aborts.md | 47 +++++++++ src/ir/lower/expr.zig | 98 +++++++++++++++++-- src/ir/semantic_diagnostics.zig | 17 +++- 22 files changed, 369 insertions(+), 11 deletions(-) create mode 100644 examples/diagnostics/1196-diagnostics-array-literal-head-unknown-type.sx create mode 100644 examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.exit create mode 100644 examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.stderr create mode 100644 examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.stdout create mode 100644 examples/optionals/0914-optionals-typed-array-literal-null-element.sx create mode 100644 examples/optionals/expected/0914-optionals-typed-array-literal-null-element.exit create mode 100644 examples/optionals/expected/0914-optionals-typed-array-literal-null-element.stderr create mode 100644 examples/optionals/expected/0914-optionals-typed-array-literal-null-element.stdout create mode 100644 examples/types/0199-types-tuple-positional-optional-element.sx create mode 100644 examples/types/0200-types-positional-struct-literal-variable-element.sx create mode 100644 examples/types/expected/0199-types-tuple-positional-optional-element.exit create mode 100644 examples/types/expected/0199-types-tuple-positional-optional-element.stderr create mode 100644 examples/types/expected/0199-types-tuple-positional-optional-element.stdout create mode 100644 examples/types/expected/0200-types-positional-struct-literal-variable-element.exit create mode 100644 examples/types/expected/0200-types-positional-struct-literal-variable-element.stderr create mode 100644 examples/types/expected/0200-types-positional-struct-literal-variable-element.stdout create mode 100644 issues/0176-protocol-typed-struct-field-method-call-aborts.md diff --git a/examples/diagnostics/1196-diagnostics-array-literal-head-unknown-type.sx b/examples/diagnostics/1196-diagnostics-array-literal-head-unknown-type.sx new file mode 100644 index 00000000..d6ff4bf1 --- /dev/null +++ b/examples/diagnostics/1196-diagnostics-array-literal-head-unknown-type.sx @@ -0,0 +1,21 @@ +// A typed array/slice literal head (`([N]T).[…]` / `([]T).[…]`) names its +// element type exactly like a declaration annotation, so an UNDEFINED element +// type name must be rejected with the same `unknown type ''` diagnostic +// the declaration path emits — NOT silently compiled. +// +// Regression (issues 0173–0175 adversarial review): the 0173 fix taught the +// lowering's `resolveArrayLiteralType` to resolve a structural `[N]?T` head, +// but for an UNDEFINED element name the resolver returned a forward-reference +// empty-struct STUB instead of `.unresolved`. So `([2]?Undefined).[…]` +// compiled silently (exit 0, "ok") with a wrong empty-struct element, where +// `x: [2]?Undefined = ---` correctly errored. The unknown-type checker +// (`semantic_diagnostics.zig` `walkBodyTypes`) now validates the array +// literal's `type_expr` head through the same `checkTypeNodeForUnknown` walk a +// declaration uses, so a genuinely-undeclared head element name is a loud, +// located error (exit 1) — never a silent empty-struct compile or a raw panic. +#import "modules/std.sx"; + +main :: () { + arr := ([2]?Undefined).[ null, null ]; + print("ok\n"); +} diff --git a/examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.exit b/examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.exit new file mode 100644 index 00000000..d00491fd --- /dev/null +++ b/examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.exit @@ -0,0 +1 @@ +1 diff --git a/examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.stderr b/examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.stderr new file mode 100644 index 00000000..008a28be --- /dev/null +++ b/examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.stderr @@ -0,0 +1,5 @@ +error: unknown type 'Undefined' + --> examples/diagnostics/1196-diagnostics-array-literal-head-unknown-type.sx:19:17 + | +19 | arr := ([2]?Undefined).[ null, null ]; + | ^^^^^^^^^ diff --git a/examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.stdout b/examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.stdout new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/diagnostics/expected/1196-diagnostics-array-literal-head-unknown-type.stdout @@ -0,0 +1 @@ + diff --git a/examples/optionals/0914-optionals-typed-array-literal-null-element.sx b/examples/optionals/0914-optionals-typed-array-literal-null-element.sx new file mode 100644 index 00000000..9b2cafdf --- /dev/null +++ b/examples/optionals/0914-optionals-typed-array-literal-null-element.sx @@ -0,0 +1,32 @@ +// Typed `.[ ... ]` array literal whose element type is an optional and whose +// elements include a bare `null` resolve the element type from the literal's +// type head, so `null` lowers as `const_null(?T)` (not `.unresolved`). +// +// Regression (issue 0173): `([N]?T).[ ... ]` reached LLVM with an +// `.unresolved`-typed `const_null` and panicked, because the array-literal +// type-head resolver had no arm for an `array_type_expr`/`slice_type_expr` +// head — the `?T` element type was lost. +#import "modules/std.sx"; + +Pt :: struct { x: i64 = 0; y: i64 = 0; } + +main :: () { + a := ([2]?i64).[ null, 7 ]; + print("{}\n", a[1] ?? -1); // 7 + + b := ([3]?i64).[ null, null, 5 ]; + print("{} {} {}\n", b[0] ?? -1, b[1] ?? -1, b[2] ?? -1); // -1 -1 5 + + // Optional struct payload element + a bare null sibling. + c := ([2]?Pt).[ null, .{ x = 1, y = 2 } ]; + p := c[1] ?? Pt.{}; + print("{} {}\n", p.x, p.y); // 1 2 + + // A typed slice head `[]?T` with a null element resolves too. + s : []?i64 = .[ null, 3 ]; + print("{}\n", s[1] ?? -1); // 3 + + // A non-optional typed `.[...]` array still works (no regression). + d := ([3]i64).[ 1, 2, 3 ]; + print("{} {} {}\n", d[0], d[1], d[2]); // 1 2 3 +} diff --git a/examples/optionals/expected/0914-optionals-typed-array-literal-null-element.exit b/examples/optionals/expected/0914-optionals-typed-array-literal-null-element.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/optionals/expected/0914-optionals-typed-array-literal-null-element.exit @@ -0,0 +1 @@ +0 diff --git a/examples/optionals/expected/0914-optionals-typed-array-literal-null-element.stderr b/examples/optionals/expected/0914-optionals-typed-array-literal-null-element.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/optionals/expected/0914-optionals-typed-array-literal-null-element.stderr @@ -0,0 +1 @@ + diff --git a/examples/optionals/expected/0914-optionals-typed-array-literal-null-element.stdout b/examples/optionals/expected/0914-optionals-typed-array-literal-null-element.stdout new file mode 100644 index 00000000..20b5bf7d --- /dev/null +++ b/examples/optionals/expected/0914-optionals-typed-array-literal-null-element.stdout @@ -0,0 +1,5 @@ +7 +-1 -1 5 +1 2 +3 +1 2 3 diff --git a/examples/types/0199-types-tuple-positional-optional-element.sx b/examples/types/0199-types-tuple-positional-optional-element.sx new file mode 100644 index 00000000..1b74038a --- /dev/null +++ b/examples/types/0199-types-tuple-positional-optional-element.sx @@ -0,0 +1,34 @@ +// A positional literal `.{ a, b }` whose target is a TUPLE coerces each +// element to the tuple's per-position field type — so an optional field gets a +// properly wrapped `{T,i1}` value, an int element narrows/widens to a float +// field, etc. +// +// Regression (issue 0174): the positional struct-literal path coerced +// array/vector elements and struct fields but NOT tuple fields, so a bare +// `i64` was stored straight into a `{i64,i1}` optional slot — a present +// optional read back as absent. +#import "modules/std.sx"; + +main :: () { + // Optional + float fields. + t : (?i64, f64) = .{ 7, 3.0 }; + print("{} {}\n", t.0 ?? -1, t.1); // 7 3.000000 + + // int -> float coercion on a tuple element. + u : (f64, i64) = .{ 3, 4 }; + print("{} {}\n", u.0, u.1); // 3.000000 4 + + // Named tuple. + n : (x: ?i64, y: f64) = .{ 5, 2.5 }; + print("{} {}\n", n.x ?? -1, n.y); // 5 2.500000 + + // Variable elements flowing into an optional tuple field. + a := 9; + b := 1.5; + v : (?i64, f64) = .{ a, b }; + print("{} {}\n", v.0 ?? -1, v.1); // 9 1.500000 + + // A bare `null` element into an optional tuple field. + w : (?i64, i64) = .{ null, 8 }; + print("{} {}\n", w.0 ?? -1, w.1); // -1 8 +} diff --git a/examples/types/0200-types-positional-struct-literal-variable-element.sx b/examples/types/0200-types-positional-struct-literal-variable-element.sx new file mode 100644 index 00000000..22bbddcc --- /dev/null +++ b/examples/types/0200-types-positional-struct-literal-variable-element.sx @@ -0,0 +1,63 @@ +// A positional struct literal `S.{ x, ... }` whose first element is a bare +// VARIABLE reference stores the variable's value, not zero. +// +// Regression (issue 0175): the parser PUNS a leading bare identifier into a +// named field `x = x` (the `Vec4.{ w, z }` shorthand), because it can't tell — +// without the struct definition — whether `x` names a field or is a positional +// value. A genuinely positional `.{ x, 2 }` (x not a field) arrived as a +// spurious mixed named/positional literal and the named branch left every real +// field at its default (`0 0`). Lowering now reclassifies a punned name that +// does NOT match any field as positional, and coerces positional elements from +// the lowered value's actual type. +#import "modules/std.sx"; + +P :: struct { a: i64 = 0; b: i64 = 0; } +Q :: struct { a: i64 = 0; b: f64 = 0.0; } +Inner :: struct { v: i64 = 0; } +Outer :: struct { inner: Inner; tag: i64 = 0; } + +foo :: () -> i64 { return 9; } + +main :: () { + x := 5; + + // Positional, variable first element (the core repro). + p : P = .{ x, 2 }; + print("{} {}\n", p.a, p.b); // 5 2 + + // Mixed variable + expression elements. + m : P = .{ x, x + 10 }; + print("{} {}\n", m.a, m.b); // 5 15 + + // i32 variable -> i64 field coercion. + y : i32 = 7; + c : P = .{ y, 2 }; + print("{} {}\n", c.a, c.b); // 7 2 + + // int -> float field coercion (positional). + q : Q = .{ 3, 2 }; + print("{} {}\n", q.a, q.b); // 3 2.000000 + + // Call-expression element. + e : P = .{ foo(), 1 }; + print("{} {}\n", e.a, e.b); // 9 1 + + // Genuine shorthand: `a`/`b` ARE fields of P, so punning is correct. + a := 11; + b := 22; + sh : P = .{ a, b }; + print("{} {}\n", sh.a, sh.b); // 11 22 + + // Mixed named + shorthand (spec form): `b = 99, a`. + mn : P = .{ b = 99, a }; + print("{} {}\n", mn.a, mn.b); // 11 99 + + // Nested [N]Struct positional with a variable element. + arr : [2]P = .{ .{ x, 2 }, .{ 3, 4 } }; + print("{} {} {} {}\n", arr[0].a, arr[0].b, arr[1].a, arr[1].b); // 5 2 3 4 + + // Struct-literal-valued positional field (nested untyped literal resolves + // against its slot type). + o : Outer = .{ .{ v = x }, 9 }; + print("{} {}\n", o.inner.v, o.tag); // 5 9 +} diff --git a/examples/types/expected/0199-types-tuple-positional-optional-element.exit b/examples/types/expected/0199-types-tuple-positional-optional-element.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/types/expected/0199-types-tuple-positional-optional-element.exit @@ -0,0 +1 @@ +0 diff --git a/examples/types/expected/0199-types-tuple-positional-optional-element.stderr b/examples/types/expected/0199-types-tuple-positional-optional-element.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0199-types-tuple-positional-optional-element.stderr @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0199-types-tuple-positional-optional-element.stdout b/examples/types/expected/0199-types-tuple-positional-optional-element.stdout new file mode 100644 index 00000000..f4aedd9c --- /dev/null +++ b/examples/types/expected/0199-types-tuple-positional-optional-element.stdout @@ -0,0 +1,5 @@ +7 3.000000 +3.000000 4 +5 2.500000 +9 1.500000 +-1 8 diff --git a/examples/types/expected/0200-types-positional-struct-literal-variable-element.exit b/examples/types/expected/0200-types-positional-struct-literal-variable-element.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/types/expected/0200-types-positional-struct-literal-variable-element.exit @@ -0,0 +1 @@ +0 diff --git a/examples/types/expected/0200-types-positional-struct-literal-variable-element.stderr b/examples/types/expected/0200-types-positional-struct-literal-variable-element.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0200-types-positional-struct-literal-variable-element.stderr @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0200-types-positional-struct-literal-variable-element.stdout b/examples/types/expected/0200-types-positional-struct-literal-variable-element.stdout new file mode 100644 index 00000000..1025998e --- /dev/null +++ b/examples/types/expected/0200-types-positional-struct-literal-variable-element.stdout @@ -0,0 +1,9 @@ +5 2 +5 15 +7 2 +3 2.000000 +9 1 +11 22 +11 99 +5 2 3 4 +5 9 diff --git a/issues/0173-typed-array-literal-null-element-unresolved.md b/issues/0173-typed-array-literal-null-element-unresolved.md index 0b6878a3..ef37689f 100644 --- a/issues/0173-typed-array-literal-null-element-unresolved.md +++ b/issues/0173-typed-array-literal-null-element-unresolved.md @@ -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 ''` +> 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 `.[...]` diff --git a/issues/0174-tuple-positional-literal-element-not-coerced.md b/issues/0174-tuple-positional-literal-element-not-coerced.md index fcba8e3a..e865a6c8 100644 --- a/issues/0174-tuple-positional-literal-element-not-coerced.md +++ b/issues/0174-tuple-positional-literal-element-not-coerced.md @@ -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 diff --git a/issues/0175-positional-struct-literal-variable-element-zeroed.md b/issues/0175-positional-struct-literal-variable-element-zeroed.md index eb221269..41e8871e 100644 --- a/issues/0175-positional-struct-literal-variable-element-zeroed.md +++ b/issues/0175-positional-struct-literal-variable-element-zeroed.md @@ -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 diff --git a/issues/0176-protocol-typed-struct-field-method-call-aborts.md b/issues/0176-protocol-typed-struct-field-method-call-aborts.md new file mode 100644 index 00000000..17eb9390 --- /dev/null +++ b/issues/0176-protocol-typed-struct-field-method-call-aborts.md @@ -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. diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index b5941ee8..88bfc90c 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -136,8 +136,47 @@ pub fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: a else &.{}; - // Check if any field_init has a name (named literal) - const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null; + // Check if any field_init has a name (named literal). + // + // The parser PUNS a bare identifier element `.{ x, ... }` into a named + // field `x = x` (the shorthand `Vec4.{ w, z }` form, specs §Struct + // Literals), because it cannot know — without the struct definition — + // whether `x` names a field or is a positional value. A POSITIONAL literal + // whose first element is a bare variable (`.{ x, 2 }`, `x` not a field of + // the target) therefore arrives here as `[name=x][name=null]` — a spurious + // mix that the named branch below mis-reorders (the unmatched punned name + // leaves every real field at its default, zeroing the value — issue 0175). + // + // Disambiguate using the struct definition we now have: a punned bare-ident + // field whose name does NOT match any declared field is not a real named + // field — it is a positional element the parser over-eagerly named. If ANY + // such non-field punned name is present, treat the whole literal as + // positional (the only consistent reading: a true named literal names only + // real fields). An explicit `name = expr` (value ≠ bare ident of same name) + // that misses a field is still a genuine — and erroneous — named field, so + // it is NOT reclassified here. + const has_names = blk: { + if (sl.field_inits.len == 0 or sl.field_inits[0].name == null) break :blk false; + if (struct_fields.len > 0) { + for (sl.field_inits) |fi| { + const fname = fi.name orelse continue; + const is_punned = fi.value.data == .identifier and + std.mem.eql(u8, fi.value.data.identifier.name, fname); + if (!is_punned) continue; + var matches_field = false; + for (struct_fields) |sf| { + if (std.mem.eql(u8, self.module.types.getString(sf.name), fname)) { + matches_field = true; + break; + } + } + // A punned name that is not a field name → this was a positional + // element the parser named; the literal is positional. + if (!matches_field) break :blk false; + } + } + break :blk true; + }; if (has_names and struct_fields.len > 0) { // Named literal: reorder fields to match struct declaration order @@ -223,22 +262,48 @@ pub fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: a else => .unresolved, } else .unresolved; + // A TUPLE target `(T0, T1, …)` is neither a struct (so `struct_fields` is + // empty) nor an array/vector (so `array_elem_ty` is `.unresolved`) — yet a + // positional `.{ a, b }` against it must still coerce element `i` to the + // tuple's per-position field type, exactly as a struct positional element + // is coerced to `struct_fields[i].ty`. Without this a bare element flows + // into the field slot with the wrong shape (e.g. a bare `i64` into a + // `{i64,i1}` optional slot — the present optional reads back as absent). + // Issue 0174. `TupleInfo.fields[i]` is the i-th tuple field type. + const tuple_fields: []const TypeId = if (!ty.isBuiltin()) switch (self.module.types.get(ty)) { + .tuple => |t| t.fields, + else => &.{}, + } else &.{}; + var fields = std.ArrayList(Ref).empty; defer fields.deinit(self.alloc); for (sl.field_inits, 0..) |fi, i| { const saved_tt = self.target_type; - if (array_elem_ty != .unresolved) self.target_type = array_elem_ty; + // Steer literal lowering with the destination element/field type so a + // nested untyped literal element (`.{ .{ v = x }, … }`, `null`, an enum + // literal) resolves against its real slot type — mirrors the named + // branch (which sets `target_type` to `sf.ty`). The actual wrap/erase + // still happens in `coerceToType` below. + const elem_target: TypeId = if (i < struct_fields.len) + struct_fields[i].ty + else if (i < tuple_fields.len) + tuple_fields[i] + else + array_elem_ty; + if (elem_target != .unresolved) self.target_type = elem_target; var val = self.lowerExpr(fi.value); self.target_type = saved_tt; - // Coerce field value to match struct field type - if (i < struct_fields.len) { - const src_ty = self.inferExprType(fi.value); - val = self.coerceToType(val, src_ty, struct_fields[i].ty); - } else if (array_elem_ty != .unresolved) { + // Coerce field value to match the destination field/element type. + // Coerce from the value's ACTUAL lowered type (`getRefType`) rather + // than a re-inferred source type: a re-inference of a punned positional + // identifier (`.{ x, … }`, parser-named `x = x`) could disagree with + // the SSA value's real type and mis-narrow it. The lowered ref's type + // is authoritative (issue 0175). + if (elem_target != .unresolved) { const src_ty = self.builder.getRefType(val); - if (src_ty != array_elem_ty) { - val = self.coerceToType(val, src_ty, array_elem_ty); + if (src_ty != elem_target) { + val = self.coerceToType(val, src_ty, elem_target); } } fields.append(self.alloc, val) catch unreachable; @@ -1633,6 +1698,19 @@ pub fn resolveArrayLiteralType(self: *Lowering, te: *const Node) TypeId { if (self.headTypeLeak(inner.name, te.span)) return .unresolved; return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map); }, + // Structural type heads on a typed `.[...]` literal — `[N]T`, `[]T`. + // These resolve through the canonical `resolveAstType` compound path + // (which recurses into the element, so `[N]?T` correctly carries the + // optional element). Without these arms an `array_type_expr` / + // `slice_type_expr` head fell through to `else => .unresolved`, so a + // typed `([2]?i64).[ ... ]` lost its `?i64` element type — the null + // element then reached LLVM as `const_null(.unresolved)` and panicked + // (issue 0173). `resolveTypeWithBindings` is the lowering-side resolver + // (carries generic bindings); it delegates to `resolveAstType` for + // these plain structural shapes. + .array_type_expr, + .slice_type_expr, + => return self.resolveTypeWithBindings(te), .field_access => |fa| { // Module.Type — try to resolve the field as a type name const name_id = self.module.types.internString(fa.field); diff --git a/src/ir/semantic_diagnostics.zig b/src/ir/semantic_diagnostics.zig index a7e14063..164c8319 100644 --- a/src/ir/semantic_diagnostics.zig +++ b/src/ir/semantic_diagnostics.zig @@ -647,7 +647,22 @@ pub const UnknownTypeChecker = struct { for (sl.field_inits) |fi| self.walkBodyTypes(fi.value, declared, in_scope, type_vals); if (sl.init_block) |ib| self.walkBodyTypes(ib, declared, in_scope, type_vals); }, - .array_literal => |al| for (al.elements) |e| self.walkBodyTypes(e, declared, in_scope, type_vals), + .array_literal => |al| { + // A TYPED array/slice literal head (`([N]T).[…]` / `([]T).[…]`) + // names its element type exactly like a declaration annotation — + // validate it through the same unknown-type walk. Without this, + // an undefined element name (`([2]?Undefined).[…]`) bypassed the + // checker and reached the lowering's forward-ref stub, silently + // compiling with an empty-struct element instead of erroring + // like the `x: [2]?Undefined` declaration path (issues 0173–0175 + // adversarial review). `checkTypeNodeForUnknown` recurses the + // `[N]?T` / `[]T` head down to its leaf type name and skips + // forward-refs (`declared`), generics (`in_scope`), aliases, and + // parameterized element types — so only genuinely-undeclared + // names are flagged. + if (al.type_expr) |th| self.checkTypeNodeForUnknown(th, declared, in_scope.items, type_vals.items); + for (al.elements) |e| self.walkBodyTypes(e, declared, in_scope, type_vals); + }, .force_unwrap => |fu| self.walkBodyTypes(fu.operand, declared, in_scope, type_vals), .null_coalesce => |nc| { self.walkBodyTypes(nc.lhs, declared, in_scope, type_vals);