@celox-sim/celox 0.1.13 → 0.1.14

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.
package/src/e2e.test.ts CHANGED
@@ -9,22 +9,25 @@
9
9
  */
10
10
 
11
11
  import path from "node:path";
12
- import { describe, test, expect, afterEach } from "vitest";
13
- import { Simulator } from "./simulator.js";
14
- import { Simulation } from "./simulation.js";
12
+ import { afterEach, describe, expect, test } from "vitest";
15
13
  import { readFourState } from "./dut.js";
16
- import { X, FourState, SimulationTimeoutError } from "./types.js";
17
14
  import {
18
- createSimulatorBridge,
19
- loadNativeAddon,
20
- parseNapiLayout,
21
- parseSignalPath,
22
- type RawNapiAddon,
23
- type RawNapiSimulatorHandle,
15
+ createSimulatorBridge,
16
+ loadNativeAddon,
17
+ parseNapiLayout,
18
+ parseSignalPath,
19
+ type RawNapiAddon,
20
+ type RawNapiSimulatorHandle,
24
21
  } from "./napi-helpers.js";
22
+ import { Simulation } from "./simulation.js";
23
+ import { Simulator } from "./simulator.js";
24
+ import { FourState, SimulationTimeoutError, X } from "./types.js";
25
25
 
26
26
  // Fixture project directories
27
- const FIXTURES_DIR = path.resolve(import.meta.dirname ?? __dirname, "../fixtures");
27
+ const FIXTURES_DIR = path.resolve(
28
+ import.meta.dirname ?? __dirname,
29
+ "../fixtures",
30
+ );
28
31
  const ADDER_PROJECT = path.join(FIXTURES_DIR, "adder");
29
32
  const COUNTER_PROJECT = path.join(FIXTURES_DIR, "counter_project");
30
33
  const CELOX_TOML_PROJECT = path.join(FIXTURES_DIR, "celox_toml");
