Let a closure specification name the closure's captured variables - #208
Draft
coord-e wants to merge 1 commit into
Draft
Let a closure specification name the closure's captured variables#208coord-e wants to merge 1 commit into
coord-e wants to merge 1 commit into
Conversation
coord-e
force-pushed
the
claude/macro-argument-index-shift-f7sa5t
branch
2 times, most recently
from
August 9, 2026 02:49
5b7d469 to
705dcfb
Compare
`closure!` clauses could only name the closure's arguments; its environment sat
in the companion formula functions as an unnameable dummy parameter. Add a
`captures` clause that restates the captured variables a clause wants to name:
let f = thrust_macros::closure!(
captures(n: i32),
ensures(result == x + n),
|x: i32| -> i32 { x + n },
);
The environment already carries the captures as a structured value — a closure's
`FunctionType` takes the tupled upvars as its leading parameter — so the clause
side is all that was missing.
`captures` becomes a tuple pattern in the companion's environment parameter,
marked `#[thrust::closure_env]`. Rather than binding the pattern positionally,
the plugin matches each name against `closure_captures`: a closure captures in
the order its body first uses each variable, which is not the order a clause
would naturally list them in, and two captures of the same type would otherwise
swap silently. Only the captures a clause names need restating, in any order.
A capture is restated with the type it has where the closure is written, except
that a mutable borrow is named as the `&mut` it is, so a clause can say both what
the capture was on entry (`*acc`) and what it becomes (`!acc`). A shared borrow
is read through instead, so adding or removing `move` does not change how a
clause names the variable.
Naming something the closure does not capture, restating a capture with a type
of a different sort, and a capture taken field by field each report an error
against the `captures` entry. Naming captures of a closure called through `&mut`
reports an error too: that environment is a `Mut`, a shape the analyzer does not
yet represent consistently between a closure's definition and its call sites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTDWVnDzef1Kx97mHWUsTU
coord-e
force-pushed
the
claude/macro-argument-index-shift-f7sa5t
branch
from
August 9, 2026 09:09
705dcfb to
026feda
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
closure!clauses could only name the closure's arguments; its environment sat in the companion formula functions as an unnameable dummy parameter. This adds acapturesclause, so a clause can talk about captured state:Only the captures a clause names need restating, and in any order.
This is the "environment exposure" follow-up left open by #189.
How it works
The environment already carries the captures as a structured value —
replace_closure_modelmapsTyKind::Closuretomodel::Closure<tupled_upvars_ty>, which builds to the upvar tuple'srty, so a closure'sFunctionTypetakes it as the leading parameter and the inferred pvars already range over it. The clause side was all that was missing.thrust-macros/src/closure.rs—captures(..)becomes a tuple pattern in the companion's environment parameter ((n, b,): (i32, bool,)), replacing the previous()dummy, marked#[thrust::closure_env].src/analyze/annot_fn.rs— binds that pattern by name rather than by position, matching each againsttcx.closure_captures. A closure captures in the order its body first uses each variable, which is not the order a clause would naturally list them in; with positional binding two captures of the same type would swap silently.src/analyze/annot.rs— the new attribute path.No change to
FunctionType, subtyping, or the CHC encoding.How a capture is restated
A capture is restated with the type it has where the closure is written, with one exception:
move, orCopyused by value)n: i32nn: i32— read throughnacc: &mut i32*accon entry,!accon exitShared borrows are read through so that adding or removing
movedoes not change how a clause is written. A mutable borrow is left as theMutit is, because a clause has to say which of the two values it means; that is the existingMutvocabulary, not new surface.Errors
Each reports against the offending
capturesentry:`m` is not captured by this closure`n` is captured as `i32``p` is captured field by field, which a closure specification cannot name&mutthis closure is called through `&mut`, so a specification cannot name its captures yetThat last one is a pre-existing analyzer limitation rather than a limitation of this feature: when a closure is called through
&mut selfits environment becomes&mut Tuple<..>, and the pvar for it is declared with one sort and applied with another (A2_Mut<Tuple<Mut<Int>>>vsA1_Tuple<Mut<Int>>). That shape already fails onmainthrough plainpre!/post!, with noclosure!involved, and it is orthogonal to capture mode — aFnMutclosure with a by-value capture hits it too. Reporting it keepsclosure!from turning it into a panic.Tests
Passing/failing pairs following the repo convention:
closure_captures— a shared-borrow capture named inensures.closure_captures_order— the closure capturesbbeforenbecause its body usesbfirst, whilecaptureslistsnfirst. The env is(own bool, own int)and the installed postcondition resolves toν = ($1 + $0.1), i.e. index1forn; positional binding would have taken$0.0and picked up thebool. This is the regression test for name resolution.closure_captures_mut— a mutable-borrow capture, relating*accto!acc.All three capture modes are covered. The full UI suite shows no regressions: the failing set is identical to the base tree's pre-existing solver-limitation failures (32), with 268 → 276 passing.
Out of scope
s.arather thans; naming that would need place paths, not just identifiers. Reported as an error.&mut— blocked on the pre-existing environment-sort inconsistency described above, not on anything in this change.