CIRCT 22.0.0git
Loading...
Searching...
No Matches
types.py
Go to the documentation of this file.
1# ===-----------------------------------------------------------------------===#
2# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
3# See https://llvm.org/LICENSE.txt for license information.
4# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5# ===-----------------------------------------------------------------------===#
6#
7# The structure of the Python classes and hierarchy roughly mirrors the C++
8# side, but wraps the C++ objects. The wrapper classes sometimes add convenience
9# functionality and serve to return wrapped versions of the returned objects.
10#
11# ===-----------------------------------------------------------------------===#
12
13from __future__ import annotations
14
15from . import esiCppAccel as cpp
16
17from typing import TYPE_CHECKING
18
19if TYPE_CHECKING:
20 from .accelerator import HWModule
21
22from concurrent.futures import Future
23from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
24import sys
25import traceback
26
27
28def _get_esi_type(cpp_type: cpp.Type):
29 """Get the wrapper class for a C++ type."""
30 if isinstance(cpp_type, cpp.ChannelType):
31 return _get_esi_type(cpp_type.inner)
32
33 for cpp_type_cls, wrapper_cls in __esi_mapping.items():
34 if isinstance(cpp_type, cpp_type_cls):
35 return wrapper_cls.wrap_cpp(cpp_type)
36 return ESIType.wrap_cpp(cpp_type)
37
38
39# Mapping from C++ types to wrapper classes
40__esi_mapping: Dict[Type, Type] = {}
41
42
43class ESIType:
44
45 def __init__(self, id: str):
46 self._init_from_cpp(cpp.Type(id))
47
48 @classmethod
49 def wrap_cpp(cls, cpp_type: cpp.Type):
50 """Wrap a C++ ESI type with its corresponding Python ESI Type."""
51 instance = cls.__new__(cls)
52 instance._init_from_cpp(cpp_type)
53 return instance
54
55 def _init_from_cpp(self, cpp_type: cpp.Type):
56 """Initialize instance attributes from a C++ type object."""
57 self.cpp_type = cpp_type
58
59 @property
60 def supports_host(self) -> Tuple[bool, Optional[str]]:
61 """Does this type support host communication via Python? Returns either
62 '(True, None)' if it is, or '(False, reason)' if it is not."""
63
64 if self.bit_width % 8 != 0:
65 return (False, "runtime only supports types with multiple of 8 bits")
66 return (True, None)
67
68 def is_valid(self, obj) -> Tuple[bool, Optional[str]]:
69 """Is a Python object compatible with HW type? Returns either '(True,
70 None)' if it is, or '(False, reason)' if it is not."""
71 assert False, "unimplemented"
72
73 @property
74 def bit_width(self) -> int:
75 """Size of this type, in bits. Negative for unbounded types."""
76 assert False, "unimplemented"
77
78 @property
79 def max_size(self) -> int:
80 """Maximum size of a value of this type, in bytes."""
81 bitwidth = int((self.bit_width + 7) / 8)
82 if bitwidth < 0:
83 return bitwidth
84 return bitwidth
85
86 def serialize(self, obj) -> bytearray:
87 """Convert a Python object to a bytearray."""
88 assert False, "unimplemented"
89
90 def deserialize(self, data: bytearray) -> Tuple[object, bytearray]:
91 """Convert a bytearray to a Python object. Return the object and the
92 leftover bytes."""
93 assert False, "unimplemented"
94
95 def __str__(self) -> str:
96 return str(self.cpp_type)
97
98
100
101 def __init__(self, id: str):
102 self._init_from_cpp(cpp.VoidType(id))
103
104 def is_valid(self, obj) -> Tuple[bool, Optional[str]]:
105 if obj is not None:
106 return (False, f"void type cannot must represented by None, not {obj}")
107 return (True, None)
108
109 @property
110 def bit_width(self) -> int:
111 return 8
112
113 def serialize(self, obj) -> bytearray:
114 # By convention, void is represented by a single byte of value 0.
115 return bytearray([0])
116
117 def deserialize(self, data: bytearray) -> Tuple[object, bytearray]:
118 if len(data) == 0:
119 raise ValueError(f"void type cannot be represented by {data}")
120 return (None, data[1:])
121
122
123__esi_mapping[cpp.VoidType] = VoidType
124
125
127
128 def __init__(self, id: str, width: int):
129 self._init_from_cpp(cpp.BitsType(id, width))
130
131 def is_valid(self, obj) -> Tuple[bool, Optional[str]]:
132 if not isinstance(obj, (bytearray, bytes, list)):
133 return (False, f"invalid type: {type(obj)}")
134 if isinstance(obj, list) and not all(
135 [isinstance(b, int) and b.bit_length() <= 8 for b in obj]):
136 return (False, f"list item too large: {obj}")
137 if len(obj) != self.max_size:
138 return (False, f"wrong size: {len(obj)}")
139 return (True, None)
140
141 @property
142 def bit_width(self) -> int:
143 return self.cpp_type.width
144
145 def serialize(self, obj: Union[bytearray, bytes, List[int]]) -> bytearray:
146 if isinstance(obj, bytearray):
147 return obj
148 if isinstance(obj, bytes) or isinstance(obj, list):
149 return bytearray(obj)
150 raise ValueError(f"cannot convert {obj} to bytearray")
151
152 def deserialize(self, data: bytearray) -> Tuple[bytearray, bytearray]:
153 return (data[0:self.max_size], data[self.max_size:])
154
155
156__esi_mapping[cpp.BitsType] = BitsType
157
158
160
161 def __init__(self, id: str, width: int):
162 self._init_from_cpp(cpp.IntegerType(id, width))
163
164 @property
165 def bit_width(self) -> int:
166 return self.cpp_type.width
167
168
170
171 def __init__(self, id: str, width: int):
172 self._init_from_cpp(cpp.UIntType(id, width))
173
174 def is_valid(self, obj) -> Tuple[bool, Optional[str]]:
175 if not isinstance(obj, int):
176 return (False, f"must be an int, not {type(obj)}")
177 if obj < 0 or obj.bit_length() > self.bit_widthbit_width:
178 return (False, f"out of range: {obj}")
179 return (True, None)
180
181 def __str__(self) -> str:
182 return f"uint{self.bit_width}"
183
184 def serialize(self, obj: int) -> bytearray:
185 return bytearray(int.to_bytes(obj, self.max_sizemax_size, "little"))
186
187 def deserialize(self, data: bytearray) -> Tuple[int, bytearray]:
188 return (int.from_bytes(data[0:self.max_sizemax_size],
189 "little"), data[self.max_sizemax_size:])
190
191
192__esi_mapping[cpp.UIntType] = UIntType
193
194
196
197 def __init__(self, id: str, width: int):
198 self._init_from_cpp(cpp.SIntType(id, width))
199
200 def is_valid(self, obj) -> Tuple[bool, Optional[str]]:
201 if not isinstance(obj, int):
202 return (False, f"must be an int, not {type(obj)}")
203 if obj < 0:
204 if (-1 * obj) > 2**(self.bit_widthbit_width - 1):
205 return (False, f"out of range: {obj}")
206 elif obj < 0:
207 if obj >= 2**(self.bit_widthbit_width - 1) - 1:
208 return (False, f"out of range: {obj}")
209 return (True, None)
210
211 def __str__(self) -> str:
212 return f"sint{self.bit_width}"
213
214 def serialize(self, obj: int) -> bytearray:
215 return bytearray(int.to_bytes(obj, self.max_sizemax_size, "little", signed=True))
216
217 def deserialize(self, data: bytearray) -> Tuple[int, bytearray]:
218 return (int.from_bytes(data[0:self.max_sizemax_size], "little",
219 signed=True), data[self.max_sizemax_size:])
220
221
222__esi_mapping[cpp.SIntType] = SIntType
223
224
226
227 def __init__(self, id: str, fields: List[Tuple[str, "ESIType"]]):
228 # Convert Python ESIType fields to cpp Type fields
229 cpp_fields = [(name, field_type.cpp_type) for name, field_type in fields]
230 self._init_from_cpp_init_from_cpp(cpp.StructType(id, cpp_fields))
231
232 def _init_from_cpp(self, cpp_type: cpp.StructType):
233 """Initialize instance attributes from a C++ type object."""
234 super()._init_from_cpp(cpp_type)
235 # For wrap_cpp path, we need to convert C++ fields back to Python
236 self.fields = [(name, _get_esi_type(ty)) for (name, ty) in cpp_type.fields]
237
238 @property
239 def bit_width(self) -> int:
240 widths = [ty.bit_width for (_, ty) in self.fields]
241 if any([w < 0 for w in widths]):
242 return -1
243 return sum(widths)
244
245 def is_valid(self, obj) -> Tuple[bool, Optional[str]]:
246 fields_count = 0
247 if not isinstance(obj, dict):
248 if not hasattr(obj, "__dict__"):
249 return (False, "must be a dict or have __dict__ attribute")
250 obj = obj.__dict__
251
252 for (fname, ftype) in self.fields:
253 if fname not in obj:
254 return (False, f"missing field '{fname}'")
255 fvalid, reason = ftype.is_valid(obj[fname])
256 if not fvalid:
257 return (False, f"invalid field '{fname}': {reason}")
258 fields_count += 1
259 if fields_count != len(obj):
260 return (False, "missing fields")
261 return (True, None)
262
263 def serialize(self, obj) -> bytearray:
264 ret = bytearray()
265 if not isinstance(obj, dict):
266 obj = obj.__dict__
267 ordered_fields = reversed(
268 self.fields) if self.cpp_type.reverse else self.fields
269 for (fname, ftype) in ordered_fields:
270 fval = obj[fname]
271 ret.extend(ftype.serialize(fval))
272 return ret
273
274 def deserialize(self, data: bytearray) -> Tuple[Dict[str, Any], bytearray]:
275 ret = {}
276 ordered_fields = reversed(
277 self.fields) if self.cpp_type.reverse else self.fields
278 for (fname, ftype) in ordered_fields:
279 (fval, data) = ftype.deserialize(data)
280 ret[fname] = fval
281 return (ret, data)
282
283
284__esi_mapping[cpp.StructType] = StructType
285
286
288
289 def __init__(self, id: str, element_type: "ESIType", size: int):
290 self._init_from_cpp_init_from_cpp(cpp.ArrayType(id, element_type.cpp_type, size))
291
292 def _init_from_cpp(self, cpp_type: cpp.ArrayType):
293 """Initialize instance attributes from a C++ type object."""
294 super()._init_from_cpp(cpp_type)
295 self.element_type = _get_esi_type(cpp_type.element)
296 self.size = cpp_type.size
297
298 @property
299 def bit_width(self) -> int:
300 return self.element_type.bit_width * self.size
301
302 def is_valid(self, obj) -> Tuple[bool, Optional[str]]:
303 if not isinstance(obj, list):
304 return (False, f"must be a list, not {type(obj)}")
305 if len(obj) != self.size:
306 return (False, f"wrong size: expected {self.size} not {len(obj)}")
307 for (idx, e) in enumerate(obj):
308 evalid, reason = self.element_type.is_valid(e)
309 if not evalid:
310 return (False, f"invalid element {idx}: {reason}")
311 return (True, None)
312
313 def serialize(self, lst: list) -> bytearray:
314 ret = bytearray()
315 for e in reversed(lst):
316 ret.extend(self.element_type.serialize(e))
317 return ret
318
319 def deserialize(self, data: bytearray) -> Tuple[List[Any], bytearray]:
320 ret = []
321 for _ in range(self.size):
322 (obj, data) = self.element_type.deserialize(data)
323 ret.append(obj)
324 ret.reverse()
325 return (ret, data)
326
327
328__esi_mapping[cpp.ArrayType] = ArrayType
329
330
331class Port:
332 """A unidirectional communication channel. This is the basic communication
333 method with an accelerator."""
334
335 def __init__(self, owner: BundlePort, cpp_port: cpp.ChannelPort):
336 self.owner = owner
337 self.cpp_port = cpp_port
338 self.type = _get_esi_type(cpp_port.type)
339
340 def connect(self, buffer_size: Optional[int] = None):
341 (supports_host, reason) = self.type.supports_host
342 if not supports_host:
343 raise TypeError(f"unsupported type: {reason}")
344
345 self.cpp_port.connect(buffer_size)
346 return self
347
348 def disconnect(self):
349 self.cpp_port.disconnect()
350
351
353 """A unidirectional communication channel from the host to the accelerator."""
354
355 def __init__(self, owner: BundlePort, cpp_port: cpp.WriteChannelPort):
356 super().__init__(owner, cpp_port)
357 self.cpp_port: cpp.WriteChannelPort = cpp_port
358
359 def __serialize_msg(self, msg=None) -> bytearray:
360 valid, reason = self.type.is_valid(msg)
361 if not valid:
362 raise ValueError(
363 f"'{msg}' cannot be converted to '{self.type}': {reason}")
364 msg_bytes: bytearray = self.type.serialize(msg)
365 return msg_bytes
366
367 def write(self, msg=None) -> bool:
368 """Write a typed message to the channel. Attempts to serialize 'msg' to what
369 the accelerator expects, but will fail if the object is not convertible to
370 the port type."""
371 self.cpp_port.write(self.__serialize_msg(msg))
372 return True
373
374 def try_write(self, msg=None) -> bool:
375 """Like 'write', but uses the non-blocking tryWrite method of the underlying
376 port. Returns True if the write was successful, False otherwise."""
377 return self.cpp_port.tryWrite(self.__serialize_msg(msg))
378
379
381 """A unidirectional communication channel from the accelerator to the host."""
382
383 def __init__(self, owner: BundlePort, cpp_port: cpp.ReadChannelPort):
384 super().__init__(owner, cpp_port)
385 self.cpp_port: cpp.ReadChannelPort = cpp_port
386
387 def read(self) -> object:
388 """Read a typed message from the channel. Returns a deserialized object of a
389 type defined by the port type."""
390
391 buffer = self.cpp_port.read()
392 (msg, leftover) = self.type.deserialize(buffer)
393 if len(leftover) != 0:
394 raise ValueError(f"leftover bytes: {leftover}")
395 return msg
396
397
399 """A collections of named, unidirectional communication channels."""
400
401 # When creating a new port, we need to determine if it is a service port and
402 # instantiate it correctly.
403 def __new__(cls, owner: HWModule, cpp_port: cpp.BundlePort):
404 # TODO: add a proper registration mechanism for service ports.
405 if isinstance(cpp_port, cpp.Function):
406 return super().__new__(FunctionPort)
407 if isinstance(cpp_port, cpp.Callback):
408 return super().__new__(CallbackPort)
409 if isinstance(cpp_port, cpp.MMIORegion):
410 return super().__new__(MMIORegion)
411 if isinstance(cpp_port, cpp.Telemetry):
412 return super().__new__(TelemetryPort)
413 return super().__new__(cls)
414
415 def __init__(self, owner: HWModule, cpp_port: cpp.BundlePort):
416 self.owner = owner
417 self.cpp_port = cpp_port
418
419 def write_port(self, channel_name: str) -> WritePort:
420 return WritePort(self, self.cpp_port.getWrite(channel_name))
421
422 def read_port(self, channel_name: str) -> ReadPort:
423 return ReadPort(self, self.cpp_port.getRead(channel_name))
424
425
426class MessageFuture(Future):
427 """A specialization of `Future` for ESI messages. Wraps the cpp object and
428 deserializes the result. Hopefully overrides all the methods necessary for
429 proper operation, which is assumed to be not all of them."""
430
431 def __init__(self, result_type: Type, cpp_future: cpp.MessageDataFuture):
432 self.result_type = result_type
433 self.cpp_future = cpp_future
434
435 def running(self) -> bool:
436 return True
437
438 def done(self) -> bool:
439 return self.cpp_future.valid()
440
441 def result(self, timeout: Optional[Union[int, float]] = None) -> Any:
442 # TODO: respect timeout
443 self.cpp_future.wait()
444 result_bytes = self.cpp_future.get()
445 (msg, leftover) = self.result_type.deserialize(result_bytes)
446 if len(leftover) != 0:
447 raise ValueError(f"leftover bytes: {leftover}")
448 return msg
449
450 def add_done_callback(self, fn: Callable[[Future], object]) -> None:
451 raise NotImplementedError("add_done_callback is not implemented")
452
453
455 """A region of memory-mapped I/O space. This is a collection of named
456 channels, which are either read or read-write. The channels are accessed
457 by name, and can be connected to the host."""
458
459 def __init__(self, owner: HWModule, cpp_port: cpp.MMIORegion):
460 super().__init__(owner, cpp_port)
461 self.region = cpp_port
462
463 @property
464 def descriptor(self) -> cpp.MMIORegionDesc:
465 return self.region.descriptor
466
467 def read(self, offset: int) -> bytearray:
468 """Read a value from the MMIO region at the given offset."""
469 return self.region.read(offset)
470
471 def write(self, offset: int, data: bytearray) -> None:
472 """Write a value to the MMIO region at the given offset."""
473 self.region.write(offset, data)
474
475
477 """A pair of channels which carry the input and output of a function."""
478
479 def __init__(self, owner: HWModule, cpp_port: cpp.BundlePort):
480 super().__init__(owner, cpp_port)
481 self.arg_type = self.write_port("arg").type
482 self.result_type = self.read_port("result").type
483 self.connected = False
484
485 def connect(self):
486 self.cpp_port.connect()
487 self.connected = True
488
489 def call(self, *args: Any, **kwargs: Any) -> Future:
490 """Call the function with the given argument and returns a future of the
491 result."""
492
493 # Accept either positional or keyword arguments, but not both.
494 if len(args) > 0 and len(kwargs) > 0:
495 raise ValueError("cannot use both positional and keyword arguments")
496
497 # Handle arguments: for single positional arg, unwrap it from tuple
498 if len(args) == 1:
499 selected = args[0]
500 elif len(args) > 1:
501 selected = args
502 else:
503 selected = kwargs
504
505 valid, reason = self.arg_type.is_valid(selected)
506 if not valid:
507 raise ValueError(
508 f"'{selected}' cannot be converted to '{self.arg_type}': {reason}")
509 arg_bytes: bytearray = self.arg_type.serialize(selected)
510 cpp_future = self.cpp_port.call(arg_bytes)
511 return MessageFuture(self.result_type, cpp_future)
512
513 def __call__(self, *args: Any, **kwds: Any) -> Future:
514 return self.call(*args, **kwds)
515
516
518 """Callback ports are the inverse of function ports -- instead of calls to the
519 accelerator, they get called from the accelerator. Specify the function which
520 you'd like the accelerator to call when you call `connect`."""
521
522 def __init__(self, owner: HWModule, cpp_port: cpp.BundlePort):
523 super().__init__(owner, cpp_port)
524 self.arg_type = self.read_port("arg").type
525 self.result_type = self.write_port("result").type
526 self.connected = False
527
528 def connect(self, cb: Callable[[Any], Any]):
529
530 def type_convert_wrapper(cb: Callable[[Any], Any],
531 msg: bytearray) -> Optional[bytearray]:
532 try:
533 (obj, leftover) = self.arg_type.deserialize(msg)
534 if len(leftover) != 0:
535 raise ValueError(f"leftover bytes: {leftover}")
536 result = cb(obj)
537 if result is None:
538 return None
539 return self.result_type.serialize(result)
540 except Exception as e:
541 traceback.print_exception(e)
542 return None
543
544 self.cpp_port.connect(lambda x: type_convert_wrapper(cb=cb, msg=x))
545 self.connected = True
546
547
549 """Telemetry ports report an individual piece of information from the
550 acceelerator. The method of accessing telemetry will likely change in the
551 future."""
552
553 def __init__(self, owner: HWModule, cpp_port: cpp.BundlePort):
554 super().__init__(owner, cpp_port)
555 self.connected = False
556
557 def connect(self):
558 self.cpp_port.connect()
559 self.connected = True
560
561 def read(self) -> Future:
562 cpp_future = self.cpp_port.read()
563 return MessageFuture(self.cpp_port.type, cpp_future)
Tuple[bool, Optional[str]] is_valid(self, obj)
Definition types.py:302
_init_from_cpp(self, cpp.ArrayType cpp_type)
Definition types.py:292
Tuple[List[Any], bytearray] deserialize(self, bytearray data)
Definition types.py:319
__init__(self, str id, "ESIType" element_type, int size)
Definition types.py:289
bytearray serialize(self, list lst)
Definition types.py:313
bytearray serialize(self, Union[bytearray, bytes, List[int]] obj)
Definition types.py:145
int bit_width(self)
Definition types.py:142
Tuple[bytearray, bytearray] deserialize(self, bytearray data)
Definition types.py:152
Tuple[bool, Optional[str]] is_valid(self, obj)
Definition types.py:131
__init__(self, str id, int width)
Definition types.py:128
__new__(cls, HWModule owner, cpp.BundlePort cpp_port)
Definition types.py:403
WritePort write_port(self, str channel_name)
Definition types.py:419
ReadPort read_port(self, str channel_name)
Definition types.py:422
__init__(self, HWModule owner, cpp.BundlePort cpp_port)
Definition types.py:415
__init__(self, HWModule owner, cpp.BundlePort cpp_port)
Definition types.py:522
connect(self, Callable[[Any], Any] cb)
Definition types.py:528
int bit_width(self)
Definition types.py:74
Tuple[bool, Optional[str]] supports_host(self)
Definition types.py:60
Tuple[object, bytearray] deserialize(self, bytearray data)
Definition types.py:90
_init_from_cpp(self, cpp.Type cpp_type)
Definition types.py:55
__init__(self, str id)
Definition types.py:45
Tuple[bool, Optional[str]] is_valid(self, obj)
Definition types.py:68
int max_size(self)
Definition types.py:79
wrap_cpp(cls, cpp.Type cpp_type)
Definition types.py:49
str __str__(self)
Definition types.py:95
bytearray serialize(self, obj)
Definition types.py:86
Future call(self, *Any args, **Any kwargs)
Definition types.py:489
__init__(self, HWModule owner, cpp.BundlePort cpp_port)
Definition types.py:479
Future __call__(self, *Any args, **Any kwds)
Definition types.py:513
int bit_width(self)
Definition types.py:165
__init__(self, str id, int width)
Definition types.py:161
None write(self, int offset, bytearray data)
Definition types.py:471
bytearray read(self, int offset)
Definition types.py:467
__init__(self, HWModule owner, cpp.MMIORegion cpp_port)
Definition types.py:459
cpp.MMIORegionDesc descriptor(self)
Definition types.py:464
Any result(self, Optional[Union[int, float]] timeout=None)
Definition types.py:441
__init__(self, Type result_type, cpp.MessageDataFuture cpp_future)
Definition types.py:431
None add_done_callback(self, Callable[[Future], object] fn)
Definition types.py:450
__init__(self, BundlePort owner, cpp.ChannelPort cpp_port)
Definition types.py:335
connect(self, Optional[int] buffer_size=None)
Definition types.py:340
__init__(self, BundlePort owner, cpp.ReadChannelPort cpp_port)
Definition types.py:383
object read(self)
Definition types.py:387
Tuple[int, bytearray] deserialize(self, bytearray data)
Definition types.py:217
Tuple[bool, Optional[str]] is_valid(self, obj)
Definition types.py:200
bytearray serialize(self, int obj)
Definition types.py:214
__init__(self, str id, int width)
Definition types.py:197
__init__(self, str id, List[Tuple[str, "ESIType"]] fields)
Definition types.py:227
bytearray serialize(self, obj)
Definition types.py:263
Tuple[Dict[str, Any], bytearray] deserialize(self, bytearray data)
Definition types.py:274
Tuple[bool, Optional[str]] is_valid(self, obj)
Definition types.py:245
_init_from_cpp(self, cpp.StructType cpp_type)
Definition types.py:232
__init__(self, HWModule owner, cpp.BundlePort cpp_port)
Definition types.py:553
__init__(self, str id, int width)
Definition types.py:171
Tuple[int, bytearray] deserialize(self, bytearray data)
Definition types.py:187
Tuple[bool, Optional[str]] is_valid(self, obj)
Definition types.py:174
bytearray serialize(self, int obj)
Definition types.py:184
int bit_width(self)
Definition types.py:110
Tuple[object, bytearray] deserialize(self, bytearray data)
Definition types.py:117
Tuple[bool, Optional[str]] is_valid(self, obj)
Definition types.py:104
__init__(self, str id)
Definition types.py:101
bytearray serialize(self, obj)
Definition types.py:113
bool try_write(self, msg=None)
Definition types.py:374
bytearray __serialize_msg(self, msg=None)
Definition types.py:359
bool write(self, msg=None)
Definition types.py:367
__init__(self, BundlePort owner, cpp.WriteChannelPort cpp_port)
Definition types.py:355
_get_esi_type(cpp.Type cpp_type)
Definition types.py:28