Skip to content

Feature: closure capture by reference (or compile-time warning for write-only [mut x] captures) #27172

@eptx

Description

@eptx

Environment

  • V version: V 0.5.1 99f141f74 (verified no relevant commits land between that and current master b6dfae560)
  • OS-agnostic

Context

docs.md §Closures (lines 3245–3266) documents that [mut x] makes the closure's copy mutable but does not capture x by reference, so writes inside the closure are invisible to the caller. This is by design and works for stateful counter patterns. It is, however, a silent footgun for the much more common pattern of "let a helper closure update some local I'm building up."

The footgun

Downstream code routinely writes:

mut cur := ?T(none)
flush := fn [mut cur] (val T) {
    if mut t := cur { /* mutate t */; cur = t }
}
for v in items { flush(v) }
// cur is silently unchanged.

The closure compiles, runs, and the mutation lands on the closure's private copy. The outer cur is never updated. There is no warning; the surface syntax is identical to what would be a sound pattern in most languages with explicit by-reference syntax (Rust &mut, C++ &, Swift inout).

In CX (a V-native data interchange library) this exact pattern landed in three conformance-test runners and silently made every fixture mismatch pass for one release cycle. The discovery was an accident.

Possible fixes

Listed in order of preference:

  1. Reference-capture syntax — e.g. [&mut x], [ref x], or something else V-shaped. Makes the by-reference intent explicit at the capture site; today there is no way to express it.
  2. Compile-time warning — when a captured [mut x] is written to inside the closure but never re-read outside the closure's own call sites, emit "captured copy of x is modified but the change does not propagate; pass x as a mut parameter to a regular function instead." Closes the footgun without changing semantics.
  3. Documentation-only fix — surface the gotcha more prominently in docs.md §Closures and link it from the troubleshooting page. The current paragraph is technically accurate but easy to skim past.

Workaround used downstream

Rewrite the closure as a regular fn (...mut cur ...) function and call it explicitly. Works fine but breaks the closure-shaped reading of the call site.

Note

You can use the 👍 reaction to increase the issue's priority for developers.

Please note that only the 👍 reaction to the issue itself counts as a vote.
Other reactions and those to comments will not be taken into account.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Feature/Enhancement RequestThis issue is made to request a feature or an enhancement to an existing one.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions