@celox-sim/celox 0.1.12 → 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/dut.test.ts CHANGED
@@ -1,10 +1,6 @@
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";
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { createDut, type DirtyState, readFourState } from "./dut.js";
3
+ import type { NativeSimulatorHandle, PortInfo, SignalLayout } from "./types.js";
8
4
  import { FourState, X } from "./types.js";
9
5
 
10
6
  // ---------------------------------------------------------------------------
@@ -12,17 +8,17 @@ import { FourState, X } from "./types.js";
12
8
  // ---------------------------------------------------------------------------
13
9
 
14
10
  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
- };
11
+ return {
12
+ tick: vi.fn(),
13
+ tickN: vi.fn(),
14
+ evalComb: vi.fn(),
15
+ dump: vi.fn(),
16
+ dispose: vi.fn(),
17
+ };
22
18
  }
23
19
 
24
20
  function makeBuffer(size: number): SharedArrayBuffer {
25
- return new SharedArrayBuffer(size);
21
+ return new SharedArrayBuffer(size);
26
22
  }
27
23
 
28
24
  // ---------------------------------------------------------------------------
@@ -30,112 +26,148 @@ function makeBuffer(size: number): SharedArrayBuffer {
30
26
  // ---------------------------------------------------------------------------
31
27
 
32
28
  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: bigint }>(buffer, layout, ports, handle, state);
45
-
46
- dut.a = 42n;
47
- expect(state.dirty).toBe(true);
48
- expect(dut.a).toBe(42n);
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: bigint }>(buffer, layout, ports, handle, state);
65
-
66
- dut.a = 0xABCDn;
67
- expect(dut.a).toBe(0xABCDn);
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: bigint }>(buffer, layout, ports, handle, state);
82
-
83
- dut.a = 0xDEAD_BEEFn;
84
- expect(dut.a).toBe(0xDEAD_BEEFn);
85
- });
86
-
87
- test("write and read 48-bit value", () => {
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: bigint }>(buffer, layout, ports, handle, state);
99
-
100
- const val = 0x1234_5678_9ABCn;
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: bigint }>(buffer, layout, ports, handle, state);
135
-
136
- dut.a = 0xFFn; // Only lower 4 bits should be stored
137
- expect(dut.a).toBe(0x0Fn);
138
- });
29
+ test("write and read 8-bit input", () => {
30
+ const buffer = makeBuffer(64);
31
+ const layout: Record<string, SignalLayout> = {
32
+ a: {
33
+ offset: 0,
34
+ width: 8,
35
+ byteSize: 1,
36
+ is4state: false,
37
+ direction: "input",
38
+ },
39
+ };
40
+ const ports: Record<string, PortInfo> = {
41
+ a: { direction: "input", type: "logic", width: 8 },
42
+ };
43
+ const handle = mockHandle();
44
+ const state: DirtyState = { dirty: false };
45
+
46
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
47
+
48
+ dut.a = 42n;
49
+ expect(state.dirty).toBe(true);
50
+ expect(dut.a).toBe(42n);
51
+ // Reading an input doesn't trigger evalComb
52
+ expect(handle.evalComb).not.toHaveBeenCalled();
53
+ });
54
+
55
+ test("write and read 16-bit input", () => {
56
+ const buffer = makeBuffer(64);
57
+ const layout: Record<string, SignalLayout> = {
58
+ a: {
59
+ offset: 0,
60
+ width: 16,
61
+ byteSize: 2,
62
+ is4state: false,
63
+ direction: "input",
64
+ },
65
+ };
66
+ const ports: Record<string, PortInfo> = {
67
+ a: { direction: "input", type: "logic", width: 16 },
68
+ };
69
+ const handle = mockHandle();
70
+ const state: DirtyState = { dirty: false };
71
+
72
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
73
+
74
+ dut.a = 0xabcdn;
75
+ expect(dut.a).toBe(0xabcdn);
76
+ });
77
+
78
+ test("write and read 32-bit input", () => {
79
+ const buffer = makeBuffer(64);
80
+ const layout: Record<string, SignalLayout> = {
81
+ a: {
82
+ offset: 0,
83
+ width: 32,
84
+ byteSize: 4,
85
+ is4state: false,
86
+ direction: "input",
87
+ },
88
+ };
89
+ const ports: Record<string, PortInfo> = {
90
+ a: { direction: "input", type: "logic", width: 32 },
91
+ };
92
+ const handle = mockHandle();
93
+ const state: DirtyState = { dirty: false };
94
+
95
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
96
+
97
+ dut.a = 0xdead_beefn;
98
+ expect(dut.a).toBe(0xdead_beefn);
99
+ });
100
+
101
+ test("write and read 48-bit value", () => {
102
+ const buffer = makeBuffer(64);
103
+ const layout: Record<string, SignalLayout> = {
104
+ a: {
105
+ offset: 0,
106
+ width: 48,
107
+ byteSize: 8,
108
+ is4state: false,
109
+ direction: "input",
110
+ },
111
+ };
112
+ const ports: Record<string, PortInfo> = {
113
+ a: { direction: "input", type: "logic", width: 48 },
114
+ };
115
+ const handle = mockHandle();
116
+ const state: DirtyState = { dirty: false };
117
+
118
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
119
+
120
+ const val = 0x1234_5678_9abcn;
121
+ dut.a = val;
122
+ expect(dut.a).toBe(val);
123
+ });
124
+
125
+ test("write and read 64-bit BigInt value", () => {
126
+ const buffer = makeBuffer(64);
127
+ const layout: Record<string, SignalLayout> = {
128
+ a: {
129
+ offset: 0,
130
+ width: 64,
131
+ byteSize: 8,
132
+ is4state: false,
133
+ direction: "input",
134
+ },
135
+ };
136
+ const ports: Record<string, PortInfo> = {
137
+ a: { direction: "input", type: "logic", width: 64 },
138
+ };
139
+ const handle = mockHandle();
140
+ const state: DirtyState = { dirty: false };
141
+
142
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
143
+
144
+ const val = 0xdead_beef_cafe_baben;
145
+ (dut as any).a = val;
146
+ expect(dut.a).toBe(val);
147
+ });
148
+
149
+ test("8-bit write masks to width", () => {
150
+ const buffer = makeBuffer(64);
151
+ const layout: Record<string, SignalLayout> = {
152
+ a: {
153
+ offset: 0,
154
+ width: 4,
155
+ byteSize: 1,
156
+ is4state: false,
157
+ direction: "input",
158
+ },
159
+ };
160
+ const ports: Record<string, PortInfo> = {
161
+ a: { direction: "input", type: "logic", width: 4 },
162
+ };
163
+ const handle = mockHandle();
164
+ const state: DirtyState = { dirty: false };
165
+
166
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
167
+
168
+ dut.a = 0xffn; // Only lower 4 bits should be stored
169
+ expect(dut.a).toBe(0x0fn);
170
+ });
139
171
  });
140
172
 
141
173
  // ---------------------------------------------------------------------------
@@ -143,86 +175,130 @@ describe("createDut — scalar ports", () => {
143
175
  // ---------------------------------------------------------------------------
144
176
 
145
177
  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: bigint; readonly sum: bigint }>(
160
- buffer, layout, ports, handle, state,
161
- );
162
-
163
- // Write input → dirty
164
- dut.a = 100n;
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: bigint }>(
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: bigint }>(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: bigint }>(buffer, layout, ports, handle, state);
221
-
222
- expect(() => {
223
- dut.sum = 42n;
224
- }).toThrow("Cannot write to output port 'sum'");
225
- });
178
+ test("reading output when dirty triggers evalComb", () => {
179
+ const buffer = makeBuffer(64);
180
+ const layout: Record<string, SignalLayout> = {
181
+ a: {
182
+ offset: 0,
183
+ width: 16,
184
+ byteSize: 2,
185
+ is4state: false,
186
+ direction: "input",
187
+ },
188
+ sum: {
189
+ offset: 4,
190
+ width: 17,
191
+ byteSize: 4,
192
+ is4state: false,
193
+ direction: "output",
194
+ },
195
+ };
196
+ const ports: Record<string, PortInfo> = {
197
+ a: { direction: "input", type: "logic", width: 16 },
198
+ sum: { direction: "output", type: "logic", width: 17 },
199
+ };
200
+ const handle = mockHandle();
201
+ const state: DirtyState = { dirty: false };
202
+
203
+ const dut = createDut<{ a: bigint; readonly sum: bigint }>(
204
+ buffer,
205
+ layout,
206
+ ports,
207
+ handle,
208
+ state,
209
+ );
210
+
211
+ // Write input dirty
212
+ dut.a = 100n;
213
+ expect(state.dirty).toBe(true);
214
+
215
+ // Read output → evalComb should be called
216
+ void dut.sum;
217
+ expect(handle.evalComb).toHaveBeenCalledTimes(1);
218
+ expect(state.dirty).toBe(false);
219
+ });
220
+
221
+ test("reading output when clean does NOT trigger evalComb", () => {
222
+ const buffer = makeBuffer(64);
223
+ const layout: Record<string, SignalLayout> = {
224
+ sum: {
225
+ offset: 0,
226
+ width: 17,
227
+ byteSize: 4,
228
+ is4state: false,
229
+ direction: "output",
230
+ },
231
+ };
232
+ const ports: Record<string, PortInfo> = {
233
+ sum: { direction: "output", type: "logic", width: 17 },
234
+ };
235
+ const handle = mockHandle();
236
+ const state: DirtyState = { dirty: false };
237
+
238
+ const dut = createDut<{ readonly sum: bigint }>(
239
+ buffer,
240
+ layout,
241
+ ports,
242
+ handle,
243
+ state,
244
+ );
245
+
246
+ void dut.sum;
247
+ expect(handle.evalComb).not.toHaveBeenCalled();
248
+ });
249
+
250
+ test("reading input does NOT trigger evalComb even when dirty", () => {
251
+ const buffer = makeBuffer(64);
252
+ const layout: Record<string, SignalLayout> = {
253
+ a: {
254
+ offset: 0,
255
+ width: 8,
256
+ byteSize: 1,
257
+ is4state: false,
258
+ direction: "input",
259
+ },
260
+ };
261
+ const ports: Record<string, PortInfo> = {
262
+ a: { direction: "input", type: "logic", width: 8 },
263
+ };
264
+ const handle = mockHandle();
265
+ const state: DirtyState = { dirty: true };
266
+
267
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
268
+
269
+ void dut.a;
270
+ expect(handle.evalComb).not.toHaveBeenCalled();
271
+ });
272
+
273
+ test("writing to output throws", () => {
274
+ const buffer = makeBuffer(64);
275
+ const layout: Record<string, SignalLayout> = {
276
+ sum: {
277
+ offset: 0,
278
+ width: 17,
279
+ byteSize: 4,
280
+ is4state: false,
281
+ direction: "output",
282
+ },
283
+ };
284
+ const ports: Record<string, PortInfo> = {
285
+ sum: { direction: "output", type: "logic", width: 17 },
286
+ };
287
+ const handle = mockHandle();
288
+ const state: DirtyState = { dirty: false };
289
+
290
+ const dut = createDut<{ sum: bigint }>(
291
+ buffer,
292
+ layout,
293
+ ports,
294
+ handle,
295
+ state,
296
+ );
297
+
298
+ expect(() => {
299
+ dut.sum = 42n;
300
+ }).toThrow("Cannot write to output port 'sum'");
301
+ });
226
302
  });
227
303
 
228
304
  // ---------------------------------------------------------------------------
@@ -230,24 +306,36 @@ describe("createDut — dirty tracking", () => {
230
306
  // ---------------------------------------------------------------------------
231
307
 
232
308
  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: bigint }>(buffer, layout, ports, handle, state);
247
-
248
- expect(Object.keys(dut as object)).toEqual(["a"]);
249
- expect((dut as any).clk).toBeUndefined();
250
- });
309
+ test("clock ports are not exposed on the DUT", () => {
310
+ const buffer = makeBuffer(64);
311
+ const layout: Record<string, SignalLayout> = {
312
+ clk: {
313
+ offset: 0,
314
+ width: 1,
315
+ byteSize: 1,
316
+ is4state: false,
317
+ direction: "input",
318
+ },
319
+ a: {
320
+ offset: 1,
321
+ width: 8,
322
+ byteSize: 1,
323
+ is4state: false,
324
+ direction: "input",
325
+ },
326
+ };
327
+ const ports: Record<string, PortInfo> = {
328
+ clk: { direction: "input", type: "clock", width: 1 },
329
+ a: { direction: "input", type: "logic", width: 8 },
330
+ };
331
+ const handle = mockHandle();
332
+ const state: DirtyState = { dirty: false };
333
+
334
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
335
+
336
+ expect(Object.keys(dut as object)).toEqual(["a"]);
337
+ expect((dut as any).clk).toBeUndefined();
338
+ });
251
339
  });
252
340
 
253
341
  // ---------------------------------------------------------------------------
