uc-toffee 2.0.0__py3-none-any.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.
Files changed (51) hide show
  1. toffee/__init__.py +267 -0
  2. toffee/__main__.py +3 -0
  3. toffee/_artifacts.py +56 -0
  4. toffee/_asyncio_observer.py +164 -0
  5. toffee/_cleanup.py +27 -0
  6. toffee/_context.py +26 -0
  7. toffee/_coverage_capture.py +43 -0
  8. toffee/_coverage_exclusions.py +119 -0
  9. toffee/_coverage_runtime.py +623 -0
  10. toffee/_driver_runtime.py +36 -0
  11. toffee/_native_coverage.py +238 -0
  12. toffee/_pytest.py +384 -0
  13. toffee/_scoreboard_matching.py +41 -0
  14. toffee/agent.py +298 -0
  15. toffee/async_drivers.py +384 -0
  16. toffee/backend.py +1552 -0
  17. toffee/cli.py +58 -0
  18. toffee/combinators.py +79 -0
  19. toffee/components.py +97 -0
  20. toffee/coverage.py +1850 -0
  21. toffee/coverage_report.py +659 -0
  22. toffee/data.py +844 -0
  23. toffee/declarative/__init__.py +22 -0
  24. toffee/declarative/_compiler.py +422 -0
  25. toffee/declarative/_declarations.py +404 -0
  26. toffee/declarative/_runtime.py +232 -0
  27. toffee/decorators.py +181 -0
  28. toffee/events.py +83 -0
  29. toffee/execution.py +285 -0
  30. toffee/external.py +79 -0
  31. toffee/interfaces.py +166 -0
  32. toffee/ir.py +682 -0
  33. toffee/monitors.py +259 -0
  34. toffee/protocols.py +106 -0
  35. toffee/py.typed +0 -0
  36. toffee/reactor.py +391 -0
  37. toffee/scoreboard.py +963 -0
  38. toffee/signals.py +131 -0
  39. toffee/staged_drivers.py +174 -0
  40. toffee/subscriptions.py +64 -0
  41. toffee/sync_drivers.py +94 -0
  42. toffee/templates/coverage_site.css +3 -0
  43. toffee/templates/unified_coverage.html +108 -0
  44. toffee/transfers.py +220 -0
  45. toffee/triggers.py +129 -0
  46. uc_toffee-2.0.0.dist-info/METADATA +117 -0
  47. uc_toffee-2.0.0.dist-info/RECORD +51 -0
  48. uc_toffee-2.0.0.dist-info/WHEEL +5 -0
  49. uc_toffee-2.0.0.dist-info/entry_points.txt +3 -0
  50. uc_toffee-2.0.0.dist-info/licenses/LICENSE +127 -0
  51. uc_toffee-2.0.0.dist-info/top_level.txt +1 -0
