CIRCT 23.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 // Narrow variant at MSB end: small = high byte of big (little-endian
502 // layout means high byte is at byte_offset = union_bytes - 1 = 1).
503 assert(u.small() == 0xCA);
504 expectBytes(u, std::array<uint8_t, 2>{0xFE, 0xCA}, "Union big");
505
506 u.small(0xA5);
507 assert(u.small() == 0xA5);
508 // Writing the narrow variant overwrites only its byte slot at the
509 // MSB end; the LSB byte retains its previous content.
510 expectBytes(u, std::array<uint8_t, 2>{0xFE, 0xA5}, "Union small");
511}
512
513// ---------------------------------------------------------------------------
514// Window helpers (header / data frame round-trip)
515// ---------------------------------------------------------------------------
516
517void testWindowList() {
518 std::vector<uint32_t> elements = {0xAAAA0001u, 0xBBBB0002u, 0xCCCC0003u};
519 ListWindow win(0xDEAD, elements);
520
521 // Header-field accessor reads the static `tag`. Count comes from the
522 // `<list>_count()` accessor and matches the number of data frames.
523 assert(win.tag() == 0xDEAD);
524 assert(win.items_count() == elements.size());
525
526 // Vector helper materialises the data into a flat std::vector.
527 auto got = win.items_vector();
528 assert(got.size() == elements.size());
529 for (std::size_t i = 0; i < elements.size(); ++i)
530 assert(got[i] == elements[i]);
531
532 // Range accessor produces the same values lazily.
533 std::size_t i = 0;
534 for (uint32_t v : win.items())
535 assert(v == elements[i++]);
536
537 // The window splits into three wire segments: header, data, footer.
538 assert(win.numSegments() == 3);
539 auto header_seg = win.segment(0);
540 auto data_seg = win.segment(1);
541 auto footer_seg = win.segment(2);
542 assert(data_seg.size == elements.size() * sizeof(uint32_t));
543 // Content is MSB-aligned within the 32-bit frame: `tag` (ui16) occupies
544 // the top 16 bits (bytes 2..3) and the count (ui16) the low 16 bits
545 // (bytes 0..1). Here the content fills the frame exactly, so header bytes =
546 // [count_lo, count_hi, tag_lo, tag_hi].
547 assert(header_seg.size == 4);
548 assert(header_seg.data[0] == (static_cast<uint8_t>(elements.size()) & 0xFF));
549 assert(header_seg.data[1] == 0); // count high byte (size < 256)
550 assert(header_seg.data[2] == 0xAD);
551 assert(header_seg.data[3] == 0xDE);
552 // Footer is a zero-count header with the same layout.
553 assert(footer_seg.size == 4);
554 assert(footer_seg.data[0] == 0);
555 assert(footer_seg.data[1] == 0);
556}
557
558// ---------------------------------------------------------------------------
559// ---------------------------------------------------------------------------
560// Multi-burst chunking: a window whose count field is too narrow to encode
561// the whole list in one burst must split the list across several
562// header/data bursts (terminated by a zero-count footer) and the read-side
563// deserializer must reassemble it back into the original list.
564// ---------------------------------------------------------------------------
565
566void testWindowListMultiBurst() {
567 // SmallListWindow has a 2-bit count field, so at most 3 items fit in one
568 // burst. A 7-item list therefore splits into 3 bursts of 3 + 3 + 1 items.
569 std::vector<uint32_t> elements = {0x11111111u, 0x22222222u, 0x33333333u,
570 0x44444444u, 0x55555555u, 0x66666666u,
571 0x77777777u};
572 SmallListWindow win(0xBEEF, elements);
573
574 assert(win.tag() == 0xBEEF);
575 assert(win.items_count() == elements.size());
576
577 // ceil(7 / 3) = 3 bursts -> 3 headers + 3 data + 1 footer = 7 segments.
578 assert(win.numSegments() == 2 * 3 + 1);
579
580 // Each header / footer frame is 4 bytes. Data segments carry 3, 3, then 1
581 // item(s), one ui32 (4 bytes) per item.
582 assert(win.segment(0).size == 4); // burst 0 header
583 assert(win.segment(1).size == 3 * sizeof(uint32_t)); // burst 0 data
584 assert(win.segment(2).size == 4); // burst 1 header
585 assert(win.segment(3).size == 3 * sizeof(uint32_t)); // burst 1 data
586 assert(win.segment(4).size == 4); // burst 2 header
587 assert(win.segment(5).size == 1 * sizeof(uint32_t)); // burst 2 data
588 assert(win.segment(6).size == 4); // footer
589
590 // The header content is MSB-aligned within the 32-bit frame, exactly as
591 // CIRCT lowers the frame union: `tag` (ui16) sits at bits [31:16]
592 // (bytes 2..3) and the 2-bit count immediately below it at bits [15:14] --
593 // the top two bits of byte 1 -- with the remaining low bits zero. Only the
594 // first burst's header carries the static tag; later bursts and the footer
595 // leave it zero. (The old byte-granular codegen put the count in the low
596 // bits of byte 1, which the hardware would have read as zero.)
597 auto headerCount = [](const auto &s) -> unsigned {
598 return (s.data[1] >> 6) & 0x3;
599 };
600 assert(headerCount(win.segment(0)) == 3);
601 assert(headerCount(win.segment(2)) == 3);
602 assert(headerCount(win.segment(4)) == 1);
603 assert(headerCount(win.segment(6)) == 0);
604 // Exact header-frame bytes. Burst 0 (count 3, tag 0xBEEF):
605 // byte0 = 0x00 (pad), byte1 = 0xC0 (count 3 at bits [15:14]),
606 // byte2 = 0xEF, byte3 = 0xBE (tag, little-endian, at bits [31:16]).
607 assert(win.segment(0).data[0] == 0x00);
608 assert(win.segment(0).data[1] == 0xC0);
609 assert(win.segment(0).data[2] == 0xEF);
610 assert(win.segment(0).data[3] == 0xBE);
611 // Later bursts repeat the count but not the tag (bytes 2..3 stay zero).
612 assert(win.segment(2).data[1] == 0xC0); // count 3
613 assert(win.segment(2).data[2] == 0x00);
614 assert(win.segment(2).data[3] == 0x00);
615 assert(win.segment(4).data[1] == 0x40); // count 1
616 // Footer: zero count, zero tag.
617 assert(win.segment(6).data[1] == 0x00);
618
619 // Round-trip: flatten the multi-burst stream and feed it back through the
620 // generated serial-list deserializer. It must rebuild one window holding
621 // all 7 items with the original tag.
622 std::unique_ptr<SmallListWindow> decoded;
623 SmallListWindow::TypeDeserializer deser(
624 [&](std::unique_ptr<SmallListWindow> &out) {
625 decoded = std::move(out);
626 return true;
627 });
628 std::unique_ptr<esi::SegmentedMessageData> msg =
629 std::make_unique<esi::MessageData>(win.toMessageData());
630 bool pushed = deser.push(msg);
631 assert(pushed);
632 assert(decoded);
633 assert(decoded->tag() == 0xBEEF);
634 auto reassembled = decoded->items_vector();
635 assert(reassembled.size() == elements.size());
636 for (std::size_t j = 0; j < elements.size(); ++j)
637 assert(reassembled[j] == elements[j]);
638}
639
640// ---------------------------------------------------------------------------
641// View-class fields (esi::MutableBitVector, esi::Int, esi::UInt)
642// ---------------------------------------------------------------------------
643
644// Build a wide MutableBitVector from an arbitrary-length byte buffer
645// (LSB-first wire order). Useful for hand-constructing > 64-bit values in
646// the harness.
647esi::MutableBitVector bvFromBytes(std::initializer_list<uint8_t> bytes,
648 std::size_t width) {
649 std::vector<uint8_t> storage(bytes.begin(), bytes.end());
650 // The MutableBitVector(vector<byte>&&, width) ctor requires the storage
651 // size to cover `width` bits. Pad with zeros if the caller supplied
652 // fewer bytes than that.
653 std::size_t need = (width + 7) / 8;
654 if (storage.size() < need)
655 storage.resize(need, 0);
656 return esi::MutableBitVector(std::move(storage), width);
657}
658
659void testWideUnsigned() {
660 // WideU has two byte-aligned wide-int fields: ui128 at the LSB end
661 // (bits 0..127), ui96 at bits 128..223. Total 224 bits = 28 bytes.
662 WideU s;
663 // Construct a 96-bit value: low half = 0x0123456789ABCDEF,
664 // high half = 0xCAFEBABEu (32 bits worth of upper bytes).
665 auto u96 = bvFromBytes(
666 {
667 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, // low 64 bits
668 0xBE, 0xBA, 0xFE, 0xCA, // bits 64..95
669 },
670 96);
671 auto u128 = bvFromBytes(
672 {
673 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, // low 64 bits
674 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // high 64 bits
675 },
676 128);
677
678 s.u96(u96).u128(u128);
679
680 // Round-trip: read the field back out and compare byte-for-byte.
681 auto got96 = s.u96();
682 assert(got96.width() == 96);
683 for (std::size_t i = 0; i < 96; ++i)
684 assert(got96.getBit(i) == u96.getBit(i));
685 auto got128 = s.u128();
686 assert(got128.width() == 128);
687 for (std::size_t i = 0; i < 128; ++i)
688 assert(got128.getBit(i) == u128.getBit(i));
689
690 // Wire layout: ui128 at byte 0..15 (LSB), then ui96 at byte 16..27.
691 std::array<uint8_t, 28> expected{
692 // u128 first (LSB):
693 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, //
694 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, //
695 // u96 next:
696 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, //
697 0xBE, 0xBA, 0xFE, 0xCA, //
698 };
699 expectBytes(s, expected, "WideU");
700
701 // Constructor takes the same value-class params, in manifest order.
702 WideU cons(u96, u128);
703 assert(wireBytes(cons) == wireBytes(s));
704}
705
706void testWideSigned() {
707 // si128 max = all ones except the sign bit = 0x7FFF...FF.
708 // si128 -1 = all ones.
709 WideS s;
710 auto s96_zero = bvFromBytes({}, 96);
711 auto s128_neg_one = bvFromBytes(
712 {
713 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, //
714 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, //
715 },
716 128);
717 s.s96(s96_zero).s128(s128_neg_one);
718
719 auto got = s.s128();
720 for (std::size_t i = 0; i < 128; ++i)
721 assert(got.getBit(i)); // -1 has every bit set
722 auto got96 = s.s96();
723 for (std::size_t i = 0; i < 96; ++i)
724 assert(!got96.getBit(i));
725
726 // Wire bytes: low 16 bytes all-ones (s128), then 12 zero bytes (s96).
727 std::array<uint8_t, 28> expected{};
728 for (std::size_t i = 0; i < 16; ++i)
729 expected[i] = 0xFF;
730 expectBytes(s, expected, "WideS s128=-1, s96=0");
731}
732
733void testBitsField() {
734 // BitsType > 64 routes through the view-class accessor (non-owning
735 // `esi::BitVector` view); narrower Bits stay on the native int
736 // accessors and are covered by the other tests above.
737 BitsField f;
738 auto wide = bvFromBytes(
739 {
740 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, //
741 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, //
742 },
743 128);
744 f.wide(wide);
745
746 auto got_wide = f.wide();
747 assert(got_wide.width() == 128);
748 for (std::size_t i = 0; i < 128; ++i)
749 assert(got_wide.getBit(i) == wide.getBit(i));
750}
751
752void testWideMisaligned() {
753 // WideMisaligned: tag (ui3) at bits 0..2, payload (ui128) at bits
754 // 3..130 — i.e. a wide value at a non-byte-aligned offset. Hits the
755 // bit-shifted view accessor.
756 WideMisaligned m;
757 auto payload = bvFromBytes(
758 {
759 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, //
760 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, //
761 },
762 128);
763 m.tag(0b101).payload(payload);
764 assert(m.tag() == 0b101);
765
766 auto got = m.payload();
767 assert(got.width() == 128);
768 for (std::size_t i = 0; i < 128; ++i)
769 assert(got.getBit(i) == payload.getBit(i));
770
771 // Re-setting `tag` must not disturb `payload`, and vice versa.
772 m.tag(0b010);
773 auto got2 = m.payload();
774 for (std::size_t i = 0; i < 128; ++i)
775 assert(got2.getBit(i) == payload.getBit(i));
776 assert(m.tag() == 0b010);
777
778 // A narrower input value must be zero-extended; a wider input is
779 // truncated. Verify the former here (the latter is exercised
780 // implicitly because the view-class setter clamps to bit_width).
781 auto narrow = bvFromBytes({0xAA}, 8); // only 8 bits of input
782 m.payload(narrow);
783 auto got3 = m.payload();
784 // Low 8 bits == 0xAA, upper 120 bits == 0.
785 for (std::size_t i = 0; i < 8; ++i)
786 assert(got3.getBit(i) == ((0xAA >> i) & 1));
787 for (std::size_t i = 8; i < 128; ++i)
788 assert(!got3.getBit(i));
789 assert(m.tag() == 0b010); // still preserved
790}
791
792// ---------------------------------------------------------------------------
793// Array of view-class elements: indexed + lazy whole-array accessors
794// ---------------------------------------------------------------------------
795
796// Round-trip a 3-element array of `ui128` through both the per-element
797// indexed accessors (`items(i)` / `items(i, v)`) and the lazy
798// whole-array accessor (`items()` returning a
799// `std::views::iota | std::views::transform` range of per-element
800// views, plus the `std::array<view, N>` whole-array setter and matching
801// ctor). The byte-copy whole-array path used for native-int arrays
802// doesn't apply here because the view's storage layout differs from
803// the wire layout.
804void testArrayOfViewsAligned() {
805 ArrViews a;
806 std::array<esi::MutableBitVector, 3> source;
807 for (std::size_t i = 0; i < 3; ++i) {
808 source[i] = bvFromBytes(
809 {
810 static_cast<uint8_t>(0x10 + i),
811 static_cast<uint8_t>(0x20 + i),
812 static_cast<uint8_t>(0x30 + i),
813 static_cast<uint8_t>(0x40 + i),
814 static_cast<uint8_t>(0x50 + i),
815 static_cast<uint8_t>(0x60 + i),
816 static_cast<uint8_t>(0x70 + i),
817 static_cast<uint8_t>(0x80 + i),
818 static_cast<uint8_t>(0x91 + i),
819 static_cast<uint8_t>(0xA2 + i),
820 static_cast<uint8_t>(0xB3 + i),
821 static_cast<uint8_t>(0xC4 + i),
822 static_cast<uint8_t>(0xD5 + i),
823 static_cast<uint8_t>(0xE6 + i),
824 static_cast<uint8_t>(0xF7 + i),
825 static_cast<uint8_t>(0x08 + i),
826 },
827 128);
828 a.items(i, source[i]);
829 }
830 for (std::size_t i = 0; i < 3; ++i) {
831 auto got = a.items(i);
832 assert(got.width() == 128);
833 for (std::size_t b = 0; b < 128; ++b)
834 assert(got.getBit(b) == source[i].getBit(b));
835 }
836
837 // Independence: writing element 1 must not disturb elements 0 or 2.
838 auto fresh = bvFromBytes({0xAA}, 128);
839 a.items(1, fresh);
840 for (std::size_t b = 0; b < 128; ++b) {
841 assert(a.items(0).getBit(b) == source[0].getBit(b));
842 assert(a.items(2).getBit(b) == source[2].getBit(b));
843 }
844
845 // Lazy whole-array accessor (std::views::iota | std::views::transform):
846 // yields per-element views on demand. Random-access, so size() and
847 // r[i] both work, and we can range-for it.
848 auto range = a.items();
849 assert(std::ranges::size(range) == 3);
850 std::size_t i = 0;
851 for (auto view : range) {
852 assert(view.width() == 128);
853 for (std::size_t b = 0; b < 128; ++b)
854 assert(view.getBit(b) == a.items(i).getBit(b));
855 ++i;
856 }
857 assert(i == 3);
858
859 // Whole-array setter taking `std::array<view, N>` and the matching
860 // ctor: build a fresh struct from the array argument and confirm the
861 // round-trip matches.
862 std::array<esi::UIntView, 3> bulk = {
863 esi::UIntView(source[0]),
864 esi::UIntView(source[1]),
865 esi::UIntView(source[2]),
866 };
867 ArrViews ctor_built(bulk);
868 for (std::size_t k = 0; k < 3; ++k) {
869 auto got = ctor_built.items(k);
870 for (std::size_t b = 0; b < 128; ++b)
871 assert(got.getBit(b) == source[k].getBit(b));
872 }
873 // Whole-array setter on an existing instance.
874 ArrViews bulk_set;
875 bulk_set.items(bulk);
876 for (std::size_t k = 0; k < 3; ++k) {
877 auto got = bulk_set.items(k);
878 for (std::size_t b = 0; b < 128; ++b)
879 assert(got.getBit(b) == source[k].getBit(b));
880 }
881}
882
883void testArrayOfViewsMisaligned() {
884 // `items` is the FIRST manifest field, so on the wire it lands at the
885 // top of the buffer with `tag` (ui3) at bits 0..2 and the 3 ui128
886 // elements at bits 3..130, 131..258, 259..386. Every element is
887 // bit-misaligned -- exercises the runtime per-bit setter loop and the
888 // sub-byte BitVector view constructor.
889 ArrViewsMis m;
890 m.tag(0b101);
891 std::array<esi::MutableBitVector, 3> source;
892 for (std::size_t i = 0; i < 3; ++i) {
893 source[i] = bvFromBytes(
894 {
895 static_cast<uint8_t>(0xC0 + i),
896 static_cast<uint8_t>(0xC1 + i),
897 static_cast<uint8_t>(0xC2 + i),
898 static_cast<uint8_t>(0xC3 + i),
899 static_cast<uint8_t>(0xC4 + i),
900 static_cast<uint8_t>(0xC5 + i),
901 static_cast<uint8_t>(0xC6 + i),
902 static_cast<uint8_t>(0xC7 + i),
903 static_cast<uint8_t>(0xC8 + i),
904 static_cast<uint8_t>(0xC9 + i),
905 static_cast<uint8_t>(0xCA + i),
906 static_cast<uint8_t>(0xCB + i),
907 static_cast<uint8_t>(0xCC + i),
908 static_cast<uint8_t>(0xCD + i),
909 static_cast<uint8_t>(0xCE + i),
910 static_cast<uint8_t>(0xCF + i),
911 },
912 128);
913 m.items(i, source[i]);
914 }
915 assert(m.tag() == 0b101);
916 for (std::size_t i = 0; i < 3; ++i) {
917 auto got = m.items(i);
918 assert(got.width() == 128);
919 for (std::size_t b = 0; b < 128; ++b)
920 assert(got.getBit(b) == source[i].getBit(b));
921 }
922}
923
924} // namespace
925
926int main() {
927#if defined(_MSC_VER) && defined(_DEBUG)
928 _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
929 _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
930 _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
931 _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);
932 _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
933 _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
934 _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
935#endif
936
937 testStandardWidthUnsigned();
938 testStandardWidthSigned();
939 testByteAlignedOddWidthUnsigned();
940 testByteAlignedOddWidthSigned();
941 testSubByteUnsigned();
942 testSubByteSigned();
943 testBoolField();
944 testNestedStruct();
945 testNestedStructMisaligned();
946 testArrayOfIntegers();
947 testSubByteUnsignedArray();
948 testSubByteBoolArray();
949 testSubByteSignedArray();
950 testOddWidthArray();
951 testSubByteStructArray();
952 testNestedIntArray();
953 testNestedStructArray();
954 testUnion();
955 testWindowList();
956 testWindowListMultiBurst();
957 testWideUnsigned();
958 testWideSigned();
959 testBitsField();
960 testWideMisaligned();
961 testArrayOfViewsAligned();
962 testArrayOfViewsMisaligned();
963 std::printf("OK\n");
964 return 0;
965}
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()