@@ -255,48 +343,72 @@ describe("createDut — clock ports", () => {
255
343
  // ---------------------------------------------------------------------------
256
344
 
257
345
  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: bigint;
285
- a: bigint;
286
- b: bigint;
287
- readonly sum: bigint;
288
- }>(buffer, layout, ports, handle, state);
289
-
290
- dut.a = 100n;
291
- dut.b = 200n;
292
- // sum read triggers evalComb
293
- expect(dut.sum).toBe(300n);
294
- expect(handle.evalComb).toHaveBeenCalledTimes(1);
295
-
296
- // second read without changes → no evalComb
297
- expect(dut.sum).toBe(300n);
298
- expect(handle.evalComb).toHaveBeenCalledTimes(1);
299
- });
346
+ test("Adder-like module with a, b, sum", () => {
347
+ const buffer = makeBuffer(64);
348
+ const layout: Record<string, SignalLayout> = {
349
+ rst: {
350
+ offset: 0,
351
+ width: 1,
352
+ byteSize: 1,
353
+ is4state: false,
354
+ direction: "input",
355
+ },
356
+ a: {
357
+ offset: 2,
358
+ width: 16,
359
+ byteSize: 2,
360
+ is4state: false,
361
+ direction: "input",
362
+ },
363
+ b: {
364
+ offset: 4,
365
+ width: 16,
366
+ byteSize: 2,
367
+ is4state: false,
368
+ direction: "input",
369
+ },
370
+ sum: {
371
+ offset: 8,
372
+ width: 17,
373
+ byteSize: 4,
374
+ is4state: false,
375
+ direction: "output",
376
+ },
377
+ };
378
+ const ports: Record<string, PortInfo> = {
379
+ clk: { direction: "input", type: "clock", width: 1 },
380
+ rst: { direction: "input", type: "reset", width: 1 },
381
+ a: { direction: "input", type: "logic", width: 16 },
382
+ b: { direction: "input", type: "logic", width: 16 },
383
+ sum: { direction: "output", type: "logic", width: 17 },
384
+ };
385
+ const handle = mockHandle();
386
+ // Simulate evalComb by writing result into buffer
387
+ (handle.evalComb as ReturnType<typeof vi.fn>).mockImplementation(() => {
388
+ const view = new DataView(buffer);
389
+ const a = view.getUint16(2, true);
390
+ const b = view.getUint16(4, true);
391
+ view.setUint32(8, a + b, true);
392
+ });
393
+
394
+ const state: DirtyState = { dirty: false };
395
+ const dut = createDut<{
396
+ rst: bigint;
397
+ a: bigint;
398
+ b: bigint;
399
+ readonly sum: bigint;
400
+ }>(buffer, layout, ports, handle, state);
401
+
402
+ dut.a = 100n;
403
+ dut.b = 200n;
404
+ // sum read triggers evalComb
405
+ expect(dut.sum).toBe(300n);
406
+ expect(handle.evalComb).toHaveBeenCalledTimes(1);
407
+
408
+ // second read without changes → no evalComb
409
+ expect(dut.sum).toBe(300n);
410
+ expect(handle.evalComb).toHaveBeenCalledTimes(1);
411
+ });
300
412
  });
301
413
 
302
414
  // ---------------------------------------------------------------------------
@@ -304,145 +416,193 @@ describe("createDut — multiple signals", () => {
304
416
  // ---------------------------------------------------------------------------
305
417
 
306
418
  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: bigint }>(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(0n);
325
- expect(mask).toBe(0xFFn);
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: bigint }>(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(0b1010n);
344
- expect(mask).toBe(0b0100n);
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: bigint }>(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: bigint }>(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: bigint }>(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(0xFFn);
400
-
401
- // Then write a defined value — mask should clear
402
- dut.a = 42n;
403
- const [value, maskAfter] = readFourState(buffer, layout.a);
404
- expect(value).toBe(42n);
405
- expect(maskAfter).toBe(0n);
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: bigint }>(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(0xABn);
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: bigint }>(buffer, layout, ports, handle, state);
442
-
443
- (dut as any).a = X;
444
- expect(state.dirty).toBe(true);
445
- });
419
+ test("write X to a 4-state signal", () => {
420
+ // 8-bit signal: 1 byte value + 1 byte mask = 2 bytes
421
+ const buffer = makeBuffer(64);
422
+ const layout: Record<string, SignalLayout> = {
423
+ a: {
424
+ offset: 0,
425
+ width: 8,
426
+ byteSize: 1,
427
+ is4state: true,
428
+ direction: "input",
429
+ },
430
+ };
431
+ const ports: Record<string, PortInfo> = {
432
+ a: { direction: "input", type: "logic", width: 8, is4state: true },
433
+ };
434
+ const handle = mockHandle();
435
+ const state: DirtyState = { dirty: false };
436
+
437
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
438
+
439
+ (dut as any).a = X;
440
+ // Value should be 0, mask should be 0xFF
441
+ const [value, mask] = readFourState(buffer, layout.a);
442
+ expect(value).toBe(0n);
443
+ expect(mask).toBe(0xffn);
444
+ });
445
+
446
+ test("write FourState to a 4-state signal", () => {
447
+ const buffer = makeBuffer(64);
448
+ const layout: Record<string, SignalLayout> = {
449
+ a: {
450
+ offset: 0,
451
+ width: 8,
452
+ byteSize: 1,
453
+ is4state: true,
454
+ direction: "input",
455
+ },
456
+ };
457
+ const ports: Record<string, PortInfo> = {
458
+ a: { direction: "input", type: "logic", width: 8, is4state: true },
459
+ };
460
+ const handle = mockHandle();
461
+ const state: DirtyState = { dirty: false };
462
+
463
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
464
+
465
+ (dut as any).a = FourState(0b1010, 0b0100);
466
+ const [value, mask] = readFourState(buffer, layout.a);
467
+ expect(value).toBe(0b1010n);
468
+ expect(mask).toBe(0b0100n);
469
+ });
470
+
471
+ test("writing X to non-4-state signal throws", () => {
472
+ const buffer = makeBuffer(64);
473
+ const layout: Record<string, SignalLayout> = {
474
+ a: {
475
+ offset: 0,
476
+ width: 8,
477
+ byteSize: 1,
478
+ is4state: false,
479
+ direction: "input",
480
+ },
481
+ };
482
+ const ports: Record<string, PortInfo> = {
483
+ a: { direction: "input", type: "logic", width: 8 },
484
+ };
485
+ const handle = mockHandle();
486
+ const state: DirtyState = { dirty: false };
487
+
488
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
489
+
490
+ expect(() => {
491
+ (dut as any).a = X;
492
+ }).toThrow("not 4-state");
493
+ });
494
+
495
+ test("writing FourState to non-4-state signal throws", () => {
496
+ const buffer = makeBuffer(64);
497
+ const layout: Record<string, SignalLayout> = {
498
+ a: {
499
+ offset: 0,
500
+ width: 8,
501
+ byteSize: 1,
502
+ is4state: false,
503
+ direction: "input",
504
+ },
505
+ };
506
+ const ports: Record<string, PortInfo> = {
507
+ a: { direction: "input", type: "logic", width: 8 },
508
+ };
509
+ const handle = mockHandle();
510
+ const state: DirtyState = { dirty: false };
511
+
512
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
513
+
514
+ expect(() => {
515
+ (dut as any).a = FourState(0xa5, 0x0f);
516
+ }).toThrow("not 4-state");
517
+ });
518
+
519
+ test("writing defined value to 4-state signal clears mask", () => {
520
+ const buffer = makeBuffer(64);
521
+ const layout: Record<string, SignalLayout> = {
522
+ a: {
523
+ offset: 0,
524
+ width: 8,
525
+ byteSize: 1,
526
+ is4state: true,
527
+ direction: "input",
528
+ },
529
+ };
530
+ const ports: Record<string, PortInfo> = {
531
+ a: { direction: "input", type: "logic", width: 8, is4state: true },
532
+ };
533
+ const handle = mockHandle();
534
+ const state: DirtyState = { dirty: false };
535
+
536
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
537
+
538
+ // First write X
539
+ (dut as any).a = X;
540
+ const [, maskBefore] = readFourState(buffer, layout.a);
541
+ expect(maskBefore).toBe(0xffn);
542
+
543
+ // Then write a defined value — mask should clear
544
+ dut.a = 42n;
545
+ const [value, maskAfter] = readFourState(buffer, layout.a);
546
+ expect(value).toBe(42n);
547
+ expect(maskAfter).toBe(0n);
548
+ });
549
+
550
+ test("reading 4-state output returns value part only", () => {
551
+ const buffer = makeBuffer(64);
552
+ const view = new DataView(buffer);
553
+ const layout: Record<string, SignalLayout> = {
554
+ y: {
555
+ offset: 0,
556
+ width: 8,
557
+ byteSize: 1,
558
+ is4state: true,
559
+ direction: "output",
560
+ },
561
+ };
562
+ const ports: Record<string, PortInfo> = {
563
+ y: { direction: "output", type: "logic", width: 8, is4state: true },
564
+ };
565
+ const handle = mockHandle();
566
+ const state: DirtyState = { dirty: false };
567
+
568
+ const dut = createDut<{ readonly y: bigint }>(
569
+ buffer,
570
+ layout,
571
+ ports,
572
+ handle,
573
+ state,
574
+ );
575
+
576
+ // Set value=0xAB, mask=0x0F (lower 4 bits are X)
577
+ view.setUint8(0, 0xab);
578
+ view.setUint8(1, 0x0f);
579
+
580
+ // DUT getter returns the value part
581
+ expect(dut.y).toBe(0xabn);
582
+ });
583
+
584
+ test("write X sets dirty flag", () => {
585
+ const buffer = makeBuffer(64);
586
+ const layout: Record<string, SignalLayout> = {
587
+ a: {
588
+ offset: 0,
589
+ width: 8,
590
+ byteSize: 1,
591
+ is4state: true,
592
+ direction: "input",
593
+ },
594
+ };
595
+ const ports: Record<string, PortInfo> = {
596
+ a: { direction: "input", type: "logic", width: 8, is4state: true },
597
+ };
598
+ const handle = mockHandle();
599
+ const state: DirtyState = { dirty: false };
600
+
601
+ const dut = createDut<{ a: bigint }>(buffer, layout, ports, handle, state);
602
+
603
+ (dut as any).a = X;
604
+ expect(state.dirty).toBe(true);
605
+ });
446
606
  });
