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

Refined ABI type-check on assignment #540

Merged
merged 34 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
13a00ad
initial attempt
ahangsu Sep 22, 2022
6acab46
testcase
ahangsu Sep 22, 2022
9bc879d
changelog
ahangsu Sep 22, 2022
116a88f
new test case and corner case for array casees
ahangsu Sep 30, 2022
b7354f9
update type spec assignable check scheme
ahangsu Oct 3, 2022
bccb799
Merge branch 'master' into refined-abi-assignment-type-check
ahangsu Oct 7, 2022
dfc619e
Add illustrative testing DSL for type_spec_is_assignable_to (#548)
michaeldiamant Oct 11, 2022
4b07317
use Mike's categories to categorize test cases in 3 parts
ahangsu Oct 11, 2022
7d8a76d
minor, unorder declaration
ahangsu Oct 11, 2022
0cd68d4
declare methods on testing testcase coverage
ahangsu Oct 11, 2022
67d5454
minor, testing enforce testcase non empty
ahangsu Oct 11, 2022
08cec0a
skip on abstract case
ahangsu Oct 11, 2022
ca44031
update coverage test on test case for bidirectional
ahangsu Oct 11, 2022
0b17cf4
unsafe bidirectional testcase coverage
ahangsu Oct 11, 2022
bafca69
coverage for safe assignment
ahangsu Oct 11, 2022
178ee21
no overlapping name
ahangsu Oct 11, 2022
d125f30
(Un)safe_bidirectional abstract class not existing boundary check
ahangsu Oct 12, 2022
b1f2511
Uint test cases for unsafe_bidirectional
ahangsu Oct 12, 2022
e4d638c
Bidirectional check over identical typespecs, in unsafe_bidirectional
ahangsu Oct 12, 2022
dada5cf
Add list of type specs to skip in safe assignment
ahangsu Oct 12, 2022
4315dd0
remove duplicates in safe bidirectional, for check is O(n^2) anyways
ahangsu Oct 12, 2022
352b929
chaneg skip list to skip set in safe assignment
ahangsu Oct 12, 2022
3256254
allowing uint8 byte mutual assignable
ahangsu Oct 12, 2022
858b976
Update CHANGELOG.md by comment
ahangsu Oct 13, 2022
312e96f
documentation on behavior of type-assignment-to-check
ahangsu Oct 13, 2022
d12d158
some more mechanism on namedTuple comparison for assignability
ahangsu Oct 13, 2022
4998577
better indentation
ahangsu Oct 13, 2022
570d8aa
only inheritance from NamedTuple can be constructed, no inheritance-i…
ahangsu Oct 13, 2022
a72b606
better guard on NamedTuple inheritance construction
ahangsu Oct 13, 2022
a67153e
use issubclass rather than handwrite algorithm
ahangsu Oct 13, 2022
f51d758
update documentation to explain better
ahangsu Oct 13, 2022
2f3b173
strengthen namedTupleTypeSpec equality, testcases
ahangsu Oct 13, 2022
b8c6ee9
phrasing in `NamedTuple` construction docstring
ahangsu Oct 13, 2022
d9dc827
update changelog
ahangsu Oct 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unreleased

## Added
* Added `abi.type_spec_is_assignable_to` to check for compatible ABI type assignments. ([#540](https://github.com/algorand/pyteal/pull/540))

## Fixed
* Erroring on constructing an odd length hex string. ([#539](https://github.com/algorand/pyteal/pull/539))
* Incorrect behavior when overriding a method name ([#550](https://github.com/algorand/pyteal/pull/550))
Expand Down
2 changes: 2 additions & 0 deletions pyteal/ast/abi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
type_spec_from_annotation,
type_specs_from_signature,
contains_type_spec,
type_spec_is_assignable_to,
)

__all__ = [
Expand Down Expand Up @@ -167,4 +168,5 @@
"algosdk_from_annotation",
"algosdk_from_type_spec",
"contains_type_spec",
"type_spec_is_assignable_to",
]
4 changes: 2 additions & 2 deletions pyteal/ast/abi/reference_type.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Final, TypeVar, cast
from typing import Final, TypeVar, cast
from abc import abstractmethod
from pyteal.ast.abi.type import BaseType, TypeSpec
from pyteal.ast.abi.uint import NUM_BITS_IN_BYTE, uint_decode
Expand Down Expand Up @@ -215,7 +215,7 @@ def params(self) -> AppParamObject:
Application.__module__ = "pyteal.abi"


ReferenceTypeSpecs: Final[List[TypeSpec]] = [
ReferenceTypeSpecs: Final[list[TypeSpec]] = [
AccountTypeSpec(),
AssetTypeSpec(),
ApplicationTypeSpec(),
Expand Down
4 changes: 2 additions & 2 deletions pyteal/ast/abi/transaction.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from typing import Union, cast, List, Final
from typing import Union, cast, Final
from pyteal.ast.abi.type import BaseType, ComputedValue, TypeSpec
from pyteal.ast.expr import Expr
from pyteal.ast.int import Int
Expand Down Expand Up @@ -259,7 +259,7 @@ def __init__(self):

ApplicationCallTransaction.__module__ = "pyteal.abi"

TransactionTypeSpecs: Final[List[TypeSpec]] = [
TransactionTypeSpecs: Final[list[TypeSpec]] = [
TransactionTypeSpec(),
PaymentTransactionTypeSpec(),
KeyRegisterTransactionTypeSpec(),
Expand Down
50 changes: 49 additions & 1 deletion pyteal/ast/abi/tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from collections import OrderedDict

from pyteal.types import TealType
from pyteal.errors import TealInputError
from pyteal.errors import TealInputError, TealInternalError
from pyteal.ast.expr import Expr
from pyteal.ast.seq import Seq
from pyteal.ast.int import Int
Expand Down Expand Up @@ -522,6 +522,13 @@ def __init__(
self.instance_class: type["NamedTuple"] = instance_class
super().__init__(*value_type_specs)

def __eq__(self, other: object) -> bool:
return (
isinstance(other, NamedTupleTypeSpec)
and self.instance_class == other.instance_class
and self.value_type_specs() == other.value_type_specs()
)

def annotation_type(self) -> "type[NamedTuple]":
return self.instance_class

Expand Down Expand Up @@ -552,9 +559,50 @@ class User(abi.NamedTuple):

my_user = User()

.. automethod:: __init_subclass__
.. automethod:: __getattr__
"""

def __init_subclass__(cls) -> None:
"""This method ensures one only constructs directly from `NamedTuple`,
rather than inheriting from `NamedTuple`'s inheritance.

We want to prohibit the following examples:

.. code-block:: python

from pyteal import *

class LegalInheritance(abi.NamedTuple):
a: abi.Field[abi.Uint64]

# following are bad cases we guard against

class IllegalInheritance0(LegalInheritance):
a: abi.Field[abi.Uint64]

class IllegalInheritance1(LegalInheritance, abi.NamedTuple):
a: abi.Field[abi.Uint64]
"""
is_named_tuple_in_bases = False

for base_t in cls.__bases__:
if base_t == NamedTuple:
is_named_tuple_in_bases = True
elif issubclass(base_t, NamedTuple):
raise TealInternalError(
f"Cannot construct {cls} by inheriting {cls.__bases__}. "
f"Must be constructed by direct inheritance from NamedTuple"
)

if not is_named_tuple_in_bases:
raise TealInternalError(
"Unexpected: did not find NamedTuple in __bases__,"
"did not find NamedTuple in __bases__ member's __bases__"
)

super().__init_subclass__()

def __init__(self):
if type(self) is NamedTuple:
raise TealInputError("NamedTuple must be subclassed")
Expand Down
19 changes: 19 additions & 0 deletions pyteal/ast/abi/tuple_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,3 +925,22 @@ def test_NamedTuple_getitem(test_case: type[abi.NamedTuple]):

with pytest.raises(pt.TealInputError):
tuple_value.f0 = abi.Uint64()


def test_NamedTupleTypeSpec():
class Point(abi.NamedTuple):
x: abi.Field[abi.Uint64]
y: abi.Field[abi.Uint64]

class AccountRecord(abi.NamedTuple):
algoBalance: abi.Field[abi.Uint64]
assetBalance: abi.Field[abi.Uint64]

p = Point()
ar = AccountRecord()

assert p.type_spec() == p.type_spec()
assert ar.type_spec() == ar.type_spec()
assert p.type_spec() != ar.type_spec()
assert not abi.type_spec_is_assignable_to(p.type_spec(), ar.type_spec())
assert not abi.type_spec_is_assignable_to(ar.type_spec(), p.type_spec())
94 changes: 94 additions & 0 deletions pyteal/ast/abi/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,97 @@ def type_specs_from_signature(sig: str) -> tuple[list[TypeSpec], Optional[TypeSp
return_type = type_spec_from_algosdk(sdk_method.returns.type)

return [type_spec_from_algosdk(arg.type) for arg in sdk_method.args], return_type


def type_spec_is_assignable_to(a: TypeSpec, b: TypeSpec) -> bool:
"""Decides if the value of type :code:`a` can be assigned to or interpreted as another value of type :code:`b`.

This method return true if and only if all of the following properties hold:

* value of type :code:`a` has identical encoding as value of type :code:`b`
* type :code:`b` is as general as, or more general than type :code:`a`

For `abi.NamedTuple`, we allow mutual assigning between `abi.Tuple` and `abi.NamedTuple`.
But between `abi.NamedTuple`, we only return true when the type specs are identical, or we cannot compare against generality.

Some examples are illustrated as following:

=========================== =========================== ============= ====================================================================
Type :code:`a` Type :code:`b` Assignable? Reason
=========================== =========================== ============= ====================================================================
:code:`DynamicArray[Byte]` :code:`DynamicBytes` :code:`True` :code:`DynamicBytes` is as general as :code:`DynamicArray[Byte]`
:code:`DynamicBytes` :code:`DynamicArray[Byte]` :code:`True` :code:`DynamicArray[Byte]` is as general as :code:`DynamicBytes`
:code:`StaticArray[Byte,N]` :code:`StaticBytes[N]` :code:`True` :code:`StaticBytes[N]` is as general as :code:`StaticArray[Byte,N]`
:code:`StaticBytes[N]` :code:`StaticArray[Byte,N]` :code:`True` :code:`StaticArray[Byte,N]` is as general as :code:`StaticBytes[N]`
:code:`String` :code:`DynamicBytes` :code:`True` :code:`DynamicBytes` is more general than :code:`String`
:code:`DynamicBytes` :code:`String` :code:`False` :code:`String` is more specific than :code:`DynamicBytes`
:code:`Address` :code:`StaticBytes[32]` :code:`True` :code:`StaticBytes[32]` is more general than :code:`Address`
:code:`StaticBytes[32]` :code:`Address` :code:`False` :code:`Address` is more specific than :code:`StaticBytes[32]`
:code:`PaymentTransaction` :code:`Transaction` :code:`True` :code:`Transaction` is more general than :code:`PaymentTransaction`
:code:`Transaction` :code:`PaymentTransaction` :code:`False` :code:`PaymentTransaction` is more specific than :code`Transaction`
:code:`Uint8` :code:`Byte` :code:`True` :code:`Uint8` is as general as :code:`Byte`
:code:`Byte` :code:`Uint8` :code:`True` :code:`Byte` is as general as :code:`Uint8`
=========================== =========================== ============= ====================================================================

Args:
a: The abi.TypeSpec of the value on the right hand side of the assignment.
b: The abi.TypeSpec of the value on the left hand side of the assignment.

Returns:
A boolean result on if type :code:`a` is assignable to type :code:`b`.
"""

from pyteal.ast.abi import (
TupleTypeSpec,
NamedTupleTypeSpec,
ArrayTypeSpec,
StaticArrayTypeSpec,
DynamicArrayTypeSpec,
StringTypeSpec,
AddressTypeSpec,
UintTypeSpec,
)

match a, b:
case NamedTupleTypeSpec(), NamedTupleTypeSpec():
return a == b
case TupleTypeSpec(), TupleTypeSpec():
a, b = cast(TupleTypeSpec, a), cast(TupleTypeSpec, b)
if a.length_static() != b.length_static():
return False
return all(
map(
lambda ab: type_spec_is_assignable_to(ab[0], ab[1]),
zip(a.value_type_specs(), b.value_type_specs()),
)
)
case ArrayTypeSpec(), ArrayTypeSpec():
a, b = cast(ArrayTypeSpec, a), cast(ArrayTypeSpec, b)
if not type_spec_is_assignable_to(a.value_type_spec(), b.value_type_spec()):
return False
match a, b:
case AddressTypeSpec(), StaticArrayTypeSpec():
a, b = cast(AddressTypeSpec, a), cast(StaticArrayTypeSpec, b)
return a.length_static() == b.length_static()
case StaticArrayTypeSpec(), AddressTypeSpec():
return False
case StaticArrayTypeSpec(), StaticArrayTypeSpec():
a, b = cast(StaticArrayTypeSpec, a), cast(StaticArrayTypeSpec, b)
return a.length_static() == b.length_static()
case StringTypeSpec(), DynamicArrayTypeSpec():
return True
case DynamicArrayTypeSpec(), StringTypeSpec():
return False
case DynamicArrayTypeSpec(), DynamicArrayTypeSpec():
return True
return False
case UintTypeSpec(), UintTypeSpec():
a, b = cast(UintTypeSpec, a), cast(UintTypeSpec, b)
return a.size == b.size

if isinstance(a, type(b)):
return True
elif str(a) == str(b):
return True

return False
Loading