You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Support generating indirectly recursive records consistently #734
FsCheck currently has inconsistent support for generating indirectly recursive records.
If a record type is immediately recursive, FsCheck raises an exception. No problem; that makes sense.
#r "nuget: FsCheck,3.3.2"openFsCheck.FSharptypeR={ R :R }
ArbMap.defaults
|> ArbMap.generate<R>|> Gen.sample 3// System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.// ---> System.Exception: Recursive record types cannot be generated automatically: …
But if the record type is not immediately recursive, the user experience is not ideal: the user either sees a stack overflow exception, or, if FsCheck is being run via another test runner (xUnit, NUnit), the test seems simply not to run at all, because the test process crashes.
#r "nuget: FsCheck,3.3.2"openFsCheck.FSharptypeR={ U :U }andU= U ofR| V ofint
ArbMap.defaults
|> ArbMap.generate<R>|> Gen.sample 3// [|{ U = V -10 }; { U = U { U = U { U = U { U = V 27 } } } }; { U = V 8 }|]
Is there a good reason for FsCheck not to apply size-control logic similar to what it already does for mutually-recursive unions to all indirectly-recursive records by default, instead of just blowing the stack and forcing the user to write a custom generator? (There could well be a good reason. That's why I'm asking.)
We can extend the latter to fail also for indirectly recursive record types, but that would also block your last use case.
Feels like an advanced case that's hard to handle sensibly on the FsCheck level - I don't feel too bad about making people write a custom generator if they're playing with mutually recursive records/unions :)
Activity
kurtschelfthout commentedon Apr 26, 2026
I'm guessing it's because
listis treated like anarrayor tuple type of arbitrary length, and so its size doesn't decrease fast enough to "overtake" the stack overflow. In the reflective generator for other discriminated unions there is an explicit check for recursive cases https://github.com/fscheck/FsCheck/blob/master/src/FsCheck/Internals.ReflectiveGenerator.fs#L104 which makes it more unlikely they are picked. There is also an explicit check for directly recursive record types https://github.com/fscheck/FsCheck/blob/master/src/FsCheck/Internals.ReflectiveGenerator.fs#L124We can extend the latter to fail also for indirectly recursive record types, but that would also block your last use case.
Feels like an advanced case that's hard to handle sensibly on the FsCheck level - I don't feel too bad about making people write a custom generator if they're playing with mutually recursive records/unions :)
kurtschelfthout commentedon Oct 21, 2026
Specifying the distributor like in #444 would help with this. Likely needs a breaking change.