Size notion. In this README "size" means whatever the Enumerable instance chooses — usually one pay per constructor, but a user-defined instance can charge differently. The driver only relies on: each part is finite, parts are ordered by increasing size, and recursion is productive (guaranteed by pay).
This package is a building block for MoonBit QuickCheck. It backs the "small check" mode (exhaustive testing for small sizes) and is independently useful whenever you want a deterministic, size-indexed view of a type.
flowchart LR
T["Type T"] --> E["Enumerate[T]<br/>LazyList of Finite[T]"]
E --> P0["part₀ : Finite[T]<br/>(size-0 values)"]
E --> P1["part₁ : Finite[T]<br/>(size-1 values)"]
E --> P2["part₂ : Finite[T]<br/>(size-2 values)"]
E --> Pdots["…"]
P2 --> Card["fCard : BigInt<br/>(count)"]
P2 --> Idx["fIndex : BigInt → T<br/>(selector)"]moon add moonbitlang/quickcheck{
"import": [
{ "path": "moonbitlang/quickcheck/feat", "alias": "feat" }
]
}///|
pub(all) struct Finite[T] {
fCard : BigInt
fIndex : (BigInt) -> T
}///|
test "hand-rolled finite chunk" {
let f : @feat.Finite[String] = {
fCard: 2,
fIndex: i => {
if i == 0 {
"left"
} else {
guard i == 1 else { abort("index out of bounds") }
"right"
}
},
}
debug_inspect(
f.to_array(),
content=(
#|(2, <List: ["left", "right"]>)
),
)
}///|
test "disjoint union of two singleton parts" {
let left = @feat.singleton("left").eval().head()
let right = @feat.singleton("right").eval().head()
let joined = left + right
debug_inspect(
joined.to_array(),
content=(
#|(2, <List: ["left", "right"]>)
),
)
}///|
test "for x in finite" {
let acc : Array[BigInt] = []
let finite : @feat.Finite[BigInt] = { fCard: 4, fIndex: i => i }
for x in finite {
acc.push(x)
}
@debug.assert_eq(acc, [0, 1, 2, 3])
}///|
pub(all) struct Enumerate[T] {
parts : LazyList[Finite[T]]
}///|
test "singleton has size 0" {
let e = @feat.singleton(42)
let parts = e.eval()
// The first (and only) part holds the single value.
debug_inspect(
parts.head().to_array(),
content=(
#|(1, <List: [42]>)
),
)
}
///|
test "pay shifts everything one size up" {
// Before pay: part₀ = {42}
// After pay: part₀ = {}, part₁ = {42}
let shifted = @feat.pay(() => @feat.singleton(42))
let parts = shifted.eval()
debug_inspect(
parts.head().to_array(),
content=(
#|(0, <List: []>)
),
)
debug_inspect(
parts.tail().head().to_array(),
content=(
#|(1, <List: [42]>)
),
)
}This is the main user-facing trait of the package. Implement Enumerable for a type T and you get: indexed access (enumerate()[i]), size-bounded sampling via feat_random, and deterministic prefix testing via small_check. Everything else in this package either consumes or produces an Enumerable.
///|
pub(open) trait Enumerable {
enumerate() -> Enumerate[Self]
}| # | Property | What breaks if you violate it |
|---|---|---|
| 1 | Cardinalities are non-negative. Every part has fCard : BigInt and must satisfy fCard >= 0. Parts with fCard == 0 are empty and collapse away. | The package assumes every part behaves like a finite set; a negative card produced by hand breaks that model and will lead to wrong indexing behaviour. |
| 2 | Productivity under recursion. Every recursive self-reference inside an Enumerable instance must be guarded by a pay(...). An Enumerate[T] is a LazyList of parts, so it may be infinite — but each part must be reachable in finite time. | Evaluating enumerate() blows the stack or loops forever. |
| 3 | Total indexing per part. For every i with 0 <= i < part.fCard, part.fIndex(i) must return a valid T. | Indexing aborts with "index out of bounds". |
| Type | Shape | Notes |
|---|---|---|
| Unit | singleton(()) | Size 0, card 1. |
| Bool | pay(true + false) | Size 1, card 2 (inside pay). |
| Byte | flat Finite of card 256 | Size 0; indexes 0..255 directly. |
| Char | flat Finite of card 1,112,064 | Size 0; skips the UTF-16 surrogate range. |
| Int / Int64 | interleaved 0, 1, -1, 2, -2, ... | Size 0 through infinity; infinite parts each of card 1. |
| UInt / UInt64 | 0, 1, 2, ... with pay per step | Each successor costs one unit of size. |
| Option[E] where E : Enumerable | pay(None + E::enumerate().fmap(Some)) | None at size 1, Some(x) at 1 + size(x). |
| Result[T, E] where T, E : Enumerable | pay(Err + Ok) | Both arms cost one pay. |
| List[E] where E : Enumerable | pay(empty + Cons(e, lst)) | Each cons cell costs one pay. |
| (A, B) where A, B : Enumerable | pay(product(A::enumerate(), B::enumerate())) | Size = 1 + sum of component sizes. |
flowchart TD
UserType["your recursive type T"] --> Constructors["one branch per constructor"]
Constructors --> Leaf["Leaf constructor:<br/>singleton(value)"]
Constructors --> Recursive["Recursive constructor:<br/>unary or product"]
Leaf --> Union["+ (union)"]
Recursive --> Union
Union --> Pay["pay(() => …)"]
Pay --> Result["Enumerate[T]"]///|
enum Tree {
Leaf
Node(Tree, Tree)
}
///|
impl @feat.Enumerable for Tree with enumerate() {
// One size unit per constructor; the recursive children are reached via
// `unary`, which goes through the built-in `Enumerable` instance for
// `(Tree, Tree)`. That instance itself inserts a `pay`, which is what keeps
// the fixpoint productive.
@feat.pay(() => {
@feat.singleton(Leaf) +
@feat.unary((pair : (Tree, Tree)) => Node(pair.0, pair.1))
})
}
///|
impl Show for Tree with output(self, logger) {
match self {
Leaf => logger.write_string("Leaf")
Node(l, r) => {
logger.write_string("Node(")
l.output(logger)
logger.write_string(", ")
r.output(logger)
logger.write_string(")")
}
}
}
///|
test "enumerate the first few binary trees" {
let trees : @feat.Enumerate[Tree] = Enumerable::enumerate()
inspect(trees[0], content="Leaf")
inspect(trees[1], content="Node(Leaf, Leaf)")
}| Pitfall | Why it hurts | Fix |
|---|---|---|
| Unguarded self-reference: T::enumerate() called without a surrounding pay. | Recursion diverges (contract #2). | Wrap the body in @feat.pay(() => ...). Going through unary + the pair instance achieves the same because the built-in (A, B) instance inserts its own pay. |
| Computing a large Cartesian product with product before unioning. | Not wrong, just slow — the resulting parts get large and indexing locality suffers. | Prefer unary for a single-pair constructor, or consts([...]) for a disjunction — these keep the structure flat. |
| Mixing eager List of Enumerate with consts at the top of a recursive definition. | The List itself is eager: its elements are forced when the consts is reached, which can run into the recursion before pay kicks in. | Ensure the consts(...) is inside pay, or use + between lazy Enumerates. |
| Forgetting that zero-cardinality parts short-circuit. | Empty parts are skipped cheaply, but a hand-rolled Finite with a bogus non-zero fCard will still be indexed. | If you need an empty part, set fCard: 0 and use an aborting fIndex. |
///|
test "index into Bool's enumeration" {
// Enumerable::enumerate() for Bool yields [true, false] (inside pay).
let e : @feat.Enumerate[Bool] = Enumerable::enumerate()
inspect(e[0], content="true")
inspect(e[1], content="false")
}
///|
test "index into a list enumeration" {
let e : @feat.Enumerate[@list.List[Bool]] = Enumerable::enumerate()
// Sizes grow as more cons cells are added.
debug_inspect(
e[0],
content=(
#|<List: []>
),
)
debug_inspect(
e[1],
content=(
#|<List: [true]>
),
)
debug_inspect(
e[2],
content=(
#|<List: [false]>
),
)
}///|
test "materialize every Bool at size 1" {
let parts = (Enumerable::enumerate() : @feat.Enumerate[Bool]).eval()
// part 0 is empty (Bool is defined with a pay).
debug_inspect(
parts.head().to_array(),
content=(
#|(0, <List: []>)
),
)
// part 1 holds both booleans.
debug_inspect(
parts.tail().head().to_array(),
content=(
#|(2, <List: [true, false]>)
),
)
}| Operation | Signature | Meaning |
|---|---|---|
| Enumerate::fmap(e, f) | Enumerate[T] -> (T -> U) -> Enumerate[U] | Re-label every element |
| e1 + e2 | Enumerate[T] -> Enumerate[T] -> Enumerate[T] | Interleave by size |
| product(e1, e2) | Enumerate[A] -> Enumerate[B] -> Enumerate[(A, B)] | Pair every A with every B, still size-indexed |
| pay(() => …) | (() -> Enumerate[T]) -> Enumerate[T] | Charge 1 unit of size |
| unary(f) | (T -> U) -> Enumerate[U] where T : Enumerable | Shortcut for T::enumerate().fmap(f) |
///|
test "fmap rewrites every element in place" {
let bools : @feat.Enumerate[Bool] = Enumerable::enumerate()
let labels = bools.fmap(b => if b { "yes" } else { "no" })
assert_eq(labels[0], "yes")
assert_eq(labels[1], "no")
}
///|
test "product generates every pair in part order" {
let pairs = @feat.product(zero_or_one_part(), zero_or_one_part())
debug_inspect(pairs[0], content="(0, 0)")
debug_inspect(pairs[1], content="(0, 1)")
debug_inspect(pairs[2], content="(1, 0)")
debug_inspect(pairs[3], content="(1, 1)")
}
///|
fn zero_or_one_part() -> @feat.Enumerate[BigInt] {
{ parts: Cons({ fCard: 2, fIndex: i => i }, @lazy.LazyRef::from_value(Nil)) }
}| Situation | Prefer |
|---|---|
| "Try every value up to size 10." | Feat (indexing in a loop) |
| "Find a counterexample in a space I can't enumerate in reasonable time." | QuickCheck (random Arbitrary) |
| "Deterministic, reproducible fuzz corpus across runs." | Feat (size-indexed, no RNG) |
| "I need shrinking to a small counterexample." | QuickCheck + Shrink or falsify |
| Name | What it does |
|---|---|
| empty() | Enumerate[T] with no elements |
| singleton(x) | One-element enumeration at size 0 |
| pay(thunk) | Shift every part one size up |
| a + b | Interleaved disjoint union of two enumerations |
| product(a, b) | Pair-up enumeration; size is the sum of component sizes |
| consts(list) | pay-wrapped union of a List of enumerations |
| unary(f) | T::enumerate().fmap(f) for T : Enumerable |
| Finite::iter, Finite::to_array | Inspect a Finite[T] part once you have one |
| Type | Purpose |
|---|---|
| Finite[T] | Cardinality + indexer (BigInt -> T); usually obtained from eval() |
| Enumerate[T] | LazyList[Finite[T]]; one element per size |
| Enumerable trait | enumerate() -> Enumerate[Self] |
impl Enumerable for Unitimpl Enumerable for Boolimpl Enumerable for Byteimpl Enumerable for Charimpl Enumerable for Intimpl Enumerable for Int64impl Enumerable for UIntimpl Enumerable for UInt64impl Enumerable for Option[E]impl Enumerable for Result[T, E]impl Enumerable for List[E]impl Enumerable for Tuple2[A, B]test {
let bools : Enumerate[Bool] = Enumerable::enumerate()
assert_eq(bools[0], true)
assert_eq(bools[1], false)
}test {
let bools : Enumerate[Bool] = Enumerable::enumerate()
let labels = bools.fmap(b => if b { "yes" } else { "no" })
assert_eq(labels[0], "yes")
assert_eq(labels[1], "no")
}test {
let squares : Finite[BigInt] = { fCard: 4, fIndex: j => j * j }
@debug.assert_eq(squares.iter().collect(), [0, 1, 4, 9])
}test {
let f : Finite[BigInt] = { fCard: 3, fIndex: j => j }
debug_inspect(
f.to_array(),
content=(
#|(3, <List: [0, 1, 2]>)
),
)
}test {
let e : Enumerate[Int] = empty()
debug_inspect(e.eval(), content="[]")
}test {
let e : Enumerate[String] = singleton("leaf")
assert_eq(e.at(0), "leaf")
}fn[A : Enumerable + Debug, B : Testable] small_check(f : (A) -> B, max_size? : Int, expect? : Expected, abort? : Bool, verbose? : Bool) -> Unit raise Failurefn[A : Enumerable + Debug, B : Testable] small_check_error(f : (A) -> B raise, max_size? : Int, expect? : Expected, abort? : Bool, verbose? : Bool) -> Unit raise Failurefn[A : Enumerable + Debug, B : Testable] small_check_error_silence(f : (A) -> B raise, max_size? : Int, expect? : Expected, abort? : Bool, verbose? : Bool) -> Stringfn[A : Enumerable + Debug, B : Testable] small_check_silence(f : (A) -> B, max_size? : Int, expect? : Expected, abort? : Bool, verbose? : Bool) -> StringAutomatic testing of MoonBit programs