wavekit 0.7.0a1__tar.gz → 0.7.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 (36) hide show
  1. {wavekit-0.7.0a1 → wavekit-0.7.1}/PKG-INFO +164 -121
  2. wavekit-0.7.1/README.md +533 -0
  3. {wavekit-0.7.0a1 → wavekit-0.7.1}/pyproject.toml +1 -1
  4. {wavekit-0.7.0a1 → wavekit-0.7.1}/src/wavekit/__init__.py +21 -19
  5. wavekit-0.7.1/src/wavekit/pattern/__init__.py +23 -0
  6. {wavekit-0.7.0a1 → wavekit-0.7.1}/src/wavekit/pattern/compiler.py +76 -36
  7. wavekit-0.7.1/src/wavekit/pattern/dsl.py +414 -0
  8. wavekit-0.7.1/src/wavekit/pattern/result.py +243 -0
  9. wavekit-0.7.1/src/wavekit/pattern/runtime.py +587 -0
  10. {wavekit-0.7.0a1 → wavekit-0.7.1}/src/wavekit/pattern/steps.py +46 -62
  11. {wavekit-0.7.0a1 → wavekit-0.7.1}/src/wavekit/readers/base.py +230 -284
  12. {wavekit-0.7.0a1 → wavekit-0.7.1}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +200 -300
  13. wavekit-0.7.1/src/wavekit/readers/fsdb/reader.py +210 -0
  14. wavekit-0.7.1/src/wavekit/readers/fst/reader.py +206 -0
  15. wavekit-0.7.1/src/wavekit/readers/hierarchy.py +368 -0
  16. wavekit-0.7.1/src/wavekit/readers/matcher.py +410 -0
  17. wavekit-0.7.1/src/wavekit/readers/range.py +16 -0
  18. wavekit-0.7.1/src/wavekit/readers/vcd/reader.py +178 -0
  19. {wavekit-0.7.0a1 → wavekit-0.7.1}/src/wavekit/waveform.py +143 -106
  20. wavekit-0.7.0a1/README.md +0 -490
  21. wavekit-0.7.0a1/src/wavekit/pattern/__init__.py +0 -9
  22. wavekit-0.7.0a1/src/wavekit/pattern/dsl.py +0 -314
  23. wavekit-0.7.0a1/src/wavekit/pattern/result.py +0 -112
  24. wavekit-0.7.0a1/src/wavekit/pattern/runtime.py +0 -526
  25. wavekit-0.7.0a1/src/wavekit/readers/fsdb/reader.py +0 -231
  26. wavekit-0.7.0a1/src/wavekit/readers/fst/reader.py +0 -206
  27. wavekit-0.7.0a1/src/wavekit/readers/pattern_parser.py +0 -90
  28. wavekit-0.7.0a1/src/wavekit/readers/vcd/reader.py +0 -161
  29. wavekit-0.7.0a1/src/wavekit/scope.py +0 -317
  30. wavekit-0.7.0a1/src/wavekit/signal.py +0 -87
  31. {wavekit-0.7.0a1 → wavekit-0.7.1}/LICENSE +0 -0
  32. {wavekit-0.7.0a1 → wavekit-0.7.1}/build.py +0 -0
  33. {wavekit-0.7.0a1 → wavekit-0.7.1}/src/wavekit/pattern/errors.py +0 -0
  34. {wavekit-0.7.0a1 → wavekit-0.7.1}/src/wavekit/readers/expr_parser.py +0 -0
  35. {wavekit-0.7.0a1 → wavekit-0.7.1}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
  36. {wavekit-0.7.0a1 → wavekit-0.7.1}/src/wavekit/readers/value_change.pyx +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavekit
3
- Version: 0.7.0a1
3
+ Version: 0.7.1
4
4
  Summary: a fundamental package for digital circuit waveform analysis
5
5
  License-File: LICENSE
6
6
  Author: cxzzzz
@@ -30,29 +30,29 @@ Description-Content-Type: text/markdown
30
30
 
31
31
  English | [中文](README_ZH.md)
