wavekit 0.5.6__tar.gz → 0.6.1__tar.gz

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 (25) hide show
  1. {wavekit-0.5.6 → wavekit-0.6.1}/PKG-INFO +62 -9
  2. {wavekit-0.5.6 → wavekit-0.6.1}/README.md +60 -8
  3. {wavekit-0.5.6 → wavekit-0.6.1}/build.py +9 -2
  4. {wavekit-0.5.6 → wavekit-0.6.1}/pyproject.toml +5 -2
  5. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/__init__.py +4 -0
  6. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/pattern/__init__.py +2 -1
  7. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/pattern/dsl.py +84 -20
  8. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/pattern/engine.py +68 -30
  9. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/pattern/result.py +1 -1
  10. wavekit-0.6.1/src/wavekit/pattern/steps.py +216 -0
  11. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +51 -39
  12. wavekit-0.6.1/src/wavekit/readers/fst/reader.py +261 -0
  13. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/readers/vcd/reader.py +18 -0
  14. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/waveform.py +163 -1
  15. wavekit-0.5.6/src/wavekit/pattern/steps.py +0 -142
  16. {wavekit-0.5.6 → wavekit-0.6.1}/LICENSE +0 -0
  17. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/pattern/instance.py +0 -0
  18. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/readers/base.py +0 -0
  19. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/readers/expr_parser.py +0 -0
  20. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
  21. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/readers/fsdb/reader.py +0 -0
  22. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/readers/pattern_parser.py +0 -0
  23. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/readers/value_change.pyx +0 -0
  24. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/scope.py +0 -0
  25. {wavekit-0.5.6 → wavekit-0.6.1}/src/wavekit/signal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavekit
3
- Version: 0.5.6
3
+ Version: 0.6.1
4
4
  Summary: a fundamental package for digital circuit waveform analysis
5
5
  License-File: LICENSE
6
6
  Author: cxzzzz
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Requires-Dist: numpy (>=2.0.0,<3.0.0)
17
+ Requires-Dist: pylibfst (>=0.2.1,<0.3.0)
17
18
  Requires-Dist: pytest (>=8.2.2,<9.0.0)
18
19
  Requires-Dist: vcdvcd (>=2.3.5,<3.0.0)
19
20
  Project-URL: Repository, https://github.com/cxzzzz/wavekit
