Skip to content

Issues 2498 2#2

Open
lanceherman wants to merge 2 commits intomasterfrom
issues-2498-2
Open

Issues 2498 2#2
lanceherman wants to merge 2 commits intomasterfrom
issues-2498-2

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 by enabling compile-time evaluation for the built-in functions convert, slice, and extract32 when all arguments are literals.
  • Enhancements

    • Enhanced type inference for these built-in functions, leading to more consistent behavior in type selection.

@coderabbitai
Copy link

coderabbitai bot commented May 8, 2025

Walkthrough

Constant folding logic has been introduced for the built-in functions convert, slice, and extract32 in the Vyper compiler. Each function now attempts to evaluate calls at compile time when all arguments are literals, returning folded AST nodes or raising exceptions for invalid or unprocessable cases. Type inference logic was also refined for these functions.

Changes

File(s) Change Summary
vyper/builtins/functions.py Added _try_fold methods to Convert, Slice, and Extract32 for constant folding support. Updated infer_arg_types methods to improve type selection consistency for these built-ins.

Sequence Diagram(s)

sequenceDiagram
    participant UserCode
    participant ASTNode
    participant Convert/Slice/Extract32
    participant ConstantFolder

    UserCode->>ASTNode: Call built-in with literal args
    ASTNode->>Convert/Slice/Extract32: Dispatch to built-in
    Convert/Slice/Extract32->>ConstantFolder: Attempt _try_fold(node)
    alt All args are literals & valid
        ConstantFolder-->>Convert/Slice/Extract32: Return folded AST node
        Convert/Slice/Extract32-->>ASTNode: Return folded node
    else Invalid or non-literal args
        ConstantFolder-->>Convert/Slice/Extract32: Raise exception
    end
Loading

Poem

In Vyper’s code, a change unfolds,
Where constants now are brave and bold.
Convert, slice, extract—oh what glee!
Compile-time magic, fast as can be.
With folded nodes, the code runs light—
A rabbit hops in pure delight! 🐇✨

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

♻️ Duplicate comments (2)
vyper/builtins/functions.py (2)

420-425: Same Enum comparison problem as in convert.infer_arg_types

Identical sorted() lambda is used here – recommend the same str(t.typ) guard.


968-973: sorted() ordering hazard (again)

Same lambda pattern – see previous comments.

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

330-373: Inconsistent return node type for Hex inputs

slice() returns:

  • vy_ast.Bytes for Bytes and Hex inputs
  • vy_ast.Str for Str inputs

Returning Bytes for a Hex literal discards the hex-ness (developers may expect a Hex). Downstream passes may re-infer an incorrect type.

Two options:

  1. Emit vy_ast.Hex when the source is vy_ast.Hex.
  2. Document that slice always produces runtime bytes, never a hex literal.

Option 1 keeps surprise to a minimum:

-        elif isinstance(value, vy_ast.Hex):
-            ...
-            return vy_ast.Bytes.from_node(node, value=result)
+        elif isinstance(value, vy_ast.Hex):
+            ...
+            return vy_ast.Hex.from_node(node, value="0x"+result.hex())
📜 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 6cd3a7e.

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

Comment on lines +255 to +260
self._validate_arg_types(node)
possible = sorted(
get_possible_types_from_node(node.args[0]),
key=lambda t: (t.typ, getattr(t, "bits", 0))
)
value_type = possible[0]
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

sorted() key may raise at runtime

possible = sorted(..., key=lambda t: (t.typ, getattr(t, "bits", 0)))

If t.typ is an Enum (or any non-orderable object) Python 3 will raise TypeError. This sneaks through unit tests if all first elements are equal but explodes on heterogeneous sets.

Safer alternative:

- key=lambda t: (t.typ, getattr(t, "bits", 0))
+ key=lambda t: (str(t.typ), getattr(t, "bits", 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
self._validate_arg_types(node)
possible = sorted(
get_possible_types_from_node(node.args[0]),
key=lambda t: (t.typ, getattr(t, "bits", 0))
)
value_type = possible[0]
self._validate_arg_types(node)
possible = sorted(
get_possible_types_from_node(node.args[0]),
key=lambda t: (str(t.typ), getattr(t, "bits", 0))
)
value_type = possible[0]

Comment on lines +194 to +246
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

target_typedef = type_from_annotation(target_type)
if not isinstance(target_typedef, (BoolT, AddressT, IntegerT, BytesM_T)):
raise UnfoldableNode

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):
if len(value.value) != 42: # 0x + 40 hex chars
raise InvalidLiteral("Address must be exactly 20 bytes", node.args[0])
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)
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):
if len(value.bytes_value) != target_typedef.length:
raise InvalidLiteral(
f"Expected {target_typedef.length} bytes, got {len(value.bytes_value)}",
node.args[0]
)
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

Potentially unsafe conversions & missing edge-case validation in _try_fold

  1. DecimalT ➞ IntegerT truncates silently via int(value.value).
    Fractions are discarded instead of being rejected or rounded – this may yield unexpected compile-time values.

  2. Hex ➞ AddressT only checks len(value.value)==42, but does not:
    • validate the literal actually represents 20 bytes (bytes.fromhex / .bytes_value would be safer)
    • reject literals without the "0x" prefix (e.g. "deadbeef…") – the length check alone would pass.

  3. Converting to BytesM_T assumes the slice must be exactly target_typedef.length. Allowing shorter literals to left-pad or throwing a dedicated error would be clearer.

  4. No constant-folding path exists for:
    Int ➞ Address (very common in assembly-style patterns)
    Bytes literals to BytesM_T (only Hex handled).

