@celox-sim/celox 0.1.5 → 0.1.7

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/index.ts CHANGED
@@ -15,6 +15,8 @@ export type {
15
15
  SimulatorOptions,
16
16
  EventHandle,
17
17
  FourStateValue,
18
+ LoopBreak,
19
+ TrueLoopSpec,
18
20
  } from "./types.js";
19
21
 
20
22
  /** @internal */
@@ -29,6 +31,9 @@ export type {
29
31
  // 4-state helpers
30
32
  export { X, FourState, isFourStateValue } from "./types.js";
31
33
 
34
+ // Error types
35
+ export { SimulationTimeoutError } from "./types.js";
36
+
32
37
  // Simulator (event-based)
33
38
  export { Simulator } from "./simulator.js";
34
39
 
@@ -36,7 +41,7 @@ export { Simulator } from "./simulator.js";
36
41
  export { Simulation } from "./simulation.js";
37
42
 
38
43
  /** @internal */
39
- export { createDut, readFourState } from "./dut.js";
44
+ export { createDut, createChildDut, readFourState } from "./dut.js";
40
45
  /** @internal */
41
46
  export type { DirtyState } from "./dut.js";
42
47
 
@@ -44,14 +49,17 @@ export type { DirtyState } from "./dut.js";
44
49
  export {
45
50
  loadNativeAddon,
46
51
  parseNapiLayout,
52
+ parseHierarchyLayout,
47
53
  buildPortsFromLayout,
48
54
  wrapDirectSimulatorHandle,
49
55
  wrapDirectSimulationHandle,
50
56
  createSimulatorBridge,
51
57
  createSimulationBridge,
58
+ parseSignalPath,
59
+ buildNapiOpts,
52
60
  } from "./napi-helpers.js";
53
61
  /** @internal */
54
- export type { RawNapiAddon, RawNapiSimulatorHandle, RawNapiSimulationHandle } from "./napi-helpers.js";
62
+ export type { HierarchyNode, RawNapiAddon, RawNapiSimulatorHandle, RawNapiSimulationHandle } from "./napi-helpers.js";
55
63
 
56
64
  // NAPI bridge (backward compat — re-exports from napi-helpers)
57
65
  // Consumers that import from "./napi-bridge.js" still work.
package/src/matchers.ts CHANGED
@@ -70,7 +70,7 @@ function isFourStateRef(v: unknown): v is FourStateRef {
70
70
  // Matcher implementations
71
71
  // ---------------------------------------------------------------------------
72
72
 
73
- function getMask(received: unknown): number | bigint {
73
+ function getMask(received: unknown): bigint {
74
74
  if (!isFourStateRef(received)) {
75
75
  throw new TypeError(
76
76
  "toBeX/toBeAllX/toBeNotX matchers require a FourStateRef. " +
@@ -84,7 +84,7 @@ function getMask(received: unknown): number | bigint {
84
84
  const customMatchers = {
85
85
  toBeX(received: unknown) {
86
86
  const mask = getMask(received);
87
- const pass = mask !== 0 && mask !== 0n;
87
+ const pass = mask !== 0n;
88
88
  return {
89
89
  pass,
90
90
  message: () =>
@@ -100,10 +100,7 @@ const customMatchers = {
100
100
  }
101
101
  const [, mask] = readFourState(received.buffer, received.layout);
102
102
  const width = received.layout.width;
103
- const allOnes =
104
- width <= 53
105
- ? (width === 53 ? Number.MAX_SAFE_INTEGER : (1 << width) - 1)
106
- : (1n << BigInt(width)) - 1n;
103
+ const allOnes = (1n << BigInt(width)) - 1n;
107
104
  const pass = mask === allOnes;
108
105
  return {
109
106
  pass,
@@ -116,7 +113,7 @@ const customMatchers = {
116
113
 
117
114
  toBeNotX(received: unknown) {
118
115
  const mask = getMask(received);
119
- const pass = mask === 0 || mask === 0n;
116
+ const pass = mask === 0n;
120
117
  return {
121
118
  pass,
122
119
  message: () =>
@@ -11,11 +11,13 @@
11
11
 
12
12
  import type {
13
13
  CreateResult,
14
+ LoopBreak,
14
15
  NativeSimulatorHandle,
15
16
  NativeSimulationHandle,
16
17
  PortInfo,
17
18
  SignalLayout,
18
19
  SimulatorOptions,
20
+ TrueLoopSpec,
19
21
  } from "./types.js";
20
22
  import type { NativeCreateFn } from "./simulator.js";
21
23
  import type { NativeCreateSimulationFn } from "./simulation.js";
@@ -27,6 +29,7 @@ import type { NativeCreateSimulationFn } from "./simulation.js";
27
29
  export interface RawNapiSimulatorHandle {
28
30
  readonly layoutJson: string;
29
31
  readonly eventsJson: string;
32
+ readonly hierarchyJson: string;
30
33
  readonly stableSize: number;
31
34
  readonly totalSize: number;
32
35
  tick(eventId: number): void;
@@ -40,6 +43,7 @@ export interface RawNapiSimulatorHandle {
40
43
  export interface RawNapiSimulationHandle {
41
44
  readonly layoutJson: string;
42
45
  readonly eventsJson: string;
46
+ readonly hierarchyJson: string;
43
47
  readonly stableSize: number;
44
48
  readonly totalSize: number;
45
49
  addClock(eventId: number, period: number, initialDelay: number): void;
@@ -47,14 +51,42 @@ export interface RawNapiSimulationHandle {
47
51
  runUntil(endTime: number): void;
48
52
  step(): number | null;
49
53
  time(): number;
54
+ nextEventTime(): number | null;
50
55
  evalComb(): void;
51
56
  dump(timestamp: number): void;
52
57
  sharedMemory(): Uint8Array;
53
58
  dispose(): void;
54
59
  }
55
60
 
61
+ export interface NapiFalseLoop {
62
+ from: NapiSignalPath;
63
+ to: NapiSignalPath;
64
+ }
65
+
66
+ export interface NapiTrueLoop {
67
+ from: NapiSignalPath;
68
+ to: NapiSignalPath;
69
+ maxIter: number;
70
+ }
71
+
72
+ export interface NapiSignalPath {
73
+ instancePath: NapiInstanceSegment[];
74
+ varPath: string[];
75
+ }
76
+
77
+ export interface NapiInstanceSegment {
78
+ name: string;
79
+ index: number;
80
+ }
81
+
56
82
  export interface NapiOptions {
57
83
  fourState?: boolean;
84
+ vcd?: string;
85
+ optimize?: boolean;
86
+ falseLoops?: NapiFalseLoop[];
87
+ trueLoops?: NapiTrueLoop[];
88
+ clockType?: string;
89
+ resetType?: string;
58
90
  }
59
91
 
60
92
  export interface RawNapiAddon {
@@ -102,6 +134,98 @@ export function loadNativeAddon(addonPath?: string): RawNapiAddon {
102
134
  }
103
135
  }
104
136
 
137
+ // ---------------------------------------------------------------------------
138
+ // Signal path parsing
139
+ // ---------------------------------------------------------------------------
140
+
141
+ /**
142
+ * Parse a signal path string into instance-path + var-path components.
143
+ *
144
+ * Format: `instanceSeg1.instanceSeg2:varSeg1.varSeg2`
145
+ * - `:` separates instance path from variable path
146
+ * - Without `:`, the whole string is the variable path
147
+ * - Instance segments may include `[N]` array indices
148
+ *
149
+ * Examples:
150
+ * - `"v"` → { instancePath: [], varPath: ["v"] }
151
+ * - `"p2:i"` → { instancePath: [{name:"p2",index:0}], varPath: ["i"] }
152
+ * - `"a.b[3]:x.y"` → { instancePath: [{name:"a",index:0},{name:"b",index:3}], varPath: ["x","y"] }
153
+ */
154
+ export function parseSignalPath(path: string): NapiSignalPath {
155
+ const colonIdx = path.indexOf(":");
156
+ if (colonIdx < 0) {
157
+ return { instancePath: [], varPath: path.split(".") };
158
+ }
159
+
160
+ const instPart = path.slice(0, colonIdx);
161
+ const varPart = path.slice(colonIdx + 1);
162
+
163
+ const instancePath: NapiInstanceSegment[] = [];
164
+ if (instPart.length > 0) {
165
+ for (const seg of instPart.split(".")) {
166
+ const bracketIdx = seg.indexOf("[");
167
+ if (bracketIdx >= 0) {
168
+ const name = seg.slice(0, bracketIdx);
169
+ const index = Number.parseInt(seg.slice(bracketIdx + 1, -1), 10);
170
+ instancePath.push({ name, index });
171
+ } else {
172
+ instancePath.push({ name: seg, index: 0 });
173
+ }
174
+ }
175
+ }
176
+
177
+ return { instancePath, varPath: varPart.split(".") };
178
+ }
179
+
180
+ /**
181
+ * Build NapiOptions from SimulatorOptions.
182
+ * Returns `undefined` when no options are set (to skip the NAPI options arg).
183
+ */
184
+ export function buildNapiOpts(options?: SimulatorOptions): NapiOptions | undefined {
185
+ if (!options) return undefined;
186
+
187
+ const napiOpts: NapiOptions = {};
188
+ let hasOpt = false;
189
+
190
+ if (options.fourState) {
191
+ napiOpts.fourState = options.fourState;
192
+ hasOpt = true;
193
+ }
194
+ if (options.vcd) {
195
+ napiOpts.vcd = options.vcd;
196
+ hasOpt = true;
197
+ }
198
+ if (options.optimize != null) {
199
+ napiOpts.optimize = options.optimize;
200
+ hasOpt = true;
201
+ }
202
+ if (options.falseLoops && options.falseLoops.length > 0) {
203
+ napiOpts.falseLoops = options.falseLoops.map((lb: LoopBreak) => ({
204
+ from: parseSignalPath(lb.from),
205
+ to: parseSignalPath(lb.to),
206
+ }));
207
+ hasOpt = true;
208
+ }
209
+ if (options.trueLoops && options.trueLoops.length > 0) {
210
+ napiOpts.trueLoops = options.trueLoops.map((tl: TrueLoopSpec) => ({
211
+ from: parseSignalPath(tl.from),
212
+ to: parseSignalPath(tl.to),
213
+ maxIter: tl.maxIter,
214
+ }));
215
+ hasOpt = true;
216
+ }
217
+ if (options.clockType) {
218
+ napiOpts.clockType = options.clockType;
219
+ hasOpt = true;
220
+ }
221
+ if (options.resetType) {
222
+ napiOpts.resetType = options.resetType;
223
+ hasOpt = true;
224
+ }
225
+
226
+ return hasOpt ? napiOpts : undefined;
227
+ }
228
+
105
229
  // ---------------------------------------------------------------------------
106
230
  // Layout parsing helpers
107
231
  // ---------------------------------------------------------------------------
@@ -114,6 +238,7 @@ interface RawSignalLayout {
114
238
  direction: string;
115
239
  type_kind: string;
116
240
  array_dims?: number[];
241
+ associated_clock?: string;
117
242
  }
118
243
 
119
244
  /**
@@ -122,11 +247,11 @@ interface RawSignalLayout {
122
247
  * the DUT-compatible layout (without type_kind).
123
248
  */
124
249
  export function parseNapiLayout(json: string): {
125
- signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }>;
250
+ signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[]; associatedClock?: string }>;
126
251
  forDut: Record<string, SignalLayout>;
127
252
  } {
128
253
  const raw: Record<string, RawSignalLayout> = JSON.parse(json);
129
- const signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }> = {};
254
+ const signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[]; associatedClock?: string }> = {};
130
255
  const forDut: Record<string, SignalLayout> = {};
131
256
 
132
257
  for (const [name, r] of Object.entries(raw)) {
@@ -137,13 +262,16 @@ export function parseNapiLayout(json: string): {
137
262
  is4state: r.is_4state,
138
263
  direction: r.direction as "input" | "output" | "inout",
139
264
  };
140
- const entry: SignalLayout & { typeKind: string; arrayDims?: number[] } = {
265
+ const entry: SignalLayout & { typeKind: string; arrayDims?: number[]; associatedClock?: string } = {
141
266
  ...sl,
142
267
  typeKind: r.type_kind,
143
268
  };
144
269
  if (r.array_dims && r.array_dims.length > 0) {
145
270
  entry.arrayDims = r.array_dims;
146
271
  }
272
+ if (r.associated_clock) {
273
+ entry.associatedClock = r.associated_clock;
274
+ }
147
275
  signals[name] = entry;
148
276
  forDut[name] = sl;
149
277
  }
@@ -164,19 +292,14 @@ export function buildPortsFromLayout(
164
292
  for (const [name, sig] of Object.entries(signals)) {
165
293
  const typeKind = sig.typeKind;
166
294
  let portType: "clock" | "reset" | "logic" | "bit";
167
- switch (typeKind) {
168
- case "clock":
169
- portType = "clock";
170
- break;
171
- case "reset":
172
- portType = "reset";
173
- break;
174
- case "bit":
175
- portType = "bit";
176
- break;
177
- default:
178
- portType = "logic";
179
- break;
295
+ if (typeKind === "clock") {
296
+ portType = "clock";
297
+ } else if (typeKind.startsWith("reset")) {
298
+ portType = "reset";
299
+ } else if (typeKind === "bit") {
300
+ portType = "bit";
301
+ } else {
302
+ portType = "logic";
180
303
  }
181
304
 
182
305
  const port: PortInfo = {
@@ -194,6 +317,78 @@ export function buildPortsFromLayout(
194
317
  return ports;
195
318
  }
196
319
 
320
+ // ---------------------------------------------------------------------------
321
+ // Hierarchy layout
322
+ // ---------------------------------------------------------------------------
323
+
324
+ export interface HierarchyNode {
325
+ moduleName: string;
326
+ signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }>;
327
+ forDut: Record<string, SignalLayout>;
328
+ ports: Record<string, PortInfo>;
329
+ children: Record<string, HierarchyNode[]>;
330
+ }
331
+
332
+ interface RawHierarchyNode {
333
+ module_name: string;
334
+ signals: Record<string, RawSignalLayout>;
335
+ children: Record<string, RawHierarchyNode[]>;
336
+ }
337
+
338
+ /**
339
+ * Parse the hierarchy JSON from NAPI into a HierarchyNode tree.
340
+ * Converts snake_case keys to camelCase and auto-detects ports.
341
+ */
342
+ export function parseHierarchyLayout(
343
+ json: string,
344
+ events: Record<string, number>,
345
+ ): HierarchyNode {
346
+ const raw: RawHierarchyNode = JSON.parse(json);
347
+ return convertHierarchyNode(raw, events);
348
+ }
349
+
350
+ function convertHierarchyNode(
351
+ raw: RawHierarchyNode,
352
+ events: Record<string, number>,
353
+ ): HierarchyNode {
354
+ const signals: Record<string, SignalLayout & { typeKind: string; arrayDims?: number[] }> = {};
355
+ const forDut: Record<string, SignalLayout> = {};
356
+
357
+ for (const [name, r] of Object.entries(raw.signals)) {
358
+ const sl: SignalLayout = {
359
+ offset: r.offset,
360
+ width: r.width,
361
+ byteSize: r.byte_size > 0 ? r.byte_size : Math.ceil(r.width / 8),
362
+ is4state: r.is_4state,
363
+ direction: r.direction as "input" | "output" | "inout",
364
+ };
365
+ const entry: SignalLayout & { typeKind: string; arrayDims?: number[] } = {
366
+ ...sl,
367
+ typeKind: r.type_kind,
368
+ };
369
+ if (r.array_dims && r.array_dims.length > 0) {
370
+ entry.arrayDims = r.array_dims;
371
+ }
372
+ signals[name] = entry;
373
+ forDut[name] = sl;
374
+ }
375
+
376
+ const ports = buildPortsFromLayout(signals, events);
377
+
378
+ const children: Record<string, HierarchyNode[]> = {};
379
+ for (const [name, instances] of Object.entries(raw.children)) {
380
+ children[name] = instances.map((inst) => convertHierarchyNode(inst, events));
381
+ }
382
+
383
+ return {
384
+ moduleName: raw.module_name,
385
+ signals,
386
+ forDut,
387
+ ports,
388
+ children,
389
+ };
390
+ }
391
+
197
392
  // ---------------------------------------------------------------------------
198
393
  // Handle wrapping — zero-copy direct operations
199
394
  // ---------------------------------------------------------------------------
@@ -246,6 +441,9 @@ export function wrapDirectSimulationHandle(
246
441
  time(): number {
247
442
  return raw.time();
248
443
  },
444
+ nextEventTime(): number | null {
445
+ return raw.nextEventTime();
446
+ },
249
447
  evalComb(): void {
250
448
  raw.evalComb();
251
449
  },
@@ -289,17 +487,19 @@ export function createSimulatorBridge(addon: RawNapiAddon): NativeCreateFn {
289
487
  return (
290
488
  source: string,
291
489
  moduleName: string,
292
- _options: SimulatorOptions,
490
+ options: SimulatorOptions,
293
491
  ): CreateResult<NativeSimulatorHandle> => {
294
- const raw = new addon.NativeSimulatorHandle(source, moduleName);
492
+ const napiOpts = buildNapiOpts(options);
493
+ const raw = new addon.NativeSimulatorHandle(source, moduleName, napiOpts);
295
494
 
296
495
  const layout = parseLegacyLayout(raw.layoutJson);
297
496
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
497
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
298
498
 
299
499
  const buf = raw.sharedMemory().buffer;
300
500
  const handle = wrapDirectSimulatorHandle(raw);
301
501
 
302
- return { buffer: buf, layout, events, handle };
502
+ return { buffer: buf, layout, events, handle, hierarchy };
303
503
  };
304
504
  }
305
505
 
@@ -311,16 +511,18 @@ export function createSimulationBridge(addon: RawNapiAddon): NativeCreateSimulat
311
511
  return (
312
512
  source: string,
313
513
  moduleName: string,
314
- _options: SimulatorOptions,
514
+ options: SimulatorOptions,
315
515
  ): CreateResult<NativeSimulationHandle> => {
316
- const raw = new addon.NativeSimulationHandle(source, moduleName);
516
+ const napiOpts = buildNapiOpts(options);
517
+ const raw = new addon.NativeSimulationHandle(source, moduleName, napiOpts);
317
518
 
318
519
  const layout = parseLegacyLayout(raw.layoutJson);
319
520
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
521
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
320
522
 
321
523
  const buf = raw.sharedMemory().buffer;
322
524
  const handle = wrapDirectSimulationHandle(raw);
323
525
 
324
- return { buffer: buf, layout, events, handle };
526
+ return { buffer: buf, layout, events, handle, hierarchy };
325
527
  };
326
528
  }