@celox-sim/celox 0.1.4 → 0.1.6

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/simulation.ts CHANGED
@@ -7,16 +7,21 @@
7
7
 
8
8
  import type {
9
9
  CreateResult,
10
+ FourStateValue,
10
11
  ModuleDefinition,
11
12
  NativeSimulationHandle,
13
+ SignalLayout,
12
14
  SimulatorOptions,
13
15
  } from "./types.js";
14
- import { createDut, type DirtyState } from "./dut.js";
16
+ import { SimulationTimeoutError } from "./types.js";
17
+ import { createDut, readFourState, type DirtyState } from "./dut.js";
15
18
  import {
16
19
  loadNativeAddon,
17
20
  parseNapiLayout,
21
+ parseHierarchyLayout,
18
22
  buildPortsFromLayout,
19
23
  wrapDirectSimulationHandle,
24
+ buildNapiOpts,
20
25
  } from "./napi-helpers.js";
21
26
 
22
27
  /**
@@ -49,6 +54,9 @@ export class Simulation<P = Record<string, unknown>> {
49
54
  private readonly _dut: P;
50
55
  private readonly _events: Record<string, number>;
51
56
  private readonly _state: DirtyState;
57
+ private readonly _buffer: ArrayBuffer | SharedArrayBuffer;
58
+ private readonly _layout: Record<string, SignalLayout & { typeKind?: string; associatedClock?: string }>;
59
+ private readonly _clocks = new Map<string, { period: number; eventId: number }>();
52
60
  private _disposed = false;
53
61
 
54
62
  private constructor(
@@ -56,11 +64,15 @@ export class Simulation<P = Record<string, unknown>> {
56
64
  dut: P,
57
65
  events: Record<string, number>,
58
66
  state: DirtyState,
67
+ buffer: ArrayBuffer | SharedArrayBuffer,
68
+ layout: Record<string, SignalLayout & { typeKind?: string; associatedClock?: string }>,
59
69
  ) {
60
70
  this._handle = handle;
61
71
  this._dut = dut;
62
72
  this._events = events;
63
73
  this._state = state;
74
+ this._buffer = buffer;
75
+ this._layout = layout;
64
76
  }
65
77
 
66
78
  /**
@@ -91,8 +103,8 @@ export class Simulation<P = Record<string, unknown>> {
91
103
  );
92
104
  }
93
105
 
94
- const { fourState, vcd } = options ?? {};
95
- const result = createFn(module.source, module.name, { fourState, vcd });
106
+ const { fourState, vcd, optimize, falseLoops, trueLoops, clockType, resetType } = options ?? {};
107
+ const result = createFn(module.source, module.name, { fourState, vcd, optimize, falseLoops, trueLoops, clockType, resetType });
96
108
  const state: DirtyState = { dirty: false };
97
109
 
98
110
  const dut = createDut<P>(
@@ -101,9 +113,10 @@ export class Simulation<P = Record<string, unknown>> {
101
113
  module.ports,
102
114
  result.handle,
103
115
  state,
116
+ result.hierarchy,
104
117
  );
105
118
 
106
- return new Simulation<P>(result.handle, dut, result.events, state);
119
+ return new Simulation<P>(result.handle, dut, result.events, state, result.buffer, result.layout);
107
120
  }
108
121
 
109
122
  /**
@@ -124,11 +137,12 @@ export class Simulation<P = Record<string, unknown>> {
124
137
  options?: SimulatorOptions & { nativeAddonPath?: string },
125
138
  ): Simulation<P> {
126
139
  const addon = loadNativeAddon(options?.nativeAddonPath);
127
- const napiOpts = options?.fourState ? { fourState: options.fourState } : undefined;
140
+ const napiOpts = buildNapiOpts(options);
128
141
  const raw = new addon.NativeSimulationHandle(source, top, napiOpts);
129
142
 
130
143
  const layout = parseNapiLayout(raw.layoutJson);
131
144
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
145
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
132
146
 
133
147
  const ports = buildPortsFromLayout(layout.signals, events);
134
148
 
@@ -136,9 +150,9 @@ export class Simulation<P = Record<string, unknown>> {
136
150
 
137
151
  const state: DirtyState = { dirty: false };
138
152
  const handle = wrapDirectSimulationHandle(raw);
139
- const dut = createDut<P>(buf, layout.forDut, ports, handle, state);
153
+ const dut = createDut<P>(buf, layout.forDut, ports, handle, state, hierarchy);
140
154
 
141
- return new Simulation<P>(handle, dut, events, state);
155
+ return new Simulation<P>(handle, dut, events, state, buf, layout.signals);
142
156
  }
143
157
 
144
158
  /**
@@ -159,11 +173,12 @@ export class Simulation<P = Record<string, unknown>> {
159
173
  options?: SimulatorOptions & { nativeAddonPath?: string },
160
174
  ): Simulation<P> {
161
175
  const addon = loadNativeAddon(options?.nativeAddonPath);
162
- const napiOpts = options?.fourState ? { fourState: options.fourState } : undefined;
176
+ const napiOpts = buildNapiOpts(options);
163
177
  const raw = addon.NativeSimulationHandle.fromProject(projectPath, top, napiOpts);
164
178
 
165
179
  const layout = parseNapiLayout(raw.layoutJson);
166
180
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
181
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
167
182
 
168
183
  const ports = buildPortsFromLayout(layout.signals, events);
169
184
 
@@ -171,9 +186,9 @@ export class Simulation<P = Record<string, unknown>> {
171
186
 
172
187
  const state: DirtyState = { dirty: false };
173
188
  const handle = wrapDirectSimulationHandle(raw);
174
- const dut = createDut<P>(buf, layout.forDut, ports, handle, state);
189
+ const dut = createDut<P>(buf, layout.forDut, ports, handle, state, hierarchy);
175
190
 
176
- return new Simulation<P>(handle, dut, events, state);
191
+ return new Simulation<P>(handle, dut, events, state, buf, layout.signals);
177
192
  }
178
193
 
179
194
  /** The DUT accessor object — read/write ports as plain properties. */
