Skip to content

checker: keep map-copy guard for option-map or-unwraps (#27867)#27870

Open
medvednikov wants to merge 16 commits into
masterfrom
fix-27867-or-unwrap-option-map
Open

checker: keep map-copy guard for option-map or-unwraps (#27867)#27870
medvednikov wants to merge 16 commits into
masterfrom
fix-27867-or-unwrap-option-map

Conversation

@medvednikov

@medvednikov medvednikov commented Jul 19, 2026

Copy link
Copy Markdown
Member

Background

#27867 reports that x := c.translations or { panic('...') } fails with cannot copy map: call move or clone method (or use a reference) when translations is a ?map[...].

This PR originally tried to exempt such or-unwraps from the map-copy guard when the option source looked immutable. Over many review rounds it became clear that the exemption cannot be made sound: V has no ownership/data-flow analysis, so an immutable binding is not proof that the unwrapped map is unaliased. For example:

mut base := {'x': 1}
opt := ?map[string]int(base)   // immutable binding, but wraps base's mutable storage
x := opt or { panic('missing') }
base['x'] = 2                  // observed through x

No local is_mut / is_ptr / receiver / provenance check can prove freshness, and "prove fresh" applied rigorously rejects essentially all lvalue sources — i.e. it collapses back to keeping the guard.

What this PR does now

Restores master's behavior: an option-map or-unwrap is a map copy and keeps the cannot copy map diagnostic, just like m2 := m1. vlib/v/checker/assign.v is identical to master (0-line diff). Adds one regression test documenting that the guard applies to the option-map or-unwrap forms (field / variable / index).

#27867 resolution

The map is unwrapped with an explicit copy, which compiles today:

x := (c.translations or { panic('...') }).clone()

If instead V should make the bare form compile, the only sound route is to auto-clone the unwrapped map (like mut a := arr[..] already does) — a deliberate language-design choice (implicit allocation) that would be a separate change.

An `or {}` block unwraps an option/result into a fresh value, so the
unwrapped expression must not be treated as an lvalue that aliases the
original map. Previously `x := c.some_map or { ... }` (and the variable
and index-expression forms) wrongly triggered the `cannot copy map: call
`move` or `clone` method` error, even though the equivalent `get() or {}`
call and option array field cases already compiled.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@medvednikov medvednikov left a comment

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.

I found two blocking correctness gaps in the current implementation:

  1. Exempting every checked IndexExpr permits shallow map copies from container storage, bypassing the aliasing protection this check implements.
  2. The top-level AST match misses parenthesized versions of the same or-unwrap expression, so the original diagnostic still occurs there.

CI/check results were intentionally not assessed.

Comment thread vlib/v/checker/assign.v Outdated
// call and option array field cases.
right_has_or_block := match right {
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.

Comment thread vlib/v/checker/assign.v Outdated
// 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.

@medvednikov

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fa2514901c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
// call and option array field cases.
right_has_or_block := match right {
ast.Ident { right.or_expr.kind != .absent }
ast.IndexExpr { right.or_expr.kind != .absent }

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 👍 / 👎.

… or-unwrap

Follow-up to the review on #27870. The previous fix exempted every checked
`or {}` unwrap of a map from the copy guard, which let a *mutable* destination
silently alias container/field storage, e.g.
`mut y := xs[0] or { panic('') }; y['x'] = 2` mutated `xs[0]`.

Mirror V's array guard, which is mutability-aware: relax the map guard only for
an immutable declaration (`x := opt_map or { ... }`), where `x` can never become
a mutable alias. Mutable destinations (`mut x := m[k] or { ... }`,
`x = m[k] or { ... }`) keep requiring `clone`/`move`. Parenthesized unwraps are
handled via `remove_par()`, so `(c.m) or { ... }` compiles too.

Adds a mutation-based checker error test for the mutable index/selector forms,
and extends the runtime test with the parenthesized case.
@medvednikov

Copy link
Copy Markdown
Member Author

Thanks — both points were valid. Fixed in 756eced.

The root cause of gap 1 is that the map-copy guard, unlike the array guard, is not mutability-aware. Confirmed empirically that the aliasing footgun only exists for a mutable destination — and it applies equally to the SelectorExpr form from the issue, not just IndexExpr:

mut xs := [{'x': 1}]
mut y := xs[0] or { panic('') }
y['x'] = 2   // mutated xs[0]

So instead of exempting every or-unwrap, I now mirror V's array guard and relax the map guard only for an immutable declaration (x := opt_map or { ... }), where x can never become a mutable alias. Mutable destinations keep requiring clone/move:

  • translations := c.translations or { panic('') } → compiles (the issue)
  • mut y := xs[0] or { panic('') } → still errors (your P1 example)
  • mut b := c.m or { panic('') } → still errors

Gap 2 (parenthesized form) is handled with remove_par(), so (c.m) or { ... } now works too.

Added a mutation-based checker error test (option_map_or_unwrap_mut_alias_err.vv) covering the mutable index and selector forms, and extended the runtime test with the parenthesized case. The full compiler_errors_test.v harness and all 216 vlib/v/tests/options/ tests pass locally.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 756eced9a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
// 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 👍 / 👎.

Follow-up to the review on #27870. For `map[K]?map[...]`, an index `or` block
that returns `none` (or another `?map`) does not unwrap the option: the result
keeps the option-map type, so `v := m[k] or { none }` copies an option handle
that still aliases the map stored in `m`.

Gate the immutable or-unwrap exemption on the result type actually clearing
option/result (`!right_type.has_flag(.option) && !right_type.has_flag(.result)`).
The option-clearing form `m[k] or { panic(...) }` stays exempt (issue #27867);
the option-preserving form is kept under the map-copy guard.

Adds a checker error test for the option-preserving index `or`.
@medvednikov

Copy link
Copy Markdown
Member Author

Good catch — fixed in 2a6f7dd.

Confirmed the bug: for map[K]?map[...], v := m[k] or { none } keeps the option-map type (v prints as Option({...})), so my exemption was suppressing the guard on what is still an option handle aliasing the map in m.

The exemption is now gated on the or expression actually clearing the option/result:

if node.op == .decl_assign && left is ast.Ident && !left.is_mut
    && !right_type.has_flag(.option) && !right_type.has_flag(.result) {

Since right_type is the post-or type, this covers the selector/ident forms too. Added option_map_or_unwrap_preserves_option_err.vv as a regression test. Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass locally.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a6f7dd221

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
Comment on lines +900 to +901
if node.op == .decl_assign && left is ast.Ident && !left.is_mut
&& !right_type.has_flag(.option) && !right_type.has_flag(.result) {

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 copy guard for mutable option sources

Fresh evidence in this revision is that the new exemption still keys only off the destination being immutable, while the added alias regression test covers only mut x := .... For mut opt := ?map[string]int({'x': 1}); x := opt or { panic('missing') }; opt?['x'] = 2, this bypass suppresses the existing map-copy diagnostic even though x shares the same map storage and observes the later mutation, which is the aliasing the m2 := m1 guard is meant to prevent. Keep requiring clone/move unless the unwrapped option source itself cannot be mutated later.

Useful? React with 👍 / 👎.

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.

This was resolved in 8c4713c (the review is anchored to the earlier 2a6f7dd revision). The exemption no longer keys only off the destination — it now also requires the unwrapped source to be immutable, via a conservative helper that walks to the root:

fn assign_or_unwrap_source_is_immutable(expr ast.Expr) bool {
    return match expr {
        ast.Ident { !expr.is_mut() }
        ast.SelectorExpr { assign_or_unwrap_source_is_immutable(expr.expr) }
        ast.IndexExpr { assign_or_unwrap_source_is_immutable(expr.left) }
        ast.ParExpr { assign_or_unwrap_source_is_immutable(expr.expr) }
        else { false }
    }
}

Your exact example now errors on HEAD:

$ v run x.v
x.v:7:7: error: cannot copy map: call `move` or `clone` method (or use a reference)
   7 |     x := opt or { panic('missing') }
         |          ~~~

I've also extended option_map_or_unwrap_mut_source_err.vv to use your exact mut opt := ?map[string]int({'x': 1}) form and to cover mutable variable, field, and index sources (previously the only alias test, ..._mut_alias_err.vv, covered just the mut x := ... destination). Verified that mutable param and mutable-receiver-field sources are guarded too. Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass.

Follow-up to the review on #27870. The immutable or-unwrap exemption only
checked that the destination local was immutable, but if the *source* option
map is mutable it can be mutated later and the immutable copy observes it, e.g.
`mut opt := ...; x := opt or { panic('') }; opt?['x'] = 2` left `x` aliasing
`opt`'s storage.

Gate the exemption on the unwrapped source itself being immutable via a new
conservative `assign_or_unwrap_source_is_immutable` helper (walks Ident /
SelectorExpr / IndexExpr / ParExpr roots; anything unknown keeps the guard).
Immutable-source unwraps (`c := Category{}; x := c.f or { ... }`) stay exempt
per issue #27867; mutable variables/fields/indexes keep requiring clone/move.

Adds a checker error test covering the mutable variable, field and index
sources.
@medvednikov

Copy link
Copy Markdown
Member Author

Fixed in 8c4713c.

Confirmed: mut opt := ?map[string]int{}; opt = {'x': 1}; x := opt or { panic('missing') }; opt?['x'] = 2 printed x = {'x': 2}x observed the later mutation of the mutable source.

The exemption now also requires the unwrapped source to be immutable, via a conservative recursive helper:

fn assign_or_unwrap_source_is_immutable(expr ast.Expr) bool {
    return match expr {
        ast.Ident { !expr.is_mut() }
        ast.SelectorExpr { assign_or_unwrap_source_is_immutable(expr.expr) }
        ast.IndexExpr { assign_or_unwrap_source_is_immutable(expr.left) }
        ast.ParExpr { assign_or_unwrap_source_is_immutable(expr.expr) }
        else { false }
    }
}

It walks to the root and only exempts plainly-immutable sources; anything unknown keeps the guard. So:

  • c := Category{}; x := c.f or { panic('') } → immutable source → exempt (issue Cannot or-unwrap ?map[string]T field #27867)
  • mut opt := ...; x := opt or { panic('') } → mutable var → guarded
  • mut c := ...; x := c.f or { panic('') } → mutable field → guarded
  • mut m := ...; x := m[k] or { panic('') } → mutable index → guarded

Added option_map_or_unwrap_mut_source_err.vv covering the mutable variable/field/index sources. Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass locally.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c4713c4d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
Comment on lines +37 to +39
ast.Ident { !expr.is_mut() }
ast.SelectorExpr { assign_or_unwrap_source_is_immutable(expr.expr) }
ast.IndexExpr { assign_or_unwrap_source_is_immutable(expr.left) }

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 Reject unwraps through immutable pointers to mutable data

When the immutable root is a pointer/reference to mutable storage, this returns true even though the underlying option map can still be mutated later through the original mutable owner. For example, with mut c := Category{translations: {'x': 1}}; p := &c; x := p.translations or { panic('missing') }; c.translations?['x'] = 2, the new exemption suppresses the map-copy guard because p is immutable, but x aliases the same map and observes the later mutation. Treat pointer roots (or selector receivers whose type is a pointer) as mutable/unknown so the existing clone/move diagnostic is preserved.

Useful? React with 👍 / 👎.

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.

Good catch — real hole, fixed in b870fb7. Confirmed your exact example was suppressing the guard on the previous revision (checker allowed it), while master rejects it.

assign_or_unwrap_source_is_immutable now treats a pointer/reference anywhere in the chain as mutable/unknown:

ast.Ident {
    if expr.obj is ast.Var {
        !expr.is_mut() && !expr.obj.typ.is_ptr()
    } else {
        !expr.is_mut()
    }
}
ast.SelectorExpr {
    !expr.expr_type.is_ptr() && assign_or_unwrap_source_is_immutable(expr.expr)
}
ast.IndexExpr {
    !expr.left_type.is_ptr() && assign_or_unwrap_source_is_immutable(expr.left)
}

So the guard is preserved for:

  • p := &c; x := p.translations or { ... } (pointer variable root)
  • fn f(p &Category) { x := p.translations or { ... } } (immutable reference parameter — receiver expr_type is a pointer)
  • arr := [&c]; x := arr[0].translations or { ... } (pointer array-element receiver — caught by the expr_type check even though the array root itself is immutable)

Added option_map_or_unwrap_ptr_source_err.vv covering all three. Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass.

…sion test

The map-copy guard for mutable option sources was already implemented in the
previous commit; this only strengthens the regression coverage. Use the exact
`mut opt := ?map[string]int({'x': 1})` form from the review thread and keep the
mutable variable/field/index source cases, so the guarded aliasing is explicit.
No checker changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66afd396ed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
// recognised; anything unknown returns false, keeping the map-copy guard.
fn assign_or_unwrap_source_is_immutable(expr ast.Expr) bool {
return match expr {
ast.Ident { !expr.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 guard for immutable option aliases

This treats any non-mut identifier as storage that cannot be mutated later, but an immutable ?map binding can still wrap a map that is mutable through another alias, e.g. mut base := {'x': 1}; opt := ?map[string]int(base); x := opt or { panic('missing') }; base['x'] = 2 now bypasses the map-copy guard and x observes the later mutation. The same applies to ?map parameters passed from mutable callers, so suppressing the guard needs an ownership/freshness check rather than just !expr.is_mut().

Useful? React with 👍 / 👎.

Follow-up to the review on #27870. assign_or_unwrap_source_is_immutable treated
an immutable pointer root as immutable, but an immutable pointer can still alias
storage its owner mutates later, e.g.
`mut c := Category{...}; p := &c; x := p.translations or { panic('') }; c.translations?['x'] = 2`
left `x` observing the mutation.

Treat a pointer/reference anywhere in the chain as mutable/unknown: reject when
the root variable's type is a pointer, and when a selector receiver
(`expr_type`) or index container (`left_type`) is a pointer, keeping the
`clone`/`move` diagnostic.

Adds a checker error test covering the pointer variable root, immutable
reference parameter, and pointer array-element receiver.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b870fb739a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
// would be observed through `x`.
// 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.

P2 Badge Do not exempt shared unwrap declarations

When the declaration is shared b := c.translations or { ... }, left.is_mut is still false, so this path marks the RHS as safe and suppresses the map-copy guard even though b is mutable under lock b { b['x'] = 2 }. That creates the same shallow alias with the map still stored in c that the existing guard rejects for shared b := a; exclude shared declarations (for example via left.info.share == .shared_t) from this immutable-destination exemption.

Useful? React with 👍 / 👎.

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.

Addressed in 6448418, with a nuance worth flagging: on this codebase the shared case was already rejected. The parser sets is_mut for shared/atomic declarations (parser.v:1553: is_mut := p.tok.kind == .key_mut || is_shared || is_atomic), so !left.is_mut already excluded shared destinations. I instrumented it to be sure — for shared b := c.translations or { ... } the exemption evaluated to false and the guard fired, matching master.

That said, relying on is_mut incidentally covering shared is fragile for a safety bypass, so I made it explicit as you suggested:

left_is_lockable_dest := left is ast.Ident && left.info is ast.IdentVar
    && left.info.share in [.shared_t, .atomic_t]
...
if node.op == .decl_assign && left is ast.Ident && !left.is_mut && !left_is_lockable_dest
    && !right_type.has_flag(.option) && !right_type.has_flag(.result) {

Now the exemption is independent of the parser's is_mut-includes-shared detail (and also covers atomic). Added option_map_or_unwrap_shared_dest_err.vv locking in that shared b := c.translations or { ... } keeps the cannot copy map diagnostic. Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass.

…opy guard

Follow-up to the review on #27870. A `shared b := c.translations or { ... }`
destination is mutable under `lock b { b['x'] = 2 }`, so the immutable-destination
exemption must not apply. The parser already sets `is_mut` for shared/atomic
declarations (so `!left.is_mut` currently excludes them), but check
`left.info.share` explicitly so this safety bypass no longer depends on that
incidental detail.

Adds a checker error test for the shared destination.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6448418396

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
Comment on lines +952 to +953
right_is_immutable_or_unwrap = has_or_block
&& assign_or_unwrap_source_is_immutable(unwrapped_right)

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 Preserve map-copy guard for lvalue or-block defaults

When the option source is immutable but the or block returns another map lvalue, this exemption suppresses the existing copy diagnostic even though the value can come from that mutable fallback. For example, mut fallback := {'x': 1}; opt := maybe_none(); x := opt or { fallback }; fallback['x'] = 2 now bypasses the guard because only opt is checked for immutability, but x aliases fallback and observes the later mutation; inspect the block's returned expression or keep the guard when the default path can return a map lvalue.

Useful? React with 👍 / 👎.

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.

Fixed in e65b20e. Confirmed x := opt or { fallback } (mutable fallback) aliased and observed the later mutation; the exemption now also inspects the or block's fallback value and only holds when the default cannot alias mutable storage — a fresh value (map literal / by-value call), an immutable lvalue, or a block that yields no value (diverges/propagates). or { fallback } now keeps the cannot copy map diagnostic; or { panic(...) }, or { {...} }, and or { immutable_map } still compile. Added option_map_or_unwrap_mut_default_err.vv.

Bigger picture — I want to be straight with you rather than keep patching: this is the 8th aliasing edge on this exemption, and the underlying one from an earlier round is still open and not fixable with a local heuristic:

mut base := {'x': 1}
opt := ?map[string]int(base)   // opt is an immutable binding, but wraps base's map
x := opt or { panic('') }      // x aliases base
base['x'] = 2                  // observed through x

opt is immutable, so assign_or_unwrap_source_is_immutable (correctly) can't see that its value aliases base — that needs data-flow/ownership info the checker doesn't have. Since maps share storage and V has no ownership tracking, no !is_mut()-style check can be made sound — there will always be another edge.

There are two sound resolutions, and I think it's worth picking one instead of continuing:

  1. Revert the exemptionx := opt or {} errors like m2 := m1; users write (opt or {}).clone() (already compiles today). Most consistent with V's explicit-map-copy rule; closes Cannot or-unwrap ?map[string]T field #27867 as "use .clone()".
  2. Auto-clone the unwrapped map (like mut a := arr[..]) — bare form compiles and runs as the issue asks, and it's sound because x is fresh. Deletes this whole heuristic and every past/future aliasing edge at once.

Happy to implement either — which do you prefer?

…lvalue

Follow-up to the review on #27870. `x := opt or { fallback }` makes `x` the
block's value when the option is empty, so a mutable map lvalue default (e.g.
`or { fallback }`) leaves `x` aliasing that storage. The exemption checked only
the option source's immutability, not the default path.

Also require the `or` block default to be non-aliasing: a fresh value (map
literal / by-value call), an immutable lvalue, or a block that yields no value
(diverges or propagates). Otherwise keep the clone/move diagnostic.

Adds a checker error test for the mutable-lvalue or-block default.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e65b20ebe4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
Comment on lines +47 to +48
ast.SelectorExpr {
!expr.expr_type.is_ptr() && assign_or_unwrap_source_is_immutable(expr.expr)

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 Treat pointer aliases as mutable unwrap sources

Fresh evidence is that the final helper still uses raw Type.is_ptr() checks rather than unaliasing pointer aliases. For a supported selector receiver like type CategoryRef = &Category; p := CategoryRef(&c); x := p.translations or { panic('missing') }, the receiver type can be the alias, so this branch classifies the source as immutable and suppresses the map-copy guard even though c.translations?['x'] = 2 can still mutate the same map storage through the original owner. Please treat alias-to-pointer receivers (and the corresponding Ident root) as mutable/unknown before applying this exemption.

Useful? React with 👍 / 👎.

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.

Fixed in 5e63eb7. Confirmed the alias-to-pointer receiver bypassed the guard on HEAD while master rejects it. The helper now fully unaliases types before every pointer check (Ident root, selector receiver, index container) via c.table.fully_unaliased_type(...).is_ptr(), so type CategoryRef = &Category is treated as a pointer:

ast.SelectorExpr {
    !c.table.fully_unaliased_type(expr.expr_type).is_ptr()
        && c.assign_or_unwrap_source_is_immutable(expr.expr)
}

(The helpers became Checker methods to reach the table.) Added option_map_or_unwrap_alias_ptr_source_err.vv. Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass.

This is round 9 of tightening this exemption, and it doesn't change the core issue I raised on the previous thread: the opt := ?map(mutable_base); x := opt or { ... } alias is still open and can't be fixed by any local is_mut/is_ptr heuristic — it needs ownership/data-flow info the checker doesn't have. I'll keep addressing concrete edges, but the only ways to actually close this out are revert the exemption (users write (opt or {}).clone()) or auto-clone the unwrapped map (sound, fixes #27867, deletes the whole heuristic). Happy to do either as soon as you pick one.

Follow-up to the review on #27870. The immutable-source helper used raw
Type.is_ptr(), so a `type Ref = &Category` receiver was classified as immutable
and the map-copy guard was suppressed, even though the underlying `&Category`
can mutate the same map through the original owner.

Fully unalias types before the pointer check (Ident root, selector receiver,
index container). The helpers are now Checker methods so they can reach the
table. Adds a checker error test for the alias-to-pointer receiver.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e63eb7f10

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
Comment on lines +93 to +94
ast.Ident, ast.SelectorExpr, ast.IndexExpr, ast.ParExpr {
c.assign_or_unwrap_source_is_immutable((last as ast.ExprStmt).expr)

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 Keep guard for immutable default aliases

When the or block's fallback is an immutable map parameter or another immutable alias of caller-owned mutable storage, this treats it as safe solely because the fallback lvalue is not mut. For example, fn pick(fallback map[string]int) map[string]int { opt := maybe_none(); x := opt or { fallback }; return x } now bypasses the map-copy diagnostic, but a caller can pass a mutable map and mutate it after pick, so the returned x aliases and observes that mutation. This is the same lvalue map copy that x := fallback would reject, so the exemption should only apply to fresh/owned fallback values.

Useful? React with 👍 / 👎.

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.

Fixed in b599c51. Confirmed x := opt or { fallback } with an immutable map parameter aliased the caller's map and observed the later mutation. The or-block default check now only accepts genuinely fresh/owned values (map literal or by-value call) or blocks that yield no value; any lvalue default — even an immutable one — keeps the cannot copy map diagnostic:

return match (last as ast.ExprStmt).expr {
    ast.MapInit, ast.CallExpr { true }
    else { false }
}

Added option_map_or_unwrap_immut_default_alias_err.vv (immutable param + immutable local defaults). Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass.

Worth naming explicitly: this is the same unsoundness as the still-open source case — an immutable binding is not proof of freshness, because a map lvalue can alias caller-owned mutable storage. You've now confirmed it for the default path; it remains true for the source path (opt := ?map(mutable_base); x := opt or { ... }), which no local is_mut/is_ptr check can catch. That's round 10. The exemption cannot be made sound this way; the only closures are revert (users write (opt or {}).clone()) or auto-clone (sound, fixes #27867, deletes the heuristic). Say the word and I'll land either.

… lvalues

Follow-up to the review on #27870. The or-block default check accepted an
immutable map lvalue, but an immutable map parameter/field can alias
caller-owned storage the caller mutates later, so `x := opt or { fallback }`
returns a value that aliases and observes that mutation — the same copy
`x := fallback` rejects.

Restrict assign_or_block_default_is_safe to genuinely fresh/owned values (map
literal or by-value call) or blocks that yield no value; any lvalue default now
keeps the map-copy guard.

Adds a checker error test for immutable parameter and immutable local defaults.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b599c5156a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
Comment on lines +87 to +89
last := or_expr.stmts.last()
if last !is ast.ExprStmt {
return true

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 Filter semicolons before trusting the or-block default

When an or block ends with a trailing semicolon, the parser leaves a SemicolonStmt at the end and check_or_expr already filters those before deciding which expression supplies the block value. This helper instead reads the raw last statement and returns safe for any non-ExprStmt, so x := opt or { fallback; } bypasses the map-copy guard even though the none path still returns the map lvalue fallback and can observe later mutations. Filter SemicolonStmt here before classifying the fallback value.

Useful? React with 👍 / 👎.

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.

Fixed in 8f35843. The helper now filters SemicolonStmt (matching check_or_expr) before classifying the block value, and I also tightened the fallthrough: previously any non-ExprStmt last statement was treated as safe, which was too permissive. Now only a genuinely diverging terminator (return/break/continue; panic/exit are calls already classified as fresh) is safe — any other or unclassifiable last statement keeps the guard.

valid_stmts := or_expr.stmts.filter(it !is ast.SemicolonStmt)
...
match last {
    ast.ExprStmt { match last.expr { ast.MapInit, ast.CallExpr { true } else { false } } }
    ast.Return, ast.BranchStmt { true }
    else { false }
}

One clarification from testing: x := opt or { fallback; } is rejected on the normal compile path with expression evaluated but not used (identically on master and this branch), so the trailing-semicolon block never compiles and there's no reachable aliasing footgun — the map-copy difference was only observable under -check (which keeps collecting errors past the first). I therefore didn't add a harness regression test: the error-test harness uses the normal path where both versions produce identical output, so it can't isolate this change. The fix is still correct defensively and now matches master under -check. Full compiler_errors_test.v (1598 tests) and all 216 vlib/v/tests/options/ tests pass.

Same standing note as the prior threads: the source-side opt := ?map(mutable_base); x := opt or { ... } alias remains open and unfixable by any local heuristic, so revert or auto-clone are still the only ways to actually close this out. Ready to land either.

Follow-up to the review on #27870. assign_or_block_default_is_safe read the raw
last statement of the or-block and returned safe for any non-ExprStmt, so a
trailing `;` (`or { fallback; }`) made it skip the map-copy guard. Filter
SemicolonStmt first (matching check_or_expr), and only treat a genuinely
diverging terminator (`return`/`break`/`continue`; `panic`/`exit` are calls
handled as fresh values) as safe — any other/unclassifiable last statement now
keeps the guard.

No separate test: `or { d; }` is rejected with 'expression evaluated but not
used' on the normal compile path (identically on master), so the map-copy
difference is only observable under -check, which the error-test harness does
not use.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f35843884

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
return match last {
ast.ExprStmt {
match last.expr {
ast.MapInit, ast.CallExpr { true }

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 Treat fallback calls as aliasing unless proven fresh

When the or block's default value is a call, this unconditionally marks it safe, but a call can simply return a caller-owned map alias. For example, fn id(m map[string]int) map[string]int { return m } with x := opt or { id(fallback) } is now accepted when opt is an immutable source; if fallback is mutated later, x observes the same storage, bypassing the lvalue-default guard added for or { fallback }. Keep the map-copy guard for arbitrary map-returning calls unless the callee is known to produce fresh storage, such as a literal/clone/move/noreturn path.

Useful? React with 👍 / 👎.

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.

Fixed in 6565262. Confirmed x := opt or { id(fallback) } aliased fallback and observed the later mutation. The or-block default check no longer trusts arbitrary calls:

fn assign_call_default_produces_fresh_map(expr ast.Expr) bool {
    if expr is ast.CallExpr {
        return expr.is_noreturn || expr.kind in [.clone, .clone_to_depth, .move]
            || (expr.is_method && expr.name in ['clone', 'move'])
    }
    return false
}

So only clone/move results (fresh storage) or @[noreturn] calls (panic/exit, no value) are exempt; or { id(fallback) } now keeps the cannot copy map diagnostic, while or { panic(...) }, or { fallback.clone() }, or { exit(1) }, and or { {...} } still compile. Added option_map_or_unwrap_call_default_alias_err.vv. Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass.

Round 12. As with the source side, this is the interprocedural-freshness problem: without data-flow the checker can't tell a fresh-returning call from an aliasing one, so I've restricted to the syntactically-known-fresh set you listed. The core opt := ?map(mutable_base); x := opt or { ... } source alias is still open and unfixable locally. revert or auto-clone remain the only ways to actually close this out — ready to land either.

Follow-up to the review on #27870. The or-block default check treated any
CallExpr as fresh, but an arbitrary map-returning call can return a caller-owned
alias (`fn id(m map) map { return m }`), so `x := opt or { id(fallback) }` aliased
`fallback` and observed later mutations.

Only exempt calls known to produce fresh storage — `clone`/`move` results — or
`@[noreturn]` calls (`panic`/`exit`) that yield no value. Any other map-returning
call now keeps the map-copy guard.

Adds a checker error test for the aliasing call default.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6565262cf8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
Comment on lines +121 to +122
return expr.is_noreturn || expr.kind in [.clone, .clone_to_depth, .move]
|| (expr.is_method && expr.name in ['clone', 'move'])

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 Restrict fresh defaults to builtin clone/move

Because CallKind is assigned from the callee name, this also treats user-defined functions or methods named clone/move as fresh. For example, a Holder.clone() map[string]int method that just returns a stored map lets x := opt or { holder.clone() } bypass the guard; if that stored map is reachable through a mutable alias, later mutations are observed through x. Please only exempt the builtin map clone/move operations, or otherwise prove the call returns fresh storage.

Useful? React with 👍 / 👎.

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.

Fixed in b82d49b. Confirmed a user-defined Holder.clone() map[string]int returning a stored map let x := opt or { holder.clone() } bypass the guard. The clone/move exemption now requires the call's receiver to be a map, so only the builtin map operations qualify:

return expr.is_method && expr.name in ['clone', 'move']
    && c.table.final_sym(expr.receiver_type).kind == .map

(final_sym so a type MyMap = map[...] receiver still counts.) Now or { holder.clone() } keeps the cannot copy map diagnostic, while the builtin or { fallback.clone() } / or { fallback.move() }, or { panic(...) }, and or { {...} } still compile. Added option_map_or_unwrap_user_clone_default_err.vv. Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass.

Round 13. The source-side opt := ?map(mutable_base); x := opt or { ... } alias is still open and unfixable by any local heuristic — revert or auto-clone remain the only ways to actually close this out. Ready to land either.

Follow-up to the review on #27870. CallKind and the method name alone do not
distinguish the builtin map clone/move from a user-defined `clone`/`move` that
returns a stored (aliasable) map, so `x := opt or { holder.clone() }` bypassed the
guard. Require the clone/move call's receiver to be a map (final_sym.kind == .map)
before treating it as fresh; noreturn calls still qualify.

Adds a checker error test for a user-defined clone method default.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b82d49bd9f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
Comment on lines +126 to +127
return expr.is_method && expr.name in ['clone', 'move']
&& c.table.final_sym(expr.receiver_type).kind == .map

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 Reject alias-map clone/move defaults

This still treats any .clone()/.move() call whose receiver ultimately unaliases to map as a fresh builtin map operation. Fresh evidence in this revision is the final_sym(expr.receiver_type).kind == .map check: checker/fn.v resolves alias receiver methods before map builtins when left_sym.kind == .alias && left_sym.has_method(method_name), so type M = map[string]int; fn (m M) clone() map[string]int { return map[string]int(m) } can be used in x := opt or { holder.m.clone() }; the guard is suppressed, but x aliases holder.m and observes later mutations. Require that the call actually resolved to the builtin map method, not just that the receiver unaliases to a map.

Useful? React with 👍 / 👎.

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.

Fixed in 01d21af. Confirmed type MapAlias = map[string]int with a user-defined clone() let x := opt or { holder.m.clone() } alias holder.m (master rejects it; HEAD suppressed the guard). The check now uses the receiver type directly, not unaliased:

return expr.is_method && expr.name in ['clone', 'move']
    && c.table.sym(expr.receiver_type).kind == .map

A map carries no user methods, so a direct .map receiver is unambiguously the builtin; an alias receiver is .alias and no longer qualifies (matching fn.v, which resolves the alias's own method first). or { holder.m.clone() } now keeps the cannot copy map diagnostic, while builtin or { fallback.clone() } / or { fallback.move() }, or { panic(...) }, and or { {...} } still compile. Added option_map_or_unwrap_alias_user_clone_default_err.vv. Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass.

Round 14. Same standing note: the source-side opt := ?map(mutable_base); x := opt or { ... } alias is still open and unfixable by any local heuristic — revert or auto-clone remain the only ways to actually close this out. Ready to land either.

…faults

Follow-up to the review on #27870. Using final_sym to check the clone/move
receiver still trusted an alias whose base is a map, but a map type alias can
define its own clone/move (`type M = map[...]; fn (m M) clone() ...`), which
fn.v resolves before the map builtin — so `x := opt or { holder.m.clone() }`
aliased holder.m. Check the receiver type directly (`sym().kind == .map`): a map
carries no user methods, so a direct map receiver is the builtin, while an alias
receiver is `.alias` and no longer qualifies.

Adds a checker error test for an alias with a user-defined clone default.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01d21af409

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
Comment on lines +99 to +102
match last.expr {
ast.MapInit { true }
ast.CallExpr { c.assign_call_default_produces_fresh_map(last.expr) }
else { false }

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 Strip parens before classifying safe defaults

When the or block's value is parenthesized, this match sees an ast.ParExpr and falls into the unsafe case, so valid fresh defaults like x := opt or { (map[string]int{}) } or x := opt or { (fallback.clone()) } still hit the map-copy diagnostic even though the unparenthesized forms are accepted. Unwrap last.expr.remove_par() before this classification so harmless parentheses do not change checker behavior.

Useful? React with 👍 / 👎.

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.

Fixed in c31f53b. remove_par() the default value before classifying it:

ast.ExprStmt {
    default_expr := last.expr.remove_par()
    match default_expr {
        ast.MapInit { true }
        ast.CallExpr { c.assign_call_default_produces_fresh_map(default_expr) }
        else { false }
    }
}

Now or { (map[string]int{}) } and or { (fallback.clone()) } compile like their unparenthesized forms, while parenthesized unsafe defaults stay guarded — or { (fallback) } and or { (holder.clone()) } still emit cannot copy map (remove_par exposes the lvalue/user-call, which then fails the freshness check). Extended the runtime regression test with the parenthesized fresh cases. Full compiler_errors_test.v and all 216 vlib/v/tests/options/ tests pass.

(This one was a false-negative rather than an aliasing hole — thanks.) The standing source-side opt := ?map(mutable_base) alias is still open and unfixable locally; revert or auto-clone remain the only ways to fully close this out.

Follow-up to the review on #27870. A parenthesized fresh default
(`or { (map[string]int{}) }`, `or { (fallback.clone()) }`) matched ast.ParExpr
and fell into the unsafe branch, so harmless parentheses wrongly triggered the
map-copy diagnostic while the unparenthesized forms compiled. remove_par() the
default value before classifying it.

Extends the runtime regression test with parenthesized fresh defaults.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c31f53bca4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/checker/assign.v Outdated
return match expr {
ast.Ident {
if expr.obj is ast.Var {
!expr.is_mut() && !c.table.fully_unaliased_type(expr.obj.typ).is_ptr()

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 guard for immutable option-map aliases

Fresh evidence in this revision is that this helper still returns true for any non-mut, non-pointer ast.Var, treating immutability as ownership. An immutable option map can still be a shallow wrapper around mutable storage, e.g. mut base := {'x': 1}; opt := ?map[string]int(base); x := opt or { panic('missing') }; base['x'] = 2; the new bypass suppresses the map-copy error at the unwrap and x observes the later mutation. Keep requiring clone/move unless the unwrapped source is proven fresh/owned, not merely immutable.

Useful? React with 👍 / 👎.

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.

Resolved by reverting the exemption in 73ce15c. You're right, and this is the terminal version of the whole thread: assign_or_unwrap_source_is_immutable treated immutability as ownership, and those aren't the same — opt := ?map(base) is an immutable binding over base's mutable storage. There's no local is_mut/is_ptr/receiver/provenance check that can prove freshness (it needs whole-program data-flow V doesn't have), and applied rigorously the "prove fresh" requirement rejects essentially all lvalue sources — i.e. it collapses to keeping the guard.

So I've restored master's behavior: an option-map or-unwrap is a map copy and keeps the cannot copy map diagnostic, exactly like m2 := m1. checker/assign.v is now identical to master (0-line diff); all the exemption helpers and their probe tests are removed, and I left a single regression test documenting that the guard applies to the option-map or-unwrap forms.

For #27867 the unwrap is written with an explicit copy — x := (c.f or { ... }).clone() — which compiles today.

…/move

The exemption added earlier tried to let `x := opt_map or { ... }` compile when
the option source looked immutable. As the review on #27870 showed across many
rounds, this cannot be made sound: V has no ownership/data-flow, so an immutable
binding is not proof the unwrapped map is unaliased — e.g.
`mut base := {'x':1}; opt := ?map[string]int(base); x := opt or { panic('') }`
leaves `x` aliasing `base`. No local is_mut/is_ptr/receiver check can prove
freshness, so restore master's behavior: an option-map or-unwrap is a map copy
and keeps the `cannot copy map` guard, exactly like `m2 := m1`.

For #27867, unwrap with an explicit copy: `x := (c.f or { ... }).clone()`.

assign.v is now identical to master; adds a single regression test documenting
that the guard applies to option-map or-unwrap forms.
@medvednikov medvednikov changed the title checker: fix or-unwrapping an option map field/var/index (fix #27867) checker: keep map-copy guard for option-map or-unwraps (#27867) Jul 20, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

vlib/v/checker/tests/option_map_or_unwrap_requires_clone_err.vv:22:13: error: cannot copy map: call `move` or `clone` method (or use a reference)

P2 Badge Stop expecting the #27867 repro to fail

The linked issue (#27867, #27867) expects c.translations or { panic(...) } on a ?map field to compile and then panic when the field is none, but this new expected output requires the old cannot copy map diagnostic for that same field-unwrapping form. compiler_errors_test will now fail as soon as the checker is actually fixed, so the reported production bug remains locked in instead of being covered by a passing/run test.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

vlib/v/checker/tests/option_map_or_unwrap_requires_clone_err.vv:22:13: error: cannot copy map: call `move` or `clone` method (or use a reference)

P2 Badge Stop expecting the #27867 repro to fail

The linked issue (#27867, #27867) expects c.translations or { panic(...) } on a ?map field to compile and then panic when the field is none, but this new expected output requires the old cannot copy map diagnostic for that same field-unwrapping form. compiler_errors_test will now fail as soon as the checker is actually fixed, so the reported production bug remains locked in instead of being covered by a passing/run test.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant