@celox-sim/celox 0.0.1 → 0.1.4

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.
@@ -0,0 +1,965 @@
1
+ /**
2
+ * End-to-end tests for the TypeScript testbench.
3
+ *
4
+ * These tests exercise the full pipeline:
5
+ * Veryl source → Rust JIT (via NAPI) → SharedArrayBuffer bridge → TS DUT → verify
6
+ *
7
+ * Unlike the unit tests which use mock handles, these tests use the real
8
+ * `celox-napi` native addon compiled from the Rust simulator.
9
+ */
10
+
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";
15
+ import { readFourState } from "./dut.js";
16
+ import { X, FourState } from "./types.js";
17
+ import {
18
+ createSimulatorBridge,
19
+ loadNativeAddon,
20
+ parseNapiLayout,
21
+ type RawNapiAddon,
22
+ type RawNapiSimulatorHandle,
23
+ } from "./napi-helpers.js";
24
+
25
+ // Fixture project directories
26
+ const FIXTURES_DIR = path.resolve(import.meta.dirname ?? __dirname, "../fixtures");
27
+ const ADDER_PROJECT = path.join(FIXTURES_DIR, "adder");
28
+ const COUNTER_PROJECT = path.join(FIXTURES_DIR, "counter_project");
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Test Veryl sources
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const ADDER_SOURCE = `
35
+ module Adder (
36
+ clk: input clock,
37
+ rst: input reset,
38
+ a: input logic<16>,
39
+ b: input logic<16>,
40
+ sum: output logic<17>,
41
+ ) {
42
+ always_comb {
43
+ sum = a + b;
44
+ }
45
+ }
46
+ `;
47
+
48
+ const COUNTER_SOURCE = `
49
+ module Counter (
50
+ clk: input clock,
51
+ rst: input reset,
52
+ en: input logic,
53
+ count: output logic<8>,
54
+ ) {
55
+ var count_r: logic<8>;
56
+
57
+ always_ff (clk, rst) {
58
+ if_reset {
59
+ count_r = 0;
60
+ } else if en {
61
+ count_r = count_r + 1;
62
+ }
63
+ }
64
+
65
+ always_comb {
66
+ count = count_r;
67
+ }
68
+ }
69
+ `;
70
+
71
+ const MULTIPLEXER_SOURCE = `
72
+ module Mux4 (
73
+ sel: input logic<2>,
74
+ d0: input logic<8>,
75
+ d1: input logic<8>,
76
+ d2: input logic<8>,
77
+ d3: input logic<8>,
78
+ y: output logic<8>,
79
+ ) {
80
+ always_comb {
81
+ case sel {
82
+ 2'd0: y = d0;
83
+ 2'd1: y = d1;
84
+ 2'd2: y = d2;
85
+ 2'd3: y = d3;
86
+ default: y = 0;
87
+ }
88
+ }
89
+ }
90
+ `;
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Simulator (event-based) e2e tests — fromSource API
94
+ // ---------------------------------------------------------------------------
95
+
96
+ describe("E2E: Simulator.fromSource (event-based)", () => {
97
+ test("combinational adder: a + b = sum", () => {
98
+ interface AdderPorts {
99
+ rst: number;
100
+ a: number;
101
+ b: number;
102
+ readonly sum: number;
103
+ }
104
+
105
+ const sim = Simulator.fromSource<AdderPorts>(ADDER_SOURCE, "Adder");
106
+
107
+ sim.dut.a = 100;
108
+ sim.dut.b = 200;
109
+ sim.tick();
110
+ expect(sim.dut.sum).toBe(300);
111
+
112
+ sim.dut.a = 0xFFFF;
113
+ sim.dut.b = 1;
114
+ sim.tick();
115
+ expect(sim.dut.sum).toBe(0x10000);
116
+
117
+ sim.dut.a = 0;
118
+ sim.dut.b = 0;
119
+ sim.tick();
120
+ expect(sim.dut.sum).toBe(0);
121
+
122
+ sim.dispose();
123
+ });
124
+
125
+ test("combinational adder: lazy evalComb on output read", () => {
126
+ interface AdderPorts {
127
+ rst: number;
128
+ a: number;
129
+ b: number;
130
+ readonly sum: number;
131
+ }
132
+
133
+ const sim = Simulator.fromSource<AdderPorts>(ADDER_SOURCE, "Adder");
134
+
135
+ sim.dut.a = 42;
136
+ sim.dut.b = 58;
137
+ expect(sim.dut.sum).toBe(100);
138
+
139
+ sim.dispose();
140
+ });
141
+
142
+ test("sequential counter: counts on clock edges", () => {
143
+ interface CounterPorts {
144
+ rst: number;
145
+ en: number;
146
+ readonly count: number;
147
+ }
148
+
149
+ const sim = Simulator.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
150
+
151
+ // Reset the counter
152
+ sim.dut.rst = 1;
153
+ sim.tick();
154
+ sim.dut.rst = 0;
155
+ sim.tick();
156
+ expect(sim.dut.count).toBe(0);
157
+
158
+ // Enable counting
159
+ sim.dut.en = 1;
160
+ sim.tick();
161
+ expect(sim.dut.count).toBe(1);
162
+
163
+ sim.tick();
164
+ expect(sim.dut.count).toBe(2);
165
+
166
+ sim.tick();
167
+ expect(sim.dut.count).toBe(3);
168
+
169
+ // Disable counting
170
+ sim.dut.en = 0;
171
+ sim.tick();
172
+ expect(sim.dut.count).toBe(3);
173
+
174
+ // Re-enable
175
+ sim.dut.en = 1;
176
+ sim.tick(5);
177
+ expect(sim.dut.count).toBe(8);
178
+
179
+ sim.dispose();
180
+ });
181
+
182
+ test("combinational multiplexer", () => {
183
+ interface Mux4Ports {
184
+ sel: number;
185
+ d0: number;
186
+ d1: number;
187
+ d2: number;
188
+ d3: number;
189
+ readonly y: number;
190
+ }
191
+
192
+ const sim = Simulator.fromSource<Mux4Ports>(MULTIPLEXER_SOURCE, "Mux4");
193
+
194
+ sim.dut.d0 = 0xAA;
195
+ sim.dut.d1 = 0xBB;
196
+ sim.dut.d2 = 0xCC;
197
+ sim.dut.d3 = 0xDD;
198
+
199
+ sim.dut.sel = 0;
200
+ expect(sim.dut.y).toBe(0xAA);
201
+
202
+ sim.dut.sel = 1;
203
+ expect(sim.dut.y).toBe(0xBB);
204
+
205
+ sim.dut.sel = 2;
206
+ expect(sim.dut.y).toBe(0xCC);
207
+
208
+ sim.dut.sel = 3;
209
+ expect(sim.dut.y).toBe(0xDD);
210
+
211
+ sim.dispose();
212
+ });
213
+ });
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Simulation (time-based) e2e tests — fromSource API
217
+ // ---------------------------------------------------------------------------
218
+
219
+ describe("E2E: Simulation.fromSource (time-based)", () => {
220
+ test("counter with timed clock: step-by-step", () => {
221
+ interface CounterPorts {
222
+ rst: number;
223
+ en: number;
224
+ readonly count: number;
225
+ }
226
+
227
+ const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
228
+
229
+ sim.addClock("clk", { period: 10 });
230
+ expect(sim.time()).toBe(0);
231
+
232
+ // Reset
233
+ sim.dut.rst = 1;
234
+ sim.runUntil(20);
235
+ sim.dut.rst = 0;
236
+ sim.dut.en = 1;
237
+
238
+ sim.runUntil(100);
239
+
240
+ const count = sim.dut.count;
241
+ expect(count).toBeGreaterThan(0);
242
+ expect(sim.time()).toBe(100);
243
+
244
+ sim.dispose();
245
+ });
246
+ });
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Simulator (event-based) e2e tests — fromProject API
250
+ // ---------------------------------------------------------------------------
251
+
252
+ describe("E2E: Simulator.fromProject (event-based)", () => {
253
+ test("combinational adder from project directory", () => {
254
+ interface AdderPorts {
255
+ rst: number;
256
+ a: number;
257
+ b: number;
258
+ readonly sum: number;
259
+ }
260
+
261
+ const sim = Simulator.fromProject<AdderPorts>(ADDER_PROJECT, "Adder");
262
+
263
+ sim.dut.a = 100;
264
+ sim.dut.b = 200;
265
+ sim.tick();
266
+ expect(sim.dut.sum).toBe(300);
267
+
268
+ sim.dut.a = 0xFFFF;
269
+ sim.dut.b = 1;
270
+ sim.tick();
271
+ expect(sim.dut.sum).toBe(0x10000);
272
+
273
+ sim.dispose();
274
+ });
275
+
276
+ test("sequential counter from project directory", () => {
277
+ interface CounterPorts {
278
+ rst: number;
279
+ en: number;
280
+ readonly count: number;
281
+ }
282
+
283
+ const sim = Simulator.fromProject<CounterPorts>(COUNTER_PROJECT, "Counter");
284
+
285
+ // Reset the counter
286
+ sim.dut.rst = 1;
287
+ sim.tick();
288
+ sim.dut.rst = 0;
289
+ sim.tick();
290
+ expect(sim.dut.count).toBe(0);
291
+
292
+ // Enable counting
293
+ sim.dut.en = 1;
294
+ sim.tick();
295
+ expect(sim.dut.count).toBe(1);
296
+
297
+ sim.tick();
298
+ expect(sim.dut.count).toBe(2);
299
+
300
+ sim.tick();
301
+ expect(sim.dut.count).toBe(3);
302
+
303
+ sim.dispose();
304
+ });
305
+ });
306
+
307
+ // ---------------------------------------------------------------------------
308
+ // Simulation (time-based) e2e tests — fromProject API
309
+ // ---------------------------------------------------------------------------
310
+
311
+ describe("E2E: Simulation.fromProject (time-based)", () => {
312
+ test("counter with timed clock from project directory", () => {
313
+ interface CounterPorts {
314
+ rst: number;
315
+ en: number;
316
+ readonly count: number;
317
+ }
318
+
319
+ const sim = Simulation.fromProject<CounterPorts>(COUNTER_PROJECT, "Counter");
320
+
321
+ sim.addClock("clk", { period: 10 });
322
+ expect(sim.time()).toBe(0);
323
+
324
+ // Reset
325
+ sim.dut.rst = 1;
326
+ sim.runUntil(20);
327
+ sim.dut.rst = 0;
328
+ sim.dut.en = 1;
329
+
330
+ sim.runUntil(100);
331
+
332
+ const count = sim.dut.count;
333
+ expect(count).toBeGreaterThan(0);
334
+ expect(sim.time()).toBe(100);
335
+
336
+ sim.dispose();
337
+ });
338
+ });
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // Backward compat: Simulator.create() with manual ModuleDefinition
342
+ // ---------------------------------------------------------------------------
343
+
344
+ describe("E2E: Simulator.create (backward compat)", () => {
345
+ test("combinational adder via Simulator.create()", () => {
346
+ interface AdderPorts {
347
+ rst: number;
348
+ a: number;
349
+ b: number;
350
+ readonly sum: number;
351
+ }
352
+
353
+ const addon = loadNativeAddon();
354
+ const nativeCreateSimulator = createSimulatorBridge(addon);
355
+
356
+ const sim = Simulator.create<AdderPorts>(
357
+ {
358
+ __celox_module: true,
359
+ name: "Adder",
360
+ source: ADDER_SOURCE,
361
+ ports: {
362
+ clk: { direction: "input", type: "clock", width: 1 },
363
+ rst: { direction: "input", type: "reset", width: 1 },
364
+ a: { direction: "input", type: "logic", width: 16 },
365
+ b: { direction: "input", type: "logic", width: 16 },
366
+ sum: { direction: "output", type: "logic", width: 17 },
367
+ },
368
+ events: ["clk"],
369
+ },
370
+ { __nativeCreate: nativeCreateSimulator },
371
+ );
372
+
373
+ sim.dut.a = 100;
374
+ sim.dut.b = 200;
375
+ sim.tick();
376
+ expect(sim.dut.sum).toBe(300);
377
+
378
+ sim.dispose();
379
+ });
380
+ });
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // 4-state simulation e2e tests
384
+ // ---------------------------------------------------------------------------
385
+
386
+ const AND_OR_SOURCE = `
387
+ module AndOr (
388
+ a: input logic,
389
+ b: input logic,
390
+ y_and: output logic,
391
+ y_or: output logic,
392
+ ) {
393
+ assign y_and = a & b;
394
+ assign y_or = a | b;
395
+ }
396
+ `;
397
+
398
+ const LOGIC_BIT_MIX_SOURCE = `
399
+ module LogicBitMix (
400
+ a_logic: input logic<8>,
401
+ b_bit: input bit<8>,
402
+ y_logic_from_bit: output logic<8>,
403
+ y_bit_from_logic: output bit<8>,
404
+ ) {
405
+ assign y_bit_from_logic = a_logic;
406
+ assign y_logic_from_bit = b_bit;
407
+ }
408
+ `;
409
+
410
+ const FF_SOURCE = `
411
+ module FF (
412
+ clk: input clock,
413
+ rst: input reset,
414
+ d: input logic<8>,
415
+ q: output logic<8>,
416
+ ) {
417
+ always_ff (clk, rst) {
418
+ if_reset {
419
+ q = 8'd0;
420
+ } else {
421
+ q = d;
422
+ }
423
+ }
424
+ }
425
+ `;
426
+
427
+ const ADDER_4STATE_SOURCE = `
428
+ module Adder4S (
429
+ a: input logic<8>,
430
+ b: input logic<8>,
431
+ y: output logic<8>,
432
+ ) {
433
+ assign y = a + b;
434
+ }
435
+ `;
436
+
437
+ describe("E2E: 4-state simulation", () => {
438
+ let raw: RawNapiSimulatorHandle | undefined;
439
+ let addon: RawNapiAddon;
440
+
441
+ try {
442
+ addon = loadNativeAddon();
443
+ } catch (e) {
444
+ throw new Error(`Failed to load NAPI addon for 4-state tests: ${e}`);
445
+ }
446
+
447
+ afterEach(() => {
448
+ raw?.dispose();
449
+ raw = undefined;
450
+ });
451
+
452
+ test("initial values: logic ports start as X, bit ports start as 0", () => {
453
+ const source = `
454
+ module InitTest (
455
+ a: input logic<8>,
456
+ b: input bit<8>,
457
+ ) {}
458
+ `;
459
+ raw = new addon.NativeSimulatorHandle(source, "InitTest", { fourState: true });
460
+ const layout = parseNapiLayout(raw.layoutJson);
461
+ const buf = raw.sharedMemory().buffer;
462
+
463
+ // logic port should have mask=0xFF (all X)
464
+ const [valA, maskA] = readFourState(buf, layout.forDut.a);
465
+ expect(valA).toBe(0);
466
+ expect(maskA).toBe(0xFF);
467
+
468
+ // bit port should have mask=0 (defined)
469
+ // bit is not 4-state, so no mask — reading its value should be 0
470
+ expect(layout.forDut.b.is4state).toBe(false);
471
+ });
472
+
473
+ test("writing X clears value and sets mask", () => {
474
+ interface Ports {
475
+ a: number;
476
+ readonly y_and: number;
477
+ }
478
+
479
+ const sim = Simulator.fromSource<Ports>(AND_OR_SOURCE, "AndOr", { fourState: true });
480
+ raw = undefined; // sim manages its own handle
481
+
482
+ // Write X to input 'a' via DUT
483
+ (sim.dut as any).a = X;
484
+
485
+ // We can't inspect mask through DUT getter (it only returns value),
486
+ // so this test verifies X write doesn't throw and propagation works.
487
+ // For detailed mask inspection, see the raw NAPI tests below.
488
+ sim.dispose();
489
+ });
490
+
491
+ test("AND: 0 & X = 0 (dominant zero)", () => {
492
+ raw = new addon.NativeSimulatorHandle(AND_OR_SOURCE, "AndOr", { fourState: true });
493
+ const layout = parseNapiLayout(raw.layoutJson);
494
+ const buf = raw.sharedMemory().buffer;
495
+ const view = new DataView(buf);
496
+ const events: Record<string, number> = JSON.parse(raw.eventsJson);
497
+
498
+ const sigA = layout.forDut.a;
499
+ const sigB = layout.forDut.b;
500
+ const sigYAnd = layout.forDut.y_and;
501
+ const sigYOr = layout.forDut.y_or;
502
+
503
+ // a = 0 (value=0, mask=0)
504
+ view.setUint8(sigA.offset, 0);
505
+ view.setUint8(sigA.offset + sigA.byteSize, 0);
506
+
507
+ // b = X (value=0, mask=1)
508
+ view.setUint8(sigB.offset, 0);
509
+ view.setUint8(sigB.offset + sigB.byteSize, 1);
510
+
511
+ raw.evalComb();
512
+
513
+ // 0 & X = 0 (mask should be 0 — dominant zero)
514
+ const [vAnd, mAnd] = readFourState(buf, sigYAnd);
515
+ expect(vAnd).toBe(0);
516
+ expect(mAnd).toBe(0);
517
+
518
+ // 0 | X = X (mask should be 1)
519
+ const [vOr, mOr] = readFourState(buf, sigYOr);
520
+ expect(vOr).toBe(0);
521
+ expect(mOr).toBe(1);
522
+ });
523
+
524
+ test("OR: 1 | X = 1 (dominant one)", () => {
525
+ raw = new addon.NativeSimulatorHandle(AND_OR_SOURCE, "AndOr", { fourState: true });
526
+ const layout = parseNapiLayout(raw.layoutJson);
527
+ const buf = raw.sharedMemory().buffer;
528
+ const view = new DataView(buf);
529
+
530
+ const sigA = layout.forDut.a;
531
+ const sigB = layout.forDut.b;
532
+ const sigYOr = layout.forDut.y_or;
533
+
534
+ // a = 1 (value=1, mask=0)
535
+ view.setUint8(sigA.offset, 1);
536
+ view.setUint8(sigA.offset + sigA.byteSize, 0);
537
+
538
+ // b = X (value=0, mask=1)
539
+ view.setUint8(sigB.offset, 0);
540
+ view.setUint8(sigB.offset + sigB.byteSize, 1);
541
+
542
+ raw.evalComb();
543
+
544
+ // 1 | X = 1 (mask should be 0 — dominant one)
545
+ const [vOr, mOr] = readFourState(buf, sigYOr);
546
+ expect(vOr).toBe(1);
547
+ expect(mOr).toBe(0);
548
+ });
549
+
550
+ test("logic-to-bit assignment strips X mask", () => {
551
+ raw = new addon.NativeSimulatorHandle(LOGIC_BIT_MIX_SOURCE, "LogicBitMix", { fourState: true });
552
+ const layout = parseNapiLayout(raw.layoutJson);
553
+ const buf = raw.sharedMemory().buffer;
554
+ const view = new DataView(buf);
555
+
556
+ const sigALogic = layout.forDut.a_logic;
557
+ const sigYBitFromLogic = layout.forDut.y_bit_from_logic;
558
+
559
+ // a_logic = all-X (value=0, mask=0xFF)
560
+ view.setUint8(sigALogic.offset, 0);
561
+ view.setUint8(sigALogic.offset + sigALogic.byteSize, 0xFF);
562
+
563
+ raw.evalComb();
564
+
565
+ // y_bit_from_logic is bit type — X should be stripped (mask=0)
566
+ expect(sigYBitFromLogic.is4state).toBe(false);
567
+ });
568
+
569
+ test("bit-to-logic assignment has no X", () => {
570
+ raw = new addon.NativeSimulatorHandle(LOGIC_BIT_MIX_SOURCE, "LogicBitMix", { fourState: true });
571
+ const layout = parseNapiLayout(raw.layoutJson);
572
+ const buf = raw.sharedMemory().buffer;
573
+ const view = new DataView(buf);
574
+
575
+ const sigBBit = layout.forDut.b_bit;
576
+ const sigYLogicFromBit = layout.forDut.y_logic_from_bit;
577
+
578
+ // b_bit = 0xAA (bit type, always defined)
579
+ view.setUint8(sigBBit.offset, 0xAA);
580
+
581
+ raw.evalComb();
582
+
583
+ // y_logic_from_bit should be 0xAA with mask=0
584
+ const [vLogic, mLogic] = readFourState(buf, sigYLogicFromBit);
585
+ expect(vLogic).toBe(0xAA);
586
+ expect(mLogic).toBe(0);
587
+ });
588
+
589
+ test("arithmetic with X produces all-X output", () => {
590
+ raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
591
+ const layout = parseNapiLayout(raw.layoutJson);
592
+ const buf = raw.sharedMemory().buffer;
593
+ const view = new DataView(buf);
594
+
595
+ const sigA = layout.forDut.a;
596
+ const sigB = layout.forDut.b;
597
+ const sigY = layout.forDut.y;
598
+
599
+ // a = 42 (defined), b = X (all X)
600
+ view.setUint8(sigA.offset, 42);
601
+ view.setUint8(sigA.offset + sigA.byteSize, 0); // mask=0
602
+
603
+ view.setUint8(sigB.offset, 0);
604
+ view.setUint8(sigB.offset + sigB.byteSize, 0xFF); // mask=0xFF
605
+
606
+ raw.evalComb();
607
+
608
+ // a + X = all-X
609
+ const [, mY] = readFourState(buf, sigY);
610
+ expect(mY).toBe(0xFF);
611
+ });
612
+
613
+ test("defined inputs in 4-state mode behave like 2-state", () => {
614
+ raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
615
+ const layout = parseNapiLayout(raw.layoutJson);
616
+ const buf = raw.sharedMemory().buffer;
617
+ const view = new DataView(buf);
618
+
619
+ const sigA = layout.forDut.a;
620
+ const sigB = layout.forDut.b;
621
+ const sigY = layout.forDut.y;
622
+
623
+ // a = 100 (defined), b = 55 (defined)
624
+ view.setUint8(sigA.offset, 100);
625
+ view.setUint8(sigA.offset + sigA.byteSize, 0);
626
+
627
+ view.setUint8(sigB.offset, 55);
628
+ view.setUint8(sigB.offset + sigB.byteSize, 0);
629
+
630
+ raw.evalComb();
631
+
632
+ const [vY, mY] = readFourState(buf, sigY);
633
+ expect(vY).toBe(155);
634
+ expect(mY).toBe(0);
635
+ });
636
+
637
+ test("FF captures X from input, reset clears X", () => {
638
+ raw = new addon.NativeSimulatorHandle(FF_SOURCE, "FF", { fourState: true });
639
+ const layout = parseNapiLayout(raw.layoutJson);
640
+ const buf = raw.sharedMemory().buffer;
641
+ const view = new DataView(buf);
642
+ const events: Record<string, number> = JSON.parse(raw.eventsJson);
643
+
644
+ const sigRst = layout.forDut.rst;
645
+ const sigD = layout.forDut.d;
646
+ const sigQ = layout.forDut.q;
647
+ const clkEventId = events.clk;
648
+
649
+ // 1. Reset: rst=1, d=X
650
+ view.setUint8(sigRst.offset, 1);
651
+ view.setUint8(sigRst.offset + sigRst.byteSize, 0); // rst is defined
652
+
653
+ view.setUint8(sigD.offset, 0);
654
+ view.setUint8(sigD.offset + sigD.byteSize, 0xFF); // d = all-X
655
+
656
+ raw.tick(clkEventId);
657
+
658
+ // After reset, q should be 0 with mask=0
659
+ const [vQ1, mQ1] = readFourState(buf, sigQ);
660
+ expect(vQ1).toBe(0);
661
+ expect(mQ1).toBe(0);
662
+
663
+ // 2. Release reset, d = partial X (value=0xA5, mask=0x0F)
664
+ view.setUint8(sigRst.offset, 0);
665
+ view.setUint8(sigRst.offset + sigRst.byteSize, 0);
666
+
667
+ view.setUint8(sigD.offset, 0xA5);
668
+ view.setUint8(sigD.offset + sigD.byteSize, 0x0F);
669
+
670
+ raw.tick(clkEventId);
671
+
672
+ // FF should capture X mask from d
673
+ const [, mQ2] = readFourState(buf, sigQ);
674
+ expect(mQ2).toBe(0x0F);
675
+
676
+ // 3. Reset again: should clear X
677
+ view.setUint8(sigRst.offset, 1);
678
+ view.setUint8(sigRst.offset + sigRst.byteSize, 0);
679
+
680
+ raw.tick(clkEventId);
681
+
682
+ const [vQ3, mQ3] = readFourState(buf, sigQ);
683
+ expect(vQ3).toBe(0);
684
+ expect(mQ3).toBe(0);
685
+ });
686
+
687
+ test("FourState write through DUT sets value and mask", () => {
688
+ raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
689
+ const layout = parseNapiLayout(raw.layoutJson);
690
+ const buf = raw.sharedMemory().buffer;
691
+ const view = new DataView(buf);
692
+
693
+ const sigA = layout.forDut.a;
694
+
695
+ // Write via DUT-style: FourState(0b1010_0101, 0b0000_1111)
696
+ // value=0xA5, mask=0x0F means lower 4 bits are X
697
+ view.setUint8(sigA.offset, 0xA5);
698
+ view.setUint8(sigA.offset + sigA.byteSize, 0x0F);
699
+
700
+ const [vA, mA] = readFourState(buf, sigA);
701
+ expect(vA).toBe(0xA5);
702
+ expect(mA).toBe(0x0F);
703
+ });
704
+
705
+ test("setting defined value clears X mask", () => {
706
+ raw = new addon.NativeSimulatorHandle(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
707
+ const layout = parseNapiLayout(raw.layoutJson);
708
+ const buf = raw.sharedMemory().buffer;
709
+ const view = new DataView(buf);
710
+
711
+ const sigA = layout.forDut.a;
712
+
713
+ // Start with X
714
+ view.setUint8(sigA.offset, 0);
715
+ view.setUint8(sigA.offset + sigA.byteSize, 0xFF);
716
+
717
+ const [, mBefore] = readFourState(buf, sigA);
718
+ expect(mBefore).toBe(0xFF);
719
+
720
+ // Write a defined value (clear mask)
721
+ view.setUint8(sigA.offset, 42);
722
+ view.setUint8(sigA.offset + sigA.byteSize, 0);
723
+
724
+ const [vAfter, mAfter] = readFourState(buf, sigA);
725
+ expect(vAfter).toBe(42);
726
+ expect(mAfter).toBe(0);
727
+ });
728
+
729
+ test("4-state through DUT high-level API (fromSource with fourState)", () => {
730
+ interface Ports {
731
+ a: number;
732
+ b: number;
733
+ readonly y: number;
734
+ }
735
+
736
+ const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
737
+
738
+ // Write defined values — should behave like 2-state
739
+ sim.dut.a = 100;
740
+ sim.dut.b = 55;
741
+ expect(sim.dut.y).toBe(155);
742
+
743
+ // Write X to a — output should propagate X (value reads as 0)
744
+ (sim.dut as any).a = X;
745
+ // After writing X, the value part of 'y' is implementation-defined
746
+ // but the read should not throw
747
+ const _yVal = sim.dut.y;
748
+ expect(typeof _yVal).toBe("number");
749
+
750
+ // Write FourState with partial X
751
+ (sim.dut as any).a = FourState(0xA0, 0x0F);
752
+ const _yVal2 = sim.dut.y;
753
+ expect(typeof _yVal2).toBe("number");
754
+
755
+ sim.dispose();
756
+ });
757
+ });
758
+
759
+ // ---------------------------------------------------------------------------
760
+ // 4-state: high-level DUT API (Simulator.fromSource)
761
+ // ---------------------------------------------------------------------------
762
+
763
+ describe("E2E: 4-state high-level DUT API", () => {
764
+ test("counter in 4-state mode: reset clears X, counting works", () => {
765
+ interface CounterPorts {
766
+ rst: number;
767
+ en: number;
768
+ readonly count: number;
769
+ }
770
+
771
+ const sim = Simulator.fromSource<CounterPorts>(
772
+ COUNTER_SOURCE, "Counter", { fourState: true },
773
+ );
774
+
775
+ // In 4-state mode, count starts as X. Reset should clear it.
776
+ sim.dut.rst = 1;
777
+ sim.tick();
778
+ sim.dut.rst = 0;
779
+ sim.tick();
780
+ expect(sim.dut.count).toBe(0);
781
+
782
+ // Enable counting — should work exactly like 2-state
783
+ sim.dut.en = 1;
784
+ sim.tick();
785
+ expect(sim.dut.count).toBe(1);
786
+
787
+ sim.tick();
788
+ expect(sim.dut.count).toBe(2);
789
+
790
+ sim.tick();
791
+ expect(sim.dut.count).toBe(3);
792
+
793
+ sim.dispose();
794
+ });
795
+
796
+ test("multiplexer with X selector produces X output", () => {
797
+ const addon = loadNativeAddon();
798
+ const raw = new addon.NativeSimulatorHandle(MULTIPLEXER_SOURCE, "Mux4", { fourState: true });
799
+ const layout = parseNapiLayout(raw.layoutJson);
800
+ const buf = raw.sharedMemory().buffer;
801
+ const view = new DataView(buf);
802
+
803
+ const sigSel = layout.forDut.sel;
804
+ const sigD0 = layout.forDut.d0;
805
+ const sigY = layout.forDut.y;
806
+
807
+ // Set d0 = 0xAA (defined)
808
+ view.setUint8(sigD0.offset, 0xAA);
809
+ view.setUint8(sigD0.offset + sigD0.byteSize, 0);
810
+
811
+ // Set sel = X
812
+ view.setUint8(sigSel.offset, 0);
813
+ view.setUint8(sigSel.offset + sigSel.byteSize, 0x03);
814
+
815
+ raw.evalComb();
816
+
817
+ // With X selector, output should be all-X
818
+ const [, mY] = readFourState(buf, sigY);
819
+ expect(mY).toBe(0xFF);
820
+
821
+ raw.dispose();
822
+ });
823
+
824
+ test("FF via DUT API: write X input, tick, read output", () => {
825
+ interface FFPorts {
826
+ rst: number;
827
+ d: number;
828
+ readonly q: number;
829
+ }
830
+
831
+ const sim = Simulator.fromSource<FFPorts>(FF_SOURCE, "FF", { fourState: true });
832
+
833
+ // Reset to clear initial X
834
+ sim.dut.rst = 1;
835
+ sim.tick();
836
+ sim.dut.rst = 0;
837
+ expect(sim.dut.q).toBe(0);
838
+
839
+ // Write a defined value
840
+ sim.dut.d = 0x42;
841
+ sim.tick();
842
+ expect(sim.dut.q).toBe(0x42);
843
+
844
+ // Write X to d, tick — q should capture it (value read still returns a number)
845
+ (sim.dut as any).d = X;
846
+ sim.tick();
847
+ expect(typeof sim.dut.q).toBe("number");
848
+
849
+ // Write defined value again — q should recover
850
+ sim.dut.d = 0x99;
851
+ sim.tick();
852
+ expect(sim.dut.q).toBe(0x99);
853
+
854
+ sim.dispose();
855
+ });
856
+
857
+ test("X to defined transition: adder recovers from X", () => {
858
+ interface Ports {
859
+ a: number;
860
+ b: number;
861
+ readonly y: number;
862
+ }
863
+
864
+ const sim = Simulator.fromSource<Ports>(ADDER_4STATE_SOURCE, "Adder4S", { fourState: true });
865
+
866
+ // Start with X
867
+ (sim.dut as any).a = X;
868
+ sim.dut.b = 10;
869
+ // Output has X — just verify it doesn't crash
870
+ expect(typeof sim.dut.y).toBe("number");
871
+
872
+ // Clear X by writing defined values
873
+ sim.dut.a = 20;
874
+ sim.dut.b = 30;
875
+ expect(sim.dut.y).toBe(50);
876
+
877
+ sim.dispose();
878
+ });
879
+ });
880
+
881
+ // ---------------------------------------------------------------------------
882
+ // 4-state: Simulation (time-based) tests
883
+ // ---------------------------------------------------------------------------
884
+
885
+ describe("E2E: 4-state Simulation (time-based)", () => {
886
+ test("FF with clock-driven 4-state: reset clears X, captures defined values", () => {
887
+ interface FFPorts {
888
+ rst: number;
889
+ d: number;
890
+ readonly q: number;
891
+ }
892
+
893
+ const sim = Simulation.fromSource<FFPorts>(FF_SOURCE, "FF", { fourState: true });
894
+
895
+ sim.addClock("clk", { period: 10 });
896
+ expect(sim.time()).toBe(0);
897
+
898
+ // Reset to clear initial X on q
899
+ sim.dut.rst = 1;
900
+ sim.runUntil(20);
901
+ sim.dut.rst = 0;
902
+ expect(sim.dut.q).toBe(0);
903
+
904
+ // Drive d with defined value
905
+ sim.dut.d = 0x55;
906
+ sim.runUntil(40);
907
+ expect(sim.dut.q).toBe(0x55);
908
+
909
+ // Drive d with different value
910
+ sim.dut.d = 0xAA;
911
+ sim.runUntil(60);
912
+ expect(sim.dut.q).toBe(0xAA);
913
+
914
+ sim.dispose();
915
+ });
916
+
917
+ test("counter in 4-state time-based mode", () => {
918
+ interface CounterPorts {
919
+ rst: number;
920
+ en: number;
921
+ readonly count: number;
922
+ }
923
+
924
+ const sim = Simulation.fromSource<CounterPorts>(
925
+ COUNTER_SOURCE, "Counter", { fourState: true },
926
+ );
927
+
928
+ sim.addClock("clk", { period: 10 });
929
+
930
+ // Reset
931
+ sim.dut.rst = 1;
932
+ sim.runUntil(20);
933
+ sim.dut.rst = 0;
934
+ sim.dut.en = 1;
935
+
936
+ sim.runUntil(100);
937
+
938
+ const count = sim.dut.count;
939
+ expect(count).toBeGreaterThan(0);
940
+ expect(sim.time()).toBe(100);
941
+
942
+ sim.dispose();
943
+ });
944
+
945
+ test("4-state combinational in time-based simulation", () => {
946
+ interface Ports {
947
+ a: number;
948
+ b: number;
949
+ readonly y: number;
950
+ }
951
+
952
+ const sim = Simulation.fromSource<Ports>(
953
+ ADDER_4STATE_SOURCE, "Adder4S", { fourState: true },
954
+ );
955
+
956
+ // No clock needed for pure combinational — just set values and read
957
+ sim.dut.a = 100;
958
+ sim.dut.b = 55;
959
+ // runUntil(0) to force eval
960
+ sim.runUntil(0);
961
+ expect(sim.dut.y).toBe(155);
962
+
963
+ sim.dispose();
964
+ });
965
+ });