From 4c20c91eed14fdff0cc5a3f294331c12f22530ec Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 14 Apr 2026 23:55:22 +0300 Subject: [PATCH] checker: add @[nocopy] attribute to prevent struct value copying (fixes #26721) --- vlib/v/checker/assign.v | 18 ++++++++++ vlib/v/checker/fn.v | 7 ++++ vlib/v/checker/struct.v | 10 ++++++ vlib/v/checker/tests/nocopy_struct_err.out | 42 ++++++++++++++++++++++ vlib/v/checker/tests/nocopy_struct_err.vv | 34 ++++++++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 vlib/v/checker/tests/nocopy_struct_err.out create mode 100644 vlib/v/checker/tests/nocopy_struct_err.vv diff --git a/vlib/v/checker/assign.v b/vlib/v/checker/assign.v index 9c027cd41..69dac437d 100644 --- a/vlib/v/checker/assign.v +++ b/vlib/v/checker/assign.v @@ -4,6 +4,19 @@ module checker import v.ast +@[inline] +fn (mut c Checker) is_nocopy_struct(typ_ ast.Type) bool { + mut typ := c.unwrap_generic(typ_).clear_option_and_result() + if typ == 0 || typ.is_ptr() { + return false + } + mut sym := c.table.final_sym(typ) + if sym.kind == .generic_inst && sym.info is ast.GenericInst { + sym = c.table.sym(ast.new_type(sym.info.parent_idx)) + } + return sym.info is ast.Struct && sym.info.attrs.contains('nocopy') +} + fn assign_expr_is_auto_deref(expr ast.Expr) bool { return expr.is_auto_deref_var() } @@ -722,6 +735,11 @@ or use an explicit `unsafe{ a[..] }`, if you do not want a copy of the slice.', c.error('cannot copy map: call `move` or `clone` method (or use a reference)', right.pos()) } + if is_assign && !c.inside_unsafe && !left.is_blank_ident() + && c.is_nocopy_struct(left_type_unwrapped) && c.is_nocopy_struct(right_type_unwrapped) + && right !is ast.StructInit { + c.error('cannot copy @[nocopy] struct: use a reference instead', right.pos()) + } if left_sym.kind == .function && right_sym.info is ast.FnType { return_sym := c.table.sym(right_sym.info.func.return_type) mut missing_fn_concrete_types := false diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index a8e60723a..9a501427f 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -1171,6 +1171,9 @@ fn (mut c Checker) anon_fn(mut node ast.AnonFn) ast.Type { } else { var.typ = ptyp } + if c.is_nocopy_struct(var.typ) { + c.error('cannot capture @[nocopy] struct by value: use a reference instead', var.pos) + } if c.type_has_unresolved_generic_parts(declared_parent_typ) { has_generic = true } @@ -2351,6 +2354,10 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. } continue } + if !c.inside_unsafe && !param.is_mut && node.language == .v + && c.is_nocopy_struct(final_param_typ) { + c.error('cannot pass @[nocopy] struct by value: use a reference instead', call_arg.pos) + } if param.typ.is_ptr() && !param.is_mut && !call_arg.typ.is_any_kind_of_pointer() && call_arg.expr.is_literal() && func.language == .v && !c.pref.translated { c.error('literal argument cannot be passed as reference parameter `${c.table.type_to_str(param.typ)}`', diff --git a/vlib/v/checker/struct.v b/vlib/v/checker/struct.v index c61386ade..21d804ab9 100644 --- a/vlib/v/checker/struct.v +++ b/vlib/v/checker/struct.v @@ -160,6 +160,11 @@ fn (mut c Checker) struct_decl(mut node ast.StructDecl) { c.error('cannot copy map: call `clone` method (or use a reference)', field.default_expr.pos()) } + if c.is_nocopy_struct(field.typ) && c.is_nocopy_struct(field.default_expr_typ) + && field.default_expr !is ast.StructInit { + c.error('cannot copy @[nocopy] struct: use a reference instead', + field.default_expr.pos()) + } for mut symfield in struct_sym.info.fields { if symfield.name == field.name { symfield.default_expr_typ = field.default_expr_typ @@ -956,6 +961,11 @@ fn (mut c Checker) struct_init(mut node ast.StructInit, is_field_zero_struct_ini c.error('cannot assign a const map to mut struct field, call `clone` method (or use a reference)', init_field.expr.pos()) } + if c.is_nocopy_struct(exp_type) && c.is_nocopy_struct(got_type) + && init_field.expr !is ast.StructInit { + c.error('cannot copy @[nocopy] struct: use a reference instead', + init_field.expr.pos()) + } if exp_type_sym.kind == .array && got_type_sym.kind == .array { init_field_expr_pos := init_field.expr.pos() if init_field.expr is ast.IndexExpr && init_field.expr.left is ast.Ident diff --git a/vlib/v/checker/tests/nocopy_struct_err.out b/vlib/v/checker/tests/nocopy_struct_err.out new file mode 100644 index 000000000..3fa5ef741 --- /dev/null +++ b/vlib/v/checker/tests/nocopy_struct_err.out @@ -0,0 +1,42 @@ +vlib/v/checker/tests/nocopy_struct_err.vv:11:19: error: cannot copy @[nocopy] struct: use a reference instead + 9 | + 10 | struct Holder { + 11 | field Resource = base_resource + | ~~~~~~~~~~~~~ + 12 | } + 13 | +vlib/v/checker/tests/nocopy_struct_err.vv:22:10: error: cannot copy @[nocopy] struct: use a reference instead + 20 | handle: 2 + 21 | } + 22 | copy := resource + | ~~~~~~~~ + 23 | takes_value(resource) + 24 | _ = Holder{ +vlib/v/checker/tests/nocopy_struct_err.vv:23:14: error: cannot pass @[nocopy] struct by value: use a reference instead + 21 | } + 22 | copy := resource + 23 | takes_value(resource) + | ~~~~~~~~ + 24 | _ = Holder{ + 25 | field: resource +vlib/v/checker/tests/nocopy_struct_err.vv:25:10: error: cannot copy @[nocopy] struct: use a reference instead + 23 | takes_value(resource) + 24 | _ = Holder{ + 25 | field: resource + | ~~~~~~~~ + 26 | } + 27 | _ = fn [resource] () { +vlib/v/checker/tests/nocopy_struct_err.vv:27:10: error: cannot capture @[nocopy] struct by value: use a reference instead + 25 | field: resource + 26 | } + 27 | _ = fn [resource] () { + | ~~~~~~~~ + 28 | _ = resource.handle + 29 | } +vlib/v/checker/tests/nocopy_struct_err.vv:30:14: error: cannot capture @[nocopy] struct by value: use a reference instead + 28 | _ = resource.handle + 29 | } + 30 | _ = fn [mut resource] () { + | ~~~~~~~~ + 31 | _ = resource.handle + 32 | } diff --git a/vlib/v/checker/tests/nocopy_struct_err.vv b/vlib/v/checker/tests/nocopy_struct_err.vv new file mode 100644 index 000000000..f898b54cc --- /dev/null +++ b/vlib/v/checker/tests/nocopy_struct_err.vv @@ -0,0 +1,34 @@ +@[nocopy] +struct Resource { + handle int +} + +const base_resource = Resource{ + handle: 1 +} + +struct Holder { + field Resource = base_resource +} + +fn takes_value(res Resource) { + _ = res +} + +fn main() { + mut resource := Resource{ + handle: 2 + } + copy := resource + takes_value(resource) + _ = Holder{ + field: resource + } + _ = fn [resource] () { + _ = resource.handle + } + _ = fn [mut resource] () { + _ = resource.handle + } + _ = copy +} -- 2.39.5