Skip to content

Issue #2498 has been resolved.#1

Open
lanceherman wants to merge 1 commit intomasterfrom
issues-2498
Open

Issue #2498 has been resolved.#1
lanceherman wants to merge 1 commit intomasterfrom
issues-2498

Conversation

@lanceherman
Copy link
Owner

@lanceherman lanceherman commented May 8, 2025

What I did

How I did it

How to verify it

Commit message

Commit message for the final, squashed PR. (Optional, but reviewers will appreciate it! Please see our commit message style guide for what we would ideally like to see in a commit message.)

Description for the changelog

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->

Summary by CodeRabbit

  • New Features
    • Improved performance for certain built-in functions by enabling constant folding for type conversion, slicing, and data extraction operations during compilation.
  • Bug Fixes
    • Enhanced error handling for invalid arguments in conversion, slicing, and extraction operations, providing clearer feedback for out-of-bounds or unsupported inputs.

@coderabbitai
Copy link

coderabbitai bot commented May 8, 2025

Walkthrough

Constant folding capabilities have been added to the Convert, Slice, and Extract32 builtin functions in the Vyper compiler by introducing _try_fold methods. The infer_arg_types method in Convert is also simplified, removing complex filtering and allowing conversions between identical types.

Changes

File(s) Change Summary
vyper/builtins/functions.py Added _try_fold methods to Convert, Slice, and Extract32 for constant folding; simplified infer_arg_types logic in Convert.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Compiler
    participant Convert/Slice/Extract32

    User->>Compiler: Provide code with constant expressions
    Compiler->>Convert/Slice/Extract32: Call _try_fold on constant nodes
    Convert/Slice/Extract32-->>Compiler: Return folded constant or raise exception
    Compiler-->>User: Output optimized code with folded constants
Loading

Poem

In the warren of code, new magic unfolds,
Constants now fold as the compiler beholds.
Convert, Slice, Extract32—
Crunching the numbers, slicing right through!
With simpler types and folding delight,
Vyper hops forward, code shining bright.
🐇✨

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
vyper/builtins/functions.py (1)

317-349: Minor: preserve literal kind & length metadata in folded slice()

The folding returns a generic vy_ast.Bytes (or vy_ast.Str) without
setting .length, losing information that could still be statically
known and useful to later passes / optimisers (e.g. gas estimates).

Consider:

-            result = value.value[start_val:start_val + length_val]
-            return vy_ast.Bytes.from_node(node, value=result)
+            result = value.value[start_val : start_val + length_val]
+            lit = vy_ast.Bytes.from_node(node, value=result)
+            lit._metadata["type"] = BytesT(length_val)
+            return lit

Same applies to the vy_ast.Hex branch which also converts the value to
Bytes – if this is intentional, add a short comment.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11522b8 and 2f4c123.

📒 Files selected for processing (1)
  • vyper/builtins/functions.py (3 hunks)

Comment on lines 245 to 249
def infer_arg_types(self, node, expected_return_typ=None):
validate_call_args(node, 2)

self._validate_arg_types(node)
value_type = get_possible_types_from_node(node.args[0]).pop()
target_type = type_from_annotation(node.args[1])
value_types = get_possible_types_from_node(node.args[0])

# For `convert` of integer literals, we need to match type inference rules in
# convert.py codegen routines.
# TODO: This can probably be removed once constant folding for `convert` is implemented
if len(value_types) > 1 and all(isinstance(v, IntegerT) for v in value_types):
# Get the smallest (and unsigned if available) type for non-integer target types
# (note this is different from the ordering returned by `get_possible_types_from_node`)
if not isinstance(target_type, IntegerT):
value_types = sorted(value_types, key=lambda v: (v.is_signed, v.bits), reverse=True)
else:
# filter out the target type from list of possible types
value_types = [i for i in value_types if not target_type.compare_type(i)]

value_type = value_types.pop()

# block conversions between same type
if target_type.compare_type(value_type):
raise InvalidType(f"Value and target type are both '{target_type}'", node)

return [value_type, TYPE_T(target_type)]
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Non-deterministic type selection in infer_arg_types

get_possible_types_from_node(...).pop() takes an arbitrary element from a set.
Two identical compilations can therefore pick different source types when multiple are possible, leading to flaky error messages or, worse, silently different IR.

Use a deterministic selection strategy, e.g. sort by priority:

