README

#Feat — Functional Enumeration of Algebraic Types

A MoonBit port of the ideas from Feat: functional enumeration of algebraic types (Duregård, Jansson, Wang, 2012). Feat turns an algebraic type into a bijection between the non-negative integers and its values, so you can enumerate, index, and sample finite parts of arbitrarily large types without ever materializing the whole set.

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.

#Why enumeration?

Random testing (QuickCheck) and exhaustive testing (SmallCheck) are two sides of the same coin:

  • Random is cheap, finds bugs lurking behind large inputs, but misses corner cases clustered near the "small" end of the space.
  • Exhaustive catches every small-input bug but blows up combinatorially.

Feat's Enumerate[T] interleaves both: values are partitioned by a user-chosen notion of size, so you can pick out the i-th value overall or the j-th value at size k — deterministically, without running the generator from the start.

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)"]

#Install & Import

feat lives inside moonbitlang/quickcheck. Add the main package and then import the sub-package in your moon.pkg.json:

moon add moonbitlang/quickcheck

{ "import": [ { "path": "moonbitlang/quickcheck/feat", "alias": "feat" } ] }


#The two core types

#Finite[T] — a random-access indexed chunk

A Finite[T] is a pair of a cardinality and an indexer. No values are stored; fIndex(i) computes the i-th element on demand.

///|
pub(all) struct Finite[T] {
fCard : BigInt
fIndex : (BigInt) -> T
}

Most users do not construct Finites via helper functions. Instead, they usually get them out of Enumerate::eval() and inspect them with Finite::iter / Finite::to_array. If you do need a custom chunk, you can write one directly:

///|
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"]>)
),
)
}

Finites are still composable once you have them. The most common case is to combine parts produced by enumerations:

///|
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"]>)
),
)
}

Every Finite[T] is also iterable — for x in finite { ... } desugars to finite.iter(), which walks fIndex(0)..fIndex(fCard - 1) lazily. Use it whenever you want to stream a chunk's contents without materialising the full list via to_array:

///|
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])
}

#Enumerate[T] — a lazy list of Finite[T]

An Enumerate[T] is a lazy stream of parts, where the k-th part contains all values of size k. Because the tail is lazy, infinite types (List, Tree, recursive enums…) are perfectly legal.

///|
pub(all) struct Enumerate[T] {
parts : LazyList[Finite[T]]
}

The pay combinator advances the size counter by one — it is the only way to consume "fuel" and the reason a recursive enumeration doesn't diverge:

///|
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]>)
),
)
}


#The Enumerable trait — deriving enumerations

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.

#Signature

///|
pub(open) trait Enumerable {
enumerate() -> Enumerate[Self]
}

  • pub(open) — anyone can add an instance for their own type.
  • The method is nullary: the enumeration of T depends only on T, not on any runtime state.
  • Return type Enumerate[Self] is the lazy list-of-parts value documented above — see Enumerate[T].

#The contract

An implementation must guarantee three properties so the driver can use it safely:

#PropertyWhat breaks if you violate it
1Cardinalities 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.
2Productivity 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.
3Total 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".

These are the same invariants that the provided combinators already preserve — so if you stick to singleton, union / +, product, unary, consts, and pay, you get them for free.

#Built-in instances

TypeShapeNotes
Unitsingleton(())Size 0, card 1.
Boolpay(true + false)Size 1, card 2 (inside pay).
Byteflat Finite of card 256Size 0; indexes 0..255 directly.
Charflat Finite of card 1,112,064Size 0; skips the UTF-16 surrogate range.
Int / Int64interleaved 0, 1, -1, 2, -2, ...Size 0 through infinity; infinite parts each of card 1.
UInt / UInt640, 1, 2, ... with pay per stepEach successor costs one unit of size.
Option[E] where E : Enumerablepay(None + E::enumerate().fmap(Some))None at size 1, Some(x) at 1 + size(x).
Result[T, E] where T, E : Enumerablepay(Err + Ok)Both arms cost one pay.
List[E] where E : Enumerablepay(empty + Cons(e, lst))Each cons cell costs one pay.
(A, B) where A, B : Enumerablepay(product(A::enumerate(), B::enumerate()))Size = 1 + sum of component sizes.

Key observation: all non-primitive instances insert exactly one pay per constructor boundary. That's the rule you follow when writing your own impl.

#Implementing Enumerable for your own type

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]"]

Three rules, and that's it:

  1. One + summand per constructor. Leaf constructors become singleton(...); constructors that carry children use unary(pair => Cons(pair.0, pair.1)) or product(...).
  2. Wrap the whole thing in a pay(...). The pay is what gives the fixpoint a chance to suspend before recursing into T::enumerate() again — this is the productivity guarantee (contract item 2).
  3. Reach child enumerations through Enumerable::enumerate(), not by re-constructing them. That way the compiler's inference picks up user-defined instances and built-ins uniformly.

A minimal recursive example (binary tree of Leaf | Node):

///|
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)")
}

#Common pitfalls

