@celox-sim/celox 0.1.5 → 0.1.7

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.
@@ -5,15 +5,16 @@ import type {
5
5
  ModuleDefinition,
6
6
  NativeSimulationHandle,
7
7
  } from "./types.js";
8
+ import { SimulationTimeoutError } from "./types.js";
8
9
 
9
10
  // ---------------------------------------------------------------------------
10
11
  // Mock helpers
11
12
  // ---------------------------------------------------------------------------
12
13
 
13
14
  interface TopPorts {
14
- rst: number;
15
- d: number;
16
- readonly q: number;
15
+ rst: bigint;
16
+ d: bigint;
17
+ readonly q: bigint;
17
18
  }
18
19
 
19
20
  const TopModule: ModuleDefinition<TopPorts> = {
@@ -29,7 +30,10 @@ const TopModule: ModuleDefinition<TopPorts> = {
29
30
  events: ["clk"],
30
31
  };
31
32
 
32
- function createMockNative(): {
33
+ function createMockNative(opts?: {
34
+ resetTypeKind?: string;
35
+ associatedClock?: string;
36
+ }): {
33
37
  create: NativeCreateSimulationFn;
34
38
  handle: NativeSimulationHandle;
35
39
  buffer: SharedArrayBuffer;
@@ -50,9 +54,12 @@ function createMockNative(): {
50
54
  currentTime += 5;
51
55
  const view = new DataView(buffer);
52
56
  view.setUint8(4, view.getUint8(2));
57
+ // Toggle clock (offset 6) on each step to simulate half-period=5
58
+ view.setUint8(6, view.getUint8(6) === 0 ? 1 : 0);
53
59
  return currentTime;
54
60
  }),
55
61
  time: vi.fn().mockImplementation(() => currentTime),
62
+ nextEventTime: vi.fn().mockReturnValue(null),
56
63
  evalComb: vi.fn().mockImplementation(() => {
57
64
  const view = new DataView(buffer);
58
65
  view.setUint8(4, view.getUint8(2));
@@ -61,13 +68,16 @@ function createMockNative(): {
61
68
  dispose: vi.fn(),
62
69
  };
63
70
 
71
+ const resetTypeKind = opts?.resetTypeKind ?? "reset_async_high";
72
+ const associatedClock = opts?.associatedClock;
73
+
64
74
  const create: NativeCreateSimulationFn = vi.fn().mockReturnValue({
65
75
  buffer,
66
76
  layout: {
67
- clk: { offset: 6, width: 1, byteSize: 1, is4state: false, direction: "input" },
68
- rst: { offset: 0, width: 1, byteSize: 1, is4state: false, direction: "input" },
69
- d: { offset: 2, width: 8, byteSize: 1, is4state: false, direction: "input" },
70
- q: { offset: 4, width: 8, byteSize: 1, is4state: false, direction: "output" },
77
+ clk: { offset: 6, width: 1, byteSize: 1, is4state: false, direction: "input", typeKind: "clock" },
78
+ rst: { offset: 0, width: 1, byteSize: 1, is4state: false, direction: "input", typeKind: resetTypeKind, ...(associatedClock ? { associatedClock } : {}) },
79
+ d: { offset: 2, width: 8, byteSize: 1, is4state: false, direction: "input", typeKind: "logic" },
80
+ q: { offset: 4, width: 8, byteSize: 1, is4state: false, direction: "output", typeKind: "logic" },
71
81
  },
72
82
  events: { clk: 0 },
73
83
  handle,
@@ -117,11 +127,11 @@ describe("Simulation", () => {
117
127
  __nativeCreate: mock.create,
118
128
  });
119
129
 
120
- sim.dut.d = 42;
130
+ sim.dut.d = 42n;
121
131
  sim.runUntil(100);
122
132
 
123
133
  expect(mock.handle.runUntil).toHaveBeenCalledWith(100);
124
- expect(sim.dut.q).toBe(42);
134
+ expect(sim.dut.q).toBe(42n);
125
135
  });
126
136
 
127
137
  test("step", () => {
@@ -130,11 +140,11 @@ describe("Simulation", () => {
130
140
  __nativeCreate: mock.create,
131
141
  });
132
142
 
133
- sim.dut.d = 0xAB;
143
+ sim.dut.d = 0xABn;
134
144
  const t = sim.step();
135
145
 
136
146
  expect(t).toBe(5);
137
- expect(sim.dut.q).toBe(0xAB);
147
+ expect(sim.dut.q).toBe(0xABn);
138
148
  });
139
149
 
140
150
  test("time", () => {
@@ -182,4 +192,202 @@ describe("Simulation", () => {
182
192
  Simulation.create(TopModule);
183
193
  }).toThrow("Native simulator binding not loaded");
184
194
  });
195
+
196
+ test("runUntil with maxSteps: fast path when omitted", () => {
197
+ const mock = createMockNative();
198
+ const sim = Simulation.create(TopModule, {
199
+ __nativeCreate: mock.create,
200
+ });
201
+
202
+ sim.runUntil(100);
203
+ // Without maxSteps, the Rust fast-path should be used
204
+ expect(mock.handle.runUntil).toHaveBeenCalledWith(100);
205
+ });
206
+
207
+ test("runUntil with maxSteps: throws SimulationTimeoutError", () => {
208
+ const mock = createMockNative();
209
+ const sim = Simulation.create(TopModule, {
210
+ __nativeCreate: mock.create,
211
+ });
212
+
213
+ // step mock increments time by 5 each call, so reaching 10000 requires 2000 steps
214
+ // With maxSteps=10 we'll exhaust before getting there
215
+ expect(() => sim.runUntil(10000, { maxSteps: 10 })).toThrow(
216
+ SimulationTimeoutError,
217
+ );
218
+ });
219
+
220
+ test("runUntil with maxSteps: timeout error has correct properties", () => {
221
+ const mock = createMockNative();
222
+ const sim = Simulation.create(TopModule, {
223
+ __nativeCreate: mock.create,
224
+ });
225
+
226
+ try {
227
+ sim.runUntil(10000, { maxSteps: 5 });
228
+ expect.unreachable();
229
+ } catch (e) {
230
+ expect(e).toBeInstanceOf(SimulationTimeoutError);
231
+ const err = e as SimulationTimeoutError;
232
+ expect(err.steps).toBe(5);
233
+ expect(err.time).toBeGreaterThan(0);
234
+ }
235
+ });
236
+
237
+ test("waitUntil: returns time when condition is met", () => {
238
+ const mock = createMockNative();
239
+ const sim = Simulation.create(TopModule, {
240
+ __nativeCreate: mock.create,
241
+ });
242
+
243
+ sim.dut.d = 42n;
244
+ let callCount = 0;
245
+ const t = sim.waitUntil(() => {
246
+ callCount++;
247
+ return callCount >= 3;
248
+ });
249
+
250
+ expect(t).toBeGreaterThan(0);
251
+ expect(callCount).toBe(3);
252
+ });
253
+
254
+ test("waitUntil: throws on timeout", () => {
255
+ const mock = createMockNative();
256
+ const sim = Simulation.create(TopModule, {
257
+ __nativeCreate: mock.create,
258
+ });
259
+
260
+ expect(() =>
261
+ sim.waitUntil(() => false, { maxSteps: 5 }),
262
+ ).toThrow(SimulationTimeoutError);
263
+ });
264
+
265
+ test("waitForCycles: counts rising edges", () => {
266
+ const mock = createMockNative();
267
+ const sim = Simulation.create(TopModule, {
268
+ __nativeCreate: mock.create,
269
+ });
270
+
271
+ sim.addClock("clk", { period: 10 });
272
+ const t = sim.waitForCycles("clk", 3);
273
+ // clk toggles each step: 0→1→0→1→0→1 (5 steps for 3 rising edges)
274
+ expect(mock.handle.step).toHaveBeenCalledTimes(5);
275
+ expect(t).toBe(25);
276
+ });
277
+
278
+ test("waitForCycles: throws without addClock", () => {
279
+ const mock = createMockNative();
280
+ const sim = Simulation.create(TopModule, {
281
+ __nativeCreate: mock.create,
282
+ });
283
+
284
+ expect(() => sim.waitForCycles("clk", 3)).toThrow("No clock registered");
285
+ });
286
+
287
+ test("reset: active-high with associatedClock steps until target time", () => {
288
+ const mock = createMockNative({ associatedClock: "clk" });
289
+ const sim = Simulation.create(TopModule, {
290
+ __nativeCreate: mock.create,
291
+ });
292
+
293
+ sim.addClock("clk", { period: 10 });
294
+ sim.reset("rst");
295
+ // Default activeCycles=2: 3 steps for 2 rising edges (0→1→0→1)
296
+ expect(mock.handle.step).toHaveBeenCalledTimes(3);
297
+ // Released to inactive value (0 for active-high)
298
+ const view = new DataView(mock.buffer);
299
+ expect(view.getUint8(0)).toBe(0);
300
+ });
301
+
302
+ test("reset: custom activeCycles with associatedClock", () => {
303
+ const mock = createMockNative({ associatedClock: "clk" });
304
+ const sim = Simulation.create(TopModule, {
305
+ __nativeCreate: mock.create,
306
+ });
307
+
308
+ sim.addClock("clk", { period: 10 });
309
+ sim.reset("rst", { activeCycles: 3 });
310
+ // 3 cycles: 5 steps for 3 rising edges (0→1→0→1→0→1)
311
+ expect(mock.handle.step).toHaveBeenCalledTimes(5);
312
+ });
313
+
314
+ test("reset: explicit duration overrides cycle calculation", () => {
315
+ const mock = createMockNative({ associatedClock: "clk" });
316
+ const sim = Simulation.create(TopModule, {
317
+ __nativeCreate: mock.create,
318
+ });
319
+
320
+ sim.addClock("clk", { period: 10 });
321
+ sim.reset("rst", { duration: 50 });
322
+ // Explicit duration → runUntil(0 + 50 = 50)
323
+ expect(mock.handle.runUntil).toHaveBeenCalledWith(50);
324
+ });
325
+
326
+ test("reset: active-low with associatedClock asserts 0 then releases to 1", () => {
327
+ const mock = createMockNative({
328
+ resetTypeKind: "reset_async_low",
329
+ associatedClock: "clk",
330
+ });
331
+ const sim = Simulation.create(TopModule, {
332
+ __nativeCreate: mock.create,
333
+ });
334
+
335
+ sim.addClock("clk", { period: 10 });
336
+ sim.reset("rst");
337
+ // active-low: releases to 1
338
+ const view = new DataView(mock.buffer);
339
+ expect(view.getUint8(0)).toBe(1);
340
+ // activeCycles=2: 3 steps for 2 rising edges
341
+ expect(mock.handle.step).toHaveBeenCalledTimes(3);
342
+ });
343
+
344
+ test("reset: throws when no associatedClock and no duration", () => {
345
+ // No associatedClock in layout
346
+ const mock = createMockNative();
347
+ const sim = Simulation.create(TopModule, {
348
+ __nativeCreate: mock.create,
349
+ });
350
+
351
+ expect(() => sim.reset("rst")).toThrow("has no associated clock");
352
+ });
353
+
354
+ test("reset: no associatedClock but duration specified works", () => {
355
+ // No associatedClock in layout, but duration is given
356
+ const mock = createMockNative();
357
+ const sim = Simulation.create(TopModule, {
358
+ __nativeCreate: mock.create,
359
+ });
360
+
361
+ sim.reset("rst", { duration: 100 });
362
+ expect(mock.handle.runUntil).toHaveBeenCalledWith(100);
363
+ });
364
+
365
+ test("reset: throws when associatedClock not registered via addClock", () => {
366
+ const mock = createMockNative({ associatedClock: "clk" });
367
+ const sim = Simulation.create(TopModule, {
368
+ __nativeCreate: mock.create,
369
+ });
370
+ // addClock not called
371
+ expect(() => sim.reset("rst")).toThrow(
372
+ "No clock registered for 'clk'",
373
+ );
374
+ });
375
+
376
+ test("reset: throws on non-reset port", () => {
377
+ const mock = createMockNative();
378
+ const sim = Simulation.create(TopModule, {
379
+ __nativeCreate: mock.create,
380
+ });
381
+
382
+ expect(() => sim.reset("d")).toThrow("not a reset signal");
383
+ });
384
+
385
+ test("reset: throws on unknown port", () => {
386
+ const mock = createMockNative();
387
+ const sim = Simulation.create(TopModule, {
388
+ __nativeCreate: mock.create,
389
+ });
390
+
391
+ expect(() => sim.reset("nonexistent")).toThrow("Unknown port");
392
+ });
185
393
  });
package/src/simulation.ts CHANGED
@@ -7,16 +7,21 @@
7
7
 
8
8
  import type {
9
9
  CreateResult,
10
+ FourStateValue,
10
11
  ModuleDefinition,
11
12
  NativeSimulationHandle,
13
+ SignalLayout,
12
14
  SimulatorOptions,
13
15
  } from "./types.js";
14
- import { createDut, type DirtyState } from "./dut.js";
16
+ import { SimulationTimeoutError } from "./types.js";
17
+ import { createDut, readFourState, type DirtyState } from "./dut.js";
15
18
  import {
16
19
  loadNativeAddon,
17
20
  parseNapiLayout,
21
+ parseHierarchyLayout,
18
22
  buildPortsFromLayout,
19
23
  wrapDirectSimulationHandle,
24
+ buildNapiOpts,
20
25
  } from "./napi-helpers.js";
21
26
 
22
27
  /**
@@ -49,6 +54,9 @@ export class Simulation<P = Record<string, unknown>> {
49
54
  private readonly _dut: P;
50
55
  private readonly _events: Record<string, number>;
51
56
  private readonly _state: DirtyState;
57
+ private readonly _buffer: ArrayBuffer | SharedArrayBuffer;
58
+ private readonly _layout: Record<string, SignalLayout & { typeKind?: string; associatedClock?: string }>;
59
+ private readonly _clocks = new Map<string, { period: number; eventId: number }>();
52
60
  private _disposed = false;
53
61
 
54
62
  private constructor(
@@ -56,11 +64,15 @@ export class Simulation<P = Record<string, unknown>> {
56
64
  dut: P,
57
65
  events: Record<string, number>,
58
66
  state: DirtyState,
67
+ buffer: ArrayBuffer | SharedArrayBuffer,
68
+ layout: Record<string, SignalLayout & { typeKind?: string; associatedClock?: string }>,
59
69
  ) {
60
70
  this._handle = handle;
61
71
  this._dut = dut;
62
72
  this._events = events;
63
73
  this._state = state;
74
+ this._buffer = buffer;
75
+ this._layout = layout;
64
76
  }
65
77
 
66
78
  /**
@@ -91,8 +103,8 @@ export class Simulation<P = Record<string, unknown>> {
91
103
  );
92
104
  }
93
105
 
94
- const { fourState, vcd } = options ?? {};
95
- const result = createFn(module.source, module.name, { fourState, vcd });
106
+ const { fourState, vcd, optimize, falseLoops, trueLoops, clockType, resetType } = options ?? {};
107
+ const result = createFn(module.source, module.name, { fourState, vcd, optimize, falseLoops, trueLoops, clockType, resetType });
96
108
  const state: DirtyState = { dirty: false };
97
109
 
98
110
  const dut = createDut<P>(
@@ -101,9 +113,10 @@ export class Simulation<P = Record<string, unknown>> {
101
113
  module.ports,
102
114
  result.handle,
103
115
  state,
116
+ result.hierarchy,
104
117
  );
105
118
 
106
- return new Simulation<P>(result.handle, dut, result.events, state);
119
+ return new Simulation<P>(result.handle, dut, result.events, state, result.buffer, result.layout);
107
120
  }
108
121
 
109
122
  /**
@@ -124,11 +137,12 @@ export class Simulation<P = Record<string, unknown>> {
124
137
  options?: SimulatorOptions & { nativeAddonPath?: string },
125
138
  ): Simulation<P> {
126
139
  const addon = loadNativeAddon(options?.nativeAddonPath);
127
- const napiOpts = options?.fourState ? { fourState: options.fourState } : undefined;
140
+ const napiOpts = buildNapiOpts(options);
128
141
  const raw = new addon.NativeSimulationHandle(source, top, napiOpts);
129
142
 
130
143
  const layout = parseNapiLayout(raw.layoutJson);
131
144
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
145
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
132
146
 
133
147
  const ports = buildPortsFromLayout(layout.signals, events);
134
148
 
@@ -136,9 +150,9 @@ export class Simulation<P = Record<string, unknown>> {
136
150
 
137
151
  const state: DirtyState = { dirty: false };
138
152
  const handle = wrapDirectSimulationHandle(raw);
139
- const dut = createDut<P>(buf, layout.forDut, ports, handle, state);
153
+ const dut = createDut<P>(buf, layout.forDut, ports, handle, state, hierarchy);
140
154
 
141
- return new Simulation<P>(handle, dut, events, state);
155
+ return new Simulation<P>(handle, dut, events, state, buf, layout.signals);
142
156
  }
143
157
 
144
158
  /**
@@ -159,11 +173,12 @@ export class Simulation<P = Record<string, unknown>> {
159
173
  options?: SimulatorOptions & { nativeAddonPath?: string },
160
174
  ): Simulation<P> {
161
175
  const addon = loadNativeAddon(options?.nativeAddonPath);
162
- const napiOpts = options?.fourState ? { fourState: options.fourState } : undefined;
176
+ const napiOpts = buildNapiOpts(options);
163
177
  const raw = addon.NativeSimulationHandle.fromProject(projectPath, top, napiOpts);
164
178
 
165
179
  const layout = parseNapiLayout(raw.layoutJson);
166
180
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
181
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
167
182
 
168
183
  const ports = buildPortsFromLayout(layout.signals, events);
169
184
 
@@ -171,9 +186,9 @@ export class Simulation<P = Record<string, unknown>> {
171
186
 
172
187
  const state: DirtyState = { dirty: false };
173
188
  const handle = wrapDirectSimulationHandle(raw);
174
- const dut = createDut<P>(buf, layout.forDut, ports, handle, state);
189
+ const dut = createDut<P>(buf, layout.forDut, ports, handle, state, hierarchy);
175
190
 
176
- return new Simulation<P>(handle, dut, events, state);
191
+ return new Simulation<P>(handle, dut, events, state, buf, layout.signals);
177
192
  }
178
193
 
179
194
  /** The DUT accessor object — read/write ports as plain properties. */
@@ -194,6 +209,7 @@ export class Simulation<P = Record<string, unknown>> {
194
209
  this.ensureAlive();
195
210
  const eventId = this.resolveEvent(name);
196
211
  this._handle.addClock(eventId, opts.period, opts.initialDelay ?? 0);
212
+ this._clocks.set(name, { period: opts.period, eventId });
197
213
  }
198
214
 
199
215
  /**
@@ -211,11 +227,33 @@ export class Simulation<P = Record<string, unknown>> {
211
227
  /**
212
228
  * Run the simulation until the given time.
213
229
  * Processes all scheduled events up to and including `endTime`.
214
- * evalComb is called internally; dirty is cleared on return.
230
+ *
231
+ * When `maxSteps` is provided, steps are counted in TS and a
232
+ * `SimulationTimeoutError` is thrown if the budget is exhausted before
233
+ * reaching `endTime`. Without `maxSteps` the fast Rust path is used.
215
234
  */
216
- runUntil(endTime: number): void {
235
+ runUntil(endTime: number, opts?: { maxSteps?: number }): void {
217
236
  this.ensureAlive();
218
- this._handle.runUntil(endTime);
237
+ if (opts?.maxSteps == null) {
238
+ this._handle.runUntil(endTime);
239
+ this._state.dirty = false;
240
+ return;
241
+ }
242
+ const max = opts.maxSteps;
243
+ let steps = 0;
244
+ while (this._handle.time() < endTime) {
245
+ const t = this._handle.step();
246
+ if (t == null) break;
247
+ steps++;
248
+ if (steps >= max) {
249
+ this._state.dirty = false;
250
+ throw new SimulationTimeoutError(
251
+ `runUntil: exceeded ${max} steps at time ${this._handle.time()} (target ${endTime})`,
252
+ this._handle.time(),
253
+ steps,
254
+ );
255
+ }
256
+ }
219
257
  this._state.dirty = false;
220
258
  }
221
259
 
@@ -237,6 +275,153 @@ export class Simulation<P = Record<string, unknown>> {
237
275
  return this._handle.time();
238
276
  }
239
277
 
278
+ /**
279
+ * Peek at the time of the next scheduled event without advancing.
280
+ *
281
+ * @returns The time of the next event, or `null` if no events are scheduled.
282
+ */
283
+ nextEventTime(): number | null {
284
+ this.ensureAlive();
285
+ return this._handle.nextEventTime();
286
+ }
287
+
288
+ /**
289
+ * Step until `condition()` returns true.
290
+ *
291
+ * @returns The simulation time when the condition became true.
292
+ * @throws SimulationTimeoutError if `maxSteps` is exceeded.
293
+ */
294
+ waitUntil(
295
+ condition: () => boolean,
296
+ opts?: { maxSteps?: number },
297
+ ): number {
298
+ this.ensureAlive();
299
+ const max = opts?.maxSteps ?? 100_000;
300
+ let steps = 0;
301
+ while (!condition()) {
302
+ const t = this._handle.step();
303
+ this._state.dirty = false;
304
+ if (t == null) break;
305
+ steps++;
306
+ if (steps >= max) {
307
+ throw new SimulationTimeoutError(
308
+ `waitUntil: condition not met after ${max} steps at time ${this._handle.time()}`,
309
+ this._handle.time(),
310
+ steps,
311
+ );
312
+ }
313
+ }
314
+ return this._handle.time();
315
+ }
316
+
317
+ /**
318
+ * Wait for `count` rising edges of the given clock.
319
+ *
320
+ * Detects actual 0→1 transitions by reading the clock signal directly
321
+ * from the shared buffer (clock ports are excluded from the DUT proxy).
322
+ * The clock must have been registered via `addClock`.
323
+ *
324
+ * @returns The simulation time after the edges are observed.
325
+ * @throws SimulationTimeoutError if `maxSteps` is exceeded.
326
+ */
327
+ waitForCycles(
328
+ clock: string,
329
+ count: number,
330
+ opts?: { maxSteps?: number },
331
+ ): number {
332
+ this.ensureAlive();
333
+ if (!this._clocks.has(clock)) {
334
+ throw new Error(
335
+ `No clock registered for '${clock}'. Call addClock() first.`,
336
+ );
337
+ }
338
+ const sig = this._layout[clock];
339
+ if (!sig) {
340
+ throw new Error(`No layout entry for clock '${clock}'.`);
341
+ }
342
+ const view = new DataView(this._buffer);
343
+ const readClk = () => view.getUint8(sig.offset);
344
+ let prev = readClk();
345
+ let remaining = count;
346
+ return this.waitUntil(() => {
347
+ const curr = readClk();
348
+ if (prev === 0 && curr !== 0) remaining--;
349
+ prev = curr;
350
+ return remaining <= 0;
351
+ }, opts);
352
+ }
353
+
354
+ /**
355
+ * Assert and release a reset signal.
356
+ *
357
+ * The active level is determined automatically from the Veryl type:
358
+ * - `reset` / `reset_async_high` / `reset_sync_high` → active-high (1)
359
+ * - `reset_async_low` / `reset_sync_low` → active-low (0)
360
+ *
361
+ * For sync resets (with an associated clock from FfDeclaration), advances
362
+ * `activeCycles` worth of the associated clock's period using `runUntil`.
363
+ * For async resets without an associated clock, `duration` must be specified.
364
+ * An explicit `duration` overrides cycle-based calculation for either type.
365
+ */
366
+ reset(
367
+ signal: string,
368
+ opts?: { activeCycles?: number; duration?: number },
369
+ ): void {
370
+ this.ensureAlive();
371
+ const sig = this._layout[signal];
372
+ if (!sig) {
373
+ throw new Error(
374
+ `Unknown port '${signal}'. Available: ${Object.keys(this._layout).join(", ")}`,
375
+ );
376
+ }
377
+ const typeKind = sig.typeKind ?? "";
378
+ if (!typeKind.startsWith("reset")) {
379
+ throw new Error(
380
+ `Port '${signal}' is not a reset signal (type_kind: '${typeKind}').`,
381
+ );
382
+ }
383
+ const isActiveLow = typeKind === "reset_async_low" || typeKind === "reset_sync_low";
384
+ const activeValue = isActiveLow ? 0n : 1n;
385
+ const inactiveValue = isActiveLow ? 1n : 0n;
386
+
387
+ const dut = this._dut as Record<string, unknown>;
388
+ dut[signal] = activeValue;
389
+
390
+ const associatedClock = sig.associatedClock;
391
+
392
+ if (opts?.duration != null) {
393
+ // Explicit duration (works for both sync and async resets)
394
+ this._handle.runUntil(this._handle.time() + opts.duration);
395
+ } else if (associatedClock) {
396
+ // Clock-associated reset → advance by activeCycles
397
+ const cycles = opts?.activeCycles ?? 2;
398
+ this.waitForCycles(associatedClock, cycles);
399
+ } else {
400
+ // No associated clock and no explicit duration → error
401
+ throw new Error(
402
+ `Reset '${signal}' has no associated clock. Specify opts.duration.`,
403
+ );
404
+ }
405
+
406
+ dut[signal] = inactiveValue;
407
+ this._state.dirty = false;
408
+ }
409
+
410
+ /**
411
+ * Read the raw 4-state (value + mask) pair for the named port.
412
+ */
413
+ fourState(portName: string): FourStateValue {
414
+ this.ensureAlive();
415
+ const sig = this._layout[portName];
416
+ if (!sig) {
417
+ throw new Error(
418
+ `Unknown port '${portName}'. Available: ${Object.keys(this._layout).join(", ")}`,
419
+ );
420
+ }
421
+ const [value, mask] = readFourState(this._buffer, sig);
422
+ return { __fourState: true, value, mask };
423
+ }
424
+
240
425
  /** Write current signal values to VCD at the given timestamp. */
241
426
  dump(timestamp: number): void {
242
427
  this.ensureAlive();
@@ -11,10 +11,10 @@ import type {
11
11
  // ---------------------------------------------------------------------------
12
12
 
13
13
  interface AdderPorts {
14
- rst: number;
15
- a: number;
16
- b: number;
17
- readonly sum: number;
14
+ rst: bigint;
15
+ a: bigint;
16
+ b: bigint;
17
+ readonly sum: bigint;
18
18
  }
19
19
 
20
20
  const AdderModule: ModuleDefinition<AdderPorts> = {
@@ -80,11 +80,11 @@ describe("Simulator", () => {
80
80
  __nativeCreate: mock.create,
81
81
  });
82
82
 
83
- sim.dut.a = 100;
84
- sim.dut.b = 200;
83
+ sim.dut.a = 100n;
84
+ sim.dut.b = 200n;
85
85
  sim.tick();
86
86
 
87
- expect(sim.dut.sum).toBe(300);
87
+ expect(sim.dut.sum).toBe(300n);
88
88
  expect(mock.handle.tick).toHaveBeenCalledTimes(1);
89
89
  });
90
90
 
@@ -176,7 +176,7 @@ describe("Simulator", () => {
176
176
  __nativeCreate: mock.create,
177
177
  });
178
178
 
179
- sim.dut.a = 100;
179
+ sim.dut.a = 100n;
180
180
  sim.tick();
181
181
 
182
182
  // After tick, reading output should NOT trigger evalComb