Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix immutability for ARC4 struct and address types #337

Merged
merged 8 commits into from
Nov 7, 2024
8 changes: 6 additions & 2 deletions examples/amm/out_unoptimized/ConstantProductAMM.approval.teal
Original file line number Diff line number Diff line change
Expand Up @@ -588,10 +588,14 @@ do_asset_transfer:
// asset_receiver=receiver,
// ).submit()
itxn_begin
// amm/contract.py:357
// def do_asset_transfer(*, receiver: Account, asset: Asset, amount: UInt64) -> None:
// amm/contract.py:359
// xfer_asset=asset,
frame_dig -2
// amm/contract.py:360
// asset_amount=amount,
frame_dig -1
// amm/contract.py:361
// asset_receiver=receiver,
frame_dig -3
itxn_field AssetReceiver
itxn_field AssetAmount
Expand Down
8 changes: 4 additions & 4 deletions examples/auction/out_unoptimized/Auction.approval.teal
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,8 @@ opt_into_asset:
// auction/contract.py:36
// asset_receiver=Global.current_application_address,
global CurrentApplicationAddress
// auction/contract.py:26
// def opt_into_asset(self, asset: Asset) -> None:
// auction/contract.py:37
// xfer_asset=asset,
frame_dig -1
itxn_field XferAsset
itxn_field AssetReceiver
Expand Down Expand Up @@ -525,8 +525,8 @@ claim_asset:
// asset_amount=self.asa_amount,
// ).submit()
itxn_begin
// auction/contract.py:98
// def claim_asset(self, asset: Asset) -> None:
// auction/contract.py:102
// xfer_asset=asset,
frame_dig -1
// auction/contract.py:103
// asset_close_to=self.previous_bidder,
Expand Down
2 changes: 2 additions & 0 deletions examples/merkle/out/MerkleTree.ssa.ir

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions examples/merkle/puya.log

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions examples/sizes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@
application/Reference 177 167 - | 92 83 -
arc4_dynamic_arrays/DynamicArray 2695 1931 - | 1733 1138 -
arc4_numeric_comparisons/UIntNOrdering 1100 908 - | 786 597 -
arc4_types/Arc4Address 85 62 - | 37 18 -
arc4_types/Arc4Address 79 18 - | 34 11 -
arc4_types/Arc4Arrays 623 376 - | 368 182 -
arc4_types/Arc4BoolEval 751 14 - | 167 8 -
arc4_types/Arc4BoolType 381 69 - | 307 46 -
arc4_types/Arc4DynamicBytes 377 185 - | 213 100 -
arc4_types/Arc4DynamicStringArray 283 124 - | 172 53 -
arc4_types/Arc4MutableParams 471 286 - | 292 141 -
arc4_types/Arc4MutableParams 1110 644 - | 699 343 -
arc4_types/Arc4Mutation 2958 1426 - | 1977 593 -
arc4_types/Arc4NumericTypes 749 186 - | 243 26 -
arc4_types/Arc4RefTypes 85 46 - | 32 27 -
arc4_types/Arc4StringTypes 455 35 - | 245 13 -
arc4_types/Arc4StructsFromAnotherModule 67 12 - | 49 6 -
arc4_types/Arc4StructsType 302 239 - | 204 121 -
arc4_types/Arc4StructsType 389 239 - | 259 121 -
arc4_types/Arc4TuplesType 795 136 - | 537 58 -
arc4_types/MutableParams2 334 202 - | 193 97 -
arc_28/EventEmitter 186 133 - | 100 64 -
asset/Reference 268 261 - | 144 141 -
auction/Auction 592 522 - | 328 281 -
Expand Down Expand Up @@ -108,7 +109,7 @@
state_totals 65 32 - | 32 16 -
stress_tests/BruteForceRotationSearch 228 163 - | 152 106 -
string_ops 156 154 - | 58 55 -
struct_in_box/Example 243 206 - | 127 99 -
struct_in_box/Example 242 206 - | 127 99 -
stubs/BigUInt 192 121 - | 126 73 -
stubs/Bytes 944 279 - | 606 153 -
stubs/String 701 167 - | 416 54 -
Expand All @@ -128,6 +129,6 @@
unassigned_expression/Unassigned 158 119 - | 81 57 -
undefined_phi_args/Baddie 319 282 - | 174 157 -
unssa/UnSSA 432 368 - | 241 204 -
voting/VotingRoundApp 1593 1483 - | 734 649 -
voting/VotingRoundApp 1590 1483 - | 733 649 -
with_reentrancy/WithReentrancy 255 242 - | 132 122 -
Total 69200 53576 53517 | 32843 21764 21720
Total 70250 54092 54033 | 33494 22056 22012
4 changes: 3 additions & 1 deletion examples/struct_in_box/out/ExampleContract.ssa.ir

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,10 @@ write_to_box:
// @subroutine
// def write_to_box(self, user: UserStruct) -> None:
proto 1 1
frame_dig -1
// struct_in_box/contract.py:20
// box_key = user.id.bytes
frame_dig -1
dup
intc_2 // 2
intc_3 // 8
extract3 // on error: Index access is out of bounds
Expand All @@ -181,7 +182,6 @@ write_to_box:
// op.Box.put(box_key, user.bytes)
frame_dig -1
box_put
frame_dig -1
retsub


Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions examples/struct_in_box/puya.log

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion examples/voting/out/VotingRoundApp.ssa.ir

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 11 additions & 12 deletions examples/voting/out_unoptimized/VotingRoundApp.approval.teal
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,10 @@ store_option_counts:
// @subroutine
// def store_option_counts(self, option_counts: VoteIndexArray) -> None:
proto 1 1
frame_dig -1
// voting/voting.py:219
// assert option_counts.length, "option_counts should be non-empty"
frame_dig -1
dup
intc_0 // 0
extract_uint16
intc_0 // 0
Expand All @@ -310,33 +311,33 @@ store_option_counts:
store_option_counts_for_header@1:
// voting/voting.py:223
// for item in option_counts:
frame_dig 3
frame_dig 2
frame_dig 1
<
bz store_option_counts_after_for@4
frame_dig -1
extract 2 0
frame_dig 2
frame_dig 3
intc_1 // 1
*
intc_1 // 1
extract3 // on error: Index access is out of bounds
// voting/voting.py:224
// total_options += item.native
btoi
frame_dig 0
frame_dig 1
+
frame_bury 0
frame_dig 2
frame_bury 1
frame_dig 3
intc_1 // 1
+
frame_bury 2
frame_bury 3
b store_option_counts_for_header@1

store_option_counts_after_for@4:
// voting/voting.py:225
// assert total_options <= 128, "Can't have more than 128 vote options"
frame_dig 0
frame_dig 1
dup
pushint 128 // 128
<=
Expand All @@ -352,8 +353,6 @@ store_option_counts_after_for@4:
bytec 14 // "total_options"
swap
app_global_put
frame_dig -1
frame_bury 0
retsub


Expand Down Expand Up @@ -808,8 +807,8 @@ close_after_for@14:
bytec 13 // "nft_image_url"
app_global_get_ex
assert // check self.nft_image_url exists
// voting/voting.py:144
// note += "]}}"
// voting/voting.py:153
// note=note,
uncover 2
itxn_field Note
itxn_field ConfigAssetURL
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions examples/voting/puya.log

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions src/puya/awst/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1337,11 +1337,15 @@ def accept(self, visitor: StatementVisitor[T]) -> T:
@attrs.frozen
class BytesAugmentedAssignment(Statement):
target: Lvalue = attrs.field(
validator=[expression_has_wtype(wtypes.bytes_wtype, wtypes.string_wtype)]
validator=[
expression_has_wtype(wtypes.bytes_wtype, wtypes.string_wtype, wtypes.arc4_string_alias)
]
)
op: BytesBinaryOperator
value: Expression = attrs.field(
validator=[expression_has_wtype(wtypes.bytes_wtype, wtypes.string_wtype)]
validator=[
expression_has_wtype(wtypes.bytes_wtype, wtypes.string_wtype, wtypes.arc4_string_alias)
]
)

@value.validator
Expand Down
53 changes: 40 additions & 13 deletions src/puya/awst/validation/arc4_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,53 @@ def visit_tuple_expression(self, expr: awst_nodes.TupleExpression) -> None:
_check_for_arc4_copy(item, "being passed to a tuple expression")

def visit_for_in_loop(self, statement: awst_nodes.ForInLoop) -> None:
# statement.items is immediately checked before entering the for body
# so don't need to worry about preserving _for_items through multiple loops
self._for_items = statement.items
super().visit_for_in_loop(statement)
self._for_items = None

# looping is essentially assigning so check sequence
sequence = statement.sequence
while isinstance(sequence, awst_nodes.Enumeration | awst_nodes.Reversed):
sequence = sequence.expr
if ( # mutable tuples cannot be iterated in a semantically correct way
isinstance(sequence.wtype, wtypes.WTuple)
and _is_referable_expression(sequence)
and _is_arc4_mutable(sequence.wtype)
):
logger.error(
"tuple of mutable ARC4 values cannot be iterated",
location=sequence.source_location,
)
elif ( # arrays of mutable types, must be modified and iterated by index
isinstance(sequence.wtype, wtypes.ARC4Array)
and _is_referable_expression(sequence)
and _is_arc4_mutable(sequence.wtype.element_type)
):
logger.error(
"cannot directly iterate an ARC4 array of mutable objects,"
" construct a for-loop over the indexes instead",
location=sequence.source_location,
)

def visit_assignment_expression(self, expr: awst_nodes.AssignmentExpression) -> None:
_check_assignment(expr.target, expr.value)
expr.value.accept(self)

def visit_subroutine_call_expression(self, expr: awst_nodes.SubroutineCallExpression) -> None:
super().visit_subroutine_call_expression(expr)
for arg in expr.args:
match arg.value:
case awst_nodes.VarExpression():
# Var expressions don't need copy as we implicitly return the latest value and
# update the var
continue
case awst_nodes.AppStateExpression() | awst_nodes.AppAccountStateExpression():
message = "being passed to a subroutine from state"
case _:
message = "being passed to a subroutine"
_check_for_arc4_copy(arg.value, message)
for arg_ in expr.args:
for arg in _expand_tuple_items(arg_.value):
match arg:
case awst_nodes.VarExpression():
# Var expressions don't need copy as we implicitly return the latest value
# and update the var
continue
case awst_nodes.AppStateExpression() | awst_nodes.AppAccountStateExpression():
message = "being passed to a subroutine from state"
case _:
message = "being passed to a subroutine"
_check_for_arc4_copy(arg, message)

def visit_new_array(self, expr: awst_nodes.NewArray) -> None:
super().visit_new_array(expr)
Expand Down Expand Up @@ -131,7 +157,8 @@ def _check_for_arc4_copy(expr: awst_nodes.Expression, context_desc: str) -> None
def _expand_tuple_items(expr: awst_nodes.Expression) -> Iterator[awst_nodes.Expression]:
match expr:
case awst_nodes.TupleExpression(items=items):
yield from items
for item in items:
yield from _expand_tuple_items(item)
case _:
yield expr

Expand Down
Loading
Loading