Consider tightening validation and clarifying behaviour. Example for the DecimalT issue:

-            result = int(value.value)
+            if isinstance(value, vy_ast.Decimal) and value.value % 1 != 0:
+                raise InvalidLiteral("Cannot truncate decimal when converting to integer", node.args[0])
+            result = int(value.value)
📝 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
target_typedef = type_from_annotation(target_type)
if not isinstance(target_typedef, (BoolT, AddressT, IntegerT, BytesM_T)):
raise UnfoldableNode
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):
if len(value.value) != 42: # 0x + 40 hex chars
raise InvalidLiteral("Address must be exactly 20 bytes", node.args[0])
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)
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):
if len(value.bytes_value) != target_typedef.length:
raise InvalidLiteral(
f"Expected {target_typedef.length} bytes, got {len(value.bytes_value)}",
node.args[0]
)
result = value.value
else:
raise UnfoldableNode
return vy_ast.Hex.from_node(node, value=result)
raise UnfoldableNode
elif isinstance(target_typedef, IntegerT):
if not isinstance(value, (vy_ast.Int, vy_ast.Decimal)):
raise UnfoldableNode
+ if isinstance(value, vy_ast.Decimal) and value.value % 1 != 0:
+ raise InvalidLiteral("Cannot truncate decimal when converting to integer", node.args[0])
result = int(value.value)
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)

Comment on lines +920 to +960
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 start_val < 0:
raise ArgumentException("Start index cannot be negative", node.args[1])

output_type = self.infer_kwarg_types(node)["output_type"].typedef

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

if isinstance(output_type, BytesM_T):
return vy_ast.Hex.from_node(node, value=f"0x{result.hex()}")
elif isinstance(output_type, IntegerT):
return vy_ast.Int.from_node(node, value=int.from_bytes(result, "big"))
elif isinstance(output_type, AddressT):
return vy_ast.Hex.from_node(node, value=f"0x{result.hex()}")
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 may violate the requested output size

The function always extracts 32 bytes, yet:

  • output_type may be BytesM_T(n) with n ≠ 32
  • AddressT expects 20 bytes but receives 32

This creates silently-wrong literals and type mismatch down the road.

Minimal fix:

@@
-        if isinstance(output_type, BytesM_T):
-            return vy_ast.Hex.from_node(node, value=f"0x{result.hex()}")
+        if isinstance(output_type, BytesM_T):
+            expected = output_type.length
+            if expected != 32:
+                result = result[:expected]
+            return vy_ast.Hex.from_node(node, value=f"0x{result.hex()}")
         elif isinstance(output_type, IntegerT):
@@
-        elif isinstance(output_type, AddressT):
-            return vy_ast.Hex.from_node(node, value=f"0x{result.hex()}")
+        elif isinstance(output_type, AddressT):
+            # right-align as per ABI: take the last 20 bytes
+            return vy_ast.Hex.from_node(node, value=f"0x{result[-20:].hex()}")

Consider additionally rejecting BytesM_T with length > 32 at semantic level.

📝 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()
start = node.args[1].get_folded_value()
if not isinstance(start, vy_ast.Int):
raise UnfoldableNode
start_val = start.value
if start_val < 0:
raise ArgumentException("Start index cannot be negative", node.args[1])
output_type = self.infer_kwarg_types(node)["output_type"].typedef
if isinstance(value, vy_ast.Bytes):
if start_val + 32 > len(value.value):
raise ArgumentException(
f"extract32 out of bounds: start={start_val}, max_length={len(value.value)}",
node
)
result = value.value[start_val:start_val + 32]
elif isinstance(value, vy_ast.Hex):
if start_val + 32 > len(value.bytes_value):
raise ArgumentException(
f"extract32 out of bounds: start={start_val}, max_length={len(value.bytes_value)}",
node
)
result = value.bytes_value[start_val:start_val + 32]
else:
raise UnfoldableNode
if isinstance(output_type, BytesM_T):
return vy_ast.Hex.from_node(node, value=f"0x{result.hex()}")
elif isinstance(output_type, IntegerT):
return vy_ast.Int.from_node(node, value=int.from_bytes(result, "big"))
elif isinstance(output_type, AddressT):
return vy_ast.Hex.from_node(node, value=f"0x{result.hex()}")
else:
raise UnfoldableNode
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 start_val < 0:
raise ArgumentException("Start index cannot be negative", node.args[1])
output_type = self.infer_kwarg_types(node)["output_type"].typedef
if isinstance(value, vy_ast.Bytes):
if start_val + 32 > len(value.value):
raise ArgumentException(
f"extract32 out of bounds: start={start_val}, max_length={len(value.value)}",
node
)
result = value.value[start_val:start_val + 32]
elif isinstance(value, vy_ast.Hex):
if start_val + 32 > len(value.bytes_value):
raise ArgumentException(
f"extract32 out of bounds: start={start_val}, max_length={len(value.bytes_value)}",
node
)
result = value.bytes_value[start_val:start_val + 32]
else:
raise UnfoldableNode
if isinstance(output_type, BytesM_T):
expected = output_type.length
if expected != 32:
result = result[:expected]
return vy_ast.Hex.from_node(node, value=f"0x{result.hex()}")
elif isinstance(output_type, IntegerT):
return vy_ast.Int.from_node(node, value=int.from_bytes(result, "big"))
elif isinstance(output_type, AddressT):
# right-align as per ABI: take the last 20 bytes
return vy_ast.Hex.from_node(node, value=f"0x{result[-20:].hex()}")
else:
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