Skip to content

Support generating indirectly recursive records consistently #734

Open

Description

@brianrourkeboll

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"

open FsCheck.FSharp

type R = { 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"

open FsCheck.FSharp

type R = { Rs : R list }

ArbMap.defaults
|> ArbMap.generate<R>
|> Gen.sample 3
// Stack overflow 😭.

FsCheck actually does support generating indirectly recursive records in one scenario: the record is recursive through a mutually-recursive multi-case union:

#r "nuget: FsCheck,3.3.2"

open FsCheck.FSharp

type R = { U : U }
and U = U of R | V of int

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.)

Activity

  1. kurtschelfthout commented on Apr 26, 2026

    @kurtschelfthout
    Member

    I'm guessing it's because list is treated like an array or 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#L124

    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 :)

  2. kurtschelfthout commented on Oct 21, 2026

    @kurtschelfthout
    Member

    Specifying the distributor like in #444 would help with this. Likely needs a breaking change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions