Skip to content

Generics overhaul & improvements #2324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 47 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
28844a5
Overhaul analysis code for generics
FnControlOption May 16, 2025
3471b8f
Determine whether a container is generic at type resolution
FnControlOption May 17, 2025
6f4085b
Small cleanup
FnControlOption May 17, 2025
8e55d1f
Avoid infinite recursion
FnControlOption May 17, 2025
2bc26fd
Fix completions and semantic tokens for generic methods
FnControlOption May 17, 2025
f47667e
Add a few more tests
FnControlOption May 17, 2025
8061b80
Resolve bound type parameters in decl literals
FnControlOption May 17, 2025
4820d2e
Add some more tests
FnControlOption May 17, 2025
034d387
Resolve bound type parameters in generic union declarations
FnControlOption May 17, 2025
80b24ba
.
FnControlOption May 17, 2025
ff7471f
Call resolveGenericType inside resolveTypeWithContainer
FnControlOption May 17, 2025
b541bbc
Improve completions for function taking a generic struct arg
FnControlOption May 17, 2025
ab65325
Improve completions of decl literal for generic type
FnControlOption May 17, 2025
942dae9
Improve completions of decl literal in generic decl
FnControlOption May 17, 2025
cdbb9c0
Resolve bound type params of recursive generic
FnControlOption May 17, 2025
8034781
Replace `from` field with `container_type` in `DeclWithHandle`
FnControlOption May 18, 2025
ff9c458
Fix resolved type for decl literal on alias in generic type
FnControlOption May 18, 2025
46b244e
.
FnControlOption May 18, 2025
b54e0c6
Avoid copying more type parameters than necessary
FnControlOption May 18, 2025
c79004f
.
FnControlOption May 18, 2025
89e0b12
Revert hack that is no longer needed
FnControlOption May 18, 2025
4994eef
Cleanup
FnControlOption May 18, 2025
9568992
.
FnControlOption May 18, 2025
09bf7e0
Fix infinite recursiion
FnControlOption May 18, 2025
37a42d4
Merge branch 'master' into wip-generics
FnControlOption May 18, 2025
3010924
.
FnControlOption May 18, 2025
e87047a
Don't return null from resolveGenericType
FnControlOption May 18, 2025
48e59c5
.
FnControlOption May 18, 2025
ca5da86
Merge branch 'master' into wip-generics
FnControlOption May 18, 2025
4068497
Todo: Fix infinite recursion with pointer to nested generic anytype
FnControlOption May 18, 2025
a14b69e
Fix infinite indirect recursion with pointer to generic anytype
FnControlOption May 18, 2025
78468dd
.
FnControlOption May 18, 2025
f9501fb
.
FnControlOption May 19, 2025
3d46090
Use a hash map for preventing infinite recursion
FnControlOption May 19, 2025
6335ca5
Cleanup
FnControlOption May 19, 2025
7436067
.
FnControlOption May 19, 2025
f9b21ce
Add helper functions to TokenWithHandle
FnControlOption May 19, 2025
f374869
.
FnControlOption May 20, 2025
8152b81
Merge branch 'master' into wip-generics
FnControlOption May 23, 2025
5fb5a8b
Apply requested changes from review
FnControlOption May 24, 2025
7dbbfd8
Rename .generic to .type_parameter
FnControlOption May 24, 2025
eb12d6a
Rename `Function.return_type` to `return_value`
FnControlOption May 24, 2025
fa4c57a
Fix eql for array type (bug introduced in 0a28fce)
FnControlOption May 24, 2025
a4b183f
Merge branch 'master' into wip-generics
FnControlOption May 25, 2025
3781645
Update todo's
FnControlOption May 25, 2025
2a21b45
resolveReturnTypeOfFuncNode -> resolveValueTypeOfFuncNode
FnControlOption May 25, 2025
b8ac160
resolveValueTypeOfFuncNode -> resolveReturnValueOfFuncNode
FnControlOption May 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/DocumentScope.zig
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ pub const Scope = struct {
};

