@celox-sim/celox 0.1.30 → 0.1.32

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.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * WASM bridge for running simulation via browser WebAssembly.
3
+ *
4
+ * When the NAPI addon is compiled for wasm32 (no JIT backend), the
5
+ * NativeSimulatorHandle exposes `combWasmBytes()` and `eventWasmBytes(name)`
6
+ * instead of `tick()` / `evalComb()`. This bridge instantiates those WASM
7
+ * modules and presents a handle that is compatible with the existing
8
+ * Simulator / DUT code.
9
+ *
10
+ * @module
11
+ */
12
+ import type { NativeSimulatorHandle } from "./types.js";
13
+ /**
14
+ * Handle shape returned by a wasm32-compiled NativeSimulatorHandle.
15
+ * Extends the standard metadata getters with WASM bytecode accessors.
16
+ */
17
+ export interface RawWasmSimulatorHandle {
18
+ readonly layoutJson: string;
19
+ readonly eventsJson: string;
20
+ readonly hierarchyJson: string;
21
+ readonly warningsJson: string;
22
+ readonly stableSize: number;
23
+ readonly totalSize: number;
24
+ combWasmBytes(): Uint8Array | number[];
25
+ eventWasmBytes(name: string): Uint8Array | number[];
26
+ dispose(): void;
27
+ }
28
+ /**
29
+ * Detect if a handle was produced by a wasm32-compiled addon.
30
+ *
31
+ * WASM handles expose `combWasmBytes()` instead of `tick()`.
32
+ */
33
+ export declare function isWasmHandle(handle: unknown): handle is RawWasmSimulatorHandle;
34
+ /** Result of creating a WASM simulator bridge. */
35
+ export interface WasmBridgeResult {
36
+ /** Handle compatible with the existing Simulator code. */
37
+ handle: NativeSimulatorHandle;
38
+ /** The raw shared memory as a Uint8Array (backed by WebAssembly.Memory). */
39
+ sharedMemory: Uint8Array;
40
+ }
41
+ /**
42
+ * Create a NativeSimulatorHandle-compatible wrapper from a wasm32-compiled
43
+ * NAPI handle.
44
+ *
45
+ * 1. Reads metadata (layout, events, sizes) from the raw handle.
46
+ * 2. Creates a WebAssembly.Memory large enough for the simulation state.
47
+ * 3. Synchronously compiles and instantiates the combinational and event
48
+ * WASM modules, importing the shared memory.
49
+ * 4. Returns a handle whose `tick()` / `evalComb()` drive the WASM instances.
50
+ *
51
+ * @returns A bridge result with the wrapped handle and shared memory view.
52
+ */
53
+ export declare function createWasmSimulatorBridge(raw: RawWasmSimulatorHandle): WasmBridgeResult;
54
+ /**
55
+ * Asynchronous version of `createWasmSimulatorBridge` that uses
56
+ * `WebAssembly.compile()` + `WebAssembly.instantiate()`.
57
+ *
58
+ * Preferred for large modules in browsers where synchronous compilation
59
+ * may be rejected.
60
+ */
61
+ export declare function createWasmSimulatorBridgeAsync(raw: RawWasmSimulatorHandle): Promise<WasmBridgeResult>;
62
+ //# sourceMappingURL=wasm-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasm-bridge.d.ts","sourceRoot":"","sources":["../src/wasm-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAmCxD;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,aAAa,IAAI,UAAU,GAAG,MAAM,EAAE,CAAC;IACvC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,EAAE,CAAC;IACpD,OAAO,IAAI,IAAI,CAAC;CAChB;AAMD;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,MAAM,EAAE,OAAO,GACb,MAAM,IAAI,sBAAsB,CAOlC;AAMD,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAChC,0DAA0D;IAC1D,MAAM,EAAE,qBAAqB,CAAC;IAC9B,4EAA4E;IAC5E,YAAY,EAAE,UAAU,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACxC,GAAG,EAAE,sBAAsB,GACzB,gBAAgB,CAqDlB;AAED;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CACnD,GAAG,EAAE,sBAAsB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CAsD3B"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * WASM bridge for running simulation via browser WebAssembly.
3
+ *
4
+ * When the NAPI addon is compiled for wasm32 (no JIT backend), the
5
+ * NativeSimulatorHandle exposes `combWasmBytes()` and `eventWasmBytes(name)`
6
+ * instead of `tick()` / `evalComb()`. This bridge instantiates those WASM
7
+ * modules and presents a handle that is compatible with the existing
8
+ * Simulator / DUT code.
9
+ *
10
+ * @module
11
+ */
12
+ // ---------------------------------------------------------------------------
13
+ // Detection
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Detect if a handle was produced by a wasm32-compiled addon.
17
+ *
18
+ * WASM handles expose `combWasmBytes()` instead of `tick()`.
19
+ */
20
+ export function isWasmHandle(handle) {
21
+ return (typeof handle === "object" &&
22
+ handle !== null &&
23
+ typeof handle.combWasmBytes === "function" &&
24
+ typeof handle.tick !== "function");
25
+ }
26
+ /**
27
+ * Create a NativeSimulatorHandle-compatible wrapper from a wasm32-compiled
28
+ * NAPI handle.
29
+ *
30
+ * 1. Reads metadata (layout, events, sizes) from the raw handle.
31
+ * 2. Creates a WebAssembly.Memory large enough for the simulation state.
32
+ * 3. Synchronously compiles and instantiates the combinational and event
33
+ * WASM modules, importing the shared memory.
34
+ * 4. Returns a handle whose `tick()` / `evalComb()` drive the WASM instances.
35
+ *
36
+ * @returns A bridge result with the wrapped handle and shared memory view.
37
+ */
38
+ export function createWasmSimulatorBridge(raw) {
39
+ const totalSize = raw.totalSize;
40
+ const stableSize = raw.stableSize;
41
+ // Create shared WebAssembly.Memory
42
+ const pages = Math.max(1, Math.ceil(totalSize / 65536));
43
+ const memory = new WebAssembly.Memory({ initial: pages });
44
+ // Compile and instantiate comb WASM module (synchronous)
45
+ const combBytes = new Uint8Array(raw.combWasmBytes());
46
+ const combModule = new WebAssembly.Module(combBytes);
47
+ const combInstance = new WebAssembly.Instance(combModule, {
48
+ env: { memory },
49
+ });
50
+ // Parse events and instantiate per-event WASM modules
51
+ const events = JSON.parse(raw.eventsJson);
52
+ const eventInstances = new Map();
53
+ for (const [name, id] of Object.entries(events)) {
54
+ const bytes = new Uint8Array(raw.eventWasmBytes(name));
55
+ const mod = new WebAssembly.Module(bytes);
56
+ const inst = new WebAssembly.Instance(mod, { env: { memory } });
57
+ eventInstances.set(id, inst);
58
+ }
59
+ const sharedMemory = new Uint8Array(memory.buffer, 0, stableSize);
60
+ const handle = {
61
+ tick(eventId) {
62
+ // eval_comb → eval_apply_ff → eval_comb
63
+ combInstance.exports.run();
64
+ const evInst = eventInstances.get(eventId);
65
+ if (evInst)
66
+ evInst.exports.run();
67
+ combInstance.exports.run();
68
+ },
69
+ tickN(eventId, count) {
70
+ for (let i = 0; i < count; i++) {
71
+ this.tick(eventId);
72
+ }
73
+ },
74
+ evalComb() {
75
+ combInstance.exports.run();
76
+ },
77
+ dump(_timestamp) {
78
+ // No VCD support in browser WASM mode
79
+ },
80
+ dispose() {
81
+ raw.dispose();
82
+ },
83
+ };
84
+ return { handle, sharedMemory };
85
+ }
86
+ /**
87
+ * Asynchronous version of `createWasmSimulatorBridge` that uses
88
+ * `WebAssembly.compile()` + `WebAssembly.instantiate()`.
89
+ *
90
+ * Preferred for large modules in browsers where synchronous compilation
91
+ * may be rejected.
92
+ */
93
+ export async function createWasmSimulatorBridgeAsync(raw) {
94
+ const totalSize = raw.totalSize;
95
+ const stableSize = raw.stableSize;
96
+ const pages = Math.max(1, Math.ceil(totalSize / 65536));
97
+ const memory = new WebAssembly.Memory({ initial: pages });
98
+ // Compile comb module
99
+ const combBytes = new Uint8Array(raw.combWasmBytes());
100
+ const combModule = await WebAssembly.compile(combBytes);
101
+ const combInstance = await WebAssembly.instantiate(combModule, {
102
+ env: { memory },
103
+ });
104
+ // Parse events and compile per-event modules
105
+ const events = JSON.parse(raw.eventsJson);
106
+ const eventInstances = new Map();
107
+ const eventEntries = Object.entries(events);
108
+ await Promise.all(eventEntries.map(async ([name, id]) => {
109
+ const bytes = new Uint8Array(raw.eventWasmBytes(name));
110
+ const mod = await WebAssembly.compile(bytes);
111
+ const inst = await WebAssembly.instantiate(mod, { env: { memory } });
112
+ eventInstances.set(id, inst);
113
+ }));
114
+ const sharedMemory = new Uint8Array(memory.buffer, 0, stableSize);
115
+ const handle = {
116
+ tick(eventId) {
117
+ combInstance.exports.run();
118
+ const evInst = eventInstances.get(eventId);
119
+ if (evInst)
120
+ evInst.exports.run();
121
+ combInstance.exports.run();
122
+ },
123
+ tickN(eventId, count) {
124
+ for (let i = 0; i < count; i++) {
125
+ this.tick(eventId);
126
+ }
127
+ },
128
+ evalComb() {
129
+ combInstance.exports.run();
130
+ },
131
+ dump(_timestamp) {
132
+ // No VCD support in browser WASM mode
133
+ },
134
+ dispose() {
135
+ raw.dispose();
136
+ },
137
+ };
138
+ return { handle, sharedMemory };
139
+ }
140
+ //# sourceMappingURL=wasm-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wasm-bridge.js","sourceRoot":"","sources":["../src/wasm-bridge.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celox-sim/celox",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "TypeScript runtime for Celox HDL simulation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
- "@celox-sim/celox-napi": "0.1.30"
32
+ "@celox-sim/celox-napi": "0.1.32"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "vitest": ">=1.0.0"
package/src/e2e.test.ts CHANGED
@@ -14,6 +14,7 @@ import { readFourState } from "./dut.js";
14
14
  import {
15
15
  createSimulatorBridge,
16
16
  loadNativeAddon,
17
+ type NapiTestResult,
17
18
  parseNapiLayout,
18
19
  parseSignalPath,
19
20
  type RawNapiAddon,
@@ -561,8 +562,8 @@ module InitTest (
561
562
  view.setUint8(sigA.offset, 0);
562
563
  view.setUint8(sigA.offset + sigA.byteSize, 0);
563
564
 
564
- // b = X (value=0, mask=1)
565
- view.setUint8(sigB.offset, 0);
565
+ // b = X (value=1, mask=1) — X encoding: v=1, m=1
566
+ view.setUint8(sigB.offset, 1);
566
567
  view.setUint8(sigB.offset + sigB.byteSize, 1);
567
568
 
568
569
  raw.evalComb();
@@ -572,9 +573,9 @@ module InitTest (
572
573
  expect(vAnd).toBe(0n);
573
574
  expect(mAnd).toBe(0n);
574
575
 
575
- // 0 | X = X (mask should be 1)
576
+ // 0 | X = X (value=1, mask=1 in X encoding)
576
577
  const [vOr, mOr] = readFourState(buf, sigYOr);
577
- expect(vOr).toBe(0n);
578
+ expect(vOr).toBe(1n);
578
579
  expect(mOr).toBe(1n);
579
580
  });
580
581
 
@@ -598,8 +599,8 @@ module InitTest (
598
599
  view.setUint8(sigA.offset, 1);
599
600
  view.setUint8(sigA.offset + sigA.byteSize, 0);
600
601
 
601
- // b = X (value=0, mask=1)
602
- view.setUint8(sigB.offset, 0);
602
+ // b = X (value=1, mask=1) — X encoding: v=1, m=1
603
+ view.setUint8(sigB.offset, 1);
603
604
  view.setUint8(sigB.offset + sigB.byteSize, 1);
604
605
 
605
606
  raw.evalComb();
@@ -1889,6 +1890,36 @@ describe("E2E: celox.toml test sources", () => {
1889
1890
  expect(err.steps).toBe(5);
1890
1891
  sim.dispose();
1891
1892
  });
1893
+
1894
+ test("exclude patterns filter out matching files", () => {
1895
+ // The fixture has test_veryl/Excluded.veryl with invalid syntax,
1896
+ // but celox.toml excludes it via `exclude = ["test_veryl/Excluded.veryl"]`.
1897
+ // If exclude works, fromProject succeeds (the broken file is skipped).
1898
+ // If exclude is broken, the parse error will cause an exception.
1899
+ const sim = Simulator.fromProject<{
1900
+ rst: bigint;
1901
+ d: bigint;
1902
+ readonly q: bigint;
1903
+ }>(CELOX_TOML_PROJECT, "Reg");
1904
+ sim.dut.rst = 1n;
1905
+ sim.dut.d = 42n;
1906
+ sim.tick();
1907
+ expect(sim.dut.q).toBe(42n);
1908
+ sim.dispose();
1909
+ });
1910
+
1911
+ test("genTs excludes files matching exclude patterns", () => {
1912
+ const addon = loadNativeAddon();
1913
+ const json = addon.genTs(CELOX_TOML_PROJECT);
1914
+ const output = JSON.parse(json) as {
1915
+ modules: Array<{ moduleName: string }>;
1916
+ };
1917
+ const names = output.modules.map((m) => m.moduleName);
1918
+ expect(names).toContain("Adder");
1919
+ expect(names).toContain("Reg");
1920
+ // Excluded.veryl is excluded by the glob pattern
1921
+ expect(names).not.toContain("Excluded");
1922
+ });
1892
1923
  });
1893
1924
 
1894
1925
  // ---------------------------------------------------------------------------
@@ -2074,7 +2105,7 @@ module Top (
2074
2105
  bus: modport Bus::consumer [2],
2075
2106
  out: output logic<8>,
2076
2107
  ) {
2077
- assign out = bus.data[0] + bus.data[1];
2108
+ assign out = bus[0].data + bus[1].data;
2078
2109
  }
2079
2110
  `;
2080
2111
 
@@ -2657,3 +2688,95 @@ describe("E2E: non-byte-aligned unpacked array port", () => {
2657
2688
  sim.dispose();
2658
2689
  });
2659
2690
  });
2691
+
2692
+ // ---------------------------------------------------------------------------
2693
+ // Native testbench (run_test / run_test_detailed)
2694
+ // ---------------------------------------------------------------------------
2695
+
2696
+ const TB_COUNTER_SOURCE = `
2697
+ module Counter_tb (
2698
+ clk: input clock ,
2699
+ rst: input reset ,
2700
+ cnt: output logic<32>,
2701
+ ) {
2702
+ always_ff {
2703
+ if_reset {
2704
+ cnt = 0;
2705
+ } else {
2706
+ cnt += 1;
2707
+ }
2708
+ }
2709
+ }
2710
+ `;
2711
+
2712
+ const TB_PASS_SOURCE = `
2713
+ ${TB_COUNTER_SOURCE}
2714
+ #[test(t)]
2715
+ module CounterTbPass {
2716
+ inst clk: $tb::clock_gen;
2717
+ inst rst: $tb::reset_gen;
2718
+ var cnt: logic<32>;
2719
+ inst dut: Counter_tb (clk, rst, cnt);
2720
+ initial {
2721
+ rst.assert(clk);
2722
+ clk.next (10);
2723
+ $assert (cnt == 32'd10);
2724
+ $finish ();
2725
+ }
2726
+ }
2727
+ `;
2728
+
2729
+ const TB_FAIL_SOURCE = `
2730
+ ${TB_COUNTER_SOURCE}
2731
+ #[test(t)]
2732
+ module CounterTbFail {
2733
+ inst clk: $tb::clock_gen;
2734
+ inst rst: $tb::reset_gen;
2735
+ var cnt: logic<32>;
2736
+ inst dut: Counter_tb (clk, rst, cnt);
2737
+ initial {
2738
+ rst.assert(clk);
2739
+ clk.next (5);
2740
+ $assert (cnt == 32'd99);
2741
+ $assert (cnt == 32'd5);
2742
+ $finish ();
2743
+ }
2744
+ }
2745
+ `;
2746
+
2747
+ describe("E2E: native testbench (runTest)", () => {
2748
+ const addon = loadNativeAddon();
2749
+
2750
+ test("passing testbench returns passed=true with all assertions passed", () => {
2751
+ const result: NapiTestResult = addon.runTest(
2752
+ [{ content: TB_PASS_SOURCE, path: "test.veryl" }],
2753
+ "CounterTbPass",
2754
+ );
2755
+ expect(result.passed).toBe(true);
2756
+ expect(result.assertions.length).toBe(1);
2757
+ expect(result.assertions[0]!.passed).toBe(true);
2758
+ });
2759
+
2760
+ test("failing testbench returns passed=false and collects all assertions", () => {
2761
+ const result: NapiTestResult = addon.runTest(
2762
+ [{ content: TB_FAIL_SOURCE, path: "test.veryl" }],
2763
+ "CounterTbFail",
2764
+ );
2765
+ expect(result.passed).toBe(false);
2766
+ // Both assertions are collected (not stopped at first failure)
2767
+ expect(result.assertions.length).toBe(2);
2768
+ expect(result.assertions[0]!.passed).toBe(false);
2769
+ expect(result.assertions[1]!.passed).toBe(true);
2770
+ });
2771
+
2772
+ test("assertion results include source location", () => {
2773
+ const result: NapiTestResult = addon.runTest(
2774
+ [{ content: TB_PASS_SOURCE, path: "test.veryl" }],
2775
+ "CounterTbPass",
2776
+ );
2777
+ const a = result.assertions[0]!;
2778
+ expect(a.file).toBeDefined();
2779
+ expect(a.line).toBeGreaterThan(0);
2780
+ expect(a.column).toBeGreaterThan(0);
2781
+ });
2782
+ });
package/src/index.ts CHANGED
@@ -66,5 +66,17 @@ export {
66
66
  Z,
67
67
  } from "./types.js";
68
68
 
69
+ // WASM bridge for browser simulation
70
+ /** @internal */
71
+ export type {
72
+ RawWasmSimulatorHandle,
73
+ WasmBridgeResult,
74
+ } from "./wasm-bridge.js";
75
+ export {
76
+ createWasmSimulatorBridge,
77
+ createWasmSimulatorBridgeAsync,
78
+ isWasmHandle,
79
+ } from "./wasm-bridge.js";
80
+
69
81
  // NAPI bridge (backward compat — re-exports from napi-helpers)
70
82
  // Consumers that import from "./napi-bridge.js" still work.
@@ -22,6 +22,7 @@ import type {
22
22
  SourceFile,
23
23
  TrueLoopSpec,
24
24
  } from "./types.js";
25
+ import { createWasmSimulatorBridge, isWasmHandle } from "./wasm-bridge.js";
25
26
 
26
27
  // ---------------------------------------------------------------------------
27
28
  // Raw NAPI handle shapes (what the .node addon actually exports)
@@ -34,11 +35,15 @@ export interface RawNapiSimulatorHandle {
34
35
  readonly warningsJson: string;
35
36
  readonly stableSize: number;
36
37
  readonly totalSize: number;
37
- tick(eventId: number): void;
38
- tickN(eventId: number, count: number): void;
39
- evalComb(): void;
40
- dump(timestamp: number): void;
41
- sharedMemory(): Uint8Array;
38
+ // Native (JIT) methods — present when built for native target
39
+ tick?(eventId: number): void;
40
+ tickN?(eventId: number, count: number): void;
41
+ evalComb?(): void;
42
+ dump?(timestamp: number): void;
43
+ sharedMemory?(): Uint8Array;
44
+ // WASM methods — present when built for wasm32 target
45
+ combWasmBytes?(): Uint8Array | number[];
46
+ eventWasmBytes?(name: string): Uint8Array | number[];
42
47
  dispose(): void;
43
48
  }
44
49
 
@@ -98,11 +103,14 @@ export interface NapiOptimizeOptions {
98
103
  inlineCommitForwarding?: boolean;
99
104
  eliminateDeadWorkingStores?: boolean;
100
105
  reschedule?: boolean;
106
+ coalesceStores?: boolean;
101
107
  }
102
108
 
103
109
  export interface NapiOptions {
104
110
  fourState?: boolean;
105
111
  vcd?: string;
112
+ optLevel?: string;
113
+ passOverrides?: string[];
106
114
  optimize?: boolean;
107
115
  optimizeOptions?: NapiOptimizeOptions;
108
116
  craneliftOptLevel?: string;
@@ -123,6 +131,19 @@ export interface NapiSourceFile {
123
131
  path: string;
124
132
  }
125
133
 
134
+ export interface NapiAssertionResult {
135
+ passed: boolean;
136
+ message?: string;
137
+ file?: string;
138
+ line?: number;
139
+ column?: number;
140
+ }
141
+
142
+ export interface NapiTestResult {
143
+ passed: boolean;
144
+ assertions: NapiAssertionResult[];
145
+ }
146
+
126
147
  export interface RawNapiAddon {
127
148
  NativeSimulatorHandle: {
128
149
  new (
@@ -150,6 +171,16 @@ export interface RawNapiAddon {
150
171
  };
151
172
  genTs(projectPath: string): string;
152
173
  clearJitCache(): void;
174
+ runTest(
175
+ sources: NapiSourceFile[],
176
+ top: string,
177
+ options?: NapiOptions,
178
+ ): NapiTestResult;
179
+ runTestFromProject(
180
+ projectPath: string,
181
+ top: string,
182
+ options?: NapiOptions,
183
+ ): NapiTestResult;
153
184
  }
154
185
 
155
186
  // ---------------------------------------------------------------------------
@@ -248,6 +279,18 @@ export function buildNapiOpts(
248
279
  napiOpts.vcd = options.vcd;
249
280
  hasOpt = true;
250
281
  }
282
+ if (options.optLevel) {
283
+ napiOpts.optLevel = options.optLevel;
284
+ hasOpt = true;
285
+ }
286
+ if (options.passOverrides && options.passOverrides.length > 0) {
287
+ // Convert camelCase pass names to snake_case for NAPI
288
+ napiOpts.passOverrides = options.passOverrides.map((s) => {
289
+ // Pass through +/- prefix and sir: prefix as-is; the Rust side parses them.
290
+ return s;
291
+ });
292
+ hasOpt = true;
293
+ }
251
294
  if (options.optimize != null) {
252
295
  napiOpts.optimize = options.optimize;
253
296
  hasOpt = true;
@@ -331,6 +374,7 @@ export function buildNapiOpts(
331
374
  if (oo.eliminateDeadWorkingStores != null)
332
375
  napiOo.eliminateDeadWorkingStores = oo.eliminateDeadWorkingStores;
333
376
  if (oo.reschedule != null) napiOo.reschedule = oo.reschedule;
377
+ if (oo.coalesceStores != null) napiOo.coalesceStores = oo.coalesceStores;
334
378
  napiOpts.optimizeOptions = napiOo;
335
379
  hasOpt = true;
336
380
  }
@@ -523,6 +567,19 @@ export function parseHierarchyLayout(
523
567
  events: Record<string, number>,
524
568
  ): HierarchyNode {
525
569
  const raw: RawHierarchyNode = JSON.parse(json);
570
+
571
+ // WASM-compiled addon may return an empty object `{}` for hierarchy.
572
+ // Treat it as an empty hierarchy node.
573
+ if (!raw.signals && !raw.module_name) {
574
+ return {
575
+ moduleName: "",
576
+ signals: {},
577
+ forDut: {},
578
+ ports: {},
579
+ children: {},
580
+ };
581
+ }
582
+
526
583
  return convertHierarchyNode(raw, events);
527
584
  }
528
585
 
@@ -603,16 +660,16 @@ export function wrapDirectSimulatorHandle(
603
660
  ): NativeSimulatorHandle {
604
661
  return {
605
662
  tick(eventId: number): void {
606
- raw.tick(eventId);
663
+ raw.tick!(eventId);
607
664
  },
608
665
  tickN(eventId: number, count: number): void {
609
- raw.tickN(eventId, count);
666
+ raw.tickN!(eventId, count);
610
667
  },
611
668
  evalComb(): void {
612
- raw.evalComb();
669
+ raw.evalComb!();
613
670
  },
614
671
  dump(timestamp: number): void {
615
- raw.dump(timestamp);
672
+ raw.dump!(timestamp);
616
673
  },
617
674
  dispose(): void {
618
675
  raw.dispose();
@@ -705,8 +762,17 @@ export function createSimulatorBridge(addon: RawNapiAddon): NativeCreateFn {
705
762
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
706
763
  const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
707
764
 
708
- const buf = raw.sharedMemory().buffer;
709
- const handle = wrapDirectSimulatorHandle(raw);
765
+ // Detect WASM-compiled addon and use the bridge
766
+ let buf: ArrayBuffer | SharedArrayBuffer;
767
+ let handle: NativeSimulatorHandle;
768
+ if (isWasmHandle(raw)) {
769
+ const bridge = createWasmSimulatorBridge(raw);
770
+ buf = bridge.sharedMemory.buffer;
771
+ handle = bridge.handle;
772
+ } else {
773
+ buf = raw.sharedMemory!().buffer;
774
+ handle = wrapDirectSimulatorHandle(raw);
775
+ }
710
776
 
711
777
  const warnings: string[] = JSON.parse(raw.warningsJson ?? "[]");
712
778
 
@@ -726,6 +792,14 @@ export function createSimulationBridge(
726
792
  moduleName: string,
727
793
  options: SimulatorOptions,
728
794
  ): CreateResult<NativeSimulationHandle> => {
795
+ // WASM addon does not export NativeSimulationHandle
796
+ if (!addon.NativeSimulationHandle) {
797
+ throw new Error(
798
+ "Simulation is not supported in WASM mode. " +
799
+ "The WASM-compiled addon only supports event-based Simulator, not time-based Simulation.",
800
+ );
801
+ }
802
+
729
803
  const napiOpts = buildNapiOpts(options);
730
804
  const napiSources = sources.map((s) => ({
731
805
  content: s.content,
package/src/simulation.ts CHANGED
@@ -25,6 +25,7 @@ import type {
25
25
  SourceFile,
26
26
  } from "./types.js";
27
27
  import { SimulationTimeoutError } from "./types.js";
28
+ import { isWasmHandle } from "./wasm-bridge.js";
28
29
 
29
30
  /**
30
31
  * Placeholder for the NAPI binding's `createSimulation()`.
@@ -190,12 +191,31 @@ export class Simulation<P = Record<string, unknown>> {
190
191
  ): Simulation<P> {
191
192
  const addon = loadNativeAddon(options?.nativeAddonPath);
192
193
  const napiOpts = buildNapiOpts(options);
194
+ // WASM addon does not support time-based Simulation (no NativeSimulationHandle).
195
+ // Check before attempting to construct the handle.
196
+ if (!addon.NativeSimulationHandle) {
197
+ throw new Error(
198
+ "Simulation is not supported in WASM mode. " +
199
+ "The WASM-compiled addon only supports event-based Simulator, not time-based Simulation. " +
200
+ "Use Simulator.fromSource() instead.",
201
+ );
202
+ }
203
+
193
204
  const raw = new addon.NativeSimulationHandle(
194
205
  [{ content: source, path: "" }],
195
206
  top,
196
207
  napiOpts,
197
208
  );
198
209
 
210
+ if (isWasmHandle(raw)) {
211
+ raw.dispose();
212
+ throw new Error(
213
+ "Simulation is not supported in WASM mode. " +
214
+ "The WASM-compiled addon only supports event-based Simulator, not time-based Simulation. " +
215
+ "Use Simulator.fromSource() instead.",
216
+ );
217
+ }
218
+
199
219
  const layout = parseNapiLayout(raw.layoutJson);
200
220
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
201
221
  const rawHierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
@@ -251,12 +271,31 @@ export class Simulation<P = Record<string, unknown>> {
251
271
  ): Simulation<P> {
252
272
  const addon = loadNativeAddon(options?.nativeAddonPath);
253
273
  const napiOpts = buildNapiOpts(options);
274
+
275
+ // WASM addon does not support time-based Simulation (no NativeSimulationHandle).
276
+ if (!addon.NativeSimulationHandle) {
277
+ throw new Error(
278
+ "Simulation is not supported in WASM mode. " +
279
+ "The WASM-compiled addon only supports event-based Simulator, not time-based Simulation. " +
280
+ "Use Simulator.fromProject() instead.",
281
+ );
282
+ }
283
+
254
284
  const raw = addon.NativeSimulationHandle.fromProject(
255
285
  projectPath,
256
286
  top,
257
287
  napiOpts,
258
288
  );
259
289
 
290
+ if (isWasmHandle(raw)) {
291
+ raw.dispose();
292
+ throw new Error(
293
+ "Simulation is not supported in WASM mode. " +
294
+ "The WASM-compiled addon only supports event-based Simulator, not time-based Simulation. " +
295
+ "Use Simulator.fromProject() instead.",
296
+ );
297
+ }
298
+
260
299
  const layout = parseNapiLayout(raw.layoutJson);
261
300
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
262
301
  const rawHierarchy = parseHierarchyLayout(raw.hierarchyJson, events);