@celox-sim/celox 0.0.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dut.d.ts +35 -0
- package/dist/dut.d.ts.map +1 -0
- package/dist/dut.js +342 -0
- package/dist/dut.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/napi-bridge.d.ts +7 -0
- package/dist/napi-bridge.d.ts.map +1 -0
- package/dist/napi-bridge.js +7 -0
- package/dist/napi-bridge.js.map +1 -0
- package/dist/napi-helpers.d.ts +104 -0
- package/dist/napi-helpers.d.ts.map +1 -0
- package/dist/napi-helpers.js +207 -0
- package/dist/napi-helpers.js.map +1 -0
- package/dist/simulation.d.ts +111 -0
- package/dist/simulation.d.ts.map +1 -0
- package/dist/simulation.js +187 -0
- package/dist/simulation.js.map +1 -0
- package/dist/simulator.d.ts +92 -0
- package/dist/simulator.d.ts.map +1 -0
- package/dist/simulator.js +171 -0
- package/dist/simulator.js.map +1 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -7
- package/src/dut.test.ts +534 -0
- package/src/dut.ts +449 -0
- package/src/e2e.bench.ts +240 -0
- package/src/e2e.test.ts +965 -0
- package/src/index.ts +57 -0
- package/src/matchers.ts +154 -0
- package/src/napi-bridge.ts +13 -0
- package/src/napi-helpers.ts +326 -0
- package/src/simulation.test.ts +185 -0
- package/src/simulation.ts +273 -0
- package/src/simulator.test.ts +191 -0
- package/src/simulator.ts +266 -0
- package/src/types.ts +164 -0
- package/README.md +0 -45
package/src/dut.ts
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUT (Device Under Test) accessor factory.
|
|
3
|
+
*
|
|
4
|
+
* Builds a plain object with Object.defineProperty getter/setters that
|
|
5
|
+
* read and write directly via DataView on a SharedArrayBuffer.
|
|
6
|
+
* No Proxy is used — every port becomes a concrete property whose
|
|
7
|
+
* accessors are fully visible to V8 inline caches.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
NativeHandle,
|
|
12
|
+
PortInfo,
|
|
13
|
+
SignalLayout,
|
|
14
|
+
FourStateValue,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
import { isFourStateValue } from "./types.js";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Internal dirty-tracking state shared between DUT and Simulator/Simulation
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mutable state shared between the DUT accessor and its owning
|
|
24
|
+
* Simulator/Simulation instance. The Simulator clears `dirty` after
|
|
25
|
+
* tick()/runUntil(); the DUT sets it on any input write and checks it
|
|
26
|
+
* before any output read.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export interface DirtyState {
|
|
30
|
+
dirty: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// DataView helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Read an unsigned integer of the given byte-size (little-endian), masked to width. */
|
|
38
|
+
function readNumber(view: DataView, offset: number, width: number): number {
|
|
39
|
+
if (width <= 8) {
|
|
40
|
+
const raw = view.getUint8(offset);
|
|
41
|
+
return width === 8 ? raw : raw & ((1 << width) - 1);
|
|
42
|
+
}
|
|
43
|
+
if (width <= 16) {
|
|
44
|
+
const raw = view.getUint16(offset, true);
|
|
45
|
+
return width === 16 ? raw : raw & ((1 << width) - 1);
|
|
46
|
+
}
|
|
47
|
+
if (width <= 32) {
|
|
48
|
+
const raw = view.getUint32(offset, true);
|
|
49
|
+
return width === 32 ? raw : (raw & ((1 << width) - 1)) >>> 0;
|
|
50
|
+
}
|
|
51
|
+
// 33..53 bits — fits safely in a JS number
|
|
52
|
+
const lo = view.getUint32(offset, true);
|
|
53
|
+
const hi = view.getUint32(offset + 4, true) & ((1 << (width - 32)) - 1);
|
|
54
|
+
return lo + hi * 0x1_0000_0000;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Write an unsigned integer of the given byte-size (little-endian). */
|
|
58
|
+
function writeNumber(
|
|
59
|
+
view: DataView,
|
|
60
|
+
offset: number,
|
|
61
|
+
width: number,
|
|
62
|
+
value: number,
|
|
63
|
+
): void {
|
|
64
|
+
if (width <= 8) {
|
|
65
|
+
view.setUint8(offset, value & ((1 << width) - 1));
|
|
66
|
+
} else if (width <= 16) {
|
|
67
|
+
view.setUint16(offset, value & ((1 << width) - 1), true);
|
|
68
|
+
} else if (width <= 32) {
|
|
69
|
+
view.setUint32(offset, value >>> 0, true);
|
|
70
|
+
} else {
|
|
71
|
+
// 33..53 bits
|
|
72
|
+
view.setUint32(offset, value >>> 0, true);
|
|
73
|
+
view.setUint32(offset + 4, Math.floor(value / 0x1_0000_0000) >>> 0, true);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Read a wide value (≥ 54 bits) as BigInt, little-endian. */
|
|
78
|
+
function readBigInt(view: DataView, offset: number, byteSize: number): bigint {
|
|
79
|
+
let result = 0n;
|
|
80
|
+
// Read 8 bytes at a time, then remaining bytes
|
|
81
|
+
const fullWords = Math.floor(byteSize / 8);
|
|
82
|
+
for (let i = 0; i < fullWords; i++) {
|
|
83
|
+
const word = view.getBigUint64(offset + i * 8, true);
|
|
84
|
+
result |= word << BigInt(i * 64);
|
|
85
|
+
}
|
|
86
|
+
const remaining = byteSize % 8;
|
|
87
|
+
if (remaining > 0) {
|
|
88
|
+
const base = offset + fullWords * 8;
|
|
89
|
+
for (let i = 0; i < remaining; i++) {
|
|
90
|
+
result |= BigInt(view.getUint8(base + i)) << BigInt(fullWords * 64 + i * 8);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Write a wide value (≥ 54 bits) as BigInt, little-endian. */
|
|
97
|
+
function writeBigInt(
|
|
98
|
+
view: DataView,
|
|
99
|
+
offset: number,
|
|
100
|
+
byteSize: number,
|
|
101
|
+
value: bigint,
|
|
102
|
+
): void {
|
|
103
|
+
const fullWords = Math.floor(byteSize / 8);
|
|
104
|
+
for (let i = 0; i < fullWords; i++) {
|
|
105
|
+
view.setBigUint64(offset + i * 8, value & 0xFFFF_FFFF_FFFF_FFFFn, true);
|
|
106
|
+
value >>= 64n;
|
|
107
|
+
}
|
|
108
|
+
const remaining = byteSize % 8;
|
|
109
|
+
if (remaining > 0) {
|
|
110
|
+
const base = offset + fullWords * 8;
|
|
111
|
+
for (let i = 0; i < remaining; i++) {
|
|
112
|
+
view.setUint8(base + i, Number(value & 0xFFn));
|
|
113
|
+
value >>= 8n;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Read a signal value from the DataView. Returns number or bigint. */
|
|
119
|
+
function readSignal(
|
|
120
|
+
view: DataView,
|
|
121
|
+
sig: SignalLayout,
|
|
122
|
+
): number | bigint {
|
|
123
|
+
if (sig.width <= 53) {
|
|
124
|
+
return readNumber(view, sig.offset, sig.width);
|
|
125
|
+
}
|
|
126
|
+
return readBigInt(view, sig.offset, sig.byteSize);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Write a signal value to the DataView. Accepts number or bigint. */
|
|
130
|
+
function writeSignal(
|
|
131
|
+
view: DataView,
|
|
132
|
+
sig: SignalLayout,
|
|
133
|
+
value: number | bigint,
|
|
134
|
+
): void {
|
|
135
|
+
if (sig.width <= 53 && typeof value === "number") {
|
|
136
|
+
writeNumber(view, sig.offset, sig.width, value);
|
|
137
|
+
} else {
|
|
138
|
+
const bigVal = typeof value === "bigint" ? value : BigInt(value);
|
|
139
|
+
writeBigInt(view, sig.offset, sig.byteSize, bigVal);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Write a 4-state value (value + mask) to the DataView. */
|
|
144
|
+
function writeFourState(
|
|
145
|
+
view: DataView,
|
|
146
|
+
sig: SignalLayout,
|
|
147
|
+
fsv: FourStateValue,
|
|
148
|
+
): void {
|
|
149
|
+
writeSignal(view, sig, fsv.value);
|
|
150
|
+
// Mask is stored immediately after the value bytes
|
|
151
|
+
const maskLayout: SignalLayout = {
|
|
152
|
+
offset: sig.offset + sig.byteSize,
|
|
153
|
+
width: sig.width,
|
|
154
|
+
byteSize: sig.byteSize,
|
|
155
|
+
is4state: false,
|
|
156
|
+
direction: sig.direction,
|
|
157
|
+
};
|
|
158
|
+
writeSignal(view, maskLayout, fsv.mask);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Write all-X mask for a signal. */
|
|
162
|
+
function writeAllX(view: DataView, sig: SignalLayout): void {
|
|
163
|
+
// Value = 0, mask = all 1s
|
|
164
|
+
writeSignal(view, sig, sig.width <= 53 ? 0 : 0n);
|
|
165
|
+
const allOnes =
|
|
166
|
+
sig.width <= 53
|
|
167
|
+
? (sig.width === 53 ? Number.MAX_SAFE_INTEGER : (1 << sig.width) - 1)
|
|
168
|
+
: (1n << BigInt(sig.width)) - 1n;
|
|
169
|
+
const maskLayout: SignalLayout = {
|
|
170
|
+
offset: sig.offset + sig.byteSize,
|
|
171
|
+
width: sig.width,
|
|
172
|
+
byteSize: sig.byteSize,
|
|
173
|
+
is4state: false,
|
|
174
|
+
direction: sig.direction,
|
|
175
|
+
};
|
|
176
|
+
writeSignal(view, maskLayout, allOnes);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// DUT factory
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create a DUT accessor object with defineProperty-based getters/setters.
|
|
185
|
+
*
|
|
186
|
+
* @param buffer SharedArrayBuffer from NAPI create()
|
|
187
|
+
* @param layout Per-signal byte layout within the buffer
|
|
188
|
+
* @param portDefs Port metadata from the ModuleDefinition
|
|
189
|
+
* @param handle Native handle (for evalComb calls)
|
|
190
|
+
* @param state Shared dirty-tracking state
|
|
191
|
+
*/
|
|
192
|
+
export function createDut<P>(
|
|
193
|
+
buffer: ArrayBuffer | SharedArrayBuffer,
|
|
194
|
+
layout: Record<string, SignalLayout>,
|
|
195
|
+
portDefs: Record<string, PortInfo>,
|
|
196
|
+
handle: NativeHandle,
|
|
197
|
+
state: DirtyState,
|
|
198
|
+
): 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
|
+
return obj;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Define a single scalar signal property on the target object. */
|
|
250
|
+
function defineSignalProperty(
|
|
251
|
+
target: object,
|
|
252
|
+
name: string,
|
|
253
|
+
view: DataView,
|
|
254
|
+
sig: SignalLayout,
|
|
255
|
+
port: PortInfo | undefined,
|
|
256
|
+
handle: NativeHandle,
|
|
257
|
+
state: DirtyState,
|
|
258
|
+
): void {
|
|
259
|
+
const isOutput = port?.direction === "output";
|
|
260
|
+
const isInput = port?.direction === "input";
|
|
261
|
+
|
|
262
|
+
Object.defineProperty(target, name, {
|
|
263
|
+
get(): number | bigint {
|
|
264
|
+
// Output reads: lazy evalComb if dirty
|
|
265
|
+
if (state.dirty && !isInput) {
|
|
266
|
+
handle.evalComb();
|
|
267
|
+
state.dirty = false;
|
|
268
|
+
}
|
|
269
|
+
return readSignal(view, sig);
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
set(value: number | bigint | symbol | FourStateValue) {
|
|
273
|
+
if (isOutput) {
|
|
274
|
+
throw new Error(`Cannot write to output port '${name}'`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (value === Symbol.for("veryl:X")) {
|
|
278
|
+
if (!sig.is4state) {
|
|
279
|
+
throw new Error(`Port '${name}' is not 4-state; cannot assign X`);
|
|
280
|
+
}
|
|
281
|
+
writeAllX(view, sig);
|
|
282
|
+
} else if (isFourStateValue(value)) {
|
|
283
|
+
if (!sig.is4state) {
|
|
284
|
+
throw new Error(`Port '${name}' is not 4-state; cannot assign FourState`);
|
|
285
|
+
}
|
|
286
|
+
writeFourState(view, sig, value);
|
|
287
|
+
} else {
|
|
288
|
+
writeSignal(view, sig, value as number | bigint);
|
|
289
|
+
// Clear mask when writing a defined value to a 4-state signal
|
|
290
|
+
if (sig.is4state) {
|
|
291
|
+
const maskLayout: SignalLayout = {
|
|
292
|
+
offset: sig.offset + sig.byteSize,
|
|
293
|
+
width: sig.width,
|
|
294
|
+
byteSize: sig.byteSize,
|
|
295
|
+
is4state: false,
|
|
296
|
+
direction: sig.direction,
|
|
297
|
+
};
|
|
298
|
+
writeSignal(view, maskLayout, sig.width <= 53 ? 0 : 0n);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
state.dirty = true;
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
enumerable: true,
|
|
306
|
+
configurable: false,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Nested interface accessor
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
function createNestedDut(
|
|
315
|
+
view: DataView,
|
|
316
|
+
layout: Record<string, SignalLayout>,
|
|
317
|
+
members: Record<string, PortInfo>,
|
|
318
|
+
prefix: string,
|
|
319
|
+
handle: NativeHandle,
|
|
320
|
+
state: DirtyState,
|
|
321
|
+
): object {
|
|
322
|
+
const obj = Object.create(null);
|
|
323
|
+
|
|
324
|
+
for (const [memberName, memberPort] of Object.entries(members)) {
|
|
325
|
+
const qualifiedName = `${prefix}.${memberName}`;
|
|
326
|
+
const sig = layout[qualifiedName];
|
|
327
|
+
if (!sig) continue;
|
|
328
|
+
|
|
329
|
+
if (memberPort.interface) {
|
|
330
|
+
const nested = createNestedDut(
|
|
331
|
+
view,
|
|
332
|
+
layout,
|
|
333
|
+
memberPort.interface,
|
|
334
|
+
qualifiedName,
|
|
335
|
+
handle,
|
|
336
|
+
state,
|
|
337
|
+
);
|
|
338
|
+
Object.defineProperty(obj, memberName, {
|
|
339
|
+
value: nested,
|
|
340
|
+
enumerable: true,
|
|
341
|
+
configurable: false,
|
|
342
|
+
writable: false,
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
defineSignalProperty(obj, memberName, view, sig, memberPort, handle, state);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return obj;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Array port accessor
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
function createArrayDut(
|
|
357
|
+
view: DataView,
|
|
358
|
+
baseSig: SignalLayout,
|
|
359
|
+
port: PortInfo,
|
|
360
|
+
handle: NativeHandle,
|
|
361
|
+
state: DirtyState,
|
|
362
|
+
): object {
|
|
363
|
+
const dims = port.arrayDims!;
|
|
364
|
+
const elementWidth = port.width;
|
|
365
|
+
const elementByteSize = Math.ceil(elementWidth / 8);
|
|
366
|
+
const totalElements = dims.reduce((a, b) => a * b, 1);
|
|
367
|
+
const isOutput = port.direction === "output";
|
|
368
|
+
const isInput = port.direction === "input";
|
|
369
|
+
const baseOffset = baseSig.offset;
|
|
370
|
+
const is4state = baseSig.is4state;
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
length: totalElements,
|
|
374
|
+
|
|
375
|
+
at(i: number): number | bigint {
|
|
376
|
+
if (state.dirty && !isInput) {
|
|
377
|
+
handle.evalComb();
|
|
378
|
+
state.dirty = false;
|
|
379
|
+
}
|
|
380
|
+
const offset = baseOffset + i * elementByteSize;
|
|
381
|
+
if (elementWidth <= 53) {
|
|
382
|
+
return readNumber(view, offset, elementWidth);
|
|
383
|
+
}
|
|
384
|
+
return readBigInt(view, offset, elementByteSize);
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
set(i: number, value: number | bigint | symbol | FourStateValue): void {
|
|
388
|
+
if (isOutput) {
|
|
389
|
+
throw new Error("Cannot write to output array port");
|
|
390
|
+
}
|
|
391
|
+
const offset = baseOffset + i * elementByteSize;
|
|
392
|
+
if (value === Symbol.for("veryl:X")) {
|
|
393
|
+
if (!is4state) {
|
|
394
|
+
throw new Error("Array port is not 4-state; cannot assign X");
|
|
395
|
+
}
|
|
396
|
+
const elemSig: SignalLayout = {
|
|
397
|
+
offset, width: elementWidth, byteSize: elementByteSize,
|
|
398
|
+
is4state, direction: baseSig.direction,
|
|
399
|
+
};
|
|
400
|
+
writeAllX(view, elemSig);
|
|
401
|
+
} else if (isFourStateValue(value)) {
|
|
402
|
+
if (!is4state) {
|
|
403
|
+
throw new Error("Array port is not 4-state; cannot assign FourState");
|
|
404
|
+
}
|
|
405
|
+
const elemSig: SignalLayout = {
|
|
406
|
+
offset, width: elementWidth, byteSize: elementByteSize,
|
|
407
|
+
is4state, direction: baseSig.direction,
|
|
408
|
+
};
|
|
409
|
+
writeFourState(view, elemSig, value);
|
|
410
|
+
} else if (elementWidth <= 53 && typeof value === "number") {
|
|
411
|
+
writeNumber(view, offset, elementWidth, value);
|
|
412
|
+
if (is4state) writeNumber(view, offset + elementByteSize, elementWidth, 0);
|
|
413
|
+
} else {
|
|
414
|
+
const bigVal = typeof value === "bigint" ? value : BigInt(value as number);
|
|
415
|
+
writeBigInt(view, offset, elementByteSize, bigVal);
|
|
416
|
+
if (is4state) writeBigInt(view, offset + elementByteSize, elementByteSize, 0n);
|
|
417
|
+
}
|
|
418
|
+
state.dirty = true;
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// 4-state read helper (exported for advanced use)
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Read the raw 4-state (value, mask) pair for a signal.
|
|
429
|
+
* Mask bits set to 1 indicate X/Z.
|
|
430
|
+
*/
|
|
431
|
+
export function readFourState(
|
|
432
|
+
buffer: ArrayBuffer | SharedArrayBuffer,
|
|
433
|
+
sig: SignalLayout,
|
|
434
|
+
): [value: number | bigint, mask: number | bigint] {
|
|
435
|
+
if (!sig.is4state) {
|
|
436
|
+
throw new Error("Signal is not 4-state");
|
|
437
|
+
}
|
|
438
|
+
const view = new DataView(buffer);
|
|
439
|
+
const value = readSignal(view, sig);
|
|
440
|
+
const maskSig: SignalLayout = {
|
|
441
|
+
offset: sig.offset + sig.byteSize,
|
|
442
|
+
width: sig.width,
|
|
443
|
+
byteSize: sig.byteSize,
|
|
444
|
+
is4state: false,
|
|
445
|
+
direction: sig.direction,
|
|
446
|
+
};
|
|
447
|
+
const mask = readSignal(view, maskSig);
|
|
448
|
+
return [value, mask];
|
|
449
|
+
}
|
package/src/e2e.bench.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance benchmarks — mirrors `crates/celox/benches/simulation.rs`
|
|
3
|
+
* and `crates/celox/benches/overhead.rs`.
|
|
4
|
+
*
|
|
5
|
+
* Measures the same operations so JS and Rust numbers are directly comparable:
|
|
6
|
+
* 1. Build (JIT compile)
|
|
7
|
+
* 2. Single tick
|
|
8
|
+
* 3. 1M ticks in a loop
|
|
9
|
+
* 4. Simulator::tick vs Simulation::step overhead
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { bench, describe, afterAll } from "vitest";
|
|
13
|
+
import { Simulator } from "./simulator.js";
|
|
14
|
+
import { Simulation } from "./simulation.js";
|
|
15
|
+
import type { ModuleDefinition } from "./types.js";
|
|
16
|
+
import {
|
|
17
|
+
loadNativeAddon,
|
|
18
|
+
createSimulatorBridge,
|
|
19
|
+
} from "./napi-helpers.js";
|
|
20
|
+
|
|
21
|
+
const CODE = `
|
|
22
|
+
module Top #(
|
|
23
|
+
param N: u32 = 1000,
|
|
24
|
+
)(
|
|
25
|
+
clk: input clock,
|
|
26
|
+
rst: input reset,
|
|
27
|
+
cnt: output logic<32>[N],
|
|
28
|
+
) {
|
|
29
|
+
for i in 0..N: g {
|
|
30
|
+
always_ff (clk, rst) {
|
|
31
|
+
if_reset {
|
|
32
|
+
cnt[i] = 0;
|
|
33
|
+
} else {
|
|
34
|
+
cnt[i] += 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
interface TopPorts {
|
|
42
|
+
rst: number;
|
|
43
|
+
readonly cnt: { at(i: number): number; readonly length: number };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("simulation", () => {
|
|
47
|
+
bench(
|
|
48
|
+
"simulation_build_top_n1000",
|
|
49
|
+
() => {
|
|
50
|
+
const sim = Simulator.fromSource<TopPorts>(CODE, "Top");
|
|
51
|
+
sim.dispose();
|
|
52
|
+
},
|
|
53
|
+
{ iterations: 3, time: 0 },
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const sim = Simulator.fromSource<TopPorts>(CODE, "Top");
|
|
57
|
+
|
|
58
|
+
// Reset sequence
|
|
59
|
+
sim.dut.rst = 1;
|
|
60
|
+
sim.tick();
|
|
61
|
+
sim.dut.rst = 0;
|
|
62
|
+
sim.tick();
|
|
63
|
+
|
|
64
|
+
afterAll(() => {
|
|
65
|
+
sim.dispose();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
bench("simulation_tick_top_n1000_x1", () => {
|
|
69
|
+
sim.tick();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
bench(
|
|
73
|
+
"simulation_tick_top_n1000_x1000000",
|
|
74
|
+
() => {
|
|
75
|
+
for (let i = 0; i < 1_000_000; i++) {
|
|
76
|
+
sim.tick();
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{ iterations: 3, time: 0 },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Testbench pattern: write input + tick + read back
|
|
83
|
+
bench("testbench_tick_top_n1000_x1", () => {
|
|
84
|
+
sim.dut.rst = 0;
|
|
85
|
+
sim.tick();
|
|
86
|
+
// biome-ignore lint: read to measure full testbench cycle
|
|
87
|
+
sim.dut.rst;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
bench(
|
|
91
|
+
"testbench_tick_top_n1000_x1000000",
|
|
92
|
+
() => {
|
|
93
|
+
for (let i = 0; i < 1_000_000; i++) {
|
|
94
|
+
sim.dut.rst = 0;
|
|
95
|
+
sim.tick();
|
|
96
|
+
// biome-ignore lint: read to measure full testbench cycle
|
|
97
|
+
sim.dut.rst;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{ iterations: 3, time: 0 },
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Array access via .at() — use ModuleDefinition with arrayDims
|
|
104
|
+
const addon = loadNativeAddon();
|
|
105
|
+
const TopModule: ModuleDefinition<TopPorts> = {
|
|
106
|
+
__celox_module: true,
|
|
107
|
+
name: "Top",
|
|
108
|
+
source: CODE,
|
|
109
|
+
ports: {
|
|
110
|
+
clk: { direction: "input", type: "clock", width: 1 },
|
|
111
|
+
rst: { direction: "input", type: "reset", width: 1 },
|
|
112
|
+
cnt: { direction: "output", type: "logic", width: 32, arrayDims: [1000] },
|
|
113
|
+
},
|
|
114
|
+
events: ["clk"],
|
|
115
|
+
};
|
|
116
|
+
const simArr = Simulator.create<TopPorts>(TopModule, {
|
|
117
|
+
__nativeCreate: createSimulatorBridge(addon),
|
|
118
|
+
});
|
|
119
|
+
simArr.dut.rst = 1;
|
|
120
|
+
simArr.tick();
|
|
121
|
+
simArr.dut.rst = 0;
|
|
122
|
+
simArr.tick();
|
|
123
|
+
|
|
124
|
+
afterAll(() => {
|
|
125
|
+
simArr.dispose();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
bench("testbench_array_tick_top_n1000_x1", () => {
|
|
129
|
+
simArr.dut.rst = 0;
|
|
130
|
+
simArr.tick();
|
|
131
|
+
// biome-ignore lint: read array element to measure .at() overhead
|
|
132
|
+
simArr.dut.cnt.at(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
bench(
|
|
136
|
+
"testbench_array_tick_top_n1000_x1000000",
|
|
137
|
+
() => {
|
|
138
|
+
for (let i = 0; i < 1_000_000; i++) {
|
|
139
|
+
simArr.dut.rst = 0;
|
|
140
|
+
simArr.tick();
|
|
141
|
+
// biome-ignore lint: read array element to measure .at() overhead
|
|
142
|
+
simArr.dut.cnt.at(0);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{ iterations: 3, time: 0 },
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Overhead comparison — mirrors `crates/celox/benches/overhead.rs`.
|
|
151
|
+
*
|
|
152
|
+
* Compares Simulator.tick() vs Simulation.step() to measure the
|
|
153
|
+
* scheduling overhead of the time-based API.
|
|
154
|
+
*/
|
|
155
|
+
describe("overhead", () => {
|
|
156
|
+
// Simulator.tick — same as Rust simulator_tick_x10000
|
|
157
|
+
const simTick = Simulator.fromSource<TopPorts>(CODE, "Top");
|
|
158
|
+
simTick.dut.rst = 1;
|
|
159
|
+
simTick.tick();
|
|
160
|
+
simTick.dut.rst = 0;
|
|
161
|
+
simTick.tick();
|
|
162
|
+
|
|
163
|
+
afterAll(() => {
|
|
164
|
+
simTick.dispose();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
bench(
|
|
168
|
+
"simulator_tick_x10000",
|
|
169
|
+
() => {
|
|
170
|
+
for (let i = 0; i < 10_000; i++) {
|
|
171
|
+
simTick.tick();
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{ iterations: 3, time: 0 },
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Simulation.step — same as Rust simulation_step_x20000
|
|
178
|
+
const simStep = Simulation.fromSource<TopPorts>(CODE, "Top");
|
|
179
|
+
simStep.addClock("clk", { period: 10 });
|
|
180
|
+
|
|
181
|
+
afterAll(() => {
|
|
182
|
+
simStep.dispose();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
bench(
|
|
186
|
+
"simulation_step_x20000",
|
|
187
|
+
() => {
|
|
188
|
+
// 20000 steps = 10000 cycles (rising + falling)
|
|
189
|
+
for (let i = 0; i < 20_000; i++) {
|
|
190
|
+
simStep.step();
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{ iterations: 3, time: 0 },
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Simulation (time-based) benchmarks — mirrors the simulation describe
|
|
199
|
+
* above but uses the Simulation API instead of Simulator.
|
|
200
|
+
*/
|
|
201
|
+
describe("simulation-time-based", () => {
|
|
202
|
+
bench(
|
|
203
|
+
"simulation_time_build_top_n1000",
|
|
204
|
+
() => {
|
|
205
|
+
const sim = Simulation.fromSource<TopPorts>(CODE, "Top");
|
|
206
|
+
sim.dispose();
|
|
207
|
+
},
|
|
208
|
+
{ iterations: 3, time: 0 },
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const sim = Simulation.fromSource<TopPorts>(CODE, "Top");
|
|
212
|
+
sim.addClock("clk", { period: 10 });
|
|
213
|
+
|
|
214
|
+
afterAll(() => {
|
|
215
|
+
sim.dispose();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
bench("simulation_time_step_x1", () => {
|
|
219
|
+
sim.step();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
bench(
|
|
223
|
+
"simulation_time_step_x1000000",
|
|
224
|
+
() => {
|
|
225
|
+
for (let i = 0; i < 1_000_000; i++) {
|
|
226
|
+
sim.step();
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{ iterations: 3, time: 0 },
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
bench(
|
|
233
|
+
"simulation_time_runUntil_1000000",
|
|
234
|
+
() => {
|
|
235
|
+
const base = sim.time();
|
|
236
|
+
sim.runUntil(base + 1_000_000);
|
|
237
|
+
},
|
|
238
|
+
{ iterations: 3, time: 0 },
|
|
239
|
+
);
|
|
240
|
+
});
|