@celox-sim/celox 0.1.11 → 0.1.13

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,12 +477,283 @@ 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
  // ---------------------------------------------------------------------------
483
670
  // Interface (nested) ports
484
671
  // ---------------------------------------------------------------------------
485
672
 
673
+ describe("createDut — interface ports with array members", () => {
674
+ test("interface member with arrayDims uses array accessor", () => {
675
+ // Interface array pattern: bus: modport Bus::consumer [2]
676
+ // Veryl expands to bus.data with arrayDims:[2], bus.valid with arrayDims:[2]
677
+ const buffer = makeBuffer(64);
678
+ const layout: Record<string, SignalLayout> = {
679
+ "bus.data": { offset: 0, width: 8, byteSize: 2, is4state: false, direction: "input" },
680
+ "bus.valid": { offset: 2, width: 1, byteSize: 1, is4state: false, direction: "input" },
681
+ };
682
+ const ports: Record<string, PortInfo> = {
683
+ bus: {
684
+ direction: "inout",
685
+ type: "logic",
686
+ width: 0,
687
+ interface: {
688
+ data: { direction: "input", type: "logic", width: 8, arrayDims: [2] },
689
+ valid: { direction: "input", type: "logic", width: 1, arrayDims: [2] },
690
+ },
691
+ },
692
+ };
693
+ const handle = mockHandle();
694
+ const state: DirtyState = { dirty: false };
695
+
696
+ const dut = createDut<{
697
+ bus: {
698
+ data: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
699
+ valid: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
700
+ };
701
+ }>(buffer, layout, ports, handle, state);
702
+
703
+ // data: 2 elements of 8 bits each
704
+ dut.bus.data.set(0, 0xAAn);
705
+ dut.bus.data.set(1, 0xBBn);
706
+ expect(dut.bus.data.at(0)).toBe(0xAAn);
707
+ expect(dut.bus.data.at(1)).toBe(0xBBn);
708
+ expect(dut.bus.data.length).toBe(2);
709
+
710
+ // valid: 2 elements of 1 bit each (bit-packed)
711
+ dut.bus.valid.set(0, 1n);
712
+ dut.bus.valid.set(1, 0n);
713
+ expect(dut.bus.valid.at(0)).toBe(1n);
714
+ expect(dut.bus.valid.at(1)).toBe(0n);
715
+ expect(dut.bus.valid.length).toBe(2);
716
+ });
717
+
718
+ test("interface with mixed scalar and array members", () => {
719
+ const buffer = makeBuffer(64);
720
+ const layout: Record<string, SignalLayout> = {
721
+ "bus.data": { offset: 0, width: 8, byteSize: 2, is4state: false, direction: "input" },
722
+ "bus.valid": { offset: 4, width: 1, byteSize: 1, is4state: false, direction: "input" },
723
+ };
724
+ const ports: Record<string, PortInfo> = {
725
+ bus: {
726
+ direction: "inout",
727
+ type: "logic",
728
+ width: 0,
729
+ interface: {
730
+ data: { direction: "input", type: "logic", width: 8, arrayDims: [2] },
731
+ valid: { direction: "input", type: "logic", width: 1 },
732
+ },
733
+ },
734
+ };
735
+ const handle = mockHandle();
736
+ const state: DirtyState = { dirty: false };
737
+
738
+ const dut = createDut<{
739
+ bus: {
740
+ data: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
741
+ valid: bigint;
742
+ };
743
+ }>(buffer, layout, ports, handle, state);
744
+
745
+ // Array member
746
+ dut.bus.data.set(0, 0x11n);
747
+ dut.bus.data.set(1, 0x22n);
748
+ expect(dut.bus.data.at(0)).toBe(0x11n);
749
+ expect(dut.bus.data.at(1)).toBe(0x22n);
750
+
751
+ // Scalar member
752
+ dut.bus.valid = 1n;
753
+ expect(dut.bus.valid).toBe(1n);
754
+ });
755
+ });
756
+
486
757
  describe("createDut — interface ports", () => {
487
758
  test("nested interface members", () => {
488
759
  const buffer = makeBuffer(64);
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
 
@@ -427,6 +446,14 @@ function createNestedDut(
427
446
  configurable: false,
428
447
  writable: false,
429
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
+ });
430
457
  } else {
431
458
  defineSignalProperty(obj, memberName, view, sig, memberPort, handle, state);
432
459
  }
@@ -439,6 +466,61 @@ function createNestedDut(
439
466
  // Array port accessor
440
467
  // ---------------------------------------------------------------------------
441
468
 
469
+ /**
470
+ * Read a single element from a bit-packed byte region.
471
+ *
472
+ * The JIT stores array elements contiguously at the bit level:
473
+ * element i occupies bits [i*W .. (i+1)*W - 1] starting at baseOffset.
474
+ * Used when elementWidth < 8 (sub-byte elements).
475
+ */
476
+ function readBitPackedElement(
477
+ view: DataView,
478
+ baseOffset: number,
479
+ elementWidth: number,
480
+ i: number,
481
+ ): 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;
493
+ }
494
+
495
+ /**
496
+ * Write a single element into a bit-packed byte region (read-modify-write).
497
+ *
498
+ * Used when elementWidth < 8 (sub-byte elements).
499
+ */
500
+ function writeBitPackedElement(
501
+ view: DataView,
502
+ baseOffset: number,
503
+ elementWidth: number,
504
+ i: number,
505
+ value: number,
506
+ ): 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
+ }
522
+ }
523
+
442
524
  function createArrayDut(
443
525
  view: DataView,
444
526
  baseSig: SignalLayout,
@@ -448,13 +530,61 @@ function createArrayDut(
448
530
  ): object {
449
531
  const dims = port.arrayDims!;
450
532
  const elementWidth = port.width;
451
- const elementByteSize = Math.ceil(elementWidth / 8);
452
533
  const totalElements = dims.reduce((a, b) => a * b, 1);
453
534
  const isOutput = port.direction === "output";
454
535
  const isInput = port.direction === "input";
455
536
  const baseOffset = baseSig.offset;
456
537
  const is4state = baseSig.is4state;
457
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
+
458
588
  return {
459
589
  length: totalElements,
460
590