Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
24 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
12 changes: 12 additions & 0 deletions tests/functional/codegen/types/test_dynamic_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,18 @@ def uoo(inp: DynArray[Foobar, 2]) -> DynArray[DynArray[Foobar, 2], 2]:
print("Passed list output tests")


def test_nested_dynarray_empty_and_flag_literal(get_contract):
code = """
flag Foo:
Member1

@external
def foo():
tmp: DynArray[DynArray[Foo, 5], 5] = [[], [Foo.Member1]]
"""
get_contract(code)


def test_array_accessor(get_contract):
array_accessor = """
@external
Expand Down
27 changes: 26 additions & 1 deletion tests/functional/syntax/test_dynamic_array.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import pytest

from vyper import compile_code
from vyper.exceptions import CodegenPanic, StructureException, TypeMismatch, UndeclaredDefinition
from vyper.exceptions import (
CodegenPanic,
CompilerPanic,
StructureException,
TypeMismatch,
UndeclaredDefinition,
)

fail_list = [
(
Expand Down Expand Up @@ -64,6 +70,15 @@ def foo(x: DynArray[uint256, INF]) -> DynArray[uint256, 5]:
""",
TypeMismatch,
),
pytest.param(
"""
@external
def foo():
x: uint256 = [].pop()
""",
StructureException,
marks=pytest.mark.xfail(raises=CompilerPanic),
Comment thread
Sporarum marked this conversation as resolved.
),
]


Expand Down Expand Up @@ -112,6 +127,16 @@ def bar() -> DynArray[uint256, INF]: nonpayable
interface IFoo:
def bar() -> DynArray[uint256, ...]: nonpayable
""", # DynArray with wildcard in interface return type
"""
@external
def foo():
tmp: DynArray[Bytes[3], 1] = [[b"abc"], []][0]
""",
"""
@external
def foo():
tmp: DynArray[Bytes[3], 1] = [[], [b"abc"]][1]
""",
]


Expand Down
40 changes: 40 additions & 0 deletions tests/functional/syntax/test_nested_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ def foo(x: int128[2][2]) -> int128:
""",
TypeMismatch,
),
(
"""
bar: int128[2][2]
@external
def foo():
self.bar = [[], [1, 2]]
""",
TypeMismatch,
),
]


Expand All @@ -58,6 +67,22 @@ def test_nested_list_fail(bad_code, exc):
compiler.compile_code(bad_code)


def test_nested_dynarray_mismatched_inner_bytes():
code = """
@external
def foo():
tmp1: DynArray[Bytes[3], 5] = []
tmp2: DynArray[Bytes[5], 5] = []
tmp: DynArray[DynArray[Bytes[3], 5], 5] = [tmp1, tmp2]
"""
with pytest.raises(TypeMismatch) as excinfo:
compiler.compile_code(code)
assert excinfo.value.message == (
"Expected DynArray[DynArray[Bytes[3], 5], 5] but literal can only be cast as"
" DynArray[Bytes[5], 5][2] or DynArray[DynArray[Bytes[5], 5], 2]."
)


valid_list = [
"""
bar: int128[3][3]
Expand All @@ -66,11 +91,26 @@ def foo():
self.bar = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
""",
"""
bar: int128[3][3][3]
@external
def foo():
self.bar = [
[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
]
""",
"""
bar: decimal[3][3]
@external
def foo():
self.bar = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]
""",
"""
@external
def foo():
tmp: uint256 = [[], [1]][1][0]
""",
]


Expand Down
14 changes: 2 additions & 12 deletions vyper/semantics/analysis/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
StructT,
TupleT,
VyperType,
_BytestringT,
is_type_t,
map_void,
)
Expand Down Expand Up @@ -1087,18 +1086,9 @@ def visit_Compare(self, node: vy_ast.Compare, typ: VyperType) -> None:
else:
# ex. a < b
cmp_typ = get_common_types(node.left, node.right).pop()
if isinstance(cmp_typ, _BytestringT):
# for bytestrings, get_common_types automatically downcasts
# to the smaller common type - that will annotate with the
# wrong type, instead use get_exact_type_from_node (which
# resolves to the right type for bytestrings anyways).
ltyp = get_exact_type_from_node(node.left)
rtyp = get_exact_type_from_node(node.right)
else:
ltyp = rtyp = cmp_typ

self.visit(node.left, ltyp)
self.visit(node.right, rtyp)
self.visit(node.left, cmp_typ)
self.visit(node.right, cmp_typ)

