diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 52bfcf68955742..217101cef430bf 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -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) { diff --git a/vlib/v/tests/generics/generic_struct_init_with_comptime_if_test.v b/vlib/v/tests/generics/generic_struct_init_with_comptime_if_test.v new file mode 100644 index 00000000000000..8ec935cd634ced --- /dev/null +++ b/vlib/v/tests/generics/generic_struct_init_with_comptime_if_test.v @@ -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 +}