dau-sim 0.1.0__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.
Files changed (77) hide show
  1. dau_sim/__init__.py +9 -0
  2. dau_sim/adapters/__init__.py +6 -0
  3. dau_sim/adapters/selectors.py +127 -0
  4. dau_sim/adapters/vcd.py +375 -0
  5. dau_sim/api.py +94 -0
  6. dau_sim/backends/__init__.py +1 -0
  7. dau_sim/backends/cocotb_backend.py +955 -0
  8. dau_sim/benchmarks/__init__.py +1 -0
  9. dau_sim/benchmarks/bench_compile_partitioning.py +10 -0
  10. dau_sim/benchmarks/bench_cross_simulators.py +216 -0
  11. dau_sim/benchmarks/bench_selective_settle.py +11 -0
  12. dau_sim/benchmarks/compile_partitioning.py +43 -0
  13. dau_sim/benchmarks/results/Darwin-CPython-3.12-64bit/0001_cross-runtime-500k.json +195 -0
  14. dau_sim/benchmarks/results/Darwin-CPython-3.12-64bit/0002_cross-runtime-500k.json +195 -0
  15. dau_sim/benchmarks/results/cross-runtime-500k.json +223 -0
  16. dau_sim/benchmarks/selective_settle.py +60 -0
  17. dau_sim/cli.py +123 -0
  18. dau_sim/compiler/__init__.py +2 -0
  19. dau_sim/compiler/codegen.py +841 -0
  20. dau_sim/compiler/compile.py +1660 -0
  21. dau_sim/compiler/depanalysis.py +340 -0
  22. dau_sim/compiler/eval.py +194 -0
  23. dau_sim/compiler/eval4.py +282 -0
  24. dau_sim/compiler/flatten.py +200 -0
  25. dau_sim/compiler/resolve.py +129 -0
  26. dau_sim/compiler/rewrite.py +86 -0
  27. dau_sim/extension/cdn/index.js +1 -0
  28. dau_sim/extension/cdn/index.js.map +7 -0
  29. dau_sim/extension/css/index.css +0 -0
  30. dau_sim/extension/esm/index.js +1 -0
  31. dau_sim/extension/esm/index.js.map +7 -0
  32. dau_sim/extension/html/index.html +13 -0
  33. dau_sim/extension/index.html +13 -0
  34. dau_sim/frontends/__init__.py +15 -0
  35. dau_sim/frontends/amaranth_frontend.py +614 -0
  36. dau_sim/frontends/pyslang_frontend.py +555 -0
  37. dau_sim/integrations/__init__.py +12 -0
  38. dau_sim/integrations/verilator.py +90 -0
  39. dau_sim/integrations/verilator_profiles.py +50 -0
  40. dau_sim/ir/__init__.py +4 -0
  41. dau_sim/ir/expr.py +153 -0
  42. dau_sim/ir/module.py +172 -0
  43. dau_sim/ir/printer.py +183 -0
  44. dau_sim/ir/stmt.py +105 -0
  45. dau_sim/ir/types.py +150 -0
  46. dau_sim/ir/validate.py +166 -0
  47. dau_sim/perf.py +141 -0
  48. dau_sim/runtime/__init__.py +1 -0
  49. dau_sim/testbench.py +336 -0
  50. dau_sim/tests/cocotb_benches/__init__.py +1 -0
  51. dau_sim/tests/cocotb_benches/ready_valid_sum_tb.py +59 -0
  52. dau_sim/tests/sv/ready_valid_sum.sv +39 -0
  53. dau_sim/tests/sv/ready_valid_sum_tb.sv +102 -0
  54. dau_sim/tests/test_api.py +53 -0
  55. dau_sim/tests/test_cli.py +70 -0
  56. dau_sim/tests/test_cocotb_backend.py +945 -0
  57. dau_sim/tests/test_cocotb_examples.py +739 -0
  58. dau_sim/tests/test_compiler.py +608 -0
  59. dau_sim/tests/test_eval.py +365 -0
  60. dau_sim/tests/test_frontend_amaranth.py +1384 -0
  61. dau_sim/tests/test_frontend_sv.py +691 -0
  62. dau_sim/tests/test_ir.py +327 -0
  63. dau_sim/tests/test_nonsynthesizable.py +693 -0
  64. dau_sim/tests/test_perf.py +100 -0
  65. dau_sim/tests/test_ready_valid_sum_cocotb.py +29 -0
  66. dau_sim/tests/test_selectors.py +188 -0
  67. dau_sim/tests/test_sim_comb.py +570 -0
  68. dau_sim/tests/test_sim_seq.py +657 -0
  69. dau_sim/tests/test_testbench.py +781 -0
  70. dau_sim/tests/test_vcd.py +500 -0
  71. dau_sim/tests/test_verilator_integration.py +29 -0
  72. dau_sim/tests/test_verilator_profiles.py +50 -0
  73. dau_sim-0.1.0.dist-info/METADATA +123 -0
  74. dau_sim-0.1.0.dist-info/RECORD +77 -0
  75. dau_sim-0.1.0.dist-info/WHEEL +4 -0
  76. dau_sim-0.1.0.dist-info/entry_points.txt +2 -0
  77. dau_sim-0.1.0.dist-info/licenses/LICENSE +201 -0