-        value_type = get_possible_types_from_node(node.args[0]).pop()
+        possible = sorted(
+            get_possible_types_from_node(node.args[0]),
+            key=lambda t: (t.typ, getattr(t, "bits", 0))  # or any stable rule
+        )
+        value_type = possible[0]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def infer_arg_types(self, node, expected_return_typ=None):
validate_call_args(node, 2)
self._validate_arg_types(node)
value_type = get_possible_types_from_node(node.args[0]).pop()
target_type = type_from_annotation(node.args[1])
value_types = get_possible_types_from_node(node.args[0])
# For `convert` of integer literals, we need to match type inference rules in
# convert.py codegen routines.
# TODO: This can probably be removed once constant folding for `convert` is implemented
if len(value_types) > 1 and all(isinstance(v, IntegerT) for v in value_types):
# Get the smallest (and unsigned if available) type for non-integer target types
# (note this is different from the ordering returned by `get_possible_types_from_node`)
if not isinstance(target_type, IntegerT):
value_types = sorted(value_types, key=lambda v: (v.is_signed, v.bits), reverse=True)
else:
# filter out the target type from list of possible types
value_types = [i for i in value_types if not target_type.compare_type(i)]
value_type = value_types.pop()
# block conversions between same type
if target_type.compare_type(value_type):
raise InvalidType(f"Value and target type are both '{target_type}'", node)
return [value_type, TYPE_T(target_type)]
def infer_arg_types(self, node, expected_return_typ=None):
self._validate_arg_types(node)
- value_type = get_possible_types_from_node(node.args[0]).pop()
+ possible = sorted(
+ get_possible_types_from_node(node.args[0]),
+ key=lambda t: (t.typ, getattr(t, "bits", 0)) # stable ordering
+ )
+ value_type = possible[0]
target_type = type_from_annotation(node.args[1])
return [value_type, TYPE_T(target_type)]

Comment on lines +893 to +915
def _try_fold(self, node):
validate_call_args(node, 2)
value = node.args[0].get_folded_value()
start = node.args[1].get_folded_value()

if not isinstance(start, vy_ast.Int):
raise UnfoldableNode

start_val = start.value

if isinstance(value, vy_ast.Bytes):
if start_val + 32 > len(value.value):
raise ArgumentException("extract32 out of bounds", node)
result = value.value[start_val:start_val + 32]
return vy_ast.Bytes.from_node(node, value=result)
elif isinstance(value, vy_ast.Hex):
if start_val + 32 > len(value.bytes_value):
raise ArgumentException("extract32 out of bounds", node)
result = value.bytes_value[start_val:start_val + 32]
return vy_ast.Bytes.from_node(node, value=result)
else:
raise UnfoldableNode

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

extract32() folding ignores output_type kwarg and returns wrong AST node

Issues:

  1. The kw-argument output_type (default bytes32) is not examined, so
    extract32(b, 0, output_type=int128) still folds to a Bytes node.
  2. Even for the default case the function emits a dynamic Bytes
    literal, not a fixed-length BytesM_T(32) or vy_ast.Hex, breaking
    the return-type contract.
  3. No negative-index / bounds checking for start < 0.
@@
-            result = value.value[start_val:start_val + 32]
-            return vy_ast.Bytes.from_node(node, value=result)
+            result = value.value[start_val : start_val + 32]
+
+            out_typedef = self.infer_kwarg_types(node)["output_type"].typedef
+
+            if isinstance(out_typedef, BytesM_T):
+                return vy_ast.Hex.from_node(node, value=result.hex())
+            elif isinstance(out_typedef, IntegerT):
+                return vy_ast.Int.from_node(node, value=int.from_bytes(result, "big"))
+            elif isinstance(out_typedef, AddressT):
+                return vy_ast.Hex.from_node(node, value="0x" + result.hex())
+            else:
+                raise CompilerPanic("Unhandled output_type in extract32 folding")

Failing to honour output_type can propagate a mismatched type deep into
IR generation and surface as opaque post-optimisation crashes.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +194 to +238
def _try_fold(self, node):
validate_call_args(node, 2)
value = node.args[0].get_folded_value()
target_type = node.args[1].get_folded_value()
if not isinstance(target_type, vy_ast.Name):
raise UnfoldableNode

# Get the target type
target_typedef = type_from_annotation(target_type)
if not isinstance(target_typedef, (BoolT, AddressT, IntegerT, BytesM_T)):
raise UnfoldableNode

# Convert the value based on target type
if isinstance(target_typedef, BoolT):
if isinstance(value, vy_ast.Int):
result = bool(value.value)
elif isinstance(value, vy_ast.Bool):
result = value.value
else:
raise UnfoldableNode
return vy_ast.Bool.from_node(node, value=result)

elif isinstance(target_typedef, AddressT):
if isinstance(value, vy_ast.Hex):
result = value.value
else:
raise UnfoldableNode
return vy_ast.Hex.from_node(node, value=result)

elif isinstance(target_typedef, IntegerT):
if isinstance(value, (vy_ast.Int, vy_ast.Decimal)):
result = int(value.value)
else:
raise UnfoldableNode
return vy_ast.Int.from_node(node, value=result)

