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

feat(internal): Add support for constant value default args in puya #347

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 8 additions & 5 deletions src/puya/arc32.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from puya import log
from puya.errors import InternalError
from puya.models import (
ABIMethodArgConstantDefault,
ABIMethodArgDefault,
ARC4ABIMethod,
ARC4BareMethod,
ARC4CreateOption,
Expand Down Expand Up @@ -91,26 +93,27 @@ def _get_signature(method: ARC4ABIMethod) -> str:


def _encode_default_arg(
metadata: ContractMetaData, source: str, loc: SourceLocation | None
metadata: ContractMetaData, source: ABIMethodArgDefault, loc: SourceLocation | None
) -> JSONDict:
if state := metadata.global_state.get(source):
if isinstance(source, ABIMethodArgConstantDefault):
return {"source": "constant", "data": source.value}
if state := metadata.global_state.get(source.name):
return {
"source": "global-state",
# TODO: handle non utf-8 bytes
"data": state.key.decode("utf-8"),
}
if state := metadata.local_state.get(source):
if state := metadata.local_state.get(source.name):
return {
"source": "local-state",
"data": state.key.decode("utf-8"),
}
for method in metadata.arc4_methods:
if isinstance(method, ARC4ABIMethod) and method.name == source:
if isinstance(method, ARC4ABIMethod) and method.name == source.name:
return {
"source": "abi-method",
"data": _encode_abi_method(method),
}
# TODO: constants
raise InternalError(f"Cannot find source '{source}' on {metadata.ref}", loc)


Expand Down
26 changes: 25 additions & 1 deletion src/puya/ir/arc4_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
)
from puya.errors import CodeError, InternalError
from puya.models import (
ABIMethodArgConstantDefault,
ARC4ABIMethod,
ARC4ABIMethodConfig,
ARC4BareMethod,
Expand Down Expand Up @@ -480,7 +481,7 @@ def _validate_default_args(
args_by_name = {a.name: a for a in method.args}
for (
parameter_name,
source_name,
default_source,
) in method.arc4_method_config.default_args.items():
# any invalid parameter matches should have been caught earlier
parameter = args_by_name[parameter_name]
Expand All @@ -492,6 +493,17 @@ def _validate_default_args(
case "account":
param_arc4_type = "address"

if isinstance(default_source, ABIMethodArgConstantDefault):
if not _is_valid_client_literal_for_arc4_type(
default_source.value, param_arc4_type
):
logger.warning(
f"'{default_source.value}' is not a valid"
f" default value for parameter '{parameter_name}'"
)
continue

source_name = default_source.name
try:
source = known_sources[source_name]
except KeyError as ex:
Expand Down Expand Up @@ -681,6 +693,18 @@ def _get_abi_signature(subroutine: awst_nodes.ContractMethod, config: ARC4ABIMet
return f"{config.name}({','.join(arg_types)}){return_type}"


def _is_valid_client_literal_for_arc4_type(literal: str | int, arc4_type_alias: str) -> bool:
Copy link
Contributor

Choose a reason for hiding this comment

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

ARC-56 supports any ABI type, and I think ARC-32 did too, but the rules around encoding weren't clear.

So we should probably allow any constant value AVM / ABI value to be used in user code (which also means the default values should be part of AWST?). And then deal with how that value should be encoded into the spec within ARC-32 and ARC-56 code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the ARC32 spec only lists int or str (not even bytes) but it's possible the clients actually support more than this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thats where its ambiguous in ARC-32, once encoded in to ARC-32/ ARC-56 it's only a str or int (i.e. it's encoded form in hex/base64 for ARC4 types), but the actual value in the program can be any high level type.

i.e. this should be valid (once python supports kwarg default values)

class MyStruct(arc4.String, frozen=True):
    foo: arc4.String
    bar: arc4.String

@arc4.abimethod
def foo(
    self, 
    my_struct: MyStruct = MyStruct(arc4.String("foo"), arc4.String("bar")),
) -> None:
    ...

ARC-32

{
    "source": "constant", 
    /* data is UTF-8 encoded bytes for constant (format not defined in ARC-32)*/
    "data": "\\x00\\x04\\x00\\x09\\x03foo\\x03bar"
}

ARC-56 will be something like

{
      "source": "literal",
      /* data is base64 encoded bytes for constant*/
      "data": "AAQACQNmb28DYmFy",
      "type": "(string,string)"
}

if arc4_type_alias.startswith(("uint", "ufixed")):
return isinstance(literal, int)

match arc4_type_alias:
case "byte" | "bool":
return isinstance(literal, int)
case "address" | "string":
return isinstance(literal, str)
return False


def _wtype_to_arc4(wtype: wtypes.WType, loc: SourceLocation | None = None) -> str:
match wtype:
case wtypes.ARC4Type(arc4_name=arc4_name):
Expand Down
15 changes: 14 additions & 1 deletion src/puya/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ class ARC4BareMethodConfig:
create: ARC4CreateOption = ARC4CreateOption.disallow


@attrs.frozen(kw_only=True)
class ABIMethodArgConstantDefault:
value: int | str


@attrs.frozen(kw_only=True)
class ABIMethodArgMemberDefault:
name: str


ABIMethodArgDefault = ABIMethodArgMemberDefault | ABIMethodArgConstantDefault


@attrs.frozen(kw_only=True)
class ARC4ABIMethodConfig:
source_location: SourceLocation
Expand All @@ -77,7 +90,7 @@ class ARC4ABIMethodConfig:
create: ARC4CreateOption = ARC4CreateOption.disallow
name: str
readonly: bool = False
default_args: immutabledict[str, str] = immutabledict()
default_args: immutabledict[str, ABIMethodArgDefault] = immutabledict()
"""Mapping is from parameter -> source"""
structs: immutabledict[str, ARC32StructDef] = immutabledict()

Expand Down
6 changes: 4 additions & 2 deletions src/puyapy/awst_build/arc4_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from puya.awst import wtypes
from puya.errors import CodeError, InternalError
from puya.models import (
ABIMethodArgDefault,
ABIMethodArgMemberDefault,
ARC4ABIMethodConfig,
ARC4BareMethodConfig,
ARC4CreateOption,
Expand Down Expand Up @@ -183,7 +185,7 @@ def get_arc4_abimethod_data(
readonly = default_readonly

# map "default_args" param
default_args = dict[str, str]()
default_args = dict[str, ABIMethodArgDefault]()
match evaluated_args.pop("default_args", {}):
case {**options}:
method_arg_names = func_types.keys() - {"output"}
Expand All @@ -198,7 +200,7 @@ def get_arc4_abimethod_data(
else:
# if it's in method_arg_names, it's a str
assert isinstance(parameter, str)
default_args[parameter] = value
default_args[parameter] = ABIMethodArgMemberDefault(name=value)
case invalid_default_args_option:
context.error(f"invalid default_args option: {invalid_default_args_option}", dec_loc)

Expand Down
Loading