@celox-sim/celox 0.1.5 → 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/dist/dut.d.ts +13 -6
- package/dist/dut.d.ts.map +1 -1
- package/dist/dut.js +79 -6
- 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 +31 -13
- package/dist/simulator.js.map +1 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/dut.ts +93 -5
- package/src/e2e.bench.ts +164 -1
- package/src/e2e.test.ts +432 -30
- package/src/index.ts +10 -2
- package/src/napi-helpers.ts +224 -22
- package/src/simulation.test.ts +213 -5
- package/src/simulation.ts +198 -13
- package/src/simulator.ts +38 -10
- package/src/types.ts +51 -0
package/src/napi-helpers.ts
CHANGED
|
@@ -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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
490
|
+
options: SimulatorOptions,
|
|
293
491
|
): CreateResult<NativeSimulatorHandle> => {
|
|
294
|
-
const
|
|
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
|
-
|
|
514
|
+
options: SimulatorOptions,
|
|
315
515
|
): CreateResult<NativeSimulationHandle> => {
|
|
316
|
-
const
|
|
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
|
}
|
package/src/simulation.test.ts
CHANGED
|
@@ -5,6 +5,7 @@ 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
|
|
@@ -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,
|
|
@@ -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 = 42;
|
|
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
|
});
|