Skip to content

Let a purity template fit wherever its bound does - #12023

Merged
danog merged 1 commit into
vimeo:masterfrom
danog:fix-purity-template-constructor-binding
Sep 28, 2026
Merged

danog merged 1 commit into
vimeo:masterfrom
danog:fix-purity-template-constructor-binding

Conversation

@danog

@danog danog commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

A closure whose purity is a purity template (Closure[P](): int) never fit a capability set, not even its own bound. So passing it to a constructor that binds a class purity template bound nothing, and the object fell back to the template's default purity:

/** @psalm-purity-template C */
final class Box {
    /** @param Closure[C](): int $cb */
    public function __construct(private Closure $cb) {}
}

/**
 * @psalm-purity-template P
 * @param Closure[P](): int $f
 * @return Box[P]
 */
function makeBox(Closure $f): Box {
    return new Box($f); // was Box[impure]: InvalidReturnStatement
}

Now the comparator lets a purity template fit wherever its bound does, so C is bound to P. The added tests check that makeBox(fn(): int => 1)->fire() is accepted in a pure function, and that the same call with a closure that echoes is reported as ImpureMethodCall.

This lets a function forward the purity of a closure parameter to the object it returns, instead of inheriting it through @psalm-purity-from-template when it never calls the closure itself.

The full test suite passes except for failures that come from the test machine's environment (callmap/reflection mismatches for its extensions, zend.assertions, a loaded apcu).

🤖 Generated with Claude Code

A closure whose purity is a purity template (`Closure[P](): int`) never fit a
capability set, even its own bound: passed to a constructor binding a class
purity template (`new Box($f)` with `@param Closure[C](): int $cb`), it bound
nothing and the object fell back to the default purity (`Box[impure]`
instead of `Box[P]`).

Co-Authored-By: Claude Opus 5.5 
@danog danog added the release:fix The PR will be included in 'Fixes' section of the release notes label Sep 28, 2026
@danog
danog merged commit 8cfe7b4 into vimeo:master Sep 28, 2026
62 of 63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:fix The PR will be included in 'Fixes' section of the release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant