diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 190d769..036f740 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1241,8 +1241,14 @@ pub const Server = struct { collectInlayHints(allocator, we.body, symbols, fn_signatures, source, hints); }, .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); }, + .struct_decl => |sd| { + for (sd.methods) |m| collectInlayHints(allocator, m, symbols, fn_signatures, source, hints); + }, .var_decl => |vd| { // Only show hint when type is inferred (:= syntax) if (vd.type_annotation != null) return; @@ -1265,6 +1271,14 @@ pub const Server = struct { } 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 switch (cd.value.data) { .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( allocator: std.mem.Allocator, span: sx.ast.Span, diff --git a/src/sema.zig b/src/sema.zig index c8141c1..02ba8ab 100644 --- a/src/sema.zig +++ b/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) } }, .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) } }, + .parameterized_type_expr => |pte| self.instantiateGeneric(pte.name, pte.args) orelse 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 /// `List(Move)`). Null for non-nameable args (e.g. value params like `3`). fn argTypeName(node: *const Node) ?[]const u8 { @@ -1041,7 +1065,16 @@ pub const Analyzer = struct { try self.analyzeNode(fe.iterable); try self.pushScope(); 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 (!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); } +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" { const parser_mod = @import("parser.zig"); var arena = std.heap.ArenaAllocator.init(std.testing.allocator);