lsp/sema: resolve method-return types, slice .ptr/.len, tagged enums

ev := events.ptr[i] (events := g_plat.poll_events()) was <unresolved> through three gaps:

1. Return types went through Type.fromTypeExpr, which only handles a bare type_expr — so any []T / *T / List(T) return became void. An impl method 'poll_events -> []Event' registered as void and, merged after the protocol's correct signature, clobbered it. resolveReturnType now uses fieldType.

2. Struct/protocol methods were never put in fn_signatures, so recv.method() and Type.static() return types never resolved. registerMethodSig now adds them by bare name (first-wins), which is what resolveCalleeName already assumed.

3. .ptr/.len field access was string-only (and string.ptr wrongly returned string_type); now handles slices/arrays and returns the proper many-pointer element.

4. Tagged enums (payload variants) were only a symbol, never in a lookup registry; now also recorded in enum_types so the name resolves as a type.

Net: events -> []Event, events.ptr -> [*]Event, ev -> Event. Regression test added; confirmed end-to-end via the LSP inlay hint.
This commit is contained in:
agra
2026-05-31 10:04:08 +03:00
parent 8be1deea93
commit 4415274894

View File

@@ -130,7 +130,7 @@ pub const Analyzer = struct {
fn registerTopLevelDeclPrefixed(self: *Analyzer, node: *Node, ns_prefix: ?[]const u8) !void {
switch (node.data) {
.fn_decl => |fd| {
const ret_ty = resolveReturnType(fd) orelse
const ret_ty = self.resolveReturnType(fd) orelse
if (fd.is_arrow) self.inferFnReturnType(fd.params, fd.body) else null;
try self.addSymbol(fd.name, .function, ret_ty, node.span);
// Populate fn_signatures registry
@@ -198,8 +198,10 @@ pub const Analyzer = struct {
},
.enum_decl => |ed| {
if (ed.variant_types.len > 0) {
// Tagged enum with payloads
// Tagged enum with payloads. Also recorded in `enum_types` so
// the name resolves as a type (e.g. a `[*]Event` element).
try self.addSymbol(ed.name, .enum_type, .{ .union_type = ed.name }, node.span);
try self.enum_types.put(ed.name, ed.variant_names);
} else {
// Payload-less enum
try self.addSymbol(ed.name, .enum_type, .{ .enum_type = ed.name }, node.span);
@@ -254,6 +256,15 @@ pub const Analyzer = struct {
.type_params = tp_slice,
});
}
for (sd.methods) |mnode| {
if (mnode.data == .fn_decl)
try self.registerMethodSig(mnode.data.fn_decl.name, ns_prefix, mnode.data.fn_decl.return_type);
}
},
.protocol_decl => |pd| {
for (pd.methods) |m| {
try self.registerMethodSig(m.name, ns_prefix, m.return_type);
}
},
.union_decl => |ud| {
try self.addSymbol(ud.name, .enum_type, .{ .union_type = ud.name }, node.span);
@@ -275,6 +286,19 @@ pub const Analyzer = struct {
}
}
/// Register a method's return type in `fn_signatures` under its bare name
/// (first-wins) so `recv.method()` call inference can resolve it. Params
/// are omitted — only the return type is consulted for type inference.
fn registerMethodSig(self: *Analyzer, name: []const u8, ns_prefix: ?[]const u8, ret_node: ?*Node) !void {
const key = if (ns_prefix) |pfx|
try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ pfx, name })
else
name;
if (self.fn_signatures.contains(key)) return;
const ret = if (ret_node) |rt| self.fieldType(rt) else Type.void_type;
try self.fn_signatures.put(key, .{ .param_types = &.{}, .return_type = ret });
}
/// Resolve a type annotation node to a Type.
/// Handles primitives, type_expr, array_type_expr, parameterized_type_expr,
/// type aliases, enum types, and struct types.
@@ -536,9 +560,14 @@ pub const Analyzer = struct {
},
.field_access => |fa| {
const obj_ty = self.inferExprType(fa.object);
if (obj_ty == .string_type) {
if (std.mem.eql(u8, fa.field, "len")) return Type.s(64);
if (std.mem.eql(u8, fa.field, "ptr")) return .string_type;
// `.len` / `.ptr` on the built-in containers (string, slice, array).
if (std.mem.eql(u8, fa.field, "len")) {
if (obj_ty == .string_type or obj_ty.isSlice() or obj_ty.isArray()) return Type.s(64);
}
if (std.mem.eql(u8, fa.field, "ptr")) {
if (obj_ty == .string_type) return .{ .many_pointer_type = .{ .element_name = "u8" } };
if (obj_ty.isSlice()) return .{ .many_pointer_type = .{ .element_name = obj_ty.slice_type.element_name } };
if (obj_ty.isArray()) return .{ .many_pointer_type = .{ .element_name = obj_ty.array_type.element_name } };
}
if (obj_ty.isStruct()) {
if (self.struct_types.get(obj_ty.struct_type)) |info| {
@@ -797,7 +826,7 @@ pub const Analyzer = struct {
fn analyzeNode(self: *Analyzer, node: *Node) !void {
switch (node.data) {
.fn_decl => |fd| {
const local_ret_ty = resolveReturnType(fd) orelse
const local_ret_ty = self.resolveReturnType(fd) orelse
if (fd.is_arrow) self.inferFnReturnType(fd.params, fd.body) else null;
try self.addSymbol(fd.name, .function, local_ret_ty, node.span);
// Register fn_signatures for local functions (for return type hints + hover)
@@ -1142,9 +1171,12 @@ pub const Analyzer = struct {
}
}
fn resolveReturnType(fd: ast.FnDecl) ?Type {
fn resolveReturnType(self: *Analyzer, fd: ast.FnDecl) ?Type {
if (fd.return_type) |rt| {
return Type.fromTypeExpr(rt);
// `fieldType`, not `Type.fromTypeExpr` — the latter only handles a
// bare `type_expr`, so a `[]T` / `*T` / `[*]T` return silently
// became void and clobbered correct signatures on merge.
return self.fieldType(rt);
}
return null;
}
@@ -1863,6 +1895,35 @@ test "sema: generic index resolves with realistic List/Move (methods, cross-refs
try std.testing.expectEqualStrings("Square", f_ty.?.struct_type);
}
test "sema: method-return slice + .ptr index + tagged-enum element" {
const parser_mod = @import("parser.zig");
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const source =
"Event :: enum { none; click: s64; }" ++
"Plat :: protocol { poll :: () -> []Event; }" ++
"go :: (p: *Plat) { evs := p.poll(); e := evs.ptr[0]; }";
var parser = parser_mod.Parser.init(alloc, source);
const root = try parser.parse();
var an = Analyzer.init(alloc);
const res = try an.analyze(root);
var evs_ty: ?Type = null;
var e_ty: ?Type = null;
for (res.symbols) |sym| {
if (std.mem.eql(u8, sym.name, "evs")) evs_ty = sym.ty;
if (std.mem.eql(u8, sym.name, "e")) e_ty = sym.ty;
}
// `p.poll()` resolves to its slice return type, not void.
try std.testing.expect(evs_ty != null and evs_ty.? == .slice_type);
try std.testing.expectEqualStrings("Event", evs_ty.?.slice_type.element_name);
// `evs.ptr[0]` resolves to the (tagged-enum) element type.
try std.testing.expect(e_ty != null);
try std.testing.expectEqualStrings("Event", e_ty.?.toName().?);
}
test "sema: variable shadowing in same scope is allowed" {
const parser_mod = @import("parser.zig");