dau_sim/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from dau_sim.api import SimulationResult, Simulator
4
+
5
+ __all__ = (
6
+ "SimulationResult",
7
+ "Simulator",
8
+ "__version__",
9
+ )
@@ -0,0 +1,6 @@
1
+ """CSP adapters for I/O (VCD output, stimulus injection, etc.)."""
2
+
3
+ from dau_sim.adapters.selectors import match_signals, select_signals
4
+ from dau_sim.adapters.vcd import traces_to_vcd, write_vcd
5
+
6
+ __all__ = ["match_signals", "select_signals", "traces_to_vcd", "write_vcd"]
@@ -0,0 +1,127 @@
1
+ """Signal selectors: glob/regex patterns for filtering simulation traces.
2
+
3
+ Use signal selectors to choose which signals to include in VCD output
4
+ or testbench observation, avoiding the overhead of tracing everything
5
+ in large designs.
6
+
7
+ Usage::
8
+
9
+ from dau_sim.adapters.selectors import select_signals
10
+
11
+ # Glob patterns
12
+ filtered = select_signals(traces, include=["count", "en"])
13
+ filtered = select_signals(traces, include=["*clk*"])
14
+ filtered = select_signals(traces, exclude=["rst", "*internal*"])
15
+
16
+ # Regex patterns
17
+ filtered = select_signals(traces, include=[r"data_\\d+"], regex=True)
18
+
19
+ # With CompiledModule
20
+ traces = cm.run(cycles=100, inputs={"en": 1})
21
+ cm.write_vcd("out.vcd", traces, signals=["count", "en"])
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import fnmatch
27
+ import re
28
+
29
+ __all__ = ["select_signals", "match_signals"]
30
+
31
+
32
+ def match_signals(
33
+ names: list[str] | set[str],
34
+ *,
35
+ include: list[str] | None = None,
36
+ exclude: list[str] | None = None,
37
+ regex: bool = False,
38
+ ) -> list[str]:
39
+ """Return signal names matching the include/exclude filters.
40
+
41
+ Parameters
42
+ ----------
43
+ names
44
+ All available signal names.
45
+ include
46
+ Patterns to include. If ``None``, all names are included.
47
+ exclude
48
+ Patterns to exclude. Applied after include filtering.
49
+ regex
50
+ If ``True``, patterns are Python regexes.
51
+ If ``False`` (default), patterns are Unix glob patterns.
52
+
53
+ Returns
54
+ -------
55
+ list[str]
56
+ Matching signal names, in the order they appear in *names*.
57
+ """
58
+ result = list(names)
59
+
60
+ if include is not None:
61
+ result = _apply_patterns(result, include, regex, mode="include")
62
+
63
+ if exclude is not None:
64
+ excluded = set(_apply_patterns(result, exclude, regex, mode="include"))
65
+ result = [n for n in result if n not in excluded]
66
+
67
+ return result
68
+
69
+
70
+ def select_signals(
71
+ traces: dict[str, list],
72
+ *,
73
+ include: list[str] | None = None,
74
+ exclude: list[str] | None = None,
75
+ regex: bool = False,
76
+ ) -> dict[str, list]:
77
+ """Filter a traces dict to only include matching signals.
78
+
79
+ Parameters
80
+ ----------
81
+ traces
82
+ Signal name → trace list mapping (from ``CompiledModule.run()``).
83
+ include
84
+ Glob or regex patterns for signals to keep.
85
+ exclude
86
+ Glob or regex patterns for signals to remove.
87
+ regex
88
+ Interpret patterns as regex (default: glob).
89
+
90
+ Returns
91
+ -------
92
+ dict[str, list]
93
+ Filtered traces dict.
94
+ """
95
+ matched = match_signals(
96
+ list(traces.keys()),
97
+ include=include,
98
+ exclude=exclude,
99
+ regex=regex,
100
+ )
101
+ return {name: traces[name] for name in matched}
102
+
103
+
104
+ def _apply_patterns(
105
+ names: list[str],
106
+ patterns: list[str],
107
+ regex: bool,
108
+ mode: str,
109
+ ) -> list[str]:
110
+ """Return names that match any of the given patterns."""
111
+ matched = []
112
+ seen = set()
113
+
114
+ for pattern in patterns:
115
+ if regex:
116
+ compiled = re.compile(pattern)
117
+ for name in names:
118
+ if name not in seen and compiled.search(name):
119
+ matched.append(name)
120
+ seen.add(name)
121
+ else:
122
+ for name in names:
123
+ if name not in seen and fnmatch.fnmatch(name, pattern):
124
+ matched.append(name)
125
+ seen.add(name)
126
+
127
+ return matched
@@ -0,0 +1,375 @@
1
+ """VCD (Value Change Dump) writer for dau-sim simulation traces.
2
+
3
+ Produces IEEE 1364-2001 compliant VCD files from the simulation trace
4
+ format (``dict[str, list[tuple[datetime, int]]]``) produced by
5
+ ``CompiledModule.run()``.
6
+
7
+ Usage::
8
+
9
+ from dau_sim.adapters.vcd import write_vcd, traces_to_vcd
10
+
11
+ # From a CompiledModule run:
12
+ traces = compiled.run(cycles=100, inputs={"d": 1})
13
+ write_vcd("out.vcd", traces, module=compiled.module)
14
+
15
+ # Or get VCD as a string:
16
+ vcd_str = traces_to_vcd(traces, module=compiled.module)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import io
22
+ import string
23
+ from datetime import datetime
24
+ from pathlib import Path
25
+ from typing import TextIO
26
+
27
+ from dau_sim.ir.module import Module
28
+ from dau_sim.ir.types import EdgePolarity, Shape
29
+
30
+ _ID_CHARS = string.printable[:-5] # printable minus whitespace
31
+
32
+ __all__ = (
33
+ "traces_to_vcd",
34
+ "write_vcd",
35
+ )
36
+
37
+
38
+ def _make_id(index: int) -> str:
39
+ """Generate a short VCD identifier code from an integer index.
40
+
41
+ VCD identifiers are one or more printable ASCII characters (33–126).
42
+ """
43
+ if index < 0:
44
+ raise ValueError("index must be non-negative")
45
+ result = []
46
+ base = len(_ID_CHARS)
47
+ while True:
48
+ result.append(_ID_CHARS[index % base])
49
+ index = index // base
50
+ if index == 0:
51
+ break
52
+ index -= 1 # shift so 0→first char, not 1→first char on next digit
53
+ return "".join(result)
54
+
55
+
56
+ def _datetime_to_timescale_ticks(
57
+ timestamps: list[datetime],
58
+ timescale_ns: int = 1,
59
+ ) -> list[int]:
60
+ """Convert datetime timestamps to integer VCD time ticks.
61
+
62
+ All timestamps are relative to the first timestamp in the list.
63
+ """
64
+ if not timestamps:
65
+ return []
66
+ base = timestamps[0]
67
+ ticks = []
68
+ for ts in timestamps:
69
+ delta = ts - base
70
+ ns = int(delta.total_seconds() * 1_000_000_000)
71
+ ticks.append(ns // timescale_ns)
72
+ return ticks
73
+
74
+
75
+ def _format_value(value: int, width: int) -> str:
76
+ """Format an integer value as a VCD binary literal."""
77
+ if width == 1:
78
+ return str(value & 1)
79
+ # Multi-bit: binary string, MSB first, padded to width
80
+ if value < 0:
81
+ # Two's complement representation for signed values
82
+ value = value & ((1 << width) - 1)
83
+ bits = bin(value)[2:] # strip '0b'
84
+ return "b" + bits.zfill(width)
85
+
86
+
87
+ def _write_header(
88
+ out: TextIO,
89
+ module_name: str,
90
+ signals: dict[str, tuple[str, int, str]],
91
+ timescale: str = "1ns",
92
+ date: str | None = None,
93
+ ) -> None:
94
+ """Write the VCD header section.
95
+
96
+ Parameters
97
+ ----------
98
+ out : TextIO
99
+ Output stream.
100
+ module_name : str
101
+ Top-level scope name.
102
+ signals : dict
103
+ Maps signal name → (vcd_id, width, var_type).
104
+ var_type is "wire", "reg", etc.
105
+ timescale : str
106
+ VCD timescale string (e.g. "1ns", "1ps").
107
+ date : str | None
108
+ Date string for $date section.
109
+ """
110
+ if date:
111
+ out.write(f"$date\n {date}\n$end\n")
112
+ out.write("$version dau-sim 0.1.0 $end\n")
113
+ out.write(f"$timescale {timescale} $end\n")
114
+
115
+ # Scope
116
+ out.write(f"$scope module {module_name} $end\n")
117
+ for sig_name, (vcd_id, width, var_type) in signals.items():
118
+ out.write(f"$var {var_type} {width} {vcd_id} {sig_name} $end\n")
119
+ out.write("$upscope $end\n")
120
+
121
+ out.write("$enddefinitions $end\n")
122
+
123
+
124
+ def _write_initial_values(
125
+ out: TextIO,
126
+ signals: dict[str, tuple[str, int, str]],
127
+ initial_values: dict[str, int],
128
+ ) -> None:
129
+ """Write the $dumpvars section with initial values."""
130
+ out.write("$dumpvars\n")
131
+ for sig_name, (vcd_id, width, _) in signals.items():
132
+ val = initial_values.get(sig_name, 0)
133
+ formatted = _format_value(val, width)
134
+ if width == 1:
135
+ out.write(f"{formatted}{vcd_id}\n")
136
+ else:
137
+ out.write(f"{formatted} {vcd_id}\n")
138
+ out.write("$end\n")
139
+
140
+
141
+ def _write_changes(
142
+ out: TextIO,
143
+ signals: dict[str, tuple[str, int, str]],
144
+ traces: dict[str, list[tuple[datetime, int]]],
145
+ timescale_ns: int = 1,
146
+ base_time: datetime | None = None,
147
+ ) -> None:
148
+ """Write time-stamped value changes."""
149
+ # Collect all events across all signals, sorted by time
150
+ events: list[tuple[int, str, int]] = [] # (tick, signal_name, value)
151
+
152
+ # Find the global base time if not provided
153
+ if base_time is None:
154
+ for sig_name, trace in traces.items():
155
+ if trace and (base_time is None or trace[0][0] < base_time):
156
+ base_time = trace[0][0]
157
+
158
+ if base_time is None:
159
+ return
160
+
161
+ for sig_name, trace in traces.items():
162
+ if sig_name not in signals:
163
+ continue
164
+ for ts, val in trace:
165
+ delta = ts - base_time
166
+ ns = int(delta.total_seconds() * 1_000_000_000)
167
+ tick = ns // timescale_ns
168
+ events.append((tick, sig_name, val))
169
+
170
+ # Sort by tick, then by signal name for determinism
171
+ events.sort(key=lambda e: (e[0], e[1]))
172
+
173
+ current_tick = -1
174
+ for tick, sig_name, value in events:
175
+ if tick != current_tick:
176
+ out.write(f"#{tick}\n")
177
+ current_tick = tick
178
+ vcd_id, width, _ = signals[sig_name]
179
+ formatted = _format_value(value, width)
180
+ if width == 1:
181
+ out.write(f"{formatted}{vcd_id}\n")
182
+ else:
183
+ out.write(f"{formatted} {vcd_id}\n")
184
+
185
+
186
+ def traces_to_vcd(
187
+ traces: dict[str, list[tuple[datetime, int]]],
188
+ *,
189
+ module: Module | None = None,
190
+ timescale: str = "1ns",
191
+ scope: str | None = None,
192
+ ) -> str:
193
+ """Convert simulation traces to a VCD string.
194
+
195
+ Parameters
196
+ ----------
197
+ traces : dict
198
+ Signal traces from ``CompiledModule.run()``.
199
+ module : Module | None
200
+ IR module (used for signal widths and port directions).
201
+ If None, all signals are assumed 1-bit wire.
202
+ timescale : str
203
+ VCD timescale (e.g. "1ns", "1ps", "10ns").
204
+ scope : str | None
205
+ Top-level scope name. Defaults to module name or "top".
206
+
207
+ Returns
208
+ -------
209
+ str
210
+ Complete VCD file content.
211
+ """
212
+ buf = io.StringIO()
213
+ _traces_to_vcd_stream(buf, traces, module=module, timescale=timescale, scope=scope)
214
+ return buf.getvalue()
215
+
216
+
217
+ def write_vcd(
218
+ path: str | Path,
219
+ traces: dict[str, list[tuple[datetime, int]]],
220
+ *,
221
+ module: Module | None = None,
222
+ timescale: str = "1ns",
223
+ scope: str | None = None,
224
+ ) -> None:
225
+ """Write simulation traces to a VCD file.
226
+
227
+ Parameters
228
+ ----------
229
+ path : str or Path
230
+ Output file path.
231
+ traces : dict
232
+ Signal traces from ``CompiledModule.run()``.
233
+ module : Module | None
234
+ IR module for signal metadata.
235
+ timescale : str
236
+ VCD timescale string.
237
+ scope : str | None
238
+ Top-level scope name.
239
+ """
240
+ with open(path, "w") as f:
241
+ _traces_to_vcd_stream(f, traces, module=module, timescale=timescale, scope=scope)
242
+
243
+
244
+ def _traces_to_vcd_stream(
245
+ out: TextIO,
246
+ traces: dict[str, list[tuple[datetime, int]]],
247
+ *,
248
+ module: Module | None = None,
249
+ timescale: str = "1ns",
250
+ scope: str | None = None,
251
+ ) -> None:
252
+ """Core implementation: write VCD to a stream."""
253
+ # Parse timescale to get ns multiplier
254
+ timescale_ns = _parse_timescale_ns(timescale)
255
+
256
+ # Build signal metadata
257
+ shapes: dict[str, Shape] = {}
258
+ var_types: dict[str, str] = {}
259
+
260
+ if module is not None:
261
+ scope_name = scope or module.name
262
+ for p in module.ports:
263
+ shapes[p.name] = p.shape
264
+ var_types[p.name] = "wire"
265
+ for s in module.signals:
266
+ shapes[s.name] = s.shape
267
+ var_types[s.name] = "reg"
268
+ else:
269
+ scope_name = scope or "top"
270
+
271
+ # Build VCD signal table: name → (id, width, type)
272
+ signal_table: dict[str, tuple[str, int, str]] = {}
273
+ idx = 0
274
+ for sig_name in sorted(traces.keys()):
275
+ shape = shapes.get(sig_name, Shape(1, False))
276
+ vtype = var_types.get(sig_name, "wire")
277
+ signal_table[sig_name] = (_make_id(idx), shape.width, vtype)
278
+ idx += 1
279
+
280
+ # Write header
281
+ _write_header(out, scope_name, signal_table, timescale=timescale)
282
+
283
+ # Clock synthesis setup
284
+ # Detect clock signals and their active/inactive values from the module.
285
+ # The simulation engine only emits trace data on active clock edges,
286
+ # so the VCD writer must synthesize the inactive (opposite) edges to
287
+ # produce a proper square-wave clock in the output.
288
+ clock_info: dict[str, tuple[int, int]] = {} # {clk_name: (active, inactive)}
289
+ if module is not None:
290
+ for cd in module.clock_domains:
291
+ if cd.clk in signal_table:
292
+ if cd.edge == EdgePolarity.POSEDGE:
293
+ clock_info[cd.clk] = (1, 0)
294
+ else:
295
+ clock_info[cd.clk] = (0, 1)
296
+
297
+ # Infer half-period from the gap between consecutive trace timestamps.
298
+ half_period_td = None
299
+ if clock_info:
300
+ for trace in traces.values():
301
+ if len(trace) >= 2:
302
+ half_period_td = (trace[1][0] - trace[0][0]) / 2
303
+ break
304
+
305
+ synthesize_clocks = bool(clock_info and half_period_td is not None)
306
+
307
+ # Initial values
308
+ initial_values: dict[str, int] = {}
309
+ if synthesize_clocks:
310
+ # With clock synthesis the VCD timeline is shifted back by one
311
+ # half-period so that $dumpvars represents the state *before* the
312
+ # first active edge. Clocks start at their inactive level and
313
+ # non-clock signals start at zero (reset state).
314
+ for sig_name in signal_table:
315
+ if sig_name in clock_info:
316
+ initial_values[sig_name] = clock_info[sig_name][1]
317
+ else:
318
+ initial_values[sig_name] = 0
319
+ else:
320
+ for sig_name, trace in traces.items():
321
+ initial_values[sig_name] = trace[0][1] if trace else 0
322
+
323
+ # Write initial values
324
+ _write_initial_values(out, signal_table, initial_values)
325
+
326
+ # Compute the base time from the full (untrimmed) traces
327
+ base_time: datetime | None = None
328
+ for trace in traces.values():
329
+ if trace and (base_time is None or trace[0][0] < base_time):
330
+ base_time = trace[0][0]
331
+
332
+ if synthesize_clocks and base_time is not None:
333
+ base_time = base_time - half_period_td
334
+
335
+ # Build value-change traces
336
+ trimmed: dict[str, list[tuple[datetime, int]]] = {}
337
+ if synthesize_clocks:
338
+ # Include ALL entries for non-clock signals (dumpvars has zeros,
339
+ # so the first trace entry is a real change at the first posedge).
340
+ for sig_name, trace in traces.items():
341
+ if sig_name in clock_info:
342
+ # Synthesize clock toggle events
343
+ active_val, inactive_val = clock_info[sig_name]
344
+ events: list[tuple[datetime, int]] = []
345
+ for i, (ts, _) in enumerate(trace):
346
+ events.append((ts, active_val))
347
+ if i < len(trace) - 1:
348
+ mid = ts + (trace[i + 1][0] - ts) / 2
349
+ events.append((mid, inactive_val))
350
+ elif half_period_td:
351
+ events.append((ts + half_period_td, inactive_val))
352
+ trimmed[sig_name] = events
353
+ else:
354
+ trimmed[sig_name] = list(trace)
355
+ else:
356
+ # Original behaviour: first entry is in $dumpvars, changes start
357
+ # from the second entry onward.
358
+ for sig_name, trace in traces.items():
359
+ trimmed[sig_name] = trace[1:] if len(trace) > 1 else []
360
+
361
+ _write_changes(out, signal_table, trimmed, timescale_ns=timescale_ns, base_time=base_time)
362
+
363
+
364
+ def _parse_timescale_ns(timescale: str) -> int:
365
+ """Parse a VCD timescale string to nanoseconds.
366
+
367
+ Supports: "1ns", "10ns", "100ns", "1us", "1ps", etc.
368
+ """
369
+ ts = timescale.strip().lower()
370
+ units = {"ps": 0.001, "ns": 1, "us": 1000, "ms": 1_000_000, "s": 1_000_000_000}
371
+ for suffix, mult in units.items():
372
+ if ts.endswith(suffix):
373
+ num = int(ts[: -len(suffix)])
374
+ return max(1, int(num * mult))
375
+ return 1 # default to 1ns
dau_sim/api.py ADDED
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import timedelta
5
+
6
+ from dau_sim.compiler import compile_module
7
+ from dau_sim.compiler.compile import CompiledModule
8
+ from dau_sim.ir.module import Module
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class SimulationResult:
13
+ """Container for simulation output with interactive helpers."""
14
+
15
+ module_name: str
16
+ traces: dict[str, list[tuple[object, int]]]
17
+
18
+ def latest(self, signal: str) -> int | None:
19
+ """Return the most recent value for a signal, if available."""
20
+ values = self.traces.get(signal, [])
21
+ if not values:
22
+ return None
23
+ return values[-1][1]
24
+
25
+ def to_rows(self, *, signals: list[str] | None = None) -> list[dict[str, object]]:
26
+ """Return trace points as row dictionaries for notebooks/data tools."""
27
+ selected = signals if signals is not None else sorted(self.traces.keys())
28
+ rows: list[dict[str, object]] = []
29
+ for name in selected:
30
+ for ts, value in self.traces.get(name, []):
31
+ rows.append({"signal": name, "timestamp": ts, "value": value})
32
+ return rows
33
+
34
+
35
+ class Simulator:
36
+ """High-level interactive API for compiling and running designs."""
37
+
38
+ def __init__(self, compiled: CompiledModule):
39
+ self._compiled = compiled
40
+
41
+ @property
42
+ def module(self) -> Module:
43
+ return self._compiled.module
44
+
45
+ @property
46
+ def compiled(self) -> CompiledModule:
47
+ return self._compiled
48
+
49
+ @classmethod
50
+ def from_module(cls, module: Module, *, four_state: bool = False) -> "Simulator":
51
+ return cls(compile_module(module, four_state=four_state))
52
+
53
+ @classmethod
54
+ def from_sv(cls, source: str, *, top: str | None = None, four_state: bool = False) -> "Simulator":
55
+ from dau_sim.frontends import parse_sv
56
+
57
+ module = parse_sv(source, top=top)
58
+ return cls.from_module(module, four_state=four_state)
59
+
60
+ @classmethod
61
+ def from_sv_file(cls, path: str, *, top: str | None = None, four_state: bool = False) -> "Simulator":
62
+ from dau_sim.frontends import parse_sv_file
63
+
64
+ module = parse_sv_file(path, top=top)
65
+ return cls.from_module(module, four_state=four_state)
66
+
67
+ @classmethod
68
+ def from_amaranth(cls, design, *, four_state: bool = False) -> "Simulator":
69
+ from dau_sim.frontends import from_amaranth
70
+
71
+ module = from_amaranth(design)
72
+ return cls.from_module(module, four_state=four_state)
73
+
74
+ def run(
75
+ self,
76
+ *,
77
+ cycles: int = 10,
78
+ clock_period: timedelta = timedelta(microseconds=1),
79
+ inputs: dict[str, int] | None = None,
80
+ clocks: dict[str, timedelta] | None = None,
81
+ ) -> SimulationResult:
82
+ traces = self._compiled.run(cycles=cycles, clock_period=clock_period, inputs=inputs, clocks=clocks)
83
+ return SimulationResult(module_name=self._compiled.module.name, traces=traces)
84
+
85
+ def write_vcd(
86
+ self,
87
+ path: str,
88
+ result: SimulationResult,
89
+ *,
90
+ timescale: str = "1ns",
91
+ signals: list[str] | None = None,
92
+ exclude: list[str] | None = None,
93
+ ) -> None:
94
+ self._compiled.write_vcd(path, result.traces, timescale=timescale, signals=signals, exclude=exclude)
@@ -0,0 +1 @@
1
+ """Backends: simulator interface implementations (e.g. cocotb)."""