Skip to content

feat(quickcheck): add char_range and zip/zip_with/zip_with3 generator combinators - #3977

Merged
bobzhang merged 3 commits into
mainfrom
hongbo/qc-generator-combinators
Aug 5, 2026
Merged

feat(quickcheck): add char_range and zip/zip_with/zip_with3 generator combinators#3977
bobzhang merged 3 commits into
mainfrom
hongbo/qc-generator-combinators

Conversation

@bobzhang

@bobzhang bobzhang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #3955, first of three scoped PRs from the feedback in #3955 (comment) (migrating bobzhang/toml's property tests to this package — moonbit-community/toml-parser#122).

  • char_range(lower, upper): characters in an inclusive range. Bounds are validated once at construction (non-empty, no surrogate code points), so the per-sample conversion uses safe to_char() — no unsafe_to_char.
  • zip / zip_with / zip_with3: applicative combination of two or three generators, named after Array's zip_with. Implemented via flat_map/map so they follow the existing state-splitting discipline.

Pure addition — no existing behavior or snapshot changes.

Test plan: new bounds/panic/snapshot tests in generator_test.mbt; moon test quickcheck 60/60; moon check --deny-warn, moon fmt, moon info clean.

🤖 Generated with Claude Code

… combinators

Migrating a real generator suite (bobzhang/toml's round-trip model) to
this package required re-deriving these from flat_map/map ~15 times:

- char_range generates characters in an inclusive range, validated at
  construction (non-empty, no surrogate code points) so the per-sample
  conversion needs no unsafe cast.
- zip / zip_with / zip_with3 combine generators applicatively, following
  the naming of Array's zip_with.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 5, 2026 09:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds new generator combinators and a character-range generator to the quickcheck package, extending the property-testing generator API with higher-level composition helpers and a Unicode-oriented primitive generator.

Changes:

  • Add Generator::zip, Generator::zip_with, and Generator::zip_with3 combinators built from existing flat_map/map primitives.
  • Add char_range(lower, upper) generator for inclusive character ranges with upfront validation.
  • Add unit/snapshot tests covering char_range behavior and the new zip combinators.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
quickcheck/pkg.generated.mbti Exposes char_range and Generator::{zip, zip_with, zip_with3} in the generated public interface.
quickcheck/generator.mbt Implements char_range and the new zip combinators on Generator.
quickcheck/generator_test.mbt Adds tests for char_range bounds/panic behavior and deterministic samples for zip combinators.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread quickcheck/generator.mbt Outdated
Comment on lines +164 to +172
guard lower <= upper else {
abort("char_range: lower bound exceeds upper bound")
}
guard upper.to_int() < 0xD800 || lower.to_int() > 0xDFFF else {
abort("char_range: range contains surrogate code points")
}
int_range(lower.to_int(), upper.to_int() + 1).map(code => {
code.to_char().unwrap()
})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in 4318770: bounds are now validated as Unicode scalar values (0..=0x10FFFF, non-surrogate), so an invalid Char produced via unsafe_to_char aborts at construction instead of panicking per sample.

Comment thread quickcheck/generator.mbt
/// The bounds must form a non-empty range of Unicode scalar values: this
/// function aborts if `lower` exceeds `upper` or if the range contains
/// surrogate code points, so every generated code point is a valid `Char`.
pub fn char_range(lower : Char, upper : Char) -> Generator[Char] {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

can we make char_range smarter to handle cases

lower .. 0xD800 0xDFFF..uper

instead of failing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 4318770 — a range spanning the surrogate block now draws from the contiguous index space of its valid scalars and shifts past the block, so char_range('\u{D000}', '\u{E800}') works and never emits a surrogate (covered by a new test asserting both sides of the block are reached).

Comment thread quickcheck/generator.mbt Outdated
guard upper.to_int() < 0xD800 || lower.to_int() > 0xDFFF else {
abort("char_range: range contains surrogate code points")
}
int_range(lower.to_int(), upper.to_int() + 1).map(code => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not reusing int_range?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It does reuse int_range — the draw is a single int_range over the collapsed index space (range width minus the surrogate gap), then a shift past the block in map. Kept that shape in the fix.

…unds as scalar values

Review feedback: a range spanning the surrogate block now draws from the
contiguous index space of its valid scalars (still a single int_range)
and shifts past the block, instead of failing. Bounds are validated as
Unicode scalar values so unsafe_to_char-produced invalid bounds abort at
construction rather than panicking per sample.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bobzhang
bobzhang requested a review from CAIMEOX August 5, 2026 10:08
@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 5766

Coverage increased (+0.001%) to 90.313%

Details

  • Coverage increased (+0.001%) from the base build.
  • Patch coverage: 1 uncovered change across 1 file (12 of 13 lines covered, 92.31%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
quickcheck/generator.mbt 13 12 92.31%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 17890
Covered Lines: 16157
Line Coverage: 90.31%
Coverage Strength: 169516.98 hits per line

💛 - Coveralls

@bobzhang
bobzhang merged commit 55266d7 into main Aug 5, 2026
15 checks passed
@bobzhang
bobzhang deleted the hongbo/qc-generator-combinators branch August 5, 2026 11:57
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.

4 participants