wavekit 0.5.0__tar.gz → 0.5.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 (24) hide show
  1. {wavekit-0.5.0 → wavekit-0.5.2}/PKG-INFO +1 -1
  2. {wavekit-0.5.0 → wavekit-0.5.2}/pyproject.toml +1 -1
  3. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/__init__.py +2 -0
  4. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/readers/base.py +3 -3
  5. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/readers/fsdb/npi_fsdb_reader.pyx +129 -233
  6. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/readers/fsdb/reader.py +93 -41
  7. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/readers/vcd/reader.py +0 -3
  8. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/scope.py +85 -45
  9. wavekit-0.5.2/src/wavekit/signal.py +87 -0
  10. wavekit-0.5.0/src/wavekit/signal.py +0 -49
  11. {wavekit-0.5.0 → wavekit-0.5.2}/LICENSE +0 -0
  12. {wavekit-0.5.0 → wavekit-0.5.2}/README.md +0 -0
  13. {wavekit-0.5.0 → wavekit-0.5.2}/build.py +0 -0
  14. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/pattern/__init__.py +0 -0
  15. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/pattern/dsl.py +0 -0
  16. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/pattern/engine.py +0 -0
  17. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/pattern/instance.py +0 -0
  18. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/pattern/result.py +0 -0
  19. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/pattern/steps.py +0 -0
  20. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/readers/expr_parser.py +0 -0
  21. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/readers/fsdb/npi_fsdb.pxd +0 -0
  22. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/readers/pattern_parser.py +0 -0
  23. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/readers/value_change.pyx +0 -0
  24. {wavekit-0.5.0 → wavekit-0.5.2}/src/wavekit/waveform.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wavekit
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: a fundamental package for digital circuit waveform analysis
5
5
  License-File: LICENSE
6
6
  Author: cxzzzz
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wavekit"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  description = "a fundamental package for digital circuit waveform analysis"
5
5
  authors = ["cxzzzz <cxz19961010@outlook.com>"]
6
6
  readme = "README.md"
@@ -20,6 +20,7 @@ from .pattern import PatternError as PatternError
20
20
  from .readers.vcd.reader import VcdReader as VcdReader
21
21
  from .scope import Scope as Scope
22
22
  from .signal import Signal as Signal
23
+ from .signal import SignalCompositeType as SignalCompositeType
23
24
  from .waveform import Waveform as Waveform
24
25
 
