lsp/sema: resolve generic-struct field indexing in hover

inferExprType returned <unresolved> for 'legal.items[i]' (a List(Move) indexed) for two reasons: index_expr only handled string/array — not many-pointers/slices — and generic instantiation was dropped (List(Move) tracked as bare List, so T never bound to Move).

Fixes: (1) fieldType preserves pointer/slice element names (the old Type.fromTypeExpr only handled plain type_expr nodes, so [*]T became unresolved); (2) index_expr/slice_expr resolve many-pointer + slice elements via a registry-aware resolveTypeNameStr that knows user structs/enums (unlike Type.fromName); (3) instantiateGeneric monomorphizes List(Move) into a struct_types entry with T->Move substituted. So legal.items -> [*]Move and m -> Move. Regression test added.
This commit is contained in:
agra
2026-05-30 18:25:28 +03:00
parent fb8a5399f1
commit 7c7a5ad5c7

View File

@@ -46,6 +46,7 @@ pub const FnSignature = struct {
pub const StructTypeInfo = struct {
field_names: []const []const u8,
field_types: []const Type,
type_params: []const []const u8 = &.{},
};
pub const TypeMap = std.AutoHashMap(*const Node, Type);
@@ -207,6 +208,11 @@ pub const Analyzer = struct {
},
.struct_decl => |sd| {
try self.addSymbol(sd.name, .struct_type, .{ .struct_type = sd.name }, node.span);
var tp_names = std.ArrayList([]const u8).empty;
for (sd.type_params) |p| {
try tp_names.append(self.allocator, p.name);
}
const tp_slice = try tp_names.toOwnedSlice(self.allocator);
// Populate struct_types registry, expanding #using entries
if (sd.using_entries.len > 0) {
var all_names = std.ArrayList([]const u8).empty;
@@ -227,23 +233,25 @@ pub const Analyzer = struct {
}
if (i < sd.field_names.len) {
try all_names.append(self.allocator, sd.field_names[i]);
const resolved = Type.fromTypeExpr(sd.field_types[i]) orelse Type.unresolved;
const resolved = self.fieldType(sd.field_types[i]);
try all_types.append(self.allocator, resolved);
}
}
try self.struct_types.put(sd.name, .{
.field_names = try all_names.toOwnedSlice(self.allocator),
.field_types = try all_types.toOwnedSlice(self.allocator),
.type_params = tp_slice,
});
} else {
var field_types = std.ArrayList(Type).empty;
for (sd.field_types) |ft| {
const resolved = Type.fromTypeExpr(ft) orelse Type.unresolved;
const resolved = self.fieldType(ft);
try field_types.append(self.allocator, resolved);
}
try self.struct_types.put(sd.name, .{
.field_names = sd.field_names,
.field_types = try field_types.toOwnedSlice(self.allocator),
.type_params = tp_slice,
});
}
},
@@ -344,6 +352,117 @@ pub const Analyzer = struct {
return .void_type;
}
/// Resolve a bare type-name string against the registry (aliases, enums,
/// structs), falling back to primitive spellings. Unlike `Type.fromName`,
/// this knows user-defined types; returns `unresolved` when it can't place
/// the name.
fn resolveTypeNameStr(self: *Analyzer, name: []const u8) Type {
if (Type.fromName(name)) |t| return t;
if (self.type_aliases.get(name)) |target| {
if (Type.fromName(target)) |t| return t;
if (self.struct_types.contains(target)) return .{ .struct_type = target };
if (self.enum_types.contains(target)) return .{ .enum_type = target };
}
if (self.enum_types.contains(name)) return .{ .enum_type = name };
if (self.struct_types.contains(name)) return .{ .struct_type = name };
return Type.unresolved;
}
/// Extract the element/pointee name from a type-expr node, keeping generic
/// param names (`T`) intact for later substitution. Compound shapes fall
/// back to their spelled form.
fn typeExprName(self: *Analyzer, node: *Node) []const u8 {
return switch (node.data) {
.type_expr => |te| te.name,
.identifier => |id| id.name,
else => (self.resolveTypeNode(node)).displayName(self.allocator) catch "",
};
}
/// Resolve a struct field's declared type, preserving the raw element/
/// pointee name of pointer/slice shapes so generic params (`T`) survive
/// into `instantiateGeneric`'s substitution. Bare names resolve through the
/// registry; the element name is resolved lazily at index/field time.
fn fieldType(self: *Analyzer, node: *Node) Type {
return switch (node.data) {
.type_expr => |te| self.resolveTypeNameStr(te.name),
.identifier => |id| self.resolveTypeNameStr(id.name),
.many_pointer_type_expr => |mp| .{ .many_pointer_type = .{ .element_name = self.typeExprName(mp.element_type) } },
.pointer_type_expr => |p| .{ .pointer_type = .{ .pointee_name = self.typeExprName(p.pointee_type) } },
.slice_type_expr => |s| .{ .slice_type = .{ .element_name = self.typeExprName(s.element_type) } },
else => self.resolveTypeNode(node),
};
}
/// The type name an instantiation arg node carries (`Move` in
/// `List(Move)`). Null for non-nameable args (e.g. value params like `3`).
fn argTypeName(node: *const Node) ?[]const u8 {
return switch (node.data) {
.type_expr => |te| te.name,
.identifier => |id| id.name,
else => null,
};
}
/// Swap a single type name through a param→arg map (`T` → `Move`).
fn substName(name: []const u8, params: []const []const u8, args: []const []const u8) []const u8 {
for (params, 0..) |p, i| {
if (i < args.len and std.mem.eql(u8, p, name)) return args[i];
}
return name;
}
/// Substitute generic params in an already-resolved field type. Only the
/// name-carrying shapes need rewriting; the rest pass through.
fn substType(ty: Type, params: []const []const u8, args: []const []const u8) Type {
return switch (ty) {
.many_pointer_type => |i| .{ .many_pointer_type = .{ .element_name = substName(i.element_name, params, args) } },
.slice_type => |i| .{ .slice_type = .{ .element_name = substName(i.element_name, params, args) } },
.array_type => |i| .{ .array_type = .{ .length = i.length, .element_name = substName(i.element_name, params, args) } },
.pointer_type => |i| .{ .pointer_type = .{ .pointee_name = substName(i.pointee_name, params, args) } },
.struct_type => |n| .{ .struct_type = substName(n, params, args) },
else => ty,
};
}
/// Instantiate `base(args...)` as a monomorphized struct entry so field
/// access resolves the generic params (`List(Move).items` → `[*]Move`).
/// Returns the instance type, or null when `base` isn't a generic struct,
/// the arity mismatches, or an arg isn't nameable.
fn instantiateGeneric(self: *Analyzer, base: []const u8, arg_nodes: []const *Node) ?Type {
const info = self.struct_types.get(base) orelse return null;
if (info.type_params.len == 0 or arg_nodes.len != info.type_params.len) return null;
var args = std.ArrayList([]const u8).empty;
for (arg_nodes) |an| {
const nm = argTypeName(an) orelse return null;
args.append(self.allocator, nm) catch return null;
}
// Mangle "base(A,B)".
var name_buf = std.ArrayList(u8).empty;
name_buf.appendSlice(self.allocator, base) catch return null;
name_buf.append(self.allocator, '(') catch return null;
for (args.items, 0..) |a, i| {
if (i > 0) name_buf.append(self.allocator, ',') catch return null;
name_buf.appendSlice(self.allocator, a) catch return null;
}
name_buf.append(self.allocator, ')') catch return null;
const mangled = name_buf.toOwnedSlice(self.allocator) catch return null;
if (self.struct_types.contains(mangled)) return .{ .struct_type = mangled };
var new_fts = std.ArrayList(Type).empty;
for (info.field_types) |ft| {
new_fts.append(self.allocator, substType(ft, info.type_params, args.items)) catch return null;
}
self.struct_types.put(mangled, .{
.field_names = info.field_names,
.field_types = new_fts.toOwnedSlice(self.allocator) catch return null,
}) catch return null;
return .{ .struct_type = mangled };
}
/// Infer the type of an expression node without LLVM.
/// Uses fn_signatures for call return types, struct_types for field access,
/// symbols for identifier types, and Type.widen for arithmetic promotion.
@@ -438,15 +557,16 @@ pub const Analyzer = struct {
.index_expr => |ie| {
const obj_ty = self.inferExprType(ie.object);
if (obj_ty == .string_type) return Type.u(8);
if (obj_ty.isArray()) {
return Type.fromName(obj_ty.array_type.element_name) orelse Type.unresolved;
}
if (obj_ty.isArray()) return self.resolveTypeNameStr(obj_ty.array_type.element_name);
if (obj_ty.isManyPointer()) return self.resolveTypeNameStr(obj_ty.many_pointer_type.element_name);
if (obj_ty.isSlice()) return self.resolveTypeNameStr(obj_ty.slice_type.element_name);
return Type.unresolved;
},
.slice_expr => |se| {
const obj_ty = self.inferExprType(se.object);
if (obj_ty == .string_type) return .string_type;
if (obj_ty.isArray()) return .{ .slice_type = .{ .element_name = obj_ty.array_type.element_name } };
if (obj_ty.isManyPointer()) return .{ .slice_type = .{ .element_name = obj_ty.many_pointer_type.element_name } };
if (obj_ty.isSlice()) return obj_ty;
return .void_type;
},
@@ -466,6 +586,7 @@ pub const Analyzer = struct {
// Handle parameterized struct: List(s32).{} parses as call node
if (te.data == .call) {
if (self.resolveCalleeName(te.data.call)) |callee| {
if (self.instantiateGeneric(callee, te.data.call.args)) |inst| return inst;
if (self.struct_types.contains(callee)) return .{ .struct_type = callee };
}
}
@@ -492,6 +613,7 @@ pub const Analyzer = struct {
.array_literal => .void_type,
.type_expr => |te| .{ .meta_type = .{ .name = te.name } },
.parameterized_type_expr => |pte| {
if (self.instantiateGeneric(pte.name, pte.args)) |inst| return inst;
if (self.struct_types.contains(pte.name)) return .{ .struct_type = pte.name };
return .void_type;
},
@@ -1634,6 +1756,36 @@ test "sema: var_decl infers struct type from parameterized call literal" {
try std.testing.expect(found_list);
}
test "sema: index into generic List(T).items resolves the element struct" {
const parser_mod = @import("parser.zig");
const source =
"Move :: struct { score: s64; }" ++
"List :: struct ($T: Type) { items: [*]T = null; len: s64; }" ++
"main :: () { legal := List(Move).{}; m := legal.items[0]; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var parser = parser_mod.Parser.init(alloc, source);
const root = try parser.parse();
var analyzer = Analyzer.init(alloc);
const result = try analyzer.analyze(root);
var found_m = false;
for (result.symbols) |sym| {
if (std.mem.eql(u8, sym.name, "m")) {
found_m = true;
const ty = sym.ty orelse return error.TestUnexpectedResult;
try std.testing.expect(ty == .struct_type);
try std.testing.expectEqualStrings("Move", ty.struct_type);
break;
}
}
try std.testing.expect(found_m);
}
test "sema: variable shadowing in same scope is allowed" {
const parser_mod = @import("parser.zig");