rtl-buddy-view 0.2.1__py3-none-any.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.
- rtl_buddy_view/__init__.py +5 -0
- rtl_buddy_view/__main__.py +4 -0
- rtl_buddy_view/_cst_cache.py +43 -0
- rtl_buddy_view/_filelist.py +52 -0
- rtl_buddy_view/_offsets.py +23 -0
- rtl_buddy_view/_vcd_reader.py +356 -0
- rtl_buddy_view/_verible_install.py +264 -0
- rtl_buddy_view/_viewer_bundle/assets/index-CCzn_aMT.css +1 -0
- rtl_buddy_view/_viewer_bundle/assets/index-DBl-YxCO.js +7267 -0
- rtl_buddy_view/_viewer_bundle/index.html +20 -0
- rtl_buddy_view/annotations.py +431 -0
- rtl_buddy_view/axi_perf_annotations.py +419 -0
- rtl_buddy_view/cli.py +458 -0
- rtl_buddy_view/cst_cache.py +225 -0
- rtl_buddy_view/extractor.py +289 -0
- rtl_buddy_view/frontend/__init__.py +52 -0
- rtl_buddy_view/frontend/slang.py +23 -0
- rtl_buddy_view/frontend/verible.py +976 -0
- rtl_buddy_view/graph.py +157 -0
- rtl_buddy_view/offsets.py +48 -0
- rtl_buddy_view/overlays/__init__.py +361 -0
- rtl_buddy_view/overlays/axi_perf.py +59 -0
- rtl_buddy_view/overlays/clock.py +50 -0
- rtl_buddy_view/overlays/clock_tb.py +49 -0
- rtl_buddy_view/overlays/reset.py +41 -0
- rtl_buddy_view/overlays/wave.py +64 -0
- rtl_buddy_view/query.py +130 -0
- rtl_buddy_view/render/__init__.py +10 -0
- rtl_buddy_view/render/dot.py +1339 -0
- rtl_buddy_view/render/json_render.py +970 -0
- rtl_buddy_view/render/mermaid.py +268 -0
- rtl_buddy_view/render/tree.py +288 -0
- rtl_buddy_view/reset_annotations.py +397 -0
- rtl_buddy_view/tb_clock_map.py +409 -0
- rtl_buddy_view/viewer_bundle.py +34 -0
- rtl_buddy_view/wave_annotations.py +201 -0
- rtl_buddy_view-0.2.1.dist-info/METADATA +366 -0
- rtl_buddy_view-0.2.1.dist-info/RECORD +41 -0
- rtl_buddy_view-0.2.1.dist-info/WHEEL +4 -0
- rtl_buddy_view-0.2.1.dist-info/entry_points.txt +2 -0
- rtl_buddy_view-0.2.1.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Deprecated alias for :mod:`rtl_buddy_view.cst_cache`.
|
|
2
|
+
|
|
3
|
+
The module was promoted to public API in view#109 — see
|
|
4
|
+
``rtl_buddy_view.cst_cache``. This shim re-exports every public name
|
|
5
|
+
from the new location and emits a ``DeprecationWarning`` on import.
|
|
6
|
+
It will be removed in the next minor release.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import warnings as _warnings
|
|
12
|
+
|
|
13
|
+
from rtl_buddy_view.cst_cache import ( # noqa: F401 (re-exports)
|
|
14
|
+
_VERIBLE_VERSION_BY_BINARY,
|
|
15
|
+
_atomic_write_json,
|
|
16
|
+
_cache_path_for,
|
|
17
|
+
_query_version,
|
|
18
|
+
cache_root,
|
|
19
|
+
content_hash,
|
|
20
|
+
get_or_compute,
|
|
21
|
+
is_disabled,
|
|
22
|
+
verible_version,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"_VERIBLE_VERSION_BY_BINARY",
|
|
27
|
+
"_atomic_write_json",
|
|
28
|
+
"_cache_path_for",
|
|
29
|
+
"_query_version",
|
|
30
|
+
"cache_root",
|
|
31
|
+
"content_hash",
|
|
32
|
+
"get_or_compute",
|
|
33
|
+
"is_disabled",
|
|
34
|
+
"verible_version",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
_warnings.warn(
|
|
38
|
+
"rtl_buddy_view._cst_cache is deprecated; import from "
|
|
39
|
+
"rtl_buddy_view.cst_cache instead. The underscore-prefixed module "
|
|
40
|
+
"will be removed in the next minor release.",
|
|
41
|
+
DeprecationWarning,
|
|
42
|
+
stacklevel=2,
|
|
43
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Filelist parsing.
|
|
2
|
+
|
|
3
|
+
Phase 1 supports the trivial subset: one absolute-or-relative file
|
|
4
|
+
path per line, blank lines and ``#`` / ``//`` comments ignored.
|
|
5
|
+
``+incdir+`` and ``-y`` library directives, ``+define+`` macros, and
|
|
6
|
+
``-f`` recursive includes will land alongside the broader extractor —
|
|
7
|
+
they're listed here so future work has a clear target but raise
|
|
8
|
+
``FilelistError`` if encountered today rather than silently being
|
|
9
|
+
treated as filenames.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FilelistError(ValueError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_UNSUPPORTED_PREFIXES = ("+incdir+", "+define+", "-y", "-f")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_filelist(path: Path) -> list[Path]:
|
|
25
|
+
text = path.read_text()
|
|
26
|
+
files: list[Path] = []
|
|
27
|
+
base = path.parent
|
|
28
|
+
for lineno, raw in enumerate(text.splitlines(), start=1):
|
|
29
|
+
line = raw.split("#", 1)[0].split("//", 1)[0].strip()
|
|
30
|
+
if not line:
|
|
31
|
+
continue
|
|
32
|
+
if line.startswith(_UNSUPPORTED_PREFIXES):
|
|
33
|
+
raise FilelistError(
|
|
34
|
+
f"{path}:{lineno}: directive {line.split()[0]!r} is not "
|
|
35
|
+
f"supported in Phase 1; see issue #1 for the planned "
|
|
36
|
+
f"filelist surface."
|
|
37
|
+
)
|
|
38
|
+
candidate = (base / line).resolve()
|
|
39
|
+
if candidate.is_dir():
|
|
40
|
+
# A directory entry is an include dir, not a source file —
|
|
41
|
+
# e.g. a ``+incdir+`` path that an upstream generator (rb
|
|
42
|
+
# hier's filelist strip) reduced to a bare path. The Verible
|
|
43
|
+
# frontend parses each source standalone and never consumes
|
|
44
|
+
# include dirs, so skip it rather than letting ``read_text``
|
|
45
|
+
# blow up with IsADirectoryError downstream.
|
|
46
|
+
continue
|
|
47
|
+
if not candidate.exists():
|
|
48
|
+
raise FilelistError(f"{path}:{lineno}: file does not exist: {candidate}")
|
|
49
|
+
files.append(candidate)
|
|
50
|
+
if not files:
|
|
51
|
+
raise FilelistError(f"{path}: filelist is empty")
|
|
52
|
+
return files
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Deprecated alias for :mod:`rtl_buddy_view.offsets`.
|
|
2
|
+
|
|
3
|
+
The module was promoted to public API in view#109 — see
|
|
4
|
+
``rtl_buddy_view.offsets``. This shim re-exports :class:`OffsetIndex`
|
|
5
|
+
from the new location and emits a ``DeprecationWarning`` on import.
|
|
6
|
+
It will be removed in the next minor release.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import warnings as _warnings
|
|
12
|
+
|
|
13
|
+
from rtl_buddy_view.offsets import OffsetIndex # noqa: F401 (re-export)
|
|
14
|
+
|
|
15
|
+
__all__ = ["OffsetIndex"]
|
|
16
|
+
|
|
17
|
+
_warnings.warn(
|
|
18
|
+
"rtl_buddy_view._offsets is deprecated; import from "
|
|
19
|
+
"rtl_buddy_view.offsets instead. The underscore-prefixed module "
|
|
20
|
+
"will be removed in the next minor release.",
|
|
21
|
+
DeprecationWarning,
|
|
22
|
+
stacklevel=2,
|
|
23
|
+
)
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Pure-Python VCD reader for the Phase 8 wave snapshot overlay.
|
|
2
|
+
|
|
3
|
+
Reads a Value Change Dump file, walks to a target time T, and returns
|
|
4
|
+
the last-known value of every variable at-or-before T. Designed for
|
|
5
|
+
the offline-snapshot use case — single sample point, no time-series
|
|
6
|
+
output, no continuous streaming. The wave overlay
|
|
7
|
+
(:mod:`rtl_buddy_view.overlays.wave`) is the only consumer.
|
|
8
|
+
|
|
9
|
+
Why not pyvcd / pylibfst:
|
|
10
|
+
|
|
11
|
+
* pyvcd is read-write but its reader path is uncommon; bringing it
|
|
12
|
+
in just for one sample point would add a third-party dep and a
|
|
13
|
+
packaging concern (rtl-buddy-view stays at two runtime deps today:
|
|
14
|
+
typer + jsonschema).
|
|
15
|
+
* pylibfst requires a native libfst build, which we don't want to
|
|
16
|
+
inflict on a default install. FST support is a Phase-8 follow-up;
|
|
17
|
+
v1 ships VCD only.
|
|
18
|
+
|
|
19
|
+
The parser covers the subset of the VCD spec our fixtures + typical
|
|
20
|
+
simulator outputs (verilator, iverilog, modelsim) emit — header
|
|
21
|
+
sections, ``$timescale``, nested ``$scope module`` / ``$upscope``,
|
|
22
|
+
``$var <type> <width> <id> <name>``, ``$enddefinitions``, scalar
|
|
23
|
+
value changes (``0!``, ``1!``, ``x!``, ``z!``), and vector changes
|
|
24
|
+
(``b<bits> <id>``). Real-number changes (``r<float> <id>``) and
|
|
25
|
+
strings (``s<string> <id>``) parse but render as a passthrough
|
|
26
|
+
literal. Arrays / structs aren't modelled; producers serialise them
|
|
27
|
+
to per-bit signals.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import re
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class VcdParseError(ValueError):
|
|
38
|
+
"""Raised on malformed input.
|
|
39
|
+
|
|
40
|
+
The CLI catches this and surfaces ``overlay wave: <message>`` to
|
|
41
|
+
the user — distinct from a generic ``ValueError`` so the wave
|
|
42
|
+
overlay's loader can attach extra context without colliding with
|
|
43
|
+
other overlay errors.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
#: Power-of-ten multipliers from each unit to fs. Anything outside this
|
|
48
|
+
#: set in a VCD ``$timescale`` is rejected — femtosecond resolution is
|
|
49
|
+
#: the canonical hub-side encoding so a sub-fs spec would lose precision
|
|
50
|
+
#: silently downstream.
|
|
51
|
+
_UNIT_TO_FS: dict[str, int] = {
|
|
52
|
+
"fs": 1,
|
|
53
|
+
"ps": 1_000,
|
|
54
|
+
"ns": 1_000_000,
|
|
55
|
+
"us": 1_000_000_000,
|
|
56
|
+
"ms": 1_000_000_000_000,
|
|
57
|
+
"s": 1_000_000_000_000_000,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Time spec for CLI / overlay use. ``end`` is the sentinel for
|
|
62
|
+
# "sample at the last recorded time in the file".
|
|
63
|
+
_TIMESPEC_RE = re.compile(
|
|
64
|
+
r"^\s*(?P<num>\d+(?:\.\d+)?)\s*(?P<unit>fs|ps|ns|us|ms|s)\s*$",
|
|
65
|
+
re.IGNORECASE,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_time_spec(spec: str) -> int | None:
|
|
70
|
+
"""Convert ``"12500ns"`` / ``"100us"`` / ``"end"`` to fs.
|
|
71
|
+
|
|
72
|
+
Returns the time in femtoseconds, or ``None`` for the ``end``
|
|
73
|
+
sentinel (the reader expands that to the last recorded time at
|
|
74
|
+
sample time, when it actually knows the file's tail).
|
|
75
|
+
|
|
76
|
+
Raises :class:`VcdParseError` on garbage input.
|
|
77
|
+
"""
|
|
78
|
+
if spec.strip().lower() == "end":
|
|
79
|
+
return None
|
|
80
|
+
m = _TIMESPEC_RE.match(spec)
|
|
81
|
+
if m is None:
|
|
82
|
+
raise VcdParseError(
|
|
83
|
+
f"unrecognised time spec {spec!r}; expected NUMBER+UNIT "
|
|
84
|
+
f"(e.g. 12500ns, 1.5us) or the literal 'end'."
|
|
85
|
+
)
|
|
86
|
+
num = float(m.group("num"))
|
|
87
|
+
unit = m.group("unit").lower()
|
|
88
|
+
return int(round(num * _UNIT_TO_FS[unit]))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class VcdSnapshot:
|
|
93
|
+
"""Result of sampling a VCD at a specific time.
|
|
94
|
+
|
|
95
|
+
* ``t_fs`` — the time the sample was taken at, in fs. May not equal
|
|
96
|
+
the user's requested time exactly: the parser walks change records
|
|
97
|
+
and stops when the next ``#T`` would exceed the target, so the
|
|
98
|
+
returned ``t_fs`` is the highest in-range change point. (When the
|
|
99
|
+
user asked for ``end``, this is the last record in the file.)
|
|
100
|
+
* ``signals`` — ``hier_path → SV-literal value``. Values use the
|
|
101
|
+
surfer / handle_bits convention (binary literal characters,
|
|
102
|
+
including ``x`` / ``z``) so the renderer can format consistently
|
|
103
|
+
with the live ``wave_values_changed`` path.
|
|
104
|
+
* ``timescale_fs`` — the file's tick→fs multiplier. Useful for
|
|
105
|
+
tests + error messages; not consumed by the overlay.
|
|
106
|
+
* ``declared`` — every variable seen in a ``$var`` declaration,
|
|
107
|
+
including ones with no transitions before ``t_fs``. The overlay
|
|
108
|
+
paints ``??`` (or skips) for these.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
t_fs: int
|
|
112
|
+
signals: dict[str, str]
|
|
113
|
+
timescale_fs: int = 1
|
|
114
|
+
declared: set[str] = field(default_factory=set)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def sample_vcd(path: Path, target_fs: int | None) -> VcdSnapshot:
|
|
118
|
+
"""Read ``path`` and return the values of every variable at
|
|
119
|
+
``target_fs`` (in femtoseconds). When ``target_fs`` is ``None``,
|
|
120
|
+
sample at the file's last recorded time.
|
|
121
|
+
|
|
122
|
+
Raises :class:`VcdParseError` on malformed input. Files with no
|
|
123
|
+
``$var`` declarations raise rather than returning an empty
|
|
124
|
+
snapshot — silent emptiness almost always indicates the user
|
|
125
|
+
pointed at the wrong file.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
with path.open("r", encoding="utf-8", errors="replace") as fh:
|
|
129
|
+
return _parse(fh.read(), target_fs)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _parse(text: str, target_fs: int | None) -> VcdSnapshot:
|
|
133
|
+
# Two-phase parser. Phase 1 walks $-keyword blocks for the header
|
|
134
|
+
# (timescale, $scope, $var, …) until ``$enddefinitions $end``.
|
|
135
|
+
# Phase 2 walks the body line-by-line for ``#T`` timestamps and
|
|
136
|
+
# value-change records. Splitting on $end inside the header tokens
|
|
137
|
+
# lets us collapse multi-line blocks ($comment, $version, …) into
|
|
138
|
+
# a single bracketed unit.
|
|
139
|
+
timescale_fs = 1
|
|
140
|
+
# VCD's id-code is a (1:N) reference: a single id can be declared
|
|
141
|
+
# in multiple $var lines when the same wire is exposed under
|
|
142
|
+
# several hierarchy paths (e.g. tb.dut.clk + tb.dut.u_ff.clk both
|
|
143
|
+
# bound to the same testbench-side wire). Aliasing is part of the
|
|
144
|
+
# spec, so we track every path each id resolves to.
|
|
145
|
+
id_to_paths: dict[str, list[str]] = {}
|
|
146
|
+
id_to_width: dict[str, int] = {}
|
|
147
|
+
declared: set[str] = set()
|
|
148
|
+
scope_stack: list[str] = []
|
|
149
|
+
|
|
150
|
+
# Split into tokens for the header pass, then locate the boundary.
|
|
151
|
+
# The body starts right after the ``$end`` that closes
|
|
152
|
+
# ``$enddefinitions``.
|
|
153
|
+
end_def_idx = text.find("$enddefinitions")
|
|
154
|
+
if end_def_idx < 0:
|
|
155
|
+
raise VcdParseError("no $enddefinitions block found; not a VCD file?")
|
|
156
|
+
# Step past ``$enddefinitions`` itself before scanning for the
|
|
157
|
+
# closing ``$end`` — ``$enddefinitions`` is a prefix of ``$end``
|
|
158
|
+
# so a naive ``find('$end', end_def_idx)`` lands on the keyword
|
|
159
|
+
# itself and the body / header split collapses.
|
|
160
|
+
after_end_def = text.find("$end", end_def_idx + len("$enddefinitions"))
|
|
161
|
+
if after_end_def < 0:
|
|
162
|
+
raise VcdParseError("$enddefinitions present but not closed by $end.")
|
|
163
|
+
header_text = text[: after_end_def + len("$end")]
|
|
164
|
+
body_text = text[after_end_def + len("$end") :]
|
|
165
|
+
|
|
166
|
+
header_tokens = header_text.split()
|
|
167
|
+
i = 0
|
|
168
|
+
n = len(header_tokens)
|
|
169
|
+
while i < n:
|
|
170
|
+
tok = header_tokens[i]
|
|
171
|
+
if tok == "$timescale":
|
|
172
|
+
block, j = _block_to_end(header_tokens, i + 1)
|
|
173
|
+
if not block:
|
|
174
|
+
raise VcdParseError("empty $timescale block.")
|
|
175
|
+
num, unit = _parse_timescale_block(block)
|
|
176
|
+
timescale_fs = num * _UNIT_TO_FS[unit]
|
|
177
|
+
i = j
|
|
178
|
+
elif tok == "$scope":
|
|
179
|
+
block, j = _block_to_end(header_tokens, i + 1)
|
|
180
|
+
# ``$scope module <name> $end`` — the last word is the name.
|
|
181
|
+
if not block:
|
|
182
|
+
raise VcdParseError("$scope block missing name.")
|
|
183
|
+
scope_stack.append(block[-1])
|
|
184
|
+
i = j
|
|
185
|
+
elif tok == "$upscope":
|
|
186
|
+
_, j = _block_to_end(header_tokens, i + 1)
|
|
187
|
+
if scope_stack:
|
|
188
|
+
scope_stack.pop()
|
|
189
|
+
i = j
|
|
190
|
+
elif tok == "$var":
|
|
191
|
+
block, j = _block_to_end(header_tokens, i + 1)
|
|
192
|
+
# ``$var <type> <width> <id> <name> [<slice>] $end``
|
|
193
|
+
if len(block) < 4:
|
|
194
|
+
raise VcdParseError(f"$var with too few fields: {block!r}")
|
|
195
|
+
try:
|
|
196
|
+
width = int(block[1])
|
|
197
|
+
except ValueError as exc:
|
|
198
|
+
raise VcdParseError(f"non-integer width in $var: {block!r}") from exc
|
|
199
|
+
vcd_id = block[2]
|
|
200
|
+
name = block[3]
|
|
201
|
+
hier_path = ".".join(scope_stack + [name])
|
|
202
|
+
id_to_paths.setdefault(vcd_id, []).append(hier_path)
|
|
203
|
+
# Width should be consistent across aliased paths; the
|
|
204
|
+
# spec doesn't allow conflicting widths on the same id.
|
|
205
|
+
# We trust the first declaration.
|
|
206
|
+
id_to_width.setdefault(vcd_id, width)
|
|
207
|
+
declared.add(hier_path)
|
|
208
|
+
i = j
|
|
209
|
+
elif tok == "$enddefinitions":
|
|
210
|
+
_, j = _block_to_end(header_tokens, i + 1)
|
|
211
|
+
i = j
|
|
212
|
+
# End of header; body comes from ``body_text``.
|
|
213
|
+
break
|
|
214
|
+
elif tok.startswith("$"):
|
|
215
|
+
# $comment / $date / $version / inline $dumpvars in header,
|
|
216
|
+
# anything else we don't model — drain to matching $end.
|
|
217
|
+
_, j = _block_to_end(header_tokens, i + 1)
|
|
218
|
+
i = j
|
|
219
|
+
else:
|
|
220
|
+
# Bare token in the header (whitespace, stray identifier
|
|
221
|
+
# from a malformed file). Skip.
|
|
222
|
+
i += 1
|
|
223
|
+
|
|
224
|
+
if not declared:
|
|
225
|
+
raise VcdParseError(
|
|
226
|
+
"no $var declarations found before $enddefinitions; is this "
|
|
227
|
+
"really a VCD file?"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# ----- body pass --------------------------------------------------
|
|
231
|
+
#
|
|
232
|
+
# We track the current value per id and the latest fully-applied
|
|
233
|
+
# timestamp. A line beginning with ``#`` is a timestamp marker; a
|
|
234
|
+
# change record applies on top of the prior timestamp. We stop at
|
|
235
|
+
# the first ``#T`` whose value exceeds the target.
|
|
236
|
+
current: dict[str, str] = {}
|
|
237
|
+
last_emitted_fs = 0
|
|
238
|
+
|
|
239
|
+
for raw in body_text.splitlines():
|
|
240
|
+
line = raw.strip()
|
|
241
|
+
if not line:
|
|
242
|
+
continue
|
|
243
|
+
if line.startswith("$"):
|
|
244
|
+
# $dumpvars / $dumpall / $dumpon / $dumpoff blocks wrap a
|
|
245
|
+
# batch of value changes. We process the inner records as
|
|
246
|
+
# usual on subsequent iterations; the $-keywords + their
|
|
247
|
+
# trailing $end are no-ops at this layer.
|
|
248
|
+
continue
|
|
249
|
+
if line[0] == "#":
|
|
250
|
+
try:
|
|
251
|
+
tick = int(line[1:])
|
|
252
|
+
except ValueError as exc:
|
|
253
|
+
raise VcdParseError(f"non-integer timestamp on line {line!r}") from exc
|
|
254
|
+
new_t_fs = tick * timescale_fs
|
|
255
|
+
if target_fs is not None and new_t_fs > target_fs:
|
|
256
|
+
break
|
|
257
|
+
last_emitted_fs = new_t_fs
|
|
258
|
+
continue
|
|
259
|
+
_apply_change(line, id_to_paths, id_to_width, current)
|
|
260
|
+
|
|
261
|
+
if target_fs is None:
|
|
262
|
+
t_fs = last_emitted_fs
|
|
263
|
+
else:
|
|
264
|
+
t_fs = min(target_fs, last_emitted_fs)
|
|
265
|
+
|
|
266
|
+
# Expand id-keyed `current` to all aliased hierarchy paths.
|
|
267
|
+
signals: dict[str, str] = {}
|
|
268
|
+
for ident, value in current.items():
|
|
269
|
+
for path in id_to_paths.get(ident, ()):
|
|
270
|
+
signals[path] = value
|
|
271
|
+
|
|
272
|
+
return VcdSnapshot(
|
|
273
|
+
t_fs=t_fs,
|
|
274
|
+
signals=signals,
|
|
275
|
+
timescale_fs=timescale_fs,
|
|
276
|
+
declared=declared,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _apply_change(
|
|
281
|
+
line: str,
|
|
282
|
+
id_to_paths: dict[str, list[str]],
|
|
283
|
+
id_to_width: dict[str, int],
|
|
284
|
+
current: dict[str, str],
|
|
285
|
+
) -> None:
|
|
286
|
+
ch0 = line[0]
|
|
287
|
+
if ch0 in "01xzXZuU-lLhHwW":
|
|
288
|
+
value = ch0.lower()
|
|
289
|
+
ident = line[1:]
|
|
290
|
+
if ident in id_to_paths:
|
|
291
|
+
current[ident] = value
|
|
292
|
+
return
|
|
293
|
+
if ch0 in "bB":
|
|
294
|
+
sp = line.find(" ")
|
|
295
|
+
if sp < 0:
|
|
296
|
+
return
|
|
297
|
+
bits = line[1:sp]
|
|
298
|
+
ident = line[sp + 1 :]
|
|
299
|
+
if ident in id_to_paths:
|
|
300
|
+
w = id_to_width[ident]
|
|
301
|
+
# VCD permits truncated leading zeros; pad to declared
|
|
302
|
+
# width so the consumer can render fixed-width hex without
|
|
303
|
+
# re-deriving the bit count.
|
|
304
|
+
if len(bits) < w:
|
|
305
|
+
pad = bits[0] if (bits and bits[0] in "xzXZ") else "0"
|
|
306
|
+
bits = bits.rjust(w, pad)
|
|
307
|
+
elif len(bits) > w:
|
|
308
|
+
bits = bits[-w:]
|
|
309
|
+
current[ident] = bits.lower()
|
|
310
|
+
return
|
|
311
|
+
if ch0 in "rR":
|
|
312
|
+
sp = line.find(" ")
|
|
313
|
+
if sp < 0:
|
|
314
|
+
return
|
|
315
|
+
real_lit = line[1:sp]
|
|
316
|
+
ident = line[sp + 1 :]
|
|
317
|
+
if ident in id_to_paths:
|
|
318
|
+
current[ident] = f"r{real_lit}"
|
|
319
|
+
return
|
|
320
|
+
if ch0 in "sS":
|
|
321
|
+
sp = line.find(" ")
|
|
322
|
+
if sp < 0:
|
|
323
|
+
return
|
|
324
|
+
str_lit = line[1:sp]
|
|
325
|
+
ident = line[sp + 1 :]
|
|
326
|
+
if ident in id_to_paths:
|
|
327
|
+
current[ident] = f"s{str_lit}"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _block_to_end(tokens: list[str], start: int) -> tuple[list[str], int]:
|
|
331
|
+
"""Read tokens from ``start`` until ``$end``; return ``(words, idx_after)``.
|
|
332
|
+
|
|
333
|
+
``idx_after`` points past the matched ``$end`` so the caller can
|
|
334
|
+
advance ``i = idx_after`` and continue.
|
|
335
|
+
"""
|
|
336
|
+
out: list[str] = []
|
|
337
|
+
j = start
|
|
338
|
+
while j < len(tokens):
|
|
339
|
+
if tokens[j] == "$end":
|
|
340
|
+
return out, j + 1
|
|
341
|
+
out.append(tokens[j])
|
|
342
|
+
j += 1
|
|
343
|
+
raise VcdParseError("reached EOF inside a $-block (missing $end).")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _parse_timescale_block(words: list[str]) -> tuple[int, str]:
|
|
347
|
+
"""Pull ``(num, unit)`` from a ``$timescale`` block's words.
|
|
348
|
+
|
|
349
|
+
Tolerates the unit being a separate token (``1 ns``) or fused
|
|
350
|
+
(``1ns``).
|
|
351
|
+
"""
|
|
352
|
+
blob = "".join(words)
|
|
353
|
+
m = _TIMESPEC_RE.match(blob)
|
|
354
|
+
if m is None:
|
|
355
|
+
raise VcdParseError(f"unrecognised $timescale: {blob!r}")
|
|
356
|
+
return int(round(float(m.group("num")))), m.group("unit").lower()
|