@@ -24,6 +24,10 @@ const Generator = gen.Generator;
2424/// // Convert generated integers to strings
2525/// const string_gen = combinators.map(i32, []const u8, gen.int(i32), intToString);
2626/// ```
27+ ///
28+ /// Note: If base_gen allocates memory and map_fn transforms to a different type,
29+ /// the base value is freed after transformation. If map_fn returns a type that
30+ /// also needs freeing, you must provide that through the result generator.
2731pub fn map (
2832 comptime T : type ,
2933 comptime U : type ,
@@ -33,9 +37,16 @@ pub fn map(
3337 const MapGenerator = struct {
3438 fn generate (tc : * TestCase ) core.GenError ! U {
3539 const base_value = try base_gen .generateFn (tc );
36- return map_fn (base_value );
40+ const result = map_fn (base_value );
41+ // Free the base value after transformation if it was allocated
42+ if (base_gen .freeFn ) | freeFn | {
43+ freeFn (tc .allocator , base_value );
44+ }
45+ return result ;
3746 }
3847 };
48+ // Note: freeFn is null because the mapped result U may have different
49+ // memory semantics. Users should use a wrapper if U needs freeing.
3950 return .{ .generateFn = MapGenerator .generate , .shrinkFn = null , .freeFn = null };
4051}
4152
@@ -50,6 +61,8 @@ pub fn map(
5061/// // Generate a length, then a list of that length
5162/// const list_gen = combinators.flatMap(usize, []const u8, gen.intRange(usize, 1, 10), makeListGen);
5263/// ```
64+ ///
65+ /// Note: The base value is freed after the next generator is created and produces a result.
5366pub fn flatMap (
5467 comptime T : type ,
5568 comptime U : type ,
@@ -60,7 +73,20 @@ pub fn flatMap(
6073 fn generate (tc : * TestCase ) core.GenError ! U {
6174 const base_value = try base_gen .generateFn (tc );
6275 const next_gen = flat_fn (base_value );
63- return next_gen .generateFn (tc );
76+ const result = try next_gen .generateFn (tc );
77+ // Free the base value after we're done using it
78+ if (base_gen .freeFn ) | freeFn | {
79+ freeFn (tc .allocator , base_value );
80+ }
81+ return result ;
82+ }
83+
84+ fn free (allocator : std.mem.Allocator , value : U ) void {
85+ // Try to free using the result generator's freeFn
86+ // Note: This is a best-effort approach since we don't know which
87+ // specific generator was used (depends on base_value at runtime)
88+ _ = allocator ;
89+ _ = value ;
6490 }
6591 };
6692 return .{ .generateFn = FlatMapGenerator .generate , .shrinkFn = null , .freeFn = null };
@@ -219,3 +245,66 @@ test "filter memory leak regression test" {
219245
220246 try runner .check (allocator , filtered_gen , Props .prop_no_op , opts );
221247}
248+
249+ test "regression: map combinator frees base value" {
250+ // Bug: Map combinator didn't free the base value after transformation
251+ // Fix: Added freeFn call after map_fn is applied
252+ const runner = @import ("runner.zig" );
253+ const allocator = std .testing .allocator ;
254+
255+ // Generate a string (which allocates) and map it to its length (which doesn't)
256+ const str_gen = comptime gen .string (.{ .min_len = 1 , .max_len = 10 });
257+
258+ const getLen = struct {
259+ fn func (s : []const u8 ) usize {
260+ return s .len ;
261+ }
262+ }.func ;
263+
264+ const len_gen = map ([]const u8 , usize , str_gen , getLen );
265+
266+ const opts = runner.Options { .seed = 222 , .num_runs = 20 };
267+
268+ const Props = struct {
269+ fn prop_check_len (len : usize ) ! void {
270+ // Just verify the length is in expected range
271+ try std .testing .expect (len >= 1 and len <= 10 );
272+ }
273+ };
274+
275+ // If map doesn't free the base string, this will leak memory
276+ // and the test allocator will catch it
277+ try runner .check (allocator , len_gen , Props .prop_check_len , opts );
278+ }
279+
280+ test "flatMap combinator chains generators" {
281+ const runner = @import ("runner.zig" );
282+ const allocator = std .testing .allocator ;
283+
284+ // Generate a number, then use it to determine which generator to use
285+ const makeGen = struct {
286+ fn make (x : i32 ) gen.Generator (i32 ) {
287+ // If x is even, return 0; if odd, return 1
288+ if (@mod (x , 2 ) == 0 ) {
289+ return gen .constant (@as (i32 , 0 ));
290+ } else {
291+ return gen .constant (@as (i32 , 1 ));
292+ }
293+ }
294+ }.make ;
295+
296+ const flat_gen = flatMap (i32 , i32 , gen .intRange (i32 , 0 , 10 ), makeGen );
297+
298+ const Props = struct {
299+ fn prop (x : i32 ) ! void {
300+ // Result should be either 0 or 1
301+ try std .testing .expect (x == 0 or x == 1 );
302+ }
303+ };
304+
305+ try runner .check (allocator , flat_gen , Props .prop , .{ .seed = 333 , .num_runs = 20 });
306+ }
307+
308+ // Note: sized combinator not tested because it requires generators that take
309+ // runtime size parameters, but most generators use comptime parameters.
310+ // It's primarily for custom use cases.
0 commit comments