447
607
 
448
608
  // ---------------------------------------------------------------------------
@@ -450,272 +610,491 @@ describe("createDut — 4-state", () => {
450
610
  // ---------------------------------------------------------------------------
451
611
 
452
612
  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: { at(i: number): bigint; set(i: number, v: bigint): void; length: number } }>(
466
- buffer, layout, ports, handle, state,
467
- );
468
-
469
- dut.data.set(0, 0xAAn);
470
- dut.data.set(1, 0xBBn);
471
- dut.data.set(2, 0xCCn);
472
- dut.data.set(3, 0xDDn);
473
-
474
- expect(dut.data.at(0)).toBe(0xAAn);
475
- expect(dut.data.at(1)).toBe(0xBBn);
476
- expect(dut.data.at(2)).toBe(0xCCn);
477
- expect(dut.data.at(3)).toBe(0xDDn);
478
- expect(dut.data.length).toBe(4);
479
- });
480
-
481
- test("1-bit elements (logic [N]): bit-packed read/write", () => {
482
- // logic[4]: 4 elements of 1 bit each = 1 byte total in native memory
483
- const buffer = makeBuffer(64);
484
- const layout: Record<string, SignalLayout> = {
485
- push: { offset: 0, width: 1, byteSize: 1, is4state: false, direction: "input" },
486
- };
487
- const ports: Record<string, PortInfo> = {
488
- push: { direction: "input", type: "logic", width: 1, arrayDims: [4] },
489
- };
490
- const handle = mockHandle();
491
- const state: DirtyState = { dirty: false };
492
-
493
- const dut = createDut<{ push: { at(i: number): bigint; set(i: number, v: bigint): void; length: number } }>(
494
- buffer, layout, ports, handle, state,
495
- );
496
-
497
- dut.push.set(0, 1n);
498
- dut.push.set(1, 1n);
499
- dut.push.set(2, 0n);
500
- dut.push.set(3, 1n);
501
-
502
- expect(dut.push.at(0)).toBe(1n);
503
- expect(dut.push.at(1)).toBe(1n);
504
- expect(dut.push.at(2)).toBe(0n);
505
- expect(dut.push.at(3)).toBe(1n);
506
- expect(dut.push.length).toBe(4);
507
-
508
- // Verify native layout: all 4 elements packed in byte 0
509
- const view = new DataView(buffer);
510
- expect(view.getUint8(0)).toBe(0b1011); // bits 0,1,3 set = 0x0B
511
- expect(view.getUint8(1)).toBe(0); // no overflow into byte 1
512
- });
513
-
514
- test("1-bit elements: writes don't corrupt adjacent elements", () => {
515
- const buffer = makeBuffer(64);
516
- const layout: Record<string, SignalLayout> = {
517
- bits: { offset: 0, width: 1, byteSize: 1, is4state: false, direction: "input" },
518
- };
519
- const ports: Record<string, PortInfo> = {
520
- bits: { direction: "input", type: "logic", width: 1, arrayDims: [8] },
521
- };
522
- const handle = mockHandle();
523
- const state: DirtyState = { dirty: false };
524
-
525
- const dut = createDut<{ bits: { at(i: number): bigint; set(i: number, v: bigint): void } }>(
526
- buffer, layout, ports, handle, state,
527
- );
528
-
529
- // Set element 0; verify element 1 unaffected
530
- dut.bits.set(0, 1n);
531
- expect(dut.bits.at(0)).toBe(1n);
532
- dut.bits.set(1, 1n);
533
- expect(dut.bits.at(0)).toBe(1n); // element 0 must not be corrupted
534
- expect(dut.bits.at(1)).toBe(1n);
535
-
536
- // Clear element 0; element 1 must remain set
537
- dut.bits.set(0, 0n);
538
- expect(dut.bits.at(0)).toBe(0n);
539
- expect(dut.bits.at(1)).toBe(1n);
540
- });
541
-
542
- test("1-bit elements: 8 elements span exactly one byte", () => {
543
- const buffer = makeBuffer(64);
544
- const layout: Record<string, SignalLayout> = {
545
- bits: { offset: 2, width: 1, byteSize: 1, is4state: false, direction: "input" },
546
- };
547
- const ports: Record<string, PortInfo> = {
548
- bits: { direction: "input", type: "logic", width: 1, arrayDims: [8] },
549
- };
550
- const handle = mockHandle();
551
- const state: DirtyState = { dirty: false };
552
-
553
- const dut = createDut<{ bits: { at(i: number): bigint; set(i: number, v: bigint): void } }>(
554
- buffer, layout, ports, handle, state,
555
- );
556
-
557
- // Set all 8 bits
558
- for (let i = 0; i < 8; i++) dut.bits.set(i, 1n);
559
- const view = new DataView(buffer);
560
- expect(view.getUint8(2)).toBe(0xff);
561
-
562
- // Clear alternating bits
563
- for (let i = 0; i < 8; i += 2) dut.bits.set(i, 0n);
564
- expect(view.getUint8(2)).toBe(0b10101010);
565
- });
566
-
567
- test("elementWidth=3: elements spanning byte boundaries read/write correctly", () => {
568
- // logic<3>[6]: 6 × 3 = 18 bits = 3 bytes
569
- // Element 2: bitStart=6, spans bytes 0→1 ← cross-byte write/read path
570
- // Element 5: bitStart=15, spans bytes 1→2 ← cross-byte write/read path
571
- const buffer = makeBuffer(64);
572
- const layout: Record<string, SignalLayout> = {
573
- data: { offset: 0, width: 3, byteSize: 1, is4state: false, direction: "input" },
574
- };
575
- const ports: Record<string, PortInfo> = {
576
- data: { direction: "input", type: "logic", width: 3, arrayDims: [6] },
577
- };
578
- const handle = mockHandle();
579
- const state: DirtyState = { dirty: false };
580
-
581
- const dut = createDut<{ data: { at(i: number): bigint; set(i: number, v: bigint): void; length: number } }>(
582
- buffer, layout, ports, handle, state,
583
- );
584
-
585
- // Set all 6 elements to distinct values 1..6
586
- for (let i = 0; i < 6; i++) dut.data.set(i, BigInt(i + 1));
587
- for (let i = 0; i < 6; i++) expect(dut.data.at(i)).toBe(BigInt(i + 1));
588
-
589
- // Overwrite the two cross-byte elements and verify neighbours are unaffected
590
- dut.data.set(2, 0b101n); // bitStart=6, crosses byte 0→1
591
- expect(dut.data.at(2)).toBe(5n);
592
- expect(dut.data.at(1)).toBe(2n); // neighbour below
593
- expect(dut.data.at(3)).toBe(4n); // neighbour above
594
-
595
- dut.data.set(5, 0b110n); // bitStart=15, crosses byte 1→2
596
- expect(dut.data.at(5)).toBe(6n);
597
- expect(dut.data.at(4)).toBe(5n); // neighbour below
598
- });
599
-
600
- test("1-bit 4-state: X writes value=0 and mask=1 at correct byte offset", () => {
601
- // logic[8] with 4-state: value in byte 0 (bits 0-7 = elements 0-7),
602
- // mask in byte 1 (bits 0-7 = masks for elements 0-7).
603
- // totalValueBytes = ceil(8*1/8) = 1, so maskBase = offset + 1.
604
- const buffer = makeBuffer(64);
605
- const layout: Record<string, SignalLayout> = {
606
- bits: { offset: 0, width: 1, byteSize: 1, is4state: true, direction: "input" },
607
- };
608
- const ports: Record<string, PortInfo> = {
609
- bits: { direction: "input", type: "logic", width: 1, arrayDims: [8], is4state: true },
610
- };
611
- const handle = mockHandle();
612
- const state: DirtyState = { dirty: false };
613
-
614
- const dut = createDut<{ bits: { at(i: number): bigint; set(i: number, v: unknown): void } }>(
615
- buffer, layout, ports, handle, state,
616
- );
617
-
618
- const view = new DataView(buffer);
619
-
620
- // Assign X to element 3: value bit 3 → 0, mask bit 3 → 1
621
- dut.bits.set(3, X);
622
- expect(view.getUint8(0) & 0b00001000).toBe(0); // value: bit 3 = 0
623
- expect(view.getUint8(1) & 0b00001000).toBe(0b00001000); // mask: bit 3 = 1
624
-
625
- // Write a defined 1 to element 3: value bit 3 → 1, mask bit 3 → 0
626
- dut.bits.set(3, 1n);
627
- expect(view.getUint8(0) & 0b00001000).toBe(0b00001000); // value: bit 3 = 1
628
- expect(view.getUint8(1) & 0b00001000).toBe(0); // mask: bit 3 cleared
629
-
630
- // Setting elements 0 and 7 to X should only affect those mask bits
631
- dut.bits.set(0, X);
632
- dut.bits.set(7, X);
633
- expect(view.getUint8(1)).toBe(0b10000001); // mask: bits 0 and 7 set
634
- });
635
-
636
- test("3-bit 4-state: mask region starts at ceil(totalBits/8) bytes after value", () => {
637
- // logic<3>[5]: 5 × 3 = 15 bits totalValueBytes = ceil(15/8) = 2
638
- // value occupies bytes 0-1, mask occupies bytes 2-3; maskBase = offset + 2
639
- const buffer = makeBuffer(64);
640
- const layout: Record<string, SignalLayout> = {
641
- data: { offset: 0, width: 3, byteSize: 1, is4state: true, direction: "input" },
642
- };
643
- const ports: Record<string, PortInfo> = {
644
- data: { direction: "input", type: "logic", width: 3, arrayDims: [5], is4state: true },
645
- };
646
- const handle = mockHandle();
647
- const state: DirtyState = { dirty: false };
648
-
649
- const dut = createDut<{ data: { set(i: number, v: unknown): void } }>(
650
- buffer, layout, ports, handle, state,
651
- );
652
-
653
- const view = new DataView(buffer);
654
-
655
- // Assign X to element 0 (bitStart=0, no cross-byte): value bits 0-2 = 0, mask bits 0-2 = 0b111
656
- dut.data.set(0, X);
657
- expect(view.getUint8(0) & 0b111).toBe(0); // value byte 0: bits 0-2 cleared
658
- expect(view.getUint8(2) & 0b111).toBe(0b111); // mask byte 2: bits 0-2 set
659
- expect(view.getUint8(1)).toBe(0); // value byte 1: untouched
660
- expect(view.getUint8(3)).toBe(0); // mask byte 3: untouched
661
-
662
- // Write a defined value to element 0, verify mask cleared
663
- dut.data.set(0, 0b110n);
664
- expect(view.getUint8(0) & 0b111).toBe(0b110); // value bits 0-2 = 6
665
- expect(view.getUint8(2) & 0b111).toBe(0); // mask bits 0-2 cleared
666
- });
613
+ test("read/write array elements", () => {
614
+ // 4 elements of 8 bits each = 4 bytes
615
+ const buffer = makeBuffer(64);
616
+ const layout: Record<string, SignalLayout> = {
617
+ data: {
618
+ offset: 0,
619
+ width: 8,
620
+ byteSize: 4,
621
+ is4state: false,
622
+ direction: "input",
623
+ },
624
+ };
625
+ const ports: Record<string, PortInfo> = {
626
+ data: { direction: "input", type: "logic", width: 8, arrayDims: [4] },
627
+ };
628
+ const handle = mockHandle();
629
+ const state: DirtyState = { dirty: false };
630
+
631
+ const dut = createDut<{
632
+ data: {
633
+ at(i: number): bigint;
634
+ set(i: number, v: bigint): void;
635
+ length: number;
636
+ };
637
+ }>(buffer, layout, ports, handle, state);
638
+
639
+ dut.data.set(0, 0xaan);
640
+ dut.data.set(1, 0xbbn);
641
+ dut.data.set(2, 0xccn);
642
+ dut.data.set(3, 0xddn);
643
+
644
+ expect(dut.data.at(0)).toBe(0xaan);
645
+ expect(dut.data.at(1)).toBe(0xbbn);
646
+ expect(dut.data.at(2)).toBe(0xccn);
647
+ expect(dut.data.at(3)).toBe(0xddn);
648
+ expect(dut.data.length).toBe(4);
649
+ });
650
+
651
+ test("1-bit elements (logic [N]): bit-packed read/write", () => {
652
+ // logic[4]: 4 elements of 1 bit each = 1 byte total in native memory
653
+ const buffer = makeBuffer(64);
654
+ const layout: Record<string, SignalLayout> = {
655
+ push: {
656
+ offset: 0,
657
+ width: 1,
658
+ byteSize: 1,
659
+ is4state: false,
660
+ direction: "input",
661
+ },
662
+ };
663
+ const ports: Record<string, PortInfo> = {
664
+ push: { direction: "input", type: "logic", width: 1, arrayDims: [4] },
665
+ };
666
+ const handle = mockHandle();
667
+ const state: DirtyState = { dirty: false };
668
+
669
+ const dut = createDut<{
670
+ push: {
671
+ at(i: number): bigint;
672
+ set(i: number, v: bigint): void;
673
+ length: number;
674
+ };
675
+ }>(buffer, layout, ports, handle, state);
676
+
677
+ dut.push.set(0, 1n);
678
+ dut.push.set(1, 1n);
679
+ dut.push.set(2, 0n);
680
+ dut.push.set(3, 1n);
681
+
682
+ expect(dut.push.at(0)).toBe(1n);
683
+ expect(dut.push.at(1)).toBe(1n);
684
+ expect(dut.push.at(2)).toBe(0n);
685
+ expect(dut.push.at(3)).toBe(1n);
686
+ expect(dut.push.length).toBe(4);
687
+
688
+ // Verify native layout: all 4 elements packed in byte 0
689
+ const view = new DataView(buffer);
690
+ expect(view.getUint8(0)).toBe(0b1011); // bits 0,1,3 set = 0x0B
691
+ expect(view.getUint8(1)).toBe(0); // no overflow into byte 1
692
+ });
693
+
694
+ test("1-bit elements: writes don't corrupt adjacent elements", () => {
695
+ const buffer = makeBuffer(64);
696
+ const layout: Record<string, SignalLayout> = {
697
+ bits: {
698
+ offset: 0,
699
+ width: 1,
700
+ byteSize: 1,
701
+ is4state: false,
702
+ direction: "input",
703
+ },
704
+ };
705
+ const ports: Record<string, PortInfo> = {
706
+ bits: { direction: "input", type: "logic", width: 1, arrayDims: [8] },
707
+ };
708
+ const handle = mockHandle();
709
+ const state: DirtyState = { dirty: false };
710
+
711
+ const dut = createDut<{
712
+ bits: { at(i: number): bigint; set(i: number, v: bigint): void };
713
+ }>(buffer, layout, ports, handle, state);
714
+
715
+ // Set element 0; verify element 1 unaffected
716
+ dut.bits.set(0, 1n);
717
+ expect(dut.bits.at(0)).toBe(1n);
718
+ dut.bits.set(1, 1n);
719
+ expect(dut.bits.at(0)).toBe(1n); // element 0 must not be corrupted
720
+ expect(dut.bits.at(1)).toBe(1n);
721
+
722
+ // Clear element 0; element 1 must remain set
723
+ dut.bits.set(0, 0n);
724
+ expect(dut.bits.at(0)).toBe(0n);
725
+ expect(dut.bits.at(1)).toBe(1n);
726
+ });
727
+
728
+ test("1-bit elements: 8 elements span exactly one byte", () => {
729
+ const buffer = makeBuffer(64);
730
+ const layout: Record<string, SignalLayout> = {
731
+ bits: {
732
+ offset: 2,
733
+ width: 1,
734
+ byteSize: 1,
735
+ is4state: false,
736
+ direction: "input",
737
+ },
738
+ };
739
+ const ports: Record<string, PortInfo> = {
740
+ bits: { direction: "input", type: "logic", width: 1, arrayDims: [8] },
741
+ };
742
+ const handle = mockHandle();
743
+ const state: DirtyState = { dirty: false };
744
+
745
+ const dut = createDut<{
746
+ bits: { at(i: number): bigint; set(i: number, v: bigint): void };
747
+ }>(buffer, layout, ports, handle, state);
748
+
749
+ // Set all 8 bits
750
+ for (let i = 0; i < 8; i++) dut.bits.set(i, 1n);
751
+ const view = new DataView(buffer);
752
+ expect(view.getUint8(2)).toBe(0xff);
753
+
754
+ // Clear alternating bits
755
+ for (let i = 0; i < 8; i += 2) dut.bits.set(i, 0n);
756
+ expect(view.getUint8(2)).toBe(0b10101010);
757
+ });
758
+
759
+ test("elementWidth=3: elements spanning byte boundaries read/write correctly", () => {
760
+ // logic<3>[6]: 6 × 3 = 18 bits = 3 bytes
761
+ // Element 2: bitStart=6, spans bytes 0→1 ← cross-byte write/read path
762
+ // Element 5: bitStart=15, spans bytes 1→2 cross-byte write/read path
763
+ const buffer = makeBuffer(64);
764
+ const layout: Record<string, SignalLayout> = {
765
+ data: {
766
+ offset: 0,
767
+ width: 3,
768
+ byteSize: 1,
769
+ is4state: false,
770
+ direction: "input",
771
+ },
772
+ };
773
+ const ports: Record<string, PortInfo> = {
774
+ data: { direction: "input", type: "logic", width: 3, arrayDims: [6] },
775
+ };
776
+ const handle = mockHandle();
777
+ const state: DirtyState = { dirty: false };
778
+
779
+ const dut = createDut<{
780
+ data: {
781
+ at(i: number): bigint;
782
+ set(i: number, v: bigint): void;
783
+ length: number;
784
+ };
785
+ }>(buffer, layout, ports, handle, state);
786
+
787
+ // Set all 6 elements to distinct values 1..6
788
+ for (let i = 0; i < 6; i++) dut.data.set(i, BigInt(i + 1));
789
+ for (let i = 0; i < 6; i++) expect(dut.data.at(i)).toBe(BigInt(i + 1));
790
+
791
+ // Overwrite the two cross-byte elements and verify neighbours are unaffected
792
+ dut.data.set(2, 0b101n); // bitStart=6, crosses byte 0→1
793
+ expect(dut.data.at(2)).toBe(5n);
794
+ expect(dut.data.at(1)).toBe(2n); // neighbour below
795
+ expect(dut.data.at(3)).toBe(4n); // neighbour above
796
+
797
+ dut.data.set(5, 0b110n); // bitStart=15, crosses byte 1→2
798
+ expect(dut.data.at(5)).toBe(6n);
799
+ expect(dut.data.at(4)).toBe(5n); // neighbour below
800
+ });
801
+
802
+ test("1-bit 4-state: X writes value=0 and mask=1 at correct byte offset", () => {
803
+ // logic[8] with 4-state: value in byte 0 (bits 0-7 = elements 0-7),
804
+ // mask in byte 1 (bits 0-7 = masks for elements 0-7).
805
+ // totalValueBytes = ceil(8*1/8) = 1, so maskBase = offset + 1.
806
+ const buffer = makeBuffer(64);
807
+ const layout: Record<string, SignalLayout> = {
808
+ bits: {
809
+ offset: 0,
810
+ width: 1,
811
+ byteSize: 1,
812
+ is4state: true,
813
+ direction: "input",
814
+ },
815
+ };
816
+ const ports: Record<string, PortInfo> = {
817
+ bits: {
818
+ direction: "input",
819
+ type: "logic",
820
+ width: 1,
821
+ arrayDims: [8],
822
+ is4state: true,
823
+ },
824
+ };
825
+ const handle = mockHandle();
826
+ const state: DirtyState = { dirty: false };
827
+
828
+ const dut = createDut<{
829
+ bits: { at(i: number): bigint; set(i: number, v: unknown): void };
830
+ }>(buffer, layout, ports, handle, state);
831
+
832
+ const view = new DataView(buffer);
833
+
834
+ // Assign X to element 3: value bit 3 → 0, mask bit 3 → 1
835
+ dut.bits.set(3, X);
836
+ expect(view.getUint8(0) & 0b00001000).toBe(0); // value: bit 3 = 0
837
+ expect(view.getUint8(1) & 0b00001000).toBe(0b00001000); // mask: bit 3 = 1
838
+
839
+ // Write a defined 1 to element 3: value bit 3 → 1, mask bit 3 → 0
840
+ dut.bits.set(3, 1n);
841
+ expect(view.getUint8(0) & 0b00001000).toBe(0b00001000); // value: bit 3 = 1
842
+ expect(view.getUint8(1) & 0b00001000).toBe(0); // mask: bit 3 cleared
843
+
844
+ // Setting elements 0 and 7 to X should only affect those mask bits
845
+ dut.bits.set(0, X);
846
+ dut.bits.set(7, X);
847
+ expect(view.getUint8(1)).toBe(0b10000001); // mask: bits 0 and 7 set
848
+ });
849
+
850
+ test("3-bit 4-state: mask region starts at ceil(totalBits/8) bytes after value", () => {
851
+ // logic<3>[5]: 5 × 3 = 15 bits → totalValueBytes = ceil(15/8) = 2
852
+ // value occupies bytes 0-1, mask occupies bytes 2-3; maskBase = offset + 2
853
+ const buffer = makeBuffer(64);
854
+ const layout: Record<string, SignalLayout> = {
855
+ data: {
856
+ offset: 0,
857
+ width: 3,
858
+ byteSize: 1,
859
+ is4state: true,
860
+ direction: "input",
861
+ },
862
+ };
863
+ const ports: Record<string, PortInfo> = {
864
+ data: {
865
+ direction: "input",
866
+ type: "logic",
867
+ width: 3,
868
+ arrayDims: [5],
869
+ is4state: true,
870
+ },
871
+ };
872
+ const handle = mockHandle();
873
+ const state: DirtyState = { dirty: false };
874
+
875
+ const dut = createDut<{ data: { set(i: number, v: unknown): void } }>(
876
+ buffer,
877
+ layout,
878
+ ports,
879
+ handle,
880
+ state,
881
+ );
882
+
883
+ const view = new DataView(buffer);
884
+
885
+ // Assign X to element 0 (bitStart=0, no cross-byte): value bits 0-2 = 0, mask bits 0-2 = 0b111
886
+ dut.data.set(0, X);
887
+ expect(view.getUint8(0) & 0b111).toBe(0); // value byte 0: bits 0-2 cleared
888
+ expect(view.getUint8(2) & 0b111).toBe(0b111); // mask byte 2: bits 0-2 set
889
+ expect(view.getUint8(1)).toBe(0); // value byte 1: untouched
890
+ expect(view.getUint8(3)).toBe(0); // mask byte 3: untouched
891
+
892
+ // Write a defined value to element 0, verify mask cleared
893
+ dut.data.set(0, 0b110n);
894
+ expect(view.getUint8(0) & 0b111).toBe(0b110); // value bits 0-2 = 6
895
+ expect(view.getUint8(2) & 0b111).toBe(0); // mask bits 0-2 cleared
896
+ });
667
897
  });
