@celox-sim/celox 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/dut.ts CHANGED
@@ -7,13 +7,13 @@
7
7
  * accessors are fully visible to V8 inline caches.
8
8
  */
9
9
 
10
+ import type { HierarchyNode } from "./napi-helpers.js";
10
11
  import type {
11
- NativeHandle,
12
- PortInfo,
13
- SignalLayout,
14
- FourStateValue,
12
+ FourStateValue,
13
+ NativeHandle,
14
+ PortInfo,
15
+ SignalLayout,
15
16
  } from "./types.js";
16
- import type { HierarchyNode } from "./napi-helpers.js";
17
17
  import { isFourStateValue } from "./types.js";
18
18
 
19
19
  // ---------------------------------------------------------------------------
@@ -28,7 +28,7 @@ import { isFourStateValue } from "./types.js";
28
28
  * @internal
29
29
  */
30
30
  export interface DirtyState {
31
- dirty: boolean;
31
+ dirty: boolean;
32
32
  }
33
33
 
34
34
  // ---------------------------------------------------------------------------
@@ -37,141 +37,139 @@ export interface DirtyState {
37
37
 
38
38
  /** Read an unsigned integer of the given byte-size (little-endian), masked to width. */
39
39
  function readNumber(view: DataView, offset: number, width: number): number {
40
- if (width <= 8) {
41
- const raw = view.getUint8(offset);
42
- return width === 8 ? raw : raw & ((1 << width) - 1);
43
- }
44
- if (width <= 16) {
45
- const raw = view.getUint16(offset, true);
46
- return width === 16 ? raw : raw & ((1 << width) - 1);
47
- }
48
- if (width <= 32) {
49
- const raw = view.getUint32(offset, true);
50
- return width === 32 ? raw : (raw & ((1 << width) - 1)) >>> 0;
51
- }
52
- // 33..53 bits — fits safely in a JS number
53
- const lo = view.getUint32(offset, true);
54
- const hi = view.getUint32(offset + 4, true) & ((1 << (width - 32)) - 1);
55
- return lo + hi * 0x1_0000_0000;
40
+ if (width <= 8) {
41
+ const raw = view.getUint8(offset);
42
+ return width === 8 ? raw : raw & ((1 << width) - 1);
43
+ }
44
+ if (width <= 16) {
45
+ const raw = view.getUint16(offset, true);
46
+ return width === 16 ? raw : raw & ((1 << width) - 1);
47
+ }
48
+ if (width <= 32) {
49
+ const raw = view.getUint32(offset, true);
50
+ return width === 32 ? raw : (raw & ((1 << width) - 1)) >>> 0;
51
+ }
52
+ // 33..53 bits — fits safely in a JS number
53
+ const lo = view.getUint32(offset, true);
54
+ const hi = view.getUint32(offset + 4, true) & ((1 << (width - 32)) - 1);
55
+ return lo + hi * 0x1_0000_0000;
56
56
  }
57
57
 
58
58
  /** Write an unsigned integer of the given byte-size (little-endian). */
59
59
  function writeNumber(
60
- view: DataView,
61
- offset: number,
62
- width: number,
63
- value: number,
60
+ view: DataView,
61
+ offset: number,
62
+ width: number,
63
+ value: number,
64
64
  ): void {
65
- if (width <= 8) {
66
- view.setUint8(offset, value & ((1 << width) - 1));
67
- } else if (width <= 16) {
68
- view.setUint16(offset, value & ((1 << width) - 1), true);
69
- } else if (width <= 32) {
70
- view.setUint32(offset, value >>> 0, true);
71
- } else {
72
- // 33..53 bits
73
- view.setUint32(offset, value >>> 0, true);
74
- view.setUint32(offset + 4, Math.floor(value / 0x1_0000_0000) >>> 0, true);
75
- }
65
+ if (width <= 8) {
66
+ view.setUint8(offset, value & ((1 << width) - 1));
67
+ } else if (width <= 16) {
68
+ view.setUint16(offset, value & ((1 << width) - 1), true);
69
+ } else if (width <= 32) {
70
+ view.setUint32(offset, value >>> 0, true);
71
+ } else {
72
+ // 33..53 bits
73
+ view.setUint32(offset, value >>> 0, true);
74
+ view.setUint32(offset + 4, Math.floor(value / 0x1_0000_0000) >>> 0, true);
75
+ }
76
76
  }
77
77
 
78
78
  /** Read a wide value (≥ 54 bits) as BigInt, little-endian. */
79
79
  function readBigInt(view: DataView, offset: number, byteSize: number): bigint {
80
- let result = 0n;
81
- // Read 8 bytes at a time, then remaining bytes
82
- const fullWords = Math.floor(byteSize / 8);
83
- for (let i = 0; i < fullWords; i++) {
84
- const word = view.getBigUint64(offset + i * 8, true);
85
- result |= word << BigInt(i * 64);
86
- }
87
- const remaining = byteSize % 8;
88
- if (remaining > 0) {
89
- const base = offset + fullWords * 8;
90
- for (let i = 0; i < remaining; i++) {
91
- result |= BigInt(view.getUint8(base + i)) << BigInt(fullWords * 64 + i * 8);
92
- }
93
- }
94
- return result;
80
+ let result = 0n;
81
+ // Read 8 bytes at a time, then remaining bytes
82
+ const fullWords = Math.floor(byteSize / 8);
83
+ for (let i = 0; i < fullWords; i++) {
84
+ const word = view.getBigUint64(offset + i * 8, true);
85
+ result |= word << BigInt(i * 64);
86
+ }
87
+ const remaining = byteSize % 8;
88
+ if (remaining > 0) {
89
+ const base = offset + fullWords * 8;
90
+ for (let i = 0; i < remaining; i++) {
91
+ result |=
92
+ BigInt(view.getUint8(base + i)) << BigInt(fullWords * 64 + i * 8);
93
+ }
94
+ }
95
+ return result;
95
96
  }
96
97
 
97
98
  /** Write a wide value (≥ 54 bits) as BigInt, little-endian. */
98
99
  function writeBigInt(
99
- view: DataView,
100
- offset: number,
101
- byteSize: number,
102
- value: bigint,
100
+ view: DataView,
101
+ offset: number,
102
+ byteSize: number,
103
+ value: bigint,
103
104
  ): void {
104
- const fullWords = Math.floor(byteSize / 8);
105
- for (let i = 0; i < fullWords; i++) {
106
- view.setBigUint64(offset + i * 8, value & 0xFFFF_FFFF_FFFF_FFFFn, true);
107
- value >>= 64n;
108
- }
109
- const remaining = byteSize % 8;
110
- if (remaining > 0) {
111
- const base = offset + fullWords * 8;
112
- for (let i = 0; i < remaining; i++) {
113
- view.setUint8(base + i, Number(value & 0xFFn));
114
- value >>= 8n;
115
- }
116
- }
105
+ const fullWords = Math.floor(byteSize / 8);
106
+ for (let i = 0; i < fullWords; i++) {
107
+ view.setBigUint64(offset + i * 8, value & 0xffff_ffff_ffff_ffffn, true);
108
+ value >>= 64n;
109
+ }
110
+ const remaining = byteSize % 8;
111
+ if (remaining > 0) {
112
+ const base = offset + fullWords * 8;
113
+ for (let i = 0; i < remaining; i++) {
114
+ view.setUint8(base + i, Number(value & 0xffn));
115
+ value >>= 8n;
116
+ }
117
+ }
117
118
  }
118
119
 
119
120
  /** Read a signal value from the DataView. Always returns bigint. */
120
- function readSignal(
121
- view: DataView,
122
- sig: SignalLayout,
123
- ): bigint {
124
- if (sig.width <= 53) {
125
- return BigInt(readNumber(view, sig.offset, sig.width));
126
- }
127
- return readBigInt(view, sig.offset, sig.byteSize);
121
+ function readSignal(view: DataView, sig: SignalLayout): bigint {
122
+ if (sig.width <= 53) {
123
+ return BigInt(readNumber(view, sig.offset, sig.width));
124
+ }
125
+ return readBigInt(view, sig.offset, sig.byteSize);
128
126
  }
129
127
 
130
128
  /** Write a signal value to the DataView. Accepts bigint (number accepted for compat). */
131
129
  function writeSignal(
132
- view: DataView,
133
- sig: SignalLayout,
134
- value: bigint | number,
130
+ view: DataView,
131
+ sig: SignalLayout,
132
+ value: bigint | number,
135
133
  ): void {
136
- const bigVal = typeof value === "bigint" ? value : BigInt(value);
137
- if (sig.width <= 53) {
138
- writeNumber(view, sig.offset, sig.width, Number(bigVal));
139
- } else {
140
- writeBigInt(view, sig.offset, sig.byteSize, bigVal);
141
- }
134
+ const bigVal = typeof value === "bigint" ? value : BigInt(value);
135
+ if (sig.width <= 53) {
136
+ writeNumber(view, sig.offset, sig.width, Number(bigVal));
137
+ } else {
138
+ writeBigInt(view, sig.offset, sig.byteSize, bigVal);
139
+ }
142
140
  }
143
141
 
144
142
  /** Write a 4-state value (value + mask) to the DataView. */
145
143
  function writeFourState(
146
- view: DataView,
147
- sig: SignalLayout,
148
- fsv: FourStateValue,
144
+ view: DataView,
145
+ sig: SignalLayout,
146
+ fsv: FourStateValue,
149
147
  ): void {
150
- writeSignal(view, sig, fsv.value);
151
- // Mask is stored immediately after the value bytes
152
- const maskLayout: SignalLayout = {
153
- offset: sig.offset + sig.byteSize,
154
- width: sig.width,
155
- byteSize: sig.byteSize,
156
- is4state: false,
157
- direction: sig.direction,
158
- };
159
- writeSignal(view, maskLayout, fsv.mask);
148
+ writeSignal(view, sig, fsv.value);
149
+ // Mask is stored immediately after the value bytes
150
+ const maskLayout: SignalLayout = {
151
+ offset: sig.offset + sig.byteSize,
152
+ width: sig.width,
153
+ byteSize: sig.byteSize,
154
+ is4state: false,
155
+ direction: sig.direction,
156
+ };
157
+ writeSignal(view, maskLayout, fsv.mask);
160
158
  }
161
159
 
162
160
  /** Write all-X mask for a signal. */
163
161
  function writeAllX(view: DataView, sig: SignalLayout): void {
164
- // Value = 0, mask = all 1s
165
- writeSignal(view, sig, 0n);
166
- const allOnes = (1n << BigInt(sig.width)) - 1n;
167
- const maskLayout: SignalLayout = {
168
- offset: sig.offset + sig.byteSize,
169
- width: sig.width,
170
- byteSize: sig.byteSize,
171
- is4state: false,
172
- direction: sig.direction,
173
- };
174
- writeSignal(view, maskLayout, allOnes);
162
+ // Value = 0, mask = all 1s
163
+ writeSignal(view, sig, 0n);
164
+ const allOnes = (1n << BigInt(sig.width)) - 1n;
165
+ const maskLayout: SignalLayout = {
166
+ offset: sig.offset + sig.byteSize,
167
+ width: sig.width,
168
+ byteSize: sig.byteSize,
169
+ is4state: false,
170
+ direction: sig.direction,
171
+ };
172
+ writeSignal(view, maskLayout, allOnes);
175
173
  }
176
174
 
177
175
  // ---------------------------------------------------------------------------
@@ -189,86 +187,86 @@ function writeAllX(view: DataView, sig: SignalLayout): void {
189
187
  * @param hierarchy Optional hierarchy node for child instance access
190
188
  */
191
189
  export function createDut<P>(
192
- buffer: ArrayBuffer | SharedArrayBuffer,
193
- layout: Record<string, SignalLayout>,
194
- portDefs: Record<string, PortInfo>,
195
- handle: NativeHandle,
196
- state: DirtyState,
197
- hierarchy?: HierarchyNode,
190
+ buffer: ArrayBuffer | SharedArrayBuffer,
191
+ layout: Record<string, SignalLayout>,
192
+ portDefs: Record<string, PortInfo>,
193
+ handle: NativeHandle,
194
+ state: DirtyState,
195
+ hierarchy?: HierarchyNode,
198
196
  ): P {
199
- const view = new DataView(buffer);
200
- const obj = Object.create(null) as P;
201
-
202
- // Iterate portDefs (not layout) so that interface ports are discovered
203
- // even though their individual members are the ones that appear in layout.
204
- for (const [name, port] of Object.entries(portDefs)) {
205
- // Skip clock ports — they are controlled via tick()/addClock()
206
- if (port.type === "clock") continue;
207
-
208
- // Check for nested interface
209
- if (port.interface) {
210
- const nestedObj = createNestedDut(
211
- view,
212
- layout,
213
- port.interface,
214
- name,
215
- handle,
216
- state,
217
- );
218
- Object.defineProperty(obj, name, {
219
- value: nestedObj,
220
- enumerable: true,
221
- configurable: false,
222
- writable: false,
223
- });
224
- continue;
225
- }
226
-
227
- const sig = layout[name];
228
- if (!sig) continue;
229
-
230
- // Check for array port
231
- if (port.arrayDims && port.arrayDims.length > 0) {
232
- const arrayObj = createArrayDut(view, sig, port, handle, state);
233
- Object.defineProperty(obj, name, {
234
- value: arrayObj,
235
- enumerable: true,
236
- configurable: false,
237
- writable: false,
238
- });
239
- continue;
240
- }
241
-
242
- // Scalar port — define getter/setter
243
- defineSignalProperty(obj as object, name, view, sig, port, handle, state);
244
- }
245
-
246
- // Attach child instance accessors from hierarchy
247
- if (hierarchy) {
248
- for (const [childName, instances] of Object.entries(hierarchy.children)) {
249
- if (instances.length === 1) {
250
- const childDut = createChildDut(buffer, instances[0]!, handle, state);
251
- Object.defineProperty(obj, childName, {
252
- value: childDut,
253
- enumerable: true,
254
- configurable: false,
255
- writable: false,
256
- });
257
- } else if (instances.length > 1) {
258
- const childDuts = instances.map((inst) =>
259
- createChildDut(buffer, inst, handle, state),
260
- );
261
- Object.defineProperty(obj, childName, {
262
- value: childDuts,
263
- enumerable: true,
264
- configurable: false,
265
- writable: false,
266
- });
267
- }
268
- }
269
- }
270
-
271
- return obj;
197
+ const view = new DataView(buffer);
198
+ const obj = Object.create(null) as P;
199
+
200
+ // Iterate portDefs (not layout) so that interface ports are discovered
201
+ // even though their individual members are the ones that appear in layout.
202
+ for (const [name, port] of Object.entries(portDefs)) {
203
+ // Skip clock ports — they are controlled via tick()/addClock()
204
+ if (port.type === "clock") continue;
205
+
206
+ // Check for nested interface
207
+ if (port.interface) {
208
+ const nestedObj = createNestedDut(
209
+ view,
210
+ layout,
211
+ port.interface,
212
+ name,
213
+ handle,
214
+ state,
215
+ );
216
+ Object.defineProperty(obj, name, {
217
+ value: nestedObj,
218
+ enumerable: true,
219
+ configurable: false,
220
+ writable: false,
221
+ });
222
+ continue;
223
+ }
224
+
225
+ const sig = layout[name];
226
+ if (!sig) continue;
227
+
228
+ // Check for array port
229
+ if (port.arrayDims && port.arrayDims.length > 0) {
230
+ const arrayObj = createArrayDut(view, sig, port, handle, state);
231
+ Object.defineProperty(obj, name, {
232
+ value: arrayObj,
233
+ enumerable: true,
234
+ configurable: false,
235
+ writable: false,
236
+ });
237
+ continue;
238
+ }
239
+
240
+ // Scalar port — define getter/setter
241
+ defineSignalProperty(obj as object, name, view, sig, port, handle, state);
242
+ }
243
+
244
+ // Attach child instance accessors from hierarchy
245
+ if (hierarchy) {
246
+ for (const [childName, instances] of Object.entries(hierarchy.children)) {
247
+ if (instances.length === 1) {
248
+ const childDut = createChildDut(buffer, instances[0]!, handle, state);
249
+ Object.defineProperty(obj, childName, {
250
+ value: childDut,
251
+ enumerable: true,
252
+ configurable: false,
253
+ writable: false,
254
+ });
255
+ } else if (instances.length > 1) {
256
+ const childDuts = instances.map((inst) =>
257
+ createChildDut(buffer, inst, handle, state),
258
+ );
259
+ Object.defineProperty(obj, childName, {
260
+ value: childDuts,
261
+ enumerable: true,
262
+ configurable: false,
263
+ writable: false,
264
+ });
265
+ }
266
+ }
267
+ }
268
+
269
+ return obj;
272
270
  }
273
271
 
274
272
  /**
@@ -276,140 +274,143 @@ export function createDut<P>(
276
274
  * Recursively creates accessors for the child's signals and its own children.
277
275
  */
278
276
  export function createChildDut(
279
- buffer: ArrayBuffer | SharedArrayBuffer,
280
- hierarchy: HierarchyNode,
281
- handle: NativeHandle,
282
- state: DirtyState,
277
+ buffer: ArrayBuffer | SharedArrayBuffer,
278
+ hierarchy: HierarchyNode,
279
+ handle: NativeHandle,
280
+ state: DirtyState,
283
281
  ): object {
284
- const view = new DataView(buffer);
285
- const obj = Object.create(null);
286
-
287
- // Define signal properties for this child instance
288
- for (const [name, port] of Object.entries(hierarchy.ports)) {
289
- if (port.type === "clock") continue;
290
-
291
- // Handle nested interface port
292
- if (port.interface) {
293
- const nestedObj = createNestedDut(
294
- view,
295
- hierarchy.forDut,
296
- port.interface,
297
- name,
298
- handle,
299
- state,
300
- );
301
- Object.defineProperty(obj, name, {
302
- value: nestedObj,
303
- enumerable: true,
304
- configurable: false,
305
- writable: false,
306
- });
307
- continue;
308
- }
309
-
310
- const sig = hierarchy.forDut[name];
311
- if (!sig) continue;
312
-
313
- if (port.arrayDims && port.arrayDims.length > 0) {
314
- const arrayObj = createArrayDut(view, sig, port, handle, state);
315
- Object.defineProperty(obj, name, {
316
- value: arrayObj,
317
- enumerable: true,
318
- configurable: false,
319
- writable: false,
320
- });
321
- continue;
322
- }
323
-
324
- defineSignalProperty(obj, name, view, sig, port, handle, state);
325
- }
326
-
327
- // Recursively attach children
328
- for (const [childName, instances] of Object.entries(hierarchy.children)) {
329
- if (instances.length === 1) {
330
- const childDut = createChildDut(buffer, instances[0]!, handle, state);
331
- Object.defineProperty(obj, childName, {
332
- value: childDut,
333
- enumerable: true,
334
- configurable: false,
335
- writable: false,
336
- });
337
- } else if (instances.length > 1) {
338
- const childDuts = instances.map((inst) =>
339
- createChildDut(buffer, inst, handle, state),
340
- );
341
- Object.defineProperty(obj, childName, {
342
- value: childDuts,
343
- enumerable: true,
344
- configurable: false,
345
- writable: false,
346
- });
347
- }
348
- }
349
-
350
- return obj;
282
+ const view = new DataView(buffer);
283
+ const obj = Object.create(null);
284
+
285
+ // Define signal properties for this child instance
286
+ for (const [name, port] of Object.entries(hierarchy.ports)) {
287
+ if (port.type === "clock") continue;
288
+
289
+ // Handle nested interface port
290
+ if (port.interface) {
291
+ const nestedObj = createNestedDut(
292
+ view,
293
+ hierarchy.forDut,
294
+ port.interface,
295
+ name,
296
+ handle,
297
+ state,
298
+ );
299
+ Object.defineProperty(obj, name, {
300
+ value: nestedObj,
301
+ enumerable: true,
302
+ configurable: false,
303
+ writable: false,
304
+ });
305
+ continue;
306
+ }
307
+
308
+ const sig = hierarchy.forDut[name];
309
+ if (!sig) continue;
310
+
311
+ if (port.arrayDims && port.arrayDims.length > 0) {
312
+ const arrayObj = createArrayDut(view, sig, port, handle, state);
313
+ Object.defineProperty(obj, name, {
314
+ value: arrayObj,
315
+ enumerable: true,
316
+ configurable: false,
317
+ writable: false,
318
+ });
319
+ continue;
320
+ }
321
+
322
+ defineSignalProperty(obj, name, view, sig, port, handle, state);
323
+ }
324
+
325
+ // Recursively attach children
326
+ for (const [childName, instances] of Object.entries(hierarchy.children)) {
327
+ if (instances.length === 1) {
328
+ const childDut = createChildDut(buffer, instances[0]!, handle, state);
329
+ Object.defineProperty(obj, childName, {
330
+ value: childDut,
331
+ enumerable: true,
332
+ configurable: false,
333
+ writable: false,
334
+ });
335
+ } else if (instances.length > 1) {
336
+ const childDuts = instances.map((inst) =>
337
+ createChildDut(buffer, inst, handle, state),
338
+ );
339
+ Object.defineProperty(obj, childName, {
340
+ value: childDuts,
341
+ enumerable: true,
342
+ configurable: false,
343
+ writable: false,
344
+ });
345
+ }
346
+ }
347
+
348
+ return obj;
351
349
  }
352
350
 
353
351
  /** Define a single scalar signal property on the target object. */
354
352
  function defineSignalProperty(
355
- target: object,
356
- name: string,
357
- view: DataView,
358
- sig: SignalLayout,
359
- port: PortInfo | undefined,
360
- handle: NativeHandle,
361
- state: DirtyState,
353
+ target: object,
354
+ name: string,
355
+ view: DataView,
356
+ sig: SignalLayout,
357
+ port: PortInfo | undefined,
358
+ handle: NativeHandle,
359
+ state: DirtyState,
362
360
  ): void {
363
- const isOutput = port?.direction === "output";
364
- const isInput = port?.direction === "input";
365
-
366
- Object.defineProperty(target, name, {
367
- get(): bigint {
368
- // Output reads: lazy evalComb if dirty
369
- if (state.dirty && !isInput) {
370
- handle.evalComb();
371
- state.dirty = false;
372
- }
373
- return readSignal(view, sig);
374
- },
375
-
376
- set(value: bigint | number | symbol | FourStateValue) {
377
- if (isOutput) {
378
- throw new Error(`Cannot write to output port '${name}'`);
379
- }
380
-
381
- if (value === Symbol.for("veryl:X")) {
382
- if (!sig.is4state) {
383
- throw new Error(`Port '${name}' is not 4-state; cannot assign X`);
384
- }
385
- writeAllX(view, sig);
386
- } else if (isFourStateValue(value)) {
387
- if (!sig.is4state) {
388
- throw new Error(`Port '${name}' is not 4-state; cannot assign FourState`);
389
- }
390
- writeFourState(view, sig, value);
391
- } else {
392
- const bigVal = typeof value === "bigint" ? value : BigInt(value as number);
393
- writeSignal(view, sig, bigVal);
394
- // Clear mask when writing a defined value to a 4-state signal
395
- if (sig.is4state) {
396
- const maskLayout: SignalLayout = {
397
- offset: sig.offset + sig.byteSize,
398
- width: sig.width,
399
- byteSize: sig.byteSize,
400
- is4state: false,
401
- direction: sig.direction,
402
- };
403
- writeSignal(view, maskLayout, 0n);
404
- }
405
- }
406
-
407
- state.dirty = true;
408
- },
409
-
410
- enumerable: true,
411
- configurable: false,
412
- });
361
+ const isOutput = port?.direction === "output";
362
+ const isInput = port?.direction === "input";
363
+
364
+ Object.defineProperty(target, name, {
365
+ get(): bigint {
366
+ // Output reads: lazy evalComb if dirty
367
+ if (state.dirty && !isInput) {
368
+ handle.evalComb();
369
+ state.dirty = false;
370
+ }
371
+ return readSignal(view, sig);
372
+ },
373
+
374
+ set(value: bigint | number | symbol | FourStateValue) {
375
+ if (isOutput) {
376
+ throw new Error(`Cannot write to output port '${name}'`);
377
+ }
378
+
379
+ if (value === Symbol.for("veryl:X")) {
380
+ if (!sig.is4state) {
381
+ throw new Error(`Port '${name}' is not 4-state; cannot assign X`);
382
+ }
383
+ writeAllX(view, sig);
384
+ } else if (isFourStateValue(value)) {
385
+ if (!sig.is4state) {
386
+ throw new Error(
387
+ `Port '${name}' is not 4-state; cannot assign FourState`,
388
+ );
389
+ }
390
+ writeFourState(view, sig, value);
391
+ } else {
392
+ const bigVal =
393
+ typeof value === "bigint" ? value : BigInt(value as number);
394
+ writeSignal(view, sig, bigVal);
395
+ // Clear mask when writing a defined value to a 4-state signal
396
+ if (sig.is4state) {
397
+ const maskLayout: SignalLayout = {
398
+ offset: sig.offset + sig.byteSize,
399
+ width: sig.width,
400
+ byteSize: sig.byteSize,
401
+ is4state: false,
402
+ direction: sig.direction,
403
+ };
404
+ writeSignal(view, maskLayout, 0n);
405
+ }
406
+ }
407
+
408
+ state.dirty = true;
409
+ },
410
+
411
+ enumerable: true,
412
+ configurable: false,
413
+ });
413
414
  }
414
415
 
415
416
  // ---------------------------------------------------------------------------
@@ -417,49 +418,57 @@ function defineSignalProperty(
417
418
  // ---------------------------------------------------------------------------
418
419
 
419
420
  function createNestedDut(
420
- view: DataView,
421
- layout: Record<string, SignalLayout>,
422
- members: Record<string, PortInfo>,
423
- prefix: string,
424
- handle: NativeHandle,
425
- state: DirtyState,
421
+ view: DataView,
422
+ layout: Record<string, SignalLayout>,
423
+ members: Record<string, PortInfo>,
424
+ prefix: string,
425
+ handle: NativeHandle,
426
+ state: DirtyState,
426
427
  ): object {
427
- const obj = Object.create(null);
428
-
429
- for (const [memberName, memberPort] of Object.entries(members)) {
430
- const qualifiedName = `${prefix}.${memberName}`;
431
- const sig = layout[qualifiedName];
432
- if (!sig) continue;
433
-
434
- if (memberPort.interface) {
435
- const nested = createNestedDut(
436
- view,
437
- layout,
438
- memberPort.interface,
439
- qualifiedName,
440
- handle,
441
- state,
442
- );
443
- Object.defineProperty(obj, memberName, {
444
- value: nested,
445
- enumerable: true,
446
- configurable: false,
447
- writable: false,
448
- });
449
- } else if (memberPort.arrayDims && memberPort.arrayDims.length > 0) {
450
- const arrayObj = createArrayDut(view, sig, memberPort, handle, state);
451
- Object.defineProperty(obj, memberName, {
452
- value: arrayObj,
453
- enumerable: true,
454
- configurable: false,
455
- writable: false,
456
- });
457
- } else {
458
- defineSignalProperty(obj, memberName, view, sig, memberPort, handle, state);
459
- }
460
- }
461
-
462
- return obj;
428
+ const obj = Object.create(null);
429
+
430
+ for (const [memberName, memberPort] of Object.entries(members)) {
431
+ const qualifiedName = `${prefix}.${memberName}`;
432
+ const sig = layout[qualifiedName];
433
+ if (!sig) continue;
434
+
435
+ if (memberPort.interface) {
436
+ const nested = createNestedDut(
437
+ view,
438
+ layout,
439
+ memberPort.interface,
440
+ qualifiedName,
441
+ handle,
442
+ state,
443
+ );
444
+ Object.defineProperty(obj, memberName, {
445
+ value: nested,
446
+ enumerable: true,
447
+ configurable: false,
448
+ writable: false,
449
+ });
450
+ } else if (memberPort.arrayDims && memberPort.arrayDims.length > 0) {
451
+ const arrayObj = createArrayDut(view, sig, memberPort, handle, state);
452
+ Object.defineProperty(obj, memberName, {
453
+ value: arrayObj,
454
+ enumerable: true,
455
+ configurable: false,
456
+ writable: false,
457
+ });
458
+ } else {
459
+ defineSignalProperty(
460
+ obj,
461
+ memberName,
462
+ view,
463
+ sig,
464
+ memberPort,
465
+ handle,
466
+ state,
467
+ );
468
+ }
469
+ }
470
+
471
+ return obj;
463
472
  }
464
473
 
465
474
  // ---------------------------------------------------------------------------
@@ -474,22 +483,22 @@ function createNestedDut(
474
483
  * Used when elementWidth < 8 (sub-byte elements).
475
484
  */
476
485
  function readBitPackedElement(
477
- view: DataView,
478
- baseOffset: number,
479
- elementWidth: number,
480
- i: number,
486
+ view: DataView,
487
+ baseOffset: number,
488
+ elementWidth: number,
489
+ i: number,
481
490
  ): number {
482
- const bitStart = i * elementWidth;
483
- const byteStart = baseOffset + (bitStart >> 3);
484
- const bitShift = bitStart & 7;
485
- const mask = (1 << elementWidth) - 1;
486
- if (bitShift + elementWidth <= 8) {
487
- return (view.getUint8(byteStart) >> bitShift) & mask;
488
- }
489
- // Spans two bytes
490
- const lo = view.getUint8(byteStart);
491
- const hi = view.getUint8(byteStart + 1);
492
- return ((lo | (hi << 8)) >> bitShift) & mask;
491
+ const bitStart = i * elementWidth;
492
+ const byteStart = baseOffset + (bitStart >> 3);
493
+ const bitShift = bitStart & 7;
494
+ const mask = (1 << elementWidth) - 1;
495
+ if (bitShift + elementWidth <= 8) {
496
+ return (view.getUint8(byteStart) >> bitShift) & mask;
497
+ }
498
+ // Spans two bytes
499
+ const lo = view.getUint8(byteStart);
500
+ const hi = view.getUint8(byteStart + 1);
501
+ return ((lo | (hi << 8)) >> bitShift) & mask;
493
502
  }
494
503
 
495
504
  /**
@@ -498,149 +507,192 @@ function readBitPackedElement(
498
507
  * Used when elementWidth < 8 (sub-byte elements).
499
508
  */
500
509
  function writeBitPackedElement(
501
- view: DataView,
502
- baseOffset: number,
503
- elementWidth: number,
504
- i: number,
505
- value: number,
510
+ view: DataView,
511
+ baseOffset: number,
512
+ elementWidth: number,
513
+ i: number,
514
+ value: number,
506
515
  ): void {
507
- const bitStart = i * elementWidth;
508
- const byteStart = baseOffset + (bitStart >> 3);
509
- const bitShift = bitStart & 7;
510
- const mask = (1 << elementWidth) - 1;
511
- const maskedValue = value & mask;
512
-
513
- const lo = view.getUint8(byteStart);
514
- view.setUint8(byteStart, (lo & ~((mask << bitShift) & 0xff)) | ((maskedValue << bitShift) & 0xff));
515
-
516
- if (bitShift + elementWidth > 8) {
517
- const bitsInLo = 8 - bitShift;
518
- const hi = view.getUint8(byteStart + 1);
519
- const hiMask = mask >> bitsInLo;
520
- view.setUint8(byteStart + 1, (hi & ~hiMask) | (maskedValue >> bitsInLo));
521
- }
516
+ const bitStart = i * elementWidth;
517
+ const byteStart = baseOffset + (bitStart >> 3);
518
+ const bitShift = bitStart & 7;
519
+ const mask = (1 << elementWidth) - 1;
520
+ const maskedValue = value & mask;
521
+
522
+ const lo = view.getUint8(byteStart);
523
+ view.setUint8(
524
+ byteStart,
525
+ (lo & ~((mask << bitShift) & 0xff)) | ((maskedValue << bitShift) & 0xff),
526
+ );
527
+
528
+ if (bitShift + elementWidth > 8) {
529
+ const bitsInLo = 8 - bitShift;
530
+ const hi = view.getUint8(byteStart + 1);
531
+ const hiMask = mask >> bitsInLo;
532
+ view.setUint8(byteStart + 1, (hi & ~hiMask) | (maskedValue >> bitsInLo));
533
+ }
522
534
  }
523
535
 
524
536
  function createArrayDut(
525
- view: DataView,
526
- baseSig: SignalLayout,
527
- port: PortInfo,
528
- handle: NativeHandle,
529
- state: DirtyState,
537
+ view: DataView,
538
+ baseSig: SignalLayout,
539
+ port: PortInfo,
540
+ handle: NativeHandle,
541
+ state: DirtyState,
530
542
  ): object {
531
- const dims = port.arrayDims!;
532
- const elementWidth = port.width;
533
- const totalElements = dims.reduce((a, b) => a * b, 1);
534
- const isOutput = port.direction === "output";
535
- const isInput = port.direction === "input";
536
- const baseOffset = baseSig.offset;
537
- const is4state = baseSig.is4state;
538
-
539
- if (elementWidth < 8) {
540
- // Sub-byte elements: the JIT stores them bit-packed.
541
- // Element i occupies bits [i*W .. (i+1)*W - 1] starting at baseOffset.
542
- // For 4-state signals the mask region starts immediately after the value bytes.
543
- const totalValueBytes = Math.ceil(totalElements * elementWidth / 8);
544
- const maskBase = baseOffset + totalValueBytes;
545
-
546
- return {
547
- length: totalElements,
548
-
549
- at(i: number): bigint {
550
- if (state.dirty && !isInput) {
551
- handle.evalComb();
552
- state.dirty = false;
553
- }
554
- return BigInt(readBitPackedElement(view, baseOffset, elementWidth, i));
555
- },
556
-
557
- set(i: number, value: bigint | number | symbol | FourStateValue): void {
558
- if (isOutput) {
559
- throw new Error("Cannot write to output array port");
560
- }
561
- if (value === Symbol.for("veryl:X")) {
562
- if (!is4state) {
563
- throw new Error("Array port is not 4-state; cannot assign X");
564
- }
565
- writeBitPackedElement(view, baseOffset, elementWidth, i, 0);
566
- writeBitPackedElement(view, maskBase, elementWidth, i, (1 << elementWidth) - 1);
567
- } else if (isFourStateValue(value)) {
568
- if (!is4state) {
569
- throw new Error("Array port is not 4-state; cannot assign FourState");
570
- }
571
- writeBitPackedElement(view, baseOffset, elementWidth, i, Number(value.value));
572
- writeBitPackedElement(view, maskBase, elementWidth, i, Number(value.mask));
573
- } else {
574
- const bigVal = typeof value === "bigint" ? value : BigInt(value as number);
575
- writeBitPackedElement(view, baseOffset, elementWidth, i, Number(bigVal));
576
- if (is4state) {
577
- writeBitPackedElement(view, maskBase, elementWidth, i, 0);
578
- }
579
- }
580
- state.dirty = true;
581
- },
582
- };
583
- }
584
-
585
- // elementWidth >= 8: byte-aligned stride.
586
- const elementByteSize = Math.ceil(elementWidth / 8);
587
-
588
- return {
589
- length: totalElements,
590
-
591
- at(i: number): bigint {
592
- if (state.dirty && !isInput) {
593
- handle.evalComb();
594
- state.dirty = false;
595
- }
596
- const offset = baseOffset + i * elementByteSize;
597
- if (elementWidth <= 53) {
598
- return BigInt(readNumber(view, offset, elementWidth));
599
- }
600
- return readBigInt(view, offset, elementByteSize);
601
- },
602
-
603
- set(i: number, value: bigint | number | symbol | FourStateValue): void {
604
- if (isOutput) {
605
- throw new Error("Cannot write to output array port");
606
- }
607
- const offset = baseOffset + i * elementByteSize;
608
- if (value === Symbol.for("veryl:X")) {
609
- if (!is4state) {
610
- throw new Error("Array port is not 4-state; cannot assign X");
611
- }
612
- const elemSig: SignalLayout = {
613
- offset, width: elementWidth, byteSize: elementByteSize,
614
- is4state, direction: baseSig.direction,
615
- };
616
- writeAllX(view, elemSig);
617
- } else if (isFourStateValue(value)) {
618
- if (!is4state) {
619
- throw new Error("Array port is not 4-state; cannot assign FourState");
620
- }
621
- const elemSig: SignalLayout = {
622
- offset, width: elementWidth, byteSize: elementByteSize,
623
- is4state, direction: baseSig.direction,
624
- };
625
- writeFourState(view, elemSig, value);
626
- } else {
627
- const bigVal = typeof value === "bigint" ? value : BigInt(value as number);
628
- const elemSig: SignalLayout = {
629
- offset, width: elementWidth, byteSize: elementByteSize,
630
- is4state, direction: baseSig.direction,
631
- };
632
- writeSignal(view, elemSig, bigVal);
633
- if (is4state) {
634
- const maskSig: SignalLayout = {
635
- offset: offset + elementByteSize, width: elementWidth,
636
- byteSize: elementByteSize, is4state: false, direction: baseSig.direction,
637
- };
638
- writeSignal(view, maskSig, 0n);
639
- }
640
- }
641
- state.dirty = true;
642
- },
643
- };
543
+ const dims = port.arrayDims!;
544
+ const elementWidth = port.width;
545
+ const totalElements = dims.reduce((a, b) => a * b, 1);
546
+ const isOutput = port.direction === "output";
547
+ const isInput = port.direction === "input";
548
+ const baseOffset = baseSig.offset;
549
+ const is4state = baseSig.is4state;
550
+
551
+ if (elementWidth < 8) {
552
+ // Sub-byte elements: the JIT stores them bit-packed.
553
+ // Element i occupies bits [i*W .. (i+1)*W - 1] starting at baseOffset.
554
+ // For 4-state signals the mask region starts immediately after the value bytes.
555
+ const totalValueBytes = Math.ceil((totalElements * elementWidth) / 8);
556
+ const maskBase = baseOffset + totalValueBytes;
557
+
558
+ return {
559
+ length: totalElements,
560
+
561
+ at(i: number): bigint {
562
+ if (state.dirty && !isInput) {
563
+ handle.evalComb();
564
+ state.dirty = false;
565
+ }
566
+ return BigInt(readBitPackedElement(view, baseOffset, elementWidth, i));
567
+ },
568
+
569
+ set(i: number, value: bigint | number | symbol | FourStateValue): void {
570
+ if (isOutput) {
571
+ throw new Error("Cannot write to output array port");
572
+ }
573
+ if (value === Symbol.for("veryl:X")) {
574
+ if (!is4state) {
575
+ throw new Error("Array port is not 4-state; cannot assign X");
576
+ }
577
+ writeBitPackedElement(view, baseOffset, elementWidth, i, 0);
578
+ writeBitPackedElement(
579
+ view,
580
+ maskBase,
581
+ elementWidth,
582
+ i,
583
+ (1 << elementWidth) - 1,
584
+ );
585
+ } else if (isFourStateValue(value)) {
586
+ if (!is4state) {
587
+ throw new Error(
588
+ "Array port is not 4-state; cannot assign FourState",
589
+ );
590
+ }
591
+ writeBitPackedElement(
592
+ view,
593
+ baseOffset,
594
+ elementWidth,
595
+ i,
596
+ Number(value.value),
597
+ );
598
+ writeBitPackedElement(
599
+ view,
600
+ maskBase,
601
+ elementWidth,
602
+ i,
603
+ Number(value.mask),
604
+ );
605
+ } else {
606
+ const bigVal =
607
+ typeof value === "bigint" ? value : BigInt(value as number);
608
+ writeBitPackedElement(
609
+ view,
610
+ baseOffset,
611
+ elementWidth,
612
+ i,
613
+ Number(bigVal),
614
+ );
615
+ if (is4state) {
616
+ writeBitPackedElement(view, maskBase, elementWidth, i, 0);
617
+ }
618
+ }
619
+ state.dirty = true;
620
+ },
621
+ };
622
+ }
623
+
624
+ // elementWidth >= 8: byte-aligned stride.
625
+ const elementByteSize = Math.ceil(elementWidth / 8);
626
+
627
+ return {
628
+ length: totalElements,
629
+
630
+ at(i: number): bigint {
631
+ if (state.dirty && !isInput) {
632
+ handle.evalComb();
633
+ state.dirty = false;
634
+ }
635
+ const offset = baseOffset + i * elementByteSize;
636
+ if (elementWidth <= 53) {
637
+ return BigInt(readNumber(view, offset, elementWidth));
638
+ }
639
+ return readBigInt(view, offset, elementByteSize);
640
+ },
641
+
642
+ set(i: number, value: bigint | number | symbol | FourStateValue): void {
643
+ if (isOutput) {
644
+ throw new Error("Cannot write to output array port");
645
+ }
646
+ const offset = baseOffset + i * elementByteSize;
647
+ if (value === Symbol.for("veryl:X")) {
648
+ if (!is4state) {
649
+ throw new Error("Array port is not 4-state; cannot assign X");
650
+ }
651
+ const elemSig: SignalLayout = {
652
+ offset,
653
+ width: elementWidth,
654
+ byteSize: elementByteSize,
655
+ is4state,
656
+ direction: baseSig.direction,
657
+ };
658
+ writeAllX(view, elemSig);
659
+ } else if (isFourStateValue(value)) {
660
+ if (!is4state) {
661
+ throw new Error("Array port is not 4-state; cannot assign FourState");
662
+ }
663
+ const elemSig: SignalLayout = {
664
+ offset,
665
+ width: elementWidth,
666
+ byteSize: elementByteSize,
667
+ is4state,
668
+ direction: baseSig.direction,
669
+ };
670
+ writeFourState(view, elemSig, value);
671
+ } else {
672
+ const bigVal =
673
+ typeof value === "bigint" ? value : BigInt(value as number);
674
+ const elemSig: SignalLayout = {
675
+ offset,
676
+ width: elementWidth,
677
+ byteSize: elementByteSize,
678
+ is4state,
679
+ direction: baseSig.direction,
680
+ };
681
+ writeSignal(view, elemSig, bigVal);
682
+ if (is4state) {
683
+ const maskSig: SignalLayout = {
684
+ offset: offset + elementByteSize,
685
+ width: elementWidth,
686
+ byteSize: elementByteSize,
687
+ is4state: false,
688
+ direction: baseSig.direction,
689
+ };
690
+ writeSignal(view, maskSig, 0n);
691
+ }
692
+ }
693
+ state.dirty = true;
694
+ },
695
+ };
644
696
  }
645
697
 
646
698
  // ---------------------------------------------------------------------------
@@ -652,21 +704,21 @@ function createArrayDut(
652
704
  * Mask bits set to 1 indicate X.
653
705
  */
654
706
  export function readFourState(
655
- buffer: ArrayBuffer | SharedArrayBuffer,
656
- sig: SignalLayout,
707
+ buffer: ArrayBuffer | SharedArrayBuffer,
708
+ sig: SignalLayout,
657
709
  ): [value: bigint, mask: bigint] {
658
- if (!sig.is4state) {
659
- throw new Error("Signal is not 4-state");
660
- }
661
- const view = new DataView(buffer);
662
- const value = readSignal(view, sig);
663
- const maskSig: SignalLayout = {
664
- offset: sig.offset + sig.byteSize,
665
- width: sig.width,
666
- byteSize: sig.byteSize,
667
- is4state: false,
668
- direction: sig.direction,
669
- };
670
- const mask = readSignal(view, maskSig);
671
- return [value, mask];
710
+ if (!sig.is4state) {
711
+ throw new Error("Signal is not 4-state");
712
+ }
713
+ const view = new DataView(buffer);
714
+ const value = readSignal(view, sig);
715
+ const maskSig: SignalLayout = {
716
+ offset: sig.offset + sig.byteSize,
717
+ width: sig.width,
718
+ byteSize: sig.byteSize,
719
+ is4state: false,
720
+ direction: sig.direction,
721
+ };
722
+ const mask = readSignal(view, maskSig);
723
+ return [value, mask];
672
724
  }