Skip to content

fix: match version specs with compiler pragma semantics#7

Merged
banteg merged 2 commits into
masterfrom
claude/vyupgrade-issue-6-j21i1n
Jul 8, 2026
Merged

fix: match version specs with compiler pragma semantics#7
banteg merged 2 commits into
masterfrom
claude/vyupgrade-issue-6-j21i1n

Conversation

@charles-cooper

Copy link
Copy Markdown
Member

Fixes #6.

Problem

Source-version inference selected the prerelease target 0.5.0a3 as the source validation compiler for a ^0.4.2 pragma, so the source compile failed with:

vyper.exceptions.VersionException: Version specification "~=0.4.2" is not compatible with compiler version "0.5.0a3"

Root cause

known_versions_satisfying() expanded ^0.4.2 to >=0.4.2, <0.5.0 and evaluated the clauses with raw PEP 440 ordering comparisons. Under PEP 440 ordering, 0.5.0a3 < 0.5.0 is true, so the 0.5.0 alphas leaked through the caret's upper bound and compiler_version_for_source_validation() picked the newest one.

Vyper itself checks pragmas differently (vyper.ast.pre_parser.validate_version_pragma): it normalizes ^X.Y.Z to ~=X.Y.Z and bare versions to ==X.Y.Z, then tests SpecifierSet.contains(compiler_version, prereleases=True). PEP 440 specifier semantics reject a prerelease at an exclusive ordered bound (<0.5.0 never admits 0.5.0a3), so vyupgrade's matcher was strictly more permissive than the compiler it invokes.

Fix

known_versions_satisfying() now mirrors the compiler's check exactly: the spec is normalized the same way and candidates are tested with SpecifierSet.contains(..., prereleases=True). The old clause-based matcher is kept only as a fallback for specs SpecifierSet cannot parse. With this, membership in the candidate list is equivalent to "this compiler accepts this pragma", so this class of divergence cannot recur.

For the issue's repro, source validation now picks 0.4.3:

Repro.vy
  VY001 modernized version pragma (line 1)
  source compile (0.4.3): passed
  target compile: passed
  ABI unchanged: True

This also corrects compiler_version_for_spec("<0.5.0"), which previously resolved to 0.5.0a3 (same bug class).

Test updates

Two existing assertions used >=0.5.0a1,<0.5.0 as a broad alpha pragma, but the real compiler rejects that spec (0.5.0aN is a prerelease of the excluded bound 0.5.0 — verified against vyper 0.5.0a3). They now use >=0.5.0a1,<0.6.0, the form the CLI fixtures already use, plus an explicit assertion that >=0.5.0a1,<0.5.0 matches nothing. A new regression test covers the ^0.4.2 / 0.5.0a3 scenario and legacy ^0.1.0b14 caret pragmas.

ruff check, the full unit suite (484 passed), and tests/test_cli_integration.py (real compilers) all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JtAsisni8yijKyn2e29q1R


Generated by Claude Code

Source-version inference matched pragma specs with raw PEP 440 ordering
comparisons, so caret ranges like ^0.4.2 (upper bound <0.5.0) admitted
the 0.5.0 alphas, which sort below 0.5.0. Source validation then picked
the prerelease target as the source compiler and the compile failed with
a VersionException, since Vyper itself checks pragmas with SpecifierSet
semantics that exclude prereleases of an exclusive bound.

Match candidate versions the way vyper.ast.pre_parser does: normalize
bare versions to exact pins and npm-style carets to PEP 440
compatible-release clauses, then test with SpecifierSet.contains(...,
prereleases=True). The clause-based matcher remains as a fallback for
specs SpecifierSet cannot parse.

Fixes #6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JtAsisni8yijKyn2e29q1R
@charles-cooper charles-cooper requested a review from banteg July 8, 2026 16:47
known_versions_satisfying() now evaluates valid pragmas through packaging's SpecifierSet, but compiler_version_for_spec() still decided whether to choose the lowest or newest matching compiler with the legacy regex parser. That parser only recognizes full X.Y.Z versions, so valid PEP 440 compatible-release specs such as ~=0.4 were treated as if they had no lower bound.

The result was that compiler_version_for_spec("~=0.4") selected the newest known matching compiler, currently 0.5.0a3, instead of the source floor 0.4.0. That was inconsistent with the new SpecifierSet-based matching path and could reintroduce prerelease target selection for valid compiler-style pragmas.

Teach _has_lower_bound() to inspect SpecifierSet specifiers when the pragma parses as PEP 440. Compatible-release, greater-than, and greater-or-equal operators are lower bounds; exact pins are lower bounds unless they are wildcard pins, preserving the existing behavior of selecting the newest compiler for ==0.4.*.

Add regressions for ~=0.4 and ~=0.4.2 selecting their source floors, plus ==0.4.* continuing to select the newest patch in that wildcard range.
@banteg banteg merged commit 7be9819 into master Jul 8, 2026
3 checks passed
@banteg banteg deleted the claude/vyupgrade-issue-6-j21i1n branch July 8, 2026 17:53
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.

Source-version inference uses prerelease target for ^0.4.2 source

3 participants