Instructions for AI coding agents working on vyupgrade.
vyupgrade is a compiler-backed codemod for Vyper contracts. It rewrites legacy
Vyper source to a requested target compiler, then validates the result by
compiling the original and migrated source and comparing ABI, method identifiers,
and storage layout. Treat the compiler-backed validation path as a core safety
feature, not an optional smoke check.
src/vyupgrade/cli.py— command-line orchestration, config loading, report generation, and write/diff/check behavior.src/vyupgrade/engine.py— shared source compilation, AST-isolated rewrite, target-overlay validation, artifact comparison, and typed validation policy.src/vyupgrade/rules.py— ordered rule pipeline. New rule groups must be imported here and inserted at the correct stage.src/vyupgrade/rule_registry.py— rule descriptors and version gating.src/vyupgrade/rule_groups/— migration rule implementations, grouped by subject area.src/vyupgrade/versions.py— supported Vyper versions, pragma/spec parsing, source/target crossing logic, and default EVM version mapping.src/vyupgrade/compiler.py— compiler subprocess setup, temporary target overlays, dependency inference, and ABI/method-identifier comparisons.src/vyupgrade/storage_layout.py— typed, fail-closed storage artifact parsing, canonicalization, target-AST evidence, and layout comparison.src/vyupgrade/analysis.pyandsrc/vyupgrade/ast_facts.py— lightweight source facts and compiler AST helpers for type-aware rewrites.src/vyupgrade/source.pyandsrc/vyupgrade/rule_helpers.py— edit, masking, parsing, insertion, and replacement helpers shared by rules.docs/vyper-syntax-history.md— source-visible Vyper syntax history used as source material for migrations.docs/migration-coverage.md— mapping from each syntax-history entry to an automated rule, diagnostic, explicit no-op, or validation-only behavior.DEVELOPMENT.md— maintainer setup, architecture, rule-writing, corpus, and release workflow reference.scripts/corpus.py— corpus import, dedupe, and compiler-backed smoke tooling.scripts/smoke-wheel.sh— package smoke test used by CI and publish workflows.
Use the locked environment unless intentionally updating dependencies.
uv run --locked ruff check src tests scripts/corpus.py scripts/release_notes.py scripts/release_preflight.py
uv run --locked pytest
scripts/smoke-wheel.shFor a targeted change, run the matching focused tests first, then the full suite. Useful focused commands:
uv run --locked pytest tests/rule_groups/test_<area>.py
uv run --locked pytest tests/test_versions.py tests/test_docs.py tests/test_rule_registry.py
uv run --locked pytest tests/test_cli.py tests/test_compiler.py tests/test_storage_layout.pytests/test_cli_integration.py invokes real Vyper compilers and can be slower
than the default unit tests. Run it when changing compiler selection, overlays,
validation outputs, target pragmas, or alpha/final target support.
- Prefer a
Fixonly when the rewrite is local, deterministic, and intended to preserve public behavior. Use aDiagnosticfor ambiguous or behavior-changing cases. - Every emitted code must be version-gated by a
Rule(..., changes=(...))descriptor that is part ofRULESinsrc/vyupgrade/rules.py. - Fix codes use
VY###; diagnostic codes useVYD###. Do not reuse a code. - Rules must be idempotent: running
apply_rules()twice should not make a second change. - Rewrites must avoid comments, string literals, and docstrings unless the rule
intentionally targets documentation syntax. Use
context.code_mask,span_is_code(), and helpers fromsource.py/rule_helpers.py. - For multiple edits, use
TextEditplusapply_edits()and avoid overlapping spans. When nested matches are possible, prefer the existinginnermost_non_overlapping()helper. - Preserve imports, pragmas, formatting, and user comments where practical.
- Keep compiler validation meaningful. Do not silence ABI, method identifier, or
storage layout differences unless there is an existing canonicalization reason
in
storage_layout.pyand tests cover it.
Rule activation lives in src/vyupgrade/rule_registry.py:
crossing("VY###", "0.x.y")— normal historical migration. It runs when the source floor is older than the introduced version and the target is at or after it.target_floor("VY###", "0.x.y")— target-capability rule. It runs whenever the requested target is at or after the version, even if the source floor is already at or after that version. Use for legacy syntax cleanups that may be present under broad, missing, or beta-era pragmas.target_update("VY###", "0.x.y")— currently gated liketarget_floor, but used to signal intentional target-directed updates such as pragma rewriting.
select and ignore are rule-code based. A descriptor with several codes runs
when any descriptor code is enabled, but emitted fixes/diagnostics are filtered
again before returning.
- Read the upstream Vyper release notes and linked PRs. Only track source-visible syntax or spelling changes: decorators, keywords, declarations, type names, builtin names/signatures, call syntax, imports, pragmas, literals, and newly accepted source forms.
- Update
docs/vyper-syntax-history.mdwith a new heading and concise before/after examples. Exclude backend-only, optimizer-only, ABI-layout-only, CLI-only, and pure runtime semantic changes unless source text must change. - Update
src/vyupgrade/versions.py:- add prereleases to
ALPHA_RELEASE_VERSIONSwhen they remain opt-in; - extend the
(minor, last_patch)ranges inKNOWN_VERSIONSandSUPPORTED_RELEASE_VERSIONSfor new final releases; - adjust
default_evm_version()if Vyper changed its default EVM version; - update source syntax floors in
_source_syntax_floor()when broad pragmas need a newer compiler to parse newly introduced syntax.
- add prereleases to
- Decide whether the default target should move. If it moves, update all
defaults and examples together:
Config.target_version,cli.py, README, docs, tests, and any pyproject examples. - Classify every syntax-history entry in
docs/migration-coverage.mdas one of automated rewrite, diagnostic, no-op, or validation-only. This file may not use markdown tables, may not contain TODO-style gaps, and must mention every version-gated rule code. - Implement automated rules in the closest existing
src/vyupgrade/rule_groups/module, or create a new focused module only when the domain does not fit. Add the module tosrc/vyupgrade/rules.pyin an order that preserves assumptions of later rules. - Add tests before considering the migration complete. Cover the before/after rewrite, diagnostics for unsafe cases, comments/strings/docstrings, version gating, idempotence, and compiler validation when relevant.
- Run the focused tests, then the full validation commands. For broad release
support, also run a corpus smoke with
scripts/corpus.py smokeagainst a representative manifest. - Update
README.md,CHANGELOG.md, and release notes when changing supported versions or user-facing behavior.
- Use
RuleContext.factsfor lightweight source facts such as interfaces, structs, storage variables, function decorators, loop variables, and return types. - Use
config.source_astplus helpers inast_facts.pywhen compiler spans or parsed constants are needed.engine.prepare_migrations()populates a per-file AST-backed config from source compilation when available. - Prefer local helpers over hand-rolled parsing when splitting arguments, inserting imports, finding matching delimiters, or computing line numbers.
- If a rewrite depends on mutability or return types of external calls, update
built-in interface facts in
vyper_builtins.pyand add tests that prove the call becomesstaticcallorextcallcorrectly. - If a rewrite introduces imports, use
insert_import()and ensure duplicate imports are not produced. - If behavior may change, emit a diagnostic and document the manual review path.
Aggressive behavior-changing rewrites should be guarded by
config.aggressive.
The test suite enforces several maintenance contracts:
tests/test_rule_registry.pychecks that rule codes used in source are present inRULE_CHANGES.tests/test_docs.pychecks thatmigration-coverage.mdreferences all version-gated rules, tracks all version headings from syntax history, contains no tables, and has no unresolved TODO/gap language.tests/test_versions.pychecks supported versions, spec resolution, syntax floors, patch-level crossings, and default EVM mappings.
When these fail after adding a version, update the docs or version map rather than relaxing the tests.
CHANGELOG.md is the source for GitHub release notes. scripts/release_notes.py
extracts the section matching the pushed tag. Publishing is handled by
.github/workflows/publish.yml on v* tags using PyPI Trusted Publishing after
lint, tests, and scripts/smoke-wheel.sh pass.
Before tagging a release, update pyproject.toml version and CHANGELOG.md, run
all validation commands, then tag with a v prefix such as v0.4.2.