@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/dist/dut.d.ts +1 -1
- package/dist/dut.d.ts.map +1 -1
- package/dist/dut.js +98 -2
- package/dist/dut.js.map +1 -1
- package/dist/napi-helpers.d.ts +5 -0
- package/dist/napi-helpers.d.ts.map +1 -1
- package/dist/napi-helpers.js +34 -2
- package/dist/napi-helpers.js.map +1 -1
- package/dist/simulation.d.ts +1 -0
- package/dist/simulation.d.ts.map +1 -1
- package/dist/simulation.js +6 -3
- package/dist/simulation.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/dut.test.ts +187 -0
- package/src/dut.ts +124 -2
- package/src/e2e.test.ts +371 -2
- package/src/napi-helpers.ts +37 -2
- package/src/simulation.ts +6 -2
- package/src/types.ts +1 -1
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
|
|
644
|
+
* Mask bits set to 1 indicate X.
|
|
523
645
|
*/
|
|
524
646
|
export function readFourState(
|
|
525
647
|
buffer: ArrayBuffer | SharedArrayBuffer,
|