Skip to content

fix[lang]: fix in and not in bytesM folding#5107

Open
banteg wants to merge 6 commits into
vyperlang:masterfrom
banteg:fix/fold-membership-bytes-case
Open

fix[lang]: fix in and not in bytesM folding#5107
banteg wants to merge 6 commits into
vyperlang:masterfrom
banteg:fix/fold-membership-bytes-case

Conversation

@banteg

@banteg banteg commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What I did

Fix #5104: in/not in folding of bytesM literals compared the raw source strings case-sensitively, so e.g. 0xA9059CBB in [method_id("transfer(address,uint256)", output_type=bytes4)] silently folded to False. #4254 fixed the same bug for ==/!= but missed the membership branch.

How I did it

Lowercase Hex values on both sides of the membership test in ConstantFolder.visit_Compare, mirroring the Eq/NotEq path.

How to verify it

@external
def foo() -> bool:
    return 0xAB00000000000000000000000000000000000001 in [0xab00000000000000000000000000000000000001]

folds to True (previously False; the runtime-evaluated equivalent has always returned True). Added mixed-case in/not in cases to test_fold_compare.py which check folded == runtime.

Commit message

bytesM literals are case-insensitive (they represent the same value
no matter if the literal is lower- or upper-case), but membership
folding compared the raw source strings. GH 4254 fixed this for `==`
and `!=` but missed the `In`/`NotIn` branch, so membership tests over
literals could silently fold to the wrong boolean.

Description for the changelog

Fix case-sensitivity of constant folding for in/not in over bytesM literals.

Cute Animal Picture

dog

bytesM literals are case-insensitive (they represent the same value
no matter if the literal is lower- or upper-case), but membership
folding compared the raw source strings. GH 4254 fixed this for `==`
and `\!=` but missed the `In`/`NotIn` branch, so e.g.
`0xA9059CBB in [method_id("transfer(address,uint256)", output_type=bytes4)]`
silently folded to `False`.
if isinstance(left, vy_ast.Hex):
# Hex values are str, convert to be case-unsensitive.
lvalue = lvalue.lower()
if any(isinstance(i, vy_ast.Hex) for i in right.elements):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think this works for recursive lists? like list of list of hex elements. i think it would be architecturally better to have an earlier normalization step (would have to be after address checksum validation though)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 504a506. Replaced the top-level hex special case with recursive literal comparison keys for lists/tuples. Hex values are lowercased only while deriving comparison keys, so checksum-sensitive AST literals are not rewritten globally. Added nested-list membership/equality coverage. Validation: compare-folding pytest and black check passed.

@charles-cooper charles-cooper left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConstantFolder(module_ast).run()


def _comparison_key(node):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind of baroque and non-performant. i would prefer normalization in Hex ctor and perhaps save the original value on Hex value for later address checksum validation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3aec412. Hex.value is now normalized at AST construction and Hex.original_value preserves the source spelling for address checksum validation. The compare fold now uses a simple recursive literal equality helper instead of the previous hex-specific comparison-key path. Validation: test_fold_compare.py, test_constants.py, and black check passed.

Comment thread vyper/ast/nodes.py
if isinstance(node.op, vy_ast.NotEq):
value = not value
return vy_ast.NameConstant.from_node(node, value=value)
else:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this work? like does 0x0A compare gt 0x0a?

not sure we need any of the literal_eq machinery at all

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified in d052cee. I removed the _literal_eq / recursive list-tuple comparison machinery and restored scalar-only folding here. Hex literals now normalize at Hex.value, so in / not in and == / != compare normalized values directly; ordered hex comparisons remain unfoldable, with a small guard test for 0x0A > 0x0a. Validation: focused fold-compare tests, constants syntax tests, and black check passed.

Comment on lines +12 to +29
def _is_literal_compare_node(node):
return isinstance(node, (vy_ast.Constant, vy_ast.List, vy_ast.Tuple))


def _literal_eq(left, right):
if not _is_literal_compare_node(left) or not _is_literal_compare_node(right):
raise UnfoldableNode("Node contains invalid field(s) for evaluation")

if type(left) is not type(right):
return False

if isinstance(left, vy_ast.Constant):
return left.value == right.value

