Replies: 18 comments 21 replies
-
ImplementationI implemented utility functions:
Finding dominatorsThe The rest of the Testing Finding DominatorsBefore moving onto building the other util functions, I wanted to be confident of my solution since the other functions would rely on this one. I created a test script which takes a bril program along with a command line argument. I added support for a It works by calling
And to build each path in the first place also has poor time complexity. We must DFS from the start node to all possible exit nodes and concat those lists together. So in order to test this, the complexity is large, but we are very confident our solution is correct. And note that the complexity to actually find the dominators is solved fast. I ran this against all programs in Building a Dominator TreeI initially tried to implemented the naive approach to building a dominator tree from this paper. However, I felt the wording used in the paper was not descriptive enough to recreate this on my own in a straightforward manner. Instead, I opted to use the mapping returned from the The time complexity of this is bounded by the time that it takes for Testing Dominator TreeI testing this by writing an
I ran this against all benchmarks and passed. This makes me very confident of my solution. The time complexity of the test is quite cheap as well. It also passes against the turnt output in the Print TreeI also opted to implement a function that printed out the dominator tree for graphviz. I manually inspected for the files in Implementing Dom FrontierImplementing The complexity for this function is O(mn) because each node in the tree recursively processes all successors within each recursive call. It is likley that Testing Dom FrontierTesting this was also straight forward.
I ran this for all in benchmarks and passed. The time complexity of this test is worse than checking the OverallOverall, I feel quite confident with my solution because of thorough testing, felt that I had nice clean code, and felt that I worked hard to have good time complexities for my solution. I also felt that I have a much more intimate understanding of domination theory after building and testing my implementation. I'm quite happy about how all this came out. |
Beta Was this translation helpful? Give feedback.
-
ImplementationMy implementation is here.
IdeasFinding dominatorsI followed the pseudo code to take intersection of each predecessors's dominator and adding itself. Build dominance treeThe idea is to identify the immediate dominators in each node's set of dominators. My implementation checks if a vertex's dominator dominates other dominators in the set, if it doesn't, then it's an immediate dominator. Find domination frontierMy implementation checks the successors of vertices dominated by the current vertex. If it is not dominated by the current vertex, it's in the frontier. A new thing for me is that a vertex's dominance frontier can contain itself. VerificationI implemented a verifier to check if the domination relation is correct. For a given vertex and its dominators, the verifier visits all the predecessors of the vertex and make sure every upstream path goes through every dominator. If any upstream path doesn't go through a dominator when it meets the root (entry) block, the verification fails. DiscussionsFinding Dominators with Worklist AlgorithmI remember someone brought up whether we can use last lesson's worklist algorithm to find dominators. I tried this idea and it worked. The implementation is quite straightforward: VisualizationI find it very helpful to visualize the CFGl. I may be the last one to know about graphviz for visualization. I implemented a simple visualizer to plot CFG from a Bril function: |
Beta Was this translation helpful? Give feedback.
-
In this task, I implemented several utility functions to construct the following sets of nodes based on the definitions:
All of them are very straightforward. Code can be found in my repository. DominatorsThe update rule of the dominator is very simple, and I can write it using higher-order functions ( Another tricky thing here is that I first thought entry block should be the block that has no predecessors, but later I found I was wrong -- if the entry point of a program is a loop, then it may have a backedge which means the first block in the program even has a predecessor. Thus, to make things work normally, the best way is to automatically generate an Dominance TreeTo construct the dominance tree, we should first get the strict dominators and find the immediate dominators. Finding strict dominators is easy, which just needs to eliminate the node itself from the dominator set. For finding the immediate dominators, we should follow its definition -- for all the node B test its strict dominators, if some node A is not in the dominator sets of the other node that strictly dominates B, then A is the immediate dominator of B. After we get the immediate dominators, we have already got the edges for the dominator tree. Thus, I built a class Dominance frontierWe have the strict dominators, and constructing domination frontier is also straightforward -- "A’s dominance frontier contains B iff A does not strictly dominate B, but A does dominate some predecessor of B". Though the implementation is easy, the result is surprising. From the TestingsFor testing, I used a brute force method to test if some nodes are exactly the dominators of a given node. Basically, I used DFS to do the CFG traversal and generate paths from the entry point to the given node. A stack-like structure is maintained. Each time a node is visited, it will be added to the path, and at the end of an iteration, the newly added node will be popped out to restore the previous state. When the traversal reaches the destination node, this path will be added to a global path list. Finally, I test if the dominator is contained in all the paths. If so, the implementation is correct. |
Beta Was this translation helpful? Give feedback.
-
ImplementationIt's here.
I do not support any command line options, as my testing strategy does not involve exposing these functions. dominatorscopy and paste from the lecture really, the most interesting part was probably checking for termination, which cleanly fits into reducing over a list. dominance-treeThere is almost definitely a better way to do this. I took the definition at face value, and started with a map of dominators and removed anything that wasn't immediate. dom-frontierImplemented by calculating a "potential frontier" of all of the successors of the dominatees, and then removing those which are dominated. I messed this up initially since i didn't get the strict/non-strict parts correct. testingFor this assignment, I thought back to my 3110 days of reading the pragmatic programmer, and decided not to test the dominators since they are a very internal mechanism. Instead, I wanted to implement something that used them, and then test that. I know based on how i implemented it that if the dominance frontier is correct, the rest is probably right since it is heavily dependent on the other stuff. I chose to implement SSA as my testing strategy, since it was familiar content from 4120, and the reference interpreter can execute the φ instruction. This means i can convert a program to SSA, and then run it back through the interpreter and the output should be the same, which is pretty cool. I used the benchmarks in the repo for this. This testing revealed the bug that I had messed up the strict/non strict stuff with the dominance frontier. I also found the bug that I wasn't dealing with arguments correctly in SSA, but that's not really a dominance bug. discussionThis assignment didn't present too many major issues, and I got back into a more comfortable workflow where most of my testing was done at the REPL. This basically gives the power of a really good debugger where I can interact with the code and dynamically recompile it, so it is significantly faster to work with than command line based testing, though that became important to verify that I was done. |
Beta Was this translation helpful? Give feedback.
-
Dominator UtilitiesI implemented the three dominator utilities in Haskell. The main reason for doing it in Haskell was (aside from the fact that it's fun) I wanted the ability to test my implementation using QuickCheck. My Main.hs file takes in a JSON Bril program and prints the output of a dominator utility. There is support for the following:
Using it is as simple as a TestingFuzzing with QuickCheckThis was arguably the more fun part. I wrote an
So I resorted to admitting arbitrary CFGs with the change that its blocks have exactly one successor with high probability (to mimic "real" programs; I tried to find some statistics online about the real world programs' CFGs but couldn't). This had two effects; this uncovered a lot of edge cases in my implementation (around CFGs with unreachable blocks) and since this made my CFG generation quite fast I was able to run 10000s of tests which made me quite confident in my implementation (when I later fixed those edge cases). SpecificationI wrote properties that the three utility functions should satisfy, briefly summarized below:
Results and ExperienceI manually verified the output of my functions on the examples given in
Implementing first the JSON conversion utilities and then all this in Haskell was a lot of work. I had to think very hard about how to represent the computation of dominators etc. purely declaratively which gave me a greater sense of what these structures are rather than just how to compute them, for example finding a succinct expression for strict dominators, and children in a domination tree was quite challenging. QuickCheck also quickly decimated many first attempts, and I discovered a lot of subtleties in the definitions. For instance, when finding the dominators, you must initialize the dominators for the entry block to itself and never update it. Another problem was in cases where the CFG is not completely reachable from the entry block, in which case the dominators of a block that cannot be reached from the entry block must be an empty set. Another case was the realization that the domination frontier of a block can contain itself. |
Beta Was this translation helpful? Give feedback.
-
SummaryMy implementation is found at: dominator-utilities and tests are at tests. I implemented the dominator utilities suggested in lesson 5, finding the dominators of a node, the strict dominators, the immediate dominator, the dominator tree and the dominance frontier. I followed the pseudocode and definitions for each of these constructs from the lesson 5 page, almost exactly as stated. For the most part, I found that these definitions worked intuitively, and the algorithm appeared to work, except for some small situations I discovered during testing. TestingI wrote several test programs to check my implementation of the utilities. These programs have different structures, such as loops that include the entry, unreachable blocks of code, and one that is a small web of basic blocks. I made these programs small so that I could verify the result manually, by calculating each of these dominator results by hand, and then checking with the program's calculated results. ResultsHere's a simple program that I caused me some trouble with debugging my implementation:
and the results
Something left desired is making larger CFGs to test on, and making these CFGs be examples of "tricky" edge cases. To also help validate that the dominators are calculated correctly, I have a function called check_dominators, which verifies that node B really is dominated by node A if every path from the entry to B contains A in it. I don't allow loops in these paths, because I reasoned even if there was a loop, we could remove the loop and continue trying to find our way from the entry to B. To calculate these paths, I repeatedly use DFS from the entry node and search my way towards node B. This validation function is called every time the get_dominators function is called, to validate the result actually makes sense. I run this on the turnt tests, and no exception is raised, meaning that the validator function cannot find a counter example path to the dominators I found. I also do the same for calculating dominance frontiers, where I make sure there is at least one predecessor node, of the node in the frontier, which is dominated by the original node. This validation function is also called every time build_domination_frontiers is called. DifficultiesImplementing dominators ended up being a bit trickier than I imagined. In particular, I was caught off guard by some edge cases when computing dominators, which ended up exposing some confusion that I had with the dominator definitions. This caused me to change some implementational details when calculating dominators. For example, I wrote an example program where the CFG consists of 2 basic blocks (A and B, A being an entry), forming a loop. The naive algorithm computes A as being dominated by both B and A, while B is dominated both A and B. This does not make sense as the path from the entry to the entry does not include B in it. I changed the algorithm to only consider A to be dominated by itself. Another problem arising over computation of dominators is with unreachable blocks of code. By definition, these unreachable blocks of code should be dominated by every basic block, since there is no path from the entry to these unreachable blocks. However, this produces unintuitive results later on, when computing the dominator tree and the dominance frontier. I decided to do a DFS to find all the reachable blocks of code, and set the dominators of all unreachable blocks to be only the entry block of the CFG. I don't think this is a justified decision, though it appears to give more intuitive results when calculating dominance frontiers in testing. I also had a few issues with finding a unique immediate dominator, but this was fixed through some debugging, and realizing that I had implemented one of the sub-algorithms incorrectly. |
Beta Was this translation helpful? Give feedback.
-
ImplementationThe implementation can be found here. The code can be run with one argument:
To compute the dominators in a CFG, I first checked whether the first block had any predecessors. If it did, then I added an entry node (with a fresh name) with its successor being the first block. Then I followed the pseudocode we went over in class. To generate the dominance tree, I first computed the dominators of the CFG. Next, I computed the inverse of this dictionary, which maps blocks to blocks they dominate. Then I iterated through each To generate the dominance frontier, I first computed the dominators of the CFG. For each pair TestingThe test file can be found here. The arguments are the same as in I wrote a function DiscussionI briefly struggled to figure out why this test case that I added was marking jmp3 as a dominator of jmp1. This ended up just being a bug in the function I wrote that adds an entry node- I had accidentally wrote the function so that even if the first block in the CFG had an incoming edge, it wouldn't add a new entry node. |
Beta Was this translation helpful? Give feedback.
-
The pull request for L5 at my repo can be found here: gsvic/CornellCS6120#2 What's New
ImplementationThe code for L5 can be found here. All implementations were added ass extra methods to the already existing CFG class. MethodsDominatorsImplemented as the Dominator TreeImplemented as the Domination FrontierImplemented as the Check if a Block Dominates Another BlockI created a custom method that checks the validity of the output of the
In summary, it recursively traverses all the possible paths from An additional test for this method is added in the unit tests. TestingUnit tests for each case are provided in the test directory. I borrowed all the test-cases from |
Beta Was this translation helpful? Give feedback.
-
My code is here: atucker/bril#4 ImplementationI implemented
Finding dominatorsThis mostly worked the way I would have expected it to from the video, though python's reduce returns the starting output if there's nothing that it gets reduced with, so I needed to special-case the situation for when there's no predecessors. Dominator treeFor a while I was trying to keep track of a structure which, for each node in the dominator tree, kept track of its ancestors. It took me a bit to realize that this should always be the dominator set, and so I didn't need to keep track of it separately. Dominator frontierThe main trick here for me was realizing that predecessors meant predecessors in the CFG, not in the dominator tree, and I was confusedly trying to figure out how something could dominate a node but something that it dominated. After that it was pretty straightforward. TestingI tested the dominators, dominator tree, and dominator frontier code using the tests in the examples folder, with everything passing. I also tested the correctness of my dominator finding in a more involved way described below. PrintingThe most fun part of the assignment for me was reading the prompt at the end to figure out how to test your code. My first thought was "I'll just add print statements that tell me whenever I enter a new block". But then I noticed that basic BRIL doesn't actually have strings... Instead of fixing this by adding strings, I realized that I could just encode short strings as integers while adding the print instructions to the bril program, print the integers while the bril program runs, and then write a python program to convert the integers back into strings. ASCII uses 8 bits per character, and looking through an ascii table I decided that I could get most of what I want by just encoding SPACE - ? and ` - DEL. This is only 64 characters, so I could encode it using 6 bits. Since BRIL uses 64 bit strings, I figured this meant I could have a 4 bit prefix followed by 10 characters which each use 6 bits. For simplicity sake, I right-padded every string with SPACE, so I could just take every 64 bit integer that starts with my prefix and convert it into the corresponding string. After setting this up, I made a turnt directory where I modify the bril programs to print out which cfg block they're in, run the modified program, save it, and then run a program to check that whenever we see a print statement for a cfg block entry (i.e., any bril output which got covered to a string) the only blocks we've seen before are in the dominator set for the block label we just got to. The turnt script is here. After that, I filled the directory with some of the other tests we've used. The check code just prints True if everything is okay, so after checking that all the programs resulted in True, I created the .out files. This setup isn't great in that it doesn't handle >10 character strings at all, but I think a future extension might just be to have a convention where we end on the DEL character (unusable AFAICT in BRIL) whenever we want to append the next string to the current one. I could also imagine a more thoughtfully chosen character set, perhaps by just listing a set of substitutions to convert into/out of in the character encoding/decoding functions. |
Beta Was this translation helpful? Give feedback.
-
My code is here. The dominator functions are under dom.py, and the test files are under test/dom. ImplementationGet Dominators
Get Domination Tree
Get Domination Frontier
TestingTo test |
Beta Was this translation helpful? Give feedback.
-
My implementation is here. Usage
DominatorsThis was a fairly straightforward implementation from the algorithm in class. However, I also encountered my first major bug which I figured out from testing with my Dominator TreeNow I needed to invert the relationship and to build the tree a bit easier from the top down. I also needed to make it immediate to get the levels on the tree and strict to avoid self references. The tree is just a tuple tree. Dominance FrontierI was a bit confused about the definition at first, particularly with the meaning of predecessors, but I eventually understood it meant the predecessors of the CFG. Then the implementation was pretty straight forward, but it is something that I struggled with for a while. TestingI have a pretty basic |
Beta Was this translation helpful? Give feedback.
-
My code is here Implement dominance utilities
Examples:
Test CorrectnessAll the testcases under
Test the Implementations AlgorithmicallyI use DFS (Depth First Search) algorithm on the Control-Flow-Graph to test the implementations. Using DFS traversal, I can get all the possible paths from Entry to B. Then I check if every path include A: if so, A dominates B correctly; otherwise, A is not the dominator of B. The code passes all the testcases. If I manually make it wrong as follows in the benchmark
The testing function would print the error message as follows:
DiscussionIt is worth to note that when testing the |
Beta Was this translation helpful? Give feedback.
-
My work is here, split into two files:
It was a reasonably straightforward process, more or less proceeding as discussed in class. I found myself wrestling with Python a fair bit; I'm realizing that I really am a functional programmer at heart and so I tend to want/expect things like partial application and immutability. That said, who among us can argue against a Python gem like I tested my work using Turnt the whole time, borrowing from the course repository's examples/test/dom/ and also adding skipped.bril from a previous lesson. I'm glad I did, since it proved to be an interesting example (see below). Compared to what others seem to have done re: testing, my work is quite meagre, essentially just "testing by eye". Plus, it wasn't even that easy: variations in Python's set-ordering meant that Turnt repeatedly threw false alarms. I ran into two interesting issues: Issue 1: Entry labels that have predecessors. I was a bit stumped but then found a helpful thread about it on Zulip. I followed the advice given there and was out of the woods pretty quickly. Many thanks to @5hubh4m for posting that. Issue 2: My example skipped.bril shows what happens when one tries to compute the domination relation using a CFG that is malformed (for some definition of malformed). The CFG marks "l0" and "l1" as the preds of "end", but in fact "l1" is unreachable (it has no preds) and will always be skipped. Having it around in the CFG causes an incorrect calculation of the dominators. Had we run a cleanup pass using CFG data before running the domination algorithm, we would have killed off the unreachable block "l1" and avoided this issue. |
Beta Was this translation helpful? Give feedback.
-
Sorry for the late post! My code can be found here. Find DominatorsI provide a function find_dominators to find the dominators of a given CFG. This function uses the algorithm given in class to find dominators. Vertices are iterated in reverse post-order to achieve linear runtime on reducible CFGs. Special care is required for the entry node, which must be initialized correctly and not modified during the iteration. Output is given as a HashMap from String to a HashSet of Strings that provides the dominators for that block. find_dominators_num is also provided which returns the same output, but with block numbers from the CFG rather than block names. This method makes it easier to access information about a block in the CFG. Form Dominance TreeThe dominance tree is represented as a pair of rust structs, using the arena allocation pattern. Each tree node has a block label, a parent index, and a vector of child indices. The tree is constructed by first finding the immediate dominators, which is determined using the definition directly, then using the immediate dominators to construct edges in the tree. Get Dominance FrontierThe dominance frontier is computed directly using the definition that A's dominance frontier contains B iff A does not strictly dominate B, but A does dominate some predecessor of B. The dominance frontier is also represented as a HashMap from a String to a HashSet of Strings. TestingA testing framework for dominators is built in Arena AllocationOne of the more challenging parts of this assignment for we was how to represent the dominance tree correctly in Rust. I struggled for a bit trying to use a more C-like style to point tree nodes to each other, but ended up learning about the wonders of arena allocated data structures. In this pattern, all nodes are owned by a vector in a parent struct, and then the "pointers" are actually just indexes into the vector that owns all the nodes. Using this pattern you are able to completely avoid the challenges of ownership and borrowing that arise in tree-like data structures. Example arena allocation pattern:
|
Beta Was this translation helpful? Give feedback.
-
Sorry for late post. My implementation is here. One might also want to check out the related files Find dominatorsI implemented a function I wrote a helper function Before going on I should mention that the dominator map is represented as a bidirectional graph where edges are unlabelled. Thus the successors of each node is exactly its dominators. More particularly, The function Dominator treeThe dominator tree is also represented as a graph. It is actually just a BFS on the graph starting from the entry node. I attempted to do the BFS functionally, but it felt quite unnatural, so i resorted to imperative ways. I used a stack to basically store the edges of the tree to be constructed, a queue for the BFS algorithm, and a Hashset to keep track of which nodes have been visited. All of this is inside a while loop. (I haven't written while loop many times in OCaml, and it felt quite satisfying...) Oh and when doing the BFS, self-edges are ignored. Dominance Frontier[TODO edit later] I haven't finished implementing this, but it should be easy. TestingI have written a few property tests to ensure that my dominance graph is working properly. The tests require that a given dominance graph is a partial order. In graph theoretic terms, every node needs to have a self-loop(reflexivity), there can be no loops except for self-loops(antisymmetry), and if node v is reachable from v by some path then there is an edge from u to v(transitivity). However these tests were written before I changed some type signatures, so I will need to update them. [TODO test dominance tree]. Haven't gotten around to implementing this, but I plan on first checking that it is in fact a tree: there is a unique path between any two nodes. This can be checked efficiently by doing a BFS, DFS, or whatever graph search starting from the entry node. For a graph to be a tree one must not be able to visit the same node twice. I believe that given a dominance tree, if one takes the transitive closure of tree and then add self-loops to each node, it should give you back the original dominator graph. These two properties combined should suffice to show that the tree is correct. DifficultiesI spent many many more hours on this task than I should have because I kept switching my implementation around to make my program structure more "elegant", whatever that means... If you dig into my git commit history(with the very unhelpful commit messages), you can see that I fiddled around with many different implementations of the dominance map. I even changed the cfg representation many times. Initially my dominance map is just a dictionary from strings to string sets. However the step of inverting the edges to get the dominance tree(isn't it really the submissive tree?) felt clunky, so I tried using the ocamlgraph library. But that thing isn't suitable for what I want because it doesn't view vertices with the same label as the same vertex... So I wrote an entire generic graph module from scratch, which is now being used for both the cfg and the dominance analysis. In hindsight this is really silly, but I did have quite a bit of fun in the process. It also allowed me to output graphviz files from generic graphs, like this: Updates:{ Second of all, finding the dominator tree is not as simple as doing a BFS on the dominator graph. In fact, since the entry dominates everything, BFS just gives you a tree of depth 2 that looks like a leaf rake... I resorted to just filtering the "dominated nodes" of b by the idom relation, which is inefficient but intuitive. Testing -I did a few more tests ensuring that the dom tree is correct. } |
Beta Was this translation helpful? Give feedback.
-
Sorry for the late post. Here is my implementation The implementation for all three tasks is under examples/self_dom.py. The codes are fairly straight forward, but I did not have enough time to actually refine it (Sorry about that), so it's more like a proof-of-work and might not be edge-case-prone. Find DominatorsMy implementation works exactly in the same way as pseudo-code provided in lecture. Initially, I tried to set up the dom relation such that every block maps to empty set from the very beginning, as I feel like this is more intuitive and should work as intended. However, it turned out that having every block maps mapped to all blocks is more efficient. Because the first approach requires a couple of ramping up iterations. The rest is fairly straight-forward and I didn't have much issues. The only thing I concerned about is that I reused the code from Dominator treeThis might be the hardest part of the assignment and took me sometime to actually get the algorithm right. Still, my implementation on Dominator Tree might be overcomplicated and inefficient. I used multiple loops over the computed
Dominance FrontierThis is the easiest part of this assignment. I basically just followed the definition and add the set of vertices that satisfy the properties. However, my implementation is inefficient (O(n^2) worst case) and I still need to figure out how to make it faster. TestingI have tested my implementation on all tests under test/dom, with turnt and human observation. At the moment, I am still writing tests for edge-cases. |
Beta Was this translation helpful? Give feedback.
-
Hi, sorry for the later post too. My implementation is here. It includes passes for:
Some example of the output for
I also added the general test pass for finding dominators. Here, I just implemented the very naive DFS-based walk across the CFG to get all possible paths between the root node and the target node and checked that all the paths indeed include the dominant that we are verifying. The algorithm repeats this for all dominants and for all nodes. I tested the implementation of the dominators finder with the test above, and for other passes, I just verified the output manually on the given test cases and some other manually-created test here. |
Beta Was this translation helpful? Give feedback.
-
My implementation is here.
|
Beta Was this translation helpful? Give feedback.
-
In this task, you have implemented some fundamental operators on CFGs involving dominators!
Beta Was this translation helpful? Give feedback.
All reactions