elif isinstance(target_typedef, BytesM_T):
if isinstance(value, vy_ast.Hex):
result = value.value
else:
raise UnfoldableNode
return vy_ast.Hex.from_node(node, value=result)

raise UnfoldableNode

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Constant-folded convert() skips range / size validation – can emit ill-typed literals at compile time

The new folding branch happily constructs the target literal without any of the safety checks that the runtime implementation performs:

  • For IntegerT targets no ast_bounds (min / max) verification is done, so convert(2**300, int128) would now be folded into an out-of-range vy_ast.Int which later passes type-checking because its value has already been frozen.
  • For AddressT the hex literal length is not verified (should be exactly 20 bytes / 40 hex chars).
  • For BytesM_T the produced literal is not checked against target_typedef.length.
  • Silent narrowing of DecimalT → IntegerT truncates without a bounds check or a ZeroDivisionException- style guard used elsewhere.

This opens a correctness hole: an invalid literal is accepted during folding and only surfaces at execution, bypassing compile-time guarantees.

@@
-        elif isinstance(target_typedef, IntegerT):
-            if isinstance(value, (vy_ast.Int, vy_ast.Decimal)):
-                result = int(value.value)
-            else:
-                raise UnfoldableNode
-            return vy_ast.Int.from_node(node, value=result)
+        elif isinstance(target_typedef, IntegerT):
+            if not isinstance(value, (vy_ast.Int, vy_ast.Decimal)):
+                raise UnfoldableNode
+            result = int(value.value)
+            # validate range            ↓↓↓
+            lo, hi = target_typedef.ast_bounds
+            if result < lo or result > hi:
+                raise InvalidLiteral(
+                    f"value {result} out of range for {target_typedef}", node.args[0]
+                )
+            return vy_ast.Int.from_node(node, value=result)

Similar guards are needed for the AddressT and BytesM_T branches.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _try_fold(self, node):
validate_call_args(node, 2)
value = node.args[0].get_folded_value()
target_type = node.args[1].get_folded_value()
if not isinstance(target_type, vy_ast.Name):
raise UnfoldableNode
# Get the target type
target_typedef = type_from_annotation(target_type)
if not isinstance(target_typedef, (BoolT, AddressT, IntegerT, BytesM_T)):
raise UnfoldableNode
# Convert the value based on target type
if isinstance(target_typedef, BoolT):
if isinstance(value, vy_ast.Int):
result = bool(value.value)
elif isinstance(value, vy_ast.Bool):
result = value.value
else:
raise UnfoldableNode
return vy_ast.Bool.from_node(node, value=result)
elif isinstance(target_typedef, AddressT):
if isinstance(value, vy_ast.Hex):
result = value.value
else:
raise UnfoldableNode
return vy_ast.Hex.from_node(node, value=result)
elif isinstance(target_typedef, IntegerT):
if isinstance(value, (vy_ast.Int, vy_ast.Decimal)):
result = int(value.value)
else:
raise UnfoldableNode
return vy_ast.Int.from_node(node, value=result)
elif isinstance(target_typedef, BytesM_T):
if isinstance(value, vy_ast.Hex):
result = value.value
else:
raise UnfoldableNode
return vy_ast.Hex.from_node(node, value=result)
raise UnfoldableNode
def _try_fold(self, node):
validate_call_args(node, 2)
value = node.args[0].get_folded_value()
target_type = node.args[1].get_folded_value()
if not isinstance(target_type, vy_ast.Name):
raise UnfoldableNode
# Get the target type
target_typedef = type_from_annotation(target_type)
if not isinstance(target_typedef, (BoolT, AddressT, IntegerT, BytesM_T)):
raise UnfoldableNode
# Convert the value based on target type
if isinstance(target_typedef, BoolT):
if isinstance(value, vy_ast.Int):
result = bool(value.value)
elif isinstance(value, vy_ast.Bool):
result = value.value
else:
raise UnfoldableNode
return vy_ast.Bool.from_node(node, value=result)
elif isinstance(target_typedef, AddressT):
if isinstance(value, vy_ast.Hex):
result = value.value
else:
raise UnfoldableNode
return vy_ast.Hex.from_node(node, value=result)
elif isinstance(target_typedef, IntegerT):
if not isinstance(value, (vy_ast.Int, vy_ast.Decimal)):
raise UnfoldableNode
result = int(value.value)
# validate range
lo, hi = target_typedef.ast_bounds
if result < lo or result > hi:
raise InvalidLiteral(
f"value {result} out of range for {target_typedef}", node.args[0]
)
return vy_ast.Int.from_node(node, value=result)
elif isinstance(target_typedef, BytesM_T):
if isinstance(value, vy_ast.Hex):
result = value.value
else:
raise UnfoldableNode
return vy_ast.Hex.from_node(node, value=result)
raise UnfoldableNode

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.

1 participant

Comments