@@ -194,6 +209,7 @@ export class Simulation<P = Record<string, unknown>> {
194
209
  this.ensureAlive();
195
210
  const eventId = this.resolveEvent(name);
196
211
  this._handle.addClock(eventId, opts.period, opts.initialDelay ?? 0);
212
+ this._clocks.set(name, { period: opts.period, eventId });
197
213
  }
198
214
 
199
215
  /**
@@ -211,11 +227,33 @@ export class Simulation<P = Record<string, unknown>> {
211
227
  /**
212
228
  * Run the simulation until the given time.
213
229
  * Processes all scheduled events up to and including `endTime`.
214
- * evalComb is called internally; dirty is cleared on return.
230
+ *
231
+ * When `maxSteps` is provided, steps are counted in TS and a
232
+ * `SimulationTimeoutError` is thrown if the budget is exhausted before
233
+ * reaching `endTime`. Without `maxSteps` the fast Rust path is used.
215
234
  */
216
- runUntil(endTime: number): void {
235
+ runUntil(endTime: number, opts?: { maxSteps?: number }): void {
217
236
  this.ensureAlive();
218
- this._handle.runUntil(endTime);
237
+ if (opts?.maxSteps == null) {
238
+ this._handle.runUntil(endTime);
239
+ this._state.dirty = false;
240
+ return;
241
+ }
242
+ const max = opts.maxSteps;
243
+ let steps = 0;
244
+ while (this._handle.time() < endTime) {
245
+ const t = this._handle.step();
246
+ if (t == null) break;
247
+ steps++;
248
+ if (steps >= max) {
249
+ this._state.dirty = false;
250
+ throw new SimulationTimeoutError(
251
+ `runUntil: exceeded ${max} steps at time ${this._handle.time()} (target ${endTime})`,
252
+ this._handle.time(),
253
+ steps,
254
+ );
255
+ }
256
+ }
219
257
  this._state.dirty = false;
220
258
  }
221
259
 
@@ -237,6 +275,153 @@ export class Simulation<P = Record<string, unknown>> {
237
275
  return this._handle.time();
238
276
  }
239
277
 
278
+ /**
279
+ * Peek at the time of the next scheduled event without advancing.
280
+ *
281
+ * @returns The time of the next event, or `null` if no events are scheduled.
282
+ */
283
+ nextEventTime(): number | null {
284
+ this.ensureAlive();
285
+ return this._handle.nextEventTime();
286
+ }
287
+
288
+ /**
289
+ * Step until `condition()` returns true.
290
+ *
291
+ * @returns The simulation time when the condition became true.
292
+ * @throws SimulationTimeoutError if `maxSteps` is exceeded.
293
+ */
294
+ waitUntil(
295
+ condition: () => boolean,
296
+ opts?: { maxSteps?: number },
297
+ ): number {
298
+ this.ensureAlive();
299
+ const max = opts?.maxSteps ?? 100_000;
300
+ let steps = 0;
301
+ while (!condition()) {
302
+ const t = this._handle.step();
303
+ this._state.dirty = false;
304
+ if (t == null) break;
305
+ steps++;
306
+ if (steps >= max) {
307
+ throw new SimulationTimeoutError(
308
+ `waitUntil: condition not met after ${max} steps at time ${this._handle.time()}`,
309
+ this._handle.time(),
310
+ steps,
311
+ );
312
+ }
313
+ }
314
+ return this._handle.time();
315
+ }
316
+
317
+ /**
318
+ * Wait for `count` rising edges of the given clock.
319
+ *
320
+ * Detects actual 0→1 transitions by reading the clock signal directly
321
+ * from the shared buffer (clock ports are excluded from the DUT proxy).
322
+ * The clock must have been registered via `addClock`.
323
+ *
324
+ * @returns The simulation time after the edges are observed.
325
+ * @throws SimulationTimeoutError if `maxSteps` is exceeded.
326
+ */
327
+ waitForCycles(
328
+ clock: string,
329
+ count: number,
330
+ opts?: { maxSteps?: number },
331
+ ): number {
332
+ this.ensureAlive();
333
+ if (!this._clocks.has(clock)) {
334
+ throw new Error(
335
+ `No clock registered for '${clock}'. Call addClock() first.`,
336
+ );
337
+ }
338
+ const sig = this._layout[clock];
339
+ if (!sig) {
340
+ throw new Error(`No layout entry for clock '${clock}'.`);
341
+ }
342
+ const view = new DataView(this._buffer);
343
+ const readClk = () => view.getUint8(sig.offset);
344
+ let prev = readClk();
345
+ let remaining = count;
346
+ return this.waitUntil(() => {
347
+ const curr = readClk();
348
+ if (prev === 0 && curr !== 0) remaining--;
349
+ prev = curr;
350
+ return remaining <= 0;
351
+ }, opts);
352
+ }
353
+
354
+ /**
355
+ * Assert and release a reset signal.
356
+ *
357
+ * The active level is determined automatically from the Veryl type:
358
+ * - `reset` / `reset_async_high` / `reset_sync_high` → active-high (1)
359
+ * - `reset_async_low` / `reset_sync_low` → active-low (0)
360
+ *
361
+ * For sync resets (with an associated clock from FfDeclaration), advances
362
+ * `activeCycles` worth of the associated clock's period using `runUntil`.
363
+ * For async resets without an associated clock, `duration` must be specified.
364
+ * An explicit `duration` overrides cycle-based calculation for either type.
365
+ */
366
+ reset(
367
+ signal: string,
368
+ opts?: { activeCycles?: number; duration?: number },
369
+ ): void {
370
+ this.ensureAlive();
371
+ const sig = this._layout[signal];
372
+ if (!sig) {
373
+ throw new Error(
374
+ `Unknown port '${signal}'. Available: ${Object.keys(this._layout).join(", ")}`,
375
+ );
376
+ }
377
+ const typeKind = sig.typeKind ?? "";
378
+ if (!typeKind.startsWith("reset")) {
379
+ throw new Error(
380
+ `Port '${signal}' is not a reset signal (type_kind: '${typeKind}').`,
381
+ );
382
+ }
383
+ const isActiveLow = typeKind === "reset_async_low" || typeKind === "reset_sync_low";
384
+ const activeValue = isActiveLow ? 0 : 1;
385
+ const inactiveValue = isActiveLow ? 1 : 0;
386
+
387
+ const dut = this._dut as Record<string, unknown>;
388
+ dut[signal] = activeValue;
389
+
390
+ const associatedClock = sig.associatedClock;
391
+
392
+ if (opts?.duration != null) {
393
+ // Explicit duration (works for both sync and async resets)
394
+ this._handle.runUntil(this._handle.time() + opts.duration);
395
+ } else if (associatedClock) {
396
+ // Clock-associated reset → advance by activeCycles
397
+ const cycles = opts?.activeCycles ?? 2;
398
+ this.waitForCycles(associatedClock, cycles);
399
+ } else {
400
+ // No associated clock and no explicit duration → error
401
+ throw new Error(
402
+ `Reset '${signal}' has no associated clock. Specify opts.duration.`,
403
+ );
404
+ }
405
+
406
+ dut[signal] = inactiveValue;
407
+ this._state.dirty = false;
408
+ }
409
+
410
+ /**
411
+ * Read the raw 4-state (value + mask) pair for the named port.
412
+ */
413
+ fourState(portName: string): FourStateValue {
414
+ this.ensureAlive();
415
+ const sig = this._layout[portName];
416
+ if (!sig) {
417
+ throw new Error(
418
+ `Unknown port '${portName}'. Available: ${Object.keys(this._layout).join(", ")}`,
419
+ );
420
+ }
421
+ const [value, mask] = readFourState(this._buffer, sig);
422
+ return { __fourState: true, value, mask };
423
+ }
424
+
240
425
  /** Write current signal values to VCD at the given timestamp. */
241
426
  dump(timestamp: number): void {
242
427
  this.ensureAlive();
package/src/simulator.ts CHANGED
@@ -8,16 +8,20 @@
8
8
  import type {
9
9
  CreateResult,
10
10
  EventHandle,
11
+ FourStateValue,
11
12
  ModuleDefinition,
12
13
  NativeSimulatorHandle,
14
+ SignalLayout,
13
15
  SimulatorOptions,
14
16
  } from "./types.js";
15
- import { createDut, type DirtyState } from "./dut.js";
17
+ import { createDut, readFourState, type DirtyState } from "./dut.js";
16
18
  import {
17
19
  loadNativeAddon,
18
20
  parseNapiLayout,
21
+ parseHierarchyLayout,
19
22
  buildPortsFromLayout,
20
23
  wrapDirectSimulatorHandle,
24
+ buildNapiOpts,
21
25
  } from "./napi-helpers.js";
22
26
 
23
27
  /**
@@ -54,6 +58,8 @@ export class Simulator<P = Record<string, unknown>> {
54
58
  private readonly _events: Record<string, number>;
55
59
  private readonly _defaultEventId: number;
56
60
  private readonly _state: DirtyState;
61
+ private readonly _buffer: ArrayBuffer | SharedArrayBuffer;
62
+ private readonly _layout: Record<string, SignalLayout>;
57
63
  private _disposed = false;
58
64
 
59
65
  private constructor(
@@ -61,11 +67,15 @@ export class Simulator<P = Record<string, unknown>> {
61
67
  dut: P,
62
68
  events: Record<string, number>,
63
69
  state: DirtyState,
70
+ buffer: ArrayBuffer | SharedArrayBuffer,
71
+ layout: Record<string, SignalLayout>,
64
72
  ) {
65
73
  this._handle = handle;
66
74
  this._dut = dut;
67
75
  this._events = events;
68
76
  this._state = state;
77
+ this._buffer = buffer;
78
+ this._layout = layout;
69
79
  const keys = Object.keys(events);
70
80
  this._defaultEventId = keys.length > 0 ? events[keys[0]!]! : -1;
71
81
  }
@@ -98,8 +108,8 @@ export class Simulator<P = Record<string, unknown>> {
98
108
  );
99
109
  }
100
110
 
101
- const { fourState, vcd } = options ?? {};
102
- const result = createFn(module.source, module.name, { fourState, vcd });
111
+ const { fourState, vcd, optimize, falseLoops, trueLoops, clockType, resetType } = options ?? {};
112
+ const result = createFn(module.source, module.name, { fourState, vcd, optimize, falseLoops, trueLoops, clockType, resetType });
103
113
  const state: DirtyState = { dirty: false };
104
114
 
105
115
  const dut = createDut<P>(
@@ -108,9 +118,10 @@ export class Simulator<P = Record<string, unknown>> {
108
118
  module.ports,
109
119
  result.handle,
110
120
  state,
121
+ result.hierarchy,
111
122
  );
112
123
 
113
- return new Simulator<P>(result.handle, dut, result.events, state);
124
+ return new Simulator<P>(result.handle, dut, result.events, state, result.buffer, result.layout);
114
125
  }
115
126
 
116
127
  /**
@@ -133,11 +144,12 @@ export class Simulator<P = Record<string, unknown>> {
133
144
  options?: SimulatorOptions & { nativeAddonPath?: string },
134
145
  ): Simulator<P> {
135
146
  const addon = loadNativeAddon(options?.nativeAddonPath);
136
- const napiOpts = options?.fourState ? { fourState: options.fourState } : undefined;
147
+ const napiOpts = buildNapiOpts(options);
137
148
  const raw = new addon.NativeSimulatorHandle(source, top, napiOpts);
138
149
 
139
150
  const layout = parseNapiLayout(raw.layoutJson);
140
151
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
152
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
141
153
 
142
154
  const ports = buildPortsFromLayout(layout.signals, events);
143
155
 
@@ -145,9 +157,9 @@ export class Simulator<P = Record<string, unknown>> {
145
157
 
146
158
  const state: DirtyState = { dirty: false };
147
159
  const handle = wrapDirectSimulatorHandle(raw);
148
- const dut = createDut<P>(buf, layout.forDut, ports, handle, state);
160
+ const dut = createDut<P>(buf, layout.forDut, ports, handle, state, hierarchy);
149
161
 
150
- return new Simulator<P>(handle, dut, events, state);
162
+ return new Simulator<P>(handle, dut, events, state, buf, layout.forDut);
151
163
  }
152
164
 
153
165
  /**
@@ -167,11 +179,12 @@ export class Simulator<P = Record<string, unknown>> {
167
179
  options?: SimulatorOptions & { nativeAddonPath?: string },
168
180
  ): Simulator<P> {
169
181
  const addon = loadNativeAddon(options?.nativeAddonPath);
170
- const napiOpts = options?.fourState ? { fourState: options.fourState } : undefined;
182
+ const napiOpts = buildNapiOpts(options);
171
183
  const raw = addon.NativeSimulatorHandle.fromProject(projectPath, top, napiOpts);
172
184
 
173
185
  const layout = parseNapiLayout(raw.layoutJson);
174
186
  const events: Record<string, number> = JSON.parse(raw.eventsJson);
187
+ const hierarchy = parseHierarchyLayout(raw.hierarchyJson, events);
175
188
 
176
189
  const ports = buildPortsFromLayout(layout.signals, events);
177
190
 
@@ -179,9 +192,9 @@ export class Simulator<P = Record<string, unknown>> {
179
192
 
180
193
  const state: DirtyState = { dirty: false };
181
194
  const handle = wrapDirectSimulatorHandle(raw);
182
- const dut = createDut<P>(buf, layout.forDut, ports, handle, state);
195
+ const dut = createDut<P>(buf, layout.forDut, ports, handle, state, hierarchy);
183
196
 
184
- return new Simulator<P>(handle, dut, events, state);
197
+ return new Simulator<P>(handle, dut, events, state, buf, layout.forDut);
185
198
  }
186
199
 
187
200
  /** The DUT accessor object — read/write ports as plain properties. */
@@ -240,6 +253,21 @@ export class Simulator<P = Record<string, unknown>> {
240
253
  return { name, id };
241
254
  }
242
255
 
256
+ /**
257
+ * Read the raw 4-state (value + mask) pair for the named port.
258
+ */
259
+ fourState(portName: string): FourStateValue {
260
+ this.ensureAlive();
261
+ const sig = this._layout[portName];
262
+ if (!sig) {
263
+ throw new Error(
264
+ `Unknown port '${portName}'. Available: ${Object.keys(this._layout).join(", ")}`,
265
+ );
266
+ }
267
+ const [value, mask] = readFourState(this._buffer, sig);
268
+ return { __fourState: true, value, mask };
269
+ }
270
+
243
271
  /** Write current signal values to VCD at the given timestamp. */
244
272
  dump(timestamp: number): void {
245
273
  this.ensureAlive();
package/src/types.ts CHANGED
@@ -58,6 +58,10 @@ export interface SignalLayout {
58
58
  /** If true, an equal-sized mask follows immediately after the value. */
59
59
  readonly is4state: boolean;
60
60
  readonly direction: "input" | "output" | "inout";
61
+ /** The Veryl type kind (e.g. "clock", "reset_async_high", "logic"). */
62
+ readonly typeKind?: string;
63
+ /** For reset signals, the name of the associated clock (from FfDeclaration). */
64
+ readonly associatedClock?: string;
61
65
  }
62
66
 
63
67
  // ---------------------------------------------------------------------------
@@ -86,6 +90,7 @@ export interface NativeSimulationHandle {
86
90
  runUntil(endTime: number): void;
87
91
  step(): number | null;
88
92
  time(): number;
93
+ nextEventTime(): number | null;
89
94
  evalComb(): void;
90
95
  dump(timestamp: number): void;
91
96
  dispose(): void;
@@ -110,6 +115,8 @@ export interface CreateResult<H extends NativeHandle = NativeHandle> {
110
115
  readonly events: Record<string, number>;
111
116
  /** Native control handle. */
112
117
  readonly handle: H;
118
+ /** Full instance hierarchy (optional — present when NAPI provides it). */
119
+ readonly hierarchy?: import("./napi-helpers.js").HierarchyNode;
113
120
  }
114
121
 
115
122
  // ---------------------------------------------------------------------------
@@ -121,6 +128,16 @@ export interface SimulatorOptions {
121
128
  fourState?: boolean;
122
129
  /** Path to write VCD waveform output. */
123
130
  vcd?: string;
131
+ /** Enable Cranelift optimization passes. */
132
+ optimize?: boolean;
133
+ /** False-loop declarations to ignore during compilation. */
134
+ falseLoops?: LoopBreak[];
135
+ /** True-loop declarations with convergence limits. */
136
+ trueLoops?: TrueLoopSpec[];
137
+ /** Clock polarity. Default: "posedge". */
138
+ clockType?: "posedge" | "negedge";
139
+ /** Reset type. Default: "async_low". */
140
+ resetType?: "async_high" | "async_low" | "sync_high" | "sync_low";
124
141
  }
125
142
 
126
143
  // ---------------------------------------------------------------------------
@@ -162,3 +179,37 @@ export function isFourStateValue(v: unknown): v is FourStateValue {
162
179
  (v as FourStateValue).__fourState === true
163
180
  );
164
181
  }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Simulation timeout error
185
+ // ---------------------------------------------------------------------------
186
+
187
+ /**
188
+ * Thrown when a simulation helper exceeds its step budget.
189
+ */
190
+ export class SimulationTimeoutError extends Error {
191
+ readonly time: number;
192
+ readonly steps: number;
193
+
194
+ constructor(message: string, time: number, steps: number) {
195
+ super(message);
196
+ this.name = "SimulationTimeoutError";
197
+ this.time = time;
198
+ this.steps = steps;
199
+ }
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Loop-break types (for Phase 3c)
204
+ // ---------------------------------------------------------------------------
205
+
206
+ /** Specifies a false-loop to ignore during compilation. */
207
+ export interface LoopBreak {
208
+ from: string;
209
+ to: string;
210
+ }
211
+
212
+ /** Specifies a true-loop with a convergence iteration limit. */
213
+ export interface TrueLoopSpec extends LoopBreak {
214
+ maxIter: number;
215
+ }