Skip to content

feat(quickcheck): companion property-testing package (RFC #115, Option C) - #116

Open
bobzhang wants to merge 2 commits into
mainfrom
feat/quickcheck-companion
Open

feat(quickcheck): companion property-testing package (RFC #115, Option C)#116
bobzhang wants to merge 2 commits into
mainfrom
feat/quickcheck-companion

Conversation

@bobzhang

Copy link
Copy Markdown
Collaborator

Implements Option C from the RFC in #115 — a companion package that exposes
reusable quickcheck support for TomlValue while keeping the core parser free
of any quickcheck dependency. Parse-only consumers pay nothing; only importers
of bobzhang/toml/quickcheck take on moonbitlang/quickcheck and its
transitive weight.

The design constraint (why a newtype, not impl Arbitrary for TomlValue)

A companion package cannot implement Arbitrary/Shrink directly on
TomlValue: both the traits (moonbitlang/core/quickcheck,
moonbitlang/quickcheck/shrink) and the type (bobzhang/toml) are foreign
to 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 own
modifiers package uses (Positive, NonEmptyArray, ...): wrap the value in a
local newtype that carries the trait instances.

Public API

pub fn roundtrippable_document() -> @gen.Gen[@toml.TomlValue]  // safe fragment
pub fn shrink_document(@toml.TomlValue) -> Iter[@toml.TomlValue]
pub fn round_trips(@toml.TomlValue) -> Bool                    // parse(v.to_string()) == v
pub(all) struct RoundTrippable(@toml.TomlValue)                // + Arbitrary + Shrink impls

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-drivenquick_check_fn reads 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_shrink directly:

@qc.quick_check(@qc.forall_shrink(
  @quickcheck.roundtrippable_document(),
  @quickcheck.shrink_document,
  fn(value) { @quickcheck.round_trips(value) },
), max_success=100)

Notes

  • RoundTrippable mirrors the internal qc_model SimpleValue proxy, but as a
    newtype over the real TomlValue rather than a parallel type + conversions
    — a possible path to retiring that proxy later (not done here).
  • Trade-off to keep in mind: round-trip safety now lives in the generator
    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-TomlValue
    Arbitrary (floats/mixed arrays are out of scope by design).

Testing

  • New package: 4 tests (trait-driven + explicit idioms, plus two README
    doc-tests), 500 cases each in the test file.
  • moon check clean (zero warnings), moon fmt, moon info committed.
  • Full workspace suite: 413 passed.

Closes the implementation side of #115 (the decision itself is still open there).

🤖 Generated with Claude Code

bobzhang and others added 2 commits July 24, 2026 16:20
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>
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.

1 participant