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

Add Replace #413

Merged
merged 4 commits into from
Jun 30, 2022
Merged
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
1 change: 1 addition & 0 deletions pyteal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ __all__ = [
"Or",
"Pop",
"Reject",
"Replace",
Copy link
Contributor

Choose a reason for hiding this comment

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

@jdtzmn Unrelated to line: While reviewing the PR, I added a couple of tests in jdtzmn#1.

Can you see if you feel it's worth pulling into the PR? Otherwise, we can close the child PR.

"Return",
"ScratchIndex",
"ScratchLoad",
Expand Down
2 changes: 2 additions & 0 deletions pyteal/ast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
# ternary ops
from pyteal.ast.ternaryexpr import Divw, Ed25519Verify, SetBit, SetByte
from pyteal.ast.substring import Substring, Extract, Suffix
from pyteal.ast.replace import Replace

# more ops
from pyteal.ast.naryexpr import NaryExpr, And, Or, Concat
Expand Down Expand Up @@ -272,6 +273,7 @@
"ExtractUint16",
"ExtractUint32",
"ExtractUint64",
"Replace",
"Log",
"While",
"For",
Expand Down
86 changes: 86 additions & 0 deletions pyteal/ast/replace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import cast, TYPE_CHECKING

from pyteal.types import TealType, require_type
from pyteal.errors import verifyTealVersion
from pyteal.ir import TealOp, Op, TealBlock
from pyteal.ast.expr import Expr
from pyteal.ast.int import Int
from pyteal.ast.ternaryexpr import TernaryExpr

if TYPE_CHECKING:
from pyteal.compiler import CompileOptions


class ReplaceExpr(Expr):
"""An expression for replacing a section of a byte string at a given start index"""

def __init__(self, original: Expr, start: Expr, replacement: Expr) -> None:
super().__init__()

require_type(original, TealType.bytes)
require_type(start, TealType.uint64)
require_type(replacement, TealType.bytes)

self.original = original
self.start = start
self.replacement = replacement

# helper method for correctly populating op
def __get_op(self, options: "CompileOptions"):
s = cast(Int, self.start).value
if s < 2**8:
return Op.replace2
else:
return Op.replace3

def __teal__(self, options: "CompileOptions"):
if not isinstance(self.start, Int):
return TernaryExpr(
Op.replace3,
(TealType.bytes, TealType.uint64, TealType.bytes),
TealType.bytes,
self.original,
self.start,
self.replacement,
).__teal__(options)

op = self.__get_op(options)

verifyTealVersion(
op.min_version,
options.version,
"TEAL version too low to use op {}".format(op),
)

s = cast(Int, self.start).value
if op == Op.replace2:
return TealBlock.FromOp(
options, TealOp(self, op, s), self.original, self.replacement
)
elif op == Op.replace3:
return TealBlock.FromOp(
options, TealOp(self, op), self.original, self.start, self.replacement
)

def __str__(self):
return "(Replace {} {} {})".format(self.original, self.start, self.replacement)

def type_of(self):
return TealType.bytes

def has_return(self):
return False


def Replace(original: Expr, start: Expr, replacement: Expr) -> Expr:
"""
Replace a portion of original bytes with new bytes at a given starting point.

Requires TEAL version 7 or higher.

Args:
original: The value containing the original bytes. Must evaluate to bytes.
start: The index of the byte where replacement starts. Must evaluate to an integer less than Len(original).
replacement: The value containing the replacement bytes. Must evaluate to bytes with length at most Len(original) - start.
"""
return ReplaceExpr(original, start, replacement)
97 changes: 97 additions & 0 deletions pyteal/ast/replace_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import pytest

import pyteal as pt

teal6Options = pt.CompileOptions(version=6)
teal7Options = pt.CompileOptions(version=7)


def test_replace_immediate():
args = [pt.Bytes("my string"), pt.Int(0), pt.Bytes("abcdefghi")]
expr = pt.Replace(args[0], args[1], args[2])
assert expr.type_of() == pt.TealType.bytes

expected = pt.TealSimpleBlock(
[
pt.TealOp(args[0], pt.Op.byte, '"my string"'),
pt.TealOp(args[2], pt.Op.byte, '"abcdefghi"'),
pt.TealOp(expr, pt.Op.replace2, 0),
]
)

actual, _ = expr.__teal__(teal7Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

assert actual == expected

with pytest.raises(pt.TealInputError):
expr.__teal__(teal6Options)


def test_replace_stack_int():
my_string = "*" * 257
args = [pt.Bytes(my_string), pt.Int(256), pt.Bytes("ab")]
expr = pt.Replace(args[0], args[1], args[2])
assert expr.type_of() == pt.TealType.bytes

expected = pt.TealSimpleBlock(
[
pt.TealOp(args[0], pt.Op.byte, '"{my_string}"'.format(my_string=my_string)),
pt.TealOp(args[1], pt.Op.int, 256),
pt.TealOp(args[2], pt.Op.byte, '"ab"'),
pt.TealOp(expr, pt.Op.replace3),
]
)

actual, _ = expr.__teal__(teal7Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

assert actual == expected

with pytest.raises(pt.TealInputError):
expr.__teal__(teal6Options)


# Mirrors `test_replace_stack_int`, but attempts replacement with start != pt.Int.
def test_replace_stack_not_int():
my_string = "*" * 257
add = pt.Add(pt.Int(254), pt.Int(2))
args = [pt.Bytes(my_string), add, pt.Bytes("ab")]
expr = pt.Replace(args[0], args[1], args[2])
assert expr.type_of() == pt.TealType.bytes

expected = pt.TealSimpleBlock(
[
pt.TealOp(args[0], pt.Op.byte, '"{my_string}"'.format(my_string=my_string)),
pt.TealOp(pt.Int(254), pt.Op.int, 254),
pt.TealOp(pt.Int(2), pt.Op.int, 2),
pt.TealOp(add, pt.Op.add),
pt.TealOp(args[2], pt.Op.byte, '"ab"'),
pt.TealOp(expr, pt.Op.replace3),
]
)

actual, _ = expr.__teal__(teal7Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

with pt.TealComponent.Context.ignoreExprEquality():
assert actual == expected

with pytest.raises(pt.TealInputError):
expr.__teal__(teal6Options)


def test_replace_invalid():
with pytest.raises(pt.TealTypeError):
pt.Replace(pt.Bytes("my string"), pt.Int(0), pt.Int(1))

with pytest.raises(pt.TealTypeError):
pt.Replace(
pt.Bytes("my string"), pt.Bytes("should be int"), pt.Bytes("abcdefghi")
)

with pytest.raises(pt.TealTypeError):
pt.Replace(pt.Bytes("my string"), pt.Txn.sender(), pt.Bytes("abcdefghi"))
12 changes: 6 additions & 6 deletions pyteal/ast/substring.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def __init__(self, stringArg: Expr, startArg: Expr, endArg: Expr) -> None:
self.endArg = endArg

# helper method for correctly populating op
def __getOp(self, options: "CompileOptions"):
def __get_op(self, options: "CompileOptions"):
s, e = cast(Int, self.startArg).value, cast(Int, self.endArg).value
l = e - s

Expand Down Expand Up @@ -58,7 +58,7 @@ def __teal__(self, options: "CompileOptions"):
self.endArg,
).__teal__(options)

op = self.__getOp(options)
op = self.__get_op(options)

verifyTealVersion(
op.min_version,
Expand Down Expand Up @@ -121,7 +121,7 @@ def __init__(self, stringArg: Expr, startArg: Expr, lenArg: Expr) -> None:
self.lenArg = lenArg

# helper method for correctly populating op
def __getOp(self, options: "CompileOptions"):
def __get_op(self, options: "CompileOptions"):
s, l = cast(Int, self.startArg).value, cast(Int, self.lenArg).value
if s < 2**8 and l > 0 and l < 2**8:
return Op.extract
Expand All @@ -139,7 +139,7 @@ def __teal__(self, options: "CompileOptions"):
self.lenArg,
).__teal__(options)

op = self.__getOp(options)
op = self.__get_op(options)

verifyTealVersion(
op.min_version,
Expand Down Expand Up @@ -186,7 +186,7 @@ def __init__(
self.startArg = startArg

# helper method for correctly populating op
def __getOp(self, options: "CompileOptions"):
def __get_op(self, options: "CompileOptions"):
if not isinstance(self.startArg, Int):
return Op.substring3

Expand All @@ -197,7 +197,7 @@ def __getOp(self, options: "CompileOptions"):
return Op.substring3

def __teal__(self, options: "CompileOptions"):
op = self.__getOp(options)
op = self.__get_op(options)

verifyTealVersion(
op.min_version,
Expand Down
82 changes: 82 additions & 0 deletions pyteal/ast/substring_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,88 @@ def test_suffix_stack():
assert actual == expected


@pytest.mark.parametrize("op", [pt.Op.extract3, pt.Op.substring3])
def test_startArg_not_int(op: pt.Op):
my_string = "*" * 257
add = pt.Add(pt.Int(254), pt.Int(2))
args = [pt.Bytes(my_string), add, pt.Int(257)]

def generate_expr() -> pt.Expr:
match op:
case pt.Op.extract3:
return pt.Extract(args[0], args[1], args[2])
case pt.Op.substring3:
return pt.Substring(args[0], args[1], args[2])
case _:
raise Exception(f"Unsupported {op=}")

expr = generate_expr()
assert expr.type_of() == pt.TealType.bytes

expected = pt.TealSimpleBlock(
[
pt.TealOp(args[0], pt.Op.byte, '"{my_string}"'.format(my_string=my_string)),
pt.TealOp(pt.Int(254), pt.Op.int, 254),
pt.TealOp(pt.Int(2), pt.Op.int, 2),
pt.TealOp(add, pt.Op.add),
pt.TealOp(args[2], pt.Op.int, 257),
pt.TealOp(None, op),
]
)

actual, _ = expr.__teal__(teal5Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

with pt.TealComponent.Context.ignoreExprEquality():
assert actual == expected

if op == pt.Op.extract3:
with pytest.raises(pt.TealInputError):
expr.__teal__(teal4Options)


@pytest.mark.parametrize("op", [pt.Op.extract3, pt.Op.substring3])
def test_endArg_not_int(op: pt.Op):
my_string = "*" * 257
add = pt.Add(pt.Int(254), pt.Int(3))
args = [pt.Bytes(my_string), pt.Int(256), add]

def generate_expr() -> pt.Expr:
match op:
case pt.Op.extract3:
return pt.Extract(args[0], args[1], args[2])
case pt.Op.substring3:
return pt.Substring(args[0], args[1], args[2])
case _:
raise Exception(f"Unsupported {op=}")

expr = generate_expr()
assert expr.type_of() == pt.TealType.bytes

expected = pt.TealSimpleBlock(
[
pt.TealOp(args[0], pt.Op.byte, '"{my_string}"'.format(my_string=my_string)),
pt.TealOp(args[1], pt.Op.int, 256),
pt.TealOp(pt.Int(254), pt.Op.int, 254),
pt.TealOp(pt.Int(3), pt.Op.int, 3),
pt.TealOp(add, pt.Op.add),
pt.TealOp(None, op),
]
)

actual, _ = expr.__teal__(teal5Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

with pt.TealComponent.Context.ignoreExprEquality():
assert actual == expected

if op == pt.Op.extract3:
with pytest.raises(pt.TealInputError):
expr.__teal__(teal4Options)


def test_suffix_invalid():
with pytest.raises(pt.TealTypeError):
pt.Suffix(pt.Int(0), pt.Int(0))
Expand Down
2 changes: 2 additions & 0 deletions pyteal/ir/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ def min_version(self) -> int:
gitxnas = OpType("gitxnas", Mode.Application, 6)
gloadss = OpType("gloadss", Mode.Application, 6)
acct_params_get = OpType("acct_params_get", Mode.Application, 6)
replace2 = OpType("replace2", Mode.Signature | Mode.Application, 7)
replace3 = OpType("replace3", Mode.Signature | Mode.Application, 7)
# fmt: on


Expand Down