diff --git a/src/lsp/document.zig b/src/lsp/document.zig index 9f23037..25ed7b1 100644 --- a/src/lsp/document.zig +++ b/src/lsp/document.zig @@ -339,6 +339,7 @@ pub const DocumentStore = struct { } // Run sema on this file's own AST + analyzer.source = doc.source; doc.sema = analyzer.analyze(root) catch null; if (doc.sema != null) { doc.last_good_sema = doc.sema; diff --git a/src/lsp/server.zig b/src/lsp/server.zig index c399714..190d769 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -203,6 +203,30 @@ pub const Server = struct { } } + // A struct field / method / enum variant under the cursor — matched by + // (owner type, name) across loaded documents. + for (sema.member_refs) |mt| { + if (offset < mt.span.start or offset >= mt.span.end) continue; + var buf = std.ArrayList(u8).empty; + defer buf.deinit(self.allocator); + try buf.append(self.allocator, '['); + var first = true; + var dit = self.documents.by_path.iterator(); + while (dit.next()) |entry| { + const odoc = entry.value_ptr.*; + const osema = odoc.sema orelse continue; + for (osema.member_refs) |mr| { + if (!std.mem.eql(u8, mr.name, mt.name)) continue; + // Owner must match, treating an unknown owner ("") as a wildcard. + if (mr.owner.len != 0 and mt.owner.len != 0 and !std.mem.eql(u8, mr.owner, mt.owner)) continue; + if (mr.is_def and !include_decl) continue; + try self.appendRefLoc(&buf, &first, odoc, mr.span); + } + } + try buf.append(self.allocator, ']'); + return try self.sendResponse(id_json, buf.items); + } + // Resolve the target symbol: a reference at the cursor, or a definition. var target_idx: ?u32 = null; if (sx.sema.findReferenceAtOffset(sema.references, offset)) |ri| { diff --git a/src/sema.zig b/src/sema.zig index ecc967d..b099155 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -37,6 +37,15 @@ pub const Reference = struct { symbol_index: u32, }; +/// A reference to a struct field / method / enum variant. These aren't symbols, +/// so they're tracked separately and matched by (owner type, name). +pub const MemberRef = struct { + span: Span, // the member-name token + name: []const u8, + owner: []const u8 = "", // owning type name, "" if unknown (bare enum literal) + is_def: bool = false, // declaration site rather than a use +}; + pub const FnSignature = struct { param_types: []const Type, return_type: Type, @@ -54,6 +63,7 @@ pub const TypeMap = std.AutoHashMap(*const Node, Type); pub const SemaResult = struct { symbols: []const Symbol, references: []const Reference, + member_refs: []const MemberRef, diagnostics: []const Diagnostic, fn_signatures: std.StringHashMap(FnSignature), struct_types: std.StringHashMap(StructTypeInfo), @@ -66,6 +76,9 @@ pub const Analyzer = struct { allocator: std.mem.Allocator, symbols: std.ArrayList(Symbol), references: std.ArrayList(Reference), + member_refs: std.ArrayList(MemberRef), + /// Source text — lets `spanOf` map an AST string slice back to a Span. + source: []const u8 = "", diagnostics: std.ArrayList(Diagnostic), scope_depth: u32, /// Stack of symbol counts at each scope entry, for popScope cleanup. @@ -84,6 +97,7 @@ pub const Analyzer = struct { .allocator = allocator, .symbols = std.ArrayList(Symbol).empty, .references = std.ArrayList(Reference).empty, + .member_refs = std.ArrayList(MemberRef).empty, .diagnostics = std.ArrayList(Diagnostic).empty, .scope_depth = 0, .scope_starts = std.ArrayList(u32).empty, @@ -112,6 +126,7 @@ pub const Analyzer = struct { return .{ .symbols = try self.symbols.toOwnedSlice(self.allocator), .references = try self.references.toOwnedSlice(self.allocator), + .member_refs = try self.member_refs.toOwnedSlice(self.allocator), .diagnostics = try self.diagnostics.toOwnedSlice(self.allocator), .fn_signatures = self.fn_signatures, .struct_types = self.struct_types, @@ -207,6 +222,7 @@ pub const Analyzer = struct { try self.addSymbol(ed.name, .enum_type, .{ .enum_type = ed.name }, node.span); try self.enum_types.put(ed.name, ed.variant_names); } + for (ed.variant_names) |vn| self.recordMemberRef(vn, ed.name, true); }, .struct_decl => |sd| { try self.addSymbol(sd.name, .struct_type, .{ .struct_type = sd.name }, node.span); @@ -256,9 +272,12 @@ pub const Analyzer = struct { .type_params = tp_slice, }); } + for (sd.field_names) |fname| self.recordMemberRef(fname, sd.name, true); for (sd.methods) |mnode| { - if (mnode.data == .fn_decl) + if (mnode.data == .fn_decl) { try self.registerMethodSig(mnode.data.fn_decl.name, ns_prefix, mnode.data.fn_decl.return_type); + self.recordMemberRef(mnode.data.fn_decl.name, sd.name, true); + } } }, .protocol_decl => |pd| { @@ -799,6 +818,23 @@ pub const Analyzer = struct { try gop.value_ptr.append(self.allocator, idx); } + /// Span of an AST string slice that points into `source` (field/variant/ + /// method names are such slices). Returns {0,0} for synthetic strings. + fn spanOf(self: *Analyzer, s: []const u8) Span { + if (self.source.len == 0 or s.len == 0) return .{ .start = 0, .end = 0 }; + const base = @intFromPtr(self.source.ptr); + const p = @intFromPtr(s.ptr); + if (p < base or p + s.len > base + self.source.len) return .{ .start = 0, .end = 0 }; + const start: u32 = @intCast(p - base); + return .{ .start = start, .end = start + @as(u32, @intCast(s.len)) }; + } + + fn recordMemberRef(self: *Analyzer, name: []const u8, owner: []const u8, is_def: bool) void { + const span = self.spanOf(name); + if (span.end == 0) return; // not locatable in source + self.member_refs.append(self.allocator, .{ .span = span, .name = name, .owner = owner, .is_def = is_def }) catch {}; + } + fn resolveIdentifier(self: *Analyzer, name: []const u8, span: Span) !void { // Use symbol index for O(1) name lookup, then walk backwards through indices if (self.symbol_index.get(name)) |indices| { @@ -914,6 +950,12 @@ pub const Analyzer = struct { }, .field_access => |fa| { try self.analyzeNode(fa.object); + var owner_ty = self.inferExprType(fa.object); + if (owner_ty.isPointer()) owner_ty = self.resolveTypeNameStr(owner_ty.pointer_type.pointee_name); + self.recordMemberRef(fa.field, owner_ty.toName() orelse "", false); + }, + .enum_literal => |el| { + self.recordMemberRef(el.name, "", false); }, .if_expr => |ie| { try self.analyzeNode(ie.condition); @@ -937,7 +979,13 @@ pub const Analyzer = struct { }, .match_expr => |me| { try self.analyzeNode(me.subject); + var subj_ty = self.inferExprType(me.subject); + if (subj_ty.isPointer()) subj_ty = self.resolveTypeNameStr(subj_ty.pointer_type.pointee_name); + const subj_owner = subj_ty.toName() orelse ""; for (me.arms) |arm| { + if (arm.pattern) |pat| { + if (pat.data == .enum_literal) self.recordMemberRef(pat.data.enum_literal.name, subj_owner, false); + } try self.pushScope(); if (arm.capture) |cap_name| { try self.addSymbol(cap_name, .variable, null, arm.body.span); @@ -1023,7 +1071,6 @@ pub const Analyzer = struct { try self.addSymbol(ud.name, .enum_type, .{ .union_type = ud.name }, node.span); }, // Leaf nodes — nothing to recurse into - .enum_literal, .int_literal, .float_literal, .bool_literal, @@ -1954,6 +2001,36 @@ test "sema: field access + index through a *Struct param" { try std.testing.expectEqualStrings("Cell", c_ty.?.struct_type); } +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); + defer arena.deinit(); + const alloc = arena.allocator(); + + const source = + "Color :: enum { red; green; }" ++ + "P :: struct { x: s64; m :: (self: P) -> s64 { self.x; } }" ++ + "use :: (p: *P) { a := p.x; b := p.m(); c := Color.red; }"; + 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 x_use = false; + var m_use = false; + var red_use = false; + for (res.member_refs) |mr| { + if (mr.is_def) continue; + if (std.mem.eql(u8, mr.name, "x") and std.mem.eql(u8, mr.owner, "P")) x_use = true; + if (std.mem.eql(u8, mr.name, "m") and std.mem.eql(u8, mr.owner, "P")) m_use = true; + if (std.mem.eql(u8, mr.name, "red") and std.mem.eql(u8, mr.owner, "Color")) red_use = true; + } + try std.testing.expect(x_use); + try std.testing.expect(m_use); + try std.testing.expect(red_use); +} + test "sema: variable shadowing in same scope is allowed" { const parser_mod = @import("parser.zig");