nortl 1.4.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 (60) hide show
  1. nortl/__init__.py +85 -0
  2. nortl/components/__init__.py +8 -0
  3. nortl/components/channel.py +132 -0
  4. nortl/components/timer.py +73 -0
  5. nortl/core/__init__.py +40 -0
  6. nortl/core/checker.py +135 -0
  7. nortl/core/common/__init__.py +4 -0
  8. nortl/core/common/access.py +25 -0
  9. nortl/core/common/debug.py +6 -0
  10. nortl/core/common/naming_helper.py +33 -0
  11. nortl/core/constructs/__init__.py +13 -0
  12. nortl/core/constructs/condition.py +143 -0
  13. nortl/core/constructs/fork_join.py +84 -0
  14. nortl/core/constructs/loop.py +138 -0
  15. nortl/core/engine.py +575 -0
  16. nortl/core/exceptions.py +139 -0
  17. nortl/core/manager/__init__.py +6 -0
  18. nortl/core/manager/scratch_manager.py +128 -0
  19. nortl/core/manager/signal_manager.py +71 -0
  20. nortl/core/modifiers.py +136 -0
  21. nortl/core/module.py +181 -0
  22. nortl/core/operations.py +834 -0
  23. nortl/core/parameter.py +88 -0
  24. nortl/core/process.py +451 -0
  25. nortl/core/protocols.py +628 -0
  26. nortl/core/renderers/__init__.py +0 -0
  27. nortl/core/renderers/operations/__init__.py +34 -0
  28. nortl/core/renderers/operations/arithmetics.py +38 -0
  29. nortl/core/renderers/operations/base.py +111 -0
  30. nortl/core/renderers/operations/comparison.py +44 -0
  31. nortl/core/renderers/operations/logic.py +38 -0
  32. nortl/core/renderers/operations/misc.py +26 -0
  33. nortl/core/renderers/operations/slice.py +30 -0
  34. nortl/core/signal.py +878 -0
  35. nortl/core/state.py +201 -0
  36. nortl/py.typed +0 -0
  37. nortl/renderer/__init__.py +5 -0
  38. nortl/renderer/mermaid_renderer.py +38 -0
  39. nortl/renderer/networkx_renderer.py +29 -0
  40. nortl/renderer/verilog_renderer.py +325 -0
  41. nortl/renderer/verilog_utils/__init__.py +6 -0
  42. nortl/renderer/verilog_utils/formatter.py +29 -0
  43. nortl/renderer/verilog_utils/process.py +226 -0
  44. nortl/renderer/verilog_utils/structural.py +146 -0
  45. nortl/renderer/verilog_utils/utils.py +23 -0
  46. nortl/utils/__init__.py +0 -0
  47. nortl/utils/parse_utils.py +37 -0
  48. nortl/utils/templates/testbench.sv +41 -0
  49. nortl/utils/test_wrapper.py +218 -0
  50. nortl/utils/type_aliases.py +15 -0
  51. nortl/verilog_library/__init__.py +74 -0
  52. nortl/verilog_library/nortl_clock_gate.sv +20 -0
  53. nortl/verilog_library/nortl_count_down_timer.sv +50 -0
  54. nortl/verilog_library/nortl_delay.sv +66 -0
  55. nortl/verilog_library/nortl_edge_detector.sv +34 -0
  56. nortl/verilog_library/nortl_sync.sv +28 -0
  57. nortl-1.4.0.dist-info/METADATA +105 -0
  58. nortl-1.4.0.dist-info/RECORD +60 -0
  59. nortl-1.4.0.dist-info/WHEEL +4 -0
  60. nortl-1.4.0.dist-info/licenses/LICENSE +11 -0
