-
Notifications
You must be signed in to change notification settings - Fork 159
QuickCheck Mini (WIP) #3955
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
QuickCheck Mini (WIP) #3955
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
7bea43a
introduce shrink for common datatypes
CAIMEOX b122918
Merge branch 'main' into caimeo/qcmini
CAIMEOX 0cbfec4
improve List and Char shrink
CAIMEOX 0e92f5d
resolve dependency between Gen and QuickCheck root
CAIMEOX af0170e
Merge remote-tracking branch 'origin/main' into caimeo/qcmini
CAIMEOX 253ed70
rearrage shrink instances and fix shrink array
CAIMEOX d67b14b
simple quickcheck driver
CAIMEOX 42bb1b4
add elements and scale for Gen
CAIMEOX b55b796
support discard and filter
CAIMEOX a47dbac
Merge branch 'main' into caimeo/qcmini
CAIMEOX 94bcef2
Shrink instances for collections
CAIMEOX c8b535a
Use s table hash to bypass random Js seed
CAIMEOX 0fce2ef
tweak: move Gen to root Generator
CAIMEOX 150a8ce
Merge branch 'main' into caimeo/qcmini
CAIMEOX d106146
Merge branch 'main' into caimeo/qcmini
CAIMEOX 8b62e2f
remove arbitrary (can be easily derived from trait instance)
CAIMEOX File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| // Copyright 2026 International Digital Economy Academy | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| ///| | ||
| fn[T] removes(k : Int, n : Int, xs : List[T]) -> Iter[List[T]] { | ||
| guard k <= n else { [||] } | ||
| let xs_drop = xs.drop(k) | ||
| if xs_drop.is_empty() { | ||
| [||] | ||
| } else { | ||
| let xs_take = xs.take(k) | ||
| removes(k, n - k, xs_drop).map(x => xs_take.concat(x)).add([|xs_drop|]) | ||
|
CAIMEOX marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
|
||
| ///| | ||
| pub impl[T : @shrink.Shrink] @shrink.Shrink for List[T] with fn shrink(xs) { | ||
| let n = xs.length() | ||
| fn shr_sub_terms(lst : List[T]) { | ||
| match lst { | ||
| Empty => Iter::empty() | ||
| More(x, tail=xs) => | ||
| @shrink.Shrink::shrink(x) | ||
| .map(x_ => xs.add(x_)) | ||
| .concat(shr_sub_terms(xs).map(xs_ => xs_.add(x))) | ||
| } | ||
| } | ||
|
|
||
| [ | ||
| for k = n / 2; k > 0; k = k / 2 => k | ||
| ] | ||
| .rev_iter() | ||
| .flat_map(k => removes(k, n, xs)) | ||
| .concat(shr_sub_terms(xs)) | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| // Copyright 2026 International Digital Economy Academy | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| ///| | ||
| pub using @splitmix {type RandomState} | ||
|
|
||
| ///| | ||
| /// A size-aware random value generator. | ||
| struct Gen[T] { | ||
| generate : (Int, RandomState) -> T | ||
| } | ||
|
|
||
| ///| | ||
| /// Creates a generator from a function of size and random state. | ||
| pub fn[T] Gen::new(generate : (Int, RandomState) -> T) -> Gen[T] { | ||
| { generate, } | ||
| } | ||
|
|
||
| ///| | ||
| /// Runs a generator with an explicit size and random state. | ||
| pub fn[T] Gen::run(self : Gen[T], size : Int, state : RandomState) -> T { | ||
| (self.generate)(size, state) | ||
| } | ||
|
|
||
| ///| | ||
| /// Generates one deterministic sample. | ||
| pub fn[T] Gen::sample( | ||
| self : Gen[T], | ||
| size? : Int = 100, | ||
| seed? : UInt64 = 37, | ||
| ) -> T { | ||
| self.run(size, @splitmix.new(seed~)) | ||
| } | ||
|
|
||
| ///| | ||
| /// Generates several deterministic samples from one random state. | ||
| pub fn[T] Gen::samples( | ||
| self : Gen[T], | ||
| count? : Int = 10, | ||
| size? : Int = 100, | ||
| seed? : UInt64 = 37, | ||
| ) -> Array[T] { | ||
| let state = @splitmix.new(seed~) | ||
| Array::makei(count, _ => self.run(size, state)) | ||
| } | ||
|
|
||
| ///| | ||
| /// Creates a generator that always returns `value`. | ||
| pub fn[T] pure(value : T) -> Gen[T] { | ||
| Gen::new((_, _) => value) | ||
| } | ||
|
|
||
| ///| | ||
| /// Transforms the output of a generator. | ||
| pub fn[T, U] Gen::map(self : Gen[T], transform : (T) -> U) -> Gen[U] { | ||
| Gen::new((size, state) => transform(self.run(size, state))) | ||
| } | ||
|
|
||
| ///| | ||
| /// Sequences a generator with a generator-producing function. | ||
| pub fn[T, U] Gen::flat_map(self : Gen[T], transform : (T) -> Gen[U]) -> Gen[U] { | ||
| Gen::new((size, state) => { | ||
| let next_state = state.split() | ||
| transform(self.run(size, state)).run(size, next_state) | ||
| }) | ||
| } | ||
|
|
||
| ///| | ||
| /// Creates a generator that can inspect the current size. | ||
| pub fn[T] sized(create : (Int) -> Gen[T]) -> Gen[T] { | ||
| Gen::new((size, state) => create(size).run(size, state)) | ||
| } | ||
|
|
||
| ///| | ||
| /// Runs a generator with a fixed size. | ||
| pub fn[T] Gen::resize(self : Gen[T], size : Int) -> Gen[T] { | ||
| Gen::new((_, state) => self.run(size, state)) | ||
| } | ||
|
|
||
| ///| | ||
| /// Creates a generator from an `Arbitrary` implementation. | ||
| pub fn[T : @quickcheck.Arbitrary] arbitrary() -> Gen[T] { | ||
| Gen::new((size, state) => @quickcheck.Arbitrary::arbitrary(size, state)) | ||
| } | ||
|
|
||
| ///| | ||
| /// Generates an integer in the half-open interval `[lower, upper)`. | ||
| pub fn int_range(lower : Int, upper : Int) -> Gen[Int] { | ||
| guard lower < upper else { | ||
| if lower == upper { | ||
| return pure(lower) | ||
| } | ||
| abort("int_range: lower bound exceeds upper bound") | ||
| } | ||
| let width = (upper - lower).reinterpret_as_uint() | ||
| Gen::new((_, state) => { | ||
| (state.next_uint() % width).reinterpret_as_int() + lower | ||
| }) | ||
| } | ||
|
|
||
| ///| | ||
| /// Randomly selects one of the supplied generators. | ||
| pub fn[T] one_of(generators : Array[Gen[T]]) -> Gen[T] { | ||
| guard !generators.is_empty() else { abort("one_of: empty array") } | ||
| int_range(0, generators.length()).flat_map(index => generators[index]) | ||
| } | ||
|
|
||
| ///| | ||
| /// Randomly selects a generator according to its weight. | ||
| pub fn[T] frequency(generators : Array[(UInt, Gen[T])]) -> Gen[T] { | ||
| guard !generators.is_empty() else { abort("frequency: empty array") } | ||
| let total = for pair in generators; total = 0U { | ||
| let (weight, _) = pair | ||
| continue total + weight | ||
| } nobreak { | ||
| total | ||
| } | ||
| guard total > 0 else { abort("frequency: total weight is zero") } | ||
| Gen::new((size, state) => { | ||
| let choice = state.next_uint() % total | ||
| for pair in generators; remaining = choice { | ||
| let (weight, generator) = pair | ||
| if remaining < weight { | ||
| break generator.run(size, state) | ||
| } | ||
| continue remaining - weight | ||
| } nobreak { | ||
| abort("frequency: invalid weights") | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| ///| | ||
| /// Generates an array with exactly `size` independently drawn elements. | ||
| pub fn[T] Gen::array_with_size(self : Gen[T], size : Int) -> Gen[Array[T]] { | ||
| guard size >= 0 else { abort("array_with_size: negative size") } | ||
| Gen::new((sample_size, state) => { | ||
| Array::makei(size, _ => self.run(sample_size, state)) | ||
| }) | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Copyright 2026 International Digital Economy Academy | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| ///| | ||
| test "construct, map, and flat_map" { | ||
| let from_size = @gen.Gen::new((size, _) => size) | ||
| inspect(from_size.sample(size=12), content="12") | ||
| let value = @gen.pure(20).map(x => x + 1).flat_map(x => @gen.pure(x * 2)) | ||
| inspect(value.sample(), content="42") | ||
| } | ||
|
|
||
| ///| | ||
| test "size control" { | ||
| let generator = @gen.sized(size => @gen.pure(size)) | ||
| inspect(generator.sample(size=9), content="9") | ||
| inspect(generator.resize(3).sample(size=9), content="3") | ||
| } | ||
|
|
||
| ///| | ||
| test "arbitrary generator" { | ||
| let value : Unit = @gen.arbitrary().sample() | ||
| inspect(value, content="()") | ||
| } | ||
|
|
||
| ///| | ||
| test "range and choices" { | ||
| for value in @gen.int_range(-4, 7).samples(count=64) { | ||
| guard value >= -4 && value < 7 else { | ||
| fail("int_range generated an out-of-range value") | ||
| } | ||
| } | ||
| for value in @gen.one_of([@gen.pure(1), @gen.pure(2)]).samples(count=32) { | ||
| guard value == 1 || value == 2 else { | ||
| fail("one_of generated a value outside its inputs") | ||
| } | ||
| } | ||
| for | ||
| value in @gen.frequency([(1U, @gen.pure("a")), (3U, @gen.pure("b"))]).samples( | ||
| count=32, | ||
| ) { | ||
| guard value == "a" || value == "b" else { | ||
| fail("frequency generated a value outside its inputs") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ///| | ||
| test "fixed-size array" { | ||
| let values = @gen.pure(7).array_with_size(3).sample() | ||
| inspect(values.length(), content="3") | ||
| inspect(values.all(value => value == 7), content="true") | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { | ||
| "moonbitlang/core/quickcheck", | ||
| "moonbitlang/core/quickcheck/splitmix", | ||
| "moonbitlang/core/builtin", | ||
| "moonbitlang/core/array", | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| // Generated using `moon info`, DON'T EDIT IT | ||
| package "moonbitlang/core/quickcheck/gen" | ||
|
|
||
| import { | ||
| "moonbitlang/core/quickcheck", | ||
| "moonbitlang/core/quickcheck/splitmix", | ||
| } | ||
|
|
||
| // Values | ||
| pub fn[T : @quickcheck.Arbitrary] arbitrary() -> Gen[T] | ||
|
|
||
| pub fn[T] frequency(Array[(UInt, Gen[T])]) -> Gen[T] | ||
|
|
||
| pub fn int_range(Int, Int) -> Gen[Int] | ||
|
|
||
| pub fn[T] one_of(Array[Gen[T]]) -> Gen[T] | ||
|
|
||
| pub fn[T] pure(T) -> Gen[T] | ||
|
|
||
| pub fn[T] sized((Int) -> Gen[T]) -> Gen[T] | ||
|
|
||
| // Errors | ||
|
|
||
| // Types and methods | ||
| type Gen[T] | ||
| pub fn[T] Gen::array_with_size(Self[T], Int) -> Self[Array[T]] | ||
| pub fn[T, U] Gen::flat_map(Self[T], (T) -> Self[U]) -> Self[U] | ||
| pub fn[T, U] Gen::map(Self[T], (T) -> U) -> Self[U] | ||
| pub fn[T] Gen::new((Int, @splitmix.RandomState) -> T) -> Self[T] | ||
|
CAIMEOX marked this conversation as resolved.
Outdated
|
||
| pub fn[T] Gen::resize(Self[T], Int) -> Self[T] | ||
| pub fn[T] Gen::run(Self[T], Int, @splitmix.RandomState) -> T | ||
| pub fn[T] Gen::sample(Self[T], size? : Int, seed? : UInt64) -> T | ||
| pub fn[T] Gen::samples(Self[T], count? : Int, size? : Int, seed? : UInt64) -> Array[T] | ||
|
|
||
| // Type aliases | ||
| pub using @splitmix {type RandomState} | ||
|
|
||
| // Traits | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.