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

@@ -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);

View File

@@ -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 01730175
// 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);