Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions vlib/v/checker/checker.v
Original file line number Diff line number Diff line change
Expand Up @@ -3210,6 +3210,39 @@ fn (mut c Checker) stmts_ending_with_expression(mut stmts []ast.Stmt, expected_o

fn (mut c Checker) unwrap_generic(typ ast.Type) ast.Type {
if typ.has_flag(.generic) {
// Get the type's symbol name to check for potential conflicts
sym := c.table.sym(typ)
type_name := sym.name

// Check if there's a conflict between function-level generic and struct init generic
// (both have the same generic name, e.g., 'T')
mut has_conflict := false
if c.inside_generic_struct_init && c.table.cur_fn != unsafe { nil } {
struct_generic_names := c.cur_struct_generic_types.map(c.table.sym(it).name)
if type_name in c.table.cur_fn.generic_names && type_name in struct_generic_names {
has_conflict = true
}
}

// If there's a conflict, prioritize function-level generic parameters (more outer scope)
// This prevents function-level generic parameters from being incorrectly resolved
// using struct init concrete types when both use the same name (e.g., T).
if has_conflict {
if t_typ := c.table.convert_generic_type(typ, c.table.cur_fn.generic_names,
c.table.cur_concrete_types)
{
return t_typ
}
if c.inside_lambda && c.table.cur_lambda.call_ctx != unsafe { nil } {
if t_typ := c.table.convert_generic_type(typ, c.table.cur_lambda.func.decl.generic_names,
c.table.cur_lambda.call_ctx.concrete_types)
{
return t_typ
}
}
}

// Original order: struct init first, then function-level
if c.inside_generic_struct_init {
generic_names := c.cur_struct_generic_types.map(c.table.sym(it).name)
if t_typ := c.table.convert_generic_type(typ, generic_names, c.cur_struct_concrete_types) {
Expand Down
74 changes: 74 additions & 0 deletions vlib/v/tests/generics/generic_struct_init_with_comptime_if_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Test for issue #24471
// When returning a generic struct from a generic method with $if T is Type,
// the compiler should correctly resolve the function's generic parameter T
// instead of incorrectly using the struct init's concrete types.

module main

struct Type1 {
item f32
}

struct Type2 {
item int
}

struct GenericThing[T] {
part1 T
part2 T
}

struct AnotherGenericThing[T] {
part3 T
}

// This used to fail with: `int` has no property `item`
// The bug was that inside `AnotherGenericThing[int]{...}`, the compiler
// incorrectly resolved T (the function's generic param) to `int` instead
// of keeping it as the receiver's actual type.
pub fn (thing GenericThing[T]) weird() AnotherGenericThing[int] {
$if T is Type2 {
return AnotherGenericThing[int]{thing.part1.item + thing.part2.item}
} $else $if T is Type1 {
return AnotherGenericThing[int]{int(thing.part1.item + thing.part2.item)}
} $else {
$compile_error('unrecognised type')
}
}

fn test_generic_struct_init_with_comptime_if() {
// Test with Type1 (f32 fields)
thing1 := GenericThing{Type1{1.5}, Type1{2.5}}
result1 := thing1.weird()
assert result1.part3 == 4

// Test with Type2 (int fields)
thing2 := GenericThing{Type2{1}, Type2{2}}
result2 := thing2.weird()
assert result2.part3 == 3
}

// Additional test case: generic struct return with different type parameter
struct GenericResult[T] {
val T
}

fn (thing GenericThing[T]) to_result() GenericResult[int] {
$if T is Type2 {
return GenericResult[int]{thing.part1.item + thing.part2.item}
} $else $if T is Type1 {
return GenericResult[int]{int(thing.part1.item + thing.part2.item)}
} $else {
return GenericResult[int]{0}
}
}

fn test_generic_result_with_comptime_if() {
g1 := GenericThing{Type1{3.0}, Type1{4.0}}
r1 := g1.to_result()
assert r1.val == 7

g2 := GenericThing{Type2{5}, Type2{6}}
r2 := g2.to_result()
assert r2.val == 11
}
Loading