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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user