@celox-sim/celox 0.1.5 → 0.1.7

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
@@ -13,6 +13,7 @@ import type {
13
13
  SignalLayout,
14
14
  FourStateValue,
15
15
  } from "./types.js";
16
+ import type { HierarchyNode } from "./napi-helpers.js";
16
17
  import { isFourStateValue } from "./types.js";
17
18
 
18
19
  // ---------------------------------------------------------------------------
@@ -115,27 +116,27 @@ function writeBigInt(
115
116
  }
116
117
  }
117
118
 
118
- /** Read a signal value from the DataView. Returns number or bigint. */
119
+ /** Read a signal value from the DataView. Always returns bigint. */
119
120
  function readSignal(
120
121
  view: DataView,
121
122
  sig: SignalLayout,
122
- ): number | bigint {
123
+ ): bigint {
123
124
  if (sig.width <= 53) {
124
- return readNumber(view, sig.offset, sig.width);
125
+ return BigInt(readNumber(view, sig.offset, sig.width));
125
126
  }
126
127
  return readBigInt(view, sig.offset, sig.byteSize);
127
128
  }
128
129
 
129
- /** Write a signal value to the DataView. Accepts number or bigint. */
130
+ /** Write a signal value to the DataView. Accepts bigint (number accepted for compat). */
130
131
  function writeSignal(
131
132
  view: DataView,
132
133
  sig: SignalLayout,
133
- value: number | bigint,
134
+ value: bigint | number,
134
135
  ): void {
135
- if (sig.width <= 53 && typeof value === "number") {
136
- writeNumber(view, sig.offset, sig.width, value);
136
+ const bigVal = typeof value === "bigint" ? value : BigInt(value);
137
+ if (sig.width <= 53) {
138
+ writeNumber(view, sig.offset, sig.width, Number(bigVal));
137
139
  } else {
138
- const bigVal = typeof value === "bigint" ? value : BigInt(value);
139
140
  writeBigInt(view, sig.offset, sig.byteSize, bigVal);
140
141
  }
141
142
  }
@@ -161,11 +162,8 @@ function writeFourState(
161
162
  /** Write all-X mask for a signal. */
162
163
  function writeAllX(view: DataView, sig: SignalLayout): void {
163
164
  // 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;
165
+ writeSignal(view, sig, 0n);
166
+ const allOnes = (1n << BigInt(sig.width)) - 1n;
169
167
  const maskLayout: SignalLayout = {
170
168
  offset: sig.offset + sig.byteSize,
171
169
  width: sig.width,
@@ -183,11 +181,12 @@ function writeAllX(view: DataView, sig: SignalLayout): void {
183
181
  /**
184
182
  * Create a DUT accessor object with defineProperty-based getters/setters.
185
183
  *
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
184
+ * @param buffer SharedArrayBuffer from NAPI create()
185
+ * @param layout Per-signal byte layout within the buffer
186
+ * @param portDefs Port metadata from the ModuleDefinition
187
+ * @param handle Native handle (for evalComb calls)
188
+ * @param state Shared dirty-tracking state
189
+ * @param hierarchy Optional hierarchy node for child instance access
191
190
  */
192
191
  export function createDut<P>(
193
192
  buffer: ArrayBuffer | SharedArrayBuffer,
@@ -195,6 +194,7 @@ export function createDut<P>(
195
194
  portDefs: Record<string, PortInfo>,
196
195
  handle: NativeHandle,
197
196
  state: DirtyState,
197
+ hierarchy?: HierarchyNode,
198
198
  ): P {
199
199
  const view = new DataView(buffer);
200
200
  const obj = Object.create(null) as P;
@@ -243,6 +243,91 @@ export function createDut<P>(
243
243
  defineSignalProperty(obj as object, name, view, sig, port, handle, state);
244
244
  }
245
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;
272
+ }
273
+
274
+ /**
275
+ * Create a child instance DUT accessor from a HierarchyNode.
276
+ * Recursively creates accessors for the child's signals and its own children.
277
+ */
278
+ export function createChildDut(
279
+ buffer: ArrayBuffer | SharedArrayBuffer,
280
+ hierarchy: HierarchyNode,
281
+ handle: NativeHandle,
282
+ state: DirtyState,
283
+ ): 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
+ const sig = hierarchy.forDut[name];
292
+ if (!sig) continue;
293
+
294
+ if (port.arrayDims && port.arrayDims.length > 0) {
295
+ const arrayObj = createArrayDut(view, sig, port, handle, state);
296
+ Object.defineProperty(obj, name, {
297
+ value: arrayObj,
298
+ enumerable: true,
299
+ configurable: false,
300
+ writable: false,
301
+ });
302
+ continue;
303
+ }
304
+
305
+ defineSignalProperty(obj, name, view, sig, port, handle, state);
306
+ }
307
+
308
+ // Recursively attach children
309
+ for (const [childName, instances] of Object.entries(hierarchy.children)) {
310
+ if (instances.length === 1) {
311
+ const childDut = createChildDut(buffer, instances[0]!, handle, state);
312
+ Object.defineProperty(obj, childName, {
313
+ value: childDut,
314
+ enumerable: true,
315
+ configurable: false,
316
+ writable: false,
317
+ });
318
+ } else if (instances.length > 1) {
319
+ const childDuts = instances.map((inst) =>
320
+ createChildDut(buffer, inst, handle, state),
321
+ );
322
+ Object.defineProperty(obj, childName, {
323
+ value: childDuts,
324
+ enumerable: true,
325
+ configurable: false,
326
+ writable: false,
327
+ });
328
+ }
329
+ }
330
+
246
331
  return obj;
247
332
  }
248
333
 
@@ -260,7 +345,7 @@ function defineSignalProperty(
260
345
  const isInput = port?.direction === "input";
261
346
 
262
347
  Object.defineProperty(target, name, {
263
- get(): number | bigint {
348
+ get(): bigint {
264
349
  // Output reads: lazy evalComb if dirty
265
350
  if (state.dirty && !isInput) {
266
351
  handle.evalComb();
@@ -269,7 +354,7 @@ function defineSignalProperty(
269
354
  return readSignal(view, sig);
270
355
  },
271
356
 
272
- set(value: number | bigint | symbol | FourStateValue) {
357
+ set(value: bigint | number | symbol | FourStateValue) {
273
358
  if (isOutput) {
274
359
  throw new Error(`Cannot write to output port '${name}'`);
275
360
  }
@@ -285,7 +370,8 @@ function defineSignalProperty(
285
370
  }
286
371
  writeFourState(view, sig, value);
287
372
  } else {
288
- writeSignal(view, sig, value as number | bigint);
373
+ const bigVal = typeof value === "bigint" ? value : BigInt(value as number);
374
+ writeSignal(view, sig, bigVal);
289
375
  // Clear mask when writing a defined value to a 4-state signal
290
376
  if (sig.is4state) {
291
377
  const maskLayout: SignalLayout = {
@@ -295,7 +381,7 @@ function defineSignalProperty(
295
381
  is4state: false,
296
382
  direction: sig.direction,
297
383
  };
298
- writeSignal(view, maskLayout, sig.width <= 53 ? 0 : 0n);
384
+ writeSignal(view, maskLayout, 0n);
299
385
  }
300
386
  }
301
387
 
@@ -372,19 +458,19 @@ function createArrayDut(
372
458
  return {
373
459
  length: totalElements,
374
460
 
375
- at(i: number): number | bigint {
461
+ at(i: number): bigint {
376
462
  if (state.dirty && !isInput) {
377
463
  handle.evalComb();
378
464
  state.dirty = false;
379
465
  }
380
466
  const offset = baseOffset + i * elementByteSize;
381
467
  if (elementWidth <= 53) {
382
- return readNumber(view, offset, elementWidth);
468
+ return BigInt(readNumber(view, offset, elementWidth));
383
469
  }
384
470
  return readBigInt(view, offset, elementByteSize);
385
471
  },
386
472
 
387
- set(i: number, value: number | bigint | symbol | FourStateValue): void {
473
+ set(i: number, value: bigint | number | symbol | FourStateValue): void {
388
474
  if (isOutput) {
389
475
  throw new Error("Cannot write to output array port");
390
476
  }
@@ -407,13 +493,20 @@ function createArrayDut(
407
493
  is4state, direction: baseSig.direction,
408
494
  };
409
495
  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
496
  } else {
414
497
  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);
498
+ const elemSig: SignalLayout = {
499
+ offset, width: elementWidth, byteSize: elementByteSize,
500
+ is4state, direction: baseSig.direction,
501
+ };
502
+ writeSignal(view, elemSig, bigVal);
503
+ if (is4state) {
504
+ const maskSig: SignalLayout = {
505
+ offset: offset + elementByteSize, width: elementWidth,
506
+ byteSize: elementByteSize, is4state: false, direction: baseSig.direction,
507
+ };
508
+ writeSignal(view, maskSig, 0n);
509
+ }
417
510
  }
418
511
  state.dirty = true;
419
512
  },
@@ -431,7 +524,7 @@ function createArrayDut(
431
524
  export function readFourState(
432
525
  buffer: ArrayBuffer | SharedArrayBuffer,
433
526
  sig: SignalLayout,
434
- ): [value: number | bigint, mask: number | bigint] {
527
+ ): [value: bigint, mask: bigint] {
435
528
  if (!sig.is4state) {
436
529
  throw new Error("Signal is not 4-state");
437
530
  }
package/src/e2e.bench.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  import { bench, describe, afterAll } from "vitest";
13
13
  import { Simulator } from "./simulator.js";
14
14
  import { Simulation } from "./simulation.js";
15
- import type { ModuleDefinition } from "./types.js";
15
+ import type { ModuleDefinition, SimulationTimeoutError } from "./types.js";
16
16
  import {
17
17
  loadNativeAddon,
18
18
  createSimulatorBridge,
@@ -39,8 +39,8 @@ const CODE = `
39
39
  `;
40
40
 
41
41
  interface TopPorts {
42
- rst: number;
43
- readonly cnt: { at(i: number): number; readonly length: number };
42
+ rst: bigint;
43
+ readonly cnt: { at(i: number): bigint; readonly length: number };
44
44
  }
45
45
 
46
46
  describe("simulation", () => {
@@ -56,9 +56,9 @@ describe("simulation", () => {
56
56
  const sim = Simulator.fromSource<TopPorts>(CODE, "Top");
57
57
 
58
58
  // Reset sequence
59
- sim.dut.rst = 1;
59
+ sim.dut.rst = 1n;
60
60
  sim.tick();
61
- sim.dut.rst = 0;
61
+ sim.dut.rst = 0n;
62
62
  sim.tick();
63
63
 
64
64
  afterAll(() => {
@@ -81,7 +81,7 @@ describe("simulation", () => {
81
81
 
82
82
  // Testbench pattern: write input + tick + read back
83
83
  bench("testbench_tick_top_n1000_x1", () => {
84
- sim.dut.rst = 0;
84
+ sim.dut.rst = 0n;
85
85
  sim.tick();
86
86
  // biome-ignore lint: read to measure full testbench cycle
87
87
  sim.dut.rst;
@@ -91,7 +91,7 @@ describe("simulation", () => {
91
91
  "testbench_tick_top_n1000_x1000000",
92
92
  () => {
93
93
  for (let i = 0; i < 1_000_000; i++) {
94
- sim.dut.rst = 0;
94
+ sim.dut.rst = 0n;
95
95
  sim.tick();
96
96
  // biome-ignore lint: read to measure full testbench cycle
97
97
  sim.dut.rst;
@@ -116,9 +116,9 @@ describe("simulation", () => {
116
116
  const simArr = Simulator.create<TopPorts>(TopModule, {
117
117
  __nativeCreate: createSimulatorBridge(addon),
118
118
  });
119
- simArr.dut.rst = 1;
119
+ simArr.dut.rst = 1n;
120
120
  simArr.tick();
121
- simArr.dut.rst = 0;
121
+ simArr.dut.rst = 0n;
122
122
  simArr.tick();
123
123
 
124
124
  afterAll(() => {
@@ -126,7 +126,7 @@ describe("simulation", () => {
126
126
  });
127
127
 
128
128
  bench("testbench_array_tick_top_n1000_x1", () => {
129
- simArr.dut.rst = 0;
129
+ simArr.dut.rst = 0n;
130
130
  simArr.tick();
131
131
  // biome-ignore lint: read array element to measure .at() overhead
132
132
  simArr.dut.cnt.at(0);
@@ -136,7 +136,7 @@ describe("simulation", () => {
136
136
  "testbench_array_tick_top_n1000_x1000000",
137
137
  () => {
138
138
  for (let i = 0; i < 1_000_000; i++) {
139
- simArr.dut.rst = 0;
139
+ simArr.dut.rst = 0n;
140
140
  simArr.tick();
141
141
  // biome-ignore lint: read array element to measure .at() overhead
142
142
  simArr.dut.cnt.at(0);
@@ -155,9 +155,9 @@ describe("simulation", () => {
155
155
  describe("overhead", () => {
156
156
  // Simulator.tick — same as Rust simulator_tick_x10000
157
157
  const simTick = Simulator.fromSource<TopPorts>(CODE, "Top");
158
- simTick.dut.rst = 1;
158
+ simTick.dut.rst = 1n;
159
159
  simTick.tick();
160
- simTick.dut.rst = 0;
160
+ simTick.dut.rst = 0n;
161
161
  simTick.tick();
162
162
 
163
163
  afterAll(() => {
@@ -238,3 +238,166 @@ describe("simulation-time-based", () => {
238
238
  { iterations: 3, time: 0 },
239
239
  );
240
240
  });
241
+
242
+ /**
243
+ * Phase 3b: Testbench helpers benchmarks.
244
+ *
245
+ * Compares waitForCycles vs manual step loop, and runUntil with/without
246
+ * maxSteps guard to measure overhead.
247
+ */
248
+ describe("testbench-helpers", () => {
249
+ const COUNTER_CODE = `
250
+ module Counter (
251
+ clk: input clock,
252
+ rst: input reset,
253
+ en: input logic,
254
+ count: output logic<8>,
255
+ ) {
256
+ var count_r: logic<8>;
257
+
258
+ always_ff (clk, rst) {
259
+ if_reset {
260
+ count_r = 0;
261
+ } else if en {
262
+ count_r = count_r + 1;
263
+ }
264
+ }
265
+
266
+ always_comb {
267
+ count = count_r;
268
+ }
269
+ }
270
+ `;
271
+
272
+ interface CounterPorts {
273
+ rst: bigint;
274
+ en: bigint;
275
+ readonly count: bigint;
276
+ }
277
+
278
+ // waitForCycles benchmark
279
+ const simWait = Simulation.fromSource<CounterPorts>(COUNTER_CODE, "Counter");
280
+ simWait.addClock("clk", { period: 10 });
281
+ simWait.dut.rst = 1n;
282
+ simWait.runUntil(20);
283
+ simWait.dut.rst = 0n;
284
+ simWait.dut.en = 1n;
285
+
286
+ afterAll(() => {
287
+ simWait.dispose();
288
+ });
289
+
290
+ bench(
291
+ "waitForCycles_x1000",
292
+ () => {
293
+ simWait.waitForCycles("clk", 1000);
294
+ },
295
+ { iterations: 3, time: 0 },
296
+ );
297
+
298
+ bench(
299
+ "manual_step_loop_x2000",
300
+ () => {
301
+ for (let i = 0; i < 2000; i++) {
302
+ simWait.step();
303
+ }
304
+ },
305
+ { iterations: 3, time: 0 },
306
+ );
307
+
308
+ // runUntil: fast Rust path vs guarded TS path
309
+ const simRun = Simulation.fromSource<CounterPorts>(COUNTER_CODE, "Counter");
310
+ simRun.addClock("clk", { period: 10 });
311
+ simRun.dut.rst = 1n;
312
+ simRun.runUntil(20);
313
+ simRun.dut.rst = 0n;
314
+ simRun.dut.en = 1n;
315
+
316
+ afterAll(() => {
317
+ simRun.dispose();
318
+ });
319
+
320
+ bench(
321
+ "runUntil_fast_path_100000",
322
+ () => {
323
+ const base = simRun.time();
324
+ simRun.runUntil(base + 100_000);
325
+ },
326
+ { iterations: 3, time: 0 },
327
+ );
328
+
329
+ bench(
330
+ "runUntil_guarded_100000",
331
+ () => {
332
+ const base = simRun.time();
333
+ simRun.runUntil(base + 100_000, { maxSteps: 1_000_000 });
334
+ },
335
+ { iterations: 3, time: 0 },
336
+ );
337
+ });
338
+
339
+ /**
340
+ * Phase 3c: Optimize flag benchmarks.
341
+ *
342
+ * Compares build time and tick performance with and without optimization.
343
+ */
344
+ describe("optimize-flag", () => {
345
+ bench(
346
+ "build_without_optimize",
347
+ () => {
348
+ const sim = Simulator.fromSource<TopPorts>(CODE, "Top");
349
+ sim.dispose();
350
+ },
351
+ { iterations: 3, time: 0 },
352
+ );
353
+
354
+ bench(
355
+ "build_with_optimize",
356
+ () => {
357
+ const sim = Simulator.fromSource<TopPorts>(CODE, "Top", {
358
+ optimize: true,
359
+ });
360
+ sim.dispose();
361
+ },
362
+ { iterations: 3, time: 0 },
363
+ );
364
+
365
+ const simNoOpt = Simulator.fromSource<TopPorts>(CODE, "Top");
366
+ simNoOpt.dut.rst = 1n;
367
+ simNoOpt.tick();
368
+ simNoOpt.dut.rst = 0n;
369
+ simNoOpt.tick();
370
+
371
+ const simOpt = Simulator.fromSource<TopPorts>(CODE, "Top", {
372
+ optimize: true,
373
+ });
374
+ simOpt.dut.rst = 1n;
375
+ simOpt.tick();
376
+ simOpt.dut.rst = 0n;
377
+ simOpt.tick();
378
+
379
+ afterAll(() => {
380
+ simNoOpt.dispose();
381
+ simOpt.dispose();
382
+ });
383
+
384
+ bench(
385
+ "tick_x10000_without_optimize",
386
+ () => {
387
+ for (let i = 0; i < 10_000; i++) {
388
+ simNoOpt.tick();
389
+ }
390
+ },
391
+ { iterations: 3, time: 0 },
392
+ );
393
+
394
+ bench(
395
+ "tick_x10000_with_optimize",
396
+ () => {
397
+ for (let i = 0; i < 10_000; i++) {
398
+ simOpt.tick();
399
+ }
400
+ },
401
+ { iterations: 3, time: 0 },
402
+ );
403
+ });