@@ -13,25 +13,45 @@ typedef struct {
1313 i32 y ;
1414} Point2D ;
1515
16+ typedef struct {
17+ Point2D min ;
18+ Point2D max ;
19+ } Bounds ;
20+
21+ typedef struct {
22+ i32 id ;
23+ Bounds bbox ;
24+ Point2D centroid ;
25+ } Region ;
26+
1627// The extension hook. Must be defined BEFORE Misra/Std/Io.h is processed in
1728// this TU so that IOFMT(x) picks up the extra _Generic arm. The macro body
18- // references symbols `_write_Point2D` / `_read_Point2D` which only need to
19- // be declared at the WriteFmt / StrReadFmt call sites further down -- the
20- // definitions in this file (below the Io.h include) satisfy that.
29+ // references `_write_T` / `_read_T` symbols which only need to be declared
30+ // at the WriteFmt / StrReadFmt call sites further down -- the definitions
31+ // in this file (below the Io.h include) satisfy that.
32+ //
33+ // All three user types live in the same hook because the writer for the
34+ // outer types (Bounds, Region) recursively expands IOFMT on inner-type
35+ // arguments -- every arm must be visible at every call site.
2136#define IOFMT_USER_CASES_ (x , addr ) \
2237Point2D: \
23- TO_TYPE_SPECIFIC_IO(Point2D, addr),
38+ TO_TYPE_SPECIFIC_IO(Point2D, addr), Bounds : TO_TYPE_SPECIFIC_IO(Bounds, addr), \
39+ Region : TO_TYPE_SPECIFIC_IO(Region, addr),
2440
2541#include <Misra/Std/Io.h>
2642
2743#include "../Util/TestRunner.h"
2844
29- // Forward declarations so the `_Generic` arms inside IOFMT can resolve
30- // both `_write_Point2D` and `_read_Point2D` from any call site below --
31- // including from inside `_write_Point2D` 's own body, which calls
32- // StrAppendFmt and thus expands IOFMT .
45+ // Forward declarations so the `_Generic` arms inside IOFMT resolve every
46+ // user `_write_T` / `_read_T` symbol from every call site -- including
47+ // nested expansions from inside an outer writer 's own body. Pattern A in
48+ // the extending-io guide .
3349bool _write_Point2D (Str * o , FmtInfo * info , Point2D * p );
3450Zstr _read_Point2D (Zstr i , FmtInfo * info , Point2D * p );
51+ bool _write_Bounds (Str * o , FmtInfo * info , Bounds * b );
52+ Zstr _read_Bounds (Zstr i , FmtInfo * info , Bounds * b );
53+ bool _write_Region (Str * o , FmtInfo * info , Region * r );
54+ Zstr _read_Region (Zstr i , FmtInfo * info , Region * r );
3555
3656bool _write_Point2D (Str * o , FmtInfo * info , Point2D * p ) {
3757 (void )info ;
@@ -70,10 +90,82 @@ Zstr _read_Point2D(Zstr i, FmtInfo *info, Point2D *p) {
7090 return i ;
7191}
7292
93+ // Bounds delegates to Point2D for its two corners. Verifies the
94+ // `_Generic` arm for `Point2D` resolves correctly from inside another
95+ // user-type writer.
96+ bool _write_Bounds (Str * o , FmtInfo * info , Bounds * b ) {
97+ (void )info ;
98+ if (!o || !b ) {
99+ return false;
100+ }
101+ return StrAppendFmt (o , "[{}..{}]" , b -> min , b -> max );
102+ }
103+
104+ Zstr _read_Bounds (Zstr i , FmtInfo * info , Bounds * b ) {
105+ (void )info ;
106+ if (!i || !b ) {
107+ return i ;
108+ }
109+ while (* i == ' ' || * i == '\t' ) {
110+ i ++ ;
111+ }
112+ if (* i != '[' ) {
113+ return i ;
114+ }
115+ i ++ ;
116+ FmtInfo inner = {0 };
117+ i = _read_Point2D (i , & inner , & b -> min );
118+ while (* i == '.' ) {
119+ i ++ ;
120+ }
121+ i = _read_Point2D (i , & inner , & b -> max );
122+ while (* i == ' ' || * i == '\t' ) {
123+ i ++ ;
124+ }
125+ if (* i == ']' ) {
126+ i ++ ;
127+ }
128+ return i ;
129+ }
130+
131+ // Region is 3-deep: it embeds Bounds (which itself embeds Point2D) plus
132+ // a standalone Point2D plus an in-tree i32. Exercises a writer that mixes
133+ // nested user types, repeated user types, and built-ins in one call.
134+ bool _write_Region (Str * o , FmtInfo * info , Region * r ) {
135+ (void )info ;
136+ if (!o || !r ) {
137+ return false;
138+ }
139+ return StrAppendFmt (o , "{}:{}@{}" , r -> id , r -> bbox , r -> centroid );
140+ }
141+
142+ Zstr _read_Region (Zstr i , FmtInfo * info , Region * r ) {
143+ (void )info ;
144+ if (!i || !r ) {
145+ return i ;
146+ }
147+ FmtInfo inner = {0 };
148+ i = _read_i32 (i , & inner , & r -> id );
149+ while (* i == ' ' || * i == '\t' || * i == ':' ) {
150+ i ++ ;
151+ }
152+ i = _read_Bounds (i , & inner , & r -> bbox );
153+ while (* i == ' ' || * i == '\t' || * i == '@' ) {
154+ i ++ ;
155+ }
156+ i = _read_Point2D (i , & inner , & r -> centroid );
157+ return i ;
158+ }
159+
73160bool test_user_type_write_basic (void );
74161bool test_user_type_write_mixed_args (void );
75162bool test_user_type_read_basic (void );
76163bool test_user_type_round_trip (void );
164+ bool test_nested_user_type_write (void );
165+ bool test_nested_user_type_round_trip (void );
166+ bool test_deep_nested_user_type_write (void );
167+ bool test_deep_nested_user_type_round_trip (void );
168+ bool test_nested_user_type_mixed_with_builtins (void );
77169
78170bool test_user_type_write_basic (void ) {
79171 WriteFmt ("Testing user-type write through IOFMT_USER_CASES_\n" );
@@ -136,6 +228,121 @@ bool test_user_type_round_trip(void) {
136228 return ok ;
137229}
138230
231+ bool test_nested_user_type_write (void ) {
232+ WriteFmt ("Testing user-type-in-user-type writer (Bounds embeds Point2D)\n" );
233+
234+ DefaultAllocator alloc = DefaultAllocatorInit ();
235+ Str out = StrInit (& alloc );
236+
237+ Bounds b = {
238+ .min = { .x = 0 , .y = 0 },
239+ .max = {.x = 10 , .y = 20 }
240+ };
241+ bool ok = StrAppendFmt (& out , "{}" , b );
242+ ok = ok && (ZstrCompare (StrBegin (& out ), "[(0, 0)..(10, 20)]" ) == 0 );
243+
244+ StrDeinit (& out );
245+ DefaultAllocatorDeinit (& alloc );
246+ return ok ;
247+ }
248+
249+ bool test_nested_user_type_round_trip (void ) {
250+ WriteFmt ("Testing nested user-type round-trip (Bounds)\n" );
251+
252+ DefaultAllocator alloc = DefaultAllocatorInit ();
253+ Str out = StrInit (& alloc );
254+
255+ Bounds src = {
256+ .min = {.x = -5 , .y = -7 },
257+ .max = { .x = 3 , .y = 11 }
258+ };
259+ Bounds dst = {0 };
260+
261+ bool ok = StrAppendFmt (& out , "{}" , src );
262+ Zstr in = StrBegin (& out );
263+ StrReadFmt (in , "{}" , dst );
264+
265+ ok = ok && (dst .min .x == src .min .x ) && (dst .min .y == src .min .y );
266+ ok = ok && (dst .max .x == src .max .x ) && (dst .max .y == src .max .y );
267+
268+ StrDeinit (& out );
269+ DefaultAllocatorDeinit (& alloc );
270+ return ok ;
271+ }
272+
273+ bool test_deep_nested_user_type_write (void ) {
274+ WriteFmt ("Testing 3-level nested user-type writer (Region -> Bounds -> Point2D)\n" );
275+
276+ DefaultAllocator alloc = DefaultAllocatorInit ();
277+ Str out = StrInit (& alloc );
278+
279+ Region r = {
280+ .id = 42 ,
281+ .bbox = {.min = {.x = 0 , .y = 0 }, .max = {.x = 100 , .y = 50 }},
282+ .centroid = { .x = 50 , .y = 25 },
283+ };
284+ bool ok = StrAppendFmt (& out , "{}" , r );
285+ ok = ok && (ZstrCompare (StrBegin (& out ), "42:[(0, 0)..(100, 50)]@(50, 25)" ) == 0 );
286+
287+ StrDeinit (& out );
288+ DefaultAllocatorDeinit (& alloc );
289+ return ok ;
290+ }
291+
292+ bool test_deep_nested_user_type_round_trip (void ) {
293+ WriteFmt ("Testing 3-level nested user-type round-trip (Region)\n" );
294+
295+ DefaultAllocator alloc = DefaultAllocatorInit ();
296+ Str out = StrInit (& alloc );
297+
298+ Region src = {
299+ .id = -1234 ,
300+ .bbox = {.min = {.x = -10 , .y = -20 }, .max = {.x = 30 , .y = 40 }},
301+ .centroid = { .x = 10 , .y = 10 },
302+ };
303+ Region dst = {0 };
304+
305+ bool ok = StrAppendFmt (& out , "{}" , src );
306+ Zstr in = StrBegin (& out );
307+ StrReadFmt (in , "{}" , dst );
308+
309+ ok = ok && (dst .id == src .id );
310+ ok = ok && (dst .bbox .min .x == src .bbox .min .x ) && (dst .bbox .min .y == src .bbox .min .y );
311+ ok = ok && (dst .bbox .max .x == src .bbox .max .x ) && (dst .bbox .max .y == src .bbox .max .y );
312+ ok = ok && (dst .centroid .x == src .centroid .x ) && (dst .centroid .y == src .centroid .y );
313+
314+ StrDeinit (& out );
315+ DefaultAllocatorDeinit (& alloc );
316+ return ok ;
317+ }
318+
319+ bool test_nested_user_type_mixed_with_builtins (void ) {
320+ WriteFmt ("Testing nested user types mixed with built-ins in one call\n" );
321+
322+ DefaultAllocator alloc = DefaultAllocatorInit ();
323+ Str out = StrInit (& alloc );
324+
325+ Region region = {
326+ .id = 7 ,
327+ .bbox = {.min = {.x = 1 , .y = 2 }, .max = {.x = 3 , .y = 4 }},
328+ .centroid = { .x = 2 , .y = 3 },
329+ };
330+ Point2D origin = {.x = 0 , .y = 0 };
331+ f64 score = 1.5 ;
332+
333+ // Single format call mixing: built-in f64 + user Region (which itself
334+ // expands to an i32 plus a nested Bounds plus a Point2D) + a separate
335+ // Point2D arg. Forces the per-arg _Generic to dispatch four distinct
336+ // arms (i32 implicit, Region, Point2D, f64) and proves nested user
337+ // types and built-ins coexist in one IOFMT expansion.
338+ bool ok = StrAppendFmt (& out , "score={.1} region={} origin={}" , score , region , origin );
339+ ok = ok && (ZstrCompare (StrBegin (& out ), "score=1.5 region=7:[(1, 2)..(3, 4)]@(2, 3) origin=(0, 0)" ) == 0 );
340+
341+ StrDeinit (& out );
342+ DefaultAllocatorDeinit (& alloc );
343+ return ok ;
344+ }
345+
139346int main (void ) {
140347 WriteFmt ("[INFO] Starting Io.UserTypes tests\n\n" );
141348
@@ -144,6 +351,11 @@ int main(void) {
144351 test_user_type_write_mixed_args ,
145352 test_user_type_read_basic ,
146353 test_user_type_round_trip ,
354+ test_nested_user_type_write ,
355+ test_nested_user_type_round_trip ,
356+ test_deep_nested_user_type_write ,
357+ test_deep_nested_user_type_round_trip ,
358+ test_nested_user_type_mixed_with_builtins ,
147359 };
148360
149361 int total_tests = sizeof (tests ) / sizeof (tests [0 ]);
0 commit comments