return len(left.elements) == len(right.elements) and all(
_literal_eq(l, r) for l, r in zip(left.elements, right.elements)
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this machinery is superfluous and confusing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, removed in d052cee. The PR now relies on normalized Hex.value and the existing scalar constant-folding path instead of adding recursive literal comparison support. Same focused fold-compare, constants syntax, and black checks passed.

@charles-cooper charles-cooper left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thanks!

@charles-cooper charles-cooper enabled auto-merge (squash) June 26, 2026 20:13
@github-actions

Copy link
Copy Markdown

📊 Bytecode Size Changes (venom)

No changes detected.

Full bytecode sizes

Contract legacy-O2 legacy-Os -O2 -O3 -Os
curvefi/amm/stableswap/meta_implementation/meta_implementation_v_700.vy 23610 22805 20786 19755 19315
curvefi/legacy/CurveStableSwapMetaNG.vy 24952 23578 20490 19871 19175
curvefi/amm/stableswap/implementation/implementation_v_700.vy 24962 23769 20264 19393 18845
curvefi/legacy/CurveStableSwapNG.vy 24473 23298 19677 18844 18314
curvefi/amm/tricryptoswap/implementation/implementation_v_200.vy 20724 19959 18020 17518 16961
yearnfi/VaultV3.vy 19972 19063 17017 15131 14477
curvefi/amm/twocryptoswap/implementation/implementation_v_210.vy 17634 16894 15685 15132 14648
curvefi/legacy/CurveCryptoSwap2.vy 18947 18382 15634 15092 14711
yearnfi/VaultV2.vy 16676 15763 13863 13405 12574
curvefi/amm/stableswap/factory/factory_v_100.vy 13317 11825 11788
curvefi/gauge/child_gauge/implementation/implementation_v_110.vy 10270 9775 9182
curvefi/gauge/child_gauge/implementation/implementation_v_100.vy 9998 9502 8920
curvefi/amm/stableswap/views/views_v_120.vy 12784 12368 9970 9413 9532
curvefi/amm/tricryptoswap/math/math_v_200.vy 11189 11126 9520 8162 8424
curvefi/legacy/CurveCryptoMathOptimized3.vy 11188 11125 9519 8162 8424
curvefi/gauge/child_gauge/implementation/implementation_v_020.vy 10665 9947 9013 8565 8028
curvefi/helpers/router/router_v_110.vy 7003 6376 6607
curvefi/amm/tricryptoswap/views/views_v_200.vy 7821 7776 6697 6445 6514
curvefi/registries/metaregistry/metaregistry_v_110.vy 7590 6732 6651 5888 5727
curvefi/helpers/stable_swap_meta_zap/stable_swap_meta_zap_v_100.vy 7302 7067 6379 6012 6114
curvefi/amm/twocryptoswap/views/views_v_200.vy 6991 6946 6272 6026 6089
curvefi/amm/twocryptoswap/factory/factory_v_200.vy 5540 5252 5994 4697 4845
curvefi/amm/tricryptoswap/factory/factory_v_200.vy 5246 5021 5923 4926 5039
curvefi/registries/metaregistry/registry_handlers/stableswap/handler_v_110.vy 6633 6259 5859 5094 5562
curvefi/amm/twocryptoswap/math/math_v_210.vy 6800 6800 5581 5107 5112
curvefi/gauge/child_gauge/factory/factory_v_201.vy 4844 4547 4266 4014 3780
yearnfi/VaultFactory.vy 3765 3617 3985 2505 2965
curvefi/registries/metaregistry/registry_handlers/tricryptoswap/handler_v_110.vy 4241 3939 3838 3547 3547
curvefi/registries/metaregistry/registry_handlers/twocryptoswap/handler_v_110.vy 4186 3884 3777 3417 3433
curvefi/gauge/child_gauge/factory/factory_v_100.vy 4183 3914 3694 3428 3213
curvefi/helpers/rate_provider/rate_provider_v_101.vy 3260 3260 2779 2437 2450
curvefi/registries/address_provider/address_provider_v_201.vy 2973 2782 2713 2569 2424
curvefi/amm/stableswap/math/math_v_100.vy 3067 3046 2598 2403 2423
curvefi/helpers/rate_provider/rate_provider_v_100.vy 2847 2841 2357 2050 2058
curvefi/helpers/deposit_and_stake_zap/deposit_and_stake_zap_v_100.vy 2322 2316 2123 1889 1921
curvefi/governance/relayer/taiko/relayer_v_001.vy 2068 2064 1784 1579 1612
curvefi/governance/relayer/polygon_cdk/relayer_v_101.vy 1556 1523 1587 1405 1406
curvefi/governance/relayer/arb_orbit/relayer_v_101.vy 1266 1262 1280 1124 1155
curvefi/governance/relayer/op_stack/relayer_v_101.vy 1186 1182 1219 1066 1094
curvefi/governance/relayer/not_rollup/relayer_v_100.vy 1168 1153 1208 1061 1073
curvefi/governance/vault/vault_v_100.vy 885 866 864
curvefi/governance/relayer/relayer_v_100.vy 496 496 600 505 510
curvefi/governance/agent/agent_v_100.vy 541 541 443 415 419
curvefi/governance/agent/agent_v_101.vy 541 541 443 415 419

@charles-cooper

Copy link
Copy Markdown
Member

hmm, maybe address literals got fucked up now? https://github.com/vyperlang/vyper/actions/runs/27589755492/job/83742065481?pr=5107 and #5107 (comment)

@charles-cooper charles-cooper disabled auto-merge June 26, 2026 20:37
@banteg

banteg commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Confirmed. Hex.value is normalized lowercase on this branch, and legacy parse_Hex was still checksum-validating against that normalized spelling for AddressT literals.

Pushed a4294f34d to keep using the normalized value for codegen numerics, but validate address checksum spelling against Hex.original_value. I also added a non-fuzz regression for convert(0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE, bool) so this path is covered outside the fuzz split.

Validation:

  • uv run --python /opt/homebrew/bin/python3.12 pytest -q tests/functional/builtins/codegen/test_convert.py::test_convert_checksum_address_literal_bool
  • uv run --python /opt/homebrew/bin/python3.12 pytest -q -m fuzzing 'tests/functional/builtins/codegen/test_convert.py::test_convert_passing[i_typ5-o_typ5-0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE]'
  • uv run ruff check vyper/codegen/expr.py tests/functional/builtins/codegen/test_convert.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4294f34d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vyper/ast/nodes.py
# AST copies and folded-value snapshots can rebuild Hex from existing fields.
# Preserve the source spelling when it is already available.
self.original_value = getattr(self, "original_value", self.value)
self.value = self.value.lower()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve case for bytesM validation

Lowercasing Hex.value here runs before semantic validation, but BytesM_T.validate_literal still checks node.value to reject mixed uppercase/lowercase hex literals. As a result, inputs such as foo: constant(bytes20) = 0x6b175474e89094c44da98b954eedeac495271d0F are normalized and accepted as bytes20 instead of raising the existing checksum/mixed-case InvalidLiteral, weakening the typo protection covered by tests/unit/ast/nodes/test_hex.py. Please keep that validator on original_value or delay normalization until after validation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b780703. BytesM_T.validate_literal now checks mixed-case spelling from Hex.original_value before constant folding uses the normalized value, restoring the existing invalid-literal coverage including checksum-shaped bytes20 literals.

Validation: uv run --python 3.12 -m pytest tests/unit/ast tests/functional/syntax/test_constants.py tests/functional/builtins/codegen/test_convert.py -q (1715 passed).

Comment thread vyper/ast/nodes.py
)

DICT_AST_SKIPLIST = ("full_source_code", "node_source_code")
DICT_AST_SKIPLIST = ("full_source_code", "node_source_code", "original_value")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit original hex spelling in AST output

Because to_dict() now omits original_value while Hex.value is normalized, ast_dict/annotated_ast_dict output for a checksummed address contains the lowercased literal instead of the source spelling. If that dict is round-tripped through dict_to_ast (the public AST utility), the rebuilt Hex.original_value is the lowercased address and subsequent address validation rejects it as a checksum mismatch, so AST export/import no longer preserves valid contracts containing checksummed address literals. Please either serialize the original spelling as the value for Hex nodes or include enough data to restore it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b780703. Hex.to_dict now emits the original source spelling as value, preserving the public AST format and allowing dict_to_ast to reconstruct checksum-sensitive literals. Added a checksummed-address round-trip regression.

Validation: uv run --python 3.12 -m pytest tests/unit/ast tests/functional/syntax/test_constants.py tests/functional/builtins/codegen/test_convert.py -q (1715 passed), plus focused Ruff, Black, and git diff --check.

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.

in/not in folding of bytesM literals is case-sensitive

2 participants