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
@@ -0,0 +1,226 @@
1
+ from collections import deque
2
+ from typing import Deque, Dict, List, Literal, Optional, Tuple, Union
3
+
4
+ from .utils import VerilogRenderable, to_verilog_renderable
5
+
6
+
7
+ class VerilogAssignment:
8
+ def __init__(self, tgt: Union[str, VerilogRenderable], src: Union[str, VerilogRenderable]) -> None:
9
+ self.tgt = to_verilog_renderable(tgt)
10
+ self.src = to_verilog_renderable(src)
11
+ self.operator = '<='
12
+
13
+ def render(self) -> str:
14
+ return f'{self.tgt} {self.operator} {self.src};'
15
+
16
+
17
+ class VerilogBlock:
18
+ def __init__(self) -> None:
19
+ self.items: Deque[VerilogRenderable] = deque([])
20
+
21
+ def render(self) -> str:
22
+ content: List[str] = ['begin']
23
+ content.append(self.render_items())
24
+ content.append('end')
25
+
26
+ return '\n'.join(content)
27
+
28
+ def render_items(self) -> str:
29
+ content: List[str] = []
30
+ content.extend(item.render() for item in self.items)
31
+ return '\n'.join(content)
32
+
33
+ def add(self, item: VerilogRenderable) -> None:
34
+ self.items.append(item)
35
+
36
+ def __len__(self) -> int:
37
+ return len(self.items)
38
+
39
+
40
+ class VerilogCase:
41
+ def __init__(self, tgt_signal: str) -> None:
42
+ self.tgt_signal = tgt_signal
43
+ self.cases: Dict[str, VerilogBlock] = {}
44
+ self.compress_output = True
45
+
46
+ def add_case(self, value: str, block: Optional[VerilogBlock] = None) -> None:
47
+ if block is None:
48
+ self.cases[value] = VerilogBlock()
49
+ else:
50
+ self.cases[value] = block
51
+
52
+ def add_item(self, case: str, item: VerilogRenderable) -> None:
53
+ if case not in self.cases:
54
+ self.add_case(case)
55
+
56
+ self.cases[case].add(item)
57
+
58
+ def compress_cases(self) -> Dict[str, str]:
59
+ new_cases: Dict[str, str] = {}
60
+
61
+ for case, block in self.cases.items():
62
+ if len(block) != 0:
63
+ old_case = ''
64
+ new_block = block.render()
65
+
66
+ for existing_case, existing_block in new_cases.items():
67
+ if existing_block == new_block:
68
+ old_case = existing_case
69
+
70
+ if old_case == '':
71
+ new_cases[case] = new_block
72
+ else:
73
+ del new_cases[old_case]
74
+ new_cases[f'{case}, {old_case}'] = new_block
75
+
76
+ return new_cases
77
+
78
+ def render(self) -> str:
79
+ content: List[str] = []
80
+
81
+ if all(len(block) == 0 for block in self.cases.values()):
82
+ return ''
83
+
84
+ content.append(f'case ({self.tgt_signal})')
85
+
86
+ if self.compress_output:
87
+ for case, block in self.compress_cases().items():
88
+ content.append(f'{case}: {block}')
89
+ else:
90
+ for case, vblock in self.cases.items():
91
+ if len(vblock) != 0:
92
+ content.append(f'{case}: {vblock.render()}')
93
+
94
+ content.append('endcase')
95
+
96
+ return '\n'.join(content)
97
+
98
+
99
+ class VerilogIf:
100
+ def __init__(self, condition: Union[VerilogRenderable, str]) -> None:
101
+ self.condition: VerilogRenderable = to_verilog_renderable(condition)
102
+ self.true_branch = VerilogBlock()
103
+ self.false_branch = VerilogBlock()
104
+
105
+ def render(self) -> str:
106
+ if self.condition.render() == '1':
107
+ return self.true_branch.render_items()
108
+
109
+ content = [f'if ({self.condition})']
110
+ content.append(self.true_branch.render())
111
+
112
+ if len(self.false_branch) > 0:
113
+ content.append('else ')
114
+ content.append(self.false_branch.render())
115
+
116
+ return '\n'.join(content)
117
+
118
+
119
+ class VerilogProcess:
120
+ def __init__(self) -> None:
121
+ self.sensitivity: List[Tuple[Literal['posedge', 'negedge'], VerilogRenderable]] = []
122
+ self.content = VerilogBlock()
123
+ self.render_sensitivity = True
124
+ self.block_label = 'always'
125
+
126
+ def _add(self, item: VerilogRenderable) -> None:
127
+ self.content.add(item)
128
+
129
+ def add_sensitivity(self, edge: Literal['posedge', 'negedge'], signal: VerilogRenderable) -> None:
130
+ self.sensitivity.append((edge, signal))
131
+
132
+ def render(self) -> str:
133
+ content = []
134
+
135
+ line = f'{self.block_label}'
136
+
137
+ if self.render_sensitivity:
138
+ if self.sensitivity == []:
139
+ line = f'{line} @(*)'
140
+ else:
141
+ sens_lst = []
142
+ for edge, signal in self.sensitivity:
143
+ sens_lst.append(f'{edge} {signal}')
144
+ line = f'{line} @({" or ".join(sens_lst)})'
145
+
146
+ content.append(line)
147
+
148
+ content.append(self.content.render())
149
+
150
+ return '\n'.join(content)
151
+
152
+
153
+ class AlwaysFF(VerilogProcess):
154
+ def __init__(self, clk: Union[VerilogRenderable, str], reset: Union[VerilogRenderable, str]) -> None:
155
+ super().__init__()
156
+ self.clk = clk
157
+ self.reset = reset
158
+
159
+ self.behavior = VerilogIf(to_verilog_renderable(reset))
160
+ self._add(self.behavior)
161
+
162
+ self.block_label = 'always_ff'
163
+
164
+ def add_reset(self, item: VerilogRenderable) -> None:
165
+ self.behavior.true_branch.add(item)
166
+
167
+ def add(self, item: VerilogRenderable) -> None:
168
+ self.behavior.false_branch.add(item)
169
+
170
+ def render(self) -> str:
171
+ self.add_sensitivity('posedge', to_verilog_renderable(self.clk))
172
+ self.add_sensitivity('posedge', to_verilog_renderable(self.reset))
173
+
174
+ return super().render()
175
+
176
+
177
+ class AlwaysComb(VerilogProcess):
178
+ def __init__(self) -> None:
179
+ super().__init__()
180
+ self.block_label = 'always_comb'
181
+ self.render_sensitivity = False
182
+
183
+ def add(self, item: VerilogRenderable) -> None:
184
+ self._add(item)
185
+
186
+
187
+ class AlwaysLatch(VerilogProcess):
188
+ def __init__(self, latch_en: VerilogRenderable) -> None:
189
+ super().__init__()
190
+
191
+ self.behavior = VerilogIf(latch_en)
192
+ self._add(self.behavior)
193
+
194
+ self.block_label = 'always_latch'
195
+ self.render_sensitivity = False
196
+
197
+ def add(self, item: VerilogRenderable) -> None:
198
+ self.behavior.true_branch.add(item)
199
+
200
+
201
+ class VerilogPrint:
202
+ def __init__(self, line: str, *args: str) -> None:
203
+ self.line = line
204
+ self.args = args
205
+
206
+ def render(self) -> str:
207
+ line = f'$display("{self.line}", {",".join(self.args)});'
208
+ return line
209
+
210
+
211
+ class VerilogPrintf:
212
+ def __init__(self, fname: str, line: str, *args: str) -> None:
213
+ self.fname = fname
214
+ self.line = line
215
+ self.args = args
216
+
217
+ def render(self) -> str:
218
+ ret = []
219
+ ret.append('begin')
220
+ ret.append('int fd;')
221
+ ret.append(f'fd = $fopen({self.fname}, "w+");')
222
+ ret.append(f'$fdisplay("{self.line}", {",".join(self.args)});')
223
+ ret.append('$fclose(fd);')
224
+ ret.append('end')
225
+
226
+ return '\n'.join(ret)
@@ -0,0 +1,146 @@
1
+ from typing import Dict, List, Optional, Union
2
+
3
+ from .formatter import VerilogFormatter
4
+ from .utils import VerilogRenderable
5
+
6
+
7
+ class VerilogDeclaration:
8
+ def __init__(
9
+ self,
10
+ verilog_type: str,
11
+ name: Union[str, List[str]],
12
+ width: Union[int, str, VerilogRenderable] = 0,
13
+ connections: Optional[Dict[str, str]] = None,
14
+ params: Optional[Dict[str, str]] = None,
15
+ members: Optional[Union[List[str], Dict[str, int]]] = None,
16
+ ):
17
+ self.verilog_type = verilog_type
18
+ self.name = name
19
+ self.connections = connections
20
+ self.params = params
21
+ self.members = members
22
+ self.width = width
23
+
24
+ def add_member(self, name: str, value: Optional[Union[int]] = None) -> None:
25
+ if self.members is None:
26
+ if value is not None:
27
+ self.members = {}
28
+ else:
29
+ self.members = []
30
+
31
+ if isinstance(self.members, list):
32
+ if value is not None:
33
+ raise RuntimeError('Cannot add an encoded member to an enum without all items to be encoded!')
34
+ self.members.append(name)
35
+ else:
36
+ if value is None:
37
+ raise RuntimeError('Cannot add an un-encoded member to an enum where all items are encoded!')
38
+ self.members[name] = value
39
+
40
+ def add_parameter(self, name: str, value: Union[str, int]) -> None:
41
+ if self.params is None:
42
+ self.params = {}
43
+
44
+ if isinstance(value, int):
45
+ self.params[name] = str(value)
46
+ else:
47
+ self.params[name] = value
48
+
49
+ def add_connection(self, src: str, tgt: str) -> None:
50
+ if self.connections is None:
51
+ self.connections = {}
52
+
53
+ self.connections[src] = tgt
54
+
55
+ def render(self) -> str: # noqa: C901
56
+ content: List[str] = []
57
+ connection_lst = []
58
+ param_lst = []
59
+
60
+ if self.connections is not None:
61
+ connection_lst = [f'.{x}({y})' for x, y in self.connections.items()]
62
+ if self.params is not None:
63
+ param_lst = [f'.{x}({y})' for x, y in self.params.items()]
64
+
65
+ name_str = ''
66
+
67
+ content.append(self.verilog_type)
68
+
69
+ if isinstance(self.name, list):
70
+ name_str = ', '.join(self.name)
71
+ else:
72
+ name_str = self.name
73
+
74
+ if isinstance(self.width, int):
75
+ if self.width > 1:
76
+ content.append(f'[{self.width - 1}:0]')
77
+ else:
78
+ content.append(f'[{self.width}-1:0]')
79
+
80
+ if self.verilog_type.startswith('enum'):
81
+ if self.members is None:
82
+ raise RuntimeError(f'Tried to create enum {name_str} without any values. Something is wrong here.')
83
+
84
+ if isinstance(self.members, List):
85
+ item_str = ', '.join(self.members)
86
+ else:
87
+ item_str = ', '.join([f'{item} = {value}' for item, value in self.members.items()])
88
+
89
+ content.append(f'{{{item_str}}}')
90
+
91
+ if self.params is not None:
92
+ content.append(f'#({", ".join(param_lst)})')
93
+
94
+ content.append(name_str)
95
+
96
+ if not any(t in self.verilog_type for t in ['wire', 'logic', 'reg']) and not self.verilog_type.startswith(
97
+ 'enum'
98
+ ): # other net types are out of scope for now
99
+ content.append(f'({", ".join(connection_lst)})')
100
+
101
+ return ' '.join(content)
102
+
103
+
104
+ class VerilogModule:
105
+ def __init__(self, name: str) -> None:
106
+ self.name = name
107
+ self.ports: List[VerilogDeclaration] = []
108
+ self.parameters: Dict[str, Optional[Union[int, str]]] = {}
109
+ self.signals: List[VerilogDeclaration] = []
110
+ self.instances: List[VerilogDeclaration] = []
111
+ self.functionals: List[VerilogRenderable] = []
112
+
113
+ def render(self) -> str:
114
+ content = []
115
+ line = f'module {self.name} '
116
+
117
+ if len(self.parameters) != 0:
118
+ params = []
119
+ for p, val in self.parameters.items():
120
+ if val is None:
121
+ params.append(f'parameter {p}')
122
+ else:
123
+ params.append(f'parameter {p} = {val}')
124
+
125
+ line = f'{line} #(\n{",\n".join(params)}) '
126
+
127
+ ports = [p.render() for p in self.ports]
128
+
129
+ line = f'{line} ({",\n".join(ports)});'
130
+
131
+ content.append(line)
132
+
133
+ content.extend([item.render() + ';' for item in self.signals + self.instances])
134
+ content.extend([item.render() for item in self.functionals])
135
+
136
+ content.append('endmodule')
137
+
138
+ code = '\n'.join(content)
139
+
140
+ return VerilogFormatter(code).format()
141
+
142
+ def get_instance(self, name: str) -> VerilogDeclaration:
143
+ for item in self.instances:
144
+ if item.name == name:
145
+ return item
146
+ raise RuntimeError(f'Could not find an instance named {name}.')
@@ -0,0 +1,23 @@
1
+ from typing import Protocol, Union
2
+
3
+ from nortl.core.operations import RawText
4
+
5
+
6
+ class VerilogRenderable(Protocol):
7
+ """Protocol for Verilog Renderable.
8
+
9
+ Compared to the [nortl.core.Renderable][nortl.core.operations.Renderable], it doesn't require the [OperationTrait Mixin][nortl.core.operations.OperationTrait] and `target` argument for `render()`.
10
+ """
11
+
12
+ def render(self) -> str: ...
13
+
14
+
15
+ def to_verilog_renderable(value: Union[str, VerilogRenderable]) -> VerilogRenderable:
16
+ """Convert value to Verilog Renderable.
17
+
18
+ Compared to [nortl.core.to_renderable][nortl.core.operations.to_renderable], this also supports strings.
19
+ """
20
+ if isinstance(value, str):
21
+ return RawText(value)
22
+ else:
23
+ return value
File without changes
@@ -0,0 +1,37 @@
1
+ from typing import Optional, Tuple
2
+
3
+ __all__ = [
4
+ 'parse_int',
5
+ ]
6
+
7
+
8
+ def parse_int(string: str) -> Tuple[int, Optional[int]]:
9
+ """Parses an integer from a string.
10
+
11
+ For numbers in binary, octal or hexadecimal representation, it infers the width.
12
+
13
+ Arguments:
14
+ string: Integer value as a string.
15
+
16
+ Returns:
17
+ Tuple of the value and inferred width.
18
+ """
19
+
20
+ try:
21
+ # Parse binary, octal or hexadecimal number
22
+ if string.startswith('0b'):
23
+ value = int(string, 2)
24
+ width = len(string.split('0b', maxsplit=1)[1])
25
+ elif string.startswith('0o'):
26
+ value = int(string, 8)
27
+ width = len(string.split('0o', maxsplit=1)[1]) * 3
28
+ elif string.startswith('0x'):
29
+ value = int(string, 16)
30
+ width = len(string.split('0x', maxsplit=1)[1]) * 4
31
+ else:
32
+ value = int(string)
33
+ width = None
34
+ except ValueError:
35
+ raise ValueError(f"Unable to create Const from '{string}', failed to convert it to integer.")
36
+
37
+ return value, width
@@ -0,0 +1,41 @@
1
+ module testbench();
2
+
3
+ logic CLK, RST;
4
+
5
+ initial begin
6
+ CLK = 0;
7
+ while(1)
8
+ begin
9
+ #(10);
10
+ CLK = ~CLK;
11
+ end
12
+ end
13
+
14
+ logic finish, passed, timeout;
15
+
16
+ my_engine DUT(
17
+ .RST_ASYNC_I(RST),
18
+ .CLK_I(CLK),
19
+ .timeout(timeout),
20
+ .passed(passed),
21
+ .finish(finish)
22
+ );
23
+
24
+ initial begin
25
+ RST = 1;
26
+ #(100);
27
+ RST = 0;
28
+
29
+ @(posedge finish);
30
+
31
+ // variable dump section
32
+
33
+ $display("timeout=%d;\n", timeout);
34
+ $display("errors=%d;\n", DUT.error_ctr);
35
+ $display("passed=%d;\n", passed);
36
+
37
+ $finish();
38
+ end
39
+
40
+
41
+ endmodule
@@ -0,0 +1,218 @@
1
+ import re
2
+ import subprocess
3
+ from abc import ABC, abstractmethod
4
+ from inspect import currentframe, getframeinfo
5
+ from pathlib import Path
6
+ from tempfile import TemporaryDirectory
7
+ from types import FrameType
8
+
9
+ import pytest
10
+
11
+ from nortl.core import CoreEngine
12
+ from nortl.core.constructs import Condition, ElseCondition, Fork
13
+ from nortl.core.modifiers import UnregisteredRead
14
+ from nortl.core.operations import to_renderable
15
+ from nortl.core.protocols import Renderable
16
+ from nortl.renderer.verilog_renderer import VerilogRenderer
17
+
18
+
19
+ class NoRTLTestBase(ABC):
20
+ """Base class for noRTL tests with Verilog simulation.
21
+
22
+ Subclasses must implement:
23
+ - build_engine(): Create and configure the engine to be tested
24
+ - generate_testbench(): Generate the Verilog testbench file
25
+
26
+ The simulation is automatically run before each test method.
27
+ Test methods can access:
28
+ - self.engine: The engine instance
29
+ - self.simulation_result: The subprocess result from simulation
30
+ - self.simulation_output: Parsed simulation output (string)
31
+ """
32
+
33
+ @abstractmethod
34
+ def init_sequence(self) -> CoreEngine:
35
+ """This function must initialize the engine that is to be tested. In the most trivial case, this means, that an instance of CoreEngine is created.
36
+
37
+ Returns:
38
+ CoreEngine: Initialized engine
39
+ """
40
+ pass
41
+
42
+ def verify_final_state(self, engine: CoreEngine) -> None:
43
+ """This state-sequence is called after the dut and testbench threads have ended. It can be used to verify the final state of the dut."""
44
+ pass
45
+
46
+ def the_testbench(self, engine: CoreEngine) -> None:
47
+ """The state sequence that is run in parallel to the engine-under-Test. This may use the self.assert* functions to verify the behavior.
48
+
49
+ Arguments:
50
+ engine (CoreEngine): engine instance
51
+ """
52
+ pass
53
+
54
+ @abstractmethod
55
+ def dut(self, engine: CoreEngine) -> None:
56
+ """The dut function is the state sequence that is to be tested. It is executed in parallel to the testbench.
57
+
58
+ A call to self.assert* is not forbidden but not advised to create a good seperation between dut and test code.
59
+ """
60
+ pass
61
+
62
+ def get_test_engine(self) -> CoreEngine:
63
+ self.engine = self.init_sequence()
64
+
65
+ self.finish_flag = self.engine.define_output('finish', 1, 0)
66
+ self.state_flag = self.engine.define_output('passed', 1, 0)
67
+ self.error_ctr = self.engine.define_local('error_ctr', 16, 0)
68
+ self.timeout = self.engine.define_output('timeout', 1, 0)
69
+ ctr = self.engine.define_local('timeout_ctr', 16, 0)
70
+
71
+ self.engine.sync()
72
+
73
+ # FIXME: Introduce Timeout-timer
74
+ with Fork(self.engine, 'DUT') as t_dut:
75
+ self.dut(self.engine)
76
+ with Fork(self.engine, 'testbench') as t_tb:
77
+ self.the_testbench(self.engine)
78
+ with Fork(self.engine, 'timeout') as t_timeout:
79
+ self.engine.sync()
80
+ self.engine.set(ctr, ctr + 1)
81
+ with Condition(self.engine, ctr == 0xFFFF):
82
+ self.engine.set(self.timeout, 1)
83
+ t_timeout.finish()
84
+
85
+ self.engine.sync()
86
+
87
+ self.engine.wait_for((t_dut.finished & t_tb.finished) | t_timeout.finished)
88
+
89
+ with Condition(self.engine, t_timeout.finished):
90
+ t_dut.cancel()
91
+ t_tb.cancel()
92
+ if not hasattr(self, 'expect_timeout'):
93
+ self.assertTrue(0) # Show that we had a timeout
94
+ else:
95
+ self.assertTrue(self.expect_timeout)
96
+ with ElseCondition(self.engine):
97
+ t_timeout.cancel()
98
+
99
+ self.verify_final_state(self.engine)
100
+
101
+ self.engine.sync()
102
+
103
+ self.finish_simulation()
104
+
105
+ return self.engine
106
+
107
+ def increment_error_ctr(self) -> None:
108
+ self.engine.sync()
109
+
110
+ with Condition(self.engine, self.error_ctr != 0xFFFF):
111
+ self.engine.set(self.error_ctr, self.error_ctr + 1)
112
+ with ElseCondition(self.engine):
113
+ pass
114
+
115
+ self.engine.sync()
116
+
117
+ def finish_simulation(self) -> None:
118
+ self.engine.sync()
119
+
120
+ with Condition(self.engine, self.error_ctr == 0):
121
+ self.engine.set(self.state_flag, 1)
122
+ with ElseCondition(self.engine):
123
+ self.engine.set(self.state_flag, 0)
124
+
125
+ self.engine.set(self.finish_flag, 1)
126
+ self.engine.sync()
127
+
128
+ def print_line(self, frame: FrameType) -> None:
129
+ fi = getframeinfo(frame)
130
+ if fi.code_context is None:
131
+ return
132
+ code_context = fi.code_context[0]
133
+ code_context = re.sub('#.*', '', code_context)
134
+ code_context = code_context.lstrip().rstrip()
135
+ self.engine.print(f'Assertion \\"{code_context}\\" failed at {fi.filename}:{frame.f_lineno}')
136
+
137
+ def assertTrue(self, condition: Renderable | int) -> None: # noqa: N802
138
+ with Condition(self.engine, UnregisteredRead(~to_renderable(condition))):
139
+ frame = currentframe()
140
+ if frame is not None:
141
+ frame = frame.f_back
142
+ if frame is not None:
143
+ self.print_line(frame)
144
+ self.increment_error_ctr()
145
+ self.finish_simulation()
146
+ with ElseCondition(self.engine):
147
+ pass
148
+ self.engine.sync()
149
+
150
+ def assertEqual(self, value1: Renderable | int, value2: Renderable | int) -> None: # noqa: N802
151
+ with Condition(self.engine, UnregisteredRead(to_renderable(value1) != to_renderable(value2))):
152
+ frame = currentframe()
153
+ if frame is not None:
154
+ frame = frame.f_back
155
+ if frame is not None:
156
+ self.print_line(frame)
157
+
158
+ self.engine.print('Left-hand side: 0x%h', to_renderable(value1))
159
+ self.engine.print('Right-hand side: 0x%h', to_renderable(value2))
160
+ self.increment_error_ctr()
161
+ self.finish_simulation()
162
+ with ElseCondition(self.engine):
163
+ pass
164
+ self.engine.sync()
165
+
166
+ def test_compile_and_run(self) -> None:
167
+ """Automatically executed fixture.
168
+
169
+ Process:
170
+ 1. Builds the engine
171
+ 2. Generates and compiles Verilog
172
+ 3. Runs the simulation
173
+ 4. Makes results available to test methods
174
+
175
+ """
176
+ # Create engine from derived class
177
+ self.engine = self.get_test_engine()
178
+
179
+ with TemporaryDirectory() as tempdir_str:
180
+ tmp_path = Path(tempdir_str)
181
+
182
+ # Generate Verilog files
183
+ self.verilog_file: Path = tmp_path / 'engine.sv'
184
+ self.testbench_file: Path = Path(__file__).parent / 'templates' / 'testbench.sv'
185
+
186
+ renderer = VerilogRenderer(self.engine)
187
+ with open(self.verilog_file, 'w') as fptr:
188
+ fptr.write(renderer.render())
189
+
190
+ # Compile with Icarus Verilog
191
+ self.compiled_file: Path = tmp_path / 'engine.vvp'
192
+ compile_result = subprocess.run(
193
+ ['iverilog', '-g2005-sv', '-o', str(self.compiled_file), str(self.verilog_file), str(self.testbench_file)],
194
+ capture_output=True,
195
+ text=True,
196
+ )
197
+
198
+ if compile_result.returncode != 0:
199
+ pytest.fail(f'Verilog compilation failed:\n{compile_result.stderr}')
200
+
201
+ # Run simulation automatically
202
+ self._run_simulation()
203
+ self.simulation_output: str = self.simulation_result.stdout
204
+
205
+ res = re.findall(r'passed=\s*\d+', self.simulation_output)[0]
206
+ print(self.simulation_output)
207
+
208
+ pass_state = int(res.split('=')[1]) == 1
209
+ print(pass_state)
210
+
211
+ if not pass_state:
212
+ pytest.fail('Verilog simulation discovered errors')
213
+
214
+ def _run_simulation(self) -> None:
215
+ """Internal method to execute the simulation."""
216
+ cmd = ['vvp', str(self.compiled_file)]
217
+
218
+ self.simulation_result = subprocess.run(cmd, capture_output=True, text=True)