@celox-sim/celox 0.1.5 → 0.1.6

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.
@@ -11,11 +11,13 @@
11
11
 
12
12
  import type {
13
13
  CreateResult,
14
+ LoopBreak,
14
15
  NativeSimulatorHandle,
15
16
  NativeSimulationHandle,
16
17
  PortInfo,
17
18
  SignalLayout,
18
19
  SimulatorOptions,
20
+ TrueLoopSpec,
19
21
  } from "./types.js";
20
22
  import type { NativeCreateFn } from "./simulator.js";
21
23
  import type { NativeCreateSimulationFn } from "./simulation.js";
@@ -27,6 +29,7 @@ import type { NativeCreateSimulationFn } from "./simulation.js";
27
29
  export interface RawNapiSimulatorHandle {
28
30
  readonly layoutJson: string;
29
31
  readonly eventsJson: string;
32
+ readonly hierarchyJson: string;
30
33
  readonly stableSize: number;
31
34
  readonly totalSize: number;
32
35
  tick(eventId: number): void;
@@ -40,6 +43,7 @@ export interface RawNapiSimulatorHandle {
40
43
  export interface RawNapiSimulationHandle {
41
44
  readonly layoutJson: string;
42
45
  readonly eventsJson: string;
46
+ readonly hierarchyJson: string;
43
47
  readonly stableSize: number;
44
48
  readonly totalSize: number;
45
49
  addClock(eventId: number, period: number, initialDelay: number): void;
@@ -47,14 +51,42 @@ export interface RawNapiSimulationHandle {
47
51
  runUntil(endTime: number): void;
48
52
  step(): number | null;
49
53
  time(): number;
54
+ nextEventTime(): number | null;
50
55
  evalComb(): void;
51
56
  dump(timestamp: number): void;
52
57
  sharedMemory(): Uint8Array;
53
58
  dispose(): void;
54
59
  }
55
60
 
61
+ export interface NapiFalseLoop {
62
+ from: NapiSignalPath;
63
+ to: NapiSignalPath;
64
+ }
65
+
66
+ export interface NapiTrueLoop {
67
+ from: NapiSignalPath;
68
+ to: NapiSignalPath;
69
+ maxIter: number;
70
+ }
71
+
72
+ export interface NapiSignalPath {
73
+ instancePath: NapiInstanceSegment[];
74
+ varPath: string[];
75
+ }
76
+
77
+ export interface NapiInstanceSegment {
78
+ name: string;
79
+ index: number;
80
+ }
81
+
56
82
  export interface NapiOptions {
57
83
  fourState?: boolean;
84
+ vcd?: string;
85
+ optimize?: boolean;
86
+ falseLoops?: NapiFalseLoop[];
87
+ trueLoops?: NapiTrueLoop[];
88
+ clockType?: string;
89
+ resetType?: string;
58
90
  }
59
91
 
60
92
  export interface RawNapiAddon {
@@ -102,6 +134,98 @@ export function loadNativeAddon(addonPath?: string): RawNapiAddon {
102
134
  }
103
135
  }
104
136
 
137
+ // ---------------------------------------------------------------------------
138
+ // Signal path parsing
139
+ // ---------------------------------------------------------------------------
140
+
141
+ /**
142
+ * Parse a signal path string into instance-path + var-path components.
143
+ *
144
+ * Format: `instanceSeg1.instanceSeg2:varSeg1.varSeg2`
145
+ * - `:` separates instance path from variable path
146
+ * - Without `:`, the whole string is the variable path
147
+ * - Instance segments may include `[N]` array indices
148
+ *
149
+ * Examples:
150
+ * - `"v"` → { instancePath: [], varPath: ["v"] }
151
+ * - `"p2:i"` → { instancePath: [{name:"p2",index:0}], varPath: ["i"] }
152
+ * - `"a.b[3]:x.y"` → { instancePath: [{name:"a",index:0},{name:"b",index:3}], varPath: ["x","y"] }
153
+ */
154
+ export function parseSignalPath(path: string): NapiSignalPath {
155
+ const colonIdx = path.indexOf(":");
156
+ if (colonIdx < 0) {
157
+ return { instancePath: [], varPath: path.split(".") };
158
+ }
159
+
160
+ const instPart = path.slice(0, colonIdx);
161
+ const varPart = path.slice(colonIdx + 1);
162
+
163
+ const instancePath: NapiInstanceSegment[] = [];
164
+ if (instPart.length > 0) {
165
+ for (const seg of instPart.split(".")) {
166
+ const bracketIdx = seg.indexOf("[");
167
+ if (bracketIdx >= 0) {
168
+ const name = seg.slice(0, bracketIdx);
169
+ const index = Number.parseInt(seg.slice(bracketIdx + 1, -1), 10);
170
+ instancePath.push({ name, index });
171
+ } else {
172
+ instancePath.push({ name: seg, index: 0 });
173
+ }
174
+ }
175
+ }
176
+
177
+ return { instancePath, varPath: varPart.split(".") };
178
+ }
179
+
180
+ /**
181
+ * Build NapiOptions from SimulatorOptions.
182
+ * Returns `undefined` when no options are set (to skip the NAPI options arg).
183
+ */
184
+ export function buildNapiOpts(options?: SimulatorOptions): NapiOptions | undefined {
185
+ if (!options) return undefined;
186
+
187
+ const napiOpts: NapiOptions = {};
188
+ let hasOpt = false;
189
+
190
+ if (options.fourState) {
191
+ napiOpts.fourState = options.fourState;
192
+ hasOpt = true;
193
+ }
194
+ if (options.vcd) {
195
+ napiOpts.vcd = options.vcd;
196
+ hasOpt = true;
197
+ }
198
+ if (options.optimize != null) {
199
+ napiOpts.optimize = options.optimize;
200
+ hasOpt = true;
201
+ }
202
+ if (options.falseLoops && options.falseLoops.length > 0) {
203
+ napiOpts.falseLoops = options.falseLoops.map((lb: LoopBreak) => ({
204
+ from: parseSignalPath(lb.from),
205
+ to: parseSignalPath(lb.to),
206
+ }));
207
+ hasOpt = true;
208
+ }
209
+ if (options.trueLoops && options.trueLoops.length > 0) {
210
+ napiOpts.trueLoops = options.trueLoops.map((tl: TrueLoopSpec) => ({
211
+ from: parseSignalPath(tl.from),
212
+ to: parseSignalPath(tl.to),
213
+ maxIter: tl.maxIter,
214
+ }));
215
+ hasOpt = true;
216
+ }
217
+ if (options.clockType) {
218
+ napiOpts.clockType = options.clockType;
219
+ hasOpt = true;
220
+ }
221
+ if (options.resetType) {
222
+ napiOpts.resetType = options.resetType;
223
+ hasOpt = true;
224
+ }
225
+
226
+ return hasOpt ? napiOpts : undefined;
227
+ }
228
+
105
229
  // ---------------------------------------------------------------------------
106
230
  // Layout parsing helpers
107
231
  // ---------------------------------------------------------------------------
@@ -114,6 +238,7 @@ interface RawSignalLayout {
114
238
  direction: string;
115
239
  type_kind: string;
116
240
  array_dims?: number[];
241
+ associated_clock?: string;
117
242
  }
118
243
 
119
244
  /**
@@ -122,11 +247,11 @@ interface RawSignalLayout {
122
247
  * the DUT-compatible layout (without type_kind).
123
248
  */
124
249
  export function parseNapiLayout(json: string): {
125
- signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }>;
250
+ signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[]; associatedClock?: string }>;
126
251
  forDut: Record<string, SignalLayout>;
127
252
  } {
128
253
  const raw: Record<string, RawSignalLayout> = JSON.parse(json);
129
- const signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }> = {};
254
+ const signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[]; associatedClock?: string }> = {};
130
255
  const forDut: Record<string, SignalLayout> = {};
