wavekit 0.8.0__cp313-cp313-pyemscripten_2025_0_wasm32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wavekit/__init__.py ADDED
@@ -0,0 +1,73 @@
1
+ """Clock-synchronised analysis for VCD, FST, and FSDB waveforms."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import metadata
6
+
7
+ try:
8
+ __version__ = metadata.version('wavekit')
9
+ except metadata.PackageNotFoundError:
10
+ __version__ = 'unknown'
11
+
12
+ from .readers.clock_domain import ClockDomain as ClockDomain
13
+ from .readers.fst.reader import FstReader as FstReader
14
+ from .readers.hierarchy import Node as Node
15
+ from .readers.hierarchy import Scope as Scope
16
+ from .readers.hierarchy import Signal as Signal
17
+ from .readers.hierarchy import SignalCompositeType as SignalCompositeType
18
+ from .readers.matcher import BraceCapture as BraceCapture
19
+ from .readers.matcher import Capture as Capture
20
+ from .readers.matcher import ExactCapture as ExactCapture
21
+ from .readers.matcher import RegexCapture as RegexCapture
22
+ from .readers.matcher import WildcardCapture as WildcardCapture
23
+ from .readers.range import Range as Range
24
+ from .readers.vcd.reader import VcdReader as VcdReader
25
+ from .waveform import Waveform as Waveform
26
+
27
+ __all__ = [
28
+ 'Waveform',
29
+ 'VcdReader',
30
+ 'FsdbReader',
31
+ 'FstReader',
32
+ 'ClockDomain',
33
+ 'Node',
34
+ 'Scope',
35
+ 'Range',
36
+ 'Signal',
37
+ 'SignalCompositeType',
38
+ 'Capture',
39
+ 'ExactCapture',
40
+ 'BraceCapture',
41
+ 'RegexCapture',
42
+ 'WildcardCapture',
43
+ 'has_fsdb_support',
44
+ ]
45
+
46
+ try:
47
+ from .readers.fsdb.npi_fsdb_reader import fsdb_runtime_available as _fsdb_runtime_available
48
+ from .readers.fsdb.reader import FsdbReader as FsdbReader
49
+ except Exception as _fsdb_import_error:
50
+ _fsdb_available = False
51
+
52
+ def has_fsdb_support() -> bool:
53
+ """Check whether the Verdi FSDB runtime is available."""
54
+ return False
55
+
56
+ class _FsdbReaderStub:
57
+ """Placeholder that raises an error when the Verdi FSDB runtime is unavailable."""
58
+
59
+ def __init__(self, *args, **kwargs):
60
+ raise RuntimeError(
61
+ 'FsdbReader requires the Verdi FSDB runtime (libNPI.so).\n\n'
62
+ 'Set WAVEKIT_NPI_LIB to the library path, set VERDI_HOME to the Verdi '
63
+ 'installation directory, or ensure libNPI.so is in LD_LIBRARY_PATH.\n\n'
64
+ f'Import error: {_fsdb_import_error}' # noqa: F821
65
+ )
66
+
67
+ FsdbReader = _FsdbReaderStub # type: ignore[assignment]
68
+ else:
69
+ _fsdb_available = True
70
+
71
+ def has_fsdb_support() -> bool:
72
+ """Check whether the Verdi FSDB runtime library is installed."""
73
+ return _fsdb_runtime_available()
wavekit/expression.py ADDED
@@ -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 ``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
@@ -0,0 +1,23 @@
1
+ """Protocol pattern matching for waveform analysis."""
2
+
3
+ from .dsl import Pattern as Pattern
4
+ from .dsl import collect as collect
5
+ from .dsl import match as match
6
+ from .errors import PatternError as PatternError
7
+ from .result import MatchPoint as MatchPoint
8
+ from .result import MatchRecord as MatchRecord
9
+ from .result import MatchRecords as MatchRecords
10
+ from .result import MatchStatus as MatchStatus
11
+ from .steps import Channel as Channel
12
+
13
+ __all__ = [
14
+ 'Pattern',
15
+ 'match',
16
+ 'collect',
17
+ 'MatchPoint',
18
+ 'MatchRecord',
19
+ 'MatchRecords',
20
+ 'MatchStatus',
21
+ 'PatternError',
22
+ 'Channel',
23
+ ]
@@ -0,0 +1,212 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Hashable
4
+ from typing import Any
5
+
6
+ from ..waveform import Waveform
7
+ from .errors import PatternError
8
+ from .runtime import PatternContext
9
+ from .steps import (
10
+ BranchStep,
11
+ CaptureStep,
12
+ Channel,
13
+ ChannelValue,
14
+ Condition,
15
+ ConsumeStep,
16
+ DelayStep,
17
+ IntValue,
18
+ LoopStep,
19
+ MessageValue,
20
+ RepeatStep,
21
+ RequireStep,
22
+ SignalValue,
23
+ Step,
24
+ WaitStep,
25
+ )
26
+
27
+
28
+ def infer_declarative_axis(steps: list[Step]) -> Waveform | None:
29
+ """Infer a minimal first-static-waveform scan axis hint from steps."""
30
+ # Axis inference is intentionally minimal. It only finds a static waveform
31
+ # so start/end_cycle can be translated to indices; full cycle-axis alignment
32
+ # validation remains lazy in PatternRuntime.note_waveform().
33
+ for step in steps:
34
+ if isinstance(step, (WaitStep, ConsumeStep)):
35
+ if isinstance(step.cond, Waveform):
36
+ return step.cond
37
+ if isinstance(step.require, Waveform):
38
+ return step.require
39
+ elif isinstance(step, DelayStep):
40
+ if isinstance(step.require, Waveform):
41
+ return step.require
42
+ elif isinstance(step, CaptureStep):
43
+ if isinstance(step.signal, Waveform):
44
+ return step.signal
45
+ elif isinstance(step, RequireStep):
46
+ if isinstance(step.cond, Waveform):
47
+ return step.cond
48
+ elif isinstance(step, LoopStep):
49
+ if isinstance(step.until, Waveform):
50
+ return step.until
51
+ if isinstance(step.when, Waveform):
52
+ return step.when
53
+ waveform = infer_declarative_axis(step.body_template)
54
+ if waveform is not None:
55
+ return waveform
56
+ elif isinstance(step, RepeatStep):
57
+ waveform = infer_declarative_axis(step.body_template)
58
+ if waveform is not None:
59
+ return waveform
60
+ elif isinstance(step, BranchStep):
61
+ if isinstance(step.cond, Waveform):
62
+ return step.cond
63
+ if step.true_body:
64
+ waveform = infer_declarative_axis(step.true_body)
65
+ if waveform is not None:
66
+ return waveform
67
+ if step.false_body:
68
+ waveform = infer_declarative_axis(step.false_body)
69
+ if waveform is not None:
70
+ return waveform
71
+ return None
72
+
73
+
74
+ def compile_declarative_pattern(steps: list[Step]) -> Callable[[PatternContext], Any]:
75
+ """Compile declarative pattern steps into a program body."""
76
+
77
+ def eval_condition(cond: Condition, ctx: PatternContext) -> bool:
78
+ if isinstance(cond, Waveform):
79
+ return bool(ctx.value(cond))
80
+ if isinstance(cond, bool):
81
+ return cond
82
+ if callable(cond):
83
+ return bool(cond(ctx.index, ctx.captures))
84
+ raise PatternError(
85
+ f'condition must be a Waveform, bool, or callable, got {type(cond).__name__}'
86
+ )
87
+
88
+ def eval_signal(signal: SignalValue, ctx: PatternContext) -> Any:
89
+ if isinstance(signal, Waveform):
90
+ return ctx.value(signal)
91
+ if callable(signal):
92
+ return signal(ctx.index, ctx.captures)
93
+ raise PatternError(f'signal must be a Waveform or callable, got {type(signal).__name__}')
94
+
95
+ def eval_int(value: IntValue, ctx: PatternContext) -> int:
96
+ if callable(value):
97
+ value = value(ctx.index, ctx.captures)
98
+ if isinstance(value, int):
99
+ return value
100
+ raise PatternError(f'integer value must be an int or callable, got {type(value).__name__}')
101
+
102
+ def eval_message(message: MessageValue | None, ctx: PatternContext) -> str | None:
103
+ if message is None or isinstance(message, str):
104
+ return message
105
+ if callable(message):
106
+ return str(message(ctx.index, ctx.captures))
107
+ raise PatternError(f'message must be a str or callable, got {type(message).__name__}')
108
+
109
+ def adapt_message(message: MessageValue | None, ctx: PatternContext):
110
+ if message is None:
111
+ return None
112
+ return lambda message=message: eval_message(message, ctx)
113
+
114
+ def adapt_channel(
115
+ channel: ChannelValue, ctx: PatternContext
116
+ ) -> Channel | Hashable | Callable[[], Channel | Hashable]:
117
+ if callable(channel):
118
+ return lambda channel=channel: channel(ctx.index, ctx.captures)
119
+ return channel
120
+
121
+ def run_steps(ctx: PatternContext, step_list: list[Step]) -> None:
122
+ for step in step_list:
123
+ if isinstance(step, WaitStep):
124
+ ctx.wait(
125
+ lambda step=step: eval_condition(step.cond, ctx),
126
+ require=None
127
+ if step.require is None
128
+ else lambda step=step: eval_condition(step.require, ctx),
129
+ require_message=adapt_message(step.require_message, ctx),
130
+ )
131
+ elif isinstance(step, ConsumeStep):
132
+ ctx.consume(
133
+ lambda step=step: eval_condition(step.cond, ctx),
134
+ channel=adapt_channel(step.channel, ctx),
135
+ require=None
136
+ if step.require is None
137
+ else lambda step=step: eval_condition(step.require, ctx),
138
+ require_message=adapt_message(step.require_message, ctx),
139
+ )
140
+ elif isinstance(step, DelayStep):
141
+ ctx.delay(
142
+ eval_int(step.n, ctx),
143
+ require=None
144
+ if step.require is None
145
+ else lambda step=step: eval_condition(step.require, ctx),
146
+ require_message=adapt_message(step.require_message, ctx),
147
+ )
148
+ elif isinstance(step, CaptureStep):
149
+ ctx.capture(step.name, eval_signal(step.signal, ctx), mode=step.mode)
150
+ elif isinstance(step, RequireStep):
151
+ ctx.require(
152
+ lambda step=step: eval_condition(step.cond, ctx),
153
+ message=adapt_message(step.message, ctx),
154
+ )
155
+ elif isinstance(step, BranchStep):
156
+ if eval_condition(step.cond, ctx):
157
+ if step.true_body:
158
+ run_steps(ctx, step.true_body)
159
+ elif step.false_body:
160
+ run_steps(ctx, step.false_body)
161
+ elif isinstance(step, RepeatStep):
162
+ count = eval_int(step.n, ctx)
163
+ if count < 0:
164
+ raise PatternError(f'repeat count must be non-negative, got {count}')
165
+ for _ in range(count):
166
+ run_steps(ctx, step.body_template)
167
+ elif isinstance(step, LoopStep):
168
+ if step.when is not None:
169
+ while eval_condition(step.when, ctx):
170
+ ctx._guard_operation()
171
+ run_steps(ctx, step.body_template)
172
+ elif step.until is not None:
173
+ while True:
174
+ ctx._guard_operation()
175
+ run_steps(ctx, step.body_template)
176
+ if eval_condition(step.until, ctx):
177
+ break
178
+ else:
179
+ raise PatternError("loop step must specify either 'until' or 'when'")
180
+ else:
181
+ raise PatternError(f'Unknown step type: {type(step).__name__}')
182
+
183
+ def body(ctx: PatternContext) -> object:
184
+ first_step = steps[0] if steps else None
185
+ if isinstance(first_step, WaitStep):
186
+ # The first blocking step selects candidate start cycles; later
187
+ # blocking steps describe timing inside a matched transaction.
188
+ if not eval_condition(first_step.cond, ctx):
189
+ if first_step.require is not None:
190
+ ctx.require(
191
+ lambda first_step=first_step: eval_condition(first_step.require, ctx),
192
+ message=adapt_message(first_step.require_message, ctx),
193
+ )
194
+ return None
195
+ run_steps(ctx, steps[1:])
196
+ elif isinstance(first_step, ConsumeStep):
197
+ if not ctx.try_consume(
198
+ lambda first_step=first_step: eval_condition(first_step.cond, ctx),
199
+ adapt_channel(first_step.channel, ctx),
200
+ ):
201
+ if first_step.require is not None:
202
+ ctx.require(
203
+ lambda first_step=first_step: eval_condition(first_step.require, ctx),
204
+ message=adapt_message(first_step.require_message, ctx),
205
+ )
206
+ return None
207
+ run_steps(ctx, steps[1:])
208
+ else:
209
+ run_steps(ctx, steps)
210
+ return ctx.OK
211
+
212
+ return body