CIRCT 24.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 category (standard / odd / sub-byte integer widths,
7bool, view-class, nested struct, array, union, window) and checks both the
8user-visible round-trip and the underlying wire bytes. Reviewers can read the
9harness directly to see what the 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# View-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 accessors). 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 # Standard 8/16/32/64-bit 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 # Byte-aligned but non-standard width (e.g. ui24).
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 # 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 # Nested array of sub-byte ints: `2 x 4 x ui3`. The element type is itself
175 # the non-byte-packable array `4 x ui3` (12 wire bits but 4 bytes in the
176 # C++ `std::array<uint8_t, 4>`). The old flat-copy path handled this case
177 # incorrectly -- it emitted a corrupt whole-array copy and no indexed
178 # accessor at all -- because `is_packed_array` only matched scalar/struct
179 # elements, not array elements. The recursive (un)packer places each `ui3`
180 # leaf at its true wire offset `i * 12 + j * 3`.
181 nested3_inner = types.StructType(
182 "@Nested3::inner",
183 [("rows",
184 types.ArrayType("!hw.array<2x!hw.array<4xui3>>",
185 types.ArrayType("!hw.array<4xui3>", _uint3, 4), 2))])
186 nested3 = types.TypeAlias("@Nested3", "Nested3", nested3_inner)
187
188 # Nested array of sub-byte STRUCT elements: `2 x 3 x SbCell` (each cell 5
189 # wire bits). Array-of-array-of-aggregate: the recursive packer copies each
190 # cell's bits at its true wire offset `i * 15 + j * 5`.
191 nested_cell_inner = types.StructType("@NestedCell::inner", [
192 ("grid",
193 types.ArrayType("!hw.array<2x!hw.array<3xSbCell>>",
194 types.ArrayType("!hw.array<3xSbCell>", sb_cell, 3), 2))
195 ])
196 nested_cell = types.TypeAlias("@NestedCell", "NestedCell", nested_cell_inner)
197
198 # Union with one narrow and one wide variant.
199 union_inner = types.UnionType("@UnionTwo::inner", [("small", _uint8),
200 ("big", _uint16)])
201 union_two = types.TypeAlias("@UnionTwo", "UnionTwo", union_inner)
202
203 union_subbyte_inner = types.UnionType("@UnionSubByte::inner",
204 [("tiny", _uint3),
205 ("signed_small", _sint5),
206 ("cell", sb_cell), ("big", _uint12)])
207 union_subbyte = types.TypeAlias("@UnionSubByte", "UnionSubByte",
208 union_subbyte_inner)
209
210 # Window helper with one static `tag` header field and a list of ui32.
211 list_id = "!esi.list<ui32>"
212 list_type = types.ListType(list_id, _uint32)
213 win_arg_inner = types.StructType(
214 "@ListWindow::arg",
215 [("tag", _uint16), ("items", list_type)],
216 )
217 win_header_inner = types.StructType(
218 "@ListWindow::header",
219 [("tag", _uint16), ("items_count", _uint16)],
220 )
221 win_data_inner = types.StructType(
222 "@ListWindow::data",
223 [("items", types.ArrayType("!hw.array<1xui32>", _uint32, 1))],
224 )
225 win_lowered = types.UnionType(
226 "@ListWindow::lowered",
227 [("header", win_header_inner), ("data", win_data_inner)],
228 )
229 window_id = ('!esi.window<"ListWindow", @ListWindow::arg, '
230 '[<"header", [<"tag">, <"items" countWidth 16>]>, '
231 '<"data", [<"items", 1>]>]>')
232 list_window_inner = types.WindowType(
233 window_id,
234 "ListWindow",
235 win_arg_inner,
236 win_lowered,
237 [
238 types.WindowType.Frame(
239 "header",
240 [
241 types.WindowType.Field("tag", 0, 0),
242 types.WindowType.Field("items", 0, 16),
243 ],
244 ),
245 types.WindowType.Frame(
246 "data",
247 [types.WindowType.Field("items", 1, 0)],
248 ),
249 ],
250 )
251 # Hand-name the window so the harness can spell `ListWindow` directly.
252 list_window = types.TypeAlias("@ListWindow", "ListWindow", list_window_inner)
253
254 # Window helper with a *narrow* (2-bit) count field. `maxBatch = 3`, so any
255 # list longer than three items must be chunked into multiple header/data
256 # bursts on the write side (and reassembled by the read-side
257 # `SerialListTypeDeserializer`). Mirrors `ListWindow` otherwise.
258 sw_arg_inner = types.StructType(
259 "@SmallListWindow::arg",
260 [("tag", _uint16), ("items", list_type)],
261 )
262 sw_header_inner = types.StructType(
263 "@SmallListWindow::header",
264 [("tag", _uint16), ("items_count", _uint2)],
265 )
266 sw_data_inner = types.StructType(
267 "@SmallListWindow::data",
268 [("items", types.ArrayType("!hw.array<1xui32>", _uint32, 1))],
269 )
270 sw_lowered = types.UnionType(
271 "@SmallListWindow::lowered",
272 [("header", sw_header_inner), ("data", sw_data_inner)],
273 )
274 sw_window_id = ('!esi.window<"SmallListWindow", @SmallListWindow::arg, '
275 '[<"header", [<"tag">, <"items" countWidth 2>]>, '
276 '<"data", [<"items", 1>]>]>')
277 small_list_window_inner = types.WindowType(
278 sw_window_id,
279 "SmallListWindow",
280 sw_arg_inner,
281 sw_lowered,
282 [
283 types.WindowType.Frame(
284 "header",
285 [
286 types.WindowType.Field("tag", 0, 0),
287 types.WindowType.Field("items", 0, 2),
288 ],
289 ),
290 types.WindowType.Frame(
291 "data",
292 [types.WindowType.Field("items", 1, 0)],
293 ),
294 ],
295 )
296 small_list_window = types.TypeAlias("@SmallListWindow", "SmallListWindow",
297 small_list_window_inner)
298
299 narrow_data_list = types.ListType("!esi.list<ui7>", _uint7)
300 narrow_data_arg = types.StructType("@NarrowDataWindow::arg",
301 [("tag", _uint16),
302 ("items", narrow_data_list)])
303 narrow_data_frame = types.StructType(
304 "@NarrowDataWindow::data",
305 [("items", types.ArrayType("!hw.array<1xui7>", _uint7, 1))])
306 narrow_data_lowered = types.UnionType("@NarrowDataWindow::lowered",
307 [("header", sw_header_inner),
308 ("data", narrow_data_frame)])
309 narrow_data_window_inner = types.WindowType(
310 '!esi.window<"NarrowDataWindow", @NarrowDataWindow::arg, '
311 '[<"header", [<"tag">, <"items" countWidth 2>]>, '
312 '<"data", [<"items", 1>]>]>',
313 "NarrowDataWindow",
314 narrow_data_arg,
315 narrow_data_lowered,
316 small_list_window_inner.frames,
317 )
318 narrow_data_window = types.TypeAlias("@NarrowDataWindow", "NarrowDataWindow",
319 narrow_data_window_inner)
320
321 # View-class fields backed by `esi::MutableBitVector` /
322 # `esi::Int` / `esi::UInt` from `esi/Values.h`. Covers BitsType at
323 # both narrow and wide widths, plus signed/unsigned integers above
324 # the 64-bit native ceiling. Layout fields cover byte-aligned and
325 # bit-misaligned wide-int offsets so both the aligned and the
326 # bit-shifted view accessors get exercised.
327 wide_u_inner = types.StructType(
328 "@WideU::inner",
329 [("u96", _uint96), ("u128", _uint128)],
330 )
331 wide_u = types.TypeAlias("@WideU", "WideU", wide_u_inner)
332 wide_s_inner = types.StructType(
333 "@WideS::inner",
334 [("s96", _sint96), ("s128", _sint128)],
335 )
336 wide_s = types.TypeAlias("@WideS", "WideS", wide_s_inner)
337 # Bits-typed fields > 64 bits route through the value-class path
338 # (`esi::BitVector` view); narrower Bits stay on the native int paths
339 # and don't need a dedicated harness here.
340 bits_inner = types.StructType(
341 "@BitsField::inner",
342 [("wide", _bits128)],
343 )
344 bits_field = types.TypeAlias("@BitsField", "BitsField", bits_inner)
345 # Place the wide UInt field *before* a 3-bit tag in the manifest.
346 # Field order is reversed on the wire (`cpp_type.reverse=True`), so the
347 # LAST manifest field lands at wire bit 0. Listing `payload` first and
348 # `tag` last puts `tag` at bits 0..2 (byte-aligned) and the 128-bit
349 # `payload` at bits 3..130 — exercising the bit-shifted view
350 # accessor for a wide field at a non-byte-aligned offset rather
351 # than only the byte-aligned case.
352 wide_mis_inner = types.StructType(
353 "@WideMisaligned::inner",
354 [("payload", _uint128), ("tag", _uint3)],
355 )
356 wide_mis = types.TypeAlias("@WideMisaligned", "WideMisaligned",
357 wide_mis_inner)
358
359 # Array of view-class elements. The planner used to skip parent types
360 # that reached this construct; now the emitter handles them by emitting
361 # per-element indexed accessors that build fresh views into `_bytes`.
362 # Use ui128 (a view-class type) at multiple element widths to exercise
363 # both byte-aligned and bit-misaligned per-element offsets.
364 arr3_u128_type = types.ArrayType("!hw.array<3xui128>", _uint128, 3)
365 arr_views_inner = types.StructType(
366 "@ArrViews::inner",
367 [("items", arr3_u128_type)],
368 )
369 arr_views = types.TypeAlias("@ArrViews", "ArrViews", arr_views_inner)
370 # Same array placed after a 3-bit tag so the array starts at bit 3
371 # and successive elements land at non-byte-aligned per-element
372 # offsets (3, 131, 259, ...).
373 arr_views_mis_inner = types.StructType(
374 "@ArrViewsMis::inner",
375 [("items", arr3_u128_type), ("tag", _uint3)],
376 )
377 arr_views_mis = types.TypeAlias("@ArrViewsMis", "ArrViewsMis",
378 arr_views_mis_inner)
379
380 return [
381 std_u, std_s, odd_u, odd_s, sub_u, sub_s, bool_field, outer, misaligned,
382 arr4, u3_arr, bits1_arr, s5_arr, u24_arr, sb_cell, sb_cell_arr, nested3,
383 nested_cell, union_two, union_subbyte, list_window, small_list_window,
384 narrow_data_window, wide_u, wide_s, bits_field, wide_mis, arr_views,
385 arr_views_mis
386 ]
387
388
389# ---------------------------------------------------------------------------
390# Harness build + run
391# ---------------------------------------------------------------------------
392
393
394@requires_cmake
396 """Compile `codegen_harness.cpp` against a freshly-generated `types.h`
397 and run it. The harness asserts every wire-format and accessor invariant
398 end-to-end; this Python test just drives the cmake build and reports
399 failures.
400 """
401 from esiaccel.utils import get_dll_dir
402 esi_dll_path = get_dll_dir()
403 if sys.platform == "win32":
404 runtime_lib = esi_dll_path / "ESICppRuntime.lib"
405 runtime_dll = esi_dll_path / "ESICppRuntime.dll"
406 else:
407 runtime_lib = esi_dll_path / "libESICppRuntime.so"
408 runtime_dll = None
409
410 # Generate the header into `<generated>/codegen_harness/types.h` so the
411 # harness's `#include "codegen_harness/types.h"` resolves under
412 # `<generated>`, which we feed to CMake as the include root.
413 generated_dir = tmp_path / "generated"
414 (generated_dir / "codegen_harness").mkdir(parents=True)
416 emitter = CppTypeEmitter(planner)
417 emitter.write_header(generated_dir / "codegen_harness", "esi_system")
418
419 build_dir = tmp_path / "build"
420 configure_cmd = [
421 "cmake",
422 "-S",
423 str(_HARNESS_DIR),
424 "-B",
425 str(build_dir),
426 "-DCMAKE_BUILD_TYPE=Debug",
427 f"-DCODEGEN_HARNESS_GENERATED_DIR={generated_dir}",
428 f"-DESI_RUNTIME_LIB={runtime_lib}",
429 ]
430 if sys.platform == "win32":
431 configure_cmd.append(f"-DESI_RUNTIME_DLL={runtime_dll}")
432 configure_proc = subprocess.run(configure_cmd, capture_output=True, text=True)
433 if configure_proc.returncode != 0:
434 pytest.fail(
435 "cmake configure failed for the codegen harness "
436 f"(rc={configure_proc.returncode}):\n"
437 f"--- cmd ---\n{' '.join(configure_cmd)}\n"
438 f"--- stdout ---\n{configure_proc.stdout}\n"
439 f"--- stderr ---\n{configure_proc.stderr}",
440 pytrace=False,
441 )
442
443 build_cmd = [
444 "cmake", "--build",
445 str(build_dir), "--target", "codegen_harness", "--config", "Debug"
446 ]
447 build_proc = subprocess.run(build_cmd, capture_output=True, text=True)
448 if build_proc.returncode != 0:
449 # Print the generated header alongside the compile failure so a
450 # reviewer can diff the codegen output against what the harness
451 # expects to see.
452 pytest.fail(
453 "codegen_harness.cpp failed to compile against the generated "
454 f"types.h (rc={build_proc.returncode}):\n"
455 f"--- cmd ---\n{' '.join(build_cmd)}\n"
456 f"--- stdout ---\n{build_proc.stdout}\n"
457 f"--- stderr ---\n{build_proc.stderr}\n"
458 "--- generated types.h ---\n" +
459 (generated_dir / "codegen_harness" / "types.h").read_text(),
460 pytrace=False,
461 )
462
463 # CMake places the executable under the build directory; on multi-config
464 # generators (e.g. Visual Studio) the output lives under
465 # `build_dir/<Config>/`. Walk the build dir to find whatever was built.
466 binary_name = "codegen_harness" + sysconfig.get_config_var("EXE")
467 candidates = list(build_dir.rglob(binary_name))
468 if not candidates:
469 pytest.fail(
470 f"codegen_harness binary not found under {build_dir}; "
471 "the cmake build reported success but produced no executable.",
472 pytrace=False,
473 )
474 binary = candidates[0]
475
476 run_proc = subprocess.run([str(binary)], capture_output=True, text=True)
477 if run_proc.returncode != 0 or run_proc.stdout.strip() != "OK":
478 pytest.fail(
479 f"codegen_harness reported failure (rc={run_proc.returncode}):\n"
480 f"--- stdout ---\n{run_proc.stdout}\n"
481 f"--- stderr ---\n{run_proc.stderr}",
482 pytrace=False,
483 )
484
485
486# ---------------------------------------------------------------------------
487# Lightweight Python-level checks for behaviours the harness can't drive.
488# ---------------------------------------------------------------------------
489
490
491def _emit(type_table, system_name: str = "test_ns") -> str:
492 """Run the planner + emitter against `type_table` and return `types.h`."""
493 planner = CppTypePlanner(type_table)
494 emitter = CppTypeEmitter(planner)
495 with tempfile.TemporaryDirectory() as tmpdir:
496 emitter.write_header(Path(tmpdir), system_name)
497 return (Path(tmpdir) / "types.h").read_text()
498
499
501 """A struct whose payload type has no bounded width (e.g. `!esi.any`)
502 cannot be expressed in the raw-bytes layout; the codegen drops the
503 whole struct and leaves an `Unsupported type` comment behind so callers
504 see why the symbol they expected is missing."""
505 any_t = types.AnyType("!esi.any")
506 s = types.StructType("@any_struct", [("tag", _uint8), ("data", any_t)])
507
508 hdr = _emit([s])
509 assert "// Unsupported type '<@any_struct>'" in hdr
510
511
513 """Golden check of the per-port-kind `connect()` snippets for ALL kinds.
514
515 The round-trip harness only exercises the types; the live port-kind paths
516 are covered by the (heavyweight) cosim integration suite. This test pins
517 the generated resolution code for every kind -- including the ones the
518 sample manifest doesn't contain (Callback / FromHost / MMIO / Metric /
519 Bundle) -- so a typo in the `CppPortKind` table can't slip through.
520 """
521 from esiaccel.codegen.ports import CPP_PORT_KINDS, CPP_BUNDLE_KIND
522
523 kinds = {cls.__name__: kind for cls, kind in CPP_PORT_KINDS}
524 kinds["BundlePort"] = CPP_BUNDLE_KIND
525 ae = 'esi::AppID("p")'
526 aei = 'esi::AppID("p", idx)'
527
528 def scalar(name: str) -> str:
529 k = kinds[name]
530 return k.scalar_find_code(f"p{k.param_suffix}", ae)
531
532 def indexed(name: str) -> str:
533 return kinds[name].indexed_find_code("p_backing", aei)
534
535 # --- scalar resolution snippets ---
536 assert scalar("FunctionPort") == (
537 "auto *p_port =\n"
538 " esi::findPortAsOrThrow<esi::services::FuncService::Function>(\n"
539 ' rawModule, esi::AppID("p"));')
540 assert scalar("CallbackPort") == (
541 "auto *p_port =\n"
542 " esi::findPortAsOrThrow<esi::services::CallService::Callback>(\n"
543 ' rawModule, esi::AppID("p"));')
544 assert scalar("ToHostPort") == (
545 "auto &p_chan =\n"
546 " esi::findPortAsOrThrow<esi::services::ChannelService::ToHost>(\n"
547 ' rawModule, esi::AppID("p"))->getRawRead("data");')
548 assert scalar("FromHostPort") == (
549 "auto &p_chan =\n"
550 " esi::findPortAsOrThrow<esi::services::ChannelService::FromHost>(\n"
551 ' rawModule, esi::AppID("p"))->getRawWrite("data");')
552 assert scalar("MMIORegion") == (
553 "auto &p_svc =\n"
554 " *esi::findPortAsOrThrow<esi::services::MMIO::MMIORegion>(\n"
555 ' rawModule, esi::AppID("p"));')
556 assert scalar("MetricPort") == (
557 "auto &p_svc =\n"
558 " *esi::findPortAsOrThrow<esi::services::TelemetryService::Metric>(\n"
559 ' rawModule, esi::AppID("p"));')
560 assert scalar("BundlePort") == (
561 'auto &p_port = esi::findPortOrThrow(rawModule, esi::AppID("p"));')
562
563 # --- indexed (IndexedPorts<T>) try_emplace bodies ---
564 assert indexed("FunctionPort") == (
565 " p_backing.try_emplace(\n"
566 " static_cast<int>(idx),\n"
567 " esi::findPortAsOrThrow<esi::services::FuncService::Function>(\n"
568 ' rawModule, esi::AppID("p", idx)));')
569 assert indexed("ToHostPort") == (
570 " auto *svc =\n"
571 " esi::findPortAsOrThrow<esi::services::ChannelService::ToHost>(\n"
572 ' rawModule, esi::AppID("p", idx));\n'
573 " p_backing.try_emplace(\n"
574 " static_cast<int>(idx),\n"
575 ' svc->getRawRead("data"));')
576 assert indexed("FromHostPort") == (
577 " auto *svc =\n"
578 " esi::findPortAsOrThrow<esi::services::ChannelService::FromHost>(\n"
579 ' rawModule, esi::AppID("p", idx));\n'
580 " p_backing.try_emplace(\n"
581 " static_cast<int>(idx),\n"
582 ' svc->getRawWrite("data"));')
583 assert indexed("MMIORegion") == (
584 " p_backing.try_emplace(\n"
585 " static_cast<int>(idx),\n"
586 " esi::findPortAsOrThrow<esi::services::MMIO::MMIORegion>(\n"
587 ' rawModule, esi::AppID("p", idx)));')
588 assert indexed("BundlePort") == (
589 " p_backing.try_emplace(\n"
590 " static_cast<int>(idx),\n"
591 " &esi::findPortOrThrow(rawModule, esi::AppID(\"p\", idx)));")
592
593 # --- per-kind field invariants ---
594 assert [
595 k.connectable
596 for k in (kinds["FunctionPort"], kinds["CallbackPort"],
597 kinds["ToHostPort"], kinds["FromHostPort"], kinds["MMIORegion"],
598 kinds["MetricPort"], kinds["BundlePort"])
599 ] == [True, False, True, True, False, True, False]
600 assert kinds["FunctionPort"].param_suffix == "_port"
601 assert kinds["ToHostPort"].param_suffix == "_chan"
602 assert kinds["MMIORegion"].param_suffix == "_svc"
603 assert (kinds["FunctionPort"].member_template ==
604 "esi::TypedFunction<{p}Args, {p}Result>")
605 assert kinds["MMIORegion"].member_template is None
606 assert (
607 kinds["MMIORegion"].indexed_elem == "esi::services::MMIO::MMIORegion *")
608 assert kinds["FunctionPort"].alias_kind == "func"
609 assert kinds["ToHostPort"].alias_kind == "chan"
610 assert kinds["MMIORegion"].alias_kind is None
_build_harness_manifest()
test_port_find_code_golden()
str _emit(type_table, str system_name="test_ns")
test_codegen_round_trip(tmp_path)
test_unbounded_field_skips_struct_with_comment()