def set_storage_slots(vyper_module: vy_ast.Module) -> None: """ Parse module-level Vyper AST to calculate the layout of storage variables. """ # Allocate storage slots from 0 # note storage is word-addressable, not byte-addressable storage_slot = 0 for node in vyper_module.get_children(vy_ast.FunctionDef): type_ = node._metadata["type"] if type_.nonreentrant is not None: type_.set_reentrancy_key_position(StorageSlot(storage_slot)) # TODO use one byte - or bit - per reentrancy key # requires either an extra SLOAD or caching the value of the # location in memory at entrance storage_slot += 1 for node in vyper_module.get_children(vy_ast.AnnAssign): type_ = node.target._metadata["type"] type_.set_position(StorageSlot(storage_slot)) # CMC 2021-07-23 note that HashMaps get assigned a slot here. # I'm not sure if it's safe to avoid allocating that slot # for HashMaps because downstream code might use the slot # ID as a salt. storage_slot += math.ceil(type_.size_in_bytes / 32)
def set_storage_slots(vyper_module: vy_ast.Module) -> StorageLayout: """ Parse module-level Vyper AST to calculate the layout of storage variables. Returns the layout as a dict of variable name -> variable info """ # Allocate storage slots from 0 # note storage is word-addressable, not byte-addressable storage_slot = 0 ret: Dict[str, Dict] = {} for node in vyper_module.get_children(vy_ast.FunctionDef): type_ = node._metadata["type"] if type_.nonreentrant is None: continue variable_name = f"nonreentrant.{type_.nonreentrant}" # a nonreentrant key can appear many times in a module but it # only takes one slot. after the first time we see it, do not # increment the storage slot. if variable_name in ret: _slot = ret[variable_name]["slot"] type_.set_reentrancy_key_position(StorageSlot(_slot)) continue type_.set_reentrancy_key_position(StorageSlot(storage_slot)) # TODO this could have better typing but leave it untyped until # we nail down the format better ret[variable_name] = { "type": "nonreentrant lock", "location": "storage", "slot": storage_slot, } # TODO use one byte - or bit - per reentrancy key # requires either an extra SLOAD or caching the value of the # location in memory at entrance storage_slot += 1 for node in vyper_module.get_children(vy_ast.AnnAssign): if node.get("annotation.func.id") == "immutable": continue type_ = node.target._metadata["type"] type_.set_position(StorageSlot(storage_slot)) # this could have better typing but leave it untyped until # we understand the use case better ret[node.target.id] = {"type": str(type_), "location": "storage", "slot": storage_slot} # CMC 2021-07-23 note that HashMaps get assigned a slot here. # I'm not sure if it's safe to avoid allocating that slot # for HashMaps because downstream code might use the slot # ID as a salt. storage_slot += math.ceil(type_.size_in_bytes / 32) return ret
def set_storage_slots(vyper_module: vy_ast.Module) -> None: """ Parse module-level Vyper AST to calculate the layout of storage variables. """ available_slot = 0 for node in vyper_module.get_children(vy_ast.AnnAssign): type_ = node.target._metadata["type"] type_.set_position(StorageSlot(available_slot)) available_slot += math.ceil(type_.size_in_bytes / 32)
def set_code_offsets(vyper_module: vy_ast.Module) -> None: offset = 0 for node in vyper_module.get_children( vy_ast.AnnAssign, filters={"annotation.func.id": "immutable"} ): type_ = node._metadata["type"] type_.set_position(CodeOffset(offset)) offset += math.ceil(type_.size_in_bytes / 32) * 32
def _get_module_definitions( base_node: vy_ast.Module) -> Tuple[OrderedDict, Dict]: functions: OrderedDict = OrderedDict() events: Dict = {} for node in base_node.get_children(vy_ast.FunctionDef): if "external" in [ i.id for i in node.decorator_list if isinstance(i, vy_ast.Name) ]: func = ContractFunction.from_FunctionDef(node) if node.name in functions: # compare the input arguments of the new function and the previous one # if one function extends the inputs, this is a valid function name overload existing_args = list(functions[node.name].arguments) new_args = list(func.arguments) for a, b in zip(existing_args, new_args): if not isinstance(a, type(b)): raise NamespaceCollision( f"Interface contains multiple functions named '{node.name}' " "with incompatible input types", base_node, ) if len(new_args) <= len(existing_args): # only keep the `ContractFunction` with the longest set of input args continue functions[node.name] = func for node in base_node.get_children(vy_ast.AnnAssign, {"annotation.func.id": "public"}): name = node.target.id if name in functions: raise NamespaceCollision( f"Interface contains multiple functions named '{name}'", base_node) functions[name] = ContractFunction.from_AnnAssign(node) for node in base_node.get_children(vy_ast.EventDef): name = node.name if name in functions or name in events: raise NamespaceCollision( f"Interface contains multiple objects named '{name}'", base_node) events[name] = Event.from_EventDef(node) return functions, events
def remove_unused_statements(vyper_module: vy_ast.Module) -> None: """ Remove statement nodes that are unused after type checking. Once type checking is complete, we can remove now-meaningless statements to simplify the AST prior to IR generation. Arguments --------- vyper_module : Module Top-level Vyper AST node. """ # constant declarations - values were substituted within the AST during folding for node in vyper_module.get_children(vy_ast.AnnAssign, {"annotation.func.id": "constant"}): vyper_module.remove_from_body(node) # `implements: interface` statements - validated during type checking for node in vyper_module.get_children(vy_ast.AnnAssign, {"target.id": "implements"}): vyper_module.remove_from_body(node)
def validate_functions(vy_module: vy_ast.Module) -> None: """Analyzes a vyper ast and validates the function-level namespaces.""" err_list = ExceptionList() namespace = get_namespace() for node in vy_module.get_children(vy_ast.FunctionDef): with namespace.enter_scope(): try: FunctionNodeVisitor(vy_module, node, namespace) except VyperException as e: err_list.append(e) err_list.raise_if_not_empty()
def remove_constant_declarations(vyper_module: vy_ast.Module) -> None: """ Remove constant declaration nodes. Values for constants are subsituted within the AST during folding. Once type checking is complete their declarations are removed to simplify the AST prior to IR generation. Arguments --------- vyper_module : Module Top-level Vyper AST node. """ for node in vyper_module.get_children(vy_ast.AnnAssign, {"annotation.func.id": "constant"}): vyper_module.remove_from_body(node)
def set_code_offsets(vyper_module: vy_ast.Module) -> Dict: ret = {} offset = 0 for node in vyper_module.get_children( vy_ast.VariableDecl, filters={"annotation.func.id": "immutable"}): type_ = node._metadata["type"] type_.set_position(CodeOffset(offset)) len_ = math.ceil(type_.size_in_bytes / 32) * 32 # this could have better typing but leave it untyped until # we understand the use case better ret[node.target.id] = { "type": str(type_), "offset": offset, "length": len_ } offset += len_ return ret
def set_storage_slots_with_overrides( vyper_module: vy_ast.Module, storage_layout_overrides: StorageLayout ) -> StorageLayout: """ Parse module-level Vyper AST to calculate the layout of storage variables. Returns the layout as a dict of variable name -> variable info """ ret: Dict[str, Dict] = {} reserved_slots = StorageAllocator() # Search through function definitions to find non-reentrant functions for node in vyper_module.get_children(vy_ast.FunctionDef): type_ = node._metadata["type"] # Ignore functions without non-reentrant if type_.nonreentrant is None: continue variable_name = f"nonreentrant.{type_.nonreentrant}" # re-entrant key was already identified if variable_name in ret: _slot = ret[variable_name]["slot"] type_.set_reentrancy_key_position(StorageSlot(_slot)) continue # Expect to find this variable within the storage layout override if variable_name in storage_layout_overrides: reentrant_slot = storage_layout_overrides[variable_name]["slot"] # Ensure that this slot has not been used, and prevents other storage variables # from using the same slot reserved_slots.reserve_slot_range(reentrant_slot, 1, variable_name) type_.set_reentrancy_key_position(StorageSlot(reentrant_slot)) ret[variable_name] = { "type": "nonreentrant lock", "location": "storage", "slot": reentrant_slot, } else: raise StorageLayoutException( f"Could not find storage_slot for {variable_name}. " "Have you used the correct storage layout file?", node, ) # Iterate through variables for node in vyper_module.get_children(vy_ast.AnnAssign): # Ignore immutable parameters if node.get("annotation.func.id") == "immutable": continue type_ = node.target._metadata["type"] # Expect to find this variable within the storage layout overrides if node.target.id in storage_layout_overrides: var_slot = storage_layout_overrides[node.target.id]["slot"] # Calculate how many storage slots are required storage_length = math.ceil(type_.size_in_bytes / 32) # Ensure that all required storage slots are reserved, and prevents other variables # from using these slots reserved_slots.reserve_slot_range(var_slot, storage_length, node.target.id) type_.set_position(StorageSlot(var_slot)) ret[node.target.id] = {"type": str(type_), "location": "storage", "slot": var_slot} else: raise StorageLayoutException( f"Could not find storage_slot for {node.target.id}. " "Have you used the correct storage layout file?", node, ) return ret
def generate_public_variable_getters(vyper_module: vy_ast.Module) -> None: """ Create getter functions for public variables. Arguments --------- vyper_module : Module Top-level Vyper AST node. """ for node in vyper_module.get_children(vy_ast.AnnAssign, {"annotation.func.id": "public"}): func_type = node._metadata["type"] input_types, return_type = func_type.get_signature() input_nodes = [] # use the annotation node as a base to build the input args and return type # starting with `args[0]` to remove the surrounding `public()` call` annotation = copy.deepcopy(node.annotation.args[0]) # the base return statement is an `Attribute` node, e.g. `self.<var_name>` # for each input type we wrap it in a `Subscript` to access a specific member return_stmt: vy_ast.VyperNode = vy_ast.Attribute( value=vy_ast.Name(id="self"), attr=func_type.name ) for i, type_ in enumerate(input_types): if not isinstance(annotation, vy_ast.Subscript): # if we get here something has failed in type checking raise CompilerPanic("Mismatch between node and input type while building getter") if annotation.value.get("id") == "HashMap": # type: ignore # for a HashMap, split the key/value types and use the key type as the next arg arg, annotation = annotation.slice.value.elements # type: ignore else: # for other types, build an input arg node from the expected type # and remove the outer `Subscript` from the annotation arg = vy_ast.Name(id=type_._id) annotation = annotation.value input_nodes.append(vy_ast.arg(arg=f"arg{i}", annotation=arg)) # wrap the return statement in a `Subscript` return_stmt = vy_ast.Subscript( value=return_stmt, slice=vy_ast.Index(value=vy_ast.Name(id=f"arg{i}")) ) # after iterating the input types, the remaining annotation node is our return type return_node = annotation if isinstance(return_node, vy_ast.Name) and return_node.id != return_type._id: # special case when the return type is an interface # TODO allow interfaces as return types and remove this return_node.id = return_type._id # join everything together as a new `FunctionDef` node, annotate it # with the type, and append it to the existing `Module` node expanded = vy_ast.FunctionDef.from_node( node.annotation, name=func_type.name, args=vy_ast.arguments(args=input_nodes, defaults=[],), body=[vy_ast.Return(value=return_stmt)], decorator_list=[vy_ast.Name(id="external"), vy_ast.Name(id="view")], returns=return_node, ) expanded._metadata["type"] = func_type vyper_module.add_to_body(expanded)