@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.
@@ -9,104 +9,127 @@
9
9
  * - Creating bridge functions for Simulator.create() / Simulation.create()
10
10
  */
11
11
 
12
+ import type { NativeCreateSimulationFn } from "./simulation.js";
13
+ import type { NativeCreateFn } from "./simulator.js";
12
14
  import type {
13
- CreateResult,
14
- LoopBreak,
15
- NativeSimulatorHandle,
16
- NativeSimulationHandle,
17
- PortInfo,
18
- SignalLayout,
19
- SimulatorOptions,
20
- TrueLoopSpec,
15
+ CreateResult,
16
+ LoopBreak,
17
+ NativeSimulationHandle,
18
+ NativeSimulatorHandle,
19
+ PortInfo,
20
+ SignalLayout,
21
+ SimulatorOptions,
22
+ SourceFile,
23
+ TrueLoopSpec,
21
24
  } from "./types.js";
22
- import type { NativeCreateFn } from "./simulator.js";
23
- import type { NativeCreateSimulationFn } from "./simulation.js";
24
25
 
25
26
  // ---------------------------------------------------------------------------
26
27
  // Raw NAPI handle shapes (what the .node addon actually exports)
27
28
  // ---------------------------------------------------------------------------
28
29
 
29
30
  export interface RawNapiSimulatorHandle {
30
- readonly layoutJson: string;
31
- readonly eventsJson: string;
32
- readonly hierarchyJson: string;
33
- readonly stableSize: number;
34
- readonly totalSize: number;
35
- tick(eventId: number): void;
36
- tickN(eventId: number, count: number): void;
37
- evalComb(): void;
38
- dump(timestamp: number): void;
39
- sharedMemory(): Uint8Array;
40
- dispose(): void;
31
+ readonly layoutJson: string;
32
+ readonly eventsJson: string;
33
+ readonly hierarchyJson: string;
34
+ readonly stableSize: number;
35
+ readonly totalSize: number;
36
+ tick(eventId: number): void;
37
+ tickN(eventId: number, count: number): void;
38
+ evalComb(): void;
39
+ dump(timestamp: number): void;
40
+ sharedMemory(): Uint8Array;
41
+ dispose(): void;
41
42
  }
42
43
 
43
44
  export interface RawNapiSimulationHandle {
44
- readonly layoutJson: string;
45
- readonly eventsJson: string;
46
- readonly hierarchyJson: string;
47
- readonly stableSize: number;
48
- readonly totalSize: number;
49
- readonly defaultMaxSteps: number | null;
50
- addClock(eventId: number, period: number, initialDelay: number): void;
51
- schedule(eventId: number, time: number, value: number): void;
52
- runUntil(endTime: number): void;
53
- step(): number | null;
54
- time(): number;
55
- nextEventTime(): number | null;
56
- evalComb(): void;
57
- dump(timestamp: number): void;
58
- sharedMemory(): Uint8Array;
59
- dispose(): void;
45
+ readonly layoutJson: string;
46
+ readonly eventsJson: string;
47
+ readonly hierarchyJson: string;
48
+ readonly stableSize: number;
49
+ readonly totalSize: number;
50
+ readonly defaultMaxSteps: number | null;
51
+ addClock(eventId: number, period: number, initialDelay: number): void;
52
+ schedule(eventId: number, time: number, value: number): void;
53
+ runUntil(endTime: number): void;
54
+ step(): number | null;
55
+ time(): number;
56
+ nextEventTime(): number | null;
57
+ evalComb(): void;
58
+ dump(timestamp: number): void;
59
+ sharedMemory(): Uint8Array;
60
+ dispose(): void;
60
61
  }
61
62
 
62
63
  export interface NapiFalseLoop {
63
- from: NapiSignalPath;
64
- to: NapiSignalPath;
64
+ from: NapiSignalPath;
65
+ to: NapiSignalPath;
65
66
  }
66
67
 
67
68
  export interface NapiTrueLoop {
68
- from: NapiSignalPath;
69
- to: NapiSignalPath;
70
- maxIter: number;
69
+ from: NapiSignalPath;
70
+ to: NapiSignalPath;
71
+ maxIter: number;
71
72
  }
72
73
 
73
74
  export interface NapiSignalPath {
74
- instancePath: NapiInstanceSegment[];
75
- varPath: string[];
75
+ instancePath: NapiInstanceSegment[];
76
+ varPath: string[];
76
77
  }
77
78
 
78
79
  export interface NapiInstanceSegment {
79
- name: string;
80
- index: number;
80
+ name: string;
81
+ index: number;
81
82
  }
82
83
 
83
84
  export interface NapiParamOverride {
84
- name: string;
85
- value: number;
85
+ name: string;
86
+ value: number;
86
87
  }
87
88
 
