Skip to content

Commit

Permalink
Add box ops
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonpaulos committed Dec 21, 2023
1 parent e4bdfbe commit fa81388
Show file tree
Hide file tree
Showing 5 changed files with 358 additions and 184 deletions.
2 changes: 2 additions & 0 deletions pyteal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ __all__ = [
"BoxLen",
"BoxPut",
"BoxReplace",
"BoxResize",
"BoxSplice",
"Break",
"Btoi",
"Bytes",
Expand Down
4 changes: 4 additions & 0 deletions pyteal/ast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
from pyteal.ast.acct import AccountParam, AccountParamObject
from pyteal.ast.box import (
BoxCreate,
BoxResize,
BoxDelete,
BoxExtract,
BoxReplace,
BoxSplice,
BoxLen,
BoxGet,
BoxPut,
Expand Down Expand Up @@ -212,8 +214,10 @@
"BitwiseXor",
"Block",
"BoxCreate",
"BoxResize",
"BoxDelete",
"BoxExtract",
"BoxSplice",
"BoxGet",
"BoxLen",
"BoxPut",
Expand Down
108 changes: 107 additions & 1 deletion pyteal/ast/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,49 @@ def has_return(self):
BoxCreate.__module__ = "pyteal"


class BoxResize(Expr):
"""Resize an existing box.
If the new size is larger than the old size, zero bytes will be added to the end of the box.
If the new size is smaller than the old size, the box will be truncated from the end.
"""

def __init__(self, name: Expr, size: Expr) -> None:
"""
Args:
name: The key used to reference this box. Must evaluate to a bytes.
size: The new number of bytes to reserve for this box. Must evaluate to a uint64.
"""

super().__init__()
require_type(name, TealType.bytes)
require_type(size, TealType.uint64)
self.name = name
self.size = size

def __teal__(self, options: "CompileOptions"):
verifyProgramVersion(
minVersion=Op.box_resize.min_version,
version=options.version,
msg=f"{Op.box_resize} unavailable",
)
return TealBlock.FromOp(
options, TealOp(self, Op.box_resize), self.name, self.size
)

def __str__(self):
return f"(box_resize {self.name} {self.size})"

def type_of(self):
return TealType.none

def has_return(self):
return False


BoxResize.__module__ = "pyteal"


class BoxDelete(Expr):
"""Deletes a box given its name."""

Expand Down Expand Up @@ -83,7 +126,10 @@ def has_return(self):


class BoxReplace(Expr):
"""Replaces bytes in a box given its name, start index, and value."""
"""Replaces bytes in a box given its name, start index, and value.
Also see BoxSplice.
"""

def __init__(self, name: Expr, start: Expr, value: Expr) -> None:
"""
Expand Down Expand Up @@ -165,6 +211,66 @@ def has_return(self):
BoxExtract.__module__ = "pyteal"


class BoxSplice(Expr):
"""Splice content into a box."""

def __init__(
self, name: Expr, start: Expr, length: Expr, new_content: Expr
) -> None:
"""
Replaces the range of bytes from `start` through `start + length` with `new_content`.
Recall that boxes are constant length, so this operation will not change the length of the
box. Instead content may be adjusted as so:
* If the length of the new content is less than `length`, zero bytes will be added to the
end of the box to make up the difference.
* If the length of the new content is greater than `length`, bytes will be truncated from
the end of the box to make up the difference.
Args:
name: The name of the box to modify. Must evaluate to bytes.
start: The byte index into the box to start writing. Must evaluate to uint64.
length: The length of the bytes to be replaced. Must evaluate to uint64.
new_content: The new content to write into the box. Must evaluate to bytes.
"""
super().__init__()
require_type(name, TealType.bytes)
require_type(start, TealType.uint64)
require_type(length, TealType.uint64)
require_type(new_content, TealType.bytes)
self.name = name
self.start = start
self.length = length
self.new_content = new_content

def __teal__(self, options: "CompileOptions"):
verifyProgramVersion(
minVersion=Op.box_splice.min_version,
version=options.version,
msg=f"{Op.box_splice} unavailable",
)
return TealBlock.FromOp(
options,
TealOp(self, Op.box_splice),
self.name,
self.start,
self.length,
self.new_content,
)

def __str__(self):
return f"(box_splice {self.name} {self.start} {self.length} {self.new_content})"

def type_of(self):
return TealType.none

def has_return(self):
return False


BoxSplice.__module__ = "pyteal"


def BoxLen(name: Expr) -> MaybeValue:
"""
Get the byte length of the box specified by its name.
Expand Down
80 changes: 67 additions & 13 deletions pyteal/ast/box_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,38 @@

avm7Options = pt.CompileOptions(version=7)
avm8Options = pt.CompileOptions(version=8)
avm10Options = pt.CompileOptions(version=10)

POSITIVE_TEST_CASES: list[Tuple[pt.Expr, pt.TealType]] = [
(pt.BoxCreate(pt.Bytes("box"), pt.Int(10)), pt.TealType.uint64),
(pt.BoxDelete(pt.Bytes("box")), pt.TealType.uint64),
(pt.BoxExtract(pt.Bytes("box"), pt.Int(2), pt.Int(4)), pt.TealType.bytes),
POSITIVE_TEST_CASES: list[Tuple[int, pt.Expr, pt.TealType]] = [
(8, pt.BoxCreate(pt.Bytes("box"), pt.Int(10)), pt.TealType.uint64),
(8, pt.BoxDelete(pt.Bytes("box")), pt.TealType.uint64),
(8, pt.BoxExtract(pt.Bytes("box"), pt.Int(2), pt.Int(4)), pt.TealType.bytes),
(
8,
pt.BoxReplace(pt.Bytes("box"), pt.Int(3), pt.Bytes("replace")),
pt.TealType.none,
),
(pt.BoxLen(pt.Bytes("box")), pt.TealType.none),
(pt.BoxGet(pt.Bytes("box")), pt.TealType.none),
(pt.BoxPut(pt.Bytes("box"), pt.Bytes("goonery")), pt.TealType.none),
(8, pt.BoxLen(pt.Bytes("box")), pt.TealType.none),
(8, pt.BoxGet(pt.Bytes("box")), pt.TealType.none),
(8, pt.BoxPut(pt.Bytes("box"), pt.Bytes("goonery")), pt.TealType.none),
(10, pt.BoxResize(pt.Bytes("box"), pt.Int(16)), pt.TealType.none),
(
10,
pt.BoxSplice(pt.Bytes("box"), pt.Int(5), pt.Int(2), pt.Bytes("replacement")),
pt.TealType.none,
),
]


@pytest.mark.parametrize("test_case, test_case_type", POSITIVE_TEST_CASES)
def test_compile_version_and_type(test_case, test_case_type):
@pytest.mark.parametrize("version, test_case, test_case_type", POSITIVE_TEST_CASES)
def test_compile_version_and_type(version, test_case, test_case_type):
with pytest.raises(pt.TealInputError):
test_case.__teal__(avm7Options)
test_case.__teal__(pt.CompileOptions(version=version - 1))

test_case.__teal__(avm8Options)
test_case.__teal__(pt.CompileOptions(version=version))
assert test_case.type_of() == test_case_type
assert not test_case.has_return()

return


INVALID_TEST_CASES: list[Tuple[list[pt.Expr], type | Callable[..., pt.MaybeValue]]] = [
([pt.Bytes("box"), pt.Bytes("ten")], pt.BoxCreate),
Expand All @@ -41,6 +47,12 @@ def test_compile_version_and_type(test_case, test_case_type):
([pt.Int(12)], pt.BoxLen),
([pt.Int(45)], pt.BoxGet),
([pt.Bytes("box"), pt.Int(123)], pt.BoxPut),
([pt.Int(1), pt.Int(2)], pt.BoxResize),
([pt.Bytes("box"), pt.Bytes("b")], pt.BoxResize),
([pt.Bytes("box"), pt.Int(123), pt.Int(456), pt.Int(7)], pt.BoxSplice),
([pt.Bytes("box"), pt.Int(123), pt.Bytes("456"), pt.Bytes("x")], pt.BoxSplice),
([pt.Bytes("box"), pt.Bytes("123"), pt.Int(456), pt.Bytes("x")], pt.BoxSplice),
([pt.Int(8), pt.Int(123), pt.Int(456), pt.Bytes("x")], pt.BoxSplice),
]


Expand Down Expand Up @@ -69,6 +81,25 @@ def test_box_create_compile():
assert expected == actual


def test_box_resize_compile():
name_arg: pt.Expr = pt.Bytes("eineName")
size_arg: pt.Expr = pt.Int(10)
expr: pt.Expr = pt.BoxResize(name_arg, size_arg)

expected = pt.TealSimpleBlock(
[
pt.TealOp(name_arg, pt.Op.byte, '"eineName"'),
pt.TealOp(size_arg, pt.Op.int, 10),
pt.TealOp(expr, pt.Op.box_resize),
]
)
actual, _ = expr.__teal__(avm10Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

assert expected == actual


def test_box_delete_compile():
name_arg: pt.Expr = pt.Bytes("eineName")
expr: pt.Expr = pt.BoxDelete(name_arg)
Expand Down Expand Up @@ -125,6 +156,29 @@ def test_box_replace():
assert expected == actual


def test_box_splice():
name_arg: pt.Expr = pt.Bytes("eineName")
srt_arg: pt.Expr = pt.Int(10)
len_arg: pt.Expr = pt.Int(17)
replace_arg: pt.Expr = pt.Bytes("replace-str")
expr: pt.Expr = pt.BoxSplice(name_arg, srt_arg, len_arg, replace_arg)

expected = pt.TealSimpleBlock(
[
pt.TealOp(name_arg, pt.Op.byte, '"eineName"'),
pt.TealOp(srt_arg, pt.Op.int, 10),
pt.TealOp(len_arg, pt.Op.int, 17),
pt.TealOp(replace_arg, pt.Op.byte, '"replace-str"'),
pt.TealOp(expr, pt.Op.box_splice),
]
)
actual, _ = expr.__teal__(avm10Options)
actual.addIncoming()
actual = pt.TealBlock.NormalizeBlocks(actual)

assert expected == actual


def test_box_length():
name_arg: pt.Expr = pt.Bytes("eineName")
expr: pt.MaybeValue = pt.BoxLen(name_arg)
Expand Down
Loading

0 comments on commit fa81388

Please sign in to comment.