From f4348a83a9469e3b6a5038fc6b4398f8bdf22c58 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 14 Apr 2026 23:31:58 +0300 Subject: [PATCH] checker: fix wrong check error when using `mut` + `interface` in function parameter (fixes #26159) --- vlib/v/ast/types.v | 29 ++++- vlib/v/checker/checker.v | 100 ++++++++++++++++-- vlib/v/checker/fn.v | 12 ++- vlib/v/gen/c/cgen.v | 15 +-- ...n_mut_interface_param_impl_receiver_test.v | 38 +++++++ 5 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 vlib/v/tests/fns/fn_mut_interface_param_impl_receiver_test.v diff --git a/vlib/v/ast/types.v b/vlib/v/ast/types.v index ef2c2b9d7..5b2bedcdf 100644 --- a/vlib/v/ast/types.v +++ b/vlib/v/ast/types.v @@ -213,10 +213,11 @@ pub mut: @[minify] pub struct Interface { pub mut: - types []Type // all types that implement this interface - fields []StructField - methods []Fn - embeds []Type + types []Type // all types that implement this interface immutably + mut_types []Type // all types that require a mutable interface binding + fields []StructField + methods []Fn + embeds []Type // `I1 is I2` conversions conversions shared map[int][]Type // generic interface support @@ -228,6 +229,26 @@ pub mut: name_pos token.Pos } +pub fn (info &Interface) has_implementor(typ Type, include_mut_types bool) bool { + if info.types.any(it.idx() == typ.idx()) { + return true + } + return include_mut_types && info.mut_types.any(it.idx() == typ.idx()) +} + +pub fn (info &Interface) implementor_types(include_mut_types bool) []Type { + mut implementors := info.types.clone() + if !include_mut_types { + return implementors + } + for typ in info.mut_types { + if !implementors.any(it.idx() == typ.idx()) { + implementors << typ + } + } + return implementors +} + pub struct Enum { pub: vals []string diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index a27a4cad6..fc20dc533 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -1952,6 +1952,14 @@ fn (c &Checker) generic_names_for_type_parent(typ_sym &ast.TypeSymbol) []string } fn (mut c Checker) type_implements(typ ast.Type, interface_type ast.Type, pos token.Pos) bool { + return c.type_implements_with_mut_receiver(typ, interface_type, pos, false) +} + +fn (mut c Checker) type_implements_allowing_mut_receiver(typ ast.Type, interface_type ast.Type, pos token.Pos) bool { + return c.type_implements_with_mut_receiver(typ, interface_type, pos, true) +} + +fn (mut c Checker) type_implements_with_mut_receiver(typ ast.Type, interface_type ast.Type, pos token.Pos, allow_mut_receiver bool) bool { mut resolved_interface_type := c.unwrap_generic(interface_type) if typ == resolved_interface_type { return true @@ -2009,7 +2017,7 @@ fn (mut c Checker) type_implements(typ ast.Type, interface_type ast.Type, pos to // terminate early, since otherwise we get an infinite recursion/segfault: return false } - return c.type_implements(typ, inferred_type, pos) + return c.type_implements_with_mut_receiver(typ, inferred_type, pos, allow_mut_receiver) } } if inter_sym.kind == .generic_inst { @@ -2021,10 +2029,8 @@ fn (mut c Checker) type_implements(typ ast.Type, interface_type ast.Type, pos to } // do not check the same type more than once if mut inter_sym.info is ast.Interface { - for t in inter_sym.info.types { - if t.idx() == utyp.idx() { - return true - } + if inter_sym.info.has_implementor(utyp, allow_mut_receiver) { + return true } } if utyp.idx() == resolved_interface_type.idx() { @@ -2046,7 +2052,7 @@ fn (mut c Checker) type_implements(typ ast.Type, interface_type ast.Type, pos to mut source_types := []ast.Type{} match typ_sym.info { ast.Interface { - source_types = typ_sym.info.types.clone() + source_types = typ_sym.info.implementor_types(true) } else {} } @@ -2158,6 +2164,7 @@ fn (mut c Checker) type_implements(typ ast.Type, interface_type ast.Type, pos to } else { inter_sym.methods } + mut requires_mut_receiver := false // voidptr is an escape hatch, it should be allowed to be passed if utyp != ast.voidptr_type && utyp != ast.nil_type && !(interface_type.has_flag(.option) && utyp == ast.none_type) { @@ -2179,8 +2186,72 @@ fn (mut c Checker) type_implements(typ ast.Type, interface_type ast.Type, pos to continue } } - method = c.resolve_method_for_concrete_type(method, typ_sym) - msg := c.table.is_same_method(imethod, method) + // Resolve generic parameters for concrete generic types + match typ_sym.info { + ast.Struct, ast.Interface, ast.SumType { + if typ_sym.info.concrete_types.len > 0 + && typ_sym.info.parent_type.has_flag(.generic) { + parent_sym := c.table.sym(typ_sym.info.parent_type) + match parent_sym.info { + ast.Struct, ast.Interface, ast.SumType { + generic_names := + parent_sym.info.generic_types.map(c.table.sym(it).name) + if rt := c.table.convert_generic_type(method.return_type, + generic_names, typ_sym.info.concrete_types) + { + method.return_type = rt + } + method.params = method.params.clone() + for mut param in method.params { + if pt := c.table.convert_generic_type(param.typ, generic_names, + typ_sym.info.concrete_types) + { + param.typ = pt + } + } + } + else {} + } + } + } + ast.GenericInst { + parent_sym := c.table.sym(ast.new_type(typ_sym.info.parent_idx)) + match parent_sym.info { + ast.Struct, ast.Interface, ast.SumType { + generic_names := parent_sym.info.generic_types.map(c.table.sym(it).name) + if rt := c.table.convert_generic_type(method.return_type, + generic_names, typ_sym.info.concrete_types) + { + method.return_type = rt + } + method.params = method.params.clone() + for mut param in method.params { + if pt := c.table.convert_generic_type(param.typ, generic_names, + typ_sym.info.concrete_types) + { + param.typ = pt + } + } + } + else {} + } + } + else {} + } + mut checked_method := ast.Fn{ + ...imethod + params: imethod.params.clone() + } + mut used_mut_receiver := false + if allow_mut_receiver && checked_method.params.len > 0 && method.params.len > 0 + && !checked_method.params[0].is_mut && method.params[0].is_mut { + checked_method.params[0] = ast.Param{ + ...checked_method.params[0] + is_mut: true + } + used_mut_receiver = true + } + msg := c.table.is_same_method(checked_method, method) if msg.len > 0 { sig := c.table.fn_signature(imethod, skip_receiver: false) typ_sig := c.table.fn_signature(method, skip_receiver: false) @@ -2190,6 +2261,9 @@ fn (mut c Checker) type_implements(typ ast.Type, interface_type ast.Type, pos to pos) return false } + if used_mut_receiver { + requires_mut_receiver = true + } } if !are_methods_implemented { @@ -2220,8 +2294,14 @@ fn (mut c Checker) type_implements(typ ast.Type, interface_type ast.Type, pos to } } if !is_interface_upcast && utyp != ast.voidptr_type && utyp != ast.nil_type - && utyp != ast.none_type && !inter_sym.info.types.contains(utyp) { - inter_sym.info.types << utyp + && utyp != ast.none_type { + if requires_mut_receiver { + if !inter_sym.info.mut_types.contains(utyp) { + inter_sym.info.mut_types << utyp + } + } else if !inter_sym.info.types.contains(utyp) { + inter_sym.info.types << utyp + } } if !inter_sym.info.types.contains(ast.voidptr_type) { inter_sym.info.types << ast.voidptr_type diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index 600817ee0..a8e60723a 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -2324,7 +2324,9 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. resolved_param_typ = t } } - if c.type_implements(arg_typ, resolved_param_typ, call_arg.expr.pos()) { + if c.type_implements_with_mut_receiver(arg_typ, resolved_param_typ, + call_arg.expr.pos(), param.is_mut) + { if !arg_typ.is_any_kind_of_pointer() && !c.inside_unsafe && arg_typ_sym.kind != .interface { c.mark_as_referenced(mut &call_arg.expr, true) @@ -2588,7 +2590,9 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. utyp := c.unwrap_generic(typ) unwrap_sym := c.table.sym(unwrap_typ) if unwrap_sym.kind == .interface { - if c.type_implements(utyp, unwrap_typ, call_arg.expr.pos()) { + if c.type_implements_with_mut_receiver(utyp, unwrap_typ, + call_arg.expr.pos(), param.is_mut) + { if !utyp.is_any_kind_of_pointer() && !c.inside_unsafe && c.table.sym(utyp).kind != .interface { c.mark_as_referenced(mut &call_arg.expr, true) @@ -3541,7 +3545,9 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) } // Handle expected interface if final_arg_sym.kind == .interface { - if c.type_implements(got_arg_typ, final_arg_typ, arg.expr.pos()) { + if c.type_implements_with_mut_receiver(got_arg_typ, final_arg_typ, arg.expr.pos(), + param.is_mut) + { if !got_arg_typ.is_any_kind_of_pointer() && !c.inside_unsafe { got_arg_typ_sym := c.table.sym(got_arg_typ) if got_arg_typ_sym.kind != .interface { diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 717eeec5f..263fba170 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -1373,12 +1373,13 @@ pub fn (mut g Gen) write_typeof_functions() { continue } already_generated_ifaces[sym.cname] = true + impl_types := inter_info.implementor_types(true) g.definitions.writeln('${g.static_non_parallel}string v_typeof_interface_${sym.cname}(u32 sidx);') if g.pref.parallel_cc { g.extern_out.writeln('extern string v_typeof_interface_${sym.cname}(u32 sidx);') } g.writeln('${g.static_non_parallel}string v_typeof_interface_${sym.cname}(u32 sidx) {') - for t in inter_info.types { + for t in impl_types { sub_sym := g.table.sym(ast.mktyp(t)) if sub_sym.kind == .interface { continue @@ -1402,7 +1403,7 @@ pub fn (mut g Gen) write_typeof_functions() { if g.pref.parallel_cc && interface_idx_static_prefix == '' { g.extern_out.writeln('extern u32 v_typeof_interface_idx_${sym.cname}(u32 sidx);') } - for t in inter_info.types { + for t in impl_types { sub_sym := g.table.sym(ast.mktyp(t)) if sub_sym.kind == .interface { continue @@ -2633,12 +2634,13 @@ pub fn (mut g Gen) write_interface_typedef(sym ast.TypeSymbol) { pub fn (mut g Gen) write_interface_typesymbol_declaration(sym ast.TypeSymbol) { info := sym.info as ast.Interface struct_name := c_name(sym.cname) + impl_types := info.implementor_types(true) g.type_definitions.writeln('struct ${struct_name} {') g.type_definitions.writeln('\tunion {') g.type_definitions.writeln('\t\tvoid* _object;') - for variant in info.types { + for variant in impl_types { mk_typ := ast.mktyp(variant) - if mk_typ != variant && mk_typ in info.types { + if mk_typ != variant && mk_typ in impl_types { continue } vsym := g.table.sym(mk_typ) @@ -10873,6 +10875,7 @@ fn (mut g Gen) interface_table() string { continue } already_generated_ifaces[isym.cname] = true + impl_types := inter_info.implementor_types(true) // interface_name is for example Speaker interface_name := isym.cname // generate a struct that references interface methods @@ -10907,7 +10910,7 @@ fn (mut g Gen) interface_table() string { // // Exclude interface types from the table length — an interface can't // be its own concrete implementor (happens with nested generic interfaces). - iname_table_length := inter_info.types.filter(g.table.sym(it).kind != .interface).len + iname_table_length := impl_types.filter(g.table.sym(it).kind != .interface).len if iname_table_length == 0 { // msvc can not process `static struct x[0] = {};` methods_struct.writeln('${g.static_modifier}${methods_struct_name} ${interface_name}_name_table[1];') @@ -10928,7 +10931,7 @@ fn (mut g Gen) interface_table() string { mut already_generated_mwrappers := map[string]int{} iinidx_minimum_base := 1000 // Note: NOT 0, to avoid map entries set to 0 later, so `if already_generated_mwrappers[name] > 0 {` works. mut current_iinidx := iinidx_minimum_base - for st in inter_info.types { + for st in impl_types { st_sym_info := g.table.sym(st) // Skip interface types that appear in their own implementors list. // An interface can't be its own concrete implementor; this happens diff --git a/vlib/v/tests/fns/fn_mut_interface_param_impl_receiver_test.v b/vlib/v/tests/fns/fn_mut_interface_param_impl_receiver_test.v new file mode 100644 index 000000000..f3c3a2531 --- /dev/null +++ b/vlib/v/tests/fns/fn_mut_interface_param_impl_receiver_test.v @@ -0,0 +1,38 @@ +interface UserRepository { + find_by_id(id string) !User +} + +struct CreateUserUseCase { +mut: + repo UserRepository +} + +fn new_create_user_usecase(mut repo UserRepository) CreateUserUseCase { + return CreateUserUseCase{ + repo: repo + } +} + +struct SQLiteUserRepository { +mut: + calls int +} + +struct User { + calls int +} + +fn (mut repo SQLiteUserRepository) find_by_id(id string) !User { + repo.calls += id.len + return User{ + calls: repo.calls + } +} + +fn test_mut_interface_parameter_accepts_mut_receiver_implementation() { + mut sqlite_repo := SQLiteUserRepository{} + mut use_case := new_create_user_usecase(mut sqlite_repo) + user := use_case.repo.find_by_id('1') or { panic(err) } + assert user.calls == 1 + assert sqlite_repo.calls == 1 +} -- 2.39.5