ufcs
This commit is contained in:
@@ -4087,6 +4087,28 @@ pub const CodeGen = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// UFCS: obj.method(args...) → method(obj, args...)
|
||||
if (call_node.callee.data == .field_access) {
|
||||
const fa = call_node.callee.data.field_access;
|
||||
const method_name = fa.field;
|
||||
// Check if a free function with this name exists
|
||||
const method_z = self.allocator.dupeZ(u8, method_name) catch method_name;
|
||||
if (self.generic_templates.contains(method_name) or
|
||||
c.LLVMGetNamedFunction(self.module, method_z.ptr) != null)
|
||||
{
|
||||
// Build new args: [obj, original_args...]
|
||||
var ufcs_args = try self.allocator.alloc(*Node, call_node.args.len + 1);
|
||||
ufcs_args[0] = fa.object;
|
||||
for (call_node.args, 0..) |arg, i| {
|
||||
ufcs_args[i + 1] = arg;
|
||||
}
|
||||
return self.genCallByName(method_name, .{
|
||||
.callee = call_node.callee,
|
||||
.args = ufcs_args,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve callee — must be an identifier
|
||||
if (call_node.callee.data != .identifier) return self.emitError("callee must be an identifier");
|
||||
const callee_name = call_node.callee.data.identifier.name;
|
||||
|
||||
@@ -163,6 +163,20 @@ pub const Server = struct {
|
||||
return;
|
||||
};
|
||||
|
||||
// Check if cursor is on a qualified name (e.g. "std.print" or UFCS "list.append")
|
||||
if (extractQualifiedName(analysis.source, offset)) |qn| {
|
||||
// Try namespace import first (e.g. std.print)
|
||||
if (try self.resolveImportedLocation(id_json, analysis, qn.ns, qn.member)) |_| return;
|
||||
|
||||
// Try UFCS: obj.method → find free function "method"
|
||||
if (findSymbolByName(analysis.sema.symbols, qn.member)) |si| {
|
||||
const sym = analysis.sema.symbols[si];
|
||||
if (sym.kind == .function) {
|
||||
if (try self.sendSymbolLocation(id_json, uri, analysis, sym)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if cursor is on a reference → jump to definition
|
||||
if (sx.sema.findReferenceAtOffset(analysis.sema.references, offset)) |ref_idx| {
|
||||
const ref = analysis.sema.references[ref_idx];
|
||||
@@ -178,11 +192,6 @@ pub const Server = struct {
|
||||
if (try self.sendSymbolLocation(id_json, uri, analysis, sym)) return;
|
||||
}
|
||||
|
||||
// Check if cursor is on a qualified name (e.g. "std.print")
|
||||
if (extractQualifiedName(analysis.source, offset)) |qn| {
|
||||
if (try self.resolveImportedLocation(id_json, analysis, qn.ns, qn.member)) |_| return;
|
||||
}
|
||||
|
||||
// Check if cursor is on an #import "path" string → open the file
|
||||
if (findImportPathAtOffset(analysis.source, offset)) |import_path| {
|
||||
const file_path = uriToFilePath(uri) orelse "";
|
||||
@@ -364,11 +373,14 @@ pub const Server = struct {
|
||||
const line = std.math.cast(u32, jsonInt(jsonGet(position, "line") orelse return) orelse return) orelse return;
|
||||
const character = std.math.cast(u32, jsonInt(jsonGet(position, "character") orelse return) orelse return) orelse return;
|
||||
|
||||
// Check if cursor is right after a dot — if so, do dot-completion
|
||||
// Check if cursor is after a dot (possibly with partial identifier typed)
|
||||
if (self.documents.get(uri)) |doc| {
|
||||
if (positionToOffset(doc.text, line, character)) |off| {
|
||||
if (off > 0 and doc.text[off - 1] == '.') {
|
||||
try self.handleDotCompletion(id_json, uri, doc.text, off);
|
||||
// Scan backwards past any identifier characters to find a dot
|
||||
var scan = off;
|
||||
while (scan > 0 and isIdentChar(doc.text[scan - 1])) scan -= 1;
|
||||
if (scan > 0 and doc.text[scan - 1] == '.') {
|
||||
try self.handleDotCompletion(id_json, uri, doc.text, scan);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -448,13 +460,21 @@ pub const Server = struct {
|
||||
if (self.sema_cache.get(uri)) |analysis| {
|
||||
// Check if prefix is a namespace — offer its inner declarations
|
||||
if (!try self.collectNamespaceCompletions(&items, analysis, prefix)) {
|
||||
// Otherwise look up prefix as a struct/enum type name in sema symbols
|
||||
// Try as type name directly (e.g. Vec2., Color.)
|
||||
try self.collectMemberCompletions(&items, analysis, prefix);
|
||||
|
||||
// Try as variable name — resolve to type and offer fields + UFCS methods
|
||||
if (items.items.len == 0) {
|
||||
if (resolveVariableType(analysis, prefix)) |type_name| {
|
||||
try self.collectMemberCompletions(&items, analysis, type_name);
|
||||
try self.collectUfcsCompletions(&items, analysis, type_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const items_json = try lsp.completionItemsJson(self.allocator, items.items);
|
||||
const items_json = try lsp.completionListJson(self.allocator, items.items);
|
||||
const resp = try lsp.jsonRpcResponse(self.allocator, id_json, items_json);
|
||||
try self.transport.writeMessage(resp);
|
||||
}
|
||||
@@ -1603,6 +1623,109 @@ pub const Server = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Resolve a variable/param name to its struct type name via sema type info.
|
||||
fn resolveVariableType(analysis: DocumentAnalysis, var_name: []const u8) ?[]const u8 {
|
||||
var i = analysis.sema.symbols.len;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
const sym = analysis.sema.symbols[i];
|
||||
if (!std.mem.eql(u8, sym.name, var_name)) continue;
|
||||
if (sym.kind != .variable and sym.kind != .param) continue;
|
||||
const ty = sym.ty orelse return null;
|
||||
return switch (ty) {
|
||||
.struct_type => |name| name,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Extract the base type name from a parameter's type expression.
|
||||
/// Unwraps pointer wrappers and parameterized types to get the core name.
|
||||
/// e.g. *List($T) → "List", Vec2 → "Vec2", *Vec2 → "Vec2"
|
||||
fn extractBaseTypeName(type_node: *sx.ast.Node) ?[]const u8 {
|
||||
return switch (type_node.data) {
|
||||
.type_expr => |te| te.name,
|
||||
.pointer_type_expr => |pte| extractBaseTypeName(pte.pointee_type),
|
||||
.parameterized_type_expr => |pte| pte.name,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Collect UFCS-compatible functions: free functions whose first parameter
|
||||
/// type matches the given struct type name (directly or through a pointer).
|
||||
fn collectUfcsCompletions(self: *Server, items: *std.ArrayList(lsp.CompletionItem), analysis: DocumentAnalysis, type_name: []const u8) !void {
|
||||
if (analysis.root.data != .root) return;
|
||||
|
||||
for (analysis.root.data.root.decls) |decl| {
|
||||
try self.collectUfcsFromDecl(items, decl, type_name);
|
||||
}
|
||||
}
|
||||
|
||||
fn collectUfcsFromDecl(self: *Server, items: *std.ArrayList(lsp.CompletionItem), decl: *sx.ast.Node, type_name: []const u8) !void {
|
||||
switch (decl.data) {
|
||||
.fn_decl => |fd| {
|
||||
if (fd.params.len > 0) {
|
||||
const base = extractBaseTypeName(fd.params[0].type_expr) orelse return;
|
||||
if (!std.mem.eql(u8, base, type_name)) return;
|
||||
|
||||
const detail = try self.formatUfcsDetail(fd.params[1..], fd.return_type);
|
||||
try items.append(self.allocator, .{
|
||||
.label = fd.name,
|
||||
.kind = @intFromEnum(lsp.CompletionItemKind.Method),
|
||||
.detail = detail,
|
||||
});
|
||||
}
|
||||
},
|
||||
.const_decl => |cd| {
|
||||
if (cd.value.data == .lambda) {
|
||||
const lam = cd.value.data.lambda;
|
||||
if (lam.params.len > 0) {
|
||||
const base = extractBaseTypeName(lam.params[0].type_expr) orelse return;
|
||||
if (!std.mem.eql(u8, base, type_name)) return;
|
||||
|
||||
const detail = try self.formatUfcsDetail(lam.params[1..], lam.return_type);
|
||||
try items.append(self.allocator, .{
|
||||
.label = cd.name,
|
||||
.kind = @intFromEnum(lsp.CompletionItemKind.Method),
|
||||
.detail = detail,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
.namespace_decl => |ns| {
|
||||
for (ns.decls) |inner| {
|
||||
try self.collectUfcsFromDecl(items, inner, type_name);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Format detail string for a UFCS method (params excluding the first/self param).
|
||||
fn formatUfcsDetail(self: *Server, params: []const sx.ast.Param, return_type: ?*sx.ast.Node) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).empty;
|
||||
try buf.append(self.allocator, '(');
|
||||
for (params, 0..) |param, pi| {
|
||||
if (pi > 0) try buf.appendSlice(self.allocator, ", ");
|
||||
try buf.appendSlice(self.allocator, param.name);
|
||||
try buf.appendSlice(self.allocator, ": ");
|
||||
if (param.type_expr.data == .type_expr) {
|
||||
try buf.appendSlice(self.allocator, param.type_expr.data.type_expr.name);
|
||||
} else {
|
||||
try buf.appendSlice(self.allocator, "?");
|
||||
}
|
||||
}
|
||||
try buf.append(self.allocator, ')');
|
||||
if (return_type) |rt| {
|
||||
try buf.appendSlice(self.allocator, " -> ");
|
||||
if (rt.data == .type_expr) {
|
||||
try buf.appendSlice(self.allocator, rt.data.type_expr.name);
|
||||
}
|
||||
}
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
/// Try to resolve a variable/param name to its struct type name.
|
||||
fn resolveStructTypeName(analysis: DocumentAnalysis, var_name: []const u8) ?[]const u8 {
|
||||
var i = analysis.sema.symbols.len;
|
||||
|
||||
@@ -204,7 +204,18 @@ pub const DocumentSymbol = struct {
|
||||
|
||||
/// Build completion items JSON array.
|
||||
pub fn completionItemsJson(allocator: std.mem.Allocator, items: []const CompletionItem) ![]const u8 {
|
||||
return completionItemsJsonInner(allocator, items, false);
|
||||
}
|
||||
|
||||
/// Build a CompletionList object with isIncomplete: false, preventing the client
|
||||
/// from supplementing results with its own word-based suggestions.
|
||||
pub fn completionListJson(allocator: std.mem.Allocator, items: []const CompletionItem) ![]const u8 {
|
||||
return completionItemsJsonInner(allocator, items, true);
|
||||
}
|
||||
|
||||
fn completionItemsJsonInner(allocator: std.mem.Allocator, items: []const CompletionItem, as_list: bool) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).empty;
|
||||
if (as_list) try buf.appendSlice(allocator, "{\"isIncomplete\":false,\"items\":");
|
||||
try buf.append(allocator, '[');
|
||||
for (items, 0..) |item, idx| {
|
||||
if (idx > 0) try buf.append(allocator, ',');
|
||||
@@ -225,6 +236,7 @@ pub fn completionItemsJson(allocator: std.mem.Allocator, items: []const Completi
|
||||
}
|
||||
}
|
||||
try buf.append(allocator, ']');
|
||||
if (as_list) try buf.append(allocator, '}');
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
|
||||
73
src/sema.zig
73
src/sema.zig
@@ -385,6 +385,12 @@ pub const Analyzer = struct {
|
||||
if (self.struct_types.contains(target)) return .{ .struct_type = target };
|
||||
}
|
||||
} else if (sl.type_expr) |te| {
|
||||
// Handle parameterized struct: List(s32).{} parses as call node
|
||||
if (te.data == .call) {
|
||||
if (self.resolveCalleeName(te.data.call)) |callee| {
|
||||
if (self.struct_types.contains(callee)) return .{ .struct_type = callee };
|
||||
}
|
||||
}
|
||||
return self.inferExprType(te);
|
||||
}
|
||||
return .void_type;
|
||||
@@ -397,6 +403,10 @@ pub const Analyzer = struct {
|
||||
.null_literal => .void_type,
|
||||
.array_literal => .void_type,
|
||||
.type_expr => |te| .{ .meta_type = .{ .name = te.name } },
|
||||
.parameterized_type_expr => |pte| {
|
||||
if (self.struct_types.contains(pte.name)) return .{ .struct_type = pte.name };
|
||||
return .void_type;
|
||||
},
|
||||
else => .void_type,
|
||||
};
|
||||
}
|
||||
@@ -554,7 +564,8 @@ pub const Analyzer = struct {
|
||||
if (vd.value) |val| {
|
||||
try self.analyzeNode(val);
|
||||
}
|
||||
const ty = resolveTypeAnnotation(vd.type_annotation);
|
||||
const ty = resolveTypeAnnotation(vd.type_annotation) orelse
|
||||
if (vd.value) |val| self.inferExprType(val) else null;
|
||||
try self.addSymbol(vd.name, .variable, ty, node.span);
|
||||
},
|
||||
.enum_decl => |ed| {
|
||||
@@ -1057,3 +1068,63 @@ test "sema: enum and struct declarations" {
|
||||
try std.testing.expectEqual(SymbolKind.struct_type, result.symbols[1].kind);
|
||||
try std.testing.expectEqualStrings("main", result.symbols[2].name);
|
||||
}
|
||||
|
||||
test "sema: var_decl infers struct type from parameterized struct literal" {
|
||||
const parser_mod = @import("parser.zig");
|
||||
|
||||
const source = "List :: struct { len: s64; } main :: () { list := List.{}; }";
|
||||
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);
|
||||
|
||||
// Find the 'list' variable symbol
|
||||
var found_list = false;
|
||||
for (result.symbols) |sym| {
|
||||
if (std.mem.eql(u8, sym.name, "list")) {
|
||||
found_list = true;
|
||||
try std.testing.expectEqual(SymbolKind.variable, sym.kind);
|
||||
// Must have inferred struct type
|
||||
const ty = sym.ty orelse return error.TestUnexpectedResult;
|
||||
try std.testing.expect(ty == .struct_type);
|
||||
try std.testing.expectEqualStrings("List", ty.struct_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
try std.testing.expect(found_list);
|
||||
}
|
||||
|
||||
test "sema: var_decl infers struct type from parameterized call literal" {
|
||||
const parser_mod = @import("parser.zig");
|
||||
|
||||
// List(s32).{} — parser produces struct_literal with type_expr = call node
|
||||
const source = "List :: struct { len: s64; } main :: () { list := List(s32).{}; }";
|
||||
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);
|
||||
|
||||
// Find the 'list' variable symbol
|
||||
var found_list = false;
|
||||
for (result.symbols) |sym| {
|
||||
if (std.mem.eql(u8, sym.name, "list")) {
|
||||
found_list = true;
|
||||
try std.testing.expectEqual(SymbolKind.variable, sym.kind);
|
||||
const ty = sym.ty orelse return error.TestUnexpectedResult;
|
||||
try std.testing.expect(ty == .struct_type);
|
||||
try std.testing.expectEqualStrings("List", ty.struct_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
try std.testing.expect(found_list);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user