Skip to content

Commit

Permalink
Chapter 7 (also 8 but not officially) (SeaOfNodes#48)
Browse files Browse the repository at this point in the history
* SeaOfNodes#44 Start on chapter 7

* SeaOfNodes#44
Temp hack on printing to not break on cycles.
Parse while statement.

* SeaOfNodes#44 Remove unnecessary method

* SeaOfNodes#44 Remove unnecessary phi map

* SeaOfNodes#44 Phi should have existing nodes from both scopes as input

* SeaOfNodes#44 We need to only insert phis once for each name that is looked up. To do so,
we need to know when a named is looked up first time inside the loop body.
The phi must have its second input added later on after the loop is fully parsed.

* SeaOfNodes#44 Add phi also when update() is called.
Only add phi if name was known at loop start

* SeaOfNodes#44 Only create phi in loop body if var binding in head != body.
During finish() lookup in body directly.

* SeaOfNodes#44 Update the print output to make it similar to that in Xmilia's fork

* SeaOfNodes#44 Disable peephole for phis & region involved in while loop

* Fix broken tests

Remove "disablePeephole" set in some tests, and remaining set for other tests, so a bulk test-all fails because disablePeephole is true for all.
Reset disablePeephole in Node.reset
Switch HashSet for BitSet (touches all).

* Restructure Node class

Group printing, group node & edge manipulations, group optimizations.
Also, fix LoopRegion idom, it does not use Region idom but uses the original

* SeaOfNodes#44 Node printing

* SeaOfNodes#44 Node printing

* SeaOfNodes#44 Fix control edges in loop region
Change label of loop region
Fix the output count in dump

* SeaOfNodes#44 Revise dump output

* SeaOfNodes#44 When a LoopRegionNode is duplicated, ensure the dupe is also a LoopRegionNode

* SeaOfNodes#44 When a LoopScopeNode is merged, we merge the names set too

* SeaOfNodes#44 When a LoopScopeNode is merged, we merge the names set too

* SeaOfNodes#44 Fix print1() on RegionNode to use label()

* SeaOfNodes#44 Fix bug caused by incorrect comparison of nodes between body and head of loop; the check must be performed before any phis are created

* Revert "SeaOfNodes#44 Fix bug caused by incorrect comparison of nodes between body and head of loop; the check must be performed before any phis are created"

This reverts commit 5f72970.

* SeaOfNodes#44 Fix bug caused by incorrect comparison of nodes between body and head of loop; the check must be performed before any phis are created

* SeaOfNodes#44 when we create a phi incrementally for a loop region, all scopes that are cloned from the original must get the same phi update

* SeaOfNodes#44 test case

* SeaOfNodes#44 test case

* Squashed commit of the following:

commit 15666e2
Author: Cliff Click <[email protected]>
Date:   Mon Nov 27 09:45:40 2023 -0800

    lazy/eager phis

    Eager phis by default, but can flip Parser.LAZY to switch to lazy phis.  Several tests golden answers change when flipping this flag due to minor node renumbering.
    Phi combines same_inputs and singleUniqueInput.
    Fancier basic-block-like printing.  Uses isMultiHead & isMultiTail which show up in many Nodes.
    Remove blanks in triple-quote strings.

commit 6ea7018
Author: Cliff Click <[email protected]>
Date:   Sun Nov 26 11:13:22 2023 -0800

    Remove dead scopes

    Add a debug find

commit a52c06a
Author: Cliff Click <[email protected]>
Date:   Sun Nov 26 10:21:48 2023 -0800

    No constraints on backedges

    nicer loopy graphs

commit b5ab550
Author: Cliff Click <[email protected]>
Date:   Sun Nov 26 10:13:22 2023 -0800

    Fix regression

commit 30a7f5c
Author: Cliff Click <[email protected]>
Date:   Sat Nov 25 21:51:47 2023 -0800

    minor cleanup

commit ad742b7
Author: Cliff Click <[email protected]>
Date:   Sat Nov 25 21:47:05 2023 -0800

    Fixes lazy phi

    Scope now takes a 'loop' option on dupping; the dup'ed Scope is filled with "Lazy Phi" sentinels - markers to indicate a Phi is need if this variable is ever referenced.  If its never referenced, at the loop exit the "Lazy Phi" unwinds back to the original value.  Lookup and update calls lazily insert concrete Phis over "Lazy Phis" at first touch.

    Rename _allScopes to _xScopes its "active" not "all" scopes now.
    Remove try/finally block at end, because a compiler bug throws, then attempts a GraphViz on a broken graph, then throws again, hiding the original bug.
    Removed 1 extra layer of Scopes.
    Scope printing now prints in edge order; added a reversemap for printing.
    More touches to ascii printing.

* Remove flag to parseBlock

* Bugfix for lazy on empty loop body

Remove extra phi name-matching code

* Keep head alive until entire end_loop merging is done

* Create chapter 8 as a copy of chapter 7

* SeaOfNodes#44 Chapter 7 is now eager phis only (SeaOfNodes#46)

* SeaOfNodes#44 Chapter 7 is now eager phis only

* MOVE IR Printing to dedicated class to declutter Node. Remove Ary (SeaOfNodes#49)

* SeaOfNodes#44 Move IR printing to dedicated class to declutter Node, and remove Ary

* SeaOfNodes#44 refactor Node.delUse() to use common del()

* SeaOfNodes#44 Some cleanups

* SeaOfNodes#44 Some cleanups

* SeaOfNodes#44 Some cleanups

* SeaOfNodes#44 Some cleanups

* Revert "SeaOfNodes#44 Some cleanups"

This reverts commit 2702c33.

* SeaOfNodes#44 Some cleanups

* SeaOfNodes#44 Some cleanups

* SeaOfNodes#44 Some cleanups

* SeaOfNodes#44 Fix typo

* SeaOfNodes#44 Fix markdown formatting

* SeaOfNodes#44 Remove line numbers from code snippets

---------

Co-authored-by: Cliff Click <[email protected]>
  • Loading branch information
dibyendumajumdar and cliffclick authored Dec 3, 2023
1 parent 21a1fc4 commit 6415e3a
Show file tree
Hide file tree
Showing 75 changed files with 9,729 additions and 13 deletions.
6 changes: 4 additions & 2 deletions .dir-locals.el
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
;;; Directory Local Variables
;;; For more information see (info "(emacs) Directory Variables")

((dired-mode . ((c-basic-offset . 4))))
(
(dired-mode . ((c-basic-offset . 4)))
(java-mode . ((c-basic-offset . 4)))
)
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,17 @@ The simple language will be a small subset of C.
* [Chapter 4](chapter04/README.md): A non-constant external variable input named `arg`. Binary and Comparison operators involving constants and `arg`. Non-zero values will be truthy. Peephole optimizations involving algebraic simplifications.
* [Chapter 5](chapter05/README.md): `if` statement. CFG construction.
* [Chapter 6](chapter06/README.md): Peephole optimization around dead control flow.
* Chapter 7: `while` statement.
* Chapter 8: Global Value Numbering.
* Chapter 9: Functions and calls.
* Chapter 10: Boolean operators `&&`, `||` and `!` including short circuit.
* Chapter 11: Memory effects: general memory edges in SSA. Peephole optimization around
* [Chapter 7](chapter07/README.md): `while` statement. Looping construct - eager phi approach.
* Chapter 8: Looping construct continued, lazy phi creation, `break` and `continue` statements.
* Chapter 9: Global Value Numbering.
* Chapter 10: Functions and calls.
* Chapter 11: Boolean operators `&&`, `||` and `!` including short circuit.
* Chapter 12: Memory effects: general memory edges in SSA. Peephole optimization around
load-after-store/store-after-store.
* Chapter 12: Equivalence class aliasing, fine grained peephole optimizations. Free ptr-to analysis in SoN; but does not
* Chapter 13: Equivalence class aliasing, fine grained peephole optimizations. Free ptr-to analysis in SoN; but does not
handle aliasing in arrays.
* Chapter 13: One dimensional static length array type. Array load/store.
* Chapter 14: Global Code Motion - unwind SoN to CFG. Scheduling.
* Chapter 15: Instruction selection, BURS.
* Chapter 16: Backend register allocation.
* Chapter 17: Garbage Collection.
* Chapter 14: One dimensional static length array type. Array load/store.
* Chapter 15: Global Code Motion - unwind SoN to CFG. Scheduling.
* Chapter 16: Instruction selection, BURS.
* Chapter 17: Backend register allocation.
* Chapter 18: Garbage Collection.
92 changes: 92 additions & 0 deletions chapter07/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
SHELL := /bin/bash
.DELETE_ON_ERROR:

# for printing variable values
# usage: make print-VARIABLE
# > VARIABLE = value_of_variable
print-% : ; @echo $* = $($*)

# literal space
space := $() $()

# Decide OS-specific questions
# jar-file seperator
ifeq ($(OS),Windows_NT)
SEP = ;
else
SEP = :
endif
# Find a reasonable ctags.
CTAGS = $(shell which ctags)
# Hack for MacOS: /usr/bin/ctags is unfriendly, so look for ctags from brew
ifeq ($(UNAME),Darwin)
CTAGS = $(shell brew list ctags 2> /dev/null | grep bin/ctags)
endif

# Fun Args to javac. Mostly limit to java8 source definitions, and fairly
# aggressive lint warnings.
JAVAC_ARGS = -g

# Source code
SIMPLE := com/seaofnodes/simple
SRC := src/main/java
TST := src/test/java
CLZDIR:= build/classes
main_javas := $(wildcard $(SRC)/$(SIMPLE)/*java $(SRC)/$(SIMPLE)/*/*java)
test_javas := $(wildcard $(TST)/$(SIMPLE)/*java $(TST)/$(SIMPLE)/*/*java)
main_classes := $(patsubst $(SRC)/%java,$(CLZDIR)/main/%class,$(main_javas))
test_classes := $(patsubst $(TST)/%java,$(CLZDIR)/test/%class,$(test_javas))
test_cp := $(patsubst $(TST)/$(SIMPLE)/%.java,com.seaofnodes.simple.%,$(test_javas))
classes = $(main_classes) $(test_classes)
# All the libraries
libs = $(wildcard lib/*jar)
jars = $(subst $(space),$(SEP),$(libs))


default_targets := $(main_classes) $(test_classes)
# Optionally add ctags to the default target if a reasonable one was found.
ifneq ($(CTAGS),)
default_targets += tags
endif

default: $(default_targets)

# Compile just the out-of-date files
$(main_classes): build/classes/main/%class: $(SRC)/%java
@echo "compiling " $@ " because " $?
@[ -d $(CLZDIR)/main ] || mkdir -p $(CLZDIR)/main
@javac $(JAVAC_ARGS) -cp "$(CLZDIR)/main$(SEP)$(jars)" -sourcepath $(SRC) -d $(CLZDIR)/main $(main_javas)

$(test_classes): $(CLZDIR)/test/%class: $(TST)/%java $(main_classes)
@echo "compiling " $@ " because " $?
@[ -d $(CLZDIR)/test ] || mkdir -p $(CLZDIR)/test
@javac $(JAVAC_ARGS) -cp "$(CLZDIR)/test$(SEP)$(CLZDIR)/main$(SEP)$(jars)" -sourcepath $(TST) -d $(CLZDIR)/test $(test_javas)

# Base launch line for JVM tests
JVM=nice java -ea -cp "build/classes/main${SEP}${jars}${SEP}$(CLZDIR)/test"

tests: $(default_targets)
$(JVM) org.junit.runner.JUnitCore $(test_cp)

.PHONY: clean
clean:
rm -rf build
rm -f TAGS
(find . -name "*~" -exec rm {} \; 2>/dev/null; exit 0)

# Download libs from maven
lib: lib/junit-4.12.jar lib/hamcrest-core-1.3.jar

# Jars
lib/junit-4.12.jar:
@[ -d lib ] || mkdir -p lib
@(cd lib; wget https://repo1.maven.org/maven2/junit/junit/4.12/junit-4.12.jar)

lib/hamcrest-core-1.3.jar:
@[ -d lib ] || mkdir -p lib
@(cd lib; wget https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar)

# Build emacs tags (part of a tasty emacs ide experience)
tags: $(main_javas) $(test_javas)
@rm -f TAGS
@$(CTAGS) -e --recurse=yes --extra=+q --fields=+fksaiS $(SRC) $(TST)
227 changes: 227 additions & 0 deletions chapter07/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# Chapter 7

In this chapter we introduce the `while` statement.

Here is the [complete language grammar](docs/07-grammar.md) for this chapter.

## Dealing With Back Edges

The complication introduced by looping constructs is that variables flow back into the body of the loop on iteration.
For example:

```java
while(arg < 10) {
arg = arg + 1;
}
return arg;
```

The variable `arg` is assigned a new value inside the body of the loop, and this then flows back into the body of the loop.

In general, we will rewrite the looping construct as follows:

```java
loop_head:
if( !(arg < 10) )
goto loop_exit;
arg = arg + 1;
goto loop_head;

loop_exit:
```

Above is for illustration only, we do not have labels and `goto` statements in the language.

From an SSA[^1] point of view, since `arg` flows back, it requires a `phi` node at the head. Conceptually we would like the outcome to be:

```java
// arg1 represents incoming arg
//
loop_head:
arg2 = phi(arg1, arg3);
if( !(arg2 < 10) )
goto loop_exit;
arg3 = arg2 + 1;
goto loop_head;

loop_exit:
```

Notice that the phi for `arg2` refers to `arg3`, which is not known at the time we parse the `while` loop predicate. This is the crux of the problem that we need
to solve in order to successfully construct the Sea of Nodes graph, which is always in SSA form.

Recall from [Chapter 5](../chapter05/README.md) that when parsing `if` statements, we clone the symbol tables as we go past the `if` predicate.
Later we merge the two sets of symbol tables at a `Region` - creating phis for all names that encountered a change in definition within the two
branches of the `if` statement. In the case of the `if` statement, the phis are created at the merge point when we already know the definitions
that are being merged.

The essential idea for loop constructs is to eagerly create phis for all names in the symbol tables *before* we enter the loop's `if` condition,
since we do not know which names will be redefined inside the body of the loop. When the loop terminates, we go back and remove unnecessary
phis. We call this approach the "eager phi" approach.[^2]

In [Chapter 8](../chapter08) we will implement a "lazy phi" approach that creates phis only when we encounter redefinition of a name.

## New Node Types

Our list of nodes remains the same as in [Chapter 5](../chapter05/README.md), however, we create a subtype of `Region` named `Loop` to better
encapsulate some of the additional logic required. A key aspect of this is to temporarily disable peepholes of the `Region` and any phis
created until we complete parsing the loop body. This is because our phis are not fully constructed until the loop end.

## Detailed Steps

1. We start by creating a new subclass of `Region`, the `Loop`. The `Loop` gets two control inputs,
the first one is the entry point, i.e. the current binding to `$ctrl`, and second one (`null`) is a placeholder for the back edge that is
set after loop is parsed. The absence of a back edge is used as an indicator to switch off peepholes of the region and
associated phis.

```java
ctrl(new LoopNode(ctrl(),null).peephole());
```

The newly created region becomes the current control.

2. We duplicate the current Scope node. This involves duplicating all the symbols at
every level with the scope, and creating phis for every symbol except the `$ctrl` binding.

```java
// Make a new Scope for the body.
_scope = _scope.dup(true);
```

Note that the `dup` call is given an argument `true`. This triggers creating phis. The code
that creates the phis in the `dup()` method is shown below.

```java
// boolean loop=true if this is a loop region

String[] reverse = reverse_names();
dup.add_def(ctrl()); // Control input is just copied
for( int i=1; i<nIns(); i++ ) {
if ( !loop ) { dup.add_def(in(i)); }
else {
// Loop region
// Create a phi node with second input as null - to be filled in
// by endLoop() below
dup.add_def(new PhiNode(reverse[i], ctrl(), in(i), null).peephole());
// Ensure our node has the same phi in case we created one
set_def(i, dup.in(i));
}
}
```

Inside the `else` block both the duplicated scope and the original scope get the same phi node.

3. Next we set up the `if` condition, very much like we do with regular ifs.

```java
// Parse predicate
var pred = require(parseExpression(), ")");
// IfNode takes current control and predicate
IfNode ifNode = (IfNode)new IfNode(ctrl(), pred).<IfNode>keep().peephole();
// Setup projection nodes
Node ifT = new ProjNode(ifNode, 0, "True" ).peephole();
ifNode.unkeep();
Node ifF = new ProjNode(ifNode, 1, "False").peephole();
```

4. We set the control token to the `True` projection node `ifT`, but before that we make another clone of
the current scope.

```java
// The exit scope, accounting for any side effects in the predicate
var exit = _scope.dup();
exit.ctrl(ifF);
```

The new scope is saved as the exit scope that will live after the loop ends, therefore `$ctrl` in the exit scope is
set to the `False` projection. The exit scope captures any side effects of the loop's predicate.

5. We now set the control to the `True` projection and parse the loop body.

```java
// Parse the true side, which corresponds to loop body
ctrl(ifT); // set ctrl token to ifTrue projection
parseStatement(); // Parse loop body
```

6. After the loop body is parsed, we go back and process all the phis we created earlier.

```java
// The true branch loops back, so whatever is current control gets
// added to head loop as input
head.endLoop(_scope, exit);
```

The `endLoop` method sets the second control of the loop region to the control from the back edge.
It then goes through all the phis and sets the second data input on the phi to the corresponding entry
from the loop body; phis that were not used are peepholed and get replaced by the original input.

```java
Node ctrl = ctrl();
ctrl.set_def(2,back.ctrl());
for( int i=1; i<nIns(); i++ ) {
PhiNode phi = (PhiNode)in(i);
assert phi.region()==ctrl && phi.in(2)==null;
phi.set_def(2,back.in(i));
// Do an eager useless-phi removal
Node in = phi.peephole();
if( in != phi )
phi.subsume(in);
}
```

7. Finally, both the original scope (head) we started with, and the duplicate created for the body are killed.
At exit the false control is the current control, and exit scope is set as the current scope.

### Visualization

The example quoted above is shown below at an intermediate state:

![Graph1](./docs/07-graph1.svg)

* Three Scopes are shown, reading clockwise, the loop head, exit and the body.

The final graph looks like this:

![Graph2](./docs/07-graph2.svg)

## More Complex Examples

### Nested Loops

```java
int sum = 0;
int i = 0;
while(i < arg) {
i = i + 1;
int j = 0;
while( j < arg ) {
sum = sum + j;
j = j + 1;
}
}
return sum;
```

![Graph2](./docs/07-graph3.svg)

### Loop With Nested If

```java
int a = 1;
int b = 2;
while(a < 10) {
if (a == 2) a = 3;
else b = 4;
}
return b;
```

![Graph2](./docs/07-graph4.svg)


[^1]: Cytron, R. et al (1991).
Efficiently computing static single assignment form and the control dependence graph, in ACM Transactions on Programming Languages and Systems, 13(4):451-490, 1991.

[^2]: Click, C. (1995).
Combining Analyses, Combining Optimizations, 103.
Loading

0 comments on commit 6415e3a

Please sign in to comment.