pub const OptionalIndex = enum(u32) {
root,
none = std.math.maxInt(u32),
_,

Expand Down
1,231 changes: 680 additions & 551 deletions src/analysis.zig

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions src/ast.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1572,11 +1572,46 @@ pub fn nodesOverlappingIndex(allocator: std.mem.Allocator, tree: Ast, index: usi
};

var context: Context = .{ .index = index, .allocator = allocator };
defer context.nodes.deinit(allocator);
try iterateChildren(tree, .root, &context, error{OutOfMemory}, Context.append);
try context.nodes.append(allocator, .root);
return try context.nodes.toOwnedSlice(allocator);
}

/// returns a list of nodes that overlap with the given source code index.
/// the list may include nodes that were discarded during error recovery in the Zig parser.
/// sorted from smallest to largest.
/// caller owns the returned memory.
/// this function can be removed when the parser has been improved.
pub fn nodesOverlappingIndexIncludingParseErrors(allocator: std.mem.Allocator, tree: Ast, source_index: usize) error{OutOfMemory}![]Ast.Node.Index {
const NodeLoc = struct {
node: Ast.Node.Index,
loc: offsets.Loc,

fn lessThan(_: void, lhs: @This(), rhs: @This()) bool {
return rhs.loc.start < lhs.loc.start and lhs.loc.end < rhs.loc.end;
}
};

var node_locs: std.ArrayListUnmanaged(NodeLoc) = .empty;
defer node_locs.deinit(allocator);
for (0..tree.nodes.len) |i| {
const node: Ast.Node.Index = @enumFromInt(i);
const loc = offsets.nodeToLoc(tree, node);
if (loc.start <= source_index and source_index <= loc.end) {
try node_locs.append(allocator, .{ .node = node, .loc = loc });
}
}

std.mem.sort(NodeLoc, node_locs.items, {}, NodeLoc.lessThan);

const nodes = try allocator.alloc(Ast.Node.Index, node_locs.items.len);
for (node_locs.items, nodes) |node_loc, *node| {
node.* = node_loc.node;
}
return nodes;
}

