diff --git a/editors/vscode/package.json b/editors/vscode/package.json index c80051f..6b71013 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -42,6 +42,9 @@ } }, "configurationDefaults": { + "[sx]": { + "editor.wordBasedSuggestions": "off" + }, "editor.tokenColorCustomizations": { "textMateRules": [ { diff --git a/examples/24-list.sx b/examples/24-list.sx index 2c146b5..4dad4ac 100644 --- a/examples/24-list.sx +++ b/examples/24-list.sx @@ -25,8 +25,11 @@ main :: () { list := List(s32).{}; append(list, 3); append(list, 1); - append(list, 4); - append(list, 1); - append(list, 5); + + list.append(3); + list.append(4); + list.append(1); + list.append(5); + print("{}\n", list); } \ No newline at end of file diff --git a/examples/modules/std.sx b/examples/modules/std.sx index a0c8f55..f5c757e 100644 --- a/examples/modules/std.sx +++ b/examples/modules/std.sx @@ -3,6 +3,9 @@ write :: (str: string) -> void #builtin; sqrt :: (x: $T) -> T #builtin; size_of :: ($T: Type) -> s64 #builtin; alloc :: (size: s64) -> string #builtin; +malloc :: (size: s64) -> *void #builtin; +memcpy :: (dst: *void, src: *void, size: s64) -> *void #builtin; +free :: (ptr: *void) -> void #builtin; type_of :: (val: $T) -> Type #builtin; type_name :: ($T: Type) -> string #builtin; field_count :: ($T: Type) -> s64 #builtin; diff --git a/src/codegen.zig b/src/codegen.zig index 69d188b..17a3083 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -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; diff --git a/src/lsp/server.zig b/src/lsp/server.zig index f476ed8..86b9194 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -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; diff --git a/src/lsp/types.zig b/src/lsp/types.zig index 74e2f9a..2f1cdbb 100644 --- a/src/lsp/types.zig +++ b/src/lsp/types.zig @@ -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; } diff --git a/src/sema.zig b/src/sema.zig index 44c2138..d6452ae 100644 --- a/src/sema.zig +++ b/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); +}