PitfallWhy it hurtsFix
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.

#Where the trait is consumed

  • unary(f) requires the input type of f to be Enumerable.
  • feat_random(size) takes T : Enumerable and turns it into a Gen[T] by drawing uniformly from parts 0..=size.
  • small_check and its silent/error variants live in this package and use the same Enumerable ordering for deterministic prefix testing.


#Using an enumeration

#Indexing

Enumerate::at(i) (also written e[i]) is the "i-th value, overall" view. Sizes are walked in order: all size-0 values, then all size-1 values, etc.

///|
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]>
),
)
}

#Sampling the whole of size k

eval() exposes the underlying LazyList[Finite[T]]. Combined with Finite::to_array, that lets you pull out every value at a specific size — the SmallCheck-style "show me everything of size ≤ k" pattern.

///|
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]>)
),
)
}

#Mapping and combining

Enumerate[T] is a functor together with a disjoint-union + and a size-aware Cartesian product:

OperationSignatureMeaning
Enumerate::fmap(e, f)Enumerate[T] -> (T -> U) -> Enumerate[U]Re-label every element
e1 + e2Enumerate[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 : EnumerableShortcut 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)) }
}


#When to reach for Feat vs. random QuickCheck

SituationPrefer
"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

For large-scale property tests, Feat is also used to seed an initial corpus, which is then handed to the random driver.

#API Reference (quick scan)

#Values

NameWhat 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 + bInterleaved 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_arrayInspect a Finite[T] part once you have one

#Types

TypePurpose
Finite[T]Cardinality + indexer (BigInt -> T); usually obtained from eval()
Enumerate[T]LazyList[Finite[T]]; one element per size
Enumerable traitenumerate() -> Enumerate[Self]

#Further reading

#License

Apache-2.0.

#
Expected

Re-export Expected from internal/state so lower-level property runners can spell run expectations without importing driver internals.

#
LazyList

A call-by-need list: either empty (Nil) or a head value paired with a thunk producing the rest (Cons(head, LazyRef<tail>)). Because the tail is a LazyRef, infinite lists (repeat, infinite_stream) are representable and cheap — only the prefix you iterate is forced.

#
Testable

Anything that can be handed to a QuickCheck-style driver.

Implementors turn themselves into a Property, which a driver then evaluates. Built-in instances exist for Bool, Unit, results, optional values, generators, and the Arrow wrappers.

#
Enumerable

pub(open) trait Enumerable {
fn enumerate() -> Enumerate[Self]
}

Types that can enumerate all of their values in size-indexed order.

enumerate() returns an Enumerate[Self]: a lazy stream of parts where the k-th part contains every value of size k. Primitive types already implement this trait; algebraic composites (sums, products, recursive types) compose via singleton / + / product / unary wrapped in a single pay.

See the package README for the full contract, the built-in instance table, and the recipe for implementing Enumerable on a user-defined recursive type.
impl Enumerable for Unit
impl Enumerable for Bool
impl Enumerable for Byte
impl Enumerable for Char
impl Enumerable for Int
impl Enumerable for Int64
impl Enumerable for UInt
impl Enumerable for Option[E]
impl Enumerable for Result[T, E]
impl Enumerable for Tuple2[A, B]

#
Enumerate

pub(all) struct Enumerate[T] {
parts :
LazyList
[Finite[T]]
}

A lazy list of Finite[T] parts, one part per size class.

Conceptually: Enumerate[T] partitions the type T by a user- chosen notion of "size" (usually one unit per constructor, charged via pay). Part k is a Finite[T] — a cardinality plus an indexer — covering every value of size k. The outer LazyList can be infinite: T = List[E] has parts at every size, no bound.

Invariants (preserved by the provided combinators):
  • every part has fCard >= 0;
  • recursive references are guarded by pay so forcing the outer list is productive;
  • each part's fIndex is total on [0, fCard).
impl Add for Enumerate[T]

#
Enumerate::at

#alias(en_index, deprecated="Use `_[_]` instead")
#alias("_[_]")
fn[T] Enumerate::at(self : Enumerate[T], idx :
BigInt
) -> T

Return the i-th value overall: walk parts from size 0 upward, subtracting each part's cardinality from idx until idx falls inside the current part, then read the value via fIndex.

Aborts on out-of-range indices (reaches Nil).

test {
let bools : Enumerate[Bool] = Enumerable::enumerate()
assert_eq(bools[0], true)
assert_eq(bools[1], false)
}

#
Enumerate::eval

Expose the underlying size-indexed parts. Callers typically use this together with Finite::to_array to materialise every value of a specific size.

#
Enumerate::fmap

fn[T, U] Enumerate::fmap(self : Enumerate[T], f : (T) -> U) -> Enumerate[U]

Rewrite every element without changing the structure: part shapes stay the same, only the inhabitants are relabelled via f.

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")
}

#
Enumerate::sample_finite

fn[T] Enumerate::sample_finite(self : Enumerate[T], size : Int) -> Finite[T]

Finite sampling domain used by feat_random. It aggregates the first size parts; if they are all empty, it continues to the next non-empty part so the result is still finite.

#
Finite

pub(all) struct Finite[T] {
fCard :
BigInt

fIndex : (
BigInt
) -> T
}

A finite chunk of T values, represented compactly as a cardinality plus an indexer rather than as a stored collection. No values are materialised until fIndex is called.

Fields:
  • fCard: number of elements (≥ 0). Zero denotes the empty chunk.
  • fIndex: (i : BigInt) -> T, total on [0, fCard).

Finite[T] is the basic building block used by Enumerate[T]: one part per size class. The combinators below give it a ring-like algebra (disjoint union, Cartesian product, maps, fmap).
impl Add for Finite[T]
impl Debug for Finite[T]

#
Finite::iter

fn[T] Finite::iter(self : Finite[T]) -> Iter[T]

Lazy traversal over the chunk: walks indices 0, 1, 2, …, fCard-1 and yields fIndex(i) at each step. Because MoonBit's for x in <expr> desugars to <expr>.iter(), this is what lets you use a Finite[T] directly with the loop sugar.

The traversal is lazy: early break stops calling fIndex. Like every Iter in MoonBit it is single-shot.

test {
let squares : Finite[BigInt] = { fCard: 4, fIndex: j => j * j }
@debug.assert_eq(squares.iter().collect(), [0, 1, 4, 9])
}

#
Finite::to_array

Materialise a Finite to (cardinality, list of all elements). Eagerly evaluates every element — only suitable for small chunks.

test {
let f : Finite[BigInt] = { fCard: 3, fIndex: j => j }
debug_inspect(
f.to_array(),
content=(
#|(3, <List: [0, 1, 2]>)
),
)
}

#
consts

pay-wrapped union of a list of sub-enumerations. Typical use: write one Enumerate per constructor of a sum type, then combine with consts. The outer pay charges one size unit for the constructor tag.

#
empty

fn[T] empty() -> Enumerate[T]

The empty enumeration: no values at any size. Identity for union.

test {
let e : Enumerate[Int] = empty()
debug_inspect(e.eval(), content="[]")
}

#
feat_random

fn[T : Enumerable] feat_random(size : Int) ->
Gen
[T]

Generate a value from an enumerable instance (up to a size bound). This is the Feat-backed random-sampling bridge: gen stays unaware of Enumerable, while feat can still produce ordinary generators. @alert unsafe "Experimental: May cause stack overflow"

#
pay

fn[T] pay(f : () -> Enumerate[T]) -> Enumerate[T]

Shift every size class up by one: part k of pay(f) equals part k - 1 of f(). The outer part 0 becomes empty. This is the productivity knob for recursive Enumerable instances — every recursive self-reference must be wrapped in pay or the fixpoint diverges.

#
product

fn[T, U] product(e1 : Enumerate[T], e2 : Enumerate[U]) -> Enumerate[(T, U)]

Cartesian product, keyed by sum of sizes: part k of product(a, b) contains every pair whose component sizes add to k. Order inside a part follows the diagonalisation convention (first by decreasing a-size, then by increasing b-size).

#
singleton

fn[T] singleton(val : T) -> Enumerate[T]

One-value enumeration at size 0. Building block for Enumerable instances of leaf constructors.

test {
let e : Enumerate[String] = singleton("leaf")
assert_eq(e.at(0), "leaf")
}

#
small_check

fn[A : Enumerable +
Debug
, B :
Testable
] small_check(f : (A) -> B, max_size? : Int, expect? :
Expected
, abort? : Bool, verbose? : Bool) -> Unit raise Failure

Exhaustive-ish testing via Feat enumeration: walk values of A in ascending size order, checking each one against f, up to at most max_size total test cases.

#
small_check_error

fn[A : Enumerable +
Debug
, B :
Testable
] small_check_error(f : (A) -> B raise, max_size? : Int, expect? :
Expected
, abort? : Bool, verbose? : Bool) -> Unit raise Failure

small_check variant that accepts a property that may raise. A raised error is treated as a counter-example.

#
small_check_error_silence

fn[A : Enumerable +
Debug
, B :
Testable
] small_check_error_silence(f : (A) -> B raise, max_size? : Int, expect? :
Expected
, abort? : Bool, verbose? : Bool) -> String

Silent variant of small_check_error: returns the formatted outcome as a String instead of raising.

#
small_check_silence

fn[A : Enumerable +
Debug
, B :
Testable
] small_check_silence(f : (A) -> B, max_size? : Int, expect? :
Expected
, abort? : Bool, verbose? : Bool) -> String

small_check silent variant: returns the formatted outcome as a String.

#
unary

fn[T : Enumerable, U] unary(f : (T) -> U) -> Enumerate[U]

Apply a unary constructor: unary(f) == T::enumerate().fmap(f). For multi-argument constructors, write a tuple lambda so f takes a single tuple argument — e.g. unary(p => Node(p.0, p.1)) — then unary(f) goes through the built-in pair Enumerable instance which inserts the required pay.