Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions list/extends.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ pub extend List with @debug.Debug::{to_repr}
#doc(hidden)
pub extend List with @quickcheck.Arbitrary::{arbitrary}

///|
#deprecated("Use `@shrink.Shrink::shrink` instead", skip_current_package=true)
#doc(hidden)
pub extend List with @shrink.Shrink::{shrink}

///|
#deprecated("Use the comparison operators (`<`, `<=`, `>=`, `>`) instead", skip_current_package=true)
#doc(hidden)
Expand Down
33 changes: 33 additions & 0 deletions list/list_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -1204,3 +1204,36 @@ test "List default trait" {
let ls : @list.List[Int] = Default::default()
debug_inspect(ls, content="<List: []>")
}

///|
test "shrink int list" {
let il : @list.List[Int] = List([1, 2, 3, 4, 5, 6])
debug_inspect(
@shrink.Shrink::shrink(il).collect(),
content=(
#|[
#| <List: [1, 2, 3, 4, 6]>,
Comment thread
CAIMEOX marked this conversation as resolved.
Outdated
#| <List: [1, 2, 3, 5, 6]>,
#| <List: [1, 2, 4, 5, 6]>,
#| <List: [1, 3, 4, 5, 6]>,
#| <List: [2, 3, 4, 5, 6]>,
#| <List: [4, 5, 6]>,
#| <List: [0, 2, 3, 4, 5, 6]>,
#| <List: [1, 1, 3, 4, 5, 6]>,
#| <List: [1, 0, 3, 4, 5, 6]>,
#| <List: [1, 2, 2, 4, 5, 6]>,
#| <List: [1, 2, 0, 4, 5, 6]>,
#| <List: [1, 2, 3, 3, 5, 6]>,
#| <List: [1, 2, 3, 2, 5, 6]>,
#| <List: [1, 2, 3, 0, 5, 6]>,
#| <List: [1, 2, 3, 4, 4, 6]>,
#| <List: [1, 2, 3, 4, 3, 6]>,
#| <List: [1, 2, 3, 4, 0, 6]>,
#| <List: [1, 2, 3, 4, 5, 5]>,
#| <List: [1, 2, 3, 4, 5, 3]>,
#| <List: [1, 2, 3, 4, 5, 0]>,
#|]

),
)
}
1 change: 1 addition & 0 deletions list/moon.pkg
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
"moonbitlang/core/array",
"moonbitlang/core/debug",
"moonbitlang/core/quickcheck",
"moonbitlang/core/quickcheck/shrink",
"moonbitlang/core/json",
}

Expand Down
2 changes: 2 additions & 0 deletions list/pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
"moonbitlang/core/debug",
"moonbitlang/core/json",
"moonbitlang/core/quickcheck",
"moonbitlang/core/quickcheck/shrink",
"moonbitlang/core/quickcheck/splitmix",
}

Expand Down Expand Up @@ -122,6 +123,7 @@ pub impl[A : ToJson] ToJson for List[A]
pub impl[A : @debug.Debug] @debug.Debug for List[A]
pub impl[A : @json.FromJson] @json.FromJson for List[A]
pub impl[X : @quickcheck.Arbitrary] @quickcheck.Arbitrary for List[X]
pub impl[T : @shrink.Shrink] @shrink.Shrink for List[T]

// Type aliases

Expand Down
46 changes: 46 additions & 0 deletions list/shrink.mbt
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|])
Comment thread
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))
}
151 changes: 151 additions & 0 deletions quickcheck/gen/gen.mbt
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))
})
}
63 changes: 63 additions & 0 deletions quickcheck/gen/gen_test.mbt
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")
}
6 changes: 6 additions & 0 deletions quickcheck/gen/moon.pkg
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",
}
39 changes: 39 additions & 0 deletions quickcheck/gen/pkg.generated.mbti
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]
Comment thread
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

Loading
Loading