668
898
 
669
899
  // ---------------------------------------------------------------------------
670
900
  // Interface (nested) ports
671
901
  // ---------------------------------------------------------------------------
672
902
 
903
+ describe("createDut — interface ports with array members", () => {
904
+ test("interface member with arrayDims uses array accessor", () => {
905
+ // Interface array pattern: bus: modport Bus::consumer [2]
906
+ // Veryl expands to bus.data with arrayDims:[2], bus.valid with arrayDims:[2]
907
+ const buffer = makeBuffer(64);
908
+ const layout: Record<string, SignalLayout> = {
909
+ "bus.data": {
910
+ offset: 0,
911
+ width: 8,
912
+ byteSize: 2,
913
+ is4state: false,
914
+ direction: "input",
915
+ },
916
+ "bus.valid": {
917
+ offset: 2,
918
+ width: 1,
919
+ byteSize: 1,
920
+ is4state: false,
921
+ direction: "input",
922
+ },
923
+ };
924
+ const ports: Record<string, PortInfo> = {
925
+ bus: {
926
+ direction: "inout",
927
+ type: "logic",
928
+ width: 0,
929
+ interface: {
930
+ data: { direction: "input", type: "logic", width: 8, arrayDims: [2] },
931
+ valid: {
932
+ direction: "input",
933
+ type: "logic",
934
+ width: 1,
935
+ arrayDims: [2],
936
+ },
937
+ },
938
+ },
939
+ };
940
+ const handle = mockHandle();
941
+ const state: DirtyState = { dirty: false };
942
+
943
+ const dut = createDut<{
944
+ bus: {
945
+ data: {
946
+ at(i: number): bigint;
947
+ set(i: number, v: bigint): void;
948
+ length: number;
949
+ };
950
+ valid: {
951
+ at(i: number): bigint;
952
+ set(i: number, v: bigint): void;
953
+ length: number;
954
+ };
955
+ };
956
+ }>(buffer, layout, ports, handle, state);
957
+
958
+ // data: 2 elements of 8 bits each
959
+ dut.bus.data.set(0, 0xaan);
960
+ dut.bus.data.set(1, 0xbbn);
961
+ expect(dut.bus.data.at(0)).toBe(0xaan);
962
+ expect(dut.bus.data.at(1)).toBe(0xbbn);
963
+ expect(dut.bus.data.length).toBe(2);
964
+
965
+ // valid: 2 elements of 1 bit each (bit-packed)
966
+ dut.bus.valid.set(0, 1n);
967
+ dut.bus.valid.set(1, 0n);
968
+ expect(dut.bus.valid.at(0)).toBe(1n);
969
+ expect(dut.bus.valid.at(1)).toBe(0n);
970
+ expect(dut.bus.valid.length).toBe(2);
971
+ });
972
+
973
+ test("interface with mixed scalar and array members", () => {
974
+ const buffer = makeBuffer(64);
975
+ const layout: Record<string, SignalLayout> = {
976
+ "bus.data": {
977
+ offset: 0,
978
+ width: 8,
979
+ byteSize: 2,
980
+ is4state: false,
981
+ direction: "input",
982
+ },
983
+ "bus.valid": {
984
+ offset: 4,
985
+ width: 1,
986
+ byteSize: 1,
987
+ is4state: false,
988
+ direction: "input",
989
+ },
990
+ };
991
+ const ports: Record<string, PortInfo> = {
992
+ bus: {
993
+ direction: "inout",
994
+ type: "logic",
995
+ width: 0,
996
+ interface: {
997
+ data: { direction: "input", type: "logic", width: 8, arrayDims: [2] },
998
+ valid: { direction: "input", type: "logic", width: 1 },
999
+ },
1000
+ },
1001
+ };
1002
+ const handle = mockHandle();
1003
+ const state: DirtyState = { dirty: false };
1004
+
1005
+ const dut = createDut<{
1006
+ bus: {
1007
+ data: {
1008
+ at(i: number): bigint;
1009
+ set(i: number, v: bigint): void;
1010
+ length: number;
1011
+ };
1012
+ valid: bigint;
1013
+ };
1014
+ }>(buffer, layout, ports, handle, state);
1015
+
1016
+ // Array member
1017
+ dut.bus.data.set(0, 0x11n);
1018
+ dut.bus.data.set(1, 0x22n);
1019
+ expect(dut.bus.data.at(0)).toBe(0x11n);
1020
+ expect(dut.bus.data.at(1)).toBe(0x22n);
1021
+
1022
+ // Scalar member
1023
+ dut.bus.valid = 1n;
1024
+ expect(dut.bus.valid).toBe(1n);
1025
+ });
1026
+ });
1027
+
673
1028
  describe("createDut — interface ports", () => {
674
- test("nested interface members", () => {
675
- const buffer = makeBuffer(64);
676
- const layout: Record<string, SignalLayout> = {
677
- "bus.addr": { offset: 0, width: 32, byteSize: 4, is4state: false, direction: "input" },
678
- "bus.data": { offset: 4, width: 32, byteSize: 4, is4state: false, direction: "input" },
679
- "bus.valid": { offset: 8, width: 1, byteSize: 1, is4state: false, direction: "input" },
680
- "bus.ready": { offset: 9, width: 1, byteSize: 1, is4state: false, direction: "output" },
681
- };
682
- const ports: Record<string, PortInfo> = {
683
- bus: {
684
- direction: "input",
685
- type: "logic",
686
- width: 0,
687
- interface: {
688
- addr: { direction: "input", type: "logic", width: 32 },
689
- data: { direction: "input", type: "logic", width: 32 },
690
- valid: { direction: "input", type: "logic", width: 1 },
691
- ready: { direction: "output", type: "logic", width: 1 },
692
- },
693
- },
694
- };
695
- const handle = mockHandle();
696
- (handle.evalComb as ReturnType<typeof vi.fn>).mockImplementation(() => {
697
- const view = new DataView(buffer);
698
- // mock: ready = valid
699
- view.setUint8(9, view.getUint8(8));
700
- });
701
-
702
- const state: DirtyState = { dirty: false };
703
- const dut = createDut<{
704
- bus: {
705
- addr: bigint;
706
- data: bigint;
707
- valid: bigint;
708
- readonly ready: bigint;
709
- };
710
- }>(buffer, layout, ports, handle, state);
711
-
712
- dut.bus.addr = 0x1000n;
713
- dut.bus.data = 0xFFn;
714
- dut.bus.valid = 1n;
715
-
716
- expect(dut.bus.addr).toBe(0x1000n);
717
- expect(dut.bus.data).toBe(0xFFn);
718
- expect(dut.bus.ready).toBe(1n);
719
- expect(handle.evalComb).toHaveBeenCalledTimes(1);
720
- });
1029
+ test("nested interface members", () => {
1030
+ const buffer = makeBuffer(64);
1031
+ const layout: Record<string, SignalLayout> = {
1032
+ "bus.addr": {
1033
+ offset: 0,
1034
+ width: 32,
1035
+ byteSize: 4,
1036
+ is4state: false,
1037
+ direction: "input",
1038
+ },
1039
+ "bus.data": {
1040
+ offset: 4,
1041
+ width: 32,
1042
+ byteSize: 4,
1043
+ is4state: false,
1044
+ direction: "input",
1045
+ },
1046
+ "bus.valid": {
1047
+ offset: 8,
1048
+ width: 1,
1049
+ byteSize: 1,
1050
+ is4state: false,
1051
+ direction: "input",
1052
+ },
1053
+ "bus.ready": {
1054
+ offset: 9,
1055
+ width: 1,
1056
+ byteSize: 1,
1057
+ is4state: false,
1058
+ direction: "output",
1059
+ },
1060
+ };
1061
+ const ports: Record<string, PortInfo> = {
1062
+ bus: {
1063
+ direction: "input",
1064
+ type: "logic",
1065
+ width: 0,
1066
+ interface: {
1067
+ addr: { direction: "input", type: "logic", width: 32 },
1068
+ data: { direction: "input", type: "logic", width: 32 },
1069
+ valid: { direction: "input", type: "logic", width: 1 },
1070
+ ready: { direction: "output", type: "logic", width: 1 },
1071
+ },
1072
+ },
1073
+ };
1074
+ const handle = mockHandle();
1075
+ (handle.evalComb as ReturnType<typeof vi.fn>).mockImplementation(() => {
1076
+ const view = new DataView(buffer);
1077
+ // mock: ready = valid
1078
+ view.setUint8(9, view.getUint8(8));
1079
+ });
1080
+
1081
+ const state: DirtyState = { dirty: false };
1082
+ const dut = createDut<{
1083
+ bus: {
1084
+ addr: bigint;
1085
+ data: bigint;
1086
+ valid: bigint;
1087
+ readonly ready: bigint;
1088
+ };
1089
+ }>(buffer, layout, ports, handle, state);
1090
+
1091
+ dut.bus.addr = 0x1000n;
1092
+ dut.bus.data = 0xffn;
1093
+ dut.bus.valid = 1n;
1094
+
1095
+ expect(dut.bus.addr).toBe(0x1000n);
1096
+ expect(dut.bus.data).toBe(0xffn);
1097
+ expect(dut.bus.ready).toBe(1n);
1098
+ expect(handle.evalComb).toHaveBeenCalledTimes(1);
1099
+ });
721
1100
  });