diff --git a/code/dyno-alloy/DynoAlloy.als b/code/dyno-alloy/DynoAlloy.als index da16130..4b3c5c4 100644 --- a/code/dyno-alloy/DynoAlloy.als +++ b/code/dyno-alloy/DynoAlloy.als @@ -10,8 +10,7 @@ sig Bitfield { /* A filter state has filterFlags and excludeFlags, both represented as conjunctions. */ sig FilterState { - , include: Bitfield - , exclude: Bitfield + , curFilter: Bitfield } /* Initially, no search has happeneed for a scope, so its 'found' is not set to anything. */ @@ -111,41 +110,43 @@ matchingBitfieldExists3: run { } } -pred configureState[filterState: FilterState] { +pred possibleState[filterState: FilterState] { some initialState: FilterState { - // Each lookup in scope starts with empty filter and exclude flags - bitfieldEmpty[initialState.include] and bitfieldEmpty[initialState.exclude] + // Each lookup in scope starts with empty filter flags + bitfieldEmpty[initialState.curFilter] - // The intermediate states (bf1) are used for sequencing of operations. - some bf1 : Bitfield { + // The intermediate states (bitfieldMiddle) are used for sequencing of operations. + some bitfieldMiddle : Bitfield { // Add "Public" depending on skipPrivateVisibilities - addBitfieldFlag[initialState.include, bf1, Public] or - bitfieldEqual[initialState.include, bf1] + addBitfieldFlag[initialState.curFilter, bitfieldMiddle, Public] or + bitfieldEqual[initialState.curFilter, bitfieldMiddle] // If it's a method receiver, add method or field restriction - addBitfieldFlag[bf1, filterState.include, MethodOrField] or + addBitfieldFlag[bitfieldMiddle, filterState.curFilter, MethodOrField] or // if it's not a receiver, filter to non-methods (could be overridden) - addBitfieldFlagNeg[bf1, filterState.include, Method] or - // Maybe methods are not being included but it's not a receiver, so no change. - bitfieldEqual[bf1, filterState.include] + 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] } - // Exclude filter doesn't change here - initialState.exclude = filterState.exclude } } -pred oldUpdate[toSet: Bitfield + NotSet, setTo: FilterState] { - toSet' in Bitfield and bitfieldIntersection[toSet, setTo.include, toSet'] +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.include] and oldUpdate[toSet, setTo]) or - (bitfieldIncomparable[toSet, setTo.include] and toSet = toSet') + (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.include) or - (toSet not in NotSet and oldUpdate[toSet, setTo]) + (toSet in NotSet and toSet' = setTo.curFilter) or + (toSet not in NotSet and update[toSet, setTo]) } pred excludeBitfield[found: Bitfield + NotSet, exclude: Bitfield] { @@ -163,7 +164,7 @@ fact step { all searchState: SearchState { some fs: FilterState { // This is a possible combination of lookup flags - configureState[fs] + possibleState[fs] // If a search has been performed before, take the intersection; otherwise, // just insert the current filter flags. @@ -178,21 +179,21 @@ example: run { // a way that subsequent results of searching it will miss things. eventually some symbol: Symbol, fs: FilterState, fsBroken: FilterState, exclude: Bitfield { // Some search (fs) will cause a transition / modification of the search state... - configureState[fs] + possibleState[fs] updateOrSet[searchState.found, fs] // Such that a later, valid search... (fsBroken) - configureState[fsBroken] + possibleState[fsBroken] excludeBitfield[searchState.found', exclude] // 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.include, symbol] and not bitfieldMatchesProperties[searchState.found, symbol]) + not (bitfieldMatchesProperties[fs.curFilter, symbol] and not bitfieldMatchesProperties[searchState.found, symbol]) // But would be matched by the broken search... - bitfieldMatchesProperties[fsBroken.include, symbol] + bitfieldMatchesProperties[fsBroken.curFilter, symbol] // ... to not be matched by a search with the new state: - not (bitfieldMatchesProperties[fsBroken.include, symbol] and not bitfieldMatchesProperties[exclude, symbol]) + not (bitfieldMatchesProperties[fsBroken.curFilter, symbol] and not bitfieldMatchesProperties[exclude, symbol]) } } } diff --git a/content/blog/dyno_alloy/index.md b/content/blog/dyno_alloy/index.md index 0161fb9..a89d688 100644 --- a/content/blog/dyno_alloy/index.md +++ b/content/blog/dyno_alloy/index.md @@ -205,7 +205,7 @@ mathematically, and have a model checker generate outlandish scenarios for me. Having at some point seen [Hillel Wayne's post](https://www.hillelwayne.com/post/alloy6/) about the release of [Alloy 6](https://alloytools.org/), I thought I'd give it a go. I'd never -touched alloy before this, so be warned: this is what I came up with on my +touched Alloy before this, so be warned: this is what I came up with on my own attempt. ### Modeling Flags and Bitsets in Alloy @@ -245,13 +245,13 @@ some condition holds for some arguments. This might seem abstract; as an example, here's `bitfieldEmpty`, which checks that a bitfield has no flags set. -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 27 29 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 26 28 >}} The `#` operator in Alloy is used to check the size of a set. So, to check if a bitfield is empty, I simply check if there are neither positive nor negative flags. Probably the most unusual aspect of this piece of code is that equality is written as `=`, as opposed to `==` like in most common -languages. This is because, like I said, alloy is all about relations and +languages. This is because, like I said, Alloy is all about relations and predicates, and not at all about imperative manipulation of data. So, there's no need to reserve `=` for assignment. @@ -259,7 +259,7 @@ The next step from here is a predicate that accepts two arguments, `bitfieldEqual`. As its name suggests, this predicate accepts two bitfields, and makes sure they have exactly the same flags set. -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 31 33 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 30 32 >}} So far, this has been pretty similar to just writing boolean functions in a language like C++. However, the similarity is only superficial. An easy way @@ -281,7 +281,7 @@ _does_ the output of a bitfield intersection connect to the two operands being intersected? Well, its two flag sets will be intersections of the flag sets of the inputs! -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 35 38 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 34 37 >}} Next, let's talk about what flags _do_. They are used to include and exclude symbols based on certain properties. One property is being a @@ -293,9 +293,9 @@ can have multiple properties (e.g., a public method). Unlike our bitfields, though, we won't be modeling symbols as having both positive and negative properties. That is to say, we won't have a "not public" property: the absence of the "public" property will be enough to make something private. -Here's the alloy definition for everything I just said: +Here's the Alloy definition for everything I just said: -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 60 64 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 59 63 >}} Now, we can specify how flags in a bitfield relates to properties on a symbol. We can do so by saying which flags match which properties. The @@ -304,7 +304,7 @@ The `MethodOrField` flag is more lenient, and will be satisfied by either `PMethod` or `PField`. Here's a predicate `flagMatchesProperty` that encodes all the flag-property combinations: -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 66 70 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 65 69 >}} A bitfield matching a symbol is a little bit more complicated. Said informally, the condition for a bitfield matching a symbol is twofold: @@ -319,20 +319,20 @@ informally, the condition for a bitfield matching a symbol is twofold: Each of the above two conditions translate quite literally into Alloy: -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 72 75 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 71 74 >}} We can read line 73 as "for each flag in a bitfield's positive flags, there must be some property in the symbol that matches it". Similarly, line 74 can be read out loud as "for each flag in the negative flags, no property in the symbol must match it". -We've written a fair bit of alloy. If you're anything like me, you might be +We've written a fair bit of Alloy. If you're anything like me, you might be getting a bit twitchy: how do we even check that any of this works? For this, we'll need to run our model. We will give Alloy a claim, and ask it to find a situation in which that claim holds true. The simplest claim is "there exists a bitfield". -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 77 79 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 76 78 >}} Executing this model yields a pretty interesting bitfield: one in which every single flag is set -- both the positive and negative versions. @@ -343,7 +343,7 @@ You can't be and not be a method at the same time, for instance. For for a more interesting example, let's ask for a bitfield that matches some symbol. -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 81 83 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 80 82 >}} The output here is pretty interesting too. Alloy finds a symbol and a bitfield that matches it, but they're both empty. In effect, it said: @@ -357,31 +357,31 @@ Let's try nudge it towards a more interesting case. I'm going to ask for a filter with one positive and one negative flag, and a symbol with two properties. -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 85 92 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 84 91 >}} The results are more interesting this time: we get a filter for private methods, and a private symbol that was... both a field and a method? {{< figure src="matching_bitfield_exists_2.png" caption="Alloy's spiced up output satisfying \"a bit field that matches a symbol exists\"" >}} -We never told alloy that a symbol can't be both a field and a method. It +We never told Alloy that a symbol can't be both a field and a method. It had no idea what the flags meant, just that they exist. To let Alloy know what we do -- that the two properties are incompatible -- we can use a _fact_. To me, the most natural way of phrasing this is "there is never a symbol that has both the method and field properties". Alas, Alloy doesn't have a `never` keyword; it only has `always`. So I opt instead for an alternative formulation: "there are always zero symbols that are -both methods and fields". In alloy, the claim looks like this: +both methods and fields". In Alloy, the claim looks like this: -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 94 99 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 93 98 >}} -Re-running the example program with this fact, alloy spits out a filter for +Re-running the example program with this fact, Alloy spits out a filter for public non-method symbols, and a symbol that's a public field. Public fields also aren't a thing in Chapel (all fields in a class are publicly readable in the current version of the language). Perhaps it's time for another fact. -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 100 104 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 99 103 >}} But now, Alloy fails to come up with anything at all. That makes sense: by restricting the search to a symbol with two properties, and making `PField` @@ -392,13 +392,255 @@ method, so making any of them negative would guarantee that our symbol would not be found. Let's change the example up a bit to only ask for positive flags. -{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 106 112 >}} +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 105 111 >}} -This time, alloy gives us a symbol that's a public method, and a filter +This time, Alloy gives us a symbol that's a public method, and a filter that only looks for public methods. Fair enough. {{< figure src="matching_bitfield_exists_3.png" caption="Alloy's spiced up output satisfying \"a bit field that matches a symbol exists\"" >}} +### Exploring Possible Search Configurations +So now we have a descriptioin of filters and symbols in scopes. The next thing +on the itinerary is modeling how the filters (include and exclude) are +configured during scope search in Dyno. For this, let's take a look at the +C++ code in Dyno. + +I'll be using the branch that I was working on at the time of trying to +apply Alloy. First, here's the code in C++ that defines the various flags +I'd be working with (though I've omitted flags that are not currently +used in the implementation). + +```C++ + enum { + /** Public */ + PUBLIC = 1, + /** Not public (aka private) */ + NOT_PUBLIC = 2, + /** A method or field declaration */ + METHOD_FIELD = 4, + /** Something other than (a method or field declaration) */ + NOT_METHOD_FIELD = 8, + /** A method declaration */ + METHOD = 64, + /** Something other than a method declaration */ + NOT_METHOD = 128, + }; +``` + +These are the flags that we model using a `Bitset`: `PUBLIC`, +`METHOD_FIELD`, and `METHOD` are modeled using `positiveFlags`, and +`NOT_PUBLIC`, `NOT_METHOD_FIELD`, and `NOT_METHOD` are modeled using +`negativeFlags`. There are a lot of flags here, and it's not hard to +imagine that _some_ combination of these flags will cause problems in our +system (particularly when we _know_ it's an approximation). However, the +flags aren't used arbitrarily; in fact, it wasn't too hard to track down the +most important place in the code where bitsets are built. + +```C++{linenos=true, linenostart=914} + IdAndFlags::Flags curFilter = 0; + /* ... some unrelated code ... */ + if (skipPrivateVisibilities) { + curFilter |= IdAndFlags::PUBLIC; + } + if (onlyMethodsFields) { + curFilter |= IdAndFlags::METHOD_FIELD; + } else if (!includeMethods && receiverScopes.empty()) { + curFilter |= IdAndFlags::NOT_METHOD; + } +``` + +The above code converts the current search parameters into `Bitfield` +flags. For instance, if a `use` statement is being processed that doesn't +have access to private fields, `skipPrivateVisibilities` will be set. +On the other hand, if the calling code didn't explicitly ask for methods, +and if there's no method receiver, then the last condition will be true. +These various conditions are converted into bits and applied to +`curFilter`. Then, `curFilter` is used for looking up symbols in a scope. + +It's not too hard to model this by just looking at the code, and +enumerating the possibilities. The first `if` statement can either be true +or false, and then the subsequent `if`-`else` chain creates three +possibilities in each case: either `METHOD_FIELD` is set, or `NOT_METHOD`, +or nothing. + +However, I envisioned this condition to possibly grow in complexity as more +search configurations became necessary (in that, the `NOT_METHOD` option +was an addition in my new branch). I therefore chose to model the possible +`Bitfield` values more faithfully, by mimicking the imperative C++ code. + +{{< dialog >}} +{{< message "question" "reader" >}} +Wait, something sounds off. Just earlier, you said Alloy "is not at all about +imperative manipulation of data". But now, we're going to mimic plain +imperative C++ code? +{{< /message >}} +{{< message "answer" "Daniel" >}} +Alloy the programming language is still not imperative. However, we can +model imperative behavior in Alloy. The way I see it, doing so +requires us to venture a tiny bit into the realm of semantics +for programming languages, in particular for imperative languages. This +"venture" is very minimal though, and you really don't need to know much +about semantics to understand it. +{{< /message >}} +{{< message "question" "reader" >}} +Alright. How does one model imperative behavior in Alloy? +{{< /message >}} +{{< message "answer" "Daniel" >}} +On to that next. +{{< /message >}} +{{< /dialog >}} + +The essential piece of insight to modeling an imperative language, though +it sounds a little bit tautological, is that _statements are all about +manipulating state_. For example, state could be the value of a variable. +If you start with the variable `x` storing the number `6`, and then execute +the statement `x = x * 7`, the final value of `x` will be `42`. Thus, state +has changed. To put this in terms Alloy would understand -- relations and +sets -- a statement connects (relates) states before it's executed to states +after it's executed. In our particular example, the connection would +between the state `x = 6` and the state `x = 42`. In the case of adding the +`PUBLIC` to `curFilter`, as on line 917 in the above code block, we could +state the relationship as follows: + +```Alloy +addBitfieldFlag[bitfieldBefore, bitfieldAfter, Public] +``` + +The above code states that `bitfieldAfter` (the state _after_ line 917) is +the same `Bitfield` as `bitfieldBefore` (the state _before_ line 917), except +that the `Public` flag has been added to it. + +Things are a little more complicated when it comes to modeling the whole +`if`-statement on line 916. If we wanted to be very precise, we'd need to +encode the other variables (such as `skipPrivateVisibilities`), how they're +set, and what values are possible. However, for the sake of keeping the +scope of this model manageable for the time being, I'm content to do +something simpler -- that is, acknowledge that the code on line 917 may or +may not run. If it does run, our previous `addBitfieldFlag` will be the +correct restriction on the before and after states. However, if it doesn't, +the state shouldn't change at all. Therefore, we can model lines 916 +through 918 as follows (notice the `or`): + +```Alloy +addBitfieldFlag[bitfieldBefore, bitfieldAfter, Public] or +bitfieldEqual[bitfieldBefore, bitfieldAfter] +``` + +The next thing to note is that there are two `if` statements one after another. +The state "after" the first statement is one and the same as the state +"before" the second statement. Using arrows to represent the "before-after" +relationship created by each statement, we can visualize the whole +situation as follows: + +{{< latex >}} +\text{initial state} +\xRightarrow{\text{first statement}} +\text{middle state} +\xRightarrow{\text{second statement}} +\text{final state} +{{< /latex >}} + +We'll write our Alloy code to match: + +```Alloy +/* First if statement */ +addBitfieldFlag[bitfieldBefore, bitfieldMiddle, Public] or +bitfieldEqual[bitfieldBefore, bitfieldMiddle] + +/* ... something connecting bitfieldMiddle and bitfieldAfter ... */ +``` + +From here, we can handle the second `if`/`else` chain in the same way we +did the first `if`-statement: by making all three outcomes of the chain be +possible, and creating an `or` of all of them. + +```Alloy +/* First if statement */ +addBitfieldFlag[bitfieldBefore, bitfieldMiddle, Public] or +bitfieldEqual[bitfieldBefore, bitfieldMiddle] + +/* Second if statement */ +addBitfieldFlag[bitfieldMiddle, bitfieldAfter, MethodOrField] or +addBitfieldFlagNeg[bitfieldMiddle, bitfieldAfter, Method] or +bitfieldEqual[bitfieldMiddle, bitfieldAfter] +``` + +So that helps model the relevant Dyno code. However, what we really want is +an Alloy predicate that classifies possible outcomes of the piece of code: +is a particular combination of flags possible or not? Here's the piece of +Alloy that does so: + +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 113 132 >}} + +The `FilterState` on the first line (and elsewhere, really), is new. I'm +trying to be explicit about the state in this particular computation. Its +definition is very simple: currently, the only state we care about is the +`Bitfield` corresponding to `curFilter` in the C++ code above. + +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 12 14 >}} + +There's not much more to the predicate. It says, in English, that a state +`filterState` is possible if, starting from an empty initial state +`initialState`, the model of our C++ code can end up with its particular +set of flags in the `curFilter` bitfield. + +Next, I needed to model the behavior the I described earlier: searching for +\\(A \\land \\lnot B\\), and taking the intersection of two past searches +when running subsequent searches. + +Dyno implemented this roughly as follows: + +1. It kept a mapping of (searched scope → search bitfield). Initially, this + mapping was empty. +2. When a scope was searched for the first time, its `curFilter` / search + bitfield was stored into the mapping. +3. When a scope was searched after that, the previously-stored flags in the + mapping were excluded (that's the \\(A\\land\\lnot B\\) behavior), and + the bitfield in the mapping was updated to be the intersection of + `curFilter` and the stored flags. + +We'll simplify the model by doing away with the mapping, and considering +only a single scope that is searched many times. We'll represent the stored +flags as a field `found`, which will be one of two things: either a +`Bitfield` representing the previously-stored search configuration, or a +`NotSet` sentinel value, representing a scope that hasn't been searched +yet. The Alloy code: + +{{< codelines "alloy" "dyno-alloy/dynoalloy.als" 21 23 >}} + +The `NotSet` sentinel value is defined in a very simple way: + +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 17 17 >}} + +Both of these signatures use a new keyword, `one`. This keyword means that +there's only a single instance of both `NotSet` and `SearchState` in our +model. This is in contrast to a signature like `Bitfield,` which allows +multiple bitfields to exist at the same time. I ended up with a pretty +similar predicate that implemented the "store if not set, intersect if set" +behavior in Alloy: + +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 147 150 >}} + +The first line of the predicate represents the second item from the list +above: if a scope hasn't been searched before, the new value is just the +current filter / bitfield. The second line handles the third item: +updating a previously-set filter based on new flags. I defined an +additional predicate to help with this: + +{{< codelines "Alloy" "dyno-alloy/DynoAlloy.als" 138 140 >}} + +At first, this predicate doesn't seem like much. However, if you look +closely, this single-line predicate uses a feature of Alloy we haven't +really seen: its ability to reason about time by dipping into _temporal logic_. +Notice that the predicate is written not just in terms of `toSet`, but also +`toSet'`. The tick (which I personally read as "prime") indicates that what +we're talking about is not the _current_ value of `toSet`, but its value at +the _next moment in time_. What this predicate says, then, is that _at the next +moment_, the value of `toSet` will be equal to its present value, +intersected with `curFilter`. I also had to specify that `toSet'`, the +future value of `toSet`, will still be a Bitfield after the step, and would +not revert to a `NotSet`. + {{< todo >}} The rest of the article {{< /todo >}} @@ -408,6 +650,14 @@ The rest of the article This section is temporary {{< /todo >}} + +More abstractly, +{{< sidenote "right" "notation-note" "we could denote the claim" >}} +This is in no way standard notation; I'm making it up as I go along. +{{< /sidenote >}} +that some state \\(A\\) is changed to state \\(B\\) by a statement \\(s\\) +as follows: + a small-ish program to illustrate what I mean. ```Chapel