Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions tests/functional/builtins/codegen/test_empty.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest

from vyper.compiler import compile_code
from vyper.exceptions import ArrayIndexException, InstantiationException, InvalidType, TypeMismatch
from vyper.exceptions import InstantiationException, InvalidType, TypeMismatch


@pytest.mark.parametrize(
Expand Down Expand Up @@ -771,7 +771,7 @@ def test():
def test():
a: Bytes[32] = empty(Bytes[0])
""",
ArrayIndexException,
InvalidType,
),
],
)
Expand Down
18 changes: 17 additions & 1 deletion tests/functional/codegen/types/test_array_indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from vyper import compile_code
from vyper.exceptions import CompilerPanic, TypeCheckFailure
from vyper.exceptions import CompilerPanic, InvalidOperation, TypeCheckFailure


def test_negative_ix_access(get_contract, tx_failed):
Expand Down Expand Up @@ -341,3 +341,19 @@ def foo(i: uint256) -> uint256:
with tx_failed():
c.foo(0)
pytest.xfail("compilation succeeded with correct bytecode, but should have rejected `[][i]`")


@pytest.mark.xfail(raises=InvalidOperation)
def test_index_all_empty_lists_variable_index(get_contract, tx_failed):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

are we planning to support this in future, since the return of the single of DynArray[Never, 0] will be handled correctly?
And also

@external
def foo(i: uint256) -> DynArray[uint256, 5]:
    return [[], [1]][i]

would work as I would expect so what is the blocking this one to compile?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

are we planning to support this in future, since the return of the single of DynArray[Never, 0] will be handled correctly?

I think we should

@external
def foo(i: uint256) -> DynArray[uint256, 5]:
    return [[], [1]][i]

would work as I would expect so what is the blocking this one to compile?

It does work as you would expect (0 -> [], 1 -> [1], 2+ -> reverts)

The issue is that we use the element type's abi to compile even empty lists
With [[], [1]] we infer type SArrayT(DArrayT(uint256, 1), 2) for the whole expression, so [] is compiled as a DArrayT(uint256, 2) (or , 0], doesn't matter)
But with [[], []] we infer the type to SArrayT(DArrayT(BottomT, 0), 2), so we don't know how to compile the [] elements

This can be fixed in two ways:

  1. For nodes of type DArrayT[T, 0], don't fetch the abi-encoding of the elements, just output an empty (discussed in private with @harkal as a possibility)
  2. Annotate nodes with the expected type, and not the infered type, requires/part of Simplify Typer Internals #5017

And I think we should do both (but outside of scope for this PR)

code = """
@external
def foo(i: uint256) -> DynArray[uint256, 5]:
return [[], []][i]
"""

c = get_contract(code)

assert c.foo(0) == []
assert c.foo(1) == []
with tx_failed():
c.foo(2)
84 changes: 84 additions & 0 deletions tests/functional/codegen/types/test_dynamic_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -2382,3 +2382,87 @@ def foo() -> uint256:

c = get_contract(code)
assert c.foo() == 15


def test_zero_length_dynarray_return_arg(get_contract):
code = """
@external
def foo(x: DynArray[uint256, 0]) -> DynArray[uint256, 0]:
return x
"""
c = get_contract(code)
assert c.foo([]) == []


def test_zero_length_dynarray_return_empty_literal(get_contract):
code = """
@external
def foo() -> DynArray[uint256, 0]:
return []
"""
c = get_contract(code)
assert c.foo() == []


def test_zero_length_dynarray_abi_encode(get_contract):
# the encoder elides its per-element loop at bound 0; the encoding must
# be unchanged - a 32 byte offset followed by a length word of 0.
code = """
@external
def foo() -> Bytes[64]:
return abi_encode(empty(DynArray[uint256, 0]))
"""
c = get_contract(code)
assert c.foo() == (32).to_bytes(32, "big") + (0).to_bytes(32, "big")


def test_zero_length_dynarray_in_membership(get_contract):
code = """
@external
def contains(x: DynArray[uint256, 0], needle: uint256) -> bool:
return needle in x

@external
def not_contains(x: DynArray[uint256, 0], needle: uint256) -> bool:
return needle not in x
"""
c = get_contract(code)
assert c.contains([], 5) is False
assert c.not_contains([], 5) is True


def test_zero_length_dynarray_cross_size_copy(get_contract):
code = """
@external
def foo(x: DynArray[uint256, 0]) -> uint256:
y: DynArray[uint256, 5] = x
return len(y)
"""
c = get_contract(code)
assert c.foo([]) == 0


def test_zero_length_dynarray_nested(get_contract):
code = """
@external
def foo(
x: DynArray[DynArray[uint256, 5], 0]
) -> DynArray[DynArray[uint256, 5], 0]:
return x
"""
c = get_contract(code)
assert c.foo([]) == []


def test_zero_length_dynarray_invalid_length_reverts(get_contract, tx_failed):
# ABI encoding with length > 0 for a DynArray[..., 0] argument must
# be rejected by the runtime clamp.
code = """
@external
def foo(x: DynArray[uint256, 0]) -> uint256:
return len(x)
"""
c = get_contract(code)
assert c.foo([]) == 0
with tx_failed():
c.foo([1])
19 changes: 18 additions & 1 deletion tests/functional/syntax/test_bytes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import pytest

from vyper import compiler
from vyper.exceptions import InvalidOperation, StructureException, SyntaxException, TypeMismatch
from vyper.exceptions import (
InvalidOperation,
InvalidType,
StructureException,
SyntaxException,
TypeMismatch,
)

fail_list = [
(
Expand Down Expand Up @@ -166,3 +172,14 @@ def baa():
@pytest.mark.parametrize("good_code", valid_list)
def test_bytes_success(good_code):
assert compiler.compile_code(good_code) is not None


def test_bytes_length_zero():
code = """
@external
def foo():
x: Bytes[0] = b""
"""
with pytest.raises(InvalidType) as excinfo:
compiler.compile_code(code)
assert excinfo.value.message == "Bytes cannot have length of 0"
51 changes: 51 additions & 0 deletions tests/functional/syntax/test_dynamic_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ def foo():
assert excinfo.value.message == "Cannot perform membership comparison between dislike types"


def test_dynarray_negative_length():
code = """
@external
def foo():
x: DynArray[uint256, -1] = []
"""
with pytest.raises(ArrayIndexException) as excinfo:
compile_code(code)
assert excinfo.value.message == "Subscript must be at least 0"


valid_list = [
"""
flag Foo:
Expand Down Expand Up @@ -183,6 +194,16 @@ def foo():
def foo():
x: uint256 = len([])
""",
"""
@external
def foo():
x: DynArray[uint256, 0] = []
""",
"""
@external
def foo():
x: DynArray[uint256, 4] = []
""",
]


Expand Down Expand Up @@ -285,3 +306,33 @@ def foo():
b: DynArray[uint256, INF] = a
"""
compile_inf_code(code)


zero_length_for_loop_list = [
"""
@external
def foo(x: DynArray[uint256, 0]):
for y: uint256 in x:
pass
""",
"""
@external
def foo():
x: DynArray[uint256, 0] = []
for y: uint256 in x:
pass
""",
"""
@external
def foo():
for y: uint256 in []:
pass
""",
]


@pytest.mark.parametrize("bad_code", zero_length_for_loop_list)
def test_zero_length_dynarray_for_loop_rejected(bad_code):
with pytest.raises(StructureException) as e:
compile_code(bad_code)
assert e.value.message == "For loop must have at least 1 iteration"
30 changes: 29 additions & 1 deletion tests/functional/syntax/test_list.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import pytest

from vyper import compiler
from vyper.exceptions import InvalidLiteral, StructureException, TypeMismatch
from vyper.exceptions import (
ArrayIndexException,
InvalidLiteral,
InvalidType,
StructureException,
TypeMismatch,
)

fail_list = [
(
Expand Down Expand Up @@ -315,3 +321,25 @@ def foo():
@pytest.mark.parametrize("good_code", valid_list)
def test_list_success(good_code):
assert compiler.compile_code(good_code) is not None


def test_static_array_length_zero():
code = """
@external
def foo():
x: uint256[0] = []
"""
with pytest.raises(InvalidType) as excinfo:
compiler.compile_code(code)
assert excinfo.value.message == "Static arrays cannot have a length of 0"


def test_static_array_negative_length():
code = """
@external
def foo():
x: uint256[-1] = []
"""
with pytest.raises(ArrayIndexException) as excinfo:
compiler.compile_code(code)
assert excinfo.value.message == "Subscript must be at least 0"
13 changes: 12 additions & 1 deletion tests/functional/syntax/test_string.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest

from vyper import compiler
from vyper.exceptions import InvalidLiteral, StructureException
from vyper.exceptions import InvalidLiteral, InvalidType, StructureException

valid_list = [
"""
Expand Down Expand Up @@ -67,3 +67,14 @@ def foo() -> bool:
def test_string_fail(get_contract, bad_code, exc):
with pytest.raises(exc):
compiler.compile_code(bad_code)


def test_string_length_zero():
code = """
@external
def foo():
x: String[0] = ""
"""
with pytest.raises(InvalidType) as excinfo:
compiler.compile_code(code)
assert excinfo.value.message == "String cannot have length of 0"
25 changes: 25 additions & 0 deletions tests/unit/compiler/ir/test_repeat.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
def test_repeat(get_contract_from_ir, assert_compile_failed):
good_ir = ["repeat", "i", 0, 1, 1, ["seq"]]
good_ir_2 = ["repeat", "i", 0, 0, 0, ["seq"]]
bad_ir_1 = ["repeat", 0, 0, 0, 0, ["seq"]]
bad_ir_2 = ["repeat", 0, 0, -1, -1, ["seq"]]
get_contract_from_ir(good_ir)
get_contract_from_ir(good_ir_2)
assert_compile_failed(lambda: get_contract_from_ir(bad_ir_1), Exception)
assert_compile_failed(lambda: get_contract_from_ir(bad_ir_2), Exception)


def test_repeat_bound_zero_runtime_rounds(get_contract_from_ir, tx_failed):
# bound-0 repeat with non-literal rounds enforces rounds == 0 at runtime
ir = [
"deploy",
0,
["seq", ["repeat", "i", 0, ["calldataload", 4], 0, ["seq"]], ["return", 0, 0], "stop"],
0,
]
abi = [
{
"name": "test",
"outputs": [],
"inputs": [{"type": "uint256", "name": "n"}],
"stateMutability": "nonpayable",
"type": "function",
}
]
c = get_contract_from_ir(ir, abi=abi)
c.test(0)
with tx_failed():
c.test(1)
8 changes: 6 additions & 2 deletions vyper/codegen/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from vyper.semantics.types import (
AddressT,
BoolT,
BottomT,
BytesM_T,
DArrayT,
DecimalT,
Expand Down Expand Up @@ -497,7 +498,7 @@ def append_dyn_array(darray_node, elem_node):

if not is_bounded_length(darray_node.typ.count):
raise CodegenPanic("append not yet implemented for unbounded DynArray")
assert darray_node.typ.count > 0, "jerk boy u r out"
assert darray_node.typ.count >= 0, "jerk boy u r out"

ret = ["seq"]
with darray_node.cache_when_complex("darray") as (b1, darray_node):
Expand Down Expand Up @@ -926,7 +927,10 @@ def check_assign(left, right):
def FAIL(): # pragma: no cover
raise TypeCheckFailure(f"assigning {right.typ} to {left.typ} {left} {right}")

if isinstance(left.typ, _BytestringT):
if isinstance(right.typ, BottomT):
# Universal subtype, can be widened to anything
pass
elif isinstance(left.typ, _BytestringT):
_check_assign_bytes(left, right)
elif is_array_like(left.typ):
_check_assign_list(left, right)
Expand Down
8 changes: 5 additions & 3 deletions vyper/codegen/ir_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ def __init__(
body = self.args[4]

assert (
isinstance(repeat_bound.value, int) and repeat_bound.value > 0
), f"repeat bound must be a compile-time positive integer: {self.args[2]}"
isinstance(repeat_bound.value, int) and repeat_bound.value >= 0
), f"repeat bound must be a compile-time non-negative integer: {repeat_bound}"
assert repeat_count.valency == 1, repeat_count
assert counter_ptr.valency == 1, counter_ptr
assert start.valency == 1, start
Expand All @@ -289,7 +289,9 @@ def __init__(
int_bound = int(repeat_bound.value)
self._gas += int_bound * (body.gas + 50) + 30

if repeat_count != repeat_bound:
if not (
isinstance(repeat_count.value, int) and repeat_count.value <= repeat_bound.value
):
# gas for assert(repeat_count <= repeat_bound)
self._gas += 18

Expand Down
Loading