wavekit 0.7.1__tar.gz → 0.7.2__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 (26) hide show
  1. {wavekit-0.7.1 → wavekit-0.7.2}/PKG-INFO +5 -5
  2. {wavekit-0.7.1 → wavekit-0.7.2}/README.md +3 -4
  3. {wavekit-0.7.1 → wavekit-0.7.2}/pyproject.toml +2 -1
  4. wavekit-0.7.2/src/wavekit/expression.py +165 -0
  5. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/base.py +64 -64
  6. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/waveform.py +63 -79
  7. {wavekit-0.7.1 → wavekit-0.7.2}/LICENSE +0 -0
  8. {wavekit-0.7.1 → wavekit-0.7.2}/build.py +0 -0
  9. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/__init__.py +0 -0
  10. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/__init__.py +0 -0
  11. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/compiler.py +0 -0
  12. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/dsl.py +0 -0
  13. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/errors.py +0 -0
  14. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/result.py +0 -0
  15. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/runtime.py +0 -0
  16. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/pattern/steps.py +0 -0
  17. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/expr_parser.py +0 -0
  18. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
  19. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +0 -0
  20. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/fsdb/reader.py +0 -0
  21. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/fst/reader.py +0 -0
  22. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/hierarchy.py +0 -0
  23. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/matcher.py +0 -0
  24. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/range.py +0 -0
  25. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/value_change.pyx +0 -0
  26. {wavekit-0.7.1 → wavekit-0.7.2}/src/wavekit/readers/vcd/reader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavekit
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: a fundamental package for digital circuit waveform analysis
5
5
  License-File: LICENSE
6
6
  Author: cxzzzz
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: lark (>=1.2,<2)
16
17
  Requires-Dist: numpy (>=2.0.0,<3.0.0)
17
18
  Requires-Dist: pylibfst (>=0.2.1,<0.3.0)
18
19
  Requires-Dist: pytest (>=8.2.2,<9.0.0)
@@ -294,7 +295,7 @@ Some tips for programmable patterns:
294
295
 
295
296
  | Method | Description |
296
297
  |--------|-------------|
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`). |
298
+ | `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file, *, quiet=True)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`) and suppresses the NPI startup banner by default; pass `quiet=False` to keep it. |
298
299
  | `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
299
300
  | `reader.load_unknown_mask(signal, clock, ...)` | **Experimental.** Load X/Z bit presence as an unsigned mask `Waveform`. |
300
301
  | `reader.load_matched_waveforms(signal_path, clock_path, ...)` | Batch-load matching signals. Returns `dict[CaptureKey, Waveform]`. |
@@ -319,9 +320,8 @@ Some tips for programmable patterns:
319
320
  | `$ModName` | `tb.$fifo_unit.ptr` | Match a direct-child scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
320
321
  | `$$ModName` | `tb.$$fifo_unit.ptr` | Match any-depth descendant scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
