Skip to content
This repository was archived by the owner on Nov 27, 2025. It is now read-only.

Commit f8224f1

Browse files
committed
Simplify context propagation
1 parent 5669390 commit f8224f1

File tree

7 files changed

+147
-102
lines changed

7 files changed

+147
-102
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,17 +209,17 @@ let! myVal =
209209

210210
```f#
211211
// a type containing generators for generic types
212-
// methods should return Gen<_> and are allowed to take Gen<_> and AutoGenConfig as parameters
212+
// methods should return Gen<_> and are allowed to take Gen<_> and AutoGenContext as parameters
213213
type GenericGenerators =
214214
215215
// Generate generic types
216216
static member MyGenericType<'a>(valueGen : Gen<'a>) : Gen<MyGenericType<'a>> =
217217
valueGen | Gen.map (fun x -> MyGenericType(x))
218218
219-
// Generate generic types with recursion support and access to AutoGenConfig
220-
static member ImmutableList<'a>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<ImmutableList<'a>> =
221-
if recursionContext.CanRecurse then
222-
valueGen |> Gen.list (AutoGenConfig.seqRange config) |> Gen.map ImmutableList.CreateRange
219+
// Generate generic types with recursion support and access to AutoGenContext
220+
static member ImmutableList<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<ImmutableList<'a>> =
221+
if context.CanRecurse then
222+
valueGen |> Gen.list context.CollectionRange |> Gen.map ImmutableList.CreateRange
223223
else
224224
Gen.constant ImmutableList<'a>.Empty
225225

src/Hedgehog.Experimental.CSharp.Tests/GenericGenTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ public static Gen<Uuid> UuidGen() =>
5353
public static Gen<Name> NameGen(Gen<string> gen) =>
5454
gen.Select(value => new Name("Name: " + value));
5555

56-
public static Gen<Maybe<T>> AlwaysJust<T>(AutoGenConfig config, RecursionContext recCtx, Gen<T> gen) =>
57-
recCtx.CanRecurse
56+
public static Gen<Maybe<T>> AlwaysJust<T>(AutoGenContext context, Gen<T> gen) =>
57+
context.CanRecurse
5858
? gen.Select(Maybe<T> (value) => new Maybe<T>.Just(value))
5959
: Gen.FromValue<Maybe<T>>(new Maybe<T>.Nothing());
6060

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module Hedgehog.Experimental.Tests.AutoGenContextTests
2+
3+
open Hedgehog
4+
open Xunit
5+
open Swensen.Unquote
6+
7+
type RecursiveType<'a> =
8+
{ Value: Option<RecursiveType<'a>>}
9+
member this.Depth =
10+
match this.Value with
11+
| None -> 0
12+
| Some x -> x.Depth + 1
13+
14+
type RecursiveGenerators =
15+
// override Option to always generate Some when recursion is allowed
16+
// using the AutoGenContext to assert recursion context preservation
17+
static member Option<'a>(context: AutoGenContext) =
18+
if context.CanRecurse then
19+
context.AutoGenerate<'a>() |> Gen.map Some
20+
else
21+
Gen.constant None
22+
23+
[<Fact>]
24+
let ``Should preserve recursion with generic types when using AutoGenContext.AutoGenerate``() =
25+
property {
26+
let! recDepth = Gen.int32 (Range.constant 2 5)
27+
let config =
28+
GenX.defaults
29+
|> AutoGenConfig.addGenerators<RecursiveGenerators>
30+
|> AutoGenConfig.setRecursionDepth recDepth
31+
32+
let! result = GenX.autoWith<RecursiveType<int>> config
33+
test <@ result.Depth = recDepth @>
34+
} |> Property.check

src/Hedgehog.Experimental.Tests/Hedgehog.Experimental.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<Compile Include="GenTests.fs" />
1414
<Compile Include="GenericGenTests.fs" />
1515
<Compile Include="AutoGenConfigTests.fs" />
16+
<Compile Include="AutoGenContextTests.fs" />
1617
</ItemGroup>
1718

1819
<ItemGroup>

src/Hedgehog.Experimental/AutoGenConfig.fs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ namespace Hedgehog
33
open System
44
open System.Reflection
55

6-
/// Provides recursion depth information for the currently generated type.
7-
/// Can be used by custom generators to respect recursion limits.
6+
type internal IAutoGenerator =
7+
abstract member Generate<'a> : unit -> Gen<'a>
8+
89
[<Sealed>]
9-
type RecursionContext(canRecurse: bool) =
10-
/// Indicates whether recursion is allowed for the current type being generated.
11-
member _.CanRecurse = canRecurse
10+
type AutoGenContext internal (
11+
canRecurse: bool,
12+
currentRecursionDepth: int,
13+
collectionRange: Range<int>,
14+
auto: IAutoGenerator) =
15+
member _.CanRecurse = canRecurse
16+
member _.CurrentRecursionDepth = currentRecursionDepth
17+
member _.CollectionRange = collectionRange
18+
member _.AutoGenerate<'a>() : Gen<'a> = auto.Generate<'a>()
1219

1320
type AutoGenConfig = internal {
1421
seqRange: Range<int> option
@@ -60,13 +67,8 @@ module AutoGenConfig =
6067
then Some (t.GetGenericArguments().[0])
6168
else None
6269

63-
let getAutoGenConfigType (t: Type) =
64-
if t = typeof<AutoGenConfig>
65-
then Some t
66-
else None
67-
68-
let getRecursionContextType (t: Type) =
69-
if t = typeof<RecursionContext>
70+
let getAutogenContextType (t: Type) =
71+
if t = typeof<AutoGenContext>
7072
then Some t
7173
else None
7274

@@ -77,8 +79,7 @@ module AutoGenConfig =
7779
| None -> None
7880
| Some types ->
7981
getGenType param.ParameterType
80-
|> Option.orElseWith (fun () -> getAutoGenConfigType param.ParameterType)
81-
|> Option.orElseWith (fun () -> getRecursionContextType param.ParameterType)
82+
|> Option.orElseWith (fun () -> getAutogenContextType param.ParameterType)
8283
|> Option.map (fun t -> Array.append types [| t |])
8384
) (Some [||])
8485

src/Hedgehog.Experimental/DefaultGenerators.fs

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -33,97 +33,97 @@ type DefaultGenerators =
3333
|> Range.map DateTime
3434
Gen.dateTime dateTimeRange |> Gen.map DateTimeOffset
3535

36-
static member ImmutableList<'a>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<ImmutableList<'a>> =
37-
if recursionContext.CanRecurse then
38-
valueGen |> Gen.list (AutoGenConfig.seqRange config) |> Gen.map ImmutableList.CreateRange
36+
static member ImmutableList<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<ImmutableList<'a>> =
37+
if context.CanRecurse then
38+
valueGen |> Gen.list context.CollectionRange |> Gen.map ImmutableList.CreateRange
3939
else
4040
Gen.constant ImmutableList<'a>.Empty
4141

42-
static member IImmutableList<'a>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<IImmutableList<'a>> =
43-
if recursionContext.CanRecurse then
44-
DefaultGenerators.ImmutableList(config, recursionContext, valueGen) |> Gen.map (fun x -> x :> IImmutableList<'a>)
42+
static member IImmutableList<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<IImmutableList<'a>> =
43+
if context.CanRecurse then
44+
DefaultGenerators.ImmutableList(context, valueGen) |> Gen.map (fun x -> x :> IImmutableList<'a>)
4545
else
4646
Gen.constant (ImmutableList<'a>.Empty :> IImmutableList<'a>)
4747

48-
static member ImmutableArray<'a>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<ImmutableArray<'a>> =
49-
if recursionContext.CanRecurse then
50-
valueGen |> Gen.array (AutoGenConfig.seqRange config) |> Gen.map ImmutableArray.CreateRange
48+
static member ImmutableArray<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<ImmutableArray<'a>> =
49+
if context.CanRecurse then
50+
valueGen |> Gen.array context.CollectionRange |> Gen.map ImmutableArray.CreateRange
5151
else
5252
Gen.constant (ImmutableArray<'a>.Empty)
5353

54-
static member ImmutableHashSet<'a when 'a : equality>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<ImmutableHashSet<'a>> =
55-
if recursionContext.CanRecurse then
56-
valueGen |> Gen.list (AutoGenConfig.seqRange config) |> Gen.map ImmutableHashSet.CreateRange
54+
static member ImmutableHashSet<'a when 'a : equality>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<ImmutableHashSet<'a>> =
55+
if context.CanRecurse then
56+
valueGen |> Gen.list context.CollectionRange |> Gen.map ImmutableHashSet.CreateRange
5757
else
5858
Gen.constant (ImmutableHashSet<'a>.Empty)
5959

60-
static member ImmutableSet<'a when 'a : comparison>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<ImmutableSortedSet<'a>> =
61-
if recursionContext.CanRecurse then
62-
valueGen |> Gen.list (AutoGenConfig.seqRange config) |> Gen.map ImmutableSortedSet.CreateRange
60+
static member ImmutableSet<'a when 'a : comparison>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<ImmutableSortedSet<'a>> =
61+
if context.CanRecurse then
62+
valueGen |> Gen.list context.CollectionRange |> Gen.map ImmutableSortedSet.CreateRange
6363
else
6464
Gen.constant (ImmutableSortedSet<'a>.Empty)
6565

66-
static member IImmutableSet<'a when 'a : equality>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<IImmutableSet<'a>> =
67-
if recursionContext.CanRecurse then
68-
DefaultGenerators.ImmutableHashSet(config, recursionContext, valueGen) |> Gen.map (fun x -> x :> IImmutableSet<'a>)
66+
static member IImmutableSet<'a when 'a : equality>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<IImmutableSet<'a>> =
67+
if context.CanRecurse then
68+
DefaultGenerators.ImmutableHashSet(context, valueGen) |> Gen.map (fun x -> x :> IImmutableSet<'a>)
6969
else
7070
Gen.constant (ImmutableHashSet<'a>.Empty :> IImmutableSet<'a>)
7171

72-
static member Dictionary<'k, 'v when 'k: equality>(config: AutoGenConfig, recursionContext: RecursionContext, keyGen: Gen<'k>, valueGen: Gen<'v>): Gen<Dictionary<'k, 'v>> =
73-
if recursionContext.CanRecurse then
72+
static member Dictionary<'k, 'v when 'k: equality>(context: AutoGenContext, keyGen: Gen<'k>, valueGen: Gen<'v>): Gen<Dictionary<'k, 'v>> =
73+
if context.CanRecurse then
7474
gen {
75-
let! kvps = Gen.zip keyGen valueGen |> Gen.list (AutoGenConfig.seqRange config)
75+
let! kvps = Gen.zip keyGen valueGen |> Gen.list context.CollectionRange
7676
return Dictionary(dict kvps)
7777
}
7878
else
7979
Gen.constant (Dictionary<'k, 'v>())
8080

81-
static member IDictionary<'k, 'v when 'k: equality>(config: AutoGenConfig, recursionContext: RecursionContext, keyGen: Gen<'k>, valueGen: Gen<'v>): Gen<IDictionary<'k, 'v>> =
82-
DefaultGenerators.Dictionary(config, recursionContext, keyGen, valueGen) |> Gen.map (fun x -> x :> IDictionary<'k, 'v>)
81+
static member IDictionary<'k, 'v when 'k: equality>(context: AutoGenContext, keyGen: Gen<'k>, valueGen: Gen<'v>): Gen<IDictionary<'k, 'v>> =
82+
DefaultGenerators.Dictionary(context, keyGen, valueGen) |> Gen.map (fun x -> x :> IDictionary<'k, 'v>)
8383

84-
static member ReadOnlyDictionary<'k, 'v when 'k: equality>(config: AutoGenConfig, recursionContext: RecursionContext, keyGen: Gen<'k>, valueGen: Gen<'v>): Gen<ReadOnlyDictionary<'k, 'v>> =
85-
DefaultGenerators.Dictionary(config, recursionContext, keyGen, valueGen) |> Gen.map (fun x -> ReadOnlyDictionary(x))
84+
static member ReadOnlyDictionary<'k, 'v when 'k: equality>(context: AutoGenContext, keyGen: Gen<'k>, valueGen: Gen<'v>): Gen<ReadOnlyDictionary<'k, 'v>> =
85+
DefaultGenerators.Dictionary(context, keyGen, valueGen) |> Gen.map (fun x -> ReadOnlyDictionary(x))
8686

87-
static member IReadOnlyDictionary<'k, 'v when 'k: equality>(config: AutoGenConfig, recursionContext: RecursionContext, keyGen: Gen<'k>, valueGen: Gen<'v>): Gen<IReadOnlyDictionary<'k, 'v>> =
88-
DefaultGenerators.Dictionary(config, recursionContext, keyGen, valueGen) |> Gen.map (fun x -> ReadOnlyDictionary(x))
87+
static member IReadOnlyDictionary<'k, 'v when 'k: equality>(context: AutoGenContext, keyGen: Gen<'k>, valueGen: Gen<'v>): Gen<IReadOnlyDictionary<'k, 'v>> =
88+
DefaultGenerators.Dictionary(context, keyGen, valueGen) |> Gen.map (fun x -> ReadOnlyDictionary(x))
8989

90-
static member FSharpList<'a>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<'a list> =
91-
if recursionContext.CanRecurse then
92-
valueGen |> Gen.list (AutoGenConfig.seqRange config)
90+
static member FSharpList<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<'a list> =
91+
if context.CanRecurse then
92+
valueGen |> Gen.list context.CollectionRange
9393
else
9494
Gen.constant []
9595

96-
static member List<'a>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<List<'a>> =
97-
DefaultGenerators.FSharpList(config, recursionContext, valueGen) |> Gen.map _.ToList()
96+
static member List<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<List<'a>> =
97+
DefaultGenerators.FSharpList(context, valueGen) |> Gen.map _.ToList()
9898

99-
static member IList<'a>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<IList<'a>> =
100-
DefaultGenerators.FSharpList(config, recursionContext, valueGen) |> Gen.map _.ToList()
99+
static member IList<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<IList<'a>> =
100+
DefaultGenerators.FSharpList(context, valueGen) |> Gen.map _.ToList()
101101

102-
static member IReadOnlyList<'a>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<IReadOnlyList<'a>> =
103-
DefaultGenerators.FSharpList(config, recursionContext, valueGen) |> Gen.map _.ToList().AsReadOnly()
102+
static member IReadOnlyList<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<IReadOnlyList<'a>> =
103+
DefaultGenerators.FSharpList(context, valueGen) |> Gen.map _.ToList().AsReadOnly()
104104

105-
static member Seq<'a>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<seq<'a>> =
106-
DefaultGenerators.FSharpList(config, recursionContext, valueGen) |> Gen.map Seq.ofList
105+
static member Seq<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<seq<'a>> =
106+
DefaultGenerators.FSharpList(context, valueGen) |> Gen.map Seq.ofList
107107

108-
static member Option<'a>(recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<'a option> =
109-
if recursionContext.CanRecurse then Gen.option valueGen
108+
static member Option<'a>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<'a option> =
109+
if context.CanRecurse then Gen.option valueGen
110110
else Gen.constant None
111111

112-
static member Nullable<'a when 'a : struct and 'a : (new : unit -> 'a) and 'a :> ValueType>(recursionContext: RecursionContext, valueGen: Gen<'a>): Gen<Nullable<'a>> =
113-
if recursionContext.CanRecurse
112+
static member Nullable<'a when 'a : struct and 'a : (new : unit -> 'a) and 'a :> ValueType>(context: AutoGenContext, valueGen: Gen<'a>): Gen<Nullable<'a>> =
113+
if context.CanRecurse
114114
then valueGen |> Gen.option |> Gen.map Option.toNullable
115115
else Gen.constant (Nullable<'a>())
116116

117-
static member Set<'a when 'a : comparison>(config: AutoGenConfig, recursionContext: RecursionContext, valueGen: Gen<'a>) : Gen<Set<'a>> =
118-
if recursionContext.CanRecurse then
119-
valueGen |> Gen.list (AutoGenConfig.seqRange config) |> Gen.map Set.ofList
117+
static member Set<'a when 'a : comparison>(context: AutoGenContext, valueGen: Gen<'a>) : Gen<Set<'a>> =
118+
if context.CanRecurse then
119+
valueGen |> Gen.list context.CollectionRange |> Gen.map Set.ofList
120120
else
121121
Gen.constant Set.empty
122122

123-
static member Map<'k, 'v when 'k : comparison>(config: AutoGenConfig, recursionContext: RecursionContext, keyGen: Gen<'k>, valueGen: Gen<'v>) : Gen<Map<'k, 'v>> =
124-
if recursionContext.CanRecurse then
123+
static member Map<'k, 'v when 'k : comparison>(context: AutoGenContext, keyGen: Gen<'k>, valueGen: Gen<'v>) : Gen<Map<'k, 'v>> =
124+
if context.CanRecurse then
125125
gen {
126-
let! kvps = Gen.zip keyGen valueGen |> Gen.list (AutoGenConfig.seqRange config)
126+
let! kvps = Gen.zip keyGen valueGen |> Gen.list context.CollectionRange
127127
return Map.ofList kvps
128128
}
129129
else

0 commit comments

Comments
 (0)