Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 11 additions & 1 deletion vlib/v/checker/assign.v
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,17 @@ or use an explicit `unsafe{ a[..] }`, if you do not want a copy of the slice.',
}
}
}
right_is_lvalue := if right is ast.ComptimeSelector {
// An `or {}` block unwraps an option/result into a fresh value, so the
// unwrapped expression is not an lvalue that aliases the original map
// (see vlang/v issue #27867). This mirrors the already allowed `get() or {}`
// call and option array field cases.
right_has_or_block := match right {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[P2] Inspect through parentheses before classifying the RHS. This match only sees the outer node. (c.translations or { panic('expected') }) is parsed as a ParExpr wrapping the SelectorExpr; ParExpr.is_lvalue() delegates to the inner expression, so this still emits cannot copy map. Please normalize/remove ParExpr wrappers (or make this a recursive helper) and add a parenthesized regression case.

ast.Ident { right.or_expr.kind != .absent }
ast.IndexExpr { right.or_expr.kind != .absent }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[P1] Keep the map-copy guard for checked index expressions. An or on an index handles lookup/bounds failure; it does not materialize a fresh map. Both the array and map C lowering paths copy the successful element from *tmp_opt_ptr into the option payload without cloning. Consequently this change allows, for example, mut xs := [{'x': 1}]; mut y := xs[0] or { panic('missing') }; y['x'] = 2, where y aliases xs[0] and bypasses the exact protection below. Please retain the guard for IndexExpr, or change lowering to clone/move the map and add a mutation-based regression test.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve map-copy checks for option-preserving index or

For map[K]?map[...] lookups where the or block returns none or another ?map, the index expression keeps the option-map type rather than unwrapping it. This line marks every indexed or as non-lvalue, so v := m[k] or { none } no longer gets the clone/move diagnostic and copies the optional map handle; mutating v? can alias the map stored in m. Gate this case on the expression actually clearing option/result, or keep the lvalue check when the indexed or preserves an option value.

Useful? React with 👍 / 👎.

ast.SelectorExpr { right.or_block.kind != .absent }
else { false }
}
right_is_lvalue := !right_has_or_block && if right is ast.ComptimeSelector {
right.left.is_lvalue()
} else {
right.is_lvalue()
Expand Down
55 changes: 55 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,55 @@
// Regression test for https://github.com/vlang/v/issues/27867
// or-unwrapping an option map (struct field, variable, index) used to fail
// with `cannot copy map: call move or clone method (or use a reference)`.
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
}
Loading