321
322
 
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]`.
323
+ `$` and `$$` are path-step modifiers that can combine with exact-name, brace,
324
+ and regex matchers. For example: `tb.$/fifo_(in|out)/.data_{0..3}[7:0]`.
325
325
 
326
326
  Matched-reader and hierarchy query APIs return dictionaries keyed by
327
327
  `CaptureKey = tuple[Capture, ...]`. Exact-name components are omitted, so a
@@ -272,7 +272,7 @@ Some tips for programmable patterns:
272
272
 
273
273
  | Method | Description |
274
274
  |--------|-------------|
275
- | `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`). |
275
+ | `VcdReader(file)` / `FstReader(file)` / `FsdbReader(file, *, quiet=True)` | Open a waveform file. Use as a context manager. `FsdbReader` requires Verdi runtime (`WAVEKIT_NPI_LIB`, `VERDI_HOME`, or `LD_LIBRARY_PATH`) and suppresses the NPI startup banner by default; pass `quiet=False` to keep it. |
276
276
  | `reader.load_waveform(signal, clock, ...)` | Load one signal sampled on every clock edge. Returns `Waveform`. |
277
277
  | `reader.load_unknown_mask(signal, clock, ...)` | **Experimental.** Load X/Z bit presence as an unsigned mask `Waveform`. |
278
278
  | `reader.load_matched_waveforms(signal_path, clock_path, ...)` | Batch-load matching signals. Returns `dict[CaptureKey, Waveform]`. |
@@ -297,9 +297,8 @@ Some tips for programmable patterns:
297
297
  | `$ModName` | `tb.$fifo_unit.ptr` | Match a direct-child scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
298
298
  | `$$ModName` | `tb.$$fifo_unit.ptr` | Match any-depth descendant scope by module/definition name (FSDB only) | `ExactCapture(path=..., definition=...)` |
299
299
 
300
- `$` and `$$` are path-step modifiers: they can combine with exact names, brace
301
- expansion, regex, and a trailing range selector in the same query.
302
- For example: `tb.$/fifo_(in|out)/.data_{0..3}[7:0]`.
300
+ `$` and `$$` are path-step modifiers that can combine with exact-name, brace,
301
+ and regex matchers. For example: `tb.$/fifo_(in|out)/.data_{0..3}[7:0]`.
303
302
 
304
303
  Matched-reader and hierarchy query APIs return dictionaries keyed by
305
304
  `CaptureKey = tuple[Capture, ...]`. Exact-name components are omitted, so a
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wavekit"
3
- version = "0.7.1"
3
+ version = "0.7.2"
4
4
  description = "a fundamental package for digital circuit waveform analysis"
5
5
  authors = ["cxzzzz <cxz19961010@outlook.com>"]
6
6
  readme = "README.md"
@@ -16,6 +16,7 @@ numpy = "^2.0.0"
16
16
  vcdvcd = "^2.3.5"
17
17
  pylibfst = "^0.2.1"
18
18
  pytest = "^8.2.2"
19
+ lark = ">=1.2,<2"
19
20
 
20
21
  [tool.poetry.group.dev.dependencies]
21
22
  ruff = "^0.6.2"
@@ -0,0 +1,165 @@
1
+ """Parsing and evaluating WaveKit waveform expressions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ from lark import Lark, Transformer
9
+
10
+ _EXPRESSION_GRAMMAR = r"""
11
+ ?start: expr
12
+ ?expr: bit_or
13
+ ?bit_or: bit_xor | bit_or VBAR bit_xor
14
+ ?bit_xor: bit_and | bit_xor CIRCUMFLEX bit_and
15
+ ?bit_and: comparison | bit_and AMPERSAND comparison
16
+ ?comparison: shift
17
+ | comparison EQEQ shift
18
+ | comparison NEQ shift
19
+ | comparison LE shift
20
+ | comparison LT shift
21
+ | comparison GE shift
22
+ | comparison GT shift
23
+ ?shift: sum | shift LSHIFT sum | shift RSHIFT sum
24
+ ?sum: product | sum PLUS product | sum MINUS product
25
+ ?product: unary
26
+ | product STAR unary
27
+ | product SLASH unary
28
+ | product FLOOR unary
29
+ | product PERCENT unary
30
+ | product POWER unary
31
+ ?unary: TILDE unary | PLUS unary | MINUS unary | atom
32
+ ?atom: call | path | NUMBER | group
33
+ group: LPAR expr RPAR
34
+ call: NAME LPAR [expr (COMMA expr)*] RPAR
35
+ path: segment (DOT segment)* SUBSCRIPT*
36
+ segment: segment_text | WILDCARD
37
+ segment_text: segment_piece+
38
+ ?segment_piece: NAME | MODULE | BRACE | REGEX
39
+
40
+ MODULE: /\${1,2}/
41
+ BRACE: /\{[^{}]*\}/
42
+ WILDCARD.3: "**" | "*"
43
+ REGEX: /\/(?:\\.|[^\/\\])*\//
44
+ SUBSCRIPT: /\[\d+(?::\d+)?\]/
45
+ NAME: /[A-Za-z_][A-Za-z0-9_]*/
46
+ HEX_NUMBER.2: /0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*/
47
+ BIN_NUMBER.2: /0[bB][01](?:_?[01])*/
48
+ OCT_NUMBER.2: /0[oO][0-7](?:_?[0-7])*/
49
+ DECIMAL_FLOAT_NUMBER.2: /(?:\d(?:_?\d)*)?\.(?:\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?/
50
+ EXP_NUMBER.2: /\d(?:_?\d)*[eE][+-]?\d(?:_?\d)*/
51
+ DEC_NUMBER.1: /\d(?:_?\d)*/
52
+ NUMBER: HEX_NUMBER | BIN_NUMBER | OCT_NUMBER | DECIMAL_FLOAT_NUMBER | EXP_NUMBER | DEC_NUMBER
53
+
54
+ VBAR: "|"
55
+ CIRCUMFLEX: "^"
56
+ AMPERSAND: "&"
57
+ EQEQ: "=="
58
+ NEQ: "!="
59
+ LE: "<="
60
+ LT: "<"
61
+ GE: ">="
62
+ GT: ">"
63
+ LSHIFT: "<<"
64
+ RSHIFT: ">>"
65
+ PLUS: "+"
66
+ MINUS: "-"
67
+ STAR: "*"
68
+ SLASH: "/"
69
+ FLOOR: "//"
70
+ PERCENT: "%"
71
+ POWER: "**"
72
+ TILDE: "~"
73
+ LPAR: "("
74
+ RPAR: ")"
75
+ COMMA: ","
76
+ DOT: "."
77
+
78
+ %import common.WS_INLINE
79
+ %ignore WS_INLINE
80
+ """
81
+
82
+ _EXPRESSION_PARSER = Lark(
83
+ _EXPRESSION_GRAMMAR,
84
+ parser='lalr',
85
+ lexer='contextual',
86
+ )
87
+ _EXPRESSION_FUNCTIONS: dict[str, Callable[..., Any]] = {}
88
+
89
+
90
+ def expression_function(func: Callable[..., Any]) -> Callable[..., Any]:
91
+ """Register a Waveform method as callable from an expression string."""
92
+ _EXPRESSION_FUNCTIONS[func.__name__] = func
93
+ return func
94
+
95
+
96
+ class ExpressionTransformer(Transformer):
97
+ def __init__(self, *, function_names: set[str]):
98
+ super().__init__()
99
+ self.function_names = function_names
100
+ self.path_entries: list[tuple[str, str]] = []
101
+
102
+ @staticmethod
103
+ def join(items: list[Any]) -> str:
104
+ return ''.join(str(item) for item in items)
105
+
106
+ def call(self, items: list[Any]) -> str:
107
+ name = str(items[0])
108
+ if name not in self.function_names:
109
+ raise ValueError(f'unknown expression function {name!r}')
110
+ return self.join(items)
111
+
112
+ def path(self, items: list[Any]) -> str:
113
+ path = self.join(items)
114
+ placeholder = f'__wave_{len(self.path_entries)}__'
115
+ self.path_entries.append((placeholder, path))
116
+ return placeholder
117
+
118
+ def __default__(self, data: str, children: list[Any], meta: Any) -> Any:
119
+ return self.join(children)
120
+
121
+ def __default_token__(self, token: Any) -> str:
122
+ return str(token)
123
+
124
+
125
+ def parse_expression(expr: str) -> tuple[str, list[tuple[str, str]]]:
126
+ """Parse an expression and replace each signal path with a placeholder.
127
+
128
+ Parameters
129
+ ----------
130
+ expr:
131
+ WaveKit expression containing physical signal paths.
132
+
133
+ Returns
134
+ -------
135
+ tuple[str, list[tuple[str, str]]]
136
+ The substituted expression and ordered ``(placeholder, path)`` entries.
137
+ """
138
+ tree = _EXPRESSION_PARSER.parse(expr)
139
+ transformer = ExpressionTransformer(
140
+ function_names=set(_EXPRESSION_FUNCTIONS),
141
+ )
142
+ result = transformer.transform(tree)
143
+ return str(result), transformer.path_entries
144
+
145
+
146
+ def evaluate_expression(expr: str, namespace: dict[str, Any]) -> Any:
147
+ """Evaluate an already-parsed expression against preloaded Waveforms.
148
+
149
+ Parameters
150
+ ----------
151
+ expr:
152
+ The substituted expression returned by :func:`parse_expression`.
153
+ Physical paths must be resolved and replaced before calling this
154
+ function.
155
+ namespace:
156
+ Names available to the expression, normally Waveform objects.
157
+
158
+ Returns
159
+ -------
160
+ Any
161
+ The result produced by the Waveform expression.
162
+ """
163
+ evaluation_namespace = {**_EXPRESSION_FUNCTIONS, **namespace}
164
+ code = compile(expr, '<wavekit-expression>', 'eval')
165
+ return eval(code, {'__builtins__': {}}, evaluation_namespace) # noqa: S307
@@ -1,14 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- import ast
4
3
  from abc import abstractmethod
5
4
  from dataclasses import dataclass, field
6
5
  from typing import Any, Generic, Literal, TypeVar, cast
7
6
 
8
7
  import numpy as np
9
8
 
9
+ from ..expression import evaluate_expression, parse_expression
10
10
  from ..waveform import Waveform
11
- from .expr_parser import extract_wave_paths
12
11
  from .hierarchy import Node, Scope, Signal
13
12
  from .matcher import CaptureKey
14
13
  from .value_change import value_change_to_value_array
@@ -684,35 +683,33 @@ class Reader(Generic[SignalT]):
684
683
  mode: Literal['single', 'zip'] = 'single',
685
684
  root_scope: Scope | None = None,
686
685
  ) -> Waveform | dict[CaptureKey, Waveform]:
687
- """Evaluate a waveform expression string.
688
-
689
- Wave signal paths embedded in *expr* are automatically extracted,
690
- loaded, and substituted before evaluating the resulting Python
691
- arithmetic expression.
686
+ """Evaluate a waveform expression containing physical signal paths.
692
687
 
693
688
  Parameters
694
689
  ----------
695
690
  expr:
696
- Expression string.
697
- Single mode example: ``"tb.dut.w_ptr[3:0] - tb.dut.r_ptr[3:0]"``.
698
- Zip mode example: ``"tb.dut.fifo_{0..3}.w_ptr[3:0] - tb.dut.fifo_{0..3}.r_ptr[3:0]"``.
691
+ Expression string. Signal paths may be used as operands or as
692
+ arguments to registered expression functions.
699
693
  clock:
700
694
  Clock signal used for all waveform loads.
695
+ xz_value, signed, sample_on_posedge, begin_time, end_time, begin_cycle,
696
+ end_cycle:
697
+ Forwarded to :meth:`load_matched_waveforms` for every path.
701
698
  mode:
702
- ``'single'`` (default) — every path must match exactly one
703
- signal; returns a single :class:`Waveform`.
704
- ``'zip'`` — paths matching multiple signals must all share the
705
- same set of pattern keys; single-match paths are broadcast;
706
- returns ``dict[CaptureKey, Waveform]``.
699
+ ``'single'`` requires every path to match one signal. ``'zip'``
700
+ evaluates once per shared multi-match key and broadcasts singleton
701
+ paths.
707
702
  root_scope:
708
- If provided, all signal paths in *expr* are resolved within this
709
- scope instead of the file's top-level scopes.
703
+ If provided, resolve paths within this scope.
704
+
705
+ Returns
706
+ -------
707
+ Waveform or dict[CaptureKey, Waveform]
708
+ The evaluated waveform, or one waveform per zip key.
710
709
  """
711
710
  self._validate_xz_value(xz_value)
712
- substituted, path_entries = extract_wave_paths(expr)
713
-
711
+ substituted, path_entries = parse_expression(expr)
714
712
  load_kwargs: dict[str, Any] = dict(
715
- clock=clock,
716
713
  xz_value=xz_value,
717
714
  signed=signed,
718
715
  sample_on_posedge=sample_on_posedge,
@@ -720,81 +717,84 @@ class Reader(Generic[SignalT]):
720
717
  end_time=end_time,
721
718
  begin_cycle=begin_cycle,
722
719
  end_cycle=end_cycle,
720
+ root_scope=root_scope,
723
721
  )
724
722
 
725
- # Resolve each path to its matched signal(s)
726
- matched_per_path: list[tuple[str, str, dict[CaptureKey, SignalT]]] = []
723
+ loaded_per_path: list[tuple[str, str, dict[CaptureKey, Waveform]]] = []
727
724
  for placeholder, path in path_entries:
728
- matched = self.get_matched_signals(path, root_scope=root_scope)
725
+ matched = self.load_matched_waveforms(
726
+ signal_path=path,
727
+ clock_path=clock,
728
+ **load_kwargs,
729
+ )
729
730
  if not matched:
730
731
  raise ValueError(f"path '{path}' matched no signals")
731
- matched_per_path.append((placeholder, path, matched))
732
+ loaded_per_path.append((placeholder, path, matched))
732
733
 
733
734
  if mode == 'single':
734
- for _placeholder, path, matched in matched_per_path:
735
+ for _placeholder, path, matched in loaded_per_path:
735
736
  if len(matched) > 1:
737
+ matched_names = [
738
+ wave.signal.full_name if wave.signal is not None else None
739
+ for wave in matched.values()
740
+ ]
736
741
  raise ValueError(
737
742
  f"path '{path}' matched {len(matched)} signals in mode='single',"
738
- f" use mode='zip'. Matched: {[sig.full_name for sig in matched.values()]}"
743
+ f" use mode='zip'. Matched: "
744
+ f'{matched_names}'
739
745
  )
740
- ns: dict[str, Any] = {
741
- placeholder: self.load_waveform(next(iter(matched.values())), **load_kwargs)
742
- for placeholder, _, matched in matched_per_path
746
+ namespace = {
747
+ placeholder: next(iter(matched.values()))
748
+ for placeholder, _, matched in loaded_per_path
743
749
  }
744
750
  try:
745
- code = compile(ast.parse(substituted, mode='eval'), '<eval_expr>', 'eval')
746
- return eval(code, {'__builtins__': {}}, ns) # noqa: S307
751
+ return evaluate_expression(substituted, namespace)
747
752
  except Exception as exc:
748
753
  raise ValueError(
749
- f"failed to evaluate expression '{expr}' (substituted: '{substituted}')"
754
+ f"failed to evaluate expression '{expr}' " f"(substituted: '{substituted}')"
750
755
  ) from exc
751
756
 
752
- elif mode == 'zip':
753
- multi_paths = [(ph, p, m) for ph, p, m in matched_per_path if len(m) > 1]
754
- single_paths = [(ph, p, m) for ph, p, m in matched_per_path if len(m) == 1]
757
+ if mode == 'zip':
758
+ multi_paths = [
759
+ (placeholder, path, matched)
760
+ for placeholder, path, matched in loaded_per_path
761
+ if len(matched) > 1
762
+ ]
763
+ single_paths = [
764
+ (placeholder, path, matched)
765
+ for placeholder, path, matched in loaded_per_path
766
+ if len(matched) == 1
767
+ ]
755
768
 
756
769
  if multi_paths:
757
- # All multi-match paths must share identical key sets
758
- ref_ph, ref_p, ref_matched = multi_paths[0]
759
- ref_keys = set(ref_matched.keys())
760
- for _ph, p, matched in multi_paths[1:]:
761
- if set(matched.keys()) != ref_keys:
770
+ _ref_placeholder, ref_path, ref_matched = multi_paths[0]
771
+ ref_keys = set(ref_matched)
772
+ for _placeholder, path, matched in multi_paths[1:]:
773
+ if set(matched) != ref_keys:
762
774
  raise ValueError(
763
- f'inconsistent match keys between paths: '
764
- f"'{ref_p}' has keys {ref_keys!r}, "
765
- f"'{p}' has keys {set(matched)!r}"
775
+ 'inconsistent match keys between paths: '
776
+ f"'{ref_path}' has keys {ref_keys!r}, "
777
+ f"'{path}' has keys {set(matched)!r}"
766
778
  )
767
- zip_keys: list[CaptureKey] = list(ref_keys)
779
+ zip_keys = list(ref_keys)
768
780
  else:
769
- # All paths are single-match; degenerate to single behaviour
770
781
  zip_keys = [()]
771
782
 
772
- # Pre-load broadcast waveforms (single-match paths)
773
- broadcast_ns: dict[str, Waveform] = {
774
- placeholder: self.load_waveform(next(iter(matched.values())), **load_kwargs)
783
+ broadcast_namespace = {
784
+ placeholder: next(iter(matched.values()))
775
785
  for placeholder, _, matched in single_paths
776
786
  }
777
-
778
787
  result: dict[CaptureKey, Waveform] = {}
779
- try:
780
- code = compile(ast.parse(substituted, mode='eval'), '<eval_expr>', 'eval')
781
- except SyntaxError as exc:
782
- raise ValueError(
783
- f"invalid expression '{expr}' (substituted: '{substituted}')"
784
- ) from exc
785
-
786
788
  for key in zip_keys:
787
- ns = dict(broadcast_ns)
788
- for placeholder, _, matched in multi_paths:
789
- ns[placeholder] = self.load_waveform(matched[key], **load_kwargs)
789
+ namespace = dict(broadcast_namespace)
790
+ for placeholder, _path, matched in multi_paths:
791
+ namespace[placeholder] = matched[key]
790
792
  try:
791
- result[key] = eval(code, {'__builtins__': {}}, ns) # noqa: S307
793
+ result[key] = evaluate_expression(substituted, namespace)
792
794
  except Exception as exc:
793
795
  raise ValueError(
794
- f"failed to evaluate expression '{expr}' for key {key}"
796
+ f'failed to evaluate expression {expr!r} for key {key!r}'
795
797
  ) from exc
796
-
797
798
  return result
798
799
 
799
- else:
800
- raise ValueError(f"unknown mode '{mode}', expected 'single' or 'zip'")
800
+ raise ValueError(f"unknown mode '{mode}', expected 'single' or 'zip'")
@@ -5,6 +5,7 @@ from typing import Any, Callable, Literal, Union, cast
5
5
  import numpy as np
6
6
  import numpy.typing as npt
7
7
 
8
+ from .expression import expression_function
8
9
  from .readers.hierarchy import Signal
9
10
 
10
11
 
@@ -276,6 +277,7 @@ class Waveform:
276
277
  value.astype(np.uint64) - offset,
277
278
  )
278
279
 
280
+ @expression_function
279
281
  def as_signed(self) -> Waveform:
280
282
  """Reinterpret the unsigned bit pattern as a two's-complement signed integer.
281
283
 
@@ -293,6 +295,7 @@ class Waveform:
293
295
  def _unsigned(value: np.ndarray, width: int):
294
296
  return value & ((1 << width) - 1)
295
297
 
298
+ @expression_function
296
299
  def as_unsigned(self) -> Waveform:
297
300
  """Reinterpret the signed value as an unsigned bit pattern.
298
301
 
@@ -308,10 +311,6 @@ class Waveform:
308
311
  lambda x: self._unsigned(x, width), width=self.width, signed=False
309
312
  )
310
313
 
311
- @staticmethod
312
- def _add(a, b):
313
- return a + b
314
-
315
314
  def _check_sign(self, other: WaveformOrScalar):
316
315
  if isinstance(other, Waveform) and self.signed != other.signed:
317
316
  raise ValueError('signedness mismatch')
@@ -367,9 +366,10 @@ class Waveform:
367
366
  return max_width + 1
368
367
 
369
368
  new_width = self._infer_arithmetic_op_width(inferred_width)
369
+ other_value = self._get_value(other)
370
370
 
371
371
  return self.vectorized_map(
372
- lambda x: self._add(x, self._get_value(other)),
372
+ lambda x: x + other_value,
373
373
  width=new_width,
374
374
  signed=self.signed,
375
375
  )
@@ -377,10 +377,6 @@ class Waveform:
377
377
  def __radd__(self, other: WaveformOrScalar) -> Waveform:
378
378
  return self.__add__(other)
379
379
 
380
- @staticmethod
381
- def _sub(a, b):
382
- return a - b
383
-
384
380
  def __sub__(self, other: WaveformOrScalar) -> Waveform:
385
381
  self._check_sign(other)
386
382
 
@@ -388,9 +384,10 @@ class Waveform:
388
384
  return self._optional_max_width(other)
389
385
 
390
386
  new_width = self._infer_arithmetic_op_width(inferred_width)
387
+ other_value = self._get_value(other)
391
388
 
392
389
  return self.vectorized_map(
393
- lambda x: self._sub(x, self._get_value(other)),
390
+ lambda x: x - other_value,
394
391
  width=new_width,
395
392
  signed=self.signed,
396
393
  )
@@ -402,17 +399,14 @@ class Waveform:
402
399
  return self._optional_max_width(other)
403
400
 
404
401
  new_width = self._infer_arithmetic_op_width(inferred_width)
402
+ other_value = self._get_value(other)
405
403
 
406
404
  return self.vectorized_map(
407
- lambda x: self._sub(self._get_value(other), x),
405
+ lambda x: other_value - x,
408
406
  width=new_width,
409
407
  signed=self.signed,
410
408
  )
411
409
 
412
- @staticmethod
413
- def _mul(a, b):
414
- return a * b
415
-
416
410
  def __mul__(self, other: WaveformOrScalar) -> Waveform:
417
411
  self._check_sign(other)
418
412
 
@@ -423,9 +417,10 @@ class Waveform:
423
417
  return self.width + other_width
424
418
 
425
419
  new_width = self._infer_arithmetic_op_width(inferred_width)
420
+ other_value = self._get_value(other)
426
421
 
427
422
  return self.vectorized_map(
428
- lambda x: self._mul(x, self._get_value(other)),
423
+ lambda x: x * other_value,
429
424
  width=new_width,
430
425
  signed=self.signed,
431
426
  )
@@ -433,13 +428,9 @@ class Waveform:
433
428
  def __rmul__(self, other: WaveformOrScalar) -> Waveform:
434
429
  return self.__mul__(other)
435
430
 
436
- @staticmethod
437
- def _truediv(a, b):
438
- return a / b
439
-
440
431
  def __truediv__(self, other: WaveformOrScalar) -> Waveform:
441
432
  self._check_sign(other)
442
- new_value = self._truediv(self.value, self._get_value(other))
433
+ new_value = self.value / self._get_value(other)
443
434
 
444
435
  return Waveform(
445
436
  value=new_value,
@@ -451,7 +442,7 @@ class Waveform:
451
442
 
452
443
  def __rtruediv__(self, other: WaveformOrScalar) -> Waveform:
453
444
  self._check_sign(other)
454
- new_value = self._truediv(self._get_value(other), self.value)
445
+ new_value = self._get_value(other) / self.value
455
446
 
456
447
  return Waveform(
457
448
  value=new_value,
@@ -461,14 +452,10 @@ class Waveform:
461
452
  signed=self.signed,
462
453
  )
463
454
 
464
- @staticmethod
465
- def _floordiv(a, b):
466
- return a // b
467
-
468
455
  def __floordiv__(self, other: WaveformOrScalar) -> Waveform:
469
456
  self._check_sign(other)
470
457
  new_width = self._infer_arithmetic_op_width(lambda: self.width)
471
- new_value = self._floordiv(self.value, self._get_value(other))
458
+ new_value = self.value // self._get_value(other)
472
459
 
473
460
  return Waveform(
474
461
  value=new_value,
@@ -481,7 +468,7 @@ class Waveform:
481
468
  def __rfloordiv__(self, other: WaveformOrScalar) -> Waveform:
482
469
  self._check_sign(other)
483
470
  new_width = self._infer_arithmetic_op_width(lambda: self._get_width(other))
484
- new_value = self._floordiv(self._get_value(other), self.value)
471
+ new_value = self._get_value(other) // self.value
485
472
 
486
473
  return Waveform(
487
474
  value=new_value,
@@ -491,15 +478,11 @@ class Waveform:
491
478
  signed=self.signed,
492
479
  )
493
480
 
494
- @staticmethod
495
- def _mod(a, b):
496
- return a % b
497
-
498
481
  def __mod__(self, other: WaveformOrScalar) -> Waveform:
499
482
  self._check_sign(other)
500
483
  new_width = self._infer_arithmetic_op_width(lambda: self.width)
501
484
 
502
- new_value = self._mod(self.value, self._get_value(other))
485
+ new_value = self.value % self._get_value(other)
503
486
 
504
487
  return Waveform(
505
488
  value=new_value,
@@ -513,7 +496,7 @@ class Waveform:
513
496
  self._check_sign(other)
514
497
  new_width = self._infer_arithmetic_op_width(lambda: self._get_width(other))
515
498
 
516
- new_value = self._mod(self._get_value(other), self.value)
499
+ new_value = self._get_value(other) % self.value
517
500
 
518
501
  return Waveform(
519
502
  value=new_value,
@@ -523,15 +506,11 @@ class Waveform:
523
506
  signed=self.signed,
524
507
  )
525
508
 
526
- @staticmethod
527
- def _pow(a, b):
528
- return a**b
529
-
530
509
  def __pow__(self, other: WaveformOrScalar) -> Waveform:
531
510
  self._check_sign(other)
532
511
  new_width = self._infer_arithmetic_op_width(lambda: 64)
533
512
 
534
- new_value = self._pow(self.value, self._get_value(other))
513
+ new_value = self.value ** self._get_value(other)
535
514
 
536
515
  return Waveform(
537
516
  value=new_value,
@@ -576,10 +555,6 @@ class Waveform:
576
555
  raise ValueError(f'width mismatch: {self.width} and {other}')
577
556
  return self.width
578
557
 
579
- @staticmethod
580
- def _lshift(a, b):
581
- return a << b
582
-
583
558
  def __lshift__(self, other: WaveformOrScalar) -> Waveform:
584
559
  self._check_sign(other)
585
560
  self._check_logical_op_type(other)
@@ -597,10 +572,9 @@ class Waveform:
597
572
  inferred_width=base_width + shift_width,
598
573
  )
599
574
 
600
- new_value = self._lshift(
601
- self.value.astype(np.object_) if new_width > 64 else self.value,
602
- self._get_value(other),
603
- )
575
+ new_value = (
576
+ self.value.astype(np.object_) if new_width > 64 else self.value
577
+ ) << self._get_value(other)
604
578
 
605
579
  return Waveform(
606
580
  value=new_value,
@@ -613,10 +587,6 @@ class Waveform:
613
587
  def __rlshift__(self, other: WaveformOrScalar) -> Waveform:
614
588
  return self.__lshift__(other)
615
589
 
616
- @staticmethod
617
- def _rshift(a, b):
618
- return a >> b
619
-
620
590
  def __rshift__(self, other: WaveformOrScalar) -> Waveform:
621
591
  self._check_sign(other)
622
592
  self._check_logical_op_type(other)
@@ -631,7 +601,7 @@ class Waveform:
631
601
  inferred_width=max((self.width or 0) - other, 0),
632
602
  )
633
603
 
634
- new_value = self._rshift(self.value, self._get_value(other))
604
+ new_value = self.value >> self._get_value(other)
635
605
 
636
606
  return Waveform(
637
607
  value=new_value,
@@ -655,7 +625,7 @@ class Waveform:
655
625
  inferred_width=max((self.width or 0) - other, 0),
656
626
  )
657
627
 
658
- new_value = self._rshift(self.value, self._get_value(other))
628
+ new_value = self.value >> self._get_value(other)
659
629
 
660
630
  return Waveform(
661
631
  value=new_value,
@@ -665,15 +635,12 @@ class Waveform:
665
635
  signed=self.signed,
666
636
  )
667
637
 
668
- @staticmethod
669
- def _and(a, b):
670
- return a & b
671
-
672
638
  def __and__(self, other: WaveformOrScalar) -> Waveform:
673
639
  self._check_sign(other)
674
640
  new_width = self._infer_logical_op_width(other)
641
+ other_value = self._get_value(other)
675
642
  return self.vectorized_map(
676
- lambda x: self._and(x, self._get_value(other)),
643
+ lambda x: x & other_value,
677
644
  width=new_width,
678
645
  signed=False,
679
646
  )
@@ -681,29 +648,21 @@ class Waveform:
681
648
  def __rand__(self, other: WaveformOrScalar) -> Waveform:
682
649
  return self.__and__(other)
683
650
 
684
- @staticmethod
685
- def _or(a, b):
686
- return a | b
687
-
688
651
  def __or__(self, other: WaveformOrScalar, width: int = None) -> Waveform:
689
652
  self._check_sign(other)
690
653
  new_width = self._infer_logical_op_width(other)
691
- return self.vectorized_map(
692
- lambda x: self._or(x, self._get_value(other)), width=new_width, signed=False
693
- )
654
+ other_value = self._get_value(other)
655
+ return self.vectorized_map(lambda x: x | other_value, width=new_width, signed=False)
694
656
 
695
657
  def __ror__(self, other: WaveformOrScalar, width: int = None) -> Waveform:
696
658
  return self.__or__(other, width)
697
659
 
698
- @staticmethod
699
- def _xor(a, b):
700
- return a ^ b
701
-
702
660
  def __xor__(self, other: WaveformOrScalar, width: int = None) -> Waveform:
703
661
  self._check_sign(other)
704
662
  new_width = self._infer_logical_op_width(other)
663
+ other_value = self._get_value(other)
705
664
  return self.vectorized_map(
706
- lambda x: self._xor(x, self._get_value(other)),
665
+ lambda x: x ^ other_value,
707
666
  width=new_width,
708
667
  signed=False,
709
668
  )
@@ -725,30 +684,50 @@ class Waveform:
725
684
  signed=False,
726
685
  )
727
686
 
728
- @staticmethod
729
- def _eq(a, b):
730
- return a == b
687
+ def _compare(self, other: object, comparator: Callable[[Any, Any], Any]) -> Any:
688
+ if not isinstance(other, (Waveform, int, float)):
689
+ return NotImplemented
690
+ self._check_sign(other)
691
+ return self.vectorized_map(
692
+ lambda x: comparator(x, self._get_value(other)),
693
+ width=1,
694
+ signed=False,
695
+ )
696
+
697
+ def __lt__(self, other: object) -> Any:
698
+ """Return a 1-bit waveform for ``self < other``."""
699
+ return self._compare(other, lambda a, b: a < b)
700
+
701
+ def __le__(self, other: object) -> Any:
702
+ """Return a 1-bit waveform for ``self <= other``."""
703
+ return self._compare(other, lambda a, b: a <= b)
704
+
705
+ def __gt__(self, other: object) -> Any:
706
+ """Return a 1-bit waveform for ``self > other``."""
707
+ return self._compare(other, lambda a, b: a > b)
708
+
709
+ def __ge__(self, other: object) -> Any:
710
+ """Return a 1-bit waveform for ``self >= other``."""
711
+ return self._compare(other, lambda a, b: a >= b)
731
712
 
732
713
  def __eq__(self, other: object) -> Any:
733
714
  if not isinstance(other, (Waveform, int, float)):
734
715
  return NotImplemented
735
716
  self._check_sign(other)
717
+ other_value = self._get_value(other)
736
718
  return self.vectorized_map(
737
- lambda x: self._eq(x, self._get_value(other)),
719
+ lambda x: x == other_value,
738
720
  width=1,
739
721
  signed=False,
740
722
  )
741
723
 
742
- @staticmethod
743
- def _ne(a, b):
744
- return a != b
745
-
746
724
  def __ne__(self, other: object) -> Any:
747
725
  if not isinstance(other, (Waveform, int, float)):
748
726
  return NotImplemented
749
727
  self._check_sign(other)
728
+ other_value = self._get_value(other)
750
729
  return self.vectorized_map(
751
- lambda x: self._ne(x, self._get_value(other)),
730
+ lambda x: x != other_value,
752
731
  width=1,
753
732
  signed=False,
754
733
  )
@@ -962,6 +941,7 @@ class Waveform:
962
941
  vectorized_func = np.vectorize(func)
963
942
  return self.vectorized_map(vectorized_func, width, signed)
964
943
 
944
+ @expression_function
965
945
  def falling_edge(self) -> Waveform:
966
946
  """Detect 1→0 transitions in a 1-bit waveform.
967
947
 
@@ -991,6 +971,7 @@ class Waveform:
991
971
  signed=False,
992
972
  )
993
973
 
974
+ @expression_function
994
975
  def rising_edge(self) -> Waveform:
995
976
  """Detect 0→1 transitions in a 1-bit waveform.
996
977
 
@@ -1020,6 +1001,7 @@ class Waveform:
1020
1001
  signed=False,
1021
1002
  )
1022
1003
 
1004
+ @expression_function
1023
1005
  def bit_count(self) -> Waveform:
1024
1006
  """Count the number of set bits (population count) in each sample value.
1025
1007
 
@@ -1419,6 +1401,7 @@ class Waveform:
1419
1401
  signed=self.signed,
1420
1402
  )
1421
1403
 
1404
+ @expression_function
1422
1405
  def ahead(
1423
1406
  self,
1424
1407
  n: int = 1,
@@ -1452,6 +1435,7 @@ class Waveform:
1452
1435
  """
1453
1436
  return self.relative(n, pad, pad_value)
1454
1437
 
1438
+ @expression_function
1455
1439
  def back(
1456
1440
  self,
1457
1441
  n: int = 1,
File without changes
File without changes
File without changes