32
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.
33
+ **wavekit** is a high-performance Python library for digital waveform analysis. It loads VCD, FST, and FSDB signals into NumPy-backed `Waveform` objects, and builds on them with efficient signal processing, protocol analysis, and automated verification APIs.
34
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.
35
+ > 🤖 **AI integration**: [wavekit-mcp](https://github.com/cxzzzz/wavekit-mcp) provides an MCP server for AI-assisted waveform analysis: load signals and run pattern matching from AI tools without hand-written scripts.
36
36
 
37
- ## ✨ Features
37
+ ## Features
38
38
 
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
- - **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**: Unified temporal pattern runtime 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.
39
+ - **Batch signal extraction**: use brace expansion, integer ranges, regex, and wildcards to load related signals in one call.
40
+ - **Rich analysis API**: use NumPy-style arithmetic, masking, bit slicing, edge detection, and time/cycle slicing to build signal queries in a few lines.
41
+ - **Temporal pattern matching**: use declarative and programmable Pattern APIs to extract protocol transactions, measure handshake latency, and detect timeout or hang failures.
42
+ - **High-performance waveform processing**: read VCD, FST, and FSDB files into compact NumPy-backed arrays for fast loading and efficient memory use.
43
43
 
44
- ## 📦 Installation
44
+ ## Installation
45
45
 
46
46
  ```bash
47
47
  pip install wavekit
48
48
  ```
49
49
 
50
50
  **Note**: To read FSDB files, the Verdi runtime library (`libNPI.so`) must be available at runtime. Configure via:
51
- - `WAVEKIT_NPI_LIB` — direct path to `libNPI.so`
52
- - `VERDI_HOME` — Verdi installation directory (searches `$VERDI_HOME/share/NPI/lib/...`)
53
- - `LD_LIBRARY_PATH` — system library search path
51
+ - `WAVEKIT_NPI_LIB`: direct path to `libNPI.so`
52
+ - `VERDI_HOME`: Verdi installation directory (searches `$VERDI_HOME/share/NPI/lib/...`)
53
+ - `LD_LIBRARY_PATH`: system library search path
54
54
 
55
- ## 🚀 Quick Start
55
+ ## Quick start
56
56
 
57
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.
58
58
 
@@ -64,17 +64,16 @@ Use brace expansion or regular expressions to load multiple related signals in o
64
64
  from wavekit import VcdReader
65
65
 
66
66
  with VcdReader("jtag.vcd") as f:
67
- # Brace expansion: load J_state and J_next in one call
68
- # Returns: { ('state',): Waveform, ('next',): Waveform }
67
+ # Keys contain BraceCapture objects whose groups are ("state",) / ("next",).
69
68
  waves = f.load_matched_waveforms(
70
69
  "tb.u0.J_{state,next}[3:0]",
71
- clock_pattern="tb.tck",
70
+ clock_path="tb.tck",
72
71
  )
73
72
 
74
- # Regex mode (@ prefix): capture groups become dict keys
73
+ # /regex/ is the canonical regex syntax; groups are stored by RegexCapture.
75
74
  waves = f.load_matched_waveforms(
76
- r"tb.u0.@J_([a-z]+)",
77
- clock_pattern="tb.tck",
75
+ r"tb.u0./J_([a-z]+)/",
76
+ clock_path="tb.tck",
78
77
  )
79
78
  ```
80
79
 
@@ -82,7 +81,7 @@ with VcdReader("jtag.vcd") as f:
82
81
 
83
82
  ### 2. Signal Analysis
84
83
 
85
- Waveforms support Numpy-style arithmetic, masking, and edge detection out of the box.
84
+ Waveforms support NumPy-style arithmetic, masking, and edge detection.
86
85
 
87
86
  ```python
88
87
  import numpy as np
@@ -141,8 +140,7 @@ with VcdReader("fifo_tb.vcd") as f:
141
140
  clock="fifo_tb.clk",
142
141
  )
143
142
 
