Skip to content

right-to-left evaluation in certain cases #4019

@ritzdorf

Description

@ritzdorf

Version Information

  • vyper Version (output of vyper --version OR linkable commit hash vyperlang/vyper@): b43ffac

Issue Description

The order of evaluation in Vyper is specified as left-to-right; in the
same order as the expressions appear in the the source code.

  • Due to the way IR nodes that match directly EVM opcodes are
    converted to assembly, for some built-ins and operators, it is known
    that it might not be the case as described in
    GHSA-g2xh-c426-v8mf.

  • The compiler uses a pattern cache_when_complex to cache the result
    of a complex expression and avoid double evaluation. However, this
    pattern appears to be problematic regarding the order of evaluation.

    • Given an IR node that has multiple children that should be
      evaluated in a specific order, it might be that not all
      children are cached. A cached complex child would hence be
      evaluated before any non-cached children independently of
      their order in the source code. This behavior is notably seen
      in assignments given that make_setter() caches src but not
      dst.
    • A more subtle issue is that the compiler might cache all
      children of an IR node, however, one or multiple nodes are not
      considered as complex and their evaluation is inlined.
      although in that case, a non-complex node cannot have side
      effects, its evaluation can still read (e.g a SLOAD) side
      effects from a complex node that was cached and evaluated
      first. This case appears to be problematic for the slice()
      and extract32() built-ins.
  • Another issue regarding the order of evaluation is that the order of
    evaluation of the kwargs passed to built-ins and call expressions do
    not follow the order of the source code.

POC

The following examples show multiple cases where the order of evaluation
is right-to-left.

i:uint256

@internal
def change_i() -> uint256:
    self.i += 1
    return 12

@external
def foo() -> DynArray[uint256,2]:
    x:DynArray[uint256,2] = [1,2]
    x[self.i] += self.change_i()
    return x # returns [13,2]
boo:Bytes[32]

@internal
def bar() -> uint256:
    self.boo = b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
    return 0

@internal
def baz() -> Bytes[32]:
    return self.boo

@external
def slice() -> (Bytes[32], Bytes[32]):
    self.boo = b'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
    s: Bytes[1] = slice(self.boo, self.bar(), 1)

    self.boo = b'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
    t: Bytes[1] = slice(self.baz(), self.bar(), 1)

    return s,t # (b'a', b'b')

@external
def extract32() -> (bytes32, bytes32):
    self.boo = b'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
    s: bytes32 = extract32(self.boo, self.bar())

    self.boo = b'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
    t: bytes32 = extract32(self.baz(), self.bar())

    return s,t #(b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', b'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')
@external
@payable
def bar(): return

interface Bar:
    def bar(): payable

x: uint256

@internal
def gas() -> uint256:
    self.x = 2
    return 100000

@internal
def value() -> uint256:
    self.x = 1
    return 0

@external
@payable
def foo():
    extcall Bar(self).bar(gas=self.gas(), value=self.value())    
    temp: uint256 = self.x
    extcall Bar(self).bar(value=self.value(), gas=self.gas())    
    assert self.x == temp # passes

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions