Skip to content

Conversation

@AlexeyRaga
Copy link
Collaborator

Precondition and Require wasn't properly checked while shrinking

Copy link
Member

@moodmosaic moodmosaic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Left one comment/question.

// This is used during GENERATION (not execution) to predict what the state will be
// so we can generate subsequent actions that are valid in that predicted state.
// Only checks Precondition (not Require) because we don't have actual execution values yet.
let projectState (actions: Action<'TSystem, 'TState> list) (initialState: 'TState) : 'TState =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This skips actions that fail Precondition but continues with the previous state. Is that correct? Or should it filter them out entirely during generation? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is the intent.
We just skip actions whose precondition says "cannot execute in this state".
It is not only generation, but also shrinking that is at play... The intent is that when an action is removed from an already generated valid sequence (during shrinking), we still need to validate if the action still makes sense to execute.

This is what Haskell version does, too, and this is why Update in Haskell is polymorphic in kind (concrete, symbolic).

I am trying to simplify the story around calling Update a bit, not very successfully so far, but I am still hopeful :)

Copy link
Member

@moodmosaic moodmosaic Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find fast-check's approach way more pragmatic:

A commands is defined for each one of the public functions of the SUT. They come with two methods:

  • check(m: Readonly<Model>): boolean to allow or not allow the command to be executed given the current state.
  • run(m: Model, r: Real): void to execute the command on the SUT and update the model accordingly.
    • Also checks for potential problems or inconsistencies between the model and the SUT.
  • toString() used for shrinking/labeling

This doesn't even need a special library, it can be a shuffle of command-array objects! :)


I know it sounds "too simple" but using this simple model I've done large-scale, consensus-critical, model-based testing when I was at the Stacks Foundation for PoX-4 and SIP-031 boot (core, protocol, consensus) contracts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

@AlexeyRaga AlexeyRaga Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the execution model there is fairly different from what Hedgehog does.
HH is two-stages: generation and execution. Plus automatic shrinking complicates things a lot.

For check we have Precondition and for run we have Execute. Which is the same.

But because HH has two stages: generation (with integrated shrinking trees, etc.) and execution, we have these two checks: symbolically first, and concrete later.

Here I am modeling against Haskell Hedgehog 🤷 and in my understanding the gen-time precondition is needed to only generate sequences that make sense before executing them.

I haven't checked, but fast-check probably cannot do it. It will probably interleave generation and execution, trying to generate next command?
Or will it generate invalid sequences to only realise it at the execution time?
This is not how HH works with its integrated shrinking 🤷‍♂️, different idea.

I guess that one can try building completely random sequences of actions and try executing them one-by-one while only checking at runtime for the validity of the next action...
Not sure how it matches HH idea (and implementation), but, again, here I am modeling after Haskell's HH Stateful.

@AlexeyRaga AlexeyRaga force-pushed the check-preconditions-harder branch from c02f1cf to fe72289 Compare January 7, 2026 11:34
Copy link
Member

@moodmosaic moodmosaic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really appreciate all the thought you're putting into this. Let me share some perspective that might be helpful.

I haven't done large-scale, consensus-critical, model-based testing with HH at the scale I've used fast-check's approach. I've used HH in projects, but not at that level. If you can point me to a codebase using HH's model-based testing at large scale, I'd love to see it and might be able to tell more. If you've personally used HH's model-based testing at large scale, that's more than enough validation!

But if you haven't (and are porting it here to use in .NET based on the design), I'd like to ask us to take a collective step back. I'm concerned we might be adding unnecessary complexity for no proven reason.

fast-check's approach is pragmatic and simple. fsharp-hedgehog doesn't necessarily need to replicate HH's approach 1:1 — we can take the best ideas from multiple ecosystems.

I've been using fast-check against PoX-4 (Proof of Transfer v4, the Stacks blockchain's consensus mechanism) and SIP-031 (Stacks Improvement Proposal for signer key rotation) when I was at Stacks Foundation. These are consensus-critical, production blockchain contracts. The efforts can be seen here:

I am not proposing or favoring JS/TS or fast-check over Haskell/Hedgehog/.NET/x/y/z. All I am saying is I'd rather port something I know works in practice and at scale, rather than porting something because "that's how it's done there in HH".

The two-stage generation/execution model with symbolic/concrete distinction adds real complexity. What concrete benefits does it provide that justify that complexity? Especially if we don't have evidence of it being battle-tested at scale?

@AlexeyRaga
Copy link
Collaborator Author

OK, no worries.
Since Hedgehog.Stateful is in beta stage, very deliberately, let's regroup and delete it from this repo.
We can think about another approach, and in a meantime nothing prevents me from having it as a personal package.
I am very interested implementing Hedgehog-like approach, but I can do it separately :)

I'm taking down this PR and will make another one removing Hedgehog.Stateful from here.

@AlexeyRaga AlexeyRaga closed this Jan 8, 2026
@AlexeyRaga
Copy link
Collaborator Author

As for reasoning: I value Hedgehog for its very efficient integrated shrinking.
I think that it is the main idea behind Hedgehog in the first place, and if not that, then I could use any other framework, like FSCheck or said fast-check.

I believe that it is possible to implement fast-check way on top of Hedgehog.FSharp, but I cannot see how efficient shrinking can be preserved under that model.
Not that I want complicated API per se, it is a trade-off to keep this big idea working consistently instead of saying "oh, in this corner of Hedgehog we give up and make a different choice".

I mean, shrinking can still be done, but much, much less efficiently.
I'll be happy to stand corrected on this though! :)

In my line of work (testing APIs) efficient shrinking is important: it is network, time and load on SUT that matters, this is why Hedgehog idea of efficient integrated shrinking motivates me :)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants