@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/e2e.test.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  const FIXTURES_DIR = path.resolve(import.meta.dirname ?? __dirname, "../fixtures");
28
28
  const ADDER_PROJECT = path.join(FIXTURES_DIR, "adder");
29
29
  const COUNTER_PROJECT = path.join(FIXTURES_DIR, "counter_project");
30
+ const CELOX_TOML_PROJECT = path.join(FIXTURES_DIR, "celox_toml");
30
31
 
31
32
  // ---------------------------------------------------------------------------
32
33
  // Test Veryl sources
@@ -496,9 +497,9 @@ module InitTest (
496
497
  const layout = parseNapiLayout(raw.layoutJson);
497
498
  const buf = raw.sharedMemory().buffer;
498
499
 
499
- // logic port should have mask=0xFF (all X)
500
+ // logic port should have mask=0xFF (all X), X encoding: v=1, m=1
500
501
  const [valA, maskA] = readFourState(buf, layout.forDut.a);
501
- expect(valA).toBe(0n);
502
+ expect(valA).toBe(0xFFn);
502
503
  expect(maskA).toBe(0xFFn);
503
504
 
504
505
  // bit port should have mask=0 (defined)
@@ -811,6 +812,11 @@ describe("E2E: 4-state high-level DUT API", () => {
811
812
  // In 4-state mode, count starts as X. Reset should clear it.
812
813
  // (default async_low: rst=0 is active)
813
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;
814
820
  sim.tick();
815
821
  sim.dut.rst = 1n;
816
822
  sim.tick();
@@ -1654,3 +1660,366 @@ describe("E2E: parameter override — DUT correctness", () => {
1654
1660
  sim.dispose();
1655
1661
  });
1656
1662
  });
1663
+
1664
+ // ---------------------------------------------------------------------------
1665
+ // celox.toml: test-only source directories
1666
+ // ---------------------------------------------------------------------------
1667
+
1668
+ describe("E2E: celox.toml test sources", () => {
1669
+ test("fromProject loads test-only module declared in celox.toml", () => {
1670
+ interface RegPorts {
1671
+ rst: bigint;
1672
+ d: bigint;
1673
+ readonly q: bigint;
1674
+ }
1675
+
1676
+ // `Reg` lives in test_veryl/ — not in Veryl.toml sources — but celox.toml
1677
+ // declares test_veryl/ as an additional test source directory.
1678
+ const sim = Simulator.fromProject<RegPorts>(CELOX_TOML_PROJECT, "Reg");
1679
+
1680
+ // Deassert async_low reset (rst=0 is active; rst=1 is normal operation)
1681
+ sim.dut.rst = 1n;
1682
+ sim.dut.d = 0xABn;
1683
+ sim.tick();
1684
+ expect(sim.dut.q).toBe(0xABn);
1685
+
1686
+ sim.dut.d = 0xCDn;
1687
+ expect(sim.dut.q).toBe(0xABn); // not yet clocked
1688
+ sim.tick();
1689
+ expect(sim.dut.q).toBe(0xCDn);
1690
+
1691
+ sim.dispose();
1692
+ });
1693
+
1694
+ test("genTs includes test-only module declared in celox.toml", () => {
1695
+ const addon = loadNativeAddon();
1696
+ const json = addon.genTs(CELOX_TOML_PROJECT);
1697
+ const output = JSON.parse(json) as { modules: Array<{ moduleName: string }> };
1698
+ const names = output.modules.map((m) => m.moduleName);
1699
+ // Both the regular source (Adder) and the test-only source (Reg) must appear
1700
+ expect(names).toContain("Adder");
1701
+ expect(names).toContain("Reg");
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
+ // Bug fix: 1-bit unpacked array port (logic [N])
1894
+ // ---------------------------------------------------------------------------
1895
+
1896
+ const ARRAY_1BIT_SOURCE = `
1897
+ module ArrayPassThrough (
1898
+ clk: input clock,
1899
+ rst: input reset,
1900
+ en: input logic [4],
1901
+ y0: output logic,
1902
+ y1: output logic,
1903
+ y2: output logic,
1904
+ y3: output logic,
1905
+ ) {
1906
+ always_comb {
1907
+ y0 = en[0];
1908
+ y1 = en[1];
1909
+ y2 = en[2];
1910
+ y3 = en[3];
1911
+ }
1912
+ }
1913
+ `;
1914
+
1915
+ describe("E2E: 1-bit unpacked array port (logic [N])", () => {
1916
+ test("each element is independently visible in hardware (issue #6)", () => {
1917
+ interface Ports {
1918
+ rst: bigint;
1919
+ en: { at(i: number): bigint; set(i: number, v: bigint): void; length: number };
1920
+ readonly y0: bigint;
1921
+ readonly y1: bigint;
1922
+ readonly y2: bigint;
1923
+ readonly y3: bigint;
1924
+ }
1925
+
1926
+ const sim = Simulator.fromSource<Ports>(ARRAY_1BIT_SOURCE, "ArrayPassThrough");
1927
+
1928
+ // Default: all inputs 0
1929
+ sim.tick();
1930
+ expect(sim.dut.y0).toBe(0n);
1931
+ expect(sim.dut.y1).toBe(0n);
1932
+ expect(sim.dut.y2).toBe(0n);
1933
+ expect(sim.dut.y3).toBe(0n);
1934
+
1935
+ // Set element 0 only
1936
+ sim.dut.en.set(0, 1n);
1937
+ sim.tick();
1938
+ expect(sim.dut.y0).toBe(1n);
1939
+ expect(sim.dut.y1).toBe(0n);
1940
+ expect(sim.dut.y2).toBe(0n);
1941
+ expect(sim.dut.y3).toBe(0n);
1942
+
1943
+ // Set element 1 (was silently ignored before the fix)
1944
+ sim.dut.en.set(0, 0n);
1945
+ sim.dut.en.set(1, 1n);
1946
+ sim.tick();
1947
+ expect(sim.dut.y0).toBe(0n);
1948
+ expect(sim.dut.y1).toBe(1n);
1949
+ expect(sim.dut.y2).toBe(0n);
1950
+ expect(sim.dut.y3).toBe(0n);
1951
+
1952
+ // Set elements 2 and 3
1953
+ sim.dut.en.set(1, 0n);
1954
+ sim.dut.en.set(2, 1n);
1955
+ sim.dut.en.set(3, 1n);
1956
+ sim.tick();
1957
+ expect(sim.dut.y0).toBe(0n);
1958
+ expect(sim.dut.y1).toBe(0n);
1959
+ expect(sim.dut.y2).toBe(1n);
1960
+ expect(sim.dut.y3).toBe(1n);
1961
+
1962
+ sim.dispose();
1963
+ });
1964
+
1965
+ test("length property is correct", () => {
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
+ expect(sim.dut.en.length).toBe(4);
1977
+ sim.dispose();
1978
+ });
1979
+
1980
+ test("4-state mode: X written to element i propagates only to output yi (verifies maskBase layout)", () => {
1981
+ // This test validates that the maskBase = baseOffset + totalValueBytes assumption
1982
+ // matches the actual JIT memory layout when fourState: true is used.
1983
+ interface Ports {
1984
+ rst: bigint;
1985
+ en: { at(i: number): bigint; set(i: number, v: unknown): void; length: number };
1986
+ readonly y0: bigint;
1987
+ readonly y1: bigint;
1988
+ readonly y2: bigint;
1989
+ readonly y3: bigint;
1990
+ }
1991
+
1992
+ const sim = Simulator.fromSource<Ports>(ARRAY_1BIT_SOURCE, "ArrayPassThrough", { fourState: true });
1993
+
1994
+ // Start with all defined zeros
1995
+ for (let i = 0; i < 4; i++) sim.dut.en.set(i, 0n);
1996
+ sim.tick();
1997
+
1998
+ // Set element 1 to X; only y1 should become X
1999
+ sim.dut.en.set(1, X);
2000
+ sim.tick();
2001
+
2002
+ expect(sim.fourState("y1").mask).not.toBe(0n); // y1 = X
2003
+ expect(sim.fourState("y0").mask).toBe(0n); // y0 defined
2004
+ expect(sim.fourState("y2").mask).toBe(0n); // y2 defined
2005
+ expect(sim.fourState("y3").mask).toBe(0n); // y3 defined
2006
+
2007
+ // Set element 2 to X; y2 becomes X, y1 remains X
2008
+ sim.dut.en.set(2, X);
2009
+ sim.tick();
2010
+
2011
+ expect(sim.fourState("y1").mask).not.toBe(0n);
2012
+ expect(sim.fourState("y2").mask).not.toBe(0n);
2013
+ expect(sim.fourState("y0").mask).toBe(0n);
2014
+ expect(sim.fourState("y3").mask).toBe(0n);
2015
+
2016
+ // Clear element 1 to defined 0; y1 becomes defined again
2017
+ sim.dut.en.set(1, 0n);
2018
+ sim.tick();
2019
+
2020
+ expect(sim.fourState("y1").mask).toBe(0n);
2021
+ expect(sim.fourState("y2").mask).not.toBe(0n);
2022
+
2023
+ sim.dispose();
2024
+ });
2025
+ });
@@ -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();
package/src/types.ts CHANGED
@@ -124,7 +124,7 @@ export interface CreateResult<H extends NativeHandle = NativeHandle> {
124
124
  // ---------------------------------------------------------------------------
125
125
 
126
126
  export interface SimulatorOptions {
127
- /** Enable 4-state (X/Z) simulation. Default: false. */
127
+ /** Enable 4-state (X) simulation. Default: false. */
128
128
  fourState?: boolean;
129
129
  /** Path to write VCD waveform output. */
130
130
  vcd?: string;