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:
32
src/sema.zig
32
src/sema.zig
@@ -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) } };
|
||||
|
||||
Reference in New Issue
Block a user