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:
@@ -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 '<name>'` 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");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -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 ];
|
||||
| ^^^^^^^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
7
|
||||
-1 -1 5
|
||||
1 2
|
||||
3
|
||||
1 2 3
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
7 3.000000
|
||||
3.000000 4
|
||||
5 2.500000
|
||||
9 1.500000
|
||||
-1 8
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
@@ -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 `.[...]`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user