88
89
  export interface NapiOptions {
89
- fourState?: boolean;
90
- vcd?: string;
91
- optimize?: boolean;
92
- falseLoops?: NapiFalseLoop[];
93
- trueLoops?: NapiTrueLoop[];
94
- clockType?: string;
95
- resetType?: string;
96
- extraSource?: string;
97
- parameters?: NapiParamOverride[];
90
+ fourState?: boolean;
91
+ vcd?: string;
92
+ optimize?: boolean;
93
+ falseLoops?: NapiFalseLoop[];
94
+ trueLoops?: NapiTrueLoop[];
95
+ clockType?: string;
96
+ resetType?: string;
97
+ extraSource?: string;
98
+ parameters?: NapiParamOverride[];
99
+ deadStorePolicy?: string;
100
+ }
101
+
102
+ export interface NapiSourceFile {
103
+ content: string;
104
+ path: string;
98
105
  }
99
106
 
100
107
  export interface RawNapiAddon {
101
- NativeSimulatorHandle: {
102
- new (code: string, top: string, options?: NapiOptions): RawNapiSimulatorHandle;
103
- fromProject(projectPath: string, top: string, options?: NapiOptions): RawNapiSimulatorHandle;
104
- };
105
- NativeSimulationHandle: {
106
- new (code: string, top: string, options?: NapiOptions): RawNapiSimulationHandle;
107
- fromProject(projectPath: string, top: string, options?: NapiOptions): RawNapiSimulationHandle;
108
- };
109
- genTs(projectPath: string): string;
108
+ NativeSimulatorHandle: {
109
+ new (
110
+ sources: NapiSourceFile[],
111
+ top: string,
112
+ options?: NapiOptions,
113
+ ): RawNapiSimulatorHandle;
114
+ fromProject(
115
+ projectPath: string,
116
+ top: string,
117
+ options?: NapiOptions,
118
+ ): RawNapiSimulatorHandle;
119
+ };
120
+ NativeSimulationHandle: {
121
+ new (
122
+ sources: NapiSourceFile[],
123
+ top: string,
124
+ options?: NapiOptions,
125
+ ): RawNapiSimulationHandle;
126
+ fromProject(
127
+ projectPath: string,
128
+ top: string,
129
+ options?: NapiOptions,
130
+ ): RawNapiSimulationHandle;
131
+ };
132
+ genTs(projectPath: string): string;
110
133
  }
111
134
 
112
135
  // ---------------------------------------------------------------------------
@@ -125,21 +148,21 @@ import { createRequire } from "node:module";
125
148
  * @param addonPath Explicit path to the `.node` file (overrides auto-detection).
126
149
  */
127
150
  export function loadNativeAddon(addonPath?: string): RawNapiAddon {
128
- const require = createRequire(import.meta.url);
129
-
130
- if (addonPath) {
131
- return require(addonPath) as RawNapiAddon;
132
- }
133
-
134
- try {
135
- return require("@celox-sim/celox-napi") as RawNapiAddon;
136
- } catch (e) {
137
- throw new Error(
138
- `Failed to load NAPI addon from @celox-sim/celox-napi. ` +
139
- `Build it with: pnpm run build:napi`,
140
- { cause: e },
141
- );
142
- }
151
+ const require = createRequire(import.meta.url);
152
+
153
+ if (addonPath) {
154
+ return require(addonPath) as RawNapiAddon;
155
+ }
156
+
157
+ try {
158
+ return require("@celox-sim/celox-napi") as RawNapiAddon;
159
+ } catch (e) {
160
+ throw new Error(
161
+ `Failed to load NAPI addon from @celox-sim/celox-napi. ` +
162
+ `Build it with: pnpm run build:napi`,
163
+ { cause: e },
164
+ );
165
+ }
143
166
  }
144
167
 
145
168
  // ---------------------------------------------------------------------------
@@ -160,89 +183,100 @@ export function loadNativeAddon(addonPath?: string): RawNapiAddon {
160
183
  * - `"a.b[3]:x.y"` → { instancePath: [{name:"a",index:0},{name:"b",index:3}], varPath: ["x","y"] }
161
184
  */
162
185
  export function parseSignalPath(path: string): NapiSignalPath {
163
- const colonIdx = path.indexOf(":");
164
- if (colonIdx < 0) {
165
- return { instancePath: [], varPath: path.split(".") };
166
- }
167
-
168
- const instPart = path.slice(0, colonIdx);
169
- const varPart = path.slice(colonIdx + 1);
170
-
171
- const instancePath: NapiInstanceSegment[] = [];
172
- if (instPart.length > 0) {
173
- for (const seg of instPart.split(".")) {
174
- const bracketIdx = seg.indexOf("[");
175
- if (bracketIdx >= 0) {
176
- const name = seg.slice(0, bracketIdx);
177
- const index = Number.parseInt(seg.slice(bracketIdx + 1, -1), 10);
178
- instancePath.push({ name, index });
179
- } else {
180
- instancePath.push({ name: seg, index: 0 });
181
- }
182
- }
183
- }
184
-
185
- return { instancePath, varPath: varPart.split(".") };
186
+ const colonIdx = path.indexOf(":");
187
+ if (colonIdx < 0) {
188
+ return { instancePath: [], varPath: path.split(".") };
189
+ }
190
+
191
+ const instPart = path.slice(0, colonIdx);
192
+ const varPart = path.slice(colonIdx + 1);
193
+
194
+ const instancePath: NapiInstanceSegment[] = [];
195
+ if (instPart.length > 0) {
196
+ for (const seg of instPart.split(".")) {
197
+ const bracketIdx = seg.indexOf("[");
198
+ if (bracketIdx >= 0) {
199
+ const name = seg.slice(0, bracketIdx);
200
+ const index = Number.parseInt(seg.slice(bracketIdx + 1, -1), 10);
201
+ instancePath.push({ name, index });
202
+ } else {
203
+ instancePath.push({ name: seg, index: 0 });
204
+ }
205
+ }
206
+ }
207
+
208
+ return { instancePath, varPath: varPart.split(".") };
186
209
  }
187
210
 
188
211
  /**
189
212
  * Build NapiOptions from SimulatorOptions.
190
213
  * Returns `undefined` when no options are set (to skip the NAPI options arg).
191
214
  */
192
- export function buildNapiOpts(options?: SimulatorOptions): NapiOptions | undefined {
193
- if (!options) return undefined;
194
-
195
- const napiOpts: NapiOptions = {};
196
- let hasOpt = false;
197
-
198
- if (options.fourState) {
199
- napiOpts.fourState = options.fourState;
200
- hasOpt = true;
201
- }
202
- if (options.vcd) {
203
- napiOpts.vcd = options.vcd;
204
- hasOpt = true;
205
- }
206
- if (options.optimize != null) {
207
- napiOpts.optimize = options.optimize;
208
- hasOpt = true;
209
- }
210
- if (options.falseLoops && options.falseLoops.length > 0) {
211
- napiOpts.falseLoops = options.falseLoops.map((lb: LoopBreak) => ({
212
- from: parseSignalPath(lb.from),
213
- to: parseSignalPath(lb.to),
214
- }));
215
- hasOpt = true;
216
- }
217
- if (options.trueLoops && options.trueLoops.length > 0) {
218
- napiOpts.trueLoops = options.trueLoops.map((tl: TrueLoopSpec) => ({
219
- from: parseSignalPath(tl.from),
220
- to: parseSignalPath(tl.to),
221
- maxIter: tl.maxIter,
222
- }));
223
- hasOpt = true;
224
- }
225
- if (options.clockType) {
226
- napiOpts.clockType = options.clockType;
227
- hasOpt = true;
228
- }
229
- if (options.resetType) {
230
- napiOpts.resetType = options.resetType;
231
- hasOpt = true;
232
- }
233
- if (options.extraSource) {
234
- napiOpts.extraSource = options.extraSource;
235
- hasOpt = true;
236
- }
237
- if (options.parameters && options.parameters.length > 0) {
238
- napiOpts.parameters = options.parameters.map((p) => ({
239
- name: p.name,
240
- value: typeof p.value === "bigint" ? Number(p.value) : p.value,
241
- }));
242
- hasOpt = true;
243
- }
244
-
245
- return hasOpt ? napiOpts : undefined;
215
+ export function buildNapiOpts(
216
+ options?: SimulatorOptions,
217
+ ): NapiOptions | undefined {
218
+ if (!options) return undefined;
219
+
220
+ const napiOpts: NapiOptions = {};
221
+ let hasOpt = false;
222
+
223
+ if (options.fourState) {
224
+ napiOpts.fourState = options.fourState;
225
+ hasOpt = true;
226
+ }
227
+ if (options.vcd) {
228
+ napiOpts.vcd = options.vcd;
229
+ hasOpt = true;
230
+ }
231
+ if (options.optimize != null) {
232
+ napiOpts.optimize = options.optimize;
233
+ hasOpt = true;
234
+ }
235
+ if (options.falseLoops && options.falseLoops.length > 0) {
236
+ napiOpts.falseLoops = options.falseLoops.map((lb: LoopBreak) => ({
237
+ from: parseSignalPath(lb.from),
238
+ to: parseSignalPath(lb.to),
239
+ }));
240
+ hasOpt = true;
241
+ }
242
+ if (options.trueLoops && options.trueLoops.length > 0) {
243
+ napiOpts.trueLoops = options.trueLoops.map((tl: TrueLoopSpec) => ({
244
+ from: parseSignalPath(tl.from),
245
+ to: parseSignalPath(tl.to),
246
+ maxIter: tl.maxIter,
247
+ }));
248
+ hasOpt = true;
249
+ }
250
+ if (options.clockType) {
251
+ napiOpts.clockType = options.clockType;
252
+ hasOpt = true;
253
+ }
254
+ if (options.resetType) {
255
+ napiOpts.resetType = options.resetType;
256
+ hasOpt = true;
257
+ }
258
+ if (options.extraSource) {
259
+ napiOpts.extraSource = options.extraSource;
260
+ hasOpt = true;
261
+ }
262
+ if (options.parameters && options.parameters.length > 0) {
263
+ napiOpts.parameters = options.parameters.map((p) => ({
264
+ name: p.name,
265
+ value: typeof p.value === "bigint" ? Number(p.value) : p.value,
266
+ }));
267
+ hasOpt = true;
268
+ }
269
+ if (options.deadStorePolicy && options.deadStorePolicy !== "off") {
270
+ const map: Record<string, string> = {
271
+ preserveTopPorts: "preserve_top_ports",
272
+ preserveAllPorts: "preserve_all_ports",
273
+ };
274
+ napiOpts.deadStorePolicy =
275
+ map[options.deadStorePolicy] ?? options.deadStorePolicy;
276
+ hasOpt = true;
277
+ }
278
+
279
+ return hasOpt ? napiOpts : undefined;
246
280
  }
247
281
 
248
282
  // ---------------------------------------------------------------------------
@@ -250,14 +284,14 @@ export function buildNapiOpts(options?: SimulatorOptions): NapiOptions | undefin
250
284
  // ---------------------------------------------------------------------------
251
285
 
252
286
  interface RawSignalLayout {
253
- offset: number;
254
- width: number;
255
- byte_size: number;
256
- is_4state: boolean;
257
- direction: string;
258
- type_kind: string;
259
- array_dims?: number[];
260
- associated_clock?: string;
287
+ offset: number;
288
+ width: number;
289
+ byte_size: number;
290
+ is_4state: boolean;
291
+ direction: string;
292
+ type_kind: string;
293
+ array_dims?: number[];
294
+ associated_clock?: string;
261
295
  }
262
296
 
263
297
  /**
@@ -266,36 +300,54 @@ interface RawSignalLayout {
266
300
  * the DUT-compatible layout (without type_kind).
267
301
  */
268
302
  export function parseNapiLayout(json: string): {
269
- signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[]; associatedClock?: string }>;
270
- forDut: Record<string, SignalLayout>;
303
+ signals: Record<
304
+ string,
305
+ SignalLayout & {
306
+ typeKind: string;
307
+ arrayDims?: number[];
308
+ associatedClock?: string;
309
+ }
310
+ >;
311
+ forDut: Record<string, SignalLayout>;
271
312
  } {
272
- const raw: Record<string, RawSignalLayout> = JSON.parse(json);
273
- const signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[]; associatedClock?: string }> = {};
274
- const forDut: Record<string, SignalLayout> = {};
275
-
276
- for (const [name, r] of Object.entries(raw)) {
277
- const sl: SignalLayout = {
278
- offset: r.offset,
279
- width: r.width,
280
- byteSize: r.byte_size > 0 ? r.byte_size : Math.ceil(r.width / 8),
281
- is4state: r.is_4state,
282
- direction: r.direction as "input" | "output" | "inout",
283
- };
284
- const entry: SignalLayout & { typeKind: string; arrayDims?: number[]; associatedClock?: string } = {
285
- ...sl,
286
- typeKind: r.type_kind,
287
- };
288
- if (r.array_dims && r.array_dims.length > 0) {
289
- entry.arrayDims = r.array_dims;
290
- }
291
- if (r.associated_clock) {
292
- entry.associatedClock = r.associated_clock;
293
- }
294
- signals[name] = entry;
295
- forDut[name] = sl;
296
- }
297
-
298
- return { signals, forDut };
313
+ const raw: Record<string, RawSignalLayout> = JSON.parse(json);
314
+ const signals: Record<
315
+ string,
316
+ SignalLayout & {
317
+ typeKind: string;
318
+ arrayDims?: number[];
319
+ associatedClock?: string;
320
+ }
321
+ > = {};
322
+ const forDut: Record<string, SignalLayout> = {};
323
+
324
+ for (const [name, r] of Object.entries(raw)) {
325
+ const sl: SignalLayout = {
326
+ offset: r.offset,
327
+ width: r.width,
328
+ byteSize: r.byte_size > 0 ? r.byte_size : Math.ceil(r.width / 8),
329
+ is4state: r.is_4state,
330
+ direction: r.direction as SignalLayout["direction"],
331
+ };
332
+ const entry: SignalLayout & {
333
+ typeKind: string;
334
+ arrayDims?: number[];
335
+ associatedClock?: string;
336
+ } = {
337
+ ...sl,
338
+ typeKind: r.type_kind,
339
+ };
340
+ if (r.array_dims && r.array_dims.length > 0) {
341
+ entry.arrayDims = r.array_dims;
342
+ }
343
+ if (r.associated_clock) {
344
+ entry.associatedClock = r.associated_clock;
345
+ }
346
+ signals[name] = entry;
347
+ forDut[name] = sl;
348
+ }
349
+
350
+ return { signals, forDut };
299
351
  }
300
352
 
301
353
  /**
@@ -307,67 +359,70 @@ export function parseNapiLayout(json: string): {
307
359
  * expose them as `dut.bus.data` and `dut.bus.valid`.
308
360
  */
309
361
  export function buildPortsFromLayout(
310
- signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }>,
311
- _events: Record<string, number>,
362
+ signals: Record<
363
+ string,
364
+ SignalLayout & { typeKind: string; arrayDims?: number[] }
365
+ >,
366
+ _events: Record<string, number>,
312
367
  ): Record<string, PortInfo> {
313
- // Step 1: Build flat PortInfo for every signal name
314
- const flat: Record<string, PortInfo> = {};
315
-
316
- for (const [name, sig] of Object.entries(signals)) {
317
- const typeKind = sig.typeKind;
318
- let portType: "clock" | "reset" | "logic" | "bit";
319
- if (typeKind === "clock") {
320
- portType = "clock";
321
- } else if (typeKind.startsWith("reset")) {
322
- portType = "reset";
323
- } else if (typeKind === "bit") {
324
- portType = "bit";
325
- } else {
326
- portType = "logic";
327
- }
328
-
329
- const port: PortInfo = {
330
- direction: sig.direction,
331
- type: portType,
332
- width: sig.width,
333
- is4state: sig.is4state,
334
- };
335
- if (sig.arrayDims && sig.arrayDims.length > 0) {
336
- (port as { arrayDims: readonly number[] }).arrayDims = sig.arrayDims;
337
- }
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;
368
- }
369
-
370
- return ports;
368
+ // Step 1: Build flat PortInfo for every signal name
369
+ const flat: Record<string, PortInfo> = {};
370
+
371
+ for (const [name, sig] of Object.entries(signals)) {
372
+ const typeKind = sig.typeKind;
373
+ let portType: "clock" | "reset" | "logic" | "bit";
374
+ if (typeKind === "clock") {
375
+ portType = "clock";
376
+ } else if (typeKind.startsWith("reset")) {
377
+ portType = "reset";
378
+ } else if (typeKind === "bit") {
379
+ portType = "bit";
380
+ } else {
381
+ portType = "logic";
382
+ }
383
+
384
+ const port: PortInfo = {
385
+ direction: sig.direction,
386
+ type: portType,
387
+ width: sig.width,
388
+ is4state: sig.is4state,
389
+ };
390
+ if (sig.arrayDims && sig.arrayDims.length > 0) {
391
+ (port as { arrayDims: readonly number[] }).arrayDims = sig.arrayDims;
392
+ }
393
+ flat[name] = port;
394
+ }
395
+
396
+ // Step 2: Group hierarchical signals (e.g. "bus.data") into nested PortInfo.
397
+ // Veryl interface ports are exactly one level deep, so "bus.data" → parent "bus",
398
+ // member "data". The parent entry receives an `interface` map of its members;
399
+ // the flat layout keys are kept as-is so createNestedDut can look them up.
400
+ const ports: Record<string, PortInfo> = {};
401
+ const ifMaps = new Map<string, Record<string, PortInfo>>();
402
+
403
+ for (const [name, port] of Object.entries(flat)) {
404
+ const dotIdx = name.indexOf(".");
405
+ if (dotIdx < 0) {
406
+ ports[name] = port;
407
+ continue;
408
+ }
409
+ // Hierarchical signal: first segment is the interface port name
410
+ const parentName = name.slice(0, dotIdx);
411
+ const memberName = name.slice(dotIdx + 1);
412
+ if (!ifMaps.has(parentName)) {
413
+ const ifMap: Record<string, PortInfo> = {};
414
+ ifMaps.set(parentName, ifMap);
415
+ ports[parentName] = {
416
+ direction: "inout",
417
+ type: "logic",
418
+ width: 0,
419
+ interface: ifMap,
420
+ };
421
+ }
422
+ ifMaps.get(parentName)![memberName] = port;
423
+ }
424
+
425
+ return ports;
371
426
  }
372
427
 
373
428
  // ---------------------------------------------------------------------------
@@ -375,17 +430,20 @@ export function buildPortsFromLayout(
375
430
  // ---------------------------------------------------------------------------
376
431
 
377
432
  export interface HierarchyNode {
378
- moduleName: string;
379
- signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }>;
380
- forDut: Record<string, SignalLayout>;
381
- ports: Record<string, PortInfo>;
382
- children: Record<string, HierarchyNode[]>;
433
+ moduleName: string;
434
+ signals: Record<
435
+ string,
436
+ SignalLayout & { typeKind: string; arrayDims?: number[] }
437
+ >;
438
+ forDut: Record<string, SignalLayout>;
439
+ ports: Record<string, PortInfo>;
440
+ children: Record<string, HierarchyNode[]>;
383
441
  }
384
442
 
385
443
  interface RawHierarchyNode {
386
- module_name: string;
387
- signals: Record<string, RawSignalLayout>;
388
- children: Record<string, RawHierarchyNode[]>;
444
+ module_name: string;
445
+ signals: Record<string, RawSignalLayout>;
446
+ children: Record<string, RawHierarchyNode[]>;
389
447
  }
390
448
 
391
449
  /**
@@ -393,53 +451,75 @@ interface RawHierarchyNode {
393
451
  * Converts snake_case keys to camelCase and auto-detects ports.
394
452
  */
395
453
  export function parseHierarchyLayout(
396
- json: string,
397
- events: Record<string, number>,
454
+ json: string,
455
+ events: Record<string, number>,
398
456
  ): HierarchyNode {
399
- const raw: RawHierarchyNode = JSON.parse(json);
400
- return convertHierarchyNode(raw, events);
457
+ const raw: RawHierarchyNode = JSON.parse(json);
458
+ return convertHierarchyNode(raw, events);
401
459
  }
402
460
 
403
461
  function convertHierarchyNode(
404
- raw: RawHierarchyNode,
405
- events: Record<string, number>,
462
+ raw: RawHierarchyNode,
463
+ events: Record<string, number>,
464
+ ): HierarchyNode {
465
+ const signals: Record<
466
+ string,
467
+ SignalLayout & { typeKind: string; arrayDims?: number[] }
468
+ > = {};
469
+ const forDut: Record<string, SignalLayout> = {};
470
+
471
+ for (const [name, r] of Object.entries(raw.signals)) {
472
+ const sl: SignalLayout = {
473
+ offset: r.offset,
474
+ width: r.width,
475
+ byteSize: r.byte_size > 0 ? r.byte_size : Math.ceil(r.width / 8),
476
+ is4state: r.is_4state,
477
+ direction: r.direction as SignalLayout["direction"],
478
+ };
479
+ const entry: SignalLayout & { typeKind: string; arrayDims?: number[] } = {
480
+ ...sl,
481
+ typeKind: r.type_kind,
482
+ };
483
+ if (r.array_dims && r.array_dims.length > 0) {
484
+ entry.arrayDims = r.array_dims;
485
+ }
486
+ signals[name] = entry;
487
+ forDut[name] = sl;
488
+ }
489
+
490
+ const ports = buildPortsFromLayout(signals, events);
491
+
492
+ const children: Record<string, HierarchyNode[]> = {};
493
+ for (const [name, instances] of Object.entries(raw.children)) {
494
+ children[name] = instances.map((inst) =>
495
+ convertHierarchyNode(inst, events),
496
+ );
497
+ }
498
+
499
+ return {
500
+ moduleName: raw.module_name,
501
+ signals,
502
+ forDut,
503
+ ports,
504
+ children,
505
+ };
506
+ }
507
+
508
+ /**
509
+ * Filter a hierarchy tree based on the dead store elimination policy.
510
+ *
511
+ * - `"off"` / undefined → full hierarchy (no filtering)
512
+ * - `"preserveTopPorts"` → strip children (only top-module ports survive DSE)
513
+ * - `"preserveAllPorts"` → hierarchy intact (all instance ports survive DSE)
514
+ */
515
+ export function filterHierarchyForDse(
516
+ hierarchy: HierarchyNode,
517
+ policy?: "off" | "preserveTopPorts" | "preserveAllPorts",
406
518
  ): HierarchyNode {
407
- const signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }> = {};
408
- const forDut: Record<string, SignalLayout> = {};
409
-
410
- for (const [name, r] of Object.entries(raw.signals)) {
411
- const sl: SignalLayout = {
412
- offset: r.offset,
413
- width: r.width,
414
- byteSize: r.byte_size > 0 ? r.byte_size : Math.ceil(r.width / 8),
415
- is4state: r.is_4state,
416
- direction: r.direction as "input" | "output" | "inout",
417
- };
418
- const entry: SignalLayout & { typeKind: string; arrayDims?: number[] } = {
419
- ...sl,
420
- typeKind: r.type_kind,
421
- };
422
- if (r.array_dims && r.array_dims.length > 0) {
423
- entry.arrayDims = r.array_dims;
424
- }
425
- signals[name] = entry;
426
- forDut[name] = sl;
427
- }
428
-
429
- const ports = buildPortsFromLayout(signals, events);
430
-
431
- const children: Record<string, HierarchyNode[]> = {};
432
- for (const [name, instances] of Object.entries(raw.children)) {
433
- children[name] = instances.map((inst) => convertHierarchyNode(inst, events));
434
- }
435
-
436
- return {
437
- moduleName: raw.module_name,
438
- signals,
439
- forDut,
440
- ports,
441
- children,
442
- };
519
+ if (!policy || policy === "off") return hierarchy;
520
+ if (policy === "preserveTopPorts") return { ...hierarchy, children: {} };
521
+ // "preserveAllPorts" — hierarchy intact, Rust-side ensures only ports survive
522
+ return hierarchy;
443
523
  }