@@ -24,15 +25,21 @@ Description-Content-Type: text/markdown
24
25
  [![CI](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml/badge.svg)](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
25
26
  [![PyPI version](https://img.shields.io/pypi/v/wavekit.svg)](https://pypi.org/project/wavekit/)
26
27
  [![Python Versions](https://img.shields.io/pypi/pyversions/wavekit.svg)](https://pypi.org/project/wavekit/)
28
+ [![Downloads](https://img.shields.io/pypi/dm/wavekit.svg)](https://pypi.org/project/wavekit/)
27
29
  [![License](https://img.shields.io/github/license/cxzzzz/wavekit.svg)](LICENSE)
28
30
 
29
- **Wavekit** is a fundamental Python library for digital waveform analysis. By seamlessly converting VCD and FSDB data into Numpy arrays, it empowers engineers to perform high-performance signal processing, protocol analysis, and automated verification with ease.
31
+ English | [中文](README_ZH.md)
32
+
33
+ **Wavekit** is a fundamental Python library for digital waveform analysis. By seamlessly converting VCD, FST, and FSDB data into Numpy arrays, it empowers engineers to perform high-performance signal processing, protocol analysis, and automated verification with ease.
34
+
35
+ > 🤖 **AI Integration**: [wavekit-mcp](https://github.com/cxzzzz/wavekit-mcp) — MCP server for AI-assisted waveform analysis. Let AI load signals and run pattern matching — no manual coding required.
30
36
 
31
37
  ## ✨ Features
32
38
 
33
- - **High Performance & Easy Loading**: Cython-optimized VCD/FSDB parsers with Numpy-backed storage for speed and memory efficiency, plus flexible batch signal extraction via brace expansion, integer ranges, and regular expressions.
39
+ - **Flexible Signal Extraction**: Flexible batch signal extraction via brace expansion, integer ranges, and regular expressions — load groups of related signals in one call.
34
40
  - **Rich Analysis Tools**: Numpy-like API for arithmetic, masking, bit-field manipulation, edge detection, and time/cycle slicing — compose complex signal queries in just a few lines.
35
41
  - **Pattern Matching**: NFA-based temporal pattern engine that scans waveforms in a single pass to extract protocol transactions, measure latencies, and detect timing violations.
42
+ - **High-Performance Parsing & Storage**: VCD, FST, and FSDB readers with Numpy-backed storage for fast loading and memory efficiency, handling large simulation files with ease.
36
43
 
37
44
  ## 📦 Installation
38
45
 
@@ -47,7 +54,7 @@ pip install wavekit
47
54
 
48
55
  ## 🚀 Quick Start
49
56
 
50
- > The examples below use placeholder filenames such as `sim.vcd`. Replace them with the path to your own VCD or FSDB file, and adjust signal paths to match your design hierarchy.
57
+ > The examples below use placeholder filenames such as `sim.vcd`. Replace them with the path to your own VCD, FST, or FSDB file, and adjust signal paths to match your design hierarchy.
51
58
 
52
59
  ### 1. Batch Signal Extraction
53
60
 
@@ -160,7 +167,7 @@ print(f"Read data: {valid.captures['rdata'].value}")
160
167
  **AXI Write Burst (multi-beat)**
161
168
 
162
169
  ```python
163
- beat = Pattern().wait(wvalid & wready).capture("beats[]", wdata)
170
+ beat = Pattern().wait(wvalid & wready).capture("beats", wdata, mode="list")
164
171
 
165
172
  result = (
166
173
  Pattern()
@@ -198,7 +205,7 @@ print(f"Stall durations: {stalls.duration.value} cycles")
198
205
 
199
206
  | Method | Description |
200
207
  |--------|-------------|
201
- | `VcdReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`). |
208
+ | `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`). |
202
209
  | `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
203
210
  | `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
204
211
  | `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
@@ -261,15 +268,33 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
261
268
  | `wave.rising_edge()` | True at 0→1 transitions |
262
269
  | `wave.falling_edge()` | True at 1→0 transitions |
263
270
 
271
+ **Relative time access**
272
+
273
+ | Method | Description |
274
+ |--------|-------------|
275
+ | `wave.relative(offset, pad, pad_value)` | Shift by *offset* cycles (positive = future, negative = past) |
276
+ | `wave.ahead(n, pad, pad_value)` | Look *n* cycles into the future (shorthand for `relative(n)`) |
277
+ | `wave.back(n, pad, pad_value)` | Look *n* cycles into the past (shorthand for `relative(-n)`) |
278
+
279
+ `pad` controls boundary handling: `'repeat'` (default) pads with the first/last value, `'value'` pads with a given `pad_value`.
280
+
281
+ ```python
282
+ # Rising edge detection
283
+ rising = (wave == 0) & wave.ahead()
284
+
285
+ # Compare current vs 3 cycles ago
286
+ changed = wave != wave.back(3)
287
+ ```
288
+
264
289
  ---
265
290
 
266
291
  ### Pattern
267
292
 
268
293
  | Method | Description |
269
294
  |--------|-------------|
270
- | `.wait(cond, guard=None, channel=None)` | Block until `cond` is True. `guard` is checked each waiting cycle. `channel` enforces FIFO ordering among concurrent instances. |
271
- | `.delay(n, guard=None)` | Advance `n` cycles. `delay(0)` is a no-op. |
272
- | `.capture(name, signal)` | Record signal value at current cycle. `name[]` appends to a list. |
295
+ | `.wait(cond, *, require=None, channel=None, tick=True)` | Block until `cond` is True. `require` is checked each waiting cycle (failure → `REQUIRE_VIOLATED`). `channel` binds the wait to a shared FIFO consumer group (see [Channels](#channels) below). `tick=False` matches on the current cycle without consuming it. |
296
+ | `.delay(n, *, require=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
297
+ | `.capture(name, signal, *, mode='last')` | Record signal value at current cycle. `mode='last'` (default) overwrites; `'first'` keeps the first write; `'list'` appends to a list. |
273
298
  | `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
274
299
  | `.loop(body, *, until=None, when=None)` | `until`: do-while (exit when True after body). `when`: while (exit when False before body). |
275
300
  | `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
@@ -277,6 +302,34 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
277
302
  | `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
278
303
  | `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
279
304
 
305
+ **Channels**
306
+
307
+ A `Channel` is an identity object representing a shared FIFO consumer group: at most one in-flight pattern instance may consume per cycle. Each `wait()` step has its own implicit channel, so multiple instances of the same pattern automatically serialize one-per-cycle on that step. Pass an explicit `Channel` (or `callable(index, captures) -> Channel`) when you need to override the default serialization — typically when events arrive on physically parallel buses (multi-bank memory, multi-lane retire) and several instances should consume *concurrently* by routing each to its own per-key channel.
308
+
309
+ ```python
310
+ from collections import defaultdict
311
+ from wavekit import Channel, Pattern
312
+
313
+ # Multi-bank cache: each bank has its own response port, so two banks
314
+ # can return data in the *same* cycle. Without partitioning, the default
315
+ # serialization rule would force one instance to wait an extra cycle.
316
+ # A per-bank Channel lets each in-flight read consume from its own bank.
317
+ banks = defaultdict(Channel)
318
+
319
+ result = (
320
+ Pattern()
321
+ .wait(req_valid)
322
+ .capture('bank', req_addr & 1)
323
+ .wait(
324
+ lambda i, cap: bank_valid[cap['bank']].value[i],
325
+ channel=lambda i, cap: banks[cap['bank']],
326
+ )
327
+ .capture('rdata',
328
+ lambda i, cap: bank_data[cap['bank']].value[i])
329
+ .match()
330
+ )
331
+ ```
332
+
280
333
  **`MatchResult`**
281
334
 
282
335
  | Field | Description |
@@ -3,15 +3,21 @@
3
3
  [![CI](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml/badge.svg)](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
4
4
  [![PyPI version](https://img.shields.io/pypi/v/wavekit.svg)](https://pypi.org/project/wavekit/)
5
5
  [![Python Versions](https://img.shields.io/pypi/pyversions/wavekit.svg)](https://pypi.org/project/wavekit/)
6
+ [![Downloads](https://img.shields.io/pypi/dm/wavekit.svg)](https://pypi.org/project/wavekit/)
6
7
  [![License](https://img.shields.io/github/license/cxzzzz/wavekit.svg)](LICENSE)
7
8
 
8
- **Wavekit** is a fundamental Python library for digital waveform analysis. By seamlessly converting VCD and FSDB data into Numpy arrays, it empowers engineers to perform high-performance signal processing, protocol analysis, and automated verification with ease.
9
+ English | [中文](README_ZH.md)
10
+
11
+ **Wavekit** is a fundamental Python library for digital waveform analysis. By seamlessly converting VCD, FST, and FSDB data into Numpy arrays, it empowers engineers to perform high-performance signal processing, protocol analysis, and automated verification with ease.
12
+
13
+ > 🤖 **AI Integration**: [wavekit-mcp](https://github.com/cxzzzz/wavekit-mcp) — MCP server for AI-assisted waveform analysis. Let AI load signals and run pattern matching — no manual coding required.
9
14
 
10
15
  ## ✨ Features
11
16
 
12
- - **High Performance & Easy Loading**: Cython-optimized VCD/FSDB parsers with Numpy-backed storage for speed and memory efficiency, plus flexible batch signal extraction via brace expansion, integer ranges, and regular expressions.
17
+ - **Flexible Signal Extraction**: Flexible batch signal extraction via brace expansion, integer ranges, and regular expressions — load groups of related signals in one call.
13
18
  - **Rich Analysis Tools**: Numpy-like API for arithmetic, masking, bit-field manipulation, edge detection, and time/cycle slicing — compose complex signal queries in just a few lines.
14
19
  - **Pattern Matching**: NFA-based temporal pattern engine that scans waveforms in a single pass to extract protocol transactions, measure latencies, and detect timing violations.
20
+ - **High-Performance Parsing & Storage**: VCD, FST, and FSDB readers with Numpy-backed storage for fast loading and memory efficiency, handling large simulation files with ease.
15
21
 
16
22
  ## 📦 Installation
17
23
 
@@ -26,7 +32,7 @@ pip install wavekit
26
32
 
27
33
  ## 🚀 Quick Start
28
34
 
29
- > The examples below use placeholder filenames such as `sim.vcd`. Replace them with the path to your own VCD or FSDB file, and adjust signal paths to match your design hierarchy.
35
+ > The examples below use placeholder filenames such as `sim.vcd`. Replace them with the path to your own VCD, FST, or FSDB file, and adjust signal paths to match your design hierarchy.
30
36
 
31
37
  ### 1. Batch Signal Extraction
32
38
 
@@ -139,7 +145,7 @@ print(f"Read data: {valid.captures['rdata'].value}")
139
145
  **AXI Write Burst (multi-beat)**
140
146
 
141
147
  ```python
142
- beat = Pattern().wait(wvalid & wready).capture("beats[]", wdata)
148
+ beat = Pattern().wait(wvalid & wready).capture("beats", wdata, mode="list")
143
149
 
144
150
  result = (
145
151
  Pattern()
@@ -177,7 +183,7 @@ print(f"Stall durations: {stalls.duration.value} cycles")
177
183
 
178
184
  | Method | Description |
179
185
  |--------|-------------|
180
- | `VcdReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`). |
186
+ | `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`). |
181
187
  | `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
182
188
  | `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
183
189
  | `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
@@ -240,15 +246,33 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
240
246
  | `wave.rising_edge()` | True at 0→1 transitions |
241
247
  | `wave.falling_edge()` | True at 1→0 transitions |
242
248
 
249
+ **Relative time access**
250
+
251
+ | Method | Description |
252
+ |--------|-------------|
253
+ | `wave.relative(offset, pad, pad_value)` | Shift by *offset* cycles (positive = future, negative = past) |
254
+ | `wave.ahead(n, pad, pad_value)` | Look *n* cycles into the future (shorthand for `relative(n)`) |
255
+ | `wave.back(n, pad, pad_value)` | Look *n* cycles into the past (shorthand for `relative(-n)`) |
256
+
257
+ `pad` controls boundary handling: `'repeat'` (default) pads with the first/last value, `'value'` pads with a given `pad_value`.
258
+
259
+ ```python
260
+ # Rising edge detection
261
+ rising = (wave == 0) & wave.ahead()
262
+
263
+ # Compare current vs 3 cycles ago
264
+ changed = wave != wave.back(3)
265
+ ```
266
+
243
267
  ---
244
268
 
245
269
  ### Pattern
246
270
 
247
271
  | Method | Description |
248
272
  |--------|-------------|
249
- | `.wait(cond, guard=None, channel=None)` | Block until `cond` is True. `guard` is checked each waiting cycle. `channel` enforces FIFO ordering among concurrent instances. |
250
- | `.delay(n, guard=None)` | Advance `n` cycles. `delay(0)` is a no-op. |
251
- | `.capture(name, signal)` | Record signal value at current cycle. `name[]` appends to a list. |
273
+ | `.wait(cond, *, require=None, channel=None, tick=True)` | Block until `cond` is True. `require` is checked each waiting cycle (failure → `REQUIRE_VIOLATED`). `channel` binds the wait to a shared FIFO consumer group (see [Channels](#channels) below). `tick=False` matches on the current cycle without consuming it. |
274
+ | `.delay(n, *, require=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
275
+ | `.capture(name, signal, *, mode='last')` | Record signal value at current cycle. `mode='last'` (default) overwrites; `'first'` keeps the first write; `'list'` appends to a list. |
252
276
  | `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
253
277
  | `.loop(body, *, until=None, when=None)` | `until`: do-while (exit when True after body). `when`: while (exit when False before body). |
254
278
  | `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
@@ -256,6 +280,34 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
256
280
  | `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
257
281
  | `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
258
282
 
283
+ **Channels**
284
+
285
+ A `Channel` is an identity object representing a shared FIFO consumer group: at most one in-flight pattern instance may consume per cycle. Each `wait()` step has its own implicit channel, so multiple instances of the same pattern automatically serialize one-per-cycle on that step. Pass an explicit `Channel` (or `callable(index, captures) -> Channel`) when you need to override the default serialization — typically when events arrive on physically parallel buses (multi-bank memory, multi-lane retire) and several instances should consume *concurrently* by routing each to its own per-key channel.
286
+
287
+ ```python
288
+ from collections import defaultdict
289
+ from wavekit import Channel, Pattern
290
+
291
+ # Multi-bank cache: each bank has its own response port, so two banks
292
+ # can return data in the *same* cycle. Without partitioning, the default
293
+ # serialization rule would force one instance to wait an extra cycle.
294
+ # A per-bank Channel lets each in-flight read consume from its own bank.
295
+ banks = defaultdict(Channel)
296
+
297
+ result = (
298
+ Pattern()
299
+ .wait(req_valid)
300
+ .capture('bank', req_addr & 1)
301
+ .wait(
302
+ lambda i, cap: bank_valid[cap['bank']].value[i],
303
+ channel=lambda i, cap: banks[cap['bank']],
304
+ )
305
+ .capture('rdata',
306
+ lambda i, cap: bank_data[cap['bank']].value[i])
307
+ .match()
308
+ )
309
+ ```
310
+
259
311
  **`MatchResult`**
260
312
 
261
313
  | Field | Description |
@@ -1,10 +1,15 @@
1
+ from pathlib import Path
2
+
1
3
  import numpy as np
2
4
  from Cython.Build import cythonize
3
5
  from setuptools import Extension, setup
4
6
 
7
+ for artifact in Path('src/wavekit').glob('**/*.so'):
8
+ artifact.unlink()
9
+
5
10
  extensions = [
6
11
  Extension(
7
- 'src.wavekit.readers.value_change',
12
+ 'wavekit.readers.value_change',
8
13
  sources=['src/wavekit/readers/value_change.pyx'],
9
14
  include_dirs=[np.get_include()],
10
15
  extra_compile_args=['-fpic', '-O3', '-march=native'],
@@ -12,7 +17,7 @@ extensions = [
12
17
  language='c++',
13
18
  ),
14
19
  Extension(
15
- 'src.wavekit.readers.fsdb.npi_fsdb_reader',
20
+ 'wavekit.readers.fsdb.npi_fsdb_reader',
16
21
  sources=['src/wavekit/readers/fsdb/npi_fsdb_reader.pyx'],
17
22
  include_dirs=[np.get_include()],
18
23
  libraries=['dl'],
@@ -23,8 +28,10 @@ extensions = [
23
28
  ]
24
29
 
25
30
  setup(
31
+ package_dir={'': 'src'},
26
32
  ext_modules=cythonize(
27
33
  extensions,
34
+ include_path=['src'],
28
35
  compiler_directives={
29
36
  'language_level': 3,
30
37
  'embedsignature': True,
@@ -1,17 +1,20 @@
1
1
  [tool.poetry]
2
2
  name = "wavekit"
3
- version = "0.5.6"
3
+ version = "0.6.1"
4
4
  description = "a fundamental package for digital circuit waveform analysis"
5
5
  authors = ["cxzzzz <cxz19961010@outlook.com>"]
6
6
  readme = "README.md"
7
7
  repository = "https://github.com/cxzzzz/wavekit"
8
8
  build = "build.py"
9
- include = ["src/wavekit/**/*.so"]
9
+ include = [
10
+ { path = "src/wavekit/**/*.so", format = "wheel" },
11
+ ]
10
12
 
11
13
  [tool.poetry.dependencies]
12
14
  python = "^3.9"
13
15
  numpy = "^2.0.0"
14
16
  vcdvcd = "^2.3.5"
17
+ pylibfst = "^0.2.1"
15
18
  pytest = "^8.2.2"
16
19
 
17
20
  [tool.poetry.group.dev.dependencies]
@@ -13,10 +13,12 @@ try:
13
13
  except metadata.PackageNotFoundError:
14
14
  __version__ = 'unknown'
15
15
 
16
+ from .pattern import Channel as Channel
16
17
  from .pattern import MatchResult as MatchResult
17
18
  from .pattern import MatchStatus as MatchStatus
18
19
  from .pattern import Pattern as Pattern
19
20
  from .pattern import PatternError as PatternError
21
+ from .readers.fst.reader import FstReader as FstReader
20
22
  from .readers.vcd.reader import VcdReader as VcdReader
21
23
  from .scope import Scope as Scope
22
24
  from .signal import Signal as Signal
@@ -27,6 +29,7 @@ __all__ = [
27
29
  'Waveform',
28
30
  'VcdReader',
29
31
  'FsdbReader',
32
+ 'FstReader',
30
33
  'Scope',
31
34
  'Signal',
32
35
  'SignalCompositeType',
@@ -34,6 +37,7 @@ __all__ = [
34
37
  'MatchResult',
35
38
  'MatchStatus',
36
39
  'PatternError',
40
+ 'Channel',
37
41
  'has_fsdb_support',
38
42
  ]
39
43
 
@@ -4,5 +4,6 @@ from .dsl import Pattern as Pattern
4
4
  from .engine import PatternError as PatternError
5
5
  from .result import MatchResult as MatchResult
6
6
  from .result import MatchStatus as MatchStatus
7
+ from .steps import Channel as Channel
7
8
 
8
- __all__ = ['Pattern', 'MatchResult', 'MatchStatus', 'PatternError']
9
+ __all__ = ['Pattern', 'MatchResult', 'MatchStatus', 'PatternError', 'Channel']
@@ -3,8 +3,10 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  from .steps import (
6
+ _VALID_CAPTURE_MODES,
6
7
  BranchStep,
7
8
  CaptureStep,
9
+ ChannelValue,
8
10
  DelayStep,
9
11
  LoopStep,
10
12
  RepeatStep,
@@ -15,7 +17,7 @@ from .steps import (
15
17
 
16
18
  if TYPE_CHECKING:
17
19
  from .result import MatchResult
18
- from .steps import Condition, IntValue, SignalValue
20
+ from .steps import CaptureMode, Condition, IntValue, SignalValue
19
21
 
20
22
 
21
23
  class Pattern:
@@ -43,25 +45,73 @@ class Pattern:
43
45
  def wait(
44
46
  self,
45
47
  cond: Condition,
46
- guard: Condition | None = None,
47
- channel: str | None = None,
48
+ *,
49
+ require: Condition | None = None,
50
+ channel: ChannelValue | None = None,
51
+ tick: bool = True,
48
52
  ) -> Pattern:
49
53
  """Block until *cond* becomes True.
50
54
 
55
+ By default each ``wait()`` step has its own private FIFO consumer
56
+ group: when multiple in-flight instances reach this step, only one
57
+ (the oldest) advances per cycle. Pass an explicit :class:`Channel`
58
+ via ``channel=`` to share the consumer group across multiple wait
59
+ steps (e.g., for AXI request/response pairing).
60
+
51
61
  Parameters
52
62
  ----------
53
63
  cond:
54
64
  Waveform (static) or ``callable(index, captures) -> bool`` (dynamic).
55
- guard:
56
- Must remain True every cycle while waiting; violation terminates
57
- the instance with ``REQUIRE_VIOLATED``.
65
+ require:
66
+ Optional condition that must hold every cycle while waiting.
67
+ Violation terminates the instance with ``REQUIRE_VIOLATED``.
58
68
  channel:
59
- Named channel for FIFO event-consumption (see channel docs).
69
+ Explicit :class:`Channel` (or ``callable(index, captures) -> Channel``
70
+ for dynamic key-based partitioning) that binds this wait to a
71
+ shared FIFO consumer group. ``None`` (default) uses an implicit
72
+ per-step channel.
73
+ tick:
74
+ When ``True`` (default), a successful match advances time by one
75
+ cycle before the next step is evaluated. When ``False`` the next
76
+ step evaluates on the **same** cycle (zero-cycle wait), useful
77
+ for measuring intervals like ``valid → valid & ready``.
78
+
79
+ Examples
80
+ --------
81
+ Static channel shared between request and response steps::
82
+
83
+ rsp_chan = Channel()
84
+ (Pattern()
85
+ .wait(req)
86
+ .wait(rsp, channel=rsp_chan))
87
+
88
+ Dynamic per-ID partitioning for AXI out-of-order reads::
89
+
90
+ from collections import defaultdict
91
+ chans = defaultdict(Channel)
92
+ (Pattern()
93
+ .wait(arvalid & arready)
94
+ .capture('arid', arid)
95
+ .wait(
96
+ lambda i, cap: bool((rvalid & rready)[i]) and rid[i] == cap['arid'],
97
+ channel=lambda i, cap: chans[cap['arid']],
98
+ ))
99
+
100
+ Zero-cycle wait::
101
+
102
+ (Pattern()
103
+ .wait(valid)
104
+ .wait(valid & ready, tick=False)) # measures latency in same-cycle case
60
105
  """
61
- self._steps.append(WaitStep(cond=cond, guard=guard, channel=channel))
106
+ self._steps.append(WaitStep(cond=cond, require=require, channel=channel, tick=tick))
62
107
  return self
63
108
 
64
- def delay(self, n: IntValue, guard: Condition | None = None) -> Pattern:
109
+ def delay(
110
+ self,
111
+ n: IntValue,
112
+ *,
113
+ require: Condition | None = None,
114
+ ) -> Pattern:
65
115
  """Block for exactly *n* cycles.
66
116
 
67
117
  Parameters
@@ -69,25 +119,43 @@ class Pattern:
69
119
  n:
70
120
  Static ``int`` (>= 0) or ``callable(index, captures) -> int``.
71
121
  ``delay(0)`` is an epsilon step (no cycle consumed).
72
- guard:
73
- Must remain True during the delay; violation terminates
74
- the instance with ``REQUIRE_VIOLATED``.
122
+ require:
123
+ Optional condition that must hold every cycle during the delay.
124
+ Violation terminates the instance with ``REQUIRE_VIOLATED``.
75
125
  """
76
126
  if isinstance(n, int) and n < 0:
77
127
  raise ValueError(f'delay() requires n >= 0, got {n}')
78
- self._steps.append(DelayStep(n=n, guard=guard))
128
+ self._steps.append(DelayStep(n=n, require=require))
79
129
  return self
80
130
 
81
131
  # ------------------------------------------------------------------
82
132
  # Epsilon steps
83
133
  # ------------------------------------------------------------------
84
134
 
85
- def capture(self, name: str, signal: SignalValue) -> Pattern:
135
+ def capture(
136
+ self,
137
+ name: str,
138
+ signal: SignalValue,
139
+ *,
140
+ mode: CaptureMode = 'last',
141
+ ) -> Pattern:
86
142
  """Record a signal value at the current cycle.
87
143
 
88
- Use ``name[]`` to append to a list (for use inside loop/repeat).
144
+ Parameters
145
+ ----------
146
+ name:
147
+ Capture key.
148
+ signal:
149
+ Waveform (static) or ``callable(index, captures) -> Any``.
150
+ mode:
151
+ * ``'last'`` (default) – ``cap[name]`` holds the most recent value
152
+ * ``'first'`` – ``cap[name]`` holds the first value; subsequent
153
+ writes are ignored
154
+ * ``'list'`` – ``cap[name]`` is a list, each write appends
89
155
  """
90
- self._steps.append(CaptureStep(name=name, signal=signal))
156
+ if mode not in _VALID_CAPTURE_MODES:
157
+ raise ValueError(f'capture mode must be one of {_VALID_CAPTURE_MODES}, got {mode!r}')
158
+ self._steps.append(CaptureStep(name=name, signal=signal, mode=mode))
91
159
  return self
92
160
 
93
161
  def require(self, cond: Condition) -> Pattern:
@@ -166,10 +234,6 @@ class Pattern:
166
234
  ) -> MatchResult:
167
235
  """Run the pattern engine and return a :class:`MatchResult`.
168
236
 
169
- If the first step is a ``wait``, its condition is used as the trigger
170
- (only fork instances when the condition is True). Otherwise, an
171
- instance is forked on every cycle.
172
-
173
237
  Parameters
174
238
  ----------
175
239
  start_cycle: