@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/e2e.test.ts CHANGED
@@ -497,9 +497,9 @@ module InitTest (
497
497
  const layout = parseNapiLayout(raw.layoutJson);
498
498
  const buf = raw.sharedMemory().buffer;
499
499
 
500
- // logic port should have mask=0xFF (all X)
500
+ // logic port should have mask=0xFF (all X), X encoding: v=1, m=1
501
501
  const [valA, maskA] = readFourState(buf, layout.forDut.a);
502
- expect(valA).toBe(0n);
502
+ expect(valA).toBe(0xFFn);
503
503
  expect(maskA).toBe(0xFFn);
504
504
 
505
505
  // bit port should have mask=0 (defined)
@@ -812,6 +812,11 @@ describe("E2E: 4-state high-level DUT API", () => {
812
812
  // In 4-state mode, count starts as X. Reset should clear it.
813
813
  // (default async_low: rst=0 is active)
814
814
  sim.dut.rst = 0n;
815
+ // Clear X on en before deactivating reset. In 4-state mode, en starts
816
+ // as X (v=1, m=1). The FF branch condition currently uses only the value
817
+ // bit, so X with v=1 is treated as "true" — a known limitation in FF
818
+ // conditional handling (combinational mux handles X correctly).
819
+ sim.dut.en = 0n;
815
820
  sim.tick();
816
821
  sim.dut.rst = 1n;
817
822
  sim.tick();
@@ -1561,7 +1566,7 @@ describe("E2E: parameter override — DUT correctness", () => {
1561
1566
  sim.dispose();
1562
1567
  });
1563
1568
 
1564
- test("param value reflected in logic via fromSource", () => {
1569
+ test.skip("param value reflected in logic via fromSource", () => { // blocked by upstream Veryl IR bug
1565
1570
  interface Ports {
1566
1571
  a: bigint;
1567
1572
  readonly b: bigint;
@@ -1695,4 +1700,375 @@ describe("E2E: celox.toml test sources", () => {
1695
1700
  expect(names).toContain("Adder");
1696
1701
  expect(names).toContain("Reg");
1697
1702
  });
1703
+
1704
+ test("[simulation] max_steps from celox.toml is used as waitUntil default", () => {
1705
+ // celox.toml sets [simulation] max_steps = 20; waitUntil should honour it
1706
+ // when no per-call maxSteps is provided.
1707
+ const sim = Simulation.fromProject(CELOX_TOML_PROJECT, "Reg");
1708
+ sim.addClock("clk", { period: 10 });
1709
+ expect(() => sim.waitUntil(() => false)).toThrow(SimulationTimeoutError);
1710
+ const err = (() => {
1711
+ try { sim.waitUntil(() => false); } catch (e) { return e; }
1712
+ })() as SimulationTimeoutError;
1713
+ expect(err.steps).toBe(20);
1714
+ sim.dispose();
1715
+ });
1716
+
1717
+ test("[simulation] max_steps per-call override takes precedence over celox.toml", () => {
1718
+ const sim = Simulation.fromProject(CELOX_TOML_PROJECT, "Reg");
1719
+ sim.addClock("clk", { period: 10 });
1720
+ // Per-call maxSteps=5 is lower than the toml default of 20
1721
+ expect(() => sim.waitUntil(() => false, { maxSteps: 5 })).toThrow(SimulationTimeoutError);
1722
+ const err = (() => {
1723
+ try { sim.waitUntil(() => false, { maxSteps: 5 }); } catch (e) { return e; }
1724
+ })() as SimulationTimeoutError;
1725
+ expect(err.steps).toBe(5);
1726
+ sim.dispose();
1727
+ });
1728
+ });
1729
+
1730
+ // ---------------------------------------------------------------------------
1731
+ // Interface port DUT accessor tests
1732
+ // ---------------------------------------------------------------------------
1733
+
1734
+ // A module whose top-level port is a modport interface.
1735
+ // After Veryl expansion "sig.data" and "sig.valid" become individual ports.
1736
+ // buildPortsFromLayout must group them into a nested PortInfo so that the
1737
+ // DUT exposes them as dut.sig.data / dut.sig.valid.
1738
+ const INTERFACE_PORT_SOURCE = `
1739
+ interface Signal {
1740
+ var data: logic<8>;
1741
+ var valid: logic;
1742
+ modport src {
1743
+ data: output,
1744
+ valid: output,
1745
+ }
1746
+ modport dst {
1747
+ data: input,
1748
+ valid: input,
1749
+ }
1750
+ }
1751
+
1752
+ module Top (
1753
+ sig: modport Signal::dst,
1754
+ out: output logic<8>,
1755
+ ) {
1756
+ assign out = sig.data;
1757
+ }
1758
+ `;
1759
+
1760
+ // A module that uses two instances connected via an interface so we can verify
1761
+ // that the hierarchy DUT also exposes nested accessors correctly.
1762
+ const INTERFACE_HIERARCHY_SOURCE = `
1763
+ interface Bus {
1764
+ var data: logic<8>;
1765
+ var valid: logic;
1766
+ modport producer {
1767
+ data: output,
1768
+ valid: output,
1769
+ }
1770
+ modport consumer {
1771
+ data: input,
1772
+ valid: input,
1773
+ }
1774
+ }
1775
+
1776
+ module Producer (
1777
+ bus: modport Bus::producer,
1778
+ val: input logic<8>,
1779
+ ) {
1780
+ assign bus.data = val;
1781
+ assign bus.valid = 1'b1;
1782
+ }
1783
+
1784
+ module Consumer (
1785
+ bus: modport Bus::consumer,
1786
+ out: output logic<8>,
1787
+ got: output logic,
1788
+ ) {
1789
+ assign out = bus.data;
1790
+ assign got = bus.valid;
1791
+ }
1792
+
1793
+ module Top (
1794
+ val: input logic<8>,
1795
+ out: output logic<8>,
1796
+ got: output logic,
1797
+ ) {
1798
+ inst b: Bus;
1799
+ inst p: Producer (bus: b, val: val);
1800
+ inst c: Consumer (bus: b, out: out, got: got);
1801
+ }
1802
+ `;
1803
+
1804
+ describe("E2E: interface port DUT accessor", () => {
1805
+ test("top-level interface port members accessible as nested dut properties", () => {
1806
+ interface TopPorts {
1807
+ sig: { data: bigint; valid: bigint };
1808
+ readonly out: bigint;
1809
+ }
1810
+
1811
+ const sim = Simulator.fromSource<TopPorts>(INTERFACE_PORT_SOURCE, "Top");
1812
+
1813
+ sim.dut.sig.data = 0x42n;
1814
+ expect(sim.dut.out).toBe(0x42n);
1815
+
1816
+ sim.dut.sig.data = 0xFFn;
1817
+ expect(sim.dut.out).toBe(0xFFn);
1818
+
1819
+ sim.dut.sig.data = 0x00n;
1820
+ expect(sim.dut.out).toBe(0x00n);
1821
+
1822
+ sim.dispose();
1823
+ });
1824
+
1825
+ test("writing to interface output member throws", () => {
1826
+ // bus.data / bus.valid are outputs of Top — writing should throw.
1827
+ const PRODUCER_ONLY = `
1828
+ interface Bus {
1829
+ var data: logic<8>;
1830
+ var valid: logic;
1831
+ modport src {
1832
+ data: output,
1833
+ valid: output,
1834
+ }
1835
+ }
1836
+ module Top (
1837
+ bus: modport Bus::src,
1838
+ val: input logic<8>,
1839
+ ) {
1840
+ assign bus.data = val;
1841
+ assign bus.valid = 1'b1;
1842
+ }
1843
+ `;
1844
+ const sim = Simulator.fromSource(PRODUCER_ONLY, "Top");
1845
+
1846
+ expect(() => {
1847
+ (sim.dut as any).bus.data = 0n;
1848
+ }).toThrow("Cannot write to output port 'data'");
1849
+
1850
+ sim.dispose();
1851
+ });
1852
+
1853
+ test("interface port propagates through sub-module instances", () => {
1854
+ interface TopPorts {
1855
+ val: bigint;
1856
+ readonly out: bigint;
1857
+ readonly got: bigint;
1858
+ }
1859
+
1860
+ const sim = Simulator.fromSource<TopPorts>(INTERFACE_HIERARCHY_SOURCE, "Top");
1861
+
1862
+ sim.dut.val = 0xABn;
1863
+ expect(sim.dut.out).toBe(0xABn);
1864
+ expect(sim.dut.got).toBe(1n);
1865
+
1866
+ sim.dut.val = 0x00n;
1867
+ expect(sim.dut.out).toBe(0x00n);
1868
+
1869
+ sim.dispose();
1870
+ });
1871
+
1872
+ test("multiple interface port members are independently accessible", () => {
1873
+ interface TopPorts {
1874
+ sig: { data: bigint; valid: bigint };
1875
+ readonly out: bigint;
1876
+ }
1877
+
1878
+ const sim = Simulator.fromSource<TopPorts>(INTERFACE_PORT_SOURCE, "Top");
1879
+
1880
+ sim.dut.sig.data = 0x77n;
1881
+ sim.dut.sig.valid = 1n;
1882
+ expect(sim.dut.out).toBe(0x77n);
1883
+
1884
+ sim.dut.sig.data = 0x33n;
1885
+ sim.dut.sig.valid = 0n;
1886
+ expect(sim.dut.out).toBe(0x33n);
1887
+
1888
+ sim.dispose();
1889
+ });
1890
+ });
1891
+
1892
+ // ---------------------------------------------------------------------------
1893
+ // Interface array port (modport [N])
1894
+ // ---------------------------------------------------------------------------
1895
+
1896
+ const INTERFACE_ARRAY_SOURCE = `
1897
+ interface Bus {
1898
+ var data: logic<8>;
1899
+ var valid: logic;
1900
+ modport consumer {
1901
+ data: input,
1902
+ valid: input,
1903
+ }
1904
+ }
1905
+
1906
+ module Top (
1907
+ bus: modport Bus::consumer [2],
1908
+ out: output logic<8>,
1909
+ ) {
1910
+ assign out = bus.data[0] + bus.data[1];
1911
+ }
1912
+ `;
1913
+
1914
+ describe("E2E: interface array port (modport [N])", () => {
1915
+ test("interface array members accessible via .at()/.set()", () => {
1916
+ interface TopPorts {
1917
+ bus: {
1918
+ data: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
1919
+ valid: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
1920
+ };
1921
+ readonly out: bigint;
1922
+ }
1923
+
1924
+ const sim = Simulator.fromSource<TopPorts>(INTERFACE_ARRAY_SOURCE, "Top");
1925
+
1926
+ sim.dut.bus.data.set(0, 0x10n);
1927
+ sim.dut.bus.data.set(1, 0x20n);
1928
+ expect(sim.dut.out).toBe(0x30n);
1929
+
1930
+ sim.dut.bus.data.set(0, 0xAAn);
1931
+ sim.dut.bus.data.set(1, 0x11n);
1932
+ expect(sim.dut.out).toBe(0xBBn);
1933
+
1934
+ expect(sim.dut.bus.data.length).toBe(2);
1935
+ expect(sim.dut.bus.valid.length).toBe(2);
1936
+
1937
+ sim.dispose();
1938
+ });
1939
+ });
1940
+
1941
+ // ---------------------------------------------------------------------------
1942
+ // Bug fix: 1-bit unpacked array port (logic [N])
1943
+ // ---------------------------------------------------------------------------
1944
+
1945
+ const ARRAY_1BIT_SOURCE = `
1946
+ module ArrayPassThrough (
1947
+ clk: input clock,
1948
+ rst: input reset,
1949
+ en: input logic [4],
1950
+ y0: output logic,
1951
+ y1: output logic,
1952
+ y2: output logic,
1953
+ y3: output logic,
1954
+ ) {
1955
+ always_comb {
1956
+ y0 = en[0];
1957
+ y1 = en[1];
1958
+ y2 = en[2];
1959
+ y3 = en[3];
1960
+ }
1961
+ }
1962
+ `;
1963
+
1964
+ describe("E2E: 1-bit unpacked array port (logic [N])", () => {
1965
+ test("each element is independently visible in hardware (issue #6)", () => {
1966
+ interface Ports {
1967
+ rst: bigint;
1968
+ en: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
1969
+ readonly y0: bigint;
1970
+ readonly y1: bigint;
1971
+ readonly y2: bigint;
1972
+ readonly y3: bigint;
1973
+ }
1974
+
1975
+ const sim = Simulator.fromSource<Ports>(ARRAY_1BIT_SOURCE, "ArrayPassThrough");
1976
+
1977
+ // Default: all inputs 0
1978
+ sim.tick();
1979
+ expect(sim.dut.y0).toBe(0n);
1980
+ expect(sim.dut.y1).toBe(0n);
1981
+ expect(sim.dut.y2).toBe(0n);
1982
+ expect(sim.dut.y3).toBe(0n);
1983
+
1984
+ // Set element 0 only
1985
+ sim.dut.en.set(0, 1n);
1986
+ sim.tick();
1987
+ expect(sim.dut.y0).toBe(1n);
1988
+ expect(sim.dut.y1).toBe(0n);
1989
+ expect(sim.dut.y2).toBe(0n);
1990
+ expect(sim.dut.y3).toBe(0n);
1991
+
1992
+ // Set element 1 (was silently ignored before the fix)
1993
+ sim.dut.en.set(0, 0n);
1994
+ sim.dut.en.set(1, 1n);
1995
+ sim.tick();
1996
+ expect(sim.dut.y0).toBe(0n);
1997
+ expect(sim.dut.y1).toBe(1n);
1998
+ expect(sim.dut.y2).toBe(0n);
1999
+ expect(sim.dut.y3).toBe(0n);
2000
+
2001
+ // Set elements 2 and 3
2002
+ sim.dut.en.set(1, 0n);
2003
+ sim.dut.en.set(2, 1n);
2004
+ sim.dut.en.set(3, 1n);
2005
+ sim.tick();
2006
+ expect(sim.dut.y0).toBe(0n);
2007
+ expect(sim.dut.y1).toBe(0n);
2008
+ expect(sim.dut.y2).toBe(1n);
2009
+ expect(sim.dut.y3).toBe(1n);
2010
+
2011
+ sim.dispose();
2012
+ });
2013
+
2014
+ test("length property is correct", () => {
2015
+ interface Ports {
2016
+ rst: bigint;
2017
+ en: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
2018
+ readonly y0: bigint;
2019
+ readonly y1: bigint;
2020
+ readonly y2: bigint;
2021
+ readonly y3: bigint;
2022
+ }
2023
+
2024
+ const sim = Simulator.fromSource<Ports>(ARRAY_1BIT_SOURCE, "ArrayPassThrough");
2025
+ expect(sim.dut.en.length).toBe(4);
2026
+ sim.dispose();
2027
+ });
2028
+
2029
+ test("4-state mode: X written to element i propagates only to output yi (verifies maskBase layout)", () => {
2030
+ // This test validates that the maskBase = baseOffset + totalValueBytes assumption
2031
+ // matches the actual JIT memory layout when fourState: true is used.
2032
+ interface Ports {
2033
+ rst: bigint;
2034
+ en: { at(i: number): bigint; set(i: number, v: unknown): void; length: number };
2035
+ readonly y0: bigint;
2036
+ readonly y1: bigint;
2037
+ readonly y2: bigint;
2038
+ readonly y3: bigint;
2039
+ }
2040
+
2041
+ const sim = Simulator.fromSource<Ports>(ARRAY_1BIT_SOURCE, "ArrayPassThrough", { fourState: true });
2042
+
2043
+ // Start with all defined zeros
2044
+ for (let i = 0; i < 4; i++) sim.dut.en.set(i, 0n);
2045
+ sim.tick();
2046
+
2047
+ // Set element 1 to X; only y1 should become X
2048
+ sim.dut.en.set(1, X);
2049
+ sim.tick();
2050
+
2051
+ expect(sim.fourState("y1").mask).not.toBe(0n); // y1 = X
2052
+ expect(sim.fourState("y0").mask).toBe(0n); // y0 defined
2053
+ expect(sim.fourState("y2").mask).toBe(0n); // y2 defined
2054
+ expect(sim.fourState("y3").mask).toBe(0n); // y3 defined
2055
+
2056
+ // Set element 2 to X; y2 becomes X, y1 remains X
2057
+ sim.dut.en.set(2, X);
2058
+ sim.tick();
2059
+
2060
+ expect(sim.fourState("y1").mask).not.toBe(0n);
2061
+ expect(sim.fourState("y2").mask).not.toBe(0n);
2062
+ expect(sim.fourState("y0").mask).toBe(0n);
2063
+ expect(sim.fourState("y3").mask).toBe(0n);
2064
+
2065
+ // Clear element 1 to defined 0; y1 becomes defined again
2066
+ sim.dut.en.set(1, 0n);
2067
+ sim.tick();
2068
+
2069
+ expect(sim.fourState("y1").mask).toBe(0n);
2070
+ expect(sim.fourState("y2").mask).not.toBe(0n);
2071
+
2072
+ sim.dispose();
2073
+ });
1698
2074
  });
@@ -46,6 +46,7 @@ export interface RawNapiSimulationHandle {
46
46
  readonly hierarchyJson: string;
47
47
  readonly stableSize: number;
48
48
  readonly totalSize: number;
49
+ readonly defaultMaxSteps: number | null;
49
50
  addClock(eventId: number, period: number, initialDelay: number): void;
50
51
  schedule(eventId: number, time: number, value: number): void;
51
52
  runUntil(endTime: number): void;
@@ -300,12 +301,17 @@ export function parseNapiLayout(json: string): {
300
301
  /**
301
302
  * Build PortInfo records from the NAPI layout signals.
302
303
  * This auto-detects port metadata so users don't need to hand-write ModuleDefinition.
304
+ *
305
+ * Hierarchical signal names (e.g. "bus.data", "bus.valid" from an interface port)
306
+ * are grouped into a nested PortInfo with an `interface` map so that createDut can
307
+ * expose them as `dut.bus.data` and `dut.bus.valid`.
303
308
  */
304
309
  export function buildPortsFromLayout(
305
310
  signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }>,
306
311
  _events: Record<string, number>,
307
312
  ): Record<string, PortInfo> {
308
- const ports: Record<string, PortInfo> = {};
313
+ // Step 1: Build flat PortInfo for every signal name
314
+ const flat: Record<string, PortInfo> = {};
309
315
 
310
316
  for (const [name, sig] of Object.entries(signals)) {
311
317
  const typeKind = sig.typeKind;
@@ -329,7 +335,36 @@ export function buildPortsFromLayout(
329
335
  if (sig.arrayDims && sig.arrayDims.length > 0) {
330
336
  (port as { arrayDims: readonly number[] }).arrayDims = sig.arrayDims;
331
337
  }
332
- ports[name] = port;
338
+ flat[name] = port;
339
+ }
340
+
341
+ // Step 2: Group hierarchical signals (e.g. "bus.data") into nested PortInfo.
342
+ // Veryl interface ports are exactly one level deep, so "bus.data" → parent "bus",
343
+ // member "data". The parent entry receives an `interface` map of its members;
344
+ // the flat layout keys are kept as-is so createNestedDut can look them up.
345
+ const ports: Record<string, PortInfo> = {};
346
+ const ifMaps = new Map<string, Record<string, PortInfo>>();
347
+
348
+ for (const [name, port] of Object.entries(flat)) {
349
+ const dotIdx = name.indexOf(".");
350
+ if (dotIdx < 0) {
351
+ ports[name] = port;
352
+ continue;
353
+ }
354
+ // Hierarchical signal: first segment is the interface port name
355
+ const parentName = name.slice(0, dotIdx);
356
+ const memberName = name.slice(dotIdx + 1);
357
+ if (!ifMaps.has(parentName)) {
358
+ const ifMap: Record<string, PortInfo> = {};
359
+ ifMaps.set(parentName, ifMap);
360
+ ports[parentName] = {
361
+ direction: "inout",
362
+ type: "logic",
363
+ width: 0,
364
+ interface: ifMap,
365
+ };
366
+ }
367
+ ifMaps.get(parentName)![memberName] = port;
333
368
  }
334
369
 
335
370
  return ports;
package/src/simulation.ts CHANGED
@@ -57,6 +57,7 @@ export class Simulation<P = Record<string, unknown>> {
57
57
  private readonly _buffer: ArrayBuffer | SharedArrayBuffer;
58
58
  private readonly _layout: Record<string, SignalLayout & { typeKind?: string; associatedClock?: string }>;
59
59
  private readonly _clocks = new Map<string, { period: number; eventId: number }>();
60
+ private readonly _defaultMaxSteps: number | undefined;
60
61
  private _disposed = false;
61
62
 
62
63
  private constructor(
@@ -66,6 +67,7 @@ export class Simulation<P = Record<string, unknown>> {
66
67
  state: DirtyState,
67
68
  buffer: ArrayBuffer | SharedArrayBuffer,
68
69
  layout: Record<string, SignalLayout & { typeKind?: string; associatedClock?: string }>,
70
+ defaultMaxSteps?: number,
69
71
  ) {
70
72
  this._handle = handle;
71
73
  this._dut = dut;
@@ -73,6 +75,7 @@ export class Simulation<P = Record<string, unknown>> {
73
75
  this._state = state;
74
76
  this._buffer = buffer;
75
77
  this._layout = layout;
78
+ this._defaultMaxSteps = defaultMaxSteps;
76
79
  }
77
80
 
78
81
  /**
@@ -185,12 +188,13 @@ export class Simulation<P = Record<string, unknown>> {
185
188
  const ports = buildPortsFromLayout(layout.signals, events);
186
189
 
187
190
  const buf = raw.sharedMemory().buffer;
191
+ const defaultMaxSteps = raw.defaultMaxSteps ?? undefined;
188
192
 
189
193
  const state: DirtyState = { dirty: false };
190
194
  const handle = wrapDirectSimulationHandle(raw);
191
195
  const dut = createDut<P>(buf, layout.forDut, ports, handle, state, hierarchy);
192
196
 
193
- return new Simulation<P>(handle, dut, events, state, buf, layout.signals);
197
+ return new Simulation<P>(handle, dut, events, state, buf, layout.signals, defaultMaxSteps);
194
198
  }
195
199
 
196
200
  /** The DUT accessor object — read/write ports as plain properties. */
@@ -298,7 +302,7 @@ export class Simulation<P = Record<string, unknown>> {
298
302
  opts?: { maxSteps?: number },
299
303
  ): number {
300
304
  this.ensureAlive();
301
- const max = opts?.maxSteps ?? 100_000;
305
+ const max = opts?.maxSteps ?? this._defaultMaxSteps ?? 100_000;
302
306
  let steps = 0;
303
307
  while (!condition()) {
304
308
  const t = this._handle.step();