25
26
  __all__ = [
@@ -28,6 +29,7 @@ __all__ = [
28
29
  'FsdbReader',
29
30
  'Scope',
30
31
  'Signal',
32
+ 'SignalCompositeType',
31
33
  'Pattern',
32
34
  'MatchResult',
33
35
  'MatchStatus',
@@ -38,7 +38,7 @@ class Reader:
38
38
  If the bit-range suffix is omitted and the file stores the signal with a
39
39
  range, the range is appended automatically.
40
40
 
41
- Pattern syntax (used by :meth:`get_matched_signals`,
41
+ Pattern syntax (used by :meth:`get_matched_signals`, :meth:`get_matched_scopes`,
42
42
  :meth:`load_matched_waveforms`, :meth:`eval`)
43
43
  -------------------------------------------------
44
44
  * ``{a,b,c}`` — matches ``a``, ``b``, or ``c``; captures each as a key.
@@ -233,7 +233,7 @@ class Reader:
233
233
  )
234
234
  return matched_signals
235
235
 
236
- def get_matched_scope(
236
+ def get_matched_scopes(
237
237
  self,
238
238
  pattern: str,
239
239
  root_scope: Scope | None = None,
@@ -270,7 +270,7 @@ class Reader:
270
270
  ::
271
271
 
272
272
  # Find all fifo_N sub-scopes under tb.dut
273
- scopes = reader.get_matched_scope("tb.dut.fifo_{0..3}")
273
+ scopes = reader.get_matched_scopes("tb.dut.fifo_{0..3}")
274
274
  for (idx,), scope in scopes.items():
275
275
  waves = reader.load_matched_waveforms(
276
276
  "w_ptr[2:0]", clock_pattern="clk", root_scope=scope
@@ -3,155 +3,170 @@ import math
3
3
  import sys
4
4
  import numpy as np
5
5
  cimport numpy as np
6
- #from cpython import PyBytes_FromString
7
6
  import cython
8
7
  from libcpp.vector cimport vector
9
8
  from libc.stdlib cimport malloc, free
10
9
  from libc.string cimport strcpy, strlen
11
10
  from .npi_fsdb cimport *
12
11
 
12
+ NPI_FSDB_CT_ARRAY = <int>npiFsdbSigCtArray
13
+ NPI_FSDB_CT_STRUCT = <int>npiFsdbSigCtStruct
14
+ NPI_FSDB_CT_UNION = <int>npiFsdbSigCtUnion
15
+ NPI_FSDB_CT_TAGGED_UNION = <int>npiFsdbSigCtTaggedUnion
16
+ NPI_FSDB_CT_RECORD = <int>npiFsdbSigCtRecord
13
17
 
14
- cdef class NpiFsdbScope:
15
- pass
16
18
 
19
+ cdef class NpiFsdbSignal:
20
+ """Wraps a npiFsdbSigHandle — a single signal (leaf or composite) in the FSDB hierarchy."""
21
+ cdef npiFsdbSigHandle sig_handle
22
+
23
+ @staticmethod
24
+ cdef init(npiFsdbSigHandle sig_handle):
25
+ sig = NpiFsdbSignal()
26
+ sig.sig_handle = sig_handle
27
+ return sig
28
+
29
+ def __init__(self):
30
+ self.sig_handle = NULL
31
+
32
+ def name(self) -> str:
33
+ assert self.sig_handle != NULL
34
+ name = npi_fsdb_sig_property_str(npiFsdbSigName, self.sig_handle).decode('ascii')
35
+ # NPI returns fully-qualified names like "tb.dut.field"; take only the last component.
36
+ # Array elements are named "a[0]", "a[0][1]" etc. — splitting by "." is safe since
37
+ # brackets cannot contain ".".
38
+ name = name.split(".")
39
+ name = name[len(name)-1]
40
+ return name
41
+
42
+ def has_member(self) -> bool:
43
+ assert self.sig_handle != NULL
44
+ cdef int has_member
45
+ if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member):
46
+ return has_member != 0
47
+ return False
48
+
49
+ def composite_type(self):
50
+ """Return the npiFsdbSigCompositeType_e int value, or None if not composite."""
51
+ assert self.sig_handle != NULL
52
+ cdef int has_member
53
+ cdef int ct
54
+ if not (npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member):
55
+ return None
56
+ if npi_fsdb_sig_property(npiFsdbSigCompositeType, self.sig_handle, &ct):
57
+ return ct
58
+ return None
59
+
60
+ def width(self) -> int:
61
+ """Return the total bit-width of this signal (sum of all leaf members for composites)."""
62
+ assert self.sig_handle != NULL
63
+ cdef int has_member
64
+ cdef int w
65
+ cdef npiFsdbSigIter sub_signal_iter
66
+ cdef npiFsdbSigHandle sub_signal
67
+ assert npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member)
68
+ if has_member == 0:
69
+ assert npi_fsdb_sig_property(npiFsdbSigRangeSize, self.sig_handle, &w)
70
+ return w
71
+ else:
72
+ sub_signal_iter = npi_fsdb_iter_member(self.sig_handle)
73
+ assert sub_signal_iter != NULL
74
+ w = 0
75
+ while (sub_signal := npi_fsdb_iter_sig_next(sub_signal_iter)):
76
+ w += NpiFsdbSignal.init(sub_signal).width()
77
+ return w
78
+
79
+ def range(self):
80
+ """Return the (high, low) bit-range tuple, or None for non-array composites."""
81
+ assert self.sig_handle != NULL
82
+ cdef int has_member
83
+ cdef int ct
84
+ cdef int left_range, right_range
85
+ assert npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member)
86
+ assert npi_fsdb_sig_property(npiFsdbSigLeftRange, self.sig_handle, &left_range)
87
+ assert npi_fsdb_sig_property(npiFsdbSigRightRange, self.sig_handle, &right_range)
88
+ if has_member != 0:
89
+ assert npi_fsdb_sig_property(npiFsdbSigCompositeType, self.sig_handle, &ct)
90
+ if ct == <int>npiFsdbSigCtArray:
91
+ return (left_range, right_range)
92
+ return None
93
+ return None
94
+
95
+ def member_list(self) -> list:
96
+ """Return direct member NpiFsdbSignal objects for composite signals."""
97
+ assert self.sig_handle != NULL
98
+ cdef int has_member
99
+ cdef npiFsdbSigIter member_iter
100
+ cdef npiFsdbSigHandle member_handle
17
101
 
18
- cdef class NpiFsdbNormalScope(NpiFsdbScope):
102
+ res = []
103
+ if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member:
104
+ member_iter = npi_fsdb_iter_member(self.sig_handle)
105
+ while True:
106
+ member_handle = npi_fsdb_iter_sig_next(member_iter)
107
+ if member_handle == NULL:
108
+ break
109
+ res.append(NpiFsdbSignal.init(member_handle))
110
+ npi_fsdb_iter_sig_stop(member_iter)
111
+ return res
112
+
113
+
114
+ cdef class NpiFsdbScope:
115
+ """Wraps a npiFsdbScopeHandle — a scope node (module/block) in the FSDB hierarchy."""
19
116
  cdef npiFsdbScopeHandle scope_handle
20
117
 
21
118
  @staticmethod
22
119
  cdef init(npiFsdbScopeHandle scope_handle):
23
- scope = NpiFsdbNormalScope()
120
+ scope = NpiFsdbScope()
24
121
  scope.scope_handle = scope_handle
25
122
  return scope
26
123
 
27
124
  def __init__(self):
28
125
  self.scope_handle = NULL
29
126
 
30
- def child_scope_list(self, include_signal_scope:bool = True) -> list[NpiFsdbScope]:
127
+ def child_scope_list(self) -> list:
128
+ """Return direct child NpiFsdbScope nodes (not signals)."""
31
129
  assert self.scope_handle != NULL
32
130
  res = []
33
131
  child_scope_handle_iter = npi_fsdb_iter_child_scope(self.scope_handle)
34
-
132
+
35
133
  cdef npiFsdbScopeHandle child_scope_handle
36
134
  while True:
37
135
  child_scope_handle = npi_fsdb_iter_scope_next(child_scope_handle_iter)
38
136
  if child_scope_handle == NULL:
39
137
  break
40
- res.append(NpiFsdbNormalScope.init(child_scope_handle))
138
+ res.append(NpiFsdbScope.init(child_scope_handle))
41
139
  npi_fsdb_iter_scope_stop(child_scope_handle_iter)
42
-
43
-
44
- cdef npiFsdbSigHandle child_sig_handle
45
- if include_signal_scope:
46
- child_sig_handle_iter = npi_fsdb_iter_sig(self.scope_handle)
47
- while True:
48
- child_sig_handle = npi_fsdb_iter_sig_next(child_sig_handle_iter)
49
- if child_sig_handle == NULL:
50
- break
51
- res.append(NpiFsdbSignalScope.init(child_sig_handle))
52
- npi_fsdb_iter_sig_stop(child_sig_handle_iter)
53
140
  return res
54
141
 
55
- def signal_list(self) -> list[str]:
142
+ def signal_list(self) -> list:
143
+ """Return direct child NpiFsdbSignal objects declared in this scope."""
56
144
  assert self.scope_handle != NULL
57
-
58
145
  res = []
59
- sig_handle_iter = npi_fsdb_iter_sig(self.scope_handle)
60
- cdef npiFsdbSigHandle sig_handle
61
- cdef const char* c_str
146
+ child_sig_handle_iter = npi_fsdb_iter_sig(self.scope_handle)
147
+
148
+ cdef npiFsdbSigHandle child_sig_handle
62
149
  while True:
63
- sig_handle = npi_fsdb_iter_sig_next(sig_handle_iter)
64
- if sig_handle == NULL:
150
+ child_sig_handle = npi_fsdb_iter_sig_next(child_sig_handle_iter)
151
+ if child_sig_handle == NULL:
65
152
  break
66
- c_str = npi_fsdb_sig_property_str(npiFsdbSigName, sig_handle)
67
- name = c_str.decode('ascii')
68
- res.append(name)
69
- npi_fsdb_iter_sig_stop(sig_handle_iter)
70
-
153
+ res.append(NpiFsdbSignal.init(child_sig_handle))
154
+ npi_fsdb_iter_sig_stop(child_sig_handle_iter)
71
155
  return res
72
156
 
73
157
  def name(self) -> str:
74
158
  assert self.scope_handle != NULL
75
- name = npi_fsdb_scope_property_str(npiFsdbScopeName, self.scope_handle).decode('ascii')
76
- return name
159
+ return npi_fsdb_scope_property_str(npiFsdbScopeName, self.scope_handle).decode('ascii')
77
160
 
78
161
  def type(self) -> str:
79
162
  assert self.scope_handle != NULL
80
- type_str = npi_fsdb_scope_property_str(npiFsdbScopeType, self.scope_handle).decode('ascii')
81
- return type_str
163
+ return npi_fsdb_scope_property_str(npiFsdbScopeType, self.scope_handle).decode('ascii')
82
164
 
83
165
  def def_name(self) -> str:
84
166
  assert self.scope_handle != NULL
85
- def_name = npi_fsdb_scope_property_str(npiFsdbScopeDefName, self.scope_handle).decode('ascii')
86
- return def_name
87
-
88
- cdef class NpiFsdbSignalScope(NpiFsdbScope):
89
- cdef npiFsdbSigHandle sig_handle
90
-
91
- @staticmethod
92
- cdef init(npiFsdbSigHandle sig_handle):
93
- scope = NpiFsdbSignalScope()
94
- scope.sig_handle = sig_handle
95
- return scope
96
-
97
- def __init__(self):
98
- self.sig_handle = NULL
99
-
100
- def child_scope_list(self, include_signal_scope:bool = True) -> list[NpiFsdbScope]:
101
- assert self.sig_handle != NULL
102
-
103
- cdef int has_member
104
- cdef npiFsdbSigIter member_iter
105
- cdef npiFsdbSigHandle member_handle
106
-
107
- res = []
108
- if include_signal_scope:
109
- if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member:
110
- member_iter = npi_fsdb_iter_member(self.sig_handle)
111
- while True:
112
- member_handle = npi_fsdb_iter_sig_next(member_iter)
113
- if member_handle == NULL:
114
- break
115
- res.append(NpiFsdbSignalScope.init(member_handle))
116
- npi_fsdb_iter_sig_stop(member_iter)
117
- return res
118
-
119
- def signal_list(self) -> list[str]:
120
- assert self.sig_handle != NULL
121
-
122
- cdef const char* c_str
123
- cdef npiFsdbSigIter member_iter
124
- cdef npiFsdbSigHandle member_handle
125
- cdef int has_member
126
- res = []
127
-
128
- if npi_fsdb_sig_property(npiFsdbSigHasMember, self.sig_handle, &has_member) and has_member:
129
- member_iter = npi_fsdb_iter_member(self.sig_handle)
130
- while True:
131
- member_handle = npi_fsdb_iter_sig_next(member_iter)
132
- if member_handle == NULL:
133
- break
134
- c_str = npi_fsdb_sig_property_str(npiFsdbSigName, member_handle)
135
- name = c_str.decode('ascii')
136
- name = name.split(".")
137
- name = name[len(name)-1]
138
- res.append(name)
139
- npi_fsdb_iter_sig_stop(member_iter)
140
- return res
167
+ return npi_fsdb_scope_property_str(npiFsdbScopeDefName, self.scope_handle).decode('ascii')
141
168
 
142
- def name(self) -> str:
143
- assert self.sig_handle != NULL
144
- name = npi_fsdb_sig_property_str(npiFsdbSigName, self.sig_handle).decode('ascii')
145
- #TODO: this only works for non-array type (array's name is like data[0],can't be split by ".")
146
- name = name.split(".")
147
- name = name[len(name)-1]
148
- return name
149
-
150
- def type(self) -> str:
151
- raise NotImplementedError("type property of signal scope is not supported")
152
169
 
153
- def def_name(self) -> str:
154
- raise NotImplementedError("def_name property of signal scope is not supported")
155
170
  @cython.boundscheck(False) # 关闭边界检查以提升性能
156
171
  @cython.wraparound(False) # 关闭负索引检查以提升性能
157
172
  cdef inline cstr_to_ull(char* str, unsigned int xz_value, int max_bit_num = 2147483647):
@@ -186,88 +201,6 @@ cdef inline cstr_to_bit(char* str, unsigned int xz_value):
186
201
  value = xz_value
187
202
  return value
188
203
 
189
- cdef inline fsdb_read_value_change(vct_handle: npiFsdbVctHandle, begin_time, end_time, width:int, xz_value:int):
190
-
191
- cdef npiFsdbTime cur_time
192
- cdef npiFsdbValue cur_value
193
- cdef int stat
194
-
195
- cdef vector[unsigned long long] time_array
196
-
197
- cdef int value_array_num = math.ceil(width/64)
198
- assert width <= 64*64
199
- cdef vector[unsigned long long] value_array[64]
200
-
201
- cdef int first = 1
202
- cdef int i,j
203
- cdef unsigned long long cur_int_value
204
- while True:
205
- if first:
206
- if begin_time is None:
207
- stat = npi_fsdb_goto_first(vct_handle)
208
- else:
209
- stat = npi_fsdb_goto_time(vct_handle, <npiFsdbTime> begin_time)
210
- else:
211
- stat = npi_fsdb_goto_next(vct_handle)
212
- if stat == 0:
213
- break
214
-
215
- cur_value.format = npiFsdbBinStrVal
216
- npi_fsdb_vct_time(vct_handle, &cur_time)
217
- npi_fsdb_vct_value(vct_handle, &cur_value)
218
- if cur_time > end_time:
219
- break
220
-
221
- time_array.push_back(cur_time)
222
- if width == 1: # opt for clk
223
- value_array[0].push_back(cstr_to_bit(<char*>cur_value.value.str, xz_value))
224
- else:
225
- cur_lllint_value = 0
226
- for i in range(value_array_num):
227
- value_array[i].push_back(cstr_to_ull(<char*>cur_value.value.str + 64*i, xz_value, 64))
228
-
229
- result = np.zeros((value_array[0].size(), 2), dtype=np.uint64 if width <= 64 else np.object_) # 创建一个 (n, 2) 的二维数组,dtype 为 int32
230
- for j in range(value_array[0].size()):
231
- result[j][0] = time_array[j]
232
- if width <= 64:
233
- result[j][1] = value_array[0][j]
234
- else:
235
- for i in range(value_array_num):
236
- result[j][1] = (result[j][1] << 64) + value_array[i][j]
237
-
238
- return result
239
-
240
- cdef get_signal_handle_width(npiFsdbSigHandle signal_handle):
241
- cdef int has_member
242
- cdef int width
243
- cdef npiFsdbSigIter sub_signal_iter
244
- cdef npiFsdbSigHandle sub_signal
245
- assert(npi_fsdb_sig_property(npiFsdbSigHasMember, signal_handle, &has_member))
246
- if(has_member == 0):
247
- assert(npi_fsdb_sig_property(npiFsdbSigRangeSize, signal_handle, &width))
248
- return width
249
- else:
250
- sub_signal_iter = npi_fsdb_iter_member(signal_handle)
251
- assert(sub_signal_iter != NULL)
252
- width = 0
253
- while(sub_signal := npi_fsdb_iter_sig_next(sub_signal_iter)):
254
- width += get_signal_handle_width(sub_signal)
255
- return width
256
-
257
- cdef get_signal_handle_range(npiFsdbSigHandle signal_handle):
258
- cdef int has_member
259
- cdef int type
260
- cdef int left_range, right_range
261
- assert(npi_fsdb_sig_property(npiFsdbSigHasMember, signal_handle, &has_member))
262
- assert(npi_fsdb_sig_property(npiFsdbSigLeftRange, signal_handle, &left_range))
263
- assert(npi_fsdb_sig_property(npiFsdbSigRightRange, signal_handle, &right_range))
264
- return (left_range, right_range)
265
-
266
- if(has_member != 0):
267
- assert(npi_fsdb_sig_property(npiFsdbSigCompositeType, signal_handle, &type))
268
- if(type == <int>npiFsdbSigCtArray):
269
- return (left_range, right_range)
270
- return None
271
204
 
272
205
  cdef class NpiFsdbReader:
273
206
  cdef npiFsdbFileHandle fsdb_handle
@@ -275,69 +208,36 @@ cdef class NpiFsdbReader:
275
208
 
276
209
  def __init__(self, str file):
277
210
  import os
278
-
211
+
279
212
  # 检查VERDI_HOME环境变量
280
213
  verdi_home = os.environ.get('VERDI_HOME')
281
214
  if verdi_home:
282
215
  npi_lib_path = os.path.join(verdi_home, 'share/NPI/lib/LINUX64/libNPI.so')
283
-
284
- #cdef int argc = len(sys.argv)
285
- #cdef char** argv = <char**>malloc((argc + 1) * sizeof(char*))
286
-
287
- #for i in range(argc):
288
- # argv[i] = <char*>malloc(strlen(sys.argv[i].encode('ascii')))
289
- # strcpy(argv[i], sys.argv[i].encode('ascii'))
290
- #argv[argc] = NULL # Null-terminate the array
291
216
 
292
- #npi_init(argc, argv)
293
- #self.file = file
294
- #free(argv)
295
-
296
- #cdef npiFsdbFileHandle fsdb_handle
297
217
  file_str = file.encode('utf-8')
298
218
  cdef char* file_s = file_str
299
219
  self.fsdb_handle = npi_fsdb_open(file_s)
300
220
  if(self.fsdb_handle == NULL):
301
221
  raise OSError(f"Failed to open fsdb file :{file_str}")
302
222
 
303
-
304
-
305
- def get_signal_width(
306
- self,
307
- str signal
308
- ) -> int:
309
-
310
- cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
223
+ def get_signal(self, str signal) -> NpiFsdbSignal:
224
+ """Look up a signal by its full hierarchical path and return an NpiFsdbSignal handle."""
225
+ cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
311
226
  assert signal_handle != NULL, f"can't find signal: {signal}"
312
-
313
- cdef int width = get_signal_handle_width(signal_handle)
314
- return width
315
-
316
- def get_signal_range(
317
- self,
318
- str signal
319
- ) -> tuple[int,int]:
320
-
321
- cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
322
- assert signal_handle != NULL, f"can't find signal: {signal}"
323
-
324
- return get_signal_handle_range(signal_handle)
227
+ return NpiFsdbSignal.init(signal_handle)
325
228
 
326
229
  @cython.boundscheck(False) # 关闭边界检查以提升性能
327
230
  @cython.wraparound(False) # 关闭负索引检查以提升性能
328
231
  def load_value_change(
329
232
  self,
330
- str signal,
233
+ NpiFsdbSignal signal,
331
234
  unsigned long long begin_time,
332
235
  unsigned long long end_time,
333
236
  int xz_value
334
237
  ) -> np.ndarray:
335
-
336
- cdef npiFsdbSigHandle signal_handle = npi_fsdb_sig_by_name(self.fsdb_handle, signal.encode('ascii'), NULL)
337
- assert signal_handle != NULL, f"can't find signal: {signal}"
338
- cdef int width = get_signal_handle_width(signal_handle)
339
238
 
340
- cdef npiFsdbVctHandle signal_vct_handle = npi_fsdb_create_vct(signal_handle)
239
+ cdef int width = signal.width()
240
+ cdef npiFsdbVctHandle signal_vct_handle = npi_fsdb_create_vct(signal.sig_handle)
341
241
  cdef npiFsdbTime cur_time
342
242
  cdef npiFsdbValue cur_value
343
243
  cdef int stat
@@ -358,7 +258,7 @@ cdef class NpiFsdbReader:
358
258
  stat = npi_fsdb_goto_time(signal_vct_handle, <npiFsdbTime> begin_time)
359
259
  else:
360
260
  stat = npi_fsdb_goto_next(signal_vct_handle)
361
-
261
+
362
262
  if stat == 0:
363
263
  break
364
264
 
@@ -398,11 +298,7 @@ cdef class NpiFsdbReader:
398
298
  result_object[:,1] = (result_object[:,1] << shift) + value_np_array
399
299
  return result_object
400
300
 
401
- #npi_fsdb_release_vct(signal_vct_handle)
402
- #npi_fsdb_unload_vc(self.fsdb_handle)
403
- #return result
404
-
405
- def top_scope_list(self) -> list[NpiFsdbScope]:
301
+ def top_scope_list(self) -> list:
406
302
  cdef npiFsdbScopeIter top_scope_iter
407
303
  cdef npiFsdbScopeHandle top_scope_handle
408
304
 
@@ -412,7 +308,7 @@ cdef class NpiFsdbReader:
412
308
  top_scope_handle = npi_fsdb_iter_scope_next(top_scope_iter)
413
309
  if top_scope_handle == NULL:
414
310
  break
415
- res.append(NpiFsdbNormalScope.init(top_scope_handle))
311
+ res.append(NpiFsdbScope.init(top_scope_handle))
416
312
  npi_fsdb_iter_scope_stop(top_scope_iter)
417
313
  return res
418
314
 
@@ -3,65 +3,114 @@ from __future__ import annotations
3
3
  import importlib
4
4
  from collections import defaultdict
5
5
  from collections.abc import Sequence
6
+ from dataclasses import dataclass, field
6
7
  from functools import cached_property
7
8
  from typing import Any
8
9
 
9
10
  import numpy as np
10
11
 
11
12
  from ...scope import Scope
12
- from ...signal import Signal
13
+ from ...signal import Signal, SignalCompositeType
13
14
  from ...waveform import Waveform
14
15
  from ..base import Reader
15
- from .npi_fsdb_reader import NpiFsdbReader, NpiFsdbScope, NpiFsdbSignalScope
16
+ from .npi_fsdb_reader import (
17
+ NPI_FSDB_CT_ARRAY,
18
+ NPI_FSDB_CT_RECORD,
19
+ NPI_FSDB_CT_STRUCT,
20
+ NPI_FSDB_CT_TAGGED_UNION,
21
+ NPI_FSDB_CT_UNION,
22
+ NpiFsdbReader,
23
+ NpiFsdbScope,
24
+ NpiFsdbSignal,
25
+ )
26
+
27
+
28
+ @dataclass
29
+ class FsdbSignal(Signal):
30
+ """FSDB-backed :class:`~wavekit.signal.Signal` with lazy member loading.
31
+
32
+ Extends :class:`~wavekit.signal.Signal` by holding an NPI signal handle
33
+ directly, enabling on-demand loading of composite members without any
34
+ reader reference. Construct via :meth:`from_handle`.
35
+ """
36
+
37
+ _npi_signal: NpiFsdbSignal | None = field(default=None, repr=False, compare=False)
38
+
39
+ @classmethod
40
+ def from_handle(cls, npi_sig: NpiFsdbSignal, full_name: str) -> FsdbSignal:
41
+ """Build an :class:`FsdbSignal` from an NPI signal handle and its full path."""
42
+ _npi_ct_to_enum = {
43
+ NPI_FSDB_CT_ARRAY: SignalCompositeType.ARRAY,
44
+ NPI_FSDB_CT_STRUCT: SignalCompositeType.STRUCT,
45
+ NPI_FSDB_CT_UNION: SignalCompositeType.UNION,
46
+ NPI_FSDB_CT_TAGGED_UNION: SignalCompositeType.TAGGED_UNION,
47
+ NPI_FSDB_CT_RECORD: SignalCompositeType.RECORD,
48
+ }
49
+ ct_raw = npi_sig.composite_type()
50
+ if ct_raw is None:
51
+ composite_type = None
52
+ elif ct_raw in _npi_ct_to_enum:
53
+ composite_type = _npi_ct_to_enum[ct_raw]
54
+ else:
55
+ raise ValueError(f"Unknown NPI composite type value: {ct_raw} for signal '{full_name}'")
56
+ return cls(
57
+ name=npi_sig.name(),
58
+ full_name=full_name,
59
+ width=npi_sig.width(),
60
+ range=npi_sig.range(),
61
+ composite_type=composite_type,
62
+ _npi_signal=npi_sig,
63
+ )
64
+
65
+ @cached_property
66
+ def member_list(self) -> list[Signal] | None:
67
+ if self.composite_type is None:
68
+ return None
69
+ assert self._npi_signal is not None
70
+ npi_members = self._npi_signal.member_list()
71
+ if self.composite_type == SignalCompositeType.ARRAY:
72
+ # Array members' NPI names already include the array base name at every
73
+ # level, e.g. parent "a" has members "a[0]", "a[1]", and "a[0]" further
74
+ # has members "a[0][0]", "a[0][1]", etc. The member name is therefore an
75
+ # extension of the parent name with no extra "." separator, so we must NOT
76
+ # prepend full_name (which would produce "tb.dut.a.a[0]"). Instead we use
77
+ # the parent scope path (full_name minus the signal's own local name) as
78
+ # the base, giving "tb.dut.a[0]", "tb.dut.a[0][1]", etc.
79
+ scope_path = self.full_name[: -(len(self.name) + 1)]
80
+ return [FsdbSignal.from_handle(m, f'{scope_path}.{m.name()}') for m in npi_members]
81
+ else:
82
+ return [FsdbSignal.from_handle(m, f'{self.full_name}.{m.name()}') for m in npi_members]
16
83
 
17
84
 
18
85
  class FsdbScope(Scope):
19
86
  def __init__(self, handle: NpiFsdbScope, parent_scope: FsdbScope | None, reader: FsdbReader):
20
87
  super().__init__(name=handle.name())
21
- self.handle = handle
88
+ self._npi_scope = handle
22
89
  self.parent_scope = parent_scope
23
90
  self.reader = reader
24
91
 
25
92
  @cached_property
26
93
  def signal_list(self) -> Sequence[Signal]:
27
94
  full_scope_name = self.full_name()
28
- signals = []
29
- for s in self.handle.signal_list():
30
- signal_path = f'{full_scope_name}.{s}'
31
- width = self.reader._get_signal_width(signal_path)
32
- rng = self.reader._get_signal_range(signal_path)
33
- signals.append(
34
- Signal(
35
- name=s,
36
- full_name=signal_path,
37
- width=width,
38
- range=rng,
39
- signed=False,
40
- )
41
- )
42
- return signals
95
+ return [
96
+ FsdbSignal.from_handle(npi_sig, f'{full_scope_name}.{npi_sig.name()}')
97
+ for npi_sig in self._npi_scope.signal_list()
98
+ ]
43
99
 
44
100
  @cached_property
45
101
  def child_scope_list(self) -> Sequence[Scope]:
46
- return [FsdbScope(c, self, self.reader) for c in self.handle.child_scope_list()]
47
-
48
- @cached_property
49
- def child_normal_scope_list(self) -> Sequence[Scope]:
50
- return [
51
- FsdbScope(c, self, self.reader)
52
- for c in self.handle.child_scope_list(include_signal_scope=False)
53
- ]
102
+ return [FsdbScope(c, self, self.reader) for c in self._npi_scope.child_scope_list()]
54
103
 
55
104
  @property
56
105
  def type(self) -> str:
57
106
  if not hasattr(self, '_type'):
58
- self._type = self.handle.type()
107
+ self._type = self._npi_scope.type()
59
108
  return self._type
60
109
 
61
110
  @property
62
111
  def def_name(self) -> str | None:
63
112
  if not hasattr(self, '_def_name'):
64
- self._def_name = self.handle.def_name()
113
+ self._def_name = self._npi_scope.def_name()
65
114
  return self._def_name
66
115
 
67
116
  def find_scope_by_module(self, module_name: str, depth: int = 0) -> list[Scope]:
@@ -70,11 +119,8 @@ class FsdbScope(Scope):
70
119
  return self._preloaded_module_scope[module_name]
71
120
 
72
121
  def preload_module_scope(self):
73
- if isinstance(self.handle, NpiFsdbSignalScope):
74
- return {}
75
-
76
122
  preloaded_module_scope = defaultdict(list)
77
- for c in self.child_normal_scope_list:
123
+ for c in self.child_scope_list:
78
124
  for module_name, module_scope_list in c.preload_module_scope().items():
79
125
  preloaded_module_scope[module_name].extend(module_scope_list)
80
126
 
@@ -123,9 +169,21 @@ class FsdbReader(Reader):
123
169
  signal_path = signal.full_name if isinstance(signal, Signal) else signal
124
170
  clock_path = clock.full_name if isinstance(clock, Signal) else clock
125
171
 
172
+ # Resolve NPI signal handles — reuse the handle if already available
173
+ npi_signal = (
174
+ signal._npi_signal
175
+ if isinstance(signal, FsdbSignal) and signal._npi_signal is not None
176
+ else self.file_handle.get_signal(signal_path)
177
+ )
178
+ npi_clock = (
179
+ clock._npi_signal
180
+ if isinstance(clock, FsdbSignal) and clock._npi_signal is not None
181
+ else self.file_handle.get_signal(clock_path)
182
+ )
183
+
126
184
  # Always load the full clock to compute absolute cycle numbers
127
185
  all_clock_changes = self.file_handle.load_value_change(
128
- clock_path,
186
+ npi_clock,
129
187
  begin_time=0,
130
188
  end_time=2**64 - 1,
131
189
  xz_value=0,
@@ -156,7 +214,7 @@ class FsdbReader(Reader):
156
214
  # Load signal within the requested window only (FSDB NPI provides the
157
215
  # correct initial value at begin_time even if the last change was earlier)
158
216
  signal_value_change = self.file_handle.load_value_change(
159
- signal_path,
217
+ npi_signal,
160
218
  begin_time=begin_time_actual,
161
219
  end_time=end_time_actual,
162
220
  xz_value=xz_value,
@@ -165,7 +223,7 @@ class FsdbReader(Reader):
165
223
  full_wave = self.value_change_to_waveform(
166
224
  signal_value_change,
167
225
  windowed_clock_changes,
168
- width=self.file_handle.get_signal_width(signal_path),
226
+ width=npi_signal.width(),
169
227
  signed=signed,
170
228
  sample_on_posedge=sample_on_posedge,
171
229
  signal=signal_path,
@@ -186,12 +244,6 @@ class FsdbReader(Reader):
186
244
  ]
187
245
  return self._top_scope_list
188
246
 
189
- def _get_signal_width(self, signal: str) -> int:
190
- return self.file_handle.get_signal_width(signal)
191
-
192
- def _get_signal_range(self, signal: str) -> tuple[int, int]:
193
- return self.file_handle.get_signal_range(signal)
194
-
195
247
  @property
196
248
  def begin_time(self) -> str:
197
249
  return self.file_handle.min_time()
@@ -75,9 +75,6 @@ class VcdReader(Reader):
75
75
  def end_time(self) -> int:
76
76
  return self.file_handle.endtime
77
77
 
78
- def _get_signal_width(self, signal: str) -> int:
79
- return int(self.file_handle[signal].size)
80
-
81
78
  def load_waveform(
82
79
  self,
83
80
  signal: Signal | str,
@@ -11,7 +11,7 @@ from .readers.pattern_parser import (
11
11
  PatternMap,
12
12
  split_by_range_expr,
13
13
  )
14
- from .signal import Signal
14
+ from .signal import Signal, SignalCompositeType
15
15
 
16
16
  T = TypeVar('T')
17
17
 
@@ -84,6 +84,12 @@ class Scope:
84
84
  raise NotImplementedError()
85
85
 
86
86
 
87
+ def _prepend_scope_name(value: Any, scope_name: str) -> Any:
88
+ if isinstance(value, Signal):
89
+ return dataclasses.replace(value, full_name=f'{scope_name}.{value.full_name}')
90
+ return value
91
+
92
+
87
93
  def _traverse_scope_tree(
88
94
  scope: Scope,
89
95
  descendant_scope_pattern_list: list[PatternMap],
@@ -111,11 +117,6 @@ def _traverse_scope_tree(
111
117
  dict mapping expansion key tuples to values produced by *leaf_fn*.
112
118
  """
113
119
 
114
- def prepend_scope_name(value: Any, scope_name: str) -> Any:
115
- if isinstance(value, Signal):
116
- return dataclasses.replace(value, full_name=f'{scope_name}.{value.full_name}')
117
- return value
118
-
119
120
  res: dict[tuple[Any, ...], T] = {}
120
121
  if len(descendant_scope_pattern_list) == 0:
121
122
  return res
@@ -144,11 +145,11 @@ def _traverse_scope_tree(
144
145
  for lk, lv in leaf_fn(scope, remaining).items():
145
146
  key = (scope.name,) + lk
146
147
  assert key not in res
147
- res[key] = prepend_scope_name(lv, scope.name)
148
+ res[key] = _prepend_scope_name(lv, scope.name)
148
149
 
149
150
  for child_scope in scope.child_scope_list:
150
151
  for ck, cv in _traverse_scope_tree(child_scope, remaining, leaf_fn).items():
151
- res[(scope.name,) + ck] = prepend_scope_name(cv, scope.name)
152
+ res[(scope.name,) + ck] = _prepend_scope_name(cv, scope.name)
152
153
  else:
153
154
  for child_scope in module_scopes:
154
155
  for ck, cv in _traverse_scope_tree(
@@ -159,7 +160,7 @@ def _traverse_scope_tree(
159
160
  raise ValueError('parent scope is None')
160
161
  parent_name = parent_scope.full_name(scope)
161
162
  key = (f'{parent_name}.{ck[0]}',) + ck[1:]
162
- res[key] = prepend_scope_name(cv, parent_name)
163
+ res[key] = _prepend_scope_name(cv, parent_name)
163
164
  else:
164
165
  # Exact or regex match against current scope name
165
166
  matched = False
@@ -181,14 +182,14 @@ def _traverse_scope_tree(
181
182
  key = new_k + lk
182
183
  if key in res:
183
184
  raise Exception(f'pattern {p} match more than one result')
184
- res[key] = prepend_scope_name(lv, scope.name)
185
+ res[key] = _prepend_scope_name(lv, scope.name)
185
186
 
186
187
  for child_scope in scope.child_scope_list:
187
188
  for ck, cv in _traverse_scope_tree(child_scope, remaining, leaf_fn).items():
188
189
  key = new_k + ck
189
190
  if key in res:
190
191
  raise Exception(f'pattern {p} match more than one result')
191
- res[key] = prepend_scope_name(cv, scope.name)
192
+ res[key] = _prepend_scope_name(cv, scope.name)
192
193
  return res
193
194
 
194
195
 
@@ -204,55 +205,94 @@ def match_signals(
204
205
  objects whose ``full_name`` is the complete hierarchical path.
205
206
  """
206
207
 
207
- def match_signals_in_scope(
208
- scope: Scope, signal_pattern_list: list[PatternMap]
208
+ def _match_signals_in_list(
209
+ signals: Sequence[Signal], pattern_list: list[PatternMap]
209
210
  ) -> dict[tuple[Any, ...], Signal]:
210
- def range_str_to_tuple(range_str: str) -> tuple[int, int] | None:
211
- if not range_str:
212
- return None
213
- last = re.search(r'\[(\d+)(?::(\d+))?\]$', range_str)
214
- if last is None:
215
- return None
216
- high = int(last.group(1))
217
- low = int(last.group(2)) if last.group(2) is not None else high
218
- return (high, low)
211
+ """Match *pattern_list* against *signals*, recursing into composite members.
212
+
213
+ The first element of *pattern_list* is matched against signal names in
214
+ *signals*. When more patterns remain and the matched signal is composite
215
+ (``signal.member_list is not None``), the function recurses into those
216
+ members with the remaining patterns, allowing patterns to address
217
+ struct/union members across multiple levels.
218
+
219
+ **Array signals** require special treatment because NPI reports each
220
+ element's name as an extension of the parent name with no ``"."``
221
+ separator. For example, signal ``a`` (ARRAY) has members ``a[0]``,
222
+ ``a[1]``, which in turn have members ``a[0][0]``, ``a[0][1]``, etc.
223
+ A user pattern like ``a[10][0]`` therefore cannot be resolved by a
224
+ single exact-name match at the top level. Instead, for ARRAY signals
225
+ we check whether the signal name is a string prefix of the pattern:
226
+ if yes, we recurse into members with the **same** pattern (not
227
+ advancing to the next element), letting each member self-select by the
228
+ same prefix rule until an exact match is found.
229
+ """
230
+ if not pattern_list:
231
+ return {}
232
+
233
+ def resolve_leaf(sig: Signal, sig_bare: str, range_suffix: str) -> Signal:
234
+ if range_suffix:
235
+ # Extract (high, low) from the innermost bracket of range_suffix,
236
+ # e.g. "[7:0]" → (7, 0), "[3]" → (3, 3).
237
+ m = re.search(r'\[(\d+)(?::(\d+))?\]$', range_suffix)
238
+ assert m is not None
239
+ h = int(m.group(1))
240
+ low = int(m.group(2)) if m.group(2) is not None else h
241
+ new_range: tuple[int, int] | None = (h, low)
242
+ else:
243
+ new_range = sig.range
244
+ return dataclasses.replace(
245
+ sig,
246
+ full_name=f'{sig_bare}{range_suffix}' if range_suffix else sig.name,
247
+ range=new_range,
248
+ )
219
249
 
220
250
  res: dict[tuple[Any, ...], Signal] = {}
221
- if len(signal_pattern_list) != 1:
222
- return res
223
- for signal in scope.signal_list:
224
- sig_bare, _ = split_by_range_expr(signal.name)
225
- for k, p in signal_pattern_list[0].items():
251
+ for sig in signals:
252
+ sig_bare, _ = split_by_range_expr(sig.name)
253
+ for k, p in pattern_list[0].items():
226
254
  if p[0] == '@':
227
255
  name_regex, range_suffix = split_by_range_expr(p[1:])
228
- if match := re.fullmatch(name_regex, signal.name):
256
+ if match := re.fullmatch(name_regex, sig.name):
229
257
  assert len(k) == 0
230
258
  key = (match.groups(),)
231
- if key in res:
232
- raise Exception(f'pattern {name_regex} match more than one signal')
233
- if range_suffix:
234
- new_local = f'{sig_bare}{range_suffix}'
235
- new_range = range_str_to_tuple(range_suffix)
236
- else:
237
- new_local = signal.name
238
- new_range = signal.range
239
- res[key] = dataclasses.replace(signal, full_name=new_local, range=new_range)
259
+ if len(pattern_list) == 1:
260
+ assert (
261
+ key not in res
262
+ ), f'pattern {name_regex} matches more than one signal'
263
+ res[key] = resolve_leaf(sig, sig_bare, range_suffix)
264
+ elif sig.member_list is not None:
265
+ for ck, cv in _match_signals_in_list(
266
+ sig.member_list, pattern_list[1:]
267
+ ).items():
268
+ res[key + ck] = _prepend_scope_name(cv, sig.name)
240
269
  else:
241
270
  p_bare, range_suffix = split_by_range_expr(p)
242
271
  if p_bare != sig_bare:
243
272
  continue
244
273
  key = k
245
- assert key not in res
246
- if range_suffix:
247
- new_local = f'{sig_bare}{range_suffix}'
248
- new_range = range_str_to_tuple(range_suffix)
249
- else:
250
- new_local = signal.name
251
- new_range = signal.range
252
- res[key] = dataclasses.replace(signal, full_name=new_local, range=new_range)
274
+ if sig.composite_type == SignalCompositeType.ARRAY and range_suffix:
275
+ if sig.member_list is not None:
276
+ for ck, cv in _match_signals_in_list(
277
+ sig.member_list, pattern_list
278
+ ).items():
279
+ res[key + ck] = cv
280
+ elif len(pattern_list) == 1:
281
+ assert key not in res
282
+ res[key] = resolve_leaf(sig, sig_bare, range_suffix)
283
+ elif sig.member_list is not None:
284
+ for ck, cv in _match_signals_in_list(
285
+ sig.member_list, pattern_list[1:]
286
+ ).items():
287
+ res[key + ck] = _prepend_scope_name(cv, sig.name)
253
288
  break
254
289
  return res
255
290
 
291
+ def match_signals_in_scope(
292
+ scope: Scope, signal_pattern_list: list[PatternMap]
293
+ ) -> dict[tuple[Any, ...], Signal]:
294
+ return _match_signals_in_list(scope.signal_list, signal_pattern_list)
295
+
256
296
  return _traverse_scope_tree(scope, descendant_scope_pattern_list, match_signals_in_scope)
257
297
 
258
298
 
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from functools import cached_property
6
+
7
+
8
+ class SignalCompositeType(Enum):
9
+ """Composite (non-leaf) signal type as reported by the waveform backend.
10
+
11
+ Not all backends support composite signals. When a backend does not
12
+ distinguish composite types the field is ``None``.
13
+ """
14
+
15
+ ARRAY = 'array'
16
+ STRUCT = 'struct'
17
+ UNION = 'union'
18
+ TAGGED_UNION = 'tagged_union'
19
+ RECORD = 'record'
20
+
21
+
22
+ @dataclass
23
+ class Signal:
24
+ """Metadata descriptor for a single hardware signal.
25
+
26
+ Stores the signal's local name, full hierarchical path, bit-width,
27
+ declared bit-range, and signedness. For composite signals (structs,
28
+ unions, arrays) the ``composite_type`` and ``member_list`` fields carry
29
+ the internal structure.
30
+
31
+ Attributes
32
+ ----------
33
+ name:
34
+ Local signal identifier as it appears within its parent scope,
35
+ matching the form used in the waveform file. May include a range
36
+ suffix when the file stores the signal with one, e.g. ``"data[7:0]"``
37
+ or ``"mem[3][7:0]"``. Scalar signals have no suffix, e.g. ``"clk"``.
38
+ Invariant: ``full_name == parent_scope_path + "." + name``.
39
+ full_name:
40
+ Complete hierarchical signal path, e.g. ``"tb.dut.data[7:0]"`` or
41
+ ``"tb.dut.mem[3][7:0]"``. Equal to ``parent_scope_path + "." + name``.
42
+ Pass this directly to :meth:`~wavekit.readers.base.Reader.load_waveform`.
43
+ width:
44
+ Bit-width of the signal, e.g. ``8`` for ``[7:0]``. ``None`` if not
45
+ yet resolved.
46
+ range:
47
+ The innermost (last) bit-range of the signal as a ``(high, low)``
48
+ integer tuple. For a plain vector ``data[7:0]`` this is ``(7, 0)``;
49
+ for a multi-dimensional signal ``mem[3][7:0]`` this is ``(7, 0)``
50
+ (the ``[3]`` dimension index is encoded in ``name``/``full_name`` only).
51
+ For a single-bit index ``[n]`` this is ``(n, n)``.
52
+ ``None`` if the signal is scalar, composite, or the format does not
53
+ expose range information.
54
+ signed:
55
+ Whether the signal value should be interpreted as a two's-complement
56
+ signed integer. Defaults to ``False``.
57
+ composite_type:
58
+ ``None`` for leaf (non-composite) signals. For composite signals
59
+ (struct, union, array, …) this holds the :class:`SignalCompositeType`
60
+ value describing the kind of composite. Not all backends populate
61
+ this field; backends that do not support composite introspection leave
62
+ it as ``None`` (e.g. VCD).
63
+ member_list:
64
+ ``None`` for leaf signals. For composite signals this is the list of
65
+ direct member :class:`Signal` objects, populated in the same order the
66
+ backend reports them. Always ``None`` when ``composite_type`` is
67
+ ``None``, and always a list (possibly empty) when ``composite_type``
68
+ is set. Lazily evaluated on first access; backend-specific subclasses
69
+ override the ``member_list`` cached property to provide the actual loading logic.
70
+ """
71
+
72
+ name: str
73
+ full_name: str
74
+ width: int | None
75
+ range: tuple[int, int] | None
76
+ signed: bool = False
77
+ composite_type: SignalCompositeType | None = None
78
+
79
+ @cached_property
80
+ def member_list(self) -> list[Signal] | None:
81
+ return None
82
+
83
+ def __str__(self) -> str:
84
+ return (
85
+ f"Signal(name='{self.name}', full_name='{self.full_name}', "
86
+ f'width={self.width}, signed={self.signed}, range={self.range})'
87
+ )
@@ -1,49 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
-
5
-
6
- @dataclass
7
- class Signal:
8
- """Metadata descriptor for a single hardware signal.
9
-
10
- Stores the signal's local name, full hierarchical path, bit-width,
11
- declared bit-range, and signedness.
12
-
13
- Attributes
14
- ----------
15
- name:
16
- Local signal identifier as it appears within its parent scope,
17
- matching the form used in the waveform file. May include a range
18
- suffix when the file stores the signal with one, e.g. ``"data[7:0]"``
19
- or ``"mem[3][7:0]"``. Scalar signals have no suffix, e.g. ``"clk"``.
20
- Invariant: ``full_name == parent_scope_path + "." + name``.
21
- full_name:
22
- Complete hierarchical signal path, e.g. ``"tb.dut.data[7:0]"`` or
23
- ``"tb.dut.mem[3][7:0]"``. Equal to ``parent_scope_path + "." + name``.
24
- Pass this directly to :meth:`~wavekit.readers.base.Reader.load_waveform`.
25
- width:
26
- Bit-width of the signal, e.g. ``8`` for ``[7:0]``. ``None`` if not
27
- yet resolved.
28
- range:
29
- The declared or user-requested bit-range of the signal as a
30
- ``(high, low)`` integer tuple, e.g. ``(7, 0)`` for ``[7:0]``.
31
- For single-bit selection ``[n]`` this is stored as ``(n, n)``.
32
- ``None`` if the signal is scalar or the format does not expose
33
- range information.
34
- signed:
35
- Whether the signal value should be interpreted as a two's-complement
36
- signed integer. Defaults to ``False``.
37
- """
38
-
39
- name: str
40
- full_name: str
41
- width: int | None
42
- range: tuple[int, int] | None
43
- signed: bool = False
44
-
45
- def __str__(self) -> str:
46
- return (
47
- f"Signal(name='{self.name}', full_name='{self.full_name}', "
48
- f'width={self.width}, signed={self.signed}, range={self.range})'
49
- )
File without changes
File without changes
File without changes
File without changes