enum Flag {Method, MethodOrField, Public}

/* There is a negative version for each flag (METHOD and NOT_METHOD).
     Model this as two sets, one of positive flags, and one of netative flags,
     and interpret the bitfield to be a conjunction of both flags. */
sig Bitfield {
    , positiveFlags: set Flag
    , negativeFlags: set Flag
}

/* A filter state has filterFlags and excludeFlags, both represented as conjunctions. */ 
sig FilterState {
    , curFilter: Bitfield
}

/* Initially, no search has happeneed for a scope, so its 'found' is not set to anything. */
one sig NotSet {}

/* Finally, there's a search state (whether or not a particular scope has already been
    searched with a particular configuration). */
one sig SearchState {
    , var found: Bitfield + NotSet
}


pred bitfieldEmpty[b: Bitfield] {
    #b.positiveFlags = 0 and #b.negativeFlags = 0
}

pred bitfieldEqual[b1: Bitfield, b2: Bitfield] {
    b1.positiveFlags = b2.positiveFlags and b1.negativeFlags = b2.negativeFlags
}

pred bitfieldIntersection[b1: Bitfield, b2: Bitfield, b3: Bitfield] {
    b3.positiveFlags = b1.positiveFlags & b2.positiveFlags
    b3.negativeFlags = b1.negativeFlags & b2.negativeFlags
}

pred bitfieldSubset[b1: Bitfield, b2: Bitfield] {
    b1.positiveFlags in b2.positiveFlags
    b1.negativeFlags in b2.negativeFlags
}

pred bitfieldIncomparable[b1: Bitfield, b2: Bitfield] {
    not bitfieldSubset[b1, b2]
    not bitfieldSubset[b2, b1]
}

pred addBitfieldFlag[b1: Bitfield, b2: Bitfield, flag: Flag] {
    b2.positiveFlags = b1.positiveFlags + flag
    b2.negativeFlags = b1.negativeFlags
}

pred addBitfieldFlagNeg[b1: Bitfield, b2: Bitfield, flag: Flag] {
    b2.negativeFlags = b1.negativeFlags + flag
    b2.positiveFlags = b1.positiveFlags
}

enum Property { PMethod, PField, PPublic }

sig Symbol {
    properties: set Property
}

pred flagMatchesProperty[flag: Flag, property: Property] {
    (flag = Method and property = PMethod) or
    (flag = MethodOrField and (property = PMethod or property = PField)) or
    (flag = Public and property = PPublic)
}

pred bitfieldMatchesProperties[bitfield: Bitfield, symbol: Symbol] {
    all flag: bitfield.positiveFlags | some property: symbol.properties | flagMatchesProperty[flag, property]
    all flag: bitfield.negativeFlags | no property: symbol.properties | flagMatchesProperty[flag, property]
}

bitfieldExists: run {
    some Bitfield
}

matchingBitfieldExists: run {
    some bitfield : Bitfield, symbol : Symbol | bitfieldMatchesProperties[bitfield, symbol]
}

matchingBitfieldExists2: run {
    some bitfield : Bitfield, symbol : Symbol {
        #bitfield.positiveFlags = 1
        #bitfield.negativeFlags = 1
        #symbol.properties = 2
        bitfieldMatchesProperties[bitfield, symbol]
    }
}

fact "method and field are incompatible" {
    always no symbol: Symbol | {
        PMethod in symbol.properties and PField in symbol.properties
    }
}

fact "public and field are incompatible" {
    always no symbol: Symbol | {
        PPublic in symbol.properties and PField in symbol.properties
    }
}

matchingBitfieldExists3: run {
    some bitfield : Bitfield, symbol : Symbol {
        #bitfield.positiveFlags = 2
        #symbol.properties = 2
        bitfieldMatchesProperties[bitfield, symbol]
    }
}

pred possibleState[filterState: FilterState] {
    some initialState: FilterState {
        // Each lookup in scope starts with empty filter flags
        bitfieldEmpty[initialState.curFilter]

        // The intermediate states (bitfieldMiddle) are used for sequencing of operations.
        some bitfieldMiddle : Bitfield {
            // Add "Public" depending on skipPrivateVisibilities
            addBitfieldFlag[initialState.curFilter, bitfieldMiddle, Public] or
            bitfieldEqual[initialState.curFilter, bitfieldMiddle]

            // If it's a method receiver, add method or field restriction
            addBitfieldFlag[bitfieldMiddle, filterState.curFilter, MethodOrField] or
            // if it's not a receiver, filter to non-methods (could be overridden)
            // addBitfieldFlagNeg[bitfieldMiddle, filterState.curFilter, Method] or
            // Maybe methods are not being curFilterd but it's not a receiver, so no change.
            bitfieldEqual[bitfieldMiddle, filterState.curFilter]
        }
    }
}

possibleStateExists: run {
    some filterState : FilterState | possibleState[filterState] and #filterState.curFilter.positiveFlags = 1
}

pred update[toSet: Bitfield + NotSet, setTo: FilterState] {
    toSet' in Bitfield and bitfieldIntersection[toSet, setTo.curFilter, toSet']
}

pred newUpdate[toSet: Bitfield + NotSet, setTo: FilterState] {
    (not bitfieldIncomparable[toSet, setTo.curFilter] and update[toSet, setTo]) or
    (bitfieldIncomparable[toSet, setTo.curFilter] and toSet = toSet')
}

pred updateOrSet[toSet: Bitfield + NotSet, setTo: FilterState] {
    (toSet in NotSet and toSet' = setTo.curFilter) or
    (toSet not in NotSet and update[toSet, setTo])
}

pred excludeBitfield[found: Bitfield + NotSet, exclude: Bitfield] {
    (found != NotSet and bitfieldEqual[found, exclude]) or
    (found = NotSet and bitfieldEmpty[exclude])
}

fact init {
    all searchState: SearchState | searchState.found = NotSet
}

fact step {
    always {
        // Model that a new doLookupInScope could've occurred, with any combination of flags.
        all searchState: SearchState {
            some fs: FilterState {
                // This is a possible combination of lookup flags
                possibleState[fs]

                // If a search has been performed before, take the intersection; otherwise,
                // just insert the current filter flags.
                updateOrSet[searchState.found, fs]
            }
        }
    }
}

counterexampleNotFound: run {
    all searchState: SearchState {
        // a way that subsequent results of searching will miss things.
        eventually some symbol: Symbol,
                        fs: FilterState, fsBroken: FilterState,
                        exclude1: Bitfield, exclude2: Bitfield {
            // Some search (fs) will cause a transition / modification of the search state...
            possibleState[fs]
            updateOrSet[searchState.found, fs]
            excludeBitfield[searchState.found, exclude1]
            // Such that a later, valid search... (fsBroken)
            possibleState[fsBroken]
            excludeBitfield[searchState.found', exclude2]

            // Will allow for a symbol ...
            // ... that are left out of the original search...
            not bitfieldMatchesProperties[searchState.found, symbol]
            // ... and out of the current search
            not (bitfieldMatchesProperties[fs.curFilter, symbol] and not bitfieldMatchesProperties[exclude1, symbol])
            // But would be matched by the broken search...
            bitfieldMatchesProperties[fsBroken.curFilter, symbol]
            // ... to not be matched by a search with the new state:
            not (bitfieldMatchesProperties[fsBroken.curFilter, symbol] and not bitfieldMatchesProperties[exclude2, symbol])
        }
    }
}