@celox-sim/celox 0.0.1 → 0.1.3

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,534 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { createDut, readFourState, type DirtyState } from "./dut.js";
3
+ import type {
4
+ NativeSimulatorHandle,
5
+ PortInfo,
6
+ SignalLayout,
7
+ } from "./types.js";
8
+ import { FourState, X } from "./types.js";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function mockHandle(): NativeSimulatorHandle {
15
+ return {
16
+ tick: vi.fn(),
17
+ tickN: vi.fn(),
18
+ evalComb: vi.fn(),
19
+ dump: vi.fn(),
20
+ dispose: vi.fn(),
21
+ };
22
+ }
23
+
24
+ function makeBuffer(size: number): SharedArrayBuffer {
25
+ return new SharedArrayBuffer(size);
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Basic scalar read/write
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe("createDut — scalar ports", () => {
33
+ test("write and read 8-bit input", () => {
34
+ const buffer = makeBuffer(64);
35
+ const layout: Record<string, SignalLayout> = {
36
+ a: { offset: 0, width: 8, byteSize: 1, is4state: false, direction: "input" },
37
+ };
38
+ const ports: Record<string, PortInfo> = {
39
+ a: { direction: "input", type: "logic", width: 8 },
40
+ };
41
+ const handle = mockHandle();
42
+ const state: DirtyState = { dirty: false };
43
+
44
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
45
+
46
+ dut.a = 42;
47
+ expect(state.dirty).toBe(true);
48
+ expect(dut.a).toBe(42);
49
+ // Reading an input doesn't trigger evalComb
50
+ expect(handle.evalComb).not.toHaveBeenCalled();
51
+ });
52
+
53
+ test("write and read 16-bit input", () => {
54
+ const buffer = makeBuffer(64);
55
+ const layout: Record<string, SignalLayout> = {
56
+ a: { offset: 0, width: 16, byteSize: 2, is4state: false, direction: "input" },
57
+ };
58
+ const ports: Record<string, PortInfo> = {
59
+ a: { direction: "input", type: "logic", width: 16 },
60
+ };
61
+ const handle = mockHandle();
62
+ const state: DirtyState = { dirty: false };
63
+
64
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
65
+
66
+ dut.a = 0xABCD;
67
+ expect(dut.a).toBe(0xABCD);
68
+ });
69
+
70
+ test("write and read 32-bit input", () => {
71
+ const buffer = makeBuffer(64);
72
+ const layout: Record<string, SignalLayout> = {
73
+ a: { offset: 0, width: 32, byteSize: 4, is4state: false, direction: "input" },
74
+ };
75
+ const ports: Record<string, PortInfo> = {
76
+ a: { direction: "input", type: "logic", width: 32 },
77
+ };
78
+ const handle = mockHandle();
79
+ const state: DirtyState = { dirty: false };
80
+
81
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
82
+
83
+ dut.a = 0xDEAD_BEEF;
84
+ expect(dut.a).toBe(0xDEAD_BEEF);
85
+ });
86
+
87
+ test("write and read 48-bit value (fits in number)", () => {
88
+ const buffer = makeBuffer(64);
89
+ const layout: Record<string, SignalLayout> = {
90
+ a: { offset: 0, width: 48, byteSize: 8, is4state: false, direction: "input" },
91
+ };
92
+ const ports: Record<string, PortInfo> = {
93
+ a: { direction: "input", type: "logic", width: 48 },
94
+ };
95
+ const handle = mockHandle();
96
+ const state: DirtyState = { dirty: false };
97
+
98
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
99
+
100
+ const val = 0x1234_5678_9ABC;
101
+ dut.a = val;
102
+ expect(dut.a).toBe(val);
103
+ });
104
+
105
+ test("write and read 64-bit BigInt value", () => {
106
+ const buffer = makeBuffer(64);
107
+ const layout: Record<string, SignalLayout> = {
108
+ a: { offset: 0, width: 64, byteSize: 8, is4state: false, direction: "input" },
109
+ };
110
+ const ports: Record<string, PortInfo> = {
111
+ a: { direction: "input", type: "logic", width: 64 },
112
+ };
113
+ const handle = mockHandle();
114
+ const state: DirtyState = { dirty: false };
115
+
116
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
117
+
118
+ const val = 0xDEAD_BEEF_CAFE_BABEn;
119
+ (dut as any).a = val;
120
+ expect(dut.a).toBe(val);
121
+ });
122
+
123
+ test("8-bit write masks to width", () => {
124
+ const buffer = makeBuffer(64);
125
+ const layout: Record<string, SignalLayout> = {
126
+ a: { offset: 0, width: 4, byteSize: 1, is4state: false, direction: "input" },
127
+ };
128
+ const ports: Record<string, PortInfo> = {
129
+ a: { direction: "input", type: "logic", width: 4 },
130
+ };
131
+ const handle = mockHandle();
132
+ const state: DirtyState = { dirty: false };
133
+
134
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
135
+
136
+ dut.a = 0xFF; // Only lower 4 bits should be stored
137
+ expect(dut.a).toBe(0x0F);
138
+ });
139
+ });
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Dirty tracking and evalComb
143
+ // ---------------------------------------------------------------------------
144
+
145
+ describe("createDut — dirty tracking", () => {
146
+ test("reading output when dirty triggers evalComb", () => {
147
+ const buffer = makeBuffer(64);
148
+ const layout: Record<string, SignalLayout> = {
149
+ a: { offset: 0, width: 16, byteSize: 2, is4state: false, direction: "input" },
150
+ sum: { offset: 4, width: 17, byteSize: 4, is4state: false, direction: "output" },
151
+ };
152
+ const ports: Record<string, PortInfo> = {
153
+ a: { direction: "input", type: "logic", width: 16 },
154
+ sum: { direction: "output", type: "logic", width: 17 },
155
+ };
156
+ const handle = mockHandle();
157
+ const state: DirtyState = { dirty: false };
158
+
159
+ const dut = createDut<{ a: number; readonly sum: number }>(
160
+ buffer, layout, ports, handle, state,
161
+ );
162
+
163
+ // Write input → dirty
164
+ dut.a = 100;
165
+ expect(state.dirty).toBe(true);
166
+
167
+ // Read output → evalComb should be called
168
+ void dut.sum;
169
+ expect(handle.evalComb).toHaveBeenCalledTimes(1);
170
+ expect(state.dirty).toBe(false);
171
+ });
172
+
173
+ test("reading output when clean does NOT trigger evalComb", () => {
174
+ const buffer = makeBuffer(64);
175
+ const layout: Record<string, SignalLayout> = {
176
+ sum: { offset: 0, width: 17, byteSize: 4, is4state: false, direction: "output" },
177
+ };
178
+ const ports: Record<string, PortInfo> = {
179
+ sum: { direction: "output", type: "logic", width: 17 },
180
+ };
181
+ const handle = mockHandle();
182
+ const state: DirtyState = { dirty: false };
183
+
184
+ const dut = createDut<{ readonly sum: number }>(
185
+ buffer, layout, ports, handle, state,
186
+ );
187
+
188
+ void dut.sum;
189
+ expect(handle.evalComb).not.toHaveBeenCalled();
190
+ });
191
+
192
+ test("reading input does NOT trigger evalComb even when dirty", () => {
193
+ const buffer = makeBuffer(64);
194
+ const layout: Record<string, SignalLayout> = {
195
+ a: { offset: 0, width: 8, byteSize: 1, is4state: false, direction: "input" },
196
+ };
197
+ const ports: Record<string, PortInfo> = {
198
+ a: { direction: "input", type: "logic", width: 8 },
199
+ };
200
+ const handle = mockHandle();
201
+ const state: DirtyState = { dirty: true };
202
+
203
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
204
+
205
+ void dut.a;
206
+ expect(handle.evalComb).not.toHaveBeenCalled();
207
+ });
208
+
209
+ test("writing to output throws", () => {
210
+ const buffer = makeBuffer(64);
211
+ const layout: Record<string, SignalLayout> = {
212
+ sum: { offset: 0, width: 17, byteSize: 4, is4state: false, direction: "output" },
213
+ };
214
+ const ports: Record<string, PortInfo> = {
215
+ sum: { direction: "output", type: "logic", width: 17 },
216
+ };
217
+ const handle = mockHandle();
218
+ const state: DirtyState = { dirty: false };
219
+
220
+ const dut = createDut<{ sum: number }>(buffer, layout, ports, handle, state);
221
+
222
+ expect(() => {
223
+ dut.sum = 42;
224
+ }).toThrow("Cannot write to output port 'sum'");
225
+ });
226
+ });
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Clock port is hidden
230
+ // ---------------------------------------------------------------------------
231
+
232
+ describe("createDut — clock ports", () => {
233
+ test("clock ports are not exposed on the DUT", () => {
234
+ const buffer = makeBuffer(64);
235
+ const layout: Record<string, SignalLayout> = {
236
+ clk: { offset: 0, width: 1, byteSize: 1, is4state: false, direction: "input" },
237
+ a: { offset: 1, width: 8, byteSize: 1, is4state: false, direction: "input" },
238
+ };
239
+ const ports: Record<string, PortInfo> = {
240
+ clk: { direction: "input", type: "clock", width: 1 },
241
+ a: { direction: "input", type: "logic", width: 8 },
242
+ };
243
+ const handle = mockHandle();
244
+ const state: DirtyState = { dirty: false };
245
+
246
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
247
+
248
+ expect(Object.keys(dut as object)).toEqual(["a"]);
249
+ expect((dut as any).clk).toBeUndefined();
250
+ });
251
+ });
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Multiple signals at different offsets
255
+ // ---------------------------------------------------------------------------
256
+
257
+ describe("createDut — multiple signals", () => {
258
+ test("Adder-like module with a, b, sum", () => {
259
+ const buffer = makeBuffer(64);
260
+ const layout: Record<string, SignalLayout> = {
261
+ rst: { offset: 0, width: 1, byteSize: 1, is4state: false, direction: "input" },
262
+ a: { offset: 2, width: 16, byteSize: 2, is4state: false, direction: "input" },
263
+ b: { offset: 4, width: 16, byteSize: 2, is4state: false, direction: "input" },
264
+ sum: { offset: 8, width: 17, byteSize: 4, is4state: false, direction: "output" },
265
+ };
266
+ const ports: Record<string, PortInfo> = {
267
+ clk: { direction: "input", type: "clock", width: 1 },
268
+ rst: { direction: "input", type: "reset", width: 1 },
269
+ a: { direction: "input", type: "logic", width: 16 },
270
+ b: { direction: "input", type: "logic", width: 16 },
271
+ sum: { direction: "output", type: "logic", width: 17 },
272
+ };
273
+ const handle = mockHandle();
274
+ // Simulate evalComb by writing result into buffer
275
+ (handle.evalComb as ReturnType<typeof vi.fn>).mockImplementation(() => {
276
+ const view = new DataView(buffer);
277
+ const a = view.getUint16(2, true);
278
+ const b = view.getUint16(4, true);
279
+ view.setUint32(8, a + b, true);
280
+ });
281
+
282
+ const state: DirtyState = { dirty: false };
283
+ const dut = createDut<{
284
+ rst: number;
285
+ a: number;
286
+ b: number;
287
+ readonly sum: number;
288
+ }>(buffer, layout, ports, handle, state);
289
+
290
+ dut.a = 100;
291
+ dut.b = 200;
292
+ // sum read triggers evalComb
293
+ expect(dut.sum).toBe(300);
294
+ expect(handle.evalComb).toHaveBeenCalledTimes(1);
295
+
296
+ // second read without changes → no evalComb
297
+ expect(dut.sum).toBe(300);
298
+ expect(handle.evalComb).toHaveBeenCalledTimes(1);
299
+ });
300
+ });
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // 4-state support
304
+ // ---------------------------------------------------------------------------
305
+
306
+ describe("createDut — 4-state", () => {
307
+ test("write X to a 4-state signal", () => {
308
+ // 8-bit signal: 1 byte value + 1 byte mask = 2 bytes
309
+ const buffer = makeBuffer(64);
310
+ const layout: Record<string, SignalLayout> = {
311
+ a: { offset: 0, width: 8, byteSize: 1, is4state: true, direction: "input" },
312
+ };
313
+ const ports: Record<string, PortInfo> = {
314
+ a: { direction: "input", type: "logic", width: 8, is4state: true },
315
+ };
316
+ const handle = mockHandle();
317
+ const state: DirtyState = { dirty: false };
318
+
319
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
320
+
321
+ (dut as any).a = X;
322
+ // Value should be 0, mask should be 0xFF
323
+ const [value, mask] = readFourState(buffer, layout.a);
324
+ expect(value).toBe(0);
325
+ expect(mask).toBe(0xFF);
326
+ });
327
+
328
+ test("write FourState to a 4-state signal", () => {
329
+ const buffer = makeBuffer(64);
330
+ const layout: Record<string, SignalLayout> = {
331
+ a: { offset: 0, width: 8, byteSize: 1, is4state: true, direction: "input" },
332
+ };
333
+ const ports: Record<string, PortInfo> = {
334
+ a: { direction: "input", type: "logic", width: 8, is4state: true },
335
+ };
336
+ const handle = mockHandle();
337
+ const state: DirtyState = { dirty: false };
338
+
339
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
340
+
341
+ (dut as any).a = FourState(0b1010, 0b0100);
342
+ const [value, mask] = readFourState(buffer, layout.a);
343
+ expect(value).toBe(0b1010);
344
+ expect(mask).toBe(0b0100);
345
+ });
346
+
347
+ test("writing X to non-4-state signal throws", () => {
348
+ const buffer = makeBuffer(64);
349
+ const layout: Record<string, SignalLayout> = {
350
+ a: { offset: 0, width: 8, byteSize: 1, is4state: false, direction: "input" },
351
+ };
352
+ const ports: Record<string, PortInfo> = {
353
+ a: { direction: "input", type: "logic", width: 8 },
354
+ };
355
+ const handle = mockHandle();
356
+ const state: DirtyState = { dirty: false };
357
+
358
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
359
+
360
+ expect(() => {
361
+ (dut as any).a = X;
362
+ }).toThrow("not 4-state");
363
+ });
364
+
365
+ test("writing FourState to non-4-state signal throws", () => {
366
+ const buffer = makeBuffer(64);
367
+ const layout: Record<string, SignalLayout> = {
368
+ a: { offset: 0, width: 8, byteSize: 1, is4state: false, direction: "input" },
369
+ };
370
+ const ports: Record<string, PortInfo> = {
371
+ a: { direction: "input", type: "logic", width: 8 },
372
+ };
373
+ const handle = mockHandle();
374
+ const state: DirtyState = { dirty: false };
375
+
376
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
377
+
378
+ expect(() => {
379
+ (dut as any).a = FourState(0xA5, 0x0F);
380
+ }).toThrow("not 4-state");
381
+ });
382
+
383
+ test("writing defined value to 4-state signal clears mask", () => {
384
+ const buffer = makeBuffer(64);
385
+ const layout: Record<string, SignalLayout> = {
386
+ a: { offset: 0, width: 8, byteSize: 1, is4state: true, direction: "input" },
387
+ };
388
+ const ports: Record<string, PortInfo> = {
389
+ a: { direction: "input", type: "logic", width: 8, is4state: true },
390
+ };
391
+ const handle = mockHandle();
392
+ const state: DirtyState = { dirty: false };
393
+
394
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
395
+
396
+ // First write X
397
+ (dut as any).a = X;
398
+ const [, maskBefore] = readFourState(buffer, layout.a);
399
+ expect(maskBefore).toBe(0xFF);
400
+
401
+ // Then write a defined value — mask should clear
402
+ dut.a = 42;
403
+ const [value, maskAfter] = readFourState(buffer, layout.a);
404
+ expect(value).toBe(42);
405
+ expect(maskAfter).toBe(0);
406
+ });
407
+
408
+ test("reading 4-state output returns value part only", () => {
409
+ const buffer = makeBuffer(64);
410
+ const view = new DataView(buffer);
411
+ const layout: Record<string, SignalLayout> = {
412
+ y: { offset: 0, width: 8, byteSize: 1, is4state: true, direction: "output" },
413
+ };
414
+ const ports: Record<string, PortInfo> = {
415
+ y: { direction: "output", type: "logic", width: 8, is4state: true },
416
+ };
417
+ const handle = mockHandle();
418
+ const state: DirtyState = { dirty: false };
419
+
420
+ const dut = createDut<{ readonly y: number }>(buffer, layout, ports, handle, state);
421
+
422
+ // Set value=0xAB, mask=0x0F (lower 4 bits are X)
423
+ view.setUint8(0, 0xAB);
424
+ view.setUint8(1, 0x0F);
425
+
426
+ // DUT getter returns the value part
427
+ expect(dut.y).toBe(0xAB);
428
+ });
429
+
430
+ test("write X sets dirty flag", () => {
431
+ const buffer = makeBuffer(64);
432
+ const layout: Record<string, SignalLayout> = {
433
+ a: { offset: 0, width: 8, byteSize: 1, is4state: true, direction: "input" },
434
+ };
435
+ const ports: Record<string, PortInfo> = {
436
+ a: { direction: "input", type: "logic", width: 8, is4state: true },
437
+ };
438
+ const handle = mockHandle();
439
+ const state: DirtyState = { dirty: false };
440
+
441
+ const dut = createDut<{ a: number }>(buffer, layout, ports, handle, state);
442
+
443
+ (dut as any).a = X;
444
+ expect(state.dirty).toBe(true);
445
+ });
446
+ });
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // Array ports
450
+ // ---------------------------------------------------------------------------
451
+
452
+ describe("createDut — array ports", () => {
453
+ test("read/write array elements", () => {
454
+ // 4 elements of 8 bits each = 4 bytes
455
+ const buffer = makeBuffer(64);
456
+ const layout: Record<string, SignalLayout> = {
457
+ data: { offset: 0, width: 8, byteSize: 4, is4state: false, direction: "input" },
458
+ };
459
+ const ports: Record<string, PortInfo> = {
460
+ data: { direction: "input", type: "logic", width: 8, arrayDims: [4] },
461
+ };
462
+ const handle = mockHandle();
463
+ const state: DirtyState = { dirty: false };
464
+
465
+ const dut = createDut<{ data: number[] }>(
466
+ buffer, layout, ports, handle, state,
467
+ );
468
+
469
+ (dut.data as any)[0] = 0xAA;
470
+ (dut.data as any)[1] = 0xBB;
471
+ (dut.data as any)[2] = 0xCC;
472
+ (dut.data as any)[3] = 0xDD;
473
+
474
+ expect((dut.data as any)[0]).toBe(0xAA);
475
+ expect((dut.data as any)[1]).toBe(0xBB);
476
+ expect((dut.data as any)[2]).toBe(0xCC);
477
+ expect((dut.data as any)[3]).toBe(0xDD);
478
+ expect((dut.data as any).length).toBe(4);
479
+ });
480
+ });
481
+
482
+ // ---------------------------------------------------------------------------
483
+ // Interface (nested) ports
484
+ // ---------------------------------------------------------------------------
485
+
486
+ describe("createDut — interface ports", () => {
487
+ test("nested interface members", () => {
488
+ const buffer = makeBuffer(64);
489
+ const layout: Record<string, SignalLayout> = {
490
+ "bus.addr": { offset: 0, width: 32, byteSize: 4, is4state: false, direction: "input" },
491
+ "bus.data": { offset: 4, width: 32, byteSize: 4, is4state: false, direction: "input" },
492
+ "bus.valid": { offset: 8, width: 1, byteSize: 1, is4state: false, direction: "input" },
493
+ "bus.ready": { offset: 9, width: 1, byteSize: 1, is4state: false, direction: "output" },
494
+ };
495
+ const ports: Record<string, PortInfo> = {
496
+ bus: {
497
+ direction: "input",
498
+ type: "logic",
499
+ width: 0,
500
+ interface: {
501
+ addr: { direction: "input", type: "logic", width: 32 },
502
+ data: { direction: "input", type: "logic", width: 32 },
503
+ valid: { direction: "input", type: "logic", width: 1 },
504
+ ready: { direction: "output", type: "logic", width: 1 },
505
+ },
506
+ },
507
+ };
508
+ const handle = mockHandle();
509
+ (handle.evalComb as ReturnType<typeof vi.fn>).mockImplementation(() => {
510
+ const view = new DataView(buffer);
511
+ // mock: ready = valid
512
+ view.setUint8(9, view.getUint8(8));
513
+ });
514
+
515
+ const state: DirtyState = { dirty: false };
516
+ const dut = createDut<{
517
+ bus: {
518
+ addr: number;
519
+ data: number;
520
+ valid: number;
521
+ readonly ready: number;
522
+ };
523
+ }>(buffer, layout, ports, handle, state);
524
+
525
+ dut.bus.addr = 0x1000;
526
+ dut.bus.data = 0xFF;
527
+ dut.bus.valid = 1;
528
+
529
+ expect(dut.bus.addr).toBe(0x1000);
530
+ expect(dut.bus.data).toBe(0xFF);
531
+ expect(dut.bus.ready).toBe(1);
532
+ expect(handle.evalComb).toHaveBeenCalledTimes(1);
533
+ });
534
+ });