cocotb 2.0.1__cp38-cp38-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cocotb/_ANSI.py +65 -0
- cocotb/__init__.py +127 -0
- cocotb/_base_triggers.py +515 -0
- cocotb/_bridge.py +186 -0
- cocotb/_decorators.py +515 -0
- cocotb/_deprecation.py +36 -0
- cocotb/_exceptions.py +7 -0
- cocotb/_extended_awaitables.py +419 -0
- cocotb/_gpi_triggers.py +385 -0
- cocotb/_init.py +301 -0
- cocotb/_outcomes.py +54 -0
- cocotb/_profiling.py +46 -0
- cocotb/_py_compat.py +150 -0
- cocotb/_scheduler.py +448 -0
- cocotb/_test.py +248 -0
- cocotb/_test_factory.py +312 -0
- cocotb/_test_functions.py +42 -0
- cocotb/_typing.py +7 -0
- cocotb/_utils.py +274 -0
- cocotb/_version.py +4 -0
- cocotb/_xunit_reporter.py +103 -0
- cocotb/clock.py +419 -0
- cocotb/debug.py +24 -0
- cocotb/handle.py +1752 -0
- cocotb/libs/cocotb.dll +0 -0
- cocotb/libs/cocotb.exp +0 -0
- cocotb/libs/cocotb.lib +0 -0
- cocotb/libs/cocotbfli_modelsim.dll +0 -0
- cocotb/libs/cocotbfli_modelsim.exp +0 -0
- cocotb/libs/cocotbfli_modelsim.lib +0 -0
- cocotb/libs/cocotbutils.dll +0 -0
- cocotb/libs/cocotbutils.exp +0 -0
- cocotb/libs/cocotbutils.lib +0 -0
- cocotb/libs/cocotbvhpi_aldec.dll +0 -0
- cocotb/libs/cocotbvhpi_aldec.exp +0 -0
- cocotb/libs/cocotbvhpi_aldec.lib +0 -0
- cocotb/libs/cocotbvhpi_modelsim.dll +0 -0
- cocotb/libs/cocotbvhpi_modelsim.exp +0 -0
- cocotb/libs/cocotbvhpi_modelsim.lib +0 -0
- cocotb/libs/cocotbvhpi_nvc.dll +0 -0
- cocotb/libs/cocotbvhpi_nvc.exp +0 -0
- cocotb/libs/cocotbvhpi_nvc.lib +0 -0
- cocotb/libs/cocotbvpi_aldec.dll +0 -0
- cocotb/libs/cocotbvpi_aldec.exp +0 -0
- cocotb/libs/cocotbvpi_aldec.lib +0 -0
- cocotb/libs/cocotbvpi_ghdl.dll +0 -0
- cocotb/libs/cocotbvpi_ghdl.exp +0 -0
- cocotb/libs/cocotbvpi_ghdl.lib +0 -0
- cocotb/libs/cocotbvpi_icarus.exp +0 -0
- cocotb/libs/cocotbvpi_icarus.lib +0 -0
- cocotb/libs/cocotbvpi_icarus.vpl +0 -0
- cocotb/libs/cocotbvpi_modelsim.dll +0 -0
- cocotb/libs/cocotbvpi_modelsim.exp +0 -0
- cocotb/libs/cocotbvpi_modelsim.lib +0 -0
- cocotb/libs/embed.dll +0 -0
- cocotb/libs/embed.exp +0 -0
- cocotb/libs/embed.lib +0 -0
- cocotb/libs/gpi.dll +0 -0
- cocotb/libs/gpi.exp +0 -0
- cocotb/libs/gpi.lib +0 -0
- cocotb/libs/gpilog.dll +0 -0
- cocotb/libs/gpilog.exp +0 -0
- cocotb/libs/gpilog.lib +0 -0
- cocotb/libs/pygpilog.dll +0 -0
- cocotb/libs/pygpilog.exp +0 -0
- cocotb/libs/pygpilog.lib +0 -0
- cocotb/logging.py +417 -0
- cocotb/py.typed +0 -0
- cocotb/queue.py +235 -0
- cocotb/regression.py +900 -0
- cocotb/result.py +38 -0
- cocotb/share/def/.gitignore +2 -0
- cocotb/share/def/README.md +4 -0
- cocotb/share/def/aldec.def +61 -0
- cocotb/share/def/aldec.exp +0 -0
- cocotb/share/def/aldec.lib +0 -0
- cocotb/share/def/ghdl.def +43 -0
- cocotb/share/def/ghdl.exp +0 -0
- cocotb/share/def/ghdl.lib +0 -0
- cocotb/share/def/icarus.def +43 -0
- cocotb/share/def/icarus.exp +0 -0
- cocotb/share/def/icarus.lib +0 -0
- cocotb/share/def/modelsim.def +138 -0
- cocotb/share/def/modelsim.exp +0 -0
- cocotb/share/def/modelsim.lib +0 -0
- cocotb/share/def/nvcvhpi.def +18 -0
- cocotb/share/def/nvcvhpi.exp +0 -0
- cocotb/share/def/nvcvhpi.lib +0 -0
- cocotb/share/include/cocotb_utils.h +70 -0
- cocotb/share/include/embed.h +33 -0
- cocotb/share/include/exports.h +20 -0
- cocotb/share/include/gpi.h +459 -0
- cocotb/share/include/gpi_logging.h +291 -0
- cocotb/share/include/py_gpi_logging.h +33 -0
- cocotb/share/include/vhpi_user_ext.h +26 -0
- cocotb/share/include/vpi_user_ext.h +33 -0
- cocotb/share/lib/verilator/verilator.cpp +209 -0
- cocotb/simtime.py +238 -0
- cocotb/simulator.cp38-win32.exp +0 -0
- cocotb/simulator.cp38-win32.lib +0 -0
- cocotb/simulator.cp38-win32.pyd +0 -0
- cocotb/simulator.cp38-win32.pyd.2.config +8 -0
- cocotb/simulator.pyi +107 -0
- cocotb/task.py +590 -0
- cocotb/triggers.py +67 -0
- cocotb/types/__init__.py +31 -0
- cocotb/types/_abstract_array.py +151 -0
- cocotb/types/_array.py +297 -0
- cocotb/types/_indexing.py +17 -0
- cocotb/types/_logic.py +333 -0
- cocotb/types/_logic_array.py +884 -0
- cocotb/types/_range.py +197 -0
- cocotb/types/_resolve.py +76 -0
- cocotb/utils.py +110 -0
- cocotb-2.0.1.dist-info/LICENSE +29 -0
- cocotb-2.0.1.dist-info/METADATA +44 -0
- cocotb-2.0.1.dist-info/RECORD +152 -0
- cocotb-2.0.1.dist-info/WHEEL +5 -0
- cocotb-2.0.1.dist-info/entry_points.txt +2 -0
- cocotb-2.0.1.dist-info/top_level.txt +18 -0
- cocotb_tools/__init__.py +0 -0
- cocotb_tools/_coverage.py +33 -0
- cocotb_tools/_vendor/__init__.py +3 -0
- cocotb_tools/_vendor/distutils_version.py +346 -0
- cocotb_tools/check_results.py +65 -0
- cocotb_tools/combine_results.py +152 -0
- cocotb_tools/config.py +242 -0
- cocotb_tools/ipython_support.py +99 -0
- cocotb_tools/makefiles/Makefile.deprecations +27 -0
- cocotb_tools/makefiles/Makefile.inc +198 -0
- cocotb_tools/makefiles/Makefile.sim +96 -0
- cocotb_tools/makefiles/simulators/Makefile.activehdl +72 -0
- cocotb_tools/makefiles/simulators/Makefile.cvc +61 -0
- cocotb_tools/makefiles/simulators/Makefile.dsim +39 -0
- cocotb_tools/makefiles/simulators/Makefile.ghdl +84 -0
- cocotb_tools/makefiles/simulators/Makefile.icarus +80 -0
- cocotb_tools/makefiles/simulators/Makefile.ius +93 -0
- cocotb_tools/makefiles/simulators/Makefile.modelsim +9 -0
- cocotb_tools/makefiles/simulators/Makefile.nvc +60 -0
- cocotb_tools/makefiles/simulators/Makefile.questa +29 -0
- cocotb_tools/makefiles/simulators/Makefile.questa-compat +143 -0
- cocotb_tools/makefiles/simulators/Makefile.questa-qisqrun +149 -0
- cocotb_tools/makefiles/simulators/Makefile.riviera +144 -0
- cocotb_tools/makefiles/simulators/Makefile.vcs +65 -0
- cocotb_tools/makefiles/simulators/Makefile.verilator +79 -0
- cocotb_tools/makefiles/simulators/Makefile.xcelium +104 -0
- cocotb_tools/py.typed +0 -0
- cocotb_tools/runner.py +2001 -0
- cocotb_tools/sim_versions.py +140 -0
- pygpi/__init__.py +0 -0
- pygpi/entry.py +42 -0
- pygpi/py.typed +0 -0
cocotb/_outcomes.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inspired by https://github.com/python-trio/outcome
|
|
3
|
+
|
|
4
|
+
An outcome is similar to the built-in :any:`concurrent.futures.Future`
|
|
5
|
+
or :any:`asyncio.Future`, but without being tied to a particular task model.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import abc
|
|
9
|
+
from typing import Callable, Generic, TypeVar
|
|
10
|
+
|
|
11
|
+
from cocotb._py_compat import ParamSpec
|
|
12
|
+
from cocotb._utils import remove_traceback_frames
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
P = ParamSpec("P")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def capture(
|
|
19
|
+
fn: "Callable[P, T]", *args: "P.args", **kwargs: "P.kwargs"
|
|
20
|
+
) -> "Outcome[T]":
|
|
21
|
+
"""Obtain an `Outcome` representing the result of a function call."""
|
|
22
|
+
try:
|
|
23
|
+
return Value(fn(*args, **kwargs))
|
|
24
|
+
except BaseException as e:
|
|
25
|
+
e = remove_traceback_frames(e, ["capture"])
|
|
26
|
+
return Error(e)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Outcome(Generic[T]):
|
|
30
|
+
@abc.abstractmethod
|
|
31
|
+
def get(self) -> T:
|
|
32
|
+
"""Get the value of this outcome, or throw its exception."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Value(Outcome[T]):
|
|
36
|
+
def __init__(self, value: T):
|
|
37
|
+
self.value = value
|
|
38
|
+
|
|
39
|
+
def get(self) -> T:
|
|
40
|
+
return self.value
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f"Value({self.value!r})"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Error(Outcome[T]):
|
|
47
|
+
def __init__(self, error: BaseException) -> None:
|
|
48
|
+
self.error = error
|
|
49
|
+
|
|
50
|
+
def get(self) -> T:
|
|
51
|
+
raise self.error
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
return f"Error({self.error!r})"
|
cocotb/_profiling.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright cocotb contributors
|
|
2
|
+
# Licensed under the Revised BSD License, see LICENSE for details.
|
|
3
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Debug mode controlled by environment variables
|
|
7
|
+
import cProfile
|
|
8
|
+
import os
|
|
9
|
+
import pstats
|
|
10
|
+
|
|
11
|
+
from cocotb._py_compat import AbstractContextManager, nullcontext
|
|
12
|
+
|
|
13
|
+
profiling_context: AbstractContextManager[None, None]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if "COCOTB_ENABLE_PROFILING" in os.environ:
|
|
17
|
+
_profile: cProfile.Profile
|
|
18
|
+
|
|
19
|
+
def initialize() -> None:
|
|
20
|
+
global _profile
|
|
21
|
+
_profile = cProfile.Profile()
|
|
22
|
+
|
|
23
|
+
def finalize() -> None:
|
|
24
|
+
ps = pstats.Stats(_profile).sort_stats("cumulative")
|
|
25
|
+
ps.dump_stats("cocotb.pstat")
|
|
26
|
+
|
|
27
|
+
class _profiling_context(AbstractContextManager[None, None]):
|
|
28
|
+
"""Context manager that profiles its contents"""
|
|
29
|
+
|
|
30
|
+
def __enter__(self) -> None:
|
|
31
|
+
_profile.enable()
|
|
32
|
+
|
|
33
|
+
def __exit__(self, *excinfo: object) -> None:
|
|
34
|
+
_profile.disable()
|
|
35
|
+
|
|
36
|
+
profiling_context = _profiling_context()
|
|
37
|
+
|
|
38
|
+
else:
|
|
39
|
+
|
|
40
|
+
def initialize() -> None:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def finalize() -> None:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
profiling_context = nullcontext()
|
cocotb/_py_compat.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Copyright cocotb contributors
|
|
2
|
+
# Licensed under the Revised BSD License, see LICENSE for details.
|
|
3
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
4
|
+
"""
|
|
5
|
+
Backports and compatibility shims for newer python features.
|
|
6
|
+
|
|
7
|
+
These are for internal use - users should use a third party library like `six`
|
|
8
|
+
if they want to use these shims in their own code
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from abc import ABC
|
|
13
|
+
from contextlib import AbstractContextManager
|
|
14
|
+
from typing import TypeVar, Union, overload
|
|
15
|
+
|
|
16
|
+
__all__ = (
|
|
17
|
+
"AbstractContextManager",
|
|
18
|
+
"Final",
|
|
19
|
+
"Literal",
|
|
20
|
+
"ParamSpec",
|
|
21
|
+
"Protocol",
|
|
22
|
+
"Self",
|
|
23
|
+
"StrEnum",
|
|
24
|
+
"TypeAlias",
|
|
25
|
+
"cached_property",
|
|
26
|
+
"insertion_ordered_dict",
|
|
27
|
+
"nullcontext",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
|
|
32
|
+
if sys.version_info < (3, 9):
|
|
33
|
+
from typing import Generic
|
|
34
|
+
|
|
35
|
+
T_co = TypeVar("T_co", covariant=True)
|
|
36
|
+
ExitT_co = TypeVar("ExitT_co", covariant=True)
|
|
37
|
+
|
|
38
|
+
class AbstractContextManager(AbstractContextManager, Generic[T_co, ExitT_co]): ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# backport of Python 3.7's contextlib.nullcontext
|
|
42
|
+
class nullcontext(AbstractContextManager[T, None]):
|
|
43
|
+
"""Context manager that does no additional processing.
|
|
44
|
+
Used as a stand-in for a normal context manager, when a particular
|
|
45
|
+
block of code is only sometimes used with a normal context manager:
|
|
46
|
+
|
|
47
|
+
cm = optional_cm if condition else nullcontext()
|
|
48
|
+
with cm:
|
|
49
|
+
# Perform operation, using optional_cm if condition is True
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
enter_result: T
|
|
53
|
+
|
|
54
|
+
@overload
|
|
55
|
+
def __init__(self: "nullcontext[None]", enter_result: None = None) -> None: ...
|
|
56
|
+
|
|
57
|
+
@overload
|
|
58
|
+
def __init__(self: "nullcontext[T]", enter_result: T) -> None: ...
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self: "nullcontext[Union[T, None]]", enter_result: Union[T, None] = None
|
|
62
|
+
) -> None:
|
|
63
|
+
self.enter_result = enter_result
|
|
64
|
+
|
|
65
|
+
def __enter__(self) -> T:
|
|
66
|
+
return self.enter_result
|
|
67
|
+
|
|
68
|
+
def __exit__(self, *excinfo: object) -> None:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# On python 3.7 onwards, `dict` is guaranteed to preserve insertion order.
|
|
73
|
+
# Since `OrderedDict` is a little slower that `dict`, we prefer the latter
|
|
74
|
+
# when possible.
|
|
75
|
+
if sys.version_info[:2] >= (3, 7): # noqa: UP036 | bug in ruff
|
|
76
|
+
insertion_ordered_dict = dict
|
|
77
|
+
else:
|
|
78
|
+
import collections
|
|
79
|
+
|
|
80
|
+
insertion_ordered_dict = collections.OrderedDict
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# simple, but less than optimal backport of Python 3.8's cached_property
|
|
84
|
+
if sys.version_info >= (3, 8):
|
|
85
|
+
from functools import cached_property
|
|
86
|
+
else:
|
|
87
|
+
from functools import update_wrapper
|
|
88
|
+
|
|
89
|
+
class cached_property:
|
|
90
|
+
def __init__(self, method):
|
|
91
|
+
self._method = method
|
|
92
|
+
update_wrapper(self, method)
|
|
93
|
+
|
|
94
|
+
def __get__(self, instance, owner=None):
|
|
95
|
+
if instance is None:
|
|
96
|
+
return self
|
|
97
|
+
res = self._method(instance)
|
|
98
|
+
instance.__dict__[self._method.__name__] = res
|
|
99
|
+
return res
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if sys.version_info >= (3, 8):
|
|
103
|
+
from typing import Final, Literal, Protocol
|
|
104
|
+
else:
|
|
105
|
+
from typing import Any
|
|
106
|
+
|
|
107
|
+
class FakeGetItemMetatype(type):
|
|
108
|
+
def __getitem__(cls, a: object) -> Any:
|
|
109
|
+
return Any
|
|
110
|
+
|
|
111
|
+
class FakeGetItemType(metaclass=FakeGetItemMetatype): ...
|
|
112
|
+
|
|
113
|
+
Final = FakeGetItemType
|
|
114
|
+
Literal = FakeGetItemType
|
|
115
|
+
Protocol = ABC
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if sys.version_info >= (3, 10):
|
|
119
|
+
from typing import ParamSpec, TypeAlias
|
|
120
|
+
else:
|
|
121
|
+
from typing import Any
|
|
122
|
+
|
|
123
|
+
TypeAlias = Any
|
|
124
|
+
|
|
125
|
+
class FakeParamSpecType:
|
|
126
|
+
def __init__(self, name: str) -> None: ...
|
|
127
|
+
|
|
128
|
+
def kwargs(self) -> Any:
|
|
129
|
+
return Any
|
|
130
|
+
|
|
131
|
+
def args(self) -> Any:
|
|
132
|
+
return Any
|
|
133
|
+
|
|
134
|
+
ParamSpec = FakeParamSpecType
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if sys.version_info >= (3, 11):
|
|
138
|
+
from typing import Self
|
|
139
|
+
else:
|
|
140
|
+
Self = ""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if sys.version_info >= (3, 11):
|
|
144
|
+
from enum import StrEnum
|
|
145
|
+
else:
|
|
146
|
+
from enum import Enum
|
|
147
|
+
|
|
148
|
+
class StrEnum(str, Enum):
|
|
149
|
+
def __str__(self) -> str:
|
|
150
|
+
return self.value
|
cocotb/_scheduler.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# Copyright cocotb contributors
|
|
2
|
+
# Copyright (c) 2013, 2018 Potential Ventures Ltd
|
|
3
|
+
# Copyright (c) 2013 SolarFlare Communications Inc
|
|
4
|
+
# Licensed under the Revised BSD License, see LICENSE for details.
|
|
5
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
6
|
+
|
|
7
|
+
"""Task scheduler."""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import threading
|
|
11
|
+
from bdb import BdbQuit
|
|
12
|
+
from collections import OrderedDict
|
|
13
|
+
from typing import Any, Callable, Coroutine, Dict, List, TypeVar, Union
|
|
14
|
+
|
|
15
|
+
import cocotb
|
|
16
|
+
import cocotb._gpi_triggers
|
|
17
|
+
import cocotb.handle
|
|
18
|
+
from cocotb import debug
|
|
19
|
+
from cocotb._base_triggers import Event, Trigger
|
|
20
|
+
from cocotb._bridge import external_state, external_waiter
|
|
21
|
+
from cocotb._exceptions import InternalError
|
|
22
|
+
from cocotb._gpi_triggers import (
|
|
23
|
+
GPITrigger,
|
|
24
|
+
NextTimeStep,
|
|
25
|
+
ReadWrite,
|
|
26
|
+
)
|
|
27
|
+
from cocotb._outcomes import Error, Outcome, Value, capture
|
|
28
|
+
from cocotb._profiling import profiling_context
|
|
29
|
+
from cocotb._py_compat import ParamSpec, insertion_ordered_dict
|
|
30
|
+
from cocotb.task import Task, _TaskState
|
|
31
|
+
|
|
32
|
+
T = TypeVar("T")
|
|
33
|
+
|
|
34
|
+
P = ParamSpec("P")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Scheduler:
|
|
38
|
+
"""The main Task scheduler.
|
|
39
|
+
|
|
40
|
+
How It Generally Works:
|
|
41
|
+
Tasks are `queued` to run in the scheduler with :meth:`_queue`.
|
|
42
|
+
Queueing adds the Task and an Outcome value to :attr:`_pending_tasks`.
|
|
43
|
+
The main scheduling loop is located in :meth:`_event_loop` and loops over the queued Tasks and `schedules` them.
|
|
44
|
+
:meth:`_schedule` schedules a Task -
|
|
45
|
+
continuing its execution from where it previously yielded control -
|
|
46
|
+
by injecting the Outcome value associated with the Task from the queue.
|
|
47
|
+
The Task's body will run until it finishes or reaches the next :keyword:`await` statement.
|
|
48
|
+
If a Task reaches an :keyword:`await`, :meth:`_schedule` will convert the value yielded from the Task into a Trigger with :meth:`_trigger_from_any` and its friend methods.
|
|
49
|
+
Triggers are then `primed` (with :meth:`~cocotb.triggers.Trigger._prime`)
|
|
50
|
+
with a `react` function (:meth:`_sim_react` or :meth:`_react)
|
|
51
|
+
so as to wake up Tasks waiting for that Trigger to `fire` (when the event encoded by the Trigger occurs).
|
|
52
|
+
This is accomplished by :meth:`_resume_task_upon`.
|
|
53
|
+
:meth:`_resume_task_upon` also associates the Trigger with the Task waiting on it to fire by adding them to the :attr:`_trigger2tasks` map.
|
|
54
|
+
If, instead of reaching an :keyword:`await`, a Task finishes, :meth:`_schedule` will cause the :class:`~cocotb.task.Join` trigger to fire.
|
|
55
|
+
Once a Trigger fires it calls the react function which queues all Tasks waiting for that Trigger to fire.
|
|
56
|
+
Then the process repeats.
|
|
57
|
+
|
|
58
|
+
When a Task is cancelled (:meth:`_unschedule`), it is removed from the Task queue if it is currently queued.
|
|
59
|
+
Also, the Task and Trigger are deassociated in the :attr:`_trigger2tasks` map.
|
|
60
|
+
If the cancelled Task is the last Task waiting on a Trigger, that Trigger is `unprimed` to prevent it from firing.
|
|
61
|
+
|
|
62
|
+
Simulator Phases:
|
|
63
|
+
All GPITriggers (triggers that are fired by the simulator) go through :meth:`_sim_react`
|
|
64
|
+
which looks at the fired GPITriggers to determine and track the current simulator phase cocotb is executing in.
|
|
65
|
+
|
|
66
|
+
Normal phase:
|
|
67
|
+
Corresponds to all non-ReadWrite and non-ReadOnly phases.
|
|
68
|
+
Any writes are cached for the next ReadWrite phase and do not happen immediately.
|
|
69
|
+
Scheduling :class:`~cocotb.triggers.ReadWrite` and :class:`~cocotb.triggers.ReadOnly` are valid.
|
|
70
|
+
|
|
71
|
+
ReadWrite phase:
|
|
72
|
+
Corresponds to ``cbReadWriteSynch`` (VPI) or ``vhpiCbRepLastKnownDeltaCycle`` (VHPI).
|
|
73
|
+
At the start of scheduling in this phase we play back all the *previously* cached write updates.
|
|
74
|
+
Any writes are cached for the next ReadWrite phase and do not happen immediately.
|
|
75
|
+
Scheduling :class:`~cocotb.triggers.ReadWrite` and :class:`~cocotb.triggers.ReadOnly` are valid.
|
|
76
|
+
One caveat is that scheduling a :class:`~cocotb.triggers.ReadWrite` while in this phase may not be valid.
|
|
77
|
+
If there were no writes applied at the beginning of this phase, there will be no more events in this time step,
|
|
78
|
+
and there will not be another ReadWrite phase in this time step.
|
|
79
|
+
Simulators generally handle this caveat gracefully by leaving you in the ReadWrite phase of the next time step.
|
|
80
|
+
|
|
81
|
+
ReadOnly phase
|
|
82
|
+
Corresponds to ``cbReadOnlySynch`` (VPI) or ``vhpiCbRepEndOfTimeStep`` (VHPI).
|
|
83
|
+
In this state we are not allowed to perform writes.
|
|
84
|
+
Scheduling :class:`~cocotb.triggers.ReadWrite` and :class:`~cocotb.triggers.ReadOnly` are *not* valid.
|
|
85
|
+
|
|
86
|
+
Caveats and Special Cases:
|
|
87
|
+
The scheduler treats Tests specially.
|
|
88
|
+
If a Test finishes or a Task ends with an Exception, the scheduler is put into a `terminating` state.
|
|
89
|
+
All currently queued Tasks are cancelled and all pending Triggers are unprimed.
|
|
90
|
+
This is currently spread out between :meth:`_handle_termination` and :meth:`_cleanup`.
|
|
91
|
+
In that mix of functions, the :attr:`_test_complete_cb` callback is called to inform whomever (the regression_manager) the test finished.
|
|
92
|
+
The scheduler also is responsible for starting the next Test in the Normal phase by priming a ``Timer(1)`` with the second half of test completion handling.
|
|
93
|
+
|
|
94
|
+
The scheduler is currently where simulator time phase is tracked.
|
|
95
|
+
This is mostly because this is where :meth:`_sim_react` is most conveniently located.
|
|
96
|
+
The scheduler can't currently be made independent of simulator-specific code because of the above special cases which have to respect simulator phasing.
|
|
97
|
+
|
|
98
|
+
Currently Task cancellation is accomplished with :meth:`Task.kill() <cocotb.task.Task.kill>`.
|
|
99
|
+
This function immediately cancels the Task by re-entering the scheduler.
|
|
100
|
+
This can cause issues if you are trying to cancel the Test Task or the currently executing Task.
|
|
101
|
+
|
|
102
|
+
TODO: There are attributes and methods for dealing with "externals", but I'm not quite sure how it all works yet.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
# Singleton events, recycled to avoid spurious object creation
|
|
106
|
+
_next_time_step = NextTimeStep()
|
|
107
|
+
_read_write = ReadWrite()
|
|
108
|
+
|
|
109
|
+
def __init__(self) -> None:
|
|
110
|
+
self.log = logging.getLogger("cocotb.scheduler")
|
|
111
|
+
|
|
112
|
+
# A dictionary of pending tasks for each trigger,
|
|
113
|
+
# indexed by trigger
|
|
114
|
+
self._trigger2tasks: Dict[Trigger, list[Task[object]]] = (
|
|
115
|
+
insertion_ordered_dict()
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self._scheduled_tasks: OrderedDict[Task[object], Union[BaseException, None]] = (
|
|
119
|
+
OrderedDict()
|
|
120
|
+
)
|
|
121
|
+
self._pending_threads: List[external_waiter[Any]] = []
|
|
122
|
+
self._pending_events: List[Event] = []
|
|
123
|
+
|
|
124
|
+
self._main_thread = threading.current_thread()
|
|
125
|
+
|
|
126
|
+
self._current_task: Union[Task[object], None] = None
|
|
127
|
+
|
|
128
|
+
def _sim_react(self, trigger: GPITrigger) -> None:
|
|
129
|
+
"""Called when a :class:`~cocotb.triggers.GPITrigger` fires.
|
|
130
|
+
|
|
131
|
+
This is often the entry point into Python from the simulator,
|
|
132
|
+
so this function is in charge of enabling profiling.
|
|
133
|
+
It must also track the current simulator time phase,
|
|
134
|
+
and start the unstarted event loop.
|
|
135
|
+
"""
|
|
136
|
+
with profiling_context:
|
|
137
|
+
# TODO: move state tracking to global variable
|
|
138
|
+
# and handle this via some kind of trigger-specific Python callback
|
|
139
|
+
cocotb._gpi_triggers._current_gpi_trigger = trigger
|
|
140
|
+
|
|
141
|
+
# apply inertial writes if ReadWrite
|
|
142
|
+
if trigger is self._read_write:
|
|
143
|
+
cocotb.handle._apply_scheduled_writes()
|
|
144
|
+
|
|
145
|
+
self._react(trigger)
|
|
146
|
+
self._event_loop()
|
|
147
|
+
|
|
148
|
+
def _react(self, trigger: Trigger) -> None:
|
|
149
|
+
"""Called when a :class:`~cocotb.triggers.Trigger` fires.
|
|
150
|
+
|
|
151
|
+
Finds all Tasks waiting on the Trigger that fired and queues them.
|
|
152
|
+
"""
|
|
153
|
+
if debug.debug:
|
|
154
|
+
self.log.debug("Trigger fired: %s", trigger)
|
|
155
|
+
|
|
156
|
+
# find all tasks waiting on trigger that fired
|
|
157
|
+
try:
|
|
158
|
+
scheduling = self._trigger2tasks.pop(trigger)
|
|
159
|
+
except KeyError:
|
|
160
|
+
# GPI triggers should only be ever pending if there is an
|
|
161
|
+
# associated task waiting on that trigger, otherwise it would
|
|
162
|
+
# have been unprimed already
|
|
163
|
+
if isinstance(trigger, GPITrigger):
|
|
164
|
+
self.log.warning(
|
|
165
|
+
"No tasks waiting on GPITrigger that fired: %s\n"
|
|
166
|
+
"This is due to an issue with the GPI or a simulator bug.",
|
|
167
|
+
trigger,
|
|
168
|
+
)
|
|
169
|
+
# For Python triggers this isn't actually an error - we might do
|
|
170
|
+
# event.set() without knowing whether any tasks are actually
|
|
171
|
+
# waiting on this event, for example
|
|
172
|
+
elif debug.debug:
|
|
173
|
+
self.log.debug("No tasks waiting on trigger that fired: %s", trigger)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if debug.debug:
|
|
177
|
+
debugstr = "\n\t".join([str(task) for task in scheduling])
|
|
178
|
+
if len(scheduling) > 0:
|
|
179
|
+
debugstr = "\n\t" + debugstr
|
|
180
|
+
self.log.debug(
|
|
181
|
+
"%d pending tasks for trigger %s%s",
|
|
182
|
+
len(scheduling),
|
|
183
|
+
trigger,
|
|
184
|
+
debugstr,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# queue all tasks to wake up
|
|
188
|
+
for task in scheduling:
|
|
189
|
+
# unset trigger
|
|
190
|
+
task._trigger = None
|
|
191
|
+
self._schedule_task_internal(task)
|
|
192
|
+
|
|
193
|
+
# cleanup trigger
|
|
194
|
+
trigger._cleanup()
|
|
195
|
+
|
|
196
|
+
def _event_loop(self) -> None:
|
|
197
|
+
"""Run the main event loop.
|
|
198
|
+
|
|
199
|
+
This should only be started by:
|
|
200
|
+
* The beginning of a test, when there is no trigger to react to
|
|
201
|
+
* A GPI trigger
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
while self._scheduled_tasks:
|
|
205
|
+
task, exc = self._scheduled_tasks.popitem(last=False)
|
|
206
|
+
|
|
207
|
+
if debug.debug:
|
|
208
|
+
self.log.debug("Scheduling task %s", task)
|
|
209
|
+
self._resume_task(task, exc)
|
|
210
|
+
if debug.debug:
|
|
211
|
+
self.log.debug("Scheduled task %s", task)
|
|
212
|
+
|
|
213
|
+
# remove our reference to the objects at the end of each loop,
|
|
214
|
+
# to try and avoid them being destroyed at a weird time (as
|
|
215
|
+
# happened in gh-957)
|
|
216
|
+
del task
|
|
217
|
+
|
|
218
|
+
# Schedule may have queued up some events so we'll burn through those
|
|
219
|
+
while self._pending_events:
|
|
220
|
+
if debug.debug:
|
|
221
|
+
self.log.debug(
|
|
222
|
+
"Scheduling pending event %s", self._pending_events[0]
|
|
223
|
+
)
|
|
224
|
+
self._pending_events.pop(0).set()
|
|
225
|
+
|
|
226
|
+
# no more pending tasks
|
|
227
|
+
if debug.debug:
|
|
228
|
+
self.log.debug("All tasks scheduled, handing control back to simulator")
|
|
229
|
+
|
|
230
|
+
def _unschedule(self, task: Task[Any]) -> None:
|
|
231
|
+
"""Unschedule a task and unprime dangling pending triggers.
|
|
232
|
+
|
|
233
|
+
Also:
|
|
234
|
+
* enters the scheduler termination state if the Test Task is unscheduled.
|
|
235
|
+
* creates and fires a :class:`~cocotb.task.Join` trigger.
|
|
236
|
+
* forcefully ends the Test if a Task ends with an exception.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
# remove task from queue
|
|
240
|
+
if task in self._scheduled_tasks:
|
|
241
|
+
self._scheduled_tasks.pop(task)
|
|
242
|
+
|
|
243
|
+
# Unprime the trigger this task is waiting on
|
|
244
|
+
trigger = task._trigger
|
|
245
|
+
if trigger is not None:
|
|
246
|
+
task._trigger = None
|
|
247
|
+
if task in self._trigger2tasks.setdefault(trigger, []):
|
|
248
|
+
self._trigger2tasks[trigger].remove(task)
|
|
249
|
+
if not self._trigger2tasks[trigger]:
|
|
250
|
+
trigger._unprime()
|
|
251
|
+
del self._trigger2tasks[trigger]
|
|
252
|
+
|
|
253
|
+
def _schedule_task_upon(self, task: Task[Any], trigger: Trigger) -> None:
|
|
254
|
+
"""Schedule `task` to be resumed when `trigger` fires."""
|
|
255
|
+
# TODO Move this all into Task
|
|
256
|
+
task._trigger = trigger
|
|
257
|
+
task._state = _TaskState.PENDING
|
|
258
|
+
|
|
259
|
+
trigger_tasks = self._trigger2tasks.setdefault(trigger, [])
|
|
260
|
+
trigger_tasks.append(task)
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
# TODO maybe associate the react method with the trigger object so
|
|
264
|
+
# we don't have to do a type check here.
|
|
265
|
+
if isinstance(trigger, GPITrigger):
|
|
266
|
+
trigger._prime(self._sim_react)
|
|
267
|
+
else:
|
|
268
|
+
trigger._prime(self._react)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
# discard the trigger we associated, it will never fire
|
|
271
|
+
self._trigger2tasks.pop(trigger)
|
|
272
|
+
|
|
273
|
+
# replace it with a new trigger that throws back the exception
|
|
274
|
+
self._schedule_task_internal(task, e)
|
|
275
|
+
|
|
276
|
+
def _schedule_task(self, task: Task[Any]) -> None:
|
|
277
|
+
"""Queue *task* for scheduling.
|
|
278
|
+
|
|
279
|
+
It is an error to attempt to queue a task that has already been queued.
|
|
280
|
+
"""
|
|
281
|
+
if task.done():
|
|
282
|
+
raise RuntimeError(
|
|
283
|
+
f"{task} has finished executing and can not be scheduled again. Did you call start_soon() on a finished Task?"
|
|
284
|
+
)
|
|
285
|
+
if task in self._scheduled_tasks:
|
|
286
|
+
return
|
|
287
|
+
for tasks in self._trigger2tasks.values():
|
|
288
|
+
if task in tasks:
|
|
289
|
+
return
|
|
290
|
+
self._schedule_task_internal(task)
|
|
291
|
+
|
|
292
|
+
def _schedule_task_internal(
|
|
293
|
+
self, task: Task[Any], exc: Union[BaseException, None] = None
|
|
294
|
+
) -> None:
|
|
295
|
+
# TODO Move state tracking into Task
|
|
296
|
+
task._state = _TaskState.SCHEDULED
|
|
297
|
+
self._scheduled_tasks[task] = exc
|
|
298
|
+
|
|
299
|
+
def _queue_function(self, task: Coroutine[Trigger, None, T]) -> T:
|
|
300
|
+
"""Queue a task for execution and move the containing thread
|
|
301
|
+
so that it does not block execution of the main thread any longer.
|
|
302
|
+
"""
|
|
303
|
+
# We should be able to find ourselves inside the _pending_threads list
|
|
304
|
+
matching_threads = [
|
|
305
|
+
t for t in self._pending_threads if t.thread == threading.current_thread()
|
|
306
|
+
]
|
|
307
|
+
if len(matching_threads) == 0:
|
|
308
|
+
raise RuntimeError("queue_function called from unrecognized thread")
|
|
309
|
+
|
|
310
|
+
# Raises if there is more than one match. This can never happen, since
|
|
311
|
+
# each entry always has a unique thread.
|
|
312
|
+
(t,) = matching_threads
|
|
313
|
+
|
|
314
|
+
outcome: Union[Outcome[T], None] = None
|
|
315
|
+
|
|
316
|
+
async def wrapper() -> None:
|
|
317
|
+
nonlocal outcome
|
|
318
|
+
# This function runs in the scheduler thread
|
|
319
|
+
try:
|
|
320
|
+
outcome = Value(await task)
|
|
321
|
+
except (KeyboardInterrupt, SystemExit, BdbQuit):
|
|
322
|
+
# Allow these to bubble up to the execution root to fail the sim immediately.
|
|
323
|
+
# This follows asyncio's behavior.
|
|
324
|
+
raise
|
|
325
|
+
except BaseException as e:
|
|
326
|
+
outcome = Error(e)
|
|
327
|
+
# Notify the current (scheduler) thread that we are about to wake
|
|
328
|
+
# up the background (`@external`) thread, making sure to do so
|
|
329
|
+
# before the background thread gets a chance to go back to sleep by
|
|
330
|
+
# calling thread_suspend.
|
|
331
|
+
# We need to do this here in the scheduler thread so that no more
|
|
332
|
+
# tasks run until the background thread goes back to sleep.
|
|
333
|
+
t.thread_resume()
|
|
334
|
+
event.set()
|
|
335
|
+
|
|
336
|
+
event = threading.Event()
|
|
337
|
+
# must register this with test as there's no way to clean up with threading
|
|
338
|
+
self._schedule_task_internal(cocotb.start_soon(wrapper()))
|
|
339
|
+
# The scheduler thread blocks in `thread_wait`, and is woken when we
|
|
340
|
+
# call `thread_suspend` - so we need to make sure the task is
|
|
341
|
+
# queued before that.
|
|
342
|
+
t.thread_suspend()
|
|
343
|
+
# This blocks the calling `@external` thread until the task finishes
|
|
344
|
+
event.wait()
|
|
345
|
+
assert outcome is not None
|
|
346
|
+
return outcome.get()
|
|
347
|
+
|
|
348
|
+
def _run_in_executor(
|
|
349
|
+
self, func: "Callable[P, T]", *args: "P.args", **kwargs: "P.kwargs"
|
|
350
|
+
) -> Coroutine[Trigger, None, T]:
|
|
351
|
+
"""Run the task in a separate execution thread
|
|
352
|
+
and return an awaitable object for the caller.
|
|
353
|
+
"""
|
|
354
|
+
# Create a thread
|
|
355
|
+
# Create a trigger that is called as a result of the thread finishing
|
|
356
|
+
# Create an Event object that the caller can await on
|
|
357
|
+
# Event object set when the thread finishes execution, this blocks the
|
|
358
|
+
# calling task (but not the thread) until the external completes
|
|
359
|
+
|
|
360
|
+
waiter = external_waiter[T]()
|
|
361
|
+
|
|
362
|
+
def execute_external() -> None:
|
|
363
|
+
waiter._outcome = capture(func, *args, **kwargs)
|
|
364
|
+
if debug.debug:
|
|
365
|
+
self.log.debug(
|
|
366
|
+
"Execution of external routine done %s", threading.current_thread()
|
|
367
|
+
)
|
|
368
|
+
waiter.thread_done()
|
|
369
|
+
|
|
370
|
+
async def wrapper() -> T:
|
|
371
|
+
thread = threading.Thread(
|
|
372
|
+
group=None,
|
|
373
|
+
target=execute_external,
|
|
374
|
+
name=func.__qualname__ + "_thread",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
waiter.thread = thread
|
|
378
|
+
self._pending_threads.append(waiter)
|
|
379
|
+
|
|
380
|
+
await waiter.event.wait()
|
|
381
|
+
|
|
382
|
+
return waiter.result # raises if there was an exception
|
|
383
|
+
|
|
384
|
+
return wrapper()
|
|
385
|
+
|
|
386
|
+
def _resume_task(self, task: Task[object], exc: Union[BaseException, None]) -> None:
|
|
387
|
+
"""Resume *task* with *outcome*.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
task: The task to schedule.
|
|
391
|
+
outcome: The outcome to inject into the *task*.
|
|
392
|
+
|
|
393
|
+
Scheduling runs *task* until it either finishes or reaches the next :keyword:`await` statement.
|
|
394
|
+
If *task* completes, it is unscheduled, a Join trigger fires, and test completion is inspected.
|
|
395
|
+
Otherwise, it reached an :keyword:`await` and we have a result object which is converted to a trigger,
|
|
396
|
+
that trigger is primed,
|
|
397
|
+
then that trigger and the *task* are registered with the :attr:`_trigger2tasks` map.
|
|
398
|
+
"""
|
|
399
|
+
if self._current_task is not None:
|
|
400
|
+
raise InternalError("_schedule() called while another Task is executing")
|
|
401
|
+
try:
|
|
402
|
+
self._current_task = task
|
|
403
|
+
|
|
404
|
+
trigger = task._advance(exc)
|
|
405
|
+
|
|
406
|
+
if task.done():
|
|
407
|
+
if debug.debug:
|
|
408
|
+
self.log.debug("%s completed with %s", task, task._outcome)
|
|
409
|
+
assert trigger is None
|
|
410
|
+
self._unschedule(task)
|
|
411
|
+
|
|
412
|
+
if not task.done():
|
|
413
|
+
if debug.debug:
|
|
414
|
+
self.log.debug("%r yielded %s", task, trigger)
|
|
415
|
+
if not isinstance(trigger, Trigger):
|
|
416
|
+
e = TypeError(
|
|
417
|
+
f"Coroutine yielded an object of type {type(trigger)}, which the scheduler can't "
|
|
418
|
+
f"handle: {trigger!r}\n"
|
|
419
|
+
)
|
|
420
|
+
self._schedule_task_internal(task, e)
|
|
421
|
+
else:
|
|
422
|
+
self._schedule_task_upon(task, trigger)
|
|
423
|
+
|
|
424
|
+
# We do not return from here until pending threads have completed, but only
|
|
425
|
+
# from the main thread, this seems like it could be problematic in cases
|
|
426
|
+
# where a sim might change what this thread is.
|
|
427
|
+
|
|
428
|
+
if self._main_thread is threading.current_thread():
|
|
429
|
+
for ext in self._pending_threads:
|
|
430
|
+
ext.thread_start()
|
|
431
|
+
if debug.debug:
|
|
432
|
+
self.log.debug(
|
|
433
|
+
"Blocking from %s on %s",
|
|
434
|
+
threading.current_thread(),
|
|
435
|
+
ext.thread,
|
|
436
|
+
)
|
|
437
|
+
state = ext.thread_wait()
|
|
438
|
+
if debug.debug:
|
|
439
|
+
self.log.debug(
|
|
440
|
+
"Back from wait on self %s with newstate %s",
|
|
441
|
+
threading.current_thread(),
|
|
442
|
+
state,
|
|
443
|
+
)
|
|
444
|
+
if state == external_state.EXITED:
|
|
445
|
+
self._pending_threads.remove(ext)
|
|
446
|
+
self._pending_events.append(ext.event)
|
|
447
|
+
finally:
|
|
448
|
+
self._current_task = None
|