def visit_Constant(self, node: vy_ast.Constant, typ: VyperType) -> None:
pass
Expand Down
62 changes: 14 additions & 48 deletions vyper/semantics/analysis/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from vyper.semantics.analysis.base import ExprInfo, Modifiability, ModuleInfo, VarAccess, VarInfo
from vyper.semantics.analysis.levenshtein_utils import get_levenshtein_error_suggestions
from vyper.semantics.namespace import get_namespace
from vyper.semantics.types.base import TYPE_T, VyperType
from vyper.semantics.types.base import TYPE_T, BottomT, VyperType
from vyper.semantics.types.bytestrings import BytesT, StringT
from vyper.semantics.types.infinity import is_bounded_length

Expand Down Expand Up @@ -349,28 +349,10 @@ def types_from_IfExp(self, node):

def types_from_List(self, node):
# literal array
if _is_empty_list(node):
ret = []

if len(node.elements) > 0:
# empty nested list literals `[[], []]`
subtypes = self.get_possible_types_from_node(node.elements[0])
else:
# empty list literal `[]`
# subtype can be anything
subtypes = types.PRIMITIVE_TYPES.values()

for t in subtypes:
# 1 is minimum possible length for dynarray,
# can be assigned to anything
if isinstance(t, VyperType):
ret.append(DArrayT(t, 1))
elif isinstance(t, type) and issubclass(t, VyperType):
# for typeclasses like bytestrings, use a generic type acceptor
ret.append(DArrayT(t.any(), 1))
else:
raise CompilerPanic(f"busted type {t}", node)
return ret
if len(node.elements) == 0:
# can't have an empty SArrayT
return [DArrayT(BottomT(), 1)]
Comment on lines +353 to +355

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.

#5175 (comment)

P2 Badge Reject member calls on empty list literals

Because [] now has one exact dynamic-array type, invalid code such as x: uint256 = [].pop() can resolve the pop member and its Never return type type-checks as uint256; the function visitor then treats the list literal as the modified target and hits the _handle_modification() assertion because literals have no VarAccess. Before this change the same member lookup was rejected as an ambiguous empty-list type, so this should be diagnosed instead of reaching an internal compiler assertion.

@Sporarum Sporarum Jul 1, 2026

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.

def foo():
    tmp: uint256 = [empty(uint256)].pop() # StructureException: Ambiguous type

Where the ambiguity is between DynArray[uint256, 1] and uint256[1].

So there are two ways of solving this:

  1. Implementing some sort of l-value system, required for Assign to call-return index crashes compiler #5138 and Writing self.balance panics under Venom codegen #5151 anyways
  2. Making [] also a Never[0]

Both sounds like they would be useful, but 1. solves a bigger and more serious issue.
I added an xfail test for this.

Comment on lines +353 to +355

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.

#5175 (comment)

P2 Badge Reject untyped empty list varargs before ABI typing

For vararg builtins that require an exact argument type, such as abi_encode([]), this makes the empty list exact as DynArray[Never, 1] instead of ambiguous. ABIEncode.fetch_call_return() then calls arg_t.abi_type on that inferred argument, which descends into BottomT.abi_type and raises a compiler panic; the untyped empty list should still be rejected with a normal diagnostic unless an expected element type is available.

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.

Added an error message instead

Comment on lines +353 to +355

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve expected types for all-empty subscripted arrays

Because every empty literal is materialized as DynArray[Never, 1], an all-empty nested expression selected through another expression keeps that bottom element type even after contextual validation accepts it. For example, tmp: DynArray[Foo, 5] = [[], []][0] type-checks as an empty array of Foo, but parse_Subscript still returns an IR node typed as DynArray[Never, 1], so make_setter recursively rejects the Never element against the struct element type during codegen instead of compiling the empty array.

Useful? React with 👍 / 👎.

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.

@codex I wasn't able to replicate, please go more into details

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@codex I wasn't able to replicate, please go more into details

