• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CityOfZion / neo3-boa / 858f5658-b835-4f01-86b4-3a69cebd0b86

16 Oct 2023 06:23PM UTC coverage: 91.625% (+0.004%) from 91.621%
858f5658-b835-4f01-86b4-3a69cebd0b86

push

circleci

Mirella de Medeiros
Bump version: 1.0.1 → 1.1.0

1 of 1 new or added line in 1 file covered. (100.0%)

19988 of 21815 relevant lines covered (91.63%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.6
/boa3/internal/compiler/codegenerator/vmcodemapping.py
1
from __future__ import annotations
1✔
2

3
from typing import Dict, List, Optional, Union
1✔
4

5
from boa3.internal.compiler.codegenerator.methodtokencollection import MethodTokenCollection
1✔
6
from boa3.internal.compiler.codegenerator.vmcodemap import VMCodeMap
1✔
7
from boa3.internal.compiler.compileroutput import CompilerOutput
1✔
8
from boa3.internal.model.builtin.method import IBuiltinMethod
1✔
9
from boa3.internal.neo.vm.VMCode import VMCode
1✔
10
from boa3.internal.neo.vm.opcode import OpcodeHelper
1✔
11
from boa3.internal.neo.vm.opcode.OpcodeInformation import OpcodeInformation
1✔
12
from boa3.internal.neo3.contracts.contracttypes import CallFlags
1✔
13

14

15
class VMCodeMapping:
1✔
16
    """
17
    This class is responsible for managing the Neo VM instruction during the bytecode generation.
18
    """
19
    _instance: VMCodeMapping = None
1✔
20

21
    @classmethod
1✔
22
    def instance(cls):
1✔
23
        """
24
        :return: the singleton instance
25
        """
26
        if cls._instance is None:
1✔
27
            cls._instance = cls()
1✔
28
        return cls._instance
1✔
29

30
    def __init__(self):
1✔
31
        self._code_map: VMCodeMap = VMCodeMap()
1✔
32
        self._method_tokens: MethodTokenCollection = MethodTokenCollection()
1✔
33

34
    @classmethod
1✔
35
    def reset(cls):
1✔
36
        """
37
        Resets the map to the first state
38
        """
39
        if cls._instance is not None:
1✔
40
            cls._instance._code_map.clear()
1✔
41
            cls._instance._method_tokens.clear()
1✔
42

43
    def add_method_token(self, method: IBuiltinMethod, call_flag: CallFlags) -> Optional[int]:
1✔
44
        """
45
        Creates a new method token if the method call another contract and return its id.
46
        Otherwise, returns None
47
        """
48
        if hasattr(method, 'contract_script_hash'):
1✔
49
            return self._method_tokens.append(method, call_flag)
1✔
50
        return None
×
51

52
    def get_method_token(self, method_token_id: int):
1✔
53
        """
54
        Returns the method token with given id if it exists.
55
        Otherwise, returns None
56

57
        :rtype: boa3.internal.neo3.contracts.nef.MethodToken or None
58
        """
59
        return self._method_tokens[method_token_id]
1✔
60

61
    @property
1✔
62
    def codes(self) -> List[VMCode]:
1✔
63
        """
64
        Gets a list with the included vm codes
65

66
        :return: a list of vm codes ordered by its address in the bytecode
67
        """
68
        return self._code_map.get_code_list()
1✔
69

70
    @property
1✔
71
    def code_map(self) -> Dict[int, VMCode]:
1✔
72
        """
73
        Gets a dictionary that maps each vm code with its address.
74

75
        :return: a dictionary that maps each instruction with its address. The keys are ordered by the address.
76
        """
77
        return self._code_map.get_code_map()
1✔
78

79
    def targeted_address(self) -> Dict[int, List[int]]:
1✔
80
        """
81
        Gets a dictionary that maps each address to the opcodes that targets it
82

83
        :return: a dictionary that maps the targeted instructions to its source.
84
        """
85
        target_maps = {}
1✔
86
        for code in self._code_map.get_code_with_target_list():
1✔
87
            if code.target is not None and code.target is not code:
1✔
88
                address = self.get_start_address(code)
1✔
89
                target = self.get_start_address(code.target)
1✔
90
                if target not in target_maps:
1✔
91
                    target_maps[target] = [address]
1✔
92
                else:
93
                    target_maps[target].append(address)
1✔
94
        return target_maps
1✔
95

96
    def bytecode(self) -> bytes:
1✔
97
        """
98
        Gets the bytecode of the translated code
99

100
        :return: the generated bytecode
101
        """
102
        self._remove_empty_targets()
1✔
103
        self._update_larger_codes()
1✔
104

105
        bytecode = bytearray()
1✔
106
        for code in self.codes:
1✔
107
            bytecode += code.opcode
1✔
108
            if code.data is not None:
1✔
109
                bytecode += code.data
1✔
110
        return bytes(bytecode)
1✔
111

112
    def result(self) -> CompilerOutput:
1✔
113
        """
114
        Gets the complete output of the translated code
115
        """
116
        bytecode = self.bytecode()
1✔
117
        return CompilerOutput(bytecode, self._method_tokens.to_list())
1✔
118

119
    @property
1✔
120
    def bytecode_size(self) -> int:
1✔
121
        return self._code_map.get_bytecode_size()
1✔
122

123
    def insert_code(self, vm_code: VMCode):
1✔
124
        return self._code_map.insert_code(vm_code, has_target=OpcodeHelper.has_target(vm_code.opcode))
1✔
125

126
    def get_code(self, address: int) -> Optional[VMCode]:
1✔
127
        """
128
        Gets the VM Opcode at the given position
129

130
        :param address: the position of the opcode
131
        :return: the opcode if it exists. None otherwise
132
        :rtype: VMCode or None
133
        """
134
        return self._code_map.get_code(address)
1✔
135

136
    def get_addresses(self, start_address: int, end_address: int) -> List[int]:
1✔
137
        return self._code_map.get_addresses(start_address, end_address)
1✔
138

139
    def get_start_address(self, vm_code: VMCode) -> int:
1✔
140
        """
141
        Gets the vm code's first byte address
142

143
        :param vm_code: the instruction to get the address
144
        :return: the vm code's address if it's in the map. Otherwise, return's zero.
145
        """
146
        return self._code_map.get_start_address(vm_code)
1✔
147

148
    def get_end_address(self, vm_code: VMCode) -> int:
1✔
149
        """
150
        Gets the vm code's last byte address
151

152
        :param vm_code: the instruction to get the address
153
        :return: the vm code's last address if it's in the map. Otherwise, return's zero.
154
        """
155
        return self._code_map.get_end_address(vm_code)
1✔
156

157
    def get_opcodes(self, addresses: List[int]) -> List[VMCode]:
1✔
158
        return self._code_map.get_opcodes(addresses)
1✔
159

160
    def update_vm_code(self, vm_code: VMCode, opcode: OpcodeInformation, data: bytes = bytes()):
1✔
161
        """
162
        Updates the information from an inserted code
163

164
        :param vm_code: code to be updated
165
        :param opcode: updated opcode information
166
        :param data: updated opcode data
167
        """
168
        code_size = vm_code.size
1✔
169
        vm_code._info = opcode
1✔
170
        vm_code._data = data
1✔
171
        if vm_code.size != code_size:
1✔
172
            self._update_addresses(self.get_start_address(vm_code))
1✔
173

174
    def _update_addresses(self, start_address: int = 0):
1✔
175
        """
176
        Updates the instruction map's keys when a opcode is changed
177

178
        :param start_address: the address from the changed opcode
179
        """
180
        return self._code_map.update_addresses(start_address)
1✔
181

182
    def _update_targets(self):
1✔
183
        from boa3.internal.neo.vm.type.Integer import Integer
1✔
184
        for code in self._code_map.get_code_with_target_list():
1✔
185
            if code.target is None:
1✔
186
                relative = Integer.from_bytes(code.data)
1✔
187
                absolute = self._code_map.get_start_address(code) + relative
1✔
188
                if absolute in self.code_map:
1✔
189
                    code.set_target(self.code_map[absolute])
1✔
190

191
    def _update_larger_codes(self):
1✔
192
        """
193
        Checks if each instruction data fits in its opcode maximum size and updates the opcode from those that don't
194
        """
195
        # gets a list with all instructions which its opcode has a larger equivalent, ordered by its address
196
        instr_with_small_codes = [code for code in self._code_map.get_code_list() if OpcodeHelper.has_larger_opcode(code.opcode)]
1✔
197
        instr_with_small_codes.sort(key=lambda code: self.get_start_address(code), reverse=True)
1✔
198

199
        from boa3.internal.neo.vm.opcode.OpcodeInfo import OpcodeInfo
1✔
200
        # total_len is initialized with zero because the loop must run at least once
201
        total_len = 0
1✔
202
        current_size = self.bytecode_size
1✔
203

204
        # if any instruction is updated, the following instruction addresses and the total size will change as well
205
        # with the change, previous instruction data may have overflowed the opcode maximum value
206
        # to make sure, it must check the opcodes that haven't changed again
207
        while total_len != current_size:
1✔
208
            total_len = current_size
1✔
209

210
            # verifies each instruction data length
211
            for code in instr_with_small_codes.copy():  # it's a copy because the list may change during the iteration
1✔
212
                if len(code.raw_data) > code.info.max_data_len:
1✔
213
                    # gets the shortest opcode equivalent that fits the instruction data
214
                    info = OpcodeInfo.get_info(OpcodeHelper.get_larger_opcode(code.opcode))
1✔
215
                    while len(code.raw_data) > info.max_data_len and OpcodeHelper.has_larger_opcode(info.opcode):
1✔
216
                        info = OpcodeInfo.get_info(OpcodeHelper.get_larger_opcode(code.opcode))
×
217

218
                    self.update_vm_code(code, info)
1✔
219
                    if info.opcode == OpcodeHelper.get_larger_opcode(info.opcode):
1✔
220
                        # if it's the largest equivalent, it won't be updated anymore
221
                        instr_with_small_codes.remove(code)
1✔
222
            current_size = self.bytecode_size
1✔
223

224
    def _validate_targets(self, code_or_address: Union[int, VMCode]):
1✔
225
        if isinstance(code_or_address, int):
1✔
226
            address = code_or_address
1✔
227
            code = self.get_code(address)
1✔
228
        else:
229
            code = code_or_address
×
230
            address = self.get_start_address(code)
×
231

232
        targeted_addresses = self.targeted_address()
1✔
233

234
        if address in targeted_addresses:
1✔
235
            next_address = self.get_end_address(code) + 1
1✔
236
            if next_address < self.bytecode_size:
1✔
237
                next_code = self._code_map.get_code(next_address)
1✔
238
                for source in targeted_addresses[address]:
1✔
239
                    self._code_map.get_code(source).set_target(next_code)
1✔
240

241
    def move_to_end(self, first_code_address: int, last_code_address: int) -> int:
1✔
242
        """
243
        Moves a set of instructions to the end of the current bytecode
244

245
        :param first_code_address: first instruction start address
246
        :param last_code_address: last instruction end address
247
        """
248
        result = self._code_map.move_to_end(first_code_address, last_code_address)
1✔
249
        if not isinstance(result, int):
1✔
250
            return self.bytecode_size
1✔
251

252
        self._update_targets()
1✔
253
        return result
1✔
254

255
    def remove_opcodes(self, first_code_address: int, last_code_address: int = None):
1✔
256
        if not isinstance(last_code_address, int):
1✔
257
            last_code_address = self.bytecode_size
×
258
        addresses_to_remove = self._code_map.get_addresses(first_code_address, last_code_address)
1✔
259
        for address in addresses_to_remove:
1✔
260
            self._validate_targets(address)
1✔
261
        return self._code_map.remove_opcodes_by_addresses(addresses_to_remove)
1✔
262

263
    def remove_opcodes_by_code(self, codes: List[VMCode]):
1✔
264
        addresses_to_remove = self._code_map.get_addresses_from_codes(codes)
1✔
265
        for address in addresses_to_remove:
1✔
266
            self._validate_targets(address)
1✔
267
        return self._code_map.remove_opcodes_by_addresses(addresses_to_remove)
1✔
268

269
    def _remove_empty_targets(self):
1✔
270
        """
271
        Checks if each instruction that requires a target has one set and remove those that don't
272
        """
273
        addresses_to_remove = []
1✔
274
        for code in self._code_map.get_code_with_target_list():
1✔
275
            if code.target is None or code.target is code:
1✔
276
                address = self.get_start_address(code)
1✔
277
                self._validate_targets(address)
1✔
278
                addresses_to_remove.append(address)
1✔
279

280
        if len(addresses_to_remove) > 0:
1✔
281
            self._code_map.remove_opcodes_by_addresses(addresses_to_remove)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc