property.bend source
property.bend on the hub · documented module
# Reproducible property checks with generated inputs and greedy shrinking.import Baseimport bend-kit-random@0.1.0.1/random.bend as Randimport ./generate.bend as Genimport ./shrink.bend as Shrinktype Failure<-T: Type> is Type: Failure{seed: U32, trial: Nat, value: T}# Scan candidates in order; a predicate returns its input so affine values work.def smaller(~T: Type, ~test: T -> T & Bool, xs: List<&1, T>, tested: T & Bool) -> Maybe<&1, T>: match xs: case Nil{}: (candidate, ok) = tested match ok: case False{}: Some{candidate} case True{}: None{} case Con{x, rest}: (candidate, ok) = tested match ok: case False{}: Some{candidate} case True{}: smaller(~T, ~test, rest, test(x))def smaller.start(~T: Type, ~test: T -> T & Bool, xs: List<&1, T>) -> Maybe<&1, T>: match xs: case Nil{}: None{} case Con{x, rest}: smaller(~T, ~test, rest, test(x))def attempt.pair(~T: Type, ~shrink: T -> List<&1, T>, ~test: T -> T & Bool, pair: T & T) -> T & Maybe<&1, T>: (original, probe) = pair (original, smaller.start(~T, ~test, shrink(probe)))def attempt(~T: Type, ~copy: T -> T & T, ~shrink: T -> List<&1, T>, ~test: T -> T & Bool, value: T) -> T & Maybe<&1, T>: attempt.pair(~T, ~shrink, ~test, copy(value))# Each accepted failure restarts shrinking; fuel bounds user-supplied shrinkers.def minimize(~T: Type, ~copy: T -> T & T, ~shrink: T -> List<&1, T>, ~test: T -> T & Bool, fuel: Nat, state: T & Maybe<&1, T>) -> T: match fuel: case 0n: (original, choice) = state Maybe.default(&1, T, choice, original) case 1n+p: (original, choice) = state match choice: case None{}: original case Some{candidate}: minimize(~T, ~copy, ~shrink, ~test, p, attempt(~T, ~copy, ~shrink, ~test, candidate))def minimize.start(~T: Type, ~copy: T -> T & T, ~shrink: T -> List<&1, T>, ~test: T -> T & Bool, value: T) -> T: minimize(~T, ~copy, ~shrink, ~test, 1024n, attempt(~T, ~copy, ~shrink, ~test, value))def draw.pair(~T: Type, ~test: T -> T & Bool, pair: T & Rand.Rng) -> (T & Bool) & Rand.Rng: (sample, next) = pair (test(sample), next)def draw(~T: Type, ~gen: Rand.Rng -> T & Rand.Rng, ~test: T -> T & Bool, rng: Rand.Rng) -> (T & Bool) & Rand.Rng: draw.pair(~T, ~test, gen(rng))def check.go(~T: Type, ~gen: Rand.Rng -> T & Rand.Rng, ~copy: T -> T & T, ~shrink: T -> List<&1, T>, ~test: T -> T & Bool, remaining: Nat, +seed: U32, +trial: Nat, state: (T & Bool) & Rand.Rng) -> Maybe<&1, Failure<T>>: match remaining: case 0n: None{} case 1n+p: match p: case 0n: (tested, next) = state (value, ok) = tested match ok: case False{}: Some{Failure{seed, trial, minimize.start(~T, ~copy, ~shrink, ~test, value)}} case True{}: None{} case 1n+q: (tested, next) = state (value, ok) = tested match ok: case False{}: Some{Failure{seed, trial, minimize.start(~T, ~copy, ~shrink, ~test, value)}} case True{}: check.go(~T, ~gen, ~copy, ~shrink, ~test, p, seed, (1n+trial), draw(~T, ~gen, ~test, next))def check.start(~T: Type, ~gen: Rand.Rng -> T & Rand.Rng, ~copy: T -> T & T, ~shrink: T -> List<&1, T>, ~test: T -> T & Bool, remaining: Nat, seed: U32, rng: Rand.Rng) -> Maybe<&1, Failure<T>>: match remaining: case 0n: None{} case 1n+p: check.go(~T, ~gen, ~copy, ~shrink, ~test, remaining, seed, 0n, draw(~T, ~gen, ~test, rng))# Return the first failure and its smallest reachable counterexample.def check(~T: Type, ~gen: Rand.Rng -> T & Rand.Rng, ~copy: T -> T & T, ~shrink: T -> List<&1, T>, ~test: T -> T & Bool, +seed: U32, trials: Nat) -> Maybe<&1, Failure<T>>: check.start(~T, ~gen, ~copy, ~shrink, ~test, trials, seed, Rand.seed(seed))# Print the seed and draw index so a failing check can be replayed.def report(~T: Type, ~show: T -> String, result: Maybe<&1, Failure<T>>) -> IO(Bool): match result: case None{}: IO.pure(Bool, True{}) case Some{Failure{seed, trial, value}}: do IO<Bool>: IO.print("property failed: seed=" ++ U32.show(seed) ++ " trial=" ++ Nat.show(trial) ++ " counterexample=" ++ show(value)) return False{}def run(~T: Type, ~gen: Rand.Rng -> T & Rand.Rng, ~copy: T -> T & T, ~shrink: T -> List<&1, T>, ~test: T -> T & Bool, ~show: T -> String, seed: U32, trials: Nat) -> IO(Bool): report(~T, ~show, check(~T, ~gen, ~copy, ~shrink, ~test, seed, trials))