@celox-sim/celox 0.0.1 → 0.1.3
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 +35 -0
- package/dist/dut.d.ts.map +1 -0
- package/dist/dut.js +342 -0
- package/dist/dut.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/napi-bridge.d.ts +7 -0
- package/dist/napi-bridge.d.ts.map +1 -0
- package/dist/napi-bridge.js +7 -0
- package/dist/napi-bridge.js.map +1 -0
- package/dist/napi-helpers.d.ts +104 -0
- package/dist/napi-helpers.d.ts.map +1 -0
- package/dist/napi-helpers.js +207 -0
- package/dist/napi-helpers.js.map +1 -0
- package/dist/simulation.d.ts +111 -0
- package/dist/simulation.d.ts.map +1 -0
- package/dist/simulation.js +187 -0
- package/dist/simulation.js.map +1 -0
- package/dist/simulator.d.ts +92 -0
- package/dist/simulator.d.ts.map +1 -0
- package/dist/simulator.js +171 -0
- package/dist/simulator.js.map +1 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -7
- package/src/dut.test.ts +534 -0
- package/src/dut.ts +449 -0
- package/src/e2e.bench.ts +240 -0
- package/src/e2e.test.ts +965 -0
- package/src/index.ts +57 -0
- package/src/matchers.ts +154 -0
- package/src/napi-bridge.ts +13 -0
- package/src/napi-helpers.ts +326 -0
- package/src/simulation.test.ts +185 -0
- package/src/simulation.ts +273 -0
- package/src/simulator.test.ts +191 -0
- package/src/simulator.ts +266 -0
- package/src/types.ts +164 -0
- package/README.md +0 -45
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
|
+
import { Simulation, type NativeCreateSimulationFn } from "./simulation.js";
|
|
3
|
+
import type {
|
|
4
|
+
CreateResult,
|
|
5
|
+
ModuleDefinition,
|
|
6
|
+
NativeSimulationHandle,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Mock helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
interface TopPorts {
|
|
14
|
+
rst: number;
|
|
15
|
+
d: number;
|
|
16
|
+
readonly q: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TopModule: ModuleDefinition<TopPorts> = {
|
|
20
|
+
__celox_module: true,
|
|
21
|
+
name: "Top",
|
|
22
|
+
source: "module Top ...",
|
|
23
|
+
ports: {
|
|
24
|
+
clk: { direction: "input", type: "clock", width: 1 },
|
|
25
|
+
rst: { direction: "input", type: "reset", width: 1 },
|
|
26
|
+
d: { direction: "input", type: "logic", width: 8 },
|
|
27
|
+
q: { direction: "output", type: "logic", width: 8 },
|
|
28
|
+
},
|
|
29
|
+
events: ["clk"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function createMockNative(): {
|
|
33
|
+
create: NativeCreateSimulationFn;
|
|
34
|
+
handle: NativeSimulationHandle;
|
|
35
|
+
buffer: SharedArrayBuffer;
|
|
36
|
+
} {
|
|
37
|
+
const buffer = new SharedArrayBuffer(64);
|
|
38
|
+
let currentTime = 0;
|
|
39
|
+
|
|
40
|
+
const handle: NativeSimulationHandle = {
|
|
41
|
+
addClock: vi.fn(),
|
|
42
|
+
schedule: vi.fn(),
|
|
43
|
+
runUntil: vi.fn().mockImplementation((endTime: number) => {
|
|
44
|
+
// Simulate: q = d after running
|
|
45
|
+
const view = new DataView(buffer);
|
|
46
|
+
view.setUint8(4, view.getUint8(2));
|
|
47
|
+
currentTime = endTime;
|
|
48
|
+
}),
|
|
49
|
+
step: vi.fn().mockImplementation(() => {
|
|
50
|
+
currentTime += 5;
|
|
51
|
+
const view = new DataView(buffer);
|
|
52
|
+
view.setUint8(4, view.getUint8(2));
|
|
53
|
+
return currentTime;
|
|
54
|
+
}),
|
|
55
|
+
time: vi.fn().mockImplementation(() => currentTime),
|
|
56
|
+
evalComb: vi.fn().mockImplementation(() => {
|
|
57
|
+
const view = new DataView(buffer);
|
|
58
|
+
view.setUint8(4, view.getUint8(2));
|
|
59
|
+
}),
|
|
60
|
+
dump: vi.fn(),
|
|
61
|
+
dispose: vi.fn(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const create: NativeCreateSimulationFn = vi.fn().mockReturnValue({
|
|
65
|
+
buffer,
|
|
66
|
+
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" },
|
|
71
|
+
},
|
|
72
|
+
events: { clk: 0 },
|
|
73
|
+
handle,
|
|
74
|
+
} satisfies CreateResult<NativeSimulationHandle>);
|
|
75
|
+
|
|
76
|
+
return { create, handle, buffer };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Tests
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
describe("Simulation", () => {
|
|
84
|
+
test("create and addClock", () => {
|
|
85
|
+
const mock = createMockNative();
|
|
86
|
+
const sim = Simulation.create(TopModule, {
|
|
87
|
+
__nativeCreate: mock.create,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
sim.addClock("clk", { period: 10 });
|
|
91
|
+
expect(mock.handle.addClock).toHaveBeenCalledWith(0, 10, 0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("addClock with initialDelay", () => {
|
|
95
|
+
const mock = createMockNative();
|
|
96
|
+
const sim = Simulation.create(TopModule, {
|
|
97
|
+
__nativeCreate: mock.create,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
sim.addClock("clk", { period: 10, initialDelay: 5 });
|
|
101
|
+
expect(mock.handle.addClock).toHaveBeenCalledWith(0, 10, 5);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("schedule", () => {
|
|
105
|
+
const mock = createMockNative();
|
|
106
|
+
const sim = Simulation.create(TopModule, {
|
|
107
|
+
__nativeCreate: mock.create,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
sim.schedule("clk", { time: 50, value: 1 });
|
|
111
|
+
expect(mock.handle.schedule).toHaveBeenCalledWith(0, 50, 1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("runUntil", () => {
|
|
115
|
+
const mock = createMockNative();
|
|
116
|
+
const sim = Simulation.create(TopModule, {
|
|
117
|
+
__nativeCreate: mock.create,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
sim.dut.d = 42;
|
|
121
|
+
sim.runUntil(100);
|
|
122
|
+
|
|
123
|
+
expect(mock.handle.runUntil).toHaveBeenCalledWith(100);
|
|
124
|
+
expect(sim.dut.q).toBe(42);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("step", () => {
|
|
128
|
+
const mock = createMockNative();
|
|
129
|
+
const sim = Simulation.create(TopModule, {
|
|
130
|
+
__nativeCreate: mock.create,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
sim.dut.d = 0xAB;
|
|
134
|
+
const t = sim.step();
|
|
135
|
+
|
|
136
|
+
expect(t).toBe(5);
|
|
137
|
+
expect(sim.dut.q).toBe(0xAB);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("time", () => {
|
|
141
|
+
const mock = createMockNative();
|
|
142
|
+
const sim = Simulation.create(TopModule, {
|
|
143
|
+
__nativeCreate: mock.create,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(sim.time()).toBe(0);
|
|
147
|
+
sim.runUntil(100);
|
|
148
|
+
expect(sim.time()).toBe(100);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("unknown event throws", () => {
|
|
152
|
+
const mock = createMockNative();
|
|
153
|
+
const sim = Simulation.create(TopModule, {
|
|
154
|
+
__nativeCreate: mock.create,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(() => sim.addClock("bad", { period: 10 })).toThrow("Unknown event");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("dispose prevents further operations", () => {
|
|
161
|
+
const mock = createMockNative();
|
|
162
|
+
const sim = Simulation.create(TopModule, {
|
|
163
|
+
__nativeCreate: mock.create,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
sim.dispose();
|
|
167
|
+
expect(() => sim.runUntil(100)).toThrow("disposed");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("dump delegates to handle", () => {
|
|
171
|
+
const mock = createMockNative();
|
|
172
|
+
const sim = Simulation.create(TopModule, {
|
|
173
|
+
__nativeCreate: mock.create,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
sim.dump(99);
|
|
177
|
+
expect(mock.handle.dump).toHaveBeenCalledWith(99);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("create throws without native binding", () => {
|
|
181
|
+
expect(() => {
|
|
182
|
+
Simulation.create(TopModule);
|
|
183
|
+
}).toThrow("Native simulator binding not loaded");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-based Simulation.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a NativeSimulationHandle and provides a high-level TypeScript API
|
|
5
|
+
* for clock-driven simulation with automatic scheduling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
CreateResult,
|
|
10
|
+
ModuleDefinition,
|
|
11
|
+
NativeSimulationHandle,
|
|
12
|
+
SimulatorOptions,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
import { createDut, type DirtyState } from "./dut.js";
|
|
15
|
+
import {
|
|
16
|
+
loadNativeAddon,
|
|
17
|
+
parseNapiLayout,
|
|
18
|
+
buildPortsFromLayout,
|
|
19
|
+
wrapDirectSimulationHandle,
|
|
20
|
+
} from "./napi-helpers.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Placeholder for the NAPI binding's `createSimulation()`.
|
|
24
|
+
* Stream B will provide the real implementation.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export type NativeCreateSimulationFn = (
|
|
28
|
+
source: string,
|
|
29
|
+
moduleName: string,
|
|
30
|
+
options: SimulatorOptions,
|
|
31
|
+
) => CreateResult<NativeSimulationHandle>;
|
|
32
|
+
|
|
33
|
+
let _nativeCreate: NativeCreateSimulationFn | undefined;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Register the NAPI binding at module load time.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export function setNativeSimulationCreate(fn: NativeCreateSimulationFn): void {
|
|
40
|
+
_nativeCreate = fn;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Simulation
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export class Simulation<P = Record<string, unknown>> {
|
|
48
|
+
private readonly _handle: NativeSimulationHandle;
|
|
49
|
+
private readonly _dut: P;
|
|
50
|
+
private readonly _events: Record<string, number>;
|
|
51
|
+
private readonly _state: DirtyState;
|
|
52
|
+
private _disposed = false;
|
|
53
|
+
|
|
54
|
+
private constructor(
|
|
55
|
+
handle: NativeSimulationHandle,
|
|
56
|
+
dut: P,
|
|
57
|
+
events: Record<string, number>,
|
|
58
|
+
state: DirtyState,
|
|
59
|
+
) {
|
|
60
|
+
this._handle = handle;
|
|
61
|
+
this._dut = dut;
|
|
62
|
+
this._events = events;
|
|
63
|
+
this._state = state;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a Simulation for the given module.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { Top } from "./generated/Top.js";
|
|
71
|
+
* const sim = Simulation.create(Top);
|
|
72
|
+
* sim.addClock("clk", { period: 10 });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
static create<P>(
|
|
76
|
+
module: ModuleDefinition<P>,
|
|
77
|
+
options?: SimulatorOptions & {
|
|
78
|
+
__nativeCreate?: NativeCreateSimulationFn;
|
|
79
|
+
},
|
|
80
|
+
): Simulation<P> {
|
|
81
|
+
// When the module was produced by the Vite plugin, delegate to fromProject()
|
|
82
|
+
if (module.projectPath && !options?.__nativeCreate) {
|
|
83
|
+
return Simulation.fromProject<P>(module.projectPath, module.name, options);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const createFn = options?.__nativeCreate ?? _nativeCreate;
|
|
87
|
+
if (!createFn) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Native simulator binding not loaded. " +
|
|
90
|
+
"Ensure @celox-sim/celox-napi is installed.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { fourState, vcd } = options ?? {};
|
|
95
|
+
const result = createFn(module.source, module.name, { fourState, vcd });
|
|
96
|
+
const state: DirtyState = { dirty: false };
|
|
97
|
+
|
|
98
|
+
const dut = createDut<P>(
|
|
99
|
+
result.buffer,
|
|
100
|
+
result.layout,
|
|
101
|
+
module.ports,
|
|
102
|
+
result.handle,
|
|
103
|
+
state,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return new Simulation<P>(result.handle, dut, result.events, state);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create a Simulation directly from Veryl source code.
|
|
111
|
+
*
|
|
112
|
+
* Automatically discovers ports from the NAPI layout — no
|
|
113
|
+
* `ModuleDefinition` needed.
|
|
114
|
+
*
|
|
115
|
+
* ```ts
|
|
116
|
+
* const sim = Simulation.fromSource<CounterPorts>(COUNTER_SOURCE, "Counter");
|
|
117
|
+
* sim.addClock("clk", { period: 10 });
|
|
118
|
+
* sim.runUntil(100);
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
static fromSource<P = Record<string, unknown>>(
|
|
122
|
+
source: string,
|
|
123
|
+
top: string,
|
|
124
|
+
options?: SimulatorOptions & { nativeAddonPath?: string },
|
|
125
|
+
): Simulation<P> {
|
|
126
|
+
const addon = loadNativeAddon(options?.nativeAddonPath);
|
|
127
|
+
const napiOpts = options?.fourState ? { fourState: options.fourState } : undefined;
|
|
128
|
+
const raw = new addon.NativeSimulationHandle(source, top, napiOpts);
|
|
129
|
+
|
|
130
|
+
const layout = parseNapiLayout(raw.layoutJson);
|
|
131
|
+
const events: Record<string, number> = JSON.parse(raw.eventsJson);
|
|
132
|
+
|
|
133
|
+
const ports = buildPortsFromLayout(layout.signals, events);
|
|
134
|
+
|
|
135
|
+
const buf = raw.sharedMemory().buffer;
|
|
136
|
+
|
|
137
|
+
const state: DirtyState = { dirty: false };
|
|
138
|
+
const handle = wrapDirectSimulationHandle(raw);
|
|
139
|
+
const dut = createDut<P>(buf, layout.forDut, ports, handle, state);
|
|
140
|
+
|
|
141
|
+
return new Simulation<P>(handle, dut, events, state);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create a Simulation from a Veryl project directory.
|
|
146
|
+
*
|
|
147
|
+
* Searches upward from `projectPath` for `Veryl.toml`, gathers all
|
|
148
|
+
* `.veryl` source files, and builds the simulation using the project's
|
|
149
|
+
* clock/reset settings.
|
|
150
|
+
*
|
|
151
|
+
* ```ts
|
|
152
|
+
* const sim = Simulation.fromProject<MyPorts>("./my-project", "Top");
|
|
153
|
+
* sim.addClock("clk", { period: 10 });
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
static fromProject<P = Record<string, unknown>>(
|
|
157
|
+
projectPath: string,
|
|
158
|
+
top: string,
|
|
159
|
+
options?: SimulatorOptions & { nativeAddonPath?: string },
|
|
160
|
+
): Simulation<P> {
|
|
161
|
+
const addon = loadNativeAddon(options?.nativeAddonPath);
|
|
162
|
+
const napiOpts = options?.fourState ? { fourState: options.fourState } : undefined;
|
|
163
|
+
const raw = addon.NativeSimulationHandle.fromProject(projectPath, top, napiOpts);
|
|
164
|
+
|
|
165
|
+
const layout = parseNapiLayout(raw.layoutJson);
|
|
166
|
+
const events: Record<string, number> = JSON.parse(raw.eventsJson);
|
|
167
|
+
|
|
168
|
+
const ports = buildPortsFromLayout(layout.signals, events);
|
|
169
|
+
|
|
170
|
+
const buf = raw.sharedMemory().buffer;
|
|
171
|
+
|
|
172
|
+
const state: DirtyState = { dirty: false };
|
|
173
|
+
const handle = wrapDirectSimulationHandle(raw);
|
|
174
|
+
const dut = createDut<P>(buf, layout.forDut, ports, handle, state);
|
|
175
|
+
|
|
176
|
+
return new Simulation<P>(handle, dut, events, state);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The DUT accessor object — read/write ports as plain properties. */
|
|
180
|
+
get dut(): P {
|
|
181
|
+
return this._dut;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Register a periodic clock.
|
|
186
|
+
*
|
|
187
|
+
* @param name Clock event name (must match a `clock` port).
|
|
188
|
+
* @param opts `period` in time units; optional `initialDelay`.
|
|
189
|
+
*/
|
|
190
|
+
addClock(
|
|
191
|
+
name: string,
|
|
192
|
+
opts: { period: number; initialDelay?: number },
|
|
193
|
+
): void {
|
|
194
|
+
this.ensureAlive();
|
|
195
|
+
const eventId = this.resolveEvent(name);
|
|
196
|
+
this._handle.addClock(eventId, opts.period, opts.initialDelay ?? 0);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Schedule a one-shot value change for a signal.
|
|
201
|
+
*
|
|
202
|
+
* @param name Event/signal name.
|
|
203
|
+
* @param opts `time` — absolute time to apply; `value` — value to set.
|
|
204
|
+
*/
|
|
205
|
+
schedule(name: string, opts: { time: number; value: number }): void {
|
|
206
|
+
this.ensureAlive();
|
|
207
|
+
const eventId = this.resolveEvent(name);
|
|
208
|
+
this._handle.schedule(eventId, opts.time, opts.value);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Run the simulation until the given time.
|
|
213
|
+
* Processes all scheduled events up to and including `endTime`.
|
|
214
|
+
* evalComb is called internally; dirty is cleared on return.
|
|
215
|
+
*/
|
|
216
|
+
runUntil(endTime: number): void {
|
|
217
|
+
this.ensureAlive();
|
|
218
|
+
this._handle.runUntil(endTime);
|
|
219
|
+
this._state.dirty = false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Advance to the next scheduled event.
|
|
224
|
+
*
|
|
225
|
+
* @returns The time of the processed event, or `null` if no events remain.
|
|
226
|
+
*/
|
|
227
|
+
step(): number | null {
|
|
228
|
+
this.ensureAlive();
|
|
229
|
+
const t = this._handle.step();
|
|
230
|
+
this._state.dirty = false;
|
|
231
|
+
return t;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Current simulation time. */
|
|
235
|
+
time(): number {
|
|
236
|
+
this.ensureAlive();
|
|
237
|
+
return this._handle.time();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Write current signal values to VCD at the given timestamp. */
|
|
241
|
+
dump(timestamp: number): void {
|
|
242
|
+
this.ensureAlive();
|
|
243
|
+
this._handle.dump(timestamp);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Release native resources. */
|
|
247
|
+
dispose(): void {
|
|
248
|
+
if (!this._disposed) {
|
|
249
|
+
this._disposed = true;
|
|
250
|
+
this._handle.dispose();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// -----------------------------------------------------------------------
|
|
255
|
+
// Internal
|
|
256
|
+
// -----------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
private resolveEvent(name: string): number {
|
|
259
|
+
const id = this._events[name];
|
|
260
|
+
if (id === undefined) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Unknown event '${name}'. Available: ${Object.keys(this._events).join(", ")}`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return id;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private ensureAlive(): void {
|
|
269
|
+
if (this._disposed) {
|
|
270
|
+
throw new Error("Simulation has been disposed");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
|
+
import { Simulator, type NativeCreateFn } from "./simulator.js";
|
|
3
|
+
import type {
|
|
4
|
+
CreateResult,
|
|
5
|
+
ModuleDefinition,
|
|
6
|
+
NativeSimulatorHandle,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Mock helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
interface AdderPorts {
|
|
14
|
+
rst: number;
|
|
15
|
+
a: number;
|
|
16
|
+
b: number;
|
|
17
|
+
readonly sum: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const AdderModule: ModuleDefinition<AdderPorts> = {
|
|
21
|
+
__celox_module: true,
|
|
22
|
+
name: "Adder",
|
|
23
|
+
source: "module Adder ...",
|
|
24
|
+
ports: {
|
|
25
|
+
clk: { direction: "input", type: "clock", width: 1 },
|
|
26
|
+
rst: { direction: "input", type: "reset", width: 1 },
|
|
27
|
+
a: { direction: "input", type: "logic", width: 16 },
|
|
28
|
+
b: { direction: "input", type: "logic", width: 16 },
|
|
29
|
+
sum: { direction: "output", type: "logic", width: 17 },
|
|
30
|
+
},
|
|
31
|
+
events: ["clk"],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function createMockNative(): {
|
|
35
|
+
create: NativeCreateFn;
|
|
36
|
+
handle: NativeSimulatorHandle;
|
|
37
|
+
buffer: SharedArrayBuffer;
|
|
38
|
+
} {
|
|
39
|
+
const buffer = new SharedArrayBuffer(64);
|
|
40
|
+
const evalFn = () => {
|
|
41
|
+
const view = new DataView(buffer);
|
|
42
|
+
const a = view.getUint16(2, true);
|
|
43
|
+
const b = view.getUint16(4, true);
|
|
44
|
+
view.setUint32(8, a + b, true);
|
|
45
|
+
};
|
|
46
|
+
const handle: NativeSimulatorHandle = {
|
|
47
|
+
tick: vi.fn().mockImplementation(evalFn),
|
|
48
|
+
tickN: vi.fn().mockImplementation((_eventId: number, count: number) => {
|
|
49
|
+
for (let i = 0; i < count; i++) evalFn();
|
|
50
|
+
}),
|
|
51
|
+
evalComb: vi.fn().mockImplementation(evalFn),
|
|
52
|
+
dump: vi.fn(),
|
|
53
|
+
dispose: vi.fn(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const create: NativeCreateFn = vi.fn().mockReturnValue({
|
|
57
|
+
buffer,
|
|
58
|
+
layout: {
|
|
59
|
+
clk: { offset: 12, width: 1, byteSize: 1, is4state: false, direction: "input" },
|
|
60
|
+
rst: { offset: 0, width: 1, byteSize: 1, is4state: false, direction: "input" },
|
|
61
|
+
a: { offset: 2, width: 16, byteSize: 2, is4state: false, direction: "input" },
|
|
62
|
+
b: { offset: 4, width: 16, byteSize: 2, is4state: false, direction: "input" },
|
|
63
|
+
sum: { offset: 8, width: 17, byteSize: 4, is4state: false, direction: "output" },
|
|
64
|
+
},
|
|
65
|
+
events: { clk: 0 },
|
|
66
|
+
handle,
|
|
67
|
+
} satisfies CreateResult<NativeSimulatorHandle>);
|
|
68
|
+
|
|
69
|
+
return { create, handle, buffer };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Tests
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
describe("Simulator", () => {
|
|
77
|
+
test("create and basic tick", () => {
|
|
78
|
+
const mock = createMockNative();
|
|
79
|
+
const sim = Simulator.create(AdderModule, {
|
|
80
|
+
__nativeCreate: mock.create,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
sim.dut.a = 100;
|
|
84
|
+
sim.dut.b = 200;
|
|
85
|
+
sim.tick();
|
|
86
|
+
|
|
87
|
+
expect(sim.dut.sum).toBe(300);
|
|
88
|
+
expect(mock.handle.tick).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("tick with count", () => {
|
|
92
|
+
const mock = createMockNative();
|
|
93
|
+
const sim = Simulator.create(AdderModule, {
|
|
94
|
+
__nativeCreate: mock.create,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
sim.tick(3);
|
|
98
|
+
expect(mock.handle.tickN).toHaveBeenCalledWith(0, 3);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("tick with event handle", () => {
|
|
102
|
+
const mock = createMockNative();
|
|
103
|
+
const sim = Simulator.create(AdderModule, {
|
|
104
|
+
__nativeCreate: mock.create,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const clk = sim.event("clk");
|
|
108
|
+
expect(clk.name).toBe("clk");
|
|
109
|
+
expect(clk.id).toBe(0);
|
|
110
|
+
|
|
111
|
+
sim.tick(clk);
|
|
112
|
+
expect(mock.handle.tick).toHaveBeenCalledWith(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("tick with event handle and count", () => {
|
|
116
|
+
const mock = createMockNative();
|
|
117
|
+
const sim = Simulator.create(AdderModule, {
|
|
118
|
+
__nativeCreate: mock.create,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const clk = sim.event("clk");
|
|
122
|
+
sim.tick(clk, 5);
|
|
123
|
+
expect(mock.handle.tickN).toHaveBeenCalledWith(0, 5);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("event() throws for unknown event", () => {
|
|
127
|
+
const mock = createMockNative();
|
|
128
|
+
const sim = Simulator.create(AdderModule, {
|
|
129
|
+
__nativeCreate: mock.create,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(() => sim.event("nonexistent")).toThrow("Unknown event");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("dispose prevents further operations", () => {
|
|
136
|
+
const mock = createMockNative();
|
|
137
|
+
const sim = Simulator.create(AdderModule, {
|
|
138
|
+
__nativeCreate: mock.create,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
sim.dispose();
|
|
142
|
+
expect(() => sim.tick()).toThrow("disposed");
|
|
143
|
+
expect(mock.handle.dispose).toHaveBeenCalledTimes(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("double dispose is safe", () => {
|
|
147
|
+
const mock = createMockNative();
|
|
148
|
+
const sim = Simulator.create(AdderModule, {
|
|
149
|
+
__nativeCreate: mock.create,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
sim.dispose();
|
|
153
|
+
sim.dispose(); // no-op
|
|
154
|
+
expect(mock.handle.dispose).toHaveBeenCalledTimes(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("dump delegates to handle", () => {
|
|
158
|
+
const mock = createMockNative();
|
|
159
|
+
const sim = Simulator.create(AdderModule, {
|
|
160
|
+
__nativeCreate: mock.create,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
sim.dump(42);
|
|
164
|
+
expect(mock.handle.dump).toHaveBeenCalledWith(42);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("create throws without native binding", () => {
|
|
168
|
+
expect(() => {
|
|
169
|
+
Simulator.create(AdderModule);
|
|
170
|
+
}).toThrow("Native simulator binding not loaded");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("tick clears dirty flag", () => {
|
|
174
|
+
const mock = createMockNative();
|
|
175
|
+
const sim = Simulator.create(AdderModule, {
|
|
176
|
+
__nativeCreate: mock.create,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
sim.dut.a = 100;
|
|
180
|
+
sim.tick();
|
|
181
|
+
|
|
182
|
+
// After tick, reading output should NOT trigger evalComb
|
|
183
|
+
// because tick already cleared dirty
|
|
184
|
+
void sim.dut.sum;
|
|
185
|
+
// evalComb might have been called by the first dut.sum read,
|
|
186
|
+
// but tick itself should have cleared dirty
|
|
187
|
+
const callsBefore = (mock.handle.evalComb as ReturnType<typeof vi.fn>).mock.calls.length;
|
|
188
|
+
void sim.dut.sum;
|
|
189
|
+
expect((mock.handle.evalComb as ReturnType<typeof vi.fn>).mock.calls.length).toBe(callsBefore);
|
|
190
|
+
});
|
|
191
|
+
});
|