(this time connected codex with github, let's see if it works)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@codex I wasn't able to replicate, please go more into details

(the link tells me it's connected, but third time's a charm I guess)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comment thread
Sporarum marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Materialize all-empty conditional arrays

When both arms of a conditional expression are empty lists and the contextual element type is not one of the old primitive candidates, this new bottom-typed [] makes the expression type-check, e.g. tmp: DynArray[Foo, 1] = [] if c else []. The visitor annotates each arm as the expected array, but codegen builds an if over two ~empty non-pointers and assignment then raises CompilerPanic: cannot dereference non-pointer type instead of compiling or giving a type error; either keep these untyped conditionals rejected or force the empty branches into a concrete memory array before codegen.

Useful? React with 👍 / 👎.

@Sporarum Sporarum Jul 22, 2026

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.

Unrelated to this change, see #5199

Comment thread
Sporarum marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject indexing into bottom-typed empty lists

When [] is modeled as DynArray[Never, 1], types_from_Subscript considers [][0] in bounds and returns Never; since Never is accepted as a subtype of every type, code such as struct Foo: ...; x: Foo = [][0] now passes semantic analysis even though the parent had no Foo candidate for an empty list. Codegen then sees the ~empty array and raises the internal TypeCheckFailure("indexing into zero array not allowed"), so this should be rejected during semantic analysis rather than represented with a fake length that permits index 0.

Useful? React with 👍 / 👎.

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.

This should instead be raised during constant folding, since the types do match
This would be done after changing the type of list literals such that [1, 2, 3][0] is allowed

For now while TypeCheckFailure is an internal error, the error message seem explicit enough that this is acceptable
Added an xfail test

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject empty constructor varargs before memory copying

Because bare [] now has a single exact DynArray[Never, 1] type, vararg builtins that do not have the new abi_encode/print ABI-type guard still accept it during semantic analysis. For example, raw_create(code, []) and create_from_blueprint(target, []) reach ensure_in_memory, which tries to copy a DynArray[Never, 1] and raises an internal TypeCheckFailure while assigning Never to Never; these untyped constructor arguments should be rejected before codegen.

Useful? React with 👍 / 👎.

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.

Done: a08ef05


types_list = get_common_types(*node.elements)

Expand Down Expand Up @@ -432,18 +414,6 @@ def types_from_UnaryOp(self, node):
return _validate_op(node, types_list, "validate_numeric_op")


def _is_empty_list(node):
# Checks if a node is a `List` node with an empty list for `elements`,
# including any nested `List` nodes. ex. `[]` or `[[]]` will return True,
# [1] will return False.
if not isinstance(node, vy_ast.List):
return False

if not node.elements:
return True
return all(_is_empty_list(t) for t in node.elements)


def _is_type_in_list(obj, types_list):
# check if a type object is in a list of types
return any(i.is_equivalent_to(obj) for i in types_list)
Expand Down Expand Up @@ -528,12 +498,16 @@ def get_common_types(*nodes: vy_ast.VyperNode, filter_fn: Callable = None) -> Li
tmp = []
for c in common_types:
for t in new_types:
# TODO: This can add either the supertype or the subtype to tmp depending on
# the order
if t.compare_type(c) or c.compare_type(t):
# The common type is the one such that both are subtypes

if t.is_subtype_of(c):
tmp.append(c)
break

if c.is_subtype_of(t):
tmp.append(t)
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve contextual integer types after empty sublists

When an empty sublist is the first element, common_types starts as DynArray[Never, 1]; this branch appends only the first later type that can contain it and then breaks. For a literal like [[], [1]], the first matching dynamic-array type is the default positive-integer choice (e.g. DynArray[uint256, 1]), so other valid contextual choices are discarded and assignments such as tmp: DynArray[DynArray[int128, 1], 2] = [[], [1]] are rejected depending only on element order ([[1], []] still keeps the int128 alternative).

Useful? React with 👍 / 👎.

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.

This was confusingly worded, but I think what this means is: when we have [a, b], for each type of a we loop over all types of b, and we break at the first match, thus shadowing potential further matches.
Before since [] had a type for each builtin type, this "absorbtion" was never noticed, there could never be 3 types among the types of a and b that such that a V could form.
In what codex says, that would be Never for a and uint256 and int128 for b.
The loop would break at Never <: uint256, and never add int128 to the list.

I fixed it by not breaking on a match.
This can lead to an explosion in the number of types of the list, but only if one of the elements has at least 2 types such that one is a subtype of the other:

[_: {Bytes[1], Bytes[2]}, _: {Bytes[3], Bytes[4]}]
# would be inferred to have type:
{
  Bytes[3], # Bytes[1] <: Bytes[3]
  Bytes[4], # Bytes[1] <: Bytes[4]
  Bytes[3], # Bytes[2] <: Bytes[3]
  Bytes[4], # Bytes[2] <: Bytes[4]
}

I was only able to find one kind of expression with redundant types like this: [<expr>][0]: {<types of expr>, <types of expr>}. Due to [<expr>] having a DArrayT and SArrayT with the same element type.
Given how contrived this is, and that the only impact is time/space used for combination, this is fine for now.
But we should avoiding looping entirely, by having a single type per expression (see #5017).


common_types = tmp

if filter_fn is not None:
Expand Down Expand Up @@ -598,17 +572,9 @@ def validate_expected_type(node, expected_type):

given_types = _ExprAnalyser().get_possible_types_from_node(node)

if isinstance(node, vy_ast.List):
# special case - for literal arrays we individually validate each item
for expected in expected_type:
if not isinstance(expected, (DArrayT, SArrayT)):
continue
if _validate_literal_array(node, expected):
return
else:
for given, expected in itertools.product(given_types, expected_type):
if given.is_subtype_of(expected):
return
for given, expected in itertools.product(given_types, expected_type):
if given.is_subtype_of(expected):
return
Comment on lines +561 to +563

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject len() on non-empty list literals before codegen

When the expected type is the generic DArrayT.any() used by len, this generic list subtype check now lets non-empty list literals pass semantic analysis as dynamic arrays. Legacy Len.build_IR still calls get_bytearray_length on the parsed literal, and non-empty list literals lower to multi, so x: uint256 = len([1]) hits the newly added CompilerPanic xfail instead of compiling or producing a user-facing type error; please either keep this form rejected or lower it via the dynarray/list length path.

Useful? React with 👍 / 👎.

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.

Known issue, was reported above.


# validation failed, prepare a meaningful error message
if len(expected_type) > 1:
Expand Down
17 changes: 17 additions & 0 deletions vyper/semantics/types/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def is_equivalent_to(self, other):
return self.compare_type(other) and other.compare_type(self)

def compare_type(self, other):
if isinstance(other, BottomT):
return True

if isinstance(other, self.type_):
return True
# compare two GenericTypeAcceptors -- they are the same if the base
Expand Down Expand Up @@ -407,6 +410,9 @@ def compare_type(self, other: "VyperType") -> bool:
bool
Indicates if the types are equivalent.
"""
if isinstance(other, BottomT):
return True

return isinstance(other, type(self))

def fetch_call_return(self, node: vy_ast.Call) -> Optional["VyperType"]:
Expand Down Expand Up @@ -480,6 +486,17 @@ def __init__(self, typ, default, require_literal=False):
self.require_literal = require_literal


class BottomT(VyperType):
"""
Bottom type, the ultimate subtype: is a subtype of every other type.
It is unhabited: no value has this type.

It is for example the element type for empty lists: `[]: DynArray[Never, 1]`
"""

_id = "Never" # see python's typing.Never


class _VoidType(VyperType):
_id = "(void)"

Expand Down
5 changes: 4 additions & 1 deletion vyper/semantics/types/bytestrings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from vyper import ast as vy_ast
from vyper.abi_types import ABI_Bytes, ABI_String, ABIType
from vyper.exceptions import CodegenPanic, StructureException, UnexpectedNodeType, UnexpectedValue
from vyper.semantics.types.base import VyperType
from vyper.semantics.types.base import BottomT, VyperType
from vyper.semantics.types.infinity import INF, WILDCARD, LengthUpperBound, length_to_json
from vyper.semantics.types.utils import get_index_value
from vyper.utils import ceil32
Expand Down Expand Up @@ -79,6 +79,9 @@ def resolve_wildcard(self):
return self

def compare_type(self, other):
if isinstance(other, BottomT):
return True

if not super().compare_type(other):
return False

Expand Down
10 changes: 9 additions & 1 deletion vyper/semantics/types/primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)
from vyper.utils import checksum_encode, int_bounds, is_checksum_encoded

from .base import VyperType
from .base import BottomT, VyperType
from .bytestrings import BytesT
from .infinity import INF

Expand Down Expand Up @@ -126,6 +126,9 @@ def validate_literal(self, node: vy_ast.Constant) -> None:
raise InvalidLiteral(f"Cannot mix uppercase and lowercase for {self} literal", node)

def compare_type(self, other: VyperType) -> bool:
if isinstance(other, BottomT):
return True

if not super().compare_type(other):
return False
assert isinstance(other, BytesM_T)
Expand Down Expand Up @@ -317,6 +320,9 @@ def abi_type(self) -> ABIType:
return ABI_GIntM(self.bits, self.is_signed)

def compare_type(self, other: VyperType) -> bool:
if isinstance(other, BottomT):
return True

# this function is performance sensitive
# originally:
# if not super().compare_type(other):
Expand Down Expand Up @@ -440,6 +446,8 @@ class SelfT(AddressT):
_id = "self"

def compare_type(self, other):
if isinstance(other, BottomT):
return True
# compares true to AddressT
# This checks if either is a subtype of the other, which doesn't seem correct
return isinstance(other, type(self)) or isinstance(self, type(other))
Loading
Loading