lsp: resolve for-loop capture types (go-to-def + inlay hints)
The sema analyzer bound a for-loop capture with no type, so navigating or hinting through it (m.flag, m: Move) failed. Instantiate generic field types (legal_moves: List(Move)) and infer the capture's element type from the iterable — List-like structs, slices, arrays, many- pointers, and a pointer followed to its pointee. By-ref captures bind a pointer to the element; range cursors bind s64. Inlay hints now descend into struct method bodies and emit a type label for the capture itself.
This commit is contained in:
@@ -1241,8 +1241,14 @@ pub const Server = struct {
|
|||||||
collectInlayHints(allocator, we.body, symbols, fn_signatures, source, hints);
|
collectInlayHints(allocator, we.body, symbols, fn_signatures, source, hints);
|
||||||
},
|
},
|
||||||
.for_expr => |fe| {
|
.for_expr => |fe| {
|
||||||
|
if (!std.mem.eql(u8, fe.capture_name, "_")) {
|
||||||
|
addForCaptureHint(allocator, fe.capture_name, node.span, symbols, source, hints);
|
||||||
|
}
|
||||||
collectInlayHints(allocator, fe.body, symbols, fn_signatures, source, hints);
|
collectInlayHints(allocator, fe.body, symbols, fn_signatures, source, hints);
|
||||||
},
|
},
|
||||||
|
.struct_decl => |sd| {
|
||||||
|
for (sd.methods) |m| collectInlayHints(allocator, m, symbols, fn_signatures, source, hints);
|
||||||
|
},
|
||||||
.var_decl => |vd| {
|
.var_decl => |vd| {
|
||||||
// Only show hint when type is inferred (:= syntax)
|
// Only show hint when type is inferred (:= syntax)
|
||||||
if (vd.type_annotation != null) return;
|
if (vd.type_annotation != null) return;
|
||||||
@@ -1265,6 +1271,14 @@ pub const Server = struct {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Struct methods carry their own bodies — descend so locals
|
||||||
|
// (and `for` captures) inside them get hints too.
|
||||||
|
if (cd.value.data == .struct_decl) {
|
||||||
|
for (cd.value.data.struct_decl.methods) |m| {
|
||||||
|
collectInlayHints(allocator, m, symbols, fn_signatures, source, hints);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Skip functions, types, structs, enums, unions, comptime, foreign, library
|
// Skip functions, types, structs, enums, unions, comptime, foreign, library
|
||||||
switch (cd.value.data) {
|
switch (cd.value.data) {
|
||||||
.fn_decl, .type_expr, .struct_decl, .enum_decl, .union_decl,
|
.fn_decl, .type_expr, .struct_decl, .enum_decl, .union_decl,
|
||||||
@@ -1371,6 +1385,38 @@ pub const Server = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hint for a `for` loop capture (`for xs: (m)` → `m: T`). The capture has
|
||||||
|
/// no `:=`/`::`, so the label carries its own colon and lands right after
|
||||||
|
/// the capture name, whose slice points into `source`.
|
||||||
|
fn addForCaptureHint(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
name: []const u8,
|
||||||
|
span: sx.ast.Span,
|
||||||
|
symbols: []const sx.sema.Symbol,
|
||||||
|
source: [:0]const u8,
|
||||||
|
hints: *std.ArrayList(lsp.InlayHint),
|
||||||
|
) void {
|
||||||
|
const sym = findSymbolAtSpan(symbols, span.start, name) orelse return;
|
||||||
|
const ty = sym.ty orelse return;
|
||||||
|
if (ty == .void_type) return;
|
||||||
|
const type_name = ty.displayName(allocator) catch return;
|
||||||
|
const label = std.fmt.allocPrint(allocator, ": {s}", .{type_name}) catch return;
|
||||||
|
|
||||||
|
const base = @intFromPtr(source.ptr);
|
||||||
|
const np = @intFromPtr(name.ptr);
|
||||||
|
if (np < base or np + name.len > base + source.len) return;
|
||||||
|
const end_offset: u32 = @intCast(np - base + name.len);
|
||||||
|
const loc = sx.errors.SourceLoc.compute(source, end_offset);
|
||||||
|
if (loc.line == 0 or loc.col == 0) return;
|
||||||
|
hints.append(allocator, .{
|
||||||
|
.line = loc.line - 1,
|
||||||
|
.character = loc.col - 1,
|
||||||
|
.label = label,
|
||||||
|
.padding_left = false,
|
||||||
|
.padding_right = false,
|
||||||
|
}) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
fn addReturnTypeHint(
|
fn addReturnTypeHint(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
span: sx.ast.Span,
|
span: sx.ast.Span,
|
||||||
|
|||||||
75
src/sema.zig
75
src/sema.zig
@@ -443,10 +443,34 @@ pub const Analyzer = struct {
|
|||||||
.many_pointer_type_expr => |mp| .{ .many_pointer_type = .{ .element_name = self.typeExprName(mp.element_type) } },
|
.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) } },
|
.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) } },
|
.slice_type_expr => |s| .{ .slice_type = .{ .element_name = self.typeExprName(s.element_type) } },
|
||||||
|
.parameterized_type_expr => |pte| self.instantiateGeneric(pte.name, pte.args) orelse self.resolveTypeNode(node),
|
||||||
else => self.resolveTypeNode(node),
|
else => self.resolveTypeNode(node),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The element type yielded by iterating `ty` in a `for` loop: arrays,
|
||||||
|
/// slices and many-pointers give their element; a List-like struct (one
|
||||||
|
/// with an `items: [*]T` field) gives `T`; a pointer is followed to its
|
||||||
|
/// pointee first (so `*List(Move)` still iterates `Move`).
|
||||||
|
fn elementTypeOf(self: *Analyzer, ty: Type) ?Type {
|
||||||
|
return switch (ty) {
|
||||||
|
.array_type => |i| self.resolveTypeNameStr(i.element_name),
|
||||||
|
.slice_type => |i| self.resolveTypeNameStr(i.element_name),
|
||||||
|
.many_pointer_type => |i| self.resolveTypeNameStr(i.element_name),
|
||||||
|
.pointer_type => |i| self.elementTypeOf(self.resolveTypeNameStr(i.pointee_name)),
|
||||||
|
.struct_type => |name| blk: {
|
||||||
|
const info = self.struct_types.get(name) orelse break :blk null;
|
||||||
|
for (info.field_names, info.field_types) |fname, fty| {
|
||||||
|
if (std.mem.eql(u8, fname, "items") and fty == .many_pointer_type) {
|
||||||
|
break :blk self.resolveTypeNameStr(fty.many_pointer_type.element_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :blk null;
|
||||||
|
},
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// The type name an instantiation arg node carries (`Move` in
|
/// The type name an instantiation arg node carries (`Move` in
|
||||||
/// `List(Move)`). Null for non-nameable args (e.g. value params like `3`).
|
/// `List(Move)`). Null for non-nameable args (e.g. value params like `3`).
|
||||||
fn argTypeName(node: *const Node) ?[]const u8 {
|
fn argTypeName(node: *const Node) ?[]const u8 {
|
||||||
@@ -1041,7 +1065,16 @@ pub const Analyzer = struct {
|
|||||||
try self.analyzeNode(fe.iterable);
|
try self.analyzeNode(fe.iterable);
|
||||||
try self.pushScope();
|
try self.pushScope();
|
||||||
if (!std.mem.eql(u8, fe.capture_name, "_")) {
|
if (!std.mem.eql(u8, fe.capture_name, "_")) {
|
||||||
try self.addSymbol(fe.capture_name, .variable, null, node.span);
|
var cap_ty: ?Type = null;
|
||||||
|
if (fe.range_end != null) {
|
||||||
|
cap_ty = .{ .signed = 64 };
|
||||||
|
} else if (self.elementTypeOf(self.inferExprType(fe.iterable))) |elem| {
|
||||||
|
cap_ty = if (fe.capture_by_ref)
|
||||||
|
(if (elem.toName()) |en| Type{ .pointer_type = .{ .pointee_name = en } } else elem)
|
||||||
|
else
|
||||||
|
elem;
|
||||||
|
}
|
||||||
|
try self.addSymbol(fe.capture_name, .variable, cap_ty, node.span);
|
||||||
}
|
}
|
||||||
if (fe.index_name) |idx_name| {
|
if (fe.index_name) |idx_name| {
|
||||||
if (!std.mem.eql(u8, idx_name, "_")) {
|
if (!std.mem.eql(u8, idx_name, "_")) {
|
||||||
@@ -2028,6 +2061,46 @@ test "sema: field access + index through a *Struct param" {
|
|||||||
try std.testing.expectEqualStrings("Cell", c_ty.?.struct_type);
|
try std.testing.expectEqualStrings("Cell", c_ty.?.struct_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "sema: for-loop captures resolve element, by-ref pointer, and range cursor" {
|
||||||
|
const parser_mod = @import("parser.zig");
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
const source =
|
||||||
|
"Move :: struct { flag: s64; }" ++
|
||||||
|
"List :: struct ($T: Type) { items: [*]T = null; len: s64 = 0; }" ++
|
||||||
|
"Game :: struct { legal: List(Move);" ++
|
||||||
|
" scan :: (self: *Game) {" ++
|
||||||
|
" for self.legal: (m) { a := m.flag; }" ++
|
||||||
|
" for self.legal: (*p) { b := p.flag; }" ++
|
||||||
|
" for 0..10: (i) { c := i; }" ++
|
||||||
|
" } }";
|
||||||
|
var parser = parser_mod.Parser.init(alloc, source);
|
||||||
|
const root = try parser.parse();
|
||||||
|
var an = Analyzer.init(alloc);
|
||||||
|
an.source = source;
|
||||||
|
const res = try an.analyze(root);
|
||||||
|
|
||||||
|
var m_ty: ?Type = null;
|
||||||
|
var p_ty: ?Type = null;
|
||||||
|
var i_ty: ?Type = null;
|
||||||
|
for (res.symbols) |sym| {
|
||||||
|
if (std.mem.eql(u8, sym.name, "m")) m_ty = sym.ty;
|
||||||
|
if (std.mem.eql(u8, sym.name, "p")) p_ty = sym.ty;
|
||||||
|
if (std.mem.eql(u8, sym.name, "i")) i_ty = sym.ty;
|
||||||
|
}
|
||||||
|
// by-value capture over List(Move) yields the element struct.
|
||||||
|
try std.testing.expect(m_ty != null and m_ty.? == .struct_type);
|
||||||
|
try std.testing.expectEqualStrings("Move", m_ty.?.struct_type);
|
||||||
|
// by-ref capture yields a pointer to the element.
|
||||||
|
try std.testing.expect(p_ty != null and p_ty.? == .pointer_type);
|
||||||
|
try std.testing.expectEqualStrings("Move", p_ty.?.pointer_type.pointee_name);
|
||||||
|
// range cursor is s64.
|
||||||
|
try std.testing.expect(i_ty != null and i_ty.? == .signed);
|
||||||
|
try std.testing.expect(i_ty.?.signed == 64);
|
||||||
|
}
|
||||||
|
|
||||||
test "sema: member references record fields, methods, and enum variants" {
|
test "sema: member references record fields, methods, and enum variants" {
|
||||||
const parser_mod = @import("parser.zig");
|
const parser_mod = @import("parser.zig");
|
||||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
|||||||
Reference in New Issue
Block a user