@celox-sim/celox 0.1.13 → 0.1.15

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/simulator.ts CHANGED
@@ -5,24 +5,26 @@
5
5
  * for manually controlling clock edges via `tick()`.
6
6
  */
7
7
 
8
- import type {
9
- CreateResult,
10
- EventHandle,
11
- FourStateValue,
12
- ModuleDefinition,
13
- NativeSimulatorHandle,
14
- SignalLayout,
15
- SimulatorOptions,
16
- } from "./types.js";
17
- import { createDut, readFourState, type DirtyState } from "./dut.js";
8
+ import { createDut, type DirtyState, readFourState } from "./dut.js";
18
9
  import {
19
- loadNativeAddon,
20
- parseNapiLayout,
21
- parseHierarchyLayout,
22
- buildPortsFromLayout,
23
- wrapDirectSimulatorHandle,
24
- buildNapiOpts,
10
+ buildNapiOpts,
11
+ buildPortsFromLayout,
12
+ filterHierarchyForDse,
13
+ loadNativeAddon,
14
+ parseHierarchyLayout,
15
+ parseNapiLayout,
16
+ wrapDirectSimulatorHandle,
25
17
  } from "./napi-helpers.js";
18
+ import type {
19
+ CreateResult,
20
+ EventHandle,
21
+ FourStateValue,
22
+ ModuleDefinition,
23
+ NativeSimulatorHandle,
24
+ SignalLayout,
25
+ SimulatorOptions,
26
+ SourceFile,
27
+ } from "./types.js";
26
28
 
27
29
  /**
28
30
  * Placeholder for the NAPI binding's `createSimulator()`.
@@ -32,9 +34,9 @@ import {
32
34
  * @internal
33
35
  */
34
36
  export type NativeCreateFn = (
35
- source: string,
36
- moduleName: string,
37
- options: SimulatorOptions,
37
+ sources: ReadonlyArray<SourceFile>,
38
+ moduleName: string,
39
+ options: SimulatorOptions,
38
40
  ) => CreateResult<NativeSimulatorHandle>;
39
41
 
40
42
  let _nativeCreate: NativeCreateFn | undefined;
@@ -45,7 +47,7 @@ let _nativeCreate: NativeCreateFn | undefined;
45
47
  * @internal
46
48
  */
47
49
  export function setNativeSimulatorCreate(fn: NativeCreateFn): void {
48
- _nativeCreate = fn;
50
+ _nativeCreate = fn;
49
51
  }
50
52
 
51
53
  // ---------------------------------------------------------------------------