@@ -96,157 +99,157 @@ module Mux4 (
96
99
  // ---------------------------------------------------------------------------
97
100
 
98
101
  describe("E2E: Simulator.fromSource (event-based)", () => {
99
- test("combinational adder: a + b = sum", () => {
100
- interface AdderPorts {
101
- rst: bigint;
102
- a: bigint;
103
- b: bigint;
104
- readonly sum: bigint;
105
- }
106
-
107
- const sim = Simulator.fromSource<AdderPorts>(ADDER_SOURCE, "Adder");
108
-
109
- sim.dut.a = 100n;
110
- sim.dut.b = 200n;
111
- sim.tick();
112
- expect(sim.dut.sum).toBe(300n);
113
-
114
- sim.dut.a = 0xFFFFn;
115
- sim.dut.b = 1n;
116
- sim.tick();
117
- expect(sim.dut.sum).toBe(0x10000n);
118
-
119
- sim.dut.a = 0n;
120
- sim.dut.b = 0n;
121
- sim.tick();
122
- expect(sim.dut.sum).toBe(0n);
123
-
124
- sim.dispose();
125
- });
126
-
127
- test("combinational adder: lazy evalComb on output read", () => {
128
- interface AdderPorts {
129
- rst: bigint;
130
- a: bigint;
131
- b: bigint;
132
- readonly sum: bigint;
133
- }
134
-
135
- const sim = Simulator.fromSource<AdderPorts>(ADDER_SOURCE, "Adder");
136
-
137
- sim.dut.a = 42n;
138
- sim.dut.b = 58n;
139
- expect(sim.dut.sum).toBe(100n);
140
-
141
- sim.dispose();
142
- });
143
-
144
- test("sequential counter: counts on clock edges", () => {
145
- interface CounterPorts {
146
- rst: bigint;
147
- en: bigint;
148
- readonly count: bigint;
149
- }
150
-
151
- const sim = Simulator.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
152
-
153
- // Reset the counter (default async_low: rst=0 is active)
154
- sim.dut.rst = 0n;
155
- sim.tick();
156
- sim.dut.rst = 1n;
157
- sim.tick();
158
- expect(sim.dut.count).toBe(0n);
159
-
160
- // Enable counting
161
- sim.dut.en = 1n;
162
- sim.tick();
163
- expect(sim.dut.count).toBe(1n);
164
-
165
- sim.tick();
166
- expect(sim.dut.count).toBe(2n);
167
-
168
- sim.tick();
169
- expect(sim.dut.count).toBe(3n);
170
-
171
- // Disable counting
172
- sim.dut.en = 0n;
173
- sim.tick();
174
- expect(sim.dut.count).toBe(3n);
175
-
176
- // Re-enable
177
- sim.dut.en = 1n;
178
- sim.tick(5);
179
- expect(sim.dut.count).toBe(8n);
180
-
181
- sim.dispose();
182
- });
183
-
184
- test("always_comb evaluated before FF on tick (no prior output read)", () => {
185
- interface CounterPorts {
186
- rst: number;
187
- en: number;
188
- readonly count: number;
189
- }
190
-
191
- const sim = Simulator.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
192
-
193
- // Reset (default async_low: rst=0 is active)
194
- sim.dut.rst = 0;
195
- sim.tick();
196
- sim.dut.rst = 1;
197
- sim.tick();
198
-
199
- // Write input and tick WITHOUT reading any output first.
200
- // This catches the stale-comb bug where always_ff reads stale
201
- // combinational values because eval_comb was skipped before FF.
202
- sim.dut.en = 1;
203
- sim.tick();
204
- expect(Number(sim.dut.count)).toBe(1);
205
-
206
- // Again: set input, tick, verify — no intermediate reads
207
- sim.dut.en = 1;
208
- sim.tick();
209
- expect(Number(sim.dut.count)).toBe(2);
210
-
211
- // Disable, tick, verify
212
- sim.dut.en = 0;
213
- sim.tick();
214
- expect(Number(sim.dut.count)).toBe(2);
215
-
216
- sim.dispose();
217
- });
218
-
219
- test("combinational multiplexer", () => {
220
- interface Mux4Ports {
221
- sel: bigint;
222
- d0: bigint;
223
- d1: bigint;
224
- d2: bigint;
225
- d3: bigint;
226
- readonly y: bigint;
227
- }
228
-
229
- const sim = Simulator.fromSource<Mux4Ports>(MULTIPLEXER_SOURCE, "Mux4");
230
-
231
- sim.dut.d0 = 0xAAn;
232
- sim.dut.d1 = 0xBBn;
233
- sim.dut.d2 = 0xCCn;
234
- sim.dut.d3 = 0xDDn;
235
-
236
- sim.dut.sel = 0n;
237
- expect(sim.dut.y).toBe(0xAAn);
238
-
239
- sim.dut.sel = 1n;
240
- expect(sim.dut.y).toBe(0xBBn);
241
-
242
- sim.dut.sel = 2n;
243
- expect(sim.dut.y).toBe(0xCCn);
244
-
245
- sim.dut.sel = 3n;
246
- expect(sim.dut.y).toBe(0xDDn);
247
-
248
- sim.dispose();
249
- });
102
+ test("combinational adder: a + b = sum", () => {
103
+ interface AdderPorts {
104
+ rst: bigint;
105
+ a: bigint;
106
+ b: bigint;
107
+ readonly sum: bigint;
108
+ }
109
+
110
+ const sim = Simulator.fromSource<AdderPorts>(ADDER_SOURCE, "Adder");
111
+
112
+ sim.dut.a = 100n;
113
+ sim.dut.b = 200n;
114
+ sim.tick();
115
+ expect(sim.dut.sum).toBe(300n);
116
+
117
+ sim.dut.a = 0xffffn;
118
+ sim.dut.b = 1n;
119
+ sim.tick();
120
+ expect(sim.dut.sum).toBe(0x10000n);
121
+
122
+ sim.dut.a = 0n;
123
+ sim.dut.b = 0n;
124
+ sim.tick();
125
+ expect(sim.dut.sum).toBe(0n);
126
+
127
+ sim.dispose();
128
+ });
129
+
130
+ test("combinational adder: lazy evalComb on output read", () => {
131
+ interface AdderPorts {
132
+ rst: bigint;
133
+ a: bigint;
134
+ b: bigint;
135
+ readonly sum: bigint;
136
+ }
137
+
138
+ const sim = Simulator.fromSource<AdderPorts>(ADDER_SOURCE, "Adder");
139
+
140
+ sim.dut.a = 42n;
141
+ sim.dut.b = 58n;
142
+ expect(sim.dut.sum).toBe(100n);
143
+
144
+ sim.dispose();
145
+ });
146
+
147
+ test("sequential counter: counts on clock edges", () => {
148
+ interface CounterPorts {
149
+ rst: bigint;
150
+ en: bigint;
151
+ readonly count: bigint;
152
+ }
153
+
154
+ const sim = Simulator.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
155
+
156
+ // Reset the counter (default async_low: rst=0 is active)
157
+ sim.dut.rst = 0n;
158
+ sim.tick();
159
+ sim.dut.rst = 1n;
160
+ sim.tick();
161
+ expect(sim.dut.count).toBe(0n);
162
+
163
+ // Enable counting
164
+ sim.dut.en = 1n;
165
+ sim.tick();
166
+ expect(sim.dut.count).toBe(1n);
167
+
168
+ sim.tick();
169
+ expect(sim.dut.count).toBe(2n);
170
+
171
+ sim.tick();
172
+ expect(sim.dut.count).toBe(3n);
173
+
174
+ // Disable counting
175
+ sim.dut.en = 0n;
176
+ sim.tick();
177
+ expect(sim.dut.count).toBe(3n);
178
+
179
+ // Re-enable
180
+ sim.dut.en = 1n;
181
+ sim.tick(5);
182
+ expect(sim.dut.count).toBe(8n);
183
+
184
+ sim.dispose();
185
+ });
186
+
187
+ test("always_comb evaluated before FF on tick (no prior output read)", () => {
188
+ interface CounterPorts {
189
+ rst: number;
190
+ en: number;
191
+ readonly count: number;
192
+ }
193
+
194
+ const sim = Simulator.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
195
+
196
+ // Reset (default async_low: rst=0 is active)
197
+ sim.dut.rst = 0;
198
+ sim.tick();
199
+ sim.dut.rst = 1;
200
+ sim.tick();
201
+
202
+ // Write input and tick WITHOUT reading any output first.
203
+ // This catches the stale-comb bug where always_ff reads stale
204
+ // combinational values because eval_comb was skipped before FF.
205
+ sim.dut.en = 1;
206
+ sim.tick();
207
+ expect(Number(sim.dut.count)).toBe(1);
208
+
209
+ // Again: set input, tick, verify — no intermediate reads
210
+ sim.dut.en = 1;
211
+ sim.tick();
212
+ expect(Number(sim.dut.count)).toBe(2);
213
+
214
+ // Disable, tick, verify
215
+ sim.dut.en = 0;
216
+ sim.tick();
217
+ expect(Number(sim.dut.count)).toBe(2);
218
+
219
+ sim.dispose();
220
+ });
221
+
222
+ test("combinational multiplexer", () => {
223
+ interface Mux4Ports {
224
+ sel: bigint;
225
+ d0: bigint;
226
+ d1: bigint;
227
+ d2: bigint;
228
+ d3: bigint;
229
+ readonly y: bigint;
230
+ }
231
+
232
+ const sim = Simulator.fromSource<Mux4Ports>(MULTIPLEXER_SOURCE, "Mux4");
233
+
234
+ sim.dut.d0 = 0xaan;
235
+ sim.dut.d1 = 0xbbn;
236
+ sim.dut.d2 = 0xccn;
237
+ sim.dut.d3 = 0xddn;
238
+
239
+ sim.dut.sel = 0n;
240
+ expect(sim.dut.y).toBe(0xaan);
241
+
242
+ sim.dut.sel = 1n;
243
+ expect(sim.dut.y).toBe(0xbbn);
244
+
245
+ sim.dut.sel = 2n;
246
+ expect(sim.dut.y).toBe(0xccn);
247
+
248
+ sim.dut.sel = 3n;
249
+ expect(sim.dut.y).toBe(0xddn);
250
+
251
+ sim.dispose();
252
+ });
250
253
  });
251
254
 
252
255
  // ---------------------------------------------------------------------------
@@ -254,32 +257,32 @@ describe("E2E: Simulator.fromSource (event-based)", () => {
254
257
  // ---------------------------------------------------------------------------
255
258
 
256
259
  describe("E2E: Simulation.fromSource (time-based)", () => {
257
- test("counter with timed clock: step-by-step", () => {
258
- interface CounterPorts {
259
- rst: bigint;
260
- en: bigint;
261
- readonly count: bigint;
262
- }
260
+ test("counter with timed clock: step-by-step", () => {
261
+ interface CounterPorts {
262
+ rst: bigint;
263
+ en: bigint;
264
+ readonly count: bigint;
265
+ }
263
266
 
264
- const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
267
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
265
268
 
266
- sim.addClock("clk", { period: 10 });
267
- expect(sim.time()).toBe(0);
269
+ sim.addClock("clk", { period: 10 });
270
+ expect(sim.time()).toBe(0);
268
271
 
269
- // Reset (default async_low: rst=0 is active)
270
- sim.dut.rst = 0n;
271
- sim.runUntil(20);
272
- sim.dut.rst = 1n;
273
- sim.dut.en = 1n;
272
+ // Reset (default async_low: rst=0 is active)
273
+ sim.dut.rst = 0n;
274
+ sim.runUntil(20);
275
+ sim.dut.rst = 1n;
276
+ sim.dut.en = 1n;
274
277
 
275
- sim.runUntil(100);
278
+ sim.runUntil(100);
276
279
 
277
- const count = sim.dut.count;
278
- expect(count).toBeGreaterThan(0n);
279
- expect(sim.time()).toBe(100);
280
+ const count = sim.dut.count;
281
+ expect(count).toBeGreaterThan(0n);
282
+ expect(sim.time()).toBe(100);
280
283
 
281
- sim.dispose();
282
- });
284
+ sim.dispose();
285
+ });
283
286
  });
284
287
 
285
288
  // ---------------------------------------------------------------------------
@@ -287,58 +290,58 @@ describe("E2E: Simulation.fromSource (time-based)", () => {
287
290
  // ---------------------------------------------------------------------------
288
291
 
289
292
  describe("E2E: Simulator.fromProject (event-based)", () => {
290
- test("combinational adder from project directory", () => {
291
- interface AdderPorts {
292
- rst: bigint;
293
- a: bigint;
294
- b: bigint;
295
- readonly sum: bigint;
296
- }
297
-
298
- const sim = Simulator.fromProject<AdderPorts>(ADDER_PROJECT, "Adder");
299
-
300
- sim.dut.a = 100n;
301
- sim.dut.b = 200n;
302
- sim.tick();
303
- expect(sim.dut.sum).toBe(300n);
304
-
305
- sim.dut.a = 0xFFFFn;
306
- sim.dut.b = 1n;
307
- sim.tick();
308
- expect(sim.dut.sum).toBe(0x10000n);
309
-
310
- sim.dispose();
311
- });
312
-
313
- test("sequential counter from project directory", () => {
314
- interface CounterPorts {
315
- rst: bigint;
316
- en: bigint;
317
- readonly count: bigint;
318
- }
319
-
320
- const sim = Simulator.fromProject<CounterPorts>(COUNTER_PROJECT, "Counter");
321
-
322
- // Reset the counter (default async_low: rst=0 is active)
323
- sim.dut.rst = 0n;
324
- sim.tick();
325
- sim.dut.rst = 1n;
326
- sim.tick();
327
- expect(sim.dut.count).toBe(0n);
328
-
329
- // Enable counting
330
- sim.dut.en = 1n;
331
- sim.tick();
332
- expect(sim.dut.count).toBe(1n);
333
-
334
- sim.tick();
335
- expect(sim.dut.count).toBe(2n);
336
-
337
- sim.tick();
338
- expect(sim.dut.count).toBe(3n);
339
-
340
- sim.dispose();
341
- });
293
+ test("combinational adder from project directory", () => {
294
+ interface AdderPorts {
295
+ rst: bigint;
296
+ a: bigint;
297
+ b: bigint;
298
+ readonly sum: bigint;
299
+ }
300
+
301
+ const sim = Simulator.fromProject<AdderPorts>(ADDER_PROJECT, "Adder");
302
+
303
+ sim.dut.a = 100n;
304
+ sim.dut.b = 200n;
305
+ sim.tick();
306
+ expect(sim.dut.sum).toBe(300n);
307
+
308
+ sim.dut.a = 0xffffn;
309
+ sim.dut.b = 1n;
310
+ sim.tick();
311
+ expect(sim.dut.sum).toBe(0x10000n);
312
+
313
+ sim.dispose();
314
+ });
315
+
316
+ test("sequential counter from project directory", () => {
317
+ interface CounterPorts {
318
+ rst: bigint;
319
+ en: bigint;
320
+ readonly count: bigint;
321
+ }
322
+
323
+ const sim = Simulator.fromProject<CounterPorts>(COUNTER_PROJECT, "Counter");
324
+
325
+ // Reset the counter (default async_low: rst=0 is active)
326
+ sim.dut.rst = 0n;
327
+ sim.tick();
328
+ sim.dut.rst = 1n;
329
+ sim.tick();
330
+ expect(sim.dut.count).toBe(0n);
331
+
332
+ // Enable counting
333
+ sim.dut.en = 1n;
334
+ sim.tick();
335
+ expect(sim.dut.count).toBe(1n);
336
+
337
+ sim.tick();
338
+ expect(sim.dut.count).toBe(2n);
339
+
340
+ sim.tick();
341
+ expect(sim.dut.count).toBe(3n);
342
+
343
+ sim.dispose();
344
+ });
342
345
  });
343
346
 
344
347
  // ---------------------------------------------------------------------------
@@ -346,32 +349,35 @@ describe("E2E: Simulator.fromProject (event-based)", () => {
346
349
  // ---------------------------------------------------------------------------
347
350
 
348
351
  describe("E2E: Simulation.fromProject (time-based)", () => {
349
- test("counter with timed clock from project directory", () => {
350
- interface CounterPorts {
351
- rst: bigint;
352
- en: bigint;
353
- readonly count: bigint;
354
- }
355
-
356
- const sim = Simulation.fromProject<CounterPorts>(COUNTER_PROJECT, "Counter");
357
-
358
- sim.addClock("clk", { period: 10 });
359
- expect(sim.time()).toBe(0);
360
-
361
- // Reset (default async_low: rst=0 is active)
362
- sim.dut.rst = 0n;
363
- sim.runUntil(20);
364
- sim.dut.rst = 1n;
365
- sim.dut.en = 1n;
366
-
367
- sim.runUntil(100);
368
-
369
- const count = sim.dut.count;
370
- expect(count).toBeGreaterThan(0n);
371
- expect(sim.time()).toBe(100);
372
-
373
- sim.dispose();
374
- });
352
+ test("counter with timed clock from project directory", () => {
353
+ interface CounterPorts {
354
+ rst: bigint;
355
+ en: bigint;
356
+ readonly count: bigint;
357
+ }
358
+
359
+ const sim = Simulation.fromProject<CounterPorts>(
360
+ COUNTER_PROJECT,
361
+ "Counter",
362
+ );
363
+
364
+ sim.addClock("clk", { period: 10 });
365
+ expect(sim.time()).toBe(0);
366
+
367
+ // Reset (default async_low: rst=0 is active)
368
+ sim.dut.rst = 0n;
369
+ sim.runUntil(20);
370
+ sim.dut.rst = 1n;
371
+ sim.dut.en = 1n;
372
+
373
+ sim.runUntil(100);
374
+
375
+ const count = sim.dut.count;
376
+ expect(count).toBeGreaterThan(0n);
377
+ expect(sim.time()).toBe(100);
378
+
379
+ sim.dispose();
380
+ });
375
381
  });
376
382
 
377
383
  // ---------------------------------------------------------------------------
@@ -379,41 +385,41 @@ describe("E2E: Simulation.fromProject (time-based)", () => {
379
385
  // ---------------------------------------------------------------------------
380
386
 
381
387
  describe("E2E: Simulator.create (backward compat)", () => {
382
- test("combinational adder via Simulator.create()", () => {
383
- interface AdderPorts {
384
- rst: bigint;
385
- a: bigint;
386
- b: bigint;
387
- readonly sum: bigint;
388
- }
389
-
390
- const addon = loadNativeAddon();
391
- const nativeCreateSimulator = createSimulatorBridge(addon);
392
-
393
- const sim = Simulator.create<AdderPorts>(
394
- {
395
- __celox_module: true,
396
- name: "Adder",
397
- source: ADDER_SOURCE,
398
- ports: {
399
- clk: { direction: "input", type: "clock", width: 1 },
400
- rst: { direction: "input", type: "reset", width: 1 },
401
- a: { direction: "input", type: "logic", width: 16 },
402
- b: { direction: "input", type: "logic", width: 16 },
403
- sum: { direction: "output", type: "logic", width: 17 },
404
- },
405
- events: ["clk"],
406
- },
407
- { __nativeCreate: nativeCreateSimulator },
408
- );
409
-
410
- sim.dut.a = 100n;
411
- sim.dut.b = 200n;
412
- sim.tick();
413
- expect(sim.dut.sum).toBe(300n);
414
-
415
- sim.dispose();
416
- });
388
+ test("combinational adder via Simulator.create()", () => {
389
+ interface AdderPorts {
390
+ rst: bigint;
391
+ a: bigint;
392
+ b: bigint;
393
+ readonly sum: bigint;
394
+ }
395
+
396
+ const addon = loadNativeAddon();
397
+ const nativeCreateSimulator = createSimulatorBridge(addon);
398
+
399
+ const sim = Simulator.create<AdderPorts>(
400
+ {
401
+ __celox_module: true,
402
+ name: "Adder",
403
+ source: ADDER_SOURCE,
404
+ ports: {
405
+ clk: { direction: "input", type: "clock", width: 1 },
406
+ rst: { direction: "input", type: "reset", width: 1 },
407
+ a: { direction: "input", type: "logic", width: 16 },
408
+ b: { direction: "input", type: "logic", width: 16 },
409
+ sum: { direction: "output", type: "logic", width: 17 },
410
+ },
411
+ events: ["clk"],
412
+ },
413
+ { __nativeCreate: nativeCreateSimulator },
414
+ );
415
+
416
+ sim.dut.a = 100n;
417
+ sim.dut.b = 200n;
418
+ sim.tick();
419
+ expect(sim.dut.sum).toBe(300n);
420
+
421
+ sim.dispose();
422
+ });
417
423
  });
418
424
 
419
425
  // ---------------------------------------------------------------------------
@@ -439,7 +445,7 @@ module LogicBitMix (
439
445
  y_logic_from_bit: output logic<8>,
440
446
  y_bit_from_logic: output bit<8>,
441
447
  ) {
442
- assign y_bit_from_logic = a_logic;
448
+ assign y_bit_from_logic = a_logic as u8;
443
449
  assign y_logic_from_bit = b_bit;
444
450
  }
445
451
  `;
@@ -472,325 +478,347 @@ module Adder4S (
472
478
  `;
473
479
 
474
480
  describe("E2E: 4-state simulation", () => {
475
- let raw: RawNapiSimulatorHandle | undefined;
476
- let addon: RawNapiAddon;
477
-
478
- try {
479
- addon = loadNativeAddon();
480
- } catch (e) {
481
- throw new Error(`Failed to load NAPI addon for 4-state tests: ${e}`);
482
- }
483
-
484
- afterEach(() => {
485
- raw?.dispose();
486
- raw = undefined;
487
- });
488
-
489
- test("initial values: logic ports start as X, bit ports start as 0", () => {
490
- const source = `
481
+ let raw: RawNapiSimulatorHandle | undefined;
482
+ let addon: RawNapiAddon;
483
+
484
+ try {
485
+ addon = loadNativeAddon();
486
+ } catch (e) {
487
+ throw new Error(`Failed to load NAPI addon for 4-state tests: ${e}`);
488
+ }
489
+
490
+ afterEach(() => {
491
+ raw?.dispose();
492
+ raw = undefined;
493
+ });
494
+
495
+ test("initial values: logic ports start as X, bit ports start as 0", () => {
496
+ const source = `
491
497
  module InitTest (
492
498
  a: input logic<8>,
493
499
  b: input bit<8>,
494
500
  ) {}
495
501
  `;
496
- raw = new addon.NativeSimulatorHandle(source, "InitTest", { fourState: true });
497
- const layout = parseNapiLayout(raw.layoutJson);
498
- const buf = raw.sharedMemory().buffer;
499
-
500
- // logic port should have mask=0xFF (all X), X encoding: v=1, m=1
501
- const [valA, maskA] = readFourState(buf, layout.forDut.a);
502
- expect(valA).toBe(0xFFn);
503
- expect(maskA).toBe(0xFFn);
504
-
505
- // bit port should have mask=0 (defined)
506
- // bit is not 4-state, so no mask — reading its value should be 0
507
- expect(layout.forDut.b.is4state).toBe(false);
508
- });
509
-
510
- test("writing X clears value and sets mask", () => {
511
- interface Ports {
512
- a: bigint;
513
- readonly y_and: bigint;
514
- }
515
-
516
- const sim = Simulator.fromSource<Ports>(AND_OR_SOURCE, "AndOr", { fourState: true });
517
- raw = undefined; // sim manages its own handle
518
-
519
- // Write X to input 'a' via DUT
520
- (sim.dut as any).a = X;
521
-
522
- // We can't inspect mask through DUT getter (it only returns value),
523
- // so this test verifies X write doesn't throw and propagation works.
524
- // For detailed mask inspection, see the raw NAPI tests below.
525
- sim.dispose();
526
- });
527
-
528
- test("AND: 0 & X = 0 (dominant zero)", () => {
529
- raw = new addon.NativeSimulatorHandle(AND_OR_SOURCE, "AndOr", { fourState: true });
530
- const layout = parseNapiLayout(raw.layoutJson);
531
- const buf = raw.sharedMemory().buffer;
532
- const view = new DataView(buf);
533
- const events: Record<string, number> = JSON.parse(raw.eventsJson);
534
-
535
- const sigA = layout.forDut.a;
536
- const sigB = layout.forDut.b;
537
- const sigYAnd = layout.forDut.y_and;
538
- const sigYOr = layout.forDut.y_or;
539
-
540
- // a = 0 (value=0, mask=0)
541
- view.setUint8(sigA.offset, 0);
542
- view.setUint8(sigA.offset + sigA.byteSize, 0);
543
-
544
- // b = X (value=0, mask=1)
545
- view.setUint8(sigB.offset, 0);
546
- view.setUint8(sigB.offset + sigB.byteSize, 1);
547
-
548
- raw.evalComb();
549
-
550
- // 0 & X = 0 (mask should be 0 — dominant zero)
551
- const [vAnd, mAnd] = readFourState(buf, sigYAnd);
552
- expect(vAnd).toBe(0n);
553
- expect(mAnd).toBe(0n);
554
-
555
- // 0 | X = X (mask should be 1)
556
- const [vOr, mOr] = readFourState(buf, sigYOr);
557
- expect(vOr).toBe(0n);
558
- expect(mOr).toBe(1n);
559
- });
560
-
561
- test("OR: 1 | X = 1 (dominant one)", () => {
562
- raw = new addon.NativeSimulatorHandle(AND_OR_SOURCE, "AndOr", { fourState: true });
563
- const layout = parseNapiLayout(raw.layoutJson);
564
- const buf = raw.sharedMemory().buffer;
565
- const view = new DataView(buf);
566
-
567
- const sigA = layout.forDut.a;
568
- const sigB = layout.forDut.b;
569
- const sigYOr = layout.forDut.y_or;
570
-
571
- // a = 1 (value=1, mask=0)
572
- view.setUint8(sigA.offset, 1);
573
- view.setUint8(sigA.offset + sigA.byteSize, 0);
574
-
575
- // b = X (value=0, mask=1)
576
- view.setUint8(sigB.offset, 0);
577
- view.setUint8(sigB.offset + sigB.byteSize, 1);
578
-
579
- raw.evalComb();
580
-
581
- // 1 | X = 1 (mask should be 0 — dominant one)
582
- const [vOr, mOr] = readFourState(buf, sigYOr);
583
- expect(vOr).toBe(1n);
584
- expect(mOr).toBe(0n);
585
- });
586
-
587
- test("logic-to-bit assignment strips X mask", () => {
588
- raw = new addon.NativeSimulatorHandle(LOGIC_BIT_MIX_SOURCE, "LogicBitMix", { fourState: true });
589
- const layout = parseNapiLayout(raw.layoutJson);
590
- const buf = raw.sharedMemory().buffer;
591
- const view = new DataView(buf);
592
-
593
- const sigALogic = layout.forDut.a_logic;
594
- const sigYBitFromLogic = layout.forDut.y_bit_from_logic;
595
-
596
- // a_logic = all-X (value=0, mask=0xFF)
597
- view.setUint8(sigALogic.offset, 0);
598
- view.setUint8(sigALogic.offset + sigALogic.byteSize, 0xFF);
599
-
600
- raw.evalComb();
601
-
602
- // y_bit_from_logic is bit type — X should be stripped (mask=0)
603
- expect(sigYBitFromLogic.is4state).toBe(false);
604
- });
605
-
606
- test("bit-to-logic assignment has no X", () => {
607
- raw = new addon.NativeSimulatorHandle(LOGIC_BIT_MIX_SOURCE, "LogicBitMix", { fourState: true });
608
- const layout = parseNapiLayout(raw.layoutJson);
609
- const buf = raw.sharedMemory().buffer;
610
- const view = new DataView(buf);
611
-
612
- const sigBBit = layout.forDut.b_bit;
613
- const sigYLogicFromBit = layout.forDut.y_logic_from_bit;
614
-
615
- // b_bit = 0xAA (bit type, always defined)
616
- view.setUint8(sigBBit.offset, 0xAA);
617
-
618
- raw.evalComb();
619
-
620
- // y_logic_from_bit should be 0xAA with mask=0
621
- const [vLogic, mLogic] = readFourState(buf, sigYLogicFromBit);
622
- expect(vLogic).toBe(0xAAn);
623
- expect(mLogic).toBe(0n);
624
- });
625
-
626
- test("arithmetic with X produces all-X output", () => {
627
- raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
628
- const layout = parseNapiLayout(raw.layoutJson);
629
- const buf = raw.sharedMemory().buffer;
630
- const view = new DataView(buf);
631
-
632
- const sigA = layout.forDut.a;
633
- const sigB = layout.forDut.b;
634
- const sigY = layout.forDut.y;
635
-
636
- // a = 42 (defined), b = X (all X)
637
- view.setUint8(sigA.offset, 42);
638
- view.setUint8(sigA.offset + sigA.byteSize, 0); // mask=0
639
-
640
- view.setUint8(sigB.offset, 0);
641
- view.setUint8(sigB.offset + sigB.byteSize, 0xFF); // mask=0xFF
642
-
643
- raw.evalComb();
644
-
645
- // a + X = all-X
646
- const [, mY] = readFourState(buf, sigY);
647
- expect(mY).toBe(0xFFn);
648
- });
649
-
650
- test("defined inputs in 4-state mode behave like 2-state", () => {
651
- raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
652
- const layout = parseNapiLayout(raw.layoutJson);
653
- const buf = raw.sharedMemory().buffer;
654
- const view = new DataView(buf);
655
-
656
- const sigA = layout.forDut.a;
657
- const sigB = layout.forDut.b;
658
- const sigY = layout.forDut.y;
659
-
660
- // a = 100 (defined), b = 55 (defined)
661
- view.setUint8(sigA.offset, 100);
662
- view.setUint8(sigA.offset + sigA.byteSize, 0);
663
-
664
- view.setUint8(sigB.offset, 55);
665
- view.setUint8(sigB.offset + sigB.byteSize, 0);
666
-
667
- raw.evalComb();
668
-
669
- const [vY, mY] = readFourState(buf, sigY);
670
- expect(vY).toBe(155n);
671
- expect(mY).toBe(0n);
672
- });
673
-
674
- test("FF captures X from input, reset clears X", () => {
675
- raw = new addon.NativeSimulatorHandle(FF_SOURCE, "FF", { fourState: true });
676
- const layout = parseNapiLayout(raw.layoutJson);
677
- const buf = raw.sharedMemory().buffer;
678
- const view = new DataView(buf);
679
- const events: Record<string, number> = JSON.parse(raw.eventsJson);
680
-
681
- const sigRst = layout.forDut.rst;
682
- const sigD = layout.forDut.d;
683
- const sigQ = layout.forDut.q;
684
- const clkEventId = events.clk;
685
-
686
- // 1. Assert reset (default async_low: rst=0 is active), d=X
687
- view.setUint8(sigRst.offset, 0);
688
- view.setUint8(sigRst.offset + sigRst.byteSize, 0); // rst is defined
689
-
690
- view.setUint8(sigD.offset, 0);
691
- view.setUint8(sigD.offset + sigD.byteSize, 0xFF); // d = all-X
692
-
693
- raw.tick(clkEventId);
694
-
695
- // After reset, q should be 0 with mask=0
696
- const [vQ1, mQ1] = readFourState(buf, sigQ);
697
- expect(vQ1).toBe(0n);
698
- expect(mQ1).toBe(0n);
699
-
700
- // 2. Release reset (rst=1 is inactive), d = partial X (value=0xA5, mask=0x0F)
701
- view.setUint8(sigRst.offset, 1);
702
- view.setUint8(sigRst.offset + sigRst.byteSize, 0);
703
-
704
- view.setUint8(sigD.offset, 0xA5);
705
- view.setUint8(sigD.offset + sigD.byteSize, 0x0F);
706
-
707
- raw.tick(clkEventId);
708
-
709
- // FF should capture X mask from d
710
- const [, mQ2] = readFourState(buf, sigQ);
711
- expect(mQ2).toBe(0x0Fn);
712
-
713
- // 3. Assert reset again (rst=0): should clear X
714
- view.setUint8(sigRst.offset, 0);
715
- view.setUint8(sigRst.offset + sigRst.byteSize, 0);
716
-
717
- raw.tick(clkEventId);
718
-
719
- const [vQ3, mQ3] = readFourState(buf, sigQ);
720
- expect(vQ3).toBe(0n);
721
- expect(mQ3).toBe(0n);
722
- });
723
-
724
- test("FourState write through DUT sets value and mask", () => {
725
- raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
726
- const layout = parseNapiLayout(raw.layoutJson);
727
- const buf = raw.sharedMemory().buffer;
728
- const view = new DataView(buf);
729
-
730
- const sigA = layout.forDut.a;
731
-
732
- // Write via DUT-style: FourState(0b1010_0101, 0b0000_1111)
733
- // value=0xA5, mask=0x0F means lower 4 bits are X
734
- view.setUint8(sigA.offset, 0xA5);
735
- view.setUint8(sigA.offset + sigA.byteSize, 0x0F);
736
-
737
- const [vA, mA] = readFourState(buf, sigA);
738
- expect(vA).toBe(0xA5n);
739
- expect(mA).toBe(0x0Fn);
740
- });
741
-
742
- test("setting defined value clears X mask", () => {
743
- raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
744
- const layout = parseNapiLayout(raw.layoutJson);
745
- const buf = raw.sharedMemory().buffer;
746
- const view = new DataView(buf);
747
-
748
- const sigA = layout.forDut.a;
749
-
750
- // Start with X
751
- view.setUint8(sigA.offset, 0);
752
- view.setUint8(sigA.offset + sigA.byteSize, 0xFF);
753
-
754
- const [, mBefore] = readFourState(buf, sigA);
755
- expect(mBefore).toBe(0xFFn);
756
-
757
- // Write a defined value (clear mask)
758
- view.setUint8(sigA.offset, 42);
759
- view.setUint8(sigA.offset + sigA.byteSize, 0);
760
-
761
- const [vAfter, mAfter] = readFourState(buf, sigA);
762
- expect(vAfter).toBe(42n);
763
- expect(mAfter).toBe(0n);
764
- });
765
-
766
- test("4-state through DUT high-level API (fromSource with fourState)", () => {
767
- interface Ports {
768
- a: bigint;
769
- b: bigint;
770
- readonly y: bigint;
771
- }
772
-
773
- const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
774
-
775
- // Write defined values — should behave like 2-state
776
- sim.dut.a = 100n;
777
- sim.dut.b = 55n;
778
- expect(sim.dut.y).toBe(155n);
779
-
780
- // Write X to a — output should propagate X (value reads as 0)
781
- (sim.dut as any).a = X;
782
- // After writing X, the value part of 'y' is implementation-defined
783
- // but the read should not throw
784
- const _yVal = sim.dut.y;
785
- expect(typeof _yVal).toBe("bigint");
786
-
787
- // Write FourState with partial X
788
- (sim.dut as any).a = FourState(0xA0, 0x0F);
789
- const _yVal2 = sim.dut.y;
790
- expect(typeof _yVal2).toBe("bigint");
791
-
792
- sim.dispose();
793
- });
502
+ raw = new addon.NativeSimulatorHandle(source, "InitTest", {
503
+ fourState: true,
504
+ });
505
+ const layout = parseNapiLayout(raw.layoutJson);
506
+ const buf = raw.sharedMemory().buffer;
507
+
508
+ // logic port should have mask=0xFF (all X), X encoding: v=1, m=1
509
+ const [valA, maskA] = readFourState(buf, layout.forDut.a);
510
+ expect(valA).toBe(0xffn);
511
+ expect(maskA).toBe(0xffn);
512
+
513
+ // bit port should have mask=0 (defined)
514
+ // bit is not 4-state, so no mask — reading its value should be 0
515
+ expect(layout.forDut.b.is4state).toBe(false);
516
+ });
517
+
518
+ test("writing X clears value and sets mask", () => {
519
+ interface Ports {
520
+ a: bigint;
521
+ readonly y_and: bigint;
522
+ }
523
+
524
+ const sim = Simulator.fromSource<Ports>(AND_OR_SOURCE, "AndOr", {
525
+ fourState: true,
526
+ });
527
+ raw = undefined; // sim manages its own handle
528
+
529
+ // Write X to input 'a' via DUT
530
+ (sim.dut as any).a = X;
531
+
532
+ // We can't inspect mask through DUT getter (it only returns value),
533
+ // so this test verifies X write doesn't throw and propagation works.
534
+ // For detailed mask inspection, see the raw NAPI tests below.
535
+ sim.dispose();
536
+ });
537
+
538
+ test("AND: 0 & X = 0 (dominant zero)", () => {
539
+ raw = new addon.NativeSimulatorHandle(AND_OR_SOURCE, "AndOr", {
540
+ fourState: true,
541
+ });
542
+ const layout = parseNapiLayout(raw.layoutJson);
543
+ const buf = raw.sharedMemory().buffer;
544
+ const view = new DataView(buf);
545
+ const _events: Record<string, number> = JSON.parse(raw.eventsJson);
546
+
547
+ const sigA = layout.forDut.a;
548
+ const sigB = layout.forDut.b;
549
+ const sigYAnd = layout.forDut.y_and;
550
+ const sigYOr = layout.forDut.y_or;
551
+
552
+ // a = 0 (value=0, mask=0)
553
+ view.setUint8(sigA.offset, 0);
554
+ view.setUint8(sigA.offset + sigA.byteSize, 0);
555
+
556
+ // b = X (value=0, mask=1)
557
+ view.setUint8(sigB.offset, 0);
558
+ view.setUint8(sigB.offset + sigB.byteSize, 1);
559
+
560
+ raw.evalComb();
561
+
562
+ // 0 & X = 0 (mask should be 0 — dominant zero)
563
+ const [vAnd, mAnd] = readFourState(buf, sigYAnd);
564
+ expect(vAnd).toBe(0n);
565
+ expect(mAnd).toBe(0n);
566
+
567
+ // 0 | X = X (mask should be 1)
568
+ const [vOr, mOr] = readFourState(buf, sigYOr);
569
+ expect(vOr).toBe(0n);
570
+ expect(mOr).toBe(1n);
571
+ });
572
+
573
+ test("OR: 1 | X = 1 (dominant one)", () => {
574
+ raw = new addon.NativeSimulatorHandle(AND_OR_SOURCE, "AndOr", {
575
+ fourState: true,
576
+ });
577
+ const layout = parseNapiLayout(raw.layoutJson);
578
+ const buf = raw.sharedMemory().buffer;
579
+ const view = new DataView(buf);
580
+
581
+ const sigA = layout.forDut.a;
582
+ const sigB = layout.forDut.b;
583
+ const sigYOr = layout.forDut.y_or;
584
+
585
+ // a = 1 (value=1, mask=0)
586
+ view.setUint8(sigA.offset, 1);
587
+ view.setUint8(sigA.offset + sigA.byteSize, 0);
588
+
589
+ // b = X (value=0, mask=1)
590
+ view.setUint8(sigB.offset, 0);
591
+ view.setUint8(sigB.offset + sigB.byteSize, 1);
592
+
593
+ raw.evalComb();
594
+
595
+ // 1 | X = 1 (mask should be 0 — dominant one)
596
+ const [vOr, mOr] = readFourState(buf, sigYOr);
597
+ expect(vOr).toBe(1n);
598
+ expect(mOr).toBe(0n);
599
+ });
600
+
601
+ test("logic-to-bit assignment strips X mask", () => {
602
+ raw = new addon.NativeSimulatorHandle(LOGIC_BIT_MIX_SOURCE, "LogicBitMix", {
603
+ fourState: true,
604
+ });
605
+ const layout = parseNapiLayout(raw.layoutJson);
606
+ const buf = raw.sharedMemory().buffer;
607
+ const view = new DataView(buf);
608
+
609
+ const sigALogic = layout.forDut.a_logic;
610
+ const sigYBitFromLogic = layout.forDut.y_bit_from_logic;
611
+
612
+ // a_logic = all-X (value=0, mask=0xFF)
613
+ view.setUint8(sigALogic.offset, 0);
614
+ view.setUint8(sigALogic.offset + sigALogic.byteSize, 0xff);
615
+
616
+ raw.evalComb();
617
+
618
+ // y_bit_from_logic is bit type — X should be stripped (mask=0)
619
+ expect(sigYBitFromLogic.is4state).toBe(false);
620
+ });
621
+
622
+ test("bit-to-logic assignment has no X", () => {
623
+ raw = new addon.NativeSimulatorHandle(LOGIC_BIT_MIX_SOURCE, "LogicBitMix", {
624
+ fourState: true,
625
+ });
626
+ const layout = parseNapiLayout(raw.layoutJson);
627
+ const buf = raw.sharedMemory().buffer;
628
+ const view = new DataView(buf);
629
+
630
+ const sigBBit = layout.forDut.b_bit;
631
+ const sigYLogicFromBit = layout.forDut.y_logic_from_bit;
632
+
633
+ // b_bit = 0xAA (bit type, always defined)
634
+ view.setUint8(sigBBit.offset, 0xaa);
635
+
636
+ raw.evalComb();
637
+
638
+ // y_logic_from_bit should be 0xAA with mask=0
639
+ const [vLogic, mLogic] = readFourState(buf, sigYLogicFromBit);
640
+ expect(vLogic).toBe(0xaan);
641
+ expect(mLogic).toBe(0n);
642
+ });
643
+
644
+ test("arithmetic with X produces all-X output", () => {
645
+ raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", {
646
+ fourState: true,
647
+ });
648
+ const layout = parseNapiLayout(raw.layoutJson);
649
+ const buf = raw.sharedMemory().buffer;
650
+ const view = new DataView(buf);
651
+
652
+ const sigA = layout.forDut.a;
653
+ const sigB = layout.forDut.b;
654
+ const sigY = layout.forDut.y;
655
+
656
+ // a = 42 (defined), b = X (all X)
657
+ view.setUint8(sigA.offset, 42);
658
+ view.setUint8(sigA.offset + sigA.byteSize, 0); // mask=0
659
+
660
+ view.setUint8(sigB.offset, 0);
661
+ view.setUint8(sigB.offset + sigB.byteSize, 0xff); // mask=0xFF
662
+
663
+ raw.evalComb();
664
+
665
+ // a + X = all-X
666
+ const [, mY] = readFourState(buf, sigY);
667
+ expect(mY).toBe(0xffn);
668
+ });
669
+
670
+ test("defined inputs in 4-state mode behave like 2-state", () => {
671
+ raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", {
672
+ fourState: true,
673
+ });
674
+ const layout = parseNapiLayout(raw.layoutJson);
675
+ const buf = raw.sharedMemory().buffer;
676
+ const view = new DataView(buf);
677
+
678
+ const sigA = layout.forDut.a;
679
+ const sigB = layout.forDut.b;
680
+ const sigY = layout.forDut.y;
681
+
682
+ // a = 100 (defined), b = 55 (defined)
683
+ view.setUint8(sigA.offset, 100);
684
+ view.setUint8(sigA.offset + sigA.byteSize, 0);
685
+
686
+ view.setUint8(sigB.offset, 55);
687
+ view.setUint8(sigB.offset + sigB.byteSize, 0);
688
+
689
+ raw.evalComb();
690
+
691
+ const [vY, mY] = readFourState(buf, sigY);
692
+ expect(vY).toBe(155n);
693
+ expect(mY).toBe(0n);
694
+ });
695
+
696
+ test("FF captures X from input, reset clears X", () => {
697
+ raw = new addon.NativeSimulatorHandle(FF_SOURCE, "FF", { fourState: true });
698
+ const layout = parseNapiLayout(raw.layoutJson);
699
+ const buf = raw.sharedMemory().buffer;
700
+ const view = new DataView(buf);
701
+ const events: Record<string, number> = JSON.parse(raw.eventsJson);
702
+
703
+ const sigRst = layout.forDut.rst;
704
+ const sigD = layout.forDut.d;
705
+ const sigQ = layout.forDut.q;
706
+ const clkEventId = events.clk;
707
+
708
+ // 1. Assert reset (default async_low: rst=0 is active), d=X
709
+ view.setUint8(sigRst.offset, 0);
710
+ view.setUint8(sigRst.offset + sigRst.byteSize, 0); // rst is defined
711
+
712
+ view.setUint8(sigD.offset, 0);
713
+ view.setUint8(sigD.offset + sigD.byteSize, 0xff); // d = all-X
714
+
715
+ raw.tick(clkEventId);
716
+
717
+ // After reset, q should be 0 with mask=0
718
+ const [vQ1, mQ1] = readFourState(buf, sigQ);
719
+ expect(vQ1).toBe(0n);
720
+ expect(mQ1).toBe(0n);
721
+
722
+ // 2. Release reset (rst=1 is inactive), d = partial X (value=0xA5, mask=0x0F)
723
+ view.setUint8(sigRst.offset, 1);
724
+ view.setUint8(sigRst.offset + sigRst.byteSize, 0);
725
+
726
+ view.setUint8(sigD.offset, 0xa5);
727
+ view.setUint8(sigD.offset + sigD.byteSize, 0x0f);
728
+
729
+ raw.tick(clkEventId);
730
+
731
+ // FF should capture X mask from d
732
+ const [, mQ2] = readFourState(buf, sigQ);
733
+ expect(mQ2).toBe(0x0fn);
734
+
735
+ // 3. Assert reset again (rst=0): should clear X
736
+ view.setUint8(sigRst.offset, 0);
737
+ view.setUint8(sigRst.offset + sigRst.byteSize, 0);
738
+
739
+ raw.tick(clkEventId);
740
+
741
+ const [vQ3, mQ3] = readFourState(buf, sigQ);
742
+ expect(vQ3).toBe(0n);
743
+ expect(mQ3).toBe(0n);
744
+ });
745
+
746
+ test("FourState write through DUT sets value and mask", () => {
747
+ raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", {
748
+ fourState: true,
749
+ });
750
+ const layout = parseNapiLayout(raw.layoutJson);
751
+ const buf = raw.sharedMemory().buffer;
752
+ const view = new DataView(buf);
753
+
754
+ const sigA = layout.forDut.a;
755
+
756
+ // Write via DUT-style: FourState(0b1010_0101, 0b0000_1111)
757
+ // value=0xA5, mask=0x0F means lower 4 bits are X
758
+ view.setUint8(sigA.offset, 0xa5);
759
+ view.setUint8(sigA.offset + sigA.byteSize, 0x0f);
760
+
761
+ const [vA, mA] = readFourState(buf, sigA);
762
+ expect(vA).toBe(0xa5n);
763
+ expect(mA).toBe(0x0fn);
764
+ });
765
+
766
+ test("setting defined value clears X mask", () => {
767
+ raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", {
768
+ fourState: true,
769
+ });
770
+ const layout = parseNapiLayout(raw.layoutJson);
771
+ const buf = raw.sharedMemory().buffer;
772
+ const view = new DataView(buf);
773
+
774
+ const sigA = layout.forDut.a;
775
+
776
+ // Start with X
777
+ view.setUint8(sigA.offset, 0);
778
+ view.setUint8(sigA.offset + sigA.byteSize, 0xff);
779
+
780
+ const [, mBefore] = readFourState(buf, sigA);
781
+ expect(mBefore).toBe(0xffn);
782
+
783
+ // Write a defined value (clear mask)
784
+ view.setUint8(sigA.offset, 42);
785
+ view.setUint8(sigA.offset + sigA.byteSize, 0);
786
+
787
+ const [vAfter, mAfter] = readFourState(buf, sigA);
788
+ expect(vAfter).toBe(42n);
789
+ expect(mAfter).toBe(0n);
790
+ });
791
+
792
+ test("4-state through DUT high-level API (fromSource with fourState)", () => {
793
+ interface Ports {
794
+ a: bigint;
795
+ b: bigint;
796
+ readonly y: bigint;
797
+ }
798
+
799
+ const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
800
+ fourState: true,
801
+ });
802
+
803
+ // Write defined values — should behave like 2-state
804
+ sim.dut.a = 100n;
805
+ sim.dut.b = 55n;
806
+ expect(sim.dut.y).toBe(155n);
807
+
808
+ // Write X to a — output should propagate X (value reads as 0)
809
+ (sim.dut as any).a = X;
810
+ // After writing X, the value part of 'y' is implementation-defined
811
+ // but the read should not throw
812
+ const _yVal = sim.dut.y;
813
+ expect(typeof _yVal).toBe("bigint");
814
+
815
+ // Write FourState with partial X
816
+ (sim.dut as any).a = FourState(0xa0, 0x0f);
817
+ const _yVal2 = sim.dut.y;
818
+ expect(typeof _yVal2).toBe("bigint");
819
+
820
+ sim.dispose();
821
+ });
794
822
  });
795
823
 
796
824
  // ---------------------------------------------------------------------------
@@ -798,127 +826,133 @@ module InitTest (
798
826
  // ---------------------------------------------------------------------------
799
827
 
800
828
  describe("E2E: 4-state high-level DUT API", () => {
801
- test("counter in 4-state mode: reset clears X, counting works", () => {
802
- interface CounterPorts {
803
- rst: bigint;
804
- en: bigint;
805
- readonly count: bigint;
806
- }
807
-
808
- const sim = Simulator.fromSource<CounterPorts>(
809
- COUNTER_SOURCE, "Counter", { fourState: true },
810
- );
811
-
812
- // In 4-state mode, count starts as X. Reset should clear it.
813
- // (default async_low: rst=0 is active)
814
- sim.dut.rst = 0n;
815
- // Clear X on en before deactivating reset. In 4-state mode, en starts
816
- // as X (v=1, m=1). The FF branch condition currently uses only the value
817
- // bit, so X with v=1 is treated as "true" — a known limitation in FF
818
- // conditional handling (combinational mux handles X correctly).
819
- sim.dut.en = 0n;
820
- sim.tick();
821
- sim.dut.rst = 1n;
822
- sim.tick();
823
- expect(sim.dut.count).toBe(0n);
824
-
825
- // Enable counting — should work exactly like 2-state
826
- sim.dut.en = 1n;
827
- sim.tick();
828
- expect(sim.dut.count).toBe(1n);
829
-
830
- sim.tick();
831
- expect(sim.dut.count).toBe(2n);
832
-
833
- sim.tick();
834
- expect(sim.dut.count).toBe(3n);
835
-
836
- sim.dispose();
837
- });
838
-
839
- test("multiplexer with X selector produces X output", () => {
840
- const addon = loadNativeAddon();
841
- const raw = new addon.NativeSimulatorHandle(MULTIPLEXER_SOURCE, "Mux4", { fourState: true });
842
- const layout = parseNapiLayout(raw.layoutJson);
843
- const buf = raw.sharedMemory().buffer;
844
- const view = new DataView(buf);
845
-
846
- const sigSel = layout.forDut.sel;
847
- const sigD0 = layout.forDut.d0;
848
- const sigY = layout.forDut.y;
849
-
850
- // Set d0 = 0xAA (defined)
851
- view.setUint8(sigD0.offset, 0xAA);
852
- view.setUint8(sigD0.offset + sigD0.byteSize, 0);
853
-
854
- // Set sel = X
855
- view.setUint8(sigSel.offset, 0);
856
- view.setUint8(sigSel.offset + sigSel.byteSize, 0x03);
857
-
858
- raw.evalComb();
859
-
860
- // With X selector, output should be all-X
861
- const [, mY2] = readFourState(buf, sigY);
862
- expect(mY2).toBe(0xFFn);
863
-
864
- raw.dispose();
865
- });
866
-
867
- test("FF via DUT API: write X input, tick, read output", () => {
868
- interface FFPorts {
869
- rst: bigint;
870
- d: bigint;
871
- readonly q: bigint;
872
- }
873
-
874
- const sim = Simulator.fromSource<FFPorts>(FF_SOURCE, "FF", { fourState: true });
875
-
876
- // Reset to clear initial X (default async_low: rst=0 is active)
877
- sim.dut.rst = 0n;
878
- sim.tick();
879
- sim.dut.rst = 1n;
880
- expect(sim.dut.q).toBe(0n);
881
-
882
- // Write a defined value
883
- sim.dut.d = 0x42n;
884
- sim.tick();
885
- expect(sim.dut.q).toBe(0x42n);
886
-
887
- // Write X to d, tick — q should capture it (value read still returns a bigint)
888
- (sim.dut as any).d = X;
889
- sim.tick();
890
- expect(typeof sim.dut.q).toBe("bigint");
891
-
892
- // Write defined value again — q should recover
893
- sim.dut.d = 0x99n;
894
- sim.tick();
895
- expect(sim.dut.q).toBe(0x99n);
896
-
897
- sim.dispose();
898
- });
899
-
900
- test("X to defined transition: adder recovers from X", () => {
901
- interface Ports {
902
- a: bigint;
903
- b: bigint;
904
- readonly y: bigint;
905
- }
906
-
907
- const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
908
-
909
- // Start with X
910
- (sim.dut as any).a = X;
911
- sim.dut.b = 10n;
912
- // Output has X — just verify it doesn't crash
913
- expect(typeof sim.dut.y).toBe("bigint");
914
-
915
- // Clear X by writing defined values
916
- sim.dut.a = 20n;
917
- sim.dut.b = 30n;
918
- expect(sim.dut.y).toBe(50n);
919
-
920
- sim.dispose();
921
- });
829
+ test("counter in 4-state mode: reset clears X, counting works", () => {
830
+ interface CounterPorts {
831
+ rst: bigint;
832
+ en: bigint;
833
+ readonly count: bigint;
834
+ }
835
+
836
+ const sim = Simulator.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter", {
837
+ fourState: true,
838
+ });
839
+
840
+ // In 4-state mode, count starts as X. Reset should clear it.
841
+ // (default async_low: rst=0 is active)
842
+ sim.dut.rst = 0n;
843
+ // Clear X on en before deactivating reset. In 4-state mode, en starts
844
+ // as X (v=1, m=1). The FF branch condition currently uses only the value
845
+ // bit, so X with v=1 is treated as "true" — a known limitation in FF
846
+ // conditional handling (combinational mux handles X correctly).
847
+ sim.dut.en = 0n;
848
+ sim.tick();
849
+ sim.dut.rst = 1n;
850
+ sim.tick();
851
+ expect(sim.dut.count).toBe(0n);
852
+
853
+ // Enable counting — should work exactly like 2-state
854
+ sim.dut.en = 1n;
855
+ sim.tick();
856
+ expect(sim.dut.count).toBe(1n);
857
+
858
+ sim.tick();
859
+ expect(sim.dut.count).toBe(2n);
860
+
861
+ sim.tick();
862
+ expect(sim.dut.count).toBe(3n);
863
+
864
+ sim.dispose();
865
+ });
866
+
867
+ test("multiplexer with X selector produces X output", () => {
868
+ const addon = loadNativeAddon();
869
+ const raw = new addon.NativeSimulatorHandle(MULTIPLEXER_SOURCE, "Mux4", {
870
+ fourState: true,
871
+ });
872
+ const layout = parseNapiLayout(raw.layoutJson);
873
+ const buf = raw.sharedMemory().buffer;
874
+ const view = new DataView(buf);
875
+
876
+ const sigSel = layout.forDut.sel;
877
+ const sigD0 = layout.forDut.d0;
878
+ const sigY = layout.forDut.y;
879
+
880
+ // Set d0 = 0xAA (defined)
881
+ view.setUint8(sigD0.offset, 0xaa);
882
+ view.setUint8(sigD0.offset + sigD0.byteSize, 0);
883
+
884
+ // Set sel = X
885
+ view.setUint8(sigSel.offset, 0);
886
+ view.setUint8(sigSel.offset + sigSel.byteSize, 0x03);
887
+
888
+ raw.evalComb();
889
+
890
+ // With X selector, output should be all-X
891
+ const [, mY2] = readFourState(buf, sigY);
892
+ expect(mY2).toBe(0xffn);
893
+
894
+ raw.dispose();
895
+ });
896
+
897
+ test("FF via DUT API: write X input, tick, read output", () => {
898
+ interface FFPorts {
899
+ rst: bigint;
900
+ d: bigint;
901
+ readonly q: bigint;
902
+ }
903
+
904
+ const sim = Simulator.fromSource<FFPorts>(FF_SOURCE, "FF", {
905
+ fourState: true,
906
+ });
907
+
908
+ // Reset to clear initial X (default async_low: rst=0 is active)
909
+ sim.dut.rst = 0n;
910
+ sim.tick();
911
+ sim.dut.rst = 1n;
912
+ expect(sim.dut.q).toBe(0n);
913
+
914
+ // Write a defined value
915
+ sim.dut.d = 0x42n;
916
+ sim.tick();
917
+ expect(sim.dut.q).toBe(0x42n);
918
+
919
+ // Write X to d, tick — q should capture it (value read still returns a bigint)
920
+ (sim.dut as any).d = X;
921
+ sim.tick();
922
+ expect(typeof sim.dut.q).toBe("bigint");
923
+
924
+ // Write defined value again — q should recover
925
+ sim.dut.d = 0x99n;
926
+ sim.tick();
927
+ expect(sim.dut.q).toBe(0x99n);
928
+
929
+ sim.dispose();
930
+ });
931
+
932
+ test("X to defined transition: adder recovers from X", () => {
933
+ interface Ports {
934
+ a: bigint;
935
+ b: bigint;
936
+ readonly y: bigint;
937
+ }
938
+
939
+ const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
940
+ fourState: true,
941
+ });
942
+
943
+ // Start with X
944
+ (sim.dut as any).a = X;
945
+ sim.dut.b = 10n;
946
+ // Output has X — just verify it doesn't crash
947
+ expect(typeof sim.dut.y).toBe("bigint");
948
+
949
+ // Clear X by writing defined values
950
+ sim.dut.a = 20n;
951
+ sim.dut.b = 30n;
952
+ expect(sim.dut.y).toBe(50n);
953
+
954
+ sim.dispose();
955
+ });
922
956
  });
923
957
 
924
958
  // ---------------------------------------------------------------------------
@@ -926,85 +960,87 @@ describe("E2E: 4-state high-level DUT API", () => {
926
960
  // ---------------------------------------------------------------------------
927
961
 
928
962
  describe("E2E: 4-state Simulation (time-based)", () => {
929
- test("FF with clock-driven 4-state: reset clears X, captures defined values", () => {
930
- interface FFPorts {
931
- rst: bigint;
932
- d: bigint;
933
- readonly q: bigint;
934
- }
935
-
936
- const sim = Simulation.fromSource<FFPorts>(FF_SOURCE, "FF", { fourState: true });
937
-
938
- sim.addClock("clk", { period: 10 });
939
- expect(sim.time()).toBe(0);
940
-
941
- // Reset to clear initial X on q (default async_low: rst=0 is active)
942
- sim.dut.rst = 0n;
943
- sim.runUntil(20);
944
- sim.dut.rst = 1n;
945
- expect(sim.dut.q).toBe(0n);
946
-
947
- // Drive d with defined value
948
- sim.dut.d = 0x55n;
949
- sim.runUntil(40);
950
- expect(sim.dut.q).toBe(0x55n);
951
-
952
- // Drive d with different value
953
- sim.dut.d = 0xAAn;
954
- sim.runUntil(60);
955
- expect(sim.dut.q).toBe(0xAAn);
956
-
957
- sim.dispose();
958
- });
959
-
960
- test("counter in 4-state time-based mode", () => {
961
- interface CounterPorts {
962
- rst: bigint;
963
- en: bigint;
964
- readonly count: bigint;
965
- }
966
-
967
- const sim = Simulation.fromSource<CounterPorts>(
968
- COUNTER_SOURCE, "Counter", { fourState: true },
969
- );
970
-
971
- sim.addClock("clk", { period: 10 });
972
-
973
- // Reset (default async_low: rst=0 is active)
974
- sim.dut.rst = 0n;
975
- sim.runUntil(20);
976
- sim.dut.rst = 1n;
977
- sim.dut.en = 1n;
978
-
979
- sim.runUntil(100);
980
-
981
- const count = sim.dut.count;
982
- expect(count).toBeGreaterThan(0n);
983
- expect(sim.time()).toBe(100);
984
-
985
- sim.dispose();
986
- });
987
-
988
- test("4-state combinational in time-based simulation", () => {
989
- interface Ports {
990
- a: bigint;
991
- b: bigint;
992
- readonly y: bigint;
993
- }
994
-
995
- const sim = Simulation.fromSource<Ports>(
996
- ADDER_4STATE_SOURCE, "Adder4S", { fourState: true },
997
- );
998
-
999
- // No clock needed for pure combinational — just set values and read
1000
- sim.dut.a = 100n;
1001
- sim.dut.b = 55n;
1002
- // runUntil(0) to force eval
1003
- sim.runUntil(0);
1004
- expect(sim.dut.y).toBe(155n);
1005
-
1006
- sim.dispose();
1007
- });
963
+ test("FF with clock-driven 4-state: reset clears X, captures defined values", () => {
964
+ interface FFPorts {
965
+ rst: bigint;
966
+ d: bigint;
967
+ readonly q: bigint;
968
+ }
969
+
970
+ const sim = Simulation.fromSource<FFPorts>(FF_SOURCE, "FF", {
971
+ fourState: true,
972
+ });
973
+
974
+ sim.addClock("clk", { period: 10 });
975
+ expect(sim.time()).toBe(0);
976
+
977
+ // Reset to clear initial X on q (default async_low: rst=0 is active)
978
+ sim.dut.rst = 0n;
979
+ sim.runUntil(20);
980
+ sim.dut.rst = 1n;
981
+ expect(sim.dut.q).toBe(0n);
982
+
983
+ // Drive d with defined value
984
+ sim.dut.d = 0x55n;
985
+ sim.runUntil(40);
986
+ expect(sim.dut.q).toBe(0x55n);
987
+
988
+ // Drive d with different value
989
+ sim.dut.d = 0xaan;
990
+ sim.runUntil(60);
991
+ expect(sim.dut.q).toBe(0xaan);
992
+
993
+ sim.dispose();
994
+ });
995
+
996
+ test("counter in 4-state time-based mode", () => {
997
+ interface CounterPorts {
998
+ rst: bigint;
999
+ en: bigint;
1000
+ readonly count: bigint;
1001
+ }
1002
+
1003
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter", {
1004
+ fourState: true,
1005
+ });
1006
+
1007
+ sim.addClock("clk", { period: 10 });
1008
+
1009
+ // Reset (default async_low: rst=0 is active)
1010
+ sim.dut.rst = 0n;
1011
+ sim.runUntil(20);
1012
+ sim.dut.rst = 1n;
1013
+ sim.dut.en = 1n;
1014
+
1015
+ sim.runUntil(100);
1016
+
1017
+ const count = sim.dut.count;
1018
+ expect(count).toBeGreaterThan(0n);
1019
+ expect(sim.time()).toBe(100);
1020
+
1021
+ sim.dispose();
1022
+ });
1023
+
1024
+ test("4-state combinational in time-based simulation", () => {
1025
+ interface Ports {
1026
+ a: bigint;
1027
+ b: bigint;
1028
+ readonly y: bigint;
1029
+ }
1030
+
1031
+ const sim = Simulation.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
1032
+ fourState: true,
1033
+ });
1034
+
1035
+ // No clock needed for pure combinational — just set values and read
1036
+ sim.dut.a = 100n;
1037
+ sim.dut.b = 55n;
1038
+ // runUntil(0) to force eval
1039
+ sim.runUntil(0);
1040
+ expect(sim.dut.y).toBe(155n);
1041
+
1042
+ sim.dispose();
1043
+ });
1008
1044
  });
1009
1045
 
1010
1046
  // ---------------------------------------------------------------------------
@@ -1012,225 +1048,225 @@ describe("E2E: 4-state Simulation (time-based)", () => {
1012
1048
  // ---------------------------------------------------------------------------
1013
1049
 
1014
1050
  describe("E2E: Simulation testbench helpers", () => {
1015
- test("waitForCycles: advances correct number of clock cycles", () => {
1016
- interface CounterPorts {
1017
- rst: bigint;
1018
- en: bigint;
1019
- readonly count: bigint;
1020
- }
1021
-
1022
- const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1023
- sim.addClock("clk", { period: 10 });
1024
-
1025
- // Reset (default async_low: rst=0 is active)
1026
- sim.dut.rst = 0n;
1027
- sim.runUntil(20);
1028
- sim.dut.rst = 1n;
1029
- sim.dut.en = 1n;
1030
-
1031
- const beforeTime = sim.time();
1032
- const afterTime = sim.waitForCycles("clk", 5);
1033
-
1034
- expect(afterTime).toBeGreaterThan(beforeTime);
1035
- // Each cycle = 2 steps with period 10, so 5 cycles ≈ 50 time units
1036
- expect(afterTime - beforeTime).toBe(50);
1037
-
1038
- sim.dispose();
1039
- });
1040
-
1041
- test("waitUntil: waits for condition to be met", () => {
1042
- interface CounterPorts {
1043
- rst: bigint;
1044
- en: bigint;
1045
- readonly count: bigint;
1046
- }
1047
-
1048
- const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1049
- sim.addClock("clk", { period: 10 });
1050
-
1051
- // Reset (default async_low: rst=0 is active)
1052
- sim.dut.rst = 0n;
1053
- sim.runUntil(20);
1054
- sim.dut.rst = 1n;
1055
- sim.dut.en = 1n;
1056
-
1057
- const t = sim.waitUntil(() => sim.dut.count >= 3n);
1058
- expect(sim.dut.count).toBeGreaterThanOrEqual(3n);
1059
- expect(t).toBeGreaterThan(20);
1060
-
1061
- sim.dispose();
1062
- });
1063
-
1064
- test("waitUntil: throws SimulationTimeoutError on timeout", () => {
1065
- interface CounterPorts {
1066
- rst: bigint;
1067
- en: bigint;
1068
- readonly count: bigint;
1069
- }
1070
-
1071
- const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1072
- sim.addClock("clk", { period: 10 });
1073
-
1074
- // Reset (default async_low: rst=0 is active)
1075
- sim.dut.rst = 0n;
1076
- sim.runUntil(20);
1077
- sim.dut.rst = 1n;
1078
- sim.dut.en = 0n; // disabled — count won't increase
1079
-
1080
- expect(() =>
1081
- sim.waitUntil(() => sim.dut.count >= 100n, { maxSteps: 20 }),
1082
- ).toThrow(SimulationTimeoutError);
1083
-
1084
- sim.dispose();
1085
- });
1086
-
1087
- test("reset: asserts and releases reset on counter", () => {
1088
- interface CounterPorts {
1089
- rst: bigint;
1090
- en: bigint;
1091
- readonly count: bigint;
1092
- }
1093
-
1094
- const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1095
- sim.addClock("clk", { period: 10 });
1096
-
1097
- // Count up a bit (default async_low: rst=0 is active)
1098
- sim.dut.rst = 0n;
1099
- sim.runUntil(20);
1100
- sim.dut.rst = 1n;
1101
- sim.dut.en = 1n;
1102
- sim.runUntil(100);
1103
- expect(sim.dut.count).toBeGreaterThan(0n);
1104
-
1105
- // Reset using the helper
1106
- sim.reset("rst");
1107
- expect(sim.dut.count).toBe(0n);
1108
-
1109
- sim.dispose();
1110
- });
1111
-
1112
- test("reset: explicit async_low resetType activates with 0", () => {
1113
- interface CounterPorts {
1114
- rst: bigint;
1115
- en: bigint;
1116
- readonly count: bigint;
1117
- }
1118
-
1119
- const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter", {
1120
- resetType: "async_low",
1121
- });
1122
- sim.addClock("clk", { period: 10 });
1123
-
1124
- // With async_low, rst=0 is active, rst=1 is inactive
1125
- sim.reset("rst");
1126
-
1127
- sim.dut.en = 1n;
1128
- sim.runUntil(100);
1129
- expect(sim.dut.count).toBeGreaterThan(0n);
1130
-
1131
- // Reset again using helper, verify it resets the counter
1132
- sim.reset("rst");
1133
- expect(sim.dut.count).toBe(0n);
1134
-
1135
- sim.dispose();
1136
- });
1137
-
1138
- test("reset: explicit async_high resetType activates with 1", () => {
1139
- interface CounterPorts {
1140
- rst: bigint;
1141
- en: bigint;
1142
- readonly count: bigint;
1143
- }
1144
-
1145
- const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter", {
1146
- resetType: "async_high",
1147
- });
1148
- sim.addClock("clk", { period: 10 });
1149
-
1150
- // With async_high, rst=1 is active, rst=0 is inactive
1151
- sim.reset("rst");
1152
-
1153
- sim.dut.en = 1n;
1154
- sim.runUntil(100);
1155
- expect(sim.dut.count).toBeGreaterThan(0n);
1156
-
1157
- // Reset again
1158
- sim.reset("rst");
1159
- expect(sim.dut.count).toBe(0n);
1160
-
1161
- sim.dispose();
1162
- });
1163
-
1164
- test("Simulator.fromSource: resetType option works", () => {
1165
- interface CounterPorts {
1166
- rst: bigint;
1167
- en: bigint;
1168
- readonly count: bigint;
1169
- }
1170
-
1171
- const sim = Simulator.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter", {
1172
- resetType: "async_high",
1173
- });
1174
-
1175
- // With async_high, assert reset with 1
1176
- sim.dut.rst = 1n;
1177
- sim.tick();
1178
- sim.dut.rst = 0n;
1179
- sim.tick();
1180
- expect(sim.dut.count).toBe(0n);
1181
-
1182
- // Count up
1183
- sim.dut.en = 1n;
1184
- sim.tick();
1185
- expect(sim.dut.count).toBe(1n);
1186
-
1187
- sim.dispose();
1188
- });
1189
-
1190
- test("runUntil with maxSteps: succeeds within budget", () => {
1191
- interface CounterPorts {
1192
- rst: bigint;
1193
- en: bigint;
1194
- readonly count: bigint;
1195
- }
1196
-
1197
- const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1198
- sim.addClock("clk", { period: 10 });
1199
-
1200
- // Reset (default async_low: rst=0 is active)
1201
- sim.dut.rst = 0n;
1202
- sim.runUntil(20);
1203
- sim.dut.rst = 1n;
1204
- sim.dut.en = 1n;
1205
-
1206
- // 100 time units with period 10 = 10 events, should fit in 100 steps
1207
- sim.runUntil(120, { maxSteps: 100 });
1208
- expect(sim.time()).toBe(120);
1209
-
1210
- sim.dispose();
1211
- });
1212
-
1213
- test("runUntil with maxSteps: throws on exceeded budget", () => {
1214
- interface CounterPorts {
1215
- rst: bigint;
1216
- en: bigint;
1217
- readonly count: bigint;
1218
- }
1219
-
1220
- const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1221
- sim.addClock("clk", { period: 10 });
1222
-
1223
- // Release reset so counter can count (default async_low: rst=1 is inactive)
1224
- sim.dut.rst = 1n;
1225
- sim.dut.en = 1n;
1226
-
1227
- // Very small budget for a long run
1228
- expect(() => sim.runUntil(100000, { maxSteps: 5 })).toThrow(
1229
- SimulationTimeoutError,
1230
- );
1231
-
1232
- sim.dispose();
1233
- });
1051
+ test("waitForCycles: advances correct number of clock cycles", () => {
1052
+ interface CounterPorts {
1053
+ rst: bigint;
1054
+ en: bigint;
1055
+ readonly count: bigint;
1056
+ }
1057
+
1058
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1059
+ sim.addClock("clk", { period: 10 });
1060
+
1061
+ // Reset (default async_low: rst=0 is active)
1062
+ sim.dut.rst = 0n;
1063
+ sim.runUntil(20);
1064
+ sim.dut.rst = 1n;
1065
+ sim.dut.en = 1n;
1066
+
1067
+ const beforeTime = sim.time();
1068
+ const afterTime = sim.waitForCycles("clk", 5);
1069
+
1070
+ expect(afterTime).toBeGreaterThan(beforeTime);
1071
+ // Each cycle = 2 steps with period 10, so 5 cycles ≈ 50 time units
1072
+ expect(afterTime - beforeTime).toBe(50);
1073
+
1074
+ sim.dispose();
1075
+ });
1076
+
1077
+ test("waitUntil: waits for condition to be met", () => {
1078
+ interface CounterPorts {
1079
+ rst: bigint;
1080
+ en: bigint;
1081
+ readonly count: bigint;
1082
+ }
1083
+
1084
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1085
+ sim.addClock("clk", { period: 10 });
1086
+
1087
+ // Reset (default async_low: rst=0 is active)
1088
+ sim.dut.rst = 0n;
1089
+ sim.runUntil(20);
1090
+ sim.dut.rst = 1n;
1091
+ sim.dut.en = 1n;
1092
+
1093
+ const t = sim.waitUntil(() => sim.dut.count >= 3n);
1094
+ expect(sim.dut.count).toBeGreaterThanOrEqual(3n);
1095
+ expect(t).toBeGreaterThan(20);
1096
+
1097
+ sim.dispose();
1098
+ });
1099
+
1100
+ test("waitUntil: throws SimulationTimeoutError on timeout", () => {
1101
+ interface CounterPorts {
1102
+ rst: bigint;
1103
+ en: bigint;
1104
+ readonly count: bigint;
1105
+ }
1106
+
1107
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1108
+ sim.addClock("clk", { period: 10 });
1109
+
1110
+ // Reset (default async_low: rst=0 is active)
1111
+ sim.dut.rst = 0n;
1112
+ sim.runUntil(20);
1113
+ sim.dut.rst = 1n;
1114
+ sim.dut.en = 0n; // disabled — count won't increase
1115
+
1116
+ expect(() =>
1117
+ sim.waitUntil(() => sim.dut.count >= 100n, { maxSteps: 20 }),
1118
+ ).toThrow(SimulationTimeoutError);
1119
+
1120
+ sim.dispose();
1121
+ });
1122
+
1123
+ test("reset: asserts and releases reset on counter", () => {
1124
+ interface CounterPorts {
1125
+ rst: bigint;
1126
+ en: bigint;
1127
+ readonly count: bigint;
1128
+ }
1129
+
1130
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1131
+ sim.addClock("clk", { period: 10 });
1132
+
1133
+ // Count up a bit (default async_low: rst=0 is active)
1134
+ sim.dut.rst = 0n;
1135
+ sim.runUntil(20);
1136
+ sim.dut.rst = 1n;
1137
+ sim.dut.en = 1n;
1138
+ sim.runUntil(100);
1139
+ expect(sim.dut.count).toBeGreaterThan(0n);
1140
+
1141
+ // Reset using the helper
1142
+ sim.reset("rst");
1143
+ expect(sim.dut.count).toBe(0n);
1144
+
1145
+ sim.dispose();
1146
+ });
1147
+
1148
+ test("reset: explicit async_low resetType activates with 0", () => {
1149
+ interface CounterPorts {
1150
+ rst: bigint;
1151
+ en: bigint;
1152
+ readonly count: bigint;
1153
+ }
1154
+
1155
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter", {
1156
+ resetType: "async_low",
1157
+ });
1158
+ sim.addClock("clk", { period: 10 });
1159
+
1160
+ // With async_low, rst=0 is active, rst=1 is inactive
1161
+ sim.reset("rst");
1162
+
1163
+ sim.dut.en = 1n;
1164
+ sim.runUntil(100);
1165
+ expect(sim.dut.count).toBeGreaterThan(0n);
1166
+
1167
+ // Reset again using helper, verify it resets the counter
1168
+ sim.reset("rst");
1169
+ expect(sim.dut.count).toBe(0n);
1170
+
1171
+ sim.dispose();
1172
+ });
1173
+
1174
+ test("reset: explicit async_high resetType activates with 1", () => {
1175
+ interface CounterPorts {
1176
+ rst: bigint;
1177
+ en: bigint;
1178
+ readonly count: bigint;
1179
+ }
1180
+
1181
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter", {
1182
+ resetType: "async_high",
1183
+ });
1184
+ sim.addClock("clk", { period: 10 });
1185
+
1186
+ // With async_high, rst=1 is active, rst=0 is inactive
1187
+ sim.reset("rst");
1188
+
1189
+ sim.dut.en = 1n;
1190
+ sim.runUntil(100);
1191
+ expect(sim.dut.count).toBeGreaterThan(0n);
1192
+
1193
+ // Reset again
1194
+ sim.reset("rst");
1195
+ expect(sim.dut.count).toBe(0n);
1196
+
1197
+ sim.dispose();
1198
+ });
1199
+
1200
+ test("Simulator.fromSource: resetType option works", () => {
1201
+ interface CounterPorts {
1202
+ rst: bigint;
1203
+ en: bigint;
1204
+ readonly count: bigint;
1205
+ }
1206
+
1207
+ const sim = Simulator.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter", {
1208
+ resetType: "async_high",
1209
+ });
1210
+
1211
+ // With async_high, assert reset with 1
1212
+ sim.dut.rst = 1n;
1213
+ sim.tick();
1214
+ sim.dut.rst = 0n;
1215
+ sim.tick();
1216
+ expect(sim.dut.count).toBe(0n);
1217
+
1218
+ // Count up
1219
+ sim.dut.en = 1n;
1220
+ sim.tick();
1221
+ expect(sim.dut.count).toBe(1n);
1222
+
1223
+ sim.dispose();
1224
+ });
1225
+
1226
+ test("runUntil with maxSteps: succeeds within budget", () => {
1227
+ interface CounterPorts {
1228
+ rst: bigint;
1229
+ en: bigint;
1230
+ readonly count: bigint;
1231
+ }
1232
+
1233
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1234
+ sim.addClock("clk", { period: 10 });
1235
+
1236
+ // Reset (default async_low: rst=0 is active)
1237
+ sim.dut.rst = 0n;
1238
+ sim.runUntil(20);
1239
+ sim.dut.rst = 1n;
1240
+ sim.dut.en = 1n;
1241
+
1242
+ // 100 time units with period 10 = 10 events, should fit in 100 steps
1243
+ sim.runUntil(120, { maxSteps: 100 });
1244
+ expect(sim.time()).toBe(120);
1245
+
1246
+ sim.dispose();
1247
+ });
1248
+
1249
+ test("runUntil with maxSteps: throws on exceeded budget", () => {
1250
+ interface CounterPorts {
1251
+ rst: bigint;
1252
+ en: bigint;
1253
+ readonly count: bigint;
1254
+ }
1255
+
1256
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
1257
+ sim.addClock("clk", { period: 10 });
1258
+
1259
+ // Release reset so counter can count (default async_low: rst=1 is inactive)
1260
+ sim.dut.rst = 1n;
1261
+ sim.dut.en = 1n;
1262
+
1263
+ // Very small budget for a long run
1264
+ expect(() => sim.runUntil(100000, { maxSteps: 5 })).toThrow(
1265
+ SimulationTimeoutError,
1266
+ );
1267
+
1268
+ sim.dispose();
1269
+ });
1234
1270
  });
1235
1271
 
1236
1272
  // ---------------------------------------------------------------------------
@@ -1238,89 +1274,89 @@ describe("E2E: Simulation testbench helpers", () => {
1238
1274
  // ---------------------------------------------------------------------------
1239
1275
 
1240
1276
  describe("E2E: fourState() method", () => {
1241
- test("Simulator.fourState: reads 4-state value and mask", () => {
1242
- interface Ports {
1243
- a: bigint;
1244
- b: bigint;
1245
- readonly y: bigint;
1246
- }
1247
-
1248
- const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
1249
- fourState: true,
1250
- });
1251
-
1252
- sim.dut.a = 100n;
1253
- sim.dut.b = 55n;
1254
- // Trigger evalComb via output read (Adder4S is purely combinational)
1255
- expect(sim.dut.y).toBe(155n);
1256
-
1257
- const fs = sim.fourState("y");
1258
- expect(fs.__fourState).toBe(true);
1259
- expect(fs.value).toBe(155n);
1260
- expect(fs.mask).toBe(0n);
1261
-
1262
- sim.dispose();
1263
- });
1264
-
1265
- test("Simulator.fourState: reads X mask when input is X", () => {
1266
- interface Ports {
1267
- a: bigint;
1268
- b: bigint;
1269
- readonly y: bigint;
1270
- }
1271
-
1272
- const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
1273
- fourState: true,
1274
- });
1275
-
1276
- (sim.dut as any).a = X;
1277
- sim.dut.b = 10n;
1278
- // Trigger evalComb via output read
1279
- sim.dut.y;
1280
-
1281
- const fs = sim.fourState("y");
1282
- expect(fs.mask).toBe(0xFFn); // all X from arithmetic propagation
1283
-
1284
- sim.dispose();
1285
- });
1286
-
1287
- test("Simulation.fourState: reads 4-state value", () => {
1288
- interface Ports {
1289
- a: bigint;
1290
- b: bigint;
1291
- readonly y: bigint;
1292
- }
1293
-
1294
- const sim = Simulation.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
1295
- fourState: true,
1296
- });
1297
-
1298
- sim.dut.a = 50n;
1299
- sim.dut.b = 25n;
1300
- sim.runUntil(0);
1301
-
1302
- const fs = sim.fourState("y");
1303
- expect(fs.value).toBe(75n);
1304
- expect(fs.mask).toBe(0n);
1305
-
1306
- sim.dispose();
1307
- });
1308
-
1309
- test("fourState: throws for unknown port", () => {
1310
- interface Ports {
1311
- a: bigint;
1312
- b: bigint;
1313
- readonly y: bigint;
1314
- }
1315
-
1316
- const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
1317
- fourState: true,
1318
- });
1319
-
1320
- expect(() => sim.fourState("nonexistent")).toThrow("Unknown port");
1321
-
1322
- sim.dispose();
1323
- });
1277
+ test("Simulator.fourState: reads 4-state value and mask", () => {
1278
+ interface Ports {
1279
+ a: bigint;
1280
+ b: bigint;
1281
+ readonly y: bigint;
1282
+ }
1283
+
1284
+ const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
1285
+ fourState: true,
1286
+ });
1287
+
1288
+ sim.dut.a = 100n;
1289
+ sim.dut.b = 55n;
1290
+ // Trigger evalComb via output read (Adder4S is purely combinational)
1291
+ expect(sim.dut.y).toBe(155n);
1292
+
1293
+ const fs = sim.fourState("y");
1294
+ expect(fs.__fourState).toBe(true);
1295
+ expect(fs.value).toBe(155n);
1296
+ expect(fs.mask).toBe(0n);
1297
+
1298
+ sim.dispose();
1299
+ });
1300
+
1301
+ test("Simulator.fourState: reads X mask when input is X", () => {
1302
+ interface Ports {
1303
+ a: bigint;
1304
+ b: bigint;
1305
+ readonly y: bigint;
1306
+ }
1307
+
1308
+ const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
1309
+ fourState: true,
1310
+ });
1311
+
1312
+ (sim.dut as any).a = X;
1313
+ sim.dut.b = 10n;
1314
+ // Trigger evalComb via output read
1315
+ sim.dut.y;
1316
+
1317
+ const fs = sim.fourState("y");
1318
+ expect(fs.mask).toBe(0xffn); // all X from arithmetic propagation
1319
+
1320
+ sim.dispose();
1321
+ });
1322
+
1323
+ test("Simulation.fourState: reads 4-state value", () => {
1324
+ interface Ports {
1325
+ a: bigint;
1326
+ b: bigint;
1327
+ readonly y: bigint;
1328
+ }
1329
+
1330
+ const sim = Simulation.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
1331
+ fourState: true,
1332
+ });
1333
+
1334
+ sim.dut.a = 50n;
1335
+ sim.dut.b = 25n;
1336
+ sim.runUntil(0);
1337
+
1338
+ const fs = sim.fourState("y");
1339
+ expect(fs.value).toBe(75n);
1340
+ expect(fs.mask).toBe(0n);
1341
+
1342
+ sim.dispose();
1343
+ });
1344
+
1345
+ test("fourState: throws for unknown port", () => {
1346
+ interface Ports {
1347
+ a: bigint;
1348
+ b: bigint;
1349
+ readonly y: bigint;
1350
+ }
1351
+
1352
+ const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", {
1353
+ fourState: true,
1354
+ });
1355
+
1356
+ expect(() => sim.fourState("nonexistent")).toThrow("Unknown port");
1357
+
1358
+ sim.dispose();
1359
+ });
1324
1360
  });
1325
1361
 
1326
1362
  // ---------------------------------------------------------------------------
@@ -1328,50 +1364,50 @@ describe("E2E: fourState() method", () => {
1328
1364
  // ---------------------------------------------------------------------------
1329
1365
 
1330
1366
  describe("E2E: optimize flag", () => {
1331
- test("Simulator.fromSource with optimize: true", () => {
1332
- interface AdderPorts {
1333
- rst: bigint;
1334
- a: bigint;
1335
- b: bigint;
1336
- readonly sum: bigint;
1337
- }
1338
-
1339
- const sim = Simulator.fromSource<AdderPorts>(ADDER_SOURCE, "Adder", {
1340
- optimize: true,
1341
- });
1342
-
1343
- sim.dut.a = 100n;
1344
- sim.dut.b = 200n;
1345
- sim.tick();
1346
- expect(sim.dut.sum).toBe(300n);
1347
-
1348
- sim.dispose();
1349
- });
1350
-
1351
- test("Simulation.fromSource with optimize: true", () => {
1352
- interface CounterPorts {
1353
- rst: bigint;
1354
- en: bigint;
1355
- readonly count: bigint;
1356
- }
1357
-
1358
- const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter", {
1359
- optimize: true,
1360
- });
1361
-
1362
- sim.addClock("clk", { period: 10 });
1363
-
1364
- // Reset (default async_low: rst=0 is active)
1365
- sim.dut.rst = 0n;
1366
- sim.runUntil(20);
1367
- sim.dut.rst = 1n;
1368
- sim.dut.en = 1n;
1369
- sim.runUntil(100);
1370
-
1371
- expect(sim.dut.count).toBeGreaterThan(0n);
1372
-
1373
- sim.dispose();
1374
- });
1367
+ test("Simulator.fromSource with optimize: true", () => {
1368
+ interface AdderPorts {
1369
+ rst: bigint;
1370
+ a: bigint;
1371
+ b: bigint;
1372
+ readonly sum: bigint;
1373
+ }
1374
+
1375
+ const sim = Simulator.fromSource<AdderPorts>(ADDER_SOURCE, "Adder", {
1376
+ optimize: true,
1377
+ });
1378
+
1379
+ sim.dut.a = 100n;
1380
+ sim.dut.b = 200n;
1381
+ sim.tick();
1382
+ expect(sim.dut.sum).toBe(300n);
1383
+
1384
+ sim.dispose();
1385
+ });
1386
+
1387
+ test("Simulation.fromSource with optimize: true", () => {
1388
+ interface CounterPorts {
1389
+ rst: bigint;
1390
+ en: bigint;
1391
+ readonly count: bigint;
1392
+ }
1393
+
1394
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter", {
1395
+ optimize: true,
1396
+ });
1397
+
1398
+ sim.addClock("clk", { period: 10 });
1399
+
1400
+ // Reset (default async_low: rst=0 is active)
1401
+ sim.dut.rst = 0n;
1402
+ sim.runUntil(20);
1403
+ sim.dut.rst = 1n;
1404
+ sim.dut.en = 1n;
1405
+ sim.runUntil(100);
1406
+
1407
+ expect(sim.dut.count).toBeGreaterThan(0n);
1408
+
1409
+ sim.dispose();
1410
+ });
1375
1411
  });
1376
1412
 
1377
1413
  // ---------------------------------------------------------------------------
@@ -1379,32 +1415,32 @@ describe("E2E: optimize flag", () => {
1379
1415
  // ---------------------------------------------------------------------------
1380
1416
 
1381
1417
  describe("parseSignalPath", () => {
1382
- test("simple variable path", () => {
1383
- const result = parseSignalPath("v");
1384
- expect(result.instancePath).toEqual([]);
1385
- expect(result.varPath).toEqual(["v"]);
1386
- });
1387
-
1388
- test("instance:variable split", () => {
1389
- const result = parseSignalPath("p2:i");
1390
- expect(result.instancePath).toEqual([{ name: "p2", index: 0 }]);
1391
- expect(result.varPath).toEqual(["i"]);
1392
- });
1393
-
1394
- test("nested instance with array index", () => {
1395
- const result = parseSignalPath("a.b[3]:x.y");
1396
- expect(result.instancePath).toEqual([
1397
- { name: "a", index: 0 },
1398
- { name: "b", index: 3 },
1399
- ]);
1400
- expect(result.varPath).toEqual(["x", "y"]);
1401
- });
1402
-
1403
- test("dotted variable path without instance", () => {
1404
- const result = parseSignalPath("foo.bar.baz");
1405
- expect(result.instancePath).toEqual([]);
1406
- expect(result.varPath).toEqual(["foo", "bar", "baz"]);
1407
- });
1418
+ test("simple variable path", () => {
1419
+ const result = parseSignalPath("v");
1420
+ expect(result.instancePath).toEqual([]);
1421
+ expect(result.varPath).toEqual(["v"]);
1422
+ });
1423
+
1424
+ test("instance:variable split", () => {
1425
+ const result = parseSignalPath("p2:i");
1426
+ expect(result.instancePath).toEqual([{ name: "p2", index: 0 }]);
1427
+ expect(result.varPath).toEqual(["i"]);
1428
+ });
1429
+
1430
+ test("nested instance with array index", () => {
1431
+ const result = parseSignalPath("a.b[3]:x.y");
1432
+ expect(result.instancePath).toEqual([
1433
+ { name: "a", index: 0 },
1434
+ { name: "b", index: 3 },
1435
+ ]);
1436
+ expect(result.varPath).toEqual(["x", "y"]);
1437
+ });
1438
+
1439
+ test("dotted variable path without instance", () => {
1440
+ const result = parseSignalPath("foo.bar.baz");
1441
+ expect(result.instancePath).toEqual([]);
1442
+ expect(result.varPath).toEqual(["foo", "bar", "baz"]);
1443
+ });
1408
1444
  });
1409
1445
 
1410
1446
  // ---------------------------------------------------------------------------
@@ -1486,179 +1522,195 @@ module ParamChild #(
1486
1522
  `;
1487
1523
 
1488
1524
  describe("E2E: child instance DUT access", () => {
1489
- test("Simulator: read child instance ports via dut.u_sub", () => {
1490
- const sim = Simulator.fromSource(HIERARCHY_SOURCE, "Top");
1525
+ test("Simulator: read child instance ports via dut.u_sub", () => {
1526
+ const sim = Simulator.fromSource(HIERARCHY_SOURCE, "Top");
1491
1527
 
1492
- sim.dut.top_in = 0xABn;
1493
- sim.tick();
1494
- expect(sim.dut.top_out).toBe(0xABn);
1528
+ sim.dut.top_in = 0xabn;
1529
+ sim.tick();
1530
+ expect(sim.dut.top_out).toBe(0xabn);
1495
1531
 
1496
- // Read the same value through the child instance accessor
1497
- expect((sim.dut as any).u_sub.o_data).toBe(0xABn);
1498
- expect((sim.dut as any).u_sub.i_data).toBe(0xABn);
1532
+ // Read the same value through the child instance accessor
1533
+ expect((sim.dut as any).u_sub.o_data).toBe(0xabn);
1534
+ expect((sim.dut as any).u_sub.i_data).toBe(0xabn);
1499
1535
 
1500
- // Change top-level input and verify child reflects it
1501
- sim.dut.top_in = 0x42n;
1502
- sim.tick();
1503
- expect((sim.dut as any).u_sub.i_data).toBe(0x42n);
1504
- expect((sim.dut as any).u_sub.o_data).toBe(0x42n);
1536
+ // Change top-level input and verify child reflects it
1537
+ sim.dut.top_in = 0x42n;
1538
+ sim.tick();
1539
+ expect((sim.dut as any).u_sub.i_data).toBe(0x42n);
1540
+ expect((sim.dut as any).u_sub.o_data).toBe(0x42n);
1505
1541
 
1506
- sim.dispose();
1507
- });
1542
+ sim.dispose();
1543
+ });
1508
1544
 
1509
- test("Simulation: read child instance ports via dut.u_sub", () => {
1510
- const sim = Simulation.fromSource(HIERARCHY_SOURCE, "Top");
1511
- sim.addClock("clk", { period: 10 });
1545
+ test("Simulation: read child instance ports via dut.u_sub", () => {
1546
+ const sim = Simulation.fromSource(HIERARCHY_SOURCE, "Top");
1547
+ sim.addClock("clk", { period: 10 });
1512
1548
 
1513
- sim.dut.rst = 0n;
1514
- sim.runUntil(20);
1515
- sim.dut.rst = 1n;
1549
+ sim.dut.rst = 0n;
1550
+ sim.runUntil(20);
1551
+ sim.dut.rst = 1n;
1516
1552
 
1517
- sim.dut.top_in = 0x55n;
1518
- sim.runUntil(40);
1519
- expect((sim.dut as any).u_sub.o_data).toBe(0x55n);
1520
- expect((sim.dut as any).u_sub.i_data).toBe(0x55n);
1553
+ sim.dut.top_in = 0x55n;
1554
+ sim.runUntil(40);
1555
+ expect((sim.dut as any).u_sub.o_data).toBe(0x55n);
1556
+ expect((sim.dut as any).u_sub.i_data).toBe(0x55n);
1521
1557
 
1522
- sim.dut.top_in = 0xFFn;
1523
- sim.runUntil(60);
1524
- expect((sim.dut as any).u_sub.o_data).toBe(0xFFn);
1558
+ sim.dut.top_in = 0xffn;
1559
+ sim.runUntil(60);
1560
+ expect((sim.dut as any).u_sub.o_data).toBe(0xffn);
1525
1561
 
1526
- sim.dispose();
1527
- });
1562
+ sim.dispose();
1563
+ });
1528
1564
  });
1529
1565
 
1530
1566
  describe("E2E: parameter override — DUT correctness", () => {
1531
- test("scalar width override: WIDTH 8→16 via fromSource", () => {
1532
- interface Ports {
1533
- a: bigint;
1534
- readonly b: bigint;
1535
- }
1536
-
1537
- // Default WIDTH=8 (purely combinational — read output to trigger evalComb)
1538
- const sim8 = Simulator.fromSource<Ports>(PARAM_WIDTH_SOURCE, "ParamWidth");
1539
- sim8.dut.a = 0xABn;
1540
- expect(sim8.dut.b).toBe(0xABn);
1541
- // Writing a 16-bit value to an 8-bit port should truncate
1542
- sim8.dut.a = 0xABCDn;
1543
- expect(sim8.dut.b).toBe(0xCDn); // truncated to 8 bits
1544
- sim8.dispose();
1545
-
1546
- // Override WIDTH=16 — DUT should handle 16-bit values
1547
- const sim16 = Simulator.fromSource<Ports>(PARAM_WIDTH_SOURCE, "ParamWidth", {
1548
- parameters: [{ name: "WIDTH", value: 16 }],
1549
- });
1550
- sim16.dut.a = 0xABCDn;
1551
- expect(sim16.dut.b).toBe(0xABCDn); // full 16-bit value preserved
1552
- sim16.dispose();
1553
- });
1554
-
1555
- test("scalar width override: WIDTH 8→32 via fromSource", () => {
1556
- interface Ports {
1557
- a: bigint;
1558
- readonly b: bigint;
1559
- }
1560
-
1561
- const sim = Simulator.fromSource<Ports>(PARAM_WIDTH_SOURCE, "ParamWidth", {
1562
- parameters: [{ name: "WIDTH", value: 32 }],
1563
- });
1564
- sim.dut.a = 0xDEAD_BEEFn;
1565
- expect(sim.dut.b).toBe(0xDEAD_BEEFn);
1566
- sim.dispose();
1567
- });
1568
-
1569
- test.skip("param value reflected in logic via fromSource", () => { // blocked by upstream Veryl IR bug
1570
- interface Ports {
1571
- a: bigint;
1572
- readonly b: bigint;
1573
- }
1574
-
1575
- // Default OFFSET=10
1576
- const sim10 = Simulator.fromSource<Ports>(PARAM_OFFSET_SOURCE, "ParamOffset");
1577
- sim10.dut.a = 5n;
1578
- expect(sim10.dut.b).toBe(15n);
1579
- sim10.dispose();
1580
-
1581
- // Override OFFSET=100
1582
- const sim100 = Simulator.fromSource<Ports>(PARAM_OFFSET_SOURCE, "ParamOffset", {
1583
- parameters: [{ name: "OFFSET", value: 100 }],
1584
- });
1585
- sim100.dut.a = 5n;
1586
- expect(sim100.dut.b).toBe(105n);
1587
- sim100.dispose();
1588
- });
1589
-
1590
- test("child module param propagation via fromSource", () => {
1591
- interface Ports {
1592
- a: bigint;
1593
- readonly b: bigint;
1594
- }
1595
-
1596
- // Default WIDTH=8
1597
- const sim8 = Simulator.fromSource<Ports>(PARAM_CHILD_SOURCE, "ParamChild");
1598
- sim8.dut.a = 0xABn;
1599
- expect(sim8.dut.b).toBe(0xABn);
1600
- sim8.dispose();
1601
-
1602
- // Override WIDTH=16 child also gets 16-bit ports
1603
- const sim16 = Simulator.fromSource<Ports>(PARAM_CHILD_SOURCE, "ParamChild", {
1604
- parameters: [{ name: "WIDTH", value: 16 }],
1605
- });
1606
- sim16.dut.a = 0xABCDn;
1607
- expect(sim16.dut.b).toBe(0xABCDn);
1608
- sim16.dispose();
1609
- });
1610
-
1611
- test("Simulator.create() with stale ModuleDefinition + param override", () => {
1612
- // This is the most dangerous case: ModuleDefinition has width=8 (stale),
1613
- // but we override WIDTH=16. The DUT must use runtime-derived ports
1614
- // from hierarchy, not the stale module.ports.
1615
- interface Ports {
1616
- a: bigint;
1617
- readonly b: bigint;
1618
- }
1619
-
1620
- const addon = loadNativeAddon();
1621
- const nativeCreate = createSimulatorBridge(addon);
1622
-
1623
- const sim = Simulator.create<Ports>(
1624
- {
1625
- __celox_module: true,
1626
- name: "ParamWidth",
1627
- source: PARAM_WIDTH_SOURCE,
1628
- ports: {
1629
- // STALE: these say width=8, but we'll override to 16
1630
- a: { direction: "input", type: "logic", width: 8 },
1631
- b: { direction: "output", type: "logic", width: 8 },
1632
- },
1633
- events: [],
1634
- },
1635
- {
1636
- __nativeCreate: nativeCreate,
1637
- parameters: [{ name: "WIDTH", value: 16 }],
1638
- },
1639
- );
1640
-
1641
- // If DUT used stale ports (width=8), this would truncate or corrupt
1642
- sim.dut.a = 0xABCDn;
1643
- expect(sim.dut.b).toBe(0xABCDn); // full 16-bit value preserved
1644
- sim.dispose();
1645
- });
1646
-
1647
- test("Simulation.fromSource with param override", () => {
1648
- interface Ports {
1649
- a: bigint;
1650
- readonly b: bigint;
1651
- }
1652
-
1653
- const sim = Simulation.fromSource<Ports>(PARAM_WIDTH_SOURCE, "ParamWidth", {
1654
- parameters: [{ name: "WIDTH", value: 16 }],
1655
- });
1656
-
1657
- sim.dut.a = 0xFEDCn;
1658
- sim.runUntil(0);
1659
- expect(sim.dut.b).toBe(0xFEDCn);
1660
- sim.dispose();
1661
- });
1567
+ test("scalar width override: WIDTH 8→16 via fromSource", () => {
1568
+ interface Ports {
1569
+ a: bigint;
1570
+ readonly b: bigint;
1571
+ }
1572
+
1573
+ // Default WIDTH=8 (purely combinational — read output to trigger evalComb)
1574
+ const sim8 = Simulator.fromSource<Ports>(PARAM_WIDTH_SOURCE, "ParamWidth");
1575
+ sim8.dut.a = 0xabn;
1576
+ expect(sim8.dut.b).toBe(0xabn);
1577
+ // Writing a 16-bit value to an 8-bit port should truncate
1578
+ sim8.dut.a = 0xabcdn;
1579
+ expect(sim8.dut.b).toBe(0xcdn); // truncated to 8 bits
1580
+ sim8.dispose();
1581
+
1582
+ // Override WIDTH=16 — DUT should handle 16-bit values
1583
+ const sim16 = Simulator.fromSource<Ports>(
1584
+ PARAM_WIDTH_SOURCE,
1585
+ "ParamWidth",
1586
+ {
1587
+ parameters: [{ name: "WIDTH", value: 16 }],
1588
+ },
1589
+ );
1590
+ sim16.dut.a = 0xabcdn;
1591
+ expect(sim16.dut.b).toBe(0xabcdn); // full 16-bit value preserved
1592
+ sim16.dispose();
1593
+ });
1594
+
1595
+ test("scalar width override: WIDTH 8→32 via fromSource", () => {
1596
+ interface Ports {
1597
+ a: bigint;
1598
+ readonly b: bigint;
1599
+ }
1600
+
1601
+ const sim = Simulator.fromSource<Ports>(PARAM_WIDTH_SOURCE, "ParamWidth", {
1602
+ parameters: [{ name: "WIDTH", value: 32 }],
1603
+ });
1604
+ sim.dut.a = 0xdead_beefn;
1605
+ expect(sim.dut.b).toBe(0xdead_beefn);
1606
+ sim.dispose();
1607
+ });
1608
+
1609
+ test.skip("param value reflected in logic via fromSource", () => {
1610
+ // blocked by upstream Veryl IR bug
1611
+ interface Ports {
1612
+ a: bigint;
1613
+ readonly b: bigint;
1614
+ }
1615
+
1616
+ // Default OFFSET=10
1617
+ const sim10 = Simulator.fromSource<Ports>(
1618
+ PARAM_OFFSET_SOURCE,
1619
+ "ParamOffset",
1620
+ );
1621
+ sim10.dut.a = 5n;
1622
+ expect(sim10.dut.b).toBe(15n);
1623
+ sim10.dispose();
1624
+
1625
+ // Override OFFSET=100
1626
+ const sim100 = Simulator.fromSource<Ports>(
1627
+ PARAM_OFFSET_SOURCE,
1628
+ "ParamOffset",
1629
+ {
1630
+ parameters: [{ name: "OFFSET", value: 100 }],
1631
+ },
1632
+ );
1633
+ sim100.dut.a = 5n;
1634
+ expect(sim100.dut.b).toBe(105n);
1635
+ sim100.dispose();
1636
+ });
1637
+
1638
+ test("child module param propagation via fromSource", () => {
1639
+ interface Ports {
1640
+ a: bigint;
1641
+ readonly b: bigint;
1642
+ }
1643
+
1644
+ // Default WIDTH=8
1645
+ const sim8 = Simulator.fromSource<Ports>(PARAM_CHILD_SOURCE, "ParamChild");
1646
+ sim8.dut.a = 0xabn;
1647
+ expect(sim8.dut.b).toBe(0xabn);
1648
+ sim8.dispose();
1649
+
1650
+ // Override WIDTH=16 child also gets 16-bit ports
1651
+ const sim16 = Simulator.fromSource<Ports>(
1652
+ PARAM_CHILD_SOURCE,
1653
+ "ParamChild",
1654
+ {
1655
+ parameters: [{ name: "WIDTH", value: 16 }],
1656
+ },
1657
+ );
1658
+ sim16.dut.a = 0xabcdn;
1659
+ expect(sim16.dut.b).toBe(0xabcdn);
1660
+ sim16.dispose();
1661
+ });
1662
+
1663
+ test("Simulator.create() with stale ModuleDefinition + param override", () => {
1664
+ // This is the most dangerous case: ModuleDefinition has width=8 (stale),
1665
+ // but we override WIDTH=16. The DUT must use runtime-derived ports
1666
+ // from hierarchy, not the stale module.ports.
1667
+ interface Ports {
1668
+ a: bigint;
1669
+ readonly b: bigint;
1670
+ }
1671
+
1672
+ const addon = loadNativeAddon();
1673
+ const nativeCreate = createSimulatorBridge(addon);
1674
+
1675
+ const sim = Simulator.create<Ports>(
1676
+ {
1677
+ __celox_module: true,
1678
+ name: "ParamWidth",
1679
+ source: PARAM_WIDTH_SOURCE,
1680
+ ports: {
1681
+ // STALE: these say width=8, but we'll override to 16
1682
+ a: { direction: "input", type: "logic", width: 8 },
1683
+ b: { direction: "output", type: "logic", width: 8 },
1684
+ },
1685
+ events: [],
1686
+ },
1687
+ {
1688
+ __nativeCreate: nativeCreate,
1689
+ parameters: [{ name: "WIDTH", value: 16 }],
1690
+ },
1691
+ );
1692
+
1693
+ // If DUT used stale ports (width=8), this would truncate or corrupt
1694
+ sim.dut.a = 0xabcdn;
1695
+ expect(sim.dut.b).toBe(0xabcdn); // full 16-bit value preserved
1696
+ sim.dispose();
1697
+ });
1698
+
1699
+ test("Simulation.fromSource with param override", () => {
1700
+ interface Ports {
1701
+ a: bigint;
1702
+ readonly b: bigint;
1703
+ }
1704
+
1705
+ const sim = Simulation.fromSource<Ports>(PARAM_WIDTH_SOURCE, "ParamWidth", {
1706
+ parameters: [{ name: "WIDTH", value: 16 }],
1707
+ });
1708
+
1709
+ sim.dut.a = 0xfedcn;
1710
+ sim.runUntil(0);
1711
+ expect(sim.dut.b).toBe(0xfedcn);
1712
+ sim.dispose();
1713
+ });
1662
1714
  });
1663
1715
 
1664
1716
  // ---------------------------------------------------------------------------
@@ -1666,65 +1718,77 @@ describe("E2E: parameter override — DUT correctness", () => {
1666
1718
  // ---------------------------------------------------------------------------
1667
1719
 
1668
1720
  describe("E2E: celox.toml test sources", () => {
1669
- test("fromProject loads test-only module declared in celox.toml", () => {
1670
- interface RegPorts {
1671
- rst: bigint;
1672
- d: bigint;
1673
- readonly q: bigint;
1674
- }
1675
-
1676
- // `Reg` lives in test_veryl/ — not in Veryl.toml sources — but celox.toml
1677
- // declares test_veryl/ as an additional test source directory.
1678
- const sim = Simulator.fromProject<RegPorts>(CELOX_TOML_PROJECT, "Reg");
1679
-
1680
- // Deassert async_low reset (rst=0 is active; rst=1 is normal operation)
1681
- sim.dut.rst = 1n;
1682
- sim.dut.d = 0xABn;
1683
- sim.tick();
1684
- expect(sim.dut.q).toBe(0xABn);
1685
-
1686
- sim.dut.d = 0xCDn;
1687
- expect(sim.dut.q).toBe(0xABn); // not yet clocked
1688
- sim.tick();
1689
- expect(sim.dut.q).toBe(0xCDn);
1690
-
1691
- sim.dispose();
1692
- });
1693
-
1694
- test("genTs includes test-only module declared in celox.toml", () => {
1695
- const addon = loadNativeAddon();
1696
- const json = addon.genTs(CELOX_TOML_PROJECT);
1697
- const output = JSON.parse(json) as { modules: Array<{ moduleName: string }> };
1698
- const names = output.modules.map((m) => m.moduleName);
1699
- // Both the regular source (Adder) and the test-only source (Reg) must appear
1700
- expect(names).toContain("Adder");
1701
- expect(names).toContain("Reg");
1702
- });
1703
-
1704
- test("[simulation] max_steps from celox.toml is used as waitUntil default", () => {
1705
- // celox.toml sets [simulation] max_steps = 20; waitUntil should honour it
1706
- // when no per-call maxSteps is provided.
1707
- const sim = Simulation.fromProject(CELOX_TOML_PROJECT, "Reg");
1708
- sim.addClock("clk", { period: 10 });
1709
- expect(() => sim.waitUntil(() => false)).toThrow(SimulationTimeoutError);
1710
- const err = (() => {
1711
- try { sim.waitUntil(() => false); } catch (e) { return e; }
1712
- })() as SimulationTimeoutError;
1713
- expect(err.steps).toBe(20);
1714
- sim.dispose();
1715
- });
1716
-
1717
- test("[simulation] max_steps per-call override takes precedence over celox.toml", () => {
1718
- const sim = Simulation.fromProject(CELOX_TOML_PROJECT, "Reg");
1719
- sim.addClock("clk", { period: 10 });
1720
- // Per-call maxSteps=5 is lower than the toml default of 20
1721
- expect(() => sim.waitUntil(() => false, { maxSteps: 5 })).toThrow(SimulationTimeoutError);
1722
- const err = (() => {
1723
- try { sim.waitUntil(() => false, { maxSteps: 5 }); } catch (e) { return e; }
1724
- })() as SimulationTimeoutError;
1725
- expect(err.steps).toBe(5);
1726
- sim.dispose();
1727
- });
1721
+ test("fromProject loads test-only module declared in celox.toml", () => {
1722
+ interface RegPorts {
1723
+ rst: bigint;
1724
+ d: bigint;
1725
+ readonly q: bigint;
1726
+ }
1727
+
1728
+ // `Reg` lives in test_veryl/ — not in Veryl.toml sources — but celox.toml
1729
+ // declares test_veryl/ as an additional test source directory.
1730
+ const sim = Simulator.fromProject<RegPorts>(CELOX_TOML_PROJECT, "Reg");
1731
+
1732
+ // Deassert async_low reset (rst=0 is active; rst=1 is normal operation)
1733
+ sim.dut.rst = 1n;
1734
+ sim.dut.d = 0xabn;
1735
+ sim.tick();
1736
+ expect(sim.dut.q).toBe(0xabn);
1737
+
1738
+ sim.dut.d = 0xcdn;
1739
+ expect(sim.dut.q).toBe(0xabn); // not yet clocked
1740
+ sim.tick();
1741
+ expect(sim.dut.q).toBe(0xcdn);
1742
+
1743
+ sim.dispose();
1744
+ });
1745
+
1746
+ test("genTs includes test-only module declared in celox.toml", () => {
1747
+ const addon = loadNativeAddon();
1748
+ const json = addon.genTs(CELOX_TOML_PROJECT);
1749
+ const output = JSON.parse(json) as {
1750
+ modules: Array<{ moduleName: string }>;
1751
+ };
1752
+ const names = output.modules.map((m) => m.moduleName);
1753
+ // Both the regular source (Adder) and the test-only source (Reg) must appear
1754
+ expect(names).toContain("Adder");
1755
+ expect(names).toContain("Reg");
1756
+ });
1757
+
1758
+ test("[simulation] max_steps from celox.toml is used as waitUntil default", () => {
1759
+ // celox.toml sets [simulation] max_steps = 20; waitUntil should honour it
1760
+ // when no per-call maxSteps is provided.
1761
+ const sim = Simulation.fromProject(CELOX_TOML_PROJECT, "Reg");
1762
+ sim.addClock("clk", { period: 10 });
1763
+ expect(() => sim.waitUntil(() => false)).toThrow(SimulationTimeoutError);
1764
+ const err = (() => {
1765
+ try {
1766
+ sim.waitUntil(() => false);
1767
+ } catch (e) {
1768
+ return e;
1769
+ }
1770
+ })() as SimulationTimeoutError;
1771
+ expect(err.steps).toBe(20);
1772
+ sim.dispose();
1773
+ });
1774
+
1775
+ test("[simulation] max_steps per-call override takes precedence over celox.toml", () => {
1776
+ const sim = Simulation.fromProject(CELOX_TOML_PROJECT, "Reg");
1777
+ sim.addClock("clk", { period: 10 });
1778
+ // Per-call maxSteps=5 is lower than the toml default of 20
1779
+ expect(() => sim.waitUntil(() => false, { maxSteps: 5 })).toThrow(
1780
+ SimulationTimeoutError,
1781
+ );
1782
+ const err = (() => {
1783
+ try {
1784
+ sim.waitUntil(() => false, { maxSteps: 5 });
1785
+ } catch (e) {
1786
+ return e;
1787
+ }
1788
+ })() as SimulationTimeoutError;
1789
+ expect(err.steps).toBe(5);
1790
+ sim.dispose();
1791
+ });
1728
1792
  });
1729
1793
 
1730
1794
  // ---------------------------------------------------------------------------
@@ -1802,29 +1866,29 @@ module Top (
1802
1866
  `;
1803
1867
 
1804
1868
  describe("E2E: interface port DUT accessor", () => {
1805
- test("top-level interface port members accessible as nested dut properties", () => {
1806
- interface TopPorts {
1807
- sig: { data: bigint; valid: bigint };
1808
- readonly out: bigint;
1809
- }
1869
+ test("top-level interface port members accessible as nested dut properties", () => {
1870
+ interface TopPorts {
1871
+ sig: { data: bigint; valid: bigint };
1872
+ readonly out: bigint;
1873
+ }
1810
1874
 
1811
- const sim = Simulator.fromSource<TopPorts>(INTERFACE_PORT_SOURCE, "Top");
1875
+ const sim = Simulator.fromSource<TopPorts>(INTERFACE_PORT_SOURCE, "Top");
1812
1876
 
1813
- sim.dut.sig.data = 0x42n;
1814
- expect(sim.dut.out).toBe(0x42n);
1877
+ sim.dut.sig.data = 0x42n;
1878
+ expect(sim.dut.out).toBe(0x42n);
1815
1879
 
1816
- sim.dut.sig.data = 0xFFn;
1817
- expect(sim.dut.out).toBe(0xFFn);
1880
+ sim.dut.sig.data = 0xffn;
1881
+ expect(sim.dut.out).toBe(0xffn);
1818
1882
 
1819
- sim.dut.sig.data = 0x00n;
1820
- expect(sim.dut.out).toBe(0x00n);
1883
+ sim.dut.sig.data = 0x00n;
1884
+ expect(sim.dut.out).toBe(0x00n);
1821
1885
 
1822
- sim.dispose();
1823
- });
1886
+ sim.dispose();
1887
+ });
1824
1888
 
1825
- test("writing to interface output member throws", () => {
1826
- // bus.data / bus.valid are outputs of Top — writing should throw.
1827
- const PRODUCER_ONLY = `
1889
+ test("writing to interface output member throws", () => {
1890
+ // bus.data / bus.valid are outputs of Top — writing should throw.
1891
+ const PRODUCER_ONLY = `
1828
1892
  interface Bus {
1829
1893
  var data: logic<8>;
1830
1894
  var valid: logic;
@@ -1841,52 +1905,55 @@ module Top (
1841
1905
  assign bus.valid = 1'b1;
1842
1906
  }
1843
1907
  `;
1844
- const sim = Simulator.fromSource(PRODUCER_ONLY, "Top");
1908
+ const sim = Simulator.fromSource(PRODUCER_ONLY, "Top");
1845
1909
 
1846
- expect(() => {
1847
- (sim.dut as any).bus.data = 0n;
1848
- }).toThrow("Cannot write to output port 'data'");
1910
+ expect(() => {
1911
+ (sim.dut as any).bus.data = 0n;
1912
+ }).toThrow("Cannot write to output port 'data'");
1849
1913
 
1850
- sim.dispose();
1851
- });
1914
+ sim.dispose();
1915
+ });
1852
1916
 
1853
- test("interface port propagates through sub-module instances", () => {
1854
- interface TopPorts {
1855
- val: bigint;
1856
- readonly out: bigint;
1857
- readonly got: bigint;
1858
- }
1917
+ test("interface port propagates through sub-module instances", () => {
1918
+ interface TopPorts {
1919
+ val: bigint;
1920
+ readonly out: bigint;
1921
+ readonly got: bigint;
1922
+ }
1859
1923
 
1860
- const sim = Simulator.fromSource<TopPorts>(INTERFACE_HIERARCHY_SOURCE, "Top");
1924
+ const sim = Simulator.fromSource<TopPorts>(
1925
+ INTERFACE_HIERARCHY_SOURCE,
1926
+ "Top",
1927
+ );
1861
1928
 
1862
- sim.dut.val = 0xABn;
1863
- expect(sim.dut.out).toBe(0xABn);
1864
- expect(sim.dut.got).toBe(1n);
1929
+ sim.dut.val = 0xabn;
1930
+ expect(sim.dut.out).toBe(0xabn);
1931
+ expect(sim.dut.got).toBe(1n);
1865
1932
 
1866
- sim.dut.val = 0x00n;
1867
- expect(sim.dut.out).toBe(0x00n);
1933
+ sim.dut.val = 0x00n;
1934
+ expect(sim.dut.out).toBe(0x00n);
1868
1935
 
1869
- sim.dispose();
1870
- });
1936
+ sim.dispose();
1937
+ });
1871
1938
 
1872
- test("multiple interface port members are independently accessible", () => {
1873
- interface TopPorts {
1874
- sig: { data: bigint; valid: bigint };
1875
- readonly out: bigint;
1876
- }
1939
+ test("multiple interface port members are independently accessible", () => {
1940
+ interface TopPorts {
1941
+ sig: { data: bigint; valid: bigint };
1942
+ readonly out: bigint;
1943
+ }
1877
1944
 
1878
- const sim = Simulator.fromSource<TopPorts>(INTERFACE_PORT_SOURCE, "Top");
1945
+ const sim = Simulator.fromSource<TopPorts>(INTERFACE_PORT_SOURCE, "Top");
1879
1946
 
1880
- sim.dut.sig.data = 0x77n;
1881
- sim.dut.sig.valid = 1n;
1882
- expect(sim.dut.out).toBe(0x77n);
1947
+ sim.dut.sig.data = 0x77n;
1948
+ sim.dut.sig.valid = 1n;
1949
+ expect(sim.dut.out).toBe(0x77n);
1883
1950
 
1884
- sim.dut.sig.data = 0x33n;
1885
- sim.dut.sig.valid = 0n;
1886
- expect(sim.dut.out).toBe(0x33n);
1951
+ sim.dut.sig.data = 0x33n;
1952
+ sim.dut.sig.valid = 0n;
1953
+ expect(sim.dut.out).toBe(0x33n);
1887
1954
 
1888
- sim.dispose();
1889
- });
1955
+ sim.dispose();
1956
+ });
1890
1957
  });
1891
1958
 
1892
1959
  // ---------------------------------------------------------------------------
@@ -1912,30 +1979,38 @@ module Top (
1912
1979
  `;
1913
1980
 
1914
1981
  describe("E2E: interface array port (modport [N])", () => {
1915
- test("interface array members accessible via .at()/.set()", () => {
1916
- interface TopPorts {
1917
- bus: {
1918
- data: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
1919
- valid: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
1920
- };
1921
- readonly out: bigint;
1922
- }
1923
-
1924
- const sim = Simulator.fromSource<TopPorts>(INTERFACE_ARRAY_SOURCE, "Top");
1925
-
1926
- sim.dut.bus.data.set(0, 0x10n);
1927
- sim.dut.bus.data.set(1, 0x20n);
1928
- expect(sim.dut.out).toBe(0x30n);
1929
-
1930
- sim.dut.bus.data.set(0, 0xAAn);
1931
- sim.dut.bus.data.set(1, 0x11n);
1932
- expect(sim.dut.out).toBe(0xBBn);
1933
-
1934
- expect(sim.dut.bus.data.length).toBe(2);
1935
- expect(sim.dut.bus.valid.length).toBe(2);
1936
-
1937
- sim.dispose();
1938
- });
1982
+ test("interface array members accessible via .at()/.set()", () => {
1983
+ interface TopPorts {
1984
+ bus: {
1985
+ data: {
1986
+ at(i: number): bigint;
1987
+ set(i: number, v: bigint): void;
1988
+ length: number;
1989
+ };
1990
+ valid: {
1991
+ at(i: number): bigint;
1992
+ set(i: number, v: bigint): void;
1993
+ length: number;
1994
+ };
1995
+ };
1996
+ readonly out: bigint;
1997
+ }
1998
+
1999
+ const sim = Simulator.fromSource<TopPorts>(INTERFACE_ARRAY_SOURCE, "Top");
2000
+
2001
+ sim.dut.bus.data.set(0, 0x10n);
2002
+ sim.dut.bus.data.set(1, 0x20n);
2003
+ expect(sim.dut.out).toBe(0x30n);
2004
+
2005
+ sim.dut.bus.data.set(0, 0xaan);
2006
+ sim.dut.bus.data.set(1, 0x11n);
2007
+ expect(sim.dut.out).toBe(0xbbn);
2008
+
2009
+ expect(sim.dut.bus.data.length).toBe(2);
2010
+ expect(sim.dut.bus.valid.length).toBe(2);
2011
+
2012
+ sim.dispose();
2013
+ });
1939
2014
  });
1940
2015
 
1941
2016
  // ---------------------------------------------------------------------------
@@ -1961,114 +2036,312 @@ module ArrayPassThrough (
1961
2036
  }
1962
2037
  `;
1963
2038
 
1964
- describe("E2E: 1-bit unpacked array port (logic [N])", () => {
1965
- test("each element is independently visible in hardware (issue #6)", () => {
1966
- interface Ports {
1967
- rst: bigint;
1968
- en: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
1969
- readonly y0: bigint;
1970
- readonly y1: bigint;
1971
- readonly y2: bigint;
1972
- readonly y3: bigint;
1973
- }
2039
+ // ---------------------------------------------------------------------------
2040
+ // Internal var access tests
2041
+ // ---------------------------------------------------------------------------
1974
2042
 
1975
- const sim = Simulator.fromSource<Ports>(ARRAY_1BIT_SOURCE, "ArrayPassThrough");
1976
-
1977
- // Default: all inputs 0
1978
- sim.tick();
1979
- expect(sim.dut.y0).toBe(0n);
1980
- expect(sim.dut.y1).toBe(0n);
1981
- expect(sim.dut.y2).toBe(0n);
1982
- expect(sim.dut.y3).toBe(0n);
1983
-
1984
- // Set element 0 only
1985
- sim.dut.en.set(0, 1n);
1986
- sim.tick();
1987
- expect(sim.dut.y0).toBe(1n);
1988
- expect(sim.dut.y1).toBe(0n);
1989
- expect(sim.dut.y2).toBe(0n);
1990
- expect(sim.dut.y3).toBe(0n);
1991
-
1992
- // Set element 1 (was silently ignored before the fix)
1993
- sim.dut.en.set(0, 0n);
1994
- sim.dut.en.set(1, 1n);
1995
- sim.tick();
1996
- expect(sim.dut.y0).toBe(0n);
1997
- expect(sim.dut.y1).toBe(1n);
1998
- expect(sim.dut.y2).toBe(0n);
1999
- expect(sim.dut.y3).toBe(0n);
2000
-
2001
- // Set elements 2 and 3
2002
- sim.dut.en.set(1, 0n);
2003
- sim.dut.en.set(2, 1n);
2004
- sim.dut.en.set(3, 1n);
2005
- sim.tick();
2006
- expect(sim.dut.y0).toBe(0n);
2007
- expect(sim.dut.y1).toBe(0n);
2008
- expect(sim.dut.y2).toBe(1n);
2009
- expect(sim.dut.y3).toBe(1n);
2010
-
2011
- sim.dispose();
2012
- });
2013
-
2014
- test("length property is correct", () => {
2015
- interface Ports {
2016
- rst: bigint;
2017
- en: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
2018
- readonly y0: bigint;
2019
- readonly y1: bigint;
2020
- readonly y2: bigint;
2021
- readonly y3: bigint;
2043
+ const INTERNAL_VAR_HIERARCHY_SOURCE = `
2044
+ module SubCounter (
2045
+ clk: input clock,
2046
+ rst: input reset,
2047
+ en: input logic,
2048
+ count: output logic<8>,
2049
+ ) {
2050
+ var count_r: logic<8>;
2051
+ always_ff (clk, rst) {
2052
+ if_reset {
2053
+ count_r = 0;
2054
+ } else if en {
2055
+ count_r = count_r + 1;
2056
+ }
2022
2057
  }
2058
+ assign count = count_r;
2059
+ }
2023
2060
 
2024
- const sim = Simulator.fromSource<Ports>(ARRAY_1BIT_SOURCE, "ArrayPassThrough");
2025
- expect(sim.dut.en.length).toBe(4);
2026
- sim.dispose();
2027
- });
2028
-
2029
- test("4-state mode: X written to element i propagates only to output yi (verifies maskBase layout)", () => {
2030
- // This test validates that the maskBase = baseOffset + totalValueBytes assumption
2031
- // matches the actual JIT memory layout when fourState: true is used.
2032
- interface Ports {
2033
- rst: bigint;
2034
- en: { at(i: number): bigint; set(i: number, v: unknown): void; length: number };
2035
- readonly y0: bigint;
2036
- readonly y1: bigint;
2037
- readonly y2: bigint;
2038
- readonly y3: bigint;
2061
+ module TopCounter (
2062
+ clk: input clock,
2063
+ rst: input reset,
2064
+ en: input logic,
2065
+ top_count: output logic<8>,
2066
+ ) {
2067
+ var top_reg: logic<8>;
2068
+ inst u_sub: SubCounter (
2069
+ clk,
2070
+ rst,
2071
+ en,
2072
+ count: top_count,
2073
+ );
2074
+ always_ff (clk, rst) {
2075
+ if_reset {
2076
+ top_reg = 0;
2077
+ } else {
2078
+ top_reg = top_count;
2079
+ }
2039
2080
  }
2081
+ }
2082
+ `;
2083
+
2084
+ describe("E2E: internal var access", () => {
2085
+ test("top-level internal var is accessible and tracks values", () => {
2086
+ const sim = Simulator.fromSource(COUNTER_SOURCE, "Counter");
2087
+
2088
+ // Reset
2089
+ sim.dut.rst = 0n;
2090
+ sim.tick();
2091
+ sim.dut.rst = 1n;
2092
+ sim.tick();
2093
+
2094
+ // Internal var count_r should be accessible and start at 0
2095
+ expect((sim.dut as any).count_r).toBe(0n);
2096
+
2097
+ // Enable counting and tick
2098
+ sim.dut.en = 1n;
2099
+ sim.tick();
2100
+ expect((sim.dut as any).count_r).toBe(1n);
2101
+
2102
+ sim.tick();
2103
+ expect((sim.dut as any).count_r).toBe(2n);
2040
2104
 
2041
- const sim = Simulator.fromSource<Ports>(ARRAY_1BIT_SOURCE, "ArrayPassThrough", { fourState: true });
2105
+ // count_r should equal count (output)
2106
+ expect((sim.dut as any).count_r).toBe(sim.dut.count);
2042
2107
 
2043
- // Start with all defined zeros
2044
- for (let i = 0; i < 4; i++) sim.dut.en.set(i, 0n);
2045
- sim.tick();
2108
+ sim.dispose();
2109
+ });
2046
2110
 
2047
- // Set element 1 to X; only y1 should become X
2048
- sim.dut.en.set(1, X);
2049
- sim.tick();
2111
+ test("child instance internal var is accessible", () => {
2112
+ const sim = Simulator.fromSource(
2113
+ INTERNAL_VAR_HIERARCHY_SOURCE,
2114
+ "TopCounter",
2115
+ );
2050
2116
 
2051
- expect(sim.fourState("y1").mask).not.toBe(0n); // y1 = X
2052
- expect(sim.fourState("y0").mask).toBe(0n); // y0 defined
2053
- expect(sim.fourState("y2").mask).toBe(0n); // y2 defined
2054
- expect(sim.fourState("y3").mask).toBe(0n); // y3 defined
2117
+ // Reset
2118
+ sim.dut.rst = 0n;
2119
+ sim.tick();
2120
+ sim.dut.rst = 1n;
2121
+ sim.tick();
2055
2122
 
2056
- // Set element 2 to X; y2 becomes X, y1 remains X
2057
- sim.dut.en.set(2, X);
2058
- sim.tick();
2123
+ // Top-level internal var
2124
+ expect((sim.dut as any).top_reg).toBe(0n);
2059
2125
 
2060
- expect(sim.fourState("y1").mask).not.toBe(0n);
2061
- expect(sim.fourState("y2").mask).not.toBe(0n);
2062
- expect(sim.fourState("y0").mask).toBe(0n);
2063
- expect(sim.fourState("y3").mask).toBe(0n);
2126
+ // Child instance internal var
2127
+ expect((sim.dut as any).u_sub.count_r).toBe(0n);
2064
2128
 
2065
- // Clear element 1 to defined 0; y1 becomes defined again
2066
- sim.dut.en.set(1, 0n);
2067
- sim.tick();
2129
+ // Enable counting
2130
+ sim.dut.en = 1n;
2131
+ sim.tick();
2132
+ expect((sim.dut as any).u_sub.count_r).toBe(1n);
2068
2133
 
2069
- expect(sim.fourState("y1").mask).toBe(0n);
2070
- expect(sim.fourState("y2").mask).not.toBe(0n);
2134
+ sim.tick();
2135
+ expect((sim.dut as any).u_sub.count_r).toBe(2n);
2136
+
2137
+ sim.dispose();
2138
+ });
2139
+ });
2140
+
2141
+ describe("E2E: 1-bit unpacked array port (logic [N])", () => {
2142
+ test("each element is independently visible in hardware (issue #6)", () => {
2143
+ interface Ports {
2144
+ rst: bigint;
2145
+ en: {
2146
+ at(i: number): bigint;
2147
+ set(i: number, v: bigint): void;
2148
+ length: number;
2149
+ };
2150
+ readonly y0: bigint;
2151
+ readonly y1: bigint;
2152
+ readonly y2: bigint;
2153
+ readonly y3: bigint;
2154
+ }
2155
+
2156
+ const sim = Simulator.fromSource<Ports>(
2157
+ ARRAY_1BIT_SOURCE,
2158
+ "ArrayPassThrough",
2159
+ );
2160
+
2161
+ // Default: all inputs 0
2162
+ sim.tick();
2163
+ expect(sim.dut.y0).toBe(0n);
2164
+ expect(sim.dut.y1).toBe(0n);
2165
+ expect(sim.dut.y2).toBe(0n);
2166
+ expect(sim.dut.y3).toBe(0n);
2167
+
2168
+ // Set element 0 only
2169
+ sim.dut.en.set(0, 1n);
2170
+ sim.tick();
2171
+ expect(sim.dut.y0).toBe(1n);
2172
+ expect(sim.dut.y1).toBe(0n);
2173
+ expect(sim.dut.y2).toBe(0n);
2174
+ expect(sim.dut.y3).toBe(0n);
2175
+
2176
+ // Set element 1 (was silently ignored before the fix)
2177
+ sim.dut.en.set(0, 0n);
2178
+ sim.dut.en.set(1, 1n);
2179
+ sim.tick();
2180
+ expect(sim.dut.y0).toBe(0n);
2181
+ expect(sim.dut.y1).toBe(1n);
2182
+ expect(sim.dut.y2).toBe(0n);
2183
+ expect(sim.dut.y3).toBe(0n);
2184
+
2185
+ // Set elements 2 and 3
2186
+ sim.dut.en.set(1, 0n);
2187
+ sim.dut.en.set(2, 1n);
2188
+ sim.dut.en.set(3, 1n);
2189
+ sim.tick();
2190
+ expect(sim.dut.y0).toBe(0n);
2191
+ expect(sim.dut.y1).toBe(0n);
2192
+ expect(sim.dut.y2).toBe(1n);
2193
+ expect(sim.dut.y3).toBe(1n);
2194
+
2195
+ sim.dispose();
2196
+ });
2197
+
2198
+ test("length property is correct", () => {
2199
+ interface Ports {
2200
+ rst: bigint;
2201
+ en: {
2202
+ at(i: number): bigint;
2203
+ set(i: number, v: bigint): void;
2204
+ length: number;
2205
+ };
2206
+ readonly y0: bigint;
2207
+ readonly y1: bigint;
2208
+ readonly y2: bigint;
2209
+ readonly y3: bigint;
2210
+ }
2211
+
2212
+ const sim = Simulator.fromSource<Ports>(
2213
+ ARRAY_1BIT_SOURCE,
2214
+ "ArrayPassThrough",
2215
+ );
2216
+ expect(sim.dut.en.length).toBe(4);
2217
+ sim.dispose();
2218
+ });
2219
+
2220
+ test("4-state mode: X written to element i propagates only to output yi (verifies maskBase layout)", () => {
2221
+ // This test validates that the maskBase = baseOffset + totalValueBytes assumption
2222
+ // matches the actual JIT memory layout when fourState: true is used.
2223
+ interface Ports {
2224
+ rst: bigint;
2225
+ en: {
2226
+ at(i: number): bigint;
2227
+ set(i: number, v: unknown): void;
2228
+ length: number;
2229
+ };
2230
+ readonly y0: bigint;
2231
+ readonly y1: bigint;
2232
+ readonly y2: bigint;
2233
+ readonly y3: bigint;
2234
+ }
2235
+
2236
+ const sim = Simulator.fromSource<Ports>(
2237
+ ARRAY_1BIT_SOURCE,
2238
+ "ArrayPassThrough",
2239
+ { fourState: true },
2240
+ );
2241
+
2242
+ // Start with all defined zeros
2243
+ for (let i = 0; i < 4; i++) sim.dut.en.set(i, 0n);
2244
+ sim.tick();
2245
+
2246
+ // Set element 1 to X; only y1 should become X
2247
+ sim.dut.en.set(1, X);
2248
+ sim.tick();
2249
+
2250
+ expect(sim.fourState("y1").mask).not.toBe(0n); // y1 = X
2251
+ expect(sim.fourState("y0").mask).toBe(0n); // y0 defined
2252
+ expect(sim.fourState("y2").mask).toBe(0n); // y2 defined
2253
+ expect(sim.fourState("y3").mask).toBe(0n); // y3 defined
2254
+
2255
+ // Set element 2 to X; y2 becomes X, y1 remains X
2256
+ sim.dut.en.set(2, X);
2257
+ sim.tick();
2258
+
2259
+ expect(sim.fourState("y1").mask).not.toBe(0n);
2260
+ expect(sim.fourState("y2").mask).not.toBe(0n);
2261
+ expect(sim.fourState("y0").mask).toBe(0n);
2262
+ expect(sim.fourState("y3").mask).toBe(0n);
2263
+
2264
+ // Clear element 1 to defined 0; y1 becomes defined again
2265
+ sim.dut.en.set(1, 0n);
2266
+ sim.tick();
2267
+
2268
+ expect(sim.fourState("y1").mask).toBe(0n);
2269
+ expect(sim.fourState("y2").mask).not.toBe(0n);
2270
+
2271
+ sim.dispose();
2272
+ });
2273
+ });
2274
+
2275
+ // ---------------------------------------------------------------------------
2276
+ // Dead Store Elimination (DSE) tests
2277
+ // ---------------------------------------------------------------------------
2071
2278
 
2072
- sim.dispose();
2073
- });
2279
+ describe("E2E: deadStorePolicy option", () => {
2280
+ test("preserveTopPorts: top ports work, child instances stripped from DUT", () => {
2281
+ const sim = Simulator.fromSource(HIERARCHY_SOURCE, "Top", {
2282
+ deadStorePolicy: "preserveTopPorts",
2283
+ });
2284
+
2285
+ sim.dut.top_in = 0xabn;
2286
+ sim.tick();
2287
+ expect(sim.dut.top_out).toBe(0xabn);
2288
+
2289
+ // With preserveTopPorts, children should be stripped from hierarchy
2290
+ expect((sim.dut as any).u_sub).toBeUndefined();
2291
+
2292
+ sim.dispose();
2293
+ });
2294
+
2295
+ test("preserveAllPorts: top ports AND child instance ports accessible", () => {
2296
+ const sim = Simulator.fromSource(HIERARCHY_SOURCE, "Top", {
2297
+ deadStorePolicy: "preserveAllPorts",
2298
+ });
2299
+
2300
+ sim.dut.top_in = 0x42n;
2301
+ sim.tick();
2302
+ expect(sim.dut.top_out).toBe(0x42n);
2303
+
2304
+ // With preserveAllPorts, child instance ports should remain accessible
2305
+ expect((sim.dut as any).u_sub).toBeDefined();
2306
+ expect((sim.dut as any).u_sub.o_data).toBe(0x42n);
2307
+
2308
+ sim.dispose();
2309
+ });
2310
+
2311
+ test("Simulation: preserveTopPorts strips children", () => {
2312
+ const sim = Simulation.fromSource(HIERARCHY_SOURCE, "Top", {
2313
+ deadStorePolicy: "preserveTopPorts",
2314
+ });
2315
+ sim.addClock("clk", { period: 10 });
2316
+
2317
+ sim.dut.rst = 0n;
2318
+ sim.runUntil(20);
2319
+ sim.dut.rst = 1n;
2320
+
2321
+ sim.dut.top_in = 0x55n;
2322
+ sim.runUntil(40);
2323
+ expect(sim.dut.top_out).toBe(0x55n);
2324
+ expect((sim.dut as any).u_sub).toBeUndefined();
2325
+
2326
+ sim.dispose();
2327
+ });
2328
+
2329
+ test("Simulation: preserveAllPorts keeps children", () => {
2330
+ const sim = Simulation.fromSource(HIERARCHY_SOURCE, "Top", {
2331
+ deadStorePolicy: "preserveAllPorts",
2332
+ });
2333
+ sim.addClock("clk", { period: 10 });
2334
+
2335
+ sim.dut.rst = 0n;
2336
+ sim.runUntil(20);
2337
+ sim.dut.rst = 1n;
2338
+
2339
+ sim.dut.top_in = 0xffn;
2340
+ sim.runUntil(40);
2341
+ expect(sim.dut.top_out).toBe(0xffn);
2342
+ expect((sim.dut as any).u_sub).toBeDefined();
2343
+ expect((sim.dut as any).u_sub.o_data).toBe(0xffn);
2344
+
2345
+ sim.dispose();
2346
+ });
2074
2347
  });