CS1950Y Lecture #12: Modeling Data Structures
February 21, 2018
Announcements
You can support or not support isolated nodes in your
topsort specification, we will allow both.
The Alloy 3 deadline has been extended 24 hours.
Context
Many of you have implemented a linked list in another programming language, such as Java.
In Java, we would have Node objects, with fields for items and the next node. To have access to the list at the beginning and end, we keep pointers to the start and the end of the list.
We want to ensure certain invariants are true.
Transitioning to Alloy
Today we will model a concrete data structure implementation and use Alloy to prove different properties above it.
Since the state of our list changes over time, we want a sig State ordered using util/ordering
open util/ordering[State] sig State {} sig LinkedList { start: ... end: ... }
What should we fill in for start
? Do we want to model null?
Since our linked list varies in each state, we define start
to be a relation that gives the first node in the list for each state
start: State -> lone Node, -- both of these are part of our ds end: State -> lone NodeWe do the same thing for end. Now we add a third field that will be useful for working with invariants. We want to check the list's behavior at a high level, like "this list has the nodes 1, 2, 3." So we add a relation that explicitly models what we expect to be in our list.
nodesInList: State -> (seq Node)
The parens are important! Without them, Alloy will be confused. As in Java, a Node contains two fields, the item and the next node.
sig Node { item: Int, nextnode: lone Node }
This isn't quite right, what have we forgotten? The states: a node can have different items and next pointers from one state to another.
sig Node { item: State -> Int, nextnode: State -> lone Node }
Q: Why are we allowing multiple lined lists, and not just baking it
into the State sig?
A: This would allow us to model functions on multiple lists,
like addAll or equals.
Starting to Run the Model
When we run the model, we get a tangled web of connections, and in the evaluator we see that the same state is mapped to multiple item values for a single node -- not good.
When we went from just Int to State -> Int, we lost Alloy's convenient default, which is a unary relation. Let's restore it manually:
item: State -> one Int
The instance's graph is still hard to look at. Project over state to make the visualization more convenient. In the theme settings, click on the item relation and choose to show it as an attribute rather than as an arc. Useful!
Now let's start making our model work. Since we're working with states over time, we will use the event idiom. We will make the Event sig abstract so we can have more specific types of events.
abstract sig Event { before: State, after: State }
sig AddFirst extends Event { newItem: Int, lst: LinkedList }{ -- implementation will go here later! }
Q: Should it be one Int?
A: For a single column field, one
is the default.
What are we missing about how the states evolve? What constraint do we need? Like in Tic-Tac-Tow, we only want to produce valid 'runs' of the linked list state. We need to make sure events connect pairs of states, but what else do we need? (Think induction!)
We need the base case, the initial state from which the list will evolve.
fact initialState { all l: LinkedList | { no l.start[first] -- nothing in list's start in first state no l.end[first] -- same idea for end l.nodesInList[first].isEmpty -- nothing in the list } }
What's up with the last constraint? We can't just use no because that would mean there would be no sequence, rather than an empty sequence, which is what we want.
Q: Is the empty keyword specific to sequences?
A: Yes, see the sequence documentation online.
Now we just add the standard trace fact, so our states evolve properly.
fact traces { all s: State - last | some e: Event | e.before = s and e.after = s.next }
Do we need to say some e: Event
or one e: Event
. It's cheaper to
say some, and if we get duplicate events, it is safe to ignore them.
Implementing the Logic
What do we need to do to implement our list. Think of what we might do in Java. - Allocate a new node - Make newnode.nextnode = start - Then reassign start pointer: start = newnode
Anything we're missing? Yes, we might need to deal with the end node. If end is null, the list is empty, and end becomes the new node.
Now Tim goes through the constraints quickly, for sake of time. We fill in the sig fact from earlier
sig AddFirst extends Event { newItem: Int, --- unchanged lst: LinkedList --- unchanged }{ all ol: LinkedList - lst | { ol.start[before] = ol.start[after] ol.end[before] = ol.end[after] ol.nodesInList[before] = ol.nodesInList[after] } some newNode : Node | { -- in Alloy, we can't say create a new thing, -- so we just say 'there exists some new node, let's make sure it's being used properly as our new node' -- this node is new, not been used before newNode not in Node.nextnode[before] newNode not in LinkedList.start[before] no newNode.nextnode[before] newNode.item[before] = newItem newNode.item[after] = newItem } -- not changing the value of any other nodes all n: Node - newNode | { n.item[before] = n.item[after] n.nextnode[before] = n.nextnode[after] } }
This is a high level description of adding a new node.
What can we do with this? We can check invariants on the list, to verify safety properties on the linked list so we can ensure our linked list is correct.
Quotes from Tim
- "If you didn't understand that, it's totes fine."
- "The book likes to use the Twitter operator (@)"