Replies: 25 comments 33 replies
-
I implemented the live variables analysis as a backward pass and then constructed a forward pass by moving the core loop into its own function and passing all the swappable arguments swapped. I implemented interval analysis as a forward pass and I was pleasantly surprised by how well it worked. It was surprisingly easy to add a new analysis once I had the framework in place. Live Variables This was pretty uneventful. I ended up not putting my worklist into topological sort order. All of my test programs were short enough that I didn't need the speedup and I have implemented topological sort before so it didn't seem super valuable. Interval Analysis This one had my favorite "bug" of the lesson. I ran into an issue where one of my tests was running for a very long time (it was So my algorithm was guaranteed to terminate, but it would have to reach 2^63-1 one increment at a time. I decided that was infeasible and just dropped any interval that exceeded the bounds of [-1000,1000]. My logic was that very large intervals are not that useful for optimization anyway (ignoring things like non-negative or non-zero intervals). It made me curious about how "real" compilers/analysis tools handle this case. Maybe you could identify cycles that have this property and short-cut the trip MAX_INT? It sure seems like vanilla data flow will not be able to handle this efficiently. Testing I used turnt to test my analyses. This seemed like exactly the sort of situation turnt is perfect for. Each test case needs to be carefully inspected by a human for correctness and then the only invariant to maintain is that the output hasn't changed. I used some hand-written test cases and some test cases stolen from the bril benchmarks directory. One turnt lesson I learned: don't make your formatting data-dependent. I had my data flow tool align the in/out sets for each basic block and that was nice for me but terrible for turnt. If I made a small change to fix a bug it could modify every line in the output, making the turnt diff useless. |
Beta Was this translation helpful? Give feedback.
-
Generic ImplementationI started off by implementing the generic worklist implementation of Dataflow passes here for use in my later algorithms. This was actually surprisingly simple, and I mainly just followed the pseudocode in my implementation. It also supports both forward and reverse passes, which was achieved simply by reversing PassesI implemented every recommended pass except for Initialization checking, as Reaching DefinitionsAs mentioned in class, I used sets of definitions and a meet function that was simply set union. Therefore, I simply stored variable names along with an identifier for the location of the instruction they were defined in to use as unique definition identifiers. Thus, when a definition happens, it kills all existing definitions with the same name, regardless of the location. When printing, I would access the location of these definitions to create a set of possible definitions for each variable that reach at a given point. Live VariablesThis was even simpler than reaching definitions as reads contained no values, and thus I did not even need to store which specific reads occurred, and instead just saved whether or not a read happened. Here, I once again used sets with unions, but values were just the variable names. Constant PropogationFor constant propagation my mapping was a dictionary from variables to values, each value in three possible variants:
This causes undefined behavior if a value that is undefined in some paths and defined in others is accessed, but I decided that was a valid tradeoff as these programs are malformed and could be caught somewhere else (for example in an initialization checking pass) Available ExpressionsFor available expressions, the meet function I chose was set intersection, as only expressions that are defined in all incoming paths are guaranteed to be available. Otherwise, I simply stored sets of expressions (provided I knew they had no side effects). In the transfer function, when a new definition occurs, I discard all expressions that depend on the variable (as they may now be invalid). Interval AnalysisInterval analysis was similar to constant propagation in that I defined a number of functions that performed interval folding in the transfer function. For example, if I am given two intervals Unfortunately, as integers/floats do not have a maximum or minimum value, this meant that my function was not guaranteed to ever terminate, especially when loops occur, like the following: @main(iters: int) {
one: int = const 1;
i: int = const 0;
.head:
cond: bool = lt i iters;
br cond .body .end;
.body:
print i;
i: int = add i one;
jmp .head;
.end:
print i iters;
} Here, SummaryThe most complicated passes to implement by far were constant propagation and interval analysis, as both had some extra casework to deal with when loops and branches came into play. Especially with constant propagation, I initially had multiple incorrect implementations including using set union and intersection for values, and particularly when dealing with the loop and branch cases with undeclared variables. Over all, I would say I deserve a star for this week because of all the different passes I implemented. Honestly my main lesson from this week might be that it is time to switch to rust, as my python code is getting messier and messier as each week goes by, and I kept thinking (for example in constant propagation) how useful enums and matching could be... |
Beta Was this translation helpful? Give feedback.
-
https://github.com/ananyagoenka/cs6120-lesson-tasks/tree/main/l4 I implemented a general data-flow solver to support multiple analyses, including reaching definitions, live variable analysis, and constant propagation. The solver follows a worklist-based approach, updating in/out sets for each basic block until convergence. It supports both forward and backward analyses by modifying how predecessors and successors are handled. Reaching Definitions and Live Variables Reaching definitions tracks where variables are assigned, propagating unique identifiers (variable name + block label) through the program. The challenge was ensuring definitions were properly killed when the same variable was reassigned. Live variable analysis was conceptually simpler but required careful ordering within blocks—variables should only be marked as live if they were used before being overwritten. One tricky case was handling dead stores: if a variable was defined but never used afterward, it should not be marked live. Initially, I wasn’t properly excluding these cases, leading to unnecessary propagation of variables that didn’t affect program execution. Fixing this made the results more precise. Constant Propagation Adding constant propagation required switching from sets to a mapping of variables to known values. A variable could either hold a concrete constant, be undefined (⊥), or be nonconstant (NC) if conflicting values were encountered. The merge function had to carefully track variables across branches: Arithmetic operations were handled by constant folding when all operands were known. A subtle issue arose when dividing by variables—if the divisor was NC, the entire operation had to be marked NC, while a divisor of zero required special handling to avoid division errors. Debugging & Testing Another fun bug was dealing with variables that were only defined in some branches but later used unconditionally. Initially, these cases weren’t being caught, which led to incorrect results where undefined values propagated as constants. I fixed this by ensuring any undefined variable used in an operation was immediately marked as NC. I used turnt to test my implementations against a mix of the benchmarks in the bril repository as well some hand crafted interesting cases. |
Beta Was this translation helpful? Give feedback.
-
This was admittedly a less ambitious week for me due to time constraints. I did implement a generic solver that can support any workflow analysis provided the right form inputs, as shown in class. This was surprisingly easy in a language with a decent type system and first class functions, like TypeScript. I only had time to use it to implement reaching definitions, but it was quite satisfying to simply call my worklist algorithm, pass in a few lambdas, and have a working solution just kinda fall out of the sky. My algorithm specifically returns, for each basic block, a map from variable names to a set of instructions where that variable was defined and there exists at least one path from that definition to the end of the current block without the variable being overwritten. You can run my program by Since my program does not modify code, I wasn't able to use my testing harness from last week. Instead, testing was predominately manual, involving a few handcrafted test cases that exploit loops, branches, and combinations of variables that are either overwritten or not overwritten in each of those cases. The output from these were manually examined for correctness. I also ran my test against my benchmark from week 1, which was a much larger program, and manually checked the output at the end of each function for correctness as well. In the absence of meaningful code modifications, I believe these handcrafted scenarios that test as many edge cases as I could think of are the best I was able to do in terms of convincing myself my code was correct. The most interesting design decision was deciding what I wanted to return. Originally I just returned a set of variables that were defined, but I found this wasn't actually interesting as nothing ever dies without being replaced by a variable of the same name. I then switched to a map from variables to an array of definitions, before finally switching that array for a set, for ease of computing set unions and equality checking. While basic, I did complete the task, as well as the optional general solver that can run both forwards and backwards (inside |
Beta Was this translation helpful? Give feedback.
-
Code is here. I implemented the general data flow framework, and used it to implement reaching definitions. I started on initialized variables, too, but that version is still a bit buggy-- I will update if I get it to work, but I'll just submit reaching definitions for now. I had started before Tuesday's class and so had started with the general framework instead of working on just reaching definitions in specific. I think it did make it easier for me though to abstract the different parts of the algorithm away from each other at the very start. I found this week's algorithm particularly hard to test, because it doesn't actually change the code itself (for reaching definitions); I wound up just having to come up with edge cases and test them by manually checking my expected output against what was printed to the terminal. I also ran it on my test suite I made last week to check for correctness-- though this doesn't really check for correctness of the algorithm, it just checks to make sure the program actually finishes without getting stuck in an infinite loop or hitting an error. That was enough to help me catch a pretty significant bug, though. I made the (in hindsight regrettable) design decision to store my reaching definitions as lists of instructions (just storing the instructions which generated the definition that reaches to this point). This was because in python at least, dicts (which was how the instructions were stored) cannot be stored as elements of a set. This meant I had to write my own big union function, but that wasn't too bad. However, where it really bit me in the behind was the "not getting stuck in an infinite loop" part: I wrestled with an infinite loop bug for a couple hours before realizing that when I was checking if the "out" value of the current block was unchanged or not, I had just written "if list != oldlist", and so was also classifying the case where the elements were identical but in a different order (because lists) as one where more things should be added to the worklist. This made things never terminate because the order of, say, the last two definitions could just keep getting swapped over and over infinitely. So, whoops. Honestly, having it stored in lists was a real pain, I wish I had just come up with a different workaround from the start to use sets. Still, between my testing of as many edge cases as I could think of (including cycles, re-defining variables across blocks and throughout cycles, changing arguments that had been passed in, having multiple functions, etc) and then checking for pure error-and-inf-loop free correctness via testing on the benchmarks (which also helped me catch some actually correctness errors, too, because it led me to more complex edge cases to test), I am now confident that my reaching definitions works as desired. I do think I deserve a Michelin Star this week because I implemented the general algorithm as well as reaching definitions in specific, and this general algorithm can be used for the other problems. Also, debugging made me suffer. As usual. :) |
Beta Was this translation helpful? Give feedback.
-
Here is the code: link I implemented reaching definition analysis. The implementation itself was not very hard, except for debugging one infinite loop. It was not very clear what the best way to test my code would be. I ended up running it on small handwritten examples to test specific cases (like making sure that function input arguments are killed correctly) and plotting CGFs with ins and outs for each block to make sure that merge and transfer functions are correct. I run the code on the entire benchmark suite to make sure that my code completes on all of the programs and that there are no infinite loops. I routinely use copilot for autocompletion. I find it useful when it can "guess" what I want to write next i.e. importing missing packages, but useless otherwise. I did not use any text prompts for this assignment. |
Beta Was this translation helpful? Give feedback.
-
I implemented a general data flow framework, and so far I have constant propagation/folding, initialize variables, and liveness implemented. This felt like a good application for functors/dependency injections. When it came to dealing with backwards passes, I just literally swap around pred and succ, and in_prop and out_prop before and after the analyses. I did this because I was lazy, but in hindsight, this kind of made it harder to reason about, not sure if I would recommend doing this. I started with these analyses for now because they seemed fairly simple to get correct, but I will update this post as I implement more analyses. To be honest, I did start by just immediately implementing the general framework first, but that's because I had written the general framework already prior to last class, but this didn't prove to be much of a hinderance to anything. A lot of testing was non-rigorous by eye-balling print statements of the trace properties. But I did modify my constant folding dataflow implementation to actually modify the code, and I have tested it against Brench to see that it is still correct. I found that extending a generic dataflow solver to include code modifications to be a bit tricky, after having implemented a couple of passes, the refactoring got a bit annoying. another edit: I also implemented some dead-code elimination using a liveness analysis, this seems to work on bril programs that have dead code, correctness seems to be preserved with brench on the core benchmark programs. Turns out constant folding + dce based on liveness does not beat last week's lvn + trivial dce on the benchmark. To run my code, you just have to feed in a Bril .json into I think I deserve a star this week since I implemented a pretty flexibile dataflow framework that allows for optimizations to code (or not and just extract properties), as well as several analyses. |
Beta Was this translation helpful? Give feedback.
-
Summary Testing Hardest part of task Michelin Star |
Beta Was this translation helpful? Give feedback.
-
I started out by implementing a reaching definitions dataflow analysis. It followed pretty easily from what we discussed in class, so it was nice and straightforward. The one thing I added was making sure that function parameters reach the first block of the function. I did this in my initialization step. I tested this pass by writing some programs with interesting-ish control flow (some loops and if-elses) and inspecting the output to see if it was correct. One thing that I played around with was finding a nice way to represent the values we want to track between blocks. In English, we defined it as "the set of definitions that reach a specific program point". In code, I ended up representing it as a After this, I implemented a generic dataflow analysis solver using the worklist algorithm. It looks like this
The worklist function has the signature Once this was implemented, I re-implemented by reaching defs pass to use this framework and checked that it still worked, which it did. Then, to make sure my implementation of the backward direction was correct I implemented a live variable analysis with this framework. I tested both of these using turnt, on all of the bril programs in I didn't run into any major issues, probably the hardest part was reasoning about how to represent values for the reaching defs pass. But I thought this was way easier than implementing LVN, and probably has a greater payoff since this generic type of solver is pretty powerful. |
Beta Was this translation helpful? Give feedback.
-
For this lesson, I wrote two data flow analyses: Reaching Definitions Live Variables Verification/Performance Live variable identification (and the subsequent dead code elimination) only reduce executed instructions for three benchmark. LVN already removed most of the redundant instructions it seems. |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
SummaryI implemented a generic data flow algorithm for both expressions and constant propagation, written in Kotlin and that produced a json file containing the results of the algorithm. I tested it out with some of the examples that we covered in class as well as benchmarks in the bril repo and smaller examples in bril. How does it work?In class we briefly discussed how the dataflow algorithm typically uses a worklist, but could be described by a workset instead (provided we meet the monotonicity requirements to guarantee termination) I took this approach to heart and modeled the algorithm with a workset instead (although i use a TreeSet and process blocks in order of appearance (i.e. from 0 to n in the forwards direction and from n - 1 to 0 in he backwards direction). To establish a generic version of dataflow, I split the algorithm into a strategy and an implementation; where the strategy simply defines the direction, initial value, transfer function and merge function. To allow for ease of implementation of new dataflow strategies, I provide utilities for BigUnion/BigIntersaction that support both Maps and Sets to work out of the box. With this approach in place, I implemented both reaching definitions and constant propagation and I only had to create a different strategy instance, and specify the type of the output without having to make any modifications to the underlying algorithm. Hardest part?The hardest part was to set up the right abstractions for supporting generic strategies in the dataflow algorithm, the use of Kotlin's reified generic types allowed me to get default support for BigUnion/BigIntersection without having to rely on custom implementations of these functions for each strategy. But this felt like a scenario where I was fighting the language to get things to work properly ⭐❓I'd give the results of the task a michelin star. |
Beta Was this translation helpful? Give feedback.
-
Group (@ngernest, @katherinewu312, @samuelbreckenridge) Code As a first step we implemented a data flow analysis to identify live variables. Setting up a skeleton of the solver For constant propagation, the trickiest part was getting the merge function right. We also decided to implement a generic solver that supports multiple analyses. Our code follows the high-level |
Beta Was this translation helpful? Give feedback.
-
For this task, I implemented the constant propagation analysis, because I think it's a very cool data flow analysis. I had hoped to integrate it with my LVN optimization, using it to populate the value table at the head of each basic block to actually do the optimization, but my LVN code doesn't do constant folding already and I didn't have much time this week so I didn't do it. I did extract a general-purpose solver, which only does forward analyses: to use it you create a type for your analysis and make it an instance of the Since I didn't have much time this week, I just tested it on the example programs in The analysis works and uses a generic solver, but because of my sketchy testing and basic setup, I think this is close to bare minimum to meet Michelin star quality. |
Beta Was this translation helpful? Give feedback.
-
For my task, I implemented Reaching definition analysis, because it seemed interesting as a way to remove deadcode that wouldn't be trivial. Our previous implementations would not necessarily have removed statements that get overwritten in every other basic block because LVN only looks within a basic block, but here we can see that if a statement has no reaching definitions, it can be removed. I only implemented Reading Definition analysis, and didn't go about making a general-purpose solver. I would have liked to create an interface with functions to be overridden, but I've had a tougher workload this week and just couldn't find the time. Most of the code I've written for previous assignments has been in Python and treating programs like their json dictionary implementations, which isn't very strongly typed and it's starting to get to the point where I'm paying the price for not having implemented better data structures to represent bril programs. I'm planning to just rewrite it all with nicer data structures in Rust over February break. As for testing, I wrote a few simple test bril programs and had the program print the reaching definitions at the end of each basic block. I then manually went through and checked them. Unfortunately, it's not clear what automated tests I could implement, like I did for the LVN. Even if I were to modify the brill interpreter to check the definitions at every instruction are always in the set expected reaching definitions, that still wouldn't build much more confidence in my implementation's correctness (For example, a Reaching Definitions implementations that just says all definitions always reach all instructions would pass that test.) I believe this work is only barely worth a Michelin star - I did, in fact, complete the task assigned, but I didn't go any further with it, such as building a general purpose solver or adding it to my LVN compiler optimizations from last week's task. |
Beta Was this translation helpful? Give feedback.
-
In collaboration with @bryantpark04 and @dhan0779 Defined Variables: We started by implementing a very simple defined variables analysis to track the set of variables which had been assigned a value. This basic implementation helped us become familiar with the idea of the merge and transfer functions, and we used this analysis to confirm our generic solver was functioning as expected later. Generic Solver: We created a generic solver (with some wrangling of C++ types) that implements the worklist algorithm and can be parameterized on the initial value for each block, merge function, transfer function, and direction of the analysis (forward or backward). It was pretty straightforward to write this solver by extracting the relevant parts from the initial defined variables implementation, and it was pleasantly simple to add more analyses once we had this framework set up. Reaching Definitions: We implemented reaching definitions by keeping a map of variable names to sets of definitions, where a definition is a unique identifier of some instruction (denoted Live Variables: The live variables analysis gave us a chance to explore a backwards pass analysis. This was achieved by swapping the predecessor and successor maps in the CFG, then running the worklist algorithm as usual. We use a set union of live variable names for the merge function, and for the transfer function, we step over the instructions in the block in reverse while removing their destinations and adding their uses. Constant Propagation: This was the most involved implementation since we had to do a lot of workarounds with the types in C++. In particular, we set up a variant type for representing Bril values (ints, floats, booleans). Our environment type was simply a mapping from variable names to optional Bril values, where a std::nullopt represented values that were known to be non-const (as opposed to unknown/uninitialized). Our merge function copies over var -> val mappings from the preds, and overwrites them with a std::nullopt if the values conflict with each other. Our transfer function steps through the instructions and either overwrites destination variables with std::nullopt if they depend on an unknown/non-const variable or updates them in the environment with the correct value if all of their args are also const. We also had to think carefully about updates to variables within loop bodies, as our initial implementation incorrectly propagated constants even when the variables were overwritten to a non-constant result in a loop. Testing: For testing, we created a small test script to diff outputs from the Conclusions: After implementing the generic solver, it was straightforward to add new analyses. The most involved analysis to implement was constant propagation, as there were more edge cases to consider and many type conversions to perform. Overall, this task was pretty fun to implement, and we believe our work deserves a Michelin star since we implemented the generic solver and three different analyses. |
Beta Was this translation helpful? Give feedback.
-
CodeHere are my reaching analysis template and my generic solver implementation. SummaryFor this task, I implemented a generic worklist-algorithm-based solver, wrote a "template" for reaching definitions to toss into that solver, and wrote a pretty printing function for manual testing. Also, while I didn't get it to work fully by the deadline, I started a global constant propagation analysis. How it worksOCaml's module, functor, and module type (i.e. signature) separation came in handy for my implementation. Since I started this task earlier and had a bit more time, I started off by abstracting the worklist algorithm from the get-go. I implemented a signature that exposed the As for the Reaching Analysis implementation, I did not encounter any huge issues once I had gotten down the abstract module type. It's a good thing I implemented globally-recognized indices TestingMy testing unfortunately wasn't as statistically rigorous as L3 because I didn't actually change any code, and therefore couldn't pin down any performance improvements. But, I manually verified the
Hardest partI think the hardest part was bringing the abstract math down into code. For me, it was helpful to read the additional readings and learn a bit more on lattices. I definitely saw the overlap between the abstract operations on powerset-lattice-members (i.e. Eventually, I sort of gave up on trying to map the abstract definitions into my module type signature. At that point, it became clear to me the minimum of what I would need for reaching definitions and potentially constant propagation, so I went ahead and implemented the signature as such. I think I'm missing some stuff, like StarI would give myself a star |
Beta Was this translation helpful? Give feedback.
-
I started with implementing the pseudocode, which meant just creating the generic solver. I think this approach isn't too bad, as the pseudocode for the dataflow algorithm is pretty straightforward. My biggest problem with this assignment stemmed from my earlier implementation of the cfg algorithms. It turns out that I made several decisions during that assignment that made this one much more difficult. I spent a significant amount of time going back and fixing that. I had to adjust the way I was using some data structures to make the data flow algorithm possible. I also felt that my understanding was not completely clear when I started this implementation, which definitely also led to many extra hours spent on this. This definitely helped me gain a better understanding on cfg and dataflow. Once I got all the cfg stuff down, I added support for constant propagation. I tested by copying the fmt function from the example implementation provided and comparing my outputs with the example df outputs. I think I deserve a Michelin star because I met the expectations of the assignment! |
Beta Was this translation helpful? Give feedback.
-
I first created a simple Then, I implemented a general solver for dataflow analysis in Rust, and
As usual, these are nicely I handchecked and tested with For the dataflow solver, I looked at some CMU slides which said to do postorder and reverse-postorder basic block orderings (generalizing the topological sort strategy Professor Sampson proposed in class). To test reaching definitions, I manually checked that every definition the analysis claimed to be reaching actually was by BFSing from each block + claimed definition, running it on all the core benchmarks (see check.py). Notably, this is not sufficient for a correct reaching definitions analysis, only necessary. However, I have good reason for it to be correct based on the simplicity of the code and the fact that I more rigorously confirmed the correctness of live variable analysis (the two differ only in their transfer function and traversal direction). To test live variables, I matched my output with that of Professor Sampson's For reference, here's the relevant CI code: - name: Test reaching definitions analysis
run: |
cd lesson4/dataflow
python3 check.py def ../../bril/benchmarks/**/*.bril ../../bril/examples/test/df/*.bril
- name: Test live variables analysis
run: |
cd lesson4/dataflow
python3 match_outputs.py \
"python3 ../../bril/examples/df.py live | grep ' in:'" \
"cargo run --package dataflow --quiet -- --analysis live | grep in:" \
../../bril/benchmarks/core/*.bril ../../bril/examples/test/df/*.bril \
--exclude is-decreasing.bril --exclude recfact.bril --exclude relative-primes.bril # differ on definition of basic block
- name: Snapshot test analyses on examples/test/df/ programs
run: |
cd lesson4/dataflow
cd turnt && turnt df_copied_from_bril/*.bril I believe I deserve a Michelin star because I implemented the assigned tasks and tested my implementations. Total time spent: 6 hours In other news
|
Beta Was this translation helpful? Give feedback.
-
I implemented defined variables. I didn't find it too difficult, but I tried to keep the code in the form of the generic solver so I could easily implement one when I have a chance. I enjoyed the process. I took it easy because I have a lot of other programming for other classes this week. But I'm hoping to implement a few more soon and build up my python file. I tested it using some of the test files in the folder and checked by hand. I need to figure out a better way to test it. |
Beta Was this translation helpful? Give feedback.
-
I implemented the Reaching Definitions and Live Variables data flow analyses. I first implemented Reaching Definitions, which involved rewriting the cfg from lesson 2 to be a nested dictionary with keys for the predecessors and successors of each basic block. I stored definitions as a tuple of the variable, the block the definition is in, and the offset within the block. After implementing Reaching Definitions and checking the output matched what I expected on the tests in When adapting Reaching Definitions to the generic solver, I verified the outputs didn't change, using brench for snapshot testing. When implementing Live Variables, there were luckily already expected outputs saved, so I could just compare directly against it if I matched the output format exactly. For Live Variables, I also manually inspected the output when ran on my benchmark from lesson 2; however, for Reaching Definitions, the output was far too large even on small inputs to carefully manually verify. I ended up manually inspecting on some smaller benchmarks with iterative behavior like The hardest part was probably writing the reverse pass properly in the generic solver. In particular, realizing that the handling of arguments is different took me some time. Also, the different set operators in Python were annoying to use, particularly when tuples would unintentionally get unpacked. |
Beta Was this translation helpful? Give feedback.
-
Summary Testing Hardest part of task |
Beta Was this translation helpful? Give feedback.
-
Partner: Noah Schiff (@noschiff) OverviewThis unfortunately is quite late, for which we both apologize. However, we had a great time first devising an idea for more specific data flow, and then generalizing it to a more multi-purpose system in TypeScript. We began with a specific data flow analysis for Live Variable Analysis, before generalizing it and then utilizing it to analyze Reachable Definitions and Constant Propagation. With more time, it would have been wonderful to explore extending this further for the assignment; that having been said, we fully plan to try more forms of analysis on our own. ImplementationWe implemented our algorithm in TypeScript, designing custom types for blocks, labeled block collections and the control-flow graph. Our first step was to improve on the basic blocking & CFG-generation algorithms we had explored in L2; we then went about actually using these for Dataflow analysis. We decided to use the video tutorial and our own reasoning sense to implement Live Variable Analysis; we slightly tweaked the general 'recipe' outlined in the video in developing our transfer function in order to implement this. We first began by setting up a function We then were able to factor out the relevant code to create the more general-purpose worklisting algorithm, which we were then able to extend to a few more forms. We also factored out a Subsequently, we implemented the Reachable Definitions analysis using a custom-made record type for Bril instructions, and constant propagation with numbers and booleans. Constant Propagation Notes We further reasoned that set union would not make sense here, since if two incoming blocks have different values on the same variable, it can no longer logically be treated as a constant (as we cannot guarantee which value it will take coming into the basic block). A more logical interpretation, thus, was to tweak the set union operator, but now working over mappings – such that all mappings from variables to constants were included, except if a certain value mapped to different constants from different input blocks. Thus, this extracts only the set of variable The transfer function was also straightforward. Reusing Kabir's constant propagation mapping system from L3, we were able to describe our transfer algorithm as such:
Implementing this then gave us our third data flow analysis! TestingIt was a bit harder to use a general-purpose testing framework like Brench here the way we were able to with L3 (LVN), since our code was not directly modifying Bril source files, but rather outputting results of analysis using the data structures we had created. We began by creating a straightforward script to run with TakeawaysIt is regrettable that we could not spend more time on this project, as it was incredibly fun and really brought a nice mix of math/algo/compilers all together. We are looking forward to more Dataflow work in the future! But for the time this took and the lateness, we do feel that this would deserve a star, as we went above the bounds for basic implementation and came up with both a general-purpose implementation of the algorithm and an extension to constant-propagation. |
Beta Was this translation helpful? Give feedback.
-
I implemented a sign dataflow analysis, using a generalized forwards-only worklist implementation. 1 Filling out the add/sub/mul lookup tables was quite tedious and potentially error-prone, so I switched from writing out the CorrectnessI tested my program on my submitted BBS benchmark. 2 I selected this benchmark because:
In particular, I was looking and confirmed that the following is true:
I am not fully convinced of my implementation's correctness. However, I believe that this program exercises some of the interesting cases of a sign dataflow analysis. Footnotes |
Beta Was this translation helpful? Give feedback.
-
code I didn't run brench or other large scaled tests for this task, because I couldn't think of a straightforward way to evaluate the correctness of live analysis results. Instead, I gathered some small tests from benchmark and wrote some small test cases on my own, and manually check the correctness of the live analysis result. They are all correct. The hardest part of this task is to figure out the correct way to generalize the worklist algorithm, so that different data structures can be properly handled as node in this worklist algorithm. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
This thread is for summarizing how your implementation of the data flow framework and its client optimizations went!
Beta Was this translation helpful? Give feedback.
All reactions