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 || r.sign_bit != c.sign_bit)
153 throw std::runtime_error("sign_probe mismatch for arg=" +
154 std::to_string(c.arg));
155 }
156 std::cout << "sign_probe ok\n";
157 return 0;
158}
159
160static int runSignProbe13(Accelerator *accel) {
161 esi_system::SignProbe13 mod(findProbe(accel, "sign_probe13_inst"));
162 auto connected = mod.connect();
163
164 // si13 ranges from -4096 (= -2^12) to 4095 (= 2^12 - 1). The boundary
165 // cases are where width-bounded sign extension and saturating-wrap
166 // arithmetic differ from si16 behavior; getting plus_one or neg right
167 // for both -4096 and 4095 forces the host serializer/deserializer to
168 // use the manifest's bit width, not sizeof(int16_t).
169 static constexpr int16_t kMin = -4096;
170 static constexpr int16_t kMax = 4095;
171 struct Case {
172 int16_t arg;
173 int16_t plus_one;
174 int16_t neg;
175 uint8_t sign_bit;
176 };
177 Case cases[] = {
178 {0, 1, 0, 0},
179 {-1, 0, 1, 1},
180 {1, 2, -1, 0},
181 {kMax, kMin, static_cast<int16_t>(-kMax), 0}, // 4095 + 1 wraps to -4096.
182 // -4096 is the si13 minimum: -kMin overflows back to -4096 (just like
183 // -INT16_MIN does for si16), and kMin + 1 = -4095.
184 {kMin, static_cast<int16_t>(kMin + 1), kMin, 1},
185 };
186 for (const Case &c : cases) {
187 esi_system::SignResult13 r = connected->sign_probe13(c.arg).get();
188 if (r.plus_one != c.plus_one || r.neg != c.neg || r.sign_bit != c.sign_bit)
189 throw std::runtime_error(
190 "sign_probe13 mismatch for arg=" + std::to_string(c.arg) +
191 " got plus_one=" + std::to_string(r.plus_one) + " neg=" +
192 std::to_string(r.neg) + " sign_bit=" + std::to_string(r.sign_bit));
193 }
194
195 // TODO: Out-of-range si13 args. The generated facade declares
196 // `sign_probe13Args = int16_t`, and the runtime's `toMessageData` for
197 // int16_t -> si13 just copies the low 2 wire bytes with no truncation.
198 // So a host that does ordinary int16_t arithmetic and passes a value
199 // outside [-4096, 4095] silently sends a wrong si13 to the hardware.
200 // The expected behavior is one of: (a) the codegen emits a wrapper class
201 // (e.g. `sint13_t` with a 13-bit bitfield) that masks/sign-extends on
202 // construction so this round-trip works, or (b) the runtime rejects the
203 // value at write time. Either way the assertion below should hold.
204 // Re-enable once one of those is implemented.
205 //
206 // {
207 // int16_t out_of_range = 4096; // legal int16_t, illegal si13.
208 // esi_system::SignResult13 r = connected->sign_probe13(out_of_range).get();
209 // if (r.plus_one != static_cast<int16_t>(out_of_range + 1))
210 // throw std::runtime_error(
211 // "sign_probe13 out-of-range arg mismatch: arg=" +
212 // std::to_string(out_of_range) +
213 // " plus_one=" + std::to_string(r.plus_one));
214 // }
215
216 std::cout << "sign_probe13 ok\n";
217 return 0;
218}
219
220static int runPackProbe(Accelerator *accel) {
221 esi_system::PackProbe mod(findProbe(accel, "pack_probe_inst"));
222 auto connected = mod.connect();
223
224 esi_system::PackStruct arg{};
225 arg.a = 0x01;
226 arg.b = 0x0002;
227 arg.c = 0x03;
228 arg.d = 0x00000004;
229
230 esi_system::PackStruct r = connected->pack_probe(arg).get();
231 if (r.a != 0xA1 || r.b != 0xB002 || r.c != 0xC3 || r.d != 0xD0000004)
232 throw std::runtime_error("pack_probe field mismatch");
233 std::cout << "pack_probe ok: a=0x" << std::hex << (unsigned)r.a << " b=0x"
234 << r.b << " c=0x" << (unsigned)r.c << " d=0x" << r.d << std::dec
235 << "\n";
236 return 0;
237}
238
239static int runBitPackProbe(Accelerator *accel) {
240 esi_system::BitPackProbe mod(findProbe(accel, "bit_pack_probe_inst"));
241 auto connected = mod.connect();
242
243 esi_system::BitPackArg arg{};
244 // Distinct values within each width: x=001, y=10001, z=1001, w=1100. A
245 // shift error to a different field width would produce a value outside
246 // these picks.
247 arg.x = 0b001;
248 arg.y = 0b10001;
249 arg.z = 0b1001;
250 arg.w = 0b1100;
251
252 esi_system::BitPackResult r = connected->bit_pack_probe(arg).get();
253 if (r.w_field != arg.x || r.z_field != arg.y || r.y_field != arg.z ||
254 r.x_field != arg.w)
255 throw std::runtime_error("bit_pack_probe field rotation mismatch");
256 std::cout << "bit_pack_probe ok\n";
257 return 0;
258}
259
260// Per-index sentinels (10, 20, 30, 40) make element order observable.
261// The PyCDE source reads the argument as `arg[i]` and returns
262// `arg[i] + 10*(i+1)` packed back into the result array at logical
263// index `i`. With the request bytes [0x01, 0x02, 0x03, 0x04], a wire
264// convention that places logical element `i` at wire byte `i` produces
265// a response of [11, 22, 33, 44]; a reverse-on-wire convention would
266// produce [41, 32, 23, 14]. Either way each byte uniquely identifies
267// its source index so the actual convention is observable from the
268// failure message.
269static int runArrayProbe(Accelerator *accel) {
270 // Driven through the raw FuncService port with explicitly constructed
271 // wire bytes -- not via the generated facade. Two reasons:
272 //
273 // 1. `TypedFunction<std::array<T,N>, ..., SkipTypeCheck=true>` would use
274 // `TypedPorts`' POD (memcpy) (de)serialization. That path is
275 // symmetric: it would round-trip a wrong-but-mirrored byte order as
276 // a false positive without ever exercising what the hardware
277 // actually puts on (or reads off) the wire.
278 // 2. The runtime's typed `ArrayType` (de)serializer in `types.py`
279 // reverses element order on the wire. Whether the hardware
280 // (PyCDE-emitted RTL) also reverses is exactly the convention this
281 // probe is meant to nail down. By writing/reading raw bytes the
282 // test pins down the wire format independent of any host-side
283 // mirror of that convention.
284
285 auto *func = findRawFunc(findProbe(accel, "array_probe_inst"), "array_probe");
286 func->connect();
287
288 static constexpr std::array<uint8_t, 4> kRequest = {0x01, 0x02, 0x03, 0x04};
289 MessageData resMsg =
290 func->call(MessageData(kRequest.data(), kRequest.size())).get();
291 if (resMsg.getSize() != kRequest.size())
292 throw std::runtime_error("array_probe: wrong response size (got " +
293 std::to_string(resMsg.getSize()) + ", expected " +
294 std::to_string(kRequest.size()) + ")");
295
296 static constexpr std::array<uint8_t, 4> kExpect = {11, 22, 33, 44};
297 const uint8_t *got = resMsg.getBytes();
298 for (size_t i = 0; i < kExpect.size(); ++i) {
299 if (got[i] != kExpect[i]) {
300 std::stringstream ss;
301 ss << "array_probe: wire byte " << i << " mismatch (got 0x" << std::hex
302 << static_cast<unsigned>(got[i]) << " expected 0x"
303 << static_cast<unsigned>(kExpect[i]) << ")";
304 throw std::runtime_error(ss.str());
305 }
306 }
307 std::cout << "array_probe ok (wire order: natural element-index)\n";
308 return 0;
309}
310
311ESI_PROBE_REGISTRY("serialization-probes",
312 "Hardware-vs-host serialization correctness probes for ESI.",
313 {"byte_rotate1", &runByteRotate1},
314 {"byte_pattern_const", &runBytePatternConst},
315 {"byte_pattern_echo_eq", &runBytePatternEchoEq},
316 {"sign_probe", &runSignProbe},
317 {"sign_probe13", &runSignProbe13},
318 {"pack_probe", &runPackProbe},
319 {"bit_pack_probe", &runBitPackProbe},
320 {"array_probe", &runArrayProbe}, );
Top level accelerator class.
Definition Accelerator.h:70
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)