Skip to content

Provide optional perm parameter to range loops over maps - #1100

Open
jcp19 wants to merge 4 commits into
masterfrom
claude/range-loop-exhale-perm-eob8jt
Open

Provide optional perm parameter to range loops over maps#1100
jcp19 wants to merge 4 commits into
masterfrom
claude/range-loop-exhale-perm-eob8jt

Conversation

@jcp19

@jcp19 jcp19 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

To prevent mutating the map while it is being traversed by a range loop, Gobra exhales a small but fixed permission to the map before iteration. This prevents us from traversing maps in cases where we only have _ or an amount which is only known to be positive.

This PR extends the syntax for range loops over maps, where we can pass an extra optional parameter with a permission amount or _, which stands for the permission amount we exhale when traversing the map (instead of using the default)

// if p is passed here, gobra exhales acc(m, p), instead of the default perm amount
for k, v := range m, p {

}

// _ is also allowed in the perm amount
for k, v := range m, _ {

}

claude added 3 commits August 18, 2026 12:48
The encoding of a range loop over a map exhales a fixed, very small,
amount of permission to the map for the duration of the loop body, which
guarantees that the map is not modified while iterating over it. That
fixed amount is sometimes too restrictive: it may exceed the permission
at hand, e.g. when the loop invariant only provides a symbolic amount
`p` that is not known to be greater than 1/MapExhalePermDenom.

Range clauses now accept an optional permission amount:

    for k, v := range m, p with visited { ... }

The amount is optional and, when omitted, the previous default is used.
It is checked to be strictly positive (otherwise it would not guarantee
that the range expression stays unmodified), and is only allowed when
ranging over a map, since no other encoding of a range loop exhales
permission to the range expression.

Failing the positivity check is reported as the new error
`non_positive_permission_to_range_expression`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYq5KH3pi3UeZLJthDYu4f
The permission amount is a specification-only annotation, but it was
parsed as a plain expression hanging off PRange, which is a PActualMisc.
It therefore sat in no ghost context at all, and nothing checked it for
purity: `for k := range m, impure() { }` (a call to a non-ghost impure
function) was accepted by the type checker and desugared into executable
code in a position that can only ever be specification.

Wrap the amount in a new ghost misc node, PRangePerm, so that
isEnclosingGhost holds for it, and move its well-definedness check to
wellDefGhostMisc, where it now also requires the amount to be a pure
expression. The check that an amount may only be given when ranging over
a map stays on PRange, since it needs the range expression's type.

GhostLessPrinter now drops both ghost annotations of a range clause --
the permission amount and the `with` clause -- neither of which is valid
Go, so neither belongs in the ghost-erased program.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYq5KH3pi3UeZLJthDYu4f
`for k := range m, _ { }` now exhales a wildcard amount, spelled the same
way as in an `acc` expression. This is the amount to use when all that is
at hand is a wildcard itself: the default fixed fraction is not known to
be smaller than a wildcard, so exhaling it fails.

A wildcard is not routed through the permission variable that holds a
user-provided amount, because Viper needs the literal `wildcard` at the
exhale to pick an amount small enough to be available. Consequently the
amount inhaled after the loop body is a fresh wildcard rather than the
one exhaled before it, so a wildcard amount is only useful for loops
whose invariant holds a wildcard too; both directions are covered by the
new tests. The positivity check is skipped, since wildcards are positive
by construction and cannot be compared to other amounts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYq5KH3pi3UeZLJthDYu4f
Comment thread src/main/scala/viper/gobra/ast/frontend/Ast.scala Outdated
Comment thread src/test/resources/regressions/features/loops/range_maps-fail7.gobra Outdated
@jcp19
jcp19 requested a review from ArquintL August 27, 2026 09:37
Review feedback: the extra node was not load-bearing. Its only effect was
to make isEnclosingGhost hold for the amount, and the one diagnostic that
depended on that -- "call to non-ghost impure function in ghost code" --
is subsumed by the purity check, which does not need a ghost context. The
substantive guarantees are unchanged: the amount must be pure and
assignable to perm, ghost values and ghost pure calls are still allowed,
and ghost erasure still drops it. This also matches how the `with` clause,
the range clause's other specification-only annotation, is modelled.

The checks move back to wellDefActualMisc, next to the check that an
amount may only be given when ranging over a map.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYq5KH3pi3UeZLJthDYu4f
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.

2 participants