131
256
 
132
257
  for (const [name, r] of Object.entries(raw)) {
@@ -137,13 +262,16 @@ export function parseNapiLayout(json: string): {
137
262
  is4state: r.is_4state,
138
263
  direction: r.direction as "input" | "output" | "inout",
139
264
  };
140
- const entry: SignalLayout & { typeKind: string; arrayDims?: number[] } = {
265
+ const entry: SignalLayout & { typeKind: string; arrayDims?: number[]; associatedClock?: string } = {
141
266
  ...sl,
142
267
  typeKind: r.type_kind,
143
268
  };
144
269
  if (r.array_dims && r.array_dims.length > 0) {
145
270
  entry.arrayDims = r.array_dims;
146
271
  }
272
+ if (r.associated_clock) {
273
+ entry.associatedClock = r.associated_clock;
274
+ }
147
275
  signals[name] = entry;
148
276
  forDut[name] = sl;
149
277
  }
@@ -164,19 +292,14 @@ export function buildPortsFromLayout(
164
292
  for (const [name, sig] of Object.entries(signals)) {
165
293
  const typeKind = sig.typeKind;
166
294
  let portType: "clock" | "reset" | "logic" | "bit";
167
- switch (typeKind) {
168
- case "clock":
169
- portType = "clock";
170
- break;
171
- case "reset":
172
- portType = "reset";
173
- break;
174
- case "bit":
175
- portType = "bit";
176
- break;
177
- default:
178
- portType = "logic";
179
- break;
295
+ if (typeKind === "clock") {
296
+ portType = "clock";
297
+ } else if (typeKind.startsWith("reset")) {
298
+ portType = "reset";
299
+ } else if (typeKind === "bit") {
300
+ portType = "bit";
301
+ } else {
302
+ portType = "logic";
180
303
  }
181
304
 
182
305
  const port: PortInfo = {
@@ -194,6 +317,78 @@ export function buildPortsFromLayout(
194
317
  return ports;
195
318
  }
196
319
 
320
+ // ---------------------------------------------------------------------------
321
+ // Hierarchy layout
322
+ // ---------------------------------------------------------------------------
323
+
324
+ export interface HierarchyNode {
325
+ moduleName: string;
326
+ signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }>;
327
+ forDut: Record<string, SignalLayout>;
328
+ ports: Record<string, PortInfo>;
329
+ children: Record<string, HierarchyNode[]>;
330
+ }
331
+
332
+ interface RawHierarchyNode {
333
+ module_name: string;
334
+ signals: Record<string, RawSignalLayout>;
335
+ children: Record<string, RawHierarchyNode[]>;
336
+ }
337
+
338
+ /**
339
+ * Parse the hierarchy JSON from NAPI into a HierarchyNode tree.
340
+ * Converts snake_case keys to camelCase and auto-detects ports.
341
+ */
342
+ export function parseHierarchyLayout(
343
+ json: string,
344
+ events: Record<string, number>,
345
+ ): HierarchyNode {
346
+ const raw: RawHierarchyNode = JSON.parse(json);
347
+ return convertHierarchyNode(raw, events);
348
+ }
349
+
350
+ function convertHierarchyNode(
351
+ raw: RawHierarchyNode,
352
+ events: Record<string, number>,
353
+ ): HierarchyNode {
354
+ const signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }> = {};
355
+ const forDut: Record<string, SignalLayout> = {};
356
+
357
+ for (const [name, r] of Object.entries(raw.signals)) {
358
+ const sl: SignalLayout = {
359
+ offset: r.offset,
360
+ width: r.width,
361
+ byteSize: r.byte_size > 0 ? r.byte_size : Math.ceil(r.width / 8),
362
+ is4state: r.is_4state,
363
+ direction: r.direction as "input" | "output" | "inout",
364
+ };
365
+ const entry: SignalLayout & { typeKind: string; arrayDims?: number[] } = {
366
+ ...sl,
367
+ typeKind: r.type_kind,
368
+ };
369
+ if (r.array_dims && r.array_dims.length > 0) {
370
+ entry.arrayDims = r.array_dims;
371
+ }
372
+ signals[name] = entry;
373
+ forDut[name] = sl;
374
+ }
375
+
376
+ const ports = buildPortsFromLayout(signals, events);
377
+
378
+ const children: Record<string, HierarchyNode[]> = {};
379
+ for (const [name, instances] of Object.entries(raw.children)) {
380
+ children[name] = instances.map((inst) => convertHierarchyNode(inst, events));
381
+ }
382
+
383
+ return {
384
+ moduleName: raw.module_name,
385
+ signals,
386
+ forDut,
387
+ ports,
388
+ children,
389
+ };
390
+ }
391
+
197
392
  // ---------------------------------------------------------------------------
198
393
  // Handle wrapping — zero-copy direct operations
199
394
  // ---------------------------------------------------------------------------
@@ -246,6 +441,9 @@ export function wrapDirectSimulationHandle(
246
441
  time(): number {
247
442
  return raw.time();
248
443
  },
444
+ nextEventTime(): number | null {
445
+ return raw.nextEventTime();
446
+ },
249
447
  evalComb(): void {
250
448
  raw.evalComb();
251
449
  },
@@ -289,17 +487,19 @@ export function createSimulatorBridge(addon: RawNapiAddon): NativeCreateFn {
289
487
  return (
290
488
  source: string,
291
489
  moduleName: string,
292
- _options: SimulatorOptions,
490
+ options: SimulatorOptions,
293
491
  ): CreateResult<NativeSimulatorHandle> => {
294
- const raw = new addon.NativeSimulatorHandle(source, moduleName);
492
+ const napiOpts = buildNapiOpts(options);
493
+ const raw = new addon.NativeSimulatorHandle(source, moduleName, napiOpts);
295
494
 
296
495
  const layout = parseLegacyLayout(raw.layoutJson);
297
496
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
497
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
298
498
 
299
499
  const buf = raw.sharedMemory().buffer;
300
500
  const handle = wrapDirectSimulatorHandle(raw);
301
501
 
302
- return { buffer: buf, layout, events, handle };
502
+ return { buffer: buf, layout, events, handle, hierarchy };
303
503
  };
304
504
  }
305
505
 
@@ -311,16 +511,18 @@ export function createSimulationBridge(addon: RawNapiAddon): NativeCreateSimulat
311
511
  return (
312
512
  source: string,
313
513
  moduleName: string,
314
- _options: SimulatorOptions,
514
+ options: SimulatorOptions,
315
515
  ): CreateResult<NativeSimulationHandle> => {
316
- const raw = new addon.NativeSimulationHandle(source, moduleName);
516
+ const napiOpts = buildNapiOpts(options);
517
+ const raw = new addon.NativeSimulationHandle(source, moduleName, napiOpts);
317
518
 
318
519
  const layout = parseLegacyLayout(raw.layoutJson);
319
520
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
521
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
320
522
 
321
523
  const buf = raw.sharedMemory().buffer;
322
524
  const handle = wrapDirectSimulationHandle(raw);
323
525
 
324
- return { buffer: buf, layout, events, handle };
526
+ return { buffer: buf, layout, events, handle, hierarchy };
325
527
  };
326
528
  }
@@ -5,6 +5,7 @@ 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
@@ -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,
@@ -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 = 42;
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
  });