@@ -18,6 +18,12 @@ const Generator = gen.Generator;
1818// ============================================================================
1919
2020/// Transform the output of a generator using a mapping function.
21+ ///
22+ /// Example:
23+ /// ```zig
24+ /// // Convert generated integers to strings
25+ /// const string_gen = combinators.map(i32, []const u8, gen.int(i32), intToString);
26+ /// ```
2127pub fn map (
2228 comptime T : type ,
2329 comptime U : type ,
@@ -38,6 +44,12 @@ pub fn map(
3844// ============================================================================
3945
4046/// Chain generators - use the output of one generator to create another.
47+ ///
48+ /// Example:
49+ /// ```zig
50+ /// // Generate a length, then a list of that length
51+ /// const list_gen = combinators.flatMap(usize, []const u8, gen.intRange(usize, 1, 10), makeListGen);
52+ /// ```
4153pub fn flatMap (
4254 comptime T : type ,
4355 comptime U : type ,
@@ -60,6 +72,14 @@ pub fn flatMap(
6072
6173/// Generate values that satisfy a predicate.
6274/// WARNING: This can loop indefinitely if the predicate is rarely satisfied.
75+ ///
76+ /// Example:
77+ /// ```zig
78+ /// const even_gen = combinators.filter(i32, gen.int(i32), isEven, 100);
79+ /// ```
80+ ///
81+ /// Memory lifecycle: Generated values that fail the predicate are automatically freed if associated generator has a freeFn.
82+ /// The retained value is owned by the Minish runner.
6383pub fn filter (
6484 comptime T : type ,
6585 comptime base_gen : Generator (T ),
@@ -74,11 +94,21 @@ pub fn filter(
7494 if (predicate (value )) {
7595 return value ;
7696 }
97+ // Free rejected value if generator provides freeFn
98+ if (base_gen .freeFn ) | freeFn | {
99+ freeFn (tc .allocator , value );
100+ }
77101 }
78102 return error .Overrun ;
79103 }
104+
105+ fn free (allocator : std.mem.Allocator , value : T ) void {
106+ if (base_gen .freeFn ) | freeFn | {
107+ freeFn (allocator , value );
108+ }
109+ }
80110 };
81- return .{ .generateFn = FilterGenerator .generate , .shrinkFn = null , .freeFn = null };
111+ return .{ .generateFn = FilterGenerator .generate , .shrinkFn = null , .freeFn = FilterGenerator . free };
82112}
83113
84114// ============================================================================
@@ -101,6 +131,18 @@ pub fn sized(
101131// ============================================================================
102132
103133/// Choose from generators with weighted probabilities.
134+ ///
135+ /// Example:
136+ /// ```zig
137+ /// // 90% chance of 0, 10% chance of random int
138+ /// const biased_gen = combinators.frequency(i32, &.{
139+ /// .{ .weight = 90, .gen = gen.constant(@as(i32, 0)) },
140+ /// .{ .weight = 10, .gen = gen.int(i32) }
141+ /// });
142+ /// ```
143+ ///
144+ /// Memory lifecycle: The returned value is owned by the Minish runner and will be freed automatically.
145+ /// Assumes all weighted generators share compatible memory management.
104146pub fn frequency (
105147 comptime T : type ,
106148 comptime weighted_gens : []const struct { weight : u64 , gen : Generator (T ) },
@@ -118,9 +160,62 @@ pub fn frequency(
118160 const idx = try tc .weightedChoice (& weights );
119161 return weighted_gens [idx ].gen .generateFn (tc );
120162 }
163+
164+ fn free (allocator : std.mem.Allocator , value : T ) void {
165+ // Assume homogeneity: use first generator's freeFn if available
166+ if (weighted_gens .len > 0 and weighted_gens [0 ].gen .freeFn != null ) {
167+ weighted_gens [0 ].gen .freeFn .? (allocator , value );
168+ }
169+ }
121170 };
122- return .{ .generateFn = FrequencyGenerator .generate , .shrinkFn = null , .freeFn = null };
171+ return .{ .generateFn = FrequencyGenerator .generate , .shrinkFn = null , .freeFn = FrequencyGenerator . free };
123172}
124173
125174// Note: Combinator tests are demonstrated in examples/e5_struct_and_combinators.zig
126175// They cannot be easily unit tested due to comptime parameter requirements
176+
177+ test "combinator memory leak regression tests" {
178+ const runner = @import ("runner.zig" );
179+ const allocator = std .testing .allocator ;
180+ const str_gen = comptime gen .string (.{ .min_len = 1 , .max_len = 5 });
181+
182+ const opts = runner.Options { .seed = 111 , .num_runs = 10 };
183+
184+ const Props = struct {
185+ fn prop_no_op (_ : []const u8 ) ! void {}
186+ };
187+
188+ // Test Frequency
189+ const freq_gen = frequency ([]const u8 , &.{
190+ .{ .weight = 10 , .gen = str_gen },
191+ .{ .weight = 10 , .gen = str_gen },
192+ });
193+ try runner .check (allocator , freq_gen , Props .prop_no_op , opts );
194+ }
195+
196+ test "filter memory leak regression test" {
197+ const runner = @import ("runner.zig" );
198+ const allocator = std .testing .allocator ;
199+
200+ // Generate strings, keep only those starting with 'A'.
201+ // Rejected strings (allocations) should be freed by filter.
202+ const str_gen = comptime gen .string (.{ .min_len = 1 , .max_len = 5 , .charset = .alphanumeric });
203+
204+ const startsWithA = struct {
205+ fn func (s : []const u8 ) bool {
206+ if (s .len == 0 ) return false ;
207+ return s [0 ] == 'A' ;
208+ }
209+ }.func ;
210+
211+ // We filter, max 1000 attempts to allow many rejections without failure.
212+ const filtered_gen = filter ([]const u8 , str_gen , startsWithA , 1000 );
213+
214+ const opts = runner.Options { .seed = 111 , .num_runs = 10 };
215+
216+ const Props = struct {
217+ fn prop_no_op (_ : []const u8 ) ! void {}
218+ };
219+
220+ try runner .check (allocator , filtered_gen , Props .prop_no_op , opts );
221+ }
0 commit comments