blob: e6eddc6925c48850d3cdb3d72b0b7db3d08f5d53 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package concurrency
import (
"errors"
"fmt"
"reflect"
)
// state records a state of the exploration of the space of all
// possible program states a concurrent test can encounter. The states
// of the exploration are organized using a tree data structure
// referred to as the "execution tree". Nodes of the execution tree
// represent different abstract program states and edges represent the
// different scheduling decisions that can be made at those states. A
// path from the root to a leaf thus uniquely represents an execution
// as a serialization of the scheduling decisions along the execution.
type state struct {
// depth identifies the depth of this node in the execution tree.
depth int
// parent records the pointer to the parent abstract state.
parent *state
// children maps threads to the abstract states that correspond to
// executing the next transitions of that thread.
children map[TID]*state
// explored records whether the subtree rooted at this state has
// been fully explored.
explored bool
// seeded records whether the state has been used to seed an
// exploration strategy.
seeded bool
// visited records whether the state has been visited
visited bool
// tid records the thread identifier of the thread whose transition
// lead to this state.
tid TID
// kind records the type of the transition that lead to this state.
kind transitionKind
// clock records the logical time of this state.
clock clock
// enabled records whether the transition that leads to this state
// is enabled.
enabled bool
// readSet records the identifiers of the abstract resources read by
// the transition that lead to this state.
readSet resourceSet
// writeSet records the identifiers of the abstract resources
// written by the transition that lead to this state.
writeSet resourceSet
}
// newState is the state factory.
func newState(parent *state, tid TID) *state {
depth := 0
if parent != nil {
depth = parent.depth + 1
}
return &state{
depth: depth,
children: make(map[TID]*state),
parent: parent,
tid: tid,
readSet: newResourceSet(),
writeSet: newResourceSet(),
clock: newClock(),
}
}
// addBranch adds a branch described through the given sequence of
// choices to the execution tree rooted at this state.
func (s *state) addBranch(branch []*choice, seeds *stack) error {
if len(branch) == 0 {
s.explored = true
return nil
}
choice := branch[0]
if len(s.children) != 0 {
// Check for the absence of divergence.
if err := s.checkDivergence(choice.transitions); err != nil {
return err
}
} else {
// Add new children.
for _, transition := range choice.transitions {
child := newState(s, transition.tid)
child.clock = transition.clock
child.enabled = transition.enabled
child.kind = transition.kind
child.readSet = transition.readSet
child.writeSet = transition.writeSet
s.children[child.tid] = child
}
}
next, found := s.children[choice.next]
if !found {
return errors.New(fmt.Sprintf("invalid choice (no transition fo thread %d).", choice.next))
}
next.visited = true
branch = branch[1:]
if err := next.addBranch(branch, seeds); err != nil {
return err
}
s.collectSeeds(choice.next, seeds)
s.compactTree()
return nil
}
// approach identifies and enforces exploration of transition(s)
// originating from this state that near execution of the transition
// leading to the given state.
func (s *state) approach(other *state, seeds *stack) {
candidates := make([]TID, 0)
for tid, child := range s.children {
if child.enabled && child.happensBefore(other) {
candidates = append(candidates, tid)
}
}
if len(candidates) == 0 {
for _, child := range s.children {
if child.enabled && !child.seeded && !child.visited {
seeds.Push(child)
child.seeded = true
}
}
} else {
for _, tid := range candidates {
child := s.children[tid]
if child.seeded || child.visited {
return
}
}
tid := candidates[0]
child := s.children[tid]
seeds.Push(child)
child.seeded = true
}
}
// checkDivergence checks whether the given transition matches the
// transition identified by the given thread identifier.
func (s *state) checkDivergence(transitions map[TID]*transition) error {
if len(s.children) != len(transitions) {
return errors.New(fmt.Sprintf("divergence encountered (expected %v, got %v)", len(s.children), len(transitions)))
}
for tid, t := range transitions {
child, found := s.children[tid]
if !found {
return errors.New(fmt.Sprintf("divergence encountered (no transition for thread %d)", tid))
}
if child.enabled != t.enabled {
return errors.New(fmt.Sprintf("divergence encountered (expected %v, got %v)", child.enabled, t.enabled))
}
if !child.clock.equals(t.clock) {
return errors.New(fmt.Sprintf("divergence encountered (expected %v, got %v)", child.clock, t.clock))
}
if !reflect.DeepEqual(child.readSet, t.readSet) {
return errors.New(fmt.Sprintf("divergence encountered (expected %v, got %v)", child.readSet, t.readSet))
}
if !reflect.DeepEqual(child.writeSet, t.writeSet) {
return errors.New(fmt.Sprintf("divergence encountered (expected %v, got %v)", child.writeSet, t.writeSet))
}
}
return nil
}
// collectSeeds uses dynamic partial order reduction
// (http://users.soe.ucsc.edu/~cormac/papers/popl05.pdf) to identify
// which alternative scheduling choices should be explored.
func (s *state) collectSeeds(next TID, seeds *stack) {
for tid, child := range s.children {
if tid != next && !s.mayInterfereWith(child) && !s.happensBefore(child) {
continue
}
for handle := s; handle.parent != nil; handle = handle.parent {
if handle.mayInterfereWith(child) && !handle.happensBefore(child) {
handle.parent.approach(child, seeds)
}
}
}
}
// compactTree updates the exploration status of this state and
// deallocates its children map if all of its children are explored.
func (s *state) compactTree() {
for _, child := range s.children {
if (child.seeded || child.visited) && !child.explored {
return
}
}
s.children = nil
s.explored = true
}
// generateStrategy generates a sequence of scheduling decisions that
// will steer an execution towards the state of the execution tree.
func (s *state) generateStrategy() []TID {
if s.parent == nil {
return make([]TID, 0)
}
return append(s.parent.generateStrategy(), s.tid)
}
// happensBefore checks if the logical time of the transition that
// leads to this state happened before (in the sense of Lamport's
// happens-before relation) the logical time of the transition that
// leads to the given state.
func (s *state) happensBefore(other *state) bool {
return s.clock.happensBefore(other.clock)
}
// mayInterfereWith checks if the execution of the transition that
// leads to this state may interfere with the execution of the
// transition that leads to the given state.
func (s *state) mayInterfereWith(other *state) bool {
if (s.kind == tMutexLock && other.kind == tMutexUnlock) ||
(s.kind == tMutexUnlock && other.kind == tMutexLock) {
return false
}
if (s.kind == tRWMutexLock && other.kind == tRWMutexUnlock) ||
(s.kind == tRWMutexUnlock && other.kind == tRWMutexLock) {
return false
}
if (s.kind == tRWMutexRLock && other.kind == tRWMutexUnlock) ||
(s.kind == tRWMutexRUnlock && other.kind == tRWMutexLock) {
return false
}
for k, _ := range s.readSet {
if _, found := other.writeSet[k]; found {
return true
}
}
for k, _ := range s.writeSet {
if _, found := other.readSet[k]; found {
return true
}
}
for k, _ := range s.writeSet {
if _, found := other.writeSet[k]; found {
return true
}
}
return false
}