/// returns a list of nodes that together encloses the given source code range
/// caller owns the returned memory
pub fn nodesAtLoc(allocator: std.mem.Allocator, tree: Ast, loc: offsets.Loc) error{OutOfMemory}![]Ast.Node.Index {
Expand Down
163 changes: 93 additions & 70 deletions src/features/completions.zig

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/features/hover.zig
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ fn hoverSymbolRecursive(

const def_str = switch (decl_handle.decl) {
.ast_node => |node| def: {
if (try analyser.resolveVarDeclAlias(.of(node, handle))) |result| {
if (try analyser.resolveVarDeclAlias(.{
.node_handle = .of(node, handle),
.container_type = decl_handle.container_type,
})) |result| {
return try hoverSymbolRecursive(analyser, arena, result, markup_kind, doc_strings);
}

Expand Down
18 changes: 12 additions & 6 deletions src/features/semantic_tokens.zig
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ fn colorIdentifierBasedOnType(
new_tok_mod.generic = true;
}

const has_self_param = Analyser.hasSelfParam(type_node);
const has_self_param = builder.analyser.hasSelfParam(type_node);

try writeTokenMod(builder, target_tok, if (has_self_param) .method else .function, new_tok_mod);
} else {
Expand Down Expand Up @@ -419,8 +419,13 @@ fn writeNodeTokens(builder: *Builder, node: Ast.Node.Index) error{OutOfMemory}!v
is_generic = func_ty.isGenericFunc();
if (func_ty.isTypeFunc()) {
func_name_tok_type = .type;
} else if (Analyser.hasSelfParam(func_ty)) {
func_name_tok_type = .method;
} else {
const container_ty = try builder.analyser.innermostContainer(handle, tree.tokenStart(fn_proto.ast.fn_token));
if (container_ty.data.container.scope_handle.scope != .root and
Analyser.firstParamIs(func_ty, container_ty))
{
func_name_tok_type = .method;
}
}
}

Expand Down Expand Up @@ -620,7 +625,8 @@ fn writeNodeTokens(builder: *Builder, node: Ast.Node.Index) error{OutOfMemory}!v

if (try builder.analyser.resolveTypeOfNode(.of(type_expr, handle))) |struct_type| {
switch (struct_type.data) {
.container => |scope_handle| {
.container => |info| {
const scope_handle = info.scope_handle;
field_token_type = fieldTokenType(scope_handle.toNode(), scope_handle.handle, false);
},
else => {},
Expand Down Expand Up @@ -1131,7 +1137,7 @@ fn writeFieldAccess(builder: *Builder, node: Ast.Node.Index) error{OutOfMemory}!

try writeNodeTokens(builder, lhs_node);

const lhs = try builder.analyser.resolveTypeOfNode(.{ .node = lhs_node, .handle = handle }) orelse {
const lhs = try builder.analyser.resolveTypeOfNode(.of(lhs_node, handle)) orelse {
try writeToken(builder, field_name_token, .variable);
return;
};
Expand All @@ -1148,7 +1154,7 @@ fn writeFieldAccess(builder: *Builder, node: Ast.Node.Index) error{OutOfMemory}!
const decl_node = decl_type.decl.ast_node;
if (!decl_type.handle.tree.nodeTag(decl_node).isContainerField()) break :field_blk;
if (lhs_type.data != .container) break :field_blk;
const scope_handle = lhs_type.data.container;
const scope_handle = lhs_type.data.container.scope_handle;
const tt = fieldTokenType(
scope_handle.toNode(),
scope_handle.handle,
Expand Down
2 changes: 1 addition & 1 deletion src/features/signature_help.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn fnProtoToSignatureInfo(
})});

const arg_idx = if (skip_self_param) blk: {
const has_self_param = Analyser.hasSelfParam(func_type);
const has_self_param = analyser.hasSelfParam(func_type);
break :blk commas + @intFromBool(has_self_param);
} else commas;

Expand Down
228 changes: 228 additions & 0 deletions tests/analysis/generics.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
fn Foo(T: type) type {
return struct {
fn bar(U: type, t: ?T, u: ?U) void {
_ = .{ t, u };
}

fn baz(U: type, t: T, u: U) T {
return t + u;
}

fn qux(U: type, t: T, u: U) @TypeOf(t, u) {
return t + u;
}
};
}

const foo = Foo(u8){};
// ^^^ (Foo(u8))()

// TODO this should be `fn (U: type, ?u8, ?U) void`
const bar_fn = Foo(u8).bar;
// ^^^^^^ (fn (type, ?u8, ?U) void)()

const bar_call = Foo(u8).bar(i32, null, null);
// ^^^^^^^^ (void)()

// TODO this should be `fn (U: type, i32, U) i32`
const baz_fn = Foo(i32).baz;
// ^^^^^^ (fn (type, i32, U) i32)()

const baz_call = Foo(i32).baz(u8, -42, 42);
// ^^^^^^^^ (i32)()

// TODO this should be `fn (U: type, u8, U) anytype`
const qux_fn = Foo(u8).qux;
// ^^^^^^ (fn (type, u8, U) u8)()

// TODO this should be `i32`
const qux_call = Foo(u8).qux(i32, 42, -42);
// ^^^^^^^^ (u8)()

fn fizz(T: type) ?fn () error{}!struct { ??T } {
return null;
}

// TODO this should be `fn (T: type) ?fn () error{}!struct { ??T })()`
const fizz_fn = fizz;
// ^^^^^^^ (fn (type) ?fn () error{}!struct { ??T })()

const fizz_call = fizz(u8);
// ^^^^^^^^^ (?fn () error{}!struct { ??u8 })()

comptime {
// Use @compileLog to verify the expected type with the compiler:
// @compileLog(foo);
}

fn Point1(comptime T: type) type {
return struct {
x: T,
y: T,
fn normSquared(self: Point1(T)) T {
_ = self;
// ^^^^ (Point1(T))()
}
};
}

fn parameter(comptime T: type, in: T) void {
_ = in;
// ^^ (T)()
}

fn taggedUnion(comptime T: type, in: union(enum) { a: T, b: T }) void {
switch (in) {
.a => |a| {
_ = a;
// ^ (T)()
},
.b => |b| {
_ = b;
// ^ (T)()
},
}
}

fn Option(comptime T: type) type {
return struct {
item: ?T,
const none: @This() = undefined;
const alias = none;
const default = init();
fn init() @This() {}
};
}

const option_none: Option(u8) = .none;
// ^^^^^ (Option(u8))()

const option_alias: Option(u8) = .alias;
// ^^^^^^ (Option(u8))()

const option_default: Option(u8) = .default;
// ^^^^^^^^ (Option(u8))()

const option_init: Option(u8) = .init();
// ^^^^^ (fn () Option(u8))()

fn GenericUnion(T: type) type {
return union {
field: T,
const decl: T = undefined;
};
}

const generic_union_decl = GenericUnion(u8).decl;
// ^^^^^^^^^^^^^^^^^^ (u8)()

const generic_union: GenericUnion(u8) = .{ .field = 1 };
// ^^^^^^^^^^^^^ (GenericUnion(u8))()

const generic_union_field = generic_union.field;
// ^^^^^^^^^^^^^^^^^^^ (u8)()

const generic_union_tag = GenericUnion(u8).field;
// ^^^^^^^^^^^^^^^^^ (unknown)()

fn GenericTaggedUnion(T: type) type {
return union(enum) {
field: T,
const decl: T = undefined;
};
}

const generic_tagged_union_decl = GenericTaggedUnion(u8).decl;
// ^^^^^^^^^^^^^^^^^^^^^^^^^ (u8)()

const generic_tagged_union: GenericTaggedUnion(u8) = .{ .field = 1 };
// ^^^^^^^^^^^^^^^^^^^^ (GenericTaggedUnion(u8))()

const generic_tagged_union_field = generic_tagged_union.field;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ (u8)()

const generic_tagged_union_tag = GenericTaggedUnion(u8).field;
// ^^^^^^^^^^^^^^^^^^^^^^^^ (@typeInfo(GenericTaggedUnion(u8)).@"union".tag_type.?)()

fn GenericEnum(T: type) type {
return enum {
field,
const decl: T = undefined;
};
}

const generic_enum_decl = GenericEnum(u8).decl;
// ^^^^^^^^^^^^^^^^^ (u8)()

const generic_enum: GenericEnum(u8) = .field;
// ^^^^^^^^^^^^ (GenericEnum(u8))()

const generic_enum_field = generic_enum.field;
// ^^^^^^^^^^^^^^^^^^ (unknown)()

const generic_enum_tag = GenericEnum(u8).field;
// ^^^^^^^^^^^^^^^^ (GenericEnum(u8))()

fn GenericStruct(T: type) type {
return struct {
field: T,
const decl: T = undefined;
};
}

const generic_struct_decl = GenericStruct(u8).decl;
// ^^^^^^^^^^^^^^^^^^^ (u8)()

const generic_struct: GenericStruct(u8) = .{ .field = 1 };
// ^^^^^^^^^^^^^^ (GenericStruct(u8))()

const generic_struct_field = generic_struct.field;
// ^^^^^^^^^^^^^^^^^^^^ (u8)()

const generic_struct_tag = GenericStruct(u8).field;
// ^^^^^^^^^^^^^^^^^^ (unknown)()

fn Map(Context: type) type {
return struct {
unmanaged: MapUnmanaged(Context),
ctx: Context,
const Self = @This();
fn clone(self: Self) Self {
const unmanaged = self.unmanaged.cloneContext(self.ctx);
// ^^^^^^^^^ (MapUnmanaged(either type))()
return .{ .unmanaged = unmanaged, .ctx = self.ctx };
}
fn clone2(self: Self) Self {
const unmanaged = self.unmanaged.cloneContext2(self.ctx);
// ^^^^^^^^^ (MapUnmanaged(*either type))()
return .{ .unmanaged = unmanaged, .ctx = self.ctx };
}
};
}

fn MapUnmanaged(Context: type) type {
return struct {
size: u32,
const Self = @This();
fn clone(self: Self) Self {
return self.cloneContext(@as(Context, undefined));
}
fn cloneContext(self: Self, new_ctx: anytype) MapUnmanaged(@TypeOf(new_ctx)) {
_ = self;
}
fn clone2(self: Self) Self {
return self.cloneContext2(@as(Context, undefined));
}
fn cloneContext2(self: Self, new_ctx: anytype) MapUnmanaged(*@TypeOf(new_ctx)) {
_ = self;
}
};
}

const some_list: std.ArrayListUnmanaged(u8) = .empty;
// ^^^^^^^^^ (ArrayListAlignedUnmanaged(u8))()

const some_list_items = some_list.items;
// ^^^^^^^^^^^^^^^ ([]u8)()

const std = @import("std");
Loading