nortl/core/signal.py ADDED
@@ -0,0 +1,878 @@
1
+ """Signal definition."""
2
+
3
+ from abc import ABCMeta, abstractmethod
4
+ from collections import OrderedDict
5
+ from types import TracebackType
6
+ from typing import Dict, Final, Generic, Literal, Mapping, Optional, Sequence, Set, Tuple, Type, TypeVar, Union
7
+
8
+ from typing_extensions import Self
9
+
10
+ from nortl.core.checker import StaticAccessChecker
11
+ from nortl.core.common import NamedEntity, StaticAccess
12
+ from nortl.core.exceptions import AccessAfterReleaseError, WriteViolationError
13
+ from nortl.core.modifiers import BaseModifier
14
+ from nortl.core.operations import OperationTrait
15
+ from nortl.core.protocols import (
16
+ ACCESS_CHECKS,
17
+ BIT_ORDER,
18
+ EVENT_TYPES,
19
+ SIGNAL_TYPES,
20
+ AssignmentTarget,
21
+ EngineProto,
22
+ ModuleInstanceProto,
23
+ ParameterProto,
24
+ Renderable,
25
+ SignalProto,
26
+ SignalSliceProto,
27
+ StaticAccessCheckerProto,
28
+ StaticAccessProto,
29
+ ThreadProto,
30
+ )
31
+ from nortl.utils.type_aliases import IntSlice
32
+
33
+ __all__ = [
34
+ 'ScratchSignal',
35
+ 'Signal',
36
+ 'SignalSlice',
37
+ ]
38
+
39
+ T_Signal = TypeVar('T_Signal', SignalProto, SignalSliceProto)
40
+
41
+
42
+ class ParameterizedEvent:
43
+ """Wrapper object for a parametrized event."""
44
+
45
+ def __init__(self, event: EVENT_TYPES):
46
+ self._parameter_dict: OrderedDict[str, Union[int, str, ParameterProto]] = OrderedDict()
47
+ self._event = event
48
+
49
+ def __repr__(self) -> str:
50
+ ret = str(self._event)
51
+ for k, v in self._parameter_dict.items():
52
+ ret += f'{k} {v}'
53
+
54
+ return ret
55
+
56
+ def __hash__(self) -> int:
57
+ return hash(repr(self))
58
+
59
+ def __eq__(self, other: object) -> bool:
60
+ if not isinstance(other, ParameterizedEvent):
61
+ raise NotImplementedError('Can not compare parameterized Events to other types!')
62
+ return repr(self) == repr(other)
63
+
64
+ def add_parameter(self, param: str, value: Union[int, str, ParameterProto]) -> None:
65
+ self._parameter_dict[param] = value
66
+
67
+ def get_parameter(self, param: str) -> Union[int, str, ParameterProto]:
68
+ return self._parameter_dict[param]
69
+
70
+
71
+ def pick_indexes(all_indexes: Sequence[int], index: Union[int, IntSlice]) -> Sequence[int]:
72
+ """Pick indexes covered by this slice."""
73
+ if isinstance(index, slice):
74
+ # noRTL supports reversed indexes, to better match Verilog. They must be flipped here.
75
+ # In addition, the stop index is treated as inclusive
76
+ start = min(index.start, index.stop) # type: ignore[type-var]
77
+ stop = max(index.start, index.stop) + 1 # type: ignore[type-var, operator]
78
+ return all_indexes[start:stop]
79
+ else:
80
+ return (all_indexes[index],)
81
+
82
+
83
+ def list_indexes(index: Union[int, IntSlice]) -> Sequence[int]:
84
+ """List all indexes covered by a slice."""
85
+ if isinstance(index, slice):
86
+ # noRTL supports reversed indexes, to better match Verilog. They must be flipped here.
87
+ # In addition, the stop index is treated as inclusive
88
+ start: int = min(index.start, index.stop) # type: ignore[assignment, type-var]
89
+ stop: int = max(index.start, index.stop) # type: ignore[assignment, type-var]
90
+ return tuple(range(start, stop + 1))
91
+ else:
92
+ return (index,)
93
+
94
+
95
+ def validate_slice(index: IntSlice) -> Tuple[int, int, BIT_ORDER]:
96
+ """Validate a slice and return start and stop values sorted by size."""
97
+ # The stop value for the Python slice is treated as inclusive
98
+ start, stop, step = index.start, index.stop, index.step
99
+
100
+ if start is None:
101
+ raise ValueError('Missing start position for signal slice operation!')
102
+ if stop is None:
103
+ raise ValueError('Missing stop position for signal slice operation!')
104
+ if step is not None:
105
+ raise ValueError('Providing a step size is not supported for signal slice operation!')
106
+
107
+ return min(start, stop), max(start, stop), 'L:H' if stop > start else 'H:L'
108
+
109
+
110
+ class _BaseSignal(OperationTrait, NamedEntity, metaclass=ABCMeta):
111
+ """Abstract base class for signals."""
112
+
113
+ is_primitive: Final = True
114
+
115
+ def __init__(self, name: str):
116
+ super().__init__(name)
117
+
118
+ @property
119
+ def name(self) -> str:
120
+ """Signal name."""
121
+ return self._name
122
+
123
+ # Abstract properties required by base methods
124
+ @property
125
+ @abstractmethod
126
+ def engine(self) -> EngineProto:
127
+ """NoRTL engine that this signal belongs to."""
128
+
129
+ @property
130
+ @abstractmethod
131
+ def width(self) -> Union[int, ParameterProto, Renderable]:
132
+ """Signal width in bits."""
133
+
134
+ @property
135
+ @abstractmethod
136
+ def escaped_name(self) -> str:
137
+ """Name of signal with any special characters escaped."""
138
+
139
+ # Access control
140
+ @property
141
+ @abstractmethod
142
+ def read_accesses(self) -> Set[StaticAccessProto]:
143
+ """Mutable set of read accesses to this signal."""
144
+
145
+ @property
146
+ @abstractmethod
147
+ def write_accesses(self) -> Set[StaticAccessProto]:
148
+ """Mutable set of write accesses to this signal."""
149
+
150
+ @property
151
+ @abstractmethod
152
+ def last_read_access_thread(self) -> Optional[ThreadProto]:
153
+ """Thread performing the last read access."""
154
+
155
+ @last_read_access_thread.setter
156
+ @abstractmethod
157
+ def last_read_access_thread(self, value: ThreadProto) -> None:
158
+ """Thread performing the last read access."""
159
+
160
+ @property
161
+ @abstractmethod
162
+ def last_write_access_thread(self) -> Optional[ThreadProto]:
163
+ """Thread performing the last write access."""
164
+
165
+ @last_write_access_thread.setter
166
+ @abstractmethod
167
+ def last_write_access_thread(self, value: ThreadProto) -> None:
168
+ """Thread performing the last write access."""
169
+
170
+ @property
171
+ @abstractmethod
172
+ def access_checker(self) -> StaticAccessCheckerProto:
173
+ """Static access checker."""
174
+
175
+ def write_access(self, ignore: Set[ACCESS_CHECKS] = set()) -> None:
176
+ """Register write access from the current thread.
177
+
178
+ If the current thread differs from the last write acess, will invoke the access checker.
179
+
180
+ Raises:
181
+ ExclusiveWriteError: If the signals was written by more than one thread.
182
+ NonIdenticalRWError: If the signals was written by one, and read from another thread.
183
+ """
184
+ self.write_accesses.add(StaticAccess(self.engine.current_thread))
185
+
186
+ if self.engine.current_thread is not self.last_write_access_thread:
187
+ # Slow check is only executed, if the thread has changed.
188
+ self.access_checker.check(ignore=ignore)
189
+
190
+ self.last_write_access_thread = self.engine.current_thread
191
+
192
+ def read_access(self, ignore: Set[ACCESS_CHECKS] = set()) -> None:
193
+ """Register read access from the current thread.
194
+
195
+ If the current thread differs from the last write acess, will invoke the access checker.
196
+
197
+ Raises:
198
+ ExclusiveReadError: If the signal was read from more than one thread.
199
+ NonIdenticalRWError: If the signals was written by one, and read from another thread.
200
+ """
201
+ self.read_accesses.add(StaticAccess(self.engine.current_thread))
202
+
203
+ if self.engine.current_thread is not self.last_read_access_thread:
204
+ # Slow check is only executed, if the thread has changed.
205
+ self.access_checker.check(ignore=ignore)
206
+
207
+ self.last_read_access_thread = self.engine.current_thread
208
+
209
+ def free_access_from_thread(self, thread: ThreadProto) -> None:
210
+ """This function disables all access checks that have their origin in the given thread.
211
+
212
+ It is to be used during fork for passing signals to a forked thread. In this case, the signal
213
+ is accessed (written) by the origin thread and handed off to the spawned thread.
214
+
215
+ Since parallel running behavior is described below the actual fork context, a colliding access will
216
+ happen after the fork-block has been executed. The collision will be shown once the origin thread
217
+ will access the signal while the forked thread has not ended.
218
+ """
219
+ for access in self.write_accesses | self.read_accesses:
220
+ if access.thread == thread:
221
+ access.disable()
222
+
223
+
224
+ class _AccessControlledSignal(_BaseSignal):
225
+ """Intermediary class for signals that keep track their own access control.
226
+
227
+ This is used for signals and scratch signals.
228
+ """
229
+
230
+ def __init__(self, name: str) -> None:
231
+ super().__init__(name)
232
+
233
+ self._write_accesses: Set[StaticAccessProto] = set()
234
+ self._read_accesses: Set[StaticAccessProto] = set()
235
+ self._last_read_access_thread: Optional[ThreadProto] = None
236
+ self._last_write_access_thread: Optional[ThreadProto] = None
237
+ self._access_checker = StaticAccessChecker(self)
238
+
239
+ @property
240
+ def read_accesses(self) -> Set[StaticAccessProto]:
241
+ """Mutable set of read accesses to this signal."""
242
+ return self._read_accesses
243
+
244
+ @property
245
+ def write_accesses(self) -> Set[StaticAccessProto]:
246
+ """Mutable set of write accesses to this signal."""
247
+ return self._write_accesses
248
+
249
+ @property
250
+ def last_read_access_thread(self) -> Optional[ThreadProto]:
251
+ """Thread performing the last read access."""
252
+ return self._last_read_access_thread
253
+
254
+ @last_read_access_thread.setter
255
+ def last_read_access_thread(self, value: ThreadProto) -> None:
256
+ """Thread performing the last read access."""
257
+ self._last_read_access_thread = value
258
+
259
+ @property
260
+ def last_write_access_thread(self) -> Optional[ThreadProto]:
261
+ """Thread performing the last write access."""
262
+ return self._last_write_access_thread
263
+
264
+ @last_write_access_thread.setter
265
+ def last_write_access_thread(self, value: ThreadProto) -> None:
266
+ """Thread performing the last write access."""
267
+ self._last_write_access_thread = value
268
+
269
+ @property
270
+ def access_checker(self) -> StaticAccessCheckerProto:
271
+ """Static access checker."""
272
+ return self._access_checker
273
+
274
+
275
+ class _EventSourceSignal(Generic[T_Signal], _BaseSignal):
276
+ """Class for signals that can be used as the source for a event.
277
+
278
+ This is only the case for signals and slice signals, but not for scratch signals, due to their ephemeral nature.
279
+ """
280
+
281
+ def __init__(self, name: str) -> None:
282
+ super().__init__(name)
283
+
284
+ self._events: Dict[ParameterizedEvent, ModuleInstanceProto] = {}
285
+
286
+ @property
287
+ def events(self) -> Mapping[ParameterizedEvent, ModuleInstanceProto]:
288
+ """Events for this signal."""
289
+ return self._events
290
+
291
+ def rising(self) -> T_Signal:
292
+ """Create rising edge event."""
293
+ return self._create_edge_detector().get_connected_signal('RISING') # type: ignore[return-value]
294
+
295
+ def falling(self) -> T_Signal:
296
+ """Create falling edge event."""
297
+ return self._create_edge_detector().get_connected_signal('FALLING') # type: ignore[return-value]
298
+
299
+ def delayed(self, cycles: Union[int, ParameterProto] = 1) -> T_Signal:
300
+ """Create event for delayed signal."""
301
+ return self._create_delay(cycles).get_connected_signal('OUT') # type: ignore[return-value]
302
+
303
+ def synchronized(self) -> T_Signal:
304
+ """Create event for synchronized signal."""
305
+ return self._create_synchronized().get_connected_signal('OUT') # type: ignore[return-value]
306
+
307
+ def _create_edge_detector(self) -> ModuleInstanceProto:
308
+ """Creates the edge detector for the signal if it does not exist.
309
+
310
+ If the signal has more than one bit, this results in ValueError.
311
+ """
312
+ if self.width != 1:
313
+ raise ValueError('Edge dectors can only be used in 1-bit signals!')
314
+
315
+ if (event := ParameterizedEvent('edge')) not in self.events:
316
+ instance_name = f'I_EVENT_EDGE_DETECTOR_{self.escaped_name}'
317
+ instance = self.engine.create_module_instance(module_name='nortl_edge_detector', instance_name=instance_name)
318
+ self._events[ParameterizedEvent('edge')] = instance
319
+
320
+ signal_rising = self.engine.define_local(f'EVENT_{self.escaped_name}_rising')
321
+ signal_falling = self.engine.define_local(f'EVENT_{self.escaped_name}_falling')
322
+
323
+ self.engine.connect_module_port(instance_name, 'SIGNAL', self) # type: ignore[arg-type]
324
+ self.engine.connect_module_port(instance_name, 'RISING', signal_rising)
325
+ self.engine.connect_module_port(instance_name, 'FALLING', signal_falling)
326
+
327
+ return instance
328
+ else:
329
+ return self.events[event]
330
+
331
+ def _create_delay(self, cycles: Union[int, ParameterProto] = 1) -> ModuleInstanceProto:
332
+ """Create a delay for the signal if it does not exist."""
333
+ event = ParameterizedEvent('delay')
334
+ event.add_parameter('cycles', cycles)
335
+
336
+ if event not in self.events:
337
+ instance_name = f'I_DELAY_BY_{cycles}_{self.escaped_name}'
338
+ instance = self.engine.create_module_instance(module_name='nortl_delay', instance_name=instance_name)
339
+ self.engine.override_module_parameter(instance_name, 'DELAY_STEPS', cycles)
340
+ self.engine.override_module_parameter(instance_name, 'DATA_WIDTH', self.width)
341
+
342
+ self._events[event] = instance
343
+
344
+ delayed_signal = self.engine.define_local(f'EVENT_{self.escaped_name}_DELAY_BY_{cycles}', self.width)
345
+
346
+ self.engine.connect_module_port(instance_name, 'IN', self) # type: ignore[arg-type]
347
+ self.engine.connect_module_port(instance_name, 'OUT', delayed_signal)
348
+
349
+ return instance
350
+ else:
351
+ return self.events[event]
352
+
353
+ def _create_synchronized(self) -> ModuleInstanceProto:
354
+ """Create sync module for this signal if it does not exist."""
355
+ if (event := ParameterizedEvent('sync')) not in self.events:
356
+ instance_name = f'I_SYNC_{self.escaped_name}'
357
+ instance = self.engine.create_module_instance(module_name='nortl_sync', instance_name=instance_name)
358
+ self.engine.override_module_parameter(instance_name, 'DATA_WIDTH', self.width)
359
+
360
+ self._events[event] = instance
361
+
362
+ delayed_signal = self.engine.define_local(f'EVENT_{self.escaped_name}_SYNCED')
363
+
364
+ self.engine.connect_module_port(instance_name, 'IN', self) # type: ignore[arg-type]
365
+ self.engine.connect_module_port(instance_name, 'OUT', delayed_signal)
366
+
367
+ return instance
368
+ else:
369
+ return self.events[event]
370
+
371
+
372
+ class Signal(_AccessControlledSignal, _EventSourceSignal[SignalProto]):
373
+ """Signal definition, representing a Verilog signal.
374
+
375
+ Attributes:
376
+ engine: Finite state machine associated with this signal.
377
+ type: The role of the signal (input, output, interface, internal, local).
378
+ name: Name of the signal.
379
+ width: Width in bits of the signal.
380
+ data_type: Data type of the signal. Defaults to 'logic'.
381
+
382
+ The signal types are defined as follows:
383
+ * input, output: Port of the module
384
+ * interface: Use a system verilog interface
385
+ * local: A local register used that is not passed to the outside
386
+ * internal: a signal created by internal data structures, not necessarily visible for the user
387
+ """
388
+
389
+ def __init__(
390
+ self,
391
+ engine: EngineProto,
392
+ type: SIGNAL_TYPES,
393
+ name: str,
394
+ width: Union[int, ParameterProto, Renderable] = 1,
395
+ data_type: str = 'logic',
396
+ is_synchronized: bool = False,
397
+ pulsing: bool = False,
398
+ assignment: Optional[Renderable] = None,
399
+ ) -> None:
400
+ """Initialize a signal.
401
+
402
+ Arguments:
403
+ engine: State machine container object.
404
+ name: Signal name.
405
+ type: Signal type.
406
+ width: Width in bits (default=1).
407
+ data_type: Data type of the signal (default='logic').
408
+ is_synchronized: Indicates, if the signal is synchronous to the local clock domain.
409
+ pulsing: Wether the signal resets automatically to 0 if not written in current state
410
+ assignment: Source expression for combinational assignment.
411
+ """
412
+ if type != 'internal' and name.startswith('_'):
413
+ raise ValueError('Signal names must not start with an underscore!')
414
+ if assignment is not None:
415
+ if type == 'input':
416
+ raise ValueError('Input signals must not have a combinational assignment.')
417
+ if pulsing:
418
+ raise ValueError('Signals with a combinational assignment cannot be pulsing.')
419
+
420
+ super().__init__(name)
421
+
422
+ self._engine = engine
423
+ self._type = type
424
+ self._width = width
425
+ self._operand_width = width if isinstance(width, int) else None
426
+ self._data_type = data_type
427
+ self._is_synchronized = is_synchronized
428
+ self._pulsing = pulsing
429
+ self._assignment = assignment
430
+
431
+ @property
432
+ def engine(self) -> EngineProto:
433
+ """NoRTL engine for this signal."""
434
+ return self._engine
435
+
436
+ @property
437
+ def type(self) -> SIGNAL_TYPES:
438
+ """Signal type."""
439
+ return self._type
440
+
441
+ @property
442
+ def pulsing(self) -> bool:
443
+ """Shows, if the signal is self-resetting to zero after one cycle."""
444
+ return self._pulsing
445
+
446
+ @property
447
+ def assignment(self) -> Optional[Renderable]:
448
+ """Source expression for combination assignment."""
449
+ return self._assignment
450
+
451
+ @property
452
+ def escaped_name(self) -> str:
453
+ """Name of signal with any special characters escaped.
454
+
455
+ For regular signals, this is equal to their name. For slice signals, it contains the position.
456
+ """
457
+ return self.name
458
+
459
+ @property
460
+ def width(self) -> Union[int, ParameterProto, Renderable]:
461
+ """Signal width in bits."""
462
+ return self._width
463
+
464
+ @property
465
+ def operand_width(self) -> Optional[int]:
466
+ """Indicates the width when used as an operand.
467
+
468
+ A width of None means that the width is not fixed during execution of noRTL.
469
+ This is the case, if the signal width is based on a parameter.
470
+ """
471
+ return self._operand_width
472
+
473
+ @property
474
+ def data_type(self) -> str:
475
+ """Data type of the signal (e.g., 'logic', 'reg', etc.)."""
476
+ return self._data_type
477
+
478
+ def render(self, target: Optional[str] = None) -> str:
479
+ """Render value to target language.
480
+
481
+ Arguments:
482
+ target: Target language.
483
+ """
484
+ return self.name
485
+
486
+ def __getitem__(self, index: Union[int, IntSlice]) -> 'SignalSlice':
487
+ return SignalSlice(self, index)
488
+
489
+ def overlaps_with(self, other: AssignmentTarget) -> Union[bool, Literal['partial']]:
490
+ """Check if signal overlaps with other signal or signal slice."""
491
+ return self.name == other.name
492
+
493
+ def read_access(self, ignore: Set[ACCESS_CHECKS] = set()) -> None:
494
+ """Register read access from the current thread.
495
+
496
+ If the current thread differs from the last write acess, will invoke the access checker.
497
+
498
+ Raises:
499
+ ExclusiveReadError: If the signal was read from more than one thread.
500
+ NonIdenticalRWError: If the signals was written by one, and read from another thread.
501
+ """
502
+ if self.assignment is not None:
503
+ # Trigger read access on the assignment expression
504
+ self.assignment.read_access(ignore=ignore)
505
+ else:
506
+ super().read_access(ignore=ignore)
507
+
508
+ def write_access(self, ignore: Set[ACCESS_CHECKS] = set()) -> None:
509
+ """Register write access from the current thread.
510
+
511
+ If the current thread differs from the last write acess, will invoke the access checker.
512
+
513
+ Raises:
514
+ ExclusiveWriteError: If the signals was written by more than one thread.
515
+ NonIdenticalRWError: If the signals was written by one, and read from another thread.
516
+ WriteViolationError: If the signal is read-only.
517
+ """
518
+ if self.type == 'input':
519
+ raise WriteViolationError(f'Input signal {self.name} is read-only.')
520
+ if self.assignment is not None:
521
+ raise WriteViolationError(f'Signal {self.name} is assigned to the expression {self.assignment}. It is read-only.')
522
+ else:
523
+ super().write_access(ignore=ignore)
524
+
525
+
526
+ class _BaseSlice(_BaseSignal):
527
+ """Intermediate class for signal slices."""
528
+
529
+ def __init__(self, signal: SignalProto, index: Union[int, IntSlice]) -> None:
530
+ super().__init__(signal.name)
531
+
532
+ self._base_signal = signal
533
+ self._index = index
534
+
535
+ if isinstance(signal.width, int):
536
+ if signal.width <= 1:
537
+ raise IndexError(f'Unable to slice signal with width {signal.width}!')
538
+
539
+ if isinstance(index, int):
540
+ if index < 0:
541
+ raise IndexError(f'Index {index} is out of bounds for signal {signal.name} with width {signal.width}')
542
+ if isinstance(signal.width, int) and index not in range(0, signal.width):
543
+ raise IndexError(f'Index {index} is out of bounds for signal {signal.name} with width {signal.width}')
544
+
545
+ self._width: int = 1
546
+ self._bitorder: Optional[BIT_ORDER] = None
547
+ else:
548
+ start, stop, bitorder = validate_slice(index)
549
+ self._width = stop - start + 1
550
+ self._bitorder = bitorder
551
+
552
+ @property
553
+ def base_signal(self) -> SignalProto:
554
+ """Full-width signal of this slice."""
555
+ return self._base_signal
556
+
557
+ @property
558
+ def index(self) -> Union[int, IntSlice]:
559
+ """Index of the signal."""
560
+ return self._index
561
+
562
+ # Properties forwarded to full-width signal
563
+ @property
564
+ def engine(self) -> EngineProto:
565
+ """Finite state machine."""
566
+ return self.base_signal.engine
567
+
568
+ @property
569
+ def type(self) -> SIGNAL_TYPES:
570
+ """Signal type."""
571
+ return self.base_signal.type
572
+
573
+ @property
574
+ def pulsing(self) -> bool:
575
+ """Shows, if the signal is self-resetting to zero after one cycle."""
576
+ return self.base_signal.pulsing
577
+
578
+ @property
579
+ def escaped_name(self) -> str:
580
+ """Name of signal with any special characters escaped.
581
+
582
+ For regular signals, this is equal to their name. For slice signals, it contains the position.
583
+ """
584
+ if isinstance(self.index, int):
585
+ return f'{self.name}_{self.index}'
586
+ else:
587
+ return f'{self.name}_{self.index.start}to{self.index.stop}'
588
+
589
+ @property
590
+ def data_type(self) -> str:
591
+ """Data type of the signal (e.g., 'logic', 'reg', etc.)."""
592
+ return self.base_signal.data_type
593
+
594
+ @property
595
+ def _is_synchronized(self) -> bool:
596
+ """Indicates if the this signal is synchronized to the local clock domain."""
597
+ return self.base_signal._is_synchronized
598
+
599
+ # Additional properties
600
+ @property
601
+ def width(self) -> int:
602
+ """Signal width in bits."""
603
+ return self._width
604
+
605
+ @property
606
+ def bitorder(self) -> Optional[BIT_ORDER]:
607
+ """Bit order of signal."""
608
+ return self._bitorder
609
+
610
+ @property
611
+ def operand_width(self) -> int:
612
+ """Indicates the width when used as an operand.
613
+
614
+ A width of None means that the width is not fixed during execution of noRTL.
615
+ This is the case, if the signal width is based on a parameter.
616
+ """
617
+ return self.width
618
+
619
+ def overlaps_with(self, other: AssignmentTarget) -> Union[bool, Literal['partial']]:
620
+ """Check if signal slice overlaps with other signal or signal slice."""
621
+
622
+ if self.name != other.name:
623
+ return False
624
+
625
+ # Unwrap content of modifier
626
+ if isinstance(other, BaseModifier):
627
+ other = other.content
628
+
629
+ if isinstance(other, _BaseSlice):
630
+ # Signal slice, check if it overlaps
631
+ if isinstance(self.index, int) and isinstance(other.index, int):
632
+ # Full overlap or none
633
+ return self.index == other.index
634
+ elif isinstance(self.index, slice) and isinstance(other.index, slice) and self.index == other.index:
635
+ # Full overlap, if the slices are exactly the same
636
+ return True
637
+
638
+ own_indexes = set(list_indexes(self.index))
639
+ other_indexes = set(list_indexes(other.index))
640
+
641
+ if own_indexes.isdisjoint(other_indexes):
642
+ return False
643
+ if own_indexes == other_indexes:
644
+ return True
645
+
646
+ # In all other cases (overlap of signal and slice, parametric width, partial overlap), treat overlap as partial
647
+ return 'partial'
648
+
649
+ def render(self, target: Optional[str] = None) -> str:
650
+ """Render value to target language.
651
+
652
+ Arguments:
653
+ target: Target language.
654
+ """
655
+ if isinstance(self.index, int):
656
+ return f'{self.name}[{self.index}]'
657
+ elif self.index.start == self.index.stop:
658
+ return f'{self.name}[{self.index.start}]'
659
+ else:
660
+ return f'{self.name}[{self.index.start}:{self.index.stop}]'
661
+
662
+ def __getitem__(self, index: Union[int, IntSlice]) -> Self:
663
+ if isinstance(self.index, int):
664
+ if index == 0:
665
+ return self # Allow indexing [0] of a single bit slice again
666
+ else:
667
+ raise IndexError(f'Unable to slice {index} from single-bit signal slice {self.escaped_name}: Only index 0 can be sliced.')
668
+ else:
669
+ # Assemble list of own indexes, with reference to the base signal
670
+ own_indexes = list_indexes(self.index)
671
+ own_start: int = own_indexes[0]
672
+ own_stop: int = own_indexes[-1]
673
+
674
+ # Check that the new index doesn't go out of range (this is not caught by pick_indexes)
675
+ if isinstance(index, int):
676
+ if index < 0 or index > (own_stop - own_start):
677
+ raise IndexError(f'Unable to slice {index} from signal slice {self.escaped_name}: Index is out of range.')
678
+ else:
679
+ start, stop, bitorder = validate_slice(index)
680
+ if start < 0 or stop > (own_stop - own_start):
681
+ raise IndexError(f'Unable to slice {index} from signal slice {self.escaped_name}: Index is out of range.')
682
+ if bitorder != self.bitorder:
683
+ raise IndexError(f'Unable to slice {index} from signal slice {self.escaped_name}: Reversing the bit order is not allowed')
684
+
685
+ # Pick the indexes for the nested slice, with reference to the base signal
686
+ new_indexes = pick_indexes(own_indexes, index)
687
+
688
+ if len(new_indexes) == 0:
689
+ raise IndexError(f'Unable to slice {index} from signal slice {self.escaped_name}: slice result in zero length signal.')
690
+ if len(new_indexes) == 1:
691
+ return type(self)(self.base_signal, new_indexes[0])
692
+
693
+ # As we don't support multiple indexes, we only need to find the new minimum and maximum indexes
694
+ if bitorder == 'H:L':
695
+ return type(self)(self.base_signal, slice(max(new_indexes), min(new_indexes)))
696
+ else:
697
+ return type(self)(self.base_signal, slice(min(new_indexes), max(new_indexes)))
698
+
699
+
700
+ class SignalSlice(_BaseSlice, _EventSourceSignal[SignalSliceProto]):
701
+ """Slice of a signal."""
702
+
703
+ def __init__(self, signal: SignalProto, index: Union[int, IntSlice]) -> None:
704
+ # if not isinstance(signal.width, int):
705
+ # TODO: Do we need to pass metadata along?
706
+ # TODO: parameters or renderable widths cannot be validated
707
+ # raise NotImplementedError('Only signals with discrete width can be sliced!') # noqa: ERA001
708
+ super().__init__(signal, index)
709
+
710
+ # Forward access control to base signal
711
+ @property
712
+ def access_checker(self) -> StaticAccessCheckerProto:
713
+ """Static access checker."""
714
+ return self.base_signal.access_checker
715
+
716
+ @property
717
+ def read_accesses(self) -> Set[StaticAccessProto]:
718
+ """Mutable set of read accesses to this signal."""
719
+ return self.base_signal.read_accesses
720
+
721
+ @property
722
+ def write_accesses(self) -> Set[StaticAccessProto]:
723
+ """Mutable set of write accesses to this signal."""
724
+ return self.base_signal.write_accesses
725
+
726
+ @property
727
+ def last_read_access_thread(self) -> Optional[ThreadProto]:
728
+ """Thread performing the last read access."""
729
+ return self.base_signal.last_read_access_thread
730
+
731
+ @last_read_access_thread.setter
732
+ def last_read_access_thread(self, value: ThreadProto) -> None:
733
+ """Thread performing the last read access."""
734
+ self.base_signal.last_read_access_thread = value
735
+
736
+ @property
737
+ def last_write_access_thread(self) -> Optional[ThreadProto]:
738
+ """Thread performing the last write access."""
739
+ return self.base_signal.last_write_access_thread
740
+
741
+ @last_write_access_thread.setter
742
+ def last_write_access_thread(self, value: ThreadProto) -> None:
743
+ """Thread performing the last write access."""
744
+ self.base_signal.last_write_access_thread = value
745
+
746
+ def as_scratch_signal(self) -> 'ScratchSignal':
747
+ """Turn SignalSlice into ScratchSignal, owned by the current thread."""
748
+ return ScratchSignal(self.base_signal, self.index)
749
+
750
+
751
+ class ScratchSignal(_BaseSlice, _AccessControlledSignal):
752
+ """A scratch signal is a special kind of signal slice, that is only valid for a limited time."""
753
+
754
+ def __init__(self, signal: SignalProto, index: Union[int, IntSlice]) -> None:
755
+ super().__init__(signal, index)
756
+
757
+ self._owner = signal.engine.current_thread
758
+
759
+ # Access control for scratch signals
760
+ self._released: bool = False
761
+ self._context_ctr: int = 0
762
+ self._context_ctr_active: bool = True
763
+
764
+ @property
765
+ def owner(self) -> ThreadProto:
766
+ """Owner of this scratch signal."""
767
+ return self._owner
768
+
769
+ def __enter__(self) -> Self:
770
+ return self
771
+
772
+ def __exit__(self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]) -> None:
773
+ self.release()
774
+
775
+ def enter_context(self) -> None:
776
+ """Context counting method.
777
+
778
+ The claim/release logic relies on the concept, that a scratch variable is to be released in the context where it has been created.
779
+ A context is represented by a Condition or a Loop construct. Both are created with context managers and may be nested.
780
+ The idea of this function is to count the 'depth' of context nests that we are currently working with.
781
+
782
+ In this way, we can detect, if the user tries to release the signal in a different context than creation. Once the release function is called,
783
+ we stop context counting since the scratch signal is now inactive and the claim/release-control is now realized based on the currently running threads.
784
+
785
+ This concept assumes, that each context manager triggers the `enter_context` function during enter and the `exit_context` function during exit.
786
+ The `exit_context` function automatically releases the signal, if we leave the claiming context.
787
+ """
788
+ if self._context_ctr_active:
789
+ self._context_ctr += 1
790
+
791
+ def exit_context(self) -> None:
792
+ """Context counting method. Explanation in exit_context function."""
793
+ if self._context_ctr_active:
794
+ self._context_ctr -= 1
795
+
796
+ if self._context_ctr == -1 and self.owner == self.engine.current_thread:
797
+ self._context_ctr = 0
798
+ self.release()
799
+
800
+ # Access control
801
+ def write_access(self, ignore: Set[ACCESS_CHECKS] = set()) -> None:
802
+ """Register write access from the current thread.
803
+
804
+ If the current thread differs from the last write acess, will invoke the access checker.
805
+
806
+ Raises:
807
+ ExclusiveWriteError: If the signals was written by more than one thread.
808
+ NonIdenticalRWError: If the signals was written by one, and read from another thread.
809
+ AccessAfterReleaseError: If the scratch signal was released.
810
+ """
811
+ if self.released:
812
+ raise AccessAfterReleaseError('Tried to write to a signal that has been released previously!')
813
+ super().write_access(ignore=ignore)
814
+
815
+ def read_access(self, ignore: Set[ACCESS_CHECKS] = set()) -> None:
816
+ """Register read access from the current thread.
817
+
818
+ If the current thread differs from the last write acess, will invoke the access checker.
819
+
820
+ Raises:
821
+ ExclusiveReadError: If the signal was read from more than one thread.
822
+ NonIdenticalRWError: If the signals was written by one, and read from another thread.
823
+ AccessAfterReleaseError: If the scratch signal was released.
824
+ """
825
+ if self.released:
826
+ raise AccessAfterReleaseError('Tried to read from a signal that has been released previously!')
827
+ super().read_access(ignore=ignore)
828
+
829
+ @property
830
+ def released(self) -> bool:
831
+ """Findout, if a signal has been release yet.
832
+
833
+ The claim/release control works based on two principles:
834
+
835
+ 1. A scratch signal may only be claimed and released in a single code block. Example:
836
+ ```python
837
+ f = CoreEngine("my_engine")
838
+
839
+ with Condition(f, some_condition):
840
+ s = # new scratch signal
841
+
842
+ with ForLoop(...):
843
+ # s may not be released here
844
+
845
+ s.release() # s can be released here, since it is the same context
846
+
847
+ # After the context has ended, s is automatically released.
848
+ ```
849
+
850
+ 2. A scratch signal will appear as non-released in parallel threads and will be released once the owner thread ends. Additional access control applies.
851
+ ```python
852
+ f = CoreEngine("my_engine")
853
+
854
+ with Fork(f, "my_fork") as f1:
855
+ s = # new scratch_signals
856
+ #...
857
+ s.release()
858
+ with Fork(f, "my_second_fork") as f2:
859
+ assert s.released == False # Parallel running thread the scratch pad location!
860
+
861
+ f1.wait_for_finish()
862
+
863
+ # s is released automatically once the thread has finished.
864
+ ```
865
+
866
+ """
867
+ if self.owner != self.base_signal.engine.current_thread:
868
+ return not self.owner.running
869
+ return self._released
870
+
871
+ def release(self, force: bool = False) -> None:
872
+ if self.owner != self.base_signal.engine.current_thread and not force:
873
+ raise ValueError('Scratch register may only be released in owning thread!')
874
+ if self._context_ctr != 0 and not force:
875
+ raise ValueError('Scratch signals need to be released in the context where they were created!')
876
+
877
+ self._context_ctr_active = False
878
+ self._released = True