@@ -53,251 +55,310 @@ export function setNativeSimulatorCreate(fn: NativeCreateFn): void {
53
55
  // ---------------------------------------------------------------------------
54
56
 
55
57
  export class Simulator<P = Record<string, unknown>> {
56
- private readonly _handle: NativeSimulatorHandle;
57
- private readonly _dut: P;
58
- private readonly _events: Record<string, number>;
59
- private readonly _defaultEventId: number;
60
- private readonly _state: DirtyState;
61
- private readonly _buffer: ArrayBuffer | SharedArrayBuffer;
62
- private readonly _layout: Record<string, SignalLayout>;
63
- private _disposed = false;
64
-
65
- private constructor(
66
- handle: NativeSimulatorHandle,
67
- dut: P,
68
- events: Record<string, number>,
69
- state: DirtyState,
70
- buffer: ArrayBuffer | SharedArrayBuffer,
71
- layout: Record<string, SignalLayout>,
72
- ) {
73
- this._handle = handle;
74
- this._dut = dut;
75
- this._events = events;
76
- this._state = state;
77
- this._buffer = buffer;
78
- this._layout = layout;
79
- const keys = Object.keys(events);
80
- this._defaultEventId = keys.length > 0 ? events[keys[0]!]! : -1;
81
- }
82
-
83
- /**
84
- * Create a Simulator for the given module.
85
- *
86
- * ```ts
87
- * import { Adder } from "./generated/Adder.js";
88
- * const sim = Simulator.create(Adder);
89
- * ```
90
- */
91
- static create<P>(
92
- module: ModuleDefinition<P>,
93
- options?: SimulatorOptions & {
94
- /** Override for testing — inject a mock NAPI create function. */
95
- __nativeCreate?: NativeCreateFn;
96
- },
97
- ): Simulator<P> {
98
- // When the module was produced by the Vite plugin, delegate to fromProject()
99
- if (module.projectPath && !options?.__nativeCreate) {
100
- return Simulator.fromProject<P>(module.projectPath, module.name, options);
101
- }
102
-
103
- const createFn = options?.__nativeCreate ?? _nativeCreate;
104
- if (!createFn) {
105
- throw new Error(
106
- "Native simulator binding not loaded. " +
107
- "Ensure @celox-sim/celox-napi is installed.",
108
- );
109
- }
110
-
111
- const { fourState, vcd, optimize, falseLoops, trueLoops, clockType, resetType, parameters } = options ?? {};
112
- const result = createFn(module.source, module.name, { fourState, vcd, optimize, falseLoops, trueLoops, clockType, resetType, parameters });
113
- const state: DirtyState = { dirty: false };
114
-
115
- // Always prefer NAPI-derived ports (from hierarchy) over module.ports.
116
- // module.ports has widths/arrayDims baked at generation time, which become
117
- // stale when parameters are overridden. hierarchy.ports reflects the actual
118
- // compiled layout, consistent with fromSource()/fromProject().
119
- const portDefs = result.hierarchy?.ports ?? module.ports;
120
- const dut = createDut<P>(
121
- result.buffer,
122
- result.layout,
123
- portDefs,
124
- result.handle,
125
- state,
126
- result.hierarchy,
127
- );
128
-
129
- return new Simulator<P>(result.handle, dut, result.events, state, result.buffer, result.layout);
130
- }
131
-
132
- /**
133
- * Create a Simulator directly from Veryl source code.
134
- *
135
- * Automatically discovers ports from the NAPI layout — no
136
- * `ModuleDefinition` needed.
137
- *
138
- * ```ts
139
- * const sim = Simulator.fromSource<AdderPorts>(ADDER_SOURCE, "Adder");
140
- * sim.dut.a = 100;
141
- * sim.dut.b = 200;
142
- * sim.tick();
143
- * expect(sim.dut.sum).toBe(300);
144
- * ```
145
- */
146
- static fromSource<P = Record<string, unknown>>(
147
- source: string,
148
- top: string,
149
- options?: SimulatorOptions & { nativeAddonPath?: string },
150
- ): Simulator<P> {
151
- const addon = loadNativeAddon(options?.nativeAddonPath);
152
- const napiOpts = buildNapiOpts(options);
153
- const raw = new addon.NativeSimulatorHandle(source, top, napiOpts);
154
-
155
- const layout = parseNapiLayout(raw.layoutJson);
156
- const events: Record<string, number> = JSON.parse(raw.eventsJson);
157
- const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
158
-
159
- const ports = buildPortsFromLayout(layout.signals, events);
160
-
161
- const buf = raw.sharedMemory().buffer;
162
-
163
- const state: DirtyState = { dirty: false };
164
- const handle = wrapDirectSimulatorHandle(raw);
165
- const dut = createDut<P>(buf, layout.forDut, ports, handle, state, hierarchy);
166
-
167
- return new Simulator<P>(handle, dut, events, state, buf, layout.forDut);
168
- }
169
-
170
- /**
171
- * Create a Simulator from a Veryl project directory.
172
- *
173
- * Searches upward from `projectPath` for `Veryl.toml`, gathers all
174
- * `.veryl` source files, and builds the simulator using the project's
175
- * clock/reset settings.
176
- *
177
- * ```ts
178
- * const sim = Simulator.fromProject<MyPorts>("./my-project", "Top");
179
- * ```
180
- */
181
- static fromProject<P = Record<string, unknown>>(
182
- projectPath: string,
183
- top: string,
184
- options?: SimulatorOptions & { nativeAddonPath?: string },
185
- ): Simulator<P> {
186
- const addon = loadNativeAddon(options?.nativeAddonPath);
187
- const napiOpts = buildNapiOpts(options);
188
- const raw = addon.NativeSimulatorHandle.fromProject(projectPath, top, napiOpts);
189
-
190
- const layout = parseNapiLayout(raw.layoutJson);
191
- const events: Record<string, number> = JSON.parse(raw.eventsJson);
192
- const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
193
-
194
- const ports = buildPortsFromLayout(layout.signals, events);
195
-
196
- const buf = raw.sharedMemory().buffer;
197
-
198
- const state: DirtyState = { dirty: false };
199
- const handle = wrapDirectSimulatorHandle(raw);
200
- const dut = createDut<P>(buf, layout.forDut, ports, handle, state, hierarchy);
201
-
202
- return new Simulator<P>(handle, dut, events, state, buf, layout.forDut);
203
- }
204
-
205
- /** The DUT accessor object — read/write ports as plain properties. */
206
- get dut(): P {
207
- return this._dut;
208
- }
209
-
210
- /**
211
- * Trigger a clock edge.
212
- *
213
- * @param event Optional event handle from `this.event()`.
214
- * If omitted, ticks the first (default) event.
215
- * @param count Number of ticks. Default: 1.
216
- */
217
- tick(event?: EventHandle | number, count?: number): void;
218
- tick(count?: number): void;
219
- tick(
220
- eventOrCount?: EventHandle | number,
221
- count?: number,
222
- ): void {
223
- this.ensureAlive();
224
-
225
- let eventId: number;
226
- let ticks: number;
227
-
228
- if (typeof eventOrCount === "object" && eventOrCount !== null) {
229
- // tick(eventHandle, count?)
230
- eventId = (eventOrCount as EventHandle).id;
231
- ticks = count ?? 1;
232
- } else if (typeof eventOrCount === "number") {
233
- // tick(count) — default event
234
- eventId = this._defaultEventId;
235
- ticks = eventOrCount;
236
- } else {
237
- // tick() — default event, 1 tick
238
- eventId = this._defaultEventId;
239
- ticks = 1;
240
- }
241
-
242
- if (this._state.dirty) {
243
- this._handle.evalComb();
244
- this._state.dirty = false;
245
- }
246
-
247
- if (ticks === 1) {
248
- this._handle.tick(eventId);
249
- } else if (ticks > 1) {
250
- this._handle.tickN(eventId, ticks);
251
- }
252
- }
253
-
254
- /** Resolve an event name to a handle for use with `tick()`. */
255
- event(name: string): EventHandle {
256
- const id = this._events[name];
257
- if (id === undefined) {
258
- throw new Error(
259
- `Unknown event '${name}'. Available: ${Object.keys(this._events).join(", ")}`,
260
- );
261
- }
262
- return { name, id };
263
- }
264
-
265
- /**
266
- * Read the raw 4-state (value + mask) pair for the named port.
267
- */
268
- fourState(portName: string): FourStateValue {
269
- this.ensureAlive();
270
- const sig = this._layout[portName];
271
- if (!sig) {
272
- throw new Error(
273
- `Unknown port '${portName}'. Available: ${Object.keys(this._layout).join(", ")}`,
274
- );
275
- }
276
- const [value, mask] = readFourState(this._buffer, sig);
277
- return { __fourState: true, value, mask };
278
- }
279
-
280
- /** Write current signal values to VCD at the given timestamp. */
281
- dump(timestamp: number): void {
282
- this.ensureAlive();
283
- this._handle.dump(timestamp);
284
- }
285
-
286
- /** Release native resources. */
287
- dispose(): void {
288
- if (!this._disposed) {
289
- this._disposed = true;
290
- this._handle.dispose();
291
- }
292
- }
293
-
294
- // -----------------------------------------------------------------------
295
- // Internal
296
- // -----------------------------------------------------------------------
297
-
298
- private ensureAlive(): void {
299
- if (this._disposed) {
300
- throw new Error("Simulator has been disposed");
301
- }
302
- }
58
+ private readonly _handle: NativeSimulatorHandle;
59
+ private readonly _dut: P;
60
+ private readonly _events: Record<string, number>;
61
+ private readonly _defaultEventId: number;
62
+ private readonly _state: DirtyState;
63
+ private readonly _buffer: ArrayBuffer | SharedArrayBuffer;
64
+ private readonly _layout: Record<string, SignalLayout>;
65
+ private _disposed = false;
66
+
67
+ private constructor(
68
+ handle: NativeSimulatorHandle,
69
+ dut: P,
70
+ events: Record<string, number>,
71
+ state: DirtyState,
72
+ buffer: ArrayBuffer | SharedArrayBuffer,
73
+ layout: Record<string, SignalLayout>,
74
+ ) {
75
+ this._handle = handle;
76
+ this._dut = dut;
77
+ this._events = events;
78
+ this._state = state;
79
+ this._buffer = buffer;
80
+ this._layout = layout;
81
+ const keys = Object.keys(events);
82
+ this._defaultEventId = keys.length > 0 ? events[keys[0]!]! : -1;
83
+ }
84
+
85
+ /**
86
+ * Create a Simulator for the given module.
87
+ *
88
+ * ```ts
89
+ * import { Adder } from "./generated/Adder.js";
90
+ * const sim = Simulator.create(Adder);
91
+ * ```
92
+ */
93
+ static create<P>(
94
+ module: ModuleDefinition<P>,
95
+ options?: SimulatorOptions & {
96
+ /** Override for testing — inject a mock NAPI create function. */
97
+ __nativeCreate?: NativeCreateFn;
98
+ },
99
+ ): Simulator<P> {
100
+ const merged = { ...module.defaultOptions, ...options };
101
+
102
+ // When the module was produced by the Vite plugin, delegate to fromProject()
103
+ if (module.projectPath && !merged?.__nativeCreate) {
104
+ return Simulator.fromProject<P>(module.projectPath, module.name, merged);
105
+ }
106
+
107
+ const createFn = merged?.__nativeCreate ?? _nativeCreate;
108
+ if (!createFn) {
109
+ throw new Error(
110
+ "Native simulator binding not loaded. " +
111
+ "Ensure @celox-sim/celox-napi is installed.",
112
+ );
113
+ }
114
+
115
+ const {
116
+ fourState,
117
+ vcd,
118
+ optimize,
119
+ falseLoops,
120
+ trueLoops,
121
+ clockType,
122
+ resetType,
123
+ parameters,
124
+ deadStorePolicy,
125
+ } = merged ?? {};
126
+ const result = createFn(module.sources, module.name, {
127
+ fourState,
128
+ vcd,
129
+ optimize,
130
+ falseLoops,
131
+ trueLoops,
132
+ clockType,
133
+ resetType,
134
+ parameters,
135
+ deadStorePolicy,
136
+ });
137
+ const state: DirtyState = { dirty: false };
138
+
139
+ // Always prefer NAPI-derived ports (from hierarchy) over module.ports.
140
+ // module.ports has widths/arrayDims baked at generation time, which become
141
+ // stale when parameters are overridden. hierarchy.ports reflects the actual
142
+ // compiled layout, consistent with fromSource()/fromProject().
143
+ const hierarchy = result.hierarchy
144
+ ? filterHierarchyForDse(result.hierarchy, deadStorePolicy)
145
+ : undefined;
146
+ const portDefs = hierarchy?.ports ?? module.ports;
147
+ const dut = createDut<P>(
148
+ result.buffer,
149
+ result.layout,
150
+ portDefs,
151
+ result.handle,
152
+ state,
153
+ hierarchy,
154
+ );
155
+
156
+ return new Simulator<P>(
157
+ result.handle,
158
+ dut,
159
+ result.events,
160
+ state,
161
+ result.buffer,
162
+ result.layout,
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Create a Simulator directly from Veryl source code.
168
+ *
169
+ * Automatically discovers ports from the NAPI layout — no
170
+ * `ModuleDefinition` needed.
171
+ *
172
+ * ```ts
173
+ * const sim = Simulator.fromSource<AdderPorts>(ADDER_SOURCE, "Adder");
174
+ * sim.dut.a = 100;
175
+ * sim.dut.b = 200;
176
+ * sim.tick();
177
+ * expect(sim.dut.sum).toBe(300);
178
+ * ```
179
+ */
180
+ static fromSource<P = Record<string, unknown>>(
181
+ source: string,
182
+ top: string,
183
+ options?: SimulatorOptions & { nativeAddonPath?: string },
184
+ ): Simulator<P> {
185
+ const addon = loadNativeAddon(options?.nativeAddonPath);
186
+ const napiOpts = buildNapiOpts(options);
187
+ const raw = new addon.NativeSimulatorHandle(
188
+ [{ content: source, path: "" }],
189
+ top,
190
+ napiOpts,
191
+ );
192
+
193
+ const layout = parseNapiLayout(raw.layoutJson);
194
+ const events: Record<string, number> = JSON.parse(raw.eventsJson);
195
+ const rawHierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
196
+ const hierarchy = filterHierarchyForDse(
197
+ rawHierarchy,
198
+ options?.deadStorePolicy,
199
+ );
200
+
201
+ const ports = buildPortsFromLayout(hierarchy.signals, events);
202
+
203
+ const buf = raw.sharedMemory().buffer;
204
+
205
+ const state: DirtyState = { dirty: false };
206
+ const handle = wrapDirectSimulatorHandle(raw);
207
+ const dut = createDut<P>(
208
+ buf,
209
+ layout.forDut,
210
+ ports,
211
+ handle,
212
+ state,
213
+ hierarchy,
214
+ );
215
+
216
+ return new Simulator<P>(handle, dut, events, state, buf, layout.forDut);
217
+ }
218
+
219
+ /**
220
+ * Create a Simulator from a Veryl project directory.
221
+ *
222
+ * Searches upward from `projectPath` for `Veryl.toml`, gathers all
223
+ * `.veryl` source files, and builds the simulator using the project's
224
+ * clock/reset settings.
225
+ *
226
+ * ```ts
227
+ * const sim = Simulator.fromProject<MyPorts>("./my-project", "Top");
228
+ * ```
229
+ */
230
+ static fromProject<P = Record<string, unknown>>(
231
+ projectPath: string,
232
+ top: string,
233
+ options?: SimulatorOptions & { nativeAddonPath?: string },
234
+ ): Simulator<P> {
235
+ const addon = loadNativeAddon(options?.nativeAddonPath);
236
+ const napiOpts = buildNapiOpts(options);
237
+ const raw = addon.NativeSimulatorHandle.fromProject(
238
+ projectPath,
239
+ top,
240
+ napiOpts,
241
+ );
242
+
243
+ const layout = parseNapiLayout(raw.layoutJson);
244
+ const events: Record<string, number> = JSON.parse(raw.eventsJson);
245
+ const rawHierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
246
+ const hierarchy = filterHierarchyForDse(
247
+ rawHierarchy,
248
+ options?.deadStorePolicy,
249
+ );
250
+
251
+ const ports = buildPortsFromLayout(hierarchy.signals, events);
252
+
253
+ const buf = raw.sharedMemory().buffer;
254
+
255
+ const state: DirtyState = { dirty: false };
256
+ const handle = wrapDirectSimulatorHandle(raw);
257
+ const dut = createDut<P>(
258
+ buf,
259
+ layout.forDut,
260
+ ports,
261
+ handle,
262
+ state,
263
+ hierarchy,
264
+ );
265
+
266
+ return new Simulator<P>(handle, dut, events, state, buf, layout.forDut);
267
+ }
268
+
269
+ /** The DUT accessor object — read/write ports as plain properties. */
270
+ get dut(): P {
271
+ return this._dut;
272
+ }
273
+
274
+ /**
275
+ * Trigger a clock edge.
276
+ *
277
+ * @param event Optional event handle from `this.event()`.
278
+ * If omitted, ticks the first (default) event.
279
+ * @param count Number of ticks. Default: 1.
280
+ */
281
+ tick(event?: EventHandle | number, count?: number): void;
282
+ tick(count?: number): void;
283
+ tick(eventOrCount?: EventHandle | number, count?: number): void {
284
+ this.ensureAlive();
285
+
286
+ let eventId: number;
287
+ let ticks: number;
288
+
289
+ if (typeof eventOrCount === "object" && eventOrCount !== null) {
290
+ // tick(eventHandle, count?)
291
+ eventId = (eventOrCount as EventHandle).id;
292
+ ticks = count ?? 1;
293
+ } else if (typeof eventOrCount === "number") {
294
+ // tick(count) — default event
295
+ eventId = this._defaultEventId;
296
+ ticks = eventOrCount;
297
+ } else {
298
+ // tick() — default event, 1 tick
299
+ eventId = this._defaultEventId;
300
+ ticks = 1;
301
+ }
302
+
303
+ if (this._state.dirty) {
304
+ this._handle.evalComb();
305
+ this._state.dirty = false;
306
+ }
307
+
308
+ if (ticks === 1) {
309
+ this._handle.tick(eventId);
310
+ } else if (ticks > 1) {
311
+ this._handle.tickN(eventId, ticks);
312
+ }
313
+ }
314
+
315
+ /** Resolve an event name to a handle for use with `tick()`. */
316
+ event(name: string): EventHandle {
317
+ const id = this._events[name];
318
+ if (id === undefined) {
319
+ throw new Error(
320
+ `Unknown event '${name}'. Available: ${Object.keys(this._events).join(", ")}`,
321
+ );
322
+ }
323
+ return { name, id };
324
+ }
325
+
326
+ /**
327
+ * Read the raw 4-state (value + mask) pair for the named port.
328
+ */
329
+ fourState(portName: string): FourStateValue {
330
+ this.ensureAlive();
331
+ const sig = this._layout[portName];
332
+ if (!sig) {
333
+ throw new Error(
334
+ `Unknown port '${portName}'. Available: ${Object.keys(this._layout).join(", ")}`,
335
+ );
336
+ }
337
+ const [value, mask] = readFourState(this._buffer, sig);
338
+ return { __fourState: true, value, mask };
339
+ }
340
+
341
+ /** Write current signal values to VCD at the given timestamp. */
342
+ dump(timestamp: number): void {
343
+ this.ensureAlive();
344
+ this._handle.dump(timestamp);
345
+ }
346
+
347
+ /** Release native resources. */
348
+ dispose(): void {
349
+ if (!this._disposed) {
350
+ this._disposed = true;
351
+ this._handle.dispose();
352
+ }
353
+ }
354
+
355
+ // -----------------------------------------------------------------------
356
+ // Internal
357
+ // -----------------------------------------------------------------------
358
+
359
+ private ensureAlive(): void {
360
+ if (this._disposed) {
361
+ throw new Error("Simulator has been disposed");
362
+ }
363
+ }
303
364
  }