144
- # Zip mode: brace patterns expand per key, evaluated once per match
145
- # Returns: { (0,): Waveform, (1,): Waveform, (2,): Waveform, (3,): Waveform }
143
+ # Zip mode: brace patterns expand per typed CaptureKey and evaluate per match.
146
144
  occupancies = f.eval(
147
145
  "tb.fifo_{0..3}.w_ptr[2:0] - tb.fifo_{0..3}.r_ptr[2:0]",
148
146
  clock="tb.clk",
@@ -154,19 +152,24 @@ with VcdReader("fifo_tb.vcd") as f:
154
152
 
155
153
  ### 4. Pattern Matching
156
154
 
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.
155
+ `Pattern` scans a waveform and extracts matching transactions, such as a request/response pair, a burst, a stall interval, or another repeating timing pattern.
158
156
 
159
- There are two ways to describe a pattern:
157
+ Choose the form that matches the transaction shape:
160
158
 
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.
159
+ - **Declarative**: use `.wait()`, `.consume()`, `.capture()`, and `.loop()` steps to describe fixed flows.
160
+ - **Programmable**: use a normal Python function when the flow depends on waveform values, such as dynamic branches or per-ID routing.
161
+
162
+ In declarative patterns, the first blocking step selects transaction start cycles; later blocking steps describe timing within each matched transaction.
163
+
164
+ `match(pattern)` returns `MatchRecords`, while `collect(body)` returns a Python `list` of extracted values.
163
165
 
164
166
  #### Declarative examples
165
167
 
166
168
  **AXI-Lite read latency**
167
169
 
168
170
  ```python
169
- from wavekit import VcdReader, Pattern
171
+ from wavekit import VcdReader
172
+ from wavekit.pattern import Pattern, match, collect
170
173
 
171
174
  with VcdReader("axi_tb.vcd") as f:
172
175
  clk = "tb.clk"
@@ -176,16 +179,17 @@ with VcdReader("axi_tb.vcd") as f:
176
179
  rready = f.load_waveform("tb.dut.rready", clock=clk)
177
180
  rdata = f.load_waveform("tb.dut.rdata[31:0]", clock=clk)
178
181
 
179
- result = (
180
- Pattern(timeout=256)
182
+ pattern = (
183
+ Pattern()
181
184
  .wait(arvalid & arready) # AR handshake → transaction starts
182
185
  .wait(rvalid & rready) # R handshake → transaction ends
183
186
  .capture("rdata", rdata)
184
- .match()
185
187
  )
188
+ result = match(pattern)
186
189
 
187
190
  ok = result.filter_ok()
188
- print(f"Read latencies (cycles): {ok.duration.value}")
191
+ latency = ok.end.clock - ok.start.clock
192
+ print(f"Read latencies (cycles): {latency}")
189
193
  print(f"Read data: {ok.captures['rdata'].value}")
190
194
  ```
191
195
 
@@ -194,13 +198,12 @@ with VcdReader("axi_tb.vcd") as f:
194
198
  ```python
195
199
  beat = Pattern().consume(wvalid & wready, channel="w").capture("beats", wdata, mode="list")
196
200
 
197
- result = (
201
+ pattern = (
198
202
  Pattern()
199
203
  .wait(awvalid & awready) # AW handshake → burst starts
200
204
  .loop(beat, until=wlast) # collect each beat until wlast
201
- .timeout(512)
202
- .match()
203
205
  )
206
+ result = match(pattern)
204
207
 
205
208
  for i, inst in enumerate(result.filter_ok()):
206
209
  print(f"Burst {i}: {len(inst.captures['beats'])} beats")
@@ -211,57 +214,81 @@ for i, inst in enumerate(result.filter_ok()):
211
214
  ```python
212
215
  stall = valid & (ready == 0)
213
216
 
214
- result = (
217
+ pattern = (
215
218
  Pattern()
216
219
  .wait(stall.rising_edge()) # stall begins
217
220
  .loop(Pattern().delay(1), when=stall) # keep waiting until stall ends
218
- .match()
219
221
  )
222
+ result = match(pattern)
220
223
 
221
224
  stalls = result.filter_ok()
222
- print(f"Stall durations: {stalls.duration.value} cycles")
225
+ stall_cycles = stalls.duration.value - 1
226
+ print(f"Stall durations: {stall_cycles} cycles")
223
227
  ```
224
228
 
225
229
  #### Programmable example
226
230
 
227
- **Out-of-order AXI reads by ID**
231
+ **DMA-style command stream**
228
232
 
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.
233
+ Use programmable control flow when a command's opcode changes the following timing shape.
230
234
 
231
235
  ```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}
236
+ cmd_fire = cmd_valid & cmd_ready # precompute outside the handler
237
+ w_fire = w_valid & w_ready
238
+ rsp_fire = rsp_valid & rsp_ready
239
+ r_fire = r_valid & r_ready
240
+
241
+ OP_READ = 0
242
+ OP_WRITE = 1
243
+
244
+ def read_dma_cmd(ctx):
245
+ if not ctx.value(cmd_fire):
246
+ return None
247
+
248
+ op = int(ctx.value(cmd_op))
249
+ addr = int(ctx.value(cmd_addr))
250
+ length = int(ctx.value(cmd_len))
251
+
252
+ if op == OP_WRITE:
253
+ data = []
254
+ for _ in range(length):
255
+ ctx.consume(w_fire, channel="wdata")
256
+ data.append(int(ctx.value(w_data)))
257
+
258
+ ctx.consume(rsp_fire, channel="rsp")
259
+ return {
260
+ "op": "write",
261
+ "addr": addr,
262
+ "data": data,
263
+ "status": int(ctx.value(rsp_status)),
264
+ }
265
+
266
+ if op == OP_READ:
267
+ ctx.consume(rsp_fire, channel="rsp")
268
+ data = []
269
+ for _ in range(length):
270
+ ctx.consume(r_fire, channel="rdata")
271
+ data.append(int(ctx.value(r_data)))
272
+
273
+ return {"op": "read", "addr": addr, "data": data}
274
+
275
+ ctx.require(False, message=f"unknown DMA op {op}")
250
276
  return None
251
277
 
252
- records = Pattern(read_burst, timeout=64).collect()
278
+ commands = collect(read_dma_cmd)
279
+ print(f"Captured {len(commands)} commands")
253
280
  ```
254
281
 
255
282
  Some tips for programmable patterns:
256
283
 
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.
284
+ - Precompute fixed waveform expressions, such as `fire = valid & ready`, outside the handler so they are not rebuilt every cycle.
285
+ - Start the handler with `if ctx.value(fire): ...` to test whether the current cycle starts a transaction, and `return None` otherwise.
286
+ - Use `ctx.try_consume(...)` for non-blocking polling or arbitration between candidate channels. For a linear burst, `ctx.consume(...)` is more direct.
287
+ - Add `timeout=<cycles>` only when a blocking step needs a wait bound. In `match()`, timeout becomes `MatchStatus.Timeout(...)`; in `collect()`, it raises `PatternError`.
261
288
 
262
289
  ---
263
290
 
264
- ## 📖 API Reference
291
+ ## API reference
265
292
 
266
293
  ### Reader
267
294
 
@@ -270,22 +297,35 @@ Some tips for programmable patterns:
270
297
  | `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`). |
271
298
  | `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
272
299
  | `reader.load_unknown_mask(signal, clock, ...)` | **Experimental.** Load X/Z bit presence as an unsigned mask `Waveform`. |
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]`. |
300
+ | `reader.load_matched_waveforms(signal_path, clock_path, ...)` | Batch-load matching signals. Returns `dict[CaptureKey, Waveform]`. |
301
+ | `reader.load_matched_unknown_masks(signal_path, clock_path, ...)` | **Experimental.** Batch-load X/Z masks for matched signals. Returns `dict[CaptureKey, Waveform]`. |
275
302
  | `reader.eval(expr, clock, mode='single'\|'zip', ...)` | Evaluate an arithmetic expression with embedded signal paths. |
276
- | `reader.get_matched_signals(pattern)` | Resolve a pattern to signal paths without loading data. |
277
- | `reader.top_scope_list()` | Return root `Scope` nodes of the signal hierarchy. |
278
-
279
- **Pattern syntax** used in signal paths:
280
-
281
- | Syntax | Example | Effect |
282
- |--------|---------|--------|
283
- | `{a,b,c}` | `sig_{read,write}` | Enumerate named variants |
284
- | `{N..M}` | `fifo_{0..3}.ptr` | Integer range |
285
- | `{N..M..step}` | `lane_{0..6..2}` | Stepped range |
286
- | `@<regex>` | `@([a-z]+)_valid` | Regex with capture groups |
287
- | `$ModName` | `tb.$fifo_unit.ptr` | Match a direct-child scope by module/definition name (FSDB only) |
288
- | `$$ModName` | `tb.$$fifo_unit.ptr` | Match any-depth descendant scope by module/definition name (FSDB only) |
303
+ | `reader.get_matched_signals(path)` | Resolve a query to `Signal` objects without loading data. |
304
+ | `reader.get_matched_scopes(path)` | Resolve a query to `Scope` objects. |
305
+ | `reader.top_scopes` | Immutable tuple of root `Scope` nodes. |
306
+ | `has_fsdb_support()` | Report whether the Verdi FSDB runtime is currently available. |
307
+
308
+ ### Signal path patterns
309
+
310
+ | Syntax | Example | Effect | Capture in result key |
311
+ |--------|---------|--------|-----------------------|
312
+ | Plain name | `tb.dut.valid` | Exact-name match | None |
313
+ | `{a,b,c}` | `sig_{read,write}` | Enumerate named variants | `BraceCapture(path=..., groups=...)` |
314
+ | `{N..M}` | `fifo_{0..3}.ptr` | Integer range | `BraceCapture(path=..., groups=...)` |
315
+ | `{N..M..step}` | `lane_{0..6..2}` | Stepped range | `BraceCapture(path=..., groups=...)` |
316
+ | `/<regex>/` | `/([a-z]+)_valid/` | Canonical regex syntax with capture groups | `RegexCapture(path=..., groups=...)` |
317
+ | `@<regex>` | `@([a-z]+)_valid` | Legacy-compatible regex syntax | `RegexCapture(path=..., groups=...)` |
318
+ | `*` / `**` | `tb.*.valid` / `tb.**.valid` | Single-level / recursive wildcard | `WildcardCapture(path=...)` |
319
+ | `$ModName` | `tb.$fifo_unit.ptr` | Match a direct-child scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
320
+ | `$$ModName` | `tb.$$fifo_unit.ptr` | Match any-depth descendant scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
321
+
322
+ `$` and `$$` are path-step modifiers: they can combine with exact names, brace
323
+ expansion, regex, and a trailing range selector in the same query.
324
+ For example: `tb.$/fifo_(in|out)/.data_{0..3}[7:0]`.
325
+
326
+ Matched-reader and hierarchy query APIs return dictionaries keyed by
327
+ `CaptureKey = tuple[Capture, ...]`. Exact-name components are omitted, so a
328
+ fully exact query uses key `()`.
289
329
 
290
330
  ---
291
331
 
@@ -311,7 +351,8 @@ A `Waveform` wraps three parallel numpy arrays (`.value`, `.clock`, `.time`). Al
311
351
  | Method | Description |
312
352
  |--------|-------------|
313
353
  | `wave.map(fn, width, signed)` | Element-wise transform |
314
- | `wave.unique_consecutive()` | Remove consecutive duplicates |
354
+ | `wave.unique_consecutive()` | Remove consecutive duplicate values |
355
+ | `wave.compress()` | Compact a waveform while preserving value changes and the final sample |
315
356
  | `wave.downsample(chunk, fn)` | Aggregate into chunks |
316
357
  | `wave.as_signed()` / `wave.as_unsigned()` | Reinterpret signedness |
317
358
 
@@ -358,28 +399,25 @@ changed = wave != wave.back(3)
358
399
 
359
400
  | API | Description |
360
401
  |-----|-------------|
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. |
402
+ | `Pattern()` | Create a declarative Pattern. Add steps with builder methods; execution options live on `match()` / `collect()`. |
403
+ | `match(pattern_or_body, *, axis=None, timeout=None, timeout_message=None, start_cycle=None, end_cycle=None)` | Run a declarative Pattern or programmable check body and return `MatchRecords`. Check bodies return `ctx.OK` or `None`. |
404
+ | `collect(body, *, axis=None, timeout=None, timeout_message=None, start_cycle=None, end_cycle=None)` | Run a programmable extraction body and collect each non-`None` Python return value. |
365
405
 
366
406
  **Declarative Steps**
367
407
 
368
408
  | Method | Description |
369
409
  |--------|-------------|
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. |
372
- | `.delay(n, *, require=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
410
+ | `.wait(cond, *, require=None, require_message=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 → `MatchStatus.RequireViolated`). |
411
+ | `.consume(cond, channel, *, require=None, require_message=None)` | Block until `cond` is True and this instance can exclusively consume from `channel`. Resumes in the same cycle on success. Use this for request/response pairing and per-key routing. |
412
+ | `.delay(n, *, require=None, require_message=None)` | Advance `n` cycles. `delay(0)` is a no-op. `require` must hold every cycle. |
373
413
  | `.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. |
374
- | `.require(cond)` | Assert condition; fail with `REQUIRE_VIOLATED` if False. |
414
+ | `.require(cond)` | Assert condition; fail with `MatchStatus.RequireViolated` if False. |
375
415
  | `.loop(body, *, until=None, when=None)` | `until`: do-while (exit when True after body). `when`: while (exit when False before body). |
376
416
  | `.repeat(body, n)` | Execute body exactly `n` times. `n` may be a callable. |
377
417
  | `.branch(cond, true_body, false_body)` | Conditional branch. |
378
- | `.timeout(max_cycles)` | Terminate unfinished instances with `TIMEOUT`. |
379
418
 
380
419
  The same time and ownership operations are available inside Programmable
381
- Patterns as `await ctx.wait(...)`, `await ctx.consume(...)`, and
382
- `await ctx.delay(...)`.
420
+ patterns as `ctx.wait(...)`, `ctx.consume(...)`, and `ctx.delay(...)`.
383
421
 
384
422
  **Programmable Context**
385
423
 
@@ -388,38 +426,40 @@ Patterns as `await ctx.wait(...)`, `await ctx.consume(...)`, and
388
426
  | `ctx.value(waveform, offset=0)` | Read a scalar value at the current sample plus optional offset. |
389
427
  | `ctx.cycle(waveform, offset=0)` | Read the cycle number at the current sample plus optional offset. |
390
428
  | `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. |
429
+ | `ctx.wait(cond, require=None, require_message=None)` | Observe cycles until `cond` is true; does not consume the event. |
430
+ | `ctx.consume(cond, channel, require=None, require_message=None)` | Wait for `cond` and exclusively consume from `channel`. |
431
+ | `ctx.try_consume(cond, channel)` | Poll `channel` without blocking. Returns `True` only when both the condition and channel are available. |
432
+ | `ctx.delay(n, require=None, require_message=None)` | Advance `n` cycles. |
433
+ | `ctx.capture(name, value, mode='last')` | Record a capture for programmable `match()`. |
434
+ | `ctx.OK` | Return from programmable `match()` to record a successful match. |
396
435
 
397
436
  **Dynamic callbacks**
398
437
 
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.
438
+ Callback arguments depend on whether the callback is used in the declarative API or the programmable API:
439
+
440
+ - In declarative APIs such as `Pattern().wait(...)` and `Pattern().consume(...)`, callbacks receive `(index, captures)`.
441
+ - In programmable functions, callbacks passed to methods such as `ctx.wait(...)` and `ctx.consume(...)` take no arguments. If they need the current index, captures, or signal values, close over `ctx`.
401
442
 
402
443
  **Channels and consume vs. wait**
403
444
 
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.
445
+ `wait()` is observational: every matching instance can see the same event.
446
+ `consume()` adds ownership: only one instance can claim a given `(channel, cycle)`.
447
+ On the same channel, matches with earlier start cycles claim available events first.
408
448
 
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.
449
+ A `Channel` is an identity token for consume ownership. Pass a `Channel` object,
450
+ a hashable key, or a dynamic callback to `consume(..., channel=...)`. All
451
+ instances sharing the same channel key compete for the same logical channel.
412
452
 
413
453
  ```python
414
454
  from collections import defaultdict
415
- from wavekit import Channel, Pattern
455
+ from wavekit.pattern import Channel, Pattern, match
416
456
 
417
- # Multi-bank cache: each bank has its own response port, so two banks
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.
457
+ # Multi-bank cache: each bank has its own response port, so two banks can return
458
+ # data in the same cycle. Per-bank channels let independent requests consume
459
+ # independent response streams.
420
460
  banks = defaultdict(Channel)
421
461
 
422
- result = (
462
+ pattern = (
423
463
  Pattern()
424
464
  .wait(req_valid)
425
465
  .capture('bank', req_addr & 1)
@@ -429,27 +469,30 @@ result = (
429
469
  )
430
470
  .capture('rdata',
431
471
  lambda i, cap: bank_data[cap['bank']].value[i])
432
- .match()
433
472
  )
473
+ result = match(pattern)
434
474
  ```
435
475
 
436
- **`MatchResult`**
476
+ **`MatchRecords`**
437
477
 
438
478
  | Field | Description |
439
479
  |-------|-------------|
440
- | `.start` / `.end` | Start and end cycle of each match (both inclusive). |
441
- | `.duration` | `end - start + 1` cycles. |
442
- | `.status` | `MatchStatus.OK`, `TIMEOUT`, or `REQUIRE_VIOLATED`. |
480
+ | `.start` / `.end` | Point Waveforms. `.value` is the waveform-array sample index, `.clock` is the absolute cycle, and `.time` is the simulation timestamp. End is inclusive. |
481
+ | `.duration` | `end.value - start.value + 1` sampled cycles. |
482
+ | `.status` | `MatchStatus.OK()`, `MatchStatus.Timeout(...)`, or `MatchStatus.RequireViolated(...)`. |
443
483
  | `.captures` | `dict[str, Waveform]` of captured values. |
444
- | `.ok` | Boolean Waveform where `status == MatchStatus.OK`. |
445
- | `.failed` | Boolean Waveform where `status != MatchStatus.OK`. |
484
+ | `.ok` | Boolean Waveform where `status == MatchStatus.OK()`. |
485
+ | `.failed` | Boolean Waveform where `status != MatchStatus.OK()`. |
446
486
  | `.filter_ok()` | Return only `OK` matches. |
447
- | `.filter_status(status)` | Return only matches with the given `MatchStatus` or integer status. |
487
+ | `.filter_status(status_class)` | Return only matches with the given status class, such as `MatchStatus.Timeout`. |
448
488
  | `.filter_failed()` | Return only non-OK matches. |
449
489
 
490
+ `MatchRecords[i]` returns a single `MatchRecord`, and slices return another
491
+ `MatchRecords` batch.
492
+
450
493
  ---
451
494
 
452
- ## 🛠️ Development
495
+ ## Development
453
496
 
454
497
  This project uses [Poetry](https://python-poetry.org/) for dependency management and packaging.
455
498
 
@@ -497,9 +540,9 @@ poetry run ruff format .
497
540
  poetry run mypy .
498
541
  ```
499
542
 
500
- ## 🤝 Contributing
543
+ ## Contributing
501
544
 
502
- Contributions are welcome! Please open an issue to discuss a bug or feature request before submitting a pull request. When contributing code, make sure all tests pass and the linter reports no errors:
545
+ Issues and pull requests are welcome. Before sending a pull request, run the tests and format checks:
503
546
 
504
547
  ```bash
505
548
  poetry run pytest
@@ -507,7 +550,7 @@ poetry run ruff check .
507
550
  poetry run ruff format --check .
508
551
  ```
509
552
 
510
- ## 📄 License
553
+ ## License
511
554
 
512
555
  This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
513
556