444
524
 
445
525
  // ---------------------------------------------------------------------------
@@ -451,62 +531,62 @@ function convertHierarchyNode(
451
531
  * The buffer is shared between JS and Rust — no copies per tick.
452
532
  */
453
533
  export function wrapDirectSimulatorHandle(
454
- raw: RawNapiSimulatorHandle,
534
+ raw: RawNapiSimulatorHandle,
455
535
  ): NativeSimulatorHandle {
456
- return {
457
- tick(eventId: number): void {
458
- raw.tick(eventId);
459
- },
460
- tickN(eventId: number, count: number): void {
461
- raw.tickN(eventId, count);
462
- },
463
- evalComb(): void {
464
- raw.evalComb();
465
- },
466
- dump(timestamp: number): void {
467
- raw.dump(timestamp);
468
- },
469
- dispose(): void {
470
- raw.dispose();
471
- },
472
- };
536
+ return {
537
+ tick(eventId: number): void {
538
+ raw.tick(eventId);
539
+ },
540
+ tickN(eventId: number, count: number): void {
541
+ raw.tickN(eventId, count);
542
+ },
543
+ evalComb(): void {
544
+ raw.evalComb();
545
+ },
546
+ dump(timestamp: number): void {
547
+ raw.dump(timestamp);
548
+ },
549
+ dispose(): void {
550
+ raw.dispose();
551
+ },
552
+ };
473
553
  }
474
554
 
475
555
  /**
476
556
  * Wrap a raw NAPI simulation handle with direct (zero-copy) operations.
477
557
  */
478
558
  export function wrapDirectSimulationHandle(
479
- raw: RawNapiSimulationHandle,
559
+ raw: RawNapiSimulationHandle,
480
560
  ): NativeSimulationHandle {
481
- return {
482
- addClock(eventId: number, period: number, initialDelay: number): void {
483
- raw.addClock(eventId, period, initialDelay);
484
- },
485
- schedule(eventId: number, time: number, value: number): void {
486
- raw.schedule(eventId, time, value);
487
- },
488
- runUntil(endTime: number): void {
489
- raw.runUntil(endTime);
490
- },
491
- step(): number | null {
492
- return raw.step();
493
- },
494
- time(): number {
495
- return raw.time();
496
- },
497
- nextEventTime(): number | null {
498
- return raw.nextEventTime();
499
- },
500
- evalComb(): void {
501
- raw.evalComb();
502
- },
503
- dump(timestamp: number): void {
504
- raw.dump(timestamp);
505
- },
506
- dispose(): void {
507
- raw.dispose();
508
- },
509
- };
561
+ return {
562
+ addClock(eventId: number, period: number, initialDelay: number): void {
563
+ raw.addClock(eventId, period, initialDelay);
564
+ },
565
+ schedule(eventId: number, time: number, value: number): void {
566
+ raw.schedule(eventId, time, value);
567
+ },
568
+ runUntil(endTime: number): void {
569
+ raw.runUntil(endTime);
570
+ },
571
+ step(): number | null {
572
+ return raw.step();
573
+ },
574
+ time(): number {
575
+ return raw.time();
576
+ },
577
+ nextEventTime(): number | null {
578
+ return raw.nextEventTime();
579
+ },
580
+ evalComb(): void {
581
+ raw.evalComb();
582
+ },
583
+ dump(timestamp: number): void {
584
+ raw.dump(timestamp);
585
+ },
586
+ dispose(): void {
587
+ raw.dispose();
588
+ },
589
+ };
510
590
  }
511
591
 
512
592
  // ---------------------------------------------------------------------------
@@ -514,18 +594,18 @@ export function wrapDirectSimulationHandle(
514
594
  // ---------------------------------------------------------------------------
515
595
 
516
596
  function parseLegacyLayout(json: string): Record<string, SignalLayout> {
517
- const raw: Record<string, RawSignalLayout> = JSON.parse(json);
518
- const result: Record<string, SignalLayout> = {};
519
- for (const [name, r] of Object.entries(raw)) {
520
- result[name] = {
521
- offset: r.offset,
522
- width: r.width,
523
- byteSize: r.byte_size > 0 ? r.byte_size : Math.ceil(r.width / 8),
524
- is4state: r.is_4state,
525
- direction: r.direction as "input" | "output" | "inout",
526
- };
527
- }
528
- return result;
597
+ const raw: Record<string, RawSignalLayout> = JSON.parse(json);
598
+ const result: Record<string, SignalLayout> = {};
599
+ for (const [name, r] of Object.entries(raw)) {
600
+ result[name] = {
601
+ offset: r.offset,
602
+ width: r.width,
603
+ byteSize: r.byte_size > 0 ? r.byte_size : Math.ceil(r.width / 8),
604
+ is4state: r.is_4state,
605
+ direction: r.direction as SignalLayout["direction"],
606
+ };
607
+ }
608
+ return result;
529
609
  }
530
610
 
531
611
  // ---------------------------------------------------------------------------
@@ -537,45 +617,63 @@ function parseLegacyLayout(json: string): Record<string, SignalLayout> {
537
617
  * `Simulator.create(module, { __nativeCreate: ... })`.
538
618
  */
539
619
  export function createSimulatorBridge(addon: RawNapiAddon): NativeCreateFn {
540
- return (
541
- source: string,
542
- moduleName: string,
543
- options: SimulatorOptions,
544
- ): CreateResult<NativeSimulatorHandle> => {
545
- const napiOpts = buildNapiOpts(options);
546
- const raw = new addon.NativeSimulatorHandle(source, moduleName, napiOpts);
547
-
548
- const layout = parseLegacyLayout(raw.layoutJson);
549
- const events: Record<string, number> = JSON.parse(raw.eventsJson);
550
- const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
551
-
552
- const buf = raw.sharedMemory().buffer;
553
- const handle = wrapDirectSimulatorHandle(raw);
554
-
555
- return { buffer: buf, layout, events, handle, hierarchy };
556
- };
620
+ return (
621
+ sources: ReadonlyArray<SourceFile>,
622
+ moduleName: string,
623
+ options: SimulatorOptions,
624
+ ): CreateResult<NativeSimulatorHandle> => {
625
+ const napiOpts = buildNapiOpts(options);
626
+ const napiSources = sources.map((s) => ({
627
+ content: s.content,
628
+ path: s.path,
629
+ }));
630
+ const raw = new addon.NativeSimulatorHandle(
631
+ napiSources,
632
+ moduleName,
633
+ napiOpts,
634
+ );
635
+
636
+ const layout = parseLegacyLayout(raw.layoutJson);
637
+ const events: Record<string, number> = JSON.parse(raw.eventsJson);
638
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
639
+
640
+ const buf = raw.sharedMemory().buffer;
641
+ const handle = wrapDirectSimulatorHandle(raw);
642
+
643
+ return { buffer: buf, layout, events, handle, hierarchy };
644
+ };
557
645
  }
558
646
 
559
647
  /**
560
648
  * Create a `NativeCreateSimulationFn` from a raw NAPI addon, suitable for
561
649
  * `Simulation.create(module, { __nativeCreate: ... })`.
562
650
  */
563
- export function createSimulationBridge(addon: RawNapiAddon): NativeCreateSimulationFn {
564
- return (
565
- source: string,
566
- moduleName: string,
567
- options: SimulatorOptions,
568
- ): CreateResult<NativeSimulationHandle> => {
569
- const napiOpts = buildNapiOpts(options);
570
- const raw = new addon.NativeSimulationHandle(source, moduleName, napiOpts);
571
-
572
- const layout = parseLegacyLayout(raw.layoutJson);
573
- const events: Record<string, number> = JSON.parse(raw.eventsJson);
574
- const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
575
-
576
- const buf = raw.sharedMemory().buffer;
577
- const handle = wrapDirectSimulationHandle(raw);
578
-
579
- return { buffer: buf, layout, events, handle, hierarchy };
580
- };
651
+ export function createSimulationBridge(
652
+ addon: RawNapiAddon,
653
+ ): NativeCreateSimulationFn {
654
+ return (
655
+ sources: ReadonlyArray<SourceFile>,
656
+ moduleName: string,
657
+ options: SimulatorOptions,
658
+ ): CreateResult<NativeSimulationHandle> => {
659
+ const napiOpts = buildNapiOpts(options);
660
+ const napiSources = sources.map((s) => ({
661
+ content: s.content,
662
+ path: s.path,
663
+ }));
664
+ const raw = new addon.NativeSimulationHandle(
665
+ napiSources,
666
+ moduleName,
667
+ napiOpts,
668
+ );
669
+
670
+ const layout = parseLegacyLayout(raw.layoutJson);
671
+ const events: Record<string, number> = JSON.parse(raw.eventsJson);
672
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
673
+
674
+ const buf = raw.sharedMemory().buffer;
675
+ const handle = wrapDirectSimulationHandle(raw);
676
+
677
+ return { buffer: buf, layout, events, handle, hierarchy };
678
+ };
581
679
  }