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