toffee/__init__.py ADDED
@@ -0,0 +1,267 @@
1
+ from .backend import (
2
+ BackendCapabilities,
3
+ BackendHandle,
4
+ BackendHit,
5
+ MemoryBackend,
6
+ RunLimit,
7
+ RunResult,
8
+ StopReason,
9
+ SimulationBackend,
10
+ XCommClockBackend,
11
+ )
12
+ from ._asyncio_observer import SimulationNotSettledError
13
+ from .decorators import PythonTriggerDefinition, TriggerDefinition, pytrigger, xtrigger
14
+ from .components import Driver, Monitor, SignalDriver
15
+ from .agent import Agent
16
+ from .scoreboard import (
17
+ AcceptedDriver,
18
+ CheckContext,
19
+ Difference,
20
+ MissingExpectedError,
21
+ ObservedMonitor,
22
+ Scoreboard,
23
+ ScoreboardAssociationError,
24
+ ScoreboardError,
25
+ ScoreboardIncompleteError,
26
+ ScoreboardMismatch,
27
+ ScoreboardMode,
28
+ ScoreboardStatus,
29
+ ScoreboardTimeoutError,
30
+ structural_differences,
31
+ )
32
+ from .transfers import MISSING, TransferState, XTransfer
33
+ from .coverage import (
34
+ Bin,
35
+ BinKind,
36
+ BinSpec,
37
+ CoverageDatabase,
38
+ CoverageMergeError,
39
+ CoverageSample,
40
+ CoverageSchemaError,
41
+ CoverGroup,
42
+ CoverGroupDef,
43
+ CoverPointDef,
44
+ CrossDef,
45
+ Iff,
46
+ IllegalBinError,
47
+ IllegalHit,
48
+ IllegalPolicy,
49
+ OverlapPolicy,
50
+ )
51
+ from .data import (
52
+ Bundle,
53
+ BundleValue,
54
+ Field,
55
+ PackedArray,
56
+ PackedLayout,
57
+ PackedView,
58
+ split_packed,
59
+ )
60
+ from .async_drivers import AsyncDriver, AsyncSingleCycleDriver, DriverIncompleteError
61
+ from .sync_drivers import SyncDriver, SyncSingleCycleDriver
62
+ from .staged_drivers import AsyncMultiCycleDriver, DriveStage, SyncMultiCycleDriver
63
+ from .combinators import AllOf, AnyOf
64
+ from .events import (
65
+ ConditionEvent,
66
+ EdgeEvent,
67
+ FsmEvent,
68
+ LogicValue,
69
+ PhaseEvent,
70
+ XEvent,
71
+ XEventKind,
72
+ XPhase,
73
+ )
74
+ from .ir import (
75
+ bits,
76
+ BitsType,
77
+ BoolType,
78
+ FSM,
79
+ FsmSpec,
80
+ Hold,
81
+ Next,
82
+ Sequence,
83
+ SequenceSpec,
84
+ State,
85
+ Wait,
86
+ Within,
87
+ XExpr,
88
+ )
89
+ from .external import AsyncioEventTrigger, QueueTrigger, TaskComplete, WallTimeout
90
+ from .interfaces import Decoupled, ReadyValid, Role, Transfer
91
+ from .monitors import MonitorClosedError, MonitorOverflowError, ReadyValidMonitor, SamplingMonitor
92
+ from .reactor import (
93
+ Registration,
94
+ Subscription,
95
+ SubscriptionOverflowError,
96
+ Reactor,
97
+ )
98
+ from .subscriptions import (
99
+ BoundSubscriptionSpec,
100
+ XSubscriptionSpec,
101
+ on,
102
+ )
103
+ from .execution import Execution
104
+ from .protocols import drive_ready_valid
105
+ from .signals import as_xdata
106
+ from .triggers import (
107
+ ClockCycles,
108
+ CompiledTrigger,
109
+ ConditionMode,
110
+ DriveStable,
111
+ FallingEdge,
112
+ PhaseTrigger,
113
+ PythonPredicateTrigger,
114
+ RisingEdge,
115
+ SimTimeout,
116
+ Value,
117
+ ValueChange,
118
+ XTrigger,
119
+ )
120
+
121
+ __all__ = [
122
+ "bits",
123
+ "BitsType",
124
+ "BoolType",
125
+ "Agent",
126
+ "SimulationNotSettledError",
127
+ "AcceptedDriver",
128
+ "BackendHandle",
129
+ "BackendCapabilities",
130
+ "BackendHit",
131
+ "Bundle",
132
+ "BundleValue",
133
+ "Bin",
134
+ "BinKind",
135
+ "BinSpec",
136
+ "BoundSubscriptionSpec",
137
+ "AllOf",
138
+ "AnyOf",
139
+ "AsyncioEventTrigger",
140
+ "ClockCycles",
141
+ "CheckContext",
142
+ "CompiledTrigger",
143
+ "ConditionMode",
144
+ "ConditionEvent",
145
+ "CoverageDatabase",
146
+ "CoverageMergeError",
147
+ "CoverageSample",
148
+ "CoverageSchemaError",
149
+ "build_unified_coverage_model",
150
+ "CoverGroup",
151
+ "CoverGroupDef",
152
+ "CoverPointDef",
153
+ "CrossDef",
154
+ "Decoupled",
155
+ "DriveStable",
156
+ "Difference",
157
+ "Driver",
158
+ "SignalDriver",
159
+ "SyncDriver",
160
+ "SyncSingleCycleDriver",
161
+ "SyncMultiCycleDriver",
162
+ "EdgeEvent",
163
+ "FallingEdge",
164
+ "Field",
165
+ "FSM",
166
+ "FsmSpec",
167
+ "FsmEvent",
168
+ "generate_unified_coverage_report",
169
+ "generate_unified_coverage_site",
170
+ "Hold",
171
+ "Next",
172
+ "LogicValue",
173
+ "Iff",
174
+ "IllegalBinError",
175
+ "IllegalHit",
176
+ "IllegalPolicy",
177
+ "MemoryBackend",
178
+ "MISSING",
179
+ "MissingExpectedError",
180
+ "MonitorOverflowError",
181
+ "MonitorClosedError",
182
+ "Monitor",
183
+ "ObservedMonitor",
184
+ "OverlapPolicy",
185
+ "PackedArray",
186
+ "PackedLayout",
187
+ "PackedView",
188
+ "parse_lcov",
189
+ "PhaseEvent",
190
+ "PhaseTrigger",
191
+ "PythonPredicateTrigger",
192
+ "QueueTrigger",
193
+ "AsyncDriver",
194
+ "DriverIncompleteError",
195
+ "AsyncSingleCycleDriver",
196
+ "AsyncMultiCycleDriver",
197
+ "DriveStage",
198
+ "Registration",
199
+ "render_unified_coverage_html",
200
+ "ReadyValid",
201
+ "ReadyValidMonitor",
202
+ "SamplingMonitor",
203
+ "RisingEdge",
204
+ "RunLimit",
205
+ "RunResult",
206
+ "Role",
207
+ "Scoreboard",
208
+ "ScoreboardAssociationError",
209
+ "ScoreboardError",
210
+ "ScoreboardIncompleteError",
211
+ "ScoreboardMismatch",
212
+ "ScoreboardMode",
213
+ "ScoreboardStatus",
214
+ "ScoreboardTimeoutError",
215
+ "StopReason",
216
+ "Sequence",
217
+ "SequenceSpec",
218
+ "Execution",
219
+ "SimulationBackend",
220
+ "SimTimeout",
221
+ "State",
222
+ "Subscription",
223
+ "SubscriptionOverflowError",
224
+ "TaskComplete",
225
+ "Transfer",
226
+ "TransferState",
227
+ "Value",
228
+ "ValueChange",
229
+ "WallTimeout",
230
+ "Wait",
231
+ "Within",
232
+ "XEvent",
233
+ "XEventKind",
234
+ "XExpr",
235
+ "XPhase",
236
+ "Reactor",
237
+ "XCommClockBackend",
238
+ "XTrigger",
239
+ "XTransfer",
240
+ "XSubscriptionSpec",
241
+ "on",
242
+ "as_xdata",
243
+ "pytrigger",
244
+ "split_packed",
245
+ "structural_differences",
246
+ "xtrigger",
247
+ "TriggerDefinition",
248
+ "PythonTriggerDefinition",
249
+ "drive_ready_valid",
250
+ ]
251
+
252
+
253
+ _LAZY_REPORT_EXPORTS = {
254
+ "build_unified_coverage_model",
255
+ "generate_unified_coverage_report",
256
+ "generate_unified_coverage_site",
257
+ "parse_lcov",
258
+ "render_unified_coverage_html",
259
+ }
260
+
261
+
262
+ def __getattr__(name: str):
263
+ if name in _LAZY_REPORT_EXPORTS:
264
+ from . import coverage_report
265
+
266
+ return getattr(coverage_report, name)
267
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
toffee/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
toffee/_artifacts.py ADDED
@@ -0,0 +1,56 @@
1
+ """Atomic files and identities for the runner's exclusively owned directory."""
2
+ from __future__ import annotations
3
+
4
+ import gzip
5
+ from hashlib import file_digest
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import tempfile
10
+ from typing import Any
11
+
12
+
13
+ def fingerprint(path: Path) -> dict[str, Any]:
14
+ if path.is_symlink() or not path.is_file() or path.resolve() != path:
15
+ raise ValueError(f"artifact is not a regular non-symlink file: {path}")
16
+ with path.open("rb") as stream:
17
+ digest = file_digest(stream, "sha256").hexdigest()
18
+ return {"path": str(path), "size": path.stat().st_size, "sha256": digest}
19
+
20
+
21
+ def write(path: Path, value: Any, *, compressed: bool = False) -> dict[str, Any]:
22
+ path.parent.mkdir(parents=True, exist_ok=True)
23
+ descriptor, name = tempfile.mkstemp(prefix=".pending-", dir=path.parent)
24
+ pending = Path(name)
25
+ try:
26
+ with os.fdopen(descriptor, "wb") as raw:
27
+ if compressed:
28
+ with gzip.GzipFile(fileobj=raw, mode="wb", filename="", mtime=0) as stream:
29
+ for part in json.JSONEncoder(separators=(",", ":")).iterencode(value):
30
+ stream.write(part.encode())
31
+ else:
32
+ raw.write((json.dumps(value, indent=2, ensure_ascii=False) + "\n").encode())
33
+ raw.flush()
34
+ os.fsync(raw.fileno())
35
+ pending.replace(path)
36
+ sync_directory(path.parent)
37
+ finally:
38
+ pending.unlink(missing_ok=True)
39
+ return fingerprint(path)
40
+
41
+
42
+ def sync_directory(path: Path) -> None:
43
+ descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY)
44
+ try:
45
+ os.fsync(descriptor)
46
+ finally:
47
+ os.close(descriptor)
48
+
49
+
50
+ def read_checked(row: dict[str, Any], root: Path, *, compressed: bool = False) -> Any:
51
+ path = Path(row["path"])
52
+ if not path.is_relative_to(root) or fingerprint(path) != row:
53
+ raise ValueError(f"artifact identity or ownership mismatch: {path}")
54
+ opener = gzip.open if compressed else open
55
+ with opener(path, "rt", encoding="utf-8") as stream:
56
+ return json.load(stream)
@@ -0,0 +1,164 @@
1
+ """Observe native asyncio callbacks; never select or execute tasks ourselves."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from contextvars import ContextVar
7
+ from typing import Any
8
+
9
+
10
+ current_domain: ContextVar[CallbackDomain | None] = ContextVar(
11
+ "toffee_callback_domain", default=None,
12
+ )
13
+ _observers: dict[asyncio.AbstractEventLoop, _LoopObserver] = {}
14
+ _MISSING = object()
15
+
16
+
17
+ class SimulationNotSettledError(RuntimeError):
18
+ """Immediate simulation work exceeded the configured settling budget."""
19
+
20
+
21
+ class _PendingCall:
22
+ __slots__ = ("domain", "callback", "handle")
23
+
24
+ def __init__(self, domain: CallbackDomain, callback: Any) -> None:
25
+ self.domain = domain
26
+ self.callback = callback
27
+ self.handle: asyncio.Handle | None = None
28
+
29
+ def __call__(self, *args: Any) -> None:
30
+ try:
31
+ self.callback(*args)
32
+ finally:
33
+ self.domain.pending.discard(self)
34
+ # Break the Handle -> callback -> Handle cycle on the hot path.
35
+ self.handle = None
36
+
37
+ def __repr__(self) -> str:
38
+ return f"observed({self.callback!r})"
39
+
40
+
41
+ class _LoopObserver:
42
+ def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
43
+ if not isinstance(loop, asyncio.BaseEventLoop):
44
+ raise RuntimeError(
45
+ "Execution callback observation requires an asyncio.BaseEventLoop; "
46
+ f"unsupported loop: {type(loop).__module__}.{type(loop).__name__}"
47
+ )
48
+ self.loop = loop
49
+ self.original = original = loop.call_soon
50
+ self.original_attribute = vars(loop).get("call_soon", _MISSING)
51
+ self.users: set[CallbackDomain] = set()
52
+
53
+ def call_soon(callback, *args, context=None):
54
+ domain = current_domain.get() if context is None else context.get(current_domain)
55
+ if domain is None or domain.closed or domain.observer is not self:
56
+ # Task yields commonly have no arguments. Avoid allocating an
57
+ # unpacked keyword call on every unrelated host continuation.
58
+ if not args:
59
+ return original(callback, context=context)
60
+ return original(callback, *args, context=context)
61
+ # Let the original entry validate invalid callbacks, including its
62
+ # debug-only coroutine check. Wrapping must not hide those errors.
63
+ if not callable(callback) or (
64
+ loop.get_debug()
65
+ and (asyncio.iscoroutine(callback) or asyncio.iscoroutinefunction(callback))
66
+ ):
67
+ return original(callback, *args, context=context)
68
+ call = _PendingCall(domain, callback)
69
+ domain.pending.add(call)
70
+ try:
71
+ if not args:
72
+ handle = original(call, context=context)
73
+ elif len(args) == 1:
74
+ handle = original(call, args[0], context=context)
75
+ else:
76
+ handle = original(call, *args, context=context)
77
+ except BaseException:
78
+ domain.pending.discard(call)
79
+ raise
80
+ call.handle = handle
81
+ return handle
82
+
83
+ self.hook = call_soon
84
+ try:
85
+ loop.call_soon = call_soon
86
+ except (AttributeError, TypeError) as error:
87
+ raise RuntimeError("event loop does not support callback observation") from error
88
+
89
+ @classmethod
90
+ def attach(cls, domain: CallbackDomain) -> _LoopObserver:
91
+ loop = asyncio.get_running_loop()
92
+ observer = _observers.get(loop)
93
+ if observer is None:
94
+ observer = cls(loop)
95
+ _observers[loop] = observer
96
+ else:
97
+ observer.check()
98
+ observer.users.add(domain)
99
+ return observer
100
+
101
+ def check(self) -> None:
102
+ if self.loop.call_soon is not self.hook:
103
+ raise RuntimeError(
104
+ "loop.call_soon changed during Execution; "
105
+ "callback settlement can no longer be guaranteed"
106
+ )
107
+
108
+ def detach(self, domain: CallbackDomain) -> None:
109
+ self.users.discard(domain)
110
+ if self.users:
111
+ return
112
+ # A host-installed replacement belongs to the host, even if it wraps us.
113
+ if self.loop.call_soon is self.hook:
114
+ if self.original_attribute is _MISSING:
115
+ delattr(self.loop, "call_soon")
116
+ else:
117
+ self.loop.call_soon = self.original_attribute
118
+ _observers.pop(self.loop, None)
119
+
120
+
121
+ class CallbackDomain:
122
+ def __init__(self, max_settle_rounds: int) -> None:
123
+ self.pending: set[_PendingCall] = set()
124
+ self.closed = False
125
+ self.max_settle_rounds = max_settle_rounds
126
+ self._detached = False
127
+ self.observer = _LoopObserver.attach(self)
128
+
129
+ async def settle(self, backend: Any) -> None:
130
+ self.observer.check()
131
+ rounds = 0
132
+ while self.pending and not self.closed:
133
+ # Only our registered Handles are inspected, never the loop queue
134
+ # or Task internals. A cancelled Handle won't call our finally.
135
+ for call in tuple(self.pending):
136
+ if call.handle is not None and call.handle.cancelled():
137
+ self.pending.discard(call)
138
+ call.handle = None
139
+ call.callback = None
140
+ if not self.pending:
141
+ return
142
+ if rounds >= self.max_settle_rounds:
143
+ callbacks = ", ".join(repr(call) for call in list(self.pending)[:3])
144
+ phase = getattr(backend, "phase", None)
145
+ raise SimulationNotSettledError(
146
+ f"simulation did not settle at tick={getattr(backend, 'tick', '?')} "
147
+ f"phase={getattr(phase, 'name', phase)} after {rounds} rounds; "
148
+ f"{len(self.pending)} pending callbacks: {callbacks}"
149
+ )
150
+ await asyncio.sleep(0)
151
+ rounds += 1
152
+ self.observer.check()
153
+
154
+ def stop(self) -> None:
155
+ self.closed = True
156
+ # Scheduled callbacks still run normally, with their original owner.
157
+ self.pending.clear()
158
+
159
+ def close(self) -> None:
160
+ if self._detached:
161
+ return
162
+ self.stop()
163
+ self.observer.detach(self)
164
+ self._detached = True
toffee/_cleanup.py ADDED
@@ -0,0 +1,27 @@
1
+ """Preserve the original failure while reporting independent cleanup errors."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def raise_cleanup_errors(
7
+ message: str, errors: list[BaseException], primary: BaseException | None = None,
8
+ ) -> None:
9
+ unique: list[BaseException] = [primary] if primary is not None else []
10
+
11
+ def retain(error: BaseException) -> None:
12
+ if any(error is previous for previous in unique):
13
+ return
14
+ if isinstance(error, BaseExceptionGroup):
15
+ for nested in error.exceptions:
16
+ retain(nested)
17
+ else:
18
+ unique.append(error)
19
+
20
+ for error in errors:
21
+ retain(error)
22
+ if len(unique) == 1:
23
+ if unique[0] is not primary:
24
+ raise unique[0]
25
+ elif unique:
26
+ raise BaseExceptionGroup(message, unique)
27
+ # With only the primary failure, let the owning context propagate it.
toffee/_context.py ADDED
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from contextvars import ContextVar, Token
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from .reactor import Reactor
8
+
9
+ _current_reactor: ContextVar["Reactor | None"] = ContextVar(
10
+ "toffee_current_reactor", default=None
11
+ )
12
+
13
+
14
+ def current_reactor() -> "Reactor":
15
+ reactor = _current_reactor.get()
16
+ if reactor is None:
17
+ raise RuntimeError("XTrigger must be awaited inside an active Execution")
18
+ return reactor
19
+
20
+
21
+ def bind_reactor(reactor: "Reactor | None") -> Token["Reactor | None"]:
22
+ return _current_reactor.set(reactor)
23
+
24
+
25
+ def reset_reactor(token: Token["Reactor | None"]) -> None:
26
+ _current_reactor.reset(token)
@@ -0,0 +1,43 @@
1
+ """Private, runner-owned capture. Ordinary library use has no global collector."""
2
+ from __future__ import annotations
3
+
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar
6
+ from typing import Any
7
+ from weakref import WeakSet
8
+
9
+ _sink: ContextVar[Any] = ContextVar("toffee_coverage_capture", default=None)
10
+ _suspended: ContextVar[bool] = ContextVar("toffee_coverage_capture_suspended", default=False)
11
+ _live: WeakSet[Any] = WeakSet()
12
+
13
+
14
+ def register(group: Any) -> None:
15
+ if _suspended.get():
16
+ return
17
+ _live.add(group)
18
+ sink = _sink.get()
19
+ if sink is not None:
20
+ sink.register(group)
21
+
22
+
23
+ def live_groups():
24
+ return tuple(_live)
25
+
26
+
27
+ @contextmanager
28
+ def capture(sink: Any):
29
+ token = _sink.set(sink)
30
+ try:
31
+ yield
32
+ finally:
33
+ _sink.reset(token)
34
+
35
+
36
+ @contextmanager
37
+ def suspended():
38
+ token = _suspended.set(True)
39
+ try:
40
+ with capture(None):
41
+ yield
42
+ finally:
43
+ _suspended.reset(token)