CIRCT 23.0.0git
Loading...
Searching...
No Matches
test_codegen.py
Go to the documentation of this file.
1"""End-to-end tests for `esiaccel.codegen`.
2
3The bulk of the verification is delegated to `codegen_harness.cpp` in this
4directory: the Python side generates a `types.h` from a representative
5manifest, compiles the harness against it, and runs the result. The harness
6exercises every accessor path (Path A / B / C, bool, nested struct, array,
7union, window) and checks both the user-visible round-trip and the
8underlying wire bytes. Reviewers can read the harness directly to see what
9the codegen is contracted to do.
10
11A small number of Python-level tests cover behaviours the harness can't
12exercise — ordering, name collisions, type aliases, and the "skip with a
13comment" path for unsupported / window-containing / fully-collapsed
14structs.
15"""
16
17from __future__ import annotations
18
19import shutil
20import subprocess
21import sys
22import sysconfig
23import tempfile
24from pathlib import Path
25
26import pytest
27
28import esiaccel.types as types
29from esiaccel.codegen import CppTypePlanner, CppTypeEmitter
30
31_HARNESS_DIR = Path(__file__).parent
32
33requires_cmake = pytest.mark.skipif(shutil.which("cmake") is None,
34 reason="cmake not available")
35
36# Small set of pre-built ESI scalar types shared by the manifest builders.
37_uint1 = types.UIntType("ui1", 1)
38_uint2 = types.UIntType("ui2", 2)
39_uint3 = types.UIntType("ui3", 3)
40_uint7 = types.UIntType("ui7", 7)
41_uint8 = types.UIntType("ui8", 8)
42_uint12 = types.UIntType("ui12", 12)
43_uint16 = types.UIntType("ui16", 16)
44_uint24 = types.UIntType("ui24", 24)
45_uint32 = types.UIntType("ui32", 32)
46_uint64 = types.UIntType("ui64", 64)
47_sint5 = types.SIntType("si5", 5)
48_sint7 = types.SIntType("si7", 7)
49_sint8 = types.SIntType("si8", 8)
50_sint16 = types.SIntType("si16", 16)
51_sint24 = types.SIntType("si24", 24)
52_sint32 = types.SIntType("si32", 32)
53_sint64 = types.SIntType("si64", 64)
54
55# Path D: value-class fields backed by `esi::IntView` / `esi::UIntView` /
56# `esi::BitVector` (any width supported; only wider-than-64 cases route
57# through the view classes -- narrower Bits/Int/UInt stay on the native
58# int paths). Tested below with 96- and 128-bit widths.
59_bits128 = types.BitsType("bits128", 128)
60_uint96 = types.UIntType("ui96", 96)
61_uint128 = types.UIntType("ui128", 128)
62_sint96 = types.SIntType("si96", 96)
63_sint128 = types.SIntType("si128", 128)
64
65# ---------------------------------------------------------------------------
66# Harness Manifest Builder
67# ---------------------------------------------------------------------------
68
69
71 """Build the type table the `codegen_harness.cpp` test program references.
72
73 The aliases below give every emitted struct a stable, hand-written C++
74 name (e.g. `StdU`) so the harness can use plain identifiers rather than
75 spelling the auto-generated mangled names.
76 """
77 # Path A: standard widths.
78 std_u_inner = types.StructType(
79 "@StdU::inner",
80 [("u8", _uint8), ("u16", _uint16), ("u32", _uint32), ("u64", _uint64)],
81 )
82 std_u = types.TypeAlias("@StdU", "StdU", std_u_inner)
83
84 std_s_inner = types.StructType(
85 "@StdS::inner",
86 [("s8", _sint8), ("s16", _sint16), ("s32", _sint32), ("s64", _sint64)],
87 )
88 std_s = types.TypeAlias("@StdS", "StdS", std_s_inner)
89
90 # Path B: byte-aligned but non-standard width.
91 odd_u_inner = types.StructType("@OddU::inner", [("u24", _uint24)])
92 odd_u = types.TypeAlias("@OddU", "OddU", odd_u_inner)
93 odd_s_inner = types.StructType("@OddS::inner", [("s24", _sint24)])
94 odd_s = types.TypeAlias("@OddS", "OddS", odd_s_inner)
95
96 # Path C: sub-byte alignment.
97 sub_u_inner = types.StructType("@SubU::inner", [("u3", _uint3),
98 ("u12", _uint12)])
99 sub_u = types.TypeAlias("@SubU", "SubU", sub_u_inner)
100 sub_s_inner = types.StructType("@SubS::inner", [("s5", _sint5),
101 ("s7", _sint7)])
102 sub_s = types.TypeAlias("@SubS", "SubS", sub_s_inner)
103
104 # 1-bit bool field.
105 bool_inner = types.StructType("@BoolField::inner", [("flag", _uint1),
106 ("pad", _uint7)])
107 bool_field = types.TypeAlias("@BoolField", "BoolField", bool_inner)
108
109 # Nested struct field.
110 inner_inner = types.StructType("@Inner::inner", [("x", _uint8),
111 ("y", _uint8)])
112 inner = types.TypeAlias("@Inner", "Inner", inner_inner)
113 outer_inner = types.StructType("@Outer::inner", [("label", _uint8),
114 ("inner", inner)])
115 outer = types.TypeAlias("@Outer", "Outer", outer_inner)
116
117 # Nested struct embedded at a sub-byte bit offset. With the default
118 # `cpp_type.reverse=True`, the LAST manifest field ends up at wire bit
119 # 0, so listing `inner` first and `tag` (ui3) second puts `tag` in
120 # bits 0..2 and the 16-bit inner in bits 3..18 — i.e. the inner
121 # aggregate starts at bit 3, not byte-aligned. Exercises the
122 # `copyBitsIn`/`copyBitsOut` paths.
123 mis_inner_inner = types.StructType("@MisInner::inner", [("x", _uint8),
124 ("y", _uint8)])
125 mis_inner = types.TypeAlias("@MisInner", "MisInner", mis_inner_inner)
126 misaligned_inner = types.StructType("@Misaligned::inner",
127 [("inner", mis_inner), ("tag", _uint3)])
128 misaligned = types.TypeAlias("@Misaligned", "Misaligned", misaligned_inner)
129
130 # Array-of-integers field with the indexed accessor pair.
131 arr4_type = types.ArrayType("!hw.array<4xui8>", _uint8, 4)
132 arr4_inner = types.StructType("@Arr4::inner", [("r", arr4_type)])
133 arr4 = types.TypeAlias("@Arr4", "Arr4", arr4_inner)
134
135 # Arrays whose element storage size differs from the on-wire element
136 # width, so the whole-array accessor must unpack each element from its
137 # own wire bit offset instead of flat-copying. `std::array<uint8_t, 8>`
138 # (ui3), `std::array<bool, 8>` (ui1), `std::array<int8_t, 4>` (si5), and
139 # `std::array<uint32_t, 2>` (ui24) are all wider in memory than the
140 # bit-packed wire layout they represent.
141 u3_arr_inner = types.StructType(
142 "@U3Arr::inner",
143 [("vals", types.ArrayType("!hw.array<8xui3>", _uint3, 8))])
144 u3_arr = types.TypeAlias("@U3Arr", "U3Arr", u3_arr_inner)
145
146 bits1_arr_inner = types.StructType(
147 "@Bits1Arr::inner",
148 [("flags", types.ArrayType("!hw.array<8xui1>", _uint1, 8))])
149 bits1_arr = types.TypeAlias("@Bits1Arr", "Bits1Arr", bits1_arr_inner)
150
151 s5_arr_inner = types.StructType(
152 "@S5Arr::inner",
153 [("vals", types.ArrayType("!hw.array<4xsi5>", _sint5, 4))])
154 s5_arr = types.TypeAlias("@S5Arr", "S5Arr", s5_arr_inner)
155
156 u24_arr_inner = types.StructType(
157 "@U24Arr::inner",
158 [("vals", types.ArrayType("!hw.array<2xui24>", _uint24, 2))])
159 u24_arr = types.TypeAlias("@U24Arr", "U24Arr", u24_arr_inner)
160
161 # Array of sub-byte STRUCT elements. Each `{ui3 hi, ui2 lo}` cell is 5
162 # wire bits, so successive cells pack at a 5-bit stride on the wire but a
163 # padded 1-byte stride in the C++ `std::array<SbCell, 4>`. Exercises the
164 # aggregate arm of the packed-array accessor, which copies each element's
165 # bits into its own `_bytes` buffer.
166 sb_cell_inner = types.StructType("@SbCell::inner", [("hi", _uint3),
167 ("lo", _uint2)])
168 sb_cell = types.TypeAlias("@SbCell", "SbCell", sb_cell_inner)
169 sb_cell_arr_inner = types.StructType(
170 "@SbCellArr::inner",
171 [("cells", types.ArrayType("!hw.array<4xSbCell>", sb_cell, 4))])
172 sb_cell_arr = types.TypeAlias("@SbCellArr", "SbCellArr", sb_cell_arr_inner)
173
174 # Union with one narrow and one wide variant.
175 union_inner = types.UnionType("@UnionTwo::inner", [("small", _uint8),
176 ("big", _uint16)])
177 union_two = types.TypeAlias("@UnionTwo", "UnionTwo", union_inner)
178
179 # Window helper with one static `tag` header field and a list of ui32.
180 list_id = "!esi.list<ui32>"
181 list_type = types.ListType(list_id, _uint32)
182 win_arg_inner = types.StructType(
183 "@ListWindow::arg",
184 [("tag", _uint16), ("items", list_type)],
185 )
186 win_header_inner = types.StructType(
187 "@ListWindow::header",
188 [("tag", _uint16), ("items_count", _uint16)],
189 )
190 win_data_inner = types.StructType(
191 "@ListWindow::data",
192 [("items", types.ArrayType("!hw.array<1xui32>", _uint32, 1))],
193 )
194 win_lowered = types.UnionType(
195 "@ListWindow::lowered",
196 [("header", win_header_inner), ("data", win_data_inner)],
197 )
198 window_id = ('!esi.window<"ListWindow", @ListWindow::arg, '
199 '[<"header", [<"tag">, <"items" countWidth 16>]>, '
200 '<"data", [<"items", 1>]>]>')
201 list_window_inner = types.WindowType(
202 window_id,
203 "ListWindow",
204 win_arg_inner,
205 win_lowered,
206 [
207 types.WindowType.Frame(
208 "header",
209 [
210 types.WindowType.Field("tag", 0, 0),
211 types.WindowType.Field("items", 0, 16),
212 ],
213 ),
214 types.WindowType.Frame(
215 "data",
216 [types.WindowType.Field("items", 1, 0)],
217 ),
218 ],
219 )
220 # Hand-name the window so the harness can spell `ListWindow` directly.
221 list_window = types.TypeAlias("@ListWindow", "ListWindow", list_window_inner)
222
223 # Path D: value-class fields backed by `esi::MutableBitVector` /
224 # `esi::Int` / `esi::UInt` from `esi/Values.h`. Covers BitsType at
225 # both narrow and wide widths, plus signed/unsigned integers above
226 # the 64-bit native ceiling. Layout fields cover byte-aligned and
227 # bit-misaligned wide-int offsets so both branches of `copyBitsIn`
228 # / `copyBitsOut` get exercised.
229 wide_u_inner = types.StructType(
230 "@WideU::inner",
231 [("u96", _uint96), ("u128", _uint128)],
232 )
233 wide_u = types.TypeAlias("@WideU", "WideU", wide_u_inner)
234 wide_s_inner = types.StructType(
235 "@WideS::inner",
236 [("s96", _sint96), ("s128", _sint128)],
237 )
238 wide_s = types.TypeAlias("@WideS", "WideS", wide_s_inner)
239 # Bits-typed fields > 64 bits route through the value-class path
240 # (`esi::BitVector` view); narrower Bits stay on the native int paths
241 # and don't need a dedicated harness here.
242 bits_inner = types.StructType(
243 "@BitsField::inner",
244 [("wide", _bits128)],
245 )
246 bits_field = types.TypeAlias("@BitsField", "BitsField", bits_inner)
247 # Place the wide UInt field *before* a 3-bit tag in the manifest.
248 # Field order is reversed on the wire (`cpp_type.reverse=True`), so the
249 # LAST manifest field lands at wire bit 0. Listing `payload` first and
250 # `tag` last puts `tag` at bits 0..2 (byte-aligned) and the 128-bit
251 # `payload` at bits 3..130 — exercising the
252 # `copyBitsIn<bit_offset, 128>` / `copyBitsOut` arm of Path D rather
253 # than only the byte-aligned case.
254 wide_mis_inner = types.StructType(
255 "@WideMisaligned::inner",
256 [("payload", _uint128), ("tag", _uint3)],
257 )
258 wide_mis = types.TypeAlias("@WideMisaligned", "WideMisaligned",
259 wide_mis_inner)
260
261 # Array of view-class elements. The planner used to skip parent types
262 # that reached this construct; now the emitter handles them by emitting
263 # per-element indexed accessors that build fresh views into `_bytes`.
264 # Use ui128 (a view-class type) at multiple element widths to exercise
265 # both byte-aligned and bit-misaligned per-element offsets.
266 arr3_u128_type = types.ArrayType("!hw.array<3xui128>", _uint128, 3)
267 arr_views_inner = types.StructType(
268 "@ArrViews::inner",
269 [("items", arr3_u128_type)],
270 )
271 arr_views = types.TypeAlias("@ArrViews", "ArrViews", arr_views_inner)
272 # Same array placed after a 3-bit tag so the array starts at bit 3
273 # and successive elements land at non-byte-aligned per-element
274 # offsets (3, 131, 259, ...).
275 arr_views_mis_inner = types.StructType(
276 "@ArrViewsMis::inner",
277 [("items", arr3_u128_type), ("tag", _uint3)],
278 )
279 arr_views_mis = types.TypeAlias("@ArrViewsMis", "ArrViewsMis",
280 arr_views_mis_inner)
281
282 return [
283 std_u, std_s, odd_u, odd_s, sub_u, sub_s, bool_field, outer, misaligned,
284 arr4, u3_arr, bits1_arr, s5_arr, u24_arr, sb_cell, sb_cell_arr, union_two,
285 list_window, wide_u, wide_s, bits_field, wide_mis, arr_views,
286 arr_views_mis
287 ]
288
289
290# ---------------------------------------------------------------------------
291# Harness build + run
292# ---------------------------------------------------------------------------
293
294
295@requires_cmake
297 """Compile `codegen_harness.cpp` against a freshly-generated `types.h`
298 and run it. The harness asserts every wire-format and accessor invariant
299 end-to-end; this Python test just drives the cmake build and reports
300 failures.
301 """
302 from esiaccel.utils import get_dll_dir
303 esi_dll_path = get_dll_dir()
304 if sys.platform == "win32":
305 runtime_lib = esi_dll_path / "ESICppRuntime.lib"
306 runtime_dll = esi_dll_path / "ESICppRuntime.dll"
307 else:
308 runtime_lib = esi_dll_path / "libESICppRuntime.so"
309 runtime_dll = None
310
311 # Generate the header into `<generated>/codegen_harness/types.h` so the
312 # harness's `#include "codegen_harness/types.h"` resolves under
313 # `<generated>`, which we feed to CMake as the include root.
314 generated_dir = tmp_path / "generated"
315 (generated_dir / "codegen_harness").mkdir(parents=True)
317 emitter = CppTypeEmitter(planner)
318 emitter.write_header(generated_dir / "codegen_harness", "esi_system")
319
320 build_dir = tmp_path / "build"
321 configure_cmd = [
322 "cmake",
323 "-S",
324 str(_HARNESS_DIR),
325 "-B",
326 str(build_dir),
327 "-DCMAKE_BUILD_TYPE=Release",
328 f"-DCODEGEN_HARNESS_GENERATED_DIR={generated_dir}",
329 f"-DESI_RUNTIME_LIB={runtime_lib}",
330 ]
331 if sys.platform == "win32":
332 configure_cmd.append(f"-DESI_RUNTIME_DLL={runtime_dll}")
333 configure_proc = subprocess.run(configure_cmd, capture_output=True, text=True)
334 if configure_proc.returncode != 0:
335 pytest.fail(
336 "cmake configure failed for the codegen harness "
337 f"(rc={configure_proc.returncode}):\n"
338 f"--- cmd ---\n{' '.join(configure_cmd)}\n"
339 f"--- stdout ---\n{configure_proc.stdout}\n"
340 f"--- stderr ---\n{configure_proc.stderr}",
341 pytrace=False,
342 )
343
344 build_cmd = [
345 "cmake", "--build",
346 str(build_dir), "--target", "codegen_harness", "--config", "Release"
347 ]
348 build_proc = subprocess.run(build_cmd, capture_output=True, text=True)
349 if build_proc.returncode != 0:
350 # Print the generated header alongside the compile failure so a
351 # reviewer can diff the codegen output against what the harness
352 # expects to see.
353 pytest.fail(
354 "codegen_harness.cpp failed to compile against the generated "
355 f"types.h (rc={build_proc.returncode}):\n"
356 f"--- cmd ---\n{' '.join(build_cmd)}\n"
357 f"--- stdout ---\n{build_proc.stdout}\n"
358 f"--- stderr ---\n{build_proc.stderr}\n"
359 "--- generated types.h ---\n" +
360 (generated_dir / "codegen_harness" / "types.h").read_text(),
361 pytrace=False,
362 )
363
364 # CMake places the executable under the build directory; on multi-config
365 # generators (e.g. Visual Studio) the output lives under
366 # `build_dir/<Config>/`. Walk the build dir to find whatever was built.
367 binary_name = "codegen_harness" + sysconfig.get_config_var("EXE")
368 candidates = list(build_dir.rglob(binary_name))
369 if not candidates:
370 pytest.fail(
371 f"codegen_harness binary not found under {build_dir}; "
372 "the cmake build reported success but produced no executable.",
373 pytrace=False,
374 )
375 binary = candidates[0]
376
377 run_proc = subprocess.run([str(binary)], capture_output=True, text=True)
378 if run_proc.returncode != 0 or run_proc.stdout.strip() != "OK":
379 pytest.fail(
380 f"codegen_harness reported failure (rc={run_proc.returncode}):\n"
381 f"--- stdout ---\n{run_proc.stdout}\n"
382 f"--- stderr ---\n{run_proc.stderr}",
383 pytrace=False,
384 )
385
386
387# ---------------------------------------------------------------------------
388# Lightweight Python-level checks for behaviours the harness can't drive.
389# ---------------------------------------------------------------------------
390
391
392def _emit(type_table, system_name: str = "test_ns") -> str:
393 """Run the planner + emitter against `type_table` and return `types.h`."""
394 planner = CppTypePlanner(type_table)
395 emitter = CppTypeEmitter(planner)
396 with tempfile.TemporaryDirectory() as tmpdir:
397 emitter.write_header(Path(tmpdir), system_name)
398 return (Path(tmpdir) / "types.h").read_text()
399
400
402 """A struct whose payload type has no bounded width (e.g. `!esi.any`)
403 cannot be expressed in the raw-bytes layout; the codegen drops the
404 whole struct and leaves an `Unsupported type` comment behind so callers
405 see why the symbol they expected is missing."""
406 any_t = types.AnyType("!esi.any")
407 s = types.StructType("@any_struct", [("tag", _uint8), ("data", any_t)])
408
409 hdr = _emit([s])
410 assert "// Unsupported type '<@any_struct>'" in hdr
_build_harness_manifest()
str _emit(type_table, str system_name="test_ns")
test_codegen_round_trip(tmp_path)
test_unbounded_field_skips_struct_with_comment()