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

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

View File

@@ -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 ];
| ^^^^^^^^^

View File

@@ -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
}

View File

@@ -0,0 +1,5 @@
7
-1 -1 5
1 2
3
1 2 3

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,5 @@
7 3.000000
3.000000 4
5 2.500000
9 1.500000
-1 8

View File

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