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:
86
src/lsp/document.test.zig
Normal file
86
src/lsp/document.test.zig
Normal file
@@ -0,0 +1,86 @@
|
||||
const std = @import("std");
|
||||
const sx = struct {
|
||||
pub const sema = @import("../sema.zig");
|
||||
pub const types = @import("../types.zig");
|
||||
};
|
||||
const doc_mod = @import("document.zig");
|
||||
|
||||
// Minimal LSP test harness (issue 0099): drive the editor analyzer through the
|
||||
// real didOpen path (`DocumentStore.analyzeDocument`) and inspect the resulting
|
||||
// editor index. This is the FIRST `src/lsp/*.test.zig`; it is pulled into the
|
||||
// `zig build test` graph via the `_ = lsp.document;` reference in `src/root.zig`
|
||||
// (its tests live one struct deeper than `refAllDecls` reaches).
|
||||
|
||||
var g_test_threaded: ?std.Io.Threaded = null;
|
||||
fn test_io() std.Io {
|
||||
if (g_test_threaded == null) {
|
||||
g_test_threaded = std.Io.Threaded.init(std.heap.page_allocator, .{});
|
||||
}
|
||||
return g_test_threaded.?.io();
|
||||
}
|
||||
|
||||
/// The editor `Type` recorded for field `field` of struct `type_name` in the
|
||||
/// document's index, or null if the document/struct/field isn't present.
|
||||
fn fieldTypeOf(doc: *doc_mod.Document, type_name: []const u8, field: []const u8) ?sx.types.Type {
|
||||
const sema = doc.sema orelse return null;
|
||||
const info = sema.struct_types.get(type_name) orelse return null;
|
||||
for (info.field_names, info.field_types) |fname, fty| {
|
||||
if (std.mem.eql(u8, fname, field)) return fty;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
test "analyzeDocument: identifier array dimension folds to the const value (issue 0099)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var store = doc_mod.DocumentStore.init(alloc, test_io(), &.{});
|
||||
const src: [:0]const u8 =
|
||||
\\MAX :: 4;
|
||||
\\Thing :: struct { buf: [MAX]u8; }
|
||||
;
|
||||
const doc = try store.openOrUpdate("main.sx", src, 1);
|
||||
// Pre-fix this aborts inside `resolveTypeNode`: the array `.length` node is
|
||||
// an `identifier` (named const), but the code read `.int_literal`
|
||||
// unconditionally. Reaching the assertions at all proves the crash is gone.
|
||||
try store.analyzeDocument(doc);
|
||||
|
||||
const buf_ty = fieldTypeOf(doc, "Thing", "buf") orelse return error.SkipZigTest;
|
||||
try std.testing.expect(buf_ty == .array_type);
|
||||
try std.testing.expectEqual(@as(?u32, 4), buf_ty.array_type.length);
|
||||
try std.testing.expectEqualStrings("u8", buf_ty.array_type.element_name);
|
||||
}
|
||||
|
||||
test "analyzeDocument: int-literal array dimension still resolves to its length" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var store = doc_mod.DocumentStore.init(alloc, test_io(), &.{});
|
||||
const src: [:0]const u8 = "Buf :: struct { data: [64]u8; }";
|
||||
const doc = try store.openOrUpdate("main.sx", src, 1);
|
||||
try store.analyzeDocument(doc);
|
||||
|
||||
const data_ty = fieldTypeOf(doc, "Buf", "data") orelse return error.SkipZigTest;
|
||||
try std.testing.expect(data_ty == .array_type);
|
||||
try std.testing.expectEqual(@as(?u32, 64), data_ty.array_type.length);
|
||||
try std.testing.expectEqualStrings("u8", data_ty.array_type.element_name);
|
||||
}
|
||||
|
||||
test "analyzeDocument: unresolvable array dimension records an explicit unknown length" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var store = doc_mod.DocumentStore.init(alloc, test_io(), &.{});
|
||||
// `N` is never declared as an integer const → the dimension is unknown.
|
||||
// Must not panic and must not fabricate a concrete length.
|
||||
const src: [:0]const u8 = "Holder :: struct { slots: [N]u8; }";
|
||||
const doc = try store.openOrUpdate("main.sx", src, 1);
|
||||
try store.analyzeDocument(doc);
|
||||
|
||||
const slots_ty = fieldTypeOf(doc, "Holder", "slots") orelse return error.SkipZigTest;
|
||||
try std.testing.expect(slots_ty == .array_type);
|
||||
try std.testing.expectEqual(@as(?u32, null), slots_ty.array_type.length);
|
||||
}
|
||||
Reference in New Issue
Block a user