Automated Test-Case Reduction
cs.cornell.edu
cs.cornell.edu
Before leaving compiler team, I wanted to find a way to do this one the AST level (so parens always go in pairs, etc), but that could also complect with the bug.
I wonder if LLMs could accelerate this by more intelligently removing stuff first, iteratively?
Perses is a reducer that works on an AST and claims to only try syntactically valid candidate inputs. It also claims to be language agnostic, and I don't know how these two things go together. But it does work nicely for Java for me. https://github.com/uw-pluverse/perses / https://faculty.cc.gatech.edu/~qzhang414/papers/icse18_cheng...
Really there's no such thing as a language-agnostic test-case reducer. shrink ray is much closer than most, but all this means is that it's got some heuristics that work well for a wide variety of common languages (e.g. the bracket balancing thing). It's also got a bunch of language-specific passes.
This is sortof inherent to the problem, because in order to get good results and good performance, a test-case reducer has to have a strong idea of what sort of transformations are likely to work, which in turn means it has to have a strong idea of what sort of languages it's likely to be run on.
#define RET 42
int main(void){return RET;}
to int
main
(
void
)
{
return
42
;
}
and then randomly remove lines, recompile (if it is compile-able) and check the behaviour?1. Reduce the test data by property-based tests' auto shrinking a là QuickCheck [1]
2. Reduce the change set to a codebase, a là delta debugging [2]
The reducer is able to do both at the same time in a pretty generic way, with some restrictions on the expressivity. Seems to be a good fit for end-to-end tests.
Not only do you get shrinking 'for free', Hypothesis's shrinker is also smarter at finding opportunities to shrink.
My understanding is that Haskell’s QuickCheck can shrink functions, and with parametric polymorphism, can work with vector types and more [1], which would be difficult if not impossible in Python.
And in general, the base types covered by the shrinking in Python’s Hypothesis should have analogues by the shrinking in Haskell’s QuickCheck. And with deriving, Haskell’s QuickCheck should get shrinking ‘for free’ as in Python’s Hypothesis.
Also, Haskell’s QuickCheck extends to stateful and parallel checking, which is not supported in Python [2].
[1]: https://library.mlabs.city/mastering-quickcheck "Mastering QuickCheck: Advanced yet Practical Techniques for Property-Based Testing"
[2]: https://stevana.github.io/the_sad_state_of_property-based_te... "The sad state of property-based testing libraries: A survey of property-based testing libraries"
> My understanding is that Haskell’s QuickCheck can shrink functions [...]
Are you talking about shrinking a value of type (a -> b) (for some concrete a and b)? In Hypothesis, if you write a generator that spits out functions, you get a shrinker for free. So that shrinker will shrink functions.
As far as I can tell, QuickCheck's shrinking is based on values. I just checked, the type is still `shrink :: a -> [a]`, so that can't have changed.
In Hypothesis shrinking is based on the generation process. In Hypothesis generators act a bit like 'Functors' in Haskell, ie you can map over them. (You can also filter and `<>` and do monadic binds etc.)
So in Hypothesis you can take a generator that produces integers, and create one that produces only even numbers by (in Haskell terms) mapping (2) over it. Now, the shrinker you get for free will also only produce even numbers.
That's not possible (or at least not easily possible) in Haskell. You would have to define at least a newtype wrapper.
Have a look at https://hypothesis.works/articles/compositional-shrinking/ and https://hypothesis.works/articles/integrated-shrinking/ for some more write-up, especially about how Hypothesis can preserve information even when shrinking past a monadic bind.
If you follow some links in the two articles above, you land at https://github.com/icicle-lang/disorder.hs-ambiata/tree/mast... which is a Haskell library that shares a few design decisions with Hypothesis.
Apart from shrinking, Haskell's QuickCheck can also learn a lot from Hypothesis in terms of floating point number generation. The last time I used QuickCheck seriously, they basically generated floats by converting from a sort-of uniformly sampled fractional number. I see that this commit https://github.com/nick8325/quickcheck/commit/07942642d7987b... seems to have made float generation a lot smarter!
Hypothesis is especially 'nasty' when it generates floats. It has a good chunk of probability allocated to things like various NaNs, infinities, sub-normal numbers, zero, one float past zero, etc, plus of course some probability mass on uniformly chosen floats. See https://hypothesis.readthedocs.io/en/latest/data.html#hypoth... for the docs, but you should check out the code, too.
You are right that the generators in Hypothesis correspond to `Functor`s, so you can map over it. Indeed, in QuickCheck, its `Gen` [1] (type of `arbitrary` [2], the generator in QuickCheck) is a `Functor`, and moreover a `Monad`, so you can do monadic binds on `Gen`.
> So in Hypothesis you can take a generator that produces integers, and create one that produces only even numbers by (in Haskell terms) mapping (2) over it. Now, the shrinker you get for free will also only produce even numbers.
> That's not possible (or at least not easily possible) in Haskell. You would have to define at least a newtype wrapper.
Since `Gen` in QuickCheck is a `Functor`, to multiply its output by 2 to generate only even numbers, you can do this in Haskell as:
fmap (\x -> x*2) (arbitrary @Int)
Or, for those preferring OOP-style chaining: arbitrary @Int <&> (*2)
where `<&>` is `fmap` with arguments flipped, and `(*2) = \x -> x*2`, and `@Int` is type application to specialize `arbitrary` to generate `Int`.> Have a look at https://hypothesis.works/articles/compositional-shrinking/ and https://hypothesis.works/articles/integrated-shrinking/ for some more write-up, especially about how Hypothesis can preserve information even when shrinking past a monadic bind.
I think what you are hinting at is that, QuickCheck separates generation and shrinking, while Hypothesis combines generation and shrinking. If you prefer combining generation and shrinking, you may want to check out hedgehog in Haskell, see [3] for a discussion of this trade-off.
I think by using the `MonadGen` in hedgehog [4], you should be able to map over and bind over generators, with automatic shrinking, much like Hypothesis.
Hypothesis was first released in 2013, while hedgehog in 2017, so it is possible that hedgehog was inspired by Hypothesis or similar property-based testing libraries.
But in general, I would be surprised if such ‘functional’ APIs (map, filter, reduce, bind, etc.) could not be ported to Haskell.
[1]: https://hackage.haskell.org/package/QuickCheck-2.15.0.1/docs...
[2]: https://hackage.haskell.org/package/QuickCheck-2.15.0.1/docs...
[3]: https://tech.fpcomplete.com/blog/quickcheck-hedgehog-validit...
[4]: https://hackage.haskell.org/package/hedgehog-1.4/docs/Hedgeh...
I know, Arbitrary technically can't be a Monad, because the kinds are wrong. But I mean 'morally': you can fmap the generation process, but you can't fmap the shrinking. Exactly because generation and shrinking are separated; and shrinking is driven purely by the value and type of the generated item, but has no access to any information about the generation process.
> But in general, I would be surprised if such ‘functional’ APIs (map, filter, reduce, bind, etc.) could not be ported to Haskell.
Yes, have a look at the Jack library I already linked to.
Also, if you happen to be reducing an LLVM case you can use llvm-reduce [2].
https://pldi24.sigplan.org/home/egraphs-2024#program
Specifically, the EGSTRA paper.
Maybe it’s more critical when you’re dealing with “research-quality” code. This post seems to ignore the underlying problem: it’s rare to see research code with any tests.
Now when I see articles about things I find obvious on the front page — particularly if they’re topics I’ve seen hit the front page before (which, mind you, means “any time in the last 15 years or so”) — I have to resist an annoyed gut reaction. The fact that knowledge and skills seem obvious and boring once internalized is a kind of terrible quirk of the human condition.
We stored those discoveries in mongodb. Apparently it had failed in a web scale way and nobody has figured out how to restore from backup.
(To be clear, I don't agree with the sentiment of the comment to which you were replying, but I also don't think debugging "is research by definition".)
Honestly, I am a scientist first. I am not aware of any activity outside of academia that is more research than debugging. You start with an observed phenomenon (the bug). Then you search for the cause of it. Along the way you formulate and test hypothesis, conduct experiments, gather evidence, check the literature.
What's more: these activities are the bulk of what you do, especially in difficult cases. They are not incidental to debugging, they are the essence of the work.
Much of what is published is the outcome of processes that look a lot less like research.
[1] (E.g. I do research to win online argguments, or for a school essay, or a reporter does research for a story, or an engineer does research on how to implement some idea. None of these cases of research are academic/scientific research)