Dependencies
///|
struct RefId {
id : Int
} derive(Eq, Debug)
///|
enum Command {
Create
Read(@quickcheck_statemachine.Ref[RefId])
Write(@quickcheck_statemachine.Ref[RefId], Int)
Increment(@quickcheck_statemachine.Ref[RefId])
} derive(Eq, Debug)
///|
enum Response {
Created(@quickcheck_statemachine.Ref[RefId])
ReadValue(Int)
Written
Incremented
} derive(Eq, Debug)///|
enum Bug {
NoBug
LogicBug
} derive(Eq, Debug)
///|
struct SystemEntry {
id : Int
value : Int
} derive(Eq, Debug)
///|
struct ReferenceSystem {
refs : Array[SystemEntry]
mut next_id : Int
} derive(Eq, Debug)///|
fn reference_id(reference : @quickcheck_statemachine.Ref[RefId]) -> Int? {
match reference {
Concrete(ref_id) => Some(ref_id.id)
Symbolic(_) => None
}
}
///|
fn system_lookup(system : ReferenceSystem, id : Int) -> Int? {
for entry in system.refs {
if entry.id == id {
return Some(entry.value)
}
}
None
}
///|
fn system_update(system : ReferenceSystem, id : Int, value : Int) -> Unit {
for index = 0; index < system.refs.length(); {
if system.refs[index].id == id {
system.refs[index] = { id, value }
return
}
continue index + 1
}
}///|
fn semantics(
bug : Bug,
command : Command,
system : ReferenceSystem,
) -> Response {
match command {
Create => {
let id = system.next_id
system.next_id 1
system.refs.push({ id, value: 0 })
Created(Concrete({ id, }))
}
Read(reference) =>
match reference_id(reference) {
Some(id) =>
match system_lookup(system, id) {
Some(value) => ReadValue(value)
None => ReadValue(0)
}
None => ReadValue(0)
}
Write(reference, value) =>
match reference_id(reference) {
Some(id) => {
let stored = if bug == LogicBug && 5 <= value && value <= 10 {
value + 1
} else {
value
}
system_update(system, id, stored)
Written
}
None => Written
}
Increment(reference) =>
match reference_id(reference) {
Some(id) => {
match system_lookup(system, id) {
Some(value) => system_update(system, id, value + 1)
None => ()
}
Incremented
}
None => Incremented
}
}
}///|
struct ModelEntry {
reference : @quickcheck_statemachine.Ref[RefId]
value : Int
} derive(Eq, Debug)
///|
struct Model {
refs : Array[ModelEntry]
} derive(Eq, Debug)
///|
fn model_empty() -> Model {
{ refs: [] }
}///|
fn model_lookup(
model : Model,
reference : @quickcheck_statemachine.Ref[RefId],
) -> Int? {
for entry in model.refs {
if entry.reference == reference {
return Some(entry.value)
}
}
None
}
///|
fn model_member(
model : Model,
reference : @quickcheck_statemachine.Ref[RefId],
) -> Bool {
model_lookup(model, reference) is Some(_)
}
///|
fn model_update(
model : Model,
reference : @quickcheck_statemachine.Ref[RefId],
value : Int,
) -> Model {
let refs = model.refs.copy()
for index = 0; index < refs.length(); {
if refs[index].reference == reference {
refs[index] = { reference, value }
return { refs, }
}
continue index + 1
}
refs.push({ reference, value })
{ refs, }
}///|
fn precondition(
model : Model,
command : Command,
) -> @quickcheck_statemachine.Logic {
let ok = match command {
Create => true
Read(reference) | Write(reference, _) | Increment(reference) =>
model_member(model, reference)
}
@quickcheck_statemachine.Logic::predicate(name="known reference", value=ok)
}///|
fn transition(model : Model, command : Command, response : Response) -> Model {
match (command, response) {
(Create, Created(reference)) => model_update(model, reference, 0)
(Read(_), ReadValue(_)) => model
(Write(reference, value), Written) => model_update(model, reference, value)
(Increment(reference), Incremented) =>
match model_lookup(model, reference) {
Some(value) => model_update(model, reference, value + 1)
None => model
}
_ => model
}
}///|
fn postcondition(
model : Model,
command : Command,
response : Response,
) -> @quickcheck_statemachine.Logic {
match (command, response) {
(Create, Created(reference)) => {
let next_model = transition(model, command, response)
@quickcheck_statemachine.Logic::predicate(
name="Create",
value=model_lookup(next_model, reference) == Some(0),
)
}
(Read(reference), ReadValue(value)) =>
@quickcheck_statemachine.Logic::predicate(
name="Read",
value=model_lookup(model, reference) == Some(value),
)
(Write(_, _), Written) => @quickcheck_statemachine.Logic::top()
(Increment(_), Incremented) => @quickcheck_statemachine.Logic::top()
_ => @quickcheck_statemachine.Logic::bot()
}
}///|
fn choose_reference(
model : Model,
rng : @splitmix.RandomState,
) -> @quickcheck_statemachine.Ref[RefId] {
let index = rng.next_positive_int() % model.refs.length()
model.refs[index].reference
}
///|
fn generator(
model : Model,
size : Int,
rng : @splitmix.RandomState,
) -> Command? {
ignore(size)
if model.refs.length() == 0 {
Some(Create)
} else {
match rng.next_positive_int() % 4 {
0 => Some(Create)
1 => Some(Read(choose_reference(model, rng)))
2 =>
Some(Write(choose_reference(model, rng), rng.next_positive_int() % 16))
_ => Some(Increment(choose_reference(model, rng)))
}
}
}///|
fn shrinker(_model : Model, command : Command) -> Array[Command] {
match command {
Write(reference, value) =>
if value == 0 {
[]
} else {
[Write(reference, 0), Write(reference, value / 2)]
}
_ => []
}
}///|
fn mock(
model : Model,
command : Command,
gen_sym : @quickcheck_statemachine.GenSym,
) -> (Response, @quickcheck_statemachine.GenSym) {
match command {
Create => {
let (fresh_var, next_gen_sym) = gen_sym.fresh()
(Created(Symbolic(fresh_var)), next_gen_sym)
}
Read(reference) =>
match model_lookup(model, reference) {
Some(value) => (ReadValue(value), gen_sym)
None => (ReadValue(0), gen_sym)
}
Write(_, _) => (Written, gen_sym)
Increment(_) => (Incremented, gen_sym)
}
}
///|
fn response_vars(response : Response) -> Array[@quickcheck_statemachine.Var] {
match response {
Created(Symbolic(fresh_var)) => [fresh_var]
_ => []
}
}///|
fn reify_ref(
reference : @quickcheck_statemachine.Ref[RefId],
environment : @quickcheck_statemachine.Environment[RefId],
) -> Result[
@quickcheck_statemachine.Ref[RefId],
@quickcheck_statemachine.EnvError,
] {
match reference {
Concrete(ref_id) => Ok(Concrete(ref_id))
Symbolic(fresh_var) =>
match environment.lookup(fresh_var) {
Ok(ref_id) => Ok(Concrete(ref_id))
Err(error) => Err(error)
}
}
}
///|
fn reify_command(
command : Command,
environment : @quickcheck_statemachine.Environment[RefId],
) -> Result[Command, @quickcheck_statemachine.EnvError] {
match command {
Create => Ok(Create)
Read(reference) =>
match reify_ref(reference, environment) {
Ok(concrete) => Ok(Read(concrete))
Err(error) => Err(error)
}
Write(reference, value) =>
match reify_ref(reference, environment) {
Ok(concrete) => Ok(Write(concrete, value))
Err(error) => Err(error)
}
Increment(reference) =>
match reify_ref(reference, environment) {
Ok(concrete) => Ok(Increment(concrete))
Err(error) => Err(error)
}
}
}///|
fn bind_response(
symbolic : Response,
concrete : Response,
environment : @quickcheck_statemachine.Environment[RefId],
) -> Result[
@quickcheck_statemachine.Environment[RefId],
@quickcheck_statemachine.BindError,
] {
match (symbolic, concrete) {
(Created(Symbolic(fresh_var)), Created(Concrete(ref_id))) =>
Ok(environment.insert(fresh_var, ref_id))
(Created(Symbolic(_)), _) => Err(BindMessage("expected concrete reference"))
_ => Ok(environment)
}
}///|
fn remap_ref(
reference : @quickcheck_statemachine.Ref[RefId],
scope : Map[@quickcheck_statemachine.Var, @quickcheck_statemachine.Var],
) -> @quickcheck_statemachine.Ref[RefId]? {
match reference {
Concrete(ref_id) => Some(Concrete(ref_id))
Symbolic(fresh_var) =>
match scope.get(fresh_var) {
Some(next_var) => Some(Symbolic(next_var))
None => None
}
}
}
///|
fn remap_command(
command : Command,
scope : Map[@quickcheck_statemachine.Var, @quickcheck_statemachine.Var],
) -> Command? {
match command {
Create => Some(Create)
Read(reference) =>
match remap_ref(reference, scope) {
Some(next_reference) => Some(Read(next_reference))
None => None
}
Write(reference, value) =>
match remap_ref(reference, scope) {
Some(next_reference) => Some(Write(next_reference, value))
None => None
}
Increment(reference) =>
match remap_ref(reference, scope) {
Some(next_reference) => Some(Increment(next_reference))
None => None
}
}
}///|
fn command_name(command : Command) -> String {
match command {
Create => "create"
Read(_) => "read"
Write(_, _) => "write"
Increment(_) => "increment"
}
}
///|
fn config(
max_commands~ : Int,
seed? : UInt64 = 11,
cases? : Int = 1,
shrink? : Bool = true,
) -> @quickcheck_statemachine.RunConfig {
{
seed,
cases,
max_commands,
size: max_commands,
max_tries: 20,
shrink,
max_shrinks: 20,
max_shrink_rounds: 10,
required_labels: [],
required_command_names: [],
}
}///|
fn sm(
bug : Bug,
script? : Array[Command]? = None,
) -> @quickcheck_statemachine.StateMachine[
Model,
Model,
Command,
Command,
Response,
Response,
RefId,
ReferenceSystem,
] {
let cursor : Array[Int] = [0]
@quickcheck_statemachine.StateMachine::new(
init_symbolic_model=model_empty,
init_concrete_model=model_empty,
init_system=() => { refs: [], next_id: 0 },
generator=(model, size, rng) => {
match script {
Some(commands) => {
ignore(model)
ignore(size)
ignore(rng)
let index = cursor[0]
if index < commands.length() {
cursor[0] = index + 1
Some(commands[index])
} else {
None
}
}
None => generator(model, size, rng)
}
},
mock~,
transition_symbolic=transition,
transition_concrete=transition,
precondition~,
postcondition~,
response_vars~,
shrinker~,
remap_command~,
reify_command~,
run_command=(command, system) => semantics(bug, command, system),
bind_response~,
command_name~,
)
}///|
test "sequential property passes without the bug" {
let reference : @quickcheck_statemachine.Ref[RefId] = Symbolic(@quickcheck_statemachine.Var::{
id: 0,
})
let commands : Array[Command] = [
Create,
Write(reference, 4),
Increment(reference),
Read(reference),
]
let report = @quickcheck_statemachine.assert_check(
sm(NoBug, script=Some(commands)),
config=config(max_commands=commands.length()),
)
assert_eq(report.commands_run, 4)
}///|
test "random generation shrinks the logic bug" {
let result = @quickcheck_statemachine.check(
sm(LogicBug),
config=config(seed=37, cases=100, max_commands=8),
)
guard result
is Err(
PostconditionFailed(
step_index~,
response~,
commands~,
shrinks~,
logic~,
..
)
) else {
fail("expected a shrunk postcondition failure")
}
assert_eq(step_index, 2)
assert_true(response is ReadValue(6))
assert_eq(commands.length(), 3)
assert_true(shrinks > 0)
assert_true(!logic.eval())
}///|
test "diagnostics expose commands history and predicate names" {
let result = @quickcheck_statemachine.check(
sm(LogicBug),
config=config(seed=37, cases=100, max_commands=8),
)
guard result is Err(PostconditionFailed(commands~, history~, logic~, ..)) else {
fail("expected a postcondition failure")
}
// symbolic commands
inspect(
@quickcheck_statemachine.format_commands(commands),
content=(
#|0: Create -> Created(Symbolic({ id: 0 }))
#|1: Write(Symbolic({ id: 0 }), 5) -> Written
#|2: Read(Symbolic({ id: 0 })) -> ReadValue(5)
#|
),
)
// concrete history
inspect(
@quickcheck_statemachine.format_history(history),
content=(
#|Invocation(pid={ id: 0 }, command=Create)
#|Response(pid={ id: 0 }, response=Created(Concrete({ id: 0 })))
#|Invocation(pid={ id: 1 }, command=Write(Concrete({ id: 0 }), 5))
#|Response(pid={ id: 1 }, response=Written)
#|Invocation(pid={ id: 2 }, command=Read(Concrete({ id: 0 })))
#|Response(pid={ id: 2 }, response=ReadValue(6))
#|
),
)
// failed predicate
inspect(
@quickcheck_statemachine.format_counterexample(logic.counterexample()),
content="Read",
)
}fn[V] Environment::merge(self : Environment[V], other : Environment[V]) -> Result[Environment[V], EnvError]pub(all) enum RunFailure[MSym, MCon, CSym, CCon, RSym, RCon] {
InvalidConfig(String)
GenerationFailed(GenerationFailure[MSym, CSym, RSym], Int, Int)
InitFailed(Commands[CSym, RSym], String, Int, Int)
ReifyFailed(Int, CSym, EnvError, Commands[CSym, RSym], History[CCon, RCon], Int, Int)
ExecutionFailed(Int, CCon, CSym, MCon, Commands[CSym, RSym], History[CCon, RCon], String, Int, Int)
BindFailed(Int, RSym, RCon, BindError, Commands[CSym, RSym], History[CCon, RCon], Int, Int)
PostconditionFailed(Int, CCon, CSym, RCon, MCon, MCon, Logic, Commands[CSym, RSym], History[CCon, RCon], Int, Int)
InvariantBroken(Int, MCon, Logic, Commands[CSym, RSym], History[CCon, RCon], Int, Int)
CleanupFailed(Commands[CSym, RSym], History[CCon, RCon], String, Int, Int)
CoverageFailed(Commands[CSym, RSym], History[CCon, RCon], Array[String], Array[String], Int, Int)
} derive(Eq, Debug)pub struct StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S] {
init_symbolic_model : () -> MSym
init_concrete_model : () -> MCon
init_system : () -> S raise
generator : (MSym, Int, RandomState) -> CSym?
mock : (MSym, CSym, GenSym) -> (RSym, GenSym)
response_vars : (RSym) -> Array[Var]
transition_symbolic : (MSym, CSym, RSym) -> MSym
transition_concrete : (MCon, CCon, RCon) -> MCon
precondition : (MSym, CSym) -> Logic
postcondition : (MCon, CCon, RCon) -> Logic
invariant : (MCon) -> Logic
shrinker : (MSym, CSym) -> Array[CSym]
remap_command : (CSym, Map[Var, Var]) -> CSym?
reify_command : (CSym, Environment[V]) -> Result[CCon, EnvError]
run_command : (CCon, S) -> RCon raise
bind_response : (RSym, RCon, Environment[V]) -> Result[Environment[V], BindError]
cleanup : (MCon, S) -> Unit raise
command_name : (CSym) -> String
label : (MCon, CCon, RCon) -> Array[String]
}fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] StateMachine::new(init_symbolic_model~ : () -> MSym, init_concrete_model~ : () -> MCon, init_system~ : () -> S raise, generator~ : (MSym, Int, RandomState) -> CSym?, mock~ : (MSym, CSym, GenSym) -> (RSym, GenSym), transition_symbolic~ : (MSym, CSym, RSym) -> MSym, transition_concrete~ : (MCon, CCon, RCon) -> MCon, reify_command~ : (CSym, Environment[V]) -> Result[CCon, EnvError], run_command~ : (CCon, S) -> RCon raise, bind_response~ : (RSym, RCon, Environment[V]) -> Result[Environment[V], BindError], postcondition? : (MCon, CCon, RCon) -> Logic, precondition? : (MSym, CSym) -> Logic, invariant? : (MCon) -> Logic, response_vars? : (RSym) -> Array[Var], shrinker? : (MSym, CSym) -> Array[CSym], remap_command? : (CSym, Map[Var, Var]) -> CSym?, cleanup? : (MCon, S) -> Unit raise, command_name? : (CSym) -> String, label? : (MCon, CCon, RCon) -> Array[String]) -> StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S]fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] check(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], config? : RunConfig) -> Result[RunReport[CSym, RSym, CCon, RCon], RunFailure[MSym, MCon, CSym, CCon, RSym, RCon]]fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] first_failing_candidate(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], candidates : Array[Commands[CSym, RSym]], config : RunConfig) -> Commands[CSym, RSym]?fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] generate_commands(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], config? : RunConfig) -> Result[Commands[CSym, RSym], GenerationFailure[MSym, CSym, RSym]]fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] generate_commands_with_seed(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], config? : RunConfig, seed? : UInt64) -> Result[Commands[CSym, RSym], GenerationFailure[MSym, CSym, RSym]]fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] generate_parallel_commands(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], config? : RunConfig) -> Result[ParallelCommands[CSym, RSym], GenerationFailure[MSym, CSym, RSym]]fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] replay(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], commands : Commands[CSym, RSym], config? : RunConfig) -> Result[RunReport[CSym, RSym, CCon, RCon], RunFailure[MSym, MCon, CSym, CCon, RSym, RCon]]fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] run_commands(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], commands : Commands[CSym, RSym], config? : RunConfig) -> Result[RunReport[CSym, RSym, CCon, RCon], RunFailure[MSym, MCon, CSym, CCon, RSym, RCon]]fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] run_parallel_commands(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], commands : ParallelCommands[CSym, RSym], config? : RunConfig) -> Result[RunReport[CSym, RSym, CCon, RCon], RunFailure[MSym, MCon, CSym, CCon, RSym, RCon]]fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] run_saved_commands(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], input : String, decode : (String) -> Result[Commands[CSym, RSym], String], config? : RunConfig) -> Result[RunReport[CSym, RSym, CCon, RCon], RunFailure[MSym, MCon, CSym, CCon, RSym, RCon]]fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] shrink_and_validate(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], commands : Commands[CSym, RSym]) -> Commands[CSym, RSym]?fn[MSym, MCon, CSym, CCon, RSym, RCon, V, S] shrink_commands_once(spec : StateMachine[MSym, MCon, CSym, CCon, RSym, RCon, V, S], commands : Commands[CSym, RSym]) -> Array[Commands[CSym, RSym]]Dependencies