wavekit 0.6.1__tar.gz → 0.7.0a1__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 (29) hide show
  1. {wavekit-0.6.1 → wavekit-0.7.0a1}/PKG-INFO +140 -33
  2. {wavekit-0.6.1 → wavekit-0.7.0a1}/README.md +139 -32
  3. {wavekit-0.6.1 → wavekit-0.7.0a1}/pyproject.toml +7 -1
  4. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/pattern/__init__.py +1 -1
  5. wavekit-0.7.0a1/src/wavekit/pattern/compiler.py +172 -0
  6. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/pattern/dsl.py +118 -51
  7. wavekit-0.7.0a1/src/wavekit/pattern/errors.py +5 -0
  8. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/pattern/result.py +36 -3
  9. wavekit-0.7.0a1/src/wavekit/pattern/runtime.py +526 -0
  10. wavekit-0.7.0a1/src/wavekit/pattern/steps.py +179 -0
  11. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/readers/base.py +323 -6
  12. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +52 -21
  13. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/readers/fsdb/reader.py +27 -83
  14. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/readers/fst/reader.py +41 -96
  15. wavekit-0.7.0a1/src/wavekit/readers/vcd/reader.py +161 -0
  16. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/waveform.py +2 -2
  17. wavekit-0.6.1/src/wavekit/pattern/engine.py +0 -476
  18. wavekit-0.6.1/src/wavekit/pattern/instance.py +0 -28
  19. wavekit-0.6.1/src/wavekit/pattern/steps.py +0 -216
  20. wavekit-0.6.1/src/wavekit/readers/vcd/reader.py +0 -204
  21. {wavekit-0.6.1 → wavekit-0.7.0a1}/LICENSE +0 -0
  22. {wavekit-0.6.1 → wavekit-0.7.0a1}/build.py +0 -0
  23. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/__init__.py +0 -0
  24. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/readers/expr_parser.py +0 -0
  25. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
  26. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/readers/pattern_parser.py +0 -0
  27. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/readers/value_change.pyx +0 -0
  28. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/scope.py +0 -0
  29. {wavekit-0.6.1 → wavekit-0.7.0a1}/src/wavekit/signal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavekit
3
- Version: 0.6.1
3
+ Version: 0.7.0a1
4
4
  Summary: a fundamental package for digital circuit waveform analysis
5
5
  License-File: LICENSE
6
6
  Author: cxzzzz
@@ -25,7 +25,7 @@ Description-Content-Type: text/markdown
25
25
  [![CI](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml/badge.svg)](https://github.com/cxzzzz/wavekit/actions/workflows/python-package.yml)
26
26
  [![PyPI version](https://img.shields.io/pypi/v/wavekit.svg)](https://pypi.org/project/wavekit/)
27
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/)
28
+ [![Downloads](https://pepy.tech/badge/wavekit)](https://pepy.tech/project/wavekit)
29
29
  [![License](https://img.shields.io/github/license/cxzzzz/wavekit.svg)](LICENSE)
30
30
 
31
31
  English | [中文](README_ZH.md)
@@ -38,7 +38,7 @@ English | [中文](README_ZH.md)
38
38
 
39
39
  - **Flexible Signal Extraction**: Flexible batch signal extraction via brace expansion, integer ranges, and regular expressions — load groups of related signals in one call.
40
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.
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.
41
+ - **Pattern Matching**: Unified temporal pattern runtime that scans waveforms in a single pass to extract protocol transactions, measure latencies, and detect timing violations.
42
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.
43
43
 
44
44
  ## 📦 Installation
@@ -106,6 +106,25 @@ with VcdReader("fifo_tb.vcd") as f:
106
106
  burst_cycles = wr_en.rising_edge()
107
107
  ```
108
108
 
109
+ To inspect unknown/high-impedance source bits without changing the ordinary
110
+ two-state value model, load an unsigned unknown mask alongside the value
111
+ waveform. Each mask bit is `1` where the source sample contained `X` or `Z`.
112
+
113
+ > **Experimental**: The `load_unknown_mask` / `load_matched_unknown_masks`
114
+ > APIs are experimental and may change in a future release.
115
+
116
+ ```python
117
+ from wavekit import VcdReader
118
+
119
+ with VcdReader("fifo_tb.vcd") as f:
120
+ clock = "fifo_tb.clk"
121
+ data = f.load_waveform("fifo_tb.s_fifo.data[7:0]", clock=clock, xz_value=0)
122
+ unknown = f.load_unknown_mask("fifo_tb.s_fifo.data[7:0]", clock=clock)
123
+
124
+ # Keep only samples whose source bits were fully known.
125
+ known_data = data.mask(unknown == 0)
126
+ ```
127
+
109
128
  ---
110
129
 
111
130
  ### 3. Expression Evaluation
@@ -135,9 +154,16 @@ with VcdReader("fifo_tb.vcd") as f:
135
154
 
136
155
  ### 4. Pattern Matching
137
156
 
138
- Describe a temporal sequence of events; the engine finds all matching transactions in one pass.
157
+ `Pattern` scans a waveform and extracts all matching transactions — a request/response pair, a burst, a stall interval, or any other repeating timing pattern.
158
+
159
+ There are two ways to describe a pattern:
160
+
161
+ - **Declarative** — chain steps like `.wait()`, `.consume()`, `.capture()`, `.loop()`. Best for fixed transaction flows.
162
+ - **Programmable** — pass a handler function to `Pattern(...)`. Best for dynamic branches, per-ID routing, and other complex flows.
139
163
 
140
- **AXI-lite Read Latency**
164
+ #### Declarative examples
165
+
166
+ **AXI-Lite read latency**
141
167
 
142
168
  ```python
143
169
  from wavekit import VcdReader, Pattern
@@ -150,38 +176,37 @@ with VcdReader("axi_tb.vcd") as f:
150
176
  rready = f.load_waveform("tb.dut.rready", clock=clk)
151
177
  rdata = f.load_waveform("tb.dut.rdata[31:0]", clock=clk)
152
178
 
153
- result = (
154
- Pattern()
155
- .wait(arvalid & arready) # AR handshake → start
156
- .wait(rvalid & rready) # R handshake → end
157
- .capture("rdata", rdata)
158
- .timeout(256)
159
- .match()
160
- )
179
+ result = (
180
+ Pattern(timeout=256)
181
+ .wait(arvalid & arready) # AR handshake → transaction starts
182
+ .wait(rvalid & rready) # R handshake → transaction ends
183
+ .capture("rdata", rdata)
184
+ .match()
185
+ )
161
186
 
162
- valid = result.filter_valid()
163
- print(f"Read latencies (cycles): {valid.duration.value}")
164
- print(f"Read data: {valid.captures['rdata'].value}")
187
+ ok = result.filter_ok()
188
+ print(f"Read latencies (cycles): {ok.duration.value}")
189
+ print(f"Read data: {ok.captures['rdata'].value}")
165
190
  ```
166
191
 
167
- **AXI Write Burst (multi-beat)**
192
+ **AXI write burst (multi-beat)**
168
193
 
169
194
  ```python
170
- beat = Pattern().wait(wvalid & wready).capture("beats", wdata, mode="list")
195
+ beat = Pattern().consume(wvalid & wready, channel="w").capture("beats", wdata, mode="list")
171
196
 
172
197
  result = (
173
198
  Pattern()
174
- .wait(awvalid & awready) # AW handshake → burst start
175
- .loop(beat, until=wlast) # collect beats until wlast
199
+ .wait(awvalid & awready) # AW handshake → burst starts
200
+ .loop(beat, until=wlast) # collect each beat until wlast
176
201
  .timeout(512)
177
202
  .match()
178
203
  )
179
204
 
180
- for i, inst in enumerate(result.filter_valid()):
205
+ for i, inst in enumerate(result.filter_ok()):
181
206
  print(f"Burst {i}: {len(inst.captures['beats'])} beats")
182
207
  ```
183
208
 
184
- **Stall Detection**
209
+ **Stall detection**
185
210
 
186
211
  ```python
187
212
  stall = valid & (ready == 0)
@@ -189,14 +214,51 @@ stall = valid & (ready == 0)
189
214
  result = (
190
215
  Pattern()
191
216
  .wait(stall.rising_edge()) # stall begins
192
- .loop(Pattern().delay(1), when=stall) # wait while stalling
217
+ .loop(Pattern().delay(1), when=stall) # keep waiting until stall ends
193
218
  .match()
194
219
  )
195
220
 
196
- stalls = result.filter_valid()
221
+ stalls = result.filter_ok()
197
222
  print(f"Stall durations: {stalls.duration.value} cycles")
198
223
  ```
199
224
 
225
+ #### Programmable example
226
+
227
+ **Out-of-order AXI reads by ID**
228
+
229
+ When R beats from different IDs interleave on the bus, match each AR to its response beats by `arid` and collect results as Python dicts.
230
+
231
+ ```python
232
+ arfire = arvalid & arready # precompute outside the handler
233
+ rfire = rvalid & rready
234
+
235
+ async def read_burst(ctx):
236
+ if ctx.value(arfire):
237
+ my_id = ctx.value(arid)
238
+ beats = []
239
+
240
+ while True:
241
+ await ctx.consume(
242
+ lambda: ctx.value(rfire) and ctx.value(rid) == my_id,
243
+ channel=("r", my_id),
244
+ )
245
+ beats.append(int(ctx.value(rdata)))
246
+ if ctx.value(rlast):
247
+ break
248
+
249
+ return {"arid": my_id, "beats": beats}
250
+ return None
251
+
252
+ records = Pattern(read_burst, timeout=64).collect()
253
+ ```
254
+
255
+ Some tips for programmable patterns:
256
+
257
+ - Precompute fixed waveform expressions (like `fire = valid & ready`) outside
258
+ the handler function so they aren't rebuilt every cycle.
259
+ - Start the handler with `if ctx.value(fire): ...` and `return None`
260
+ otherwise — this tells the runtime which cycles begin a transaction.
261
+
200
262
  ---
201
263
 
202
264
  ## 📖 API Reference
@@ -207,7 +269,9 @@ print(f"Stall durations: {stalls.duration.value} cycles")
207
269
  |--------|-------------|
208
270
  | `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`). |
209
271
  | `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
272
+ | `reader.load_unknown_mask(signal, clock, ...)` | **Experimental.** Load X/Z bit presence as an unsigned mask `Waveform`. |
210
273
  | `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
274
+ | `reader.load_matched_unknown_masks(pattern, clock_pattern, ...)` | **Experimental.** Batch-load X/Z masks for matched signals. Returns `dict[tuple, Waveform]`. |
211
275
  | `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
212
276
  | `reader.get_matched_signals(pattern)` | Resolve a pattern to signal paths without loading data. |
213
277
  | `reader.top_scope_list()` | Return root `Scope` nodes of the signal hierarchy. |
@@ -290,9 +354,21 @@ changed = wave != wave.back(3)
290
354
 
291
355
  ### Pattern
292
356
 
357
+ **Construction**
358
+
359
+ | API | Description |
360
+ |-----|-------------|
361
+ | `Pattern(timeout=..., max_active=...)` | Create a Declarative Pattern. Add steps with builder methods, then call `.match()`. |
362
+ | `Pattern(async_fn, timeout=..., max_active=...)` | Create a Programmable Pattern. The async function receives `ctx`. |
363
+ | `.match(start_cycle=None, end_cycle=None)` | Run the pattern and return `MatchResult`. In a Programmable Pattern, return `ctx.OK` for success and `None` to skip. |
364
+ | `.collect(start_cycle=None, end_cycle=None)` | Programmable Pattern only. Collect each non-`None` Python return value. |
365
+
366
+ **Declarative Steps**
367
+
293
368
  | Method | Description |
294
369
  |--------|-------------|
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. |
370
+ | `.wait(cond, *, require=None)` | Block until `cond` is True without consuming the event. Resumes in the same cycle when already true; use `.delay(1)` for next-cycle behavior. `require` is checked each waiting cycle (failure → `REQUIRE_VIOLATED`). |
371
+ | `.consume(cond, channel, *, require=None)` | Block until `cond` is True and this instance can exclusively consume from `channel`. Resumes in the same cycle on success. Use this for FIFO request/response pairing and per-key routing. |
296
372
  | `.delay(n, *, require=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
297
373
  | `.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. |
298
374
  | `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
@@ -300,27 +376,54 @@ changed = wave != wave.back(3)
300
376
  | `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
301
377
  | `.branch(cond, true_body, false_body)` | Conditional branch. |
302
378
  | `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
303
- | `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
304
379
 
305
- **Channels**
380
+ The same time and ownership operations are available inside Programmable
381
+ Patterns as `await ctx.wait(...)`, `await ctx.consume(...)`, and
382
+ `await ctx.delay(...)`.
383
+
384
+ **Programmable Context**
385
+
386
+ | API | Description |
387
+ |-----|-------------|
388
+ | `ctx.value(waveform, offset=0)` | Read a scalar value at the current sample plus optional offset. |
389
+ | `ctx.cycle(waveform, offset=0)` | Read the cycle number at the current sample plus optional offset. |
390
+ | `ctx.time(waveform, offset=0)` | Read the timestamp at the current sample plus optional offset. |
391
+ | `await ctx.wait(cond, require=None)` | Observe cycles until `cond` is true; does not consume the event. |
392
+ | `await ctx.consume(cond, channel, require=None)` | Wait for `cond` and exclusively consume from `channel`. |
393
+ | `await ctx.delay(n, require=None)` | Advance `n` cycles. |
394
+ | `ctx.capture(name, value, mode='last')` | Record a capture for programmable `.match()`. |
395
+ | `ctx.OK` | Return from programmable `.match()` to record a successful match. |
396
+
397
+ **Dynamic callbacks**
398
+
399
+ Declarative callbacks use `callable(index, captures)`. `index` is the absolute
400
+ waveform sample index; it is not rebased when `match(start_cycle=...)` is used.
401
+
402
+ **Channels and consume vs. wait**
403
+
404
+ When multiple in-flight instances are waiting for the same kind of event, plain
405
+ `wait()` won't pair each request with its own response — every instance sees
406
+ every event. `consume()` solves this: it hands the event to exactly one instance
407
+ per cycle, in FIFO order.
306
408
 
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.
409
+ A `Channel` is the FIFO queue that `consume()` uses. Pass a `Channel` object, a
410
+ hashable key, or a dynamic callback to `consume(..., channel=...)`. All
411
+ instances sharing the same channel key consume from the same queue.
308
412
 
309
413
  ```python
310
414
  from collections import defaultdict
311
415
  from wavekit import Channel, Pattern
312
416
 
313
417
  # 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.
418
+ # can return data in the *same* cycle. A per-bank Channel lets each in-flight
419
+ # read consume from its own bank while preserving FIFO order within that bank.
317
420
  banks = defaultdict(Channel)
318
421
 
319
422
  result = (
320
423
  Pattern()
321
424
  .wait(req_valid)
322
425
  .capture('bank', req_addr & 1)
323
- .wait(
426
+ .consume(
324
427
  lambda i, cap: bank_valid[cap['bank']].value[i],
325
428
  channel=lambda i, cap: banks[cap['bank']],
326
429
  )
@@ -338,7 +441,11 @@ result = (
338
441
  | `.duration` | `end - start + 1` cycles. |
339
442
  | `.status` | `MatchStatus.OK`, `TIMEOUT`, or `REQUIRE_VIOLATED`. |
340
443
  | `.captures` | `dict[str, Waveform]` of captured values. |
341
- | `.filter_valid()` | Return only `OK` matches. |
444
+ | `.ok` | Boolean Waveform where `status == MatchStatus.OK`. |
445
+ | `.failed` | Boolean Waveform where `status != MatchStatus.OK`. |
446
+ | `.filter_ok()` | Return only `OK` matches. |
447
+ | `.filter_status(status)` | Return only matches with the given `MatchStatus` or integer status. |
448
+ | `.filter_failed()` | Return only non-OK matches. |
342
449
 
343
450
  ---
344
451
 
@@ -3,7 +3,7 @@
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
+ [![Downloads](https://pepy.tech/badge/wavekit)](https://pepy.tech/project/wavekit)
7
7
  [![License](https://img.shields.io/github/license/cxzzzz/wavekit.svg)](LICENSE)
8
8
 
9
9
  English | [中文](README_ZH.md)
@@ -16,7 +16,7 @@ English | [中文](README_ZH.md)
16
16
 
17
17
  - **Flexible Signal Extraction**: Flexible batch signal extraction via brace expansion, integer ranges, and regular expressions — load groups of related signals in one call.
18
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.
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.
19
+ - **Pattern Matching**: Unified temporal pattern runtime that scans waveforms in a single pass to extract protocol transactions, measure latencies, and detect timing violations.
20
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.
21
21
 
22
22
  ## 📦 Installation
@@ -84,6 +84,25 @@ with VcdReader("fifo_tb.vcd") as f:
84
84
  burst_cycles = wr_en.rising_edge()
85
85
  ```
86
86
 
87
+ To inspect unknown/high-impedance source bits without changing the ordinary
88
+ two-state value model, load an unsigned unknown mask alongside the value
89
+ waveform. Each mask bit is `1` where the source sample contained `X` or `Z`.
90
+
91
+ > **Experimental**: The `load_unknown_mask` / `load_matched_unknown_masks`
92
+ > APIs are experimental and may change in a future release.
93
+
94
+ ```python
95
+ from wavekit import VcdReader
96
+
97
+ with VcdReader("fifo_tb.vcd") as f:
98
+ clock = "fifo_tb.clk"
99
+ data = f.load_waveform("fifo_tb.s_fifo.data[7:0]", clock=clock, xz_value=0)
100
+ unknown = f.load_unknown_mask("fifo_tb.s_fifo.data[7:0]", clock=clock)
101
+
102
+ # Keep only samples whose source bits were fully known.
103
+ known_data = data.mask(unknown == 0)
104
+ ```
105
+
87
106
  ---
88
107
 
89
108
  ### 3. Expression Evaluation
@@ -113,9 +132,16 @@ with VcdReader("fifo_tb.vcd") as f:
113
132
 
114
133
  ### 4. Pattern Matching
115
134
 
116
- Describe a temporal sequence of events; the engine finds all matching transactions in one pass.
135
+ `Pattern` scans a waveform and extracts all matching transactions — a request/response pair, a burst, a stall interval, or any other repeating timing pattern.
136
+
137
+ There are two ways to describe a pattern:
138
+
139
+ - **Declarative** — chain steps like `.wait()`, `.consume()`, `.capture()`, `.loop()`. Best for fixed transaction flows.
140
+ - **Programmable** — pass a handler function to `Pattern(...)`. Best for dynamic branches, per-ID routing, and other complex flows.
117
141
 
118
- **AXI-lite Read Latency**
142
+ #### Declarative examples
143
+
144
+ **AXI-Lite read latency**
119
145
 
120
146
  ```python
121
147
  from wavekit import VcdReader, Pattern
@@ -128,38 +154,37 @@ with VcdReader("axi_tb.vcd") as f:
128
154
  rready = f.load_waveform("tb.dut.rready", clock=clk)
129
155
  rdata = f.load_waveform("tb.dut.rdata[31:0]", clock=clk)
130
156
 
131
- result = (
132
- Pattern()
133
- .wait(arvalid & arready) # AR handshake → start
134
- .wait(rvalid & rready) # R handshake → end
135
- .capture("rdata", rdata)
136
- .timeout(256)
137
- .match()
138
- )
157
+ result = (
158
+ Pattern(timeout=256)
159
+ .wait(arvalid & arready) # AR handshake → transaction starts
160
+ .wait(rvalid & rready) # R handshake → transaction ends
161
+ .capture("rdata", rdata)
162
+ .match()
163
+ )
139
164
 
140
- valid = result.filter_valid()
141
- print(f"Read latencies (cycles): {valid.duration.value}")
142
- print(f"Read data: {valid.captures['rdata'].value}")
165
+ ok = result.filter_ok()
166
+ print(f"Read latencies (cycles): {ok.duration.value}")
167
+ print(f"Read data: {ok.captures['rdata'].value}")
143
168
  ```
144
169
 
145
- **AXI Write Burst (multi-beat)**
170
+ **AXI write burst (multi-beat)**
146
171
 
147
172
  ```python
148
- beat = Pattern().wait(wvalid & wready).capture("beats", wdata, mode="list")
173
+ beat = Pattern().consume(wvalid & wready, channel="w").capture("beats", wdata, mode="list")
149
174
 
150
175
  result = (
151
176
  Pattern()
152
- .wait(awvalid & awready) # AW handshake → burst start
153
- .loop(beat, until=wlast) # collect beats until wlast
177
+ .wait(awvalid & awready) # AW handshake → burst starts
178
+ .loop(beat, until=wlast) # collect each beat until wlast
154
179
  .timeout(512)
155
180
  .match()
156
181
  )
157
182
 
158
- for i, inst in enumerate(result.filter_valid()):
183
+ for i, inst in enumerate(result.filter_ok()):
159
184
  print(f"Burst {i}: {len(inst.captures['beats'])} beats")
160
185
  ```
161
186
 
162
- **Stall Detection**
187
+ **Stall detection**
163
188
 
164
189
  ```python
165
190
  stall = valid & (ready == 0)
@@ -167,14 +192,51 @@ stall = valid & (ready == 0)
167
192
  result = (
168
193
  Pattern()
169
194
  .wait(stall.rising_edge()) # stall begins
170
- .loop(Pattern().delay(1), when=stall) # wait while stalling
195
+ .loop(Pattern().delay(1), when=stall) # keep waiting until stall ends
171
196
  .match()
172
197
  )
173
198
 
174
- stalls = result.filter_valid()
199
+ stalls = result.filter_ok()
175
200
  print(f"Stall durations: {stalls.duration.value} cycles")
176
201
  ```
177
202
 
203
+ #### Programmable example
204
+
205
+ **Out-of-order AXI reads by ID**
206
+
207
+ When R beats from different IDs interleave on the bus, match each AR to its response beats by `arid` and collect results as Python dicts.
208
+
209
+ ```python
210
+ arfire = arvalid & arready # precompute outside the handler
211
+ rfire = rvalid & rready
212
+
213
+ async def read_burst(ctx):
214
+ if ctx.value(arfire):
215
+ my_id = ctx.value(arid)
216
+ beats = []
217
+
218
+ while True:
219
+ await ctx.consume(
220
+ lambda: ctx.value(rfire) and ctx.value(rid) == my_id,
221
+ channel=("r", my_id),
222
+ )
223
+ beats.append(int(ctx.value(rdata)))
224
+ if ctx.value(rlast):
225
+ break
226
+
227
+ return {"arid": my_id, "beats": beats}
228
+ return None
229
+
230
+ records = Pattern(read_burst, timeout=64).collect()
231
+ ```
232
+
233
+ Some tips for programmable patterns:
234
+
235
+ - Precompute fixed waveform expressions (like `fire = valid & ready`) outside
236
+ the handler function so they aren't rebuilt every cycle.
237
+ - Start the handler with `if ctx.value(fire): ...` and `return None`
238
+ otherwise — this tells the runtime which cycles begin a transaction.
239
+
178
240
  ---
179
241
 
180
242
  ## 📖 API Reference
@@ -185,7 +247,9 @@ print(f"Stall durations: {stalls.duration.value} cycles")
185
247
  |--------|-------------|
186
248
  | `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`). |
187
249
  | `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
250
+ | `reader.load_unknown_mask(signal, clock, ...)` | **Experimental.** Load X/Z bit presence as an unsigned mask `Waveform`. |
188
251
  | `reader.load_matched_waveforms(pattern, clock_pattern, ...)` | Batch-load signals matching a brace/regex pattern. Returns `dict[tuple, Waveform]`. |
252
+ | `reader.load_matched_unknown_masks(pattern, clock_pattern, ...)` | **Experimental.** Batch-load X/Z masks for matched signals. Returns `dict[tuple, Waveform]`. |
189
253
  | `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
190
254
  | `reader.get_matched_signals(pattern)` | Resolve a pattern to signal paths without loading data. |
191
255
  | `reader.top_scope_list()` | Return root `Scope` nodes of the signal hierarchy. |
@@ -268,9 +332,21 @@ changed = wave != wave.back(3)
268
332
 
269
333
  ### Pattern
270
334
 
335
+ **Construction**
336
+
337
+ | API | Description |
338
+ |-----|-------------|
339
+ | `Pattern(timeout=..., max_active=...)` | Create a Declarative Pattern. Add steps with builder methods, then call `.match()`. |
340
+ | `Pattern(async_fn, timeout=..., max_active=...)` | Create a Programmable Pattern. The async function receives `ctx`. |
341
+ | `.match(start_cycle=None, end_cycle=None)` | Run the pattern and return `MatchResult`. In a Programmable Pattern, return `ctx.OK` for success and `None` to skip. |
342
+ | `.collect(start_cycle=None, end_cycle=None)` | Programmable Pattern only. Collect each non-`None` Python return value. |
343
+
344
+ **Declarative Steps**
345
+
271
346
  | Method | Description |
272
347
  |--------|-------------|
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. |
348
+ | `.wait(cond, *, require=None)` | Block until `cond` is True without consuming the event. Resumes in the same cycle when already true; use `.delay(1)` for next-cycle behavior. `require` is checked each waiting cycle (failure → `REQUIRE_VIOLATED`). |
349
+ | `.consume(cond, channel, *, require=None)` | Block until `cond` is True and this instance can exclusively consume from `channel`. Resumes in the same cycle on success. Use this for FIFO request/response pairing and per-key routing. |
274
350
  | `.delay(n, *, require=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
275
351
  | `.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. |
276
352
  | `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
@@ -278,27 +354,54 @@ changed = wave != wave.back(3)
278
354
  | `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
279
355
  | `.branch(cond, true_body, false_body)` | Conditional branch. |
280
356
  | `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
281
- | `.match(start_cycle=None, end_cycle=None)` | Run the engine; return `MatchResult`. |
282
357
 
283
- **Channels**
358
+ The same time and ownership operations are available inside Programmable
359
+ Patterns as `await ctx.wait(...)`, `await ctx.consume(...)`, and
360
+ `await ctx.delay(...)`.
361
+
362
+ **Programmable Context**
363
+
364
+ | API | Description |
365
+ |-----|-------------|
366
+ | `ctx.value(waveform, offset=0)` | Read a scalar value at the current sample plus optional offset. |
367
+ | `ctx.cycle(waveform, offset=0)` | Read the cycle number at the current sample plus optional offset. |
368
+ | `ctx.time(waveform, offset=0)` | Read the timestamp at the current sample plus optional offset. |
369
+ | `await ctx.wait(cond, require=None)` | Observe cycles until `cond` is true; does not consume the event. |
370
+ | `await ctx.consume(cond, channel, require=None)` | Wait for `cond` and exclusively consume from `channel`. |
371
+ | `await ctx.delay(n, require=None)` | Advance `n` cycles. |
372
+ | `ctx.capture(name, value, mode='last')` | Record a capture for programmable `.match()`. |
373
+ | `ctx.OK` | Return from programmable `.match()` to record a successful match. |
374
+
375
+ **Dynamic callbacks**
376
+
377
+ Declarative callbacks use `callable(index, captures)`. `index` is the absolute
378
+ waveform sample index; it is not rebased when `match(start_cycle=...)` is used.
379
+
380
+ **Channels and consume vs. wait**
381
+
382
+ When multiple in-flight instances are waiting for the same kind of event, plain
383
+ `wait()` won't pair each request with its own response — every instance sees
384
+ every event. `consume()` solves this: it hands the event to exactly one instance
385
+ per cycle, in FIFO order.
284
386
 
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.
387
+ A `Channel` is the FIFO queue that `consume()` uses. Pass a `Channel` object, a
388
+ hashable key, or a dynamic callback to `consume(..., channel=...)`. All
389
+ instances sharing the same channel key consume from the same queue.
286
390
 
287
391
  ```python
288
392
  from collections import defaultdict
289
393
  from wavekit import Channel, Pattern
290
394
 
291
395
  # 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.
396
+ # can return data in the *same* cycle. A per-bank Channel lets each in-flight
397
+ # read consume from its own bank while preserving FIFO order within that bank.
295
398
  banks = defaultdict(Channel)
296
399
 
297
400
  result = (
298
401
  Pattern()
299
402
  .wait(req_valid)
300
403
  .capture('bank', req_addr & 1)
301
- .wait(
404
+ .consume(
302
405
  lambda i, cap: bank_valid[cap['bank']].value[i],
303
406
  channel=lambda i, cap: banks[cap['bank']],
304
407
  )
@@ -316,7 +419,11 @@ result = (
316
419
  | `.duration` | `end - start + 1` cycles. |
317
420
  | `.status` | `MatchStatus.OK`, `TIMEOUT`, or `REQUIRE_VIOLATED`. |
318
421
  | `.captures` | `dict[str, Waveform]` of captured values. |
319
- | `.filter_valid()` | Return only `OK` matches. |
422
+ | `.ok` | Boolean Waveform where `status == MatchStatus.OK`. |
423
+ | `.failed` | Boolean Waveform where `status != MatchStatus.OK`. |
424
+ | `.filter_ok()` | Return only `OK` matches. |
425
+ | `.filter_status(status)` | Return only matches with the given `MatchStatus` or integer status. |
426
+ | `.filter_failed()` | Return only non-OK matches. |
320
427
 
321
428
  ---
322
429
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wavekit"
3
- version = "0.6.1"
3
+ version = "0.7.0a1"
4
4
  description = "a fundamental package for digital circuit waveform analysis"
5
5
  authors = ["cxzzzz <cxz19961010@outlook.com>"]
6
6
  readme = "README.md"
@@ -25,6 +25,7 @@ mypy = "^1.11.2"
25
25
  target-version = "py39"
26
26
  line-length = 100
27
27
  src = ["src", "tests"]
28
+ exclude = ["tmp"]
28
29
 
29
30
  [tool.ruff.lint]
30
31
  select = ["E", "F", "W", "I", "UP", "B"]
@@ -36,9 +37,14 @@ select = ["E", "F", "W", "I", "UP", "B"]
36
37
  quote-style = "single"
37
38
  line-ending = "lf"
38
39
 
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
42
+ pythonpath = ["tests"]
43
+
39
44
  [tool.mypy]
40
45
  python_version = "3.9"
41
46
  files = ["src", "tests"]
47
+ exclude = ["^tmp/"]
42
48
  warn_unused_ignores = true
43
49
  warn_redundant_casts = true
44
50
  warn_unused_configs = true
@@ -1,7 +1,7 @@
1
1
  """Protocol pattern matching for waveform analysis."""
2
2
 
3
3
  from .dsl import Pattern as Pattern
4
- from .engine import PatternError as PatternError
4
+ from .errors import PatternError as PatternError
5
5
  from .result import MatchResult as MatchResult
6
6
  from .result import MatchStatus as MatchStatus
7
7
  from .steps import Channel as Channel