feat(quickcheck): companion property-testing package (RFC #115, Option C) - #116
Open
bobzhang wants to merge 2 commits into
Open
feat(quickcheck): companion property-testing package (RFC #115, Option C)#116bobzhang wants to merge 2 commits into
bobzhang wants to merge 2 commits into
Conversation
Add `bobzhang/toml/quickcheck`, a companion package that exposes reusable quickcheck generators and shrinkers for `TomlValue` without pulling quickcheck into the core parser's runtime dependency closure — parse-only consumers pay nothing; only importers of this package take the dependency. Because both the `Arbitrary` trait (moonbitlang/core/quickcheck) and `TomlValue` (bobzhang/toml) are foreign to this package, `impl Arbitrary for TomlValue` would violate the orphan rule. Instead we follow the library's own `modifiers` pattern and wrap the value in a local `RoundTrippable` newtype that carries the trait instances. Public API: - `roundtrippable_document() : @gen.Gen[TomlValue]` — generator for the round-trip-safe fragment (no floats / mixed arrays, so `==` is meaningful) - `shrink_document(doc) : Iter[TomlValue]` — matching shrinker - `round_trips(value) : Bool` — the law `parse(v.to_string()) == v` - `RoundTrippable` newtype with `Arbitrary` + `Shrink` impls Tests and README demonstrate both idioms: trait-driven `quick_check_fn` and explicit `forall_shrink`. Full workspace suite: 413 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `pub(all)` tuple struct already exposes its field via `.0`, matching how the quickcheck library's `modifiers` (Positive, NonEmptyArray, ...) are used. Removes redundant public API surface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Option C from the RFC in #115 — a companion package that exposes
reusable quickcheck support for
TomlValuewhile keeping the core parser freeof any quickcheck dependency. Parse-only consumers pay nothing; only importers
of
bobzhang/toml/quickchecktake onmoonbitlang/quickcheckand itstransitive weight.
The design constraint (why a newtype, not
impl Arbitrary for TomlValue)A companion package cannot implement
Arbitrary/Shrinkdirectly onTomlValue: both the traits (moonbitlang/core/quickcheck,moonbitlang/quickcheck/shrink) and the type (bobzhang/toml) are foreignto it, so the impl violates the orphan rule (compiler error 4061). Only core
itself (Option B) could add a bare impl — at the cost of pulling quickcheck into
core's runtime deps.
The idiomatic escape hatch is the same one
moonbitlang/quickcheck's ownmodifierspackage uses (Positive,NonEmptyArray, ...): wrap the value in alocal newtype that carries the trait instances.
Public API
The generated fragment covers every primitive, homogeneous arrays, nested
tables, and adversarial keys (quoted, dotted, and negative-exponent-like keys
that collide with the float grammar). It deliberately excludes floats and
mixed-type arrays so that exact
==after a round-trip is meaningful.Two idioms shown
Trait-driven —
quick_check_fnreads the generator/shrinker off the newtype:@qc.quick_check_fn(fn(doc : @quickcheck.RoundTrippable) { @quickcheck.round_trips(doc.inner()) }, max_success=100)Explicit generators — hand them to
forall_shrinkdirectly:@qc.quick_check(@qc.forall_shrink( @quickcheck.roundtrippable_document(), @quickcheck.shrink_document, fn(value) { @quickcheck.round_trips(value) }, ), max_success=100)Notes
RoundTrippablemirrors the internalqc_modelSimpleValueproxy, but as anewtype over the real
TomlValuerather than a parallel type + conversions— a possible path to retiring that proxy later (not done here).
rather than in a restricted type, so the fragment is a convention, not a
type-level guarantee. This is a fuzzing/law generator, not a full-
TomlValueArbitrary (floats/mixed arrays are out of scope by design).
Testing
doc-tests), 500 cases each in the test file.
moon checkclean (zero warnings),moon fmt,moon infocommitted.Closes the implementation side of #115 (the decision itself is still open there).
🤖 Generated with Claude Code