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

CityOfZion / neo3-boa / e084b44c-1a5f-4649-92cd-d3ed06099181

05 Mar 2024 05:58PM UTC coverage: 92.023% (-0.08%) from 92.107%
e084b44c-1a5f-4649-92cd-d3ed06099181

push

circleci

Mirella de Medeiros
CU-86drpnc9z - Drop support to Python 3.10

20547 of 22328 relevant lines covered (92.02%)

1.84 hits per line

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

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

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

12

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

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

28
    def __init__(self):
2✔
29
        self._code_map: VMCodeMap = VMCodeMap()
2✔
30
        self._method_tokens: MethodTokenCollection = MethodTokenCollection()
2✔
31

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

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

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

55
        :rtype: boa3.internal.neo3.contracts.nef.MethodToken or None
56
        """
57
        return self._method_tokens[method_token_id]
2✔
58

59
    @property
2✔
60
    def codes(self) -> list[VMCode]:
2✔
61
        """
62
        Gets a list with the included vm codes
63

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

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

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

77
    def targeted_address(self) -> dict[int, list[int]]:
2✔
78
        """
79
        Gets a dictionary that maps each address to the opcodes that targets it
80

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

94
    def bytecode(self) -> bytes:
2✔
95
        """
96
        Gets the bytecode of the translated code
97

98
        :return: the generated bytecode
99
        """
100
        self._remove_empty_targets()
2✔
101
        self._update_larger_codes()
2✔
102

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

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

117
    @property
2✔
118
    def bytecode_size(self) -> int:
2✔
119
        return self._code_map.get_bytecode_size()
2✔
120

121
    def insert_code(self, vm_code: VMCode):
2✔
122
        return self._code_map.insert_code(vm_code, has_target=OpcodeHelper.has_target(vm_code.opcode))
2✔
123

124
    def get_code(self, address: int) -> VMCode | None:
2✔
125
        """
126
        Gets the VM Opcode at the given position
127

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

134
    def get_addresses(self, start_address: int, end_address: int) -> list[int]:
2✔
135
        return self._code_map.get_addresses(start_address, end_address)
2✔
136

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

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

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

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

155
    def get_opcodes(self, addresses: list[int]) -> list[VMCode]:
2✔
156
        return self._code_map.get_opcodes(addresses)
2✔
157

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

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

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

176
        :param start_address: the address from the changed opcode
177
        """
178
        return self._code_map.update_addresses(start_address)
2✔
179

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

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

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

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

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

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

222
    def _validate_targets(self, code_or_address: int | VMCode):
2✔
223
        if isinstance(code_or_address, int):
2✔
224
            address = code_or_address
2✔
225
            code = self.get_code(address)
2✔
226
        else:
227
            code = code_or_address
×
228
            address = self.get_start_address(code)
×
229

230
        targeted_addresses = self.targeted_address()
2✔
231

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

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

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

250
        self._update_targets()
2✔
251
        return result
2✔
252

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

261
    def remove_opcodes_by_code(self, codes: list[VMCode]):
2✔
262
        addresses_to_remove = self._code_map.get_addresses_from_codes(codes)
2✔
263
        for address in addresses_to_remove:
2✔
264
            self._validate_targets(address)
2✔
265
        return self._code_map.remove_opcodes_by_addresses(addresses_to_remove)
2✔
266

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

278
        if len(addresses_to_remove) > 0:
2✔
279
            self._code_map.remove_opcodes_by_addresses(addresses_to_remove)
2✔
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