@celox-sim/celox 0.1.10 → 0.1.12

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
@@ -477,6 +477,193 @@ describe("createDut — array ports", () => {
477
477
  expect(dut.data.at(3)).toBe(0xDDn);
478
478
  expect(dut.data.length).toBe(4);
479
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
+ });
480
667
  });
481
668
 
482
669
  // ---------------------------------------------------------------------------
package/src/dut.ts CHANGED
@@ -288,6 +288,25 @@ export function createChildDut(
288
288
  for (const [name, port] of Object.entries(hierarchy.ports)) {
289
289
  if (port.type === "clock") continue;
290
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
+
291
310
  const sig = hierarchy.forDut[name];
292
311
  if (!sig) continue;
293
312
 
@@ -439,6 +458,61 @@ function createNestedDut(
439
458
  // Array port accessor
440
459
  // ---------------------------------------------------------------------------
441
460
 
461
+ /**
462
+ * Read a single element from a bit-packed byte region.
463
+ *
464
+ * The JIT stores array elements contiguously at the bit level:
465
+ * element i occupies bits [i*W .. (i+1)*W - 1] starting at baseOffset.
466
+ * Used when elementWidth < 8 (sub-byte elements).
467
+ */
468
+ function readBitPackedElement(
469
+ view: DataView,
470
+ baseOffset: number,
471
+ elementWidth: number,
472
+ i: number,
473
+ ): number {
474
+ const bitStart = i * elementWidth;
475
+ const byteStart = baseOffset + (bitStart >> 3);
476
+ const bitShift = bitStart & 7;
477
+ const mask = (1 << elementWidth) - 1;
478
+ if (bitShift + elementWidth <= 8) {
479
+ return (view.getUint8(byteStart) >> bitShift) & mask;
480
+ }
481
+ // Spans two bytes
482
+ const lo = view.getUint8(byteStart);
483
+ const hi = view.getUint8(byteStart + 1);
484
+ return ((lo | (hi << 8)) >> bitShift) & mask;
485
+ }
486
+
487
+ /**
488
+ * Write a single element into a bit-packed byte region (read-modify-write).
489
+ *
490
+ * Used when elementWidth < 8 (sub-byte elements).
491
+ */
492
+ function writeBitPackedElement(
493
+ view: DataView,
494
+ baseOffset: number,
495
+ elementWidth: number,
496
+ i: number,
497
+ value: number,
498
+ ): void {
499
+ const bitStart = i * elementWidth;
500
+ const byteStart = baseOffset + (bitStart >> 3);
501
+ const bitShift = bitStart & 7;
502
+ const mask = (1 << elementWidth) - 1;
503
+ const maskedValue = value & mask;
504
+
505
+ const lo = view.getUint8(byteStart);
506
+ view.setUint8(byteStart, (lo & ~((mask << bitShift) & 0xff)) | ((maskedValue << bitShift) & 0xff));
507
+
508
+ if (bitShift + elementWidth > 8) {
509
+ const bitsInLo = 8 - bitShift;
510
+ const hi = view.getUint8(byteStart + 1);
511
+ const hiMask = mask >> bitsInLo;
512
+ view.setUint8(byteStart + 1, (hi & ~hiMask) | (maskedValue >> bitsInLo));
513
+ }
514
+ }
515
+
442
516
  function createArrayDut(
443
517
  view: DataView,
444
518
  baseSig: SignalLayout,
@@ -448,13 +522,61 @@ function createArrayDut(
448
522
  ): object {
449
523
  const dims = port.arrayDims!;
450
524
  const elementWidth = port.width;
451
- const elementByteSize = Math.ceil(elementWidth / 8);
452
525
  const totalElements = dims.reduce((a, b) => a * b, 1);
453
526
  const isOutput = port.direction === "output";
454
527
  const isInput = port.direction === "input";
455
528
  const baseOffset = baseSig.offset;
456
529
  const is4state = baseSig.is4state;
457
530
 
531
+ if (elementWidth < 8) {
532
+ // Sub-byte elements: the JIT stores them bit-packed.
533
+ // Element i occupies bits [i*W .. (i+1)*W - 1] starting at baseOffset.
534
+ // For 4-state signals the mask region starts immediately after the value bytes.
535
+ const totalValueBytes = Math.ceil(totalElements * elementWidth / 8);
536
+ const maskBase = baseOffset + totalValueBytes;
537
+
538
+ return {
539
+ length: totalElements,
540
+
541
+ at(i: number): bigint {
542
+ if (state.dirty && !isInput) {
543
+ handle.evalComb();
544
+ state.dirty = false;
545
+ }
546
+ return BigInt(readBitPackedElement(view, baseOffset, elementWidth, i));
547
+ },
548
+
549
+ set(i: number, value: bigint | number | symbol | FourStateValue): void {
550
+ if (isOutput) {
551
+ throw new Error("Cannot write to output array port");
552
+ }
553
+ if (value === Symbol.for("veryl:X")) {
554
+ if (!is4state) {
555
+ throw new Error("Array port is not 4-state; cannot assign X");
556
+ }
557
+ writeBitPackedElement(view, baseOffset, elementWidth, i, 0);
558
+ writeBitPackedElement(view, maskBase, elementWidth, i, (1 << elementWidth) - 1);
559
+ } else if (isFourStateValue(value)) {
560
+ if (!is4state) {
561
+ throw new Error("Array port is not 4-state; cannot assign FourState");
562
+ }
563
+ writeBitPackedElement(view, baseOffset, elementWidth, i, Number(value.value));
564
+ writeBitPackedElement(view, maskBase, elementWidth, i, Number(value.mask));
565
+ } else {
566
+ const bigVal = typeof value === "bigint" ? value : BigInt(value as number);
567
+ writeBitPackedElement(view, baseOffset, elementWidth, i, Number(bigVal));
568
+ if (is4state) {
569
+ writeBitPackedElement(view, maskBase, elementWidth, i, 0);
570
+ }
571
+ }
572
+ state.dirty = true;
573
+ },
574
+ };
575
+ }
576
+
577
+ // elementWidth >= 8: byte-aligned stride.
578
+ const elementByteSize = Math.ceil(elementWidth / 8);
579
+
458
580
  return {
459
581
  length: totalElements,
460
582
 
@@ -519,7 +641,7 @@ function createArrayDut(
519
641
 
520
642
  /**
521
643
  * Read the raw 4-state (value, mask) pair for a signal.
522
- * Mask bits set to 1 indicate X/Z.
644
+ * Mask bits set to 1 indicate X.
523
645
  */
524
646
  export function readFourState(
525
647
  buffer: ArrayBuffer | SharedArrayBuffer,