Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
fa25149
checker: fix or-unwrapping an option map field/var/index (fix #27867)
medvednikov Jul 19, 2026
756eced
checker: address review — only relax the map-copy guard for immutable…
medvednikov Jul 19, 2026
2a6f7dd
checker: address review — only exempt or-unwrap that clears the option
medvednikov Jul 19, 2026
8c4713c
checker: address review — keep map-copy guard for mutable option sources
medvednikov Jul 19, 2026
66afd39
tests: use the review's exact mutable-source form in the alias regres…
medvednikov Jul 19, 2026
b870fb7
checker: reject or-unwrap through immutable pointers to mutable data
medvednikov Jul 19, 2026
6448418
checker: never exempt shared/atomic or-unwrap destinations from map-c…
medvednikov Jul 19, 2026
e65b20e
checker: keep map-copy guard when or-block default returns a mutable …
medvednikov Jul 19, 2026
5e63eb7
checker: unalias types before the or-unwrap pointer check
medvednikov Jul 19, 2026
b599c51
checker: only exempt fresh/owned or-block map defaults, not immutable…
medvednikov Jul 19, 2026
8f35843
checker: filter semicolons and tighten or-block default classification
medvednikov Jul 19, 2026
6565262
checker: only exempt fresh/noreturn or-block map call defaults
medvednikov Jul 19, 2026
b82d49b
checker: restrict fresh or-block clone/move defaults to builtin map ops
medvednikov Jul 19, 2026
01d21af
checker: require direct map receiver for fresh or-block clone/move de…
medvednikov Jul 19, 2026
c31f53b
checker: strip parens before classifying or-block map defaults
medvednikov Jul 19, 2026
73ce15c
checker: revert unsound option-map or-unwrap exemption; require clone…
medvednikov Jul 20, 2026
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
19 changes: 18 additions & 1 deletion vlib/v/checker/assign.v
Original file line number Diff line number Diff line change
Expand Up @@ -886,8 +886,25 @@ or use an explicit `unsafe{ a[..] }`, if you do not want a copy of the slice.',
} else {
right.is_lvalue()
}
// `x := opt_map or { ... }` unwraps an option/result into a new immutable
// variable, so `x` can never become a mutable alias of the underlying map
// and the shallow copy is safe. This mirrors how V already accepts the
// equivalent immutable `x := opt_array_field or { ... }`. A mutable
// destination (`mut x := m[k] or { ... }`, `x = m[k] or { ... }`) keeps
// the guard, so aliasing container/field storage still requires a
// `clone`/`move`. See vlang/v issue #27867.
mut right_is_immutable_or_unwrap := false
if node.op == .decl_assign && left is ast.Ident && !left.is_mut {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the map-copy guard for mutable option sources

This exception only checks that the new local is immutable, but the existing map-copy check also rejects immutable aliases because the original map can still be mutated later. For example, mut opt := ?map[string]int{}; opt = {'x': 1}; x := opt or { panic('missing') }; opt?['x'] = 2 is now allowed, and x aliases the same map storage and observes the mutation. Limit this bypass to sources that cannot be mutated later, or require clone/move for mutable option-map variables/fields/indexes.

Useful? React with 👍 / 👎.

unwrapped_right := right.remove_par()
right_is_immutable_or_unwrap = match unwrapped_right {
ast.Ident { unwrapped_right.or_expr.kind != .absent }
ast.IndexExpr { unwrapped_right.or_expr.kind != .absent }
ast.SelectorExpr { unwrapped_right.or_block.kind != .absent }
else { false }
}
}
if left_sym.kind == .map && is_assign && right_sym.kind == .map && !c.inside_unsafe
&& !left.is_blank_ident() && right_is_lvalue
&& !left.is_blank_ident() && right_is_lvalue && !right_is_immutable_or_unwrap
&& (!right_type.is_ptr() || (right is ast.Ident && assign_expr_is_auto_deref(right))) {
// Do not allow `a = b`
c.error('cannot copy map: call `move` or `clone` method (or use a reference)',
Expand Down
14 changes: 14 additions & 0 deletions vlib/v/checker/tests/option_map_or_unwrap_mut_alias_err.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
vlib/v/checker/tests/option_map_or_unwrap_mut_alias_err.vv:14:17: error: cannot copy map: call `move` or `clone` method (or use a reference)
12 | 'x': 1
13 | }]
14 | mut a := xs[0] or { panic('missing') }
| ~~~~~~~~~~~~~~~~~~~~~~~
15 | a['x'] = 2
16 |
vlib/v/checker/tests/option_map_or_unwrap_mut_alias_err.vv:22:13: error: cannot copy map: call `move` or `clone` method (or use a reference)
20 | }
21 | }
22 | mut b := c.translations or { panic('missing') }
| ~~~~~~~~~~~~
23 | b['x'] = 2
24 |
29 changes: 29 additions & 0 deletions vlib/v/checker/tests/option_map_or_unwrap_mut_alias_err.vv
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Or-unwrapping an option map into a *mutable* destination would let the new
// variable alias the container/field storage, so it must still require an
// explicit `clone`/`move`. See vlang/v issue #27867 (the immutable
// `x := opt_map or { ... }` form is allowed, the mutable form is not).
struct Category {
mut:
translations ?map[string]int
}

fn main() {
mut xs := [{
'x': 1
}]
mut a := xs[0] or { panic('missing') }
a['x'] = 2

mut c := Category{
translations: {
'x': 1
}
}
mut b := c.translations or { panic('missing') }
b['x'] = 2

println(xs)
println(c)
println(a)
println(b)
}
70 changes: 70 additions & 0 deletions vlib/v/tests/options/option_map_field_or_unwrap_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Regression test for https://github.com/vlang/v/issues/27867
// or-unwrapping an option map (struct field, variable, index, parenthesized)
// into an immutable variable used to fail with
// `cannot copy map: call move or clone method (or use a reference)`.
// The mutable form (`mut x := m or { ... }`) still errors on purpose, see
// vlib/v/checker/tests/option_map_or_unwrap_mut_alias_err.vv.
struct Translation {
title string
}

struct Category {
title string
translations ?map[string]Translation
}

fn test_option_map_field_or_unwrap() {
c := Category{
translations: {
'en': Translation{
title: 'Hello'
}
}
}
translations := c.translations or { panic('expected') }
assert translations['en'].title == 'Hello'
}

fn test_option_map_field_or_unwrap_none() {
c := Category{}
translations := c.translations or {
map[string]Translation{}
}

assert translations.len == 0
}

fn get_option_map() ?map[string]int {
return {
'a': 1
}
}

fn test_option_map_var_or_unwrap() {
x := get_option_map()
y := x or { panic('expected') }
assert y['a'] == 1
}

fn test_option_map_index_or_unwrap() {
inner_map := {
'b': 2
}
m := {
'a': inner_map
}
inner := m['a'] or { panic('expected') }
assert inner['b'] == 2
}

fn test_option_map_field_paren_or_unwrap() {
c := Category{
translations: {
'en': Translation{
title: 'Hello'
}
}
}
translations := (c.translations or { panic('expected') })
assert translations['en'].title == 'Hello'
}
Loading