Closed alberdingk-thijm closed 3 years ago
The solution implemented in #65 performs partitioning after all other transformations have been completed. This means that map unrolling runs on the monolithic network and generates a larger-than-necessary unrolling for most partitions, but allows code to keep referencing cut keys without any issues post-unrolling. There may of course be ways to break this, but the brittle approach and restrictions for assertions described in the PR attempt to avoid this in the main case of concern, which is dealing with assertions over nodes in solutions.
This issue is to track progress on handling node and edge maps in kirigami, which currently cause issues when we remap nodes and edges once an SRP is partitioned.
Background
NV allows a user to create a total map (associative array) data type where the keys of the map are nodes or edges. This can be useful for various purposes, e.g. tracking which edges or nodes a given solution considers or uses. Maps in NV are total: that is, they are functions that return a value if it is present, or a default value if it is not. For instance, a
dict[tnode, bool]
containing the nodes0n
and2n
would returnfalse
(as a default) if we asked if3n
was in the map (map[3n]
).For reasons I am not familiar with, we have to unroll the maps into tuples before passing them into Z3 (I assume they are not supported). To do so, we replace instances of types like
dict[tnode, bool]
with a tuple, where each expression or symbolic variable we use to access the map is converted into an index: see the code here.As an example, suppose we have the following NV code (taken from
map.nv
):Here, I have a map (called a dict in NV) which maps from nodes to option integers, and returns None if an integer is not found. The simulation solution will be:
Here is the network NV looks at after map unrolling:
What's important to see here is that the attribute is now
(option[int32], option[int32])
. Hence, merge and trans now operate overUnrollingMapVars
(the first and second elements of the tuple) and the init function sets a value in the tuple.Where partitioning fails
Now, suppose I wanted to partition this network and compute the solutions separately.
First, the side containing
0n
:This all seems fine. We don't have anything too weird going on here, although I haven't given any restrictions on
h_1_0
yet. I did have to do something kind of ugly where(init 2n)
needs to actually run(init 0n)
, but I have a separate solution under consideration to avoid that.Now, let's see what map unrolling this program gives us.
In the interest of space, I've chosen to hide some of what merge and trans get unrolled to this time. The important thing is we now have a different attribute from before! Our attribute type is now a 3-tuple of
option[int32]
, where each index represents one of the nodes in this SRP, not the original SRP.So first off, we no longer have the same attributes in both the original SRP and the partitioned SRP. Could this be a problem? Well, fortunately for us, we still have a node named
1n
here, so we can actually write a require clause like this:This constrains
h_1_0
to haveSome 1
for the index representing the node1n
. This works, giving us the solution:But it only works because there happened to be a node named
1n
in the partitioned SRP. If instead we had this kind of network:And we cut the edge between
0n
and1n
, then the NV program for the side containing0n
would look exactly the same, but we wouldn't be able to create a dictionary referring to node 3n. So, this code would fail:We get back the following error:
This is a wellformedness check, so we could see what happens when we just tell NV not to check the wellformedness of our program. I recently added a flag allowing us to do that (very bad, I know, but useful for testing this). I'll only show the part that causes problems:
As you can see, 3n doesn't match any statement in the match pattern of
require
. The NV error we get back is:Much less clear than before.
Essentially, what this means is that any maps over nodes or edges aren't going to work if we unroll them before partitioning. You might say, that doesn't seem so bad, let's just do map unrolling after partitioning. But then we have a different problem: just doing map unrolling takes longer in a partitioned network than verifying the original network! How can this be? I'm not quite sure, but @DKLoehr believes it's due to the implementation of map unrolling.
An alternative option is to use integers instead of nodes. If I replace
type attribute = dict[tnode, option[int]]
withtype attribute = dict[int, option[int]]
, map unrolling produces this code:Here, the requirement on node
3n
(now just3
) was converted into index 1, since map unrolling only creates tuples to capture all of the integers we create in the program. As an aside, we explicitly forbid non-constant map keys with the exception of nodes and edges, so you wouldn't be able to write code that computes a new integer map key to add to the map forever.What this basically means is that because maps of nodes and edges are typed based on the network topology, we can't refer to nodes outside the partition in our maps. This also prevents us from doing something like:
NV will complain during map unrolling:
Now, at this point, I am inclined to ask, "Does checking for a node like
3n
violate the whole premise of hiding information about one SRP's internals from another?" Well, perhaps that is true, in which case we'd prefer to simply require that our hypothesis only let us talk about the information we receive from our neighbour on the other side of the interface, i.e. for this network:Our hypotheses should only be able to ask things about
0n
and1n
, but not2n
or3n
. In that case, because each cut edge introduces an input node, we can successfully remap any tests on extrapartition nodes to tests on the input node. This introduces a new wrinkle for how we should rewrite code like this:which is perfectly acceptable in the original network. Should it become:
One might like to know if the tests are bad to begin with, e.g.
For the topology above, this property is clearly false, but in order to determine that here, we would need to be able to recompute what values could be computed at
2n
and3n
without seeing that part of the topology. My belief is that this type of case is one where we simply can't help the user in the partitioned world: if we want to be able to test these types of programs efficiently, having to compute a way to go from a query on the value for2n
and the value for3n
here would be isomorphic to computing queries on the global network.Ways forward
As you can see, it's a tricky problem. Also to note is that currently, regardless of when map unrolling is performed, the code will still break if given this
require
clause on the0n
side:Hence, I think there are a few ways to solve this issue: