fix(lsp): identifier array dimension no longer panics the analyzer [0099]

`Analyzer.resolveTypeNode` read the array `.length` node's `.int_literal`
union field unconditionally. For a named-const dimension (`MAX :: 4;
[MAX]u8`) that node is an `identifier`, so the access tripped Zig's
checked-union panic and `sx lsp` aborted on didOpen. The main compiler
was unaffected (it folds the dim through the IR).

- New `arrayDimLength` helper switches on the dimension node tag:
  int_literal → value; identifier → a recorded module-const int value;
  anything else / out-of-u32-range → unknown. Never assumes a node shape.
- `Type.ArrayTypeInfo.length` is now `?u32`; null is an explicit "editor
  couldn't fold this dimension" marker (rendered `[_]T`), never a
  fabricated concrete length.
- New `const_int_values` registry records integer-literal consts at
  registration time for the identifier path.

Regression: first `src/lsp/*.test.zig` (the minimal LSP harness), wired
into the test graph via `src/root.zig`. Drives `analyzeDocument` over
`[MAX]u8` (folds to 4, no panic), `[64]u8` (happy-path guard), and
`[N]u8` (explicit unknown). Fail-before/pass-after verified.

Sibling audit of the resolveTypeNode/fieldType family: the array dim was
the only unchecked union-field access; all other arms recurse or
tag-check first. Noted a non-crashing display gap in server.zig hover
rendering for step B.
This commit is contained in:
agra
2026-06-05 23:33:22 +03:00
parent 8eb514a804
commit d515696e61
5 changed files with 228 additions and 3 deletions

View File

@@ -114,6 +114,13 @@ pub const Analyzer = struct {
struct_types: std.StringHashMap(StructTypeInfo),
enum_types: std.StringHashMap([]const []const u8),
type_aliases: std.StringHashMap([]const u8),
/// Module-global integer consts, by bare name → value. Lets an array
/// dimension written as a named const (`MAX :: 4; [MAX]u8`) fold to a
/// concrete editor length instead of panicking on the `.int_literal`
/// union access (issue 0099). Populated at registration time, so it shares
/// the analyzer's existing intra-pass forward-reference limitation (a const
/// declared after the struct that uses it resolves to "unknown" length).
const_int_values: std.StringHashMap(i64),
type_map: TypeMap,
pub fn init(allocator: std.mem.Allocator) Analyzer {
@@ -130,6 +137,7 @@ pub const Analyzer = struct {
.struct_types = std.StringHashMap(StructTypeInfo).init(allocator),
.enum_types = std.StringHashMap([]const []const u8).init(allocator),
.type_aliases = std.StringHashMap([]const u8).init(allocator),
.const_int_values = std.StringHashMap(i64).init(allocator),
.type_map = TypeMap.init(allocator),
};
}
@@ -217,6 +225,11 @@ pub const Analyzer = struct {
const ty = self.resolveTypeAnnotation(cd.type_annotation) orelse inferValueType(cd.value);
const kind = classifyConstDecl(cd);
try self.addSymbol(cd.name, kind, ty, node.span);
// Record integer-literal consts so a named array dimension
// (`MAX :: 4; [MAX]u8`) can fold to a concrete length (issue 0099).
if (cd.value.data == .int_literal) {
try self.const_int_values.put(cd.name, cd.value.data.int_literal.value);
}
// Populate type_aliases registry
if (cd.value.data == .type_expr) {
try self.type_aliases.put(cd.name, cd.value.data.type_expr.name);
@@ -354,6 +367,23 @@ pub const Analyzer = struct {
try self.fn_signatures.put(key, .{ .param_types = &.{}, .return_type = ret });
}
/// Fold an array dimension node to a concrete editor length, or null
/// ("unknown") when it isn't a compile-time integer this index can resolve.
/// Metadata-only — NEVER panic on an unexpected node shape and never
/// fabricate a misleading concrete length (issue 0099). A literal dim is
/// taken directly; a bare identifier naming an integer const folds to its
/// recorded value; anything else (unknown name, non-const expression,
/// out-of-`u32`-range value) is unknown.
fn arrayDimLength(self: *Analyzer, len_node: *Node) ?u32 {
const v: i64 = switch (len_node.data) {
.int_literal => |lit| lit.value,
.identifier => |id| self.const_int_values.get(id.name) orelse return null,
else => return null,
};
if (v < 0 or v > std.math.maxInt(u32)) return null;
return @intCast(v);
}
/// Resolve a type annotation node to an editor `Type` (metadata only — the
/// compiler resolves type nodes to canonical `TypeId` in `src/ir/`).
/// Handles primitives, type_expr, array_type_expr, parameterized_type_expr,
@@ -364,7 +394,7 @@ pub const Analyzer = struct {
// Array type: [N]T
if (tn.data == .array_type_expr) {
const ate = tn.data.array_type_expr;
const length: u32 = @intCast(ate.length.data.int_literal.value);
const length = self.arrayDimLength(ate.length);
const elem_type = self.resolveTypeNode(ate.element_type);
const elem_name = elem_type.displayName(self.allocator) catch return .void_type;
return .{ .array_type = .{ .element_name = elem_name, .length = length, .is_raw = typeExprIsRaw(ate.element_type) } };