@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/dist/dut.d.ts +14 -7
- package/dist/dut.d.ts.map +1 -1
- package/dist/dut.js +103 -27
- package/dist/dut.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/napi-helpers.d.ts +62 -1
- package/dist/napi-helpers.d.ts.map +1 -1
- package/dist/napi-helpers.js +154 -19
- package/dist/napi-helpers.js.map +1 -1
- package/dist/simulation.d.ts +59 -3
- package/dist/simulation.d.ts.map +1 -1
- package/dist/simulation.js +162 -16
- package/dist/simulation.js.map +1 -1
- package/dist/simulator.d.ts +7 -1
- package/dist/simulator.d.ts.map +1 -1
- package/dist/simulator.js +35 -14
- package/dist/simulator.js.map +1 -1
- package/dist/types.d.ts +36 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/dut.test.ts +66 -66
- package/src/dut.ts +124 -31
- package/src/e2e.bench.ts +176 -13
- package/src/e2e.test.ts +632 -195
- package/src/index.ts +10 -2
- package/src/matchers.ts +4 -7
- package/src/napi-helpers.ts +224 -22
- package/src/simulation.test.ts +220 -12
- package/src/simulation.ts +198 -13
- package/src/simulator.test.ts +8 -8
- package/src/simulator.ts +43 -11
- package/src/types.ts +54 -3
package/src/simulation.test.ts
CHANGED
|
@@ -5,15 +5,16 @@ import type {
|
|
|
5
5
|
ModuleDefinition,
|
|
6
6
|
NativeSimulationHandle,
|
|
7
7
|
} from "./types.js";
|
|
8
|
+
import { SimulationTimeoutError } from "./types.js";
|
|
8
9
|
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Mock helpers
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
|
|
13
14
|
interface TopPorts {
|
|
14
|
-
rst:
|
|
15
|
-
d:
|
|
16
|
-
readonly q:
|
|
15
|
+
rst: bigint;
|
|
16
|
+
d: bigint;
|
|
17
|
+
readonly q: bigint;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const TopModule: ModuleDefinition<TopPorts> = {
|
|
@@ -29,7 +30,10 @@ const TopModule: ModuleDefinition<TopPorts> = {
|
|
|
29
30
|
events: ["clk"],
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
function createMockNative(
|
|
33
|
+
function createMockNative(opts?: {
|
|
34
|
+
resetTypeKind?: string;
|
|
35
|
+
associatedClock?: string;
|
|
36
|
+
}): {
|
|
33
37
|
create: NativeCreateSimulationFn;
|
|
34
38
|
handle: NativeSimulationHandle;
|
|
35
39
|
buffer: SharedArrayBuffer;
|
|
@@ -50,9 +54,12 @@ function createMockNative(): {
|
|
|
50
54
|
currentTime += 5;
|
|
51
55
|
const view = new DataView(buffer);
|
|
52
56
|
view.setUint8(4, view.getUint8(2));
|
|
57
|
+
// Toggle clock (offset 6) on each step to simulate half-period=5
|
|
58
|
+
view.setUint8(6, view.getUint8(6) === 0 ? 1 : 0);
|
|
53
59
|
return currentTime;
|
|
54
60
|
}),
|
|
55
61
|
time: vi.fn().mockImplementation(() => currentTime),
|
|
62
|
+
nextEventTime: vi.fn().mockReturnValue(null),
|
|
56
63
|
evalComb: vi.fn().mockImplementation(() => {
|
|
57
64
|
const view = new DataView(buffer);
|
|
58
65
|
view.setUint8(4, view.getUint8(2));
|
|
@@ -61,13 +68,16 @@ function createMockNative(): {
|
|
|
61
68
|
dispose: vi.fn(),
|
|
62
69
|
};
|
|
63
70
|
|
|
71
|
+
const resetTypeKind = opts?.resetTypeKind ?? "reset_async_high";
|
|
72
|
+
const associatedClock = opts?.associatedClock;
|
|
73
|
+
|
|
64
74
|
const create: NativeCreateSimulationFn = vi.fn().mockReturnValue({
|
|
65
75
|
buffer,
|
|
66
76
|
layout: {
|
|
67
|
-
clk: { offset: 6, width: 1, byteSize: 1, is4state: false, direction: "input" },
|
|
68
|
-
rst: { offset: 0, width: 1, byteSize: 1, is4state: false, direction: "input" },
|
|
69
|
-
d: { offset: 2, width: 8, byteSize: 1, is4state: false, direction: "input" },
|
|
70
|
-
q: { offset: 4, width: 8, byteSize: 1, is4state: false, direction: "output" },
|
|
77
|
+
clk: { offset: 6, width: 1, byteSize: 1, is4state: false, direction: "input", typeKind: "clock" },
|
|
78
|
+
rst: { offset: 0, width: 1, byteSize: 1, is4state: false, direction: "input", typeKind: resetTypeKind, ...(associatedClock ? { associatedClock } : {}) },
|
|
79
|
+
d: { offset: 2, width: 8, byteSize: 1, is4state: false, direction: "input", typeKind: "logic" },
|
|
80
|
+
q: { offset: 4, width: 8, byteSize: 1, is4state: false, direction: "output", typeKind: "logic" },
|
|
71
81
|
},
|
|
72
82
|
events: { clk: 0 },
|
|
73
83
|
handle,
|
|
@@ -117,11 +127,11 @@ describe("Simulation", () => {
|
|
|
117
127
|
__nativeCreate: mock.create,
|
|
118
128
|
});
|
|
119
129
|
|
|
120
|
-
sim.dut.d =
|
|
130
|
+
sim.dut.d = 42n;
|
|
121
131
|
sim.runUntil(100);
|
|
122
132
|
|
|
123
133
|
expect(mock.handle.runUntil).toHaveBeenCalledWith(100);
|
|
124
|
-
expect(sim.dut.q).toBe(
|
|
134
|
+
expect(sim.dut.q).toBe(42n);
|
|
125
135
|
});
|
|
126
136
|
|
|
127
137
|
test("step", () => {
|
|
@@ -130,11 +140,11 @@ describe("Simulation", () => {
|
|
|
130
140
|
__nativeCreate: mock.create,
|
|
131
141
|
});
|
|
132
142
|
|
|
133
|
-
sim.dut.d =
|
|
143
|
+
sim.dut.d = 0xABn;
|
|
134
144
|
const t = sim.step();
|
|
135
145
|
|
|
136
146
|
expect(t).toBe(5);
|
|
137
|
-
expect(sim.dut.q).toBe(
|
|
147
|
+
expect(sim.dut.q).toBe(0xABn);
|
|
138
148
|
});
|
|
139
149
|
|
|
140
150
|
test("time", () => {
|
|
@@ -182,4 +192,202 @@ describe("Simulation", () => {
|
|
|
182
192
|
Simulation.create(TopModule);
|
|
183
193
|
}).toThrow("Native simulator binding not loaded");
|
|
184
194
|
});
|
|
195
|
+
|
|
196
|
+
test("runUntil with maxSteps: fast path when omitted", () => {
|
|
197
|
+
const mock = createMockNative();
|
|
198
|
+
const sim = Simulation.create(TopModule, {
|
|
199
|
+
__nativeCreate: mock.create,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
sim.runUntil(100);
|
|
203
|
+
// Without maxSteps, the Rust fast-path should be used
|
|
204
|
+
expect(mock.handle.runUntil).toHaveBeenCalledWith(100);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("runUntil with maxSteps: throws SimulationTimeoutError", () => {
|
|
208
|
+
const mock = createMockNative();
|
|
209
|
+
const sim = Simulation.create(TopModule, {
|
|
210
|
+
__nativeCreate: mock.create,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// step mock increments time by 5 each call, so reaching 10000 requires 2000 steps
|
|
214
|
+
// With maxSteps=10 we'll exhaust before getting there
|
|
215
|
+
expect(() => sim.runUntil(10000, { maxSteps: 10 })).toThrow(
|
|
216
|
+
SimulationTimeoutError,
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("runUntil with maxSteps: timeout error has correct properties", () => {
|
|
221
|
+
const mock = createMockNative();
|
|
222
|
+
const sim = Simulation.create(TopModule, {
|
|
223
|
+
__nativeCreate: mock.create,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
sim.runUntil(10000, { maxSteps: 5 });
|
|
228
|
+
expect.unreachable();
|
|
229
|
+
} catch (e) {
|
|
230
|
+
expect(e).toBeInstanceOf(SimulationTimeoutError);
|
|
231
|
+
const err = e as SimulationTimeoutError;
|
|
232
|
+
expect(err.steps).toBe(5);
|
|
233
|
+
expect(err.time).toBeGreaterThan(0);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("waitUntil: returns time when condition is met", () => {
|
|
238
|
+
const mock = createMockNative();
|
|
239
|
+
const sim = Simulation.create(TopModule, {
|
|
240
|
+
__nativeCreate: mock.create,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
sim.dut.d = 42n;
|
|
244
|
+
let callCount = 0;
|
|
245
|
+
const t = sim.waitUntil(() => {
|
|
246
|
+
callCount++;
|
|
247
|
+
return callCount >= 3;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(t).toBeGreaterThan(0);
|
|
251
|
+
expect(callCount).toBe(3);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("waitUntil: throws on timeout", () => {
|
|
255
|
+
const mock = createMockNative();
|
|
256
|
+
const sim = Simulation.create(TopModule, {
|
|
257
|
+
__nativeCreate: mock.create,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(() =>
|
|
261
|
+
sim.waitUntil(() => false, { maxSteps: 5 }),
|
|
262
|
+
).toThrow(SimulationTimeoutError);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("waitForCycles: counts rising edges", () => {
|
|
266
|
+
const mock = createMockNative();
|
|
267
|
+
const sim = Simulation.create(TopModule, {
|
|
268
|
+
__nativeCreate: mock.create,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
sim.addClock("clk", { period: 10 });
|
|
272
|
+
const t = sim.waitForCycles("clk", 3);
|
|
273
|
+
// clk toggles each step: 0→1→0→1→0→1 (5 steps for 3 rising edges)
|
|
274
|
+
expect(mock.handle.step).toHaveBeenCalledTimes(5);
|
|
275
|
+
expect(t).toBe(25);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("waitForCycles: throws without addClock", () => {
|
|
279
|
+
const mock = createMockNative();
|
|
280
|
+
const sim = Simulation.create(TopModule, {
|
|
281
|
+
__nativeCreate: mock.create,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(() => sim.waitForCycles("clk", 3)).toThrow("No clock registered");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("reset: active-high with associatedClock steps until target time", () => {
|
|
288
|
+
const mock = createMockNative({ associatedClock: "clk" });
|
|
289
|
+
const sim = Simulation.create(TopModule, {
|
|
290
|
+
__nativeCreate: mock.create,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
sim.addClock("clk", { period: 10 });
|
|
294
|
+
sim.reset("rst");
|
|
295
|
+
// Default activeCycles=2: 3 steps for 2 rising edges (0→1→0→1)
|
|
296
|
+
expect(mock.handle.step).toHaveBeenCalledTimes(3);
|
|
297
|
+
// Released to inactive value (0 for active-high)
|
|
298
|
+
const view = new DataView(mock.buffer);
|
|
299
|
+
expect(view.getUint8(0)).toBe(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("reset: custom activeCycles with associatedClock", () => {
|
|
303
|
+
const mock = createMockNative({ associatedClock: "clk" });
|
|
304
|
+
const sim = Simulation.create(TopModule, {
|
|
305
|
+
__nativeCreate: mock.create,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
sim.addClock("clk", { period: 10 });
|
|
309
|
+
sim.reset("rst", { activeCycles: 3 });
|
|
310
|
+
// 3 cycles: 5 steps for 3 rising edges (0→1→0→1→0→1)
|
|
311
|
+
expect(mock.handle.step).toHaveBeenCalledTimes(5);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("reset: explicit duration overrides cycle calculation", () => {
|
|
315
|
+
const mock = createMockNative({ associatedClock: "clk" });
|
|
316
|
+
const sim = Simulation.create(TopModule, {
|
|
317
|
+
__nativeCreate: mock.create,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
sim.addClock("clk", { period: 10 });
|
|
321
|
+
sim.reset("rst", { duration: 50 });
|
|
322
|
+
// Explicit duration → runUntil(0 + 50 = 50)
|
|
323
|
+
expect(mock.handle.runUntil).toHaveBeenCalledWith(50);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("reset: active-low with associatedClock asserts 0 then releases to 1", () => {
|
|
327
|
+
const mock = createMockNative({
|
|
328
|
+
resetTypeKind: "reset_async_low",
|
|
329
|
+
associatedClock: "clk",
|
|
330
|
+
});
|
|
331
|
+
const sim = Simulation.create(TopModule, {
|
|
332
|
+
__nativeCreate: mock.create,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
sim.addClock("clk", { period: 10 });
|
|
336
|
+
sim.reset("rst");
|
|
337
|
+
// active-low: releases to 1
|
|
338
|
+
const view = new DataView(mock.buffer);
|
|
339
|
+
expect(view.getUint8(0)).toBe(1);
|
|
340
|
+
// activeCycles=2: 3 steps for 2 rising edges
|
|
341
|
+
expect(mock.handle.step).toHaveBeenCalledTimes(3);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("reset: throws when no associatedClock and no duration", () => {
|
|
345
|
+
// No associatedClock in layout
|
|
346
|
+
const mock = createMockNative();
|
|
347
|
+
const sim = Simulation.create(TopModule, {
|
|
348
|
+
__nativeCreate: mock.create,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(() => sim.reset("rst")).toThrow("has no associated clock");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("reset: no associatedClock but duration specified works", () => {
|
|
355
|
+
// No associatedClock in layout, but duration is given
|
|
356
|
+
const mock = createMockNative();
|
|
357
|
+
const sim = Simulation.create(TopModule, {
|
|
358
|
+
__nativeCreate: mock.create,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
sim.reset("rst", { duration: 100 });
|
|
362
|
+
expect(mock.handle.runUntil).toHaveBeenCalledWith(100);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("reset: throws when associatedClock not registered via addClock", () => {
|
|
366
|
+
const mock = createMockNative({ associatedClock: "clk" });
|
|
367
|
+
const sim = Simulation.create(TopModule, {
|
|
368
|
+
__nativeCreate: mock.create,
|
|
369
|
+
});
|
|
370
|
+
// addClock not called
|
|
371
|
+
expect(() => sim.reset("rst")).toThrow(
|
|
372
|
+
"No clock registered for 'clk'",
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("reset: throws on non-reset port", () => {
|
|
377
|
+
const mock = createMockNative();
|
|
378
|
+
const sim = Simulation.create(TopModule, {
|
|
379
|
+
__nativeCreate: mock.create,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(() => sim.reset("d")).toThrow("not a reset signal");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("reset: throws on unknown port", () => {
|
|
386
|
+
const mock = createMockNative();
|
|
387
|
+
const sim = Simulation.create(TopModule, {
|
|
388
|
+
__nativeCreate: mock.create,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
expect(() => sim.reset("nonexistent")).toThrow("Unknown port");
|
|
392
|
+
});
|
|
185
393
|
});
|
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 {
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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 ? 0n : 1n;
|
|
385
|
+
const inactiveValue = isActiveLow ? 1n : 0n;
|
|
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.test.ts
CHANGED
|
@@ -11,10 +11,10 @@ import type {
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
|
|
13
13
|
interface AdderPorts {
|
|
14
|
-
rst:
|
|
15
|
-
a:
|
|
16
|
-
b:
|
|
17
|
-
readonly sum:
|
|
14
|
+
rst: bigint;
|
|
15
|
+
a: bigint;
|
|
16
|
+
b: bigint;
|
|
17
|
+
readonly sum: bigint;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const AdderModule: ModuleDefinition<AdderPorts> = {
|
|
@@ -80,11 +80,11 @@ describe("Simulator", () => {
|
|
|
80
80
|
__nativeCreate: mock.create,
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
sim.dut.a =
|
|
84
|
-
sim.dut.b =
|
|
83
|
+
sim.dut.a = 100n;
|
|
84
|
+
sim.dut.b = 200n;
|
|
85
85
|
sim.tick();
|
|
86
86
|
|
|
87
|
-
expect(sim.dut.sum).toBe(
|
|
87
|
+
expect(sim.dut.sum).toBe(300n);
|
|
88
88
|
expect(mock.handle.tick).toHaveBeenCalledTimes(1);
|
|
89
89
|
});
|
|
90
90
|
|
|
@@ -176,7 +176,7 @@ describe("Simulator", () => {
|
|
|
176
176
|
__nativeCreate: mock.create,
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
sim.dut.a =
|
|
179
|
+
sim.dut.a = 100n;
|
|
180
180
|
sim.tick();
|
|
181
181
|
|
|
182
182
|
// After tick, reading output should NOT trigger evalComb
|