CIRCT 24.0.0git
Loading...
Searching...
No Matches
codegen_harness.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
2//
3// Round-trip harness driven by `tests/unit/test_codegen.py`. The Python
4// fixture builds a small manifest, runs `esiaccel.codegen` on it, and
5// compiles this file against the resulting `types.h`. The harness asserts
6// the generated accessors round-trip user-visible values *and* that the
7// underlying wire bytes match the manifest's bit-packed layout exactly.
8//
9// Reviewing this file is the easiest way to see what the C++ codegen
10// promises end-to-end; if anything here changes the runtime semantics
11// (rather than just the textual emission), this harness is what will
12// catch it.
13
14#include "codegen_harness/types.h"
15
16#include <array>
17#include <cassert>
18#ifdef _MSC_VER
19#include <crtdbg.h>
20#include <cstdlib>
21#endif
22#include <cstdint>
23#include <cstdio>
24#include <cstring>
25#include <memory>
26#include <ranges>
27#include <type_traits>
28#include <vector>
29
30#include "esi/Values.h"
31
32using namespace esi_system;
33
34namespace {
35
36// Reinterpret `value` as a `std::array<uint8_t, sizeof(T)>` view of its
37// wire bytes. Used to assert the underlying byte layout of generated
38// types — the only safe API for that is `reinterpret_cast<uint8_t *>`,
39// which the runtime already uses internally via `MessageData::from()`.
40template <typename T>
41std::array<uint8_t, sizeof(T)> wireBytes(const T &value) {
42 std::array<uint8_t, sizeof(T)> out{};
43 const auto *src = reinterpret_cast<const uint8_t *>(&value);
44 for (std::size_t i = 0; i < sizeof(T); ++i)
45 out[i] = src[i];
46 return out;
47}
48
49template <typename T, std::size_t N>
50void expectBytes(const T &value, const std::array<uint8_t, N> &expected,
51 const char *label) {
52 static_assert(sizeof(T) == N, "wire-byte assertion size mismatch");
53 auto got = wireBytes(value);
54 for (std::size_t i = 0; i < N; ++i) {
55 if (got[i] != expected[i]) {
56 std::fprintf(stderr,
57 "%s: byte %zu mismatch: got 0x%02x expected 0x%02x\n", label,
58 i, got[i], expected[i]);
59 std::fprintf(stderr, " got :");
60 for (auto b : got)
61 std::fprintf(stderr, " %02x", b);
62 std::fprintf(stderr, "\n expected:");
63 for (auto b : expected)
64 std::fprintf(stderr, " %02x", b);
65 std::fprintf(stderr, "\n");
66 std::abort();
67 }
68 }
69}
70
71// ---------------------------------------------------------------------------
72// Standard 8/16/32/64-bit byte-aligned integers
73// ---------------------------------------------------------------------------
74
75void testStandardWidthUnsigned() {
76 StdU s;
77 s.u8(0xAB).u16(0xCAFE).u32(0xDEADBEEFu).u64(0x0123456789ABCDEFull);
78 assert(s.u8() == 0xAB);
79 assert(s.u16() == 0xCAFE);
80 assert(s.u32() == 0xDEADBEEFu);
81 assert(s.u64() == 0x0123456789ABCDEFull);
82
83 // Wire order is reverse of declaration: u64 at byte 0, u32 at byte 8,
84 // u16 at byte 12, u8 at byte 14. Each value is little-endian within
85 // its own bytes.
86 expectBytes(s,
87 std::array<uint8_t, 15>{
88 // u64 = 0x0123456789ABCDEF, little-endian:
89 0xEF,
90 0xCD,
91 0xAB,
92 0x89,
93 0x67,
94 0x45,
95 0x23,
96 0x01,
97 // u32 = 0xDEADBEEF, little-endian:
98 0xEF,
99 0xBE,
100 0xAD,
101 0xDE,
102 // u16 = 0xCAFE, little-endian:
103 0xFE,
104 0xCA,
105 // u8 = 0xAB:
106 0xAB,
107 },
108 "StdU");
109
110 // Default construction zero-initialises the wire bytes.
111 StdU empty;
112 expectBytes(
113 empty,
114 std::array<uint8_t, 15>{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
115 "StdU empty");
116
117 // Logical-order ctor matches manifest field order.
118 StdU cons(0xAB, 0xCAFE, 0xDEADBEEFu, 0x0123456789ABCDEFull);
119 assert(wireBytes(cons) == wireBytes(s));
120}
121
122void testStandardWidthSigned() {
123 StdS s;
124 s.s8(-7).s16(-1000).s32(-1234567).s64(INT64_MIN);
125 assert(s.s8() == -7);
126 assert(s.s16() == -1000);
127 assert(s.s32() == -1234567);
128 assert(s.s64() == INT64_MIN);
129
130 // Default-construct a fresh value to confirm round-trip without the
131 // chained setter; check INT64_MAX as well to verify both ends of the
132 // range.
133 StdS pos(127, 32767, 2147483647, INT64_MAX);
134 assert(pos.s8() == 127);
135 assert(pos.s64() == INT64_MAX);
136}
137
138// ---------------------------------------------------------------------------
139// Byte-aligned non-standard width (e.g. i24, i48)
140// ---------------------------------------------------------------------------
141
142void testByteAlignedOddWidthUnsigned() {
143 OddU s;
144 s.u24(0xABCDEFu);
145 assert(s.u24() == 0xABCDEFu);
146 // ui24 is 3 bytes little-endian on the wire.
147 expectBytes(s, std::array<uint8_t, 3>{0xEF, 0xCD, 0xAB}, "OddU");
148}
149
150void testByteAlignedOddWidthSigned() {
151 OddS s;
152 s.s24(-1);
153 assert(s.s24() == -1);
154 expectBytes(s, std::array<uint8_t, 3>{0xFF, 0xFF, 0xFF}, "OddS -1");
155
156 s.s24(-8388608); // si24 min
157 assert(s.s24() == -8388608);
158 expectBytes(s, std::array<uint8_t, 3>{0x00, 0x00, 0x80}, "OddS min");
159
160 s.s24(8388607); // si24 max
161 assert(s.s24() == 8388607);
162 expectBytes(s, std::array<uint8_t, 3>{0xFF, 0xFF, 0x7F}, "OddS max");
163
164 // Negative value with all three bytes non-zero. Round-trip + bit pattern.
165 s.s24(-1234567);
166 assert(s.s24() == -1234567);
167}
168
169// ---------------------------------------------------------------------------
170// Sub-byte alignment (uses the BitAccess helpers)
171// ---------------------------------------------------------------------------
172
173void testSubByteUnsigned() {
174 SubU s;
175 s.u3(0b101).u12(0xABC);
176 assert(s.u3() == 0b101);
177 assert(s.u12() == 0xABC);
178
179 // Wire layout: u12 (low 12 bits) then u3 (next 3 bits), tightly packed.
180 // bit 0..11 = 0xABC -> bytes 0=0xBC, byte 1 low nibble = 0xA, high
181 // nibble starts u3
182 // bit 12..14 = 0b101 -> byte 1 high 3 bits = 0b101 << 4 = 0x50
183 // total 15 bits in 2 bytes: byte 0 = 0xBC, byte 1 = 0x5A.
184 expectBytes(s, std::array<uint8_t, 2>{0xBC, 0x5A}, "SubU");
185}
186
187void testSubByteSigned() {
188 SubS s;
189 s.s5(-3).s7(-50);
190 assert(s.s5() == -3);
191 assert(s.s7() == -50);
192
193 // s5 = -3 (5-bit two's complement = 0x1D); s7 = -50 (7-bit = 0x4E).
194 // Wire (reversed): s7 in bit 0..6, s5 in bit 7..11.
195 // bit 0..6 = 0b1001110 (0x4E)
196 // bit 7 = s5 bit 0 (LSB of -3 = 1)
197 // bit 8..11 = s5 bit 1..4
198 // s5 = 0x1D = 0b11101 -> bits 7..11 = 11101
199 // byte 0 = (0x4E) | (1 << 7) = 0xCE
200 // byte 1 = 0b00001110 = 0x0E
201 expectBytes(s, std::array<uint8_t, 2>{0xCE, 0x0E}, "SubS");
202}
203
204void testBoolField() {
205 BoolField f;
206 f.flag(true).pad(0x5A);
207 assert(f.flag() == true);
208 assert(f.pad() == 0x5A);
209 // flag is at bit 7 (after the ui7 pad, which is wire-reversed first).
210 // byte 0 = 0x80 | 0x5A = 0xDA
211 expectBytes(f, std::array<uint8_t, 1>{0xDA}, "BoolField true");
212
213 f.flag(false);
214 expectBytes(f, std::array<uint8_t, 1>{0x5A}, "BoolField false");
215}
216
217// ---------------------------------------------------------------------------
218// Nested struct fields (aggregate accessor)
219// ---------------------------------------------------------------------------
220
221void testNestedStruct() {
222 Inner in;
223 in.x(0x12).y(0x34);
224
225 Outer o;
226 o.label(0xAB).inner(in);
227 assert(o.label() == 0xAB);
228 assert(o.inner().x() == 0x12);
229 assert(o.inner().y() == 0x34);
230
231 // Wire order: inner first (in declaration-reversed order), then label.
232 // Inner has y at byte 0, x at byte 1 (reversed inside inner too).
233 expectBytes(o, std::array<uint8_t, 3>{0x34, 0x12, 0xAB}, "Outer");
234
235 // Modifying a returned inner does NOT alias back into the outer
236 // (accessor returns a value, not a reference) — the caller has to
237 // hand the modified inner back via `outer.inner(modified)`.
238 Outer o2;
239 o2.inner(in);
240 auto copy = o2.inner();
241 copy.x(0xFF);
242 assert(o2.inner().x() == 0x12); // unchanged
243 o2.inner(copy);
244 assert(o2.inner().x() == 0xFF);
245}
246
247void testNestedStructMisaligned() {
248 // `Misaligned` wire layout (LSB-first):
249 // bits 0..2 = tag (ui3) = 0b101 (= 5)
250 // bits 3..10 = inner.y (ui8 LSB at outer bit 3) = 0x34
251 // bits 11..18 = inner.x (ui8) = 0x12
252 // bits 19..23 = padding (0)
253 // Inner is byte-aligned internally (x at inner bit 8, y at inner bit 0
254 // with the standard reverse), but the inner aggregate as a whole lands
255 // at outer bit 3 — exercising the `copyBitsIn`/`copyBitsOut` path
256 // rather than the byte-aligned fast copy.
257 MisInner mi;
258 mi.x(0x12).y(0x34);
259
260 Misaligned m;
261 m.tag(0b101).inner(mi);
262 assert(m.tag() == 0b101);
263 assert(m.inner().x() == 0x12);
264 assert(m.inner().y() == 0x34);
265
266 // Hand-derived bytes from the bit layout above.
267 expectBytes(m, std::array<uint8_t, 3>{0xA5, 0x91, 0x00},
268 "Misaligned all-set");
269
270 // Setting tag again after writing inner must not disturb inner's bits
271 // (the writer is supposed to mask only the tag's bit range).
272 m.tag(0b000);
273 assert(m.inner().x() == 0x12);
274 assert(m.inner().y() == 0x34);
275 expectBytes(m, std::array<uint8_t, 3>{0xA0, 0x91, 0x00},
276 "Misaligned tag cleared");
277
278 // Setting inner again after writing tag must not disturb tag.
279 m.tag(0b101);
280 MisInner mi2;
281 mi2.x(0xFF).y(0x00);
282 m.inner(mi2);
283 assert(m.tag() == 0b101);
284 assert(m.inner().x() == 0xFF);
285 assert(m.inner().y() == 0x00);
286 // bits 0..2 = 0b101 = 5
287 // bits 3..10 = 0x00 = 0
288 // bits 11..18 = 0xFF = all 1s spanning byte 1 high and
289 // byte 2 low bits
290 // byte 0: tag(3) | y[0..4]<<3 = 0b00000101 = 0x05
291 // byte 1: y[5..7] | x[0..4]<<3 = 0 | 0b11111<<3 = 0b11111000 = 0xF8
292 // byte 2: x[5..7] | pad = 0b00000111 = 0x07
293 expectBytes(m, std::array<uint8_t, 3>{0x05, 0xF8, 0x07},
294 "Misaligned inner all-ones");
295}
296
297// ---------------------------------------------------------------------------
298// Array fields: whole-array and indexed accessors
299// ---------------------------------------------------------------------------
300
301void testArrayOfIntegers() {
302 Arr4 a;
303 a.r({0x10, 0x20, 0x30, 0x40});
304 auto r = a.r();
305 assert(r[0] == 0x10 && r[1] == 0x20 && r[2] == 0x30 && r[3] == 0x40);
306 // Indexed read/write convenience overloads.
307 assert(a.r(2) == 0x30);
308 a.r(2, 0x99);
309 assert(a.r(2) == 0x99);
310
311 // Arrays are laid out element-0-first on the wire (matching the
312 // existing `std::array` layout), not reversed.
313 expectBytes(a, std::array<uint8_t, 4>{0x10, 0x20, 0x99, 0x40}, "Arr4");
314}
315
316// ---------------------------------------------------------------------------
317// Packed arrays: element storage size != on-wire element width
318// ---------------------------------------------------------------------------
319//
320// These exercise the per-element bit-unpacking path. Unlike `Arr4` (whose
321// `ui8` elements are whole bytes), the C++ `std::array` here is wider in
322// memory than the bit-packed wire layout, so a flat byte copy would both
323// truncate the field and misplace every element after the first.
324
325void testSubByteUnsignedArray() {
326 // `8 x ui3` => 24 wire bits (3 bytes) packed into a `std::array<uint8_t, 8>`
327 // (8 bytes in memory). Each element occupies its own 3-bit window.
328 U3Arr a;
329 a.vals({1, 2, 3, 4, 5, 6, 7, 0});
330 const std::array<uint8_t, 8> expected = {1, 2, 3, 4, 5, 6, 7, 0};
331 auto whole = a.vals();
332 for (std::size_t i = 0; i < 8; ++i) {
333 assert(whole[i] == expected[i]);
334 assert(a.vals(i) == expected[i]);
335 }
336 // bit-packed LSB-first: elem i at bits [3i, 3i+2].
337 expectBytes(a, std::array<uint8_t, 3>{0xD1, 0x58, 0x1F}, "U3Arr packed");
338
339 // A single element straddling a byte boundary (elem 2 spans bits 6..8).
340 U3Arr b;
341 b.vals(2, 7);
342 assert(b.vals(2) == 7);
343 for (std::size_t i = 0; i < 8; ++i)
344 if (i != 2)
345 assert(b.vals(i) == 0);
346 expectBytes(b, std::array<uint8_t, 3>{0xC0, 0x01, 0x00}, "U3Arr cross-byte");
347}
348
349void testSubByteBoolArray() {
350 // `8 x ui1` => 8 wire bits (1 byte) packed into a `std::array<bool, 8>`
351 // (8 bytes in memory). Each element is a single bit; reads must yield a
352 // valid `bool` (0/1) rather than dumping a raw byte into bool storage.
353 Bits1Arr a;
354 const std::array<bool, 8> flags = {true, true, false, false,
355 true, false, true, true};
356 a.flags(flags);
357 auto whole = a.flags();
358 for (std::size_t i = 0; i < 8; ++i) {
359 assert(whole[i] == flags[i]);
360 assert(a.flags(i) == flags[i]);
361 }
362 expectBytes(a, std::array<uint8_t, 1>{0xD3}, "Bits1Arr");
363}
364
365void testSubByteSignedArray() {
366 // `4 x si5` => 20 wire bits (3 bytes) packed into a `std::array<int8_t, 4>`
367 // (4 bytes in memory). Reads must sign-extend the 5-bit value.
368 S5Arr a;
369 const std::array<int8_t, 4> vals = {-1, -16, 15, 0};
370 a.vals(vals);
371 auto whole = a.vals();
372 for (std::size_t i = 0; i < 4; ++i) {
373 assert(whole[i] == vals[i]);
374 assert(a.vals(i) == vals[i]);
375 }
376 expectBytes(a, std::array<uint8_t, 3>{0x1F, 0x3E, 0x00}, "S5Arr");
377}
378
379void testOddWidthArray() {
380 // `2 x ui24` => 48 wire bits (6 bytes) packed into a `std::array<uint32_t,
381 // 2>` (8 bytes in memory). The element wire stride (3 bytes) differs from
382 // the storage stride (4 bytes), so a flat copy would corrupt elem 1.
383 U24Arr a;
384 const std::array<uint32_t, 2> vals = {0xABCDEFu, 0x123456u};
385 a.vals(vals);
386 auto whole = a.vals();
387 for (std::size_t i = 0; i < 2; ++i) {
388 assert(whole[i] == vals[i]);
389 assert(a.vals(i) == vals[i]);
390 }
391 expectBytes(a, std::array<uint8_t, 6>{0xEF, 0xCD, 0xAB, 0x56, 0x34, 0x12},
392 "U24Arr");
393}
394
395void testSubByteStructArray() {
396 // `4 x {ui3 hi, ui2 lo}`: each element is 5 wire bits (20 bits = 3 bytes)
397 // but a padded 1-byte `SbCell` in memory, so the whole-array accessor must
398 // unpack each element from its own 5-bit window. Within a cell `lo` lands
399 // at bits 0..1 and `hi` at bits 2..4 (wire order is reversed manifest
400 // order).
401 SbCellArr a;
402 std::array<SbCell, 4> cells;
403 cells[0] = SbCell(1, 0);
404 cells[1] = SbCell(2, 1);
405 cells[2] = SbCell(3, 2);
406 cells[3] = SbCell(7, 3);
407 a.cells(cells);
408
409 auto whole = a.cells();
410 for (std::size_t i = 0; i < 4; ++i) {
411 assert(whole[i].hi() == cells[i].hi());
412 assert(whole[i].lo() == cells[i].lo());
413 assert(a.cells(i).hi() == cells[i].hi());
414 assert(a.cells(i).lo() == cells[i].lo());
415 }
416 // cell i value = (hi << 2) | lo, placed at bits [5i, 5i+4].
417 expectBytes(a, std::array<uint8_t, 3>{0x24, 0xB9, 0x0F}, "SbCellArr");
418
419 // Indexed setter on a single element straddling a byte boundary (cell 1
420 // spans bits 5..9). Other cells stay zero.
421 SbCellArr b;
422 b.cells(1, SbCell(7, 3));
423 assert(b.cells(1).hi() == 7);
424 assert(b.cells(1).lo() == 3);
425 for (std::size_t i = 0; i < 4; ++i)
426 if (i != 1) {
427 assert(b.cells(i).hi() == 0);
428 assert(b.cells(i).lo() == 0);
429 }
430 expectBytes(b, std::array<uint8_t, 3>{0xE0, 0x03, 0x00},
431 "SbCellArr cross-byte");
432}
433
434// ---------------------------------------------------------------------------
435// Nested arrays (array-of-array) -- previously mis-handled by the flat-copy
436// path; now driven by the recursive per-element (un)packer.
437// ---------------------------------------------------------------------------
438
439void testNestedIntArray() {
440 // `2 x 4 x ui3`: the element type is itself the non-byte-packable array
441 // `4 x ui3` (12 wire bits, 4 bytes in memory). The old codegen emitted a
442 // corrupt whole-array copy and no indexed accessor; the recursive packer
443 // places each `ui3` leaf at wire bit `i * 12 + j * 3`.
444 Nested3 a;
445 std::array<std::array<uint8_t, 4>, 2> rows = {{{1, 2, 3, 4}, {5, 6, 7, 0}}};
446 a.rows(rows);
447
448 auto whole = a.rows();
449 for (std::size_t i = 0; i < 2; ++i)
450 for (std::size_t j = 0; j < 4; ++j) {
451 assert(whole[i][j] == rows[i][j]);
452 assert(a.rows(i)[j] == rows[i][j]);
453 }
454 // Flattened [1,2,3,4,5,6,7,0] at 3-bit strides -- identical wire bytes to
455 // the flat `8 x ui3` case.
456 expectBytes(a, std::array<uint8_t, 3>{0xD1, 0x58, 0x1F}, "Nested3");
457
458 // Indexed setter for one inner row; the other row must stay zero.
459 Nested3 b;
460 b.rows(1, {5, 6, 7, 0});
461 for (std::size_t j = 0; j < 4; ++j) {
462 assert(b.rows(1)[j] == rows[1][j]);
463 assert(b.rows(0)[j] == 0);
464 }
465 // Row 1 occupies bits 12..23: [5,6,7,0].
466 expectBytes(b, std::array<uint8_t, 3>{0x00, 0x50, 0x1F}, "Nested3 row1");
467}
468
469void testNestedStructArray() {
470 // `2 x 3 x SbCell` (each cell 5 wire bits). Array-of-array-of-sub-byte-
471 // aggregate: the recursive packer copies each cell's bits at its true
472 // wire offset `i * 15 + j * 5`.
473 NestedCell a;
474 std::array<std::array<SbCell, 3>, 2> grid = {{
475 {SbCell(1, 0), SbCell(2, 1), SbCell(3, 2)},
476 {SbCell(4, 3), SbCell(5, 0), SbCell(6, 1)},
477 }};
478 a.grid(grid);
479
480 auto whole = a.grid();
481 for (std::size_t i = 0; i < 2; ++i)
482 for (std::size_t j = 0; j < 3; ++j) {
483 assert(whole[i][j].hi() == grid[i][j].hi());
484 assert(whole[i][j].lo() == grid[i][j].lo());
485 assert(a.grid(i)[j].hi() == grid[i][j].hi());
486 assert(a.grid(i)[j].lo() == grid[i][j].lo());
487 }
488 // grid[0] reuses the first three SbCellArr cells, so bytes 0..1 match that
489 // test's leading bytes.
490 expectBytes(a, std::array<uint8_t, 4>{0x24, 0xB9, 0x49, 0x33}, "NestedCell");
491}
492
493// ---------------------------------------------------------------------------
494// Unions
495// ---------------------------------------------------------------------------
496
497void testUnion() {
498 UnionTwo u;
499 u.big(0xCAFE);
500 assert(u.big() == 0xCAFE);
501 assert(u.small() == 0xFE);
502 expectBytes(u, std::array<uint8_t, 2>{0xFE, 0xCA}, "Union big");
503
504 u.small(0xA5);
505 assert(u.small() == 0xA5);
506 // Setters preserve bits outside the selected member.
507 expectBytes(u, std::array<uint8_t, 2>{0xA5, 0xCA}, "Union small");
508
509 UnionTwo narrow;
510 narrow.small(0xA5);
511 expectBytes(narrow, std::array<uint8_t, 2>{0xA5, 0}, "Union padding");
512
513 UnionSubByte sub;
514 sub.big(0xABC);
515 assert(sub.tiny() == 4);
516 assert(sub.signed_small() == -4);
517 assert(sub.cell().hi() == 7);
518 assert(sub.cell().lo() == 0);
519 sub.tiny(5);
520 assert(sub.big() == 0xABD);
521 sub.signed_small(-7);
522 assert(sub.big() == 0xAB9);
523 sub.cell(SbCell(2, 1));
524 assert(sub.big() == 0xAA9);
525 expectBytes(sub, std::array<uint8_t, 2>{0xA9, 0x0A}, "Sub-byte union");
526}
527
528// ---------------------------------------------------------------------------
529// Window helpers (header / data frame round-trip)
530// ---------------------------------------------------------------------------
531
532void testWindowList() {
533 std::vector<uint32_t> elements = {0xAAAA0001u, 0xBBBB0002u, 0xCCCC0003u};
534 ListWindow win(0xDEAD, elements);
535
536 // Header-field accessor reads the static `tag`. Count comes from the
537 // `<list>_count()` accessor and matches the number of data frames.
538 assert(win.tag() == 0xDEAD);
539 assert(win.items_count() == elements.size());
540
541 // Vector helper materialises the data into a flat std::vector.
542 auto got = win.items_vector();
543 assert(got.size() == elements.size());
544 for (std::size_t i = 0; i < elements.size(); ++i)
545 assert(got[i] == elements[i]);
546
547 // Range accessor produces the same values lazily.
548 std::size_t i = 0;
549 for (uint32_t v : win.items())
550 assert(v == elements[i++]);
551
552 // The window splits into three wire segments: header, data, footer.
553 assert(win.numSegments() == 3);
554 auto header_seg = win.segment(0);
555 auto data_seg = win.segment(1);
556 auto footer_seg = win.segment(2);
557 assert(data_seg.size == elements.size() * sizeof(uint32_t));
558 // The header fills the frame: [count_lo, count_hi, tag_lo, tag_hi].
559 assert(header_seg.size == 4);
560 assert(header_seg.data[0] == (static_cast<uint8_t>(elements.size()) & 0xFF));
561 assert(header_seg.data[1] == 0); // count high byte (size < 256)
562 assert(header_seg.data[2] == 0xAD);
563 assert(header_seg.data[3] == 0xDE);
564 // Footer is a zero-count header with the same layout.
565 assert(footer_seg.size == 4);
566 assert(footer_seg.data[0] == 0);
567 assert(footer_seg.data[1] == 0);
568}
569
570// ---------------------------------------------------------------------------
571// ---------------------------------------------------------------------------
572// Multi-burst chunking: a window whose count field is too narrow to encode
573// the whole list in one burst must split the list across several
574// header/data bursts (terminated by a zero-count footer) and the read-side
575// deserializer must reassemble it back into the original list.
576// ---------------------------------------------------------------------------
577
578void testWindowListMultiBurst() {
579 // SmallListWindow has a 2-bit count field, so at most 3 items fit in one
580 // burst. A 7-item list therefore splits into 3 bursts of 3 + 3 + 1 items.
581 std::vector<uint32_t> elements = {0x11111111u, 0x22222222u, 0x33333333u,
582 0x44444444u, 0x55555555u, 0x66666666u,
583 0x77777777u};
584 SmallListWindow win(0xBEEF, elements);
585
586 assert(win.tag() == 0xBEEF);
587 assert(win.items_count() == elements.size());
588
589 // ceil(7 / 3) = 3 bursts -> 3 headers + 3 data + 1 footer = 7 segments.
590 assert(win.numSegments() == 2 * 3 + 1);
591
592 // Each header / footer frame is 4 bytes. Data segments carry 3, 3, then 1
593 // item(s), one ui32 (4 bytes) per item.
594 assert(win.segment(0).size == 4); // burst 0 header
595 assert(win.segment(1).size == 3 * sizeof(uint32_t)); // burst 0 data
596 assert(win.segment(2).size == 4); // burst 1 header
597 assert(win.segment(3).size == 3 * sizeof(uint32_t)); // burst 1 data
598 assert(win.segment(4).size == 4); // burst 2 header
599 assert(win.segment(5).size == 1 * sizeof(uint32_t)); // burst 2 data
600 assert(win.segment(6).size == 4); // footer
601
602 // The 18-bit header shares the frame's LSB: count at [1:0], tag at [17:2],
603 // and zero padding at [31:18]. Only the first burst carries the tag.
604 auto headerCount = [](const auto &s) -> unsigned { return s.data[0] & 0x3; };
605 assert(headerCount(win.segment(0)) == 3);
606 assert(headerCount(win.segment(2)) == 3);
607 assert(headerCount(win.segment(4)) == 1);
608 assert(headerCount(win.segment(6)) == 0);
609 assert(win.segment(0).data[0] == 0xBF);
610 assert(win.segment(0).data[1] == 0xFB);
611 assert(win.segment(0).data[2] == 0x02);
612 assert(win.segment(0).data[3] == 0x00);
613 assert(win.segment(2).data[0] == 0x03);
614 assert(win.segment(2).data[1] == 0x00);
615 assert(win.segment(2).data[2] == 0x00);
616 assert(win.segment(2).data[3] == 0x00);
617 assert(win.segment(4).data[0] == 0x01);
618 // Footer: zero count, zero tag.
619 assert(win.segment(6).data[0] == 0x00);
620
621 // Round-trip: flatten the multi-burst stream and feed it back through the
622 // generated serial-list deserializer. It must rebuild one window holding
623 // all 7 items with the original tag.
624 std::unique_ptr<SmallListWindow> decoded;
625 SmallListWindow::TypeDeserializer deser(
626 [&](std::unique_ptr<SmallListWindow> &out) {
627 decoded = std::move(out);
628 return true;
629 });
630 std::unique_ptr<esi::SegmentedMessageData> msg =
631 std::make_unique<esi::MessageData>(win.toMessageData());
632 bool pushed = deser.push(msg);
633 assert(pushed);
634 assert(decoded);
635 assert(decoded->tag() == 0xBEEF);
636 auto reassembled = decoded->items_vector();
637 assert(reassembled.size() == elements.size());
638 for (std::size_t j = 0; j < elements.size(); ++j)
639 assert(reassembled[j] == elements[j]);
640}
641
642void testWindowNarrowData() {
643 std::vector<uint8_t> elements{0x35, 0x6A};
644 NarrowDataWindow win(0xBEEF, elements);
645 assert(win.tag() == 0xBEEF);
646 assert(win.items_vector() == elements);
647
648 // The 18-bit header is wider than the 7-bit data, so each data frame has
649 // high padding both within its first byte and in the following bytes.
650 const std::vector<uint8_t> expected{0xBE, 0xFB, 0x02, 0x35, 0, 0,
651 0x6A, 0, 0, 0, 0, 0};
652 auto message = win.toMessageData();
653 assert(message.getSize() == expected.size());
654 for (std::size_t i = 0; i < expected.size(); ++i)
655 assert(message.getData()[i] == expected[i]);
656
657 std::unique_ptr<NarrowDataWindow> decoded;
658 NarrowDataWindow::TypeDeserializer deser(
659 [&](std::unique_ptr<NarrowDataWindow> &out) {
660 decoded = std::move(out);
661 return true;
662 });
663 std::unique_ptr<esi::SegmentedMessageData> msg =
664 std::make_unique<esi::MessageData>(message);
665 assert(deser.push(msg));
666 assert(decoded);
667 assert(decoded->tag() == 0xBEEF);
668 assert(decoded->items_vector() == elements);
669}
670
671// ---------------------------------------------------------------------------
672// View-class fields (esi::MutableBitVector, esi::Int, esi::UInt)
673// ---------------------------------------------------------------------------
674
675// Build a wide MutableBitVector from an arbitrary-length byte buffer
676// (LSB-first wire order). Useful for hand-constructing > 64-bit values in
677// the harness.
678esi::MutableBitVector bvFromBytes(std::initializer_list<uint8_t> bytes,
679 std::size_t width) {
680 std::vector<uint8_t> storage(bytes.begin(), bytes.end());
681 // The MutableBitVector(vector<byte>&&, width) ctor requires the storage
682 // size to cover `width` bits. Pad with zeros if the caller supplied
683 // fewer bytes than that.
684 std::size_t need = (width + 7) / 8;
685 if (storage.size() < need)
686 storage.resize(need, 0);
687 return esi::MutableBitVector(std::move(storage), width);
688}
689
690void testWideUnsigned() {
691 // WideU has two byte-aligned wide-int fields: ui128 at the LSB end
692 // (bits 0..127), ui96 at bits 128..223. Total 224 bits = 28 bytes.
693 WideU s;
694 // Construct a 96-bit value: low half = 0x0123456789ABCDEF,
695 // high half = 0xCAFEBABEu (32 bits worth of upper bytes).
696 auto u96 = bvFromBytes(
697 {
698 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, // low 64 bits
699 0xBE, 0xBA, 0xFE, 0xCA, // bits 64..95
700 },
701 96);
702 auto u128 = bvFromBytes(
703 {
704 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, // low 64 bits
705 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // high 64 bits
706 },
707 128);
708
709 s.u96(u96).u128(u128);
710
711 // Round-trip: read the field back out and compare byte-for-byte.
712 auto got96 = s.u96();
713 assert(got96.width() == 96);
714 for (std::size_t i = 0; i < 96; ++i)
715 assert(got96.getBit(i) == u96.getBit(i));
716 auto got128 = s.u128();
717 assert(got128.width() == 128);
718 for (std::size_t i = 0; i < 128; ++i)
719 assert(got128.getBit(i) == u128.getBit(i));
720
721 // Wire layout: ui128 at byte 0..15 (LSB), then ui96 at byte 16..27.
722 std::array<uint8_t, 28> expected{
723 // u128 first (LSB):
724 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, //
725 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, //
726 // u96 next:
727 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, //
728 0xBE, 0xBA, 0xFE, 0xCA, //
729 };
730 expectBytes(s, expected, "WideU");
731
732 // Constructor takes the same value-class params, in manifest order.
733 WideU cons(u96, u128);
734 assert(wireBytes(cons) == wireBytes(s));
735}
736
737void testWideSigned() {
738 // si128 max = all ones except the sign bit = 0x7FFF...FF.
739 // si128 -1 = all ones.
740 WideS s;
741 auto s96_zero = bvFromBytes({}, 96);
742 auto s128_neg_one = bvFromBytes(
743 {
744 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, //
745 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, //
746 },
747 128);
748 s.s96(s96_zero).s128(s128_neg_one);
749
750 auto got = s.s128();
751 for (std::size_t i = 0; i < 128; ++i)
752 assert(got.getBit(i)); // -1 has every bit set
753 auto got96 = s.s96();
754 for (std::size_t i = 0; i < 96; ++i)
755 assert(!got96.getBit(i));
756
757 // Wire bytes: low 16 bytes all-ones (s128), then 12 zero bytes (s96).
758 std::array<uint8_t, 28> expected{};
759 for (std::size_t i = 0; i < 16; ++i)
760 expected[i] = 0xFF;
761 expectBytes(s, expected, "WideS s128=-1, s96=0");
762}
763
764void testBitsField() {
765 // BitsType > 64 routes through the view-class accessor (non-owning
766 // `esi::BitVector` view); narrower Bits stay on the native int
767 // accessors and are covered by the other tests above.
768 BitsField f;
769 auto wide = bvFromBytes(
770 {
771 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, //
772 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, //
773 },
774 128);
775 f.wide(wide);
776
777 auto got_wide = f.wide();
778 assert(got_wide.width() == 128);
779 for (std::size_t i = 0; i < 128; ++i)
780 assert(got_wide.getBit(i) == wide.getBit(i));
781}
782
783void testWideMisaligned() {
784 // WideMisaligned: tag (ui3) at bits 0..2, payload (ui128) at bits
785 // 3..130 — i.e. a wide value at a non-byte-aligned offset. Hits the
786 // bit-shifted view accessor.
787 WideMisaligned m;
788 auto payload = bvFromBytes(
789 {
790 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, //
791 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, //
792 },
793 128);
794 m.tag(0b101).payload(payload);
795 assert(m.tag() == 0b101);
796
797 auto got = m.payload();
798 assert(got.width() == 128);
799 for (std::size_t i = 0; i < 128; ++i)
800 assert(got.getBit(i) == payload.getBit(i));
801
802 // Re-setting `tag` must not disturb `payload`, and vice versa.
803 m.tag(0b010);
804 auto got2 = m.payload();
805 for (std::size_t i = 0; i < 128; ++i)
806 assert(got2.getBit(i) == payload.getBit(i));
807 assert(m.tag() == 0b010);
808
809 // A narrower input value must be zero-extended; a wider input is
810 // truncated. Verify the former here (the latter is exercised
811 // implicitly because the view-class setter clamps to bit_width).
812 auto narrow = bvFromBytes({0xAA}, 8); // only 8 bits of input
813 m.payload(narrow);
814 auto got3 = m.payload();
815 // Low 8 bits == 0xAA, upper 120 bits == 0.
816 for (std::size_t i = 0; i < 8; ++i)
817 assert(got3.getBit(i) == ((0xAA >> i) & 1));
818 for (std::size_t i = 8; i < 128; ++i)
819 assert(!got3.getBit(i));
820 assert(m.tag() == 0b010); // still preserved
821}
822
823// ---------------------------------------------------------------------------
824// Array of view-class elements: indexed + lazy whole-array accessors
825// ---------------------------------------------------------------------------
826
827// Round-trip a 3-element array of `ui128` through both the per-element
828// indexed accessors (`items(i)` / `items(i, v)`) and the lazy
829// whole-array accessor (`items()` returning a
830// `std::views::iota | std::views::transform` range of per-element
831// views, plus the `std::array<view, N>` whole-array setter and matching
832// ctor). The byte-copy whole-array path used for native-int arrays
833// doesn't apply here because the view's storage layout differs from
834// the wire layout.
835void testArrayOfViewsAligned() {
836 ArrViews a;
837 std::array<esi::MutableBitVector, 3> source;
838 for (std::size_t i = 0; i < 3; ++i) {
839 source[i] = bvFromBytes(
840 {
841 static_cast<uint8_t>(0x10 + i),
842 static_cast<uint8_t>(0x20 + i),
843 static_cast<uint8_t>(0x30 + i),
844 static_cast<uint8_t>(0x40 + i),
845 static_cast<uint8_t>(0x50 + i),
846 static_cast<uint8_t>(0x60 + i),
847 static_cast<uint8_t>(0x70 + i),
848 static_cast<uint8_t>(0x80 + i),
849 static_cast<uint8_t>(0x91 + i),
850 static_cast<uint8_t>(0xA2 + i),
851 static_cast<uint8_t>(0xB3 + i),
852 static_cast<uint8_t>(0xC4 + i),
853 static_cast<uint8_t>(0xD5 + i),
854 static_cast<uint8_t>(0xE6 + i),
855 static_cast<uint8_t>(0xF7 + i),
856 static_cast<uint8_t>(0x08 + i),
857 },
858 128);
859 a.items(i, source[i]);
860 }
861 for (std::size_t i = 0; i < 3; ++i) {
862 auto got = a.items(i);
863 assert(got.width() == 128);
864 for (std::size_t b = 0; b < 128; ++b)
865 assert(got.getBit(b) == source[i].getBit(b));
866 }
867
868 // Independence: writing element 1 must not disturb elements 0 or 2.
869 auto fresh = bvFromBytes({0xAA}, 128);
870 a.items(1, fresh);
871 for (std::size_t b = 0; b < 128; ++b) {
872 assert(a.items(0).getBit(b) == source[0].getBit(b));
873 assert(a.items(2).getBit(b) == source[2].getBit(b));
874 }
875
876 // Lazy whole-array accessor (std::views::iota | std::views::transform):
877 // yields per-element views on demand. Random-access, so size() and
878 // r[i] both work, and we can range-for it.
879 auto range = a.items();
880 assert(std::ranges::size(range) == 3);
881 std::size_t i = 0;
882 for (auto view : range) {
883 assert(view.width() == 128);
884 for (std::size_t b = 0; b < 128; ++b)
885 assert(view.getBit(b) == a.items(i).getBit(b));
886 ++i;
887 }
888 assert(i == 3);
889
890 // Whole-array setter taking `std::array<view, N>` and the matching
891 // ctor: build a fresh struct from the array argument and confirm the
892 // round-trip matches.
893 std::array<esi::UIntView, 3> bulk = {
894 esi::UIntView(source[0]),
895 esi::UIntView(source[1]),
896 esi::UIntView(source[2]),
897 };
898 ArrViews ctor_built(bulk);
899 for (std::size_t k = 0; k < 3; ++k) {
900 auto got = ctor_built.items(k);
901 for (std::size_t b = 0; b < 128; ++b)
902 assert(got.getBit(b) == source[k].getBit(b));
903 }
904 // Whole-array setter on an existing instance.
905 ArrViews bulk_set;
906 bulk_set.items(bulk);
907 for (std::size_t k = 0; k < 3; ++k) {
908 auto got = bulk_set.items(k);
909 for (std::size_t b = 0; b < 128; ++b)
910 assert(got.getBit(b) == source[k].getBit(b));
911 }
912}
913
914void testArrayOfViewsMisaligned() {
915 // `items` is the FIRST manifest field, so on the wire it lands at the
916 // top of the buffer with `tag` (ui3) at bits 0..2 and the 3 ui128
917 // elements at bits 3..130, 131..258, 259..386. Every element is
918 // bit-misaligned -- exercises the runtime per-bit setter loop and the
919 // sub-byte BitVector view constructor.
920 ArrViewsMis m;
921 m.tag(0b101);
922 std::array<esi::MutableBitVector, 3> source;
923 for (std::size_t i = 0; i < 3; ++i) {
924 source[i] = bvFromBytes(
925 {
926 static_cast<uint8_t>(0xC0 + i),
927 static_cast<uint8_t>(0xC1 + i),
928 static_cast<uint8_t>(0xC2 + i),
929 static_cast<uint8_t>(0xC3 + i),
930 static_cast<uint8_t>(0xC4 + i),
931 static_cast<uint8_t>(0xC5 + i),
932 static_cast<uint8_t>(0xC6 + i),
933 static_cast<uint8_t>(0xC7 + i),
934 static_cast<uint8_t>(0xC8 + i),
935 static_cast<uint8_t>(0xC9 + i),
936 static_cast<uint8_t>(0xCA + i),
937 static_cast<uint8_t>(0xCB + i),
938 static_cast<uint8_t>(0xCC + i),
939 static_cast<uint8_t>(0xCD + i),
940 static_cast<uint8_t>(0xCE + i),
941 static_cast<uint8_t>(0xCF + i),
942 },
943 128);
944 m.items(i, source[i]);
945 }
946 assert(m.tag() == 0b101);
947 for (std::size_t i = 0; i < 3; ++i) {
948 auto got = m.items(i);
949 assert(got.width() == 128);
950 for (std::size_t b = 0; b < 128; ++b)
951 assert(got.getBit(b) == source[i].getBit(b));
952 }
953}
954
955} // namespace
956
957int main() {
958#if defined(_MSC_VER) && defined(_DEBUG)
959 _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
960 _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
961 _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
962 _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);
963 _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
964 _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
965 _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
966#endif
967
968 testStandardWidthUnsigned();
969 testStandardWidthSigned();
970 testByteAlignedOddWidthUnsigned();
971 testByteAlignedOddWidthSigned();
972 testSubByteUnsigned();
973 testSubByteSigned();
974 testBoolField();
975 testNestedStruct();
976 testNestedStructMisaligned();
977 testArrayOfIntegers();
978 testSubByteUnsignedArray();
979 testSubByteBoolArray();
980 testSubByteSignedArray();
981 testOddWidthArray();
982 testSubByteStructArray();
983 testNestedIntArray();
984 testNestedStructArray();
985 testUnion();
986 testWindowList();
987 testWindowListMultiBurst();
988 testWindowNarrowData();
989 testWideUnsigned();
990 testWideSigned();
991 testBitsField();
992 testWideMisaligned();
993 testArrayOfViewsAligned();
994 testArrayOfViewsMisaligned();
995 std::printf("OK\n");
996 return 0;
997}
assert(baseType &&"element must be base type")
static InstancePath empty
A mutable bit vector that owns its underlying storage.
Definition Values.h:278
Non-owning view of an unsigned bit vector with toUI64() and implicit conversions to unsigned scalar t...
Definition Values.h:205
int main()