CIRCT 23.0.0git
Loading...
Searching...
No Matches
serialization_probes.cpp
Go to the documentation of this file.
1// Driver for the SerializationProbes integration test. Each function below
2// targets exactly one of the on-the-wire serialization invariants the host
3// must agree with hardware on (byte order, sign extension, struct-field
4// order, sub-byte field packing, array element order). Mismatches surface
5// as wrong-but-distinguishable output rather than coincidentally-correct
6// answers; see the corresponding HW module's docstring for the rationale.
7
8#include "serialization_probes/BitPackProbe.h"
9#include "serialization_probes/ByteRotate1.h"
10#include "serialization_probes/PackProbe.h"
11#include "serialization_probes/SignProbe.h"
12#include "serialization_probes/SignProbe13.h"
13// Note: serialization_probes/ArrayProbe.h is intentionally not included.
14// runArrayProbe drives the raw FuncService port directly to test the wire
15// byte order; see the comment on that function for the rationale.
16
17#include "probe_runner.h"
18
19#include "esi/Accelerator.h"
20#include "esi/Manifest.h"
21#include "esi/Services.h"
22
23#include <array>
24#include <cstdint>
25#include <iostream>
26#include <sstream>
27#include <stdexcept>
28
29using namespace esi;
30
31// Resolve a child instance by AppID name from the top-level accelerator.
32// The probe modules are instantiated under named appids so the runtime
33// hierarchy carries one instance per probe.
34static esi::HWModule *findProbe(Accelerator *accel, const char *appidName) {
35 auto it = accel->getChildren().find(AppID(appidName));
36 if (it == accel->getChildren().end())
37 throw std::runtime_error(std::string("probe instance '") + appidName +
38 "' not found in accelerator hierarchy");
39 return it->second;
40}
41
42// The constant byte sequence shared by ``byte_pattern_const`` and
43// ``byte_pattern_echo_eq``. Must stay in lock-step with ``_BYTE_PATTERN``
44// in the PyCDE source.
45static constexpr std::array<uint8_t, 8> kBytePattern = {0x12, 0x34, 0x56, 0x78,
46 0x9A, 0xBC, 0xDE, 0xF0};
47
48static int runByteRotate1(Accelerator *accel) {
49 esi_system::ByteRotate1 mod(findProbe(accel, "byte_rotate1_inst"));
50 auto connected = mod.connect();
51
52 uint64_t arg = 0x0102030405060708ULL;
53 uint64_t expect = 0x0203040506070801ULL; // (arg << 8) | (arg >> 56).
54 uint64_t got = connected->byte_rotate1(arg).get();
55 if (got != expect)
56 throw std::runtime_error("byte_rotate1 mismatch: got 0x" + toHex(got) +
57 " expected 0x" + toHex(expect));
58 std::cout << "byte_rotate1 ok: 0x" << std::hex << got << std::dec << "\n";
59 return 0;
60}
61
62// Look up a FuncService::Function port by AppID on a probe instance, skipping
63// the generated facade. Used by the raw-byte probes below so the test depends
64// only on what the runtime actually puts on (or reads off) the wire.
66 const char *portName) {
67 auto it = probe->getPorts().find(AppID(portName));
68 if (it == probe->getPorts().end())
69 throw std::runtime_error(std::string("port '") + portName +
70 "' not found on probe instance");
71 auto *func = it->second.getAs<services::FuncService::Function>();
72 if (!func)
73 throw std::runtime_error(std::string("port '") + portName +
74 "' is not a FuncService::Function");
75 return func;
76}
77
78static int runBytePatternConst(Accelerator *accel) {
79 // Bypasses the generated facade and the typed deserializer so the test
80 // asserts on the *wire* bytes directly.
81 auto *func = findRawFunc(findProbe(accel, "byte_pattern_const_inst"),
82 "byte_pattern_const");
83 func->connect();
84
85 uint8_t trigger = 0; // value is ignored by the hardware.
86 MessageData resMsg = func->call(MessageData(&trigger, sizeof(trigger))).get();
87
88 if (resMsg.getSize() != kBytePattern.size())
89 throw std::runtime_error("byte_pattern_const: wrong response size (got " +
90 std::to_string(resMsg.getSize()) + ", expected " +
91 std::to_string(kBytePattern.size()) + ")");
92 const uint8_t *got = resMsg.getBytes();
93 for (size_t i = 0; i < kBytePattern.size(); ++i) {
94 if (got[i] != kBytePattern[i]) {
95 std::stringstream ss;
96 ss << "byte_pattern_const: wire byte " << i << " mismatch (got 0x"
97 << std::hex << static_cast<unsigned>(got[i]) << " expected 0x"
98 << static_cast<unsigned>(kBytePattern[i]) << ")";
99 throw std::runtime_error(ss.str());
100 }
101 }
102 std::cout << "byte_pattern_const ok\n";
103 return 0;
104}
105
107 // Bypasses the generated facade and the typed serializer so the bytes the
108 // hardware sees come straight off the wire.
109 auto *func = findRawFunc(findProbe(accel, "byte_pattern_echo_eq_inst"),
110 "byte_pattern_echo_eq");
111 func->connect();
112
113 MessageData resMsg =
114 func->call(MessageData(kBytePattern.data(), kBytePattern.size())).get();
115 if (resMsg.getSize() != 1)
116 throw std::runtime_error("byte_pattern_echo_eq: wrong response size (got " +
117 std::to_string(resMsg.getSize()) +
118 ", expected 1)");
119 uint8_t got = *resMsg.getBytes();
120 if (got != 1)
121 throw std::runtime_error(
122 "byte_pattern_echo_eq: hardware reported byte mismatch (got " +
123 std::to_string(got) + ")");
124 std::cout << "byte_pattern_echo_eq ok\n";
125 return 0;
126}
127
128static int runSignProbe(Accelerator *accel) {
129 esi_system::SignProbe mod(findProbe(accel, "sign_probe_inst"));
130 auto connected = mod.connect();
131
132 // Probe several signed values, including the boundaries where two's-
133 // complement and sign-extension bugs would show up.
134 struct Case {
135 int16_t arg;
136 int16_t plus_one;
137 int16_t neg;
138 uint8_t sign_bit;
139 };
140 Case cases[] = {
141 {0, 1, 0, 0},
142 {-1, 0, 1, 1},
143 {1, 2, -1, 0},
144 {INT16_MAX, static_cast<int16_t>(INT16_MIN), -INT16_MAX, 0},
145 // INT16_MIN: -INT16_MIN is undefined in two's complement (overflows to
146 // itself), which is the *expected* behavior the hardware reproduces.
147 {INT16_MIN, static_cast<int16_t>(INT16_MIN + 1),
148 static_cast<int16_t>(INT16_MIN), 1},
149 };
150 for (const Case &c : cases) {
151 esi_system::SignResult r = connected->sign_probe(c.arg).get();
152 if (r.plus_one() != c.plus_one || r.neg() != c.neg ||
153 r.sign_bit() != c.sign_bit)
154 throw std::runtime_error("sign_probe mismatch for arg=" +
155 std::to_string(c.arg));
156 }
157 std::cout << "sign_probe ok\n";
158 return 0;
159}
160
161static int runSignProbe13(Accelerator *accel) {
162 esi_system::SignProbe13 mod(findProbe(accel, "sign_probe13_inst"));
163 auto connected = mod.connect();
164
165 // si13 ranges from -4096 (= -2^12) to 4095 (= 2^12 - 1). The boundary
166 // cases are where width-bounded sign extension and saturating-wrap
167 // arithmetic differ from si16 behavior; getting plus_one or neg right
168 // for both -4096 and 4095 forces the host serializer/deserializer to
169 // use the manifest's bit width, not sizeof(int16_t).
170 static constexpr int16_t kMin = -4096;
171 static constexpr int16_t kMax = 4095;
172 struct Case {
173 int16_t arg;
174 int16_t plus_one;
175 int16_t neg;
176 uint8_t sign_bit;
177 };
178 Case cases[] = {
179 {0, 1, 0, 0},
180 {-1, 0, 1, 1},
181 {1, 2, -1, 0},
182 {kMax, kMin, static_cast<int16_t>(-kMax), 0}, // 4095 + 1 wraps to -4096.
183 // -4096 is the si13 minimum: -kMin overflows back to -4096 (just like
184 // -INT16_MIN does for si16), and kMin + 1 = -4095.
185 {kMin, static_cast<int16_t>(kMin + 1), kMin, 1},
186 };
187 for (const Case &c : cases) {
188 esi_system::SignResult13 r = connected->sign_probe13(c.arg).get();
189 if (r.plus_one() != c.plus_one || r.neg() != c.neg ||
190 r.sign_bit() != c.sign_bit)
191 throw std::runtime_error(
192 "sign_probe13 mismatch for arg=" + std::to_string(c.arg) +
193 " got plus_one=" + std::to_string(r.plus_one()) +
194 " neg=" + std::to_string(r.neg()) +
195 " sign_bit=" + std::to_string(r.sign_bit()));
196 }
197
198 // TODO: Out-of-range si13 args. The generated facade declares
199 // `sign_probe13Args = int16_t`, and the runtime's `toMessageData` for
200 // int16_t -> si13 just copies the low 2 wire bytes with no truncation.
201 // So a host that does ordinary int16_t arithmetic and passes a value
202 // outside [-4096, 4095] silently sends a wrong si13 to the hardware.
203 // The expected behavior is one of: (a) the codegen emits a wrapper class
204 // (e.g. `sint13_t` with a 13-bit bitfield) that masks/sign-extends on
205 // construction so this round-trip works, or (b) the runtime rejects the
206 // value at write time. Either way the assertion below should hold.
207 // Re-enable once one of those is implemented.
208 //
209 // {
210 // int16_t out_of_range = 4096; // legal int16_t, illegal si13.
211 // esi_system::SignResult13 r = connected->sign_probe13(out_of_range).get();
212 // if (r.plus_one != static_cast<int16_t>(out_of_range + 1))
213 // throw std::runtime_error(
214 // "sign_probe13 out-of-range arg mismatch: arg=" +
215 // std::to_string(out_of_range) +
216 // " plus_one=" + std::to_string(r.plus_one));
217 // }
218
219 std::cout << "sign_probe13 ok\n";
220 return 0;
221}
222
223static int runPackProbe(Accelerator *accel) {
224 esi_system::PackProbe mod(findProbe(accel, "pack_probe_inst"));
225 auto connected = mod.connect();
226
227 esi_system::PackStruct arg{};
228 arg.a(0x01);
229 arg.b(0x0002);
230 arg.c(0x03);
231 arg.d(0x00000004);
232
233 esi_system::PackStruct r = connected->pack_probe(arg).get();
234 if (r.a() != 0xA1 || r.b() != 0xB002 || r.c() != 0xC3 || r.d() != 0xD0000004)
235 throw std::runtime_error("pack_probe field mismatch");
236 std::cout << "pack_probe ok: a=0x" << std::hex << (unsigned)r.a() << " b=0x"
237 << r.b() << " c=0x" << (unsigned)r.c() << " d=0x" << r.d()
238 << std::dec << "\n";
239 return 0;
240}
241
242static int runBitPackProbe(Accelerator *accel) {
243 esi_system::BitPackProbe mod(findProbe(accel, "bit_pack_probe_inst"));
244 auto connected = mod.connect();
245
246 esi_system::BitPackArg arg{};
247 // Distinct values within each width: x=001, y=10001, z=1001, w=1100. A
248 // shift error to a different field width would produce a value outside
249 // these picks.
250 arg.x(0b001);
251 arg.y(0b10001);
252 arg.z(0b1001);
253 arg.w(0b1100);
254
255 esi_system::BitPackResult r = connected->bit_pack_probe(arg).get();
256 if (r.w_field() != arg.x() || r.z_field() != arg.y() ||
257 r.y_field() != arg.z() || r.x_field() != arg.w())
258 throw std::runtime_error("bit_pack_probe field rotation mismatch");
259 std::cout << "bit_pack_probe ok\n";
260 return 0;
261}
262
263// Per-index sentinels (10, 20, 30, 40) make element order observable.
264// The PyCDE source reads the argument as `arg[i]` and returns
265// `arg[i] + 10*(i+1)` packed back into the result array at logical
266// index `i`. With the request bytes [0x01, 0x02, 0x03, 0x04], a wire
267// convention that places logical element `i` at wire byte `i` produces
268// a response of [11, 22, 33, 44]; a reverse-on-wire convention would
269// produce [41, 32, 23, 14]. Either way each byte uniquely identifies
270// its source index so the actual convention is observable from the
271// failure message.
272static int runArrayProbe(Accelerator *accel) {
273 // Driven through the raw FuncService port with explicitly constructed
274 // wire bytes -- not via the generated facade. Two reasons:
275 //
276 // 1. `TypedFunction<std::array<T,N>, ..., SkipTypeCheck=true>` would use
277 // `TypedPorts`' POD (memcpy) (de)serialization. That path is
278 // symmetric: it would round-trip a wrong-but-mirrored byte order as
279 // a false positive without ever exercising what the hardware
280 // actually puts on (or reads off) the wire.
281 // 2. The runtime's typed `ArrayType` (de)serializer in `types.py`
282 // reverses element order on the wire. Whether the hardware
283 // (PyCDE-emitted RTL) also reverses is exactly the convention this
284 // probe is meant to nail down. By writing/reading raw bytes the
285 // test pins down the wire format independent of any host-side
286 // mirror of that convention.
287
288 auto *func = findRawFunc(findProbe(accel, "array_probe_inst"), "array_probe");
289 func->connect();
290
291 static constexpr std::array<uint8_t, 4> kRequest = {0x01, 0x02, 0x03, 0x04};
292 MessageData resMsg =
293 func->call(MessageData(kRequest.data(), kRequest.size())).get();
294 if (resMsg.getSize() != kRequest.size())
295 throw std::runtime_error("array_probe: wrong response size (got " +
296 std::to_string(resMsg.getSize()) + ", expected " +
297 std::to_string(kRequest.size()) + ")");
298
299 static constexpr std::array<uint8_t, 4> kExpect = {11, 22, 33, 44};
300 const uint8_t *got = resMsg.getBytes();
301 for (size_t i = 0; i < kExpect.size(); ++i) {
302 if (got[i] != kExpect[i]) {
303 std::stringstream ss;
304 ss << "array_probe: wire byte " << i << " mismatch (got 0x" << std::hex
305 << static_cast<unsigned>(got[i]) << " expected 0x"
306 << static_cast<unsigned>(kExpect[i]) << ")";
307 throw std::runtime_error(ss.str());
308 }
309 }
310 std::cout << "array_probe ok (wire order: natural element-index)\n";
311 return 0;
312}
313
314ESI_PROBE_REGISTRY("serialization-probes",
315 "Hardware-vs-host serialization correctness probes for ESI.",
316 {"byte_rotate1", &runByteRotate1},
317 {"byte_pattern_const", &runBytePatternConst},
318 {"byte_pattern_echo_eq", &runBytePatternEchoEq},
319 {"sign_probe", &runSignProbe},
320 {"sign_probe13", &runSignProbe13},
321 {"pack_probe", &runPackProbe},
322 {"bit_pack_probe", &runBitPackProbe},
323 {"array_probe", &runArrayProbe}, );
Top level accelerator class.
Definition Accelerator.h:77
Represents either the top level or an instance of a hardware module.
Definition Design.h:47
const std::map< AppID, BundlePort & > & getPorts() const
Access the module's ports by ID.
Definition Design.h:80
const std::map< AppID, Instance * > & getChildren() const
Access the module's children by ID.
Definition Design.h:71
A concrete flat message backed by a single vector of bytes.
Definition Common.h:155
const uint8_t * getBytes() const
Definition Common.h:166
size_t getSize() const
Get the size of the data in bytes.
Definition Common.h:180
A function call which gets attached to a service port.
Definition Services.h:353
Definition esi.py:1
std::string toHex(void *val)
Definition Common.cpp:37
#define ESI_PROBE_REGISTRY(name, description,...)
Convenience macro: defines main() with a probe registry.
static int runPackProbe(Accelerator *accel)
static services::FuncService::Function * findRawFunc(esi::HWModule *probe, const char *portName)
static esi::HWModule * findProbe(Accelerator *accel, const char *appidName)
static int runBytePatternConst(Accelerator *accel)
static int runSignProbe13(Accelerator *accel)
static int runArrayProbe(Accelerator *accel)
static constexpr std::array< uint8_t, 8 > kBytePattern
static int runBytePatternEchoEq(Accelerator *accel)
static int runSignProbe(Accelerator *accel)
static int runByteRotate1(Accelerator *accel)
static int runBitPackProbe(Accelerator *accel)